当前位置: 首页 >关注 > 列表
精读《算法题 - 最小覆盖子串》
2023-08-14 10:20:32    互联网

今天我们看一道 leetcode hard 难度题目:最小覆盖子串。

题目

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

注意:


(资料图片仅供参考)

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

示例 1:

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

思考

最容易想到的思路是,s 从下标 0~n 形成的子串逐个判断是否满足条件,如:

ADOBEC..

DOBECO..

OBECOD..

因为最小覆盖子串是连续的,所以该方法可以保证遍历到所有满足条件的子串。代码如下:

functionminWindow(s:string,t:string):string{//t剩余匹配总长度lettLeftSize=t.length//t每个字母对应出现次数表consttCharCountMap={}for(constcharoft){if(!tCharCountMap[char]){tCharCountMap[char]=0}tCharCountMap[char]++}letglobalResult=""for(leti=0;i我们用 tCharCountMap 存储 t 中每个字符出现的次数,在遍历时每次找到出现过的字符就减去 1,直到 tLeftSize 变成 0,表示 s 完全覆盖了 t。

这个方法因为执行了 n + n-1 + n-2 + ... + 1 次,所以时间复杂度是 O(n²),无法 AC,因此我们要寻找更快捷的方案。

滑动窗口

追求性能的降级方案是滑动窗口或动态规划,该题目计算的是字符串,不适合用动态规划。

那滑动窗口是否合适呢?

该题要计算的是满足条件的子串,该子串肯定是连续的,滑动窗口在连续子串匹配问题上是不会遗漏结果的,所以肯定可以用这个方案。

思路也很容易想,即:如果当前字符串覆盖 t,左指针右移,否则右指针右移。就像一个窗口扫描是否满足条件,需要右指针右移判断是否满足条件,满足条件后不一定是最优的,需要左指针继续右移找寻其他答案。

这里有一个难点是如何高效判断当前窗口内字符串是否覆盖 t,有三种想法:

第一种想法是对每个字符做一个计数器,再做一个总计数器,每当匹配到一个字符,当前字符计数器与总计数器 +1,这样直接用总计数器就能判断了。但这个方法有个漏洞,即总计数器没有包含字符类型,比如连续匹配 100 个 b,总计数器都 +1,此时其实缺的是 c,那么当 c 匹配到了之后,总计数器的值并不能判定出覆盖了。

第一种方法的优化版本可能是二进制,比如用 26 个 01 表示,但可惜每个字符出现的次数会超过 1,并不是布尔类型,所以用这种方式取巧也不行。

第二种方法是笨方法,每次递归时都判断下 s 字符串当前每个字符收集的数量是否超过 t 字符串每个字符出现的数量,坏处是每次递归都至多多循环 25 次。

笔者想到的第三种方法是,还是需要一个计数器,但这个计数器 notCoverChar 是一个 Set 类型,记录了每个 char 是否未 ready,所谓 ready 即该 char 在当前窗口内出现的次数 >= 该 char 在 t 字符串中出现的次数。同时还需要有 sCharMap、tCharMap 来记录两个字符串每个字符出现的次数,当右指针右移时,sCharMap 对应 char 计数增加,如果该 char 出现次数超过 t 该 char 出现次数,就从 notCoverChar 中移除;当左指针右移时,sCharMap 对应 char 计数减少,如果该 char 出现次数低于 t 该 char 出现次数,该 char 重新放到 notCoverChar 中。

代码如下:

functionminWindow(s:string,t:string):string{//s每个字母出现次数表constsCharMap={}//t每个字母对应出现次数表consttCharMap={}//未覆盖的字符有哪些constnotCoverChar=newSet()//计算各字符在t出现次数for(constcharoft){if(!tCharMap[char]){tCharMap[char]=0}tCharMap[char]++notCoverChar.add(char)}letleftIndex=0letrightIndex=-1letresult=""letcurrentStr=""//leftIndex|rightIndex超限才会停止while(leftIndex0if(notCoverChar.size>0){//此时窗口没有covert,rightIndex右移寻找rightIndex++constnextChar=s[rightIndex]currentStr+=nextCharif(sCharMap[nextChar]===undefined){sCharMap[nextChar]=0}sCharMap[nextChar]++//如果tCharMap有这个nextChar,且已收集数量超过t中数量,此charreadyif(tCharMap[nextChar]!==undefined&&sCharMap[nextChar]>=tCharMap[nextChar]){notCoverChar.delete(nextChar)}}else{//此时窗口正好covert,记录最短结果if(result===""){result=currentStr}elseif(currentStr.length其中还用了一些小缓存,比如 currentStr 记录当前窗口内字符串,这样当可以覆盖 t 时,随时可以拿到当前字符串,而不需要根据左右指针重新遍历。

总结

该题首先要排除动态规划,并根据连续子串特性第一时间想到滑动窗口可以覆盖到所有可能性。

滑动窗口方案想到后,需要想到如何高性能判断当前窗口内字符串可以覆盖 t,notCoverChar 就是一种不错的思路。

讨论地址是:精读《算法 - 最小覆盖子串》· Issue #496 · dt-fe/weekly

如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)

X 关闭

Copyright   2015-2023 华夏电气网版权所有  

备案号:琼ICP备2022009675号-37

邮箱: 435 227 67@qq.com