01_前缀和算法总结

422 阅读3分钟

一、简述

什么是前缀和?

所谓前缀和,是指一个数组中的前i项的和。设有数组a[0...n],那么s[i] = a[0] + ... + a[i]。这样s数组就是a数组的前缀和数组。

通常,在算法题中,前缀和计算时间复杂度为O(n),后续要查询对应的操作算法复杂度为O(1),使用前缀和后,能大大减少算法复杂度。

二、遇见的题型分类

2.1 区间加/减法

案例:若题目给出了一系列操作,要求将给出的数组范围里进行加、减操作,则可以使用该方法,如下例题。

例题源头: https://leetcode.cn/problems/range-addition/

例题题图示:

核心点

  1. 若要求[start, end]区间加上value,则只需要将初始化的后的数组(初始化的值为0),nums[start] +=value,nums[end + 1] -= value
  2. 将数组累加,即:初始化且进行操作过后的数组进行前缀和计算后,会得出最后需要的结果数组。

解释:

  • nums[start] +=value是为了让后续累加时,数组后面的值都会加上value
  • nums[end + 1] -= value是为了让超出end的之后的数组不要加上该值,反过来想,减去该值就可以

代码:

    public int[] getModifiedArray(int length, int[][] updates) {
        int result[] = new int[length];
//        for (int i = 0; i < updates.length; i++) {
//            int j = updates[i][0], k = updates[i][1], value = updates[i][2];
//            while(j <= k) {
//                result[j] += value;
//                j++;
//            }
//        }
//        前缀和算法
        for(int i = 0; i < updates.length; i++) {
            int start = updates[i][0], end = updates[i][1], value = updates[i][2];
            result[start] += value;
            if (end + 1 < length) {
                result[end + 1] -= value;
            }
//            System.out.println(Arrays.toString(result));
        }
        for(int i = 1; i < length; i++) {
            result[i] += result[i - 1];
        }
//        System.out.println("----");
//        System.out.println(Arrays.toString(result));
        return result;
    }

2.2 前后缀协同使用

解释:前缀和为前往后加,后缀和则为后往前加,思路是类似的。有些题目可能要前后缀一起使用,结果为获取相应的前缀和后缀之间的关系得出的结果。

题源头: leetcode.cn/problems/mi…

例题题图示:

核心思路:

1.假如我们在i的时候关门,代价为i前的N的个数+i后的Y的个数

2.计算 N的前缀和prefix 以及 Y的后缀和suffix

3.第i时关门的代价=prefix[i] + suffix[i + 1]

代码:

    public int bestClosingTime(String customers) {
        int length = customers.length(), result = 0;
        int suffix1[] = new int[length], prefix0[] = new int[length];
        for (int i = 0; i < length; i++) {
            if (customers.codePointAt(i) == 'N') {
                if (i == 0) {
                    prefix0[i] = 1;
                } else {
                    prefix0[i] = prefix0[i - 1] + 1;
                }
            } else {
                if (i != 0) {
                    prefix0[i] = prefix0[i - 1];
                }
            }

            int sufIndex = length - 1 - i;
            if (customers.codePointAt(sufIndex) == 'Y') {
                if (i == 0) {
                    suffix1[sufIndex] = 1;
                } else {
                    suffix1[sufIndex] = suffix1[sufIndex + 1] + 1;
                }
            } else {
                if(i != 0) {
                    suffix1[sufIndex] = suffix1[sufIndex + 1];
                }
            }
        }
//        System.out.println(Arrays.toString(prefix0));
//        System.out.println(Arrays.toString(suffix1));
        int cost = suffix1[0];
        for (int i = 0; i < length - 1; i++) {
            if (cost > prefix0[i] + suffix1[i + 1]) {
                cost = prefix0[i] + suffix1[i + 1];
                result = i + 1;
            }
        }
        if (prefix0[length - 1] < cost) {
            result = length;
        }
        return result;
    }

2.3 二维前缀和

2.3.1 计算

二维前缀和的计算,可以按行或按列进行累加计算。这里以行为例,按照每行,进行一维数组的累加前缀和就可以了。对原数组matrix[0...n][0...m] 而言,前缀和数组prifix[i][j] 表示的是第i行的前j个数的和。

这样,我们就能够通过前缀和计算子数组范围内的值了。

2.3.2 例题

例题源头: leetcode.cn/problems/O4…

例题题图示:

核心点

若要求[row1, col1, row2, col2]范围内的值

1.我们只需要从row1到row2遍历

2.值result += prefix[i][col2] - prefix[i][col1]即可,也就是加上每行对应子数组内容的值

代码:

class NumMatrix {
    public int prefixRow[][];
    public NumMatrix(int[][] matrix) {
        this.prefixRow = new int[matrix.length][matrix[0].length];
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix[0].length; j++) {
                if (j == 0) {
                    this.prefixRow[i][j] = matrix[i][j];
                } else {
                    this.prefixRow[i][j] = this.prefixRow[i][j - 1] + matrix[i][j];
                }
            }
            System.out.println(Arrays.toString(this.prefixRow[i]));
        }
    }

    public int sumRegion(int row1, int col1, int row2, int col2) {
        int result = 0;
        if (col1 == 0) {
            for (int i = row1; i <= row2; i++) {
                result += this.prefixRow[i][col2];
            }
        } else {
            for (int i = row1; i <= row2; i++) {
                result += this.prefixRow[i][col2] - this.prefixRow[i][col1 - 1];
            }
        }
        return result;
    }
}

2.4 异或相关内容

2.4.1 计算

异或的计算也满足前缀和的效果,若要求数组i~j的异或结果,我们可以逐步推出如下等式( ^为异或符)

  1. a[1] ^ a[2] ^ ...... ^ a[i] ^ a[i + 1] ^ ...... ^ a[j] ,若将其用前缀和表示我们用prefix[j] 表示该式子
  2. 那么 prefix[i - 1] = a[1] ^ a[2] ^ ...... ^ a[i - 1]
  3. 得出prefix[j] ^ prefix[i - 1] = (a[1] ^ a[2] ^ ...... ^ a[i] ^ a[i + 1] ^ ...... ^ a[j]) ^ ( a[1] ^ a[2] ^ ...... ^ a[i - 1] )
  4. 由3的式子得出,prefix[j] ^ prefix[i] = a[i] ^ ...... ^ a[j],显然和前缀和的效果一致,即:性质与前缀和类似

2.4.2 例题

例题源头: leetcode.cn/problems/xo…

例题题图示:

代码:

public int[] xorQueries(int[] arr, int[][] queries) {
        int length = arr.length, result[] = new int[queries.length];
        if (length == 1) {
            for (int i = 0; i < queries.length; i++) {
                result[i] = arr[0];
            }
            return result;
        }
        for (int i = 1; i < length; i++) {
            arr[i] ^= arr[i - 1];
        }
        for (int i = 0; i < queries.length; i++) {
            int start = queries[i][0], end = queries[i][1];
            if (start == 0) {
                result[i] = arr[end];
            } else {
                result[i] = arr[end] ^ arr[start - 1];
            }
        }
//        System.out.println(Arrays.toString(result));
        return result;
    }

2.5 差分数组

2.5.1 差分数组的定义和性质

计算方式如下:

设有数组a = { 3, 8, 6, 4, 9, 2, 5, 7, 1 },那么a的差分数组的计算方式为,为后一项减前一项的值,第一项不动,即a的差分数组b:

b = { 3, (8 - 3), (6 - 8), (4 - 6), (9 - 4), (2 - 9), (5 - 2), (7 - 5), (1 - 7)}

b = { 3, 5, -2, -2, 5, -7, 3, 2, -6 }

那么对b进行累加,我们会发现:c = { 3, 8, 6, 4, 9, 2, 5, 7, 1 },累加后的数组为a

这也就是差分数组的 性质 :差分数组的前缀和等于原数组。

2.5.2 例题

目前遇到的题目掺杂了其他的内容,不适合当例题,后续遇到合适的例题再补充。