算法——滑动窗口+前缀和

在刷leetcode时,看到一道精选的题解一次搞定前缀和觉得非常有用,文章的作者总结了关于滑动窗口和前缀和的知识点,于是想着在自己的博客做个记录,方便自己后面的学习回顾。

该作者的关于其他算法知识的总结:算法知识点总结

滑动窗口

滑动窗口这一内容复制粘贴于:滑动窗口

常见套路

滑动窗口主要用来处理连续问题。比如题目求解“连续子串 xxxx”,“连续子数组 xxxx”,就应该可以想到滑动窗口。能不能解决另说,但是这种敏感性还是要有的。

从类型上说主要有:

  • 固定窗口大小
  • 窗口大小不固定,求解最大的满足条件的窗口
  • 窗口大小不固定,求解最小的满足条件的窗口(上面的 209 题就属于这种)

后面两种我们统称为可变窗口。当然不管是哪种类型基本的思路都是一样的,不一样的仅仅是代码细节。

固定窗口大小

对于固定窗口,我们只需要固定初始化左右指针 l 和 r,分别表示的窗口的左右顶点,并且保证:

  1. l 初始化为 0
  2. 初始化 r,使得 r - l + 1 等于窗口大小
  3. 同时移动 l 和 r
  4. 判断窗口内的连续元素是否满足题目限定的条件
    • 4.1 如果满足,再判断是否需要更新最优解,如果需要则更新最优解
    • 4.2 如果不满足,则继续。

可变窗口大小

对于可变窗口,我们同样固定初始化左右指针 l 和 r,分别表示的窗口的左右顶点。后面有所不同,我们需要保证:

  1. l 和 r 都初始化为 0
  2. r 指针移动一步
  3. 判断窗口内的连续元素是否满足题目限定的条件
    • 3.1 如果满足,再判断是否需要更新最优解,如果需要则更新最优解。并尝试通过移动 l 指针缩小窗口大小。循环执行 3.1
    • 3.2 如果不满足,则继续。

形象地来看的话,就是 r 指针不停向右移动,l 指针仅仅在窗口满足条件之后才会移动,起到窗口收缩的效果。

模板代码

伪代码

初始化慢指针 = 0
初始化 ans

for 快指针 in 可迭代集合
   更新窗口内信息
   while 窗口内不符合题意
      扩展或者收缩窗口
      慢指针移动
   更新答案
返回 ans

代码

以下是 209 题目的代码,使用 Python 编写,大家意会即可。

class Solution:
    def minSubArrayLen(self, s: int, nums: List[int]) -> int:
        l = total = 0
        ans = len(nums) + 1
        for r in range(len(nums)):
            total += nums[r]
            while total >= s:
                ans = min(ans, r - l + 1)
                total -= nums[l]
                l += 1
        return  0 if ans == len(nums) + 1 else ans

题目列表(有题解)

以下题目有的信息比较直接,有的题目信息比较隐蔽,需要自己发掘

  • 【Python,JavaScript】滑动窗口(3. 无重复字符的最长子串)
  • 76. 最小覆盖子串
  • 209. 长度最小的子数组
  • 【Python】滑动窗口(438. 找到字符串中所有字母异位词)
  • 【904. 水果成篮】(Python3)
  • 【930. 和相同的二元子数组】(Java,Python)
  • 【992. K 个不同整数的子数组】滑动窗口(Python)
  • 978. 最长湍流子数组
  • 【1004. 最大连续 1 的个数 III】滑动窗口(Python3)
  • 【1234. 替换子串得到平衡字符串】[Java/C++/Python] Sliding Window
  • 【1248. 统计「优美子数组」】滑动窗口(Python)
  • 1658. 将 x 减到 0 的最小操作数

前缀和

前缀和这一个内容复制粘贴于:一次搞定前缀和。

前缀和的母题

母题0

有 N 个的正整数放到数组 A 里,现在要求一个新的数组 B,新数组的第 i 个数 B[i]是原数组 A 第 0 到第 i 个数的和。

这道题可以使用前缀和来解决。 前缀和是一种重要的预处理,能大大降低查询的时间复杂度。我们可以简单理解为“数列的前 n 项的和”。这个概念其实很容易理解,即一个数组中,第 n 位存储的是数组前 n 个数字的和。

对 [1,2,3,4,5,6] 来说,其前缀和可以是 pre=[1,3,6,10,15,21]。我们可以使用公式pre[]=pre[−1]+nums[]得到每一位前缀和的值,从而通过前缀和进行相应的计算和解题。其实前缀和的概念很简单,但困难的是如何在题目中使用前缀和以及如何使用前缀和的关系来进行解题。

母题 1

如果让你求一个数组的连续子数组总个数,你会如何求?其中连续指的是数组的索引连续。 比如 [1,3,4],其连续子数组有:[1], [3], [4], [1,3], [3,4] , [1,3,4],你需要返回 6。

一种思路是总的连续子数组个数等于:以索引为 0 结尾的子数组个数 + 以索引为 1 结尾的子数组个数 + ... + 以索引为 n - 1 结尾的子数组个数,这无疑是完备的。

算法——滑动窗口+前缀和_第1张图片

同时利用母题 0 的前缀和思路, 边遍历边求和。

实现代码如下:

def countSubArray(nums):
    pre, ans = 0, 0
    for num in nums:
        pre += 1
        ans += pre
    return ans

 母题2

继续修改下题目, 如果让你求一个数组相邻差为 1 连续子数组的总个数呢?其实就是索引差 1 的同时,值也差 1。

和上面思路类似,无非就是增加差值的判断。

实现代码如下:

def countSubArray(nums):
    ans = 1
    pre = 1
    for i in range(len(nums)):
        if nums[i] - nums[i - 1] == 1:
            pre += 1
        else:
            pre = 0
        ans += pre
    return ans

 如果值差只要大于 1 就行呢?其实改下符号就行了,这就是求上升子序列个数

母题3

如果让求出不大于 k 的子数组的个数呢?不大于 k 指的是子数组的全部元素都不大于 k。 比如 数组[1,3,4] 的子数组有 [1], [3], [4], [1,3], [3,4] , [1,3,4],不大于 3 的子数组有 [1], [3], [1,3] ,那么 [1,3,4] 不大于 3 的子数组个数就是 3。 实现函数 atMostK(k, nums)。

代码如下:

def atMostK(k, nums):
    pre, ans = 0, 0
    for i in range(len(nums)):
        if nums[i] <= k:
            pre += 1
        else:
            # 当nums[i]>k时,说明包含nums[i]的子数组不符合条件,所以pre=0
            pre = 0
        ans += pre
    return ans

母题4

如果让求出子数组最大值刚好是 k 的子数组的个数呢? 比如 [1,3,4] 子数组有 [1], [3], [4], [1,3], [3,4] , [1,3,4],子数组最大值刚好是 3 的子数组有 [3], [1,3] ,那么 [1,3,4] 子数组最大值刚好是 3 的子数组个数就是 2。实现函数 exactK(k, nums)。

实际上是 exactK 可以直接利用 atMostK,即 atMostK(k) - atMostK(k - 1),原因见下方母题 5 部分。

母题5

如果让求出子数组最大值刚好是 介于 k1 和 k2 的子数组的个数呢?实现函数 betweenK(k1, k2, nums)。

实际上是 betweenK 可以直接利用 atMostK,即 atMostK(k1, nums) - atMostK(k2 - 1, nums),其中 k1 > k2。前提是值是离散的, 比如上面我出的题都是整数。 因此我可以直接 减 1,因为 1 是两个整数最小的间隔。

算法——滑动窗口+前缀和_第2张图片

如上,小于等于 10 的区域减去 小于 5 的区域就是 大于等于 5 且小于等于 10 的区域。

注意我说的是小于 5, 不是小于等于 5。 由于整数是离散的,最小间隔是 1。因此小于 5 在这里就等价于 小于等于 4。这就是 betweenK(k1, k2, nums) = atMostK(k1) - atMostK(k2 - 1) 的原因。

因此不难看出 exactK 其实就是 betweenK 的特殊形式。 当 k1 == k2 的时候, betweenK 等价于 exactK。因此 atMostK 就是灵魂方法,一定要掌握,不明白建议多看几遍。

前缀和例题

具体的题目讲解参见原文章(懒得写了,嘿嘿)

leetcode 467. 环绕字符串中唯一的子字符串

leetcode 795. 区间子数组个数

leetcode 904. 水果成篮

leetcode 992. K 个不同整数的子数组

leetcode 代码 测试用例 测试结果 测试结果 1109. 航班预订统计

学会了上述几题之后,可以自己再练练前缀和的题:

  • 303. 区域和检索 - 数组不可变
  • 1186.删除一次得到子数组最大和
  • 1310. 子数组异或查询
  • 1371. 每个元音包含偶数次的最长子字符串

关于1109. 航班预订统计题的题解,用到了差分数组,详见下面。

差分数组

由于本人太菜,一开始不知道什么是差分数组,于是百度了一下,看到一篇文章讲的很明白,于是在此做个记录,方便自己后面查看。原文链接:差分数组(详解+例题)

差分数组的概念

差分数组就是从数组的第二项(下标为1)开始,每一项元素等于当前项减去前一项元素.

原数组 5 4 7 2 4 3 1

进行后一项减去前一项,第一项不变
5 、(4-5) 、(7-4)、(2-7)、(4-2)、(3-4)、(1-3)

差分数组就等于::5 -1 3 -5 2 -1 -2

差分数组的性质(重点!!!)

最主要的性质:差分数组求前缀和结果等于原数组

接着用上面的样例
对差分数组求前缀和,得到:5、4、7、2、4、3、1

差分数组的主要用途(重点!!!)

 差分数组最主要用于对数组部分区间进行同时加上或减去一个数

举个例子:

原数组如下,现在需要对下标从1~4的元素,让他们同时加上1,也就是使原数组变为 5 5 8 3 5 3 1

数组下标 0 1 2 3 4 5 6
数组元素 5 4 7 2 4 3 1

需要数组变成如下

数组下标 0 1 2 3 4 5 6
原数组元素 5 4 7 2 4 3 1
对下标为1-4的元素进行加1操作后 5 5 8 3 5 3 1

最朴素的想法是从下标1一直遍历到4 使(a[1]~a[4])++,这样的复杂度为O(n)级别 在此题为O(4),如果有区间长度为1w甚至1亿长的元素需要++操作呢?这时候就要使用差分数组来完成。

  1. 先求出原数组的差分数组,如下表格
  2. 对下标为1的元素进行++操作,对下标为5的元素进行减减操作,可以得到新的差分数组:5 0 3 -5 2 -2 -2
  3. 对新的差分数组求前缀和 5 5 8 3 5 3 1,即完成了需求。
数组下标 0 1 2 3 4 5 6
原数组元素 5 4 7 2 4 3 1
原数组对应的差分数组 5 -1 3 -5 2 -1 -2
新的差分数组 5 0 3 -5 2 -2 -2
对新差分数组求前缀和 5 5 8 3 5 3 1

为什么会产生这样的效果呢?

其实这就是差分数组的定义和性质造成的,由于差分数组是后一项减去前一项,并且求前缀和才变为原数组,我们很容易想到在求前缀和的时候,只要其中一个元素加加或减减,那么后面的值跟着加价或减减,,所以为了达到使a[n]~a[m]区间进行加加减减,需要对a[m+1]进行相反的加加减减,对a[n]进行相同的加加减减就可以达到该效果!
解释一下上面这段话

首先解释一下差分数组为什么会有对差分数组求前缀和得到原数组的性质,还是以上面的数组为例

数组下标 0 1 2 3 4 5 6
原数组元素arr 5 4 7 2 4 3 1
原数组对应的差分数组diff 5 -1 3 -5 2 -1 -2
对diff求前缀和diff_sum 5 4 7 2 4 3 1
  • 从下标为1的元素开始,diff[1] = arr[1] - arr[0] = 4 - 5 = -1,那么求前缀和求到第二项时(下标为1),diff_sum[1] = diff_sum[0] + diff[1] = 5 + (-1) = 5 + (4 - 5) = 4,得到原数组的第二项元素arr[1]
  • 对下标为2的元素,diff[2] = arr[2] - arr[1] = 7 - 4 = 3,那么求前缀和求到第三项时(下标为2),diff_sum[2] = diff_sum[1] + diff[2] = 4 + 3 = 4 + (7 - 4) = 7,得到原数组的第三项元素arr[2]
  • 对下标为3的元素,diff[3] = arr[3] - arr[2] = 2 - 7 = -5,那么求前缀和求到第四项时(下标为3),diff_sum[3] = diff_sum[2] + diff[3] = 7 + (-5) = 7 + (2 - 7) = 2,得到原数组的第四项元素arr[3]
  • 对下标为4的元素,diff[4] = arr[4] - arr[3] = 4 - 2 = 2,那么求前缀和求到第五项时(下标为4),diff_sum[4] = diff_sum[3] + diff[4] = 2 + 2 = 2 + (4 - 2) = 4,得到原数组的第五项元素arr[4]
  • 对下标为5的元素,diff[5] = arr[5] - arr[4] = 3 - 4 = -1,那么求前缀和求到第六项时(下标为5),diff_sum[5] = diff_sum[4] + diff[5] = 4 + (-1) = 4 + (3 - 4) = 3,得到原数组的第六项元素arr[5]
  • 对下标为6的元素,diff[6] = arr[6] - arr[5] = 1 - 3 = -2,那么求前缀和求到第七项时(下标为6),diff_sum[6] = diff_sum[5] + diff[6] = 3 + (-2) = 3 + (1 - 3) = 1,得到原数组的第六项元素arr[6]

由上面的过程可见,差分数组中从第二项元素(下标为1)开始的每个元素所减去的前一个元素的值x,都会在求前缀和时加上之前所减去的x,所以对差分数组求前缀和会得到原数组。

那么在上面的例子(对数组小标为1-4的元素加1)中,在差分数组diff下标为1的元素处加上了1(即diff[1] += 1),那么这个1就会在求前缀和的时候一直给后面的元素加上(即从下标1的位置开始,后面的元素都会加上1),所以下标为5、6的元素也会加上,所以要把下标为5的位置的元素减1,那么前面加上的这个1就会被减1抵消掉,所以相当于位置5的元素没有加上这个1,那么从位置5开始,往后也不会加上这个1,也就还是原数组的元素(求前缀和求到位置5,位置5进行了+1-1的操作,相当于位置5没有多出这个1,那么前缀和求到位置6的时候,也不会多出1,往后如果还有元素,求前缀和也不会多出1),就实现了在数组区间1-4同时加上1这个需求。

算法——滑动窗口+前缀和_第3张图片

你可能感兴趣的:(数据结构和算法,算法,滑动窗口,前缀和,差分数组,leetcode)