1. 程式人生 > >[LeetCode] Interleaving String 交織相錯的字串

[LeetCode] Interleaving String 交織相錯的字串

Given s1, s2, s3, find whether s3 is formed by the interleaving of s1 and s2.

For example,
Given:
s1 = "aabcc",
s2 = "dbbca",

When s3 = "aadbbcbcac", return true.
When s3 = "aadbbbaccc", return false.

這道求交織相錯的字串和之前那道 Word Break 拆分詞句 的題很類似,就想我之前說的只要是遇到字串的子序列或是匹配問題直接就上動態規劃Dynamic Programming,其他的都不要考慮,什麼遞迴呀的都是浮雲,千辛萬苦的寫了遞迴結果拿到OJ上妥妥Time Limit Exceeded,能把人氣昏了,所以還是直接就考慮DP解法省事些。一般來說字串匹配問題都是更新一個二維dp陣列,核心就在於找出遞推公式。那麼我們還是從題目中給的例子出發吧,手動寫出二維陣列dp如下:

  Ø d b b c a
Ø T F F F F F
a T F F F F F
a T T T T T F
b F T T F T F
c F F T T T T
c F F F T F T

首先,這道題的大前提是字串s1和s2的長度和必須等於s3的長度,如果不等於,肯定返回false。那麼當s1和s2是空串的時候,s3必然是空串,則返回true。所以直接給dp[0][0]賦值true,然後若s1和s2其中的一個為空串的話,那麼另一個肯定和s3的長度相等,則按位比較,若相同且上一個位置為True,賦True,其餘情況都賦False,這樣的二維陣列dp的邊緣就初始化好了。下面只需要找出遞推公式來更新整個陣列即可,我們發現,在任意非邊緣位置dp[i][j]時,它的左邊或上邊有可能為True或是False,兩邊都可以更新過來,只要有一條路通著,那麼這個點就可以為True。那麼我們得分別來看,如果左邊的為True,那麼我們去除當前對應的s2中的字串s2[j - 1] 和 s3中對應的位置的字元相比(計算對應位置時還要考慮已匹配的s1中的字元),為s3[j - 1 + i], 如果相等,則賦True,反之賦False。 而上邊為True的情況也類似,所以可以求出遞推公式為:

dp[i][j] = (dp[i - 1][j] && s1[i - 1] == s3[i - 1 + j]) || (dp[i][j - 1] && s2[j - 1] == s3[j - 1 + i]);

其中dp[i][j] 表示的是 s2 的前 i 個字元和 s1 的前 j 個字元是否匹配 s3 的前 i+j 個字元,根據以上分析,可寫出程式碼如下:

解法一:

class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        
if (s1.size() + s2.size() != s3.size()) return false; int n1 = s1.size(); int n2 = s2.size(); vector<vector<bool> > dp(n1 + 1, vector<bool> (n2 + 1, false)); dp[0][0] = true; for (int i = 1; i <= n1; ++i) { dp[i][0] = dp[i - 1][0] && (s1[i - 1] == s3[i - 1]); } for (int i = 1; i <= n2; ++i) { dp[0][i] = dp[0][i - 1] && (s2[i - 1] == s3[i - 1]); } for (int i = 1; i <= n1; ++i) { for (int j = 1; j <= n2; ++j) { dp[i][j] = (dp[i - 1][j] && s1[i - 1] == s3[i - 1 + j]) || (dp[i][j - 1] && s2[j - 1] == s3[j - 1 + i]); } } return dp[n1][n2]; } };

我們也可以把for迴圈合併到一起,用if條件來處理邊界情況,整體思路和上面的解法沒有太大的區別,參見程式碼如下:

解法二:

class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        if (s1.size() + s2.size() != s3.size()) return false;
        int n1 = s1.size(), n2 = s2.size();
        vector<vector<bool> > dp(n1 + 1, vector<bool> (n2 + 1, false)); 
        for (int i = 0; i <= n1; ++i) {
            for (int j = 0; j <= n2; ++j) {
                if (i == 0 && j == 0) {
                    dp[i][j] = true;
                } else if (i == 0) {
                    dp[i][j] = dp[i][j - 1] && s2[j - 1] == s3[i + j - 1];
                } else if (j == 0) {
                    dp[i][j] = dp[i - 1][j] && s1[i - 1] == s3[i + j - 1];
                } else {
                    dp[i][j] = (dp[i - 1][j] && s1[i - 1] == s3[i + j - 1]) || (dp[i][j - 1] && s2[j - 1] == s3[i + j - 1]);
                }
            }
        }
        return dp[n1][n2];
    }
};

這道題也可以使用帶優化的DFS來做,我們使用一個雜湊集合,用來儲存匹配失敗的情況,我們分別用變數i,j,和k來記錄字串s1,s2,和s3匹配到的位置,初始化的時候都傳入0。在遞迴函式中,首先根據i和j,算出key值,由於我們的雜湊集合中只能放一個數字,而我們要encode兩個數字i和j,所以通過用i乘以s3的長度再加上j來得到key,此時我們看,如果key已經在集合中,直接返回false,因為集合中存的是無法匹配的情況。然後先來處理corner case的情況,如果i等於s1的長度了,說明s1的字元都匹配完了,此時s2剩下的字元和s3剩下的字元可以直接進行匹配了,所以我們直接返回兩者是否能匹配的bool值。同理,如果j等於s2的長度了,說明s2的字元都匹配完了,此時s1剩下的字元和s3剩下的字元可以直接進行匹配了,所以我們直接返回兩者是否能匹配的bool值。如果s1和s2都有剩餘字元,那麼當s1的當前字元等於s3的當前字元,那麼呼叫遞迴函式,注意i和k都加上1,如果遞迴函式返回true,則當前函式也返回true;還有一種情況是,當s2的當前字元等於s3的當前字元,那麼呼叫遞迴函式,注意j和k都加上1,如果遞迴函式返回true,那麼當前函式也返回true。如果匹配失敗了,則將key加入集合中,並返回false即可,參見程式碼如下:

解法三:

class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        if (s1.size() + s2.size() != s3.size()) return false;
        unordered_set<int> s;
        return helper(s1, 0, s2, 0, s3, 0, s);
    }
    bool helper(string& s1, int i, string& s2, int j, string& s3, int k, unordered_set<int>& s) {
        int key = i * s3.size() + j;
        if (s.count(key)) return false;
        if (i == s1.size()) return s2.substr(j) == s3.substr(k);
        if (j == s2.size()) return s1.substr(i) == s3.substr(k);
        if ((s1[i] == s3[k] && helper(s1, i + 1, s2, j, s3, k + 1, s)) || 
            (s2[j] == s3[k] && helper(s1, i, s2, j + 1, s3, k + 1, s))) return true;
        s.insert(key);
        return false;
    }
};

既然DFS可以,那麼BFS也就坐不住了,也要出來浪一波。這裡我們需要用佇列queue來輔助運算,如果將解法一講解中的那個二維dp陣列列出來的TF圖當作一個迷宮的話,那麼BFS的目的就是要從(0, 0)位置找一條都是T的路徑通到(n1, n2)位置,這裡我們還要使用雜湊集合,不過此時儲存到是已經遍歷過的位置,佇列中還是存key值,key值的encode方法跟上面DFS解法的相同,初識時放個0進去。然後我們進行while迴圈,迴圈條件除了q不為空,還有一個是k小於n3,因為匹配完s3中所有的字元就結束了。然後由於是一層層的遍歷,所以要直接迴圈queue中元素個數的次數,在for迴圈中,對隊首元素進行解碼,得到i和j值,如果i小於n1,說明s1還有剩餘字元,如果s1當前字元等於s3當前字元,那麼把s1的下一個位置i+1跟j一起加碼算出key值,如果該key值不在於集合中,則加入集合,同時加入佇列queue中;同理,如果j小於n2,說明s2還有剩餘字元,如果s2當前字元等於s3當前字元,那麼把s2的下一個位置j+1跟i一起加碼算出key值,如果該key值不在於集合中,則加入集合,同時加入佇列queue中。for迴圈結束後,k自增1。最後如果匹配成功的話,那麼queue中應該只有一個(n1, n2)的key值,且k此時等於n3,所以當queue為空或者k不等於n3的時候都要返回false,參見程式碼如下:

解法四:

class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        if (s1.size() + s2.size() != s3.size()) return false;
        int n1 = s1.size(), n2 = s2.size(), n3 = s3.size(), k = 0;
        unordered_set<int> s;
        queue<int> q{{0}};
        while (!q.empty() && k < n3) {
            int len = q.size();
            for (int t = 0; t < len; ++t) {
                int i = q.front() / n3, j = q.front() % n3; q.pop();
                if (i < n1 && s1[i] == s3[k]) {
                    int key = (i + 1) * n3 + j;
                    if (!s.count(key)) {
                        s.insert(key);
                        q.push(key);
                    }
                }
                if (j < n2 && s2[j] == s3[k]) {
                    int key = i * n3 + j + 1;
                    if (!s.count(key)) {
                        s.insert(key);
                        q.push(key);
                    }
                }
            }
            ++k;
        }
        return !q.empty() && k == n3;
    }
};

參考資料: