[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 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  ? -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) : "";
    }
};

 

類似題目:

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 題目講解匯總(持續更新中...)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM