算法竞赛进阶指南读书笔记——0x06倍增

倍增

应用:进行递推时,若状态空间过大,则只推出 2 k 2^k 2k位置上的值。当需要其它位置上的值时,利用任意整数可以表示为若干个2的次幂项之和的性质从 2 k 2^k 2k位置的值中拼出结果。

例1 给定长度为 N N N的数列 a a a,然后在线询问若干次,每次给定一个整数 T T T,求 k m a x   s . t . ∑ i = 1 k a i ⩽ T k_{max}\space s.t.\sum\limits_{i=1}^ka_i\leqslant T kmax s.t.i=1kaiT. ( 0 ⩽ T ⩽ ∑ i = 1 N a i ) (0\leqslant T\leqslant\sum\limits_{i=1}^Na_i) (0Ti=1Nai)

思路1:二分查找

{ a n } \{a_n\} {an}进行预处理求出前缀和 S n S_n Sn,然后二分 { S n } \{S_n\} {Sn} k m a x   s . t . S k ⩽ T k_{max}\space s.t.S_k\leqslant T kmax s.t.SkT.

由于每次查找平均花费 O ( l o g N ) O(logN) O(logN),当 k k k较小时不如直接遍历。

思路2:倍增+二进制划分

先考虑这样一个问题:如何将一个未知的数 n n n进行二进制分解?

若我们从低位开始分解,由于我们并不知道 n n n的值,无法确认当前位上的是0还是1,所以无法进行分解。

当我们从高位开始分解时,若找到 k 0   s . t . 2 k 0 > n k_0\space s.t.2^{k_0}>n k0 s.t.2k0>n,我们能确定 ∀ k ⩾ k 0 , 2 k ⩾ 2 k 0 > n \forall k\geqslant k_0,2^k\geqslant2^{k_0}>n kk0,2k2k0>n,即 n n n的第 k 0 k_0 k0位及以上都不可能是1。

k 0 k_0 k0向下寻找到第一个 k 1   s . t . 2 k 1 ⩽ n k_1\space s.t.2^{k_1}\leqslant n k1 s.t.2k1n,我们能确定 n n n的第 k 1 k_1 k1位一定是1。否则若 k 1 k_1 k1位不是1,那么 n ⩽ ∑ k = 0 k 1 − 1 2 k = 2 k 1 − 1 < 2 k 1 ⩽ n ⇒ n < n n\leqslant \sum\limits_{k=0}^{k_1-1}2^k=2^{k_1}-1<2^{k_1}\leqslant n\Rightarrow n<n nk=0k112k=2k11<2k1nn<n,矛盾!所以 n n n的第 k 1 k_1 k1位是1。

n n n中扣除 2 k 1 2^{k_1} 2k1后,问题转化为分解 n − 2 k 1 n-2^{k_1} n2k1,重复上述步骤即可将 n n n完全分解。

所以我们要构造一种能使得从高位进行二进制分解的情形。

为此,我们分两个阶段来进行:

阶段1:倍增

依次考虑 S 1 , S 1 + 2 1 , S 1 + 2 1 + 2 2 , … S_1,S_{1+2^1},S_{1+2^1+2^2},\dots S1,S1+21,S1+21+22,,直到出现 S ∑ i = 0 k 0 2 i > T S_{\sum_{i=0}^{k_0}2^i}>T Si=0k02i>T停止。此时 k m a x < ∑ i = 0 k 0 2 i k_{max}<\sum\limits_{i=0}^{k_0}2^i kmax<i=0k02i.令 k = ∑ i = 0 k 0 − 1 2 i k=\sum\limits_{i=0}^{k_0-1}2^i k=i=0k012i,则 k m a x − k < 2 k 0 k_{max}-k<2^{k_0} kmaxk<2k0,可对 k m a x − k k_{max}-k kmaxk进行二进制划分。

阶段2:二进制划分

依次考虑 S k + 2 k 0 − 1 , S k + 2 k 0 − 2 , … S_{k+2^{k_0-1}},S_{k+2^{k_0-2}},\dots Sk+2k01,Sk+2k02,,若 S k + 2 k 1 ⩽ T S_{k+2^{k_1}}\leqslant T Sk+2k1T,则相当于找到了 k m a x − k k_{max}-k kmaxk的二进制最高位 k 1 k_1 k1;继续考虑 S k + 2 k 1 + 2 k 1 − 1 , S k + 2 k 1 + 2 k 1 − 2 … S_{k+2^{k_1}+2^{k_1-1}},S_{k+2^{k_1}+2^{k_1-2}}\dots Sk+2k1+2k11,Sk+2k1+2k12,相当于对 k m a x − k − 2 k 1 k_{max}-k-2^{k_1} kmaxk2k1继续进行二进制分解。当 k m < 0 k_m<0 km<0时,分解完毕, k m a x = k + ∑ i = 1 m − 1 2 k i k_{max}=k+\sum\limits_{i=1}^{m-1}2^{k_i} kmax=k+i=1m12ki.

实现:

int p = 1, k = 0, sum = 0;
while (p) {
    if (k + p <= N && sum + S[k + p] - S[k] <= T)
        sum += S[k + p] - S[k], k += p, p <<= 1; // 1.
    else p >>= 1;
}

细节:

1.数组 a a a下标从1开始。 S r − S l S_r-S_l SrSl所求的是 ( l , r ] (l,r] (l,r]上的部分和,倍增过程即 ( 0 , 1 ] + ( 1 , 1 + 2 ] + ( 1 + 2 , 1 + 2 + 4 ] + ⋯ = ( 0 , 1 + 2 + 4 + ⋯ + 2 k 0 − 1 ] = [ 1 , k ] (0,1]+(1,1+2]+(1+2,1+2+4]+\dots=(0,1+2+4+\dots+2^{k_0-1}]=[1,k] (0,1]+(1,1+2]+(1+2,1+2+4]+=(0,1+2+4++2k01]=[1,k]

由于二进制划分过程中下标仍递增,所以两个阶段可以合并,如图:

zA0T4e.png

倍增与二分查找的比较:

:利于已遍历状态空间的扩增(求增)

二分查找:利于有序状态空间的搜索(求全)

例2 Genius ACM

思路:倍增+二路归并

先解决校验值。当 M = 1 M=1 M=1时,显然 S 校 验 = ( max ⁡ S − min ⁡ S ) 2 S_{校验}=(\max S-\min S)^2 S=(maxSminS)2.

M = 2 M=2 M=2时,参考 M = 1 M=1 M=1的情形将 S S S进行排序。若所取的数 S i S_i Si不为 S 1 , S 2 , S n − 1 , S n S_1,S_2,S_{n-1},S_n S1,S2,Sn1,Sn,那么 ∣ S i − S j ∣ < max ⁡ ( ∣ S 1 − S j ∣ , ∣ S 2 − S j ∣ , ∣ S n − 1 − S j ∣ , ∣ S n − S j ∣ ) |S_i-S_j|<\max(|S_1-S_j|,|S_2-S_j|,|S_{n-1}-S_j|,|S_n-S_j|) SiSj<max(S1Sj,S2Sj,Sn1Sj,SnSj).因此,我们只需考虑 S 1 , S 2 , S n − 1 , S n S_1,S_2,S_{n-1},S_n S1,S2,Sn1,Sn的组合方式即可。

[ ( S n − S 1 ) 2 + ( S n − 1 − S 2 ) 2 ] − [ ( S n − S 2 ) 2 + ( S n − 1 − S 1 ) 2 ] = 2 S 1 S n − 1 + 2 S 2 S n [(S_n-S_1)^2+(S_{n-1}-S_2)^2]-[(S_n-S_2)^2+(S_{n-1}-S_1)^2]=2S_1S_{n-1}+2S_2S_n [(SnS1)2+(Sn1S2)2][(SnS2)2+(Sn1S1)2]=2S1Sn1+2S2Sn − 2 S 1 S n − 2 S 2 S n − 1 = 2 S 2 ( S n − S n − 1 ) − 2 S 1 ( S n − S n − 1 ) = 2 ( S 2 − S 1 ) ( S n − S n − 1 ) > 0 -2S_1S_n-2S_2S_{n-1}=2S_2(S_n-S_{n-1})-2S_1(S_n-S_{n-1})=2(S_2-S_1)(S_n-S_{n-1})>0 2S1Sn2S2Sn1=2S2(SnSn1)2S1(SnSn1)=2(S2S1)(SnSn1)>0,所以 S 1 S_1 S1 S n S_n Sn组合, S 2 S_2 S2 S n − 1 S_{n-1} Sn1组合。

类似的,当 M > 2 M>2 M>2时我们可以进行类似的组合。

所以从头尾选数进行组合即可求得 S 校 验 S_{校验} S

此处的 S 校 验 S_{校验} S类似于上题的前缀和,我们可以采用相同的框架进行求解。

需要注意的一个细节是,求 S 校 验 S_{校验} S时,我们需要对目标区间 [ l , r ] [l,r] [l,r]进行排序。这里会出现两个问题,一是每次进行排序时需花费 O ( n l o g n ) O(nlogn) O(nlogn),加上 O ( l o g n ) O(logn) O(logn)的倍增次数,算法复杂度为 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n);二是排序会对原数组造成影响,例如判断 [ l , r + 2 k ] [l,r+2k] [l,r+2k]时对 [ l , r + 2 k ] [l,r+2k] [l,r+2k]进行了排序,若此时不符合条件,区间回退至 [ l , r + k ] [l,r+k] [l,r+k],但 [ l , r + k ] [l,r+k] [l,r+k]中混入 [ r + k + 1 , r + 2 k ] [r+k+1,r+2k] [r+k+1,r+2k]的元素导致错误。

第一个问题可以采用二路归并实现:当排序区间从 [ l , r ] → [ l , r + p ] [l,r]\rightarrow[l,r+p] [l,r][l,r+p]时,可以对 [ r + 1 , r + p ] [r+1,r+p] [r+1,r+p]进行排序,再将 [ l , r ] , [ r + 1 , r + p ] [l,r],[r+1,r+p] [l,r],[r+1,r+p]进行归并,即可得到 [ l , r + p ] [l,r+p] [l,r+p]的有序区间。每次有效排序(即产生扩增时的排序)长度 n i n_i ni满足 ∑ n i = n \sum n_i=n ni=n,故 ∑ n i l o g n i < ∑ n i l o g n = n l o g n \sum n_ilogn_i<\sum n_ilogn=nlogn nilogni<nilogn=nlogn,复杂度降为 O ( n l o g n ) O(nlogn) O(nlogn).

解决第二个问题需要三个数组: a [ ] , b [ ] , t e m p [ ] a[],b[],temp[] a[],b[],temp[],其中 a [ ] a[] a[]存储原数据, t e m p [ ] temp[] temp[]为归并辅助数组, b [ ] b[] b[]为部分有序数组。三个数组工作如下:

扩增过程: [ l , r + p ] [l,r+p] [l,r+p]符合条件,区间即从 [ l , r ] [l,r] [l,r]扩增到 [ l , r + p ] [l,r+p] [l,r+p].由于右端点只增不减,所以可以确定将有序的 [ l , r + p ] [l,r+p] [l,r+p]存入 b [ ] b[] b[]不会导致后面元素的混入。

需要注意的是,我们不能仅将有序的 [ r + 1 , r + p ] [r+1,r+p] [r+1,r+p]直接放入 b [ ] b[] b[]中,因为对 [ l , r + p ] [l,r+p] [l,r+p]整体排序后, [ l , r ] [l,r] [l,r]部分也发生了改变,需将 [ l , r + p ] [l,r+p] [l,r+p]全部拷入。

试探过程:我们期望得到 [ l , r + p 0 ] [l,r+p_0] [l,r+p0]的有序序列,而这次扩增是在 [ l , r ] [l,r] [l,r]基础上的,所以 b [ ] b[] b[] [ l , r ] [l,r] [l,r]部分是有序的。为了实现二路归并的效果,我们应该把 a [ r + 1 , r + p 0 ] a[r+1,r+p_0] a[r+1,r+p0]拷入 b [ ] b[] b[]中,并对 b [ ] b[] b[]中该部分进行排序,再由归并后进入 t e m p [ ] temp[] temp[]中。

此时 t e m p [ ] temp[] temp[]中持有 [ l , r + p 0 ] [l,r+p_0] [l,r+p0]的有序序列,所以可以直接在 t e m p [ ] temp[] temp[]中求取 S 校 验 S_{校验} S.而传统的归并排序要求我们将 t e m p [ ] temp[] temp[]拷入 b [ ] b[] b[]中,但让未经确认的 [ l , r + p 0 ] [l,r+p_0] [l,r+p0]进入 b [ ] b[] b[]会使得 [ r + 1 , r + p 0 ] [r+1,r+p_0] [r+1,r+p0]的元素混入 [ l , r ] [l,r] [l,r]中,从而产生未确定元素对已确定元素的破坏。

回到扩增过程,有序的 [ l , r + p ] [l,r+p] [l,r+p]的来源即是 t e m p [ ] temp[] temp[],在试探过程中已经排好序放入 t e m p [ ] temp[] temp[]中,可以在试探成功后直接取用。

实现:

int a[N], b[N], temp[N], m, n;
long long t;
bool check(int l, int mid, int r) {
  if (r >= n)
    return 0;
  for (int i = mid + 1; i <= r; i++)
    b[i] = a[i];
  sort(b + mid + 1, b + r + 1); // STL区间左闭右开
  int i = l, j = mid + 1; // 二路归并
  for (int pos = l; pos <= r; pos++)
    if (j > r || (i <= mid && b[i] < b[j]))
      temp[pos] = b[i++];
    else
      temp[pos] = b[j++];
  long long res = 0;
  if (r - l + 1 <= 2 * m) // 数不够选
    for (int i = 0; 2 * i < r - l; i++) // l + i < r - i
      res += pow((temp[l + i] - temp[r - i]), 2);
  else
    for (int i = 0; i < m; i++)
      res += pow((temp[l + i] - temp[r - i]), 2);
  return res <= t;
}
int main() {
  int k, p, ans, l, r;
  cin >> k;
  while (k--) {
    cin >> n >> m >> t;
    for (int i = 0; i < n; i++)
      cin >> a[i];
    ans = 0, l = 0, r = 0;
    while (r < n) {
      p = 1, b[r] = a[r]; // 1.
      while (p) {
        if (check(l, r, r + p)) {
          r += p, p <<= 1;
          for (int i = l; i <= r; i++)
            b[i] = temp[i];
        } else
          p >>= 1;
      }
      ans++, l = ++r; // 1.
    }
    cout << ans << endl;
  }
  return 0;
}

细节:

1.当产生新的分段时, [ l 1 , r 1 ] [l_1,r_1] [l1,r1]划分结束, l 2 = r 1 + 1 , r 2 = l 2 l_2=r_1+1,r_2=l_2 l2=r1+1,r2=l2开始新的划分,但在试探过程中,我么假定了 [ l , r ] [l,r] [l,r]是有序序列,而有序序列应被拷入 b [ ] b[] b[]中,所以每一次开始新的分段时,需将 a [ l ] ( a [ r ] ) a[l](a[r]) a[l](a[r])拷入 b [ ] b[] b[]中以保证正确性。

ST算法

应用:求解RMQ问题

RMQ问题:给定长度为 N N N的数列 a a a,在线回答数组 a a a中下标在 l ∼ r l\sim r lr之间的数的最大值为多少

思路:倍增

根据倍增思想,我们希望引入成倍增长的量,易见区间长度是很好的选择。为了描述各个区间,规定长度后还需规定起点,所以我们将起点长度作为状态描述的参量。

定义状态 F [ i ] [ j ] ≜ max ⁡ i ⩽ k ⩽ i + 2 j − 1 a k F[i][j]\triangleq \max\limits_{i\leqslant k\leqslant i+2^j-1}a_k F[i][j]iki+2j1maxak,即从 a i a_i ai开始区间长度为 2 j 2^j 2j i + 2 j − 1 − i + 1 = 2 j i+2^j-1-i+1=2^j i+2j1i+1=2j)的区间的最大值。边界为 F [ i ] [ 0 ] = a i F[i][0]=a_i F[i][0]=ai.

接着考虑状态转移,考虑到最大值的性质:两个区间合起来的最大值等于两个区间各自的最大值的最大值,所以 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][j1],F[i+2j1][j1]}(前半个区间到 i + 2 j − 1 − 1 i+2^{j-1}-1 i+2j11,后半个区间从 i + 2 j − 1 i+2^{j-1} i+2j1开始)

状态空间大小为 n ∗ l o g n n*logn nlogn,故预处理 F [ n ] [ l o g n ] F[n][logn] F[n][logn]的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn).

接着考虑查询过程。由于两个区间合起来的最大值等于两个区间各自的最大值的最大值,所以我们可以将待查区间分为两段(重合不影响合区间,只需要求两段区间的合将待查询区间覆盖即可)。结合我们定义的状态,我们可以考虑使用 F [ l ] [ k ] F[l][k] F[l][k] F [ r − 2 k + 1 ] [ k ] F[r-2^k+1][k] F[r2k+1][k]进行查询,如图:

zAWtOg.png

故要求 { 2 k ⩽ l − r + 1 , 2 ⋅ 2 k ⩾ l − r + 1 \left\{\begin{array}{ll}2^k\leqslant l-r+1, \\2\cdot2^k\geqslant l-r+1\end{array}\right. {2klr+1,22klr+1 k = ⌊ l o g 2 ( l − r + 1 ) ⌋ k=\lfloor log_2(l-r+1)\rfloor k=log2(lr+1)即可( p − 1 ⩽ p − 1 < ⌊ p ⌋ ⩽ p p-1\leqslant p-1<\lfloor p\rfloor\leqslant p p1p1<pp)。

实现:

for (int i = 1; i <= n; i++) f[i][0] = a[i];
int t = log[n]; // 1.
for (int j = 1; j <= t; j++)
    for (int i = 1; i <= n - (1 << j) + 1; i++) // 2.
        f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);

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

细节:

1.此处使用预处理的 l o g log log表计算 l o g 2 n log_2n log2n,预处理 l o g log log表实现如下:

for (int i = 0; i <= n; i++)
    for (int j = 1 << i; j < 1 << (i + 1); j++) log[j] = i;

∀ n ∈ [ 2 i , 2 i + 1 ) , n ∈ N ∗ , ⌊ l o g 2 n ⌋ = i \forall n\in[2^{i},2^{i+1}),n\in\N^*,\lfloor log_2n\rfloor=i n[2i,2i+1),nN,log2n=i.

或者可以使用 c m a t h cmath cmath库的 l o g log log函数,该函数以10为底。由换底公式知, l o g 2 n = l g n l g 2 log_2n=\dfrac{lgn}{lg2} log2n=lg2lgn,故 l o g [ n ] = l o g ( n ) / l o g ( 2 ) log[n]=log(n)/log(2) log[n]=log(n)/log(2)

2.状转方程右边的 j − 1 j-1 j1小于左边的 j j j,所以需要按照 j j j递增的顺序进行枚举,故将 j j j放在外层循环;递推 i i i时需判断边界: F [ i ] [ j ] F[i][j] F[i][j]本身含义是 [ i , i + 2 j − 1 ] [i,i+2^j-1] [i,i+2j1],故 i + 2 j − 1 ⩽ n ⇔ i ⩽ n − 2 j + 1 i+2^j-1\leqslant n\Leftrightarrow i\leqslant n-2^j+1 i+2j1nin2j+1

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值