最小生成树

首先给定一个连通图P={V,E},其中V是点集,E是边集。那么,最小生成树就是一个图P'={V,E'},使得P'是联通的,而且:E'上每一条边的边的权值之和最小。也就是说 最小生成树 - wenjianwei1 - 算法的设计最小。
  最小生成树有着很广泛的应用。比如说有若干个点,然后我们需要将这些点用最小的费用连接起来,比如说网线的连接,每一条网线的铺设费用就是相应的边权,那么我们做一个最小生成树就可以选择若干条边,将这个网线的系统连通。
但是,其实为什么 一定是最小生成 呢?假设我们现在有一个所谓的“生成子图”,然后这个子图上有一个环,由于是一个无向图,那么我们只需要将这个环上费用最大的边删去,那么仍然这个图还是联通的,而权值却减少了。所以说,这个生成子图如果是最小的,那么一定不能有环,又因为整个图是联通的,且包含了所有原来的点,那么就只能是一棵树了。
通过上面的这个过程,我们很容易就能够想到一个生成最小生成树的算法:将所有的环中权值最大的边给删掉,最后剩下来的无向无环图的就是相应的最小生成树。这个算法其实利用了一个最小生成树的性质:环性质。就是说,如果某一条边在一个环上,而且是这个环上权值最大的一条边,那么它一定不在这个图的所有最小生成树上。通过刚才的论断,我们很轻易地就证明了这一点。因为这一条边没有意义去选嘛!
那么,我们下面来介绍以这个原理为基础的一个算法:Kruskal算法。首先,将每一条边按从小到大的顺序排序,然后不断加入从小到大的每一条边,并维护连通性,如果某一条边所连接的两个点本身在一个联通的集合上,那么就不加这一条边了。加了这一条边就合并两个联通的集合。而最后剩下来的已经加的那一些边,就构成了最小生成树。
其中,连通性很容易想到用并查集去维护,加上排序的复杂度,大概是O(|E|log2|E|+α*|V|)的复杂度。但是实际上这个时间复杂度并不优秀,比如说|E|=|V|^2的时候,光是排序就要花去很多时间了,超过了O(|V|^2)了!不过一般来说并不会那么差,整体而言还是挺好写的,代码也比较短小精悍。
下面给出Kruskal最小生成树算法的证明:首先,最后的图P'必然是联通的。因为算法中不加边有且只有一种情况,那就是这条边的加入已经 没有意义了,其所连接的两个点的集合已经连通了。那么这样加下去的话,如果P‘不连通,那么也只有P不连通了,不满足最初的假设。其次,如果加某一边进去构成了环的话,那么这一条“最后加入的边”根据排序,我们也可以马上知道这条最后加入的边的权值是最大的。根据环性质,我们就会将其删去。那么倒过来看也是一样的,我们不加这条边了,同样其实也是删去。所以说算法的正确性得到了充分的保证。
那么,如果|V|比较大,比如说达到了1000,那么就很有可能超时了。所以说我们需要另外的一种算法:Prim算法。
Prim算法主要是这样的:首先,将当前的点集合V'设为随意的一个点u 最小生成树 - wenjianwei1 - 算法的设计 V,然后不断地循环,每一次都选一条边,使得这条边权值最小,而且这条边e={u,v},使得u 最小生成树 - wenjianwei1 - 算法的设计 V’且v 最小生成树 - wenjianwei1 - 算法的设计 V‘。也就是说,选择只有一个端点在V'中的一条权值最小的边,以扩展这个点集V'。
下面给出一种可能的实现,比如说对于|E|=|V|^2的情况,我们可以对于每一个u 最小生成树 - wenjianwei1 - 算法的设计 V‘,记录其的最近点 v 最小生成树 - wenjianwei1 - 算法的设计 V’,然后每更改一个点,就再|V|^2地更新每一个点u的最近点v。不过我们很容易看出这里有很多的重复运算,比如说每一次我们新选一条边{u,v},那么其实我们涉及到的只有u和v,其他点并没有改变。那么,我们在添加一条边{u,v}的时候,我们只需要对于点集的列表V中的每一个u 最小生成树 - wenjianwei1 - 算法的设计 V’,比较其曾经的最近点v 最小生成树 - wenjianwei1 - 算法的设计 V'和添加的边{u,v}中的v那个更近即可。
这样的话,效率更高,时间复杂度为O(|V|^2),对于稠密图提高不少。
对于稀疏图而言,我们可以尝试用高级数据结构优化。我们只需要找到一个数据结构,使其支持加入一个数,取最小值和删除最小值之类。可以用堆优化(或者说优先队列、线段树、二叉排序树、甚至是单调队列?)。比如说优先队列,每一次加的时候就将新的扩展边放进去,然后每一次查询新的最小边并将其删除,最后一点一点地扩展,直至全图即可。在这里注意要用边权作为优先级。
证明的话,倒也不算很难。比如说我们扩展使用的是边(u,v),其中当前集合为S,u 最小生成树 - wenjianwei1 - 算法的设计 S,v 最小生成树 - wenjianwei1 - 算法的设计 S,而且(u,v)是连接S和V-S的最小权值的边。那么我们现在设定一棵最小生成树T,使得(u,v) 最小生成树 - wenjianwei1 - 算法的设计 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; }


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值