这周完成的还是Dynamic Programming 部分的题目,这里选择word break系列的两道题目进行分析。
一、Word Break
题目
Given a non-empty string s and a dictionary wordDict containing a list of non-empty words, determine if s can be segmented into a space-separated sequence of one or more dictionary words. You may assume the dictionary does not contain duplicate words.
For example, given
s = “leetcode”,
dict = [“leet”, “code”].
Return true because “leetcode” can be segmented as “leet code”.
我的分析
这道题可以使用动态规划来求解,简单来说,假设dp[i]表示前i个元素组成的序列是否可以正确划分,那么对于一个序列abcdab,长度为6,那么当我们判断abcda都是合法可分符合字典内容时,对于第6个元素:
- 从第0到第5个元素,从右到左进行一个遍历,即最开始遍历ab是否存在于字典中且a以前的序列是否合法可分,如果满足条件,那么就说明abcdab是合法可分的,如果a不能满足这个条件,那么继续判断dab是否存在于字典中,且前面的序列abc是否可分,依次进行下去;
- 这里有一个巧妙的点:我们判断ab是否存在的时候,其实前面的序列abcd已经遍历过ab了,那么ab一定会存在于字典中
- 整个过程的状态转移方程可以写作 dp[i] = I(s[j, i]) & dp[j], j = 0, 1, 2, ……, n。其中I( ⋅ )表示如果 s[j,i] 存在于字典中,则返回1,否则返回0;
代码
结合分析的结果可得如下代码:
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
int len = s.size();
vector<bool> dp(len+1,false);
dp[0] = true;
for(int i = 1; i <= len; i++)
//从前一个元素开始,判断新加入的元素是否存在
for(int j = i-1; j >= 0; j--){
// 判断新加入的元素是否存在
string str = s.substr(j,i-j);
// 新加入的元素和之前的序列都存在时,说明整个序列都存在
if(dp[j] && find(wordDict.begin(),wordDict.end(),str) != wordDict.end()){
dp[i] = true;
break;
}
}
return dp[len];
}
};
二、Word Break II
题目
Given a non-empty string s and a dictionary wordDict containing a list of non-empty words, add spaces in s to construct a sentence where each word is a valid dictionary word. You may assume the dictionary does not contain duplicate words.
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”].
我的分析
这道题是上一道题的引申,可以在上一道题的基础上,结合DFS来求解。
- 首先,需要判断序列是否可分,在判断序列是否可分中,使用dp[i][j]来表示,以元素i结尾的序列中,可以在j位置划分的合法性,如果在j位置可以划分,那么dp[i][j]为true。例如对于catsand,有两种划分(cat/sand以及cats/and),那么第7行(d是第7个元素),第3列和第4列的值都是true,其他列元素都是false;
- 在得到不同长度的序列的合法性及划分位置后,可以根据这些信息划分数据加入空格:从序列的原始长度出发,先从后往前得到第一个可划分的单词,之后根据剪掉这个单词之后的剩余序列的划分位置继续做划分,简单来说,在找到一个合法单词后,会把划分这个单词之后的剩余划分情况遍历完毕,之后再往前遍历,这就是一个DFS的过程。
代码
结合上述分析可以得到如下代码:
class Solution {
public:
vector<string> wordBreak(string s, vector<string>& wordDict) {
vector<string> res;
if(s.length() == 0 || wordDict.empty()) return res;
// 判断以前i个元素中可以在那些位置划分得到符合条件的一个或多个单词
vector<vector<bool> >dp(s.length()+1, vector<bool>(s.length()));
dp_wordBreak_judge(s, wordDict, dp);
// 得到合法的序列的组合形式
vector<string> tmp;
dp_wordBreak(s, wordDict, res, tmp, dp, s.length());
return res;
}
void dp_wordBreak_judge(string s, vector<string> &wordDict, vector<vector<bool> >&dp){
vector<bool>tmp(s.size()+1,false);
tmp[0] = true;
for(int i = 1; i <= s.size(); i++){
//从前一个元素开始,判断新加入的元素是否存在
for(int j = i-1; j >= 0; j--){
// 判断新加入的元素是否存在
string str = s.substr(j,i-j);
// 新加入的元素和之前的序列都存在时,说明整个序列都存在
if(tmp[j] && find(wordDict.begin(),wordDict.end(),str) != wordDict.end()){
tmp[i] = true;
dp[i][j] = true;
}
}
}
}
void dp_wordBreak(string s, vector<string> &wordDict,
vector<string>&res,vector<string> tmp, vector<vector<bool> >&dp, int cur){
int len = s.length();
if(cur == 0){
string str=tmp[tmp.size()-1];
for(int idx = tmp.size()-2; idx >=0; idx--)
str += " "+tmp[idx];
res.push_back(str);
return ;
}
for(int i = 0; i < len; i++){
// 倒序遍历第一个单词
if(dp[cur][i] == true){
// 得到最后一个单词,继续析取前面的序列
tmp.push_back(s.substr(i,cur-i));
dp_wordBreak(s, wordDict, res, tmp, dp,i);
tmp.pop_back();
}
}
}
};