Karger算法求最小割

首先要知道什么是割(cut)。割是把图的节点划分成两个集合S和T,那么有一些边的端点是分别处于S和T中的。所谓最小割就是使这种边的数目最少的划分。

理论分析

Karger算法是随机算法,它的描述很简单:

每次随机选择一条边,把边的两个端点合二为一。原来与这两个点邻接的点,现在把边连到合并后的节点去,把原来的点和边删除。

合并后可能会有平行边,在邻接矩阵里记录边的数目。把形成的自环删除。可以看到,合并前的两个点与“外界”的连接边数、合并后的点与“外界”的连接边数(即合并点的度数),这两者是一样的。所以当合并至只剩两个点时,相当于将原图的点划分成两个集合。这两个点之间的连边数就是形成的割的边数。

现在分析下算法正确的概率。因为一个图可能有多个最小割,我们来计算算法得到某个特定最小割的概率。设最小割交叉边的数目为c。那么图中每个点的度数至少为c(因为如果某个点的度数小于c,可以把这个点和其他点分开,形成的最小割边数小于c,与假设矛盾。注意这个结论在合并点过程中仍然成立,因为前面说过了,合并得到的点的度数等于它代表的集合与外界的连接数)。如果图有n个节点,那么至少有n*c/2条边。我们不能选特定的c条边,否则就不是特定的割了。不选这c条边的概率是(1-c/边数)>= (1-2/n). 每合并一次,点的数目减一。那么在整个过程中都不选那c条边的概率是>=(1-2/n)*(1-2/(n-1))*...*(1-2/3) = 2/(n*(n-1)) >= 1/n2.

这个正确概率看着好像不很高。不过我们运行算法多次。在每次运行中,没得到特定最小割的概率<=1-1/n2。那么运行m次没得到的概率<=(1-1/n2)m。由于1-1/n2<=e-1/n^2(因为1+x<=ex),当m=n2,失败的概率小于1/e。当m=n2ln(n)时,失败的概率更是不到1/n,考虑到一般图的节点数量,可以忽略不计。

插播:n个节点的图不同最小割的数量最多多少?

答案:n*(n-1)/2. 前面我们看到Karger算法找到特定最小割的概率>=2/(n*(n-1)). 设有r个最小割。因为得到这些割是互斥事件,所以得到任意一个最小割的概率>=2/(n*(n-1))*r. 因为概率最大为1,r最大为n*(n-1)/2。这是可以取到的。比如n个点的环,任意删除两条边都得到一个独特的最小割(最小割边数为2),因此为n*(n-1)/2.

算法实现

1.

虽然这个算法的描述简洁易懂,但要实现好还是让我颇费思量。先来抛个砖,给个O(n2)的实现,n是结点数。这个实现按照算法的字面描述,对于随机选定的一条边的两个结点v1,v2(v1<v2),把和v2相关的边合并到v1中去。具体的操作就是更新相关的点的度数和邻接矩阵的项。程序没有储存边,当随机生成一个边编号时,先用度数确定第一个端点,再根据这个端点的邻接矩阵项找到另一个端点。这两个过程都是O(n)的。

#include <bits/stdc++.h>
using namespace std;

class Graph
{
private:
    //For i = 1 and larger, adjList[i][0] = i.
    //For j = 1 and larger, adjList[i][j] is adjacent to i.
    vector<vector<int> > adjList;
    int n;
    vector<vector<int> > adjMatrix;
    vector<int> deg;
private:
    void getEdgeEnds(int edgeIndex, int& row, int& col);
    int initForContract();
    int contract();
public:
    int minCut(int nIter);
    int getNodeNum();
    friend void buildGraph(char *fname, Graph& g);
};

int Graph::getNodeNum()
{
    return n;
}

void Graph::getEdgeEnds(int edgeIndex, int& row, int& col)
{
    int num = 0;
    for(row=1;row<=n;row++)
    {
        if(num + deg[row] >= edgeIndex)
            break;
        num += deg[row];
    }
    for(col=1;col<=n;col++)
    {
        num += adjMatrix[row][col];
        if(num >= edgeIndex)
            break;
    }
}

int Graph::initForContract()
{
    if(deg.size() == 0)
    {
        deg.assign(n+1, 0);
        for(int i=0; i<=n; i++)
        {
            vector<int> tmp(n+1, 0);
            adjMatrix.push_back(tmp);
        }
    }
    else
    {
        for(int i=1; i<=n; i++)
            for(int j=1; j<=n; j++)
            adjMatrix[i][j] = 0;
    }
    int edgeNum = 0;
    for(int i=1; i<=n; i++)
    {
        deg[i] = 0;
        for(int j=1;j<adjList[i].size();j++)
        {
            int node = adjList[i][j];
            adjMatrix[i][node] = 1;
            deg[i]++;
        }
        edgeNum += deg[i];
    }
    return edgeNum;
}

int Graph::contract()
{
    int edgeNum = initForContract();
    for(int iter = 0;iter < n-2; iter++)
    {
        int edgeIndex = rand()%edgeNum+1;
        int v1, v2;
        getEdgeEnds(edgeIndex, v1, v2);
        if(v1 > v2)
            swap(v1, v2);
        for(int i=1; i <= n; i++)
        {
            if(i == v1)
            {
                deg[v1] -= adjMatrix[v1][v2];
                edgeNum -= 2*adjMatrix[v1][v2];
                adjMatrix[v1][v2] = adjMatrix[v2][v1] = 0;
            }
            else if(adjMatrix[v2][i])
            {
                adjMatrix[v1][i] += adjMatrix[v2][i];
                adjMatrix[i][v1] = adjMatrix[v1][i];
                deg[v1] += adjMatrix[v2][i];
                adjMatrix[v2][i] = adjMatrix[i][v2] = 0;
            }
        }
        deg[v2] = 0;
    }
    return edgeNum/2;
}

int Graph::minCut(int nIter)
{
    srand(time(NULL));
    int mincut = n*n;
    for(int i=0; i<nIter; i++)
    {
        mincut = min(mincut, contract());
    }
    return mincut;
}

void parse(char str[], vector<vector<int> > &adjList)
{
    int len = strlen(str);
    int offset=0;
    int node;
    sscanf(str,"%d%n",&node,&offset);
    vector<int> lst(1, node);
    while(offset < len)
    {
        int neibor, count;
        sscanf(str+offset, "%d%n", &neibor, &count);
        offset += count;
        lst.push_back(neibor);
        if(offset == len-1)
            break;
    }
    adjList.push_back(lst);
}
void buildGraph(char *fname, Graph &g)
{
    freopen(fname, "r", stdin);
    vector<int> tmp(1, 0);
    g.adjList.push_back(tmp);
    char str[200];
    while(gets(str)!=NULL)
        parse(str, g.adjList);
    g.n = g.adjList.size()-1;
}

int main()
{
    time_t st = clock();
    Graph g;
    buildGraph("kargerMincut.txt", g);
    int n = g.getNodeNum();
    int nIter = n*n*int(log(n*1.0));
    int mincut = g.minCut(nIter);
    time_t ed = clock();
    printf("%f\n",(double)(ed-st)/CLOCKS_PER_SEC);
    printf("%d\n",mincut);
    return 0;
}
View Code

2.

另外的方法是事先打乱边的顺序,然后遍历边,把边的两端点连起来(而不是删边了),直到只剩两个连通分量为止,剩下的边里端点在不同连通分量的边的数目就是割的大小。

查询边的端点是否处于不同的连通分量,(是的话)把两个连通分量连起来可以用并查集(此时总连通分量数减一)。

#include <bits/stdc++.h>
using namespace std;

class UnionFind
{
protected:
    int componentNum;
    vector<int> parent, rank;
    void MakeSet(int n);
    int Find(int x);
    void Union(int x, int y);
};

void UnionFind::MakeSet(int n)
{
    componentNum = n;
    parent.assign(n+1, 0);
    for(int i=1; i<=n; i++)
        parent[i] = i;
    rank.assign(n+1, 0);
}

int UnionFind::Find(int x)
{
    if(x != parent[x])
        parent[x] = Find(parent[x]);
    return parent[x];
}

void UnionFind::Union(int x, int y)
{
    int rootX = Find(x), rootY = Find(y);
    if(rootX != rootY)
    {
        if(rank[rootX] < rank[rootY])
            parent[rootX] = rootY;
        else if(rank[rootY] < rank[rootX])
            parent[rootY] = rootX;
        else
        {
            parent[rootY] = rootX;
            rank[rootX]++;
        }
        componentNum--;
    }
}

class Graph:public UnionFind
{
private:
    //For i = 1 and larger, adjList[i][0] = i.
    //For j = 1 and larger, adjList[i][j] is adjacent to i.
    vector<vector<int> > adjList;
    int n;
    vector<pair<int, int> > edgeList;
private:
    int contract();
    void makeEdgeList();
public:
    int minCut(int nIter);
    int getNodeNum();
    friend void buildGraph(char *fname, Graph& g);
};

void Graph::makeEdgeList()
{
    for(int i=1; i<=n; i++)
        for(int j=1; j<adjList[i].size(); j++)
        {
            int node = adjList[i][j];
            if(i < node)
            {
                pair<int, int> edge(i, node);
                edgeList.push_back(edge);
            }
        }
}

int Graph::getNodeNum()
{
    return n;
}

int Graph::contract()
{
    random_shuffle(edgeList.begin(), edgeList.end());
    int i;
    for(i = 0; componentNum > 2; i++)
    {
        int v1 = edgeList[i].first, v2 = edgeList[i].second;
        Union(v1, v2);
    }
    int cut = 0;
    for(; i<edgeList.size(); i++)
    {
        int v1 = edgeList[i].first, v2 = edgeList[i].second;
        int root1 = Find(v1), root2 = Find(v2);
        if(root1 != root2)
            cut++;
    }
    return cut;
}

int Graph::minCut(int nIter)
{
    makeEdgeList();
    srand(time(NULL));
    int mincut = n*n;
    for(int i=0; i<nIter; i++)
    {
        MakeSet(n);
        mincut = min(mincut, contract());
    }
    return mincut;
}

void parse(char str[], vector<vector<int> > &adjList)
{
    int len = strlen(str);
    int offset=0;
    int node;
    sscanf(str,"%d%n",&node,&offset);
    vector<int> lst(1, node);
    while(offset < len)
    {
        int neibor, count;
        sscanf(str+offset, "%d%n", &neibor, &count);
        offset += count;
        lst.push_back(neibor);
        if(offset == len-1)
            break;
    }
    adjList.push_back(lst);
}
void buildGraph(char *fname, Graph &g)
{
    freopen(fname, "r", stdin);
    vector<int> tmp(1, 0);
    g.adjList.push_back(tmp);
    char str[200];
    while(gets(str)!=NULL)
        parse(str, g.adjList);
    g.n = g.adjList.size()-1;
}

int main()
{
    time_t st = clock();
    Graph g;
    buildGraph("kargerMincut.txt", g);
    int n = g.getNodeNum();
    int nIter = n*n*int(log(1.0*n));
    int mincut = g.minCut(nIter);
    time_t ed = clock();
    printf("%f\n",(double)(ed-st)/CLOCKS_PER_SEC);
    printf("%d\n",mincut);
    return 0;
}
View Code

3.

我们来看看Karger论文里的方法。我们需要确定的其实是使得只剩两个连通分量的边序列的最短前缀。知道了这个,在剩下的边里求交叉边的数量就容易了。注意到遍历边的过程中,连通分量的数目是单调减少的,因此我们可以二分。然而每次需要O(m)(m是边数)来确定连通分量数目(用bfs之类),总复杂度O(m*log(m)),不够好。

另一种方法是先选前m/2条边,如果形成了一个连通分量,那么多了,考虑前m/4。如果形成多于两个,再考虑后m/2的前一半。如果刚好是两个,则可以直接计算割了。其实也是二分,关键是在已有的图上加边看连通分量的变化,还没想好怎么写。

参考资料:

Algorithms: Design and Analysis, Part 1

Karger, David (1993). "Global Min-cuts in RNC and Other Ramifications of a Simple Mincut Algorithm"

posted @ 2015-03-23 15:55  demoZ  阅读(6239)  评论(0编辑  收藏  举报