重温数据结构与算法之前缀和

文章目录

  • 前言
  • 一、基础
    • 1.1 定义
    • 1.2 时间复杂度
  • 二、扩展
    • 2.1 二维前缀和
    • 2.2 差分数组
    • 2.3 前缀积
  • 三、LeetCode 实战
    • 3.1 长度最小的子数组
    • 3.2 二维区域和检索 - 矩阵不可变
  • 参考

前言

前缀和(Prefix Sum),也被称为累计和,是一种在计算机编程算法领域中广泛应用的重要概念和技巧。它通过将一个序列中的元素累加起来,得到一个新的序列,其中每个元素表示原序列中对应位置及其之前所有元素的和。前缀和的简洁性和高效性使其在各种算法和问题中有着广泛的应用。

前缀和有许多实际的应用。例如,前缀和可以用于计算区间内的和。无论是静态区间查询还是动态更新的场景,前缀和都可以为我们提供快速的求解方法。它可以在常数时间内计算出任意区间的和,而不受区间长度的影响。这种特性使得前缀和在处理数据流问题时非常有用。

在本文中,我们将深入探讨前缀和的基础知识、应用案例以及优化和扩展技巧。通过学习和掌握这些内容,读者将能够充分理解前缀和的概念、原理和实际应用,为解决各种算法和数据处理问题提供有力的工具和思路。

一、基础

1.1 定义

对于给定的数组 a ,它的前缀和数组 prefix 的第 i 个元素 prefix[i] 就是原数组 a 从第一个元素累加到第 i 个元素的总和,公式如下:
p r e f i x [ i ] = ∑ k = 0 i − 1 a [ k ] , 其中 1 ≤ i ≤ n , n 为数组 a 长度 prefix[i] = \sum_{k=0}^{i-1} a[k], 其中 1 \leq i \leq n,n为数组a长度 prefix[i]=k=0i1a[k],其中1in,n为数组a长度

在计算前缀和数组时,一般会在原数组的开头加上一个初始值 0,以便在计算任意区间的元素和时能够统一处理。因此,前缀和数组的长度通常会比原数组的长度多 1。当计算前缀和数组时, 可以直接通过递推的方式求出
p r e f i x [ i ] = p r e f i x [ i − 1 ] + a [ i ] prefix[i] = prefix[i-1] + a[i] prefix[i]=prefix[i1]+a[i]
通过预先计算出原数组的前缀和数组,我们可以在快速得到任意区间 ([l,r]) 的元素和
sum ( l , r ) = p r e f i x [ r ] − p r e f i x [ l − 1 ] \text{sum}(l, r) = prefix[r] - prefix[l-1] sum(l,r)=prefix[r]prefix[l1]

1.2 时间复杂度

在计算前缀和时,我们可以通过简单的迭代和累加操作来获得相应的结果。这使得前缀和的计算过程非常高效,时间复杂度为 O ( n ) O(n) O(n)

快速得到任意区间 [ l , r ] [l,r] [l,r] 的元素和只需要将 prefix 数组内两个元素相减,时间复杂度为 O ( 1 ) O(1) O(1)

二、扩展

2.1 二维前缀和

二维前缀和是在二维数组中应用前缀和技巧的一种方法,用于高效地计算二维数组中指定子矩阵的元素和。类似于一维前缀和,二维前缀和也可以帮助我们以较低的时间复杂度快速计算出任意子矩阵的元素和。

假设我们有一个二维数组 a,其大小为 m × n m \times n m×n,我们可以通过以下方式计算二维前缀和数组 prefix:
p r e f i x [ i ] [ j ] = ∑ r = 0 i − 1 ∑ c = 0 j − 1 a [ r ] [ c ] , 其中 1 ≤ i ≤ m , 1 ≤ j ≤ n prefix[i][j] = \sum_{r=0}^{i-1} \sum_{c=0}^{j-1} a[r][c], 其中1 \leq i \leq m,1 \leq j \leq n prefix[i][j]=r=0i1c=0j1a[r][c],其中1im,1jn
计算二维前缀和也可通过递推获得
p r e f i x [ i ] [ j ] = p r e f i x [ i − 1 ] [ j ] + p r e f i x [ i ] [ j − 1 ] − p r e f i x [ i − 1 ] [ j − 1 ] + a [ i ] [ j ] prefix[i][j] = prefix[i-1][j] + prefix[i][j-1] - prefix[i - 1][j-1] + a[i][j] prefix[i][j]=prefix[i1][j]+prefix[i][j1]prefix[i1][j1]+a[i][j]

例子如下:

重温数据结构与算法之前缀和_第1张图片

同样,通过预先计算出二维数组的前缀和数组,我们可以在 O ( 1 ) O(1) O(1) 的时间复杂度内得到任意子矩阵 [ r 1 : r 2 , c 1 : c 2 ] [r1:r2, c1:c2] [r1:r2,c1:c2] 的元素和
sum ( r 1 , c 1 , r 2 , c 2 ) = p r e f i x [ r 2 ] [ c 2 ] − p r e f i x [ r 2 ] [ c 1 − 1 ] − p r e f i x [ r 1 − 1 ] [ c 2 ] + p r e f i x [ r 1 − 1 ] [ c 1 − 1 ] \text{sum}(r_1, c_1, r_2, c_2) = prefix[r_2][c_2] - prefix[r_2][c_1-1] - prefix[r_1-1][c_2] + prefix[r_1-1][c_1-1] sum(r1,c1,r2,c2)=prefix[r2][c2]prefix[r2][c11]prefix[r11][c2]+prefix[r11][c11]

举个例子:

重温数据结构与算法之前缀和_第2张图片

2.2 差分数组

差分数组是一种在处理频繁更新的数组时常用的技巧,它可以帮助我们以较低的时间复杂度完成对原数组的部分元素进行增减操作,并且能够快速还原出原数组。

差分数组的定义很简单,对于原数组 a,其差分数组 d 的每个元素 d[i] 表示原数组 a[i] 与 a[i-1] 之间的差值:$ d[i] = a[i] - a[i-1] $

通过差分数组,我们可以通过对差分数组的部分元素进行修改来实现对原数组的部分元素进行增减操作。例如,如果我们想要将原数组 a)的某个区间 [l, r] 的所有元素增加一个固定值 val,我们可以将差分数组的 d[l] 增加 val,而将差分数组的 d[r+1] 减去 val。这样,在还原原数组时,我们可以通过累加差分数组的前缀和得到:$ a[i] = a[i-1] + d[i] $

需要注意的是,为了计算差分数组,我们需要对原数组的首位元素 a[0])进行特殊处理。通常情况下,我们会在差分数组的第一个位置 d[1])上存储 a[0] 的值,即 d[1] = a[0]。这样,在还原原数组时,我们可以通过累加差分数组的前缀和并加上 a[0] 得到:
a [ i ] = a [ 0 ] + ∑ k = 1 i d [ k ] a[i] = a[0] + \sum_{k=1}^{i} d[k] a[i]=a[0]+k=1id[k]
差分数组的优点在于,它可以在 O ( 1 ) ) O(1)) O(1))的时间复杂度内完成对原数组的部分元素增减操作,并且能够快速还原出原数组。因此,当我们需要频繁对原数组进行增减操作时,使用差分数组可以提高算法的效率。

2.3 前缀积

类似前缀和,还有前缀积,后缀和,后缀积等等,相关推导公式不再列出。

前缀积和后缀积用于高效地计算出数组中每个位置左/右侧所有元素的乘积,后缀和计算数组中每个位置右侧所有元素的和。

三、LeetCode 实战

3.1 长度最小的子数组

209. 长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target

找出该数组中满足其总和大于等于 target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度**。**如果不存在符合条件的子数组,返回 0

这题求连续子数组长度,可以先计算前缀和,由于都是正整数,前缀和数组是递增的,这时就可以使用二分法求以遍历的i为起点,子数组终点总和大于等于target的最小长度,每一位都要求,加上二分法,时间复杂度为 O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n))

public int minSubArrayLen(int target, int[] nums) {
    int [] prefix = new int[nums.length + 1];
    for (int i = 0; i < nums.length; i++) {
        prefix[i + 1] = prefix[i] + nums[i];
    }
    if (prefix[nums.length] < target) return  0;
    int start = 1;
    int end = nums.length;
    int ans = Integer.MAX_VALUE;
    for (int i = 1; i <= nums.length; i++) {
        start = i;
        end = nums.length;
        while (start <= end) {
            int mid = start + (end - start) / 2;
            if (prefix[mid] - prefix[i - 1] >= target) {
                ans = Math.min(ans, mid - i + 1);
                end = mid - 1;
            }  else {
                start = mid + 1;
            }
        }
    }
    return ans;
}

3.2 二维区域和检索 - 矩阵不可变

304. 二维区域和检索 - 矩阵不可变

给定一个二维矩阵 matrix,以下类型的多个请求:

  • 计算其子矩形范围内元素的总和,该子矩阵的 左上角(row1, col1)右下角(row2, col2)

实现 NumMatrix 类:

  • NumMatrix(int[][] matrix) 给定整数矩阵 matrix 进行初始化
  • int sumRegion(int row1, int col1, int row2, int col2) 返回 左上角 (row1, col1)右下角 (row2, col2) 所描述的子矩阵的元素 总和

经典的二维前缀和题目

class NumMatrix {

    int [][] prefix;

    public NumMatrix(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        prefix = new int[m + 1][n + 1];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                prefix[i][j] = prefix[i - 1][j] + prefix[i][j - 1] + matrix[i - 1][j - 1] - prefix[i - 1][j - 1]; 
            }
        }
    }
    
    public int sumRegion(int row1, int col1, int row2, int col2) {
        return prefix[row2 + 1][col2 + 1] - prefix[row1][col2 + 1] - prefix[row2 + 1][col1] + prefix[row1][col1];
    }
}

参考

  1. 前缀和
  2. https://leetcode.cn/tag/prefix-sum/problemset/

你可能感兴趣的:(java,leetcode,java,算法,前缀和,leetcode,二分法)