• LeetCode - 76 最小覆盖子串


    题目来源

    76. 最小覆盖子串 - 力扣(LeetCode)

    题目描述

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

    注意:

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

    示例

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

    提示

    • 1 <= s.length, t.length <= 10^5
    • s 和 t 由英文字母组成

    题目解析

    本题要求的最小覆盖子串的含义是:包含某些指定字符,且要求最短的子串。

    即题目要我们在s字符串中,找到一个子串,这个子串需要包含t字符串中所有字符,这样的子串有很多,我们需要其中最短的那个子串。

    本题最容易想到的解法是滑动窗口,滑动窗口运动逻辑如下:

    需要注意的是这里的滑动窗口是不定长的滑动窗口。

     以上就是变长滑动窗口的运动逻辑。

    我们可以总结如下:

    滑动窗口的范围即:左右指针的范围,初始时左右指针都指向索引0位置,

    • 如果滑动窗口内部没有包含了t中所有字母,则右指针向右移动一格,然后继续判断滑动窗口内部是否包含了t中所有字母。
    • 如果滑动窗口内部包含了t中所有字母,(此时滑动窗口就是一个满足要求的子串,我们将他记录下来),则左指针向右移动一格,然后继续判断滑动窗口内部是否包含了t中所有字母。

    按照上面逻辑,我们可以将记录的子串比较长度,保留最短的作为题解。

    通过上面总结,我们似乎发现逻辑并不复杂,我们可以按照上面逻辑实现一下:

    1. /**
    2. * @param {string} s
    3. * @param {string} t
    4. * @return {string}
    5. */
    6. var minWindow = function (s, t) {
    7. if(s.length < t.length) return ''
    8. let l = 0
    9. let r = 0
    10. const ans = []
    11. while(r < s.length) {
    12. const subStr = s.substring(l,r+1)
    13. if(contain(subStr, t)) {
    14. ans.push(subStr)
    15. l++
    16. } else {
    17. r++
    18. }
    19. }
    20. if(!ans.length) return ''
    21. return ans.sort((a,b) => a.length - b.length)[0]
    22. };
    23. function contain(str, subStr) {
    24. const sup = {}
    25. for(let c of str) {
    26. sup[c] ? sup[c]++ : (sup[c]=1)
    27. }
    28. const sub = {}
    29. for(let c of subStr) {
    30. sub[c] ? sub[c]++ : (sub[c]=1)
    31. }
    32. let flag = true
    33. for(let c in sub) {
    34. if(!sup[c] || sub[c] > sup[c]) {
    35. flag = false
    36. break
    37. }
    38. }
    39. return flag
    40. }

     但是结果却是超时,原因是我们算法的时间复杂度达到了O(n^2),而数量级1 <= s.length, t.length <= 10^5,最终会有10^10次循环,这是肯定超时的。

    我们可以看下上面的代码,找找那部分是可以优化的?

    答案是contain函数,这个函数的作用是验证滑动窗口内部是否包含t字符串中所有字符的,但是方法比较笨,每当滑动窗口位置改变,就会重新统计字符数量,然后比较。这其中必然存在大量重复统计工作。

    利用滑动窗口解决问题,我们一定要学会差集思想,因为滑动窗口的移动是有规律的,比如每次向右移动一格,这样本轮滑动窗口状态和上一轮滑动窗口状态,必然存在交集区域,如下图BC就是两次滑动窗口的交集区域

     交集区域我们是不需要关注的,只需关注差集区域,如上图A和D,本轮滑动窗口,相当于在上一轮滑动窗口的基础上,删除了一格A,增加了一格D,这样的话,统计本轮滑动窗口内容,就不需要O(n)时间,只需要O(1)时间了。

    对于本题,滑动窗口的运动规律要复杂一点,并字符数量统计也不简单。

    首先,我们需要统计字符串t中各字符的数量,比如t="ABC",则统计结果为count: {A:1, B:1, C:1},并记录总数量为len = t.length。

    然后开始滑动窗口运动:

    滑动窗口运动中新增的字符c(即右指针向右移动一格后新增的内容),如果在t中,即count[c]存在,则count[c]--,那么此时len要不要也自减呢?

    我们看两种情况:

    用例1:

    s = "ADOBECODEBANC", t = "ABC"

    情况1:

     此时右指针指向的字符A,在t中存在,因此count[c]--,count: {A:0, B:1, C:1},此时应该len--,即len变为2。

    情况2:

     此时右指针指向的字符“B”,在t中存在,因此count[c]--,count: {A:1, B:-1, C:0},此时滑动窗口内部含有了两个B,但是这并不能意味着需要len也减去1,对于len来说,最多只能减去统计到B个数,如果B个数超出统计个数,即count[B] < 0,此时len不应该自减。

    上面是右指针运动中的特殊情况处理,对应的还有左指针运动逻辑处理:

    左指针运动,必然是找到了符合要求的子串,即滑动窗口内部含有了所有t字符,此时左指针右移一格,如上图,此时失去了A,而A刚好是t字符串内的字符,即count[A]存在,此时应该count[A]++,即将count:{A:0, B:0, C:0} 变为 count:{A:1, B:0, C:0},而此时也应该len++。

    但是对于

    此时情况是,上面的滑窗的count: {A:0, B:-1, c:0}

    当上面滑窗左指针右移一格后,变为下面的滑窗时,失去了B,刚好时t中字符,因此count[B]++,

    此时 count: {A:0, B:0, c:0},那么此时len是否也要自增1呢?答案是不需要,因为失去的B是超出统计个数的,因此失去了不需要作任何处理。

    JS算法源码

    1. /**
    2. * @param {string} s
    3. * @param {string} t
    4. * @return {string}
    5. */
    6. var minWindow = function (s, t) {
    7. let len = t.length;
    8. const count = {};
    9. for (let c of t) {
    10. count[c] ? count[c]++ : (count[c] = 1);
    11. }
    12. let i = 0;
    13. let j = 0;
    14. let minLen = s.length + 1; // 最短子串的长度
    15. let start = 0; // 最短子串的起始位置
    16. while (j < s.length) {
    17. const jc = s[j];
    18. if (count[jc]-- > 0) { // 注意:这里count[c]--必须写在if条件中,因为count[jc]可以是负数,但是len不能是负数
    19. len--;
    20. }
    21. while (len === 0) {
    22. if (minLen > j - i + 1) {
    23. minLen = j - i + 1;
    24. start = i;
    25. }
    26. const ic = s[i];
    27. if (count[ic]++ == 0) { // 此时原因请看博客评论区说明
    28. len++;
    29. }
    30. i++;
    31. }
    32. j++;
    33. }
    34. return minLen < s.length + 1 ? s.substring(start, start + minLen) : "";
    35. };

     

    Python算法源码

    1. class Solution(object):
    2. def minWindow(self, s, t):
    3. total = len(t)
    4. count = {}
    5. for c in t:
    6. if count.get(c) is None:
    7. count[c] = 0
    8. count[c] += 1
    9. i = 0
    10. j = 0
    11. minLen = len(s) + 1 # 最短子串的长度
    12. start = 0 # 最短子串的起始位置
    13. while j < len(s):
    14. jc = s[j]
    15. if count.get(jc) is not None:
    16. if count[jc] > 0:
    17. total -= 1 # 注意:count[jc]可以是负数,但是total不能是负数
    18. count[jc] -= 1
    19. while total == 0:
    20. if minLen > j - i + 1:
    21. minLen = j - i + 1
    22. start = i
    23. ic = s[i]
    24. if count.get(ic) is not None:
    25. if count[ic] == 0: # 此时原因请看博客评论区说明
    26. total += 1
    27. count[ic] += 1
    28. i += 1
    29. j += 1
    30. return s[start:start+minLen] if minLen < len(s) + 1 else ""

     

    Java算法源码

    1. class Solution {
    2. public String minWindow(String s, String t) {
    3. int len = t.length();
    4. int[] count = new int[128];
    5. for(int i=0; i
    6. char c = t.charAt(i);
    7. count[c]++;
    8. }
    9. int i=0;
    10. int j=0;
    11. int minLen = s.length() + 1;
    12. int start = 0;
    13. while(j < s.length()) {
    14. char jc = s.charAt(j);
    15. if(count[jc]-- > 0) {
    16. len--;
    17. }
    18. while(len == 0) {
    19. if(minLen > j - i + 1) {
    20. minLen = j - i + 1;
    21. start = i;
    22. }
    23. char ic = s.charAt(i);
    24. if(count[ic]++ == 0) { // 此时原因请看博客评论区说明
    25. len++;
    26. }
    27. i++;
    28. }
    29. j++;
    30. }
    31. return minLen < s.length() + 1 ? s.substring(start, start+minLen) : "";
    32. }
    33. }

  • 相关阅读:
    [附源码]Python计算机毕业设计SSM开放实验室管理系统(程序+LW)
    计算机毕业设计Java教务系统(源码+系统+mysql数据库+lw文档)
    pycharm安装
    xss-labs靶场1-5关
    计算机毕设JAVA——学习考试管理系统(基于SpringBoot+Vue前后端分离的项目)
    Linux权限
    安装.net framework报错“...扩展属性不一致”
    什么是继承?什么是组合?为何说要多用组合少用继承?
    「直播回放」电子会计档案管理,让数字化成果深度利用、可查可验
    仙境传说ro:如何在地图上刷怪教程
  • 原文地址:https://blog.csdn.net/qfc_128220/article/details/128092566