首先给定一个连通图P={V,E},其中V是点集,E是边集。那么,最小生成树就是一个图P'={V,E'},使得P'是联通的,而且:E'上每一条边的边的权值之和最小。也就是说
![最小生成树 - wenjianwei1 - 算法的设计 最小生成树 - wenjianwei1 - 算法的设计](http://img0.ph.126.net/ssZ8zU8Ll1gjwfLkQ_jQzg==/6619338175654750887.gif)
最小。
最小生成树有着很广泛的应用。比如说有若干个点,然后我们需要将这些点用最小的费用连接起来,比如说网线的连接,每一条网线的铺设费用就是相应的边权,那么我们做一个最小生成树就可以选择若干条边,将这个网线的系统连通。
但是,其实为什么
一定是最小生成
树呢?假设我们现在有一个所谓的“生成子图”,然后这个子图上有一个环,由于是一个无向图,那么我们只需要将这个环上费用最大的边删去,那么仍然这个图还是联通的,而权值却减少了。所以说,这个生成子图如果是最小的,那么一定不能有环,又因为整个图是联通的,且包含了所有原来的点,那么就只能是一棵树了。
通过上面的这个过程,我们很容易就能够想到一个生成最小生成树的算法:将所有的环中权值最大的边给删掉,最后剩下来的无向无环图的就是相应的最小生成树。这个算法其实利用了一个最小生成树的性质:环性质。就是说,如果某一条边在一个环上,而且是这个环上权值最大的一条边,那么它一定不在这个图的所有最小生成树上。通过刚才的论断,我们很轻易地就证明了这一点。因为这一条边没有意义去选嘛!
那么,我们下面来介绍以这个原理为基础的一个算法:Kruskal算法。首先,将每一条边按从小到大的顺序排序,然后不断加入从小到大的每一条边,并维护连通性,如果某一条边所连接的两个点本身在一个联通的集合上,那么就不加这一条边了。加了这一条边就合并两个联通的集合。而最后剩下来的已经加的那一些边,就构成了最小生成树。
其中,连通性很容易想到用并查集去维护,加上排序的复杂度,大概是O(|E|log2|E|+α*|V|)的复杂度。但是实际上这个时间复杂度并不优秀,比如说|E|=|V|^2的时候,光是排序就要花去很多时间了,超过了O(|V|^2)了!不过一般来说并不会那么差,整体而言还是挺好写的,代码也比较短小精悍。
下面给出Kruskal最小生成树算法的证明:首先,最后的图P'必然是联通的。因为算法中不加边有且只有一种情况,那就是这条边的加入已经
没有意义了,其所连接的两个点的集合已经连通了。那么这样加下去的话,如果P‘不连通,那么也只有P不连通了,不满足最初的假设。其次,如果加某一边进去构成了环的话,那么这一条“最后加入的边”根据排序,我们也可以马上知道这条最后加入的边的权值是最大的。根据环性质,我们就会将其删去。那么倒过来看也是一样的,我们不加这条边了,同样其实也是删去。所以说算法的正确性得到了充分的保证。
那么,如果|V|比较大,比如说达到了1000,那么就很有可能超时了。所以说我们需要另外的一种算法:Prim算法。
下面给出一种可能的实现,比如说对于|E|=|V|^2的情况,我们可以对于每一个u
V‘,记录其的最近点
v
V’,然后每更改一个点,就再|V|^2地更新每一个点u的最近点v。不过我们很容易看出这里有很多的重复运算,比如说每一次我们新选一条边{u,v},那么其实我们涉及到的只有u和v,其他点并没有改变。那么,我们在添加一条边{u,v}的时候,我们只需要对于点集的列表V中的每一个u
V’,比较其曾经的最近点v
V'和添加的边{u,v}中的v那个更近即可。
这样的话,效率更高,时间复杂度为O(|V|^2),对于稠密图提高不少。
对于稀疏图而言,我们可以尝试用高级数据结构优化。我们只需要找到一个数据结构,使其支持加入一个数,取最小值和删除最小值之类。可以用堆优化(或者说优先队列、线段树、二叉排序树、甚至是单调队列?)。比如说优先队列,每一次加的时候就将新的扩展边放进去,然后每一次查询新的最小边并将其删除,最后一点一点地扩展,直至全图即可。在这里注意要用边权作为优先级。
证明的话,倒也不算很难。比如说我们扩展使用的是边(u,v),其中当前集合为S,u
S,v
S,而且(u,v)是连接S和V-S的最小权值的边。那么我们现在设定一棵最小生成树T,使得(u,v)
T,那么我们来证明一下,T可以通过交换变成T',而不会更差。显然,因为T是生成树,所以必然存在一条边(u',v'),连接S和V-S。同时,因为T是生成树,那么S和V-S都是生成树。那么因为(u,v)不会比(u',v')更贵,所以说我们可以用(u,v)来代替(u',v')。由前面的结果,S和V-S都是生成树,那么u和u'都可以互通,v和v'也可以互通,而原来的(u',v')也就可以在不影响其他的边的情况下,转换成(u,v)而仍然保持生成树的联通性质。那么因为(u',v')连接S和V-S,用(u,v)代替(u',v'),替换出来的T'的费用,因为(u,v)是最便宜的连接S和V-S的边,所以说不可能有另外一条边比它更便宜(即比(u,v)更优)。因此,T'的费用一定不会比T要好!
这样的话,就可以证明Prim算法的正确性了。因为每一次都是选的S“外围”的最小费用边,所以说根据我们刚才证明的性质,Prim是正确的。刚才的性质好像叫做割性质。
最小生成树主要用于确保图联通而且所使用的费用最小时。在图论之中有着许多的应用!
最后,给出两个代码,首先是Kruskal算法,用于稀疏图:
# include <algorithm>
# include <cstdio>
using namespace std;
const int MAXN = 10000;
const int MAXM = 100000;
struct Edge{
int u;
int v;
int value;
bool operator<(const Edge& x) const{
return this -> value < x.value;
}
};
Edge e[MAXM];
int father[MAXN];
int find_root(int x){
return father[x] = (father[x] == x ? x : find_root(father[x]));
}
inline void union_set(int s1,int s2){
father[find_root(s1)] = find_root(s2);
}
int main(){
int Ans = 0;
int n,m;
scanf("%d%d",&n,&m);
for (int i=0;i<n;++i) father[i] = i;
for (int i=0;i<m;++i){
scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].value);
--e[i].u; --e[i].v;
}
sort(e+0,e+m);
for (int i=0;i<m;++i){
if (find_root(e[i].u) != find_root(e[i].v)){
union_set(e[i].u,e[i].v);
Ans += e[i].value;
}
}
printf("%d\n",Ans);
}
接着是Prim算法的O(n^2)算法,用于稠密图:
# include <cstdio>
using namespace std;
const int MAXN = 5010;
const int INF = 10000000;
int len[MAXN][MAXN];
int minP[MAXN];
int n,m;
bool used[MAXN];
int Ans = 0;
int main(){
scanf("%d%d",&n,&m);
int u,v,l;
int _tAns = 0;
for (int i=0;i<n;++i){
for (int j=0;j<n;++j){
len[i][j] = INF;
}
}
for (int i=0;i<m;++i){
scanf("%d%d%d",&u,&v,&l);
--u; --v;
len[u][v] = len[v][u] = l;
}
for (int i=0;i<n;++i) minP[i] = 0;
used[0] = true;
int minPosLenu,minPosLenv;
for (int i=1;i<n;++i){
minPosLenu = minPosLenv = 0;
for (int j=1;j<n;++j){
if (!used[j] && len[j][minP[j]] < len[minPosLenu][minPosLenv]){
minPosLenu = j;
minPosLenv = minP[j];
}
}
Ans += len[minPosLenu][minPosLenv];
used[minPosLenu] = true;
for (int j=0;j<n;++j){
if (len[j][minP[j]] > len[j][minPosLenu]){
minP[j] = minPosLenu;
}
}
}
printf("%d\n",Ans);
return 0;
}