万里之行,始于足下。本博客总结暑期学习到的图论模板第三部分,以便于日后查询使用。作者水平有限,难免存在疏漏不足,恳请诸位看官斧正。倘若我的文章可以帮到你,十分荣幸。感谢19级学长lsw提供的灵感。本篇博客主要讲生成树相关问题。
更新日志:
1.于2021.9.23对Kruscal算法代码部分进行了可读性的优化。
目录
1.关于生成树的前驱知识
1.何谓“生成树”?
关于生成树我们要把“生成“和”树“分开来理解。生成即为生成子图,定义:若图G的一个子图G’包含G的所有顶点,则称G’为G的一个生成子图。当然,G’的边集是G的子集了。在图论中,树就是n个结点所能形成的最小的连通图,一共有n-1条边。而生成树就是指图G的一个为树的生成子图。
2.补充:树的一些等价定义
•无向无环的连通图
•任意两个结点之间有且仅有一条简单路径的无向图
•任何边均为桥的连通图
•没有圈,且在任意不同两点间添加一条边之后所得图含唯一的一个圈的图
3.最小生成树
在无向连通图G中我们找到一个生成树T,使得T是权值和最小的生成树,那么我们称T为MST,即最小生成树。接下来着重介绍MST的两种求法。
2.Kruscal算法
一开始认定每一个点都为独立的集合。按照边权大小检查每一条边,找到代价最小的边,如果该边连接连个不同的集合,将边加入MST,合并两个集合。该算法的时间复杂度为O(eloge)(e为网中的边数)。如何合并集合?这里就需要我们之前讲过的并查集相关的知识了。
并查集相关内容传送门:C++算法学习模板收集——图论篇(1)
上例题:P3366 【模板】最小生成树
#include <bits/stdc++.h>
#define re register int//宏定义,设置加速循环
using namespace std;
const int maxm=2e5+7,maxn=5007;
int n,m,f[maxn],sum=0,ans=0;
struct Edge
{
int from,to,w;
}edge[maxm*2];
bool cmp(const Edge &a,const Edge &b)
{
return a.w<b.w;
}//cmp函数用于排序边的权值
int fi(int n)
{
if(f[n]!=n)
{
return fi(f[n]);
}
return n;
}
inline void mer(int i,int j)
{
f[fi(i)]=fi(j);
}
void init()
{
for(int i=1;i<=n;i++)
{
f[i]=i;
}
}//以上为并查集相关内容
int main()
{
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(re i=1;i<=m;i++)
{
cin>>edge[i].from>>edge[i].to>>edge[i].w;
}//存图
init();
sort(edge+1,edge+1+m,cmp);//边的排序
for(re i=1;i<=m;i++)//边的代价从小到大找喽
{
if(fi(edge[i].to)!=fi(edge[i].from))//合并操作
{
mer(edge[i].to,edge[i].from);
sum++;//计数
ans+=edge[i].w;
if(sum==n-1)
{
cout<<ans;
return 0;
}//MST有n-1条边
}
}
cout<<"orz";
return 0;
}
3.Prim算法
例题:P3366 【模板】最小生成树 没错,虽然不是同一算法,但是是同一地点
#include <bits/stdc++.h>
#define re register int
using namespace std;
typedef pair <int,int> p;
const int maxn=5005,maxm=2e5+7;
int n,m,cnt,ans,sum;
int dis[maxn],head[maxn],vis[maxn];
priority_queue<p,vector<p>,greater<p> > Q;//还是辣个优先队列优化
struct edge
{
int v,w,next;
}e[maxm*2];//结构体存边
inline void addedge(int u,int v,int w)
{
e[++cnt].v=v;
e[cnt].w=w;
e[cnt].next=head[u];
head[u]=cnt;
}//链式前向星存图
void prim()
{
dis[1]=0;
Q.push(make_pair(0,1));//初始化
while(!Q.empty()&&sum<n)
{
int d=Q.top().first,u=Q.top().second;
Q.pop();
if(vis[u])
{
continue;
}
sum++;//计数
ans+=d;
vis[u]=1;//将点及其连通的加入MST中
for(re i=head[u];i!=-1;i=e[i].next)//链式前向星的遍历
{
if(e[i].w<dis[e[i].v])
{
dis[e[i].v]=e[i].w;
Q.push(make_pair(dis[e[i].v],e[i].v));
}//更新最小边权并将其压入优先队列
}
}
}//该算法实现可以对照Dijkstra算法
int main()
{
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
memset(dis,114514,sizeof(dis));
memset(head,-1,sizeof(head));//你是一个,一个一个一个初始化
cin>>n>>m;
for(re i=1;i<=m;i++)
{
int u,v,w;
cin>>u>>v>>w;
addedge(u,v,w);
addedge(v,u,w);
}//存图
prim();//执行算法
if(sum==n)//n个点都在MST里了
{
cout<<ans;
}
else
{
cout<<"orz";
}
return 0;
}
4.Prim算法和Kruscal算法的联系
与Kruscal算法不断加边不同,Prim算法的关注点是加点。需要注意一点,如果使用优先队列优化的Prim时间复杂度并不会优于Kruscal。原理是堆的实现方式不同。因为Prim算法是找点,只有暴力实现的Prim算法会在稠密图上可能优于Kruscal算法。
5.生成树的衍生问题
这一部分我还在学习,挖个坑先,争取早日来填
1.增量最小生成树
从包含n个点的空图开始,依次加入m条带权边。每加入一条边。输出当前图中MST权值。
2.最小瓶颈树
无向图G的瓶颈生成树是这样的一个生成树,它的最大的边权值在G的所有生成树中最小。何解,二分?最小生成树?亦或最短路?
下节预告:下一次图论博客将介绍常见的图存储方法。包括邻接矩阵、链式前向星和邻接表。
“山再高,往上攀,总能登顶;路再长,走下去,定能到达。”路漫漫其修远兮,吾将上下而求索,共勉!