题目:
Given a string s and a dictionary of words dict, add spaces in s to construct a sentence where each word is a valid dictionary word.
Return all such possible sentences.
For example, given
s = "catsanddog"
,
dict = ["cat", "cats", "and", "sand", "dog"]
.
A solution is ["cats and dog", "cat sand dog"]
.
Answer 1: BF递归解 TLE
思路:每次维护一个当前结果集,然后遍历剩下的所有子串,如果子串在字典内出现,则保存一下结果。并放入下一层递归剩下的所有字符。
不过这种做法在Leetcode中会超时。
Error Code:
class Solution {
public:
vector<string> wordBreak(string s, unordered_set<string> &dict) {
vector<string> ret;
if(s.size() == 0) return ret;
wordBreak_helper(s, dict, 0, "", ret);
return ret;
}
private:
void wordBreak_helper(string s, unordered_set<string>& dict, int pos, string tmp, vector<string>& ret)
{
if(pos == s.size())
{
ret.push_back(tmp.substr(0, tmp.size()-1)); //去除空格
return;
}
for(int i = pos; i < s.size(); i++)
{
for(int j = 1; j < s.size()-i; j++)
{
string substr = s.substr(pos, j);
if(dict.find(substr) != dict.end())
{
wordBreak_helper(s, dict, i+1, tmp + " " + substr, ret);
}
}
}
}
};
Answer 2: 加入DP
思路:这道题,我们借鉴Word Break中的动态规划思路。不过这次我们需要记录每一个拆分结果,所以使用二维数组保存动态规划结果,二维数组形式vector<list<int>> dp, dp[i]表示字符串从坐标 i 开始的所有合法单词的终止位置列表。我们从字符串末尾开始,自底向上去求解动态规划数组,我们以当前stop为终止位置,循环所有合法的开始位置,如果子串在字典内,相应的在dp数组内添加元素。需要注意的是,如果从当前stop开始并无匹配的字典内单词(说明此处不可断),说明可以跳过这个stop做截止位置.
求解完所有可能的组合后,我们通过回溯法,去求解所有可能的分割情况。这次自顶向下求,从字符串头开始,遍历所有结果,直到到达字符串末尾。
Attention:
1. stop的含义,表示单词结束坐标的后一位,即下一个单词的首位或字符串length。所以stop从s.size()开始。
for(int stop = s.size(); stop >= 0; stop--)
2. 如果从当前stop开始并无任何字典内单词,可以跳过这个断点。
//如果从stop开始并无匹配的单词,可以跳过审查这个stop坐标的截止单词(stop表示单词的最后一个字母的后一位)
if(stop < s.size() && mark[stop].empty()) continue;
3. 从当前stop前一位开始,循环所有合法的开始位置,并更新动态规划数组。
/检查以当前stop截止,之前的所有在词典内的单词,并添加进mark中。
for(int start = stop - 1; start >= 0; start--)
{
string substr = s.substr(start, stop - start);
if(dict.find(substr) != dict.end())
{
mark[start].push_back(stop);
}
}
4. C++11的for的新用法。
for述句将允许简单的范围迭代:
int my_array[5] = {1, 2, 3, 4, 5}; for (int &x : my_array) { x *= 2; }
for述句的第一部份定义被用来做范围迭代的参数,就像被声明在一般for循环的参数一样,其作用域仅只于循环的范围。而在":"之后的第二区块,代表将被迭代的范围。 并且,循环变量是一个引用,可以在循环中改变它的值。
for(int& stop : mark[index])
程序中,这一句,就表示循环所有的以index开始的单词的stop位置。
5.回溯时,我们注意要维护当前的path,不能用添加子串的新串去替代path,因为这是个循环语句,我们下一个stop依然要使用原本的path(未添加子串).
for(int& stop : mark[index])
{
string substr = s.substr(index, stop - index);
string newpath = path + (index == 0 ? substr : " " + substr);
AC Code:
class Solution {
public:
vector<string> wordBreak(string s, unordered_set<string> &dict) {
//mark[i]存储了从i坐标开始,在字典内的单词的所有截止坐标。
vector<list<int>> mark(s.size(), list<int>());
//从字符串末尾至头
for(int stop = s.size(); stop >= 0; stop--)
{
//如果从stop开始并无匹配的单词,可以跳过审查这个stop坐标的截止单词(stop表示单词的最后一个字母的后一位)
if(stop < s.size() && mark[stop].empty()) continue;
//检查以当前stop截止,之前的所有在词典内的单词,并添加进mark中。
for(int start = stop - 1; start >= 0; start--)
{
string substr = s.substr(start, stop - start);
if(dict.find(substr) != dict.end())
{
mark[start].push_back(stop);
}
}
}
vector<string> ret;
generate(mark, 0, s, "", ret);
return ret;
}
private:
void generate(vector<list<int>> mark, int index, const string& s, string path, vector<string>& ret)
{
for(int& stop : mark[index])
{
string substr = s.substr(index, stop - index);
string newpath = path + (index == 0 ? substr : " " + substr);
if(stop == s.size())
ret.push_back(newpath);
else
generate(mark, stop, s, newpath, ret);
}
}
};
这道题综合性很强,将DP和Backtracking结合起来,难度较大,尤其是建立dp数组时的方法可以重点借鉴。