最小生成树快速入门教程【Prim&Kruskal】

最小生成树快速入门【Prim&Kruskal】

1 何为最小生成树?

要认识最小生成树,我们或许要先了解生成树概念

生成树(Spanning Tree):指在一个无向图中,包含图中V个顶点和V-1条边的的子连通图(不妨设V为图的节点个数)。

最小生成树(Minimum Spanning Tree,简称MST)就是生成树中权值和最小的树。

2 何以求解最小生成树?

求解最小生成树的常见算法包括普里姆算法(Prim’s Algorithm)、克鲁斯卡尔算法(Kruskal’s Algorithm)和索尔连科算法(Borůvka’s Algorithm)。这些算法在不同的情况下有不同的效率和适用性,简要概括如下:

  1. 普里姆算法 (Prim’s Algorithm)
    • 适用于边权重差异较大的图。
    • 开始于图中的一个顶,逐步增加新的边和顶点,直到形成最小生成树。
    • 每次都选择连接已选顶点集与未选顶点集中权重最小的边。
    • 算法的时间复杂度为 O(E+VlogV)(其中 E 是边数,V 是顶点数)。
  2. 克鲁斯卡尔算法 (Kruskal’s Algorithm)
    • 适用于边权重差异较小的图。
    • 先将所有按权重排序,然后按顺序选择边,只要这条边不会与已经选择的边形成环,就加入到生成树中。
    • 适用于包含多个连通分量的图。
    • 算法的时间复杂度也为 O(E+VlogV)。
  3. 索尔连科算法 (Borůvka’s Algorithm)
    • 适用于任何类型的图。
    • 初始化时每个顶点都是一个独立的树。
    • 然后每次找到每个连通分量中权重最小的边,并将其加到森林中,直到森林变成一棵树。
    • 算法的时间复杂度为 O(ElogV)。

由于蒟蒻也才刚学习,因此在这里只谈论更常用的Prim和Kruskal算法

悄悄告诉你,蒟蒻听佬说,在做算法题是,用Kruskal算法更多哦。

2.1 Prim

Prim是基于的算法,朴素的做法时间复杂度为O(n^2)(n是顶点数)堆优化后为O(nlogn);由于与图中边数无关,所以相较之下,更适用于稠密图

基本思想

  1. 初始化:选择图中的任一顶点(通常为1),并将其加入最小生成树中。
  2. 选择最小权重的边的点:在剩余的顶点中,找到与已选顶点集合连接的边是最小权重的点。
  3. 添加顶点,更新距离(权重):将这个顶点加入到最小生成树中,并更新距离。
  4. 重复步骤2和3:重复选择最小权重的边与顶点的步骤,直到所有的顶点都被加入最小生成树中。
  5. 结束:当所有的顶点都被加入最小生成树,算法结束,此时形成的树就是最小生成树。

伪代码

// 选任一点为起点(不妨为1)
// for(n-1次){
    // 每次确定一条边
    // 在所有点中找离intree中的点最近的点 
    // 加入intree,更新所有点到intree中任意点的最近距离d[]
// }

Prim核心:维护一个intree集合,每次从外面找一个最近的(相邻的)加入,加入后更新邻接点。

2.1.1 朴素实现 利用Array 【时间复杂度O(n^2)】

代码如下:

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e3 + 9;
const ll inf = 4e18,p = 998244353;
ll a[N][N],d[N]; // 用邻接矩阵来存 数据范围不能可过大
bitset<N> intree;
void solve()
{
    // input
	int n,m;cin >> n >> m;
    // 初始化inf
	memset(a,0x3f3f3f3f,sizeof(a));
	memset(d,0x3f3f3f3f,sizeof(d));
	for(int i = 1;i <= m;i ++)
	{
		ll u,v,w;cin >> u >> v >> w;
        // 无向图
		a[u][v] = min(a[u][v],w);
		a[v][u] = min(a[v][u],w);
	}
	ll ans = 0;
    // 将1顶点加入intree,并更新状态和距离
	intree[1] = true;
	d[1] = 0;
	for(int j = 1;j <= n;j ++)
	{
        // j点在树内,跳过
		if(intree[j]) continue;
		// 更新距离
		d[j] = min(d[j],a[1][j]);
	} 
	
	for(int i = 1;i < n;i ++)
	{
		int u = 1; // u是我们要找的距离intree中的点最近的点
		for(int j = 1;j <= n;j ++)
		{
            // 若u在树内,则直接换;不在,判断j是否不在树内且d[j]<d[u]
			if(intree[u] || (!intree[j] && d[j] < d[u])) u = j;
		}
		ans += d[u];
        // 已在树内,更改状态
		intree[u] = true;
		d[u] = 0;
		// 更新所有点到intree中任意点的最短距离
		for(int j = 1;j <= n;j ++)
		{
			if(intree[j]) continue;
			
			d[j] = min(d[j],a[u][j]);
		} 
	}
    // output
	cout << ans << '\n';
}
int main(void)
{
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	int _;cin >> _;
	while(_ --)
	{
		solve();
	}
	return 0;
}

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e3 + 9;
const ll inf = 4e18,p = 998244353;
ll a[N][N],d[N]; // 邻接矩阵 数据范围不能过大
bitset<N> intree;
void solve()
{
    // input
	int n,m;cin >> n >> m;
    // 初始化
	memset(a,0x3f3f3f3f,sizeof(a));
	memset(d,0x3f3f3f3f,sizeof(d));
    // 建图
	for(int i = 1;i <= m;i ++)
	{
		ll u,v,w;cin >> u >> v >> w;
		a[u][v] = min(a[u][v],w);
		a[v][u] = min(a[v][u],w);
	}
	ll ans = 0;
	
	for(int i = 1;i <= n;i ++)
	{
		int u = 1; // u是我们要找的距离intree中的点最近的点
		for(int j = 1;j <= n;j ++)
		{
			if(intree[u] || (!intree[j] && d[j] < d[u])) u = j;
		}
        // 第一次intree中并无元素,其d[u]是inf,不能加入ans
		if(d[u] < inf) ans += d[u];
		intree[u] = true;
		d[u] = 0;
		// 更新距离
		for(int j = 1;j <= n;j ++)
		{
			if(intree[j]) continue;
			
			d[j] = min(d[j],a[u][j]);
		} 
	}
	cout << ans << '\n';
}
int main(void)
{
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	int _;cin >> _;
	while(_ --)
	{
		solve();
	}
	return 0;
}
2.1.2 堆优化 利用priority_queue 【时间复杂度O(VlogV)】(V=>顶点数)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 9;
const ll inf = 4e18,p = 998244353;
// 距离(权重)
ll d[N];
// 最小生成树
bitset<N> intree;
// 边
struct Edge
{
	ll x,w; // 出点,权值
	bool operator < (const Edge& u) const
	{
		return w == u.w ? x < u.x : w > u.w;
	}
};
// 图
vector<Edge> g[N];

void solve()
{
    // input
	int n,m;cin >> n >> m;
	// 初始化
	memset(d,0x3f3f3f3f,sizeof(d));
	// 建图
	for(int i = 1;i <= m;i ++)
	{
		ll u,v,w;cin >> u >> v >> w;
		g[u].push_back({v,w});
		g[v].push_back({u,w});
	}
	ll ans = 0;
	
	priority_queue<Edge> pq;
	// 初始化,加入一个点
	d[1] = 0;
	pq.push({1,0});
	
	while(pq.size())
	{
        // 距离intree中最近的点
		auto [x,w] = pq.top();pq.pop();
		if(intree[x]) continue; // 若在树内,跳过
		intree[x] = true; // 更改状态
		
		ans += w;
		d[x] = 0;
		// 枚举所有出边
		for(auto &[y, w] :g[x])
		{
            // 不在树内并且权值更小才push
			if(!intree[y] && w < d[y])
			{
				 d[y] = w; // 更新距离
				 pq.push({y,w});
			}
		}
	}
    // 检查是否有点不在树内,若有,则未生成
	for(int i = 1;i <= n;i ++) 
	{
		if(!intree[i])
		{
			ans = -1;
			break;
		}
	}
	cout << ans << '\n';
}
int main(void)
{
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	int _ = 1; // cin >> _;
	while(_ --)
	{
		solve();
	}
	return 0;
}

2.2 Kruskal 贪心 【时间复杂度 O(mlogm)】

Kruskal是基于的算法,主要思想是贪心,所以用到了排序时间复杂度几乎是O(mlogm)(m 是边数)(因为并查集的操作可近似看作O(1)),所以更适用于稀疏图(Sparse graph)。

基本思想

贪心

1)给边从小到大排序

2)从小到大选边

{u,v,w}

若u,v已联通:跳过【判断联通用并查集】

若u,v未联通:选上,并连接

一个模板题:

P72 【模板】最小生成树
代码如下

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 9;
const ll inf = 4e18,p = 998244353;

// 距离(权重)
ll d[N];

struct Edge// 边
{
	ll u,v,w;// 入点,出点,权值
	bool operator < (const Edge& m) const // 重载运算符,排序
	{
		return w == m.w ? (u == m.u ? v < m.v : u < m.u) : w < m.w;
	}
};

// 并查集
int pre[N];
int root(int x){return pre[x] = (pre[x] == x ? x : root(pre[x]));}

void solve()
{
    // input
	int n,m;cin >> n >> m;
	// 初始化
	memset(d,0x3f3f3f3f,sizeof(d));
	vector<Edge> es;
	// 建图
	for(int i = 1;i <= m;i ++)
	{
		ll u,v,w;cin >> u >> v >> w;
		es.push_back({u,v,w});
	}
    // 排序
	sort(es.begin(),es.end());
	ll ans = 0;
    // 并查集初始化
	for(int i = 1;i <= n;i ++) pre[i] = i;
	for(auto &[u,v,w] : es)
	{
        // 如果联通,跳过
		if(root(u) == root(v)) continue;
		
		ans += w;
        // 更新状态
		pre[root(u)] = root(v);
	}
	// 判断是否形成了生成树
	for(int i = 1;i < n;i ++) if(root(i) != root(i + 1)) {cout << -1;return;}
	// output
	cout << ans << '\n';
}
int main(void)
{
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	int _ = 1; // cin >> _;
	while(_ --)
	{
		solve();
	}
	return 0;
}

虽说,这两个都可以求得最小生成树,但是我们不难发现,Prim算法中只记录了点被选择,并不知道点与点之间如何连接,当然我们可以编写记录它们如何连接的代码;但Kruskal算法算出权值的同时,也是记录下了边,包括其入点和出点,你可以画出最小生成树。次外,Kruskal在代码实现上也更简单之间,其实蒟蒻想说的是,在大多情况下,Kruskal或许会更好用。

  • 33
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值