MST,Kruskal与Boruvka,题解

\(MST\),最小生成树

对于一个无向图,每条边有边权,最小生成树即为生成树中边权最小的。

\(Kruskal\) 算法

用一个小根堆存储所有边,每次尝试加入堆顶的边。
时间复杂度 \(O(m\space\log m)\)
时间复杂度主要在排序上,可以使用并查集维护当前所有点组成的森林

\(Boruvka\) 算法

根据 \(Kruskal\) 算法,一个点所连的权值最小的边一定在生成树上。
我们可以遍历所有的点,将权值最小的非内点连上。
又每次遍历,连通块的个数至少减半,于是复杂度为 \(O(m\space\log n)\) (在查询最小边复杂度为\(O(1)\)时)
\(Boruvka\) 在处理稠密图的时候一般优于 \(Kruskal\)

例题

CF888G

你谷
题面:
\(n\) 个点,每个点 \(u\) 有个权值 \(a_u\)\(u,v\) 两点之间的距离为 \(a_u \oplus a_v\)\(MST\) 的权值
\(1 \le n \le 2e5,1 \le a_u < 2^{30},1 \le u,v \le n\)

\(Kruskal\) 做法

首先对所有数建一个 \(trie\) 树,观察 \(trie\) 树,发现恰有 \(n-1\) 个节点有两个儿子,又发现权值最小的边就是叶子结点中 \(LCA\) 最深的两个节点连的边。
优化:我们将数组排个序,这样 \(trie\) 树上的节点对应的区间是连续的,就不需要启发式合并了。

#include<iostream>
#include<algorithm>
#define ll long long
using namespace std;
const int N=2e5+5;
const ll inf=0x3f3f3f3f;
int n,a[N],rt,ch[N<<5][2],cnt,L[N<<5],R[N<<5];
void insert(int &x,int dep,int idx){//----------------插入trie树,也可以写成循环型的
    if(!x)x=++cnt;
    if(!L[x])L[x]=idx;R[x]=idx;
    if(dep<0)return;
    insert(ch[x][a[idx]>>dep&1],dep-1,idx);
}
void insert(int idx){//----------------循环型
    int u=0,c;
    for(int i=30;~i;i--){
        if(!L[u])L[u]=idx;R[u]=idx;
        c=a[idx]>>i&1;
        if(!ch[u][c])ch[u][c]=++cnt;
        u=ch[u][c];
    }
    if(!L[u])L[u]=idx;R[u]=idx;
}
ll query(int x,int val,int dep){//----------------query函数
    if(dep<0)return 0;
    int c=val>>dep&1;
    if(ch[x][c])return query(ch[x][c],val,dep-1);
    return query(ch[x][c^1],val,dep-1)+(1<<dep);
}
ll query(int u,int x,int dep){//----------------循环型query
    int c;ll ret=0;
    for(int i=dep;~i;i--){
        c=x>>i&1;
        if(ch[u][c])u=ch[u][c];
        else u=ch[u][c^1],ret|=(1<<i);
    }
    return ret;
}
ll dfs(int x,int dep){
    if(dep<0)return 0;
    if(ch[x][0]&&ch[x][1]){
        ll ans=inf;
        for(int i=L[ch[x][0]];i<=R[ch[x][0]];i++){
            ans=min(ans,query(ch[x][1],a[i],dep-1)+(1<<dep));
        }
        return dfs(ch[x][0],dep-1)+dfs(ch[x][1],dep-1)+ans;
    }
    else if(ch[x][0])return dfs(ch[x][0],dep-1);
    else if(ch[x][1])return dfs(ch[x][1],dep-1);
    return 0;
}
int main(){
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    sort(a+1,a+1+n);n=unique(a+1,a+1+n)-a-1;
    for(int i=1;i<=n;i++)insert(rt,30,i);//----------------递归型
    for(int i=1;i<=n;i++)insert(i);//----------------循环型
    cout<<dfs(rt,30)<<endl;//----------------递归型
    cout<<dfs(0,30)<<endl;//----------------循环型
    return 0;
}

\(Boruvka\) 做法

每个连通块找出权值最小的边,若无边可连,则结束。
树上启发式合并。

#include<iostream>
#include<algorithm>
#include<cstring>
#define pr pair<int,int>
#define mp make_pair
#define ll long long
using namespace std;
const int N=2e5+5;
const int inf=0x3f3f3f3f;
int n,a[N],rt[N],fa[N],cnt,ch[N*50][2],sz[N*50],tail[N*50],mn[N];
ll ans,minn[N];
inline int getfa(int u){return fa[u]==u?fa[u]:fa[u]=getfa(fa[u]);}
inline void insert(int &Rt,int s,int idx){
    if(!Rt)Rt=++cnt;
    int u=Rt,c;
    ++sz[u];
    for(int i=30;i>=0;i--){
        c=s>>i&1;
        if(!ch[u][c])ch[u][c]=++cnt;
        ++sz[u=ch[u][c]];
    }
    tail[u]=idx;
}
inline int merge(int &p,int q){
    if(!p||!q)return p=p+q;
    merge(ch[p][0],ch[q][0]);merge(ch[p][1],ch[q][1]);
    sz[p]=sz[ch[p][0]]+sz[ch[p][1]];tail[p]=tail[q];
}
inline pr query(int p,int q,int s){
    int ret=0;
    for(int i=30;i>=0;i--){
        int c=s>>i&1;
        if(ch[p][c]&&sz[ch[p][c]]-sz[ch[q][c]]>0)p=ch[p][c],q=ch[q][c];
        else p=ch[p][c^1],q=ch[q][c^1],ret|=1<<i;
    }
    return mp(ret,tail[p]);
}
void dfs(int u,int dep,int tmp){
    if(dep<0){
        cout<<tmp<<' '<<tail[u]<<endl;
        return;
    }
    for(int i=0;i<2;i++)if(ch[u][i])dfs(ch[u][i],dep-1,tmp+(i?(i<<dep):0));
}
int main(){
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    sort(a+1,a+1+n);n=unique(a+1,a+1+n)-a-1;
    for(int i=1;i<=n;i++){insert(rt[0],a[i],i);insert(rt[i],a[i],i);fa[i]=i;}
    while(1){
        memset(minn,0x3f,sizeof minn);
        bool flag=1;
        for(int i=1;i<=n;i++){
            int x=getfa(i),y,w;
            pr now=query(rt[0],rt[x],a[i]);
            y=getfa(now.second),w=now.first;
            if(x^y){
                if(w<minn[x])minn[x]=w,mn[x]=y;
                if(w<minn[y])minn[y]=w,mn[y]=x;
            }
        }
        for(int i=1;i<=n;i++){
            int x=getfa(i),y=getfa(mn[i]);
            if(minn[x]<inf&&x!=y)ans+=minn[i],merge(rt[x],rt[y]),fa[y]=x,flag=0;
        }
        if(flag)break;
    }
    cout<<ans<<endl;
    return 0;
}

GOJT1026好图

240726比赛题目。
比赛通过傅氏优化卡过了这题,实际上只有80分,现在想到了正解。
题意:
给定一张 \(n\) 个点 , \(m\) 条边的简单无向图 \(G\) ,其边权各不相同。
现将其补全为完全图,共有 \(M=\frac{n*(n-1)}2\) 条边,其边权 \(\in[1,M]\) ,且各不相同。
定义存在一种方案能使原图 \(MST\) 不变的图为好图,求 \(G\) 是否为好图。
\(n,m \le 2e5\) ,时间限制 \(1000ms\)
想法:
使用 \(Kruskal\) 生成最小生成树,同时求解是否为好图。
定义:

  • \(siz\) 为当前所有连通块能够容纳的最大边条数。
  • \(cnt\) 为当前所有连通块已经连的边(\(G\)中的边)的个数。
    则若当前考虑到边 \(v\) ,其为第 \(i\) 小边,若 \(val_v-i>siz-cnt\) 则当前图还有边 \(e\) 满足 \(val_e<val_v\) 未放入图中,则不满足 \(MST\) 的定义,输出 \(No\)
    实现:
#include<iostream>
#include<algorithm>
#include<set>
#define ll long long
using namespace std;
const int N=2e5+5;
int n,m;
struct node{
    int fr,to;ll val;
    bool operator < (const node t)const{return val<t.val;}
}e[N];
multiset<int>E[N];
int fa[N],cnt,sz[N];
ll siz;
int getfa(int u){return fa[u]==u?u:fa[u]=getfa(fa[u]);}
ll calc(int x){return (x-1)*x/2;}
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++)fa[i]=i,sz[i]=1;
    for(int i=1;i<=m;i++)cin>>e[i].fr>>e[i].to>>e[i].val,E[e[i].fr].insert(e[i].to),E[e[i].to].insert(e[i].fr);
    sort(e+1,e+1+m);
    for(int i=1;i<=m;++i){
        if(e[i].val-i>siz-cnt){cout<<"No\n";return 0;}
        int u=getfa(e[i].fr);
        int v=getfa(e[i].to);
        if(u!=v){
            siz+=sz[u]*sz[v];
            if(E[u].size()>E[v].size())swap(u,v);
            for(auto it:E[u])if(getfa(it)!=v)E[v].insert(it);else++cnt;
            sz[v]+=sz[u];
            fa[u]=v;
        }
    }
    cout<<"Yes\n";
    return 0;
}
posted @ 2024-07-22 10:07  Xie2Yue  阅读(28)  评论(0)    收藏  举报