克鲁斯卡尔重构树及简单应用

本文详细介绍了克鲁斯卡尔重构树的概念、构建方法和性质,并通过两个例题展示了其在无权图和有权图问题中的应用。在无权图问题中,重构树用于求解特定路径可达节点;在有权图问题中,利用重构树和倍增算法解决最优化路径问题。通过对边权的排序,重构树可以构建出不同性质的树,如大根堆或小根堆,进而解决相应问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

克鲁斯卡尔重构树

概念

克鲁斯卡尔重构树,顾名思义,算是克鲁斯卡尔算法的衍生算法。下面给出如何构建克鲁斯卡尔重构树。
1.我们先将边排序,不同的排序规则会使最终的树有不同的性质。
2.排序后遍历每条边,若该边连接的两个点 u , v u,v u,v不在一个连通块内,我们就将该边作为一个新点 x x x,该边的权值作为 x x x的点权,然后将 x x x作为 f i n d ( u ) find(u) find(u) f i n d ( v ) find(v) find(v)的父节点。
3.遍历完所有的边后,就会形成一棵克鲁斯卡尔重构树。

性质

现在我们思考一下这样形成的树有什么性质。
1.初始点全部是叶子节点:因为我们在构造树时只有新点会作为某些点的父节点。
2.若我们初始时将边按从小到大排序,最终形成的树是一个大根堆。
3.若我们初始时将边按从大到小排序,最终形成的树是一个小根堆。

应用

根据性质2,我们可以解决一类问题。给我们一个 n n n个点 m m m条边的无向图,让我们求从 u u u出发只经过边权不超过 x x x的边可以到达的结点。
答案就是点 u u u点权小于等于x的最高的那个祖先 子树中的所有点。

例题1 CF Qpwoeirut and Vertices

题目大意:

给我们一个无向无权图,若干个询问,每次询问给我们一个区间 [ l , r ] [l,r] [l,r],问我们只经过前k条边使得该区间内任意两点可以互相到达的k的最小值。

分析

我们首先将边的边权变为该边的编号,构建克鲁斯卡尔重构树。
对于两个点 u , v u,v u,v,经过前k条边将他们联通的k的最小值就是他们的最近公共祖先。
接着我们设 f [ i ] f[i] f[i]表示将 i i i i + 1 i+1 i+1联通的k的最小值。则将区间 [ l , r ] [l,r] [l,r]中的所有点联通的k的最小值就是 f [ l ] f[l] f[l]~ f [ r − 1 ] f[r-1] f[r1]的最大值。

做法

1.将边的编号作为边权从小到大构建克鲁斯卡尔重构树。
2.预处理出 f f f数组,使用最近公共祖先。
3.线段树维护区间最大值。

代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10,M = 4e5+10;
int h[N<<1],e[M],ne[M],idx;
void add(int a,int b){
	e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int n,m,mm;
int a[M],p[M];
int q[M],depth[M],f[M][20],z[M];
int find(int x){
	if(p[x]!=x)p[x]=find(p[x]);
	return p[x];
}
void bfs(int n){
	int hh=0,tt=0;
	memset(depth,0x3f,(sizeof (int))*(n+5));
	depth[0]=0,depth[n]=n;
	q[tt++]=n;
	memset(f[n],0,sizeof f[n]);
	while(hh!=tt){
		int t=q[hh++];
		//cout<<t<<endl;
		for(int i=h[t];~i;i=ne[i]){
			int j=e[i];
			if(depth[j]>depth[t]+1){
				depth[j]=depth[t]+1;
				q[tt++]=j;
				f[j][0]=t;
				for(int k=1;k<18;k++)
					f[j][k]=f[f[j][k-1]][k-1];
			}
		}
	}
}
int LCA(int a,int b){
	if(depth[a]<depth[b])swap(a,b);
	for(int i=17;i>=0;i--){
		if(depth[f[a][i]]>=depth[b])a=f[a][i];
	}
	if(a==b)return a;
	for(int i=17;i>=0;i--){
		if(f[a][i]!=f[b][i]){
			a=f[a][i];
			b=f[b][i];
		}
	}
	return f[a][0];
}
struct Seg{
	int l,r;
	int mx;
}tr[N<<3];
void pushup(int u){
	tr[u].mx=max(tr[u<<1].mx,tr[u<<1|1].mx);
}
void build(int u,int l,int r){
	tr[u]={l,r};
	if(l==r){
		tr[u].mx=z[l];
		return;
	}
	int mid=l+r>>1;
	build(u<<1,l,mid),build(u<<1|1,mid+1,r);
	pushup(u);
}
int query(int u,int l,int r){
	if(tr[u].l>=l&&tr[u].r<=r)return tr[u].mx;
	int mid=tr[u].l+tr[u].r>>1;
	int res=0;
	if(mid>=l)res=query(u<<1,l,r);
	if(mid<r)res=max(res,query(u<<1|1,l,r));
	return res;
}
void solve(){
	scanf("%d%d%d",&n,&m,&mm);
	memset(h,-1,(sizeof (int))*(n*2+10));
	memset(a,0,(sizeof (int))*(n*2+10));
	for(int i=1;i<=n;i++)p[i]=i;
	for(int i=1;i<=m;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		int uu=find(u),vv=find(v);
		if(uu==vv)continue;
		a[++n]=i;
		p[n]=n;
		p[uu]=p[vv]=p[n];
		add(n,uu),add(n,vv);
		
	}
	bfs(n);
	
	for(int i=1;i<n;i++){
		int lca=LCA(i,i+1);
		z[i]=a[lca];
	}
	build(1,1,n-1);
	while(mm--){
		int l,r;
		scanf("%d%d",&l,&r);
		if(l==r){
			printf("0 ");
		}
		else printf("%d ",query(1,l,r-1));
	}
	printf("\n");
}
int main(){
	int T;
	scanf("%d",&T);
	while(T--)solve();
	return 0;
}

例题2 2021上海站 Life is a Game

题目大意

给我们一个无向有权图。每个点有自己的价值。我们可以在图上来回走,走到一个点后会获得该点的价值,能够重复到达一个点但是不能重复获得该点的价值。但是,从一个点走到另一个点需要满足当前带有的价值要大于边权。若干次询问,每次询问给一个起点和一个价值,问最多可以获得多少价值。

分析

每次询问:我们肯定是先走当前可以走的边,一边走一边获得价值,同时拓展可以走的边,直到任何一个边都不能走。

那我们可以按照边权从小到大构建克鲁斯卡尔重构树,对于一个起点(叶子节点),不断地向上走,每向上走一次,他的价值就会变成所在点的子树的所有叶子节点的价值之和加上最初的价值,不断往上走直到无法继续(当前价值小于父节点的点权)。

但是,如果每次询问我们都一个一个地向上走,对于链式的树肯定会超时。
考虑使用倍增来优化。设 f [ i ] [ j ] f[i][j] f[i][j]表示从点i向上走 2 i 2^i 2i到达的点,我们设 d [ i ] [ j ] d[i][j] d[i][j]表示当前位于点 i i i,走到 f [ i ] [ j ] f[i][j] f[i][j]所需的价值。即在点i至少拥有 d [ i ] [ j ] d[i][j] d[i][j]才能够到达 f [ i ] [ j ] f[i][j] f[i][j]

d [ i ] [ j ] d[i][j] d[i][j]如何计算:
设s[i]表示点i的子树中所有叶子节点之和
我们可以把从 i i i走到 f [ i ] [ j ] f[i][j] f[i][j]分成两部分,先从 i i i走到 f [ i ] [ j − 1 ] f[i][j-1] f[i][j1],再从 f [ i ] [ j − 1 ] f[i][j-1] f[i][j1]走到 f [ i ] [ j ] f[i][j] f[i][j],设初始时有k点价值,现在我们从 f [ i ] [ j − 1 ] f[i][j-1] f[i][j1] 走到 f [ i ] [ j ] f[i][j] f[i][j] 需要 d [ f [ i ] [ j − 1 ] ] [ j − 1 ] d[f[i][j-1]][j-1] d[f[i][j1]][j1] ,即有如下不等式:

s [ f [ i ] [ j − 1 ] ] + k > = d [ f [ i ] [ j − 1 ] ] [ j − 1 ] s[f[i][j-1]]+k>=d[f[i][j-1]][j-1] s[f[i][j1]]+k>=d[f[i][j1]][j1]

k > = d [ f [ i ] [ j − 1 ] ] [ j − 1 ] − s [ f [ i ] [ j − 1 ] ] k>=d[f[i][j-1]][j-1]-s[f[i][j-1]] k>=d[f[i][j1]][j1]s[f[i][j1]]

那么我们现在处于点 i i i,当前拥有的价值为 s [ i ] + k s[i]+k s[i]+k
将上面的不等式代入则有 s [ i ] + k > = s [ i ] + d [ f [ i ] [ j − 1 ] ] [ j − 1 ] − s [ f [ i ] [ j − 1 ] ] s[i]+k>=s[i]+d[f[i][j-1]][j-1]-s[f[i][j-1]] s[i]+k>=s[i]+d[f[i][j1]][j1]s[f[i][j1]]

同时由于我们需要先从 i i i走到 f [ i ] [ j − 1 ] f[i][j-1] f[i][j1],所以还需要满足 s [ i ] + k > = d [ i ] [ j − 1 ] s[i]+k>=d[i][j-1] s[i]+k>=d[i][j1]

d [ i ] [ j ] = m a x ( s [ i ] + d [ f [ i ] [ j − 1 ] ] [ j − 1 ] − s [ f [ i ] [ j − 1 ] ] , d [ i ] [ j − 1 ] ) d[i][j]=max(s[i]+d[f[i][j-1]][j-1]-s[f[i][j-1]],d[i][j-1]) d[i][j]=max(s[i]+d[f[i][j1]][j1]s[f[i][j1]],d[i][j1])

那么每次询问的答案如何计算:
倍增的不断向上跳即可,若最后处于点 i i i,答案就是 i i i的子树中所有叶子节点的价值之和加上初始价值。

做法

1.将边从小到大排序后构建克鲁斯卡尔重构树。
2. d f s dfs dfs求出每个点的子树中所有叶子节点价值之和。
3. b f s bfs bfs预处理出 f f f数组和 d d d数组。
4.每次询问倍增地向上跳。

代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 5e5+10;
int h[N],e[N],ne[N],idx;
void add(int a,int b){
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int n,m,mm;
ll a[N];
int p[N];
struct Edge{
    int u,v,w;
}edge[N];
bool operator<(const Edge&e1,const Edge&e2){
    return e1.w<e2.w;
}
int find(int x){
    if(x!=p[x])p[x]=find(p[x]);
    return p[x];
}
int depth[N],f[N][20];
ll d[N][20];
ll s[N];
int q[N];
void dfs(int u){
    s[u]=0;
    if(h[u]==-1)s[u]=a[u];
    for(int i=h[u];~i;i=ne[i]){
        int j=e[i];
        if(!s[j]){
            dfs(j);
            s[u]+=s[j];
        }
    }
}
void bfs(){
    int hh=0,tt=0;
    memset(depth,0x3f,sizeof depth);
    depth[0]=0;depth[n]=1;
    for(int i=0;i<18;i++)f[n][i]=n;
    q[tt++]=n;
    while(hh!=tt){
        int t=q[hh++];
        for(int i=h[t];~i;i=ne[i]){
            int j=e[i];
            if(depth[j]>depth[t]+1){
                depth[j]=depth[t]+1;
                q[tt++]=j;
                f[j][0]=t;
                d[j][0]=a[t];
                for(int k=1;k<18;k++){
                    f[j][k]=f[f[j][k-1]][k-1];
                    d[j][k]=max(d[f[j][k-1]][k-1]-s[f[j][k-1]]+s[j],d[j][k-1]);
                }
            }
        }
    }
}
int main(){
    memset(h,-1,sizeof h);
    cin>>n>>m>>mm;
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=n;i++)p[i]=i;
    for(int i=1;i<=m;i++){
        cin>>edge[i].u>>edge[i].v>>edge[i].w;
    }
    sort(edge+1,edge+1+m);
    for(int i=1;i<=m;i++){
        int u=find(edge[i].u),v=find(edge[i].v);
        if(u!=v){
            a[++n]=edge[i].w;
            p[n]=n;
            add(n,u),add(n,v);
            p[u]=p[v]=n;
        }
    }
    dfs(n);
    bfs();
    while(mm--){
        int x,k;
        
        cin>>x>>k;
        for(int i=17;i>=0;i--){
            if(s[x]+k>=d[x][i]){
                x=f[x][i];
            }
        }
        cout<<s[x]+k<<endl;
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值