最小生成树

定义

给定一张边带权的无向图\(G = (V,E)\),\(n = |V|\),\(m = |E|\)。由\(V\)中n个顶点和\(E\)中n - 1条边构成的无向联通子图称为\(G\)的一棵生成树。边的权值最小之和最小的生成树称为\(G\)的最小生成树


定理

任意一棵最小生成树一定包含无向图中权值最小的边。


Kruskal算法

Kruskal算法总是维护无向图的最小生成森林,最初可认为森林由零条边构成,每个节点各自构成一棵只含一个点的树

在任意时刻从剩下的边中选取权值最小的,并且这条边的两个端点属于森林中不同的两棵树,则把该边加入森林。用并查集维护图中节点的连通性


算法流程:

1.建立并查集,每个点各自构成一个集合

2.把所有边按照边权大小从小到大排序,依次扫描每条边(\(x\),\(y\),\(z\))

3.若\(x,y\)属于同一集合,则忽略这条边,继续扫描下一条边

4.若\(x,y\)属于不同集合,合并\(x,y\)所在的集合,并把边权\(z\)累加到答案中

5.所有边扫描完毕后,4中扫过的边就构成最小生成树

时间复杂度:\(O(m log m)\)


代码:

#include <bits/stdc++.h>
using namespace std;
#define maxn 100010
struct node
{
	int x,y,z;
}edge[maxn * 2];//用struct数组存边 

int f[maxn],n,m,ans;//f为并查集数组 

bool cmp(node a,node b)//按照边权从小到大排序 
{
	if (a.z < b.z)
		return 1;
	return 0;
}

int find(int i)//并查集判联通 
{
	if (f[i] != i)
		f[i] = find(f[i]);
	return f[i]; 
}

int main()
{
	cin >> n >> m;
	for (int i = 1;i <= m;i++)
		cin >> edge[i].x >> edge[i].y >> edge[i].z;
	sort(edge + 1,edge + m + 1,cmp);
	for (int i = 1;i <= n;i++)//并查集一定一定要初始化!! 
		f[i] = i;
	for (int i = 1;i <= m;i++)
	{
		int t1 = find(edge[i].x),t2 = find(edge[i].y);
		if (t1 == t2)//若两个点本来就联通,就不用把它们合并了,直接继续处理下一条边 
			continue;
		f[t1] = t2;//合并两个集合 
		ans += edge[i].z;
	}
	cout << ans << endl;
	return 0;
}

Prim算法

Prim算法总是维护最小生成树的一部分,最初,Prim算法仅确定1号节点属于最小生成树。

在任意时刻,设已经确定属于最小生成树的点的集合为\(T\),剩余节点集合为\(S\),Prim算法找到\(min_x\in _S,_y\in_T\){\(z\)},即两个端点分别属于集合\(S\),\(T\)的权值最小的边,然后把点\(x\)从集合\(S\)中删除,加入到集合\(T\)中,并把\(z\)累加到答案中。

具体地,我们可以维护一个数组\(d\):若\(x \in S\),则\(d[x]\)表示节点\(x\)与集合\(T\)中的节点之间边权最小的边的权值。若\(x\)属于\(T\),则\(d[x]\)就等于\(x\)被加入\(T\)的时候选出的最小边的权值。

可以类比Dijkstra算法,用一个数组标记节点是否属于\(T\)。每次从未标记的节点中选出\(d\)值最小的,把它标记(加入\(T\)),同时扫描出所有边,更新另一个端点的\(d\)值。最后,最小生成树的权值总和就是\(\sum\) \(n\atop x = 2\) \(d[x]\).

Prim算法的时间复杂度为\(O(n^2)\),可以用二叉堆优化到\(O(m log n)\),但用二叉堆优化不如直接使用Kruskal算法方便。因此,Prim算法在稠密图中更优(尤其是完全图的最小生成树的求解),Kruskal算法在稀疏图中更优。

代码:

int a[maxn][maxn],d[maxn],n,m,ans;
bool v[maxn];

void prim()
{
   memset(d,0x3f,sizeof(d));//要求每个d的最小值,所以d要初始化为无穷大 
   memset(v,0,sizeof(v));//刚开始时所有点都没有被处理 
   d[1] = 0;
   for (int i = 1;i < n;i++)
   {
   	int x = 0;
   	for (int j = 1;j <= n;j++)
   		if (!v[j] && (x == 0 || d[j] < d[x]))
   			x = j;//找到边权最小的一条边 
   	v[x] = 1;//把x标记为已经处理过 
   	for (int y = 1;y <= n;y++)
   		if (!v[y])
   			d[y] = min(d[y],a[x][y]);//求出每个d的最小值 
   }
}

int main()
{
   cin >> n >> m;
   memset(a,0x3f,sizeof(a));
   for (int i = 1;i <= n;i++)//构建邻接矩阵 
   	a[i][i] = 0;
   for (int i = 1;i <= m;i++)
   {
   	int x,y,z;
   	scanf("%d%d%d",&x,&y,&z);
   	a[y][x] = a[x][y] = min(a[x][y],z);//有重边的话就取最小的边权 
   }
   prim();//跑最小生成树
   for (int i = 2;i <= n;i++)
   	ans += d[i];
   cout << ans << endl;
}
posted @ 2021-11-10 22:46  Carlotta24  阅读(146)  评论(0)    收藏  举报