Given two integer arrays A and B, return the maximum length of an subarray that appears in both arrays.
Example 1:
Input: A: [1,2,3,2,1] B: [3,2,1,4,7] Output: 3 Explanation: The repeated subarray with maximum length is [3, 2, 1].
Note:
- 1 <= len(A), len(B) <= 1000
- 0 <= A[i], B[i] < 100
這道題給了我們兩個數組A和B,讓返回連個數組的最長重復子數組。那么如果將數組換成字符串,實際這道題就是求 Longest Common Substring 的問題了,而貌似 LeetCode 上並沒有這種明顯的要求最長相同子串的題,注意需要跟最長子序列 Longest Common Subsequence 區分開,關於最長子序列會在 follow up 中討論。好,先來看這道題,既然是子數組,那么重復的地方一定是連續的,而且起點可能會是在數組中的任意地方,這樣的話,最暴力的方法就是遍歷A中的每個位置,把每個位置都當作是起點進行和B從開頭比較,每次A和B都同時前進一個,假如相等,則計數器會累加1,不相等的話,計數器會重置為0,每次用計數器 cnt 的長度來更新結果 res。然后用同樣的方法對B也處理一遍,把每個位置都當作是起點進行和A從開頭比較,每次A和B都同時前進一個,這樣最終下來,就可以求出最長重復子數組的長度,令人驚喜的是,這種暴力搜索解法的擊敗率相當的高,參見代碼如下:
解法一:
class Solution { public: int findLength(vector<int>& A, vector<int>& B) { int m = A.size(), n = B.size(), res = 0; for (int offset = 0; offset < m; ++offset) { for (int i = offset, j = 0; i < m && j < n;) { int cnt = 0; while (i < m && j < n && A[i++] == B[j++]) ++cnt; res = max(res, cnt); } } for (int offset = 0; offset < n; ++offset) { for (int i = 0, j = offset; i < m && j < n;) { int cnt = 0; while (i < m && j < n && A[i++] == B[j++]) ++cnt; res = max(res, cnt); } } return res; } };
我們還可以使用二分法+哈希表來做,別問博主怎么知道(看了題目標簽,然后去論壇上找對應的解法即可,哈哈~)。雖然解法看起來很炫,但不太簡潔,不是博主的 style,但還是收錄進來吧。這里使用二分搜索法來找什么呢?其實是來直接查找最長重疊子數組的長度的,因為這個長度是有范圍限制的,在 [0, min(m, n)] 之間,其中m和n分別是數組A和B的長度。這樣每次折半出一個 mid,然后驗證有沒有這么一個長度為 mid 的子數組在A和B中都存在。從數組中取子數組有些麻煩,可以將數組轉為字符串,取子串就相對來說容易一些了。將數組A和B都先轉化為字符串 strA 和 strB,但是這里很 tricky,轉換的方式不能是直接將整型數字轉為字符串,再連接起來,這樣會出錯,因為會導致一個整型數占據多位字符,所以這里是需要將每個整型數直接加入字符串,從而將該整型數當作 ASCII 碼來處理,尋找對應的字符,使得轉換后的 strA 和 strB 變成各種凌亂的怪異字符,不過不影響解題。這里的二分應該屬於博主之前的總結貼 LeetCode Binary Search Summary 二分搜索法小結 中的第四類,但是寫法上卻跟第三類的變形很像,因為博主平時的習慣是右邊界設置為開區間,所以初始化為 min(m, n)+1,當然博主之前就說過二分搜索的寫有各種各樣的,像這個帖子中寫法也是可以的。博主的這種寫法實際上是在找第一個不大於目標值的數,這里的目標值就是那個 helper 子函數,也就是驗證函數。如何實現這個驗證函數呢,由於是要找長度為 len 的子串是否同時存在於 strA 和 strB 中,可以用一個 HashSet 保存 strA 中所有長度為 len 的子串,然后遍歷 strB 中所有長度為 len 的子串,假如有任何一個在 HashSet 中存在,則直接返回 true,否則循環退出后,返回 false,參見代碼如下:
解法二:
class Solution { public: int findLength(vector<int>& A, vector<int>& B) { string strA = stringify(A), strB = stringify(B); int left = 0, right = min(A.size(), B.size()) + 1; while (left < right) { int mid = (left + right) / 2; if (helper(strA, strB, mid)) left = mid + 1; else right = mid; } return right - 1; } bool helper(string& strA, string& strB, int len) { unordered_set<string> st; for (int i = 0, j = len; j <= strA.size(); ++i, ++j) { st.insert(strA.substr(i, j - i)); } for (int i = 0, j = len; j <= strB.size(); ++i, ++j) { if (st.count(strB.substr(i, j - i))) return true; } return false; } string stringify(vector<int>& nums) { string res; for (int num : nums) res += num; return res; } };
對於這種求極值的問題,動態規划 Dynamic Programming 一直都是一個很好的選擇,這里使用一個二維的 DP 數組,其中 dp[i][j] 表示數組A的前i個數字和數組B的前j個數字的最長子數組的長度,如果 dp[i][j] 不為0,則A中第i個數組和B中第j個數字必須相等,比對於這兩個數組 [1,2,2] 和 [3,1,2],dp 數組為:
3 1 2
1 0 1 0
2 0 0 2
2 0 0 1
注意觀察,dp 值不為0的地方,都是當 A[i] == B[j] 的地方,而且還要加上左上方的 dp 值,即 dp[i-1][j-1],所以當前的 dp[i][j] 就等於 dp[i-1][j-1] + 1,而一旦 A[i] != B[j] 時,直接賦值為0,不用多想,因為子數組是要連續的,一旦不匹配了,就不能再增加長度了。每次算出一個 dp 值,都要用來更新結果 res,這樣就能得到最長相同子數組的長度了,參見代碼如下:
解法三:
class Solution { public: int findLength(vector<int>& A, vector<int>& B) { int res = 0, m = A.size(), n = B.size(); vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0)); for (int i = 1; i <= m; ++i) { for (int j = 1; j <= n; ++j) { dp[i][j] = (A[i - 1] == B[j - 1]) ? dp[i - 1][j - 1] + 1 : 0; res = max(res, dp[i][j]); } } return res; } };
Follow up:在開始時,博主提到了要跟最長相同子序列 Longest Common Subsequence 區分開來,雖然 LeetCode 沒有直接求最大相同子序列的題,但有幾道題利用到了求該問題的思想,比如 Delete Operation for Two Strings 和 Minimum ASCII Delete Sum for Two Strings 等,詳細討論請參見評論區一樓 :)
Github 同步地址:
https://github.com/grandyang/leetcode/issues/718
類似題目:
參考資料:
https://leetcode.com/problems/maximum-length-of-repeated-subarray/
