算法学习笔记——常用技巧:滑动窗口与前缀和思想结合(求子数组数量问题)

关于数组的连续区间的问题,应条件反射想到滑动窗口和前缀和技巧

滑动窗口

利用左右指针,在一次遍历中求解题目

  • 典型的问题是:限定一个子数组所需满足的性质,然后求:符合条件的子数组有多少个/符合条件的最长子数组是什么、长度为多少 等
  • 每次先扩展窗口,直到不符合要求时,收缩窗口
    这样,每一对“扩展和收缩”之后,始终保证窗口是满足要求的、最长的子数组,此时我们维护所需的答案,最终就能得到最优解

应用场景

使用滑动窗口(双指针)解决的问题多与「最小」、「最大」有关,这是因为我们可以在满足条件的前提下,尽可能维护最长的窗口
例如:
LeetCode 3. 无重复字符的最长子串;(VIP)
LeetCode 209. 长度最小的子数组;
LeetCode 76. 最小覆盖子串;(209题的升级版)
LeetCode 159. 至多包含两个不同字符的最长子串;(VIP)
LeetCode 424. 替换后的最长重复字符(⭐转化问题:维护窗口,除了数量最多的字符外,其余字符的个数必须小于等于k)

例题

LeetCode 904. 水果成篮
给出一个数组,求满足条件的最长子数组长度,条件是:数组中所含的数字种类<=2种

字典cnt维护当前窗口内的数字及其个数,kind维护当前窗口内的数字种类,kind超过2时,收缩窗口,不断更新窗口的最大长度r-l+1即可

前缀和

核心思路

核心思路是把多个不同区间的问题,统一为相同类型的子问题

  • 具体而言,[l,r]区间的查询,左右两边界是不固定的,我们固定一边
  • 问题转化为:[l,r]区间和=[0,r+1)区间和 - [0,l)区间和
    (所有[0,i)区间的和提前求出)

我们可以推广这种思想:把[左右界不固定的问题] 转化为两个 [左边界固定,右边界不定]的问题

变种-求子数组数量问题:滑动窗口+前缀和思想

前面介绍过,使用滑动窗口(双指针)解决的问题多与「最小」、「最大」有关
然而还有一些问题,不是最大、最小的问题,也可以用滑动窗口求解,但前提是需要把原问题转换成为容易求解的问题

问题要求数组的某个指标在[left,right]范围内/要求指标恰好为k(相当于left=right=k的情况),我们把「某范围内 / 恰好」的复杂问题转化为「最多」的滑动窗口问题

这里需要类比前缀和的思想,(并不是真正的前缀和,而是问题转化的思维)
例如:

  1. 最大值在[left,right]之间的子数组数量 = atMost(right) - atMost(left-1)
    其中atMost(n)为数组的最大值<=n的子数组数量
  2. 有k种数字的子数组数量 = atMost(k) - atMost(k-1)
    其中atMost(kind)数字种类<=kind种的子数组数量
0. 前置知识:求一个数组的子数组个数

要做到不重不漏的统计,有多种方法(例如,可以依次求长度为1、2、…的子数组分别有几个,然而这样的方法不便于代码实现)

  • 我们固定子数组的一端,依次求以nums[0]、nums[1]…结尾的子数组个数,然后叠加即可
  • 例如,对于[1,2,3]r指针从左往右,依次统计了子数组
    [1](+1)、
    [1,2][2](+2)、
    [1,2,3][2,3][3](+3),
    共6个子数组

LeetCode 713. 乘积小于K的子数组
给一个数组,求所有数字乘积小于 k 的子数组的个数
分析:
滑动窗口,每次维护以r为右边界的、满足条件的[最长]子数组窗口
每次窗口长L,则新增L个符合要求的子数组
[2,3]长为2,新增2个乘积小于7的子数组[2,2][3]

LeetCode 467. 环绕字符串中唯一的子字符串
类似的思想,以某字母结尾的子串最长为n,则就找到了n种新的不同子串

LeetCode 1358. 包含所有三种字符的子字符串数目
给出只含有a,b,c的字符串s,统计a,b,c都至少出现一次的子串数量(不同下标的相同字符串算多次)
仍然是固定右边界来统计:
维护r指针为结尾的、满足要求的[最短]滑动窗口,对于该窗口,向左扩张同样是满足要求的答案,(s='aaabc','abc'是当前窗口,而'aabc''aaabc'也满足)
因此,每次满足条件的最短窗口[l,r],那么新增的答案数量为l+1

LeetCode 340. 至多包含 K 个不同字符的最长子串

1. 求最大值在[left,right]之间的子数组数量

LeetCode 795. 区间子数组个数
给出一个数组,求满足要求的子数组数量,要求:子数组的最大值在[left,right]之间
如nums = [2,1,4,3], left = 2, right = 3,返回3(满足条件的三个子数组:[2], [2, 1], [3])

这里要统计子数组数目,不仅要保证计数不重不漏,还要满足特定条件

直接思路:
我们如果尝试用滑动窗口一边遍历一边统计会发现比较困难,例如窗口为[2,1]时,最大值在[2,3]之间的子数组数量肯定不是+2,因为[1]并不是这样的子数组;如果这里+2,统计的是最大值不超过3种的子数组数量
(怎么解决呢?如果在每个窗口的位置,依次列举并判断其包含的子数组是否满足要求,显然大大增加了计算量)

转化问题:
类比前面的前缀和思想,我们把问题转化为:
求数组的最大值<=n的子数组数量atMost(n)
最大值在[left,right]之间的子数组数量 = atMost(right) - atMost(left-1)

再特殊一点,left=right=n,就是求最大值刚好为n的子数组数量,同样可以通过 atMost(n) - atMost(n-1)求出

实现:atMost函数的实现就比较简单了,在前文的子数组计数的方法之上稍加修改即可

class Solution:
    def numSubarrayBoundedMax(self, nums: List[int], left: int, right: int) -> int:
        # 问题分解为两个子问题,它们都可以由一次遍历求出
        # 数组最大值在[left, right] 内的数组数 = 数组最大值小于等于right的数组数 - 数组最大值小于等于left-1的数组数
        def atMost(n):
            """求解最大值<=n的子数组数量"""
            # 遍历数组中的num,以数字num结尾的,满足要求的最长连续数组长为L,那么就找到了L个符合要求的子数组
            combo = 0
            ans = 0
            for num in nums:
                if num <= n:
                    combo += 1
                else:
                    combo = 0
                ans += combo
            return ans

        return atMost(right) - atMost(left - 1)
2. 求恰好包含K 个不同整数的子数组数量

LeetCode 992. K 个不同整数的子数组
给一个数组,求其中刚好有k种不同数字的子数组数量
如nums = [1,2,1,2,3], k = 2,返回7
(恰好由 2 个不同整数组成的子数组:[1,2], [2,1], [1,2], [2,3], [1,2,1], [2,1,2], [1,2,1,2].)

  • 首先,肯定要使用滑动窗口,维护窗口内的数字种类:r指针枚举所有可能的数组结尾,每次收缩l指针保证当前窗口是满足要求的最长窗口
  • 但是,我们无法通过一次滑动窗口直接得出答案:
    例如窗口为[1,2]时,刚好有2种数字的子数组数量肯定不是+2,因为[2]并不是这样的子数组,如果这里+2,统计的是数字种类不超过2种的子数组数量
    (怎么解决呢?如果在每个窗口的位置,依次列举并判断其包含的子数字是否满足要求,显然大大增加了计算量)
  • 同样是前缀和的思路转化问题:
    数字种类<=kind种的子数组数量为atMost(kind)
    有k种数字的子数组数量 = atMost(k) - atMost(k-1)
from collections import defaultdict


class Solution:
    def subarraysWithKDistinct(self, nums: List[int], k: int) -> int:
        """求最长连续子数组的长度,要求数组中元素种类==k"""

        # 分解子问题
        # 满足要求的数组个数 = 元素种类小于等于k的数组个数 - 元素种类小于等于k-1的数组个数
        def atMost(kinds):
            """元素种类小于等于kinds的数组个数"""
            L = len(nums)
            # 滑动窗口
            cnt = defaultdict(lambda: 0)
            l = 0
            # 每次向右扩展窗口,维护窗口内的元素种类数,如果元素种类>kinds,收缩窗口
            types, ans = 0, 0
            for r, num in enumerate(nums):
                # 新入窗口的元素
                cnt[num] += 1
                if cnt[num] == 1:
                    types += 1
                # 如果多于kinds种,收缩
                while types > kinds:
                    del_num = nums[l]
                    cnt[del_num] -= 1
                    if cnt[del_num] == 0:
                        types -= 1
                    l += 1
                # 此处元素种类数<=kinds
                # 统计答案,以num为结尾的,符合条件的子数组数量=当前的窗口长度
                ans += r - l + 1
            return ans

        return atMost(k) - atMost(k - 1)

你可能感兴趣的:(算法学习笔记,算法,学习,leetcode)