最小生成树

最小生成树

最小生成树(Minimum spanning trees),对于一个连通无向图G=(V, E)(V为节点集合,E为边集合), 找G中的一个无环子集 T ∈ E T \in E TE ,使之能够将所有的节点连接起来,又具有最小的权重,即使得 w ( T ) = ∑ ( u , v ) ∈ T w ( u , v ) w(T) = \sum_{(u,v) \in T} w(u,v) w(T)=(u,v)Tw(u,v)的值最小(注:最小生成树不一定唯一)

1. MST性质

一些基本的概念:

  • 安全边:在每遍循环之前,A是某棵最小生成树的一个子集,每一次我们选择一条边 (u, v) 加入集合A,使得** A ∪ ( u , v ) A \cup (u,v) A(u,v)仍是某棵最小生成树的子集**,则 (u, v) 就成为集合A的安全边。(求最小生成树就是不断的加入安全边即可)
  • 横跨切割:如果一条边 ( u , v ) ∈ E (u, v) \in E (u,v)E的一个端点在集合S中,另一个端点在集合V-S中,则称该条边横跨切割(S, V-S)
  • 尊重:如果边集A中不存在横跨该切割的边,则称该切割尊重集合A
  • 轻量级边:在横跨一个切割的所有边中,权重最小的边称为轻量级边

举个栗子

y6gWKf.png

  • 横跨切割 (S, V-S) 的边:(b, c), (c, d), (b, h), (d, f), (a, h), (e, f)
  • 轻量级边:(c, d) 是唯一的轻量级边,权重为7
  • 尊重:设集合A = {(a, b), (c, i), (c, f), (f, g), (g, h)} (途中的灰色边),则该切割 (S, V - S) 不存在横跨A的边,故切割 (S, V - S)尊重集合A

MST性质:设G = (V, E) 是一个在边E上定义了实数值权重函数w的连通无向图。设集合A为E的一个子集,且A中包含在图G的某棵最小生成树中,设 (S, V - S) 是图G中尊重集合A的任意一个切割,又设(u, v)是横跨切割 (S, V - S)的一条轻量级边,那么边(u, v)对于集合A是安全的。(证明略,可以自行参考算法导论P364)

根据MST性质,我们就能得到最小生成树的算法了

y62JeS.png

2. 具体实现

kruskal和prim算法都是GENERIC-MST算法的具体实现,二者都是通过一个具体的规则来确定GENERIC-MST算法中的第三行所描述的安全边。两个算法的时间复杂度均为 O ( E l g V ) O(ElgV) O(ElgV),详细证明参考算法导论P366和P369,算法题链接

2.1 kruskal

算法描述

y62jSI.png

集合A始终是一个森林,开始时,其节点集就是G的节点集,并且A是所有单节点树构成的森林。之后每次加入到集合A中的安全边是连接A的两个不同连通分量权重最小的边

举个栗子:

ycFTun.png

详细代码

kruskal其实主要就是用到并查集,并查集链接

#include<iostream>
#include<algorithm>
using namespace std;
#define N 5500
#define M 2100
int parent[N], depth[N];
// 并查集模板
void init(int n) {
	for (int i = 0; i <= n; i++) {
		parent[i] = i;
		depth[i] = 1;
	}
}
int find(int i) {
	while (i != parent[i]) {
		parent[i] = parent[parent[i]];
		i = parent[i];
	}
	return i;
}
inline bool connected(int p, int q) {
	p = find(p);
	q = find(q);
	return p == q;
}
void unionNode(int p, int q) {
	p = find(p);
	q = find(q);
	if (p == q) {// 根节点相同,无需合并
		return;
	}
	else if (depth[p] > depth[q]) {
		parent[q] = p;
	}
	else if (depth[p] < depth[q]) {
		parent[p] = q;
	}
	else {
		parent[q] = p;
		depth[p]++;
	}
}
// 查并集模板

struct edge
{
	int u, v, w;
	friend bool operator <(edge e1, edge e2) {
		return e1.w < e2.w;
	}
} arr[M];
int main() {
	int n, m, ans = 0, num = 0;
	cin >> n >> m;
	init(n);
	for (int i = 1; i <= m; i++) {
		cin >> arr[i].u >> arr[i].v >> arr[i].w;
	}
	sort(arr + 1, arr + m + 1);
	for (int i = 1; i <= m; i++) {
		if (connected(arr[i].u, arr[i].v))
			continue;
		ans += arr[i].w;
		num++;
		unionNode(arr[i].u, arr[i].v);
	}
	if (num < n - 1)
		cout << "orz" << endl;
	else
		cout << ans << endl;
	return 0;
}

2.2 prim

算法描述

yf8l9O.png

举个栗子:

yf8LUx.png

详细代码实现:

#include<iostream>
#include<list>
#include<map>
#include<queue>
#include<string.h>
using namespace std;
#define N 5100
list<pair<int, int> > head[N];
// 记录以每一个节点为首的边的链表
// 例如 head[u] 表示通过u节点的边的链表, first表示边的权重, second表示相邻节点
int vis[N];
// 记录每个节点是否被访问, 0未访问, 1已访问
void prim(int n) {
	int u, w, cnt = 0;
	long long ans = 0;
	priority_queue<pair<int, int>, vector<pair<int, int> >, greater<pair<int, int> > > q;
	// 优先队列, first表示边的权重, second表示到达的节点
	q.push(make_pair(0, 1));
	// 从1号节点出发开始寻找最小生成树
	while (!q.empty() && cnt < n) {
		// 取得优先队列中权重最小的边
		w = q.top().first;
		u = q.top().second;
		q.pop();
		if (vis[u] != 0)	// 如果节点已经在最小生成树中,跳过
			continue;
		vis[u] = 1;
		cnt++;
		ans += w;
		// 将到达节点相邻的未访问节点的边添加到优先队列中
		for (auto it = head[u].begin(); it != head[u].end(); it++) {
			if (vis[it->second] == 0) 
				q.push(make_pair(it->first, it->second));
		}
	}
	if (cnt < n)
		cout << "orz" << endl;
	else
		cout << ans << endl;
}
int main() {
	memset(vis, 0, sizeof(vis));
	int n, m, u, v, w;
	cin >> n >> m;
	for (int i = 0; i < m; i++) {
		cin >> u >> v >> w;
		head[u].push_back(make_pair(w, v));
		head[v].push_back(make_pair(w, u));
	}
	prim(n);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值