倍增与st表
还是写博客园里好,方便CV。
引入
有一个长度为 n 的整数数列 a。
现在有 m 个询问,每个询问的格式为两个整数 x 和 y 表示询问 a 中,第 x 个数到第 y 个数间,最大的一个数是多少。
思路
我们容易想到预处理 \(f[i][j]\) 表示第 \(i\) 个数至第 \(j\) 个数的最大值,查询仅用 \(O(1)\) 即可。
但是太慢了,我们不妨用倍增的思想建立一个st表。令 \(st[i][j]\) 表示 \([i,i+2^j-1]\) 这段区间的最大值。
显然,初始化为 \(st[i][0]=a[i]\),根据定义,可以把 \([i,i+2^j-1]\) 这段区间拆分成 \([i,i+2^{j-1}-1]\) 与 \([i+2^{j-1},i+{2^j-1}]\)。
即可推出转移方程:
for(int j=1; j<=lg[n]; j++) //可以预处理floor(log2(i)),lg[1]=0,lg[i]=lg[i/2]+1
{
for(int i=1; i<=n-(1<<j)+1; i++)
{
st[i][j]=max(st[i][j-1],st[i+(1<<(j-1))][j-1]);
}
}
维护好st表,接着处理查询的问题,对于 \([l,r]\) 这段区间,我们只需拆分成二进制一段一段的即可。
由于我们维护的是最大值,划分区间时可以有重叠部分,那我们不妨直接划分成两个区域。
若 \(j=\log_{2}(r-l+1)\),则划分成 \([l,l+2^j-1]\) 与 \([r-2^j+1,r]\)。
while(q--)
{
int l,r;
cin>>l>>r;
int j=lg[r-l+1];
cout<<max(st[l][j],st[r-(1<<j)+1][j])<<"\n";
}
然后两者结合一下,就可以 AC 了。
完整代码:(方便我 C 模板)
#include<bits/stdc++.h>
#define int long long
#define PII pair<int,int>
#define ll long long
using namespace std;
const int N=1e5+5;
int lg[N];
int st[N][30];
int n,a[N],q;
signed main() {
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
lg[1]=0;
for(int i=2; i<=N; i++) lg[i]=lg[i/2]+1;
cin>>n>>q;
for(int i=1; i<=n; i++) {
cin>>a[i];
st[i][0]=a[i];
}
for(int j=1; j<=lg[n]; j++) {
for(int i=1; i<=n-(1<<j)+1; i++) {
st[i][j]=max(st[i][j-1],st[i+(1<<(j-1))][j-1]);
}
}
while(q--) {
int l,r;
cin>>l>>r;
int j=lg[r-l+1];
cout<<max(st[l][j],st[r-(1<<j)+1][j])<<"\n";
}
return 0;
}
应用——LCA
题面
给出一棵有 \(N\) (编号 \(1\) 到 \(N(1 \le N \le 10000)\))个节点的有根树,求出指定节点对的最近公共祖先!
思路
首先考虑暴力,对于每一个查询 \((x,y)\),把 \(x\) 和 \(y\) 两个点依次往上搜,搜到第一个重复走过的点就是最近公共祖先。
这样显然会超时,我们可以加一个小优化,先把 \(x\) 和 \(y\) 提到同一层,再往上搜。
但是依然会被NKOJ极限数据卡掉,观察可知,一层一层往上搜实在太慢了,联系上文可知可以用倍增的思想来以 \(2\) 的幂次方跳。
首先预处理一个 \(f[i][j]\) 数组,表示 \(i\) 的第 \(2^j\) 个祖先。这个预处理可以和建树写在一起。
\(f[i][0] = fa\),然后 \(f[i][j]\) 就可以理解成 \(i\) 的第 \(2^{j-1}\) 个祖先的第 \(2^{j-1}\) 个祖先,即:
void dfs(int u)
{
b[u]=1;
for(int j=1;;j++)
{
f[u][j]=f[f[u][j-1]][j-1];
if(f[u][j]==0)
{
k=max(k,j-1);//计算j的最大值
break;
}
}
for(auto it:v[u])
{
if(b[it]==0)
{
d[it]=d[u]+1;//深度
f[it][0]=u;
dfs(it);
}
}
}
预处理完后就是解决查询问题,首先先把两个点提到同一深度。
if(d[u]<d[v]) swap(u,v);//默认把u提到v
for(int j=k;j>=0;j--)
{
if(d[f[u][j]]>=d[v])
{
u=f[u][j];
}
}
if(u==v) return u;//特判
接着就开始跳,直到相等。
for(int j=k;j>=0;j--)
{
if(f[u][j]!=f[v][j])//不等就跳
{
u=f[u][j];
v=f[v][j];
}
}
return f[u][0];//输出答案
模板代码(注意第 \(63\) 行):
#include<bits/stdc++.h>
#define int long long
#define PII pair<int,int>
#define ll long long
using namespace std;
const int N=1e4+5;
int lg[N],in[N];
int n,q,k,root;
int f[N][30];
int d[N];
bool b[N];
vector<int> v[N];
void dfs(int u)
{
b[u]=1;
for(int j=1;;j++)
{
f[u][j]=f[f[u][j-1]][j-1];
if(f[u][j]==0)
{
k=max(k,j-1);
break;
}
}
for(auto it:v[u])
{
if(b[it]==0)
{
d[it]=d[u]+1;
f[it][0]=u;
dfs(it);
}
}
}
int lca(int u,int v)
{
if(d[u]<d[v]) swap(u,v);
for(int j=k;j>=0;j--)
{
if(d[f[u][j]]>=d[v])
{
u=f[u][j];
}
}
if(u==v) return u;
for(int j=k;j>=0;j--)
{
if(f[u][j]!=f[v][j])
{
u=f[u][j];
v=f[v][j];
}
}
return f[u][0];
}
signed main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
d[0]=-1;//一定要写!!!防止跳到0,血的教训!!!
cin>>n;
for(int i=1;i<n;i++)
{
int x,y;
cin>>x>>y;
v[x].push_back(y);
in[y]++;
}
root=0;
for(int i=1;i<=n;i++) if(!in[i]) root=i;
dfs(root);
cin>>q;
while(q--)
{
int x,y;
cin>>x>>y;
cout<<lca(x,y)<<"\n";
}
return 0;
}