经典算法和数据结构
ST表和倍增算法
1.ST表
ST表通常用于解决区间可重复贡献问题(区间最值)
考虑这样的问题:
给定一个长度为$n$ 的数组,$q$次询问,每次询问要求查询$[l,r]$范围的数组元素最大值
基于ST表进行$O(nlogn)$预处理,$O(1)$查询
令$f[i][j]$表示区间$[i,i+2^j-1]$的最大值
区间$[l,r]$的长度为$r-l+1$,令$k=log_2(r-l+1)$
那么$ans = max(f[l][k],f[r-2^k+1][k])$
预处理过程如下:
int f[M][32];
int a[M];
int Log[M];
int n;
void build(){
Log[1]=0;
for(int i=2;i<=n;i++){
Log[i]=Log[i/2]+1;
}
for(int i=1;i<=n;i++)f[i][0]=a[i];
for(int j=1;j<=31;j++){
for(int i=1;i<=n&&(i+(1ll<<j-1))<=n;i++){
f[i][j]=max(f[i][j-1],f[i+(1ll<<j-1)][j-1]);
}
}
}
询问
int qr(int l,int r){
int k = Log[r-l+1];
return max(f[l][k],f[r-(1ll<<k)+1][k]);
}
除RMQ以外,ST表还可以处理区间按位与,按位或,区间GCD等问题
但是,ST 表能维护的信息非常有限,不能较好地扩展,并且不支持修改操作
2.倍增
基于二进制的划分思想
考虑这样的问题:
给定一个单调不减序列,多次查询,每次查询给定一个数,求该数在序列中第一次出现的位置。
可以二分答案,但也可以使用倍增
int x;cin>>x;
int p=0,step=1;
while(step){
if(p+step<=n&&a[p+step]<x){
p=p+step;
step<<=1;
}else{
step>>=1;
}
}
if(p<n&&a[p+1]==x){
cout<<p+1<<' ';
}else{
cout<<-1<<' ';
}
像这样先翻倍增长步长,再翻倍减少步长是倍增的实现方式之一
快速幂
$O(logn)$求$a^b$同样运用到了倍增
int ksm(int a,int b){
int ans = 1;
while(b){
if(b&1)ans=ans%mod*a%mod;
a=a*a%mod;
b>>=1;
}
return ans%mod;
}
求LCA
倍增法求一颗树上的两个结点的LCA
- 预处理$dep$数组和$f[i][0]$($i$结点的父节点编号)
- 预处理出整个$f$数组
- 对于查询结点$u$和$v$,先让两个结点跳到同一深度,再向上判断$LCA$
int n,m,s;
vector<int>e[M];
int f[M][32];
int dep[M];
void dfs(int u,int fa){
dep[u]=dep[fa]+1;
f[u][0]=fa;
for(int v:e[u]){
if(v==fa)continue;
dfs(v,u);
}
}
int find(int u,int v){
if(dep[u]<dep[v])swap(u,v);
for(int j=31;j>=0;j--){
if(dep[f[u][j]]>=dep[v]){
u=f[u][j];
}
if(u==v)return u;
}
for(int j=31;j>=0;j--){
if(f[u][j]!=f[v][j]){
u=f[u][j];v=f[v][j];
}
}
return f[u][0];
}
void solve(){
cin>>n>>m>>s;
for(int i=1;i<=n-1;i++){
int x,y;cin>>x>>y;
e[x].push_back(y);
e[y].push_back(x);
}
dep[0]=-1;
dfs(s,0);
for(int i=1;i<=31;i++){
for(int j=1;j<=n;j++){
f[j][i]=f[f[j][i-1]][i-1];
}
}
for(int i=1;i<=m;i++){
int u,v;cin>>u>>v;
cout<<find(u,v)<<endl;
}
}