• 最小生成树(Minimum Spanning Tree)——Prim算法与Kruskal算法+并查集


    最小生成树——Minimum Spanning Tree,是图论中比较重要的模型,通常用于解决实际生活中的路径代价最小一类的问题。我们首先用通俗的语言解释它的定义:

    对于有n个节点的有权无向连通图,寻找n-1条边,恰好将这n个节点相连,并且这n-1条边的权值之和最小。

    对于MST问题,通常常见的解法有两种:Prim算法   或者  Kruskal算法+并查集

    对于最小生成树,一定要注意其定义是在无向连通图的基础上,如果在有向图中,那么就需要另外的分析,单纯用无向图中的方法是不能得出正确解的,这一点我在比赛中确实吃过亏

    好了,进入正题:

    Prim算法:(基于点的贪心思路)由于是基于点的算法,因此适合于稠密图,一下给出代码没有经过堆优化,时间复杂度为O(N^2)

      记原图为G,生成树图为MST,其中G的节点个数为n个

      算法描述如下:

    1. 任取G中的一点,加入MST中——这一步的作用是选择一个节点作为整个算法的起点
    2. 采用贪心策略,将刚刚加入的节点记为u,以u为中心,检查与u相连且没有加入MST的节点(未访问过的节点),选择权值最小的边,如果有多条边的权值均最小,则任取一条边。——贪心策略,选择局部最优
    3. 将所选择的边中,不在MST中的那个节点,加入MST——举例来说,比如(u,v)是当前与u相连,v不再MST中,且权值最小的边,则边(u,v)被选中,并将v加入MST。
    4. 如果步骤2-3被执行了n-1次,则退出,反之则返回到步骤2。——由于Prim算法初始化时加入了起点,而步骤2-3每执行一次都会加入一个新的节点,所以只需判断执行次数。

    关于算法的正确性证明网上都有证明,这里就不再赘述。

     1 //inf为路径权上界,maxn为图的临接矩阵的点数
     2 //vis是记录是否访问过,cost[i]记录到达第i个节点的最小代价 
     3 const int inf=0x7fffffff,maxn=101;
     4 int G[maxn][maxn],vis[maxn],cost[maxn],n;
     5 //len为MST长度
     6 int prim(){
     7     memset(vis,0,sizeof(vis));
     8    //加入起始节点
     9     int pos=1,min=inf,len=0,cnt=n;
    10     vis[1]=1;
    11     for(int v=2;v<=n;v++)cost[v]=G[pos][v];
    12    //加入剩余n-1个节点
    13     while(--cnt){
    14         for(int i=1;i<=n;i++)if(!vis[i]&&cost[i]<min){
    15             pos=i;min=cost[i];
    16         }
    17         len+=min;vis[pos]=1;
    18         //以新加入的节点为中心,更新权值信息
    19         for(int i=1;i<=n;i++)if(!vis[i]&&G[pos][i]<cost[i])
    20             cost[i]=G[pos][i];
    21         min=inf;
    22     }
    23     return len;
    24 }

    结合poj上的一道水题来验证一下Prim的威力吧~亲测156k内存0ms过(C++编译器)

    poj1258:http://poj.org/problem?id=1258

    Kruskal算法:(基于边的贪心算法)基于边的贪心,由图的性质不难知道,当图为稠密图时,边的数目远大于点的数目,因此Kruskal+并查集适用于稀疏图

    1. 将所有的边按权值由小到大排序——准备工作,可借助sort()完成,但是在工程中,如果不知道边和点的数量关系,还是应该用最小值堆,而不是sort来保证效率,但在竞赛中,sort足够了
    2. 从非MST中的边中寻找一条,在不会与现有的MST构成环的前提下,权值最小的边,加入MST
    3. 如果已经加入了n-1条边,则结束,否则返回步骤2

    那么从算法描述,我们不难看到,整个算法中的核心部分是,判断当前权值最小的边是否会与MST构成环。

    那么如何实现这个判断呢?一种思路是我们通过BFS或者DFS,用遍历图的办法来判断——然而这个编程复杂度和时间复杂度都很高╮(╯-╰)╭

    我们可以从另一个角度进行考量。如果说我们给每个MST一个代表元素(representative),或者说,是一个标记,那么,对于一个不连通的无向图,每个MST就可以看作一个连通支,而每个连通支其实可以看作一个集合,连通支中的节点就是集合中的元素,而我们只关心一个新的元素是否在原先的集合中。

    那么判定元素是否在集合中,我们是不是马上想到了一种树形结构——并查集(Union-Find Set)

    并查集的数组实现如下:p[x]表示第x元素的父元素,我们规定当p[x]==x时,表示找到了这一组元素的代表元(representative),

    则可以递归的进行查找,并同时进行路径压缩,因此,不难看出,在均摊意义下,并查集的时间复杂度为O(1)。

    1 int find(int x){ return p[x]== x ? x : p[x] = find(p[x]); }

    为什么return语句可以这样和赋值语句连用?

    大家想想诸如a=b=c=1;这样的连续赋值,不难理解,其实赋值语句是有返回值的,并且返回值为左值的值,即先返回c的值1,赋给b,返回b的值1,赋给a,最后返回a的值。

    这样,我们就可以给出kruskal的完整实现了:

     1 const int maxn=100;
     2 //n为节点个数,m为边个数,r存储第i+1小的边的序号,w存储第i条边的权值,u和v存储第i条边的节点序号 
     3 int p[maxn],n,u[maxn],v[maxn],w[maxn],r[maxn],m;
     4 //并查集find
     5 int find(int x){ return p[x]==x?x:p[x]=find(p[x]); }
     6 //间接排序函数
     7 int cmp(const int i,const int j){ return w[i]<w[j]; } 
     8 int kruskal(){
     9     int len=0;
    10     for(int i=0;i<n;i++)p[i]=i;//初始化并查集
    11     for(int i=0;i<m;i++)r[i]=i;//初始化边的序号 
    12     sort(r,r+m,cmp);//<algorithm>中的优化的快排
    13     for(int i=0;i<m;i++){
    14         int e=r[i],x=find(u[e]),y=find(v[e]);
    15         if(x!=y){ len+=w[e];p[y]=x; }//并查集Union 
    16     }
    17     return len;
    18 }

    不难看出,Kruskal算法的复杂度为O(ElogE),基本上都集中在排序了,所以,工程上还可以用优先队列或者斐波那契堆来减小复杂度

    这样,无向图中的MST模型就介绍的差不多了,通常这个模型会用于解决资源最省之类的问题,不过,kruskal还没有实践过,所以,有时间我再更新一些相关习题吧~

  • 相关阅读:
    【北亚数据恢复】服务器由于重装系统导致逻辑卷改变,文件系统破坏的数据恢复案例
    【北亚数据恢复】服务器重装系统后分区消失和分区不可访问的数据恢复案例
    【北亚数据恢复】分布式存储hbase和hive数据库底层文件被误删除的数据恢复案例
    【北亚数据恢复】如何避免服务器数据丢失造成重大损失?
    【北亚数据恢复】EMC存储服务器riad5硬盘故障掉线导致服务器崩溃的数据恢复
    【北亚数据恢复】IBM FlashSystem存储raid5多硬盘故障离线的数据恢复案例
    【北亚数据恢复】通过碎片拼接技术恢复XenServer服务器磁盘中SQL Server数据库数据
    【北亚数据恢复】DELL存储服务器硬盘坏道导致raid5崩溃,存储不可用的数据恢复案例
    RedHat7系列(Centos/Debian) FireWall 防火墙 设置
    linux 终端颜色代码
  • 原文地址:https://www.cnblogs.com/luruiyuan/p/5528406.html
Copyright © 2020-2023  润新知