Kruskal重构树学习笔记

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hwzzyr/article/details/81190442

Kruskal重构树

前几天做noi2018的同步赛时,Day1T1我就拿了离线+树剖的80分暴力。
后来就知道有Kruskal重构树这样一种新科技(14年以前就有了)

什么是Kruskal重构树

简单来讲,就是在Kruskal算法进行的过程中,我们把最小生成树的边权改为点权。
这样,原树的节点个数变成2n-1个,并且有着许多有趣的性质。

Kruskal重构树的性质

1.根据我们构造的过程,这是一个二叉堆(后面再讲构造)
2.原树两点之间的边权最大值是重构树上两点Lca的权值
3.重构树中代表原树中的点的节点全是叶子节点,其余节点都代表了一条边的边权。
有了这些个性质,我们就可以解决一些问题了。

Kruskal重构树的构造

相信大家都会Kruskal
由于重构树中把原树的点权转换成为了新建节点的边权,这一过程是这样实现的。
首先对边排序
然后使用并查集辅助加边,每新建一条边时:
新建节点 i n d e x (编号从 n + 1 开始)
将原有两节点所在集合改为 i n d e x
将原有节点与 i n d e x 连边
新建节点的权值为当前边的边权
给一下简单的代码

void Ex_Kruskal() {
    int ind=n,lim=n<<1; sort(e+1,e+1+m);
    for(int i=1;i<=lim;++i) f[i]=i;
    for(int i=1;i<=m;++i) {
        int fx=getfa(e[i].a),fy=getfa(e[i].b);
        if(fx!=fy) {
            f[fx]=f[fy]=++ind;
            val[ind]=e[i].w;
            add(ind,fx); add(ind,fy);
            if(ind==lim-1) break;
        }
    } return ;
}

代码复杂度很低,时间复杂度是优秀的 O ( n l o g 2 n )

K r u s k a l 重构树的简单应用

Kruskal重构树能够更快有效解决一些静态的树剖问题,而且复杂度还很优秀。
例如:

【BZOJ3732】网络Network
Description
  给你N个点的无向图 (1 <= N <= 15,000),记为:1…N。
  图中有M条边 (1 <= M <= 30,000) ,第j条边的长度为: d_j ( 1 < = d_j < = 1,000,000,000).

  现在有 K个询问 (1 < = K < = 15,000)。
  每个询问的格式是:A B,表示询问从A点走到B点的所有路径中,最长的边最小值是多少?
Input
  第一行: N, M, K。
  第2..M+1行: 三个正整数:X, Y, and D (1 <= X <=N; 1 <= Y <= N). 表示X与Y之间有一条长度为D的边。
  第M+2..M+K+1行: 每行两个整数A B,表示询问从A点走到B点的所有路径中,最长的边最小值是多少?
Output
  对每个询问,输出最长的边最小值是多少。
Sample Input
6 6 8
1 2 5
2 3 4
3 4 3
1 4 8
2 5 7
4 6 2
1 2
1 3
1 4
2 3
2 4
5 1
6 2
6 1
Sample Output
5
5
5
4
4
7
4
5
Hint
1 <= N <= 15,000
1 <= M <= 30,000
1 <= d_j <= 1,000,000,000
1 <= K <= 15,000

我们好像都会求树上的LCA倍增最小值。
但是我们发现Kruskal重构树的做法更加优美,直接求出LCA的点权即可。

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int Maxn=15005,Maxm=30005;
inline int read() {
    static char c; int rec=0;
    while((c=getchar())<'0'||c>'9');
    while(c>='0'&&c<='9') rec=rec*10+c-'0',c=getchar();
    return rec;
}
int n,m,K;
struct Edge {int a,b,w;} e[Maxm];
inline bool operator < (const Edge &A,const Edge &B) {return A.w<B.w;}
int f[Maxn<<1],val[Maxn<<1];
inline int getfa(int x) {return x==f[x]?x:f[x]=getfa(f[x]);}
struct Branch {int next,to;} branch[Maxn<<1];
int h[Maxn<<1],cnt=0;
inline void add(int x,int y) {
    branch[++cnt].to=y; branch[cnt].next=h[x]; h[x]=cnt; return ;
}
void Ex_Kruskal() {
    int ind=n,lim=n<<1; sort(e+1,e+1+m);
    for(int i=1;i<=lim;++i) f[i]=i;
    for(int i=1;i<=m;++i) {
        int fx=getfa(e[i].a),fy=getfa(e[i].b);
        if(fx!=fy) {
            f[fx]=f[fy]=++ind;
            val[ind]=e[i].w;
            add(ind,fx); add(ind,fy);
            if(ind==lim-1) break;
        }
    } return ;
}
int size[Maxn<<1],deep[Maxn<<1];
int top[Maxn<<1],fa[Maxn<<1],son[Maxn<<1];
void Dfs1(int v,int pre,int dep) {
    size[v]=1; fa[v]=pre; deep[v]=dep;
    for(int i=h[v];i;i=branch[i].next) {
        int j=branch[i].to;
        Dfs1(j,v,dep+1); size[v]+=size[j];
        if(size[son[v]]<size[j]) son[v]=j;
    } return ;
}
void Dfs2(int v,int T) {
    top[v]=T; if(son[v]) Dfs2(son[v],T);
    for(int i=h[v];i;i=branch[i].next) {
        int j=branch[i].to;
        if(j^fa[v]&&j^son[v]) Dfs2(j,j); 
    } return ;
}
inline int Ask(int x,int y) {
    while(top[x]!=top[y]) deep[top[x]]<deep[top[y]]?y=fa[top[y]]:x=fa[top[x]];
    return deep[x]<deep[y]?val[x]:val[y];
}
int main() {
    n=read(); m=read(); K=read();
    for(int i=1;i<=m;++i) {
        int a=read(),b=read(),w=read();
        e[i]=(Edge){a,b,w};
    }
    Ex_Kruskal(); Dfs1((n<<1)-1,0,1); Dfs2((n<<1)-1,(n<<1)-1);
    for(int i=1;i<=K;++i) cout<<Ask(read(),read())<<'\n';
    return 0;
}

类似的还有 n o i p 2013   D a y 1 T 3 的货车运输。
当然,Kruskal重构树还能够解决一般树剖不能够解决的问题。
比如这道

路径权值
Description
  给定一个带权树,树上任意两点间的路径权值d(x,y)定义为x,y这两个点之间路径上的最小值,树上任意一点x的权值定义为这个点到树上其他所有点的路径权值和,即 i = 1 n d ( x , i ) ,现求树上一点,使得这个点的权值最大,输出这个值。
Input
  首先输入一个整数Q,接着每组数据首先输入一个整数 n ( 1 n 100000 ) ,表示该组数1据中树的点的个数。
  接下来 n 1 行,每行三个整数 x , y , s ( 1 x , y n , 1 s 1000 ) ,表示编号为x的节点和编号为y的节点之间存在一条权值为s的边,树上每个点的编号为 1   n
Output
  对于每组数据,首先输出数据编号,然后输出树上的点的最大权值,具体格式见输出样例。
Sample Input
2
4
1 2 2
2 4 1
2 3 1
4
1 2 1
2 4 1
2 3 1
Sample Output
Case 1: 4
Case 2: 3

这道题目怎么做呢?
显然,如果我们暴力枚举点对是不可行的。
既然是和树上路径有关的问题,点分治可不可行呢?
本蒟蒻太菜了,根本想不到啊 q w q
那我们试图算一下边的贡献,即一条边对所有经过这一条边的点对都会有这条边长度的贡献。
怎么算呢?
我们之前提到过Kruskal重构树中,两个节点的LCA节点就是两点路径上的最大/最小节点。
也就是说,对于一个非叶节点x,它左子树中的节点到右子树中的节点的路径一定会经过x节点所对应的边,反之亦然。
那么我们就可以建出Kruskal重构树之后维护当前边对于重构树子树中节点的贡献了。
这个区间加法的过程我们可以用树状数组实现。

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int Maxn=100005;
inline int read() {
    static char c; int rec=0;
    while((c=getchar())<'0'||c>'9');
    while(c>='0'&&c<='9') rec=rec*10+c-'0',c=getchar();
    return rec;
}
int T,n;
struct Edge {int a,b,w;} e[Maxn];
inline bool operator < (const Edge &A,const Edge &B) {return A.w>B.w;}
struct Branch {int next,to;} branch[Maxn<<1];
int h[Maxn<<1],cnt=0;
inline void add(int x,int y) {
    branch[++cnt].to=y; branch[cnt].next=h[x]; h[x]=cnt; return ;
}
int fa[Maxn<<1],val[Maxn<<1];
inline int getfa(int x) {return x==fa[x]?x:fa[x]=getfa(fa[x]);}
inline void Ex_Kruskal() {
    int ind=n,lim=n<<1; sort(e+1,e+n);
    for(int i=1;i<lim;++i) fa[i]=i;
    for(int i=1;i<n;++i) {
        int fx=getfa(e[i].a),fy=getfa(e[i].b);
        fa[fx]=fa[fy]=++ind;
        val[ind]=e[i].w;
        add(ind,fx); add(ind,fy);
    } return ;
}
int size[Maxn<<1],st[Maxn<<1],ed[Maxn<<1],idx;
inline void Dfs(int v) {
    size[v]=v<=n; st[v]=++idx;
    for(int i=h[v];i;i=branch[i].next)
        Dfs(branch[i].to),size[v]+=size[branch[i].to];
    ed[v]=idx;
}
struct Bit {
    int c[Maxn<<1];
    inline void reset(){memset(c,0,n<<3); return ;}
    inline void Insert(int x,int d) {while(x<=n<<1) c[x]+=d,x+=x&-x; return ;}
    inline int Ask(int x) {int rec=0; while(x>=1) rec+=c[x],x-=x&-x; return rec;}
}A;
int main() {
    T=read();
    for(int t=1;t<=T;++t) {
        cnt=0; idx=0; memset(h,0,4*(n<<1));
        n=read();
        for(int i=1;i<n;++i) {
            int x=read(),y=read(),z=read();
            e[i]=(Edge){x,y,z};
        }
        Ex_Kruskal(); Dfs((n<<1)-1);
        A.reset();
        for(int i=n+1;i<n<<1;++i) {
            int ls=0,rs;
            for(int k=h[i];k;k=branch[k].next) {
                if(ls) rs=branch[k].to;
                else ls=branch[k].to;
            }
            A.Insert(st[ls],val[i]*size[rs]);
            A.Insert(ed[ls]+1,-val[i]*size[rs]);
            A.Insert(st[rs],val[i]*size[ls]);
            A.Insert(ed[rs]+1,-val[i]*size[ls]);
        }
        int ans=0;
        for(int i=1;i<=n;++i) ans=max(ans,A.Ask(st[i]));
        cout<<"Case "<<t<<": "<<ans<<'\n';
    }
    return 0;
}

最后要提到的就是我们Kruskal重构树最常见的经典题目

【BZOJ3551】Peaks加强版
Description
  在Bytemountains有N座山峰,每座山峰有他的高度h_i。有些山峰之间有双向道路相连,共M条路径,每条路径有一个困难值,这个值越大表示越难走,现在有Q组询问,每组询问询问从点v开始只经过困难值小于等于x的路径所能到达的山峰中第k高的山峰,如果无解输出-1。
Input
  第一行三个数N,M,Q。
  第二行N个数,第i个数为h_i
  接下来M行,每行3个数a b c,表示从a到b有一条困难值为c的双向路径。
  接下来Q行,每行三个数v x k,表示一组询问。v=v xor lastans,x=x xor lastans,k=k xor lastans。如果lastans=-1则不变。
Output
  对于每组询问,输出一个整数表示答案。
Sample Input
10 11 4
1 2 3 4 5 6 7 8 9 10
1 4 4
2 5 3
9 8 2
7 8 10
7 1 4
6 7 1
6 4 8
2 1 5
10 8 10
3 4 7
3 4 6
1 5 2
1 5 6
1 5 8
8 9 2
Sample Output
6
1
-1
8
Hint
【数据范围】
N<=10^5, M,Q<=5*10^5,h_i,c,x<=10^9。

显然,我们题目中“边权小于等于x”的限制条件我们在Kruskal重构树上就变成了一个深度限制。
对于询问节点的祖先,如果祖先节点的权值是不超过x的,那么这颗子树中的所有节点我们都可以到达。
问题就转化成为了静态子树中的权值第k大,可以用可持久化线段树解决。

#include<bits/stdc++.h>
using namespace std;
const int Maxn=200005;
const int Maxm=500005;
inline int read() {
    static char c; int rec=0;
    while((c=getchar())<'0'||c>'9');
    while(c>='0'&&c<='9') rec=rec*10+c-'0',c=getchar();
    return rec;
}
int n,m,Q,N,last;
int val[Maxn<<1],table[Maxn];
int fa[Maxn<<1],st[Maxn],ed[Maxn];
inline int getfa(int x) {return x==fa[x]?x:fa[x]=getfa(fa[x]);}
struct Edge {int a,b,w;} e[Maxm];
inline bool operator < (const Edge &A,const Edge &B) {return A.w<B.w;}
namespace Sgt {
    int cnt=0,root[Maxn<<1];
    #define mid ((L+R)>>1)
    struct Dynamic_Segment_Tree {int s[2],d;} tree[Maxn*20];
    inline void Infix(int &v,int p,int L,int R,int x) {
        v=++cnt; tree[v]=tree[p]; ++tree[v].d;
        if(L==R) return ;
        int f=(x>mid); f?L=mid+1:R=mid;
        Infix(tree[v].s[f],tree[p].s[f],L,R,x);
        return ;
    }
    inline int Ask(int x,int y,int L,int R,int k) {
        if(L==R) return L;
        int sum=tree[tree[y].s[1]].d-tree[tree[x].s[1]].d;
        if(sum>=k) return Ask(tree[x].s[1],tree[y].s[1],mid+1,R,k);
        else return Ask(tree[x].s[0],tree[y].s[0],L,mid,k-sum);
    }
}
struct Branch {int next,to;} branch[Maxn<<1];
int h[Maxn<<1],cnt=0;
inline void add(int x,int y) {
    branch[++cnt].to=y; branch[cnt].next=h[x]; h[x]=cnt; return ;
}
void Ex_Kruskal() {
    int ind=n; sort(e+1,e+1+m);
    for(int i=1;i<=m;++i) {
        int fx=getfa(e[i].a),fy=getfa(e[i].b);
        if(fx!=fy) {
            fa[fx]=fa[fy]=++ind;
            val[ind]=e[i].w;
            add(ind,fx); add(ind,fy);
            if(ind==2*n-1) break;
        }
    } return ;
}
int idx=0;
int F[Maxn][18],deep[Maxn];
inline void Dfs(int v) {
    deep[v]=deep[F[v][0]]+1; st[v]=++idx;
    for(int i=1;i<18;++i)
        if(deep[v]<(1<<i)) break;
        else F[v][i]=F[F[v][i-1]][i-1];
    if(v<=n) Sgt::Infix(Sgt::root[idx],Sgt::root[idx-1],1,N,val[v]);
    else Sgt::root[idx]=Sgt::root[idx-1];
    for(int i=h[v];i;i=branch[i].next) {
        int j=branch[i].to;
        F[j][0]=v; Dfs(j);
    }
    ed[v]=idx; return ;
}
inline void Find_Pos(int &v,int lim) {
    for(int i=17;~i;--i) {
        if(deep[v]<(1<<i)) continue;
        if(val[F[v][i]]<=lim) v=F[v][i];
    } return ;
}
int main() {
    n=read(); m=read(); Q=read(); val[0]=0x3f3f3f3f;
    for(int i=1;i<=n;++i) table[i]=val[i]=read();
    sort(table+1,table+1+n); N=unique(table+1,table+1+n)-table-1;
    for(int i=1;i<=n;++i) val[i]=lower_bound(table+1,table+1+N,val[i])-table;
    for(int i=1;i<=(n<<1);++i) fa[i]=i;
    for(int i=1;i<=m;++i) {
        int a=read(),b=read(),w=read();
        e[i]=(Edge){a,b,w};
    }
    Ex_Kruskal();
    for(int i=1;i<=n;++i) if(!st[i]) Dfs(getfa(i));
    for(int i=1;i<=Q;++i) {
        int v=read()^last,x=read()^last,k=read()^last;
        Find_Pos(v,x);
        if(Sgt::tree[Sgt::root[ed[v]]].d-Sgt::tree[Sgt::root[st[v]-1]].d<k) last=-1;
        else last=table[Sgt::Ask(Sgt::root[st[v]-1],Sgt::root[ed[v]],1,N,k)];
        cout<<last<<'\n';
        last=last<0?0:last;
    }
    return 0;
}

那么我们的 N O I 2018   D a y 1 T 1 就迎刃而解了。
我们只要先求出当前图的最短路,然后用Kruskal重构树维护当前能够到达的节点的最小值即可。

猜你喜欢

转载自blog.csdn.net/hwzzyr/article/details/81190442