倍增与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}\) 个祖先,即:

\[f[i][j]=f[f[i][j-1]][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;
}
posted @ 2025-03-25 15:29  yaaaaaan  阅读(30)  评论(0)    收藏  举报