目录
倍增,就是成倍增长。指我们进行递推时,如果状态空间很大,通常线性递推无法满足时空复杂度要求,那么可以通过成倍增长的方式,只递推状态空间中 2 2 2 的整数次幂位置的值为代表。
对于其他位置上的值时,我们通过 “任意整数可以表示成若干个 2 2 2 的次幂项的和” 这一性质,使用之前求出的代表值拼成所需的值。
使用倍增算法要求递推的问题的状态空间关于 2 2 2 的次幂具有可划分性。
我们来看最基本的用法,问题:
给定一个长度为 N N N 的数列 A A A ,然后进行若干次询问,每次给定一个整数 T T T ,求出最大的 k k k ,满足 ∑ i = 1 N A [ i ] ≤ T \sum_{i = 1}^N A[i] \le T ∑i=1NA[i]≤T 。你的算法必须是在线的(必须及时回答每一个询问,不能等待收到所有询问后再统一处理)。
假设 0 ≤ T ≤ ∑ i = 1 N A [ i ] 0\le T\le \sum_{i = 1}^{N} A[i] 0≤T≤∑i=1NA[i] 。
分析:
暴力做法就是每次从前向后枚举 k k k 。
也可以使用前缀和预处理后,每次二分查询时间复杂度 O ( l o g ( n ) ) O(log(n)) O(log(n)) 。不过如果 T T T 很小,那么二分的速度还不如从前向后推。
倍增,就可以即使 l o g ( n ) log(n) log(n) 处理,也可以更快的处理 T T T 很小的情况。
我们设 p = 1 , k = 0 , s u m = 0 , S [ i ] p = 1, k = 0, sum = 0, S[i] p=1,k=0,sum=0,S[i] 是 A A A 数组的前缀和,每次我们比较 “ A A A 数组中 k k k 之后的 p p p 个数的和” 与 T T T 的关系,如果 s u m + S [ k + p ] − S [ k ] ≤ T sum + S[k + p] -S[k] \le T sum+S[k+p]−S[k]≤T ,则 s u m + = S [ k + p ] − S [ k ] sum += S[k + p] - S[k] sum+=S[k+p]−S[k], k + = p k+=p k+=p , p ∗ = 2 p ~*= 2 p ∗=2 ,相当于这个 p p p 个数可以加入进来, k k k 更新长度, p p p 增长一倍。
上述的转化为代码就是:
int p = 1, k = 0, sum = 0;
while (p > 0) {
// 需要判断是否越界
if(k + p <= n && sum + S[k + p] - S[k] <= T) {
sum += S[k + p] - S[k];
k += p;
p <<= 1;
} else p >>= 1
}
cout << sum << endl;
【例题】天才ACM
给定一个整数 M M M,对于任意一个整数集合 S S S,定义 “校验值” 如下:
从集合 S S S 中取出 M M M 对数(即 2 × M 2\times M 2×M 个数,不能重复使用集合中的数,如果 S S S 中的整数不够 M M M 对,则取到不能取为止),使得“每对数的差的平方”之和最大,这个最大值就称为集合 S S S 的“校验值”。
现在给定一个长度为 N N N 的数列 A A A 以及一个整数 T T T。
我们要把 A A A 分成若干段,使得每一段的“校验值”都不超过 T T T。
求最少需要分成几段。
分析:
先不考虑分成几段,就一段中取 M M M 对数,怎么取、怎么配对才是这个集合 S S S 的校验值?
我们举 4 4 4 个数的例子,假设这 4 4 4 个数分别是 a , b , c , d a,b,c,d a,b,c,d ,不妨设 a ≤ b ≤ c ≤ d a \le b\le c \le d a≤b≤c≤d ,假设要取 2 2 2 对数,显然有两种配对方式:
- a a a 与 b b b 配对, c c c 与 d d d 配对;
- a a a 与 d d d 配对, b b b 与 c c c 配对;
对于第一种最后的值为
(
a
−
b
)
2
+
(
c
−
d
)
2
=
a
2
+
b
2
+
c
2
+
d
2
−
2
a
b
−
2
c
d
−
4
(a - b)^2 + (c - d)^2 = a^2 + b^2+c^2+d^2-2ab-2cd-4
(a−b)2+(c−d)2=a2+b2+c2+d2−2ab−2cd−4
对于第二种最后的值为
(
a
−
d
)
2
+
(
b
−
c
)
2
=
a
2
+
b
2
+
c
2
+
d
2
−
2
a
d
−
2
b
c
−
4
(a - d)^2 + (b - c)^2 = a^2 + b^2+c^2+d^2-2ad-2bc-4
(a−d)2+(b−c)2=a2+b2+c2+d2−2ad−2bc−4
很显然 ( a − d ) 2 + ( b − c ) 2 ≥ ( a − b ) 2 + ( c − d ) 2 (a - d)^2 + (b - c)^2 \ge (a - b)^2 + (c - d)^2 (a−d)2+(b−c)2≥(a−b)2+(c−d)2 。所以最后的配对方式我们可以大胆猜测就是 最大与最小、次大与次小 ⋯ \cdots ⋯ 。
解决了集合的 “校验值” 计算问题后,我们来看怎么分段?
学了二分很显然这个问题可以通过二分答案转化判定问题来找出最小分段数。最终时间复杂度就是 O ( n 2 × l o g ( n ) ) O(n^2\times log(n)) O(n2×log(n)) 。
使用倍增则可以将复杂度降为 O ( n × l o g 2 ( n ) ) O(n \times log^2(n)) O(n×log2(n)) ,使用归并排序更可以降低到 O ( n × l o g ( n ) ) O(n \times log(n)) O(n×log(n)) 。这里我就使用了 O ( n × l o g 2 ( n ) ) O(n \times log^2(n)) O(n×log2(n)) 的算法。
和上面的一样,我们得有变量 p p p 表示倍增的量,以及保存当段的变量 L L L 和 R R R 。
- 最初
p = 1
,R = L = 1
(数组以 1 1 1 开始的)。 - 求出 [ L , R + p ] [L, R + p] [L,R+p] 这段区间的 “校验值” ,若 “校验值” ≤ T \le T ≤T ,则 R + = p , p ∗ = 2 R += p,p*=2 R+=p,p∗=2 ,否则 p / = 2 p ~/=2 p /=2 。
- 重复上一步,直到 p p p 的值为 0 0 0 ,此时 R R R 即为这一段最大划分。
最后 R > n R > n R>n 后,就说明所有段都划分好了,输出答案。
代码如下:
include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 500010;
int n, m;
LL T;
LL a[N], temp[N];
bool check(int l, int r) {
int cnt = 0;
for(int i = l; i <= r; ++i)
temp[cnt++] = a[i];
sort(temp, temp + cnt);
LL res = 0;
for (int i = 0, j = cnt - 1; i < m && i < j; i ++, j-- ) {
res += (temp[i] - temp[j]) * (temp[i] - temp[j]);
}
return res <= T;
}
int main()
{
int t;
scanf("%d", &t);
while (t -- ) {
scanf("%d%d%lld", &n, &m, &T);
for (int i = 1; i <= n; i ++ )
scanf("%lld", a + i);
int L = 1, R = 1;
int ans = 0;
while(R <= n) {
int p = 1;
while(p > 0) {
if(R + p <= n && check(L, R + p)) {
R += p;
p <<= 1;
} else p >>= 1;
}
ans++;
L = R + 1;
R = L;
}
printf("%lld\n", ans);
}
return 0;
}
ST 算法
给定一个长度为 N N N 的数列 A A A , S T ST ST 算法能在 O ( N l o g ( N ) ) O(N~log(N)) O(N log(N)) 时间的预处理后,以 O ( 1 ) O(1) O(1) 的时间复杂度在线回答 “数列 A A A 中下标在 l l l ~ r r r 之间的数的最大值是多少” 这样的区间最值问题。
一个序列的子区间个数有 N 2 N^2 N2 个,根据倍增的思想,我们首先在这个空间规模为 N 2 N^2 N2 的状态空间里选择一些 2 2 2 的整数次幂的位置作为代表值。
设 F [ i , j ] F[i,j] F[i,j] 表示数列 A A A 中下标在子区间 [ i , i + 2 j − 1 ] [i, i + 2^j - 1] [i,i+2j−1] 里的数的最大值,也就是从 i i i 开始的 2 j 2^j 2j 个数的最大值。递推边界是 F [ i , 0 ] = A [ i ] F[i,0] = A[i] F[i,0]=A[i] 。
递推过程很显然就是 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 + 2^{j - 1},j-1]) F[i,j]=max(F[i,j−1],F[i+2j−1,j−1]) ,也就是以 i i i 为起点的子区间的最大值是左右两半长度为 2 j − 1 2^{j - 1} 2j−1 的子区间的最大值中较大的一个。
void ST_init() {
for(int i = 1; i <= n; ++i) f[i][0] = a[i];
int t = log(n) / log(2) + 1;
for(int j = 1; j < t; ++j)
for(int i = 1; i <= n - (1 << j) + 1; ++i) {
f[i][j] = max(f[i][j - 1], f[i + (1<<(j - 1))][j - 1]);
}
}
预处理后,对于任意区间 [ l , r ] [l, r] [l,r] 的最值查询时,我们先计算出一个 k k k ,满足 2 k ≤ r − l + 1 < 2 k + 1 2^k\le r - l + 1 < 2 ^ {k + 1} 2k≤r−l+1<2k+1 ,也就是使 2 2 2 的 k k k 次幂小于区间长度前提下最大的 k k k 。那么 “从 l l l 开始的 2 k 2^k 2k 个数” 和 “以 r r r 结尾的 2 k 2^k 2k 个数” 这两段一定覆盖了整个区间 [ l , r ] [l, r] [l,r] ,这两段的最大值分别是 F [ l , r ] F[l,r] F[l,r] 和 F [ r − 2 k − 1 , k ] F[r-2^k - 1,k] F[r−2k−1,k],二者中较大的那个就是区间 [ l , r ] [l,r] [l,r] 的最值。
int ST_query(int l, int r) {
int k = log(r- l + 1) / log(2);
return max(f[l][k], f[r - (1 << k) + 1][k]);
}
此处的 log(n)
,如果为了更好的时间复杂度,应该预处理出
l
o
g
log
log 数组。