最小生成树——Kruskal算法
先上最小生成树定义:
在一个给定的无向图G=(V,E)中, (u,v)代表连接顶点u和v的边,而w(u,v)代表这个边的权重。若存在T为E的子集,且为无循环图,使得
中的w(T)最小,则这个T为G的最小生成树(MST)
最小生成树的几个性质:
- 最小生成树的边数必然是顶点数减一,|E| = |V| - 1。
- 最小生成树不可以有循环。
- 最小生成树不必是唯一的。
求解一个图的最小生成树有很多算法,这里介绍Kruskal算法。
算法的执行过程可以描述为:
1.将图中所有的边按照从小到大的顺序排序
2.从小到大依次考察每条边(u,v)是否要当前的生成树集中。考查结果分为两种情况:
case 1: u和v在同一连通分量中,那么加入(u,v)后会形成环,因此要放弃这种情况.
case 2: u,v不在同一连通分量中,那么加入边(u,v)一定是最优的。证明可以用cut&paste来解释。
3.对每一条边循环第二步,直到加入的边的数目=顶点数目-1(即形成了树)
根据上面的过程可以得到伪代码:
Kruskal:
将所有边排序,记第i小的边为e[i](1<=i<m)
初始化MST为空
初始化连通分量,让每个点成为一个独立的连通分量
for(int i=0;i<m;i++)
if(e[i].u和e[i].v不在同一连通分量中)
{
把边e[i]加入MST
合并e[i].u和e[i].v所在的连通分量
}
END
由上面伪代码我们可以看出,问题的关键是连通分量的查询和合并,自然我们就能想到高效的解决方案是并查集.
由此我们给出完整的代码:
假设第i条边的两个端点序号和权值分别保存在u[i],v[i]和w[i]中,而排序后第i小的边的序号保存在r[i]中(这里先将原顺序的序号放在r[i]中,然后按照w[i]来排序,排完后r[i]就是按照从小到大的序号排列的了——这称为间接排序!)
int cmp(const int i, const int j){ return w[i]<w[j]; } int find(int x) { if(x!=father[x]) return father[x]=find(father[x]); return x; } int Kruskal() { int total=0,count=0; //对n个顶点初始化并查集 for(int i=0;i<n;i++) father[i]=i; //初始化序号记录集 for(int i=0;i<m;i++) r[i]=i; sort(r,r+m,cmp); for(int i=0;i<m;i++)//对每一条边来考查 { int e = r[i]; int ru = find(u[e]); int rv = find(v[e]); if(ru!=rv) { total+=w[e]; count++; father[rv] =ru; } if(count=n-1) break; } return total; }
由此由并查集实现的Kruskal算法就搞定了!