题目I
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。
分析
- 本题和322题的零钱兑换很类似,即字典中的单词看做是硬币,将字符串s看作是要凑的硬币总数,可以选择字典中的0个或者是多个硬币
- 定义状态dp[i]表示从左到右长度为i的字符串是否可以被正确拆分
- dp[i] = anyof(dp[j] && s[j+1, i])
- 为加快在wordDict的检索速度,则将wordDict中的元素都放入unordered_set中
代码
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict){
//本题和322题的零钱兑换很类似,即字典中的单词看做是硬币,将字符串s看作是要凑的硬币总数,可以选择字典中的0个或者是多个硬币
//定义状态dp[i]表示以s[i]结尾的字符串是否可以被正确拆分
//dp[i] = anyof(dp[j] && s[j+1, i])
//为加快在wordDict的检索速度,则将wordDict中的元素都放入unordered_set中
unordered_set<string> dic(wordDict.begin(), wordDict.end());
vector<bool> dp(s.size()+1, false);
dp[0] = true;
for(int i=1;i<=s.size();i++){//1表示字符串的长度为1, 可以把这个长度当做是兑换硬币问题的要被兑换的币值
for(int j=i-1;j>=0;j--){//j表示回退指针
if(dp[j] && dic.find(s.substr(j, i-j))!=dic.end()){
dp[i] = true;
break;
}
}
}
return dp[s.size()];
}
};
题目II
给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,在字符串中增加空格来构建一个句子,使得句子中所有的单词都在词典中。返回所有这些可能的句子。
说明:
分隔时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:
输入:
s = “catsanddog”
wordDict = [“cat”, “cats”, “and”, “sand”, “dog”]
输出:
[
“cats and dog”,
“cat sand dog”
]
分析
本题相对于上一题就是要求出解空间,并求出所有的解。 主要的方法就是深度搜索+DP动态规划。对于求解空间即满足条件的所有的解一般就是使用深度搜索。所有任意长度的前缀是否可拆分是知道的(可以通过在上述动态规划问题中进行存储下来),那么如果后缀子串在单词集合中,这个后缀子串就是解的一部分。因此可以画出递归树如下:
注意从叶子结点到根节点的一条路径就是一个解,但是对于递归过程来说,如果我们以path来存储从根结点到叶子结点的路径上的单词,那么最终将路径上的单词拼接成字符串的时候,就要进行翻转一下,或者是在path进行收集的时候就每次在path头部进行插入。
class Solution {
public:
vector<string> wordBreak(string s, vector<string>& wordDict) {
//定义状态dp[i]表示以s[i]结尾的字符串是否可以被正确拆分
//dp[i] = anyof(dp[j] && s[j+1, i])
//为加快在wordDict的检索速度,则将wordDict中的元素都放入unordered_set中
vector<string> res;
unordered_set<string> dic(wordDict.begin(), wordDict.end());
vector<bool> dp(s.size()+1, false);
dp[0] = true;
vector<vector<bool> > flag(s.size()+1, vector<bool>(s.size(), false));
for(int i=1;i<=s.size();i++){//1表示字符串的长度为1, 可以把这个长度当做是兑换硬币问题的要被兑换的币值
for(int j=i-1;j>=0;j--){//j表示回退指针
if(dp[j] && dic.find(s.substr(j, i-j))!=dic.end()){
dp[i] = true;
flag[i][j] = true;// flag[i][j]表示的是s[j, i)是否可以被正确分割
}
}
}
vector<string> path;
dfs(s, wordDict, flag, path, res, s.size());
return res;
}
void dfs(string s, vector<string>& wordDict, vector<vector<bool> >& flag, vector<string>& path, vector<string>& res, int end) {
if(end==0){
string ans = "";
for(auto it = path.crbegin();it!=path.crend();it++){
ans += *it + " ";
}
ans.erase(ans.end()-1);
res.push_back(ans);
}
for(int i=end-1;i>=0;i--){
if(flag[end][i]){
path.push_back(s.substr(i, end-i));
dfs(s, wordDict,flag, path, res, i);
path.pop_back();
}
}
}
};
也可以正着进行求解路径,但是却超时了。主要的思想就是类似分割回文串。
class Solution {
public:
vector<string> wordBreak(string s, vector<string>& wordDict) {
//定义状态dp[i]表示以s[i]结尾的字符串是否可以被正确拆分
//dp[i] = anyof(dp[j] && s[j+1, i])
//为加快在wordDict的检索速度,则将wordDict中的元素都放入unordered_set中
vector<string> res;
unordered_set<string> dic(wordDict.begin(), wordDict.end());
vector<bool> dp(s.size()+1, false);
dp[0] = true;
vector<vector<bool> > flag(s.size()+1, vector<bool>(s.size(), false));
for(int i=1;i<=s.size();i++){//1表示字符串的长度为1, 可以把这个长度当做是兑换硬币问题的要被兑换的币值
for(int j=i-1;j>=0;j--){//j表示回退指针
if(dp[j] && dic.find(s.substr(j, i-j))!=dic.end()){
dp[i] = true;
flag[i][j] = true;// flag[i][j]表示的是s[j, i)是否可以被正确分割
}
}
}
vector<string> path;
dfs(s, wordDict, flag, path, res, 0);
return res;
}
void dfs(string s, vector<string>& wordDict, vector<vector<bool> >& flag, vector<string>& path, vector<string>& res, int start) {
if(start>=s.size()){
string ans = "";
for(auto it = path.begin();it!=path.end();it++){
ans += *it + " ";
}
ans.erase(ans.end()-1);
res.push_back(ans);
}
for(int i=start;i<s.size();i++){
if(flag[i+1][start]){
path.push_back(s.substr(start, i-start+1));//之前是end固定,现在是start固定
dfs(s, wordDict,flag, path, res, i+1);
path.pop_back();
}
}
}
};