差分 & 前缀和

怼烎@ 2021-10-19 02:44 475阅读 0赞

我们从问题入手。

入门问题:有已赋值的 n 个数,现在有 m 个指令

操作 1: 每一次要求将第 k 个数加上 a;

操作 2: 查询第 k 个数字的值。

$1 ≤ k ≤ n ≤ 10^5$

这一题其实用一个数组就可以维护。


问题一:有已赋值的 n 个数,现在有 m 个指令,每一个指令要求查询第 x 个数到第 y 个数的和。

$1 ≤ x ≤ y ≤ n,m ≤ 10^5$

如果暴力求和是会 TLE 的。

那应该如何解决这个问题呢?

这就要引出前缀和了。

可以看出,$\sum_{i=x}^ya_i = \sum_{j=1}^ya_j - \sum_{k=1}^{x-1}a_k$

所以我们只需要求第一个数到各个数的和即可。

求这些数很容易,O(n) 复杂度递推,接下来就可以 O(1) 查询了。


问题二:有已赋值的 n 个数,现在有 m 个指令,每一次要求将第 x 个数到第 y 个数加上 k,最后求所有数的值。

$1 ≤ x ≤ y ≤ n,m ≤ 10^5$

其实这题分块、线段树、树状数组以及其它的很多算法都可以过这题。

但是请看下一个问题:

问题三:有已赋值的 n 个数,现在有 m 个指令,每一次要求将第 x 个数到第 y 个数加上 k,最后求所有数的值。

$1 ≤ x ≤ y ≤ n,m ≤ 5*10^6$

我们发现:这个 5e6 的范围太大了,O(n log n) 以及更慢的复杂度是过不了的。

所以,下一个内容是:


差分

① 什么是差分?

差分,就是一个通过常数复杂度的标记实现求和及其它应用的一种技巧。

一般适合静态维护(前面先运算,最后求值)。

② 差分怎么用?

我们马上举一个例子:

有一个数列:

  1.     1 9 2 8 3 7 4 6 5
  2. tag:  0 0 0 0 0 0 0 0 0  // 现在还没有打上标记

现在,我们将第 3 个数到第 5 个数加上 2.

那我们就发现,可以把第 3 个数开始把后面所有的数加上 2,再从第 6 个数开始把后面所有数减去 2 呢,就相当于把第 3 个数到第 5 个数加上 2 了。

然后这么标记:

  1. a[i]: 1 9 2 8 3 7 4 6 5
  2. tag : 0 0 2 0 0 -2 0 0 0  // 打上标记*1

请注意,如果我们还是一个一个打上标记,这个算法就会退化。

所以,我们将第一个数打上标记,末尾打上相反标记,然后最后用前缀和处理。

然后,我们再将第 4 个数到第 8 个数减去 1.

按照上面的想法,我们可以从第 4 个数开始把后面所有的数减去 1,在从第 9 个数开始把后面所有的数加上 1.

  1. a[i]: 1 9 2 8 3 7 4 6 5
  2. tag : 0 0 2 -1 0 -2 0 0 1  // 打上标记*2

假设指令执行结束,那么我们这样求和:

用一个 sum 数组记录,$sum_i$ 代表前 i 个数所带的 tag 的和。(即前缀和)

  1. a[i] : 1 9 2 8 3 7 4 6 5
  2. tag : 0 0 2 -1 0 -2 0 0 1  
  3. sum[i] : 0 0 2 1 1 -1 -1 -1 0

最后,我们把 $a_i$ 加上对应的 $sum_i$,就是答案了。

时间复杂度 O(n).

代码

  1. #include <cstdio>
  2. #define N 5000010
  3. typedef long long ll;
  4. ll n, m, x, y, k, a[N], sum[N], sub[N];
  5. inline ll read(){ // 数据范围很大的情况下建议用快读
  6. ll num = 0, b = 1;
  7. char c = getchar();
  8. while(c < '0' || c > '9'){
  9. if(c == '-') b = -1;
  10. c = getchar();
  11. }
  12. while(c >= '0' && c <= '9'){
  13. num = num * 10 + c - '0';
  14. c = getchar();
  15. }
  16. return num * b;
  17. }
  18. int main(){
  19. n = read(), m = read();
  20. for(ll i=1; i<=n; i++)
  21. a[i] = read();
  22. while(m--){
  23. x = read(), y = read(), k = read();
  24. sub[x] += k;
  25. sub[y + 1] -= k; // 注意是 y + 1, 因为第 y 个数也要加 k
  26. }
  27. for(ll i=1; i<=n; i++)
  28. sum[i] = sum[i - 1] + sub[i]; // 前缀和
  29. for(ll i=1; i<=n; i++)
  30. printf("%d ", a[i] + sum[i]);
  31. return 0;
  32. }

总结

与其说这是一个算法,不如说这是一个思想。

我们会碰到很多这样的题目,看似需要比较复杂的数据结构,其实很简单。

这需要我们灵活运用所学的知识。

转载于:https://www.cnblogs.com/zengpeichen/p/11279207.html

发表评论

表情:
评论列表 (有 0 条评论,475人围观)

还没有评论,来说两句吧...

相关阅读

    相关 前缀

    前缀和: 前缀和是一种在数学和计算机科学中使用的概念。它指的是一个数组中每个元素之前(包括该元素)的所有元素的总和。 例如,对于数组 A = \[1, 2, 3, 4, 5

    相关 & 前缀

    我们从问题入手。   入门问题:有已赋值的 n 个数,现在有 m 个指令 操作 1: 每一次要求将第 k 个数加上 a; 操作 2: 查询第 k 个数字的值。 $1