第三部分 图论 - 第2章 最小生成树

定义

首先我们先了解下生成树的定义:

无向图中,一个连通图的最小连通子图称作该图的生成树(不能带环,保持连通,但边要尽可能的少)。
有n个顶点的连通图的生成树有n个顶点和n-1条边

那么最小生成树和生成树有什么关系呢?

这里的最小其实是指的边的权值之和最小,当然是要在保证它是生成树的前提下权值之和最小。
所以,对于一个连通图来说,在它的所有的生成树里面,边的权值之和最小的生成树就是该连通图的最小生成树,当然最小生成树也可以有多个,因为边的权值是可以相等的。

构建最小生成树

一般来说在无向图(有向图还不太一样)中构建最小生成树有两种算法:

Prim算法

算法流程如下:

        Prim算法(普里姆算法)一般是从图中一个点出发,让后找到与他相邻且点到集合边权最小,去联通,如此重复直到覆盖整个图为止。需要注意的是更新顺序,如果统计边权的话,需要先统计,在更新,不然在某种情况下更新会出错(如有负环)

        prim判断是否可以构成一颗最小生成树的方法是如果找到的最小点为我当前声明的最大值,则表明他与其他点不连通,自然构不成一颗最小生成树。

        当然他的时间复杂度也不是很优秀,O(n^2),但是我们可以用堆优化查找最小值那一步,但是这样做有些复杂,这里不推荐。如果遇到稀疏图就用Kruskal算法,稠密图用Prim朴素版就好了。

模板代码如下:

#include<bits/stdc++.h>
using namespace std;

const int N = 1e3+10,INF = 0x3f3f3f3f;

int n,m; 
int g[N][N],dist[N];
bool st[N];

int prim(){
    memset(dist,0x3f,sizeof dist);
    int res = 0;
    for(int i = 0; i < n; i++){
        int t = -1;
        for(int j = 1; j <= n; j++){
            if(!st[j] && (t == -1 || dist[t] > dist[j])) t= j;
        }
        //图是不连通的,因为当前的点到集合的距离为正无穷,所以直接返回最小生成树不存在
        if(i && dist[t] == INF) return INF; 
        if(i) res += dist[t];
        //因为最小生成树是看当前的点到集合的距离而不是源点距离所以是min(dist[j],g[t][j])比较
        for(int j = 1; j <= n; j++) dist[j] = min(dist[j],g[t][j]);
        st[t] = true;
    }
    return res;
}

int main()
{
    cin >> n >> m;
    memset(g,0x3f,sizeof g);
    while(m--){
        int a,b,c; cin >> a >>b >> c;
        g[a][b] = g[b][a] = min(g[a][b],c); //重边保留较小的一条
    }
    int t = prim();
    if(t == INF) cout << "impossible" << endl;
    else cout << t << endl;
    return 0;
}

Kruskal算法

        Kruskal(克鲁斯卡尔)算法,他是先进行排序,让后在从小到大去选择,如果当前元素a和b已经连通就不去管他,如果不连通就直接连通,并且把权值放入总值中。也是很好实现。因为从小到大排序,所以说每次选择一定是最优。

算法流程:

        证明:

        在这里我们如果不选这条边,那么最后的生成树一定包含一条不小于这条边的边,那么我们把这条边替换上去,结果不会变差,可证这是最优解。

#include<bits/stdc++.h>
using namespace std;

const int N = 1e6+10;

int n,m,res,cnt; 
int p[N];

struct Node{
    int a,b,w;
} a[N];

bool cmp(Node a, Node b){
    return a.w < b.w;
}

int find(int x){
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; i++) p[i] = i;
    for(int i = 0; i < m; i++){
        int aa,b,w;
        cin >> aa >> b >> w;
        a[i] = {aa,b,w};
    }
    sort(a,a+m,cmp);
    for(int i = 0; i < m; i++){
        int aa = a[i].a,b = a[i].b,w = a[i].w;
        aa = find(aa),b = find(b);
        if(aa != b){  //判断是否连通 
            p[aa] = b;
            res += w;
            cnt++;
        }
    }
    if(cnt < n-1) cout << "impossible" << endl; //判断是否能组成最小生成树 
    else cout << res << endl;
    return 0;   
}

这两种算法各有优缺,需要根据题目合理运用。

稠密图用Prim,稀疏图用kruskal

例题引入

最短网络

思路分析

        看到要求连通整幅图那么就想到用最小生成树,并且是邻接矩阵读入的而且数据范围也不大,所以明显用Prim算法模板皆可以过。

代码

#include<bits/stdc++.h>
using namespace std;

const int N = 1e3+10;
int g[N][N],dist[N];
bool st[N];
int n;

int prim(){
    int res = 0;
    memset(dist,0x3f,sizeof dist);
    for(int i = 0; i < n; i++){
        int t = -1;
        for(int j = 1; j <= n; j++){
            if(!st[j] && (t == -1 || dist[t] > dist[j])) t = j;
        }
        if(i) res += dist[t];
        st[t] = 1;
        for(int j = 1; j <= n; j++) dist[j] = min(dist[j],g[t][j]);
    }
    return res;
}

int main(){
    cin >> n;
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= n; j++) cin >> g[i][j];
    }
    cout << prim() << endl;
    return 0;
}

局域网

思路分析

        这个让求的是去掉网线的最大值,转换一下是不是让保留下来的网线最小?所以我们只需要对网线求最小生成树,从整体中去掉这些最小生成树上得网线,即使去掉网线的最大值。所以我们考虑些最短路径,用Kruskal算法(这个算法更加灵活,所以一般不是特殊输入就用这个),那么我们只需要在两个点连通的时候计入答案里(应为我们再求最小生成树的时候是不连通的时候累加,正好相反)。

代码

#include<bits/stdc++.h>
using namespace std;

const int N = 1e6+10;
struct Node{
    int a,b,w;
} a[N];

int p[N];
int n,m,res;

bool cmp(Node a,Node b){
    return a.w < b.w;
}

int find(int x){
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; i++) p[i] = i;
    for(int i = 1; i <= m; i++) cin >> a[i].a >> a[i].b >> a[i].w;
    sort(a+1,a+m+1,cmp);
    for(int i = 1; i <= m; i++){
        int aa = a[i].a,b = a[i].b,w = a[i].w;
        aa = find(aa),b = find(b);
        if(aa != b){
            p[aa] = b;
        }
        else res += w; //在不连通的时候统计答案 
    }
    cout << res << endl;
    return 0;
}

繁忙都市

思路分析

        这道题的题意可能会有一点小饶,但是不要慌,翻译一下:就是至少要几条路可以保证城市连通,然后让你求上文这几条路中最大的一条。第一问很简单,我们都直到最小生成树是n-1条边,输出n-1即可,而第二问也不难,其实求得是最小生成树中最大的边。直接模板kruskal即可

代码

#include<bits/stdc++.h>
using namespace std;

const int N = 1e5+10;
struct Node{
    int a,b,w;
} a[N];
int p[N];
int n,m,ans = -1e9;

bool cmp(Node a,Node b){
    return a.w < b.w;
}

int find(int x){
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++) p[i] = i;
    for(int i = 1; i <= m; i++) cin >> a[i].a >> a[i].b >> a[i].w;
    sort(a+1,a+m+1,cmp);
    for(int i = 1; i <= m; i++){
        int aa = find(a[i].a), b = find(a[i].b),w = find(a[i].w);
        if(aa != b){
            p[aa] = b;
            ans = max(ans,a[i].w); //统计最大那条路 
        }
    }
    cout << n-1 << " " << ans << endl; //n-1为最小生成树的边数,并且要n-1条边才可以使得城市连接 
    return 0;
}

联络员

思路分析

        这道题的题意也非常恶习呀!!好像最小生成树的题的题意就没有不恶心的。其实直白点说就是在最小生成树中有几条必选边,也有不必选边,那我们也是一眼就看出解法了吧?

        其实用灵活的Kruskal算法,提前把必选边加入集合,让后再对非必选边求最小生成树既可以。

代码

#include<bits/stdc++.h>
using namespace std;

const int N = 1e6+10;
struct Node{
    int a,b,w;
}a[N];
int p[N];
int res,n,m;

bool cmp(Node a,Node b){
    return a.w < b.w;
}

int find(int x){
    if(x != p[x]) p[x] = find(p[x]);
    return p[x];
}

int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; i++) p[i] = i;
    for(int i = 1; i <= m; i++){
        int x,aa,bb,ww;
        cin >> x >> aa >> bb >> ww;
        if(x == 1){
        	//把必选边提前加入集合,累加贡献值 
            res += ww;
            aa = find(aa),bb = find(bb);
            if(aa != bb) p[aa] = bb;
        }
        else{
            a[i] = {aa,bb,ww};  //非必选边存下,求最小生成树 
        }
    }
    sort(a+1,a+1+m,cmp);
    //模板 
    for(int i = 1; i <= m; i++){
        int aa = find(a[i].a),bb = find(a[i].b),ww = a[i].w;
        if(aa != bb){
            p[aa] = bb;
            res += ww;
        }
    }
    cout << res << endl;
    return 0;
}

连接格点

思路分析

        这道题一看不是跟上一道一样吗?

        是的,其实差不多,但是这道题有个不当人的地方那就是,输入很难很难处理,这里我们使用一个值代表了(x,y)坐标对,因为最最小生成树一般都是a->b权值是w,统一格式,然后看一下数据范围在结合上题我们用Kruskal算法,但是他的排序还是有点慢。

        所以我们想了方法,因为只有两个值1,2所以我们只需要先存纵向即可。

        处理完这些,我们只需要把已有的边提前加入集合,剩下的正常跑最小生成树

代码

#include<bits/stdc++.h>
using namespace std;

const int N = 1010,M = N*N,K = 2*N*N;
int n,m; 
int ids[N][N]; //存储二位映射到一维的值 
struct Node{
    int a,b,w;
}e[K];
int p[M];
int k;

int find(int x){
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

//处理读入 
void get_edges(){
    int dx[4] = {-1,0,1,0},dy[4] = {0,1,0,-1},dw[4] = {1,2,1,2}; //四个方向和值 
    for(int z = 0; z < 2; z++){ //判断先存的横纵顺序 
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++){
                for(int u = 0; u < 4; u++){ //遍历四个方向 
                    if(u %2 == z){
                        int x = i+dx[u],y = j+dy[u],w = dw[u];
                        if(x && x <= n && y && y <= m){ //判断是否越界 
                            int a = ids[i][j],b = ids[x][y]; 
                            if(a < b) e[k++] = {a,b,w};    //判断先后顺序因为为了避免重复更新             
                        }
                    }
                }
            } 
        }
    }
}

int main(){
    cin >> n >> m;
    for(int i = 1; i <= n*m; i++) p[i] = i;
    //构建二维映射到一维的值 
    for(int i = 1,t = 1; i <= n; i++){
        for(int j = 1; j <= m; j++,t++){
            ids[i][j] = t;
        }
    }
    int x1,x2,y1,y2;
    while(cin >> x1 >> y1 >> x2 >> y2){
        int a = ids[x1][y1],b = ids[x2][y2];
        p[find(a)] = find(b); //把已有的边存储进来集合 
    }
    int res = 0;
    get_edges();
    //正常跑最小生成树 
    for(int i = 0; i < k; i++){
        int a = find(e[i].a),b = find(e[i].b),w = e[i].w;
        if(a != b){
            p[a] = b;
            res += w;
        }
    }
    cout << res << endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值