最大流最小割_网络最大流量与割的容量的关系-程序员宅基地

技术标签: 算法  《算法训练营》进阶篇读书笔记  图论  

算法思想

最大流最小割主要是利用其定理来求最小割的容量,最大流最小割定理:任何网络中最大流的流量都等于最小割的容量,这里不给出详细证明,只论述相关概念

割: 网络的节点划分,所有节点被划分成 S , T S,T S,T两个集合,源点 s ∈ S s∈S sS,汇点 t ∈ T t∈T tT,割被记为 C U T ( S , T ) CUT(S,T) CUT(S,T),如同一刀将节点分成了两部分

割的净流量: 集合 S S S和集合 T T T间连接的边中,从 S S S T T T的边容量之和,最小割为容量最小的割

f f f为网络的一个流, C U T ( S , T ) CUT(S,T) CUT(S,T)为网络的任意一个割,那么 f f f的流值等于割的净流量 f ( S , T ) f(S,T) f(S,T) f f f的流值不超过割的容量 c ( S , T ) c(S,T) c(S,T),所有流值都小于等于割的流量

最大流最小割定理: f f f是网络的最大流, C U T ( S , T ) CUT(S,T) CUT(S,T)为网络最小割,则最大流值等于最小割容量

训练

POJ3469

题目大意:有A,B两核的CPU运行N个模块,每个模块有在对应核上运行的成本,并且有M个模块需要数据交换,如果模块在同一核上运行,则可以忽略数据交换成本,否则要花费给出的成本,求出最小总成本

思路:每个模块要么在A上,要么在B上,也就是说,最后所有点都会被划分成两个集合,这有点类似于二分图,但是这两个集合内部是有边的,不符合二分图的定义,于是可以向最小割考虑,将所有点分成两个集合,运行在A上的模块看做 S S S集合,在B上的看做 T T T集合,如图所示,切割线切到的边包括了 S S S集合中的模块在A上的运行成本 a 1 , a 2 , … , a n a_1,a_2,\dots,a_n a1,a2,,an,T集合在B上的运行成本 b 1 , b 2 , … b n b_1,b_2,\dots b_n b1,b2,bn以及模块间数据交换的成本,原问题被转换成求最小割问题,进而转换成求最大流问题
在这里插入图片描述

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int maxn=2e4+10;
const int inf=0x3f3f3f3f;
int n,m,head[maxn],cnt,d[maxn];
struct node {
    
    int next,to,cap,flow;
} e[maxn*60];
void Add(int from,int to,int cap,int flow) {
    
    e[cnt].next=head[from];
    e[cnt].to=to;
    e[cnt].cap=cap;
    e[cnt].flow=flow;
    head[from]=cnt++;
}
bool BFS(int s,int t) {
    //分层
    memset(d,0,sizeof(d));
    queue<int>q;
    q.push(s);
    d[s]=1;
    while(!q.empty()) {
    
        int u=q.front();
        q.pop();
        for(int i=head[u]; ~i; i=e[i].next) {
    
            int v=e[i].to;
            if(!d[v]&&e[i].cap>e[i].flow) {
    //如果可以增流
                d[v]=d[u]+1;
                q.push(v);
                if(v==t)return 1;
            }
        }
    }
    return 0;
}
int DFS(int u,int flow,int t) {
    
    if(u==t)return flow;
    int res=flow;//res存储当前节点的可增流值
    for(int i=head[u]; ~i&&res; i=e[i].next) {
    //遍历满足条件的邻边并增流
        int v=e[i].to;
        if(d[v]==d[u]+1&&e[i].cap>e[i].flow) {
    
            int k=DFS(v,min(res,e[i].cap-e[i].flow),t);//获得最小的回溯流
            if(!k)d[v]=0;
            e[i].flow+=k;//获得最小的回溯流后增流
            e[i^1].flow-=k;
            res-=k;//可增流值减少,因为已经有邻边增流
        }
    }
    return flow-res;//返回实际的总增流值
}
int Dinic(int s,int t) {
    
    int ans=0;//存储最大流
    while(BFS(s,t))ans+=DFS(s,inf,t);
    return ans;
}
int main() {
    
    scanf("%d%d",&n,&m);
    int s=0,t=n+1;
    memset(head,-1,sizeof(head));
    for(int i=1; i<=n; i++) {
    
        int a,b;
        scanf("%d%d",&a,&b);
        Add(s,i,a,0);//建图
        Add(i,s,0,0);//残余网络建边
        Add(i,t,b,0);
        Add(t,i,0,0);
    }
    while(m--) {
    
        int a,b,w;
        scanf("%d%d%d",&a,&b,&w);
        Add(a,b,w,0);//建图
        Add(b,a,0,0);//残余网络建边
        Add(b,a,w,0);
        Add(a,b,0,0);
    }
    printf("%d",Dinic(s,t));
    return 0;
}

LuoguP2762

题目大意:略

思路:很明显,仪器和实验构成了二分图,可以尝试用网络流相关算法解决,添加源点连接实验,仪器连接汇点,实验和仪器间由于没有容量约束,容量约束设置为无穷大,如图,将选中的实验和仪器作为S集合,其余的构成T集合
净收益=选中的实验收益-选中的仪器费用= ∑ E i ∈ S p i − ∑ I k ∈ S c k \sum_{E_i∈S}p_i-\sum_{I_k∈S}c_k EiSpiIkSck
而选中的实验收益=所有实验收益-未选择实验收益,则原式可变为

∑ E i ∈ S p i − ∑ I k ∈ S c k \sum_{E_i∈S}p_i-\sum_{I_k∈S}c_k EiSpiIkSck
= ∑ i = 1 m p i − ∑ E i ∈ T p i − ∑ I k ∈ S c k =\sum_{i=1}^mp_i-\sum_{E_i∈T}p_i-\sum_{I_k∈S}c_k =i=1mpiEiTpiIkSck
= ∑ i = 1 m p i − ( ∑ E i ∈ T p i + ∑ I k ∈ S c k ) =\sum_{i=1}^mp_i-(\sum_{E_i∈T}p_i+\sum_{I_k∈S}c_k) =i=1mpi(EiTpi+IkSck)

∑ E i ∈ T p i + ∑ I k ∈ S c k \sum_{E_i∈T}p_i+\sum_{I_k∈S}c_k EiTpi+IkSck为切割线上的边容量之和,即割,为了使总收益最大,那么就要求最小割,进而求最大流

在这里插入图片描述

代码

#include <bits/stdc++.h>
using namespace std;
const int inf=0x3f3f3f3f;
const int maxn=1e4;
int n,m,head[maxn],cnt,ve,vi,sum,d[maxn];
struct node {
    
    int to,next,cap,flow;
} e[maxn];
void Add(int from,int to,int cap,int flow) {
    
    e[cnt].cap=cap;
    e[cnt].flow=flow;
    e[cnt].to=to;
    e[cnt].next=head[from];
    head[from]=cnt++;
}
bool BFS(int s,int t) {
    //分层
    memset(d,0,sizeof(d));
    queue<int>q;
    q.push(s);
    d[s]=1;
    while(!q.empty()) {
    
        int u=q.front();
        q.pop();
        for(int i=head[u]; ~i; i=e[i].next) {
    
            int v=e[i].to;
            if(!d[v]&&e[i].cap>e[i].flow) {
    //如果可以增流
                d[v]=d[u]+1;
                q.push(v);
                if(v==t)return 1;
            }
        }
    }
    return 0;
}
int DFS(int u,int flow,int t) {
    
    if(u==t)return flow;
    int res=flow;//res存储当前节点的可增流值
    for(int i=head[u]; ~i&&res; i=e[i].next) {
    //遍历满足条件的邻边并增流
        int v=e[i].to;
        if(d[v]==d[u]+1&&e[i].cap>e[i].flow) {
    
            int k=DFS(v,min(res,e[i].cap-e[i].flow),t);//获得最小的回溯流
            if(!k)d[v]=0;
            e[i].flow+=k;//获得最小的回溯流后增流
            e[i^1].flow-=k;
            res-=k;//可增流值减少,因为已经有邻边增流
        }
    }
    return flow-res;//返回实际的总增流值
}
int Dinic(int s,int t) {
    
    int ans=0;//存储最大流
    while(BFS(s,t))
        ans+=DFS(s,inf,t);
    return ans;
}
int main() {
    
    scanf("%d%d",&m,&n);
    int s=0,t=n+m+1;
    memset(head,-1,sizeof(head));
    for(int i=1; i<=m; i++ ) {
    
        scanf("%d",&ve);
        Add(s,i,ve,0);
        Add(i,s,0,0);
        sum+=ve;
        char tools[10000];
        memset(tools,0,sizeof tools);
        cin.getline(tools,10000);
        int ulen=0,tool;
        while (sscanf(tools+ulen,"%d",&tool)==1) {
    
            //之前已经用scanf读完了赞助商同意支付该实验的费用
            //tool是该实验所需仪器的其中一个
            //这一行,你可以将读进来的编号进行储存、处理,如连边。
            Add(i,tool+m,inf,0);
            Add(tool+m,i,0,0);
            if (tool==0)
                ulen++;
            else
                while (tool) {
    
                    tool/=10;
                    ulen++;
                }
            ulen++;
        }
    }
    for(int i=1; i<=n; i++) {
    
        int w;
        scanf("%d",&w);
        Add(i+m,t,w,0);
        Add(t,i+m,0,0);
    }
    int mx=Dinic(s,t);
    for(int i=1; i<=m; i++)
        if(d[i])printf("%d ",i);
    putchar('\n');
    for(int i=m+1; i<=m+n; i++)
        if(d[i])printf("%d ",i-m);
    putchar('\n');
    printf("%d",sum-mx);
    return 0;
}

LuoguP4210

题目大意:略

思路:首先说一下,为什么是求最小割,为什么要求最小割
根据题意,将所有的点分成A,B两个大的集合,那么总收益就为 ∑ i ∈ A V a i + ∑ i ∈ B V b i − ∑ i ∈ A V b i − ∑ i ∈ B V a i − ∑ i ∈ A , j ∈ B E i , j + ∑ i ∈ A , j ∈ A E i , j + ∑ i ∈ B , j ∈ B E i , j \sum_{i∈A} V_{a_i}+\sum_{i∈B} V_{b_i}-\sum_{i∈A} V_{b_i}-\sum_{i∈B} V_{a_i}-\sum_{i∈A,j∈B}E_{i,j}+\sum_{i∈A,j∈A}E_{i,j}+\sum_{i∈B,j∈B}E_{i,j} iAVai+iBVbiiAVbiiBVaiiA,jBEi,j+iA,jAEi,j+iB,jBEi,j,也就是,所有点在A中收益+所有点在B中收益-在A中的点的B收益-在B中的点的A收益-连接A,B集合的边收益+连接A集合的边收益+连接B集合的边收益,如图,而 ∑ i ∈ A V b i + ∑ i ∈ B V a i + ∑ i ∈ A , j ∈ B E i , j \sum_{i∈A} V_{b_i}+\sum_{i∈B} V_{a_i}+\sum_{i∈A,j∈B}E_{i,j} iAVbi+iBVai+iA,jBEi,j正好对应的是图中切割线的边,正好是割容量,那么为了整体利益最大,就需要最小割,进而求最大流
在这里插入图片描述接下来是建图,将点分成A,B两部分,不妨让A作为S集合,T作为B集合,点收益直接与对应的源点汇点连接即可,难处理的是题设的边收益,
对于给定的边,如果边必属于A,那么就会丢掉B的收益,那么最小割就必然包括这条边在B上的收益,为了方便表示,将这条边在B上的收益一分为二,分给两个端点,也就是两个端点多了到汇点的两个流量,最小割要么同时经过这两个流量,要么不经过(代表边归B了),对必属于B的点也是如此
接下来考虑边的另一种情况,既属于A,也属于B,如果最小割必经过这条边,那么就必然包括这条边在A,B的收益以及负收益C,也就是将两端点相互连接,流量为ABC收益和(这里重复算了,最后对结果除以2即可)
由于数据可能是奇数不能一分为二,先将数据乘2再对结果除2

代码

#include <bits/stdc++.h>
using namespace std;
const int inf=0x3f3f3f3f;
const int maxn=1e4+10;
int n,m,cnt,head[maxn],d[maxn];
struct node {
    
    int to,next,cap,flow;
} e[maxn*80];
void Add(int from,int to,int cap,int flow) {
    
    e[cnt].to=to;
    e[cnt].next=head[from];
    e[cnt].flow=flow;
    e[cnt].cap=cap;
    head[from]=cnt++;
}
bool BFS(int s,int t) {
    //分层
    memset(d,0,sizeof(d));
    queue<int>q;
    q.push(s);
    d[s]=1;
    while(!q.empty()) {
    
        int u=q.front();
        q.pop();
        for(int i=head[u]; ~i; i=e[i].next) {
    
            int v=e[i].to;
            if(!d[v]&&e[i].cap>e[i].flow) {
    //如果可以增流
                d[v]=d[u]+1;
                q.push(v);
                if(v==t)return 1;
            }
        }
    }
    return 0;
}
int DFS(int u,int flow,int t) {
    
    if(u==t)return flow;
    int res=flow;//res存储当前节点的可增流值
    for(int i=head[u]; ~i&&res; i=e[i].next) {
    //遍历满足条件的邻边并增流
        int v=e[i].to;
        if(d[v]==d[u]+1&&e[i].cap>e[i].flow) {
    
            int k=DFS(v,min(res,e[i].cap-e[i].flow),t);//获得最小的回溯流
            if(!k)d[v]=0;
            e[i].flow+=k;//获得最小的回溯流后增流
            e[i^1].flow-=k;
            res-=k;//可增流值减少,因为已经有邻边增流
        }
    }
    return flow-res;//返回实际的总增流值
}
int Dinic(int s,int t) {
    
    int ans=0;//存储最大流
    while(BFS(s,t))
        ans+=DFS(s,inf,t);
    return ans;
}
int main() {
    
    scanf("%d%d",&n,&m);
    int s=0,t=n+1,sum=0;
    memset(head,-1,sizeof(head));
    Add(s,1,inf,0),Add(1,s,0,0);
    Add(n,t,inf,0),Add(t,n,0,0);
    for(int i=2; i<=n-1; i++) {
    
        int v;
        scanf("%d",&v);
        v*=2;
        sum+=v;
        Add(s,i,v,0),Add(i,s,0,0);
    }
    for(int i=2; i<=n-1; i++) {
    
        int v;
        scanf("%d",&v);
        v*=2;
        sum+=v;
        Add(i,t,v,0),Add(t,i,0,0);
    }
    while(m--) {
    
        int x,y,ea,eb,ec;
        scanf("%d%d%d%d%d",&x,&y,&ea,&eb,&ec);
        ea*=2;
        eb*=2;
        ec*=2;
        sum+=ea+eb;
        Add(x,y,(ea/2+eb/2+ec),0),Add(y,x,(ea/2+eb/2+ec),0);
        Add(s,x,ea/2,0),Add(x,s,0,0);
        Add(s,y,ea/2,0),Add(y,s,0,0);
        Add(x,t,eb/2,0),Add(t,x,0,0);
        Add(y,t,eb/2,0),Add(t,y,0,0);
    }
    printf("%d",(sum-Dinic(s,t))/2);
    return 0;
}

LuoguP4177

题目大意:略

思路:如果没有租/买这一种操作,那么本题就为最大权闭合子图的经典题目,普通的最大权闭合子图将源点与工作相连,边权为收益,机器与汇点相连,边权为代价,工作与机器间对应关系为无穷大,而本题多了一个租用,那么有一种边权就需要改变,可以把无穷大换成对应的租用边权,从正确性考虑,在求解最优方案时,如果总租借费用低于购买机器费用,机器与汇点的边权就不会满流,防止购买机器而增加成本,反之机器与汇点的边权就会满流,限制成本防止多次租用

代码

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=3e6;
const int inf=0x3f3f3f3f;
int n,m,cnt,head[2424],d[2424];//内存不要开的太大
struct node {
    
    int next,to,cap,flow;
} e[maxn];
void Add(int from,int to,int cap,int flow) {
    
    e[cnt].to=to;
    e[cnt].next=head[from];
    e[cnt].cap=cap;
    e[cnt].flow=flow;
    head[from]=cnt++;
}
bool BFS(int s,int t) {
    //分层
    memset(d,0,sizeof(d));
    queue<int>q;
    q.push(s);
    d[s]=1;
    while(!q.empty()) {
    
        int u=q.front();
        q.pop();
        for(int i=head[u]; ~i; i=e[i].next) {
    
            int v=e[i].to;
            if(!d[v]&&e[i].cap>e[i].flow) {
    //如果可以增流
                d[v]=d[u]+1;
                q.push(v);
                if(v==t)return 1;
            }
        }
    }
    return 0;
}
int DFS(int u,int flow,int t) {
    
    if(u==t)return flow;
    int res=flow;//res存储当前节点的可增流值
    for(int i=head[u]; ~i&&res; i=e[i].next) {
    //遍历满足条件的邻边并增流
        int v=e[i].to;
        if(d[v]==d[u]+1&&e[i].cap>e[i].flow) {
    
            int k=DFS(v,min(res,e[i].cap-e[i].flow),t);//获得最小的回溯流
            if(!k)d[v]=0;
            e[i].flow+=k;//获得最小的回溯流后增流
            e[i^1].flow-=k;
            res-=k;//可增流值减少,因为已经有邻边增流
        }
    }
    return flow-res;//返回实际的总增流值
}
int Dinic(int s,int t) {
    
    int ans=0;//存储最大流
    while(BFS(s,t))
        ans+=DFS(s,inf,t);
    return ans;
}
signed main() {
    
    scanf("%lld%lld",&n,&m);
    int s=0,t=n+m+1,sum=0;
    memset(head,-1,sizeof(head));
    for(int i=1; i<=n; i++) {
    
        int x,k;
        scanf("%lld%lld",&x,&k);
        sum+=x;
        Add(s,i,x,0);
        Add(i,s,0,0);
        while(k--) {
    
            int a,b;
            scanf("%lld%lld",&a,&b);
            Add(i,a+n,b,0);
            Add(a+n,i,0,0);
        }
    }
    for(int i=1; i<=m; i++) {
    
        int y;
        scanf("%lld",&y);
        Add(i+n,t,y,0);
        Add(t,i+n,0,0);
    }
    printf("%lld",sum-Dinic(s,t));
    return 0;
}

总结

最小割模型一般用来解决单组点双归属下的多约束最值双组点多匹配下的多约束最值,与二分图有些类似,但是最小割是可以在内部集合有边的,这点与二分图不同,也决定了它大多数情况下不能用二分图的算法,对最小割问题,需要找到划分集合的条件,清楚集合为什么要这样划分,在基础的点权边权条件之外,怎样将题设的其余条件也转换成流量在图中构造,建图始终是最关键的一步,之后的步骤其实是固定的

参考文献

  1. P4177 [CEOI2008]order 题解
  2. P4210 土地划分 题解
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/C_eeking/article/details/122779778

智能推荐

c# 调用c++ lib静态库_c#调用lib-程序员宅基地

文章浏览阅读2w次,点赞7次,收藏51次。四个步骤1.创建C++ Win32项目动态库dll 2.在Win32项目动态库中添加 外部依赖项 lib头文件和lib库3.导出C接口4.c#调用c++动态库开始你的表演...①创建一个空白的解决方案,在解决方案中添加 Visual C++ , Win32 项目空白解决方案的创建:添加Visual C++ , Win32 项目这......_c#调用lib

deepin/ubuntu安装苹方字体-程序员宅基地

文章浏览阅读4.6k次。苹方字体是苹果系统上的黑体,挺好看的。注重颜值的网站都会使用,例如知乎:font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, W..._ubuntu pingfang

html表单常见操作汇总_html表单的处理程序有那些-程序员宅基地

文章浏览阅读159次。表单表单概述表单标签表单域按钮控件demo表单标签表单标签基本语法结构<form action="处理数据程序的url地址“ method=”get|post“ name="表单名称”></form><!--action,当提交表单时,向何处发送表单中的数据,地址可以是相对地址也可以是绝对地址--><!--method将表单中的数据传送给服务器处理,get方式直接显示在url地址中,数据可以被缓存,且长度有限制;而post方式数据隐藏传输,_html表单的处理程序有那些

PHP设置谷歌验证器(Google Authenticator)实现操作二步验证_php otp 验证器-程序员宅基地

文章浏览阅读1.2k次。使用说明:开启Google的登陆二步验证(即Google Authenticator服务)后用户登陆时需要输入额外由手机客户端生成的一次性密码。实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。下载谷歌验证类库文件放到项目合适位置(我这边放在项目Vender下面)https://github.com/PHPGangsta/GoogleAuthenticatorPHP代码示例://引入谷_php otp 验证器

【Python】matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距-程序员宅基地

文章浏览阅读4.3k次,点赞5次,收藏11次。matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距

docker — 容器存储_docker 保存容器-程序员宅基地

文章浏览阅读2.2k次。①Storage driver 处理各镜像层及容器层的处理细节,实现了多层数据的堆叠,为用户 提供了多层数据合并后的统一视图②所有 Storage driver 都使用可堆叠图像层和写时复制(CoW)策略③docker info 命令可查看当系统上的 storage driver主要用于测试目的,不建议用于生成环境。_docker 保存容器

随便推点

网络拓扑结构_网络拓扑csdn-程序员宅基地

文章浏览阅读834次,点赞27次,收藏13次。网络拓扑结构是指计算机网络中各组件(如计算机、服务器、打印机、路由器、交换机等设备)及其连接线路在物理布局或逻辑构型上的排列形式。这种布局不仅描述了设备间的实际物理连接方式,也决定了数据在网络中流动的路径和方式。不同的网络拓扑结构影响着网络的性能、可靠性、可扩展性及管理维护的难易程度。_网络拓扑csdn

JS重写Date函数,兼容IOS系统_date.prototype 将所有 ios-程序员宅基地

文章浏览阅读1.8k次,点赞5次,收藏8次。IOS系统Date的坑要创建一个指定时间的new Date对象时,通常的做法是:new Date("2020-09-21 11:11:00")这行代码在 PC 端和安卓端都是正常的,而在 iOS 端则会提示 Invalid Date 无效日期。在IOS年月日中间的横岗许换成斜杠,也就是new Date("2020/09/21 11:11:00")通常为了兼容IOS的这个坑,需要做一些额外的特殊处理,笔者在开发的时候经常会忘了兼容IOS系统。所以就想试着重写Date函数,一劳永逸,避免每次ne_date.prototype 将所有 ios

如何将EXCEL表导入plsql数据库中-程序员宅基地

文章浏览阅读5.3k次。方法一:用PLSQL Developer工具。 1 在PLSQL Developer的sql window里输入select * from test for update; 2 按F8执行 3 打开锁, 再按一下加号. 鼠标点到第一列的列头,使全列成选中状态,然后粘贴,最后commit提交即可。(前提..._excel导入pl/sql

Git常用命令速查手册-程序员宅基地

文章浏览阅读83次。Git常用命令速查手册1、初始化仓库git init2、将文件添加到仓库git add 文件名 # 将工作区的某个文件添加到暂存区 git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件...

分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120-程序员宅基地

文章浏览阅读202次。分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120

【C++缺省函数】 空类默认产生的6个类成员函数_空类默认产生哪些类成员函数-程序员宅基地

文章浏览阅读1.8k次。版权声明:转载请注明出处 http://blog.csdn.net/irean_lau。目录(?)[+]1、缺省构造函数。2、缺省拷贝构造函数。3、 缺省析构函数。4、缺省赋值运算符。5、缺省取址运算符。6、 缺省取址运算符 const。[cpp] view plain copy_空类默认产生哪些类成员函数

推荐文章

热门文章

相关标签