最小生成树

15 篇文章 0 订阅
6 篇文章 0 订阅
本文详细介绍了Prim算法(普里姆算法)和Kruskal算法在最小生成树问题中的应用,包括Prim算法的逐步增长过程,Kruskal算法的边的选择策略,以及如何通过并查集优化链式前向星方法。重点讨论了如何避免回路和正确初始化顶点状态,适用于连通图的最小生成树构建和成本优化。
摘要由CSDN通过智能技术生成

树、生成树、最小生成树

:一个无向连通图,不包含回路(连通图中不存在环,该无向连通图就是树
生成树:覆盖途中每一个顶点的树
最小生成树:有权网络中满足 各边权值 之和最小的支撑树

一个有N个点的图,边一定是大于等于N-1条的。图的最小生成树,就是在这些边中选择N-1条出来,连接所有的N个点。这N-1条边的边权之和是所有方案中最小的。
应用:

要在n个城市之间铺设光缆,主要目标是要使这 n 个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,因此另一个目标是要使铺设光缆的总费用最低。这就需要找到带权的最小生成树。

在这里插入图片描述

实现最小生成树的两种算法

都可以归结为贪心算法
在这里插入图片描述

一、prim (普里姆算法)——让一棵树长大

在图里选一个作为生成树的根节点,生成树最先收录了V1这个顶点,从V1能长出去的方位(V1的各条邻边)中选择一条最小的边,这条边将带来有一个节点V2,将V2收入生成树中,于是生成树由一个顶点长大到了两个顶点……

在 已经收录到生成树中的顶点们 延伸出去的边 中找到权值最小的边,直到所有的顶点收录进生成树
当然,不能仅看边的权值,还应判断 选择这条边加入这个顶点是否会给生成树带来回路
在这里插入图片描述

dist【v】顶点V到生成树的最小距离,起初选定s为根节点,各顶点的dist【i】初始值为该顶点到根节点s的距离

不需要真正定义一个树节点,把树构建起来,对每一个顶点,存储它父节点的编号,根节点父亲编号 -1

将顶点收录进最小生成树<=> 该顶点到生成树的距离变为0
结束条件:
1、所有顶点收录进生成树中
2、所有没收录的顶点dist都是无穷大,意味者该图 不是连通图

#include <iostream>
using namespace std;
const int MAX=500;
#define inf 0x3f3f3f
int map[MAX][MAX];//存储边的关系
int dist[MAX];//顶点到生成树的距离
int vis[MAX];
int n,ee,s; 
int prim(int s){
//	初始化这棵生成树 一开始只有s这个节点
	for(int i=1;i<=n;i++){//初始化各节点到生成树的距离 
		dist[i]=map[s][i];
	} 
	vis[s]=1;
	int sum=0;
	for(int t=0;t<n-1;t++){//还有n-1个节点需要收录 
		int v=-1;
		int minx=inf;
		for(int i=1;i<=n;i++){//找距离生成树距离最小的节点 
			if(!vis[i]&&dist[i]<minx){
				v=i;
				minx=dist[i];
			} 
		} 
		if(v==-1){//n-1个顶点没全部收录就 找不到和生成树有边的节点了 
			cout<<"该图不连通"<<endl;
			return 0; 
		}
		vis[v]=1;
		sum+=dist[v];
//		顶点v收进生成树后dist[v]=0
		for(int j=1;j<=n;j++){
			if(!vis[j]&&dist[j]>map[v][j]){
				dist[j]=map[v][j];
			}
		} 
	} 
	return sum;
}
int main(){

	cin>>n>>ee>>s;
//	初始化map矩阵,一开始视作顶点间不存在边 
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			if(i!=j)map[i][j]=inf;
		}
	} 
//添加边的信息
	int x,y,w;
	for(int i=1;i<=ee;i++){
		cin>>x>>y>>w;
		map[x][y]=w;//无向图 
		map[y][x]=w;
	} 
	cout<<prim(s); 
    return 0;
}
 

例题来源
在这里插入图片描述

在这里插入图片描述

输入
4 5 1
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出
6

7-28 畅通工程之局部最小花费问题 (35 分)

已知局部联通,求最小生成树(可能局部联通,构成两个或多个不同的联通子集)
已知局部联通,求最小生成树,一开始直接用链式前向星那种优化版本,结果就是有一个测试点一直过不去。因为把给出的联通点全部当作收录进生成树的点(大错特错),可能给出的这些已经联通的顶点构成的是两个不同的联通子集,那哪个作为生成树的大本营呢?这时,对于各顶点的初始化不能直接根据初始是否修路,初始化为顶点到生成树之间的距离,只能老老实实得初始化为各顶点之间的距离,自选定一个顶点作为生成树,逐一将别的顶点收录进来

#include <iostream>
using namespace std;
const int maxn=105;
const int maxe=5004;
#define inf 0x3f3f3f
int map[maxn][maxn];//存储边的关系
int dist[maxn];//顶点到生成树的距离
int vis[maxn];
int n,ee; 
int prim(){
//	初始化这棵生成树 一开始只有s这个节点
	for(int i=1;i<=n;i++){//初始化各节点到生成树的距离 
		dist[i]=map[1][i];
	} 
	vis[1]=1;
	dist[1]=0;
	int sum=0;
	for(int t=0;t<n-1;t++){//还有n-1个节点需要收录 
		int v=-1;
		int minx=inf;
		for(int i=1;i<=n;i++){//找距离生成树距离最小的节点 
			if(!vis[i]&&dist[i]<minx){
				v=i;
				minx=dist[i];
			} 
		} 
		if(v==-1){//n-1个顶点没全部收录就 找不到和生成树有边的节点了 
			cout<<"该图不连通"<<endl;
			return 0; 
		}
		vis[v]=1;
		sum+=dist[v];
//		顶点v收进生成树后dist[v]=0
		for(int j=1;j<=n;j++){
			if(!vis[j]&&dist[j]>map[v][j]){
				dist[j]=map[v][j];
			}
		} 
	} 
	return sum;
}
int main(){
	cin>>n;
	ee=n*(n-1)/2;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			if(i!=j)map[i][j]=inf;
		}
	} 
//添加边的信息
	int x,y,w,f;
	for(int i=1;i<=ee;i++){
		cin>>x>>y>>w>>f;
		if(f==0){
			map[x][y]=w;//无向图 
			map[y][x]=w;
		}
		else map[x][y]=map[y][x]=0;
	} 
	cout<<prim(); 
    return 0;
}
以下未过的prim代码(一个测试点过不了)
#include <iostream>
#include <queue>
using namespace std;
const int inf=0x3f3f3f;
const int maxn=105;
const int maxe=5005;
struct edge{
	int to;
	int next;
	int w;
}e[maxe<<1];
int cnt=0;//边的编号,从1开始 
int head[maxn];//以i为起点的边在e数组中的编号
void add(int u,int v,int w){
	e[++cnt].to=v;
	e[cnt].w=w;
	e[cnt].next=head[u];
	head[u]=cnt; 
} 
int n,m;
int dist[maxn];//每个顶点到生成树的距离
int vis[maxn];//顶点是否收录进生成树 
//priority_queue< pair<int,int>,vector< pair<int,int> >,greater< pair<int,int> > > Q;

int prim(){
//	if(Q.empty()){//至少保证收录了一个点,可能测试数据一条已修的路都没有 
//		dist[1]=0;
//		Q.push(make_pair(0,1));
//	}
	int sum=0;
	for(int t=0;t<n;t++){
//在未收录的顶点里 找离生成树最近的顶点
		int v=0;
		int minx=inf;
		for(int i=1;i<=n;i++){//找距离生成树距离最小的节点 
			if(!vis[i]&&dist[i]<minx){
				v=i;
				minx=dist[i];
			} 
		} 
//		if(Q.empty()){
//			return sum; 
//			return 0;
//		} 
//		pair<int,int> p;
//		if(!Q.empty()){
//		p=Q.top();
//		Q.pop();
//		int v=p.second;
//		if(vis[v]){//跳过之后这次不能算进总次数,因为没收到顶点 
//			t--;//t代表收入的顶点数 
//			continue;
//		}
		vis[v]=1;
		sum+=dist[v];
//		cout<<v<<" "<<dist[v]<<endl;
		for(int j=head[v];j;j=e[j].next){
			int to=e[j].to;
			int w=e[j].w;
			if(!vis[to]&&dist[to]>w){
				dist[to]=w;
//				Q.push(make_pair(w,to));
			}
		}
	}

	return sum;
} 
int main(){//最小生成树

	cin>>n;
	 m=n*(n-1)/2;
	for(int i=1;i<=n;i++){
		dist[i]=inf;
		vis[i]=0;
	}

	int u,v,w,f;
	int flag=0;
	while(m--){
		cin>>u>>v>>w>>f;
		add(u,v,w);
		add(v,u,w);
		if(f==1){
//			vis[u]=vis[v]=1;
			dist[u]=dist[v]=0;
			flag=1;
//			Q.push(make_pair(0,u));
//			Q.push(make_pair(0,v));
		}
	}
	if(!flag)dist[1]=0;
	int res=prim(); 
	cout<<res;//随便啦,以1为源点 
    return 0;
}
//1、可能初始没有道路,直接计算原图的最小生成树,至少初始化一个顶点dist为0
//2、给定了n*(n-1)/2这么多可选的边,一定可以搞出生成树,
//v不需要初始化为-1(第二条无甚影响) 
//3、可能已有的道路已经联通了所有村庄,花费可能为0
 

kruskal做法

优先选修好路的边(某种意义上权值最小啊)
修好路的边不算近花费,计算边数照旧

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int inf=0x3f3f3f;
const int maxn=1005;
const int maxe=3005;
typedef struct edge{
	int u;
	int v;
	int w;
	int flag;
	edge(int x,int y,int z,int f):u(x),v(y),w(z),flag(f){}
	bool operator<(const edge& ee)const{
		if(flag==ee.flag)return w<ee.w;
		else return flag>ee.flag; 
	}
}edge;
//}e[maxe];结构体里有构造函数,不能这样开个数组 
// Kruskal算法无向图也不用开两倍空间啦
vector<edge> e;//sort(e.begin(),e.end() ) 
int fa[maxn]; 
int find(int x){//找x所在集合的编号 
	if(x==fa[x])return x;
	return fa[x]=find(fa[x]); 
}
int main(){//最小生成树
	int n,m;
	cin>>n;
	m=n*(n-1)/2;
	for(int i=0;i<=n;i++)fa[i]=i;
	int x,y,z,f;
	for(int i=0;i<m;i++){
		cin>>x>>y>>z>>f;
		e.push_back(edge(x,y,z,f));
	}
//	sort(e=1,e+n+1);
	sort(e.begin(),e.end() ) ; 
	int cnt=0;
	int sum=0; 
	for(int i=0;i<m;i++){//用了vector,push_back自然是从0开始的 
		if(cnt==n-1)break;//n个顶点收集n-1条边
		int u=e[i].u;
		int v=e[i].v; 
		int x=find(u);
		int y=find(v);
		if(x!=y){
			if(!e[i].flag)sum+=e[i].w;
			cnt++;
			fa[x]=y;
		}
	}
	cout<<sum;//一定会有结果 
//	if(cnt==n-1)cout<<sum;
//	else cout<<"Impossible";
//	set<int> se;//更严谨 
//	for(int i=1;i<=n;i++){
//		se.insert(find(i));
//	} 
//	if(se.size()!=1)cout<<"Impossible";
//	else cout<<sum;
    return 0;
}

链式前向星,最小堆优化注意点

1、链式前向星存储,遍历邻接结点时,明确 起点是v,终点是to,即将收录的顶点是v,不要把节点在e数组中存储的下标j搞混
2、用了 最小堆,遍历邻接结点并将dist【to】修改成立最小值后要即时插入到最小堆中进行排序
priority_queue实现机制是最小堆,堆顶元素Q.top()
队列queue的队头元素是Q.front();
3、有一处错误,/* lack---------*/处缺失了对出队节点是否被收录的判断

#include <iostream>
#include <queue> 
using namespace std;
const int MAX=500;
#define inf 0x3f3f3f
struct edge{
	int to;
	int next;
	int w;
}e[MAX];
int head[MAX];
int cnt;
void add(int u,int v,int w){
	e[++cnt].to=v;
	e[cnt].w=w;
	e[cnt].next=head[u];
	head[u]=cnt;
}
int dist[MAX];//顶点到生成树的距离
int vis[MAX];
int n,ee,s; 
int prim(int s){
//	初始化这棵生成树 一开始只有s这个节点
	for(int i=1;i<=n;i++){//初始化各节点到生成树的距离 
		dist[i]=inf;
		vis[i]=0;
	} 
	dist[s]=0;
	int sum=0;
priority_queue<pair<int,int>,vector<pair<int,int> >,greater< pair<int,int> > >Q;//小顶堆
	Q.push(make_pair(0,s)); 
	for(int t=0;t<n;t++){
/*		
		int v=-1;
		int minx=inf;
		for(int i=1;i<=n;i++){//找距离生成树距离最小的节点 
			if(!vis[i]&&dist[i]<minx){
				v=i;
				minx=dist[i];
			} 
		} 
		if(v==-1){//n-1个顶点没全部收录就 找不到和生成树有边的节点了 
			cout<<"该图不连通"<<endl;
			return 0; 
		}
*/
		if(Q.empty()){
			cout<<"Impossible";
			return 0;
		} 
		pair<int,int> p;
		p=Q.top();
		Q.pop();
		int v=p.second;
/*lack---------*/
		vis[v]=1;
		sum+=dist[v];
		for(int j=head[v];j;j=e[j].next){//起点v,终点to 
			int to=e[j].to;
			int w=e[j].w;
			if(!vis[to]&&dist[to]>w){//顶点v收进生成树后dist[v]=0
				dist[to]=w;
				Q.push(make_pair(w,to));
			}
		}
	} 
	return sum;
}
int main(){

	cin>>n>>ee>>s;
//添加边的信息
	int x,y,w;
	for(int i=1;i<=ee;i++){
		cin>>x>>y>>w;
		add(x,y,w);//无向图 
		add(y,x,w);
	} 
	cout<<prim(s); 
    return 0;
}

有一处错误,/* lack---------*/处缺失了对出队节点是否被收录的判断

例题

7-27 畅通工程之最低成本建设问题 (30 分)

6——3
5——3
收录6,5这两个点时都将3这个顶点放进了队列中,以至于其中3——5——1这条边收录后(假设这条边先收录),vis[3]标记为1,但3——6——1出队时,如果根本不考虑vis值鲁莽收录就会造成一个点重复收录两次的错误
在这里插入图片描述

#include <iostream>
#include <queue>
using namespace std;
const int inf=0x3f3f3f;
const int maxn=1005;
const int maxe=3005;
struct edge{
	int to;
	int next;
	int w;
}e[maxe<<1];
int cnt=0;//边的编号,从1开始 
int head[maxn];//以i为起点的边在e数组中的编号
void add(int u,int v,int w){
	e[++cnt].to=v;
	e[cnt].w=w;
	e[cnt].next=head[u];
	head[u]=cnt; 
} 
int n,m;
int dist[maxn];//每个顶点到生成树的距离
int vis[maxn];//顶点是否收录进生成树 
int prim(int s){
	for(int i=1;i<=n;i++){
		dist[i]=inf;
		vis[i]=0;
	}
	dist[s]=0;
	int sum=0;
priority_queue< pair<int,int>,vector< pair<int,int> >,greater< pair<int,int> > > Q;
Q.push(make_pair(0,s));
	for(int t=0;t<n;t++){
//在未收录的顶点里 找离生成树最近的顶点
//		int minx=inf;
//		int v=-1;
//		for(int i=1;i<=n;i++){
//			if(!vis[i]&&dist[i]<minx){
//				minx=dist[i];
//				v=i;
//			}
//		}
//		if(v==-1){
//			cout<<"Impossible";
//			return 0;
//		} 
//----------------------------------

//	while(vis[Q.top().second]){//这种方式会超时 
//			Q.pop();
//		}
		if(Q.empty()){
			cout<<"Impossible";
			return 0;
		} 
		pair<int,int> p;
		p=Q.top();
		Q.pop();
		int v=p.second;
		if(vis[v]){//跳过之后这次不能算进总次数,因为没收到顶点 
			t--;//t代表收入的顶点数 
			continue;
		}
		
		vis[v]=1;
		sum+=dist[v];
//		cout<<v<<" "<<dist[v]<<endl;
		for(int j=head[v];j;j=e[j].next){
			int to=e[j].to;
			int w=e[j].w;
			if(!vis[to]&&dist[to]>w){
				dist[to]=w;
				Q.push(make_pair(w,to));
			}
		}
	}
	return sum;
} 
int main(){//最小生成树

	cin>>n>>m;
	int u,v,w;
	while(m--){
		cin>>u>>v>>w;
		add(u,v,w);
		add(v,u,w);
	}
	int res=prim(1); 
	if(res)cout<<res;//随便啦,以1为源点 
    return 0;
}

kruskal做法(当模板好啦)

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int inf=0x3f3f3f;
const int maxn=1005;
const int maxe=3005;
typedef struct edge{
	int u;
	int v;
	int w;
	edge(int x,int y,int z):u(x),v(y),w(z){}
	bool operator<(const edge& ee)const{
		return w<ee.w; 
	}
}edge;
//}e[maxe];结构体里有构造函数,不能这样开个数组 
// Kruskal算法无向图也不用开两倍空间啦
vector<edge> e;//sort(e.begin(),e.end() ) 
int fa[maxn]; 
int find(int x){//找x所在集合的编号 
	if(x==fa[x])return x;
	return fa[x]=find(fa[x]); 
}
int main(){//最小生成树
	int n,m;
	cin>>n>>m;
	for(int i=0;i<=n;i++)fa[i]=i;
	int x,y,z;
	for(int i=0;i<m;i++){
		cin>>x>>y>>z;
		e.push_back(edge(x,y,z));
	}
//	sort(e=1,e+n+1);
	sort(e.begin(),e.end() ) ; 
	int cnt=0;
	int sum=0; 
	for(int i=0;i<m;i++){//用了vector,push_back自然是从0开始的 
		if(cnt==n-1)break;//n个顶点收集n-1条边
		int u=e[i].u;
		int v=e[i].v; 
		int x=find(u);
		int y=find(v);
		if(x!=y){
			sum+=e[i].w;
			cnt++;
			fa[x]=y;
		}
	}
//	if(cnt==n-1)cout<<sum;
//	else cout<<"Impossible";
	set<int> se;//更严谨 
	for(int i=1;i<=n;i++){
		se.insert(find(i));
	} 
	if(se.size()!=1)cout<<"Impossible";
	else cout<<sum;
    return 0;
}

二、kruskal算法 ——合木成林

认为在初始状况下每个顶点都是一棵树,不断收入权值最小的边,将两棵树并成一颗,最后将所有节点并成一棵树
收录的是边,不断搜寻权重最小的边(最小堆),该条边不能给生成树带来回路,并在原图中将该条边删除

在这里插入图片描述
结束条件:收录的边 < n-1&&原图中还有边

判断是否生成回路:
每个顶点都是一个独立的集合, 收录一条边,把两棵树并在一起相当于把两个集合并成一个,收录进新的边时,该边两头的顶点 u , v ,如果位于不同的集合,则不会产生回路。

#include <iostream>
#include<algorithm>
using namespace std;
const int MAX=500;
#define inf 0x3f3f3f
struct edge{
	int from;
	int to;
	int w;
	bool operator<(const edge& n)const{
		return w<n.w;
	}
}e[MAX];
int n,ee,s; 
int fa[MAX]; 
int find(int x){//寻找x所在树的根节点,即x节点所在的集合编号 
//	while(x!=fa[x]){
//		x=fa[x];
//	} 
//	return x; 都可以 
	if(x==fa[x])return x;
	return fa[x]=find(fa[x]);
} 
int merge(int x,int y){//判断xy俩节点是否处在同一集合 
	int a=find(x);
	int b=find(y);
	if(a==b)return 1;
	else{
		//fa[x]=b;//不是简答把x节点加入y节点,也许x节点所在的集合有10个节点,y节点所在集合只有y节点,如果输入给出的边是y x还好,把y节点加入x所在的集合,
		//如果输入给出的边是x y,就把x节点从原来所在集合删除了加入了y集合,这不胡闹吗,何况y集合也可能不止一个节点 
		fa[a]=b;//x,y不在同一结合,将y节点所在的集合中所有节点加入x所在的集合
		return 0; 
	}
}
int main(){
	cin>>n>>ee>>s;
//添加边的信息
	int x,y,w;
	for(int i=1;i<=ee;i++){
//		cin>>x>>y>>w;
//		e[i].from=x;
//		e[i].to=y;
//		e[i].w=w; 
		cin>>e[i].from>>e[i].to>>e[i].w;
	} 
//	cout<<kruskal(); 
	sort(e+1,e+n+1);//将各边按权值升序排列
	for(int i=1;i<=n;i++){
		fa[i]=i;//每个顶点视作一个集合(连通分量的编号)一开始各不相同 
	}//在中输入父子关系未说明根节点时也可以这样找根节点
//	int r=n;//见树形DP那篇 
//	while(fa[r]!=r){
//		r=fa[r];
//	} 
	int cnt=0;
	int sum=0; 
	for(int i=1;i<=ee;i++){//找到最小的一条边 
		int u=e[i].from; 
		int v=e[i].to; 
	//这条边的起点、终点,链式前向星本来不存起点的,邻接表嘛
//	get一个节点点根据起点遍历邻接结点
//	除了不能直接用起点终点定位以外,前向星几乎是完美的 
//	前向星用数组下标当作指针,一对e数组排序,数组下标全乱了
//		if(merge(u,v))continue; 
		if(!merge(u,v)){//这条边的两个顶点不在同一集合
			cnt++;//收了几条边 
			sum+=e[i].w;
		} 
		if(cnt==n-1)break;//n个顶点的生成树要收录n-1条边 
	} 
	if(cnt==n-1)cout<<sum;
	else cout<<"No!";
    return 0;
}
 

总结

在这里插入图片描述

在这里插入图片描述
参考博文

并查集

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值