第二课:前缀和、差分、双指针

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[i1]+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[l1]
  • 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];
}

用法总结

  1. 静态区间求和
  2. 关键是迭代公式
  3. 扩展到前缀和前缀最小值,前缀和前缀最大值等(固定右端,配合左端的前缀最小/最大值找最大/小子区间和)

前缀和的前缀最小值

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=1iy=1jA[x][y]=s[i1][j]+s[i][j1]s[i1][j1]+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=piy=qjA[x][y]=s[i][j]s[i][q1]s[p1][j]+s[p1][q1]
// 二维前缀和模板
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=AiAi1(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
a 截屏2021-06-22 上午12.06.15

不理解公式?理解下面三句话

  • 任何对于区间的操作,可以转化为两个关键点(事件)
  • 事件的影响 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.算法对比

思考

  1. 为什么求“子段和”(窗口求和)可以用前缀和“
  2. 为什么求”滑动窗口最大值“要用单调队列
  3. 遇到一道跟”子段“(窗口)有关的题,什么时候用前缀和,什么时候用双指针扫描,什么时候用单调队列?

维护的信息是关于一个点的,还是一整个候选集合(多个点)的

  • 前者用双指针扫描,后者单调队列

区间减法性质

  • 指的是[l,r]的信息可以由[1,r][1,l-1]的信息导出
  • 满足区间减法,可以用前缀和

实战

一定要记得判断越界

  • 1248-统计【优美子数组】https://leetcode-cn.com/problems/count-number-of-nice-subarrays/
    • 两变量循环,首先分离
      1. 根据奇偶性将元素转化为0/1
      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/

  • 双端队列,存下标(时间)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值