zoukankan      html  css  js  c++  java
  • 图-最小生成树-Prim与Kruskal算法

    最小生成树

    最小生成树(Minimum Spanning Tree,MST)是在一个给定的无向图G(V,E)中求一棵树 T,使得这棵树拥有图 G中的所有顶点,且所有边都是来自图 G 中的边,并且满足整棵树的边权之和最小。

    最小生成树有三个性质需要掌握:

    • 最小生成树是树,因此其边数等于顶点数减 1,且树内一定不会有环
    • 对给定的图G(V,E),其最小生成树可以不唯一,其边权之和一定是唯一的
    • 由于最小生成树是无向图上生成的,因此其根结点可以使这棵树上的任意一个结点。

    一般来说,如果题目中涉及最小生成树本身的输出,为了让最小生成树唯一,一般都会直接给出根结点。

    求解最小生成树一般有两种算法:Prim 算法和 Kruskal 算法,都采用了贪心的思想,只是贪心的策略不一样。

    Prim算法

    基本思想

    对于图 G(V, E) 设置集合 S,存放已经被访问的顶点,然后每次从集合 V-S 中选择集合 S 的最短距离最小的一个顶点(记为 u),访问并加入集合 S。之后,令顶点 u 为中介点,优化所有从 u 能到达的顶点 v 与集合 S 之间的最短距离。这样的操作执行 n 次(n为顶点个数),直到集合 S 已经包含所有顶点。

    也就是执行 n 次下面两个步骤:

    • 每次从集合 V - S 中选择与集合 S 最近的一个顶点(记为 u),访问 u 并将其加入集合 S,同时把这条离集合 S 最近的边加入最小生成树中。
    • 令顶点 u 作为集合 S 与集合 V-S 连接的接口,优化从 u 能到达的未访问顶点 v 与集合 S 的最短距离。

    可以看到 Prim 算法的思想与最短路径中 Dijkstra 算法的思想几乎相同,只是在设计最短距离的时候用集合 S 代替了 Dijkstra 中的起点 s。

    具体实现

    prim 算法需要实现两个关键的概念,即集合 S 的实现、顶点Vi 与集合 S 的最短距离。

    • 集合 S 的实现方法和 Dijkstra 中相同,即使用一个 bool型数组 vis[] 来表示顶点是否已被访问。
    • 不妨令 int 型数组 d[] 来存放顶点 Vi 与集合 S 的最短距离。初始时除了起点 s 的d[s] 赋为0,其余顶点都赋值为一个很大的数 INF,即不可达。

    可以看到 prim 算法与 **Dijkstra **算法使用的思想几乎完全相同,只有在数组 d[] 的含义上有所区别。在 Dijkstra 中 d[] 含义为起点 s 到达 Vi 的最短距离,在 prim 算法中 d[] 含义为顶点 Vi 到集合 S 的最短距离,二者的区别仅仅在于最短距离是顶点 Vi 针对的是起点s还是集合V。

    所以伪代码和 Dijkstra 很像:

    // 数组 d 为顶点与集合 S 的最短距离
    void Prim(G, d[]) {
      初始化;
      for(循环 n 次) {
        u = 使 d[u] 最小的还未访问的顶点的标号;
        记 u 已经被访问;
        for(从 u 出发能到达的所有顶点 v) {
          if(v 未被访问 && 以 u 为中介点使得 v 与集合 S 的最短距离 d[v] 更优) {
            将 G[u][v] 赋值给 v 与集合 S 的最短距离 d[v];
          }
        }
      }
    }
    

    具体实现:

    邻接矩阵版

    const int maxn = 1000;	// 最大顶点数
    const INF = 1e9;
    
    int n, G[maxn][maxn];	// n 为顶点数
    int d[maxn];					// 顶点与 S 的最短距离
    bool vis[maxn] = {false};	// 标记数组
    
    int prim(int s)
    {		// s 号为默认初始点,函数返回最小生成树的边权之和
      fill(d, d+maxn, INF);	// fill 函数整个数组赋值为 INF
      d[s] = 0;	// 只有顶点到集合 S 的距离为 0,其余全为 INF
      int ans = 0;	// 存放最小生成树的边权之和
      for(int i = 0; i < n; i++) {
        
        	int u = -1, MIN = INF;	// u 使得 d[u] 最小,MIN 存放该最小的 d[u]
         	for(int j = 0; j < n; j++) {
            if(vis[j] == false && d[j] < MIN) {
    					u = j;
              MIN = d[j];
            }
     			}
        	if(u == -1) return -1;
        	vis[u] = true;	// 标记为已访问
        	ans += d[u];		// 将与集合 S 距离最小的表加入到最小生成树
        	for(int v = 0; v < n; v++) {
            // v 未访问 && u 能到达 v && 以 u 为中介可以使 v 距离集合 s 更近
            if(vis[v] == false && G[u][v] != INF && G[u][v] < d[v]) {
              d[v] = G[u][v];	// 将 G[u][v]赋值给 d[v]
            }
          }
      }
     return ans;
    }
    
    
    

    邻接表版

    struct Node {
      int v, dis;	// v 为目标边顶点,dis 为边权
    };
    
    vector<Node> Adj[maxn];
    int n;
    int d[maxn];
    bool vis[maxn] = {false};
    
    int prim(int s) {
      fill(d, d+maxn, INF);
      d[s] = 0;
      int ans = 0;	// 最小生成树边权之和
      for(int i = 0; i < n; i++) {
        
        int u = -1, MIN = INF;
        for(int j = 0; i < n; j++) {
          if(vis[j] == false && d[j] < MIN) {
            u = j;
            MIN = d[j];
          }
        }
        // 找不到小于INF的 d[u],剩下的顶点和集合 S 不连通
        if(u == -1) return -1;
        vis[u] = true;
        ans += d[u];
        for(int j = 0; j < Adj[u].size(); j++) {
          int v = Adj[u][j].v;	// 通过邻接表直接获得 u 能到达的顶点 v
          if(vis[v] == false && Adj[u][j].dis < d[v]) {
            // v 未访问 && 以 u 为中介点可以使得 v 距离集合 S 更近
            d[v] = G[u][v];	// 将G[u][v]赋值给 d[v]
          }
        }
      }
      return ans;
    }
    
    

    Kruskal 算法

    基本思想

    Kruskal 使用了边贪心的策略,思想很简洁。

    基本思想为:在初始状态隐去图中的所有边,这样图中每个顶点都自成一个连通块。之后执行下面的步骤:

    1. 对所有边按照边权从小到大进行排序
    2. 按边权从小到大测试所有边,如果当前所连接的两个顶点不在同一个连通块中,则把这条测试边加入当前最小生成树中;否则将这条边舍弃
    3. 执行步骤2,直到最小生成树中边数等于总顶点数减 1 或者测试完所有边时结束。当结束时如果最小生成树的边数小于总顶点树减 1,说明该图不连通。

    简单来说就是:每次选择图中最小边权的边,如果两段的顶点在不同的连通块中,就把这条边加入到最小生成树中。

    具体实现

    伪代码:

    int Kruskal() {
      令最小生成树的边权之和为 ans,最小生成树的当前边数为 Num_Edge;
      将所有边按边权从小到大排序;
      for(从小到大枚举所有边) {
        if(当前测试的两个端点在不同的连通块中) {
          将该测试边加入最小生成树中;
          ans += 测试边的边权;
          最小生成树的当前边数 Num_Edge + 1;
          当边数 Num_Edge 等于顶点数减1 时结束循环;
        }
      }
      return ans;
    }
    

    伪代码中有两个细节需要注意:

    • 如何判断是否测试边的两个端点在不同的连通块中
    • 如何将测试边加入到最小生成树中

    如果把每个连通块当成一个集合,那么就转化成判断两个端点是否在同一个集合中,可以使用并查集,而合并刚好可以解决第二个问题,那么具体实现如下:

    const int maxn = 1000;
    
    struct egde {
      int u, v;
      int cost;	// 边权
    }E[maxn];
    
    // 按照边的权重排序,小权重放在前面
    bool cmp(edge a, edge b) {
      return a.cost < b.cost;
    }
    
    int father[N]; // 并查集数组
    int findFather(int x) {	// 并查集查询函数
      while(x != father[x]) {
        x = father[x];
      }
      return x;
    }
    
    // kruksal 函数返回最小生成边权之和,参数 n 为顶点个数,m 为图的边数
    int krukal(int n, int m) {
      // ans 为所求边权之和,Num_Edge 为当前生成树的边数
      int ans = 0, Num_Edge = 0;
      for(int i = 1; i <= n; i++) {
        father[i] = i;
      }	// 并查集初始化
      sort(E, E+m, cmp);
      for(int i = 0; i < m; i++) {	// 枚举所有边
        int faU = findFather(E[i].v);
        int faV = findFather(E[i].u);
        if(faU != faV) { // 不在一个集合中
          father[faU] = faV;
          ans += E[i].cost;	// 边权之和增加测试边的边权
          Num_Edge++;
          if(Num_Edge == n - 1) break;	// 边数等于顶点数减 1 时结束算法
        }
      }
      if(Num_Edge != n-1) return -1;
      else return ans; 
    }
    

    可以看到,kruksal 适合顶点数目比较多,边数较少的情况,这和 prim 算法正好相反。

    如果是 稠密图,则用 prim 算法,如果是 稀疏图,则用 kruksal 算法。

    如果图本身不连通,使用 kruksal 算法必须在连通图下讨论,不然一定不能形成一颗完整的最小生成树。

    浙大的还是畅通公车给你 是一个很好的例子。

  • 相关阅读:
    undefined symbol 问题解决记录
    2021年中国数字人民币发展研究报告
    如何画出优秀的架构图
    用SIKT模型,让用户画像效果倍增
    全面总结图表设计
    如何用一周了解一个行业
    未来社区解决方案
    增长4大阶段,实现营销倍增的核心法则
    裂变营销的3个层次,让你实现指数增长
    运营的3个层面,让你轻松获得忠实用户
  • 原文地址:https://www.cnblogs.com/veeupup/p/12545774.html
Copyright © 2011-2022 走看看