基础算法:倍增
引入
有这样一个问题:
题目描述
有 nnn 个点,第 iii 个点的编号为 iii,定义一次的操作为:
- 对于所有的 i(1≤i≤n)i(1\le i\le n)i(1≤i≤n),将第 iii 个点移动到第 aia_iai 个点的位置。
- 所有操作同时进行。
有 mmm 次询问,第 iii 次询问给定两个正整数 did_idi 和 xix_ixi,表示询问 did_idi 次操作后编号为 xix_ixi 的点是第几个点。
数据范围
1≤n,m≤1061\le n,m\le10^61≤n,m≤106
1≤ai,xi≤n1\le a_i,x_i\le n1≤ai,xi≤n
1≤di≤1091\le d_i\le10^91≤di≤109
这个时候不可能每次询问都 O(N)O(N)O(N) 遍历一遍,并且 mmm 的最大值是 10910^9109,就算只有一个 O(N)O(N)O(N) 也会超时。
这个时候,倍增登场。
倍增
顾名思义,倍增,成倍增加。
对于一个数 iii,我们定义 fi,jf_{i,j}fi,j 为数 iii 操作 2j2^j2j 次得到的结果。
为什么是 2j?2^j?2j?
因为对于任意一个正整数 xxx,都有 x=2a1×2a2×⋯×2anx=2^{a_1}\times2^{a_2}\times\cdots\times2^{a_n}x=2a1×2a2×⋯×2an。
aaa 为一个严格单调递减的正整数序列。
所以,以多个 jjj 不同的 2j2^j2j 一定可以组成目标操作次数。
回到上题中,我们设 fi,jf_{i,j}fi,j 为编号 iii 操作 2j2^j2j 次到达的位置。
首先,fi,0f_{i,0}fi,0 第 iii 个点移动 202^020,也就是 111 次,到达的点一定是 aia_iai。
对于 fi,j(j>0)f_{i,j}(j>0)fi,j(j>0),因为有:
2x=2x−1+2x−12^x=2^{x-1}+2^{x-1} 2x=2x−1+2x−1
那么 fi,jf_{i,j}fi,j 在是 iii 移动 2j−12^{j-1}2j−1 次到达的位置上再移动 2j−12^{j-1}2j−1 次,所以有:
fi,j=ffi,j−1,j−1f_{i,j}=f_{f_{i,j-1},j-1} fi,j=ffi,j−1,j−1
接下来就是解决求位置的问题。
由于 2j2^j2j 是成倍增长的,所以先确定一下 jjj 的最大取值。
230=10737418242^{30}=1073741824230=1073741824,约等于 10910^9109,因此 jjj 的最大取值是 303030。
假设问题是在 ddd 次操作后编号 xxx 的位置。
从最大取值 303030 开始枚举系数 iii,向低系数枚举,如果遇到 2i≤d2^i\le d2i≤d,就将 xxx 转移到 fx,if_{x,i}fx,i,并且将 ddd 减去 2j2^j2j。
这样枚举不会出现遗漏,因为如果 2j≤⌊b2⌋2^j\le\lfloor\frac{b}{2}\rfloor2j≤⌊2b⌋,那么在 2j+12^{j+1}2j+1 的时候也会满足条件,早就被减掉了。
系数 iii 一直枚举到 000 位置,最后的 xxx 就是答案了。
倍增求LCA
LCA,树上最近公共祖先。
对于树上的倍增,我们用 fi,jf_{i,j}fi,j 表示点 iii 向上跳 jjj 步到达的点,若点 iii 就是根节点,那么向上跳就没有点了。
对于向上跳之后两个点跳到了同一个点,就找到了 LCA。
具体过程
- 任意选取一个点作为根节点,求出树上每一个点的深度。
- 求点 xxx 和点 yyy 的 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;}
}