leetcode(13)滑动窗口系列题目

209. 长度最小的子数组

向右移动右边界,若>=target则收缩左边界

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        res = float('inf')
        sum_ = 0
        left = 0
        for right in range(len(nums)):
            sum_ += nums[right]            
            while sum_ >= target:
                sum_ -= nums[left]
                res = min(res, right - left + 1)
                left += 1                
            # print(res)
        return res if res != float('inf') else 0

904. 水果成篮

其实就是选只有两个元素的最长连续子序列,比如1,2,3,2,2最长就是2,3,2,2(只包括2或者3,而且是最长的)
注意:与209. 长度最小的子数组的区别是使用字典记录不同数字的个数,如果字典长度超过2就收缩左边界

class Solution:
    def totalFruit(self, fruits: List[int]) -> int:
        left, right, res = 0, 0, 0
        dic = Counter()
        while right < len(fruits):
            dic[fruits[right]] += 1
            while len(dic) > 2:
                dic[fruits[left]] -= 1
                if dic[fruits[left]] == 0:
                    del dic[fruits[left]]                
                left += 1
            right += 1
            res = max(res, right - left)
        return res

76. 最小覆盖子串


注意:分别用cnt和need记录每个字符的个数和字符串的总长度
当t中的所有字符都在当前的子串中出现过时收缩左边界,即need == 0

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        if len(s) < len(t):
            return ''
        cnt, need = Counter(t), len(t)
        left, minL = 0, len(s) + 1
        start, end = 0, -1
        for right in range(len(s)):
            # if s[right] in cnt:
            if cnt[s[right]] > 0:
                need -= 1
            cnt[s[right]] -= 1        
            while need == 0:
                if right - left + 1 < minL:
                    minL = right - left + 1
                    start, end = left, right
                # if s[left] in cnt:
                if cnt[s[left]] == 0:
                    need += 1
                cnt[s[left]] += 1
                left += 1
        return s[start: end + 1]

3. 无重复字符的最长子串

借助哈希set去重

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        if not s:
            return 0
        n = len(s)
        left = 0
        maxL, curL = 0, 0
        lookup = set()
        for i in range(n):
            curL += 1
            while s[i] in lookup:
                curL -= 1
                lookup.remove(s[left])
                left += 1
                
            maxL = max(maxL, curL)
            lookup.add(s[i])
        return maxL

239. 滑动窗口最大值

注意:为了可以同时弹出队首和队尾的元素,我们需要使用双端队列。满足这种单调性的双端队列一般称作「单调队列」

单调队列真是一种让人感到五味杂陈的数据结构,它的维护过程更是如此.....就拿此题来说,队头最大,往队尾方向单调......有机会站在队头的老大永远心狠手辣,当它从队尾杀进去的时候,如果它发现这里面没一个够自己打的,它会毫无人性地屠城,把原先队里的人头全部丢出去,转身建立起自己的政权,野心勃勃地准备开创一个新的王朝.....这时候,它的人格竟发生了一百八十度大反转,它变成了一位胸怀宽广的慈父!它热情地请那些新来的“小个子”们入住自己的王国......然而,这些小个子似乎天性都是一样的——嫉妒心强,倘若见到比自己还小的居然更早入住王国,它们会心狠手辣地找一个夜晚把它们通通干掉,好让自己享受更大的“蛋糕”;当然,遇到比自己强大的,它们也没辙,乖乖夹起尾巴做人。像这样的暗杀事件每天都在上演,虽然王国里日益笼罩上白色恐怖,但是好在没有后来者强大到足以干翻国王,江山还算能稳住。直到有一天,闯进来了一位真正厉害的角色,就像当年打江山的国王一样,手段狠辣,野心膨胀,于是又是大屠城......历史总是轮回的。

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        n = len(nums)
        que = collections.deque()  # 队列中存储的是数值对应的数组下标位置
        for i in range(k):  # 一轮遍历,先把窗口塞满
            while que and nums[i] >= nums[que[-1]]:  
                que.pop()  # 队尾元素较小则删除
            que.append(i)  

        res = [nums[que[0]]]  # 将第一个元素,即最大元素返回
        for i in range(k,n):
            while que and nums[i] >= nums[que[-1]]:
                que.pop()
            que.append(i)
            while que[0] <= i - k:  # 判断队首的下标位置是否在当前窗口内
                que.popleft()
            res.append(nums[que[0]])
        return res

6098. 统计得分小于 K 的子数组数目

注意:与209. 长度最小的子数组的区别是s * (right - left + 1) >= k时收缩左边界
只要某个子数组满足题目要求,在该子数组内的更短的子数组同样也满足题目要求。

class Solution:
    def countSubarrays(self, nums: List[int], k: int) -> int:
        res = left = s = 0
        for right, v in enumerate(nums):
            s += v
            while s * (right - left + 1) >= k:
                s -= nums[left]
                left += 1
            res += right - left + 1
        return res

6169. 最长优雅子数组

遍历数组中的元素 num,
当前 cur 与 n 按位与的结果不为 0,则从cur里面去掉最左边那个元素cur ^= nums[left],收缩左边界left += 1。(因为之前已经通过异或^加进来了,去除就再异或一次即可)
遍历元素过程中,记录下最大窗口长度即可。

class Solution:
    def longestNiceSubarray(self, nums: List[int]) -> int:
        left = cur = res = 0
        for right, n in enumerate(nums):
            while cur & n != 0:
                cur ^= nums[left]
                left += 1
            cur ^= n
            res = max(res, right - left + 1)
        return res

2447. 最大公因数等于 K 的子数组数目


第一步:

第二步:

第三步:

第四步:

第五步:

第六步:

利用函数 gcd() 求两个数的最大公因数
cur < k则表示以i开头的子数组已经不满足要求

class Solution:
    def subarrayGCD(self, nums: List[int], k: int) -> int:
        res = 0
        for i in range(len(nums)):
            cur = nums[i]
            for j in range(i, len(nums)):
                cur = gcd(cur, nums[j])
                if cur == k:
                    res += 1
                elif cur < k:
                    break
        return res

2470. 最小公倍数为 K 的子数组数目

利用函数 lcm() 求两个数的最小公倍数

class Solution:
    def subarrayLCM(self, nums: List[int], k: int) -> int:
        res = 0
        n = len(nums)
        for i in range(n):
            if k % nums[i] == 0:  # 如果不能整除k直接遍历下一个
                l = nums[i]
                for j in range(i, n):
                    l = lcm(l, nums[j])
                    if l == k:
                        res += 1
        return res

862. 和至少为 K 的最短子数组

前缀和+双端队列(单调队列)
遍历前缀和数组 s,对于遍历到的下标 i,如果 s[i] - s[q.front]≥k,说明当前遇到了一个可行解,更新答案。此时,将队首元素出队,直到队列为空或者 s[i] - s[q.front]<k 为止。

如果此时队列不为空,为了维持队列的严格单调递增,还需要判断队尾元素是否需要出队,如果 s[q.back] ≥s[i],则需要循环将队尾元素出队,直到队列为空或者 s[q.back] <s[i] 为止。然后将下标 i 入队。

遍历结束,如果没有找到可行解,那么返回 -1。否则,返回答案。

class Solution:
    def shortestSubarray(self, nums: List[int], k: int) -> int:
        s = list(accumulate(nums, initial = 0))
        print(s)
        q = deque()
        res = inf
        for i, v in enumerate(s):
            print(q)
            while q and v - s[q[0]] >= k:
                res = min(res, i - q.popleft())
            while q and s[q[-1]] >= v:
                q.pop()
            q.append(i)
        return res if res != inf else -1

1438. 绝对差不超过限制的最长连续子数组

Python 中 sortedcontainers 实现了有序的容器
使用 left 和 right 两个指针,分别指向滑动窗口的左右边界;定义 SortedList 保存滑动窗口的所有元素;

  • right 主动右移:right 指针每次移动一步,把 A[right] 放入滑动窗口;
  • left 被动右移:判断此时窗口内最大值和最小值的差,如果大于 limit,则 left 指针被迫右移,直至窗口内最大值和最小值的差小于等于 limit 为止;left 每次右移之前,需要把 A[left] 从 SortedList 中减去一次。
    滑动窗口长度的最大值就是所求。
class Solution:
    def longestSubarray(self, nums: List[int], limit: int) -> int:
        from sortedcontainers import SortedList
        s = SortedList()
        res = 0
        left = right = 0
        while right < len(nums):
            s.add(nums[right])
            while s[-1] - s[0] > limit:
                s.remove(nums[left])
                left += 1
            res = max(res, right - left + 1)
            right += 1
        return res

480. 滑动窗口中位数

用SortedList维持顺序

class Solution:
    def medianSlidingWindow(self, nums: List[int], k: int) -> List[float]:
        from sortedcontainers import SortedList
        s = SortedList()
        left, right = 0, k-1
        for i in range(k - 1):
            s.add(nums[i])
        res = []
        while right < len(nums):
            s.add(nums[right])
            # print(s)
            if k%2:
                res.append(s[k//2])
            else:
                res.append((s[k//2] + s[k//2 - 1])/2)
            s.remove(nums[left])
            right += 1
            left += 1
        return res

795. 区间子数组个数


从i0到i1有i1 - i0个满足要求的子数组

class Solution:
    def numSubarrayBoundedMax(self, nums: List[int], left: int, right: int) -> int:
        res, i0, i1 = 0, -1, -1
        for i, x in enumerate(nums):
            if x > right: i0 = i 
            if x >= left: i1 = i 
            res += i1 - i0 
        return res

2444. 统计定界子数组的数目

与795. 区间子数组个数 的区别就是多了一个最小值的约束
当遍历到 nums[i] 时,如果 minK 和 maxK 之前出现过,则左端点 ≤min(minI,maxI) 的子数组都是合法的,合法子数组的个数为 min(minI,maxI)+1
上一个在 [minK,maxK] 范围之外的 nums[i] 的下标,记作 i0。此时合法子数组的个数为 min(minI,maxI)−i0
​如果 min(minI,maxI)−i0<0,则表示在 i0右侧 minK 和 maxK 没有同时出现,此时合法子数组的个数为 0

class Solution:
    def countSubarrays(self, nums: List[int], minK: int, maxK: int) -> int:
        res, min_i, max_i, i0 = 0, -1, -1, -1
        for i, n in enumerate(nums):
            if n == minK: min_i = i 
            if n == maxK: max_i = i 
            if n > maxK or n < minK: i0 = i
            res += max(0, min(min_i, max_i) - i0)
        return res

1687. 从仓库到码头运输箱子

动态规划 + 单调队列
f[i] 表示把前 i 个箱子从仓库运送到相应码头的最少行程数

class Solution:
    def boxDelivering(self, boxes: List[List[int]], portsCount: int, maxBoxes: int, maxWeight: int) -> int:
        n = len(boxes)
        pre_w = list(accumulate((box[1] for box in boxes), initial = 0))
        dis = [int(a != b) for a, b in pairwise(box[0] for box in boxes)]
        pre_d = list(accumulate(dis, initial = 0))
        dp = [0] * (n + 1)
        q = deque([0])
        for i in range(1, n + 1):
            # 不能继续往后运,需要重新回到仓库,或回到上一个满足要求的码头
            while q and (i - q[0] > maxBoxes or pre_w[i] - pre_w[q[0]] > maxWeight):
                q.popleft()
            if q:  # 上一个满足要求的码头+从该码头到第i个的行程
                dp[i] = dp[q[0]] + pre_d[i - 1] - pre_d[q[0]] + 2
            if i < n:  # 直接送到第i个码头是最少行程
                while q and dp[q[-1]] - pre_d[q[-1]] >= dp[i] - pre_d[i]:
                    q.pop()
                q.append(i)
        return dp[-1]

参考资料:
闪电五连鞭带你秒杀12道中档题
滑动窗口3. 无重复字符的最长子串
滑动窗口76. 最小覆盖子串
『 一招吃遍七道 』滑动窗口的应用

posted @ 2022-05-16 10:09  YTT77  阅读(54)  评论(0编辑  收藏  举报