https://leetcode-cn.com/problems/word-break/
解法一、回溯
依照惯例,没有好的思路的时候,就先想想暴力解法怎么做。暴力解法无非是逐个单词地拆分字符串,每次选的单词可以重复。要实现暴力解法的话,肯定就是用回溯,因为回溯算法“撤销选择”的特点,保证了每次都能考虑到所有的单词。
简而言之,对于字符串s和单词word,word长度为n,当word与s[:n]相同时,就可以把word选来进行一次拆分,紧接着继续判断s[n:]是否能被拆分。当s被拆分到长度为0时,回溯算法就找到了答案。
回溯算法是一种穷举法,其时间复杂度自然就会比较高,要使其变得高效的话,采用剪枝是必需的。本题中我们如何剪枝呢?剪枝可以是剪掉那些一定不会得到解的分支,这我们在判断word == s[:n]时就进行了;剪枝也可以是剪掉重复的分支,我们尚未进行。考虑对于s = "leetcode"
和单词leet, code, 'le', 'et'
,选用leet
后会进行backtrack("code")
,但当我们依次选用'le'
和'et'
后,又会进行backtrack("code")
,这就存在了重复的分支(因为字典中的单词可以重复使用,即选择列表始终是相同的,只有要判断的s不同)。
要实现这种剪枝也容易,判断backtrack("s")
是否被执行过即可。
代码
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> set;
backtrack(s, wordDict, set);
return result;
}
private:
bool result = false;
void backtrack(string& s, vector<string>& wordDict, unordered_set<string>& set) {
if (s.size() == 0) {
result = true;
return;
}
for (auto& word : wordDict) {
int n = word.size();
string s1 = s.substr(0, n);
if (s1 == word) {
s = s.substr(n);
if (!set.count(s)) {
backtrack(s, wordDict, set);
set.insert(s);
}
s = s1 + s;
}
}
}
};
解法二、动态规划
动态规划解法要难想一点,但找到状态的表示后实现起来也不难了。
定义dp[i]
:s的前i个字符组成的子串是否能被字典中的单词拆分
状态转移方程:对于长度为m的word,如果s[:i]最后长度为m的子串等于word,那么dp[i]自然就由dp[i - m]决定
初始状态:dp[0] = true
,空串可以被拆分,其余的dp[i] = false
代码
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
int n = s.size();
bool dp[n + 1];
memset(dp, false, sizeof(dp));
dp[0] = true;
for (int i = 1; i <= n; ++i) {
for (auto& word : wordDict) {
int m = word.size();
if (i >= m && s.substr(i - m, m) == word) dp[i] = dp[i] | dp[i - m];
}
}
return dp[n];
}
};
复杂度分析
时间复杂度 O ( n m ) O(nm) O(nm),n为字符串s长度,m为wordDict大小。空间复杂度 O ( n ) O(n) O(n)。