图论算法零基础最小生成树学习与总结

Posted karshey

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图论算法零基础最小生成树学习与总结相关的知识,希望对你有一定的参考价值。

参考资料和引用来源

数据结构–最小生成树详解
AcWing 858. Prim算法求最小生成树:图解+详细代码注释(带上了保存路径)
AcWing 858. Prim算法求最小生成树 的视频
AcWing 859. Kruskal算法求最小生成树

什么是最小生成树

现在假设有一个很实际的问题:我们要在n个城市中建立一个通信网络,则连通这n个城市需要布置n-1一条通信线路,这个时候我们需要考虑如何在成本最低的情况下建立这个通信网?
于是我们就可以引入连通图来解决我们遇到的问题,n个城市就是图上的n个顶点,然后,边表示两个城市的通信线路,每条边上的权重就是我们搭建这条线路所需要的成本,所以现在我们有n个顶点的连通网可以建立不同的生成树,每一颗生成树都可以作为一个通信网,当我们构造这个连通网所花的成本最小时,搭建该连通网的生成树,就称为最小生成树。
构造最小生成树有很多算法,但是他们都是利用了最小生成树的同一种性质:MST性质(假设N=(V,E)是一个连通网,U是顶点集V的一个非空子集,如果(u,v)是一条具有最小权值的边,其中u属于U,v属于V-U,则必定存在一颗包含边(u,v)的最小生成树),下面就介绍两种使用MST性质生成最小生成树的算法:普里姆算法和克鲁斯卡尔算法。

Prim算法

朴素版prim算法(稠密图)

稠密图:边的数量是点的数量的平方的倍数。
与dijkstra很相似。

步骤:

  1. 初始化距离,把所有距离初始化为正无穷:dist[i]=0x3f3f3f3f
  2. 循环n次,每次找到不在当前生成树(连通块)中的距离最近的点,赋值给t
  3. 用t更新其他点到集合的距离(这里是与dijkstra不同的地方)
  4. 标记:st[t]=1;

点到集合的距离定义成:这个点到集合内部的所有点的边中的最小的边。

如图:左边的大圈是集合,右边是集合外的点。点到集合内的点有三条边,最短的那条就是点到集合的距离。如果没有边连向集合,那么此点到集合的距离就是正无穷。

所有点不连通的时候就不存在生成树了
注意:最小生成树里面没有环,所以输入的时候要去掉环。
若是有负环,可能出现更新自己的情况。

最小生成树的更新过程可以看这里,很清晰:
AcWing 858. Prim算法求最小生成树:图解+详细代码注释(带上了保存路径)

代码:

#include<bits/stdc++.h>
using namespace std;
#define fir(i,a,n) for(int i=a;i<=n;i++)
#define mem(a,x) memset(a,x,sizeof(a));
typedef long long ll;
const int N=500+10;
int g[N][N];
int dist[N];//存每个点到最小生成树的最短距离
int st[N];//点是否在树中
int n,m; 
int ans;
void prim()

	mem(dist,0x3f);
		
	for(int i=0;i<n;i++)//迭代n次 
	
		int t=-1;
		for(int j=1;j<=n;j++)
		
			if(!st[j]&&(t==-1||dist[j]<dist[t]))//要找树外的 
			
				t=j;//找一个离树最近的点 
			
		
		
		st[t]=1;//加进树里
		
		/*
		if(i) 第一个加进去的点不用加边,也不用管连通
		(没有加点之前没有树,自然没有到树的最短距离dist[i])
		*/	
		if(i) ans+=dist[t];//如果是第一个点,是没有边的 
		if(i&&dist[t]==0x3f3f3f3f)//最近的都是正无穷,说明不连通,没有最小生成树 
		
			ans=0x3f3f3f3f;
			return;
		
		
		for(int j=1;j<=n;j++)//用新的点t更新到所有树外的点的距离
		
			dist[j]=min(dist[j],g[t][j]);
		 		 		
	

int main()

	mem(g,0x3f);
	cin>>n>>m;
	fir(i,1,m)
	
		int a,b,c;scanf("%d%d%d",&a,&b,&c);
		if(a!=b) g[a][b]=g[b][a]=min(g[a][b],c);//无向图 注意不能有环 
	
	
	ans=0;
	prim();
	
	if(ans==0x3f3f3f3f) puts("impossible");
	else cout<<ans;
	return 0;

Kruskal

步骤:

  1. 先将所有边按照权重将从小到大排序(可以用sort来排,O(mlogm))。
  2. 枚举每条边a,b,权重是c:如果ab不连通,把ab这条边加到集合里来。(O(m))
    就类似并查集中的这两个操作:
  3. 每次记录加入的边的大小和加入边的次数,如果次数小于n-1则说明不连通,如果连通,加入的边的大小就是最小生成树的权重之和。

代码:

#include<bits/stdc++.h>
using namespace std;
#define fir(i,a,n) for(int i=a;i<=n;i++)
#define mem(a,x) memset(a,x,sizeof(a));
typedef long long ll;
const int N=1e5+10,M=2e5+10;
struct Node

	int a,b,w;
	bool operator<(const Node &W)const//重载小于号 
	
		return w<W.w;
	
node[M];

int n,m;
int p[N];//并查集 
int ans,cnt;

int find(int x)

	if(x!=p[x]) p[x]=find(p[x]);//它不是祖先,则它的祖先可能是祖先 
	return p[x];

void kruskal()

	for(int i=1;i<=n;i++) p[i]=i;//初始化并查集
	
	for(int i=1;i<=m;i++)
	
		int a=node[i].a,b=node[i].b,w=node[i].w;
		
		//找祖先查看是否连通 
		a=find(a);
		b=find(b);
		
		if(a!=b)
		
			p[a]=b;//合并a、b
			ans+=w;
			cnt++; 
		
	


int main()

	cin>>n>>m;
	fir(i,1,m)
	
		int a,b,c;scanf("%d%d%d",&a,&b,&c);
		node[i]=a,b,c;
	
	sort(node+1,node+1+m);
	
	ans=0,cnt=0;//权重之和、添加的边的次数 
	
	kruskal();
	
	if(cnt<n-1) puts("impossible");//小于n-1次说明不连通(n个点要连通肯定要有n-1次相连)
	else cout<<ans; 
	return 0;

题目总结

P1546 [USACO3.1]最短网络 Agri-Net(朴素版prim的模板题)

#include<bits/stdc++.h>
using namespace std;
#define fir(i,a,n) for(int i=a;i<=n;i++)
#define mem(a,x) memset(a,x,sizeof(a));
typedef long long ll;
const int N=100+10;
int g[N][N];
int dist[N],st[N];
int n;
int ans;

void prim()

	mem(dist,0x3f);
	
	for(int i=0;i<n;i++)
	
		int t=-1;
		for(int j=1;j<=n;j++)
		
			if(!st[j]&&(t==-1||dist[j]<dist[t]))
			
				t=j;
			
		
		
		st[t]=1;
		
		if(i&&dist[t]==0x3f3f3f3f) //虽然这题不可能不连通,但还是背模板了直接 
		
			ans=0x3f3f3f3f;
			return;
		 
		
		if(i) ans+=dist[t];
		
		for(int j=1;j<=n;j++)
		
			dist[j]=min(dist[j],g[t][j]);
		
	

int main()

	cin>>n;
	
	mem(g,0x3f);
	for(int i=1;i<=n;i++)
	
		for(int j=1;j<=n;j++)
		
			if(i!=j) cin>>g[i][j];//注意去掉环 
			else
			
				int t;cin>>t;//随便存了 
			
		
	
	
	ans=0;
	prim();
	
	cout<<ans;
	
	return 0;

P2820 局域网(kruskal的模板题)

#include<bits/stdc++.h>
using namespace std;
#define fir(i,a,n) for(int i=a;i<=n;i++)
#define mem(a,x) memset(a,x,sizeof(a));
typedef long long ll;
const int N=100+10,M=1e5+10;
/*
被除去的最大,即留下的最小,又要连通
则这是一个最小生成树
kruskal会排序所有边权,这里直接用它就可以自动删除回路 
*/

struct Node

	int a,b,w;
	bool operator<(const Node&W)const
	
		return w<W.w;
	
node[M];

int n,m;
int p[N];
int find(int x)

	if(x!=p[x]) p[x]=find(p[x]);
	return p[x];

int ans;
void kruskal()

	fir(i,1,n) p[i]=i;
	
	for(int i=1;i<=m;i++)
	
		int a=node[i].a,b=node[i].b,w=node[i].w;
		
		a=find(a);
		b=find(b);
		
		if(a!=b)
		
			p[a]=b;			
		
		else ans+=w;
		/*
		由于问的是被除去的最大值,因此:
		如果找到的不连通,说明它要被加进树里面去
		如果连通,说明这两个点间已经加过了
		而这条边就是要被删去的
		我们的答案求的就是这个 
		*/
	

int main()

	cin>>n>>m;
	fir(i,1,m)
	
		int a,b,c;scanf("%d%d%d",&a,&b,&c);
		
		/*这里其实没有必要if(c==0) c=0x3f3f3f3f; 来记录不连通 
		就算不连通把权值标为0,
		在计算的时候算上了也不会对答案有影响 
		*/ 
		node[i]=a,b,c; 
	
	
	sort(node+1,node+1+m);
	ans=0;
	kruskal();
	
	cout<<ans;
	return 0;

以上是关于图论算法零基础最小生成树学习与总结的主要内容,如果未能解决你的问题,请参考以下文章

机器学习|数学基础Mathematics for Machine Learning系列之图论:生成树算法

图论学习

图论的小总结

图论——迪杰斯特拉算法和最小生成树

软考 系统架构设计师数学与经济管理① 图论应用

软考 系统架构设计师数学与经济管理① 图论应用