SCL--无向图全局最小割(Stoer_Wagner)

2015-05-17 23:25:53

总结:由于 2010 福州 遇到了这种类型的问题,是时候来总结一下,并推出SCL了。

  推荐学习博客:博客1博客2

  概念:在一个无向连通图中,去掉某些边集能使得图被划分两个独立的连通分量,边权和最小的边集为全局最小割(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;
}
View Code

 

转载于:https://www.cnblogs.com/naturepengchen/articles/4512238.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值