RMQ 和LCA

文章详细介绍了树的深度优先搜索(DFS)及其变体,包括倍增法求最近公共祖先(LCA)的模板,以及如何利用DFS序列解决路径和颜色匹配问题。此外,还探讨了线段树在处理动态更新和查询时的懒惰标记策略,并提供了一个处理动态树结构的实例。
摘要由CSDN通过智能技术生成

倍增的模板:

void dfs(int x,int fa){//树的典型的DFS的传入参数。
	dep[x]=dep[fa]+1;//记录深度
	dp[x]=1;//记录子树大小,刚开始赋值为1.
	f[x][0]=fa;//记录自己的直接父亲。
	for(int i=1;i<=31;i++){//必须在这里就要进行x的祖先的更改,因为之后DFS到的其他人会要用到自己的一些祖先结点信息。
		f[x][i]=f[f[x][i-1]][i-1];
	}
	for(auto v:tr[x]){
		if(v==fa) continue;
		dfs(v,x);
		dp[x]+=dp[v];//更新信息。
	}
}

用倍增求LCA的模板:

int lca(int a,int b){
	if(dep[a]<dep[b]) swap(a,b);//让左边的点的深度更深。
	if(dep[a]!=dep[b]){//如果深度不一样就要对左边的点进行跳跃操作。
		for(int i=30;i>=0;i--){
			if(dep[f[a][i]]>=dep[b]) a=f[a][i];//这里是等于的时候也要跳跃。
		}
	}
	if(a==b) return a;//如果本身两个点里面一个就是另外一个的祖先,这个时候需要再这里就直接退出。
	for(int i=30;i>=0;i--){//然后看能不能继续跳,如果能就跳。
        //判断的条件是什么意思?两个是同事跳的,最后的母的是为了让a.b刚好是公共祖先的直接儿子。
        //我的理解是这样子方便书写,而且处理起来也并没有神恶魔麻烦的。
		if(f[a][i]!=f[b][i]){
			a=f[a][i];
			b=f[b][i];
		}
	}
	return f[a][0];//返回我的直接父亲,对应了上文所说的跳跃之后的参数的实际意义。
}

规定一个点往上跳dis下的模板:

//背景:我要得到x 和y的公共祖先z的直接儿子中,哪一个是x的祖先。
//就是从x跳跃dep[x]-dep[z]-1步。
		int px=x;
		int dis=dep[x]-dep[z];
		dis--;
		for(int i=30;i>=0;i--){
			if((dis>>i) &1) {//用2进制表示的时候如果这一位是1,就跳跃。可以写个数字理解一下。
				px=f[px][i];
			}
		}

三种DFS序列

DFN序列:时间戳,只记录入栈的序列。

DFS序列:每个点只会入栈出栈一次

**欧拉序列:**记录不断地入栈出栈。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZFOp9uso-1680962517844)(C:\Users\93085\AppData\Roaming\Typora\typora-user-images\image-20230328150429580.png)]

上面,一般所说的DFS序列是DFN序列,只记录入栈的顺序,上面就是12 4 58 9 6 3 7

进出栈都记录的话,最重要的就是中间两个相同的数字之间都是子树。也就是欧拉序列。

B 树

题目:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-id03wuvK-1680962517846)(C:\Users\93085\AppData\Roaming\Typora\typora-user-images\image-20230328150648949.png)]

里面提到了X到Y的路径上所有的点的颜色都要与X与Y一样,关于路径的问题就容易想到DFS序列,也就是DFS序列,因为两个点之间的所有的路径也就是DFS序列里面中两个点对应编号的中间的所有的对应的原先的编号。

用dp[i] [j]表示前i个点里面用了j种颜色的时候总共的方案数,对于一个新的点,只有两种情况需要考虑,一种是使用了原先没有使用过的颜色:这个时候dp[i] [j]=dp[i-1] [j-1] *(k-j+1).

另外一种情况是用了之前使用过的颜色,1但是,为了保证两个相同颜色之间的所有经过的点的颜色都是一致的,我们只有一种选择:让这个点和他的父亲节点的颜色一样,因为从这个点到达其他的点的路径上,一定会经过父亲这个结点,所以我么可以直接让这个点和父亲节点的颜色一致,这个时候,dp[i] [j] =dp[i-1] [j] …

然后会发现,这个题目,其实并不需要我们真的求出来一个点的DFS序列,直接写代码就可以了。但是对于DFS序列的理解,还是很重要的。

代码:

	int n,k;
	cin>>n>>k;
	for(int i=1;i<n;i++) cin>>x>>y;
	dp[1][1]=k;
	for(int i=2;i<=n;i++){
		for(int j=1;j<=k;j++){
			dp[i][j]=(dp[i-1][j]+dp[i-1][j-1]*(k-j+1))%mod;
		}
	}
	int ans=0;
	for(int i=1;i<=k;i++){
		ans=(ans+dp[n][i])%mod;
	}
	cout<<ans<<endl;

D 花花和月月种树

题目:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I7S46Oxo-1680962517846)(C:\Users\93085\AppData\Roaming\Typora\typora-user-images\image-20230328233220967.png)]

总结:原先是0号节点,之后三种类型:

1 x 表示x 新加进去一个儿子,儿子的编号是现在最大的编号加1,也应该是现在是第几个1。

2 x val x的子树里面都加上val。

3 x 输出x这个节点的权值。(这个是最重要的,也就是最后只需要知道这个点的权值,这一个点就可以了)因为最后只需要输出一个值,所以我们完全可以用tr数组,作为lazy标记来使用,具体操作在里面看

还有一个很重要的点:

现在我们要不断地建立新的节点,怎么处理?线段树是不能新加点的,也不能把每个点都预留1e5个子节点,不然会爆掉。(当然这句话,如果明白了线段树 dfs序的话,你会觉得是一个废话。。因为好像都和具体哪个点没有关系,只不过是多了两个in out 数组,仅此而已)

采用离线处理的方法来解决这个问题,刚开始把所有的信息都读入下来,存储下来,加点的操作直接建边就可以。

在已经有了这个树的前提下,跑一遍DFS序,然后之后套模板就可以。子树都加val就是区间修改,问节点全职,就是单点查询,中间tr数组说白了就是一个懒惰标记的作用。

代码:

#include <bits/stdc++.h>
using namespace std;
const int N=100005;
int n,m,in[N],out[N],tr[400105];
struct ty{
	int type,x,num;
}op[400010];
vector<int>edge[N];
int a[N];
int num=0;
void dfs(int x){
	in[x]=++num;
	for(auto v:edge[x]){
		dfs(v);
	}
	out[x]=num;//里面的dfs就搞完了。
}
void change(int k,int l,int r,int x){
	if(l==r) {
		tr[k]=0;
		return;
	}	
	int mid=(l+r)>>1;
	if(tr[k]!=0){
		tr[2*k]+=tr[k],tr[2*k+1]+=tr[k];
		tr[k]=0;
	}//用作懒惰标记。。。。。
	if(x<=mid) change(2*k,l,mid,x);
	if(x>mid) change(2*k+1,mid+1,r,x);
}
void add(int k,int l,int r,int x,int y,int val){
	if(x<=l && r<=y) {
		tr[k]+=val;
		return ;
	}
	int mid=(l+r)>>1;
	if(tr[k]!=0){
		tr[2*k]+=tr[k],tr[2*k+1]+=tr[k];
		tr[k]=0;
	}//用作懒惰标记。。。。。
	if(x<=mid) add(2*k,l,mid,x,min(mid,y),val);
	if(y>mid) add(2*k+1,mid+1,r,max(mid+1,x),y,val);
}
int find(int k,int l,int r,int x){
	if(l==r) return tr[k];
	int mid=(l+r)>>1;
	if(tr[k]!=0){
		tr[2*k]+=tr[k],tr[2*k+1]+=tr[k];
		tr[k]=0;
	}
	if(x<=mid) return find(2*k,l,mid,x);
	else return find(2*k+1,mid+1,r,x);
}
int main()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
	int number=1;
	cin>>m;
	for(int i=1;i<=m;i++){
		cin>>op[i].type;
		if(op[i].type==1) {
			cin>>op[i].x;
            op[i].x++;
			edge[op[i].x].push_back(++number);
            op[i].x=number;
		}
		if(op[i].type==2){
			cin>>op[i].x>>op[i].num;
			op[i].x++;
		}
		if(op[i].type==3){
			cin>>op[i].x;
			op[i].x++;
        }
	}
	dfs(1);
	for(int i=1;i<=m;i++){
		if(op[i].type==1){
			change(1,1,number,in[op[i].x]);
		}
		if(op[i].type==2){
			add(1,1,number,in[op[i].x],out[op[i].x],op[i].num);
		}
		if(op[i].type==3){
			int sum=find(1,1,number,in[op[i].x]);
			cout<<sum<<endl;
		}
	}
	return 0;
}

用结构题存储信息,类型1,就建边,然后我们下一次是要把新的儿子改为0,所以我们把op[i].num更改为新加进去的节点编号,之后更改in[ op[i].num] 就可以实现操作

区间更改:add(1,1,number,in[op[i].x],out[op[i].x],op[i].num); 单点查询:sum=find(1,1,number,in[op[i].x]);

E. A and B and Lecture Rooms

题目:

给定一个树,给定很多个询问,每一个询问里面有两个点,问能够找到几个点,距离这两个点的距离相等?

思路:

树,边权为1,问距离,典型的需要LCA来帮忙处理距离问题的方法。

如果两个点的距离是奇数,一定无解。如果是偶数,就要分情况讨论:

如果两个本来是相同深度的,那么路径的中点一定是LCA,可以找到,此时整个图里面

可有如下解释:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AEF56Abu-1680962517847)(C:\Users\93085\AppData\Roaming\Typora\typora-user-images\image-20230330162259023.png)]

比如对于D E LCA可以找到是A,那么所以点中除了B C 的所有子树不可以,其他的都可以,可以画图理解一下。

深度不同:

比如G C:距离为4 可以根据距离找到中点,是B。

具体过程:根据距离,dis/2就是G要跳的步数,可以跳到D处,然后D的父亲就是我们需要的中点,为什么要跳到dis/2-1的位置?因为最后结果是dp[B]-dp[D].

DP[X]存储的是 子树的大小。

代码:

#include <bits/stdc++.h>
using namespace std;
const int N=2e5+10;
vector<int>tr[N];
int n,f[N][60],dep[N],dp[N];
void dfs(int x,int fa){
	dep[x]=dep[fa]+1;
	dp[x]=1;
	f[x][0]=fa;
	for(int i=1;i<=30;i++){
		f[x][i]=f[f[x][i-1]][i-1];
	}
	for(auto v:tr[x]){
		if(v==fa) continue;
		dfs(v,x);
		dp[x]+=dp[v];
	}
}

int lca(int a,int b){
	if(dep[a]<dep[b]) swap(a,b);
	if(dep[a]!=dep[b]){
		for(int i=30;i>=0;i--){
			if(dep[f[a][i]]>=dep[b]) a=f[a][i];
		}
	}
	if(a==b) return a;
	for(int i=30;i>=0;i--){
		if(f[a][i]!=f[b][i]){
			a=f[a][i];
			b=f[b][i];
		}
	}
	return f[a][0];
}


int solve(int x,int y){
	int z=lca(x,y);
	// cout<<z<<endl;

	if(dep[x]==dep[y]){
		int px=x;
		int py=y;
		int dis=dep[x]-dep[z];
		dis--;
		for(int i=30;i>=0;i--){
			if((dis>>i) &1) {
				px=f[px][i];
				py=f[py][i];
			}
		}
		return n-dp[px]-dp[py];
	}
	else{
		int dis=dep[x]+dep[y]-2*dep[z];
		if(dis&1) return 0;
		dis/=2;
		dis--;
		int px=x;
		for(int i=31;i>=0;i--){
			if((dis>>i) &1) {
				px=f[px][i];
			}
		}
		int fa=f[px][0];
		return dp[fa]-dp[px];
	}
}

int main()
{
	cin>>n;
	for(int i=1;i<n;i++){
		int x,y; cin>>x>>y;
		tr[x].push_back(y);
		tr[y].push_back(x);
	}
	dfs(1,0);
	int q;
	cin>>q;
	while(q--){
		int x,y; cin>>x>>y;
		if(dep[x]<dep[y]) swap(x,y);
		int sum=solve(x,y);

		cout<<sum<<endl;
	}
	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值