《算法笔记》10.5小节——图算法专题->最小生成树

10.5.1 最小生成树及其性质

最小生成树是无向图中的一棵树,涵盖图G的所有顶点,并且边之和最小
最小生成树的三个性质:
1、是树,边数=顶点数-1
2、对于给定的图,最小生成树可以不唯一,但是边权值和是唯一的
3、最小生成树的根节点可以是任何一个节点,但是题目为了方便输出,一般会用题目给的节点作为根节点
P算法和K算法都是采用贪心法求最小生成树,只是贪心的策略不一样

10.5.2 prim算法

P算法与求最短路径的D算法思想几乎完全相同
区别是P算法中d[i]指的是点i距离集合S的距离
D算法中d[i]指的是点i距离起点S的距离
复杂度是O(V²),可以进行堆优化
尽量在图的顶点数目较少,而边数较多的时候使用prim算法

10.5.3 kruskal算法

采用边贪心的策略
时间复杂度主要来自对边的排序,为O(ElogE),所以适合顶点较多,边较少的情况

习题

注意P算法适用于边较多的情况,K算法适用于顶点较多的情况
问题 A: 还是畅通工程
问题描述: 某省调查乡村交通状况,得到的统计表中列出了任意两村庄间的距离。省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可),并要求铺设的公路总长度为最小。请计算最小的公路总长度。

  • 输入
测试输入包含若干测试用例。每个测试用例的第1行给出村庄数目N ( < 100 );随后的N(N-1)/2行对应村庄间的距离,每行给出一对正整数,分别是两个村庄的编号,以及此两村庄间的距离。为简单起见,村庄从1到N编号。
当N为0时,输入结束,该用例不被处理。```
 - 输出
 ```cpp
对每个测试用例,在1行里输出最小的公路总长度。
  • 样例输入
8
1 2 42
1 3 68
1 4 35
1 5 1
1 6 70
1 7 25
1 8 79
2 3 59
2 4 63
2 5 65
2 6 6
2 7 46
2 8 82
3 4 28
3 5 62
3 6 92
3 7 96
3 8 43
4 5 28
4 6 37
4 7 92
4 8 5
5 6 3
5 7 54
5 8 93
6 7 83
6 8 22
7 8 17
0
  • 样例输出
82

这题使用P算法:
因为是多点测试,注意要初始化所有的变量

#define _CRT_SECURE_NO_WARNINGS 1
#include <cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxv = 110;//表示最多100个村庄
const int INF = 1000000000;//无穷大
int n, m, G[maxv][maxv];
int d[maxv];
int vis[maxv];
int prim()
{
	fill(d, d + maxv, INF);
	fill(vis, vis + maxv, 0);
	d[1] = 0;
	int ans = 0;
	//首先找到距离集合S最小的点u以及距离d[u]
	for (int i = 1; i <=n; i++)
	{
		int u = -1,MIN = INF;
		for (int j = 1; j <= n; j++)
		{
			if (vis[j] == 0 && d[j] < MIN)
			{
				u = j;
				MIN = d[j];
			}
				
		}
		if (u == -1) return -1;
		vis[u] = 1;
		ans += d[u];
		for (int v = 1; v <= n; v++)
		{
			if (vis[v] == 0 && G[u][v] != INF && G[u][v] < d[v])
				d[v] = G[u][v];
		}
	}
	return ans;

}
int main()
{
	int u, v, w;
	while (scanf("%d", &n) != EOF)
	{
		if (n == 0)
			break;
		else
		{
			fill(G[0], G[0] + maxv * maxv, INF);
			int temp = n * (n - 1) / 2;
			for (int i = 1; i <= temp; i++)
			{
				scanf("%d%d%d", &u, &v, &w);
				G[u][v] = G[v][u] = w;
			}
			int ans = prim();
			printf("%d ", ans);
		}
		
	}
	
	return 0;
}

问题B:雀斑
问题描述:在迪克·范戴克 (Dick Van Dyke) 节目的一集中,小里奇 (Richie) 将父亲背上的雀斑连接起来,形成了自由钟的照片。唉,其中一个雀斑原来是疤痕,所以他的里普利的订婚失败了。
将 Dick 的背部视为在不同 (x,y) 位置有雀斑的平面。你的工作是告诉 Richie 如何连接点,以尽量减少使用的墨水量。里奇通过在对之间画直线来连接点,可能会在线之间提起笔。当 Richie 完成后,必须有一系列从任何雀斑到任何其他雀斑的连接线。

  • 输入
第一行包含 0 < n <= 100,即 Dick 背上雀斑的数量。对于每个雀斑,后面跟着一条线;以下每一行包含两个实数,表示雀斑的 (x,y) 坐标。
  • 输出
您的程序将一个实数打印到两个小数位:可以连接所有雀斑的墨水线的最小总长度。
  • 样例输入
3
2723.62 7940.81
8242.67 11395.00
4935.54 6761.32
9
10519.52 11593.66
12102.35 2453.15
7235.61 10010.83
211.13 4283.23
5135.06 1287.85
2337.48 2075.61
6279.72 13928.13
65.79 1677.86
5324.26 125.56
0
  • 样例输出
8199.56
32713.73

这题同样是稠密图,所以使用P算法:

#define _CRT_SECURE_NO_WARNINGS 1
#define _CRT_SECURE_NO_WARNINGS 1
#include <cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int maxv = 110;//表示最多100个村庄
const double INF = 1000000000;//无穷大
int n, m;
double G[maxv][maxv];
int flag[maxv][maxv];
double x[maxv], y[maxv];
double d[maxv];
int vis[maxv];
double prim()
{
	fill(d, d + maxv, INF);
	fill(vis, vis + maxv, 0);
	d[1] = 0;
	double ans = 0;
	//首先找到距离集合S最小的点u以及距离d[u]
	for (int i = 1; i <=n; i++)
	{
		int u = -1,MIN = INF;
		for (int j = 1; j <= n; j++)
		{
			if (vis[j] == 0 && d[j] < MIN)
			{
				u = j;
				MIN = d[j];
			}
				
		}
		if (u == -1) return -1;
		vis[u] = 1;
		ans += d[u];
		//printf("!!!\n");
		for (int v = 1; v <= n; v++)
		{
			if (vis[v] == 0 && G[u][v] != INF && G[u][v] < d[v])
				d[v] = G[u][v];
		}
	}
	return ans;

}
int main()
{
	int u, v, w;
	while (scanf("%d", &n) != EOF)
	{
		if (n == 0)
			break;
		else
		{
			fill(G[0], G[0] + maxv * maxv, INF);
			fill(flag[0], flag[0] + maxv * maxv, 0.0);
			for (int i = 1; i <= n; i++)
			{
				scanf("%lf%lf",&x[i],&y[i]);//注意double类型输入要用%lf
			}
			for (int i = 1; i <= n; i++)
			{
				for(int j=1;j<=n;j++)
				{ 
					if (flag[i][j]==0)
					{
						G[i][j] = sqrt((x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j]));
						flag[i][j] = 1;
						//printf("%f\n",G[i][j]);
					}
				}
			}
			double ans = prim();
			printf("%.2f\n", ans);
		}		
	}	
	return 0;
}

问题 C: 畅通工程
问题描述:省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可)。经过调查评估,得到的统计表中列出了有可能建设公路的若干条道路的成本。现请你编写程序,计算出全省畅通需要的最低成本。

  • 输入
测试输入包含若干测试用例。每个测试用例的第1行给出评估的道路条数 N、村庄数目M (N, M < =100 );随后的 N 行对应村庄间道路的成本,每行给出一对正整数,分别是两个村庄的编号,以及此两村庄间道路的成本(也是正整数)。为简单起见,村庄从1到M编号。当N为0时,全部输入结束,相应的结果不要输出。
  • 输出
对每个测试用例,在1行里输出全省畅通需要的最低成本。若统计数据不足以保证畅通,则输出“?”。
  • 样例输入
3 4
1 2 1
2 3 2
3 4 3
2 4
1 2 1
3 4 2
0 5
  • 样例输出
6
?

不能保证畅通即ans=-1,这题用K算法更加直观一些。

#define _CRT_SECURE_NO_WARNINGS 1
#include <cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxv = 110;//表示最多100个村庄
const int INF = 1000000000;//无穷大
int n, m, G[maxv][maxv];
int d[maxv];
int vis[maxv];
int prim()
{
	fill(d, d + maxv, INF);
	fill(vis, vis + maxv, 0);
	d[1] = 0;
	int ans = 0;
	//首先找到距离集合S最小的点u以及距离d[u]
	for (int i = 1; i <= m; i++)
	{
		int u = -1, MIN = INF;
		for (int j = 1; j <= m; j++)
		{
			if (vis[j] == 0 && d[j] < MIN)
			{
				u = j;
				MIN = d[j];
			}

		}
		if (u == -1) return -1;
		vis[u] = 1;
		ans += d[u];
		for (int v = 1; v <= m; v++)
		{
			if (vis[v] == 0 && G[u][v] != INF && G[u][v] < d[v])
				d[v] = G[u][v];
		}
	}
	return ans;

}
int main()
{
	int u, v, w;
	while (scanf("%d", &n) != EOF)
	{
		if (n == 0)
		{
			break;
		}			
		else
		{
			scanf("%d",&m);//村庄数目m
			fill(G[0], G[0] + maxv * maxv, INF);
			for (int i = 1; i <= n; i++)
			{
				scanf("%d%d%d", &u, &v, &w);
				G[u][v] = G[v][u] = w;
			}
			int ans = prim();
			if (ans != -1)
				printf("%d\n", ans);
			else
				printf("?\n");
		}

	}

	return 0;
}

问题 D: 继续畅通工程
问题描述:省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可)。现得到城镇道路统计表,表中列出了任意两城镇间修建道路的费用,以及该道路是否已经修通的状态。现请你编写程序,计算出全省畅通需要的最低成本。

  • 输入
测试输入包含若干测试用例。每个测试用例的第1行给出村庄数目N ( 1< N < 100 );随后的 N(N-1)/2 行对应村庄间道路的成本及修建状态,每行给4个正整数,分别是两个村庄的编号(从1编号到N),此两村庄间道路的成本,以及修建状态:1表示已建,0表示未建。
当N为0时输入结束。
  • 输出
每个测试用例的输出占一行,输出全省畅通需要的最低成本。
  • 样例输入
4
1 2 1 1
1 3 6 0
1 4 2 1
2 3 3 0
2 4 5 0
3 4 4 0
3
1 2 1 1
2 3 2 1
1 3 1 0
0
  • 样例输出
3
0

这题用K算法比较直观,一定要注意下标!!,一个是sort函数的下标,一个是循环的下标
方法1

#define _CRT_SECURE_NO_WARNINGS 1
#include <cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxv = 110;//表示最多100个村庄
const int maxe = 10010;
struct edge {
	int u;
	int v;
	int cost;
	int flag;//=1表示已建,=0表示未建
}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];
	}
	int z = a;
	while (a != father[a])
	{
		father[z] = x;
		a = father[a];
		z = a;
		
	}
	return x;
}
int kruskal(int n, int m)
{
	int ans = 0, Num_Edge = 0;
	for (int i = 1; i <=n; i++)//n个点
	{
		father[i] = i;
	}//初始化
	//排序从低向高	

	sort(E+1, E +1+ m, cmp);
	for (int i = 1; i <= m; i++)//m条边
	{
		int faU = findFather(E[i].u);
		int faV = findFather(E[i].v);
		if (faU != faV)
		{
			father[faU] = faV;
			ans += E[i].cost;
			Num_Edge++;
			if (Num_Edge == n - 1)
				break;
		}
	}
	if (Num_Edge != n-1) return -1;
	else return ans;
}

int main()
{
	int n;
	int m;
	while (scanf("%d", &n) != EOF)
	{
		if (n == 0)
			break;
		else
		{
			 m = n * (n - 1) / 2;
			for (int i = 1; i <=m ; i++)
			{
				scanf("%d%d%d%d", &E[i].u, &E[i].v, &E[i].cost, &E[i].flag);
				if (E[i].flag == 1)
					E[i].cost = 0;
			}
		}
		int ans = kruskal(n, m);
		printf("%d\n",ans);
	}	
	return 0;
}

方法2:
还是方法一简单一点,但是不好想。

#define _CRT_SECURE_NO_WARNINGS 1
#include <cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxv = 110;//表示最多100个村庄
const int maxe = 10010;
struct edge {
	int u;
	int v;
	int cost;
	int flag;//=1表示已建,=0表示未建
}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];
	}
	int z = a;
	while (a != father[a])
	{
		father[z] = x;
		a = father[a];
		z = a;
		
	}
	return x;
}
int kruskal(int n, int m)
{
	int ans = 0, Num_Edge = 0;
	for (int i = 1; i <=n; i++)//n个点
	{
		father[i] = i;
	}//初始化
	
	//首先让flag=1的点连通起来
	for (int i = 1; i <= m; i++)//m条边
	{
		if (E[i].flag == 1)
		{
			int faU = findFather(E[i].u);
			int faV = findFather(E[i].v);
			if (faU != faV)
			{
				father[faU] = faV;
				Num_Edge++;
			}
		}
	}

	sort(E+1, E +1+ m, cmp);
	for (int i = 1; i <= m; i++)//m条边
	{
		int faU = findFather(E[i].u);
		int faV = findFather(E[i].v);
		if (faU != faV)
		{
			father[faU] = faV;
			ans += E[i].cost;
			Num_Edge++;
			if (Num_Edge == n - 1)
				break;
		}
	}
	if (Num_Edge != n-1) return -1;
	else return ans;
}

int main()
{
	int n;
	int m;
	while (scanf("%d", &n) != EOF)
	{
		if (n == 0)
			break;
		else
		{
			 m = n * (n - 1) / 2;
			for (int i = 1; i <=m ; i++)
			{
				scanf("%d%d%d%d", &E[i].u, &E[i].v, &E[i].cost, &E[i].flag);
			}
		}
		int ans = kruskal(n, m);
		printf("%d\n",ans);
	}	
	return 0;
}

问题 E: Jungle Roads
问题描述:热带岛屿拉格里山的长老有问题。几年前,大量外援资金被用于修建村庄之间的额外道路。但是丛林无情地超越道路,因此大型道路网络的维护成本太高。长老议会必须选择停止维护一些道路。左上方的地图显示了所有正在使用的道路以及每月维护这些道路的成本(以 aacms 为单位)。当然,即使路线不像以前那么短,也需要通过某种方式在维护的道路上到达所有村庄。首席长老想告诉长老委员会,他们每个月可以在 aacms 上花费的最少金额来维护连接所有村庄的道路。在上面的地图中,村庄被标记为 A 到 I。右边的地图显示了可以最便宜地维护的道路,每月 216 aacms。你的任务是编写一个程序来解决这些问题。

  • 输入
输入由1100个数据集组成,最后一行只包含0。 每个数据集以一行开始,只包含一个数字n,它是村庄的数量,1 < n < 27,村庄被标记字母表的前 n 个字母大写。每个数据集都由 n-1 行完成,这些行以字母顺序以村庄标签开头。最后一个村庄没有线路。一个村庄的每条线都以村庄标签开始,然后是从这个村庄到字母表中带有标签的村庄的道路数量 k。如果 k 大于 0,则该行继续包含 k 条道路中每条道路的数据。每条道路的数据是道路另一端的村庄标签,然后是道路的每月维护成本(以 aacms 为单位)。维护成本将是小于 100 的正整数。行中的所有数据字段都由单个空格分隔。公路网将始终允许在所有村庄之间通行。该网络的道路永远不会超过 75 条。没有一个村庄有超过 15 条通往其他村庄的道路(字母表中的之前或之后)。在下面的示例输入中,第一个数据集与上面的地图一致。
  • 输出
每个数据集的每行输出一个整数:维护连接所有村庄的道路系统的每月最低成本(以 aacms 为单位)。警告:检查每组可能的道路的蛮力解决方案不会在一分钟的时间限制内完成。
  • 样例输入
3
A 1 B 42
B 1 C 87
6
A 2 B 13 E 55
B 1 C 1
C 1 D 20
D 1 E 4
E 1 F 76
0
  • 样例输出
129
114

这题我改了一个小时!发现是输入输出的错误!
还有要注意结构体数组E的大小取决于边的个数,其他没什么啦

#define _CRT_SECURE_NO_WARNINGS 1
#include <cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
const int maxv = 30;
const int INF = 1000000000;
struct edge {
	int u;
	int v;
	int cost;	
}E[900];
int n, m, k;
bool cmp(edge a, edge b)
{	
	return a.cost < b.cost;
}
int father[30];
int findFather(int x)
{
	int a = x;
	while (x != father[x])
	{
		x = father[x];
	}
	int z = a;
	while (a != father[a])
	{
		father[z] = x;
		a = father[a];
		z = a;		
	}
	return x;
}
int kruskal()
{
	int ans = 0, Num_Edge = 0;
	for (int i = 1; i <=n; i++)//n个点
	{
		father[i] = i;
	}//初始化
	sort(E, E+m, cmp);
	for (int i = 0; i < m; i++)//m条边
	{
		int faU = findFather(E[i].u);
		int faV = findFather(E[i].v);
		if (faU != faV)
		{
			father[faU] = faV;
			ans += E[i].cost;
			Num_Edge++;
			if (Num_Edge == n - 1)
				break;
		}
	}
	if (Num_Edge != n-1) return -1;
	else return ans;
}

int main()
{	
	char u[3], v[3];
	int w;
	while (scanf("%d", &n) != EOF)
	{		
		if (n == 0)
			break;
		m = 0;
		for (int i = 1; i <= n - 1; i++)//一共n-1个地点
		{
			scanf("%s %d",u,&k);				
			while(k--)
			{
				scanf("%s%d", v, &w);				
				E[m].u = u[0] - 'A' + 1;
				E[m].v = v[0] - 'A' + 1;
				E[m].cost = w;						
				m++;
			}
		}
		int ans = kruskal();
		printf("%d\n", ans);		
	}	
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值