Hard!
題目描述:
給定三個字符串 s1, s2, s3, 驗證 s3 是否是由 s1 和 s2 交錯組成的。
示例 1:
輸入: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac" 輸出: true
示例 2:
輸入: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc" 輸出: 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 個字符,根據以上分析,可寫出代碼如下:
C++解法一:
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條件來處理邊界情況,整體思路和上面的解法沒有太大的區別。
C++解法二:
1 class Solution { 2 public: 3 bool isInterleave(string s1, string s2, string s3) { 4 if (s1.size() + s2.size() != s3.size()) return false; 5 int n1 = s1.size(), n2 = s2.size(); 6 vector<vector<bool> > dp(n1 + 1, vector<bool> (n2 + 1, false)); 7 for (int i = 0; i <= n1; ++i) { 8 for (int j = 0; j <= n2; ++j) { 9 if (i == 0 && j == 0) { 10 dp[i][j] = true; 11 } else if (i == 0) { 12 dp[i][j] = dp[i][j - 1] && s2[j - 1] == s3[i + j - 1]; 13 } else if (j == 0) { 14 dp[i][j] = dp[i - 1][j] && s1[i - 1] == s3[i + j - 1]; 15 } else { 16 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]); 17 } 18 } 19 } 20 return dp[n1][n2]; 21 } 22 };
這道題也可以使用帶優化的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即可。
C++解法三:
1 class Solution { 2 public: 3 bool isInterleave(string s1, string s2, string s3) { 4 if (s1.size() + s2.size() != s3.size()) return false; 5 unordered_set<int> s; 6 return helper(s1, 0, s2, 0, s3, 0, s); 7 } 8 bool helper(string& s1, int i, string& s2, int j, string& s3, int k, unordered_set<int>& s) { 9 int key = i * s3.size() + j; 10 if (s.count(key)) return false; 11 if (i == s1.size()) return s2.substr(j) == s3.substr(k); 12 if (j == s2.size()) return s1.substr(i) == s3.substr(k); 13 if ((s1[i] == s3[k] && helper(s1, i + 1, s2, j, s3, k + 1, s)) || 14 (s2[j] == s3[k] && helper(s1, i, s2, j + 1, s3, k + 1, s))) return true; 15 s.insert(key); 16 return false; 17 } 18 };
既然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。
C++解法四:
1 class Solution { 2 public: 3 bool isInterleave(string s1, string s2, string s3) { 4 if (s1.size() + s2.size() != s3.size()) return false; 5 int n1 = s1.size(), n2 = s2.size(), n3 = s3.size(), k = 0; 6 unordered_set<int> s; 7 queue<int> q{{0}}; 8 while (!q.empty() && k < n3) { 9 int len = q.size(); 10 for (int t = 0; t < len; ++t) { 11 int i = q.front() / n3, j = q.front() % n3; q.pop(); 12 if (i < n1 && s1[i] == s3[k]) { 13 int key = (i + 1) * n3 + j; 14 if (!s.count(key)) { 15 s.insert(key); 16 q.push(key); 17 } 18 } 19 if (j < n2 && s2[j] == s3[k]) { 20 int key = i * n3 + j + 1; 21 if (!s.count(key)) { 22 s.insert(key); 23 q.push(key); 24 } 25 } 26 } 27 ++k; 28 } 29 return !q.empty() && k == n3; 30 } 31 };