最小生成树算法及例题

什么是生成树?对连通图进行遍历,过程中经过的点和边的组合可看成一棵树,也叫生成树

最小生成树引入

世界上有着许许多多的铁路线、公路线,想要从一个城市到另一个城市修一条线路需要许多资金,当然,修路的方式有多种多样,现在我们想知道如何修路能使得这些城市之间形成一个通信网,并且使得总耗费最少呢?

  • 假设有n个城市,那么我们最少需要修n-1条路,这是明显的,把这些路连在一起,加上城市节点,就构成了一棵生成树,在这些生成树中,边权之和最小的就是最小生成树,所以我们可以看出最小生成树不一定是唯一的
  • 最小生成树(Minimum Spanning Tree)简称MST
  • 下面两种算法都是基于贪心的思想

Prim算法

如何构建最小生成树呢?我们需要三个数组:selected,minDist,parent

  • selected数组的作用是判断节点是否已被选
  • minDist数组的作用是记录当前可供选择的边权
  • parent数组的作用是记录这些边的上一个节点
  • 将所有顶点分成两个集合U和V-U,集合U是最小生成树的节点集,最开始为空,集合V-U是尚未被加入到树中的节点,最开始为所有节点
  • 我们从任意一个节点出发,首先把它加入到集合U中,同时用它的所有边去更新minDist数组,使得数组中边总是最小
  • 接着我们搜索一遍minDist中的边,找到最小的那一个,把它所对的节点(不是parent)加入到最小生成树中,同时selected数组记录
    prim算法步骤主要有三步:Update,Scan,Add 也就是更新、搜索、加点,所以prim算法又叫做加点法

习题

洛谷模板题

  • 有两个WA点,第一个点是图是无向图,第二个是可能出现两个点之间有两条边或者更多,这个时候我们要取最短的边,这样需要在输入那里处理一下
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <iomanip>
#include <queue>
#include <stack>
using namespace std;
typedef long long ll;
const int MAXN = 5050;
int edge[MAXN][MAXN];
int selected[MAXN];
int minDist[MAXN];
int parent[MAXN];
int ans = 0;
int tot = 1;
void Prim(int num,int m){
    selected[m] = 1;
    int k,tmp;
    for(int i=1;i<=num;i++){ 
        if(edge[m][i]){
            minDist[i] = edge[m][i];
            parent[i] = m;
        }
    }
    for(int i=1;i<num;i++){
        tmp = 0x3f3f3f3f;
        for(int j=1;j<=num;j++){
            if(!selected[j]&&minDist[j]<tmp){
                k = j;
                tmp = minDist[j];
            } 
        }
        if(selected[k]) continue;
        selected[k] = 1;
        tot++;
        ans += edge[parent[k]][k];
        for(int j=1;j<=num;j++){
            if(!selected[j]&&edge[k][j]<minDist[j]&&edge[k][j]){
                minDist[j] = edge[k][j];
                parent[j] = k;
            }
        }
    }
    if(tot!=num) cout<<"orz"<<endl;
    else cout<<ans<<endl;
}
int main(){
    int n,m;
    cin>>n>>m;
    for(int i=0;i<m;i++){
        int x,y,z;
        cin>>x>>y>>z;
        if(edge[x][y]==0){
            edge[x][y] = z;
            edge[y][x] = z;
        }
        else if(z<edge[x][y]){
            edge[x][y] = z;
            edge[y][x] = z;
        }
    }
    memset(parent,-1,sizeof parent);
    memset(minDist,0x3f,sizeof minDist);
    Prim(n,1);
    return 0;
}

HDU模板题
这道题坑死我了,一直看了两个小时没找到错误,一直提示非法访问内存空间,数组大小也很合适,后来换了C++提交就过了,很奇怪

  • 和上一道题基本一样,唯一需要注意的是使用的变量和数组需要恢复原状
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <iomanip>
#include <queue>
#include <stack>
using namespace std;
typedef long long ll;
const int MAXN = 505;
int edge[MAXN][MAXN];
int selected[MAXN];
int minDist[MAXN];
int parent[MAXN];
int ans = 0;
int tot = 1;
void Prim(int num,int m){
    tot = 1;
    ans = 0;
    selected[m] = 1;
    int k,tmp;
    for(int i=1;i<=num;i++){ 
        if(edge[m][i]){
            minDist[i] = edge[m][i];
            parent[i] = m;
        }
    }
    for(int i=1;i<num;i++){
        tmp = 0x3f3f3f3f;
        for(int j=1;j<=num;j++){
            if(!selected[j]&&minDist[j]<tmp){
                k = j;
                tmp = minDist[j];
            } 
        }
        if(selected[k]) break;
        selected[k] = 1;
        tot++;
        ans += edge[parent[k]][k];
        for(int j=1;j<=num;j++){
            if(!selected[j]&&edge[k][j]<minDist[j]&&edge[k][j]){
                minDist[j] = edge[k][j];
                parent[j] = k;
            }
        }
    }
    if(tot!=num) cout<<"?"<<endl;
    else cout<<ans<<endl;
}
int main(){
    int n,m;
    while(cin>>n>>m){
        if(n==0) break;
        memset(edge,0,sizeof edge);
        for(int i=0;i<n;i++){
            int x,y,z;
            cin>>x>>y>>z;
            if(edge[x][y]==0){
                edge[x][y] = z;
                edge[y][x] = z;
            }
            else if(z<edge[x][y]){
                edge[x][y] = z;
                edge[y][x] = z;
            }
        }
        memset(parent,-1,sizeof parent);
        memset(minDist,0x3f,sizeof minDist);
        memset(selected,0,sizeof selected);
        Prim(m,1);
    }
    return 0;
}

更新一道题

P1265

  • 此题依然是模板,但这个问题是边多,内存又有限制,不适合使用方便的kruskal,需要用prim,可以借助这道题再次熟悉prim
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <iomanip>
#include <queue>
#include <stack>
using namespace std;
typedef long long ll;
const int MAXN = 5050;
int selected[MAXN];
double minDist[MAXN];
int parent[MAXN];
double ans = 0;
int tot = 1;
struct node{
    int x, y;
}st[MAXN];
double DIS(node x, node y){
    double dx = x.x - y.x;
    double dy = x.y - y.y;
    return sqrt(dx * dx + dy * dy);
}
void Prim(int num,int m){
    selected[m] = 1;
    int k;
    double tmp;
    for(int i=1;i<num;i++){ 
        double n = DIS(st[m], st[i]);
        if(n > 0){
            minDist[i] = n;
            parent[i] = m;
        }
    }
    for(int i=1;i<num;i++){
        tmp = 9999999999;
        for(int j=1;j<num;j++){
            if(!selected[j]&&minDist[j]<tmp){
                k = j;
                tmp = minDist[j];
            } 
        }
        if(selected[k]) continue;
        selected[k] = 1;
        tot++;
        ans += DIS(st[parent[k]], st[k]);
        for(int j=1;j<=num;j++){
            double n = DIS(st[k], st[j]);
            if(!selected[j] && n<minDist[j]){
                minDist[j] = n;
                parent[j] = k;
            }
        }
    }
    printf("%.2lf", ans);
}

int main(){
    int n;
    cin>>n;
    for(int i=0;i<n;i++) cin>>st[i].x>>st[i].y;
    memset(parent,-1,sizeof parent);
    memset(minDist,0x3f,sizeof minDist);
    Prim(n,0);
    return 0;
}

Kruskal算法

相对于Prim算法,Kruskal算法的思想是加边,可以称之为加边法,过程如下

  • 把所有边按照权值升序排列,从前往后依次‘回贴’边到图中,每次需要判断图中是否出现环,如果出现就舍弃转向下一条边,这样进行下去,直到所有点都在生成树里,这样就形成了最小生成树
  • 不知道有没有人会有这样的疑问,为什么Kruskal算法能够找到最小生成树,Prim是从顶点出发,向外扩散,肯定能遍历全图,这不难理解;但Kruskal是不停地加边,中间还有舍弃,难道不会漏边吗?
  • 仔细思考可以明白,我们已经将边排好序了,如果后面的边使得前面的边构成的图出现环,可以直接把这条边舍弃掉,因为前面的边已经使现在的顶点连在一片,而这条边只不过是多余的而已
  • 思路已经有了,那么具体如何实现呢?
    这里要用到一点并查集的相关知识,简单来讲,刚开始,不同的顶点属于不同的集合,每次选择一条边加进来都要把这两个点划分到一个集合里面去,这就是典型的并查集,在前面路径压缩优化一下,也就是把所有点直接跟着根节点而不是递归查找
  • 相对于Prim的实现,这一次使用Kruskal显得顺利得多,没有发现什么坑点
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <iomanip>
#include <queue>
#include <stack>
using namespace std;
typedef long long ll;
const int MAXN = 2e5+100;
struct Edge{int x,y,z;}edge[MAXN];
int set[MAXN];
bool cmp(Edge x,Edge y){
    return x.z<y.z;
}
int Findset(int x){
    if(x!=set[x]) set[x] = Findset(set[x]);
    return set[x];
}
void Kruskal(int num,int m){
    int ans = 0;
    int tot = 0;
    for(int i=1;i<=num;i++) set[i] = i;
    for(int i=0;i<m;i++){
        int x = Findset(edge[i].x);
        int y = Findset(edge[i].y);
        if(x == y) continue;
        set[x] = set[y];
        tot++;
        ans += edge[i].z;
        if(tot == num-1) break;
    }
    if(tot == num-1) cout<<ans<<endl;
    else cout<<"orz"<<endl;
}
int main(){
    int n,m;
    cin>>n>>m;
    for(int i=0;i<m;i++) cin>>edge[i].x>>edge[i].y>>edge[i].z;
    sort(edge,edge+m,cmp);
    Kruskal(n,m);
    return 0;
}
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <iomanip>
#include <queue>
#include <stack>
using namespace std;
typedef long long ll;
const int MAXN = 2e5+100;
struct Edge{int x,y,z;}edge[MAXN];
int set[MAXN];
bool cmp(Edge x,Edge y){
    return x.z<y.z;
}
int Findset(int x){
    if(x!=set[x]) set[x] = Findset(set[x]);
    return set[x];
}
void Kruskal(int num,int m){
    int ans = 0;
    int tot = 0;
    for(int i=1;i<=num;i++) set[i] = i;
    for(int i=0;i<m;i++){
        int x = Findset(edge[i].x);
        int y = Findset(edge[i].y);
        if(x == y) continue;
        set[x] = set[y];
        tot++;
        ans += edge[i].z;
        if(tot == num-1) break;
    }
    if(tot == num-1) cout<<ans<<endl;
    else cout<<"?"<<endl;
}
int main(){
    int n,m;
    while(cin>>n>>m){
        if(n==0) break;
        for(int i=0;i<n;i++) cin>>edge[i].x>>edge[i].y>>edge[i].z;
        sort(edge,edge+n,cmp);
        Kruskal(m,n);
    }
    return 0;
}

一个非常好的学习视频

B站链接

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Clarence Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值