最小生成树学习笔记
定义
最小生成树是指给定一个带权连通图 G,如果里面有一个子图 G' 中的边权和加起来最小并且使得所有的点都能两两相通。
性质
从上述的定义可以看出,最小生成树有以下性质:
-
如果图 G 中有 n 个点的话,G'中的边数为 n-1 且 G' 中不含有环。
-
最小生成树可能是一个,也可能是多个。
还有一些复杂的性质感兴趣的可以自行百度。
kruskal 算法
前置知识:并查集。
kruskal 算法的基本思想就是贪心,该算法的流程也很简单。
首先我们需要将所有的边进行排序,然后枚举每一条边,如果当前的边链接的两个点早已经间接或直接相连,我们就直接跳过,不连这条边,如果连满了 n-1 条边就直接退出循环,然后我们所选的边就构成了一棵最小生成树。
在判断当前边的两点是否已经相连的办法可以用并查集来判断,也是比较方便的做法。
根据其定义可以想到,我们要贪心的话,就得先加入边权小的边,假设只有两个点,那他的最小生成树就是两点之间边权最小的边构成,当到了三个点之后,就是从前面已经链接的所有边中覆盖的点与第三个点相连的边中挑一条边权最小的边来连,因为如果要是想要把第三个点连进去,就必须要与之前的联通图中的任意一点连一条边,经过这样不断的扩大,最终会以最小的代价连进所有点。在算法流程中,其当前连接的边是必定有一个点是在联通图中,必有一个点未连进联通图中,而当前边又是剩下的边中边权最小的,所以一定是原图中一棵最小生成树的边。
以上不理解也没关系反正是我胡扯的背板子就好了
P3366 【模板】最小生成树
code:
#include<bits/stdc++.h>
#define int long long
#define N 1000100
using namespace std;
int n,m,f[N],num,ans;
struct sb{int x,y,w;}e[N];
inline int cmp(sb a,sb b){return a.w<b.w;}
inline int fid(int x){if(x==f[x])return x;else return f[x]=fid(f[x]);}
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)f[i]=i;
for(int i=1;i<=m;i++)cin>>e[i].x>>e[i].y>>e[i].w;
sort(e+1,e+m+1,cmp);
for(int i=1;i<=m;i++)
{
int xx=fid(e[i].x);
int yy=fid(e[i].y);
if(xx==yy)continue;
f[xx]=yy;
ans+=e[i].w;
num++;
if(num==n-1)break;
}
if(num==n-1)cout<<ans<<endl;
else cout<<"orz"<<endl;
return 0;
}
1487:【例 2】北极通讯网络
题目说的有点绕,但仔细读完就会发现意思是让你找一棵最小生成树,其中有 k 个点是不必连入的,也就是等同于让你连 n-k 条边。
code:
#include<bits/stdc++.h>
#define int long long
#define N 1000100
using namespace std;
double ans;
int n,k,fa[N],num,m;
struct sb{int x,y;}e1[N];
struct SB{int u,v;double w;}e[N<<5];
inline int cmp(SB a,SB b){return a.w<b.w;}
inline int fid(int x){if(fa[x]==x)return x;return fa[x]=fid(fa[x]);}
inline double js(sb u,sb v){return sqrt(pow(u.x-v.x,2)+pow(u.y-v.y,2));}
signed main()
{
cin>>n>>k;
for(int i=1;i<=n;i++)fa[i]=i,cin>>e1[i].x>>e1[i].y;
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j++)
e[++m].u=i,e[m].v=j,e[m].w=js(e1[i],e1[j]);
sort(e+1,e+m+1,cmp);
for(int i=1;i<=m;i++)
{
int xx=fid(e[i].u);
int yy=fid(e[i].v);
if(xx==yy)continue;
fa[xx]=yy;num++;
ans=max(e[i].w,ans);
if(num==n-k)break;
}
printf("%.2lf\n",ans);
return 0;
}
1488:新的开始
题目中如果要是是只有一个固定的发电站的话,那就是纯板子了,但是这个也不难,我们考虑一下如果只连边的话,这个电网里面是没有电的,所以我们需要开一个点 n+1 来存放发电站,然后把题目中的在当前点建发电站转化成当前点直接和发电站连边,也就是从 n+1 向每一个点连一条边边权为 v,这样就相当于连 n 条边。
code:
#include<bits/stdc++.h>
#define int long long
#define N 1000100
using namespace std;
int n,m,fa[N],mp[500][500],val[N],num,ans;
struct sb{int u,v,w;}e[N];
inline int cmp(sb a,sb b){return a.w<b.w;}
inline int fid(int x){if(fa[x]==x)return x;return fa[x]=fid(fa[x]);}
signed main()
{
cin>>n;
for(int i=1;i<=n+1;i++)fa[i]=i;
for(int i=1;i<=n;i++)cin>>val[i],e[++m].u=n+1,e[m].v=i,e[m].w=val[i];
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>mp[i][j],e[++m].u=i,e[m].v=j,e[m].w=mp[i][j];
sort(e+1,e+m+1,cmp);
for(int i=1;i<=m;i++)
{
int xx=fid(e[i].u);
int yy=fid(e[i].v);
if(xx==yy)continue;
fa[xx]=yy;
num++;
ans+=e[i].w;
if(num==n)break ;
}
cout<<ans<<endl;
return 0;
}
prim 算法
由于 prim 本人不是很熟练此部分参考 https://www.cnblogs.com/bcoier/p/10293059.html
我看到里面有 kruskal 的流程图所以也粘过来了
这个算法的流程本质也是贪心,首先需要我们选定任意一个节点作为根节点,然后往下开始扩展,在每一次扩展的过程中都选用待选边(链接 u,v)中权值最小的边进行扩展,然后将除此边外与 v 相连的所有边都放入待选边,如果此时加入的边与 v 相连的点中有的点实际上是已经有待定边相连了,就更新最短路,也就是取个 min,让选取的边权和尽可能的小。
可以看看上面博客的图:
P3366 【模板】最小生成树
没错就是上面那道,结合注释和图理解一下 prim 的流程。
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define int long long
#define N 1000100
using namespace std;
int n,m,head[N],dis[N],cnt,num,now=1,ans,vis[N];
//dis表示从已经在连通块里的点到当前点的最短距离,vis标记是否在连通图内,now是当前节点
struct sb{int u,v,w,next;}e[N];
inline void add(int u,int v,int w)//链式前向星存图
{
e[++cnt].u=u;
e[cnt].v=v;
e[cnt].w=w;
e[cnt].next=head[u];
head[u]=cnt;
}
signed 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);
}
for(int i=2;i<=n;i++)
dis[i]=INF;//dis一开始都设极大值,后面要取min
for(int i=head[1];i;i=e[i].next)//枚举与起点相连的边
dis[e[i].v]=min(dis[e[i].v],e[i].w);//更新dis,取min
while(1)//只要没完成就一直找
{
int minn=INF;//存放当前的最小的没在连通图里的dis的值
vis[now]=1;//当前点加入连通图,now存下标
for(int i=1;i<=n;i++)//枚举n个点
if(!vis[i]&&minn>dis[i])//找最小的不在连通图的dis
now=i,minn=dis[i];//存值
ans+=minn;//加入当前边
for(int i=head[now];i;i=e[i].next)//枚举每一个与之相连的边
if(dis[e[i].v]>e[i].w&&!vis[e[i].v])//更新dis,除在连通图的点之外
dis[e[i].v]=e[i].w;
num++;//加边数+1
if(num==n-1||minn==INF)break;//如果到n-1条边了或者minn没值了
}
if(num==n-1)cout<<ans<<endl;//输出答案
else cout<<"orz"<<endl;
return 0;
}
由于 prim 不如 kruskal 好写所以我只写了一个模板。
关于他俩谁快的问题,稠密图 prim 快一点,稀疏图 kruskal 快一点。
严格次小生成树
本来这个是不打算写的了,但是想了想还是来一发吧。
看完题目发现要求的不是最小生成树,而是一个新名词:严格次小生成树,也就是边权和严格第二小的生成树。
首先题目说的是严格次小,也就是说,如果换了一条边,但是边权和没变,这种是不算严格次小生成树的。
\(mst\) 表示当前最小生成树的边权和, \(Mst\) 表示严格次小生成树的边权和,\(maxUV\) 表示 u 到 v 的路径上的最大边权,\(MaxUV\) 表示从 u 到 v 的路径上的次大的边权。
所以我们得到两种情况:
-
当前要加入的边 \(e\ne maxUV\),那么得到一个 \(Mst\) 的侯选值 \(mst-maxUV+e\)
-
当前要加入的边 \(e = maxUV\),那么得到一个 \(Mst\) 的侯选值 \(mst-MaxUV+e\)
然后问题就转化为了如何求 \(maxUV\) 和 \(MaxUV\)。
我们可以很容易想到暴力会 T 飞,所以我们选用倍增来解决,具体就是维护两个倍增数组 \(max1[i][j]\) 存放 i 点向上跳 \(2^{j}\) 步以后到的点之间的最大边权,\(max2[i][j]\) 存放 i 点向上跳 \(2^{j}\) 步以后到的点之间的次大边权,具体看下面代码的注释。
梳理一下流程:
-
先跑一边最小生成树
-
预处理出倍增数组 \(max1\),\(max2\)。
-
倍增处理当前要加入的边 e 的起始点在最小生成树上的最大边权。
最后对所有的待选值取个 min 即可。
code:
#include<bits/stdc++.h>
#define int long long
#define INF (1ll<<62)
#define M 1000100
#define N 400010
using namespace std;
struct sb{int u,v,w,bt;}e[M];
int dep[N],f[N][21],max1[N][21],max2[N][21];
int n,m,mst,tot,head[M<<1],nxt[M<<1],v[M<<1],w[M<<1],fa[N];
inline int cmp(sb a,sb b){return a.w<b.w;}
inline int fid(int x){return fa[x]==x?x:fa[x]=fid(fa[x]);}
inline void add(int u,int v1,int w1){nxt[++tot]=head[u],head[u]=tot,v[tot]=v1,w[tot]=w1;}
inline int read(){int x=0,f=1;char ch=getchar();while(!isdigit(ch)){f=ch!='-';ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return f?x:-x;}
inline void kruskal()//最小生成树板子,不会请退役
{
for(int i=1;i<=n;i++)fa[i]=i;
sort(e+1,e+1+m,cmp);
for(int i=1;i<=m;i++)
{
int u=e[i].u,v=e[i].v,w=e[i].w;
if(fid(u)==fid(v))continue;
mst+=w;
e[i].bt=1;
add(u,v,w);
add(v,u,w);
fa[fid(u)]=fid(v);
}
}
inline void dfs(int x)//dfs预处理
{
for(int i=1;i<=18;i++)//处理倍增数组
{
f[x][i]=f[f[x][i-1]][i-1];//处理倍增的父亲节点
max1[x][i]=max(max1[x][i-1],max1[f[x][i-1]][i-1]);//处理最大值,直接对两个区间取max
max2[x][i]=max(max2[x][i-1],max2[f[x][i-1]][i-1]);//先和上面一样取max
if(max1[x][i-1]!=max1[f[x][i-1]][i-1])//如果要是最大值的两个区间内的最大值不同
max2[x][i]=max(max2[x][i],min(max1[x][i-1],max1[f[x][i-1]][i-1]));//从两个最大值里选个次小的
}
for(int i=head[x];i;i=nxt[i])//遍历与当前点相连的边
{
if(dep[v[i]])continue;//走过了就算了
f[v[i]][0]=x;//标记父节点
max1[v[i]][0]=w[i];//标记最大边权
max2[v[i]][0]=-INF;//标记次大边权
dep[v[i]]=dep[x]+1;//计算深度
dfs(v[i]);//继续搜
}
}
inline int LCA(int x,int y)//倍增求lca,不会请退役
{
if(dep[x]<dep[y])swap(x,y);
for(int i=18;i>=0;i--)
if(dep[f[x][i]]>=dep[y])x=f[x][i];
if(x==y)return x;
for(int i=18;i>=0;i--)
if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
return f[x][0];
}
inline int qmax(int x,int y,int z)//求x到y的边权最大值,z是要加进去的边权
{
int minn=-INF;//先设个最小值后面取min(设不设都行其实
for(int i=18;i>=0;i--)//倍增往上跳
{
if(dep[f[x][i]]>=dep[y])//如果跳的时候不会超过就更新
{
if(z!=max1[x][i])minn=max(minn,max1[x][i]);//情况1最大边权不等于z
else minn=max(minn,max2[x][i]);//情况二 最大边权等于z
x=f[x][i];//更新点的编号
}
}
return minn;//返回查到的需要替换的边权
}
signed main()
{
n=read(),m=read();
for(int i=1;i<=m;i++)e[i].u=read(),e[i].v=read(),e[i].w=read();//输入边的信息
kruskal();//跑一边最小生成树
dep[1]=1;//赋初值第一个点深度为1
dfs(1);//从1开始往下搜
int Mst=INF;//存放次小生成树的值
for(int i=1;i<=m;i++)
{
int u=e[i].u,v=e[i].v,w=e[i].w;//起点终点边权
if(e[i].bt)continue;//如果是树边的话就跳过
int lca=LCA(u,v);//求LCA
int maxx=qmax(u,lca,w);//求出u到lca的边权最大值
int maxy=qmax(v,lca,w);//求出v到lca的边权最大值
Mst=min(Mst,mst-max(maxx,maxy)+w);//对于每一个去掉边权最大值取当前边权的方案取min
}
cout<<Mst<<endl;//输出答案
return 0;
}