1. 程式人生 > 其它 >Kruskal 重構樹總結

Kruskal 重構樹總結

Kruskal 重構樹

前言

聽了 @Wankupi 學長講了這個東西。

於是就爬過來學了。

確實是很有意思的東西。

不過貌似也很小眾,幾乎不咋用。

但是性質確實很優美。

特殊的題目也有奇效。

前置知識

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

  2. 倍增

  3. 主席樹

至於為什麼需要這些玩意,其實並不必要

在題目裡會用到的。

定義

這個東西我找遍了各大詞條,並沒有一個合適的定義。

於是可以跳過。

實現過程

在執行 kruskal 的過程中,我們先將邊進行排序(排序的方式決定了重構樹的性質),之後遍歷每一條邊,檢視這條邊的兩個端點是否在同一個並查集之內。如果不在,那麼就新建一個節點 \(node\)

,這個點的點權等於這條邊的邊權

有一張圖畫的好啊!

圖片來源:@asd369

具體做法:

首先找到兩個端點在並查集中的根,之後檢查是否在一個並查集中。然後連邊就可以了。

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) 的權值代表了原圖中 \(u\)\(v\) 路徑上最大邊權的最小值。反之就是最小邊權的最大值。
    1. Kruskal 重構樹中的葉子結點必定是原圖中的節點,其餘的節點都是原圖的一條邊。
    1. Kruskal
      重構樹建好以後會比原圖多出 \(n-1\) 個節點(如果原圖聯通的話)

一條一條來看:

對於性質 \(1\)\(2\),比較顯然,我們就不說了。

對於性質 \(3\)\(4\),由於一開始對邊權升序排序,所以我們首先遍歷到的邊一定是權值最小的。

於是對於 Kruskal 重構樹中的某一個節點,它的子樹中任意一個節點的權值一定小於它本身。

那麼可以知道,權值越小的深度越大,權值越大的深度越小。

於是這是大根堆性質。

有了大根堆性質,我們可以發現,由於邊權升序,其實就是求解最小生成樹的過程,於是能出現在 Kruskal 重構樹中的節點必然是要滿足也出現在原圖的最小生成樹中的,那麼在找 LCA 的過程中,找到的必然是在 Kruskal 重構樹上這條路徑中深度最小的點,也就是權值最大的。對於原圖來說,這個權值最大的恰好是從 \(u\)\(v\) 最小值。

若一個點能通過一條路徑到達,那麼我們走最小生成樹上的邊也一定能到達該節點。

於是滿足了最大值最小的性質。

同理降序也能夠得出最小值最大的性質。

對於性質 \(5\),可以畫圖解決。

對於性質 \(6\),可以發現,建出 Kruskal 重構樹的過程其實也就是求解最小生成樹的過程,那麼 Kruskal 重構樹中新增加的節點數也就是最小生成樹中的邊數。而最小生成樹中的邊數最多是 \(n-1\) 條,於是 Kruskal 重構樹中新增加的節點數也就是 \(n-1\) 個。

應用

根據上面的性質們,Kruskal 重構樹有幾種常見用法:

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

這就是上面的性質 \(3\)\(4\) 了。

於是直接套板子就行了。

也給我們一個提示,遇到這種最大值最小或者最小值最大這種類似的語句,可以不急著想二分,還可以想想 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 的邊能到達的節點

根據性質 \(3\),可以發現,只需要找到邊權升序的 Kruskal 重構樹中找到深度最小的,點權不超過 \(x\) 的節點,那麼這個節點的子樹即為所求。

找這個點一般用樹上倍增

我不要!!!!樹剖黨聲嘶力竭

沒辦法這玩意還是倍增好

我們考慮當前我們找到的這個節點為 \(x\),然後我們倍增列舉它的祖先,由於是升序排序,所以它祖先的點的點權必然大於等於它的點權,於是,我們倍增的時候只要判斷如果它的祖先的點權就好了。

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

這個題其實也能用線段樹合併做。

其實這個題在題單裡躺了好久了,本來是打算線段樹合併做的,然後學了重構樹於是就用重構樹了。

主體思路是裸的,多出來的就是一個第 \(k\) 大。

這就是為啥我說需要主席樹當做前置知識

然後子樹區間第 \(k\) 大,dfs 序 + 主席樹大力維護就行了。

碼農題,不好,思維題,好!

但是思維題不會做嚶嚶嚶

其實還是有很多細節問題的。

首先問題就是關於無解情況的判斷。

肯定是對於一個滿足條件的子樹,子樹中節點個數不足 \(k\) 個。

需要注意的是,由於 Kruskal 重構樹的性質 \(5\),我們知道在 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;

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();
        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");
            continue;
        }
        else printf("%d\n",b[query(root[st1[v]-1],root[ed[v]],1,tmp,k)]);
    }
    
    
    return 0;
}

看了題解以後發現這題也可以不使用 dfs 序,由於每個節點直接對應一個區間,所以可以直接處理。

注意區間是左開右閉的。

//#define LawrenceSivan

#include<bits/stdc++.h>
using namespace std;

typedef long long ll;
#define re register
const int maxn=1e5+5;
const int maxm=5e5+5;
#define INF 0x3f3f3f3f

int n,m,q,tmp,num;

int a[maxn],b[maxn]; 

int head[maxn<<1],to[maxm<<1],nxt[maxm<<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<<5];

	int segtot,root[maxn];

	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][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];
		}
		range[u][0]=num;
		if(!head[u]){
			range[u][1]=++num;
			root[num]=modify(root[num-1],1,tmp,a[u]);
			return;
		}
		for(re int i=head[u];i;i=nxt[i]){
			int v=to[i];
			dfs(v);
		}
		range[u][1]=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<<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;
			}
		}
		build(root[0],1,tmp);
		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();
	
	while(q--){
		int v=read(),x=read(),k=read();
		for(re int i=20;~i;i--){
			if(fa[v][i]&&val[fa[v][i]]<=x)v=fa[v][i];
		}
		if(range[v][1]-range[v][0]<k){
			puts("-1");
			continue;
		}
		else printf("%d\n",b[query(root[range[v][0]],root[range[v][1]],1,tmp,k)]);
	}
	
	
	return 0;
}