【数据机构与算法】之深入解析“最小覆盖子串”的求解思路与算法示例

举报
Serendipity·y 发表于 2022/02/16 23:32:05 2022/02/16
【摘要】 一、题目要求 给你一个字符串 s、一个字符串 t,返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “”。注意: 对于 t 中重复字符,寻找的子...

一、题目要求

  • 给你一个字符串 s、一个字符串 t,返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “”。
  • 注意:
    • 对于 t 中重复字符,寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
    • 如果 s 中存在这样的子串,保证它是唯一的答案。
  • 示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"

  
 
  • 1
  • 2
  • 示例 2:
输入:s = "a", t = "a"
输出:"a"

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

  
 
  • 1
  • 2
  • 3
  • 4
  • 提示:
    • 1 <= s.length, t.length <= 105
    • s 和 t 由英文字母组成。

二、求解算法

① 滑动窗口

  • 思路:
    • 用 i,j 表示滑动窗口的左边界和右边界,通过改变 i,j 来扩展和收缩滑动窗口,可以想象成一个窗口在字符串上游走;
    • 当这个窗口包含的元素满足条件,即包含字符串 T 的所有元素,记录下这个滑动窗口的长度 j-i+1,这些长度中的最小值就是要求的结果。
  • 步骤:
    • 不断增加 j 使滑动窗口增大,直到窗口包含了 T 的所有元素;
    • 不断增加 i 使滑动窗口缩小,因为是要求最小字串,所以将不必要的元素排除在外,使长度减小,直到碰到一个必须包含的元素,这个时候不能再扔,再扔就不满足条件,记录此时滑动窗口的长度,并保存最小值;
    • 让 i 再增加一个位置,这个时候滑动窗口肯定不满足条件,那么继续从步骤一开始执行,寻找新的满足条件的滑动窗口,如此反复,直到j超出了字符串 S 范围。
  • 如何判断滑动窗口包含了 T 的所有元素?
    • 用一个字典 need 来表示当前滑动窗口中需要的各元素的数量,一开始滑动窗口为空,用 T 中各元素来初始化这个 need,当滑动窗口扩展或者收缩的时候,去维护这个 need 字典,例如当滑动窗口包含某个元素,就让 need 中这个元素的数量减 1,代表所需元素减少了 1 个;当滑动窗口移除某个元素,就让 need 中这个元素的数量加 1。
    • 记住一点:need 始终记录着当前滑动窗口下,我们还需要的元素数量,在改变 i,j 时,需同步维护 need。
    • 值得注意的是,只要某个元素包含在滑动窗口中,就会在 need 中存储这个元素的数量,如果某个元素存储的是负数代表这个元素是多余的。比如当 need 等于{‘A’:-2,‘C’:1}时,表示当前滑动窗口中,有 2 个 A 是多余的,同时还需要 1 个 C。这么做的目的就是为了上面的第二个步骤中,排除不必要的元素,数量为负的就是不必要的元素,而数量为 0 表示刚刚好。
    • 回到问题中来,那么如何判断滑动窗口包含了 T 的所有元素?结论就是当 need 中所有元素的数量都小于等于 0 时,表示当前滑动窗口不再需要任何元素。
  • 优化:
    • 如果每次判断滑动窗口是否包含了 T 的所有元素,都去遍历 need 看是否所有元素数量都小于等于 0,这个会耗费 O(k) 的时间复杂度,k 代表字典长度,最坏情况下,k 可能等于 len(S);
    • 其实这个是可以避免的,可以维护一个额外的变量 needCnt 来记录所需元素的总数量,当碰到一个所需元素 c,不仅 need[c] 的数量减少 1,同时 needCnt 也要减少 1,这样通过 needCnt 就可以知道是否满足条件,而无需遍历字典了。
    • need 记录了遍历到的所有元素,而只有 need[c]>0 大于 0 时,代表 c 就是所需元素。
  • 以 S=“DOABECODEBANC”,T=“ABC” 为例:
    • 初始状态:

在这里插入图片描述

    • 不断增加 j 使滑动窗口增大,直到窗口包含了 T 的所有元素,need 中所有元素的数量都小于等于 0,同时 needCnt 也是 0:

在这里插入图片描述

    • 不断增加 i 使滑动窗口缩小,直到碰到一个必须包含的元素 A,此时记录长度更新结果:

在这里插入图片描述

    • 让 i 再增加一个位置,开始寻找下一个满足条件的滑动窗口:

在这里插入图片描述

  • Python 示例:
    def minWindow(self, s: str, t: str) -> str:
        need=collections.defaultdict(int)
        for c in t:
            need[c]+=1
        needCnt=len(t)
        i=0
        res=(0,float('inf'))
        for j,c in enumerate(s):
            if need[c]>0:
                needCnt-=1
            need[c]-=1
            if needCnt==0:       #步骤一:滑动窗口包含了所有T元素
                while True:      #步骤二:增加i,排除多余元素
                    c=s[i] 
                    if need[c]==0:
                        break
                    need[c]+=1
                    i+=1
                if j-i<res[1]-res[0]:   #记录结果
                    res=(i,j)
                need[s[i]]+=1  #步骤三:i增加一个位置,寻找新的满足条件滑动窗口
                needCnt+=1
                i+=1
        return '' if res[1]>len(s) else s[res[0]:res[1]+1]    #如果res始终没被更新过,代表无满足条件的结果

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • Java 示例:
class Solution {
    public String minWindow(String s, String t) {
        if (s == null || s.length() == 0 || t == null || t.length() == 0){
            return "";
        }
        int[] need = new int[128];
        // 记录需要的字符的个数
        for (int i = 0; i < t.length(); i++) {
            need[t.charAt(i)]++;
        }
        // l是当前左边界,r是当前右边界,size记录窗口大小,count是需求的字符个数,start是最小覆盖串开始的index
        int l = 0, r = 0, size = Integer.MAX_VALUE, count = t.length(), start = 0;
        // 遍历所有字符
        while (r < s.length()) {
            char c = s.charAt(r);
            if (need[c] > 0) { // 需要字符c
                count--;
            }
            need[c]--; // 把右边的字符加入窗口
            if (count == 0) { // 窗口中已经包含所有字符
                while (l < r && need[s.charAt(l)] < 0) {
                    need[s.charAt(l)]++; // 释放右边移动出窗口的字符
                    l++; // 指针右移
                }
                if (r - l + 1 < size) { // 不能右移时候挑战最小窗口大小,更新最小窗口开始的start
                    size = r - l + 1;
                    start = l; // 记录下最小值时候的开始位置,最后返回覆盖串时候会用到
                }
                // l向右移动后窗口肯定不能满足了 重新开始循环
                need[s.charAt(l)]++;
                l++;
                count++;
            }
            r++;
        }
        return size == Integer.MAX_VALUE ? "" : s.substring(start, start + size);
    }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

文章来源: blog.csdn.net,作者:Serendipity·y,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/Forever_wj/article/details/122942702

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。