1 条题解

  • 0
    @ 2025-8-24 22:33:30

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar LawrenceSivan
    人必须先说很多话,然后保持静默 || AFO

    搬运于2025-08-24 22:33:30,当前版本为作者最后更新于2021-06-18 17:29:40,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    Kruskal 重构树

    updated:感谢 @cyffff 指出错误

    前言

    听了

    https://www.luogu.com.cn/user/128771
    了这个东西。

    于是就爬过来学了。

    确实是很有意思的东西。

    不过貌似也很小众,几乎不咋用。

    但是性质确实很优美。

    特殊的题目也有奇效。

    前置知识

    1. Kruskal 算法求解最小生成树。

    2. 倍增

    3. 主席树

    至于为什么需要这些玩意,其实并不必要

    在题目里会用到的。

    定义

    这个东西我找遍了各大词条,并没有一个合适的定义。

    于是可以跳过。

    实现过程

    在执行 kruskal 的过程中,我们先将边进行排序(排序的方式决定了重构树的性质),之后遍历每一条边,查看这条边的两个端点是否在同一个并查集之内。如果不在,那么就新建一个节点 nodenode,这个点的点权等于这条边的边权

    有一张图画的好啊!

    图片来源:

    https://www.luogu.com.cn/blog/asd369-zzh/p4197-peaks

    具体做法:

    首先找到两个端点在并查集中的根,之后检查是否在一个并查集中。然后连边就可以了。

    namespace Kruskal{
    	inline void add(int u,int v){
    		nxt[++cnt]=head[u];
    		to[cnt]=v;
    		head[u]=cnt;
    	}
    	
    	struct node{
    		int u,v,dis;
    		
    		inline bool operator < (const node &a)const{
    			return dis<a.dis;
    		}
    	}e[maxm<<1];
    
    	int ff[maxn];
    	
    	inline void init(){
    		for(re int i=1;i<maxn;i++){
    			ff[i]=i;
    		}
    	}
    	
    	int find(int x){
    		return x==ff[x]?x:ff[x]=find(ff[x]);
    	}
    	
    	int val[maxn<<1],tot;
    	
    	inline void kruskal(){
    		sort(e+1,e+1+m);
    		
    		init();
    		
    		for(re int i=1;i<=m;i++){
    			int x=find(e[i].u),y=find(e[i].v);
    			if(x!=y){
    				val[++tot]=e[i].dis;
    				ff[x]=ff[y]=ff[tot]=tot;
    				add(tot,x);add(tot,y);
    				fa[x][0]=fa[y][0]=tot;
    			}
    		}
    	}
    	
    }
    

    性质

      1. Kruskal 重构树是一棵树(这不是废话?!

    而且他还是一棵二叉树(虽然看上去也是废话

    还是一棵有根树,根节点就是最后新建的节点。

      1. 若原图不连通,那么建出来的 Kruskal 重构树就是一个森林。
      1. 如果一开始按照边权升序排序,那么建出的 Kruskal 重构树就是一个大根堆,反之就是小根堆。
      1. 若一开始按照边权升序排序,那么 lca(u,v) 的权值代表了原图中 uuvv 路径上最大边权的最小值。反之就是最小边权的最大值。
      1. Kruskal 重构树中的叶子结点必定是原图中的节点,其余的节点都是原图的一条边。
      1. Kruskal 重构树建好以后会比原图多出 n1n-1 个节点(如果原图联通的话)

    一条一条来看:

    对于性质 1122,比较显然,我们就不说了。

    对于性质 3344,由于一开始对边权升序排序,所以我们首先遍历到的边一定是权值最小的。

    于是对于 Kruskal 重构树中的某一个节点,它的子树中任意一个节点的权值一定小于它本身。

    那么可以知道,权值越小的深度越大,权值越大的深度越小。

    于是这是大根堆性质。

    有了大根堆性质,我们可以发现,由于边权升序,其实就是求解最小生成树的过程,于是能出现在 Kruskal 重构树中的节点必然是要满足也出现在原图的最小生成树中的,那么在找 LCA 的过程中,找到的必然是在 Kruskal 重构树上这条路径中深度最小的点,也就是权值最大的。对于原图来说,这个权值最大的恰好是从 uuvv 最小值。

    若一个点能通过一条路径到达,那么我们走最小生成树上的边也一定能到达该节点。

    于是满足了最大值最小的性质。

    同理降序也能够得出最小值最大的性质。

    对于性质 55,可以画图解决。

    对于性质 66,可以发现,建出 Kruskal 重构树的过程其实也就是求解最小生成树的过程,那么 Kruskal 重构树中新增加的节点数也就是最小生成树中的边数。而最小生成树中的边数最多是 n1n-1 条,于是 Kruskal 重构树中新增加的节点数也就是 n1n-1 个。

    应用

    根据上面的性质们,Kruskal 重构树有几种常见用法:

    u->v路径上的最大值最小 or u->v路径上的最小值最大

    这就是上面的性质 3344 了。

    于是直接套板子就行了。

    也给我们一个提示,遇到这种最大值最小或者最小值最大这种类似的语句,可以不急着想二分,还可以想想 Kruskal 重构树。

    例题就是 P1967 [NOIP2013 提高组] 货车运输

    求解路径上最小值最大。

    将边降序排序,建出 Kruskal 重构树,注意处理一下有可能是个森林。

    lca 怎么搞都行,不过我喜欢树剖,比较优雅。

    查询在 Kruskal 重构树中 lca(u,v) 的权值就好了。

    //#define LawrenceSivan
    
    #include<bits/stdc++.h>
    using namespace std;
    
    typedef long long ll;
    #define re register
    const int maxn=1e5+5;
    #define INF 0x3f3f3f3f
    
    int n,m,tot,q;
    
    struct node{
    	int u,v,dis;
    
    	inline bool operator < (const node &a)const{
    		return dis>a.dis;
    	}
    }a[maxn<<1];
    
    int head[maxn],to[maxn<<1],nxt[maxn<<1],cnt;
    
    int val[maxn<<1];
    
    inline void add(int u,int v){
    	to[++cnt]=v;
    	nxt[cnt]=head[u];
    	head[u]=cnt;
    }
    
    int dep[maxn],size[maxn],fa[maxn],son[maxn],top[maxn];
    
    bool vis[maxn];
    
    void dfs1(int u,int f){
    	size[u]=1;
    	vis[u]=true;
    	fa[u]=f;
    	dep[u]=dep[f]+1;
    	for(re int i=head[u];i;i=nxt[i]){
    		int v=to[i];
    		if(v==f)continue;
    		dfs1(v,u);
    		size[u]+=size[v];
    		if(size[v]>size[son[u]])son[u]=v;
    	}
    }
    
    void dfs2(int u,int topf){
    	top[u]=topf;
    	if(!son[u])return;
    	dfs2(son[u],topf);
    	for(re int i=head[u];i;i=nxt[i]){
    		int v=to[i];
    		if(v==fa[u]||v==son[u])continue;
    		dfs2(v,v);
    	}
    }
    
    inline int lca(int x,int y){
    	while(top[x]!=top[y]){
    		if(dep[top[x]]<dep[top[y]])swap(x,y);
    		x=fa[top[x]];
    	}
    	return dep[x]<dep[y]?x:y;
    }
    
    int ff[maxn];
    
    int find(int x){
    	return x==ff[x]?x:ff[x]=find(ff[x]);
    }
    
    inline void init(){
    	for(re int i=1;i<maxn;i++){
    		ff[i]=i;
    	}
    }
    
    inline void Kruskal(){
    	sort(a+1,a+1+m);
    
    	init();
    
    	for(re int i=1;i<=m;i++){
    		int x=find(a[i].u),y=find(a[i].v);
    		if(x!=y){
    			val[++tot]=a[i].dis;
    			ff[tot]=ff[x]=ff[y]=tot;
    			add(tot,x);
    			add(tot,y);
    		}
    	} 
    
    	for(re int i=1;i<=tot;i++){
    		if(!vis[i]){
    			int f=find(i);
    			dfs1(f,0);
    			dfs2(f,f);
    		}
    	}
    }
    
    inline int read(){
        int x=0,f=1;char ch=getchar();
        while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
        while(isdigit(ch)){x=x*10+(ch^48);ch=getchar();}
        return x*f;
    }
    
    int main(){
    #ifdef LawrenceSivan
        freopen("aa.in","r",stdin);
        freopen("aa.out","w",stdout);
    #endif
        n=read();m=read();tot=n;
    
        for(re int i=1;i<=m;i++){
        	a[i].u=read();a[i].v=read();a[i].dis=read();
        }
    
        Kruskal();
    
        q=read();
        while(q--){
    		int u=read(),v=read();
    		if(find(u)!=find(v))puts("-1");
    		else printf("%d\n",val[lca(u,v)]);
        }
    
    
    
    	return 0;
    }
    

    从 u 出发只经过边权不超过 x 的边能到达的节点

    根据性质 33,可以发现,只需要找到边权升序的 Kruskal 重构树中找到深度最小的,点权不超过 xx 的节点,那么这个节点的子树即为所求。

    找这个点一般用树上倍增

    我不要!!!!树剖党声嘶力竭

    没办法这玩意还是倍增好

    我们考虑当前我们找到的这个节点为 xx,然后我们倍增枚举它的祖先,由于是升序排序,所以它祖先的点的点权必然大于等于它的点权,于是,我们倍增的时候只要判断如果它的祖先的点权就好了。

    inline void kruskal(){
    		sort(e+1,e+1+m);
    		
    		init();
    		
    		for(re int i=1;i<=m;i++){
    			int x=find(e[i].u),y=find(e[i].v);
    			if(x!=y){
    				val[++tot]=e[i].dis;
    				ff[x]=ff[y]=ff[tot]=tot;
    				add(tot,x);add(tot,y);
    				fa[x][0]=fa[y][0]=tot;
    			}
    		}
    		dfs(tot);
    	}
    
    
    namespace BIN{
    	int fa[maxn<<1][21],range[maxn<<1][2];
    
    	void dfs(int u){
    		for(re int i=1;i<=20;i++){
    			fa[u][i]=fa[fa[u][i-1]][i-1];
    		}
    		
    		...
    	}
    }
    
    int main(){
    	while(q--){
    		int u=read(),x=read();
    		for(re int i=20;~i;i--){
    			if(fa[u][i]&&val[fa[u][i]]<=x)v=fa[u][i];
    		}
    	}
    }
    

    大概就是这样的。

    例题:P4197 Peaks & P7834 [ONTAK2010] Peaks 加强版

    这个题其实也能用线段树合并做。(指的是前面那个,这个题是强制在线的,用不了线段树合并,感谢@cyffff 指出错误)

    主体思路是裸的,多出来的就是一个第 kk 大。

    这就是为啥我说需要主席树当做前置知识

    然后子树区间第 kk 大,dfs 序 + 主席树大力维护就行了。

    码农题,不好,思维题,好!

    但是思维题不会做嘤嘤嘤

    其实还是有很多细节问题的。

    首先问题就是关于无解情况的判断。

    肯定是对于一个满足条件的子树,子树中节点个数不足 kk 个。

    需要注意的是,由于 Kruskal 重构树的性质 55,我们知道在 Kruskal 重构树中只有叶子节点才是会对答案产生贡献的,于是我们需要统计的子树大小并不是我们以往统计的那样,而是只统计叶子节点。

    实现也很简单:

    void dfs(int u){
        for(re int i=head[u];i;i=nxt[i]){
            int v=to[i];
            if(v==fa[u][0])continue;
            fa[v][0]=u;
            dfs(v);
            size[u]+=size[v];
        }
        if(!size[u])size[u]=1;
    }
    

    剩下的部分其实就好说很多了。

    注意一下离散化就行了。

    //#define LawrenceSivan
    
    #include<bits/stdc++.h>
    using namespace std;
    
    typedef long long ll;
    #define re register
    const int maxn=2e5+5;
    const int maxm=5e5+5;
    #define INF 0x3f3f3f3f
    
    int n,m,q,tmp,num,last;
    
    int a[maxn],b[maxn]; 
    
    int head[maxn<<1],to[maxn<<1],nxt[maxn<<1],cnt;
    
    
    namespace SegmentTree{
        inline void Discretization(){
            sort(b+1,b+1+n);
            tmp=unique(b+1,b+1+n)-b-1;
            for(re int i=1;i<=n;i++)a[i]=lower_bound(b+1,b+1+tmp,a[i])-b;
        }
    
        struct SegmentTree{
            int lc,rc,v;
            #define ls(x) st[x].lc
            #define rs(x) st[x].rc
        }st[maxn<<6];
    
        int segtot,root[maxn<<1];
    
        void build(int &rt,int l,int r){
            rt=++segtot;
            if(l==r)return;
    
            int mid=(l+r)>>1;
            build(ls(rt),l,mid);
            build(rs(rt),mid+1,r);
        }
    
        int modify(int rt,int l,int r,int x){
            int t=++segtot;
            ls(t)=ls(rt),rs(t)=rs(rt);
            st[t].v=st[rt].v+1;
    
            if(l==r)return t;
    
            int mid=(l+r)>>1;
            if(x<=mid)ls(t)=modify(ls(t),l,mid,x);
            else rs(t)=modify(rs(t),mid+1,r,x);
    
            return t;
        }
    
        int query(int x,int y,int l,int r,int k){
            int xx=st[rs(y)].v-st[rs(x)].v;
    
            if(l==r)return l;
    
            int mid=(l+r)>>1;
            if(k<=xx)return query(rs(x),rs(y),mid+1,r,k);
            else return query(ls(x),ls(y),l,mid,k-xx);
        }
        
    }
    
    using namespace SegmentTree;
    
    namespace BIN{
        int fa[maxn<<1][30],pos[maxn<<1],st1[maxn<<1],ed[maxn<<1],size[maxn<<1];
    
        void dfs(int u){
            pos[++num]=u;st1[u]=num;
            for(re int i=1;i<=25;i++){
                fa[u][i]=fa[fa[u][i-1]][i-1];
            }
            for(re int i=head[u];i;i=nxt[i]){
                int v=to[i];
                if(v==fa[u][0])continue;
                fa[v][0]=u;
                dfs(v);
                size[u]+=size[v];
            }
            if(!size[u])size[u]=1;
            ed[u]=num;
        }
    }
    
    using namespace BIN;
    
    namespace Kruskal{
        inline void add(int u,int v){
            nxt[++cnt]=head[u];
            to[cnt]=v;
            head[u]=cnt;
        }
        
        struct node{
            int u,v,dis;
            
            inline bool operator < (const node &a)const{
                return dis<a.dis;
            }
        }e[maxm];
    
        int ff[maxn<<1];
        
        inline void init(){
            for(re int i=1;i<maxn;i++){
                ff[i]=i;
            }
        }
        
        int find(int x){
            return x==ff[x]?x:ff[x]=find(ff[x]);
        }
        
        int val[maxn<<1],tot;
    
        inline void kruskal(){
            sort(e+1,e+1+m);
            
            init();
            
            for(re int i=1;i<=m;i++){
                int x=find(e[i].u),y=find(e[i].v);
                if(x!=y){
                    val[++tot]=e[i].dis;
                    ff[x]=ff[y]=ff[tot]=tot;
                    add(tot,x);add(tot,y);
                }
            }
            
            dfs(tot);
        }
        
    }
    
    using namespace Kruskal;
    
    inline int read(){
        int x=0,f=1;char ch=getchar();
        while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
        while(isdigit(ch)){x=x*10+(ch^48);ch=getchar();}
        return x*f;
    }
    
    int main(){
    #ifdef LawrenceSivan
        freopen("aa.in","r",stdin);
        freopen("aa.out","w",stdout);
    #endif
        n=read();m=read();q=read();tot=n;
        for(re int i=1;i<=n;i++){
            a[i]=b[i]=read();
        }
    
        Discretization();
        
        for(re int i=1;i<=m;i++){
            e[i].u=read();
            e[i].v=read();
            e[i].dis=read();
        }
        
        kruskal();
    
        for(re int i=1;i<=tot;i++){
            root[i]=root[i-1];
            if(pos[i]<=n)root[i]=modify(root[i-1],1,tmp,a[pos[i]]);
         }
        
        while(q--){
            int v=read(),x=read(),k=read();
            v=(v^last)%n+1;k=(k^last)%n+1;x=x^last;
            for(re int i=25;~i;i--){
                if(fa[v][i]&&val[fa[v][i]]<=x)v=fa[v][i];
            }
            if(size[v]<k){
                puts("-1");
                last=0;
                continue;
            }
            else printf("%d\n",last=b[query(root[st1[v]-1],root[ed[v]],1,tmp,k)]);
        }
        
        
        return 0;
    }
    
    • 1

    信息

    ID
    7165
    时间
    2500ms
    内存
    128MiB
    难度
    6
    标签
    递交数
    0
    已通过
    0
    上传者