Given strings S and T, find the minimum (contiguous) substring W of S, so that T is a subsequence of W.

If there is no such window in S that covers all characters in T, return the empty string "". If there are multiple such minimum-length windows, return the one with the left-most starting index.

Example 1:

Input:
S = "abcdebdde", T = "bde"
Output: "bcde"
Explanation:
"bcde" is the answer because it occurs before "bdde" which has the same length.
"deb" is not a smaller window because the elements of T in the window must occur in order.

Note:

  • All the strings in the input will only contain lowercase letters.
  • The length of S will be in the range [1, 20000].
  • The length of T will be in the range [1, 100].

这道题给了我们两个字符串S和T,让我们找出S的一个长度最短子串W,使得T是W的子序列,如果长度相同,取起始位置靠前的。清楚子串和子序列的区别,那么题意就不难理解,题目中给的例子也很好的解释了题意。我们经过研究可以发现,返回的子串的起始字母和T的起始字母一定相同,这样才能保证最短。那么你肯定会想先试试暴力搜索吧,以S中每个T的起始字母为起点,均开始搜索字符串T,然后维护一个子串长度的最小值。如果是这种思路,那么还是趁早打消念头吧,博主已经替你试过了,OJ 不依。原因也不难想,假如S中有大量的连续b,并且如果T也很长的话,这种算法实在是不高效啊。根据博主多年经验,这种玩字符串且还是 Hard 的题,十有八九都是要用动态规划 Dynamic Programming 来做的,那么就直接往 DP 上去想吧。DP 的第一步就是设计 dp 数组,像这种两个字符串的题,一般都是一个二维数组,想想该怎么定义。确定一个子串的两个关键要素是起始位置和长度,那么我们的 dp 值到底应该是定起始位置还是长度呢?That is a question! 仔细想一想,其实起始位置是长度的基础,因为我们一旦知道了起始位置,那么当前位置减去起始位置,就是长度了,所以我们 dp 值定为起始位置。那么 dp[i][j] 表示范围S中前i个字符包含范围T中前j个字符的子串的起始位置,注意这里的包含是子序列包含关系。然后就是确定长度了,有时候会使用字符串的原长度,有时候会多加1,看个人习惯吧,这里博主长度多加了个1。

OK,下面就是重中之重啦,求状态转移方程。一般来说,dp[i][j] 的值是依赖于之前已经求出的dp值的,在递归形式的解法中,dp数组也可以看作是记忆数组,从而省去了大量的重复计算,这也是 dp 解法凌驾于暴力搜索之上的主要原因。牛B的方法总是最难想出来的,dp 的状态转移方程就是其中之一。在脑子一片浆糊的情况下,博主的建议是从最简单的例子开始分析,比如 S = "b", T = "b", 那么我们就有 dp[1][1] = 0,因为S中的起始位置为0,长度为1的子串可以包含T。如果当 S = "d", T = "b",那么我们有 dp[1][1] = -1,因为我们的dp数组初始化均为 -1,表示未匹配或者无法匹配。下面来看一个稍稍复杂些的例子,S = "dbd", T = "bd",我们的dp数组是:

   ∅  b  d
∅ ? ? ?
d ? - -
b ? -
d ?

这里的问号是边界,我们还不知道如何初给边界赋值,我们看到,为 -1 的地方是对应的字母不相等的地方。我们首先要明确的是 dp[i][j] 中的j不能大于i,因为T的长度不能大于S的长度,所以j大于i的 dp[i][j] 一定都是-1的。再来看为1的几个位置,首先是 dp[2][1] = 1,这里表示db包含b的子串起始位置为1,make sense!然后是 dp[3][1] = 1,这里表示 dbd 包含b的子串起始位置为1,没错!然后是 dp[3][2] = 1,这里表示 dbd 包含 bd 的起始位置为1,all right! 那么我们可以观察出,当 S[i] == T[j] 的时候,实际上起始位置和 dp[i - 1][j - 1] 是一样的,比如 dbd 包含 bd 的起始位置和 db 包含b的起始位置一样,所以可以继承过来。那么当 S[i] != T[j] 的时候,怎么搞?其实是和 dp[i - 1][j] 是一样的,比如 dbd 包含b的起始位置和 db 包含b的起始位置是一样的。

嗯,这就是状态转移方程的核心了,下面再来看边界怎么赋值,由于j比如小于等于i,所以第一行的第二个位置往后一定都是-1,我们只需要给第一列赋值即可。通过前面的分析,我们知道了当 S[i] == T[j] 时,我们取的是左上角的 dp 值,表示当前字母在S中的位置,由于我们dp数组提前加过1,所以第一列的数只要赋值为当前行数即可。最终的 dp 数组如下:

   ∅  b  d
∅ - -
d - -
b -
d

为了使代码更加简洁,我们在遍历完每一行,检测如果 dp[i][n] 不为-1,说明T已经被完全包含了,且当前的位置跟起始位置都知道了,我们计算出长度来更新一个全局最小值 minLen,同时更新最小值对应的起始位置 start,最后取出这个全局最短子串,如果没有找到返回空串即可,参见代码如下:

解法一:

class Solution {
public:
string minWindow(string S, string T) {
int m = S.size(), n = T.size(), start = -, minLen = INT_MAX;
vector<vector<int>> dp(m + , vector<int>(n + , -));
for (int i = ; i <= m; ++i) dp[i][] = i;
for (int i = ; i <= m; ++i) {
for (int j = ; j <= min(i, n); ++j) {
dp[i][j] = (S[i - ] == T[j - ]) ? dp[i - ][j - ] : dp[i - ][j];
}
if (dp[i][n] != -) {
int len = i - dp[i][n];
if (minLen > len) {
minLen = len;
start = dp[i][n];
}
}
}
return (start != -) ? S.substr(start, minLen) : "";
}
};

论坛上的 danzhutest大神 提出了一种双指针的解法,其实这是优化过的暴力搜索的方法,而且居然 beat 了 100%,给跪了好嘛?!而且这双指针的跳跃方式犹如舞蹈般美妙绝伦,比那粗鄙的暴力搜索双指针不知道高到哪里去了?!举个栗子来说吧,比如当 S = "bbbbdde", T = "bde" 时,我们知道暴力搜索的双指针在S和T的第一个b匹配上之后,就开始检测S之后的字符能否包含T之后的所有字符,当匹配结束后,S的指针就会跳到第二个b开始匹配,由于有大量的重复b出现,所以每一个b都要遍历一遍,会达到平方级的复杂度,会被 OJ 无情拒绝。而下面这种修改后的算法会跳过所有重复的b,使得效率大大提升,具体是这么做的,当第一次匹配成功后,我们的双指针往前走,找到那个刚好包含T中字符的位置,比如开始指针 i = 0 时,指向S中的第一个b,指针 j = 0 时指向T中的第一个b,然后开始匹配T,当 i = 6, j = 2 时,此时完全包含了T。暴力搜索解法中此时i会回到1继续找,而这里,我们通过向前再次匹配T,会在 i = 3,j = 0 处停下,然后继续向后找,这样S中重复的b就会被跳过,从而大大的提高了效率,但是最坏情况下的时间复杂度还是 O(mn)。旋转,跳跃,我闭着眼,尘嚣看不见,你沉醉了没?博主已经沉醉在这双指针之舞中了......

解法二:

class Solution {
public:
string minWindow(string S, string T) {
int m = S.size(), n = T.size(), start = -, minLen = INT_MAX, i = , j = ;
while (i < m) {
if (S[i] == T[j]) {
if (++j == n) {
int end = i + ;
while (--j >= ) {
while (S[i--] != T[j]);
}
++i; ++j;
if (end - i < minLen) {
minLen = end - i;
start = i;
}
}
}
++i;
}
return (start != -) ? S.substr(start, minLen) : "";
}
};

类似题目:

Largest Plus Sign

Cheapest Flights Within K Stops

Domino and Tromino Tiling

Minimum Window Subsequence

Longest Continuous Increasing Subsequence

参考资料:

https://leetcode.com/problems/minimum-window-subsequence/

https://leetcode.com/problems/minimum-window-subsequence/discuss/109358/C++-DP-with-explanation-O(ST)-53ms

https://leetcode.com/problems/minimum-window-subsequence/discuss/109356/JAVA-two-pointer-solution-(12ms-beat-100)-with-explaination

LeetCode All in One 题目讲解汇总(持续更新中...)

[LeetCode] Minimum Window Subsequence 最小窗口序列的更多相关文章

  1. [LeetCode] 727. Minimum Window Subsequence 最小窗口序列

    Given strings S and T, find the minimum (contiguous) substring W of S, so that T is a subsequence of ...

  2. [LeetCode] 727. Minimum Window Subsequence 最小窗口子序列

    Given strings S and T, find the minimum (contiguous) substring W of S, so that T is a subsequenceof  ...

  3. [LeetCode] Minimum Window Substring 最小窗口子串

    Given a string S and a string T, find the minimum window in S which will contain all the characters ...

  4. [LeetCode] 76. Minimum Window Substring 最小窗口子串

    Given a string S and a string T, find the minimum window in S which will contain all the characters ...

  5. [Leetcode] minimum window substring 最小字符窗口

    Given a string S and a string T, find the minimum window in S which will contain all the characters ...

  6. [LeetCode] Sliding Window Maximum 滑动窗口最大值

    Given an array nums, there is a sliding window of size k which is moving from the very left of the a ...

  7. LC 727. Minimum Window Subsequence 【lock,hard】

    Given strings S and T, find the minimum (contiguous) substring W of S, so that T is a subsequenceof  ...

  8. [leetcode]76. Minimum Window Substring最小字符串窗口

    Given a string S and a string T, find the minimum window in S which will contain all the characters ...

  9. [leetcode]Minimum Window Substring @ Python

    原题地址:https://oj.leetcode.com/problems/minimum-window-substring/ 题意: Given a string S and a string T, ...

随机推荐

  1. [css 揭秘]:CSS揭秘 技巧(二):多重边框

    我的github地址:https://github.com/FannieGirl/ifannie/ 源码都在这上面哦! 喜欢的给我一个星吧 多重边框 问题:我们通常希望在css代码层面以更灵活的方式来 ...

  2. 关闭系统邮件提醒:you hava a new mail(转)

    有时候,在输入某些触及到系统安全或者内核方面的命令都会提醒你: You have new mail in /var/spool/mail/root 只需要在root 用户下,不设置邮件检测即可! #e ...

  3. python提示import XX 不存在,如何排查错误

    1.如何确定文件夹是一个包, 检查包中是否有__init__.py 文件, 在python模块的每一个包中,都有一个__init__.py文件(这个文件定义了包的属性和方法)然后是一些模块文件和子目录 ...

  4. linux scp 命令

    scp 命令 scp 命令 意思是 secure copy 即安全拷贝,可以把它看做是 cp 命令的高级版,可以跨主机拷贝. 经常用来在局域网内不同主机之间分享文件,或者在本机与远程主机中分享文件. ...

  5. MySQL 操作详解

    MySQL 操作详解 一.实验简介 本节实验中学习并实践 MySQL 上创建数据库.创建表.查找信息等详细的语法及参数使用方法. 二.创建并使用数据库 1. 创建并选择数据库 使用SHOW语句找出服务 ...

  6. 支付宝sdk集成,报系统繁忙 请稍后再试(ALI64)

    移动快捷支付,往往需要集成支付宝的sdk,集成的过程相对简单,只要按照支付宝的文档,进行操作一般不会出问题.            下面主要说明一下,集成sdk后报"系统繁忙 请稍后再试(A ...

  7. 树莓派3启动wifi并且配置wifi

    概述 树莓派3内置了wifi和蓝牙模块,我们不用像以前的版本那样,再去购买一个外接的模块练到raspberry上. 当我们第一次启动了树莓派的时候,必然使用了网线,但是之后的每一次使用,我们当然更希望 ...

  8. Struts2之Struts2的标签库

    前言: Struts2提供了大量的标签 ,用来帮助开发表现层页面,这些表现一方面解决了美观性的需求,因为它们具有html标签一样的外观,另一方面它们解决了功能性的需求, 因为它们具有jsp脚本一样的逻 ...

  9. 数据恢复案例分享:MSSQL 2000 错误823

    一.故障描述 MSSQL Server 2000 附加数据库错误823,附加数据库失败.数据库没有备份,不能通过备份恢复数据库,急需恢复数据库中的数据. 二.故障分析SQL Server数据库 823 ...

  10. 14-TypeScript简单工厂模式

    在TypeScript中,要调用功能,通常在调用方通过实例化被调用方对象来调用相关方法,但这种实现在调用方和被调用方形成了强耦合的关系. 另外如果被调用方有种实现,在调用方需要根据场景去实例化不同的类 ...