打败算法 —— 最小覆盖子串

本文参考

出自LeetCode上的题库 —— 最小覆盖子串,本篇解法主要在官方题解的基础上做一定修改

https://leetcode-cn.com/problems/minimum-window-substring/

最小覆盖子串问题

给定一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串,如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""
对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量

示例1:
输入: s = "ADOBECODEBANC", t = "ABC"
输出: "BANC"

示例2:
输入:s = "a", t = "a"
输出:"a"

示例3:
输入:s = "a", t = "aa"
输出:""

滑动窗口简介

基本概念:
滑动窗口控制左右两个指针,通常情况下,每次控制其中一个指针向前或向后移动,最典型的应用就是TCP协议中的发送窗口,它同时受到流量控制策略和拥塞控制策略的影响

适用范围:
对有序的字符串或列表求最值或子序列,例如本题应用滑动窗口求最小字串

基本步骤:
1、初始化左右指针的下标索引 l_index = r_index = 0,索引闭区间 [l_index, r_index] 称为一个窗口

2、不断地增加 r_index 指针扩大窗口 [l_index, r_index],直到窗口中的序列符合要求

3、此时,停止增加 r_index,转而不断增加 l_index指针缩小窗口 [l_index, r_index],直到窗口中的序列不再符合要求

4、重复第 2 和第 3 步,直到 r_index 到达序列的尽头

解题思路

按照滑动窗口的基本解题步骤,关键点在于在增大r_index 指针位置的过程中,如何判断当前的窗口已经包含 t 中所有的字符。我们可以简单的通过Python预置的map数据结构构造字符串 t 的"(字符,字符数量)"键值对,每次移动 r_index 指针后,利用map检查窗口中的字符种类和字符个数是否已经满足要求。若满足要求,记录当前的 l_index 指针和字符串长度后,开始移动 l_index ,缩减窗口大小,检查是否存在更短的覆盖字串;若不满足要求,则继续移动 r_index

滑动窗口解法

class Solution:
  ori = dict()
  cnt = dict()

  def check(self) -> bool:
  """
    判断cnt是否已经包含ori中应有的字符和字符对应的个数

  :return bool
  """
  for
key, value in self.ori.items():
    if self.cnt.get(key, -1) < value:
      return False
    return True

  def
min_window_1(self, s: str, t: str) -> str:
    self.ori.clear()
    self.cnt.clear()

    # t 中字符计数

    for c in t:
      self.ori[c] = self.ori.setdefault(c, 0) + 1

      # 初始化指针

      left = 0
      right = 0
      ans_left = -1
      length = sys.maxsize

      while right < len(s):
        cur = s[right]

        if cur in self.ori.keys():
          self.cnt[cur] = self.cnt.setdefault(cur, 0) + 1

        若窗口满足要求,则移动左指针

                
while self.check() and left <= right:
          if right - left + 1 < length:
            length = right - left + 1
            ans_left = left
          if s[left] in self.ori.keys():
            self.cnt[s[left]] -= 1
          # 继续缩减窗口大小

          left += 1
        # 移动右指针

        right += 1

    return '' if ans_left == -1 else s[ans_left:ans_left+length]

需要注意的是,因为LeetCode判题使会不断地调用 ori 和 cnt 两个 map ,为了防止各测试用例间造成干扰,需要先用 clear() 函数清空上一轮存储的字符数据

实际上,我们可以加快左指针的移动速度,直接移动到字符串 t 包含的字符的位置上,而不是使窗口每次只缩减一个字符的长度,完善后的代码如下:

def min_window_2(self, s: str, t: str) -> str:
  self.ori.clear()
  self.cnt.clear()

  # t 中字符计数

  for c in t:
    self.ori[c] = self.ori.setdefault(c, 0) + 1

  # 初始化指针

  l_index = 0
  r_index = 0
  ans_l_index = -1
  length = sys.maxsize
  # 记录左指针应该跳转的位置

  record = list()

  # 滑动窗口

  while r_index < len(s):
    cur = s[r_index]

    if cur in self.ori.keys():
      self.cnt[cur] = self.cnt.setdefault(cur, 0) + 1
      record.append(r_index)

    # 若窗口满足要求,则移动左指针

    while self.check() and l_index <= r_index:
      # 快速移动左指针

      l_index = record.pop(0)
      if r_index - l_index + 1 < length:
        length = r_index - l_index + 1
        ans_l_index = l_index
      self.cnt[s[l_index]] -= 1
      # 移动右指针

      r_index += 1

  return '' if ans_l_index == -1 else s[ans_l_index:ans_l_index+length]

posted @ 2022-02-28 14:17  咕~咕咕  阅读(42)  评论(0编辑  收藏  举报