倍增与LCA 学习笔记

倍增与LCA 学习笔记

前言

\(\quad\)首先回忆一下二分答案是什么,二分答案是每一次取中间值,判断合法,只适用于具有单调性的序列,那么什么是倍增呢?顾名思义,成倍增长呗,如果直接对数组进行递推无法满足时间复杂度的话,就要用到倍增,具体是啥呢,举个栗子,看一下以下这个序列:

\(\overbrace{\overbrace{\overbrace{\overbrace{4}^{\text{1}} \quad 5}^{\text{2}} \quad 6 \quad 8}^{\text{4}} \quad 1 \quad 4 \quad 6 \quad 10}^{\text{8}}\)

以第一个数4为例,每次呈指数增长(\(2^0,2^1,2^2,...,2^k\)),以2的整数次幂上位置的值作为代表,因为我们都知道每个数都可以被表示为若干个2的次幂项的和这一性质,所以查询区间长度为2的次幂,直接查询,如果查询区间长度非2的次幂,那么可以将其拆开为若干2的次幂项和,最后合并即可

RMQ问题

\(\quad\)那么RMQ(Range Maximum/Minimum Query的英文缩写)问题,表示为区间的最大值或最小值,通常解决RMQ问题的算法有 单调栈/队列 ST表 线段树,这里着重讲ST表

ST表

\(\quad\)通过倍增法,解决可重复贡献问题的数据结构,不能修改数据,这时你就要问了:明明线段树可以解决RMQ问题,那还要ST表有什么用, 好像确实没用 首先ST表码量比线段树小的多得多,对于没有修改的RMQ还是ST表的时间复杂度更好,那么什么是 可重复贡献问题呢 比如\(max(x, x) = x\),那么相对应的区间求max就是一个可重复贡献问题,对于查询区间重叠部分对解没有影响

图

\(\quad\)ST表可以做到\(O(nlogn)\)复杂度的预处理,并做到\(O(1)\)取得答案,具体步骤如下:

  1. 我们令 \(f(i, j)\) 表示区间 \([i, i+2^j-1]\) 的最大值,那么很显然 \(f(i, 0) = a_i\)

  2. 那么我们倍增的时候就相当于向上 跳了\(2^j-1\) 那么我们可以写出倍增的转移方程:

\[\begin{aligned} f(i, j) = max(f(i, j - 1), f(i + 2^{j-1},j-1)) \end{aligned} \]

\[\underbrace{\boxed{\begin{aligned} max\bigg( max({a_i,a_{i+1},...,a_{i+2^{j-1}-1}}) , max({a_{i+2^{j-1}},a_{i+2^{j-1}+1},...,a_{i+2^{j}-1}}) \bigg) \end{aligned}}} \\\boxed{\quad \quad \quad \quad max\bigg(a_i,a_{i+1},a_{i+2},...,a_{i+2^j-3},a_{i+2^j-2},a_{i+2^j-1}\bigg)\quad \quad \quad \quad }\]

预处理 【参考代码】

void init(){

    lg[1] = 0;
    for(int i = 2 ; i <= n ; i ++){
        lg[i] = lg[i >> 1] + 1;
    }

	for(int j = 0 ; j < M ; j ++){
		for(int i = 1 ; i + (1 << j) - 1 <= n ; i ++){
			if(!j) f[i][j] = a[i];
			else{
				f[i][j] = max(f[i][j - 1], f[i + (1 << j - 1)][j - 1]);
			}
		}
	}
}
  1. 那么对于每一个查询 \([l,r]\) 我们可以将其分开成两个区间:\([l,l+2^k-1]\) 和 \([r-2^k+1,r]\) 两个部分,其中 \(k=\lfloor log_2(r-l+1) \rfloor\)

查询 【参考代码】

int qman(int l, int r){
	int k = lg[r - l + 1];
	return max(f[l][k], f[r - (1 << k) + 1][k]);
}

P3865 【模板】ST 表 & RMQ 问题

根据上述讲解写出代码即可

P3865 【参考代码】

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int M = 17, N = 100010;
int a[N];
int n, m;
int f[N][M];

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;
}

void init(){
	for(int j = 0 ; j < M ; j ++){
		for(int i = 1 ; i + (1 << j) - 1 <= n ; i ++){
			if(!j) f[i][j] = a[i];
			else{
				f[i][j] = max(f[i][j - 1], f[i + (1 << j - 1)][j - 1]);
			}
		}
	}
}

int qman(int l, int r){
	int k = log2(r - l + 1);
	return max(f[l][k], f[r - (1 << k) + 1][k]);
}

int main(){
	n = read();
	m = read();
	for(int i = 1 ; i <= n ; i ++){
		a[i] = read();
	}
	init();
	while(m --){
		int l = read(), r = read();
		cout<<qman(l, r)<<'\n';
	}
	return 0;
}

LCA最近公共祖先

\(\quad\)首先最近公共祖先就是两个节点所有公共祖先中,距离两个节点距离最近的,具体可以看下面这个图:

图

如图,7号节点和8号节点的最近公共祖先就是4号节点

那么具体怎么求解lca呢,我们用到的思路是每次找到深度较大的节点,然后让深度较大的节点向上跳,不难发现,跳到最后两个节点到了同一个节点上,这个节点就是他们的LCA,暴力肯定过不了,本文章着重介绍 倍增法LCA tarjan求LCA 树链剖分LCA

倍增LCA

\(\quad\)倍增lca属于市面上比较流行的lca求解方式了,属于暴力思路的进阶版,具体过程是通过预处理出 \(fa\) 数组(\(fa_{u,j}\)表示u号节点的第 \(2^j\) 个祖先),这个数组可以帮助减少跳转次数,可以通过dfs遍历预处理出来

具体如何优化跳转次数?

  1. 根据暴力思路,我们要先取得两个节点中较大的节点,然后让深度较大的节点跳到两个节点深度相同,设这两个节点为\(u,v\),我们可以通过枚举深度较大的点跳跃的步数,从大到小(设深度较大的点为\(u\),则枚举一个\(k\),使得\(dep_{fa[u][k]} <= dep_v\)),向上跳转直到最后一个满足 \(dep_{fa[u][k]} <= dep_v\) 的\(u\)

  2. 到这里,我们需要判断一下如果此时\(u,v\)已经是同一个节点了,那么可以直接return,如果不是同一个节点,再枚举跳跃步数,从大到小,如果 \(fa_{u,k} \not = fa_{v,k}\) 则 \(u \leftarrow fa_{u,k},v \leftarrow fa_{v,k}\) 最后LCA就是 \(fa_{u,0}\)

倍增【参考代码】

inline void dfs(int u){
	for(int k = 1 ; k < 20 ; k ++)
		f[u][k] = f[f[u][k - 1]][k - 1];
	for(int j : e[u]){
		if(j == f[u][0]) continue;
		f[j][0] = u;
		dep[j] = dep[u] + 1;
		dfs(j);
	}
}

inline int lca(int a, int b){
	if(dep[a] < dep[b]) swap(a, b);
	for(int i = 19 ; i >= 0 ; i --) if(dep[f[a][i]] >= dep[b]) a = f[a][i];
	if(a == b) return a;
	for(int i = 19 ; i >= 0 ; i --) if(f[a][i] != f[b][i]) a = f[a][i], b = f[b][i];
	return f[a][0];
}

tarjan求LCA

未制作

tarjan求LCA 【参考代码】

int ans[N];

int p[N];
int find(int x){
	if(p[x] != x) return p[x] = find(p[x]);
	return x; 
}
void merge(int a, int b){
	a = find(a), b = find(b);
	p[a] = b;
}

bool vis[N];

void tarjan(int u){
	vis[u] = true;
	for(int j : e[u]){
		if(!vis[j]){
			tarjan(j);
			merge(j, u);
		}
	}
	
	for(auto &it : t[u]){
		int j = it.first, id = it.second;
		if(vis[j]) ans[id] = find(j);
	}
}

树链剖分lca

未制作

树链剖分lca 【参考代码】

int f[N], dep[N], sz[N], hson[N], dfn[N], rnk[N], top[N], tot;

void dfs1(int u, int fa){
	f[u] = fa;
	dep[u] = dep[fa] + 1;
	sz[u] = 1;
	hson[u] = -1;
	
	for(int j : e[u]){
		if(j == fa) continue;
		dfs1(j, u);
		sz[u] += sz[j];
		if(hson[u] == -1 || sz[hson[u]] < sz[j]){
			hson[u] = j;
		}
	}
}

void dfs2(int u, int tp){
	top[u] = tp;
	dfn[u] = ++ tot;
	rnk[tot] = u;
	
	if(hson[u] == -1) return ;
	dfs2(hson[u], tp);
	for(int j : e[u]){
		if(j != hson[u] && j != f[u]){
			dfs2(j, j); 
		}
	}
}

int lca(int a, int b){
	while(top[a] != top[b]){
		if(dep[top[a]] < dep[top[b]]) swap(a, b);
		a = f[top[a]];
	}
	if(dep[a] > dep[b]) swap(a, b);
	return a;
}
posted @ 2026-03-29 18:01  神烦doge  阅读(23)  评论(0)    收藏  举报