专题:无向图的生成树

概述

生成树是针对无向图而言的。一般地,给定一张无向图,若其任意两个顶点都互相联通并形成了一个树结构,则称这个树为该无向图的生成树。生成树有很多种,例如普通的最小(最大)生成树、k 阶最小(最大)生成树、瓶颈树等等。

分类精析

一、普通最小生成树(MST)

题目:模板题

分析与解:题目要求模板。这里我们直接给出code,并具体结合代码讲解。

//Prim 实现思想:贪心。优化数据结构:优先队列。
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N=5e3+5,M=4e5+5;
struct edge{int next,to,w;}e[M];
struct vd
{
	int v,dis;
	bool operator < (const vd &a)const { return dis>a.dis; }
};
int head[N],d[N],idx,n,m,cnt;
bool mark[N];
inline void add(int u,int v,int w) { e[++idx]={head[u],v,w};head[u]=idx; }
int prim(int s)
{
	priority_queue<vd> q;
	memset(d,0x3f,sizeof(d));
	d[s]=0;//细节:这里加入了源点,但是不能进行标记(标记了就不会扩展节点),cnt这里也不能自增 
	vd temp={s,d[s]};q.push(temp);
	int tot=0; 
	while(q.size() && cnt<n)
	{
		temp=q.top();q.pop();
		if(mark[temp.v]) continue;
		mark[temp.v]=1;
		cnt++;//选一个点,计数一次 
		tot+=temp.dis;//只有在真正选点时才记录
		for(int i=head[temp.v];i;i=e[i].next)
			if(d[e[i].to]>e[i].w && !mark[e[i].to])//没有被访问过且可以被更新
			{
				d[e[i].to]=e[i].w;
				q.push({e[i].to,d[e[i].to]});
			}
	}
	return tot;//返回这棵树权值和
} 
int main()
{
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	{
		int u,v,w;
		cin>>u>>v>>w;
		add(u,v,w),add(v,u,w);//无向边
	}
	int ans=prim(1);
	if(cnt==n) cout<<ans<<endl;//注意:Prim以点为单位更新,对于n个点的无向图的MST,应有呢个节点
	else cout<<"orz"<<endl;
	return 0;
}
需要注意的细节
  • 朴素Prim的时间复杂度是 O(n^2) ,用优先队列优化后是O(n*log2m) ;
  • Prim是以点为单位进行扩展,所以如果判断是否联通或自环,只需维护 cnt 变量,每扩展一个点就自增1。如果最后有 n 个点,说明是联通无环的。
  • 思想是对点进行贪心。
//Kruskal 应用并查集,时间复杂度为m*log2m
#include<iostream>
#include<algorithm>
using namespace std;
const int N=5e3+5,M=4e5+5;
struct edge{int u,v,w;}e[M];
int fa[N],cnt,n,m;
int cfind(int x) { return fa[x]==x?x:fa[x]=cfind(fa[x]); }
void cmerge(int x,int y) { fa[cfind(x)]=cfind(y); }
bool comp(edge a,edge b) { return a.w<b.w; }
int kruskal()
{
	int tot=0;
	sort(e+1,e+m+1,comp);
	for(int i=1;i<=n;i++) fa[i]=i;
	for(int i=1;i<=m;i++)
	{
		if(cfind(e[i].u)!=cfind(e[i].v))
		{
			cmerge(e[i].u,e[i].v);
			tot+=e[i].w;
			cnt++;//Kruskal以边为单位展开,cnt记录扩展的边数
		}
	}
	return tot;
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=m;i++)
		cin>>e[i].u>>e[i].v>>e[i].w;
	int ans=kruskal();
	if(cnt==n-1) cout<<ans<<endl;//判断条件:n个点的无向图,MST应有n-1条边 
	else cout<<"orz"<<endl;
	return 0;
}
需要注意的细节
  • 稳定的Kruskal时间复杂度通常为 O(m*log2 m) ,在数据不极端时通常采用这个方法,效率略略低于Prim。
  • Kruskal是以边为单位进行扩展,在判断联通或自环时,只需维护 cnt 变量,每扩展一条边就自增1。如果最后有 n-1 条边,说明是联通无环的。
  • 思想是对边进行贪心。

二、普通最大生成树(LST)

分析与解:同MST一样,区别只是从大到小贪心,其他完全一样,不再赘述。

三、k阶生成树

1.KLST

题目

分析与解:要求边或点不能超过k个数量。给出代码:
//Kruskal实现
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e5+5,M=1e6;
int fa[N],n,m,k;
struct edge{
	int u,v,w;
}e[M];
bool comp(edge a,edge b){
	return a.w>b.w;
}
int cfind(int x){
	return fa[x]==x?x:fa[x]=cfind(fa[x]);
}
void cmerge(int x,int y){
	fa[cfind(x)]=cfind(y);
}
int kruskal()
{
	int tot=0;
	for(int i=1;i<=n;i++) fa[i]=i;
	sort(e+1,e+m+1,comp);
	for(int i=1,cnt=0;i<=m&&cnt<k;i++)//k条边的最大生成树
		if(cfind(e[i].u)!=cfind(e[i].v))
		{
			cmerge(e[i].u,e[i].v);
			tot+=e[i].w;
			cnt++;
		}
	return tot;
}
int main()
{
	cin>>n>>m>>k;
	for(int i=1;i<=m;i++)
		cin>>e[i].u>>e[i].v>>e[i].w;
	cout<<kruskal()<<endl;
	return 0;
}
需要注意的细节:
  • 在Kruskal函数体内排序完成之后,会维护一个 cnt 变量,记录当前的边数是否超过 k 。
  • 如果在循环语句中定义 cnt ,需要注意:cnt 的范围是\left ( -1,k \right ) ,也就是从0至k-1。

2.KMST

题目

分析与解:同KLST,不过是维护一棵最小数。本题维护的对象是边。代码见下:
//Kruskal
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e3+5,M=1e4+5;
int n,m,k,fa[N];
struct edge{int u,v,w;}e[M];
int cfind(int x) { return fa[x]==x?x:fa[x]=cfind(fa[x]); }
void cmerge(int x,int y) { fa[cfind(x)]=cfind(y); }
void kruskal()
{
	int tot=0,cnt=0;
	for(int i=1;i<=n;i++) fa[i]=i;
	sort(e+1,e+m+1,[](const edge &a,const edge &b) { return a.w<b.w; });
	for(int i=1;i<=m&&cnt<n-k;i++)
		if(cfind(e[i].u)!=cfind(e[i].v))
		{
			cmerge(e[i].u,e[i].v);
			cnt++;
			tot+=e[i].w;
		}
	if(cnt==n-k) cout<<tot;
	else cout<<"No Answer";
}
int main()
{
	cin>>n>>m>>k;
	for(int i=1;i<=m;i++) cin>>e[i].u>>e[i].v>>e[i].w;
	kruskal();
	return 0;
}

posted @ 2025-05-25 19:57  枯骨崖烟  阅读(30)  评论(0)    收藏  举报  来源