最小生成树 学习笔记
最小生成树
定义
生成树:一个连通无向图的生成子图,同时要求是树。
最小生成树:边权和最小的生成树。
Kruskal 算法
Kruskal 算法是一种基于贪心的最小生成树算法,好写易懂且复杂度低。
流程很简单,首先对边排序,每次选最小的边,如果没有形成环就加入生成树。
可以这样理解,如果一条边加入生成树后形成环,那么这条边是环上边权最大的,不取这条边为最优。
对边排序可以直接存边,判环可以使用并查集。复杂度一般是 \(O(m\log m)\)。
struct edge{
int u,v;
long long w;
};
bool operator<(const edge &a,const edge &b){
return a.w<b.w;
}
DSU<n+5>a;
long long kruskal(int n,int m,edge e[],long long ans=0,int cnt=0){
a.build(n),sort(e+1,e+m+1);
for(int i=1;i<=m;i++)if(a.merge(e[i].u,e[i].v))ans+=e[i].w,cnt++;
return cnt==n-1?ans:-1;
}
Prim 算法
Prim 算法同样基于贪心,原理是不断选择与当前所选的点距离最近的点加入最小生成树。
流程:首先选 \(1\) 为根,加入生成树,距离设为 \(0\)。之后不断选择距离最小的点加入生成树,然后用这个点更新周围点的距离,直到所有点都加入生成树。
long long dis[n+5];
bool vis[n+5];
long long prim(int n,vector<pair<int,long long>>e[],int cnt=0,long long ans=0){
for(int i=1;i<=n;i++)dis[i]=0x3f3f3f3f3f3f3f3f;
dis[1]=0;
while(1){
int now=0;
for(int i=1;i<=n;i++)if((!vis[i])&&dis[i]<dis[now])now=i;
if(!now)break;
cnt++,vis[now]=1,ans+=dis[now];
for(int i=0;i<e[now].size();i++)dis[e[now][i].first]=min(dis[e[now][i].first],e[now][i].second);
}
return cnt==n?ans:-1;
}
复杂度 \(O(n^2+m)\)。
可以发现,Prim 算法很像最短路中的 Dijkstra 算法。当然其中求距离最小的部分可以用堆优化,每次取队首,更新周围点的距离,然后将更新的距离入队。
时间复杂度 \(O(n+m\log n)\)。
long long dis[n+5];
bool vis[n+5];
priority_queue<pair<long long,int>,vector<pair<long long,int>>,greater<pair<long long,int>>>q;
long long prim(int n,vector<pair<int,long long>>e[],int cnt=0,long long ans=0){
for(int i=1;i<=n;i++)dis[i]=0x3f3f3f3f3f3f3f3f;
dis[1]=0,q.push(make_pair(0,1));
while(!q.empty()){
int now=q.top().second;
q.pop();
if(vis[now])continue;
vis[now]=1,cnt++,ans+=dis[now];
for(int i=0;i<e[now].size();i++){
int v=e[now][i].first;
long long w=e[now][i].second;
if(dis[v]>w)dis[v]=w,q.push(make_pair(dis[v],v));
}
}
return cnt==n?ans:-1;
}
Borůvka 算法
Borůvka 算法是一种冷门的求最小生成树算法,类似于上面两种算法的结合。
Borůvka 算法维护的是联通块,初始时每一个点为一个联通块。
每一次 Borůvka 操作中,枚举每一条边,找出每一个联通块连出其他块的最小边。最小边的比较是双关键字的,第二关键字一般取编号。这是为了防止几个联通块之间形成环,不能出现相等的情况。然后将这些边加入生成树,合并联通块。到无法执行合并时结束算法。复杂度 \(O(m\log n)\)。
int f[n+5],minn[n+5];
bool vis[m+5];
int find(int x){
return f[x]?f[x]=find(f[x],f):x;
}
long long boruvka(int n,int m,edge e[],long long ans=0,int cnt=0){
while(1){
memset(minn,0,sizeof(minn));
for(int i=1;i<=m;i++){
if(vis[i])continue;
int f1=find(e[i].u,f),f2=find(e[i].v,f);
if(f1==f2)continue;
if(!minn[f1]||e[i].w<e[minn[f1]].w||(e[i].w==e[minn[f1]].w&&i<minn[f1]))minn[f1]=i;
if(!minn[f2]||e[i].w<e[minn[f2]].w||(e[i].w==e[minn[f2]].w&&i<minn[f2]))minn[f2]=i;
}
bool t=0;
for(int i=1;i<=n;i++)if(minn[i]&&!vis[minn[i]])flag=vis[minn[i]]=1,cnt++,ans+=e[minn[i]].w,f[find(e[minn[i]].u,f)]=find(e[minn[i]].v,f);
if(!t)break;
}
return cnt==n-1?ans:-1;
}
这个算法一般在复杂度上优势不大,并且相对难写。Borůvka 的题一般是求完全图的最小生成树,边权通过两个端点计算得到,此时可以通过一些方法快速维护最小边。
次小生成树
非严格次小生成树
次小生成树与最小生成树至少一条边不同。先用 Kruskal 求最小生成树,然后枚举所有不在最小生成树上的边。将这条边加入最小生成树,就会形成一个环,再断开环上最大的边就得到一颗可能的次小生成树。
设这条边是 \(u\to v\),那么断开的边属于最小生成树上 \(u\) 到 \(v\) 的路径。路径上最大边权可以树上倍增或树剖维护。
严格次小生成树
当替换的边与断开的边相等,得到的生成树与最小生成树的边权和相等。额外维护路径上的严格次大边权,当替换的边与断开的边相等时用严格次大边权替换即可。
#include<bits/stdc++.h>
using namespace std;
struct edge{
int u,v;
long long w;
}e[300005];
bool operator<(const edge &a,const edge &b){
return a.w<b.w;
}
template<unsigned int maxn>struct DSU{
int f[maxn],s[maxn];
void build(int n){
for(int i=1;i<=n;i++)s[i]=1;
}
int find(int x)
{
return f[x]?find(f[x]):x;
}
bool merge(int x,int y){
int f1=find(x),f2=find(y);
if(f1==f2)return 0;
if(s[f1]<s[f2])f[f1]=f2,s[f2]+=s[f1];
else f[f2]=f1,s[f1]+=s[f2];
return 1;
}
};
int n,m,d[100005],lg[100005];
long long sum,ans=0x1f3f3f3f3f3f3f3f,f[100005][20],maxn1[100005][20],maxn2[100005][20];
DSU<100005>a;
vector<pair<int,long long>>tr[100005];
bool vis[300005];
long long kruskal(int n,int m,edge e[],long long ans=0,int cnt=0){
a.build(n),sort(e+1,e+m+1);
for(int i=1;i<=m;i++)if(a.merge(e[i].u,e[i].v))vis[i]=1,tr[e[i].u].push_back(make_pair(e[i].v,e[i].w)),tr[e[i].v].push_back(make_pair(e[i].u,e[i].w)),ans+=e[i].w,cnt++;
return cnt==n-1?ans:-1;
}
void dfs(int pos,int fa,vector<pair<int,long long>>e[]){
f[pos][0]=fa,d[pos]=d[fa]+1,maxn2[pos][0]=-0x1f3f3f3f3f3f3f3f;
for(int i=1;i<=lg[d[pos]];i++){
f[pos][i]=f[f[pos][i-1]][i-1],maxn1[pos][i]=max(maxn1[pos][i-1],maxn1[f[pos][i-1]][i-1]),maxn2[pos][i]=-0x1f3f3f3f3f3f3f3f;
if(maxn1[pos][i-1]<maxn1[pos][i])maxn2[pos][i]=max(maxn2[pos][i],maxn1[pos][i-1]);
if(maxn1[f[pos][i-1]][i-1]<maxn1[pos][i])maxn2[pos][i]=max(maxn2[pos][i],maxn1[f[pos][i-1]][i-1]);
if(maxn2[pos][i-1]<maxn1[pos][i])maxn2[pos][i]=max(maxn2[pos][i],maxn2[pos][i-1]);
if(maxn2[f[pos][i-1]][i-1]<maxn1[pos][i])maxn2[pos][i]=max(maxn2[pos][i],maxn2[f[pos][i-1]][i-1]);
}
for(int i=0;i<e[pos].size();i++)if(e[pos][i].first!=fa)maxn1[e[pos][i].first][0]=e[pos][i].second,dfs(e[pos][i].first,pos,e);
}
long long query(int u,int v,long long w,long long ans=-0x1f3f3f3f3f3f3f3f){
if(d[u]<d[v])swap(u,v);
while(d[u]>d[v])ans=max(ans,maxn1[u][lg[d[u]-d[v]]-1]==w?maxn2[u][lg[d[u]-d[v]]-1]:maxn1[u][lg[d[u]-d[v]]-1]),u=f[u][lg[d[u]-d[v]]-1];
if(u==v)return ans;
for(int i=lg[d[u]]-1;i>=0;i--){
if(f[u][i]!=f[v][i]){
ans=max(ans,maxn1[u][i]==w?maxn2[u][i]:maxn1[u][i]);
ans=max(ans,maxn1[v][i]==w?maxn2[v][i]:maxn1[v][i]);
u=f[u][i],v=f[v][i];
}
}
ans=max(ans,maxn1[u][0]==w?maxn2[u][0]:maxn1[u][0]);
ans=max(ans,maxn1[v][0]==w?maxn2[v][0]:maxn1[v][0]);
return ans;
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++)cin>>e[i].u>>e[i].v>>e[i].w;
for(int i=1;i<=n;i++)lg[i]=lg[i-1]+(1<<lg[i-1]==i);
sum=kruskal(n,m,e),dfs(1,0,tr);
for(int i=1;i<=m;i++){
if(vis[i]||e[i].u==e[i].v)continue;
ans=min(ans,sum-query(e[i].u,e[i].v,e[i].w)+e[i].w);
}
return cout<<ans<<'\n',0;
}
最小树形图
最小树形图(Directed Minimum Spanning Tree)可以看做有向图的最小生成树。这里的树形图为叶向树形图,如果边都是无向的,树形图就会变成一棵有根树,而且每条边都从父亲指向儿子。
朱-刘-Edmonds 算法
一个定义:最短弧指一个节点的最短入边。
在树形图中根以外每个节点都有且仅有一条入边。如果取每条最短弧能组成树形图则为最优。
如果形成一个环,先把这个环加进答案,然后缩成一个点,删掉形成的自环。
此时如果再取这个环的入边,就要把环上这个点的入边反悔掉。设这条入边连向点 \(v\),应把边权设为 \(w-minn_v\),其中 \(minn\) 为最短弧长。缩完所有环后继续迭代直到没有环。
long long minn[n+5];
int cnt,p[n+5],num[n+5],vis[n+5];
long long dmst(int n,int m,int s,edge e[],long long ans=0){
while(1){
cnt=0;
for(int i=1;i<=n;i++)minn[i]=0x3f3f3f3f3f3f3f3f,vis[i]=num[i]=0;
for(int i=1;i<=m;i++)if(e[i].u!=e[i].v&&e[i].w<minn[e[i].v])minn[e[i].v]=e[i].w,p[e[i].v]=e[i].u;
for(int i=1,t;i<=n;i++){
if(i==s)continue;
if(minn[i]==0x3f3f3f3f3f3f3f3f)return -1;
ans+=minn[i],t=i;
while(vis[t]!=i&&!num[t]&&t!=s)vis[t]=i,t=p[t];
if(!num[t]&&t!=s){
num[t]=++cnt;
for(int j=p[t];j!=t;j=p[j])num[j]=cnt;
}
}
if(!cnt)break;
for(int i=1;i<=n;i++)if(!num[i])num[i]=++cnt;
for(int i=1;i<=m;i++)e[i].w-=minn[e[i].v],e[i].u=num[e[i].u],e[i].v=num[e[i].v];
n=cnt,s=num[s];
}
return ans;
}
每次迭代至少缩一个点,最多迭代 \(n\) 次,复杂度 \(O(nm)\)。
堆优化
改变一下上述算法的过程:沿着入边的相反方向深搜遍历整张图,维护一个栈,如果遇到返祖边就直接缩点并修改边权。复杂度不变。
这时需要一个方法快速维护最短弧。这需要支持取最短弧,删除最短弧,所有入边的边权减一个数,合并。这是可并堆的经典操作,左偏树维护即可。此外还需要一个并查集维护每个缩成的点包含哪些原来的点。
判断无解可以先加入一个 \(1\sim n\) 的环,边权为一个极大值。如果跑出来的答案大于极大值说明无解。复杂度 \(O(m+n\log n)\)。
struct node{
int u,v,d,ls,rs;
long long w,tag;
};
template<int maxn>struct leftist_tree{
node h[maxn];
int cnt;
void pushdown(int pos){
h[h[pos].ls].tag+=h[pos].tag,h[h[pos].ls].w+=h[pos].tag;
h[h[pos].rs].tag+=h[pos].tag,h[h[pos].rs].w+=h[pos].tag;
h[pos].tag=0;
}
int merge(int a,int b){
if(!a||!b)return a^b;
if(h[a].w>h[b].w)swap(a,b);
pushdown(a),h[a].rs=merge(h[a].rs,b);
if(h[h[a].ls].d<h[h[a].rs].d)swap(h[a].ls,h[a].rs);
return h[a].d=h[h[a].rs].d+1,a;
}
void push(int &rt,int u,int v,long long w){
h[++cnt].u=u,h[cnt].v=v,h[cnt].w=w,rt=merge(rt,cnt);
}
void pop(int &rt){
pushdown(rt),rt=merge(h[rt].ls,h[rt].rs);
}
};
leftist_tree<m+n*2+5>t;
int f[n*2+5],rt[n*2+5],st[n+5],top;
bool vis[n*2+5];
int find(int x){
return f[x]?f[x]=find(f[x]):x;
}
long long dmst(int n,int m,int s,edge e[],long long ans=0){
for(int i=1;i<=m;i++)t.push(rt[e[i].v],e[i].u,e[i].v,e[i].w);
for(int i=1;i<=n;i++)t.push(rt[i==n?1:i+1],i,i==n?1:i+1,0x3f3f3f3f);
st[++top]=s,vis[s]=1;
while(rt[st[top]]){
int now=find(t.h[rt[st[top]]].u);
if(now==st[top]){
t.pop(rt[now]);
continue;
}
if(!vis[now])st[++top]=now,vis[now]=1;
else{
int p=++n;
while(vis[now]){
int temp=st[top--];
vis[temp]=0,f[temp]=p,t.h[rt[temp]].tag-=t.h[rt[temp]].w;
if(find(t.h[rt[temp]].v)!=find(s))ans+=t.h[rt[temp]].w;
t.pop(rt[temp]),rt[p]=t.merge(rt[p],rt[temp]);
}
st[++top]=p,vis[p]=1;
}
}
return ans>=0x3f3f3f3f?-1:ans;
}
[[图论]]