【算法速成课1 | 题解】洛谷P3366 【模板】最小生成树 MST(Prim & Kruskal)

碎碎念:

其实这个难度的算法才适合加到《再来一遍一定记住的算法(那些你可能忘记了的算法)》专栏。

但现在这个专栏都默认是数论团建了,之后会出一个"算法速成课"专栏,

对标"再来"专栏的长篇大论,较简单的算法保证半小时以内就能看懂。

前导:

给你一堆无向边,它们互相连通。

我们想从中选出几条边,它们边权加起来最小,且互相连通。

应该怎么做?

这是求最小生成树算法,听上去很有用对吧?

最小生成树(Minimum Spanning Tree, MST)

是指在一个连通无向图 中,能够连接所有顶点边的权重总和最小的树结构。

这在生活中也很有用,比如:导航路线规划、电网选择。

解析:

求出最小生成树 的算法有两种,Prim 和 Kruskal

Prim 适合稠密图(边数接近顶点数平方的图),

Kruskal 则适合稀疏图(边数远小于顶点数平方的图)。

一般我们用 Kruskal,总体较快常数小,但两个都要学(着重背 Kruskal)。

Prim:

Prim 算法是一种贪心算法,从一个起始顶点开始,逐步扩展生成树:

  1. 从任意顶点开始,将其加入生成树集合。
  2. 重复选择与当前生成树相连的最小权重边
  3. 将新顶点加入生成树,直到覆盖所有顶点
注释代码:
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N = 5e5 + 10;

struct node{
	int x; LL v;
} ;
vector<node> G[N];
bool v[N];   //v[i]:为 ture就是 i走过,反之则没有 
int n, m; 

bool operator<(node na, node nb) {
	return na.v > nb.v;
}

void Prim(){
	LL ans = 0;
	
	priority_queue<node> Q;
	memset(v, 0, sizeof(v));
	int st = 1, sum = 1;   // st:起始点,sum:走过多少点 
	v[st] = 1;
	
	for(auto i: G[st]) {
		Q.push(i);
	}
	
	int Time = 0;   //特判是否是连通图用的循环次数计数 
	while (sum < n) {
		if (Time > 3 * m) {  //每条边最多走两遍,超过三遍那肯定不连通 
			cout << "orz" << "\n";
			return ;
		}
		Time++;   //不能放下面那个 if(v)下面!!! 
		
		auto Head = Q.top();
		Q.pop();
		
		int x = Head.x;
		if (v[x]) {   //虽然下面进堆之前就特判了
		//但是历史遗留的边的点可能已经走过了 
			continue;
		}
		v[x] = 1;
		sum++;
		ans += Head.v;
		
		for (auto j: G[x]) if(!v[j.x]) {
			Q.push(j);
		}
	}
	
	cout << ans << "\n";
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	cin >> n >> m;
	for (int i = 1; i <= m; i++) {
		int x, y; LL v;
		cin >> x >> y >> v;
		G[x].push_back({y, v});
		G[y].push_back({x, v});
	}
	Prim();
	
	return 0;
} 
关于时间复杂度:

以下 N 是点数,M 是边数。

我用的是二叉堆 + while,时间复杂度是

(实际 while 次数会比 N 多,大概在 M 左右,常数为 2)

在洛谷上平均跑 300ms 以下,常数还行。

Kruskal:

Kruskal 算法也是一种贪心算法,但它是基于边的选择:

  1. 将所有边按权重从小到大排序。
  2. 依次选择最小权重的边。
  3. 如果该边连接的两个顶点不在同一连通分量中(不形成环),则加入生成树。
  4. 重复步骤 2 和 3,直到选够 N - 1 条边(N 是节点个数)。
注释代码:
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

const int N = 5e5 + 10;
typedef long long LL;

int fa[N];
struct node {
	int x, y;
	LL v;
} a[N];

bool cmp(node na, node nb) {
	return na.v < nb.v;
}

int findfa(int x) {
	if (fa[x] == x) {
		return fa[x];
	}
	return fa[x] = findfa(fa[x]);
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	int n, m;
	cin >> n >> m;
	
	for (int i = 1; i <= m; i++) {
		cin >> a[i].x >> a[i].y >> a[i].v;
	}
	
	for (int i = 1; i <= n; i++) {
		fa[i] = i;
	}
		
	LL ans = 0;  //累计答案 
	int sum = 0;  //累计已选的边数 
	sort(a + 1, a + m + 1, cmp);
	
	for (int i = 1; i <= m; i++) {
		int tx = findfa(a[i].x);
		int ty = findfa(a[i].y);
		
		if (tx != ty) {
			sum ++;
			ans += a[i].v;
			fa[tx] = ty;
		}
		
		if (sum == n - 1) {  //找够边就退出 
			break;
		}
	}
	
	if (sum < n - 1) {   //都结束了还选不到 n - 1 条合适的边,包不连通的 
		cout << "orz" << "\n";
		return 0;
	}
	
	cout << ans << "\n";
	
	return 0;
}
关于时间复杂度:

以下 N 是点数,M 是边数。

我用的是排序 + 并查集,总时间复杂度大概是

压缩路径并查集的时间极快,基本可以算常数。

外面套个 for 就是

在洛谷上平均跑 200ms 以下。

后记:

请支持我的新专栏《算法速成课》!

快到 csp 的时间了,以后会多出一点这样的基础算法帮助大家复习!

相关推荐
Arva .1 分钟前
G1收集器
java·jvm·算法
努力毕业的小土博^_^2 分钟前
【生成式AI】Cross-Attention:多模态融合的神经网络桥梁(上篇)
人工智能·深度学习·神经网络·算法·机器学习·遥感
lingggggaaaa3 分钟前
C2远控篇&C&C++&SC转换格式&UUID标识&MAC物理&IPv4地址&减少熵值
c语言·c++·学习·安全·web安全·网络安全·免杀对抗
世界宇宙超级无敌究极特级顶级第一非常谱尼9 分钟前
RF Power Amplifiers for Wireless Communications 第三章学习笔记
笔记·学习·功放·pa·mmic
Rousson13 分钟前
硬件学习笔记--92 关于UWB的介绍
笔记·学习
玩具猴_wjh18 分钟前
12.12 学习笔记
笔记·学习
for_ever_love__19 分钟前
移动应用开发实验室25级期末考核
c++
聆风吟º22 分钟前
【顺序表习题|图解|双指针】移除元素 + 删除有序数组中的重复项
c语言·数据结构·c++·经验分享·算法
无心水22 分钟前
【神经风格迁移:蒙德里安】12、语义感知的构图重构算法:让蒙德里安风格“理解“图像内容
算法·重构·vgg·信息智能化·csdn月度精选·ai原生架构·神经风格迁移:蒙德里安
Salt_072823 分钟前
DAY33 类的装饰器
python·算法·机器学习