基础算法:倍增

引入

有这样一个问题:
题目描述
n n n 个点,第 i i i 个点的编号为 i i i,定义一次的操作为:

  • 对于所有的 i ( 1 ≤ i ≤ n ) i(1\le i\le n) i(1in),将第 i i i 个点移动到第 a i a_i ai 个点的位置。
  • 所有操作同时进行。

m m m 次询问,第 i i i 次询问给定两个正整数 d i d_i di x i x_i xi,表示询问 d i d_i di 次操作后编号为 x i x_i xi 的点是第几个点。
数据范围
1 ≤ n , m ≤ 1 0 6 1\le n,m\le10^6 1n,m106
1 ≤ a i , x i ≤ n 1\le a_i,x_i\le n 1ai,xin
1 ≤ d i ≤ 1 0 9 1\le d_i\le10^9 1di109

这个时候不可能每次询问都 O ( N ) O(N) O(N) 遍历一遍,并且 m m m 的最大值是 1 0 9 10^9 109,就算只有一个 O ( N ) O(N) O(N) 也会超时。
这个时候,倍增登场。

倍增

顾名思义,倍增,成倍增加。
对于一个数 i i i,我们定义 f i , j f_{i,j} fi,j 为数 i i i 操作 2 j 2^j 2j 次得到的结果。
为什么是 2 j ? 2^j? 2j?
因为对于任意一个正整数 x x x,都有 x = 2 a 1 × 2 a 2 × ⋯ × 2 a n x=2^{a_1}\times2^{a_2}\times\cdots\times2^{a_n} x=2a1×2a2××2an
a a a 为一个严格单调递减的正整数序列。
所以,以多个 j j j 不同的 2 j 2^j 2j 一定可以组成目标操作次数。

回到上题中,我们设 f i , j f_{i,j} fi,j 为编号 i i i 操作 2 j 2^j 2j 次到达的位置。
首先, f i , 0 f_{i,0} fi,0 i i i 个点移动 2 0 2^0 20,也就是 1 1 1 次,到达的点一定是 a i a_i ai
对于 f i , j ( j > 0 ) f_{i,j}(j>0) fi,j(j>0),因为有:
2 x = 2 x − 1 + 2 x − 1 2^x=2^{x-1}+2^{x-1} 2x=2x1+2x1
那么 f i , j f_{i,j} fi,j 在是 i i i 移动 2 j − 1 2^{j-1} 2j1 次到达的位置上再移动 2 j − 1 2^{j-1} 2j1 次,所以有:
f i , j = f f i , j − 1 , j − 1 f_{i,j}=f_{f_{i,j-1},j-1} fi,j=ffi,j1,j1
接下来就是解决求位置的问题。
由于 2 j 2^j 2j 是成倍增长的,所以先确定一下 j j j 的最大取值。
2 30 = 1073741824 ‬ 2^{30}=1073741824‬ 230=1073741824‬,约等于 1 0 9 10^9 109,因此 j j j 的最大取值是 30 30 30
假设问题是在 d d d 次操作后编号 x x x 的位置。
从最大取值 30 30 30 开始枚举系数 i i i,向低系数枚举,如果遇到 2 i ≤ d 2^i\le d 2id,就将 x x x 转移到 f x , i f_{x,i} fx,i,并且将 d d d 减去 2 j 2^j 2j
这样枚举不会出现遗漏,因为如果 2 j ≤ ⌊ b 2 ⌋ 2^j\le\lfloor\frac{b}{2}\rfloor 2j2b,那么在 2 j + 1 2^{j+1} 2j+1 的时候也会满足条件,早就被减掉了。
系数 i i i 一直枚举到 0 0 0 位置,最后的 x x x 就是答案了。

倍增求LCA

LCA,树上最近公共祖先。
对于树上的倍增,我们用 f i , j f_{i,j} fi,j 表示点 i i i 向上跳 j j j 步到达的点,若点 i i i 就是根节点,那么向上跳就没有点了。

对于向上跳之后两个点跳到了同一个点,就找到了 LCA。

具体过程

  1. 任意选取一个点作为根节点,求出树上每一个点的深度。
  2. 求点 x x x 和点 y y y 的 LCA,首先将两个点中更深的点向上跳,使两点深度相同。
  3. 两者一起向上跳,知道到达一个相同的点。
  4. 2,3 两步可以用倍增优化。

模版题:P3379 【模板】最近公共祖先(LCA)
代码

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int n,m,s;
vector<int>a[N];
int dep[N];
int f[N][50];//倍增数组
queue<int>q;
void bfs(){
	dep[s]=1;
	q.push(s);
	while(!q.empty()){
		int x=q.front();
		q.pop();
		for(int i=0;i<a[x].size();i++){
			int v=a[x][i];
			if(dep[v]!=0)continue;
			q.push(v);
			dep[v]=dep[x]+1;
			f[v][0]=x;
			for(int j=1;j<=20;j++)f[v][j]=f[f[v][j-1]][j-1];
		}
	}
}
int lca(int x,int y){
	if(dep[x]<dep[y])swap(x,y);
	for(int i=20;i>=0;i--)if(dep[f[x][i]]>=dep[y])x=f[x][i];
	if(x==y)return x;
	for(int i=20;i>=0;i--){
		if(f[x][i]!=f[y][i]){
			x=f[x][i];
			y=f[y][i];
		}
	}
	return f[x][0];
}
signed main(){
	cin>>n>>m>>s;
	int u,v;
	for(int i=1;i<n;i++){
		cin>>u>>v;
		a[u].push_back(v);
		a[v].push_back(u);
	}
	bfs();
	while(m--){
		int x,y;
		cin>>x>>y;
		cout<<lca(x,y)<<'\n';
	}
}

应用

例题:P13019 [GESP202506 八级] 树上旅行
开两个倍增数组,一个存向上跳,另一个存向下跳。

#include<bits/stdc++.h>
#define int long long
#define endl putchar('\n')
using namespace std;
const int N=1e6+5;
int read(){
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-'0',c=getchar();
	return x*f;
}
void print(int x){
	if(x<0)putchar('-'),x=-x;
	if(x<10){putchar(x+'0');return;}
	print(x/10);
	putchar(x%10+'0');
}
int n,m,k;
int up[N][31];
int down[N][31];
int fa[N],son[N];
int a[N];
signed main(){
	//ios::sync_with_stdio(0);
	n=read(),m=read();
	for(int i=1;i<=n;i++)son[i]=1e9;
	for(int i=2;i<=n;i++)fa[i]=read(),son[fa[i]]=min(son[fa[i]],i);
	fa[1]=1;
	for(int i=1;i<=n;i++)if(son[i]>=1e9)son[i]=i;
	for(int i=1;i<=n;i++)up[i][0]=fa[i],down[i][0]=son[i];
	for(int len=1;len<31;len++){
		for(int j=1;j<=n;j++){
			up[j][len]=up[up[j][len-1]][len-1];
			down[j][len]=down[down[j][len-1]][len-1];
		}
	}
	while(m--){
		int s=read();
		k=read();
		for(int i=1;i<=k;i++)a[i]=read();
		for(int i=1;i<=k;i++){
			if(a[i]>0){
				for(int j=30;j>=0&&a[i];j--){
					if((1<<j)>a[i])continue;
					a[i]-=(1<<j);
					s=up[s][j];
				}
			}
			else{
				a[i]=0-a[i];
				for(int j=30;j>=0&&a[i];j--){
					if((1<<j)>a[i])continue;
					a[i]-=(1<<j);
					s=down[s][j];
				}
			}
		}
		print(s),endl;
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值