最大流最小割

本文深入探讨了最大流最小割算法的基本思想及其在解决实际问题中的应用,通过四个具体的编程题目(POJ3469、LuoguP2762、LuoguP4210、LuoguP4177)进行案例分析,展示了如何将原问题转化为求解最小割问题,进而转换为最大流问题。总结了最小割模型在处理单组点双归属和双组点多匹配问题中的优势,并强调了建图在解题过程中的核心地位。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

算法思想

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

割: 网络的节点划分,所有节点被划分成 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 土地划分 题解
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值