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
Swill be in the range[1, 20000]. - The length of
Twill 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 ? -1 -1 b ? 1 -1 d ? 1 1
這里的問號是邊界,我們還不知道如何初給邊界賦值,我們看到,為 -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 ∅ 0 -1 -1 d 1 -1 -1 b 2 1 -1 d 3 1 1
為了使代碼更加簡潔,我們在遍歷完每一行,檢測如果 dp[i][n] 不為-1,說明T已經被完全包含了,且當前的位置跟起始位置都知道了,我們計算出長度來更新一個全局最小值 minLen,同時更新最小值對應的起始位置 start,最后取出這個全局最短子串,如果沒有找到返回空串即可,參見代碼如下:
解法一:
class Solution { public: string minWindow(string S, string T) { int m = S.size(), n = T.size(), start = -1, minLen = INT_MAX; vector<vector<int>> dp(m + 1, vector<int>(n + 1, -1)); for (int i = 0; i <= m; ++i) dp[i][0] = i; for (int i = 1; i <= m; ++i) { for (int j = 1; j <= min(i, n); ++j) { dp[i][j] = (S[i - 1] == T[j - 1]) ? dp[i - 1][j - 1] : dp[i - 1][j]; } if (dp[i][n] != -1) { int len = i - dp[i][n]; if (minLen > len) { minLen = len; start = dp[i][n]; } } } return (start != -1) ? 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 = -1, minLen = INT_MAX, i = 0, j = 0; while (i < m) { if (S[i] == T[j]) { if (++j == n) { int end = i + 1; while (--j >= 0) { while (S[i--] != T[j]); } ++i; ++j; if (end - i < minLen) { minLen = end - i; start = i; } } } ++i; } return (start != -1) ? S.substr(start, minLen) : ""; } };
類似題目:
Cheapest Flights Within K Stops
Longest Continuous Increasing Subsequence
參考資料:
https://leetcode.com/problems/minimum-window-subsequence/
