倍增
问题引入
Q1: 给定一个正数x,已知x一定可用m个二进制位表示,从高位到低位打印x每一位的状态
从数二进制位左往右第i位计算1<<i,让原数与其相减,若结果小于0则不减,并将该位状态标记为0,若大于0则减去,并将该位状态标记为1
如数101,二进制为0001100101,
1<<9为512>101,不减,第九位状态为0
1<<8为256>101,不减,第八位状态为0
1<<7为128>101,不减,第七位状态为0
1<<6为64<101,减成37,第六位状态为1
1<<5为25<32,减成5,第五位状态为1
以此类推
Q2: 给定一个正数x,打印<=x的最大的2的幂是2的几次方,注意防溢出
若x=13,则\(2^3=8\)最接近13,输出3
若x=16,则\(2^4=16\)最接近16,输出4
溢出:若x非常接近INT_MAX,则每次从\(2^{p-1}\leq x\leq2^p\)时很有可能溢出
解决方案:当\(2^p\leq \frac{x}{2}\)时就让p++
1
2
3
4
5
6
7
int q2(int x){
int power = 0;
while(1<<power)<=(x>>1){
power++;
}
return power;
}倍增算法
线段上有n个点,给定每个点i往右边跳1步能最远覆盖的点jump[i]
已知从任意点出发都能到达最后的点,并且在i<j时,必定有jump[i]<=jump[j]
- 如何构建一张表可以查询从任意点i出发,任意跳\(2^p\)步,最远能到达的点
- 如何快速计算任意两点之间,最少跳几步能到达
如该表,行代表p,表示\(2^p\)次方内对应点是否能到达,列代表点i
初始jump[i]填入第一列,即st[i][0]=jump[i],后面列的填充满足转移方程st[i][p]=st[st[i][p-1]][p-1],即跳\(2^p\)内能到达的可以转化为从\(2^{p-1}\)跳\(2^{p-1}\)内能到达
该表列最多\(\log_2n\)列
这就是ST表,我们利用ST表从x到y跳跃的过程中,先从最大步长开始,每次减少一半尝试,最后得到答案
假设计算17到77至少要多少步
我们知道至多需要60步,而60可以用六个二进制位表示
从左往右,让17往右跳\(2^i\)步,看对应数值大于还是小于77,大于则该位标0,小于则标1,且将步数转为49
最后需要的是逼近77的结果,如最后到达了73,再加一步就能到达77
例-倍增跳跃
P4155 [SCOI2015] 国旗计划
题意:给定m个点,编号1-m,所有点围城一个环,i号点顺时针到达i+1号点,最终m号点顺时针回到1号点
给定n条线段,每条线段(a,b)表示线段顺时针从a到b
保证所有线段可以把整个环覆盖,且线段间可能重合但一定互不包含
返回长度为n的结果数组ans,ans[x]表示一定选x号线段下,至少选几条线段能覆盖整个环
%E5%9F%BA%E7%A1%80%E7%AE%97%E6%B3%95/%E5%80%8D%E5%A2%9E/image-20250430113318806.png)
我们将环展开成条进行计算,即每次寻找包含在内且尽量靠右的线段跳跃,寻找离转一圈后的起始点所在的最近的线段
如从A开始,需要经过A->B->D才能到达10(2)点,故需要三条线段覆盖
构建时只需要看第i个点所在线段的末尾是哪个即可,表示一步内从i可以跳到末尾
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N = 2e5 + 10;
const int mod = 1e9 + 7;
struct node{
int idx;
int s,e;
}line[2*N];
int n, m, power;
vector<vector<int>> st;
vector<int> ans;
// 将环状展开为线性
int lgp(int n){
int ans = 0;
while((1<<ans)<=(n>>1)){
ans++;
}
return ans;
}
void build(){
/*如C(6,1),m=8
* 有1 2 3 4 5 6 7 8 9 10 11 ...
1 2 3...
故转化后的C是(6,9)
*/
for (int i = 1;i<=n;i++){
if (line[i].s>line[i].e){
line[i].e+=m;
}
}
// 按起始点排序
sort(line+1,line+1+n,[&](const node a, const node b){return a.s<b.s;});
// 对点在线性上进行复制即 A B C D A' B' C' D'
for(int i =1;i<=n;i++){
line[i+n].idx = line[i].idx;
line[i+n].s = line[i].s+m;
line[i+n].e = line[i].e+m;
}
// 新线段数量
int e = n<<1;
// 寻找跳一步的最大值, 应满足下一条线段开头<=当前线段结尾
/*如线段开头如下
3
34
56
89
102
到102就超过了结尾,故选择到89
*/
for (int i =1, j = 1;i<=e;i++){
while(j+1<=e&&line[j+1].s<=line[i].e){
j++;
}
st[i][0]=j;
}
// 倍增
for (int p = 1; p<=power; p++){
for (int i = 1;i<=e;i++){
st[i][p]=st[st[i][p-1]][p-1];
}
}
}
int jump(int i){
// i号线段的目标点是line[i].s+m
int tar = line[i].s+m,cur = i, next, ans = 1;
for(int p = power; p>=0;p--){
next = st[cur][p];
if (next!=0&&line[next].e<tar){
ans+=1<<p;
cur = next;
}
}
return ans+1; // 计算的是小于目标点,+1即为到达
}
void solve() {
cin >> n >> m;
for(int i =1;i<=n;i++){
line[i].idx = i;
cin >> line[i].s >> line[i].e;
}
power = lgp(n);
st = vector<vector<int>>(2*n+1, vector<int>(power+1,0));
ans = vector<int>(n+1,0);
build();
for (int i = 1;i<=n;i++){
ans[line[i].idx] = jump(i);
}
for (int i = 1;i<=n;i++) cout << ans[i]<<' ';
}
int main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int tomorin=1;
//cin >> tomorin;
while (tomorin--) solve();
return 0;
}例-RMQ问题
P2880 [USACO07JAN] Balanced Lineup G
仍然构造一个st表,表示覆盖范围内对应的最大值
对于状态转移,每次的最大值一定在前一个区间(\(i\to2^{p-1}\))与当前区间(\(i+2^{p-1}\to2^{p-1}\))
对于查询,寻找最接近区间长度的一个2的p次幂,取从起点往后跳\(2^p\)长度与从终点往前跳\(2^p\)长度两者最大值的最大值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <bits/stdc++.h>
#define ios ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define endl '\n'
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
typedef pair<int,int> PII;
const int N = 2e5 + 10;
const double pi = acos(-1.0);
const int mod = 1e9 + 7;
const int INF = 0x3f3f3f3f;
const double eps = 1e-6;
int n,m;
vector<vector<int>> dp1;
vector<vector<int>> dp2;
int query1(int l, int r){
int j = (int)log2(r-l+1);
return max(dp1[l][j],dp1[r-(1<<j)+1][j]);
}
int query2(int l, int r){
int j = (int)log2(r-l+1);
return min(dp2[l][j],dp2[r-(1<<j)+1][j]);
}
int main()
{
ios
cin >> n >> m;
dp1 = vector<vector<int>> (n,vector<int>(log2(n)+5,0));
dp2 = vector<vector<int>> (n,vector<int>(log2(n)+5,0));
vector<int> arr(n);
for (int i = 0;i<n;i++) cin >> arr[i];
for (int i = 0;i<n;i++) {
dp1[i][0] = arr[i];
dp2[i][0] = arr[i];
}
for (int j = 1;j<=log2(n);j++){
for (int i = 0; i+(1<<j)-1<n;i++){
dp1[i][j] = max(dp1[i][j-1],dp1[i+(1<<(j-1))][j-1]);
dp2[i][j] = min(dp2[i][j-1],dp2[i+(1<<(j-1))][j-1]);
}
}
int l,r ;
while (m--){
cin >> l >> r;
cout << query1(l-1,r-1)-query2(l-1,r-1)<<endl;
}
return 0;
}树上倍增
在树结构中也可以实现倍增查找某节点任意层祖先
建立每个节点的深度表
使用st[i][p]指代节点i往上走\(2^p\)步来到哪个祖先,其中st[i][0]是i的父节点
则倍增思想可得st[i][p]=st[st[i][p-1]][p-1]
此时给定任意节点,就可以快速查询i节点的祖先中位于第s层的祖先节点编号
适用范围
适用于可重复贡献问题
即若A与B区间有可能重叠的部分,但不影响A+B区间的答案
如RMQ,gcd,按位与,按位或等都可以使用st表高效解决
优势:构建过程复杂度\(O(n\log n)\),单次查询\(O(1)\)
劣势:静态维护,需要空间大,维护信息有限