求某字符串T中含有某字符串S的所有字符的最小子字符串。如果不存在则返回"".
算法用左右两个指针维护一个窗口。
将右指针右移,直至窗口满足条件,包含S中所有字符。
将左指针左移,直至窗口不再满足条件。此过程中每移动一次,都更新最小子字符串。
重复1、2两步。
WHY IT WORKS设想一个最naive的算法如何遍历T中的所有子字符串。以T中的每一个字符为子字符串的起始字符,从1开始,增加子字符串的长度直至触及T的尾字符,这样就是遍历了T中的所有子字符串。
比如字符串“ABCD”,以'A'开头的子字符串有"A", "AB", "ABC", "ABCD";以'B'开头的有"B", "BC", "BCD";以'C'开头的有"C", "CD";以"D"开头的有"D"。这样遍历的时间复杂度是O(n^2)。
我们把目光集中于起始字符,看看滑动窗口的效用。
滑动窗口算法中的第一步立足于某字符x,相当于以x为起始字符,寻找满足条件的子字符串。由于题中要求最短的子字符串,所以一旦满足条件就可停下,不必再往下寻找,相当于节省了一部分算力。
假设第一步中找到的子字符串以某字符y结束,且x至y这个子字符串的长度为m。则遍历到现在为止,找到的子字符串答案的长度<=m。(假设x之前还有其他元素,则1、2步已重复过数轮)
在第二步中,通过移动左指针对窗口进行收缩。假设左指针到达元素z时,窗口不再满足条件。则在左指针移动的过程中,以(x,z)开区间内的元素作为起始字符,y为结束字符进行了遍历。
将结束字符固定在y处是对naive解法的重要优化,蕴含了滑动窗口算法可以正确找出答案的主要数学原理:
对x、z之间的某一元素t,以t为起始字符且满足条件的最小子字符串必在y处结束。
证明:窗口收缩在z左侧,保证了t至y的字符串满足条件;设t至y不是最小的子字符串,则存在由t开始至字符r的的字符串满足条件,且r在y左侧,那么x至r的字符串也必满足条件,与第一步中得到的结论矛盾,故得证。
因为这个原理,x和z之间的元素只靠窗口左边界收缩就得到了遍历。时间复杂度由平方变成了线性。
在第二步中,以[x, z)区间内的元素为起始字符的所有子字符串得到遍历。下一轮次的第一步会以z为起始字符进行寻找。如此往复,随着窗口交替伸展和收缩,所有的可能性(即以所有元素作为起始字符的子字符串)都会得到遍历。
IMPLEMENTATION以上分析确定了滑动窗口算法的大致框架。至于如何记录窗口的状态、判断窗口是否满足条件,题目中挖了一个小坑。
乍一看,似乎可以用HashSet保存T中的字符(且称为重要字符),用来查看T中是否存在某字符。用另一个HashSet记录窗口中出现的重要字符,并用一个counter记录窗口中重要字符的个数,若与T的长度相等则认为符合条件。看起来天衣无缝,但如果T中存在重复字符,如"AABCC",则该方法不再有效。
可对该方法做一个小改进使之可以符合题意:用HashMap来保存重要字符及出现的次数。如果T为"AABCC",则保存为[A--2, B--1, C--2]。另用一个HashMap记录窗口中的重要字符及数量,用counter记录窗口中达到次数的不重复的重要字符数。如A出现2次则counter可加1,B出现1次counter即可加1,同理,C必须出现2次counter才可加1。通过将counter的值与第一个HashMap的size对比来判断窗口是否满足条件。