最小生成树算法总结(Kruskal,Prim)

今天复习最小生成树算法。

最小生成树指的是在一个图中选择n-1条边将所有n个顶点连起来,且n-1条边的权值之和最小。形象一点说就是找出一条路线遍历完所有点,不能形成回路且总路程最短。

Kurskal算法

kurskal算法的核心思想是将边按权值排序,每次选出权值最小的边,只要不会形成回路就加入结果集,如果形成了回路就不选这条边,类似于贪心的思想。

具体做法是先将边按权值升序排序然后依次遍历,判断是否形成回路的方法是将点划分不同集合,初始状态每个点为一个集合,只有当一条边的两端分别位于两个集合时才选择这条边,否则就丢弃,这里用到了并查集来处理集合关系,可以参考这篇博文:https://www.cnblogs.com/czc1999/p/11823820.html,选择一条边之后要将两个点合并到同一集合。

模板题: https://www.luogu.com.cn/problem/P3366

代码如下,还是比较好理解的,时间复杂度为(O(MlogM))

#include <iostream>
#include <algorithm>
using namespace std;

int n, m;
int pre[5005];
int Find(int x) { return pre[x] == x ? pre[x] : pre[x] = Find(pre[x]); }

struct line
{
	int from, to, val;
	bool operator<(line a) { return val < a.val; }
}Arr[200005];

int Kruskal()
{
	sort(Arr, Arr + m);
	int cnt = n, res = 0;
	for (int i = 0; i < m && cnt > 1; i++)
	{
		int x = Find(Arr[i].from), y = Find(Arr[i].to);
		if (x != y)//x和y不在一个集合
		{
			pre[x] = y;//合并两个集合
			cnt--;//找到了一条边
			res += Arr[i].val;
		}
	}
	return cnt == 1 ? res : -1;//如果cnt不等于1说明没找到n-1条边,无最小生成树
}

int main() {
	cin >> n >> m;
    for (int i = 0; i < n; i++)pre[i] = i;
	for (int i = 0; i < m; i++)cin >> Arr[i].from >> Arr[i].to >> Arr[i].val;
	int res = Kruskal();
	if (-1 == res)cout << "orz"; else cout << res;
	return 0;
}

prim算法

Kruskal算法是选择边的思路,而prim算法通过选择点来得到最小生成树,有点类似于Dijkstra的感觉,初始源点可以任意选择,把点划分成已选择的点和未选择的点两个集合,需要维护一个dis数组代表每个点到已选择点的最短距离,不断把dis最小的未选择点加入已选择点集合然后更新dis,当所有点都变成已选择点(dis==0)的时候就得到了最小生成树。

代码如下,真的是跟Dijkstra很像了。

#include <iostream>
#include <algorithm>
using namespace std;

#define inf 2000000000
int n, m;
int dis[5005];
int total = 0;
int head[5005], val[400005], to[400005], nextL[400005];

void AddLine(int a, int b, int c)
{
	total++;
	to[total] = b;
	val[total] = c;
	nextL[total] = head[a];
	head[a] = total;
}

int Prim()
{
	int res = 0;
	for (int i = 2; i <= n; i++)dis[i] = inf;

	for (int i = 0; i < n; i++)//循环n次找n个点
	{
		int Min = inf, u = 1;
		for (int j = 1; j <= n; j++)//找下一个最近的未选择点
		{
			if (dis[j] != 0 && dis[j] < Min)
			{
				Min = dis[j]; u = j;
			}
		}
		if (Min == inf && u != 1)return -1;//如果遍历之后未选择点dis都为inf,说明该图是非连通图
		res += dis[u];
		dis[u] = 0;
		for (int j = head[u]; j; j = nextL[j])//更新该点周围的dis
		{
			if (dis[to[j]] > val[j])dis[to[j]] = val[j];
		}
	}
	return res;
}

int main() {
	cin >> n >> m;
	int a, b, c;
	for (int i = 0; i < m; i++)
	{
		cin >> a >> b >> c;
		AddLine(a, b, c);
		AddLine(b, a, c);
	}
	int res = Prim();
	if (-1 == res)cout << "orz"; else cout << res;
	return 0;
}

堆优化

既然prim也是要每次取dis最小的点,当然也和Dijkstra一样可以用堆优化,朴素的prim时间复杂度为(O(n^2)),优化后达到(O(nlogn)),代码如下:

#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;

#define inf 2000000000
int n, m;
int dis[5005];
int total = 0;
int head[5005], val[400005], to[400005], nextL[400005];
bool mark[5005];

void AddLine(int a, int b, int c)
{
	total++;
	to[total] = b;
	val[total] = c;
	nextL[total] = head[a];
	head[a] = total;
}

typedef pair<int, int> p;
priority_queue<p, vector<p>, greater<p>> q;

int Prim()
{
	int res = 0;
	for (int i = 2; i <= n; i++)dis[i] = inf;
	q.push(p(0, 1));
	while (!q.empty())
	{
		int u=q.top().second,v=q.top().first;
		q.pop();
		if (mark[u])continue;
		mark[u] = true;
		dis[u] = 0;
		res += v;
		for (int i = head[u]; i ; i=nextL[i])
		{
			if (dis[to[i]] > val[i])
			{
				dis[to[i]] = val[i];
				q.push(p(val[i], to[i]));
			}
		}
	}
	return res;
}

int main() {
	cin >> n >> m;
	int a, b, c;
	for (int i = 0; i < m; i++)
	{
		cin >> a >> b >> c;
		AddLine(a, b, c);
		AddLine(b, a, c);
	}
	int res = Prim();
	if (-1 == res)cout << "orz"; else cout << res;
	return 0;
}

所以选择使用哪个算法就看是点多还是边多了。

原文地址:https://www.cnblogs.com/LiveForGame/p/12296967.html