还是畅通工程(prim+kruskal最小生成树)

题目连接: 还是畅通工程

题目:

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

Input
测试输入包含若干测试用例。每个测试用例的第1行给出村庄数目N ( < 100 );随后的N(N-1)/2行对应村庄间的距离,每行给出一对正整数,分别是两个村庄的编号,以及此两村庄间的距离。为简单起见,村庄从1到N编号。
当N为0时,输入结束,该用例不被处理。

Output
对每个测试用例,在1行里输出最小的公路总长度。

Sample Input
3
1 2 1
1 3 2
2 3 4
4
1 2 1
1 3 4
1 4 1
2 3 3
2 4 2
3 4 5
0

Sample Output
3
5

解题思路:

本题我会提供prim, prim+优先队列, kruskal, kruskal+优先队列 共四种代码, 两种解题思路:

1.普里姆算法:

普里姆算法的重点是图中的点, 由于我们需要让图中所有的点关联起来, 所以这个算法的核心是弄两个集合, 一个集合A表示已经搜寻完的点集, 另一个集合B表示还未搜寻的点集. 而我们的目的就是让图中所有的点都在集合A中即可.

题解出发点:

因为最终每一个点都应该在集合A中, 所以我们可取图中任一点作为我们的起始点, 称之为key点, 然后我们去寻找看看key点能到某个点(称为D点)的距离最短, 这个D点就是下一个应该被纳入集合A中的点. 假设现在D已经被我们纳入了集合A中, 当我们再找寻下一个D’点的时候, 我们应该考虑key点和D点能到的所有点中距离最近的.
这样通过贪心的思想, 我们每次都取一个min{A集合某点到B集合某点的距离}, 我们就可以完成我们的目的.

代码角度分析:

我认为困难点就在于, 把题解出发点中的A集合与B集合代码化. 其实无脑一点, 你可以开一个二维数组, dis[x][y]表示从x到y的距离. 但是这样太傻了, 太傻了, 太傻了.
所以我们有如下优化: 我们不妨就只开一个数组dis来存key点到所有点的距离, 当每次添加新的点D到集合A中时, 我们维护这个dis数组, 看看我们取 min{从key点到某点(称为X)的距离, 从D点到X点的距离}, 这样我们只需要一个一维数组即可.
剩下的分析我们看AC代码部分

普里姆代码:
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int MAXN = 105, INF = 0x3f3f3f3f;
int mp[MAXN][MAXN]; //存图
int dis[MAXN]; //表示key点到另外点的距离, 后续代码默认key点取第一个点
bool vis[MAXN]; //表示点是否在集合A中
int n;
void initialize() {
	memset(dis, 0x3f, sizeof(dis));
	memset(mp, 0x3f, sizeof(mp)); //这两步memset表示默认不可达
	memset(vis, 0, sizeof(vis)); vis[1] = 1;
	for (int i = 1; i <= n * (n - 1) / 2; ++i) {
		int a, b, c;
		scanf("%d %d %d", &a, &b, &c);
		mp[a][b] = mp[b][a] = c;
	}
	for (int i = 1; i <= n; ++i) dis[i] = mp[1][i];
	//注: dis[1] = 0, for从2开始跑都是可改可不改的, 
	//因为我们的点1已经标记为集合A的点了. 
}
void prim() {
	int res = 0;
	for (int i = 1; i <= n - 1; ++i) { //需要让其余n-1个点进入集合A
		int index = min_element(dis + 1, dis + 1 + n) - dis;
		res += dis[index]; vis[index] = 1; //更新结果, 让index进入集合A
		for (int i = 1; i <= n; ++i) {
			if (!vis[i]) dis[i] = min(dis[i], mp[index][i]); //维护dis
		}
		dis[index] = INF; //这是为了min_element()函数(可手写)
	}
	cout << res << endl;
}
int main(void)
{
	while (scanf("%d", &n), n) {
		initialize();
		prim();
	}
	return 0;
}
引入优先队列:

引入优先队列的目的也就是为了不再去寻找dis中的最小值, 整体思路几乎是一样的, 直接看代码吧.

普里姆代码(引入优先队列):

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int MAXN = 105, INF = 0x3f3f3f3f;
int mp[MAXN][MAXN]; bool vis[MAXN];
int n;
struct node {
	int dis, id;
	bool operator < (const node& t) const {
		return dis > t.dis; //优先队列由于是按照优先级排序, 因此从小到大排序要用 >
	}
};
priority_queue<node> q;
void initialize() {
	while (!q.empty()) q.pop();
	memset(vis, 0, sizeof(vis));
	memset(mp, 0x3f, sizeof(mp));
	for (int i = 1; i <= n * (n - 1) / 2; ++i) {
		int a, b, c;
		scanf("%d %d %d", &a, &b, &c);
		mp[a][b] = mp[b][a] = c;
	}
}
void prim() {
	int temp = 1; //表示当前要加入集合A的点
	int res = 0;
	for (int i = 0; i < n - 1; ++i) { //n-1次循环, 没有标记最后进入集合A的点, 但是不影响结果
		vis[temp] = 1; //temp点加入集合
		for (int j = 1; j <= n; ++j) {
			if (!vis[j]) q.push({ mp[temp][j], j }); 
		}
		while (!q.empty() && vis[q.top().id]) q.pop(); //找到我们要添加的点
		node op = q.top(); q.pop();
		res += op.dis; temp = op.id;
	}
	cout << res << endl;
}
int main(void)
{
	while (scanf("%d", &n), n) {
		initialize(); 
		prim();
	}
	return 0;
}

2.克鲁斯卡尔算法:

克鲁斯卡尔算法和普里姆算法的最大区别就是, 普里姆算法的重点是图中的点, 而克鲁斯卡尔的重点是图中的边.
这个算法比上个算法的入手难度稍微高一点点, 涉及到了并查集的基础知识.

题解出发点:

该算法希望把图中所有的边都记录下来, 从小到大进行排序, 每次取出边中的最小值, 看看通过该边相连的两个节点是否属于同一集合, 如果不是的话则将它们连通.
从而我们可以发现, 这个算法的本质是对边进行排除, 而普里姆算法则是对点进行排除, 在普里姆算法中: 我们可以确定当我们把n个点都放在集合A中, 那么我们一定完成了最小生成树. 但是在克鲁斯卡尔算法中, 我们只能知道当我们把所有的边都排除了之后, 我们才完成了最小生成树.

那么我们为什么要用到并查集呢? 像prim算法那样记录点不可以吗?

答案当然是否定的. 假如我们第一条最短边连接了点M和N, 然后把M, N两点放入集合A(代码中即用vis数组去标记), 然后第二条最短边连接了P和Q, 我们此时如果再把P和Q放入集合A中, 那么在我们的程序算法中就会认为M, N, P, Q四点都已经连通了, 这显然是错误的, 我们明明只有M和N, P和Q是连通的, 但是他们之间是不连通的.他们彼此形成了一个小集合, 是最终我们要求的集合A的两个不相关的子集.

为了避免这种情况的发生, 因此我们就需要引入并查集的概念. 但是本文不对并查集进行讲解.

代码角度分析:

我认为, kruskal算法在你理解并查集和prim算法的基础上来讲, 好像代码角度也没什么难度.
但是在此提示一点: 请注意克鲁斯卡尔算法是存的所有边的数据, 因此请注意数组的大小!!!,
对于本题来说, 如果你忽略了这点, 像普里姆算法那样开了105, 那真是各种T和WA的辛酸史.

克鲁斯卡尔代码:
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
int n, m; const int MAXN = 10000; //要开大点!
int pre[MAXN]; //存放上一级节点
struct Edge {
	int a, b, dis;
	bool operator < (const Edge& t) const {
		return dis < t.dis;
	}
}edge[MAXN];
void union_found() { //并查集的建立
	for (int i = 0; i <= n; ++i) pre[i] = i;
}
int union_find(int x) { //找到点x的根节点
	if (pre[x] == x) return x;
	return union_find(pre[x]);
}
void initialize() {
	union_found();
	m = n * (n - 1) / 2;
	for (int i = 0; i < m; ++i) {
		scanf("%d %d %d", &edge[i].a, &edge[i].b, &edge[i].dis);
	}
}
void kruskal() {
	int res = 0;
	sort(edge, edge + m); 
	for (int i = 0; i < m; ++i) {
		Edge op = edge[i];
		const int a = union_find(op.a), b = union_find(op.b);
		if (a == b) continue;
		res += op.dis;
		pre[b] = a; //注意, 在集合关联的时候, 要将两个集合的根节点进行关联
	}
	cout << res << endl;
}
int main(void)
{
	while (scanf("%d", &n), n) {
		initialize();
		kruskal();
	}
	return 0;
}
引入优先队列:

这个算法引入优先队列, 其实啥都没改变.

克鲁斯卡尔代码(引入优先队列):
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
int n, m; const int MAXN = 10000;
int pre[MAXN];
struct Edge {
	int a, b, dis;
	bool operator < (const Edge& t) const {
		return dis > t.dis; //注意大于号排序
	}
};
priority_queue<Edge> q;
void union_found() {
	for (int i = 0; i <= n; ++i) pre[i] = i;
}
int union_find(int x) {
	if (pre[x] == x) return x;
	return union_find(pre[x]);
}
void initialize() {
	union_found();
	m = n * (n - 1) / 2;
	for (int i = 0; i < m; ++i) {
		int a, b, c;
		scanf("%d %d %d", &a, &b, &c);
		q.push({ a ,b, c });
	}
}
void kruskal() {
	int res = 0;
	while (!q.empty()) {
		const Edge op = q.top(); q.pop();
		int a = union_find(op.a);
		int b = union_find(op.b);
		if (a == b) continue;
		res += op.dis;
		pre[b] = a;
	}
	cout << res << endl;
}
int main(void)
{
	while (scanf("%d", &n), n) {
		initialize();
		kruskal();
	}
	return 0;
}

算法对比分析:

普里姆(prim)算法: 一般认为时间复杂度为 O(n2), n为节点数.
克鲁斯卡尔(kruskal)算法: 一般认为时间复杂度为 O(n*logn), n为边数.
所以不难看出, 当图中边少的情况, 我们采用kruskal算法, 而边多的情况, 我们可以采用prim算法

两者的共同缺点就是, 都不能处理成环问题.

END

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

逍遥Fau

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

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

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

打赏作者

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

抵扣说明:

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

余额充值