最小生成树

最小生成树及其性质

  最小生成树是在一个给定的无向图G(V,E)中求一棵树T, 使得这棵树拥有图G中的所有顶点,且所有边都是来自图G中的边,并且满足整棵树的边权之和最小。
  最小生成树有3 个性质需要掌握:
①最小生成树是树, 因此其边数等于顶点数减1 , 且树内一定不会有环。
②对给定的图G(V,E), 其最小生成树可以不唯一, 但其边权之和一定是唯一的。
③由于最小生成树是在无向图上生成的, 因此其根结点可以是这棵树上的任意一个结点。于是,如果题目中涉及最小生成树本身的输出, 为了让最小生成树唯一, 一般都会直接给出根结点, 读者只需以给出的结点作为根结点来求解最小生成树即可。

求解问题总结

给定一个无向图,求其中包含了所有顶点的边权最小之和的树(和为多少,什么样的树)
 
 
 
求解最小生成树一般有两种算法, 即prim算法与kruskal算法。这两个算法都是采用了
贪心法的思想, 只是贪心的策略不太一样。

prim算法:类似Dijkstra(普里姆算法)

优缺点

优点:时间复杂度较低;缺点:思想较难理解
  prim算法与Dijkstra算法使用的思想几乎完全相同, 只有在数组d[]的含义上有所区别。
其中,Dijkstra算法的数组d[]含义为起点s到达顶点Vi的最短距离;
   而prim算法的数组d[]含义为顶点Vi与集合S的最短距离;
  两者的区别仅在于最短距离是顶点Vi针对“起点s”还是 “集合S”。另外,对最小生成树问题而言, 如果仅是求最小边权之和, 那么在prim算法中就可以随意指定一个顶点为初始点, 例如在下面的代码中将默认使用0号顶点为初始点。
根据上面的描述, 可以得到下面的伪代码(注意与prim算法基本思想进行联系):

//G为图, 一般设成全局变量;数组d为顶点与集合S的最短距离
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];
			}
		}
	}
}

和Dijkstra算法的伪代码进行比较后发现,Dijkstra算法和prim算法只有优化d[v]的部分不同,而其他语句都是相同的。这再次说明: Dijkstra算法和prim算法实际上是相同的思路,只不过是数组d[]的含义不同罢了。

应用案例

题1:数码宝贝拯救者——讨伐恶魔大陆

  这次亚历山大的任务是讨伐恶魔大陆。和精灵大陆一样, 恶魔大陆也有六个城市, 但是城市的分布与精灵大陆不同, 并且这里城市之间的道路是双向的。图10-44a给出了恶魔大陆的六个城市(V0至V5) 和连接它们的无向边, 边上的数字表示距离(即边权), 而城市结点的黑色表示还未被攻占。
  由于恶魔大陆提前知道了亚历山大要来攻打恶魔大陆, 因此恶魔们事先对所有道路进行了冻结, 希望以此消耗亚历山大的体力去恢复这些道路。不过亚历山大不会坐以待毙, 他在分析恶魔大陆的地图之后打算从防守最薄弱的V0开始进攻,并且使用了“ 爆裂模式” 来对抗他们(图10-44中)。在爆裂模式下, 亚历山大可以随时恢复任意一条已攻占城市所连接的道路, 但是需要消耗那条道路的距离大小的体力(也就是说, 道路有多长, 就需要消耗多少体力去恢复)。并且在恢复某条道路之后, 亚历山大会趁机攻占这条道路所连接的未攻占城市。
  为了尽可能节省体力, 亚历山大需要解决这样的一个问题:如何选择需要恢复的道路, 使得
亚历山大可以消耗最少的体力, 并保证他可以攻占所有城市。
在这里插入图片描述
  这其实就在求一棵最小生成树
①首先,亚历山大一定是每次从已攻占城市出发去攻打未攻占城市,这说明最后生成的结构一定连通。
②其次, 亚历山大在把V0攻占以后, 总是沿着一条新的道路去攻击一个新的城市, 这说明最后生成的结构的边数一定比顶点数少1。
  基于上面两点, 最后生成的结构一定是一棵树(是满足了连通、边数等于顶点数减1),而亚历山大的要求就是使这棵树的边权之和最小。当然, 如果上面的解释没有看懂的话, 也不妨先记住:这里求的就是一棵以V0为根结点的最小生成树,且接下来亚历山大所做的每一步都是prim算法的步骤。
  在这里,亚历山大对地图做出了三个修改
① 将地图上的所有边都抹去, 只有当攻占一个城市后才把这个城市连接的边显现(这一点和Dijkstra算法中相同)。
②使用“ 爆裂模式” 的能蜇, 将已攻占的城市置于一个巨型防护罩中。亚历山大可以沿着这个防护罩连接的道路去进攻未攻占的城市。
③在地图中的城市Vi (0<=i<=5)上记录城市Vi 与巨型防护罩之间的最短距离(即vi 与每个已攻占城市之间距离的最小值)。由于在①中亚历山大把所有边都抹去了,因此在初始状态下只在城市V0上标记0, 而其他城市都标记无穷大(记为INF, 见图10-44b)。为了方便叙述,在下文中某几处出现的最短距离都是指从城市vi 与当前巨型防护罩之间的最短距离。

输入
  第一行输入n:城市数(城市编号为0~n-1);m:边数;s:起点
  接下来m行输入边和边权x,y,z:城市x,y,道路长度z

输出
  消耗的最小体力为多少

输入情况:
6 10
0 1 4
0 4 1
0 5 2
1 2 6
1 5 3
2 3 6
2 5 5
3 4 4
3 5 5
4 5 3

输出情况:
15

AC代码
#include<bits/stdc++.h>
using namespace std;
const int INF=1e9;
const int maxv=1000;
map<int,int>adj[maxv];//顶点->边权
int n,m,d[maxv];//n城市数,m道路数,城市号为0~n-1
bool vis[maxv];

void init()
{
    for(int i=0;i<n;i++){
        vis[i]=false;
        d[i]=INF;
        adj[i].clear();
    }
}
int prim()
{
    int ans=0;//存放最小生成树的边权之和
    d[0]=0;
    for(int i=0;i<n;i++){
        int u=-1,MIN=INF;
        for(int j=0;j<n;j++){
            if(!vis[j]&&d[j]<MIN){
                u=j;
                MIN=d[j];
            }
        }
        if(u==-1)return -1;
        vis[u]=true;
        ans+=d[u];//把与集合S距离最短的这个边加入最小生成树
        for(map<int,int>::iterator it=adj[u].begin();it!=adj[u].end();it++){
                int v=it->first,dis=it->second;
                if(!vis[v]&&dis<d[v])
                    d[v]=dis;
        }
    }
    return ans;
}
int main(){
    int x,y,z;
    while(scanf("%d",&n)!=EOF){
        if(n==0)break;
        scanf("%d",&m);
        init();
        for(int i=0;i<m;i++){
            scanf("%d %d %d",&x,&y,&z);
            adj[x][y]=adj[y][x]=z;
        }
        printf("%d\n",prim());
    }
    return 0;
}

题2:问题 A: 还是畅通工程

http://codeup.cn/problem.php?cid=100000622&pid=0

#include<bits/stdc++.h>
using namespace std;
const int INF = 1e9;
const int maxv = 1000;
map<int, int>adj[maxv];//顶点->边权
int n, m, d[maxv];//n城市数,m道路数,城市号为0~n-1
bool vis[maxv];

void init()
{
	for (int i = 1; i<=n; i++){
		vis[i] = false;
		d[i] = INF;
		adj[i].clear();
	}
}
int prim()
{
	int ans = 0;//存放最小生成树的边权之和
	d[1] = 0;
	for (int i = 1; i<=n; i++){
		int u = -1, MIN = INF;
		for (int j = 1; j<=n; j++){
			if (!vis[j] && d[j]<MIN){
				u = j;
				MIN = d[j];
			}
		}
		if (u == -1)return -1;
		vis[u] = true;
		ans += d[u];//把与集合S距离最短的这个边加入最小生成树
		for (map<int, int>::iterator it = adj[u].begin(); it != adj[u].end(); it++){
			int v = it->first, dis = it->second;
			if (!vis[v] && dis<d[v])
				d[v] = dis;
		}
	}
	return ans;
}
int main(){
	int x, y, z;
	while (scanf("%d", &n) != EOF){
		if (n == 0)break;
		m=n*(n-1)/2;
		init();
		for (int i = 0; i<m; i++){
			scanf("%d %d %d", &x, &y, &z);
			adj[x][y] = adj[y][x] = z;
		}
		printf("%d\n", prim());
	}
	return 0;
}

题3:问题 D: 继续畅通工程

http://codeup.cn/problem.php?cid=100000622&pid=3

#include<bits/stdc++.h>
using namespace std;
const int INF = 1e9;
const int maxv = 1000;
struct node{
	int w;
	int ok;
};
map<int, node>adj[maxv];//顶点->边权
int n, m, d[maxv];//n城市数,m道路数,城市号为0~n-1
bool vis[maxv];

void init()
{
	for (int i = 1; i <= n; i++){
		vis[i] = false;
		d[i] = INF;
		adj[i].clear();
	}
}
int prim()
{
	int ans = 0;//存放最小生成树的边权之和
	d[1] = 0;
	for (int i = 1; i <= n; i++){
		int u = -1, MIN = INF;
		for (int j = 1; j <= n; j++){
			if (!vis[j] && d[j]<MIN){
				u = j;
				MIN = d[j];
			}
		}
		if (u == -1)return -1;
		vis[u] = true;
		ans += d[u];//把与集合S距离最短的这个边加入最小生成树
		for (map<int, node>::iterator it = adj[u].begin(); it != adj[u].end(); it++){
			int v = it->first;
			node dis = it->second;
			if (!vis[v]){
				if (dis.ok == 1)
					d[v] = 0;
				else if (dis.w<d[v])
					d[v] = dis.w;
			}
		}
	}
	return ans;
}
int main(){
	int x, y, z, h;
	while (scanf("%d", &n) != EOF){
		if (n == 0)break;
		m = n*(n - 1) / 2;
		init();
		for (int i = 0; i<m; i++){
			scanf("%d %d %d %d", &x, &y, &z, &h);
			adj[x][y].w = adj[y][x].w = z;
			adj[x][y].ok = adj[y][x].ok = h;
		}
		printf("%d\n", prim());
	}
	return 0;
}

题4:Freckles

题目:https://ac.nowcoder.com/acm/problem/115648
  In an episode of the Dick Van Dyke show, little Richie connects the freckles on his Dad’s back to form a picture of the Liberty Bell. Alas, one of the freckles turns out to be a scar, so his Ripley’s engagement falls through.
  Consider Dick’s back to be a plane with freckles at various (x, y) locations. Your job is to tell Richie how to connect the dots so as to minimize the amount of ink used. Richie connects the dots by drawing straight lines between pairs, possibly lifting the pen between lines. When Richie is done there must be a sequence of connected lines from any freckle to any other freckle.
Input
  The input begins with a single positive integer on a line by itself indicating the number of the cases following, each of them as described below. This line is followed by a blank line, and there is also a blank line between two consecutive inputs.
  The first line contains 0 < n ≤ 100, the number of freckles on Dick’s back. For each freckle, a line follows; each following line contains two real numbers indicating the (x, y) coordinates of the freckle.
Output
  For each test case, the output must follow the description below. The outputs of two consecutive cases will be separated by a blank line.
  Your program prints a single real number to two decimal places: the minimum total length of ink lines that can connect all the freckles.
Sample Input
1
3
1.0 1.0
2.0 2.0
2.0 4.0
Sample Output
3.41

思路

题目的意思就是有n个顶点(雀斑),要求用最少的墨水将所有雀斑都连在一起
理解为:把所有雀斑都连接在一起成为无向图,边权就是边的长度,求最小生成树
步骤:

  1. 输入所有点的x,y坐标
  2. 遍历所有两个点的组合所构成的线(n个顶点共能组成n*(n-1)/2个边)
 for(int i=0; i<n; i++) {
        for(int j=i+1; j<n; j++)
            adj[i][j]=adj[j][i]=compute(i,j);
    }
  1. 计算边长(知道两个点的坐标计算边长)
double compute(int a,int b) { //沟谷,知两直角边求斜边
    return sqrt(pow((x[a]-x[b]),2)+pow((y[a]-y[b]),2));
}
  1. prime算法
  2. **注意:**输出格式是要每组答案之间留一空行,但最后没有空行
      For each test case, the output must follow the description below. The outputs of two consecutive cases will be separated by a blank line.
AC代码
#include<cstdio>
#include<iostream>
#include<cmath>
#include<cstring>
#include<map>
using namespace std;
const double INF = 1e17;
const double eps=1e-9;
const int maxv = 1000;
double x[maxv],y[maxv];//(0~n-1)雀斑的x,y坐标
map<int,double>adj[maxv];//点(id1,id2)->长度
int n;
double d[maxv];
bool vis[maxv];

void init() {
    for(int i=0; i<n; i++) {
        vis[i]=false;
        d[i]=INF;
    }
}
double compute(int a,int b) { //沟谷,知两直角边求斜边
    return sqrt(pow((x[a]-x[b]),2)+pow((y[a]-y[b]),2));
}
void scan() {
    init();
    for (int i = 0; i<n; i++)
        scanf("%lf %lf",&x[i],&y[i]);
    for(int i=0; i<n; i++) {
        for(int j=i+1; j<n; j++)
            adj[i][j]=adj[j][i]=compute(i,j);
    }
}
double prime() {
    double ans=0;
    d[0]=0;
    for(int i=0; i<n; i++) {
        int u=-1;
        double MIN=INF;
        for(int j=0; j<n; j++) {
            if(!vis[j]&&(d[j]-MIN)<eps) {
                MIN=d[j];
                u=j;
            }
        }
        if(u==-1)
            return -1;
        vis[u]=true;
        ans+=d[u];
        for(map<int,double>::iterator it=adj[u].begin(); it!=adj[u].end(); it++) {
            int v=it->first;
            double dis=it->second;
            if(!vis[v]&&(dis-d[v])<eps)
                d[v]=dis;
        }
    }
    return ans;
}
int main() {
    double x, y;
    int sum;
    scanf("%d",&sum);
    while (sum--) {
        scanf("%d", &n);
        scan();
        printf("%.2lf\n",prime());
        if(sum)
            printf("\n");
    }
    return 0;
}

题5:问题 E: Jungle Roads

http://codeup.cn/problem.php?cid=100000622&pid=4

#include<bits/stdc++.h>
using namespace std;
const int INF = 1e9;
const int maxv = 50;
map<int, int>adj[maxv];
int n, d[maxv];
bool vis[maxv];

void init() {
	for (int i = 1; i <= n; i++){
		d[i] = INF;
		vis[i] = false;
		adj[i].clear();
	}
}
void scan() {
	init();
	int x, y, z, m;
	char t;
	for (int i = 1; i<n; i++){
		getchar();
		scanf("%c %d", &t, &m);
		x = (int)(t - 'A' + 1);
		while (m--){
			scanf(" %c %d", &t, &z);
			y = (int)(t - 'A' + 1);
			adj[x][y] = adj[y][x] = z;
		}
	}
}
int prime() {
	int ans = 0;
	d[1] = 0;
	for (int i = 1; i <= n; i++) {
		int u = -1, MIN = INF;
		for (int j = 1; j <= n; j++) {
			if (!vis[j] && d[j]<MIN) {
				MIN = d[j];
				u = j;
			}
		}
		if (u == -1)
			return -1;
		vis[u] = true;
		ans += d[u];
		for (map<int, int>::iterator it = adj[u].begin(); it != adj[u].end(); it++) {
			int v = it->first, dis = it->second;
			if (!vis[v] && dis<d[v])
				d[v] = dis;
		}
	}
	return ans;
}
int main() {
	while (scanf("%d", &n) != EOF) {
		if (n == 0)break;
		scan();
		printf("%d\n", prime());
	}
	return 0;
}

kruskal 算法:边贪心策略(克鲁斯卡尔算法")

优缺点

优点:思想简洁;缺点:时间复杂度较高
  其思想极其简洁,理解难度比prim 算法要低很多。
  kruskal 算法的基本思想为:在初始状态时隐去图中的所有边,这样图中每个顶点都自成一个连通块。之后执行下面的步骤:

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

伪代码

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

在这个伪代码里有两个细节似乎不太直观, 即

  1. 如何判断测试边的两个端点是否在不同的连通块中。(查询)
  2. 如何将测试边加入最小生成树中。(合并)

  事实上,对这两个问题,可以换一个角度来想。如果把每个连通块当作一个集合,那么就可以把问题转换为判断两个端点是否在同一个集合中, 而这个问题在前面讨论过一—对,就是并查集。并查集可以通过查询两个结点所在集合的根结点是否相同来判断它们是否在同一个集合,而合并功能恰好可以把上面提到的第二个细节解决,即只要把测试边的两个端点所在集合合并,就能达到将边加入最小生成树的效果。
  于是可以根据上面的解释,把kruskal 算法的代码写出来(建议结合伪代码学习)。另外,假设题目中顶点编号的范围是[1,n], 因此在并查集初始化时范围不能弄错。如果下标从0开始, 则整个代码中也只需要修改并查集初始化的部分即可。

应用案例

题一:讨伐恶魔大陆

题目同讨伐恶魔大陆
输入
  第一行输入n:城市数(城市编号为0~n-1);m:边数;s:起点
  接下来m行输入边和边权x,y,z:城市x,y,道路长度z

输出
  消耗的最小体力为多少

输入情况:
6 10
0 1 4
0 4 1
0 5 2
1 2 6
1 5 3
2 3 6
2 5 5
3 4 4
3 5 5
4 5 3

输出情况:
15

#include<bits/stdc++.h>
using namespace std;
const int maxv=1010;
const int INF=1e9;
struct edge { //边
    int u,v;//两个顶点
    int cost;//长度
} E[maxv];
bool cmp(edge a,edge b) {
    return a.cost<b.cost;
}
//并查集部分
int father[maxv],n,m;//并查集数组,顶点n个,边m个
void initfather() {
    for(int i=0; i<n; i++)
        father[i]=i;
}
int findFather(int x) {
    int a=x;
    while(x!=father[x]) { //找到其所在集合的根节点为x
        x=father[x];
    }
    while(a!=father[a]) { //路径压缩
        int z=a;
        a=father[a];
        father[z]=x;
    }
    return x;
}
bool Union(int a,int b) { //返回是否为同一集合,若不为则合并了
    int faA=findFather(a);
    int faB=findFather(b);
    if(faA!=faB) {
        father[faA]=faB;
        return true;
    }
    return false;
}
int kruskal()
{
    //kruskal 函数返回最小生成树的边权之和, 参数n 为顶点个数, m 为图的边数
    int ans=0,Num_Edge=0;
    initfather();//并查集初始化
    sort(E,E+m,cmp);//排序
    for(int i=0;i<m;i++){
        if(Union(E[i].u,E[i].v)){
            ans+=E[i].cost;
            Num_Edge++;
            //边数等于顶点数减1 时结束算法(那时已包含了所有顶点在树上)
            if(Num_Edge==(n-1))
                break;
        }
    }
    if(Num_Edge!=(n-1))//不足n-1就不能连接n个顶点,说明这个图不是连通图
        return -1;
    else
        return ans;
}
int main() {
    while(scanf("%d",&n)!=EOF){
        if(n==0)break;
        scanf("%d",&m);
        for(int i=0;i<m;i++)
            scanf("%d %d %d",&E[i].u,&E[i].v,&E[i].cost);
        printf("%d\n",kruskal());
    }
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Deosiree

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

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

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

打赏作者

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

抵扣说明:

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

余额充值