LeetCode 76. 最小覆盖子串
本节目标
用需求频次、窗口频次和满足种类数寻找覆盖目标字符串的最短子串。
这道题放在数组与矩阵综合中,因为它把频次表与最短窗口的收缩策略组合起来。窗口必须覆盖 t 中每种字符的指定次数,而不只是出现过这些字符。
题意与约束
给定字符串 s 与 t,返回 s 中包含 t 所有字符及其数量的最短子串。若不存在,返回空字符串;若有多个最短解,题目保证答案唯一。
例如 t 为 AABC 时,窗口需要至少包含两个 A、一个 B 和一个 C。因此仅用集合记录字符是否出现不够。
从枚举子串到维护频次
最直接的办法是枚举 s 的所有连续子串,再统计每个候选的字符频次并与 t 的需求比较。候选区间有 O(|s|²) 个;若每次重新计数,最坏时间会达到 O(|s|³ + |t|)。即使对同一起点增量统计,将复杂度降到 O(|s|² + |t|),更换起点后仍会重复计算大量重叠区间。
窗口右移时实际只加入一个字符,左移时也只移除一个字符。若同时维护窗口频次和已经满足需求的字符种类数,就能在常数级更新后判断当前区间是否可行,让两个边界都只向右移动。
用 formed 表示窗口是否可行
先用 need 统计目标频次,required 是需求中不同字符的种类数。窗口右端加入字符后,只有该字符属于 need 才更新窗口计数;当它的计数首次达到需求时,formed 加一。
当 formed 等于 required 时,每种需求字符的频次都已满足,窗口可行。此时先尝试更新最短答案,再移除左端字符;若移出的字符刚好使某种频次低于需求,formed 减一,窗口重新变为不可行。
代码实现
两份源码都只为目标中出现的字符维护窗口频次,避免无关字符干扰 formed。更新最优答案发生在左端移出前,保证记录的窗口仍覆盖整个目标。
- C++
- Python
#include <climits>
#include <string>
#include <unordered_map>
using namespace std;
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char, int> need;
unordered_map<char, int> window;
for (char ch : t) {
need[ch]++;
}
int required = static_cast<int>(need.size());
int formed = 0;
int left = 0;
int bestStart = 0;
int bestLength = INT_MAX;
for (int right = 0; right < static_cast<int>(s.size()); right++) {
char ch = s[right];
if (need.count(ch) > 0) {
window[ch]++;
if (window[ch] == need[ch]) {
formed++;
}
}
// 已覆盖所有需求时,尽可能移除左侧冗余字符。
while (formed == required) {
if (right - left + 1 < bestLength) {
bestStart = left;
bestLength = right - left + 1;
}
char leftChar = s[left];
if (need.count(leftChar) > 0) {
if (window[leftChar] == need[leftChar]) {
formed--;
}
window[leftChar]--;
}
left++;
}
}
return bestLength == INT_MAX ? "" : s.substr(bestStart, bestLength);
}
};
from collections import Counter
class Solution:
def minWindow(self, s: str, t: str) -> str:
need = Counter(t)
window: dict[str, int] = {}
required = len(need)
formed = 0
left = 0
best_start = 0
best_length = len(s) + 1
for right, ch in enumerate(s):
if ch in need:
window[ch] = window.get(ch, 0) + 1
if window[ch] == need[ch]:
formed += 1
# 已覆盖所有需求时,尽可能移除左侧冗余字符。
while formed == required:
if right - left + 1 < best_length:
best_start = left
best_length = right - left + 1
left_ch = s[left]
if left_ch in need:
if window[left_ch] == need[left_ch]:
formed -= 1
window[left_ch] -= 1
left += 1
return "" if best_length == len(s) + 1 else s[best_start:best_start + best_length]
复杂度分析
- 时间复杂度:O(|s| + |t|)。两个边界都只向右移动,目标频次只统计一次。
- 空间复杂度:O(|Σ|),其中 Σ 是需要维护频次的字符集合。
易错点
- 把 formed 记成已匹配字符总数;它应统计已达到目标频次的字符种类数。
- 左端移出需求字符时,先减少频次再判断是否破坏满足条件,容易漏掉临界值。
- 只判断每个字符出现过,无法处理目标含重复字符的情况。
模式迁移
它是滑动窗口解题框架中最短可行窗口的频次版:将窗口和达到阈值替换为所有需求频次均满足。类似结构可用于最短包含若干类别、最短覆盖多重集合等问题。