1 前缀和
- 一维数组A
- 通常将其下标变为1~n,在前面补一个0,防止计算S[r]-S[l-1]时越界
nums = [0] + nums;
- 前缀和数组S:
- s [ i ] = s [ i − 1 ] + A [ i ] s[i] = s[i-1] + A[i] s[i]=s[i−1]+A[i] // 递归思想
- 子段和——A中第l个数到第r个数的和
- s u m ( l , r ) = ∑ i = l r A [ i ] = S [ r ] − S [ l − 1 ] sum(l,r) = \sum^r_{i=l}A[i]=S[r]-S[l-1] sum(l,r)=∑i=lrA[i]=S[r]−S[l−1]
- A中都是非负数时,前缀和数组S单调递增
- 固定外层循环变量,考虑内层满足什么条件
// 前缀和模板
vector<int> input;
vector<int> s(input.size() + 1, 0);
for (int i = 0; i <= s.size() + 1; i++){
s[i+1] = s[i] + input[i];
}
用法总结
- 静态区间求和
- 关键是迭代公式
- 扩展到前缀和前缀最小值,前缀和前缀最大值等(固定右端,配合左端的前缀最小/最大值找最大/小子区间和)
前缀和的前缀最小值
vector<int> minist(s.size(), 0)
for (int n = 1; n < s.size(); n++){
minist[n] = min(minist[n - 1], s[n]);
}
[0, 5, 9, 8, 15, 23]
1.2 二维前缀和
- 矩阵ij的和
- 二维数组A
- 前缀和数组(递归)
- s [ i ] [ j ] = ∑ x = 1 i ∑ y = 1 j A [ x ] [ y ] = s [ i − 1 ] [ j ] + s [ i ] [ j − 1 ] − s [ i − 1 ] [ j − 1 ] + A [ i ] [ j ] s[i][j]= \sum^i_{x=1} \sum^j_{y=1}A[x][y]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+A[i][j] s[i][j]=∑x=1i∑y=1jA[x][y]=s[i−1][j]+s[i][j−1]−s[i−1][j−1]+A[i][j]
好好体会递归二字就能理解为啥要这么写这个公式了
- 子矩阵和——
(
p
,
q
)
(p,q)
(p,q)到
(
i
j
)
(ij)
(ij)
- s u m ( p , q , i , j ) = ∑ x = p i ∑ y = q j A [ x ] [ y ] = s [ i ] [ j ] − s [ i ] [ q − 1 ] − s [ p − 1 ] [ j ] + s [ p − 1 ] [ q − 1 ] sum(p,q,i,j)=\sum^i_{x=p}\sum^j_{y=q}A[x][y]=s[i][j]-s[i][q-1]-s[p-1][j]+s[p-1][q-1] sum(p,q,i,j)=∑x=pi∑y=qjA[x][y]=s[i][j]−s[i][q−1]−s[p−1][j]+s[p−1][q−1]
// 二维前缀和模板
vector<vector<int>> matrix; // 输入二维数组 matrix[x][y]
vector<vector<int>> s; // 前缀和二维数组
s.resize(matrix.size() + 1, vector<int>(matrix[0].size() + 1, 0))
for (int x = 0; x <= matrix.size(); x++){
for(int y = 0; y <= matrix[0].size(); y++){
s[x + 1][y + 1] = s[x][y + 1] + s[x + 1][y] - s[x][y] + matrix[x][y];
}
}
2. 差分
- 一维数组A
- 差分数组B
- B 1 = A 1 , B i = A i − A i − 1 ( 2 < = i < = n ) B_1=A_1,B_i=A_i-A_{i-1}(2<=i<=n) B1=A1,Bi=Ai−Ai−1(2<=i<=n)
- 数组A的前缀和数组C,求差分后还是原数组A
- 同理差分数组B的前缀和数组就是原数组A
- 把A的第l个数到第r个数加d,B的变化为: B l B_l Bl加 d d d, B r + 1 B_{r+1} Br+1减 d d d
不理解公式?理解下面三句话
- 任何对于区间的操作,可以转化为两个关键点(事件)
- 事件的影响 effect 从l开始,到r+1结束。开始为+,结束为-
- 最后累加影响为coEffect数组,然后求前缀和数组就是输出了
// 注意 effcet 可能在5+1结束
编号 1 2 3 4 5
thing1 10 -10
thing2 20 -20
thing3 25 -25
coEffect 10 45 -10 -20 0
ans 10 55 45 25 25
还不理解?背下面的模板
// 输入things = [l, r, effect]
vector<int> corpFlightBookings(vector<vector<int>>& things, int n) {
vector<int> coEffect(n + 2, 0); // 累加影响数组,things是从1开始的。
for (auto& thing : things){
int l = thing[0];
int r = thing[1];
int effect = thing[2];
coEffect[l] += effect;
coEffect[r + 1] -= effect;
}
vector<int> ans(n, 0); // 前缀和数组
ans[0] = coEffect[1] // 迭代公式需要用到i-1防止越界,先定义好ans[0]
for (int i = 1; i <= n; i++){
ans[i] = ans[i-1] + coEffect[i];
}
return ans;
}
3. 双指针扫描,滑动窗口
3.1 双指针扫描
用于解决一类基于“子段”的统计问题
子段:数组中连续的一段(下标范围可以用一个闭区间来表示)
这类题目的朴素做法都是两重循环的枚举,枚举左端点
l
l
l,右端点
r
(
l
<
r
)
r(l<r)
r(l<r)
优化手法都是找到枚举中的冗余部分,将其去除
- 决定能否使用双指针的条件:
- 只和两个点相关就用双指针,和中间都相关就不可
优化策略通常有:
- 固定右端点,看左端点的取值范围
- 例如左端点的取值范围是一个前缀,可以用“前缀和”等算法维护前缀信息
- 移动一个断点,看另一个端点的变化情况
- 例如一个端点跟随另一个端点单调移动,类似与滑动窗口
- 此时考虑”双指针“扫描
- 或者左右端点相向/背驰移动
- 例如一个端点跟随另一个端点单调移动,类似与滑动窗口
4. 单调栈、单调队列
单调栈:
思路
- 确定递增递减——关键在于考虑“前面不能影响到后面”的条件
- 求最大面积的题中若
h[i-1] > h[i]
,则h[i-1]
这个高度就无法影响到更后面,可单独计算
单调队列:
- 单调队列维护的是一个候选集合,前面的比较旧,后面的比较新(时间的单调性)
- 候选项的某个属性也具有单调性
- 确定递增递减的方法——考虑任意两个候选项 j 1 < j 2 j_1<j_2 j1<j2,写出 j 1 j_1 j1比 j 2 j_2 j2优的条件
排除冗余的关键:若 j 1 j_1 j1比 j 2 j_2 j2差, j 1 j_1 j1的生命周期还比 j 2 j_2 j2短,那 j 1 j_1 j1就没作用
代码套路
-
For 每个元素
- (1)while(队头过期)队头出队
- (2)判断合法性,取队头为最佳选项,计算答案
- (3)while(队尾与新元素不满足单调性)队尾出队
- (3)新元素入队
(2)(3)的顺序取决于i是不是候选项
5.算法对比
思考
- 为什么求“子段和”(窗口求和)可以用前缀和“
- 为什么求”滑动窗口最大值“要用单调队列
- 遇到一道跟”子段“(窗口)有关的题,什么时候用前缀和,什么时候用双指针扫描,什么时候用单调队列?
维护的信息是关于一个点的,还是一整个候选集合(多个点)的
- 前者用双指针扫描,后者单调队列
区间减法性质
- 指的是
[l,r]
的信息可以由[1,r]
和[1,l-1]
的信息导出 - 满足区间减法,可以用前缀和
实战
一定要记得判断越界
- 1248-统计【优美子数组】https://leetcode-cn.com/problems/count-number-of-nice-subarrays/
- 两变量循环,首先分离
-
- 根据奇偶性将元素转化为0/1
-
- 判断优美子数->>有多少子段和为k
- 前缀和加count计数的方式
- 滑动窗口
/*
其下标变为1~n,在前面补一个0
固定外层循环变量,考虑内层满足什么条件
对于每个r(1~n),考虑有几个l(1~r),使得s[r] - s[l-1] = k
对于每个i(1~n),考虑有几个j(0~i-1),使得s[i] - s[j] = k
对于每个i(1~n),考虑有几个j(0~i-1),使得s[j] = s[i] - k
对于每个i,有几个前缀和数组 s[j] 等于s[i] - k
转化为在一个数组 (s) 中统计“等于某一个数”的数的数量
*/
// 其下标变为1~n,在前面补一个0,防止计算S[r]-S[l-1]时越界
// 根据奇偶性将元素转化为0/1
vector<int> s;
s[0] = 0;
for (int i = 0; i < nums.size(); i++){
s[i+1] = nums[i] / 2;
}
// 转化为在一个数组 (s) 中统计“等于某一个数”的数的数量
// s = [0, 1, 2, 2, 2, 3]
// count = [1, 1, 3, 1] // 1个0,1个1,3个2,1个3
vector<int> count;
for (int i = 0; i < s.size(); i++){
count[s[i]] += 1;
}
// 对于每个i,有几个前缀和数组 s[j] 等于s[i] - k
for (int i = 0; i < s.size(); i++){
if (s[i] - k >= 0){
ans += count[s[i] - k];
}
}
return ans;
304 二维区域和检索https://leetcode-cn.com/problems/range-sum-query-2d-immutable/
- 先理解再背好模板!!!
1109 航班统计https://leetcode-cn.com/problems/corporate-flight-bookings/
- 理解,背好模板!!!!
53 最大子序和https://leetcode-cn.com/problems/maximum-subarray/
- 方法1:动态规划
- 方法2:前缀和s、前缀和最小值minist、子区间最大值ans
- 关键的比较
ans = max(ans, s[i] - minist[i - 1]);
- 关键的比较
167 两数之和II-输入有序数组https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/
- 固定一端,看另一断的变化
- 和不变,l减小,r增大。夹逼
- for r : 0~n-1
- 只要lr没相遇,并且l+r大于target,就增大l,直到lr相遇就下一次循环,如果l+r等于target了,就返回{i+1, j+1}//因为题目要求从1开始计数所以+1
1 两数之和-输入无序数组https://leetcode-cn.com/problems/two-sum/
- 新开二元组记录每个数和对应下标
- 排序——用上一题的模板,返回两个数
- 通过两个数去二元组插下标
15 三数之和https://leetcode-cn.com/problems/3sum/
- 两数之和等于-i
- 一定要注意,题目要求不能重复
- 因此固定i调用两数只和的时候,要注意顺序
i<l<j
才可以; - 这一题要求返回所有的数组,所以两数和定义
vector<vector<int>>
- 注意对最后的输出去重,因为可能输入数组又重复元素
- 去重太难了!!!!
11 盛最多水的容器https://leetcode-cn.com/problems/container-with-most-water/
- 双指针逼近
- ans暂存短边*(r-l)
- 短边抛弃递归
- 再求面积与ans比取max
84 模板题-柱状图中最大的矩形https://leetcode-cn.com/problems/largest-rectangle-in-histogram/
代码
- 新建栈
- 在数组最后添加0,在全部递增的情况下帮助弹栈
- for 每个元素
- while(栈顶(上个元素)比新元素更高){累加“宽度”,更新答案,弹栈}
- 递增 入栈
239 模板题-滑动窗口最大值https://leetcode-cn.com/problems/sliding-window-maximum/
- 双端队列,存下标(时间)