算法设计与分析第九周——动态规划之Word Break II
这周我们继续来看动态规划,上周的题目比较简单,目的是为了让自己更好地理解动态规划和知道如何构建简单的动态规划状态转变方程,所以这周选了一道相对难一点的题目 -> 题目链接。
Word Break II 其实是上周做的Word Break的延伸题目,我这里需要用到上周的算法过程。
题目详情
题目跟Word Break类似,给出一个非空字符串s和含有多个字符串的字典集合WordDict,找出s被分为由字典集合里的词语组成的所有句子,若不存则返回空集合,词语可以重复,且字典里的词语不重复。
样例说明:
输入s | 输入字典WordDict | 输出 |
| {“cat”,“cats”,“sand”,“and”,“dog”} | { “cats and dog”, “cat sand dog” } |
|
| { “pine apple pen apple”, |
| {“cats”, “dog”, “sand”, “and”, “cat”} | { } |
题目分析及算法设计
先来分析一下返回空集合的情况,由上周得到的 dp 数组(详情请转至Word Break)可知,如果s可被拆分,则 dp[s.size()] 必然为真,而之前的函数就是判断 s 是否可被拆分的,故只要把输入 s 和 wordDict 传入判断函数,如果判断函数返回值为假,那么就可以返回空集。
其他情况通过从后遍历 s 字符串,找出所有能够在wordDict中找到的子串,并把他们从前往后通过空格连接起来即可,构建动态规划状态转换方程如下:dp[ i ] = currSubStr + dp[ j ],其中 dp 为字符数组的数组,dp[ j ] 为上一次从s的尾部往前找到符合子串并连接之后的字符串集合,currSubStr 为当前找到的在 wordDictt 中存在的 s 的子串,dp[ i ] 为当前遍历到位置处所有满足条件的字符串的集合。状态转换的条件为 dp[ j ] 不为空,即内部有已经找到的子串的连接而成的字符串。dp[ s.size() ] 初始化为含有一个空字符串的集合。
算法包含两个循环,外循环为从s的尾部向前遍历,内循环为从外循环遍历到的位置往后遍历,以找到从 s 尾部往前的所有在wordDict里的子串,如果 dp[ j ] 不为空,那么对 dp[ j ]内的所有字符串,我们都用当前获取到的子串去连接并把他们放入 dp[ i ] 中,到最后当最外层循环遍历完毕,dp[ 0 ] 内的结果即为所求。
代码详情
bool canBreak(string s, vector<string>& wordDict) {
// dp[j] == true means s[i, j] is in the wordDict, 0 <= i < j
// dp[j] == true if dp[i] && s[i, j] is in the wordDict
bool dp[s.size() + 1];
memset(dp, false, s.size() + 1);
dp[0] = true;
set<string> dict(wordDict.begin(), wordDict.end());
for (int j = 1; j <= s.size(); j ++) {
for (int i = j - 1; i >= 0; i --) {
if (dp[i] && dict.find(s.substr(i, j - i)) != dict.end()) {
dp[j] = true;
break;
}
}
}
return dp[s.size()];
}
vector<string> wordBreak(string s, vector<string>& wordDict) {
if (!canBreak(s, wordDict)) return vector<string> ();
int len = s.size();
// dp[i] = currStr + dp[j], traverse dp from the end of s,
// dp[j] means there are some strings that can be find in the wordDict
vector<vector<string>> dp(len + 1, vector<string>());
dp[len].push_back("");
set<string> dict(wordDict.begin(), wordDict.end());
// two loops for traverse
for (int i = len - 1; i >= 0; i --) {
for (int j = i + 1; j <= len; j ++) {
string currStr = s.substr(i, j - i);
// if currStr is in the wordDict, test it
if (dict.find(currStr) != dict.end()) {
if (!dp[j].empty()) {
for (auto word : dp[j]) {
string tmp = currStr;
if (word.size()) {
// dp[i] = currStr + dp[j]
tmp = tmp + " " + word;
}
dp[i].push_back(tmp);
}
}
}
}
}
return dp[0];
}
下面使用一个具体例子来说明:输入 s 为 “catsanddog”,wordDict为 {“cat”,“cats”,“sand”,“and”,“dog”}
i | dp[ i ] | 说明 |
10 | { “” } | 当 i 从s尾部遍历,dp[10] 初始化为空字符串集合 |
7 | { “dog” } | i 遍历到7时,因为 j 从8往后遍历,当就= 10,找到一个在wordDict的s的子串“dog”,又因为 dp[10]不为空,把“dog”加入 dp[7] 中 |
4 | { “and dog” } | i 继续往前遍历,i = 4时,j 从5开始往后遍历,当 j = 7,找到“and”,又 dp[7] 不为空,故把“and dog”加入 dp[4] 中 |
3 | { “sand dog” } | i = 3时,j 从4开始遍历,当 j = 7,找到“sand”,故把“sand” + “ ” + dp[7] 的结果加入 dp[3] |
0 | { “cat sand dog”, } | i = 0时,j 从1开始往后遍历,当 j = 3,检测到“cat”,又dp[3] 不为空,故把“cat” + “ ” + dp[3] 的结果加入 dp[0],同理当 j = 4 时也一样 |
复杂度分析
求s能否被拆分复杂度为 O(n^2),后面的求拆分成的字符串复杂度为O(n^2 * d),其中 d 为 wordDict 中词语的个数。故总的复杂度为O(n^2 * d)。
谢谢阅读。