Given a list of words, each word consists of English lowercase letters.
Let's say word1
is a predecessor of word2
if and only if we can add exactly one letter anywhere in word1
to make it equal to word2
. For example, "abc"
is a predecessor of "abac"
.
A *word chain *is a sequence of words [word_1, word_2, ..., word_k]
with k >= 1
, where word_1
is a predecessor of word_2
, word_2
is a predecessor of word_3
, and so on.
Return the longest possible length of a word chain with words chosen from the given list of words
.
Example 1:
Input: words = ["a","b","ba","bca","bda","bdca"]
Output: 4
Explanation: One of the longest word chain is "a","ba","bda","bdca".
Example 2:
Input: words = ["xbc","pcxbcf","xb","cxbc","pcxbc"]
Output: 5
Constraints:
1 <= words.length <= 1000
1 <= words[i].length <= 16
words[i]
only consists of English lowercase letters.
這道題給了一個單詞數組,定義了一種前任關系,說是假如在 word1 中任意位置加上一個字符,能變成 word2 的話,那么 word1 就是 word2 的前任,實際上 word1 就是 word2 的一個子序列。現在問在整個數組中最長的前任鏈有多長,暴力搜索的話會有很多種情況,會產生大量的重復計算,所以會超時。這種玩數組求極值的題十有八九都是用動態規划 Dynamic Programming 來做的,這道題其實跟之前那道 Longest Arithmetic Subsequence 求最長的等差數列的思路是很像的。首先來定義 dp 數組,這里用一個一維的數組就行了,其中 dp[i] 表示 [0, i] 區間的單詞的最長的前任鏈。下面來推導狀態轉移方程,對於當前位置的單詞,需要遍歷前面所有的單詞,這里需要先給單詞按長度排個序,因為只有長度小1的單詞才有可能是前任,所以只需要遍歷之前所有長度正好小1的單詞,若是前任關系,則用其 dp 值加1來更新當前 dp 值即可。判斷前任關系可以放到一個子數組中來做,其實就是檢測是否是子序列,沒啥太大的難度,參見代碼如下:
解法一:
class Solution {
public:
int longestStrChain(vector<string>& words) {
int n = words.size(), res = 1;
sort(words.begin(), words.end(), [](string& a, string &b){
return a.size() < b.size();
});
vector<int> dp(n, 1);
for (int i = 1; i < n; ++i) {
for (int j = i - 1; j >= 0; --j) {
if (words[j].size() + 1 < words[i].size()) break;
if (words[j].size() == words[i].size()) continue;
if (helper(words[j], words[i])) {
dp[i] = max(dp[i], dp[j] + 1);
res = max(res, dp[i]);
}
}
}
return res;
}
bool helper(string word1, string word2) {
int m = word1.size(), n = word2.size(), i = 0;
for (int j = 0; j < n; ++j) {
if (word2[j] == word1[i]) ++i;
}
return i == m;
}
};
論壇上的高分解法在檢驗是否是前任時用了一種更好的方法,不是檢測子序列,而是將當前的單詞,按順序每次去掉一個字符,然后看剩下的字符串是否在之前出現過,是的話就說明有前任,用其 dp 值加1來更新當前 dp 值,這是一種更巧妙且簡便的方法。這里由於要快速判斷前任是否存在,所以不是用的 dp 數組,而是用了個 HashMap,對於每個遍歷到的單詞,按順序移除掉每個字符,若剩余的部分在 HashMap 中,則更新 dp 值和結果 res,參見代碼如下:
解法二:
class Solution {
public:
int longestStrChain(vector<string>& words) {
int n = words.size(), res = 1;
sort(words.begin(), words.end(), [](string& a, string& b){ return a.size() < b.size(); });
unordered_map<string, int> dp;
for (string word : words) {
dp[word] = 1;
for (int i = 0; i < word.size(); ++i) {
string pre = word.substr(0, i) + word.substr(i + 1);
if (dp.count(pre)) {
dp[word] = max(dp[word], dp[pre] + 1);
res = max(res, dp[word]);
}
}
}
return res;
}
};
Github 同步地址:
https://github.com/grandyang/leetcode/issues/1048
類似題目:
Longest Arithmetic Subsequence
參考資料:
https://leetcode.com/problems/longest-string-chain/
https://leetcode.com/problems/longest-string-chain/discuss/294890/JavaC%2B%2BPython-DP-Solution