参考《算法竞赛进阶指南》、AcWing题库

前缀和与差分

前缀和

对于一个给定的数列 AA, 它的前缀和数列 SS 是通过递推能求出的基本信息之一:

S[i]=∑j=1iA[j]S[i]=\sum_{j=1}^{i} A[j]

一个部分和, 即数列 AA 某个下标区间内的数的和, 可表示为前缀和相减的形式:

sum⁡(l,r)=∑i=1rA[i]=S[r]−S[l−1]\operatorname{sum}(l, r)=\sum_{i=1}^{r} A[i]=S[r]-S[l-1]

在二维数组 (矩阵) 中, 可类似地求出二维前缀和, 进一步计算出二维部分和。


99. 激光炸弹

地图上有 NN 个目标,用整数 Xi,YiX_{i}, Y_{i} 表示目标在地图上的位置,每个目标都有一个价值 WiW_i。

注意:不同目标可能在同一位置。

现在有一种新型的激光炸弹,可以摧毁一个包含 R×RR \times R 个位置的正方形内的所有目标。

激光炸弹的投放是通过卫星定位的,但其有一个缺点,就是其爆炸范围,即那个正方形的边必须和 x,yx,y 轴平行。

求一颗炸弹最多能炸掉地图上总价值为多少的目标。

输入格式

第一行输入正整数 NN 和 RR,分别代表地图上的目标数目和正方形的边长,数据用空格隔开。

接下来 NN 行,每行输入一组数据,每组数据包括三个整数 Xi,Yi,WiX_{i}, Y_{i}, W_{i},分别代表目标的 xx 坐标,yy 坐标和价值,数据用空格隔开。

输出格式

输出一个正整数,代表一颗炸弹最多能炸掉地图上目标的总价值数目。

数据范围

0≤R≤1090 \le R \le 10^9
0<N≤100000 < N \le 10000,
0≤Xi,Yi≤50000 \le X_{i}, Y_{i} \le 5000
0≤Wi≤10000 \le W_i \le 1000

输入样例:

1
2
3
2 1
0 0 1
1 1 1

输出样例:

1
1 

算法分析

因为 Xi,YiX_{i}, Y_{i} 的值在 0∼50000 \sim 5000 之间, 所以我们可以建立一个二维数组 AA, 其中 A[i,j]A[i, j] 就等于位置 (i,j)(i, j) 上的所有目标的价值之和。即对于每个目标, 令 A[Xi,Yi]+=WiA\left[X_{i}, Y_{i}\right]+=W_{i} 。 接下来我们求出 AA 的二维前缀和 SS, 即:

S[i,j]=∑x=1i∑y=1jA[x,y]S[i, j]=\sum_{x=1}^{i} \sum_{y=1}^{j} A[x, y]

如下图所示, 我们再观察 S[i,j],S[i−1,j],S[i,j−1],S[i−1,j−1]S[i, j], S[i-1, j], S[i, j-1], S[i-1, j-1] 的关系:

容易得到如下的递推式:

S[i,j]=S[i−1,j]+S[i,j−1]−S[i−1,j−1]+A[i,j]S[i, j]=S[i-1, j]+S[i, j-1]-S[i-1, j-1]+A[i, j]

同理, 对于任意一个边长为 RR 的正方形, 我们有:

∑x=i−R+1i∑y=j−R+1jA[x,y]=S[i,j]−S[i−R,j]−S[i,j−R]+S[i−R,j−R]\sum_{x=i-R+1}^{i} \sum_{y=j-R+1}^{j} A[x, y]=S[i, j]-S[i-R, j]-S[i, j-R]+S[i-R, j-R]

因此, 我们只需要 O(N2)O\left(N^{2}\right) 递推求出二维前缀和 SS, 然后 O(N2)O\left(N^{2}\right) 枚举边长为 RR 的正方形的右下角坐标 (i,j)(i, j), 即可通过上式 O(1)O(1) 计算出该正方形内所有目标的价值之和, 更新答案。上面给出的两个式子蕴含的思想其实就是 “容斥原理”。

值得一提的是, 上面我们把 (Xi,Yi)\left(X_{i}, Y_{i}\right) 作为一个 “格子”, 而原题中 (Xi,Yi)\left(X_{i}, Y_{i}\right) 是一个点, 并且正方形边上的目标不会被摧毀。实际上, 我们不妨认为这个点就处于 “格子” (Xi,Yi)\left(X_{i}, Y_{i}\right) 的中心位置, 格子的左上角坐标为 (Xi−0.5,Yi−0.5)\left(X_{i}-0.5, Y_{i}-0.5\right), 右下角坐标为 (Xi+0.5,Yi+0.5)\left(X_{i}+0.5, Y_{i}+0.5\right), 而正方形的右下角处于 “格线交点”上, 即一个值为 “ □.5\square .5 ” 的坐标。这个转化与原问题是等价的。另外, 本题内存限制较为严格, 我们可以省略 AA 数组, 读入时直接向 SS 中累加。

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
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 5006;
int n, r, s[N][N];

int main() {
memset(s, 0, sizeof(s));
cin >> n >> r;
while (n--) {
int x, y, z;
scanf("%d %d %d", &x, &y, &z);
s[x][y] += z;
}
for (int i = 0; i <= 5000; i++)
for (int j = 0; j <= 5000; j++)
if (!i && !j) continue;
else if (!i) s[i][j] += s[i][j-1];
else if (!j) s[i][j] += s[i-1][j];
else s[i][j] += s[i-1][j] + s[i][j-1] - s[i-1][j-1];
int ans = 0;
if(r<5001){
for (int i = r - 1; i <= 5000; i++)
for (int j = r - 1; j <= 5000; j++)
if (i == r - 1 && j == r - 1) ans = max(ans, s[i][j]);
else if (i == r - 1) ans = max(ans, s[i][j] - s[i][j-r]);
else if (j == r - 1) ans = max(ans, s[i][j] - s[i-r][j]);
else ans = max(ans, s[i][j] - s[i-r][j] - s[i][j-r] + s[i-r][j-r]);
}
else{ //r>=5001,可以覆盖所有区域
ans=s[5000][5000];
}
cout << ans << endl;
return 0;
}

Solution


差分

对于一个给定的数列 AA, 它的差分数列 BB 定义为:

B[1]=A[1],B[i]=A[i]−A[i−1](2≤i≤n)B[1]=A[1], \quad B[i]=A[i]-A[i-1](2 \leq i \leq n)

容易发现, “前缀和”与 “差分” 是一对互逆运算, 差分序列 BB 的前缀和序列就是原序列 AA, 前缀和序列 SS 的差分序列也是原序列 AA 。

把序列 AA 的区间 [l,r][l, r] 加 dd (即把 Al,Al+1,⋯ ,ArA_{l}, A_{l+1}, \cdots, A_{r} 都加上 dd ), 其差分序列 BB 的变化为 BlB_{l} 加 d,Br+1d, B_{r+1} 减 dd, 其他位置不变。这有助于我们在很多题目中, 把原序列上的 “区间操作” 转化为差分序列上的 “单点操作” 进行计算, 降低求解难度。

在 数的直径与最近公共祖先 中, 我们还会继续探讨差分技巧在树上的应用。


100. 增减序列

给定一个长度为 nn 的数列 a1,a2,…,an{a_1,a_2,…,a_n},每次可以选择一个区间 [l,r][l,r],使下标在这个区间内的数都加一或者都减一。

求至少需要多少次操作才能使数列中的所有数都一样,并求出在保证最少次数的前提下,最终得到的数列可能有多少种。

输入格式

第一行输入正整数 nn。

接下来 nn 行,每行输入一个整数,第 i+1i+1 行的整数代表 aia_i。

输出格式

第一行输出最少操作次数。

第二行输出最终能得到多少种结果。

数据范围

0<n≤1050 < n \le 10^5,
0≤ai<21474836480 \le a_i <2147483648

输入样例:

1
2
3
4
5
4
1
1
2
2

输出样例:

1
2
1
2

算法分析

求出 aa 的差分序列 bb, 其中 b1=a1,bi=ai−ai−1(2≤i≤n)b_{1}=a_{1}, b_{i}=a_{i}-a_{i-1}(2 \leq i \leq n) 。令 bn+1=0b_{n+1}=0 。题目对序列 aa 的操作, 相当于每次可以选出 b1,b2,⋯ ,bn+1b_{1}, b_{2}, \cdots, b_{n+1} 中的任意两个数, 一个加 1, 另一个减 1。目标是把 b2,b3,⋯ ,bnb_{2}, b_{3}, \cdots, b_{n} 变为全零。最终得到的数列 aa 就是由 nn 个 b1b_{1} 构成的。

从 b1,b2,⋯ ,bn+1b_{1}, b_{2}, \cdots, b_{n+1} 中任选两个数的方法可分为四类:

  1. 选 bib_{i} 和 bjb_{j}, 其中 2≤i,j≤n2 \leq i, j \leq n 。这种操作会改变 b2,b3,⋯ ,bnb_{2}, b_{3}, \cdots, b_{n} 中两个数的值。应该在保证 bib_{i} 和 bjb_{j} 一正一负的前提下, 尽量多地采取这种操作, 更快地接近目标。
  2. 选 b1b_{1} 和 bjb_{j}, 其中 2≤j≤n2 \leq j \leq n 。
  3. 选 bib_{i} 和 bn+1b_{n+1}, 其中 2≤i≤n2 \leq i \leq n 。
  4. 选 b1b_{1} 和 bn+1b_{n+1} 。这种情况没有意义, 因为它不会改变 b2,b3,⋯ ,bnb_{2}, b_{3}, \cdots, b_{n} 的值, 相当于浪费了一次操作, 一定不是最优解。

设 b2,b3,⋯ ,bnb_{2}, b_{3}, \cdots, b_{n} 中正数总和为 pp, 负数总和的绝对值为 qq 。首先以正负数配对的方式尽量执行第 1 类操作, 可执行 min⁡(p,q)\min (p, q) 次。剩余 ∣p−q∣|p-q| 个未配对, 每个可以选与 b1b_{1} 或 bn+1b_{n+1} 配对, 即执行第 2 或 3 类操作, 共需 ∣p−q∣|p-q| 次。

综上所述, 最少操作次数为 min⁡(p,q)+∣p−q∣=max⁡(p,q)\min (p, q)+|p-q|=\max (p, q) 次。根据 ∣p−q∣|p-q| 次第 2、3 类操作的选择情况, 能产生: ∣p−q∣+1|p-q|+1 种不同的 b1b_{1} 的值, 即最终得到的序列 aa 可能有 ∣p−q∣+1|p-q|+1 种。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <cmath>
#include <cstdio>
#include <iostream>
#define ll long long
using namespace std;
const int N = 100006;
ll a[N], b[N];

int main() {
int n;
cin >> n;
for (int i = 1; i <= n; i++) scanf("%lld", &a[i]);
b[1] = a[1];
for (int i = 2; i <= n; i++) b[i] = a[i] - a[i-1];
ll p = 0, q = 0;
for (int i = 2; i <= n; i++)
if (b[i] > 0) p += b[i];
else if (b[i] < 0) q -= b[i];
cout << max(p, q) << endl << abs(p - q) + 1 << endl;
return 0;
}

Solution


101. 最高的牛

有 NN 头牛站成一行,被编队为 1、2、3…N1、2、3…N,每头牛的身高都为整数。

当且仅当两头牛中间的牛身高都比它们矮时,两头牛方可看到对方。

现在,我们只知道其中最高的牛是第 PP 头,它的身高是 HH ,剩余牛的身高未知。

但是,我们还知道这群牛之中存在着 MM 对关系,每对关系都指明了某两头牛 AA 和 BB 可以相互看见。

求每头牛的身高的最大可能值是多少。

输入格式

第一行输入整数 N,P,H,MN, P, H, M,数据用空格隔开。

接下来 MM 行,每行输出两个整数 AA 和 BB ,代表牛 AA 和牛 BB 可以相互看见,数据用空格隔开。

输出格式

一共输出 NN 行数据,每行输出一个整数。

第 ii 行输出的整数代表第 ii 头牛可能的最大身高。

数据范围

1≤N≤100001 \le N \le 10000,
1≤H≤10000001 \le H \le 1000000,
1≤A,B≤100001 \le A,B \le 10000,
0≤M≤100000 \le M \le 10000

输入样例:

1
2
3
4
5
6
9 3 5 5
1 3
5 3
4 3
3 7
9 8

输出样例:

1
2
3
4
5
6
7
8
9
5
4
5
3
4
4
5
5
5

注意:

  • 此题中给出的关系对可能存在重复

算法分析

题目中的 MM 对关系带给我们的信息实际上是牛之间身高的相对大小关系。具体来说, 我们建立一个数组 CC, 数组中起初全为 0 。若一条关系指明 AiA_{i} 和 BiB_{i} 可以互相看见 (不妨设 Ai<BiA_{i}<B_{i} ), 则把数组 CC 中下标为 Ai+1A_{i}+1 到 Bi−1B_{i}-1 的数都减去 1 , 意思是在 AiA_{i} 和 BiB_{i} 中间的牛, 身高至少要比它们小 1。因为第 PP 头牛是最高的, 所以最终 C[P]C[P] 一定为 0 。其他的牛与第 PP 头牛的身高差距就体现在数组 CC 中。换言之, 最后第 ii 头牛的身高就等于 H+C[i]H+C[i] 。

如果我们朴素执行把数组 CC 中下标为 Ai+1A_{i}+1 到 Bi−1B_{i}-1 的数都减去 1 的操作, 那么整个算法的时间复杂度为 O(NM)O(N M), 复杂度过高。一个简单而高效的做法是, 额外建立一个数组 DD, 对于每对 AiA_{i} 和 BiB_{i}, 令 D[Ai+1]D\left[A_{i}+1\right] 减去 1,D[Bi]1, D\left[B_{i}\right] 加上 1 。其含义是:“身高减小1”的影响从 Ai+1A_{i}+1 开始, 持续到 Bi−1B_{i}-1, 在 BiB_{i} 结束。最后, CC 就等于 DD 的前缀和, 即 C[i]=∑j=1iD[j]C[i]=\sum_{j=1}^{i} D[j], 如下图所示。

上述优化后的算法把对一个区间的操作转化为左、右两个端点上的操作, 再通过前缀和得到原问题的解。这种思想很常用, 我们在后面还会多次遇到。该算法的时间复杂度为 O(N+M)\mathrm{O}(N+M) 。

值得提醒的是, 在本题的数据中, 一条关系 (Ai,Bi)\left(A_{i}, B_{i}\right) 可能会输入多次, 要注意检查, 对于重复的关系, 只在第一次出现时执行相关操作即可。

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
#include <map>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
map<pair<int, int>, bool> v;
const int N = 10006;
int s[N];

int main() {
int n, p, h, t;
cin >> n >> p >> h >> t;
memset(s, 0, sizeof(s));
while (t--) {
int a, b;
scanf("%d %d", &a, &b);
if (a > b) swap(a, b);
if (v[make_pair(a, b)]) continue;
s[a+1]--;
s[b]++;
v[make_pair(a, b)] = 1;
}
int ans = 0;
for (int i = 1; i <= n; i++) {
ans += s[i];
printf("%d\n", h + ans);
}
return 0;
}

Solution