您的位置:首页 > 编程语言 > C语言/C++

[C++]LeetCode: 113 Word Break II (DP && Backtacking) 求解拆分组合

2015-01-20 19:19 513 查看
题目:

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数组时的方法可以重点借鉴。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: