hot100-最小覆盖字串(day12)

目录

[最小覆盖子串(滑动窗口 + 字符计数)](#最小覆盖子串(滑动窗口 + 字符计数))

一、问题重述与核心难点

问题定义

示例直观理解

核心难点

二、从暴力到优化:思路演进

[1. 暴力解法](#1. 暴力解法)

[2. 优化核心:滑动窗口](#2. 优化核心:滑动窗口)

滑动窗口的核心逻辑

为什么滑动窗口高效?

[3. 辅助工具:字符计数数组](#3. 辅助工具:字符计数数组)

三、AC代码解析

[关键步骤拆解(结合示例 1:s=ADOBECODEBANC, t=ABC)](#关键步骤拆解(结合示例 1:s=ADOBECODEBANC, t=ABC))

四、复杂度分析

五、进阶优化

优化后的代码

优化点说明

六、边界情况与通用模板

常见边界情况

滑动窗口通用模板(子串问题)

七、总结


最小覆盖子串(滑动窗口 + 字符计数)

在字符串处理的算法题中,「最小覆盖子串」是一道经典的子串匹配 + 优化遍历问题。它要求在一个字符串中找到包含另一个字符串所有字符(含重复)的最短子串,核心考察对「滑动窗口」和「字符计数」技巧的运用。

本文将从「暴力解法→优化思路→核心算法→代码解析→进阶优化」逐步展开,让你能看懂代码,能理解背后的思维逻辑。

一、问题重述与核心难点

问题定义

传送门:最小覆盖字串

给定两个字符串 s(长度 m)和 t(长度 n),返回 s最短的窗口子串 ,使得该子串包含 t 中的每一个字符(包括重复字符)。若不存在则返回空字符串 "",且答案唯一。

示例直观理解

  • 示例 1:s=ADOBECODEBANC, t=ABC → 输出BANC解析:ADOBEC(长度 6)、CODEBA(长度 6)、BANC(长度 4)均满足条件,最短为BANC
  • 示例 3:s=a, t=aa → 输出""解析:t 需要 2 个a,但s只有 1 个,无法满足。

核心难点

  1. 如何高效判断「子串包含 t 的所有字符」 :不能每次都遍历子串和t对比(时间开销太大)。
  2. 如何找到「最短子串」:避免枚举所有可能的子串(暴力解法会超时)。

二、从暴力到优化:思路演进

1. 暴力解法

思路 :枚举s中所有可能的子串,判断每个子串是否包含t的所有字符,记录最短的那个。

  • 枚举子串:需要两层循环(左边界i、右边界j),共O(m²)个可能的子串。
  • 判断是否包含t:对每个子串,需要统计字符频率并与t对比,耗时O(m+n)
  • 总时间复杂度:O(m²(m+n)),对于m=1e4的场景直接超时。

结论:暴力解法思路简单,但效率极低,必须寻找更优的遍历方式。

2. 优化核心:滑动窗口

滑动窗口是解决「子串匹配 + 长度优化」问题的经典技巧,核心思想是用两个指针维护一个「动态窗口」,通过扩张和收缩窗口,高效遍历所有可能的有效子串

滑动窗口的核心逻辑
  • 扩张右指针(right) :扩大窗口范围,直到窗口包含t的所有字符(找到一个有效窗口)。
  • 收缩左指针(left):在窗口有效时,尽量缩小窗口范围(左指针右移),直到窗口不再有效,记录此时的最短窗口。
  • 重复上述过程 :直到右指针遍历完s
为什么滑动窗口高效?

每个字符只会被「右指针」和「左指针」各访问一次,遍历次数为O(m),时间复杂度大幅降低。

3. 辅助工具:字符计数数组

要快速判断「窗口是否包含t的所有字符」,需要统计字符频率:

  • cnt_t[128]记录t中每个字符的出现次数(ASCII 码共 128 个,数组比哈希表访问更快)。
  • cnt_s[128]记录当前窗口中每个字符的出现次数。
  • 判断条件:对所有字符ccnt_s[c] >= cnt_t[c](窗口中该字符的数量不少于t中的数量)。

三、AC代码解析

cpp 复制代码
class Solution {
    // 检查当前窗口是否满足条件:cnt_s的所有字符计数 >= cnt_t
    bool check(int cnt_s[], int cnt_t[]) {
        // 遍历大写字母(A-Z)
        for (int i = 'A'; i <= 'Z'; i++) {
            if (cnt_s[i] < cnt_t[i]) return false;
        }
        // 遍历小写字母(a-z)
        for (int i = 'a'; i <= 'z'; i++) {
            if (cnt_s[i] < cnt_t[i]) return false;
        }
        return true;
    }
public:
    string minWindow(string s, string t) {
        int cnt_s[128]{};  // 窗口内字符计数(初始化为0)
        int cnt_t[128]{};  // t中字符计数(初始化为0)
        
        // 第一步:统计t中每个字符的出现次数
        for (char c : t) {
            cnt_t[c]++;
        }
        
        int m = s.size();
        int resl = -1;  // 最优窗口的左边界(初始为-1表示无有效窗口)
        int rrer = m;  // 最优窗口的右边界(初始为m,使得初始窗口长度为m+1,方便后续更新)
        int left = 0;  // 滑动窗口的左指针
        
        // 第二步:扩张右指针,构建窗口
        for (int right = 0; right < m; right++) {
            char c = s[right];
            cnt_s[c]++;  // 右指针加入窗口,更新计数
            
            // 第三步:窗口有效时,收缩左指针,优化窗口长度
            while (check(cnt_s, cnt_t)) {
                // 更新最优窗口:当前窗口更短时
                if (right - left < rrer - resl) {
                    resl = left;
                    rrer = right;
                }
                // 左指针移出窗口,更新计数
                cnt_s[s[left]]--;
                left++;
            }
        }
        
        // 若resl仍为-1,说明无有效窗口;否则返回子串
        return resl < 0 ? "" : s.substr(resl, rrer - resl + 1);
    }
};

关键步骤拆解(结合示例 1:s=ADOBECODEBANC, t=ABC)

  1. 初始化计数数组cnt_t['A']=1, cnt_t['B']=1, cnt_t['C']=1,其余为 0。
  2. 扩张右指针
    • right=0(A):cnt_s[A]=1 → 不满足 check。
    • right=1(D):cnt_s[D]=1 → 不满足。
    • right=2(O):cnt_s[O]=1 → 不满足。
    • right=3(B):cnt_s[B]=1 → 不满足。
    • right=4(E):cnt_s[E]=1 → 不满足。
    • right=5(C):cnt_s[C]=1 → 此时cnt_s[A]=1, B=1, C=1,满足 check。
  3. 收缩左指针
    • 当前窗口[0,5](ADOBEC),长度 6。更新resl=0, rrer=5
    • 左指针移出Acnt_s[A]=0 → check 不满足,退出 while 循环。
  4. 继续扩张右指针 :直到right=10(B)、right=11(A)、right=12(N)、right=13(C),此时窗口[9,13](BANC),满足 check。
    • 收缩左指针:窗口长度 4 < 之前的 6,更新resl=9, rrer=12(BANC)。
    • 移出B后 check 不满足,循环结束。
  5. 返回结果s.substr(9,4) → "BANC"。

四、复杂度分析

  • 时间复杂度O(m * 128) → 简化为O(m)。解释:rightleft各遍历s一次(O(m)),每次check遍历 128 个 ASCII 字符(O(1)),整体为线性时间。
  • 空间复杂度O(128) → 简化为O(1)。解释:计数数组cnt_scnt_t大小固定(128),与输入规模无关。

对比暴力解法的O(m²(m+n)),滑动窗口的优势一目了然!

五、进阶优化

基础版的check函数每次遍历 128 个字符,虽然是O(1),但可以进一步优化:用一个变量valid记录「已满足cnt_s[c] >= cnt_t[c]的字符种类数」。

valid == 目标种类数(k)时,直接判定窗口有效,无需遍历数组。

优化后的代码

cpp 复制代码
class Solution {
public:
    string minWindow(string s, string t) {
        int cnt_s[128]{};
        int cnt_t[128]{};
        int k = 0;  // t中不同字符的种类数
        
        // 统计t的字符计数和种类数
        for (char c : t) {
            if (cnt_t[c] == 0) k++;  // 新种类,k++
            cnt_t[c]++;
        }
        
        int m = s.size();
        int resl = -1, rrer = m;
        int left = 0;
        int valid = 0;  // 已满足条件的字符种类数
        
        for (int right = 0; right < m; right++) {
            char c = s[right];
            cnt_s[c]++;
            
            // 当该字符的计数刚满足t的要求时,valid++
            if (cnt_s[c] == cnt_t[c]) {
                valid++;
            }
            
            // 窗口有效(所有种类都满足),收缩左指针
            while (valid == k) {
                // 更新最优窗口
                if (right - left < rrer - resl) {
                    resl = left;
                    rrer = right;
                }
                
                char d = s[left];
                // 若左指针移出的字符是满足条件的种类,valid--
                if (cnt_s[d] == cnt_t[d]) {
                    valid--;
                }
                cnt_s[d]--;
                left++;
            }
        }
        
        return resl < 0 ? "" : s.substr(resl, rrer - resl + 1);
    }
};

优化点说明

  • 减少了check函数的循环,实际运行效率提升明显(尤其当t的字符种类较少时)。
  • 时间复杂度仍为O(m),但常数项更小,更适合大数据量测试用例。

六、边界情况与通用模板

常见边界情况

  1. t的长度大于s:直接返回""(如s="a", t="aa")。
  2. st长度相等且满足条件:返回s(如s="a", t="a")。
  3. t包含重复字符:必须保证窗口中该字符的数量不小于t中的数量(如t="AA",窗口需至少 2 个A)。

滑动窗口通用模板(子串问题)

cpp 复制代码
// 初始化计数/哈希表
// 统计目标字符串的信息(如t的字符计数、种类数)
int left = 0, res = 初始值;
for (int right = 0; right < s.size(); right++) {
    // 右指针加入窗口,更新状态
    ...
    // 窗口满足条件时,收缩左指针
    while (满足条件) {
        // 更新最优解
        ...
        // 左指针移出窗口,更新状态
        ...
        left++;
    }
}
return 最优解;

该模板适用于:最小覆盖子串、长度最小的子数组、最长无重复子串等「子串优化问题」。

七、总结

最小覆盖子串的核心是「滑动窗口 + 字符计数」:

  1. 滑动窗口解决「高效遍历所有有效子串」的问题,避免枚举所有子串。
  2. 字符计数解决「快速判断窗口是否有效」的问题,替代低效的字符串对比。
  3. 进阶优化通过valid变量进一步降低常数项,提升实战效率。

掌握这一思路后,你可以轻松解决一系列子串匹配类问题。滑动窗口的关键是「动态维护窗口状态」,字符计数的关键是「精准匹配目标要求」

如果觉得本文对你有帮助,欢迎点赞、收藏,也可以在评论区交流 其他滑动窗口应用场景!

相关推荐
Rui_Freely21 小时前
Vins-Fusion之 相机—IMU在线标定(十一)
人工智能·算法·计算机视觉
yyy(十一月限定版)1 天前
算法——二分
数据结构·算法
七点半7701 天前
c++基本内容
开发语言·c++·算法
嵌入式进阶行者1 天前
【算法】基于滑动窗口的区间问题求解算法与实例:华为OD机考双机位A卷 - 最长的顺子
开发语言·c++·算法
嵌入式进阶行者1 天前
【算法】用三种解法解决字符串替换问题的实例:华为OD机考双机位A卷 - 密码解密
c++·算法·华为od
罗湖老棍子1 天前
信使(msner)(信息学奥赛一本通- P1376)四种做法
算法·图论·dijkstra·spfa·floyd·最短路算法
生成论实验室1 天前
生成论之基:“阴阳”作为元规则的重构与证成——基于《易经》与《道德经》的古典重诠与现代显象
人工智能·科技·神经网络·算法·架构
啊董dong1 天前
noi-2026年1月07号作业
数据结构·c++·算法·noi
l1t1 天前
DeepSeek辅助编写的利用唯一可选数求解数独SQL
数据库·sql·算法·postgresql