欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 财经 > 金融 > 最小覆盖子串

最小覆盖子串

2025/5/6 20:12:15 来源:https://blog.csdn.net/qq_51753728/article/details/142298310  浏览:    关键词:最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 1:

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。

示例 2:

输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。

示例 3:

输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。

思路和算法

本问题要求我们返回字符串 s 中包含字符串 t 的全部字符的最小窗口。我们称包含 t 的全部字母的窗口为「可行」窗口。

我们可以用滑动窗口的思想解决这个问题。在滑动窗口类型的问题中都会有两个指针,一个用于「延伸」现有窗口的 r 指针,和一个用于「收缩」窗口的 l 指针。在任意时刻,只有一个指针运动,而另一个保持静止。我们在 s 上滑动窗口,通过移动 r 指针不断扩张窗口。当窗口包含 t 全部所需的字符后,如果能收缩,我们就收缩窗口直到得到最小窗口。

如何判断当前的窗口包含所有 t 所需的字符呢?我们可以用一个哈希表表示 t 中所有的字符以及它们的个数,用一个哈希表动态维护窗口中所有的字符以及它们的个数,如果这个动态表中包含 t 的哈希表中的所有字符,并且对应的个数都不小于 t 的哈希表中各个字符的个数,那么当前的窗口是「可行」的。

注意:这里 t 中可能出现重复的字符,所以我们要记录字符的个数。

代码:

class Solution {
public:unordered_map <char, int> ori, cnt;bool check() {for (const auto &p: ori) {if (cnt[p.first] < p.second) {return false;}}return true;}string minWindow(string s, string t) {for (const auto &c: t) {++ori[c];}int l = 0, r = -1;int len = INT_MAX, ansL = -1, ansR = -1;while (r < int(s.size())) {if (ori.find(s[++r]) != ori.end()) {++cnt[s[r]];}while (check() && l <= r) {if (r - l + 1 < len) {len = r - l + 1;ansL = l;}if (ori.find(s[l]) != ori.end()) {--cnt[s[l]];}++l;}}return ansL == -1 ? string() : s.substr(ansL, len);}
};

代码详解:

check 函数

bool check() {for (const auto &p: ori) {if (cnt[p.first] < p.second) {return false;}}return true;
}

  • 功能:检查当前滑动窗口是否包含目标字符串 t 中的所有字符,且数量满足要求。
  • 实现:遍历 ori 中的每个字符和它的数量 (p.firstp.second),

    ori 用于存储字符(char 类型)和它们的出现次数(int 类型)的哈希表,p.first 代表字符,p.second是该字符在ori中的数量。 ​如果滑动窗口中的字符数量 (cnt[p.first]) 小于 ori 中的需求数量 (p.second),返回 false。如果所有字符都满足需求,返回 true

string minWindow(string s, string t) {for (const auto &c: t) {++ori[c];}
  • 功能:初始化 ori,记录字符串 t 中每个字符的数量。
  • 实现:遍历目标字符串 t,对 ori 中每个字符的计数进行累加。

滑动窗口的初始化

int l = 0, r = -1;
int len = INT_MAX, ansL = -1, ansR = -1;
  • lr:分别是滑动窗口的左边界和右边界。r 初始化为 -1,以便在开始时可以先增加 r
  • len:记录最小子串的长度,初始化为 INT_MAX,表示尚未找到有效子串。
  • ansLansR:记录最小子串的起始和结束位置。初始值为 -1

扩展和收缩窗口

while (r < int(s.size())) {if (ori.find(s[++r]) != ori.end()) {++cnt[s[r]];}while (check() && l <= r) {if (r - l + 1 < len) {len = r - l + 1;ansL = l;}if (ori.find(s[l]) != ori.end()) {--cnt[s[l]];}++l;}
}
  • 扩展窗口
    • r 逐步增加,扩展窗口的右边界。
    • s[r]ori 中时,更新 cnt 中对应字符的计数。
  • 收缩窗口
    • check() 返回 true,表示当前窗口满足条件,可以尝试更新最小子串的长度。
    • if (r - l + 1 < len) 判断当前窗口是否更小,若是,则更新 lenansL
    • 通过减少 cnt[s[l]],然后移动左边界 l,尝试收缩窗口。

返回结果

return ansL == -1 ? string() : s.substr(ansL, len);
  • 功能:根据 ansL 是否为 -1 返回结果。
  • 实现:如果 ansL-1,说明没有找到有效的子串,返回空字符串。否则,返回从 ansL 开始、长度为 len 的子串。

内在思想

  1. 滑动窗口:利用两个指针 lr 来定义窗口的边界,动态调整窗口的大小,以找到包含 t 所有字符的最小子串。

  2. 扩展与收缩:通过扩展右边界 r 来增加窗口中的字符,检查是否满足条件;若满足条件,则收缩左边界 l,以尽可能减小窗口的大小。

  3. 字符计数:使用哈希表 oricnt 分别记录目标字符串和当前窗口中字符的数量,以便高效地判断窗口是否满足条件。

  4. 最优解:始终保持当前找到的最小子串,并在每次发现更小的有效子串时更新结果。

这段代码采用了滑动窗口技术,结合哈希表来实现高效的最小覆盖子串查找。

示例

给定输入:s = "ABAACBAB"t = "ABC",代码旨在找到字符串 s 中包含 t 所有字符的最小子串。我们将详细描述代码的迭代过程,以解释其工作原理。

代码初始化

初始化 oricnt

ori 被初始化为 { 'A': 2, 'B': 1, 'C': 1 }

变量初始化

l = 0,左边界。

r = -1,右边界。

len = INT_MAX,最小窗口长度。

ansL = -1,最小窗口的起始位置。

ansR = -1,最小窗口的结束位置(尽管在代码中未实际使用)。

迭代过程

第 1 次迭代
  • 扩展右边界

    • r = 0,当前字符是 s[0] = 'A'
    • ori.find('A') != ori.end()true
    • cnt 变为 { 'A': 1 }
  • 检查窗口是否有效

    • check() 返回 false(窗口内字符 cnt'A': 1 不足以满足 ori'A': 2 的要求)。
第 2 次迭代
  • 扩展右边界

    • r = 1,当前字符是 s[1] = 'B'
    • ori.find('B') != ori.end()true
    • cnt 变为 { 'A': 1, 'B': 1 }
  • 检查窗口是否有效

    • check() 返回 false(窗口内字符 cnt'A': 1 不足以满足 ori'A': 2 的要求)。
第 3 次迭代
  • 扩展右边界

    • r = 2,当前字符是 s[2] = 'A'
    • ori.find('A') != ori.end()true
    • cnt 变为 { 'A': 2, 'B': 1 }
  • 检查窗口是否有效

    • check() 返回 true(当前窗口内包含了目标字符 t 中所有字符)。
    • if (r - l + 1 < len) 更新 len = 3ansL = 0(窗口 "ABA" 的长度为 3)。
  • 收缩窗口

    • s[l] = 'A'ori.find('A') != ori.end()true
    • cnt 变为 { 'A': 1, 'B': 1 }
    • l = 1
第 4 次迭代
  • 检查窗口是否有效

    • check() 返回 true(当前窗口内仍然包含所有目标字符)。
  • 更新最小窗口

    • if (r - l + 1 < len) 更新 len = 2ansL = 1(窗口 "BA" 的长度为 2)。
  • 收缩窗口

    • s[l] = 'B'ori.find('B') != ori.end()true
    • cnt 变为 { 'A': 1, 'B': 0 }
    • l = 2
第 5 次迭代
  • 检查窗口是否有效
    • check() 返回 false(当前窗口内的 'B': 0 不满足 ori'B': 1 的要求)。
第 6 次迭代
  • 扩展右边界

    • r = 3,当前字符是 s[3] = 'A'
    • ori.find('A') != ori.end()true
    • cnt 变为 { 'A': 2, 'B': 0 }
  • 检查窗口是否有效

    • check() 返回 false(当前窗口内的 'B': 0 不满足 ori'B': 1 的要求)。
第 7 次迭代
  • 扩展右边界

    • r = 4,当前字符是 s[4] = 'C'
    • ori.find('C') != ori.end()true
    • cnt 变为 { 'A': 2, 'B': 0, 'C': 1 }
  • 检查窗口是否有效

    • check() 返回 true(当前窗口内包含了目标字符 t 中所有字符)。
    • if (r - l + 1 < len) 更新 len = 5ansL = 2(窗口 "AABC" 的长度为 5)。
  • 收缩窗口

    • s[l] = 'A'ori.find('A') != ori.end()true
    • cnt 变为 { 'A': 1, 'B': 0, 'C': 1 }
    • l = 3
第 8 次迭代
  • 检查窗口是否有效

    • check() 返回 true(当前窗口内包含了目标字符 t 中所有字符)。
    • if (r - l + 1 < len) 更新 len = 4ansL = 3(窗口 "ABCA" 的长度为 4)。
  • 收缩窗口

    • s[l] = 'A'ori.find('A') != ori.end()true
    • cnt 变为 { 'A': 0, 'B': 0, 'C': 1 }
    • l = 4
第 9 次迭代
  • 检查窗口是否有效
    • check() 返回 false(当前窗口内的 'A': 0 不满足 ori'A': 2 的要求)。
第 10 次迭代
  • 扩展右边界

    • r = 5,当前字符是 s[5] = 'B'
    • ori.find('B') != ori.end()true
    • cnt 变为 { 'A': 0, 'B': 1, 'C': 1 }
  • 检查窗口是否有效

    • check() 返回 true(当前窗口内包含了目标字符 t 中所有字符)。
    • if (r - l + 1 < len) 不更新 len(当前窗口 "BCBAB" 长度为 6,大于之前找到的最小长度)。
  • 收缩窗口

    • s[l] = 'B'ori.find('B') != ori.end()true
    • cnt 变为 { 'A': 0, 'B': 0, 'C': 1 }
    • l = 5
第 11 次迭代
  • 检查窗口是否有效
    • check() 返回 false(当前窗口内的 'A': 0 不满足 ori'A': 2 的要求)。
第 12 次迭代
  • 扩展右边界

    • r = 6,当前字符是 s[6] = 'A'
    • ori.find('A') != ori.end()true
    • cnt 变为 { 'A': 1, 'B': 0, 'C': 1 }
  • 检查窗口是否有效

    • check() 返回 true(当前窗口内包含了目标字符 t 中所有字符)。
    • if (r - l + 1 < len) 不更新 len(当前窗口 "CBAB" 长度为 5,大于之前找到的最小长度)。
  • 收缩窗口

    • s[l] = 'B'ori.find('B') != ori.end()true
    • cnt 变为 { 'A': 1, 'B': -1, 'C': 1 }
    • l = 6
第 13 次迭代(继续)
  • 检查窗口是否有效

    • check() 返回 true(当前窗口内包含了目标字符 t 中所有字符)。
    • if (r - l + 1 < len) 更新 len = 3ansL = 6(窗口 "BCA" 的长度为 3)。
  • 收缩窗口

    • s[l] = 'A'ori.find('A') != ori.end()true
    • cnt 变为 { 'A': 0, 'B': -1, 'C': 1 }
    • l = 7
第 14 次迭代
  • 检查窗口是否有效

    • check() 返回 true(当前窗口内仍然包含所有目标字符 t 中的字符)。
    • if (r - l + 1 < len) 不更新 len(当前窗口 "CAB" 长度为 3,与之前的最小长度相同)。
  • 收缩窗口

    • s[l] = 'B'ori.find('B') != ori.end()true
    • cnt 变为 { 'A': 0, 'B': -2, 'C': 1 }
    • l = 8
第 15 次迭代
  • 检查窗口是否有效
    • check() 返回 false(当前窗口内的 'B': -2 不满足 ori'B': 1 的要求)。
第 16 次迭代
  • 扩展右边界

    • r = 7,当前字符是 s[7] = 'B'
    • ori.find('B') != ori.end()true
    • cnt 变为 { 'A': 0, 'B': -1, 'C': 1 }
  • 检查窗口是否有效

    • check() 返回 false(当前窗口内的 'B': -1 不满足 ori'B': 1 的要求)。
完成迭代
  • 右边界 r 达到字符串的末尾,算法停止迭代。

结果

  • 在所有迭代过程中,我们找到的最小覆盖子串是 "BCA",长度为 3。

复杂度分析

时间复杂度:最坏情况下左右指针对 s 的每个元素各遍历一遍,哈希表中对 s 中的每个元素各插入、删除一次,对 t 中的元素各插入一次。每次检查是否可行会遍历整个 t 的哈希表,哈希表的大小与字符集的大小有关,设字符集大小为 C,则渐进时间复杂度为 O(C⋅∣s∣+∣t∣)。
空间复杂度:这里用了两张哈希表作为辅助空间,每张哈希表最多不会存放超过字符集大小的键值对,我们设字符集大小为 C ,则渐进空间复杂度为 O(C)。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词