1 概念
ST表,一种离线算法,适用于可重复贡献问题,使用动态规划思想和倍增思想。
可重复贡献问题:每个元素会被重复用于求解答案,添加一些元素不会影响最后的答案。常见的有:
- 求区间最大值或最小值(RMQ)
- 求区间最大公约数(GCD)
ST表预处理复杂度 O ( n l o g n ) O(nlogn) O(nlogn) ,查询复杂度 O ( 1 ) O(1) O(1) 。
f ( i , j ) f(i,j) f(i,j):以 i i i为起点的区间长度为 2 j 2^j 2j个元素的最大值。 j j j就是倍增的次数。
2 实现
2.1 预处理
初始化: f ( i , 0 ) = a i f(i,0)=a_i f(i,0)=ai(以 i i i 为起点,区间长度为 2 0 = 1 2^0=1 20=1个元素,最大值一定是自己)
动态转移方程:
f ( i , j ) = m a x ( f ( i , j − 1 ) , f ( i + 2 j − 1 , j − 1 ) ) f(i,j)=max(f(i,j-1),f(i+2^{j-1},j-1)) f(i,j)=max(f(i,j−1),f(i+2j−1,j−1))
把求区间 [ i , i + 2 j ] [i,i+2^j] [i,i+2j] 最大值分成求 [ i , i + 2 j − 1 − 1 ] [i,i+2^{j-1}-1] [i,i+2j−1−1] 最大值和 [ i + 2 j − 1 , i + 2 j ] [i+2^{j-1},i+2^j] [i+2j−1,i+2j] 最大值的最大值,也就是求 f ( i , j − 1 ) f(i,j-1) f(i,j−1) 和 f ( i + 2 j − 1 , j − 1 ) f(i+2^{j-1},j-1) f(i+2j−1,j−1) 的最大值。
考虑到如果以 i i i 为遍历的第一层循环时,存在已知 f ( i , j − 1 ) f(i,j-1) f(i,j−1) 的值但 f ( i + 2 j − 1 , j − 1 ) f(i+2^{j-1},j-1) f(i+2j−1,j−1) 的值位置的情况,则改变预处理的策略:先遍历 j j j 再遍历 i i i。
j j j 和 i i i 的范围:
1 ≤ j ≤ l o g 2 n , 1 ≤ i ≤ n − 2 j + 1 1\le j\le log_2n, \ 1\le i\le n-2^j+1 1≤j≤log2n, 1≤i≤n−2j+1
for (int i = 1; i <= n; i++) f[i][0] = a[i];
int logn = log2(n);
for (int j = 1; j <= logn; j++) {
for (int i = 1; i + (1 << j) - 1 <= n; i++) {
f[i][j] = max(f1[i][j - 1], f1[i + (1 << (j - 1))][j - 1]);
}
}
2.2 查询
查询区间 [ l , r ] [l,r] [l,r] 公式:
令 k = l o g 2 ( r − l + 1 ) , m a x [ l , r ] = m a x ( f ( l , 2 k ) , f ( r − 2 k + 1 , k ) ) 令k=log_2(r-l+1), \ max[l,r]=max(f(l,2^k), f(r-2^k+1,k)) 令k=log2(r−l+1), max[l,r]=max(f(l,2k),f(r−2k+1,k))
把求区间 [ l , r ] [l,r] [l,r] 最大值分成求 [ l , l + 2 k − 1 ] [l,l+2^k-1] [l,l+2k−1] 最大值和 [ r − 2 k + 1 , r ] [r-2^k+1,r] [r−2k+1,r] 最大值的最大值(两个区间中间可能会有重复覆盖的情况,但由于是可重复贡献问题,不影响结果),也就是求 f ( l , k ) f(l,k) f(l,k) 和 f ( r − 2 k + 1 , k ) f(r-2^k+1,k) f(r−2k+1,k) 的最大值。
分程的两个区间一定覆盖所有元素,证明:
[
l
,
l
+
2
k
−
1
]
[l,l+2^k-1]
[l,l+2k−1] 元素个数
n
u
m
1
=
(
l
+
2
k
−
1
)
−
l
+
1
=
2
k
,
num_1=(l+2^k-1)-l+1=2^k,
num1=(l+2k−1)−l+1=2k,
[
r
−
2
k
+
1
,
r
]
[r-2^k+1,r]
[r−2k+1,r] 元素个数
n
u
m
2
=
r
−
(
r
−
2
k
+
1
)
+
1
=
2
k
,
num_2=r-(r-2^k+1)+1=2^k,
num2=r−(r−2k+1)+1=2k,
l
o
g
2
(
n
u
m
1
+
n
u
m
2
)
=
l
o
g
2
(
2
k
+
2
k
)
=
l
o
g
2
(
2
⋅
2
k
)
=
l
o
g
2
(
2
k
+
1
)
=
k
+
1
,
log_2(num_1+num_2)=log_2(2^k+2^k)=log_2(2\cdot 2^k)=log_2(2^{k+1})=k+1,
log2(num1+num2)=log2(2k+2k)=log2(2⋅2k)=log2(2k+1)=k+1,
[
l
,
r
]
[l,r]
[l,r] 元素个数
n
u
m
=
l
−
r
+
1
,
num=l-r+1,
num=l−r+1,
l
o
g
2
n
u
m
=
l
o
g
2
(
r
−
l
+
1
)
,
log_2num=log_2(r-l+1),
log2num=log2(r−l+1),
∵
k
=
l
o
g
2
(
r
−
l
+
1
)
,
\because k=log_2(r-l+1),
∵k=log2(r−l+1),
∴
k
+
1
>
l
o
g
2
(
r
−
l
+
1
)
,
\therefore k+1>log_2(r-l+1),
∴k+1>log2(r−l+1),
∴
n
u
m
1
+
n
u
m
2
>
n
u
m
.
\therefore num_1+num_2>num.
∴num1+num2>num.
int k = log2(r - l + 1);
int ans = max(f[l][k], f[r - (1 << k) + 1][k]);
3 例题
3.1 A Magic Lamp
题目描述
kiki喜欢旅行。 有一天她发现了一盏神灯,可惜神灯里的精灵并不那么善良。 kiki必须回答一个问题,然后精灵才能实现她的一个梦想。
问题是:给你一个整数,你可以精确地删除 m m m 位数字。 剩余的数字将形成一个新的整数。 你应该把它减到最小,并且不允许更改数字顺序。 现在你能帮助kiki实现她的梦想吗?
输入描述
有若干组测试用例。
每个测试用例将包含一个给定的整数(最多可以包含 1100 1100 1100 位数字。)和整数 m m m(如果整数包含 n n n 位数字,则 m m m 不会大于 n n n)。
给定的整数将不包含前导零。
输出描述
对于每种情况,在一行中输出您可以获得的最小结果。
如果结果包含前导零,则忽略它。
样例输入
178543 4
1000001 1
100001 2
12345 2
54321 2
样例输出
13
1
0
123
321
3.2 思路
删除 m m m 位,也就是保留 n − m n-m n−m 位。
根据鸽巢原理,保留的第一位数字 x 1 x_1 x1 一定在 [ 1 , m + 1 ] [1,m+1] [1,m+1] 中;
保留的第二位数字 x 2 x_2 x2 一定在 [ x 1 + 1 , m + 2 ] [x_1+1,m+2] [x1+1,m+2] 中;
……
f 定义为 pair 类型,存放当前这一位的数字和编号。
f ( i , j ) f(i,j) f(i,j):以 i i i为起点的区间长度为 2 j 2^j 2j位数字的最小值。
AC代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1100 + 10;
const int M = log2(N) + 10;
pair<char, int> f[N][M];
char s[N];
int m;
void build() { //预处理
int n = strlen(s);
for (int i = 1; i <= n; i++) f[i][0] = make_pair(s[i - 1], i); //初始化
for (int j = 1; (1 << j) <= n; j++) {
for (int i = 1; i + (1 << j) - 1 <= n; i++) {
f[i][j] = min(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]); //动态转移方程
}
}
}
int main() {
while (cin >> s >> m) {
build();
int l = 1, r = m + 1, tmp = strlen(s) - m;
bool flag = false;
while (tmp--) {
int k = log2(r - l + 1);
char ans1 = min(f[l][k], f[r - (1 << k) + 1][k]).first; //寻找保存的数字
int ans2 = min(f[l][k], f[r - (1 << k) + 1][k]).second;
if (ans1 != '0' || (ans1 == '0' && flag)) { //判断前导零
cout << ans1;
flag = true;
}
l = ans2 + 1;
r++;
}
if (!flag) cout << 0;
cout << "\n";
}
return 0;
}