打败算法 —— 最小覆盖子串
本文参考
出自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]