最小生成树 学习笔记

最小生成树

定义

生成树:一个连通无向图的生成子图,同时要求是树。

最小生成树:边权和最小的生成树。

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;
}

[[图论]]

posted @ 2024-03-01 09:39  lgh_2009  阅读(2)  评论(0编辑  收藏  举报