动态规划
注意拆分时可以重复使用字典中的单词
首先考虑状态和选择,注意题目的问法:判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。所以只要能用字典中的单词把字符串拼出来就可以了,不限制使用字典中的多少个单词,也不限制每个单词使用的次数,只要能拼出来就行。
所以状态就是一个:当前的子串[0-i]能不能被拼出来。至于选择,所有单词表中的单词没有任何限选条件,所以可以不考虑选择。
然后定义状态dp[i]表示s的子串[0, i]能不能被单词表拼出来
再然后就是重点:思考状态转移。状态转移的思考过程通常都是分类讨论,这种true/false问题的典型转移公式就是:
参考:希望用一种规律搞定背包问题 - 组合总和 Ⅳ - 力扣(LeetCode)
dp[i] = dp[i] or dp[i-num]
在这一题中,举个例子:
输入: s = "leetcode", wordDict = ["leet", "code"]
假设现在正在考虑dp[i], i = 7,也就是i指向最后一个字符 ‘e’,我们需要判断s 的[0, 7]的子串能否被单词表拼出来,我们就需要枚举 i 之前的位置:
如果在 i 之前存在一个 j ,使得dp[j] = true([0,j]的子串可以被拼出来),并且[j, i]
之间的子串刚好是某个单词表中的单词,那么就说明 [0, i]的子串可以被拼出来。
那我们枚举就能知道存在 i = 7之前的一个 j = 3,这是[0, j]的子串 "leet"可以被拼出来,而且[j, i] 的子串 “code” 刚好在单词表中,那么 dp[i] = true。
把上面的转移逻辑翻译成公式就是:
dp[i] = dp[j] &&“子串[j,i]在单词表中”,(0 <= j < i),至于判断子串是否在单词表中,使用哈希表就可以了。
最后还需要考虑状态的初始化,一般为了方便边界的处理我们会定义dp数组比s.size()多出一个位置,也就是:
vector<int> dp(s.size()+1, 0);
dp[0] = 1;
这儿我们用0代表false, 1代表true。第0个位置初始化为1是合理的,这对应的子串不就是空串吗,空串能够用单词表拼出来吗?也是可以的,一个都不选就可以了。其他位置都初始化为0,代表false,没有计算之前当然应该是false,代表不能拼出来。
所以代码就有了(字符的对应关系还是画画图更好理解):
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordset(wordDict.begin(), wordDict.end());
//dp[i]表示s的[0,i]子串能不能由单词表组成
vector<int> dp(s.size()+1, 0);
dp[0] = 1;//空串
for(int i = 1; i <= s.size(); ++i){
for(int j = i-1; j >= 0; --j){//枚举i之前的所有位置
if(dp[j] && wordset.count(s.substr(j, i-j))) dp[i] = 1;
}
}
//for(auto c : dp) cout << c << " ";
return dp[s.size()];
}
};
那我们还可以进行一步优化,枚举 i之前的位置的时候,如果找到了一个符合要求的位置可以使得dp[i] = 1 ,那么就可以break出来,因为只要有一个位置可达就可以了,毕竟是true/false问题:
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordset(wordDict.begin(), wordDict.end());
//dp[i]表示s的[0,i]子串能不能由单词表组成
vector<int> dp(s.size()+1, 0);
dp[0] = 1;//空串
for(int i = 1; i <= s.size(); ++i){
for(int j = i-1; j >= 0; --j){//枚举i之前的所有位置
if(dp[j] && wordset.count(s.substr(j, i-j))){
dp[i] = 1;
break;
}
}
}
//for(auto c : dp) cout << c << " ";
return dp[s.size()];
}
};
如果还需要优化,其实也还是可以的,我们其实没有必要枚举i之前的所有位置,还是之前的例子:
输入: s = "leetcode", wordDict = ["leet", "code"]
假设现在正在考虑dp[i], i = 7,也就是i指向最后一个字符 ‘e’,我们需要判断s 的[0, 7]的子串能否被单词表拼出来,我们就需要枚举 i 之前的位置:
假设我们正在枚举i的前一个位置j = i-1,事实上我们都知道这个位置可以直接跳过:因为j和i之间就相差一个位置,子串就只有一个字符,而单词表里面显然不存在长度为1的单词。
所以我们只需要枚举那些可以使得子串长度可能是单词表中某个单词长度的那些位置j就可以了,那单词表长度可以用一个set进行保存即可,或者使用pair结构。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordset(wordDict.begin(), wordDict.end());
unordered_set<int> lenset;
for(const auto &w : wordDict) lenset.insert(w.size());
//dp[i]表示s的[0,i]子串能不能由单词表组成
vector<int> dp(s.size()+1, 0);
dp[0] = 1;//空串
for(int i = 1; i <= s.size(); ++i){
for(int j = i-1; j >= 0; --j){//枚举i之前的所有位置
if(lenset.count(i-j) == 0) continue;//不存在这个长度,直接跳过
if(dp[j] && wordset.count(s.substr(j, i-j))){
dp[i] = 1;
break;
}
}
}
//for(auto c : dp) cout << c << " ";
return dp[s.size()];
}
};
或者直接遍历长度:
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordset(wordDict.begin(), wordDict.end());
unordered_set<int> lenset;
for(const auto &w : wordDict) lenset.insert(w.size());
//dp[i]表示s的[0,i]子串能不能由单词表组成
vector<int> dp(s.size()+1, 0);
dp[0] = 1;//空串
for(int i = 1; i <= s.size(); ++i){
for(const auto &j : lenset){//遍历长度
if(i >= j && dp[i-j] && wordset.count(s.substr(i-j, j))){
dp[i] = 1;
break;
}
}
}
//for(auto c : dp) cout << c << " ";
return dp[s.size()];
}
};