题目
76. 最小覆盖子串 - 力扣(LeetCode) (leetcode-cn.com)
思路
这道题非常经典,属于滑动窗口的经典思想。
首先看到这题估计很多人会去想动态规划,看当前子串与字符串t的关系,但是你会想动态规划方程怎么写?可能几乎没有出路。
好吧,任何算法都是暴力求解优化来的,我们先想暴力算法,把字符串s的所有子串都拿出来去看有没有覆盖字符串t,一定可行但是想都不要想一定超时,那么我们考虑剪枝,这个子串一定大于等于t,不然不可能覆盖(注:题目要求结果覆盖t的所有字符串,包括重复的)。这样我们考虑滑动窗口思想。
顾名思义,滑动窗口在字符串s上定义两个左右边界,不断移动左边界或右边界寻找答案。在此之前我们先考虑一个问题,假设你现在确定好了边界,怎么判断这个子串覆盖了t没?估计你会想对于这个子串一个个遍历,找个变量记录覆盖t的字符数。但是这样做又陷入了高时间复杂度的循环中。我们考虑用map来记录当前子串各字符个数和t的各字符个数,在移动边界之前先把t的map填好,同时保留记录覆盖t的字符数的变量(我设为num)。这样做不管子串怎样变我们只要对子串的map进行增删不就可以了吗?
然后再看移动边界的策略。我们首先初始化左右边界(left=0,right=-1,-1是为了便于循环),然后先移动右边界,在移动的过程中不断增加子串map的内容,同时记录num,当num=t.length()时,说明一个结果出来了,此时先移动左边界剔除左边多余的值,再将结果暂存。然后此时左边界再向右移动,num减一,再去移动右边界重复上面的过程。
当然上面的文字一看就迷乱了,可能不知道我在说什么。这是就拿出纸笔或者电脑画板,找个简单的例子,边画边想边敲代码。我认为这是一个非常好的做法,因为很多细节只有自己慢慢推敲才不容易乱。
上面是我随手画的图,其实一点也不精致,但是非常便于自己理清思路。
代码
我的代码思路并不新,复杂度也不占优,细节都在注释里面了,仅供参考。
#include<iostream>
#include<string>
#include<unordered_map>
using namespace std;
class Solution {
public:
string minWindow(string s, string t) {
int s_len=s.length();
int t_len=t.length();
string res="";
//判断临界条件
if(s_len==0||t_len==0||s_len<t_len){
return res;
}
//这两个map分别记录s和t的各元素个数,便于对比
unordered_map<char,int> s_map,t_map;
//先把t各元素个数记录下来
for(char c:t){
t_map[c]++;
}
int l=0;
int r=-1; //滑动窗口的左右边界
string temp; //s的子串暂存
int num=0; //temp覆盖t的字符个数
int len=INT_MAX;
//正常右边界向前
while(r<s_len){
r++;
if(s_map[s[r]]<t_map[s[r]]) num++;
s_map[s[r]]++;
if(num==t_len) { //已经构成一个答案了,记录
//先把左边界向右滑动,剔除左边多余的
while(s_map[s[l]]!=t_map[s[l]]){
s_map[s[l]]--;
l++;
}
//记录结果
temp=s.substr(l,r-l+1);
if(temp.length()<len){
res=temp;
len=temp.length();
}
//丢掉当前左边界字符,回到外层while移动右边界继续寻找
s_map[s[l]]--;
l++;
num--;//这一步很关键,因为上面内层while已经剔除多余了
}
}
return res;
}
};
int main(){
string s,t;
cin>>s;
cin>>t;
string res;
res=Solution().minWindow(s,t);
cout<<res<<endl;
return 0;
}