最小生成树

本文详细介绍了最小生成树的概念及其重要性,特别是Prim算法和Kruskal算法的原理和实现过程。Prim算法从一个顶点开始,逐步选择到生成树最短的边,直至构建完整个最小生成树。Kruskal算法则通过对所有边按权值排序,依次选择可用的最短边,同时使用并查集避免形成环。文章还提供了样例输入和输出,帮助理解这两种算法的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

prim算法

        一个连通图的生成树是它的极小连通子图,在n个顶点的情况下,有n-1条边。也就是说,生成树是对连通图而言的,是连通图的极小连通子图,包含图中的所有顶点,有且仅有 n-1 条边。
    在图论中,我们常常将树定义为一个无回路连通图。对于一个带权的无回路连通图,其每个生成树所有边上的权值之和不能不同,我们把所有边上权值之和最小的生成树称为图的最小生成树。。常见的求最小生成树的方法有两种:克鲁斯卡尔(Kruskal)算法和普里姆(Prim)算法。下面介绍Prim算法。

    那我们如何选择这n个顶点,使得这n个顶点组成的n-1条边的总长度最短呢?
    我们从顶点1开始,把顶点1加入生成树集合(顶点集合),在一端以顶点1为端点的边中,选择一条最短的,把另一个端点加入生成树;然后选择一条到生成树最短的边,把此时这条边一个端点肯定在生成树中,把另一个端点也加入生成树。依次选择到生成树最短的边……直到选择了n-1条边为止。

Prim算法的原理:
    先从顶点1开始,每次选择到生成树最小的边,然后把这条边另一个端点加入生成树;再选择到生成树最短的边,把另一个端点加入生成树;…… 直到选择了n-1条边为止。

        本算法的关键在于如何求各个顶点(不在生成树内)到生成树的最短边?
程序中我们定义了一个数组dis[i],表示顶点i到生成树的最短距离,那么就解决了这个问题。
    同时,还有一个问题:如何更新dis[i]呢?
    当我们将得到k加入到生成树中的同时,我们要判断a[k][i]是不是比dis[i]更小,如果是的话就更新dis[i]的值。
应用Prim算法构造最小生成树的过程如下:


输出最短权值和的Prim算法程序如下:


//a[i][j]:i到j的边长(须事先读入)。
//dis[i]:结点i到生成树中结点的最短距离。
//vis[i]: true:在生成树中,false:不在生成树中。 

for (i=1; i<=n; i++){
	dis[i] = a[1][i];   //初始把顶点1到各个顶点的边记为到生成树的最短距离
	vis[i] = false;  //表示初始时全部顶点都不在生成树中
}
vis[1] = true;  //把顶点1放在生成树中
ans = 0;
for (i=2; i<=n; i++){
	min = maxlongint;
	for (j=1; j<=n; j++){
		if (vis[j]==false && dis[j]<min){
			min = dis[j];
			k = j;
		}
  	ans += dis[k];
	vis[k] = true;
  	for (j=1; j<=n; j++)
		if (vis[j]==false && a[k][j]<dis[j]) dis[j] = a[k][j];   //修改dis
}
cout << ans << endl;

        如果我们要输出最小生成树的各条边,可以定义一个二维数组tree来记录边。



【例题】道路修建

         有n个村庄,从1到n进行编号。你应该修建一些道路,使得任意两个村庄都是相连的。我们说村庄A和村庄B是相连的,要么A和B之间有一条路,要么存在一个存在存在C,A和C之间有一条路,并且C和B是相连的。
    一些存在之间已经有一些路存在,现在你的工作就是再修一些路,使得所有的存在都相连起来,且被修建的所有道路长度最小。 

输入格式 
    第1行给出村庄的数目n ( 3<=n<=100 );接下来n行,第i行包含n个整数,第i行的第j个整数(1<=j<=1000)表示存在i和村庄j的距离。
接下来一个整数q(0<=q<=n*(n+1)/2)。接下来的q行,每行2个整数a和b,表示表示村庄a与村庄b已经被修建了。
输出格式 
    一个整数,表示被修建的道路的总长度,这个值是最小的。 

样例输入 
3
0 990 692
990 0 179
692 179 0
1
1 2

样例输出 
179

【分析】直接利用prim算法模板即可。程序略

 

Kruskal算法

        常见的求最小生成树的方法有两种:克鲁斯卡尔(Kruskal)算法和普里姆(Prim)算法。下面介绍Kruskal算法。
    Kruskal算法以边为主导地位,始终选择当前可用的最短边。那我们如何依次选择这n-1条边,使得这n-1条边的总长度最短呢?
    要选择最短的边,我们首先想到给这些边从小到大排序,然后从小到大依次选择可用的边……直到选择了n-1条可用边为止。

Kruskal算法的原理:
    先对所有的边按权值从小到大进行排序,然后从最小的可用边开始选,依次选择每一条边,直到选择了n-1条可用边为止。

        本算法的伪代码如下:

T置空;
    while ( T中所含边数 < n-1 ){
	从E中选取当前权值最小的边(u,v);
	从E中删除边(u,v);
	if ( 边(u,v)的两个顶点落在两个不同的连通分量上 )  将边(u,v)并入T中
    }

应用Kruskal算法构造最小生成树的过程如下:


    从上面可以看出,Kruskal算法的关键在于如何判断一条边的两个顶点是否属于同一个连通分量。如果是,则舍弃这条边,因为这样会产生回路。如果不是,则选用。例如下图(d)中,边(4,5)的权值为4,是剩下的边中最小的,选择该边,但是因为该边的两个顶点在同一个连通分量中,如果选用这条边,这个连通分量中就出现了回路(环)。而我们知道,最小生成树中是一棵树,是不能有环的。


    如何判断一条边的两个顶点是否属于同一个连通分量?我们可以使用前面学习的并查集。如果一条边的两个顶点有共同的祖先,那么它们就属于同一个连通分量。

Kruskal算法注意的几点:
    1、边的存储

struct edge{
	int a;
	int b;
	int w;
     } e[m];

    2、Kruskal算法子程序

qsort(1, m);  //将边e[i]按权值e[i].w从小到大排序
    for(int i=1; i<=m; ++i) {
	if(getf(e[i].a) == getf(e[i].b)) continue;//若这条边的两个顶点在同一个连通分量中,舍弃
	merge(e[i].a, e[i].b);  //并查集中将顶点b并入顶点a
	ans += e[i].w;  //ans为最小生成树总权值和
    }

【例题】还是畅通工程

         Problem Description
    某省调查乡村交通状况,得到的统计表中列出了任意两村庄间的距离。省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可),并要求铺设的公路总长度为最小。请计算最小的公路总长度。

Input
    第1行给出村庄数目N ( < 100 );随后的N(N-1)/2行对应村庄间的距离,每行给出一对正整数,分别是两个村庄的编号,以及此两村庄间的距离。为简单起见,村庄从1到N编号。

Output
    输出最小的公路总长度。

Sample Input
3
1 2 1
1 3 2
2 3 4

Sample Output
3

#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;
struct node{
    int a,b,w;
}e[10001];
int f[101];
int cmp(node x,node y){
    return x.w < y.w;
}
int getf(int x){
    if(f[x]!=x) f[x]=getf(f[x]);
    return f[x];
}
int main(){
    	int n;
	cin >> n;
        int i,j,k,t,num=0,ans=0,aa,bb;
        for(i=1;i<=n;i++) f[i]=i;
        t=n*(n-1)/2;
        for(i=0;i<t;i++) cin >> e[i].a >> e[i].b >> e[i].w;
        sort(e, e+t, cmp);
        for(i=0; i<t; i++){
            aa = getf(e[i].a);
            bb = getf(e[i].b);
            if(aa==bb) continue;
            f[aa] = bb;
            ans += e[i].w;
            num++;
            if(num==n-1) break;
        }
        cout << ans << endl;
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值