最小生成树:Prim算法和Kruskal算法

来源:互联网 发布:js根据日期计算星期几 编辑:程序博客网 时间:2024/05/19 00:52

介绍

         在实际生活中,我们经常碰到类似这样的一类问题:假设要在n个城市之间建立通信联络网,则连通n个城市只需要n-1条线路。这时,我们需要考虑这样一个问题,如何在最节省经费前提下建立这个通信网.换句话说,我们需要在这n个城市中找出一个包含所有城市的连通子图,使得其所有边的经费之和最小.


          这个问题可以转换为一个图论的问题:图中的每个节点看成是一个城市,节点之间的无向边表示修建该路的经费,即每条边都有其相应的权值,而我们的目标是挑选n-1条边使所有节点保持连通,并且要使得经费之和最小.


          这里存在一个显而易见的事实是:最优解中必然不存在循环(可通过反证法证明).因此,最后找出的包含所有城市的连通子图必然没有环路。这种连通且没有环路的连通图就简称为树,而在一个连通图中删除所有的环路而形成的树叫做该图的生成树.对于城市建立通信连通网,需要找出的树由于具有最小的经费之和,因此又被称为最小生成树(Minimum Cost Spanning Tree),简称MST.

解法

由于生成树必须包含原图里面的所有节点,关键的问题就在于边的选择,怎么才能找出n-1条边,使得所有节点连通,并且权重最小呢?这里,我们先来看看MST有什么特点.

设有上图所示的最小生成树T,如果删除边(u,v)T,则T将被分解成两个子树:T1和T2,因此,T1和T2分别是其所包含节点的最小生成树,此处可用反证法证明:如果T1(也可假设为T2)不是其所包含节点的最小生成树,那么,势必还存在的生成树T',那么,T'+w(u,v)+T2<T1+w(u,v)+T2,这与我们的假设T是最小生成树相矛盾,故结论成立.


这就是最小生成树的最优子结构性质,在细想一下,MST也包含了重叠子问题的性质,那么似乎我们可以用动态规划来解决.但如果用动态规划来解决MST,其时间复杂度是指数级别的,显然不是太可取,我们需要找寻更好的方法.既然MST满足最优子结构性质,那么它是否满足贪婪选择属性呢?


设图G=(V,E),U是顶点集V的一个非空子集,如果(u,v)是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边(u,v)的最小生成树.


上述的性质可以通过反证法证明,如果(u,v)不包含在G的最小生成树T中,那么,T的路径中必然存在一条连通U和V-U的边,如果将这条边以(u,v)来替换,我们将获得一个权重更低的生成树,这与T是最小生成树矛盾.

既然MST满足贪婪选择属性,那么,求解最小生成树的问题就简化了很多。总结一下,具体的步骤大概如下:

  1. 构建一棵空的最小生成树T,并将所有节点赋值为无穷大.
  2. 任选一个节点放入T,另外一个节点集合为V-T.
  3. 对V-T中节点的赋值进行更新(由于此时新加入一个节点,这些距离可能发生变化)
  4. 从V-T中选择赋值最小的节点,加入T中
  5. 如果V-T非空,继续步骤3~5,否则算法终结.
上述步骤就是图论里面经典的prim算法,用于求解最小生成树问题,这里不做过多阐释.

实现
下面代码是prim算法的一个实现,prim算法的时间复杂度依赖于所使用的数据结构而不同(堆:O(ElgV),斐波那契堆:O(E+VlgV)),这里我是用的数组实现,时间复杂度为O(V^2).
#include<algorithm>#define INF INT_MAX#define MAX_V 1000int cost[MAX_V][MAX_V];int min_cost[MAX_V];bool vis[MAX_V];//vert表示的顶点的个数int prim(int vert){    fill(vis,vis+vert,false);    fill(min_cost,min_cost+vert,INF);    min_cost[0] = 0 ;    int res = 0 ;    while(true)    {        int v = -1 ;        for(int i=0;i!=vert;++i)            if(!vis[i] && (v==-1 || min_cost[i] < min_cost[v]))                v = i ;        if(v==-1)            break;        res +=min_cost[v];        vis[v] = true;        for(int i=0;i!=vert;++i)            //此处的写法只是一种简便的写法,对于已加入vis[i]=true的顶点            //即使更新到它也不会出错,请注意上面循环中的!vis[i]这个条件            min_cost[i] = min(min_cost[i],cost[v][i]);    }    return res;}



Kruskal算法
             上面已经简约的介绍了prim算法的基本思想,通过上面的认识,我们可以知道prim算法考虑的是当前顶点是否加入以求得的最小生成树顶点中而进行推进的,那么,我们能不能换一种思路,每次考虑的是边呢?事实上这样是可以的,这也就是接下来要说的Kruskal算法(此算法也是用来求MST的)。
#include<iostream>#include<algorithm>#define MAX_E 10000#define MAX_V 110using namespace std;struct Edge{    int from,to,cost;    void set(int f,int t,int c)    {        from = f , to = t , cost = c ;    }    bool operator<(const Edge& t) const    {        return cost < t.cost;    }}e[MAX_E];int f[MAX_V];int Find(int x){    return x==f[x] ? x : f[x] = Find(f[x]) ;}bool Union(int x,int y){    int fx = Find(x),fy = Find(y);    if(fx==fy)        return false;    f[fx] = fy ;    return true;}//vert表示的是顶点的个数inline void init_find_union(int vert){    for(int i=0;i!=vert;++i)        f[i] = i ;}//vert:顶点的个数//e_num:边的条数int Kruskal(int vert,int e_num){    int cnt = vert - 1 ,res = 0 ;    init_find_union(vert);    sort(e,e+e_num);    for(int i=0;cnt && i!=e_num;++i)        if(Union(e[i].from,e[i].to))            res +=e[i].cost;    return res;}


0 0
原创粉丝点击