【算法】倍增

注:学习倍增需要对二进制有一定了解,并且清楚十进制与二进制的关系

倍增是一种预处理的算法,他用来解决形如:一个状态往后走 k k k 步会走到什么状态,那么此时我们解决这个问题的思路是处理出每一个状态走 2 j 2^j 2j 步以后走到的状态,此时我们将 k k k 进行二进制拆分,把他拆成多个二的整数次幂的和 2 a 1 + 2 a 2 + . . . + 2 a m = k 2^{a_1} + 2^{a_2} + ... + 2^{a_m} = k 2a1+2a2+...+2am=k,请注意,这个问题必须要具有累加性与合并性,即这个状态先走 2 a 1 2^{a_1} 2a1 步,再走 2 a 2 2^{a_2} 2a2 步,…,再走 2 a m 2^{a_m} 2am 后走到的状态就是这个状态往后走 k k k 步的状态。

温馨提示:你可以先去看看序列上的倍增

题意简述:

给出一棵 n n n 个节点的树和 m m m 个询问,对于第 i i i 个询问包含两个整数 u i , k i u_i, k_i ui,ki 表示要查询节点 u i u_i ui 的第 k i k_i ki 代祖先的编号,如果不存在输出 − 1 -1 1
数据范围:
1 ≤ n , m ≤ 2 × 1 0 5 1 \leq n, m \leq 2 \times 10^5 1n,m2×105

在最开头说的还记得吗?我们首先来观察这道题有没有累加性,很显然是有的,比如 k i = 2 k_i = 2 ki=2,那么点 u i u_i ui 的第 2 2 2 代祖先即 u i u_i ui 的第 1 1 1 代祖先的第 1 1 1 代祖先,比较容易看出来这个问题是具有累加性与合并性的,所以我们考虑把 k k k 拆分为二的整数次幂,这个东西直接看 k k k 的二进制就行了。那么此时我们的问题就是对于任意节点 u u u 我们怎么知道他的 2 k 2^k 2k 代祖先呢( k k k 为任意整数)。很显然我们可以使用一个类似动态规划的思想,即把问题: u u u 2 k 2^k 2k 代祖先划分成子问题来合并,得到当前问题的答案。前面我们说了,这个问题具有合并性和累加性,所以我们考虑利用累加性把第 k k k 代拆分成多个我们可以算出来的东西,进行合并得到当前的问题。很显然 u u u 2 k 2^k 2k 代祖先就等于是 u u u 2 k − 1 2^{k-1} 2k1 代祖先的 2 k − 1 2^{k-1} 2k1 代祖先( 2 k − 1 + 2 k − 1 = 2 k 2^{k-1} + 2^{k-1} = 2^k 2k1+2k1=2k)。所以可以得到转移式子:
d p [ u ] [ k ] = d p [ d p [ u ] [ k − 1 ] ] [ k − 1 ] dp[u][k] = dp[dp[u][k-1]][k-1] dp[u][k]=dp[dp[u][k1]][k1]
注:这里的 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示点 i i i 的第 2 j 2^j 2j 代祖先的编号。
接着我们就开始拆二进制啦,对于 k k k 的第 i i i,如果他是 1 1 1 那么我们就往 k k k 的拆分数组中加入 2 i 2^{i} 2i,因为 k k k 可以表示为他二进制中的 1 1 1 上的位置的对应权值之和。
至此,我们这道题就解决了

Code:

#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int n, m, f[N][25];
int main() {
	cin >> n;
	for (int i = 1, u, v; i < n; i ++ ) {
		cin >> u >> v;
		f[v][0] = u;
	}
	for (int j = 1; j <= 20; j ++ )
		for (int i = 1; i <= n; i ++ )
			f[i][j] = f[f[i][j - 1]][j - 1];
	cin >> m;
	for (int i = 0, u, v; i < m; i ++ ) {
		cin >> u >> v;
		int c = 0;
		while (v) {
			if (v & 1)
				u = f[u][c];
			++c, v >>= 1;
		}
		if (u) cout << u << '\n';
		else cout << -1 << '\n';
	}
	return 0;
}

luogu P3865

题意简述:
给出一个长度为 n n n 的数组,有 m m m 个询问,每次询问包含 l i , r i l_i, r_i li,ri,让你求出第 l i l_i li 个数到第 r i r_i ri 个数的最大值
数据范围:
1 ≤ n , m ≤ 2 × 1 0 5 1 \leq n, m \leq 2 \times 10^5 1n,m2×105
1 ≤ a i ≤ 1 0 9 1 \leq a_i \leq 10^9 1ai109

对于这种序列上的区间查询问题,我们一般的想法是对于每一个位置 i i i 算出每一个区间 [ i , i + 2 k − 1 ] [i, i + 2^k - 1] [i,i+2k1] 的最大值,所以我们可以用 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示区间 [ i , i + 2 j − 1 ] [i, i + 2^j - 1] [i,i+2j1] 的最大值。
对于 d p dp dp 的初值就是最简的子问题:长度为 1 1 1 的区间,也就是长度为 2 0 2^0 20 的区间,然而长度为 1 1 1 的区间的最大值显然就是那个数,所以我们可以得到初值:
dp[i][0] = a[i]
接着我们考虑怎么用合理复杂度推出 d p dp dp 的值。我们考虑转移。
首先我们发现: [ i , i + 2 j − 1 ] [i, i + 2^j - 1] [i,i+2j1] 这个区间可以划分为两个区间:
[ i , i + 2 j − 1 − 1 ] [i, i + 2^{j - 1} - 1] [i,i+2j11] [ i + 2 j − 1 , i + 2 j ] [i + 2^{j - 1}, i + 2^j] [i+2j1,i+2j]

因为 2 j − 1 + 2 j − 1 = 2 j 2^{j - 1} + 2^{j - 1} = 2^j 2j1+2j1=2j,前半段的长度 + 后半段的长度就等于目标区间的长度

因为最大值具有合并性,并且区间具有累加性,所以:
[ i , i + 2 j − 1 ] [i, i + 2^j - 1] [i,i+2j1] 的最大值就等于 m a x ( max( max( [ i , i + 2 j − 1 − 1 ] [i, i + 2^{j - 1} - 1] [i,i+2j11] 的最大值, [ i + 2 j − 1 , i + 2 j ] [i + 2^{j - 1}, i + 2^j] [i+2j1,i+2j] 的最大值 ) ) )
问题是我们怎么得到划分出的这两个区间的最大值呢?用 d p dp dp 值表示就可以了。
第一个区间 [ i , i + 2 j − 1 ] [i, i + 2^j - 1] [i,i+2j1] d p [ i ] [ j − 1 ] dp[i][j - 1] dp[i][j1]
第二个区间 [ i + 2 j − 1 , i + 2 j ] [i + 2^{j - 1}, i + 2^j] [i+2j1,i+2j] d p [ i + 2 j − 1 ] [ j − 1 ] dp[i + 2^{j - 1}][j - 1] dp[i+2j1][j1]
所以: d p [ i ] [ j ] = m a x ( d p [ i ] [ j − 1 ] , d p [ i + 2 j − 1 ] [ j − 1 ] ) dp[i][j] = max(dp[i][j - 1], dp[i + 2^{j - 1}][j - 1]) dp[i][j]=max(dp[i][j1],dp[i+2j1][j1])
得到 c o d e code code

for (int j = 1; j <= 20; j ++ )
	for (int i = 1; i + (1 << j) - 1 <= n; i ++ )
		dp[i][j] = max(dp[i][j - 1], dp[i + (1 << j)][j - 1]);

接下来就是怎么用他来算了,首先如果沿用前面的想法就是对于 l , r l, r l,r,把 r − l + 1 r - l + 1 rl+1 拆成二进制来做,但是如果要求查询 O ( 1 ) O(1) O(1) 呢?看似好像沿用刚才的方法是做不到的,那么我们换一种方式,我们不要求正好组成 r − l + 1 r - l + 1 rl+1,我们只要选出一些区间满足他们覆盖了 [ l , r ] [l, r] [l,r] 并且没有多覆盖其他的显然也可以(这个可以好好想一想),所以,方便起见,我们直接从左边选一个最大的长度为二的整数次幂并且没有超过 r r r 的区间,然后再从右边选一个最大的长度为二的整数次幂并且没有超过 l l l 的区间即可,如果你不清楚,可以看一下下面的图片:
在这里插入图片描述
这里的 k k k 表示满足 2 k ≤ r − l + 1 2^k \leq r - l + 1 2krl+1 的最大整数,我们可以对于每一个 x x x 都预处理出他的 k k k 这样就实现了真正的 O ( 1 ) O(1) O(1)
所以可以得到 c o d e code code

int query(int l, int r) {
	int k = bin[r - l + 1]; //bin[x]: 表示x对应的k
	return max(dp[l][k], dp[r - (1 << k) + 1][k]);
}

这里给出算 b i n bin bin 的代码:

for (int i = 2; i <= N; i ++ )
	bin[i] = bin[i >> 1] + 1; //其实 bin[x] 就表示 (int)log2(x)

Code:

#include <bits/stdc++.h>
#define pb push_back
#define ll long long
using namespace std;
const int N = 2e5 + 5;
int n, m, ST[N][30], a[N], bin[N];
int main() {
    ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
    cin >> n >> m;
    for (int i = 2; i < N; i ++ )
        bin[i] = bin[i >> 1] + 1;
    for (int i = 1; i <= n; i ++ )
        cin >> a[i], ST[i][0] = a[i];
    for (int j = 1; j <= 20; j ++ )
        for (int i = 1; i + (1 << j) - 1 <= n; i ++ )
            ST[i][j] = max(ST[i][j - 1], ST[i + (1 << (j - 1))][j - 1]);
    while (m -- ) {
        int l, r; cin >> l >> r;
        int k = bin[r - l + 1];
        cout << max(ST[l][k], ST[r - (1 << k) + 1][k]) << '\n';
    }
    return 0;
}

luogu P3379

题意简述:

给出一个 n n n 个节点的树和 m m m 个询问,每次询问包含 u i , v i u_i, v_i ui,vi 求他们的最近共先祖。

我们举个例子:
在这里插入图片描述
我们可以首先将 u u u v v v 跳到同一层,然后我们再进行处理,我们怎么实现呢?显然我们就需要将 u u u 往上跳 d e p u − d e p v dep_u - dep_v depudepv 次,这样就可以使此时的 d e p u = d e p v dep_u = dep_v depu=depv 了,这个东西可以用例题 1 1 1 的办法解决
记着我们就来做 L C A LCA LCA
在这里插入图片描述
此时 d e p u = d e p v dep_u = dep_v depu=depv 所以 L C A LCA LCA 距离 u , v u, v u,v 的距离是相同的所以我们就尝试跳正确的步数调到 L C A LCA LCA,设这个正确步数为 s s s,那么对于我跳 s ′ ( s ′ > s ) s'(s' > s) s(s>s) 步,显然他们都是和 L C A LCA LCA 一样,都是跳到了一个节点,所以对于一个步数,我并不能确定只要我跳这个步数如果是一个节点就一定是 L C A LCA LCA,此时发现: u , v u, v u,v 分别跳 k k k 步,就是 L C A LCA LCA 下面的点( k k k 是满足 u , v u, v u,v 分别跳 k k k 步不在同一个点的最大整数),所以我们直接二进制拆分 k k k 即可,也就是我们枚举从大到小 i i i,如果 u u u 2 i 2^i 2i 步和 v v v 2 i 2^i 2i 步不是一个点,那么就跳,这样不停的跳,最后就是 L C A LCA LCA 下面的点了,在往上跳一步就可以得到 L C A LCA LCA 了。

Code(以前的码风):

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int dp[N][25],n,m;
int dep[N];vector<int>E[N];
void dp_init(){
	for(int j=1;j<=20;j++)
		for(int i=1;i<=n;i++)
			dp[i][j]=dp[dp[i][j-1]][j-1];
}
void dfs(int u){
	for(int i=0;i<E[u].size();i++){
		int v=E[u][i];
		dep[v]=dep[u]+1;
		dfs(v);
	}
}
int main() {
	cin>>n;dep[1]=1;
	for(int i=1,u,v;i<n;i++){
		scanf("%d%d",&u,&v);E[u].pb(v);dp[v][0]=u;
	}dp_init(),dfs(1);cin>>m;
	for(int t=0,u,v,c;t<m;t++){
		cin>>u>>v;
		if(dep[u]<dep[v])swap(u,v);
		c=dep[u]-dep[v];
		for(int i=0;c;c/=2,i++)if(c&1)
			u=dp[u][i];
		if(u==v){
			cout<<u<<'\n';
			continue;
		}
		for(int i=20;i>=0;i--)
			if(dp[u][i]!=dp[v][i])
				u=dp[u][i],v=dp[v][i];
		cout<<dp[u][0]<<'\n';
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值