最小割
最小割问题可以简单的这样描述:给定一张带权有向图,除去几条边,使 s s s 到 t t t 不连通。求去除的边的权值和最小值。
形式化定义一下:对于图 G = ( V , E ) G=(V,E) G = ( V , E ) ,将点划分为 S S S 和 T = V − S T=V-S T = V − S 两个集合,其中源点 s ∈ S s\in S s ∈ S ,汇点 t ∈ T t\in T t ∈ T 。我们定义割 ( S , T ) (S,T) ( S , T ) 的容量为所有从 S S S 到 T T T 的边的容量之和,即 c ( S , T ) = ∑ u ∈ S , v ∈ T c ( u , v ) c(S,T)=\sum_{u\in S,v\in T}c(u,v) c ( S , T ) = ∑ u ∈ S , v ∈ T c ( u , v ) ,最小割问题就是求一个割,使得割的容量 c ( S , T ) c(S,T) c ( S , T ) 最小。
最大流最小割定理
最大流最小割定理:f ( s , t ) max = c ( s , t ) min f(s,t)_{\max}=c(s,t)_{\min} f ( s , t ) m a x = c ( s , t ) m i n 。
即对于一个网络,其最大流与最小割在数值上相等。我们在处理最小割问题时,直接求最大流即可。
求割边数
洛谷P1344 [USACO4.4]追查坏牛奶Pollutant Control
只需跑一次最大流,然后修改图上边权。对于满流的边容量设为 1 1 1 ,没满流的边容量设为 ∞ \infty ∞ ,反向边容量设为 0 0 0 ,然后再跑一次最大流,得到的就是最小割边数。为了达到这样的操作,我们在加边时多记录一个 x
,储存是否是反向边。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct Edge{ int u,v; ll c,f;+ bool x; }; void add_edge(int u,int v,int w){- edges.push_back({u,v,0,w}); - edges.push_back({v,u,0,0}); + edges.push_back({u,v,w,0,0}); + edges.push_back({v,u,0,w,1}); auto m = edges.size(); G[u].push_back(m-2); G[v].push_back(m-1); }
1 2 3 4 5 6 for (auto & e:edges){ if (e.x) e.c=0 ; else if (e.c==e.f) e.c=1 ; else e.c=INF; e.f=0 ; }
AC代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 #include <bits/stdc++.h> using namespace std;#define ll long long const int N = 100 ;const ll INF = 0x3f3f3f3f3f3f3f ;struct Edge { int u,v; ll c,f; bool x; }; vector<Edge> edges; vector<int > G[N];void add_edge (int u,int v,int w) { edges.push_back ({u,v,w,0 ,0 }); edges.push_back ({v,u,0 ,w,1 }); auto m = edges.size (); G[u].push_back (m-2 ); G[v].push_back (m-1 ); }int s,t;int d[N];bool vis[N];bool bfs () { memset (vis,0 ,sizeof (vis)); queue<int > q; q.push (s);vis[s]=1 ;d[s]=0 ; while (!q.empty ()){ auto u=q.front ();q.pop (); for (int e:G[u]){ int v=edges[e].v; if (!vis[v] && edges[e].f<edges[e].c){ vis[v]=1 ;d[v]=d[u]+1 ; q.push (v); } } } return vis[t]; }int cur[N];ll dfs (int u,ll a) { if (u==t || a==0 )return a; ll ans = 0 ; for (int & i=cur[u];i<G[u].size ();i++){ auto & e=edges[G[u][i]]; if (d[e.v]==d[u]+1 && e.f<e.c){ ll f = dfs (e.v,min (a,e.c-e.f)); ans += f;e.f += f; a-=f;edges[G[u][i]^1 ].f -= f; if (a==0 )break ; } } return ans; }ll dinic () { ll ans = 0 ; while (bfs ()){ memset (cur,0 ,sizeof (cur)); ans += dfs (s,INF); } return ans; }int main () { int n,m; scanf ("%d%d" ,&n,&m); for (int i=0 ;i<m;i++){ int u,v,w; scanf ("%d%d%d" ,&u,&v,&w); add_edge (u,v,w); } s=1 ,t=n; printf ("%lld " ,dinic ()); for (auto & e:edges){ if (e.x) e.c=0 ; else if (e.c==e.f) e.c=1 ; else e.c=INF; e.f=0 ; } printf ("%lld\n" ,dinic ()); return 0 ; }
最小割常见模型转化
割点
洛谷P1345 [USACO5.4]奶牛的电信Telecowmunication
题意:给定一张无向图,割 i i i 个点,使 s s s 到 t t t 不连通,求最少割点数。
对于割点问题,我们将一个点拆成两个。对于入边(如下图红色)连接第一个点,对于出边(如下图绿色)连接第二个点,再加入一条第一个点到第二个点的带权有向边(如下图蓝色),权值为该点点权(本题中为1)。对于其他边,边权设为 ∞ \infty ∞ ,这样对拆点后图跑最小割即为结果。
AC代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 #include <bits/stdc++.h> using namespace std;const int INF = 0x3f3f3f3f ;const int N = 105 ;struct Edge { int u,v,c,f; }; vector<Edge> edges; vector<int > G[2 *N];bool vis[2 *N];int cur[2 *N];int d[2 *N];void add_edge (int u,int v,int w) { edges.push_back ({u,v,w,0 }); edges.push_back ({v,u,0 ,0 }); auto x = edges.size (); G[u].push_back (x-2 ); G[v].push_back (x-1 ); }int n,m,s,t;bool bfs () { memset (vis,0 ,sizeof (vis)); queue<int > q; q.push (s); vis[s]=1 ; d[s]=0 ; while (!q.empty ()){ int u = q.front ();q.pop (); for (int i=0 ;i<G[u].size ();i++){ auto & e = edges[G[u][i]]; if (!vis[e.v] && e.f<e.c){ d[e.v]=d[u]+1 ; vis[e.v]=1 ; q.push (e.v); } } } return vis[t]; }int dfs (int u,int a) { if (u==t || a==0 ) return a; int flow = 0 ; for (int &i=cur[u];i<G[u].size ();i++){ auto & e = edges[G[u][i]]; if (d[u]+1 == d[e.v]){ auto f = dfs (e.v,min (a,e.c-e.f)); e.f += f; edges[G[u][i]^1 ].f -= f; a -= f; flow += f; if (a==0 ) break ; } } return flow; }int dinic () { int flow = 0 ; while (bfs ()){ memset (cur,0 ,sizeof (cur)); flow += dfs (s,INF); } return flow; }int main () { scanf ("%d%d%d%d" ,&n,&m,&s,&t); for (int i=1 ;i<=n;i++){ if (i==s || i==t) add_edge (i,i+n,INF); else add_edge (i,i+n,1 ); } t+=n; for (int i=0 ;i<m;i++){ int u,v; scanf ("%d%d" ,&u,&v); add_edge (u+n,v,INF); add_edge (v+n,u,INF); } printf ("%d\n" ,dinic ()); return 0 ; }
二者选其一问题
洛谷P2057 [SHOI2007] 善意的投票
题意:有 n n n 个人,划分到两个集合 A , B A,B A , B 中,第 i i i 个人划分到集合 A A A 花费 a i a_i a i ,划分到集合 B B B 花费 b i b_i b i 。有 m m m 个组合,每个组合有两个人 u j , v j u_j,v_j u j , v j ,若 u j , v j u_j,v_j u j , v j 没有划分到同一集合则花费 w j w_j w j 。求最小花费。
我们考虑建模用最小割解决此问题。建图方法是:从源点向每个点连一条边权为 a i a_i a i 的边,从每个点向汇点连一条边权为 b i b_i b i 的边。在每个组合内,对组合内两个点连边权为 w j w_j w j 的双向边。
考虑有三个点,组合有两个,为 { 1 , 2 } , { 1 , 3 } \{1,2\},\{1,3\} { 1 , 2 } , { 1 , 3 } ,则建出图如下。
怎么理解这样的建图方法呢?我们回到最小割的实质:将一张图划分为两个点集,使两个点集间连边权值和最小 。我们认为,若该点属于 B B B ,则与 s s s 划分到一个集合内,若该点属于 A A A 则与 t t t 划分到一个集合内。
我们先来看上图中黄色边:若一个点与 s s s 划分到一个集合内,则割容量增加它与 t t t 的边权(即划分到 B B B 的花费)。若一个点与 t t t 划分到一个集合内,则割容量增加它与 s s s 的边权(即划分到 A A A 的花费)。
再来看蓝色边:若蓝色边的起点和终点划分到了一个集合内,则这条边的边权不会被算入到容量中。若起点和终点划分到了两个集合中,则这条边的边权会被算入到割容量中。
通过这样构造图,我们保证了一个划分的花费恰好等于这个割的容量 。因此跑一遍最小割算法就是结果。
AC代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 #include <bits/stdc++.h> using namespace std;#define ll long long struct Edge { int u,v; ll f,c; }; const ll INF = 0x3f3f3f3f3f3f3f ;const int N = 305 ; vector<Edge> edges; vector<int > G[N];void add_edge (int u,int v,int w) { edges.push_back ({u,v,0 ,w}); edges.push_back ({v,u,0 ,0 }); auto cnt = edges.size (); G[u].push_back (cnt-2 ); G[v].push_back (cnt-1 ); }int s,t;bool vis[N];int d[N];bool bfs () { memset (vis,0 ,sizeof (vis)); queue<int > q; q.push (s); vis[s]=1 ; d[s]=0 ; while (!q.empty ()){ int u = q.front ();q.pop (); for (int i=0 ;i<G[u].size ();i++){ auto & e = edges[G[u][i]]; if (!vis[e.v] && e.f<e.c){ d[e.v]=d[u]+1 ; vis[e.v]=1 ; q.push (e.v); } } } return vis[t]; } int cur[N];ll dfs (int u,ll a) { if (u==t || a==0 ) return a; ll flow = 0 ; for (int &i=cur[u];i<G[u].size ();i++){ auto & e = edges[G[u][i]]; if (d[u]+1 == d[e.v]){ auto f = dfs (e.v,min (a,e.c-e.f)); e.f += f; edges[G[u][i]^1 ].f -= f; a -= f; flow += f; if (a==0 ) break ; } } if (flow==0 )d[u]=0 ; return flow; }ll dinic () { ll flow = 0 ; while (bfs ()){ memset (cur,0 ,sizeof (cur)); flow += dfs (s,INF); } return flow; }int main () { int n,m; scanf ("%d%d" ,&n,&m); s=n+1 ,t=n+2 ; for (int i=1 ;i<=n;i++){ int x;scanf ("%d" ,&x); add_edge (n+1 ,i,x); add_edge (i,n+2 ,x^1 ); } for (int i=0 ;i<m;i++){ int a,b;scanf ("%d%d" ,&a,&b); add_edge (a,b,1 ); add_edge (b,a,1 ); } printf ("%d\n" ,dinic ()); return 0 ; }
二者选其一问题(2)
洛谷P1361 小M的作物
现在问题出现了一些变化:
有 n n n 个物品,划分到两个集合 A , B A,B A , B 中,第 i i i 个物品划分到集合 A A A 收益 a i a_i a i ,划分到集合 B B B 收益 b i b_i b i 。有 m m m 个组合,每个组合有 c j c_j c j 个人,若组合中的全部划分在 A A A 中,收益 w a j w_{aj} w aj ,若全部划分在 B B B 中,收益 w b j w_{bj} w bj 。求最大收益 。
对于最大收益,我们可以考虑转化一下问题再用最小割。然后用总边权减去最小割,得到的就是最大收益。具体来说,第 i i i 个物品划分到集合 A A A 损失 b i b_i b i ,划分到集合 B B B 损失 a i a_i a i 。对于一个组合若全部划分在 A A A 中,损失 w b j w_{bj} w bj ,若全部划分在 B B B 中,损失 w a j w_{aj} w aj ,否则损失 w a j + w b j w_{aj}+w_{bj} w aj + w bj 。这样即可求最小损失。
对于组合问题,一种很常见的思路就是将这个组合打包到一起(建立虚点)。具体来说建图方法是,在上一问建图的基础上,对每个组合增加两个虚点,两个虚点分别与 s , t s,t s , t 连边,虚点与组合内点连边权为无穷的边。
若 1 , 2 , 3 {1,2,3} 1 , 2 , 3 是一个组合,建出图如下。
我们思考一下这样的正确性。
若全部划分在一个集合中
我们把与 s s s 划分在一起的点染成红色,与 t t t 划分在一起的点染成蓝色。假设与 s s s 划分在一起,则划分如下。
接下来要决定的是虚点的划分。注意到不可能把边权为无穷的边割掉,所以右边的虚点一定是红色(与 s s s 划分到一起),不然就会出现边权为无穷的边在割边集中。左边的虚点是红色还是蓝色并不会产生无穷的问题,但是我们求的是最小割,染成红色明显割容量更小。最终割边如下图绿箭头所示。
我们注意到,这时候割的容量就是我们前文所提到的损失。若全部与 t t t 划分在一起,情况类似。
若划分在两个集合中
假设 1 , 2 1,2 1 , 2 染成蓝色,3 3 3 染成红色。如下图。
同理,这两个虚点一定一个红一个蓝,否则就会出现无穷边权的边加入割边。
这时候的割容量也恰好就是我们的损失。正确性显然。
AC代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 #include <bits/stdc++.h> using namespace std;#define ll long long struct Edge { int u,v; ll f,c; }; const ll INF = 0x3f3f3f3f3f3f3f ;const int N = 4e3 +5 ; vector<Edge> edges; vector<int > G[N];void add_edge (int u,int v,ll w) { edges.push_back ({u,v,0 ,w}); edges.push_back ({v,u,0 ,0 }); auto cnt = edges.size (); G[u].push_back (cnt-2 ); G[v].push_back (cnt-1 ); }int s,t;bool vis[N];int d[N];bool bfs () { memset (vis,0 ,sizeof (vis)); queue<int > q; q.push (s); vis[s]=1 ; d[s]=0 ; while (!q.empty ()){ int u = q.front ();q.pop (); for (int i=0 ;i<G[u].size ();i++){ auto & e = edges[G[u][i]]; if (!vis[e.v] && e.f<e.c){ d[e.v]=d[u]+1 ; vis[e.v]=1 ; q.push (e.v); } } } return vis[t]; } int cur[N];ll dfs (int u,ll a) { if (u==t || a==0 ) return a; ll flow = 0 ; for (int &i=cur[u];i<G[u].size ();i++){ auto & e = edges[G[u][i]]; if (d[u]+1 == d[e.v]){ auto f = dfs (e.v,min (a,e.c-e.f)); e.f += f; edges[G[u][i]^1 ].f -= f; a -= f; flow += f; if (a==0 ) break ; } } if (flow==0 )d[u]=0 ; return flow; }ll dinic () { ll flow = 0 ; while (bfs ()){ memset (cur,0 ,sizeof (cur)); flow += dfs (s,INF); } return flow; }int main () { int n; scanf ("%d" ,&n); s=n+1 ,t=n+2 ; ll ans = 0 ; for (int i=1 ;i<=n;i++){ int a;scanf ("%d" ,&a); add_edge (s,i,a);ans+=a; } for (int i=1 ;i<=n;i++){ int b;scanf ("%d" ,&b); add_edge (i,t,b);ans+=b; } int m; scanf ("%d" ,&m); for (int i=0 ,j=t+1 ;i<m;i++,j+=2 ){ int k;scanf ("%d" ,&k); int wa,wb;scanf ("%d%d" ,&wa,&wb); ans+=wa+wb; add_edge (s,j,wa); add_edge (j+1 ,t,wb); while (k--){ int x; scanf ("%d" ,&x); add_edge (j,x,INF); add_edge (x,j+1 ,INF); } } ans -= dinic (); printf ("%lld\n" ,ans); return 0 ; }
最大权闭合图
洛谷P2762 太空飞行计划问题
先考虑求出最大净收益。
对于每个实验,连一条从源点到实验,边权为实验利益的边。
对于每个需要的仪器,连一条从实验到器材,边权为 ∞ \infty ∞ 的边。
对于每个仪器,连一条从器材到汇点,边权为器材耗费的边。
这样建图后用求最小割,答案即为实验利益总和 ∑ p \sum p ∑ p 减去最小割 c ( s , t ) min c(s,t)_{\min} c ( s , t ) m i n
我们规定,与源点在同一个集合代表被选中。若一个实验被选中,由于有到器材且边权为无穷的边,这些器材也一定被选中。这时候割的容量增加器材的价格。(即代表花费了这些买器材的钱)
若一个实验没有被选中,割的容量增加实验的利益。(即代表实验的这些钱没有收到亏损了)
在考虑求出选择的物品集合。我们回到最小割的定义:将一张图划分为两个点集,使两个点集间连边权值和最小 。选择的物品集合就是与源点在同一个点集内的点。我们可以知道,两个点集间的边的流量一定 流满,所以最后在残量网络上,与源点连通点就是选择的物品。(在Dinic算法里,就是最后一次 bfs 过程中 d 数组不为 0 0 0 )
AC代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 #include <bits/stdc++.h> using namespace std;#define ll long long struct Edge { int u,v; ll f,c; }; const ll INF = 0x3f3f3f3f3f3f3f ;const int N = 150 ; vector<Edge> edges; vector<int > G[N];void add_edge (int u,int v,ll w) { edges.push_back ({u,v,0 ,w}); edges.push_back ({v,u,0 ,0 }); auto cnt = edges.size (); G[u].push_back (cnt-2 ); G[v].push_back (cnt-1 ); }int s,t;bool vis[N];int d[N];bool bfs () { memset (vis,0 ,sizeof (vis)); queue<int > q; q.push (s); vis[s]=1 ; d[s]=0 ; while (!q.empty ()){ int u = q.front ();q.pop (); for (int i=0 ;i<G[u].size ();i++){ auto & e = edges[G[u][i]]; if (!vis[e.v] && e.f<e.c){ d[e.v]=d[u]+1 ; vis[e.v]=1 ; q.push (e.v); } } } return vis[t]; } int cur[N];ll dfs (int u,ll a) { if (u==t || a==0 ) return a; ll flow = 0 ; for (int &i=cur[u];i<G[u].size ();i++){ auto & e = edges[G[u][i]]; if (d[u]+1 == d[e.v]){ auto f = dfs (e.v,min (a,e.c-e.f)); e.f += f; edges[G[u][i]^1 ].f -= f; a -= f; flow += f; if (a==0 ) break ; } } if (flow==0 )d[u]=0 ; return flow; }ll dinic () { ll flow = 0 ; while (bfs ()){ memset (cur,0 ,sizeof (cur)); flow += dfs (s,INF); } return flow; }char tools[10000 ];int main () { int n,m; scanf ("%d%d" ,&n,&m); s=n+m+1 ,t=n+m+2 ; ll ans; for (int i=1 ;i<=n;i++){ int cost;scanf ("%d" ,&cost); add_edge (s,i,cost);ans+=cost; memset (tools,0 ,sizeof tools); cin.getline (tools,10000 ); int ulen=0 ,tool; while (sscanf (tools+ulen,"%d" ,&tool)==1 ){ add_edge (i,tool+n,INF); if (tool==0 ) ulen++; else { while (tool) { tool/=10 ; ulen++; } } ulen++; } } for (int i=n+1 ;i<=n+m;i++){ int cost;scanf ("%d" ,&cost); add_edge (i,t,cost); } ans -= dinic (); for (int i=1 ;i<=n;i++){ if (vis[i])printf ("%d " ,i); } puts ("" ); for (int i=n+1 ;i<=n+m;i++){ if (vis[i])printf ("%d " ,i-n); } puts ("" ); printf ("%lld\n" ,ans); return 0 ; }
练习:ARC085C MUL