[算法学习记录] 最小生成树

最小生成树是一个无向连通图中的一棵包含图中所有顶点的树,且具有最小总权重,在实际场景中常用于网络设计、电力传输、通信网络等领域。
最小生成树有两种常见实现算法,分别是Prim算法与Kruskal算法,它们的核心思想都是贪心。

Prim算法

Prim算法的实现方法类似于Dijkstra算法,都包含一个朴素写法与一个最优写法,不同的是,Prim算法每操作一个节点都要更新所有点到intree的距离d,具体看代码:

星码Starrycoding P72 【模板】最小生成树

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const ll N = 1e5+5, inf = 2e18;
ll n, m, d[N], sum;
bitset<N>intree;
//标记节点是否在树上 
struct Edge
{
	ll x,w;
	bool operator < (const Edge&u)const
	{
		if(w != u.w)return w > u.w;
	}
};
vector<Edge> g[N];
//构建邻接表,并重载小于号实现小根堆 
void Prim(int st)
{
	for(int i = 1;i<=n;i++)d[i] = inf;
	//初始化 
	priority_queue<Edge> pq;
	pq.push({st,d[st] = 0}); 
	while(pq.size())
	{
		int x = pq.top().x;pq.pop();
		if(intree[x]) continue;
		//如果这个点在树上,就跳过它 
		intree[x] = true;
		//把将要操作的点标记为在树上 
		sum += d[x];
		//更新总权值 
		d[x] = 0;
		//在树上的点标记为0   
		for(const auto &t : g[x])
		{
			int u = t.x,w = t.w;
			if(d[x] + w < d[u])
			{
				pq.push({u, d[u] = d[x] + w});
			}
			//更新其子节点到父节点的距离,并把子节点放入堆中 
		}
	}
	//松弛 
}
//prim算法实现最小生成树 
void solve()
{
	cin >> n >> m;
	while(m--)
	{
		int u, v, w;cin >> u >> v >> w;
		g[u].push_back({v, w});
		g[v].push_back({u, w});
		//无向图,u->v,v->u,所以添加两次 
	}
	
	Prim(1);
	
	for(int i = 1;i<=n;i++)if(!intree[i]) sum = -1;
	//检查是否所有点都在这棵最小生成树上,如果有一个点不在,说明构不成最小生成树。 
	cout << sum << '\n';
	//输出边权之和 
}

int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	//取消同步流,节省时间 
	int _ = 1;
	while(_--)solve();
	return 0;
}

Kruskal算法

Kruskal也是一种最小生成树算法,与Prim算法不同的是,Kruskal算法是通过不断选择权值最小的边直到生成一个最小生成树。
Kruskal算法的核心是贪心并查集,利用贪心的思想每次选择权值最小的边,利用并查集来管理边之间的联通。
Kruskal算法多用于稀疏图
实现方法:

  1. 将所有的边在按权值大小升序排列;
  2. 创建一个并查集来管理各点的连通性;
  3. 选择已排序的边,如果该边与最小生成树不联通,则将它们合并,并重复该步骤,直到处理完所有的边。
    代码实现:
    星码Starrycoding P72 【模板】最小生成树
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 5;
ll pre[N], ans, n, m;

struct Edge
{
	int u, v, w;
	bool operator < (const Edge&m)const
	{
		return w < m.w;
	}
	//根据权值、节点本身的位置以及其子节点的位置这个顺序升序排序(贪心) 
};
vector<Edge> es;

int root(int x)
{
	return pre[x] = (pre[x]== x ? x : root(pre[x]));
} 
//寻找一个节点的根节点并进行路径压缩以优化时间复杂度 
void solve()
{
	cin >> n >> m;
	while(m--)
	{
		int u, v, w;cin >> u >> v >> w;
		es.push_back({u, v, w});
	}
	sort(begin(es),end(es));
	//结构体中重载了小于号,排序规则被重定义根据权重w>u>v的顺序排序 
	for(int i = 1;i <= n;i++) pre[i] = i;
	//并查集初始化 ,每个点默认根节点为该节点本身 
	for(const auto &t : es)
	{
		int u = t.u,v = t.v,w = t.w;
		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)) ans = -1;
	//如果有一个点的根节点不相同,说明无法构成最小生成树 
	
	cout << ans <<"\n";
}

int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _ = 1;
	while(_--)solve();
	return 0;
}

本人初学算法,技艺不精,如有纰漏,敬请指正。

posted @ 2025-03-20 18:31  林克还是克林  阅读(29)  评论(0)    收藏  举报