首先要知道什么是割(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; }
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; }
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"