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

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

克鲁斯卡尔重构树

概念

克鲁斯卡尔重构树,顾名思义,算是克鲁斯卡尔算法的衍生算法。下面给出如何构建克鲁斯卡尔重构树。
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;
}
<think>嗯,用户想了解同构树的概念和判断方法。首先,我需要回忆一下数据结构中关于树同构的定义。根据引用[2]中的描述,同构树指的是两棵树可以通过若干次左右孩子互换得到对方。也就是说,结构上可能不完全相同,但通过交换某些节点的左右子树后可以变得相同。比如图1中的例子,交换了A、B、G的左右孩子后就变成了另一棵树,所以它们是同构的。 接下来,判断同构的条件有哪些呢?引用[2]和引用[4]提到,首先根节点的值必须相同,这点很重要。如果根节点的值不同,直接就可以判定不同构。然后递归判断子树的情况。这里有两种情况需要考虑:一种是左右子树不需要交换就能对应,另一种是交换左右子树后才能对应。所以递归的条件应该是,T1的左子树对应T2的左子树且右子树对应右子树,或者T1的左子树对应T2的右子树且右子树对应左子树。这两种情况满足其一即可。 此外,还需要处理空子树的情况。比如,当其中一个树的左子树为空,另一个也必须对应为空,否则不同构。引用[5]中的输入格式提到,节点的孩子用编号表示,空的话用“-”代替。所以在实现时需要处理这些情况,比如将输入的字符串转换为节点指针或索引。 关于数据结构,引用[3]给出了一个例子,使用结构体数组来存储树的节点,每个节点包含元素值、左孩子和右孩子的索引。这样的表示方法方便通过索引来访问子树,而不需要使用指针,特别是在C语言中可能更常见。 用户可能想知道具体的实现方法。根据引用[4],有三种方式:递归、队列和堆栈。递归是最直观的方式,符合树的结构特性。队列和堆栈可能用于非递归的遍历方法,比如层次遍历或迭代加深搜索。但递归方法更简洁,因为每次比较当前节点,然后递归比较左右子树,同时考虑交换的情况。 另外,需要考虑节点的存储内容是否相同。引用[1]提到,如果两棵树不仅结构相似,而且节点数据相同,则称为等价。但同构可能只要求结构可以通过交换得到,而不一定数据完全相同?或者根据引用[2]中的题目,同构是否要求节点数据也必须相同?比如图1中的例子,交换的是结构,但节点数据必须一致。比如,节点A的数据必须相同,否则即使结构相同,数据不同也不能算同构。因此,在判断同构时,除了结构,节点的元素也必须相同。 总结判断条件: 1. 两棵树都为空,同构。 2. 一棵空,另一不空,不同构。 3. 根节点元素不同,不同构。 4. 递归判断左右子树是否同构,或者交换后是否同构。 用户可能还会关心输入如何处理,比如引用[5]中的输入格式,每个节点给出左孩子和右孩子的编号,需要将这些转换为树的结构。例如,用数组来存储每个节点的左右孩子索引,构建树的结构,然后进行比较。 另外,需要注意特殊情况,比如只有一个节点的树,或者所有节点都只有左子树或右子树的情况。这些情况是否会影响判断逻辑? 可能的实现步骤: 1. 读取两棵树的节点信息,构建结构体数组。 2. 找到根节点(没有父节点的节点)。 3. 递归比较两棵树的根节点,检查元素是否相同。 4. 比较子树的结构,考虑交换的情况。 在代码实现中,递归函数需要处理上述四种条件。例如,在C语言中,可以通过函数返回布尔值,依次检查每个条件。如果当前节点满足元素相同,则递归检查左左和右右,或者左右和右左的情况。 用户可能还会想知道时间复杂度。递归方法的时间复杂度应该是O(n),其中n是节点数,因为每个节点最多被访问常数次。空间复杂度取决于递归的深度,最坏情况下是O(h),h是树的高度。 最后,生成相关问题时,需要考虑用户可能进一步的问题,比如如何优化判断过程,或者同构树的应用场景,比如在数据结构中的使用,或者在算法题中的变种问题。</think>### 同构树的概念与判断方法 #### 1. 同构树的定义 两棵树称为**同构**,若其中一棵树可以通过若干次**左右孩子互换**操作变换为另一棵树[^2]。例如,图1中的两棵树通过交换节点A、B、G的左右子树后变为同构,而图2中的树无法通过此类操作达成同构。 #### 2. 判断同构的核心条件 判断两棵树是否同构需满足以下条件: 1. **根节点值相同**:两棵树的根节点存储的字符必须一致; 2. **子树结构可匹配**: - 不交换左右子树时:T1的左子树与T2的左子树同构,且T1的右子树与T2的右子树同构; - 或交换左右子树时:T1的左子树与T2的右子树同构,且T1的右子树与T2的左子树同构; 3. **空子树处理**:若某棵树的子树为空,另一棵树对应位置也需为空[^4]。 #### 3. 数据结构表示 通过结构体数组存储树节点信息(以C语言为例): ```c #define MaxTree 10 typedef struct TreeNode { char Element; int Left; int Right; } TNode; TNode T1[MaxTree], T2[MaxTree]; ``` 每个节点包含字符值、左孩子编号、右孩子编号,空子树用特定符号(如`-`)表示[^3][^5]。 #### 4. 实现方法(递归法示例) ```python def is_isomorphic(root1, root2): if root1 is None and root2 is None: # 均为空树 return True if (root1 is None) != (root2 is None): # 一空一非空 return False if root1.value != root2.value: # 根节点值不同 return False # 判断不交换或交换左右子树的情况 return (is_isomorphic(root1.left, root2.left) and is_isomorphic(root1.right, root2.right)) or \ (is_isomorphic(root1.left, root2.right) and is_isomorphic(root1.right, root2.left)) ``` #### 5. 输入输出示例(引用[5]) 输入格式: ``` 3 # 树1节点数 A 1 - # 节点0: 字符A,左孩子1,右孩子空 B - 2 # 节点1: 字符B,左孩子空,右孩子2 C - - # 节点2: 字符C ``` 输出结果:若同构则输出`Yes`,否则`No`。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值