区间和

163 阅读3分钟

前缀和数组

序号题目完成
1303. 区域和检索 - 数组不可变
2304. 二维区域和检索 - 矩阵不可变
3剑指 Offer II 013. 二维子矩阵的和
41314. 矩阵区域和

在不改变原数组的情况下,计算区间和。 涉及到求区间和的问题,就要想到用前缀和解决。

一维数组

303. 区域和检索 - 数组不可变

class NumArray {
    int[] preSum;

    public NumArray(int[] nums) {
        int n = nums.length;
        preSum = new int[n];
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += nums[i];
            preSum[i] = sum;
        }
    }

    public int sumRange(int left, int right) {
        if (left == 0) {
            return preSum[right];
        } else {
            return preSum[right] - preSum[left - 1];
        }
    }
}

二维数组

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

相同问题: 剑指 Offer II 013. 二维子矩阵的和

class NumMatrix {
    int[][] preSum;

    public NumMatrix(int[][] matrix) {
        int m = matrix.length;
        int n = matrix[0].length;
        int sum = 0;
        preSum = new int[m + 1][n + 1];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                preSum[i][j] = preSum[i][j - 1] + preSum[i - 1][j] + matrix[i - 1][j - 1] - preSum[i - 1][j - 1];
            }
        }
    }

    public int sumRegion(int x1, int y1, int x2, int y2) { // 目标矩阵之和由四个相邻矩阵运算获得
        return preSum[x2 + 1][y2 + 1] - preSum[x1][y2 + 1] - preSum[x2 + 1][y1] + preSum[x1][y1];
    }
}

1314. 矩阵区域和

可以转换为:求[i-k, j-k][i+k, j+k]两个节点之间的元素和。

class Solution {
    public int[][] matrixBlockSum(int[][] mat, int k) {
        int m = mat.length;
        int n = mat[0].length;
        int[][] preSum = new int[m + 1][n + 1];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                // 先得到前缀和数组
                preSum[i][j] = preSum[i - 1][j] + preSum[i][j - 1] + mat[i - 1][j - 1] - preSum[i - 1][j - 1];
            }
        }

        int[][] target = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                int x1 = i > k ? i - k : 0;
                int x2 = i + k < m ? i + k : m - 1;
                int y1 = j > k ? j - k : 0;
                int y2 = j + k < n ? j + k : n - 1;
                target[i][j] = get(preSum, x1, y1, x2, y2);
            }
        }
        return target;
    }

    public int get(int[][] mat, int x1, int y1, int x2, int y2) {
        return mat[x2 + 1][y2 + 1] - mat[x2 + 1][y1] - mat[x1][y2 + 1] + mat[x1][y1];
    }
}

差分数组

对区间内的元素进行频繁的计算。

序号题目完成
1094. 拼车
1109. 航班预订统计
370. 区间加法
1674. 使数组互补的最少操作次数
798. 得分最高的最小轮调
1943. 描述绘画结果

370. 区间加法

class Solution {
    public int[] getModifiedArray(int length, int[][] updates) {
        int[] ans = new int[length];
        int[] diff = new int[length];
        for (int[] update : updates) {
            int x = update[0];
            int y = update[1] + 1;
            int val = update[2];
            diff[x] += val;
            if (y <= length - 1) {
                diff[y] -= val;
            }
        }
        ans[0] = diff[0];
        for (int i = 1; i < length; i++) {
            ans[i] = ans[i - 1] + diff[i];
        }
        return ans;
    }
}

1094. 拼车

class Solution {
    public boolean carPooling(int[][] trips, int capacity) {
        int max = 0;
        for (int[] trip : trips) {
            max = Math.max(max, trip[2]);
        }
        int len = max + 1;
        int[] nums = new int[len];
        // 差分数组初始值都为0,因为容量一开始就确定了,每一个元素值都相等,差分值自然也相等
        int[] diff = new int[len];
        for (int[] trip : trips) {
            int x = trip[1];
            // 到站就下车了,所以y不需要包含
            int y = trip[2];
            // 此时上车了val名乘客,对[x,y)范围内的容量减去val
            diff[x] += trip[0];
            diff[y] -= trip[0];
        }

        // 得到原数组
        for (int i = 0; i < len; i++) {
            if (i == 0) {
                nums[0] = diff[0];
            } else {
                nums[i] = nums[i - 1] + diff[i];
            }
            // 出现容量不够的情况,立刻返回false
            if (nums[i] > capacity) {
                return false;
            }
        }
        return true;
    }
}

1109. 航班预订统计

class Solution {
    public int[] corpFlightBookings(int[][] bookings, int n) {
        int[] ans = new int[n];
        int[] diff = new int[n];
        for (int[] booking : bookings) {
            int x = booking[0] - 1;
            int y = booking[1];
            int val = booking[2];
            diff[x] += val;
            if (y < n) {
                diff[y] -= val;
            }
        }
        ans[0] = diff[0];
        for (int i = 1; i < n; i++) {
            ans[i] = diff[i] + ans[i - 1];
        }
        return ans;
    }
}

1674. 使数组互补的最少操作次数

题目的要求是数组对的和都相等,我们事先并不知道最少操作数的数组对的和是多少,所以我们只能把所有可能和带来的操作数都计算一遍。

首先

因为所有的元素都是[1, limit],那我们可以知道,数组对的和的范围必然是[2, limit * 2]

接下来,我们就来计算数组对的和分别为[2, limit*2]所需要的操作数。

我们定义一个ans数组,该数组保存了数组和为index的所需的操作数,比如idx=2的位置就是,数组对和为2时,所有数组对的最小操作数。

第二步

我们来看一个数组对需要操作几次才能达到我们期望的和。

显然,对于两个数字:

  • 最少操作零次(和已经目标一样,不需要替换了)。
  • 经过一次操作,得到的和肯定在[smaller + 1, bigger + limit]之间。
  • 最多操作两次(两个数字都要换)

那么可以得到:

  • 在[2, min]这个区间,arr[i] += 2;
  • 在[min + 1, min + max]区间,arr[i] += 1;
  • 在min + max上,arr[i] += 0;
  • 在[min + max + 1, max + limit]区间上,arr[i] += 1;
  • 在[max + limit + 1, limit + limit]区间上,arr[i] += 2;

我们知道区间和频繁修改建议用差分数组,差分数组与原数组的关系:

如果需要操作[i,j]区间的值都加val,那么 diff[i] += val; diff[j+1] -= val;

最后

可以得到对差分数组的操作公式:

  • [2, min] :diff[2] +=2; diff[min+1] -=2;
  • [min + 1, min + max) :diff[min+1] +=1; diff[min+max] -=1;
  • 等于 min + max:不变
  • [min + max + 1, max + limit] :diff[min + max + 1] += 1; diff[max + limit + 1] -= 1;
  • [max + limit + 1, limit * 2] :diff[max + limit + 1] +=2; diff[limit * 2 + 1] -=2;
class Solution {
    public int minMoves(int[] nums, int limit) {
        int n = nums.length;
        int[] diff = new int[2 * limit + 2];
        // 遍历每一个数组对,因为题目是需要计算总的操作数,所以需要遍历所有的数组对
        for (int i = 0; i < n / 2; i++) {
            // 得到min和max
            int max = Math.max(nums[i], nums[n - i - 1]);
            int min = Math.min(nums[i], nums[n - i - 1]);
            if (min == 1) {
                diff[2] += 1;
            } else {
                diff[2] += 2;
                diff[min + 1] -= 1;
            }
            diff[min + max] -= 1;
            diff[min + max + 1] += 1;
            diff[max + limit + 1] += 1;
        }

        int ans = Integer.MAX_VALUE;
        int now = 0;
        // ans的取值范围[2, limit * 2]
        // ans[i] = ans[i-1] + diff[i]
        for (int i = 2; i < 2 * limit + 1; i++) {
            now += diff[i];
            ans = Math.min(ans, now);
        }
        return ans;
    }
}

798. 得分最高的最小轮调

解题思路

定义scores[k],表示进行k次轮调之后的得分总数,那对于nums里的每一个元素,得到其需要得分加1的k的区间,再来操作scores就可以了。

条件:对于nums[i]来说,i >= nums[i]的情况下,需要加1。

比如:[2,3,1,4,0]

当 i=2 时,nums[i] = 1,i > nums[i]

  • k=0,[2,3,1,4,0],移动到2的位置,+1
  • k=1,[3,1,4,0,2],移动到1的位置,+1
  • k=2,[1,4,0,2,3],移动到0的位置
  • k=3,[4,0,2,3,1],移动到4的位置,+1
  • k=4,[0,2,3,1,4],移动到3的位置,+1

当 i=3 时,nums[i] = 4,i < nums[i]

  • k=0,[2,3,1,4,0],移动到3的位置
  • k=1,[3,1,4,0,2],移动到2的位置
  • k=2,[1,4,0,2,3],移动到1的位置
  • k=3,[4,0,2,3,1],移动到0的位置
  • k=4,[0,2,3,1,4],移动到4的位置,+1

经过k次调度后,i的位置为:(i + n - k) % n

怎么得到的?
因为调度是往左移k位,正常是 i - k,考虑到越界,我们不如在数组前面加一个[2,3,1,4,0,2,3,1,4,0]这样就不会越界,加了新的一段后,i的位置变为i+n

i变化的规律是:先变小后变大
nums[i]一直不变。

对每一个元素进行分析(i >= nums[i]则加1)

(1)当i < nums[i]时:k在[0,i-x]和[i+1,n-1] 加1。

(2)i == nums[i]时,加1;

(3)当i > nums[i]时:k在[i+1, i-x+n]加1。

class Solution {
    public int bestRotation(int[] nums) {
        int n = nums.length;
        int[] diffs = new int[n];
        // 当i上的元素被调度到[nums[i], n-1]的位置上时才会被计分
        // i>x时, [i+1, i-x+n] 加1
        // i<=x时,[0,i-x][i+1,n-1] 加1
        for (int i = 0; i < n; k++) {
            int low = (i + 1) % n;
            int high = (i - nums[i] + n + 1) % n;
            // 对[low, high-1]内的得分+1
            diffs[low]++;
            diffs[high]--;
            if (low >= high) {
                diffs[0]++;
            }
        }
        // diff是对k的得分的差分数组
        int bestIndex = 0;
        int maxScore = 0;
        int score = 0;
        // 得到最大值
        for (int k = 0; k < n; k++) {
            score += diffs[k];
            if (score > maxScore) {
                bestIndex = k;
                maxScore = score;
            }
        }
        return bestIndex;
    }
}