学习笔记——倍增思想与lca


ST表

就是一个用倍增法求静态RMQ(区间最值)的算法。

预处理 \(O(nlogn)\),查询 \(O(1)\),远吊打其他算法。

算法

预处理

依次预处理出左端点为 \(l\),右端点为 \(l+2^j(1\le j\le log_2(n))\) 的所有区间最小值,递推式为:

\[d_{i,j}=\min(d_{i,j-1},d_{i+2^{j-1}\ \ ,j-1}) \]

查询

比如说要查询 \([l,r]\) 这个区间。

我们令 \(k=log_2(r-l+1)\ (2^{k}\le r-l+1\le 2^{k+1})\)

那么这个区间的最小值就是 \(\min(d_{l,k},d_{r-2^k+1,k})\)

由于 \(2^{k}\le r-l+1\le 2^{k+1}\) ,所以这两个区间只会重合但不会超出去。

所以不用怕查询错误了。

关于 log2

上面的讲述都围绕着一个东西 \(log_2(x)\),那么这个重要的东西怎么求呢?

其实很简单,我们使用 C++ 的 STL,直接写(int)(log(x)/log(2))就可以算出 \(log_2(x)\) 了。

Code

预处理

void pre_st(){
	int ln=(int)(log(n)/log(2));
	_for(i,1,n)d[i][0]=a[i];
	_for(j,1,ln)
		_for(i,1,n-(1<

查询

int query(int l,int r){
	int k=(int)(log(r-l+1)/log(2));
	return min(d[l][k],d[r-(1<

例题

P2880

板子题。

P2048

好题。

我们用 ST 表维护所有区间的最大前缀和,先算出所有以 \(l\) 为左端点时可以的右端点的区间 \([L,R]\) 和最优的右端点 \(t\),我们可以用一个堆来存储全局的最优区间。

但是我们不能只记录最优的右端点,还要找出分布在 \([L,t)\)\((t,R]\) 的它次优的右端点,次次优的右端点……

那么我们就在每次取出当前最优区间的时候把 \([L,t)\)\((t,R]\) 这两个区间的最优右端点也放入堆即可。

点击查看代码
#include 
#define _for(i,a,b) for(ll i=a;i<=b;++i)
#define for_(i,a,b) for(ll i=a;i>=b;--i)
#define ll long long
using namespace std;
const int N=1e6+10,inf=0x3f3f3f3f;
ll n,k,l,r,a[N],b[N],d[N][30],ans;
struct music{
	ll le,rl,rr,t;
	friend bool operator <(const music& aa,const music& bb){
		return b[aa.t]-b[aa.le-1]q;
inline ll rnt(){
	ll x=0,w=1;char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
	return x*w;
}void pre_st(){
	ll ln=(ll)(log(n)/log(2));
	_for(i,1,n)d[i][0]=i;
	_for(j,1,ln){
		_for(i,1,n-(1<b[y])d[i][j]=x;
			else d[i][j]=y;
		}
	}
}ll query(ll l,ll r){
	ll k=(ll)(log(r-l+1)/log(2));
	if(b[d[l][k]]>b[d[r-(1<t.rl)
			q.push(music{t.le,t.rl,t.t-1,query(t.rl,t.t-1)});
		if(t.t

lca

即最近公共祖先。

这里先只讲三种简单的做法。

树上 RMQ

前置知识:欧拉序列

我们每到一个点就记录一下。比如这棵树:

可爱的树

可以得到序列:

dfs序 1 2 3 4 5 6 7 8 9 10 11 12
编号 1 2 4 5 4 2 1 3 6 7 3 1
层数 1 2 3 4 3 2 1 2 3 3 2 1

每个点第一次出现在:

1 2 3 4 5 6 7
1 2 8 3 4 9 10

算法

我们以上面的树为例,我们要查询 \(5\)\(7\) 的 lca。

先找到 \(5\) 第一次出现在位置 \(4\)\(7\) 第一次出现在位置 \(10\)

然后可以发现序列区间 \([4,10]\) 的最小的层数对应的点就是他们的 lca:\(1\)

但这是为什么呢?

我们在正常 \(\text{dfs}\) 的过程中,要想从 \(u\) 遍历到 \(v\) 必然要经过它们的 \(\text{lca}\),也就说明它们的 \(\text{lca}\) 至少有两棵子树。

既然至少有两棵子树,就不可能遍历完一棵子树后先回到父亲再去遍历另一棵子树。

所以它们的 \(\text{lca}\) 一定是这个区间内层数最小的!

Code

void dfs(int u,int fa){
	jl[u]=1;
	c[u]=c[fa]+1;
	dfn[++cnt]=u,q[u]=cnt;
	int sz=tu[u].size();
	_for(i,0,sz-1){
		int v=tu[u][i].v;
		if(jl[v])continue;
		dfs(v,u);
		dfn[++cnt]=u;
	}
}void pre_st(){
	int ln=(int)(log(cnt)/log(2));
	_for(i,1,cnt)d[i][0]=dfn[i];
	_for(j,1,ln)_for(i,1,cnt+1-(1<

离线 Tarjan

算法

首先我们把所有查询都一股脑的给存进来。

对于一个节点 \(u\),我们都先把它当做自己的根,遍历它所有的子树,每个子树遍历完后把这棵子树的根设为 \(u\)

在遍历某个节点的时候,如果它对应的查询点 \(v\) 被访问过,那么就查询 \(v\) 当前 的祖先 \(u\),这就是它们的 \(\text{lca}\)

因为如果一个点对应的查询点被访问过,那么就一定是在 \(u\) 的另一棵子树上遍历的,既然不在一棵子树上但在同一个节点的子树们下,那么 \(u\) 就一定是它们的 \(\text{lca}\) 了!

具体实现,只用一个深搜就完成了。

Code

//tu是原来的树,t是每个点查询时对应的另一个点
void tarjan(int u){
	v[u]=1;
	int s1=tu[u].size(),s2=t[u].size();
	_for(i,0,s1-1){
		int vv=tu[u][i].v;
		if(v[vv])continue;
		tarjan(vv);
		fa[vv]=u;
	}_for(i,0,s2-1){
		int vv=t[u][i].v;
		if(v[vv])lca[t[u][i].w]=find(vv);
	}
}

倍增

算法

我们先预处理出 \(u\) 的第 \(2^i\) 个祖先,递推式为:

\[fa_{u,i}=fa_{fa_{u,i-1}\ \ ,i-1} \]

即:\(u\) 的第 \(2^i\) 个祖先为 \(u\) 的第 \(2^{i-1}\) 个祖先的第 \(2^{i-1}\) 个祖先。

我们在查询时先让深度深的往上跳,直到跳到同一高度,然后再让两个点一起跳,跳到同一节点时,那个点就是它们的 \(\text{lca}\) 了。

总之:这是一个十分优雅的暴力。

Code

void dfs(int u,int last){
	dep[u]=dep[last]+1;
	fa[u][0]=last;
	int sz=tu[u].size();
	for(int i=1;(1<

对比

算法 查询复杂度 代码长度 能否维护附加信息 维护附加信息代码长短
树上 RMQ \(O(1)\) 较长 \
离线 Tarjan \(O(n)\) (全部询问) 简短
倍增 \(O(logn)\) 简短

例题

P3379

板子题。

P2912

对于每个节点 \(u\),我们维护一下它到根节点的路径权值和 \(sum(u)\)

在找出 \(u\)\(v\)\(\text{lca}\) 后,它们的路径权值和就是:

\[sum(u)+sum(v)-2*sum(lca) \]

点击查看核心代码

\(\text{tarjan}\) 做的。

void pre(){
	_for(i,1,n)fa[i]=i;
}int find(int x){
	if(fa[x]==x)return x;
	return fa[x]=find(fa[x]);
}void tarjan(int u,int w,int f){
	sum[u]=sum[f]+w;
	v[u]=1;
	int s1=tu[u].size(),s2=t[u].size();
	_for(i,0,s1-1){
		int vv=tu[u][i].v,ww=tu[u][i].w;
		if(v[vv])continue;
		tarjan(vv,ww,u);
		fa[vv]=u;
	}_for(i,0,s2-1){
		int vv=t[u][i].v;
		if(v[vv])lca[t[u][i].w]=find(vv);
	}
}

P2245

好题。

首先我们要做一个最小生成树,来尽量降低危险值。

但难点在于:如何去维护路径最小值?

我们选择用倍增做,因为我们在递推 \(u\) 的第 \(2^i\) 个祖先的时候可以顺便递推出\(u\) 与第 \(2^i\) 个祖先的路径上的最小值。

我们在查询 \(u\rightarrow v\) 的最小值时,答案就应该"是 \(u\)\(\text{lca}\) 的最小值"和"\(v\)\(\text{lca}\) 的最小值"两者中的最小值

点击查看全部代码
#include 
#define _for(i,a,b) for(ll i=a;i<=b;++i)
#define for_(i,a,b) for(ll i=a;i>=b;--i)
#define ll long long
using namespace std;
const int N=3e5+10,inf=0x3f3f3f3f;
int n,m,q;
struct bian{int u,v,w;}b[N];
struct tree{int v,w;};
vectortr[N];
inline int rnt(){
	int x=0,w=1;char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
	return x*w;
}bool cmp(bian a,bian b){
	return a.w=c[v])
					mx=max(mx,d[u][i]),u=f[u][i];
		}if(u==v)return mx;
		for_(i,20,0){
			if(f[u][i]!=f[v][i]){
				mx=max(mx,max(d[u][i],d[v][i]));
				u=f[u][i],v=f[v][i];
			}
		}mx=max(mx,max(d[u][0],d[v][0]));
		return mx;
	}
}Lca;
int main(){
	n=rnt(),m=rnt();
	_for(i,1,m)b[i].u=rnt(),b[i].v=rnt(),b[i].w=rnt();
	Kru.kru();
	_for(i,1,n)if(!Lca.jl[i])Lca.dfs(i,0,0,i);
	q=rnt();
	while(q--){
		int x=rnt(),y=rnt();
        int qwq=Lca.lca(x,y);
		if(qwq)printf("%d\n",qwq);
        else printf("impossible\n");
	}
	return 0;
}

\[\Huge{\mathfrak{The\ End}} \]