专题:无向图的生成树
概述
生成树是针对无向图而言的。一般地,给定一张无向图,若其任意两个顶点都互相联通并形成了一个树结构,则称这个树为该无向图的生成树。生成树有很多种,例如普通的最小(最大)生成树、 阶最小(最大)生成树、瓶颈树等等。
分类精析
一、普通最小生成树(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的时间复杂度是
,用优先队列优化后是
;
- Prim是以点为单位进行扩展,所以如果判断是否联通或自环,只需维护
变量,每扩展一个点就自增1。如果最后有
个点,说明是联通无环的。
- 思想是对点进行贪心。
//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时间复杂度通常为
,在数据不极端时通常采用这个方法,效率略略低于Prim。
- Kruskal是以边为单位进行扩展,在判断联通或自环时,只需维护
变量,每扩展一条边就自增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函数体内排序完成之后,会维护一个
变量,记录当前的边数是否超过
。
- 如果在循环语句中定义
,需要注意:
的范围是
,也就是从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;
}

浙公网安备 33010602011771号