算法竞赛进阶指南读书笔记——0x03前缀和与差分

前缀和与差分

前缀和

定义: S n = ∑ i = 1 n a i S_n=\sum\limits_{i=1}^n a_i Sn=i=1nai

应用:求部分和

s u m ( l , r ) = ∑ i = l r a i = S r − S l − 1 sum(l,r)=\sum\limits_{i=l}^ra_i=S_r-S_{l-1} sum(l,r)=i=lrai=SrSl1

O ( n ) O(n) O(n)预处理前缀和, O ( 1 ) O(1) O(1)查询部分和


例1激光炸弹

思路:二维前缀和

即要求最大的二维部分和,故可以先预处理出二维前缀和,再遍历所有的二维部分和。

先求二维前缀和,记 S m , n = ∑ i = 1 m ∑ j = 1 n a i , j S_{m,n}=\sum\limits_{i=1}^m\sum\limits_{j=1}^na_{i,j} Sm,n=i=1mj=1nai,j.

仿照一维前缀和,我们有 a n = S n − S n − 1 ⇔ S n = S n − 1 + a n a_n=S_n-S_{n-1}\Leftrightarrow S_n=S_{n-1}+a_n an=SnSn1Sn=Sn1+an,利用递推初始化 S n S_n Sn;类似的,我们可以利用递推初始化 S m , n S_{m,n} Sm,n,即寻找 S m , n S_{m,n} Sm,n S m − 1 , n , S m , n − 1 , S m , n , a m , n S_{m-1,n},S_{m,n-1},S_{m,n},a_{m,n} Sm1,n,Sm,n1,Sm,n,am,n之间的关系,如下图所示:

二维前缀和

写作数学表达式即为:

S m , n = S m − 1 , n + S m , n − 1 − S m − 1 , n − 1 + a m , n S_{m,n}=S_{m-1,n}+S_{m,n-1}-S_{m-1,n-1}+a_{m,n} Sm,n=Sm1,n+Sm,n1Sm1,n1+am,n,需 O ( m ∗ n ) O(m*n) O(mn)时间完成初始化。

再利用二维前缀和求二维部分和,利用部分和区域的四个顶点,如下图所示:

二维部分和

写作数学表达式即为:

∑ i = i 1 i 2 ∑ j = j 1 j 2 a i , j = S i 2 , j 2 − S i 2 , j 1 − 1 − S i 1 − 1 , j 2 + S i 1 − 1 , j 1 − 1   ( ∗ ) \sum\limits_{i=i_1}^{i_2}\sum\limits_{j=j_1}^{j_2}a_{i,j}=S_{i_2,j_2}-S_{i_2,j_1-1}-S_{i_1-1,j_2}+S_{i_1-1,j_1-1}\ (*) i=i1i2j=j1j2ai,j=Si2,j2Si2,j11Si11,j2+Si11,j11 ()

回到本题,我们假设爆炸范围右下角坐标为 ( i , j ) (i,j) (i,j),那么左上角坐标为 ( i − R + 1 , j − R + 1 ) (i-R+1,j-R+1) (iR+1,jR+1),如图:

爆炸范围

代入 ( ∗ ) (*) ()得:

s u m ( i , j ) = S i , j − S i , j − R − S i − R , j + S i − R , j − R sum(i,j)=S_{i,j}-S_{i,j-R}-S_{i-R,j}+S_{i-R,j-R} sum(i,j)=Si,jSi,jRSiR,j+SiR,jR

这里有一个细节需要注意,即题中所给 ( X i , Y i ) (X_i,Y_i) (Xi,Yi)为格点坐标而非格子。我们可以将其视为坐标为 ( X i , Y i ) (X_i,Y_i) (Xi,Yi)格子的中点,而爆炸边界位于格子边线,如图:

vFCus1.png

此时(黑色边界)能容纳的格点数最多;反之,若选择格点边线做爆炸边界(灰色边界),则因边界上目标不会被摧毁而使容纳量降低。

此时将格点视作其所在的格子,即转化为上述分析所对应的情形。

实现:

#include <bits/stdc++.h>
using namespace std;
const int N = 5e3 + 5;
int s[N][N];
int main() {
  int n, r, x, y, w, max_x = 0, max_y = 0, ans = -1;
  cin >> n >> r;
  r = min(r, 5001); // 特判全覆盖;0化为1,5000化为5001
  for (int i = 0; i < n; i++) {
    cin >> x >> y >> w;
    s[x + 1][y + 1] += w; // 坐标从0开始
    max_x = max(max_x, x + 1), max_y = max(max_y, y + 1);
  }
  max_x = max(max_x, r), max_y = max(max_y, r); // 1.
  for (int i = 1; i <= max_x; i++)
    for (int j = 1; j <= max_y; j++)
      s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + s[i][j]; // 2.
  for (int i = r; i <= max_x; i++)
    for (int j = r; j <= max_y; j++)
      ans = max(ans, s[i][j] - s[i][j - r] - s[i - r][j] + s[i - r][j - r]);
  cout << ans << endl;
  return 0;
}

细节:

1.前缀和计算范围:先考虑上界:

(1)包含所有有点区域,即 x ⩾ max ⁡ X i , y ⩾ max ⁡ Y i x\geqslant\max X_i,y\geqslant\max Y_i xmaxXi,ymaxYi.(2)满足贴边的爆炸区域,即 x , y ⩾ R x,y\geqslant R x,yR,如下图所示:

vFACUP.png

max ⁡ X i ⩽ R \max X_i\leqslant R maxXiR时,在x方向上为了尽可能的纳入更多的格子,爆炸范围应贴边摆放;此时应保证贴边时右下角的前缀和完成初始化,否则无法得到正确的结果。

m a x X i > R max X_i>R maxXi>R时,在x方向上为了不纳入空白格子,爆炸范围下边界不会超出 m a x X i max X_i maxXi.

再考虑下界:

S 1 , 1 = a 1 , 1 S_{1,1}=a_{1,1} S1,1=a1,1,故此时仅 S 1 , 1 S_{1,1} S1,1完成初始化,而 S 1 , j , S i , 1 S_{1,j},S_{i,1} S1,j,Si,1并未完成,故需从 ( 1 , 1 ) (1,1) (1,1)开始初始化。

综上,前缀和计算范围为 { 1 ⩽ x ⩽ max ⁡ X i , 1 ⩽ y ⩽ max ⁡ Y i \left\{\begin{array}{ll}1\leqslant x\leqslant \max X_i,\\1\leqslant y\leqslant\max Y_i\end{array}\right. {1xmaxXi,1ymaxYi

2.此题所给空间不够两个数组,故将 a i , j a_{i,j} ai,j直接存在 S i , j S_{i,j} Si,j中,由于前缀和计算时右边到左边 i , j i,j i,j递增,故可保证 a i , j a_{i,j} ai,j不会被提前抹去,且 S i , j S_{i,j} Si,j计算顺序准确,从而可得到正确的 S i , j S_{i,j} Si,j.

差分

定义: b 1 = a 1 , b i = a i − a i − 1 ( 2 ⩽ i ⩽ n ) b_1=a_1,b_i=a_i-a_{i-1}(2\leqslant i\leqslant n) b1=a1,bi=aiai1(2in)

性质:与前缀和互为逆运算

略证: Δ S i = S i − S i − 1 = a i , ∑ i = 1 n Δ i = a 1 + ∑ i = 2 n ( a i − a i − 1 ) = a 1 + a n − a 1 = a n \Delta_{S_i}=S_i-S_{i-1}=a_i,\sum\limits_{i=1}^n\Delta_i=a_1+\sum\limits_{i=2}^n(a_i-a_{i-1})=a_1+a_n-a_1=a_n ΔSi=SiSi1=ai,i=1nΔi=a1+i=2n(aiai1)=a1+ana1=an

应用:将原序列上的区间操作转化为差分序列上的单点操作,即 a i . . j + d ⇔ Δ i + d , Δ j + 1 − d a_{i..j}+d\Leftrightarrow \Delta_i+d,\Delta_{j+1}-d ai..j+dΔi+d,Δj+1d

略证:

对于 Δ 1.. i − 1 \Delta_{1..i-1} Δ1..i1,由于 a 1.. i − 1 a_{1..i-1} a1..i1不变,无影响;

对于 Δ i \Delta_i Δi,由于 a i + d a_i+d ai+d a i − 1 a_{i-1} ai1不变,故 Δ i + d \Delta_i+d Δi+d

对于 Δ i + 1.. j \Delta_{i+1..j} Δi+1..j,由于 a i . . j + d a_{i..j}+d ai..j+d,故 Δ i + 1.. j \Delta_{i+1..j} Δi+1..j不变;

对于 Δ j + 1 \Delta_{j+1} Δj+1,由于 a j + d a_j+d aj+d a j + 1 a_{j+1} aj+1不变,故 Δ j + 1 − d \Delta_{j+1}-d Δj+1d

特别的,当j=n时,此时 Δ n + 1 \Delta_{n+1} Δn+1无意义,可设为任意值


例2 IncDec Sequence

思路:差分

容易想到将区间操作转化为单点操作。题中操作即转化为选择差分序列中的两个数,一个加一,一个减一,使得 Δ 2.. n = 0 \Delta_{2..n}=0 Δ2..n=0.

由于题目条件要求操作次数最少,故优先在 Δ 2.. n \Delta_{2..n} Δ2..n中操作。设其中正数之和为 p p p,负数之和为 − q -q q,两两配对并操作至无法配对,共操作 min ⁡ { p , q } \min\{p,q\} min{p,q}次。

接着处理 Δ 2.. n \Delta_{2..n} Δ2..n中剩余非0项。一方面,我们可以利用 Δ 1 \Delta_1 Δ1,它不在目标要求之中;另一方面,我们可以利用 Δ n + 1 \Delta_{n+1} Δn+1,它没有定义,可任意设值。因此,我们可以将剩余非零项与 Δ 1 , Δ n + 1 \Delta_1,\Delta_{n+1} Δ1,Δn+1配对并使之归零,需操作 ∣ p − q ∣ |p-q| pq次。

综上,至少操作 m i n { p , q } + ∣ p , q ∣ = m a x { p , q } min\{p,q\}+|p,q|=max\{p,q\} min{p,q}+p,q=max{p,q}次。

值得注意的是,我们不会将 Δ 1 , Δ n + 1 \Delta_1,\Delta_{n+1} Δ1,Δn+1配对,它对目标达成无任何贡献;从区间操作的角度来看,相当于将 a 1.. n a_{1..n} a1..n同时变化,显然无效。

换言之,中间的数受到其左右两边数大小的限制,而首尾两数只受其一边数大小的限制,我们先处理限制强的,再利用限制松的进行调整。

再考虑最终数列的种数。导致答案出现多解的根源在于处理 Δ 2.. n \Delta_{2..n} Δ2..n中剩余非0项时利用 Δ 1 , Δ n + 1 \Delta_1,\Delta_{n+1} Δ1,Δn+1情况不同。因此,解的个数即为将 ∣ p − q ∣ |p-q| pq拆成两数的组数,为 ∣ p − q ∣ + 1 |p-q|+1 pq+1组。

实现:

cin >> n >> pre;
for (int i = 1; i < n; i++) {
  cin >> cur;
  d = cur - pre; // 差分值仅取决与前驱值和当前值,可使用两个变量节省空间
  d > 0 ? p += d : q -= d;
  pre = cur;
}
cout << max(p, q) << endl; 
cout << abs(p - q) + 1 << endl;

例3 Tallest Cow

思路:差分

先证任意区间不会交叉,否则对于编号为 a , b , c , d a,b,c,d a,b,c,d的4头牛(其中 a < b < c < d a<b<c<d a<b<c<d)且 a , c a,c a,c b , d b,d b,d互相看见,由 b , d b,d b,d互相看见 ⇒ c < b \Rightarrow c<b c<b,与 b < c b<c b<c矛盾!所以不存在交叉区间。

我们可以将牛的初始高度看作等高(为0),然后通过降低部分牛的身高使其满足条件。

对于第一组关系,为了使 A , B A,B A,B能互相看见,我们将 A , B A,B A,B中间的所有牛身高降低一格,为这些牛的最高身高,如图:

第一组关系

由于我们已经证明了任意两个区间不会交叉,故与 A , B A,B A,B有关的区间与 A , B A,B A,B形成嵌套关系。

A ′ , B ′ A',B' A,B A , B A,B A,B内,直接将 A ′ , B ′ A',B' A,B间的牛身高降低一格,如图:

内嵌套

这样既满足了 A ′ , B ′ A',B' A,B的要求,又满足了 A , B A,B A,B的要求。

A ′ , B ′ A',B' A,B A , B A,B A,B内,类似的,将 A ′ , B ′ A',B' A,B间的牛身高降低一格,如图:

外嵌套

若我们只降低 A , B A,B A,B的高度,而不降低 A , B A,B A,B中间的高度,尽管 A ′ , B ′ A',B' A,B的关系得到满足,但 A , B A,B A,B关系被破坏,原因在于 A . . B A..B A..B构成一个满足条件的整体,只修改 A . . B A..B A..B的端点会破坏该整体,从而使 A , B A,B AB不再满足条件;反过来,将它们全部降低,此时 A . . B A..B A..B作为整体整体降低一格,关系得到保留。

对于区间操作,容易想到用差分降低时间复杂度;我们可以维护奶牛身高数列的差分数列,每次降低身高时在区间端点进行操作,最后再求前缀和还原出奶牛的身高。

实现:

for (int i = 0; i < m; i++) {
  cin >> A >> B;
  if (A > B) // 保证A..B中A<B
    swap(A, B);
  if (!vis[A][B]) // 去重
    b[A + 1]--, b[B]++, vis[A][B] = 1; // 1.
}
for (int i = 1; i <= n; i++) {
  s += b[i];
  cout << s + h << endl; // 2.
}

细节:

1.此处用差分维护区间操作:将 A + 1.. B − 1 A+1..B-1 A+1..B1全部减一,等价于 Δ A + 1 − 1 , Δ B + 1 \Delta_{A+1}-1,\Delta_B+1 ΔA+11,ΔB+1

2.由于不会有跨过最高牛的区间,故前缀和还原后的数列中最高牛对应高度为0,所以需将所有牛加上高度H得到真正的最大高度

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
算法竞赛进阶指南》是一本进阶级别的书籍,不适合初学者阅读。根据引用中的描述,每一章都会总结书中的难点知识,并附上例题和习题。从引用的目录可以看出,《算法竞赛进阶指南》包含了基本算法、位运算、递推与递归、前缀和差分、二分、排序、倍增、贪心等内容,还包括了基本数据结构如栈、队列、链表、Hash、字符串、Trie、二叉堆等。此外,书中还讲解了动态规划的各种子领域,如线性dp、背包、区间dp、树形dp、状态压缩dp等。对于想要深入学习算法竞赛的读者来说,《算法竞赛进阶指南》是一本很好的参考书籍。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [【算法竞赛进阶指南】学习笔记](https://blog.csdn.net/cpp_juruo/article/details/122520206)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *3* [算法竞赛进阶指南总结(一)](https://blog.csdn.net/weixin_64393298/article/details/124234703)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值