SCL--无向图全局最小割(Stoer_Wagner)
2015-05-17 23:25:53
总结:由于 2010 福州 遇到了这种类型的问题,是时候来总结一下,并推出SCL了。
概念:在一个无向连通图中,去掉某些边集能使得图被划分两个独立的连通分量,边权和最小的边集为全局最小割(Global Min_Cut)。
初想:由于要把图分量两个点集,所以我们可以随便定一个起点(比如1),再枚举其他点(2~n)为终点,跑(n-1)次最大流,求其最小值。
(割的性质使得不需要枚举每一对源汇,而只需要求 n-1 次最小割,这种去除冗余的思想十分值得借鉴。)
如果采用最高标号先流推进,每次最大流:O(n^2 * sqrt(m)),总:O(n^3 * sqrt(m)) ;而如果采用 Dinic,则总的更大:O(n^3 * m),显然是很容易TLE的。
思路:事实上,Global Min_Cut 存在一个不依赖于网络流的优秀算法:Stoer_Wagner 算法。
参考下大红书的描述:如果我们可以求出任意一个 s-t 的最小割 C,那么,G 的全局最小割必然是 C 与 G / {s,t} 的全局最小割中较小的一个。G / {s,t} 表示将 s,t
合并之后得到新图。这来源于一个显然的定理:如果 G 的全局最小割分割了 s-t 那么它就等于 s-t 的最小割,否则 s 和 t 属于割后的同一点集,那么可以合并。
接下来我们来看 SW 算法中用类似生成树算法来求最小割的过程:
(1)设 A 为一个点集,开始时为空。
(2)每次在 V \ A 中选取一个 wage(A,x) 最大的点 x 加入 A 中,直到 A = V 时停止。
式中 wage(A,x) 表示 A 中所有点与 x 相连的所有边的边权和。( wage(A,x)= Sigma(w(x,y) ), y∈A))
更新方法:每加入一个点 x 进入 A 时,对于 x 相连的每个不在 A 中的点 p,其 wage 值加上 w(x,p)。
(3)令 s 为倒数第二个加入 A 中的点,t 为最后一个加入 A 中的点。我们可以发现一个惊人的结论:最后 t 的 wage 值就是 s-t 的最小割。
【注意:为什么要每次选最大的 wage 进行拓展呢,假设当前点集 |A|=V-2,剩下 B,C 两个点,wage(B) > wage(C),如果不选 B 而选 C,
拓展了 C 之后再拓展 B,最后割的答案 ans1=wage(B) + w(B,C),显然大于 wage(C) + w(B,C),所以应该优先拓展 B。这个地方值得更深的思考】
基于以上,我们就获得了崭新的 Stoer_Wagner 算法:
※每做一次类生成树算法,用 t 的 wage 更新全局最小割的值,然后合并 s 和 t 点(方法是合并边权),这样循环 n-1 次就获得了全局最小割。
目前主流有两种写法,测试平台:POJ 2914
※写法一,用标号数组给每个点一个标号(一开始为自己),缩点就是合并标号,速度快,3000+MS
int g[MAXN][MAXN]; int v[MAXN],wg[MAXN],vis[MAXN]; int SW(int n){ int res = -1; for(int i = 0; i < n; ++i) v[i] = i; //点标号的马甲,一开始都是自己。 while(n > 1){ memset(vis,0,sizeof(vis)); //标记数组 memset(wg,0,sizeof(wg)); //权和数组 int pre = 0; //起点为0号点 vis[pre] = 1; //标记起点,虽无实际用处,仅为提醒自己 for(int i = 1; i < n; ++i){ int p = -1; for(int j = 1; j < n; ++j) if(!vis[v[j]]){ //寻找下个拓展点 wg[v[j]] += g[v[pre]][v[j]]; //下个点的权和加上边权 if(p == -1 || wg[v[j]] > wg[v[p]]) p = j; //寻找wg值最大的点 } vis[v[p]] = 1; //标记下个点已经遍历 if(i == n - 1){ //最后个点,需要合并 if(res == -1) res = wg[v[p]]; else res = min(res,wg[v[p]]); //更新res,取最小割 for(int j = 0; j < n; ++j){ g[v[pre]][v[j]] += g[v[p]][v[j]]; g[v[j]][v[pre]] += g[v[j]][v[p]]; } v[p] = v[--n]; } pre = p; } } return res; }
※写法二:思路清晰,不采用缩点而采用删点,速度较慢,8000+MS
int g[MAXN][MAXN]; int wg[MAXN]; bool vis[MAXN],del[MAXN]; int N,M; int Solve(int &st,int &ed){ st = ed = -1; memset(vis,0,sizeof(vis)); //生成树前的必要初始化 memset(wg,0,sizeof(wg)); int min_cut = -1,p = 0; for(int i = 0; i < N; ++i){ int tmax = -1; //找后继最大的权点 for(int j = 0; j < N; ++j) if(!del[j] && !vis[j]){ if(p == -1 || wg[j] > tmax){ p = j; tmax = wg[j]; } } if(p == ed) return min_cut; //点没变,无法扩展 st = ed; ed = p; min_cut = tmax; vis[p] = 1; for(int j = 0; j < N; ++j) //拓展点后更新权和数组 if(!del[j] && !vis[j]) wg[j] += g[p][j]; } return min_cut; } int SW(){ int ans = -1,st,ed; memset(del,0,sizeof(del)); for(int i = 1; i < N; ++i){ //删点N-1次 int min_cut = Solve(st,ed); //传起点、终点 if(ans == -1) ans = min_cut; else ans = min(ans,min_cut); if(ans == 0) return 0; //最小割为0,直接剪枝,返回0 del[ed] = 1; for(int j = 0; j < N; ++j) if(!del[j] && j != st){ g[st][j] += g[ed][j]; //删点后合并边权 g[j][st] += g[j][ed]; } } return ans; }