前缀和与差分---概念+例题

目录

前缀和

概念

例题

差分

概念

例题


前缀和


概念

在高中学习了数列的概念,跟现在的数组很像。可以类比来看,数组的前缀和与数列的前n项和其实可以看成一个概念。很多算法题利用前缀和的思想也就是可以使用高中里面的前n项和来求解数组里面任何一个区间的元素的和。其公式为:

S{_n} = S{_{n - 1}} + a{_n}

结合实际例子来看:假设现在有数组arr:

arr = {1,2,3,4,5};

扫描二维码关注公众号,回复: 14796993 查看本文章

我们现在需要求数组里面下标区间为 [i , j] 的元素的大小Sum,计算公式为:

Sum = S{_j} - S{_{j - 1}}

注意:数组下标是从0开始计算的.

就具体步骤而言,前缀和的操作方法主要分为一下几步:

  • 预处理:先求出数组元素的总和S{_n} (n为数组长度)
  • 在循环遍历时维护一个数组a, 其中a[i] 表示下标从 0 到 i 的元素的和;
  • 在求具体的区间和的时候使用上面的结论即可.

算法的时间复杂度分析:        

前缀和算法的时间复杂度可以达到 O(n).

例题

1、链接:3956. 截断数组 - AcWing题库

思路如右图所示,不难得到解题步骤:

  1. 先遍历数组,预处理求出数组的总和s,如果s不是三的倍数,则表示无解;
  2. 从前往后枚举第二个点,判断此时数组的前缀和为 (3 / s) * 2:
    1. 是:则枚举第一个点,寻找前面数组前缀和为3/s的点的数量,即为cnt;
    2. 不是:则continue进入下一次循环
  3. 将每次得到的cnt求和,得到全部的方案数ans输出。结束.

本题的Java代码如下:

import java.util.Scanner;

import static java.lang.System.exit;

public class Acwing_3959 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int[] nums = new int[n + 5];
        int[] s = new int[n + 5];  // 表示数组的前n项和
        long ans = 0, sum = 0, cnt = 0;
   // ans表示总的方案数,sum表示数组的综合,cnt表示在当前元素之前有多少个第一个元素符合要求
        for (int i = 1; i <= n; i++) {
            nums[i] = scanner.nextInt();
            sum += nums[i];  // 计算数组的总和
            if(i == 1) s[i] = nums[i];
            else s[i] = nums[i] + s[i - 1];   // 计算数组的前n项和
        }
        if(sum % 3 != 0 || n < 3) {
            System.out.println(0);
            exit(0);
        }
        long average = sum / 3;
        for (int i = 1; i < n; i++) {
            if(s[i] == average * 2) ans += cnt; 
            if(s[i] == average) ++cnt;
        }
        System.out.println(ans);
    }
}

完成此题时有以下需要注意的技巧:

  • 前缀和数组最好从下标1开始,可以避免数组元素全为0不好计算子数组的数量的情况;

差分


概念

在前面了解了前缀和,和前缀和很像的一类算法还有差分。同样是对字符串做处理,前缀和可以在 o(1) 时间内求出数组某一段区间的元素的和,差分则可以在平均 o(n) 的时间内给数组某一段区间的元素加上(或减去)一个数。

  算法思路如下:

假设我们需要给数组下标为2到3的元素加上数字c , 则可以维护一个差分数组b, 开始时b的元素全部为0,将 b[2] 变为c, 同时将b[4] 变为 -c, 然后求差分数组的前缀和s[i], s[i]表示下标为 i 处的前缀和。最后将原数组的元素加上对应的差分数组的前缀和即可。不难证明,这种方法的时间复杂度为 o(n) , 而且跟增加操作的次数无关。也就是说,使用差分算法,不管对数组的区间增加多少次,算法的时间复杂度总是 o(n).

注意,在差分数组中求前缀和是很简单的操作。因为增加数组元素是一定要遍历原数组和差分数组的,只需要在增加原数组之前使用 S{_n} = S{_{n - 1}} + a{_n}求出前缀和即可,使用一个前缀和数组就可以在不影响算法整体时间复杂度的前提下完成求前缀和。

上面陈述的是实际解题中经常使用的一种思路,现在给出差分的定义:

定义:差分数组的每一个元素可以是原数组的当前元素与其前一个元素之差(i≠0),差分数组的第一个元素和原数组的第一个元素相等;

推论:差分数组的前缀和数组就是原数组。

证明:假设原数组为:

                        int arr = {2, 3, 4, 6, 7};

  不难求出差分数组为:

                        int b = {2, 1, 1, 2, 1};    将此差分数组求前缀和即可得到原数组。

不难证明对一般情况,此推论仍成立。

额外提醒:在实际操作中,我们需要将差分数组和元素组的下标从1开始计数,由于笔者水平限制,暂时还不能给出这样做的具体原理,但是大量的实践证明,这样做确确实实在无形中避免很多问题。

例题

题目链接:3729. 改变数组元素 - AcWing题库

图1 Acwing_3729(例题1截图)

解题思路:

根据差分的思想,可以将需要输出的数组看成原数组,在程序中维护一个差分数组。按照经验,差分数组和原数组的下标均应从1开始。采用“逻辑加”的概念来完成差分数组的前缀和与原数组的相加,即1 + 1 = 1, 将原数组的后面三个元素变成1的需求,可以转换实现为逻辑加1,原数组长度在变化,但是每一次都不难求出需要逻辑加1的元素的前后下标,那么这个题的整体需求就是跟原数组的指定区间逻辑加上1,操作有很多次。如果使用一般的方法,不难看出绝对会超出时间限制,因此采用差分的方法是本题的正解之一。

解题步骤如下:

  1. 准备一个全为0的差分数组,长度足够大;
  2. 同时遍历原数组和题设的整数数组,由于题设的整数数组只需要使用一次,因此可以取消数组的转存,将数组的输入和使用合在一起,用一个变量ret代替数组;
  3. 将得到的re转化为差分数组的下标区间(不难证明,下标区间为[i - ret + 1 , i +1]),随后分别给差分数组的这两个对应元素(k[i - ret + 1] 和 k[i + 1])加上1和-1。
  4. 维护差分数组的前缀和数组,将其和原数组对应相加即可。注意:将差分数组的前缀和与原数组相加的时候不可使用“逻辑加”,因为前缀和数组代表了增加1的重数,1表示增加了1个1,2表示增加了两个2,只有等全部的前缀和数组处理完成之后才能将大于1的数变成1,将小于1的数变成0;
  5. 将原数组逻辑处理后输出。结束.

本题的Java如下:

import java.util.Scanner;

public class Acwing_3729 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int t = scanner.nextInt();
        int[] k = new int[100005];   // 差分数组
        while(t-- > 0){
            int n = scanner.nextInt();
            int[] arr = new int[n + 5];
            for(int i = 1; i <= n; i++){
                int ret = scanner.nextInt();
                if(i - ret + 1 <= 1) k[1] += 1;
                else k[i - ret + 1] += 1;
                k[i + 1] += -1;
            }
            for (int i = 1; i <= n; i++) {
                arr[i] = arr[i - 1] + k[i];
                k[i] = 0;
                System.out.printf("%d ", arr[i] > 0 ? 1 : 0);
            }
            k[n + 1] = 0;
            System.out.println();
        }
    }
}

 PS:c站的富文本编辑器是真的难搞,这是我之前的博客,在word上已经写好了的,转到这个上面来都弄了半天。尤其是公式编辑器,真的难受。

@SoftwareTeacher 能优化以下吗~~~~~

猜你喜欢

转载自blog.csdn.net/qq_61567032/article/details/129152315