引入
有这样一个问题:
题目描述
有
n
n
n 个点,第
i
i
i 个点的编号为
i
i
i,定义一次的操作为:
- 对于所有的 i ( 1 ≤ i ≤ n ) i(1\le i\le n) i(1≤i≤n),将第 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
1≤n,m≤106
1
≤
a
i
,
x
i
≤
n
1\le a_i,x_i\le n
1≤ai,xi≤n
1
≤
d
i
≤
1
0
9
1\le d_i\le10^9
1≤di≤109
这个时候不可能每次询问都
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=2x−1+2x−1
那么
f
i
,
j
f_{i,j}
fi,j 在是
i
i
i 移动
2
j
−
1
2^{j-1}
2j−1 次到达的位置上再移动
2
j
−
1
2^{j-1}
2j−1 次,所以有:
f
i
,
j
=
f
f
i
,
j
−
1
,
j
−
1
f_{i,j}=f_{f_{i,j-1},j-1}
fi,j=ffi,j−1,j−1
接下来就是解决求位置的问题。
由于
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
2i≤d,就将
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
2j≤⌊2b⌋,那么在
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。
具体过程
- 任意选取一个点作为根节点,求出树上每一个点的深度。
- 求点 x x x 和点 y y y 的 LCA,首先将两个点中更深的点向上跳,使两点深度相同。
- 两者一起向上跳,知道到达一个相同的点。
- 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;
}
}
804

被折叠的 条评论
为什么被折叠?



