嘉兴建站模板,广州商旅网站制作,iis如何做网站,成都高级网站建设前缀和技巧适用于快速、频繁地计算一个索引区间内的元素之和。
1. 一维数组中的前缀和
先看一道例题#xff0c;力扣第 303 题「区域和检索 - 数组不可变」#xff0c;让你计算数组区间内元素的和#xff0c;这是一道标准的前缀和问题#xff1a;
题目要求你实现这样一个…前缀和技巧适用于快速、频繁地计算一个索引区间内的元素之和。
1. 一维数组中的前缀和
先看一道例题力扣第 303 题「区域和检索 - 数组不可变」让你计算数组区间内元素的和这是一道标准的前缀和问题
题目要求你实现这样一个类
class NumArray {public NumArray(int[] nums) {}/* 查询闭区间 [left, right] 的累加和 */public int sumRange(int left, int right) {}
}sumRange 函数需要计算并返回一个索引区间之内的元素和没学过前缀和的人可能写出如下代码
class NumArray {private int[] nums;public NumArray(int[] nums) {this.nums nums;}public int sumRange(int left, int right) {int res 0;for (int i left; i right; i) {res nums[i];}return res;}
}
这样可以达到效果但是效率很差因为 sumRange 方法会被频繁调用而它的时间复杂度是 O(N)其中 N 代表 nums 数组的长度。
这道题的最优解法是使用前缀和技巧将 sumRange 函数的时间复杂度降为 O(1)说白了就是不要在 sumRange 里面用 for 循环咋整
直接看代码实现
class NumArray {// 前缀和数组private int[] preSum;/* 输入一个数组构造前缀和 */public NumArray(int[] nums) {// preSum[0] 0便于计算累加和preSum new int[nums.length 1];// 计算 nums 的累加和for (int i 1; i preSum.length; i) {preSum[i] preSum[i - 1] nums[i - 1];}}/* 查询闭区间 [left, right] 的累加和 */public int sumRange(int left, int right) {return preSum[right 1] - preSum[left];}
}
核心思路是我们 new 一个新的数组 preSum 出来preSum[i] 记录 nums[0…i-1] 的累加和看图 10 3 5 2 看这个 preSum 数组如果我想求索引区间 [1, 4] 内的所有元素之和就可以通过 preSum[5] - preSum[1] 得出。
这样sumRange 函数仅仅需要做一次减法运算避免了每次进行 for 循环调用最坏时间复杂度为常数 O(1)。
这个技巧在生活中运用也挺广泛的比方说你们班上有若干同学每个同学有一个期末考试的成绩满分 100 分那么请你实现一个 API输入任意一个分数段返回有多少同学的成绩在这个分数段内。
那么你可以先通过计数排序的方式计算每个分数具体有多少个同学然后利用前缀和技巧来实现分数段查询的 API
int[] scores; // 存储着所有同学的分数
// 试卷满分 100 分
int[] count new int[100 1]
// 记录每个分数有几个同学
for (int score : scores)count[score]
// 构造前缀和
for (int i 1; i count.length; i)count[i] count[i] count[i-1];
// 利用 count 这个前缀和数组进行分数段查询接下来我们看一看前缀和思路在二维数组中如何运用。
1.1 算法实践
二维矩阵中的前缀和 这是力扣第 304 题「二维区域和检索 - 矩阵不可变」其实和上一题类似上一题是让你计算子数组的元素之和这道题让你计算二维矩阵中子矩阵的元素之和
比如说输入的 matrix 如下图
按照题目要求矩阵左上角为坐标原点 (0, 0)那么 sumRegion([2,1,4,3]) 就是图中红色的子矩阵你需要返回该子矩阵的元素和 8。
当然你可以用一个嵌套 for 循环去遍历这个矩阵但这样的话 sumRegion 函数的时间复杂度就高了你算法的格局就低了。
注意任意子矩阵的元素和可以转化成它周边几个大矩阵的元素和的运算
而这四个大矩阵有一个共同的特点就是左上角都是 (0, 0) 原点。
那么做这道题更好的思路和一维数组中的前缀和是非常类似的我们可以维护一个二维 preSum 数组专门记录以原点为顶点的矩阵的元素之和就可以用几次加减运算算出任何一个子矩阵的元素和
class NumMatrix {// 定义preSum[i][j] 记录 matrix 中子矩阵 [0, 0, i-1, j-1] 的元素和private int[][] preSum;public NumMatrix(int[][] matrix) {int m matrix.length, n matrix[0].length;if (m 0 || n 0) return;// 构造前缀和矩阵preSum new int[m 1][n 1];for (int i 1; i m; i) {for (int j 1; j n; j) {// 计算每个矩阵 [0, 0, i, j] 的元素和preSum[i][j] preSum[i-1][j] preSum[i][j-1] matrix[i - 1][j - 1] - preSum[i-1][j-1];}}}// 计算子矩阵 [x1, y1, x2, y2] 的元素和public int sumRegion(int x1, int y1, int x2, int y2) {// 目标矩阵之和由四个相邻矩阵运算获得return preSum[x21][y21] - preSum[x1][y21] - preSum[x21][y1] preSum[x1][y1];}
}这样sumRegion 函数的时间复杂度也用前缀和技巧优化到了 O(1)这是典型的「空间换时间」思路。
前缀和技巧就讲到这里应该说这个算法技巧是会者不难难者不会实际运用中还是要多培养自己的思维灵活性做到一眼看出题目是一个前缀和问题。
除了本文举例的基本用法前缀和数组经常和其他数据结构或算法技巧相结合我会在 前缀和技巧高频习题 中举例讲解。
1.2 前缀和数组总结
前缀和数组适用场景原始数组不会被修改的情况下频繁查询某个区间的累加和前缀和数组优势前缀和数组是典型的空间换时间的解决方案将 前缀和 进行便利存储O(n) 时空复杂度然后 O(1) 时间复杂度得到最终数组区间和
2. 差分数组
本文讲一个和前缀和思想非常类似的算法技巧「差分数组」差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减。
比如说我给你输入一个数组 nums然后又要求给区间 nums[2…6] 全部加 1再给 nums[3…9] 全部减 3再给 nums[0…4] 全部加 2再给…
一通操作猛如虎然后问你最后 nums 数组的值是什么
常规的思路很容易你让我给区间 nums[i…j] 加上 val那我就一个 for 循环给它们都加上呗还能咋样这种思路的时间复杂度是 O(N)由于这个场景下对 nums 的修改非常频繁所以效率会很低下。
这里就需要差分数组的技巧类似前缀和技巧构造的 preSum 数组我们先对 nums 数组构造一个 diff 差分数组diff[i] 就是 nums[i] 和 nums[i-1] 之差
int[] diff new int[nums.length];
// 构造差分数组
diff[0] nums[0];
for (int i 1; i nums.length; i) {diff[i] nums[i] - nums[i - 1];
}通过这个 diff 差分数组是可以反推出原始数组 nums 的代码逻辑如下 int[] res new int[diff.length];
// 根据差分数组构造结果数组
res[0] diff[0];
for (int i 1; i diff.length; i) {res[i] res[i - 1] diff[i];
}这样构造差分数组 diff就可以快速进行区间增减的操作如果你想对区间 nums[i…j] 的元素全部加 3那么只需要让 diff[i] 3然后再让 diff[j1] - 3 即可 原理很简单回想 diff 数组反推 nums 数组的过程diff[i] 3 意味着给 nums[i…] 所有的元素都加了 3然后 diff[j1] - 3 又意味着对于 nums[j1…] 所有元素再减 3那综合起来是不是就是对 nums[i…j] 中的所有元素都加 3 了
只要花费 O(1) 的时间修改 diff 数组就相当于给 nums 的整个区间做了修改。多次修改 diff然后通过 diff 数组反推即可得到 nums 修改后的结果。
现在我们把差分数组抽象成一个类包含 increment 方法和 result 方法
// 差分数组工具类
class Difference {// 差分数组private int[] diff;/* 输入一个初始数组区间操作将在这个数组上进行 */public Difference(int[] nums) {assert nums.length 0;diff new int[nums.length];// 根据初始数组构造差分数组diff[0] nums[0];for (int i 1; i nums.length; i) {diff[i] nums[i] - nums[i - 1];}}/* 给闭区间 [i, j] 增加 val可以是负数*/public void increment(int i, int j, int val) {diff[i] val;if (j 1 diff.length) {diff[j 1] - val;}}/* 返回结果数组 */public int[] result() {int[] res new int[diff.length];// 根据差分数组构造结果数组res[0] diff[0];for (int i 1; i diff.length; i) {res[i] res[i - 1] diff[i];}return res;}
}
这里注意一下 increment 方法中的 if 语句 public void increment(int i, int j, int val) {diff[i] val;if (j 1 diff.length) {diff[j 1] - val;}
}当 j1 diff.length 时说明是对 nums[i] 及以后的整个数组都进行修改那么就不需要再给 diff 数组减 val 了。
2.1 算法实践
首先力扣第 370 题「区间加法」 就直接考察了差分数组技巧
那么我们直接复用刚才实现的 Difference 类就能把这道题解决掉
int[] getModifiedArray(int length, int[][] updates) {// nums 初始化为全 0int[] nums new int[length];// 构造差分解法Difference df new Difference(nums);for (int[] update : updates) {int i update[0];int j update[1];int val update[2];df.increment(i, j, val);}return df.result();
}当然实际的算法题可能需要我们对题目进行联想和抽象不会这么直接地让你看出来要用差分数组技巧这里看一下力扣第 1109 题「航班预订统计」
函数签名如下 int[] corpFlightBookings(int[][] bookings, int n)这个题目就在那绕弯弯其实它就是个差分数组的题我给你翻译一下
给你输入一个长度为 n 的数组 nums其中所有元素都是 0。再给你输入一个 bookings里面是若干三元组 (i, j, k)每个三元组的含义就是要求你给 nums 数组的闭区间 [i-1,j-1] 中所有元素都加上 k。请你返回最后的 nums 数组是多少
Note
因为题目说的 n 是从 1 开始计数的而数组索引从 0 开始所以对于输入的三元组 (i, j, k)数组区间应该对应 [i-1,j-1]。
这么一看不就是一道标准的差分数组题嘛我们可以直接复用刚才写的类
int[] corpFlightBookings(int[][] bookings, int n) {// nums 初始化为全 0int[] nums new int[n];// 构造差分解法Difference df new Difference(nums);for (int[] booking : bookings) {// 注意转成数组索引要减一哦int i booking[0] - 1;int j booking[1] - 1;int val booking[2];// 对区间 nums[i..j] 增加 valdf.increment(i, j, val);}// 返回最终的结果数组return df.result();
}这道题就解决了。
还有一道很类似的题目是力扣第 1094 题「拼车」我简单描述下题目
你是一个开公交车的司机公交车的最大载客量为 capacity沿途要经过若干车站给你一份乘客行程表 int[][] trips其中 trips[i] [num, start, end] 代表着有 num 个旅客要从站点 start 上车到站点 end 下车请你计算是否能够一次把所有旅客运送完毕不能超过最大载客量 capacity。
函数签名如下
boolean carPooling(int[][] trips, int capacity);比如输入
trips [[2,1,5],[3,3,7]], capacity 4这就不能一次运完因为 trips[1] 最多只能上 2 人否则车就会超载。
相信你已经能够联想到差分数组技巧了trips[i] 代表着一组区间操作旅客的上车和下车就相当于数组的区间加减只要结果数组中的元素都小于 capacity就说明可以不超载运输所有旅客。
但问题是差分数组的长度车站的个数应该是多少呢题目没有直接给但给出了数据取值范围
0 trips[i][1] trips[i][2] 1000车站编号从 0 开始最多到 1000也就是最多有 1001 个车站那么我们的差分数组长度可以直接设置为 1001这样索引刚好能够涵盖所有车站的编号 boolean carPooling(int[][] trips, int capacity) {// 最多有 1001 个车站int[] nums new int[1001];// 构造差分解法Difference df new Difference(nums);for (int[] trip : trips) {// 乘客数量int val trip[0];// 第 trip[1] 站乘客上车int i trip[1];// 第 trip[2] 站乘客已经下车// 即乘客在车上的区间是 [trip[1], trip[2] - 1]int j trip[2] - 1;// 进行区间操作df.increment(i, j, val);}int[] res df.result();// 客车自始至终都不应该超载for (int i 0; i res.length; i) {if (capacity res[i]) {return false;}}return true;
}至此这道题也解决了。
最后差分数组和前缀和数组都是比较常见且巧妙的算法技巧分别适用不同的场景而且是会者不难难者不会。所以关于差分数组的使用你学会了吗
更多经典的数组/链表技巧习题见 数组链表解题技巧精讲。
2.2 差分数组总结
差分数组适用场景差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减个人体感差分数组的适用范围更广一些尤其是针对问题的抽象如公交车航班问题
参考文献
[1] 小而美的算法技巧前缀和数组 [1] 小而美的算法技巧差分数组