Loading

<数据结构>图的最小生成树

最小生成树问题

  1. 最小生成树问题(Mininum Spanning Tree MST): 在给定无向图中,确定一棵树T,满足三个条件:a.包含图的所有顶点;b.边都是图的边;c.整棵树的边权之和最小

  2. MST的性质: 包含n-1个结点;连通;树不唯一(最小边权和唯一)

Prim算法:点贪心

基本思想:类Dijstra

与Dijstr思想类似,只不过d[]的含义不同

d[v]:v与已被标记的顶点构成的集合s的最短距离。

毕竟,要求的是最小生成树,而集合s中的顶点又都在树中,所以需要关注顶点v与集合s的最短距离而不是与源点s(根节点)的最短距离。

而用中介点(此时是结合s中的所有点而不是到源点s距离最小的点)优化其他点的思想就是Dijstra算法的内核,所以只需要改变Dijstra算法中d[]数组的含义,即可顺利实现Prim算法。

如对Dijstra算法不太熟悉,可参看:<数据结构>图的最短路径问题

伪代码

建议与Dijstra算法的伪代码进行比较阅读

Prim(G, d[]){
    初始化;
    for(循环n次){
        u = 使d[u]最小的还未被访问的顶点的标号;
        记u已被访问;
        for(从u出发能到达的所有顶点v){
            if(v未被访问&&以u为中介点使得v与**集合s**的最短距离d[v]更优){  //唯一与Dijstra算法不同之处
                将G[u][v]赋值给v与集合s的最短距离d[v];//将G[u][v]赋给d[v] 而不再是 d[u] + G[u][v]
            }
        }
    }
}

代码实现

增加int ans记录边权和。

#include<stdio.h>
#include<algorithm>
using namespace std;

#define MAXV 100 
#define INF 100000000 
int n, G[MAXV][MAXV];   //邻接矩阵实现图G
int d[MAXV];            //顶点与集合s的最短距离
bool vis[MAXV] = {false};

int Prim(){ 
    fill(d, d+MAXV*MAXV, INF);
    d[0] = 0;  //只有0号顶点与s距离为0,其余为INF
    int ans = 0;     //存放边权之和
    for(int i = 0; i < n; i++){  //寻找 到集合s距离最短&&未被标记的顶点
        int u = -1, MIN = INF;
        for(int j = 0; j < n; j++){
            if(vis[j] == false && d[j] < MIN){
                u = j;
                MIN = d[j];
            }
        }


        if(u == -1) return ;  //图不全连通,无法构建最小生成树
        vis[u] = true; 
        ans += d[u];    //边权增加
        for(int v = 0; v < n; v++){
            //如果v未访问 && u能到达v && 以u为中介点可以使d[v]更优
            if(vis[v] == false && G[u][v] != INF && G[u][v] < d[v]){
                d[v] = G[u][v];
            }
        }
    }

    return ans; //返回最小边权之和
}

复杂度分析:O(VlogV + E)

同Dijstra算法。

kruskal算法:边贪心

基本思想: 充分利用MST性质

遵循下述三个步骤:

  1. 对所有边按边权从小到大排序
  2. 按边权测试所有边,如果当前测试边所连接的两个顶点不在同一个连通块,则把这条测试边加入最小生成树中;否则将边舍弃。
  3. 执行步骤2,直到树中的边数 == 顶点数-1。

伪代码

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

关键要解决两个问题

  1. 将所有边按边权从小到大排序————>sort函数(自行定义cmp)
  2. 当前测试边的两个端点在不同的连通块中————>并查集(顺带可以完成“将该测试边加入最小生成树;”)

代码实现

#include<stdio.h>
#include<algorithm>
using namespace std;
const int MAXV = 110;
const int MAXE = 10010;

//定义边集合
struct edge{
    int u, v;
    int cost;
}E[MAXE];
bool cmp(edge a, edge b){  //按边权从小到大
    return a.cost < b.cost;
}
//并查集部分
int father[MAXV];
int findFather(int x){
    int a = x;
    while(x != father[x])
        x = father[x];
    
    //路径压缩:让x的子结点直接指向x,减少中间路径
    while(a != father[a]){
        int z = a;
        a = father[a];
        father[a] = x;
    }

    return x;
}

//kruskal部分,返回最小生成树的边权之和,参数n为顶点个数,m为图的边数
int kruskal(int n, int m){
    //ans为所求边权之和,Num_Edge为当前生成树的边数
    int ans = 0, Num_Edge = 0;
    for(int i = 0; i < n; i++){ //顶点范围是0-(n-1)
        father[i] = i;   //并查集初始化
    }
    sort(E, E+m, cmp); //所有边按边权从小到达排序
    for(int i = 0; i < m; i++){ //枚举所有边
        int faU = findFather(E[i].u); //查询两个端点所在的集合的根节点
        int faV = findFather(E[i].v);
        if(faU != faV){ //如果不在一个集合中
            father[faU] = faV;//合并集合(即吧测试边加入最小生成树中)
            ans += E[i].cost;//边权和增加
            Num_Edge ++;//当前生成树的边数+1
            if(Num_Edge == n-1) break; //边数 == n-1时结束算法
        }
    }

    if(Num_Edge != n-1) return -1;   //不连通时返回-1
    else return ans;
}

int main(){
    int n,m;
    scanf("%d%d",&n,&m);  //顶点数、边数
    for(int i = 0; i < m; i++){
        scanf("%d%d%d", &E[i].u,&E[i].v,&E[i].cost);  //两端点编号, 边权
    }
    int ans = kruskal(n,m);  //算法入口
    printf("%d\n", ans);
    return 0;
}

复杂度分析:O(ElogE)

  • 主要来源:sort()函数[O(ElogE)];本质是快速排序
  • 次要来源:一重for循环[O(E)]

算法选择

一般情况下

  • Prim: 稠密图
  • kruskal: 稀疏图
posted @ 2021-11-26 01:22  咪啪魔女  阅读(96)  评论(0编辑  收藏  举报