倍增:ST与LCA

倍增算法之:ST表 与 RMQ

讲解:

倍增思想,就是每次在原基础上往前“跳” \(2^n\)
RMQ参考 https://blog.csdn.net/qq_31759205/article/details/75008659
RMQ 问题,即区间最值查询问题,通常的做法(我会的做法)有 暴力、线段树……
这里介绍一种比较高效的算法:ST表。
ST 表可以做到 \(O(nlogn)\) 的预处理和 \(O(1)\) 的查询,是一种在码量和复杂度上都非常优秀的方法。

预处理:

  • \(a\) 是要求区间最值的序列,\(f_{i,j}\) 表示从第 \(i\) 个数起连续 \(2^j\) 个数中的最大值。(DP状态)

for example:
a={3,2,4,5,6,8,1,2,9,7};
\(f_{1,0}\)表示从第一个数开始,长度为 \(2^0=1\) 的最大值,其实就是这个数 \(3\)

  • 很容易看出 \(f_{i,0} = a_i\)。(DP的初始值)
  • 我们把 \(f_{i,j}\) 平均分成 \(2\) 段(因为 \(i\)\(i+j-1\) 之间一定有偶数个数),从 \(i\)\(i+2^(j-1) -1\) 为一段,\(i+2^(j-1)\)\(i+2^j-1\) 为一段,每段长度都是 \(2^(j-1)\)。我们可以得出状态转移方程:\(f_{i,j}=max\{f_{i,j-1},f_{i+2^(j-1),j-1}\}\)

查询:

  • 假如我们要查询的区间为 \(i\)\(j\),那么我们需要找到覆盖整个闭区间(左边界取 \(i\),右边界取 \(j\))的最小幂(可以重叠)

比如我们要查询 1,2,3,4,5 中的最大值,我们可以先查询 1,2,3,4 中的最大值,再查询 2,3,4,5 中的最大值,最后取 max

  • 因为这个区间长度为 \(j-i+1\),所以我们就可以取 \(k=log2(j-i+1)\) 则有 \(RMQ(i,j)= max\{f_{i,k},f_{j-2^k+1,k}\}\).

洛谷模板题传送门:ST 表 && RMQ 问题

代码:

#include<bits/stdc++.h>

using namespace std;

int n,m;
int f[100100][100];

inline int read()
{
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-')f=-1;ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		x=x*10+ch-48;ch=getchar();
	}
	return x*f;
}

int main()
{
	n=read();m=read();
	for(int i=1;i<=n;i++)
	{
		f[i][0]=read();
	}
	for(int j=1;(1<<j)<=n;j++)
	{
		for(int i=1;i+(1<<j)-1<=n;i++)
		{
			f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
		}
	}
	while(m--)
	{
		int k=0;
		int l,r;
		l=read();r=read();
		while((1<<(k+1))<=r-l+1)k++;
		int ans=max(f[l][k],f[r-(1<<k)+1][k]);
		printf("%d\n",ans);
	}
	return 0;
 } 

倍增求 LCA

讲解:

LCA,即为最近公共祖先。
我们可以记录一个深度 \(depth_i\) 表示节点 \(i\) 的深度,每往上跳一次,深度就减 1
定义 \(f_{i,j}\) 表示从 \(i\) 点向上跳 \(2^j\) 步会跳到哪里。
前面也用到了一个倍增的常用方法:\(2^j = 2^{j-1}+2^{j-1}\)
则状态转移方程为

\(f_{i,j}=f_{f_{i,j-1},j-1}\)

代码:

#include<bits/stdc++.h>

using namespace std;

const int maxn=500100;
const int maxm=500100; 

int n,m,root;
int depth[maxn];//表示第i个节点的深度 
int en=0;
int fir[maxn];
int f[maxn][25];

struct edge{
	int v,next;
}ed[maxm*2];

void add_edge(int u,int v)
{
	en++;
	ed[en].v=v;
	ed[en].next=fir[u];
	fir[u]=en;
}

void dfs(int now,int fa)//当前节点为now,fa是他的父亲节点 
{
	depth[now]=depth[fa]+1;
	f[now][0]=fa;//从这个点向上跳2^0=1步,到达父亲
	for(int i=1;i<=20;i++)
	{
		f[now][i]=f[f[now][i-1]][i-1];//转移方程 
	 } 
	for(int i=fir[now];i;i=ed[i].next)//遍历所有出边 
		if(ed[i].v!=fa) dfs(ed[i].v,now);
}

int get_lca(int a,int b)
{
	if(depth[a]<depth[b])swap(a,b);//把 a 变成深度更深的点 
	for(int i=20;i>=0;i--)//a 向上跳,直到与 b 深度一致. 
	{
		if(depth[f[a][i]]>=depth[b])//没有跳过
			a=f[a][i]; 
	}
	//求lca
	if(a==b) return a; 
	for(int i=20;i>=0;i--)
	{
		if(f[a][i]!=f[b][i])//说明两个点同时向上跳了 2^i 步后仍然没有重合,没有跳到lca
			a=f[a][i],b=f[b][i]; 
	}
	return f[a][0];//返回他们的父节点 
}

int main()
{
	cin>>n>>m>>root;
	depth[root]=0;
	for(int i=1;i<n;i++)
	{
		int p1,p2;
		cin>>p1>>p2;
		add_edge(p1,p2);
		add_edge(p2,p1);
	}
	dfs(root,0);
	for(int i=1;i<=m;i++)
	{
		int a,b;
		cin>>a>>b;
		int p=get_lca(a,b);
		cout<<p<<"\n";
	}
	return 0;
 } 
posted @ 2024-09-02 22:22  lazy_ZJY  阅读(4)  评论(0编辑  收藏  举报