之前刷LeetCode。碰到了这道题。
一开始没想到要用动态规划来解。
后来看了一下答案给的代码,又仔细研究了一下,发现确实是动态规划。
中途看了很多其他人的博客解释,都没把动态规划讲清楚。
所以干脆就自己来写一份详解吧。
首先,把题目贴上来
sdicts
For example, given
s"leetcode"
,
dict["leet", "code"]
.
"leetcode"
"leet code"
.
简单来说,就是s能不能由dict中存储的单词拼出来。
注意了,这里有陷阱。不能简单的从前往后substr递增的检查是否包含在dict中,否则会有问题。
下面开始介绍怎么用动态规划来讲。
该说的都说了,接下来到实际问题里面去体会。
先贴出官网给出的标准答案
bool wordBreak(string s, unordered_set<string> &dict) { vector<bool> flag(s.length()+1,false); flag[0] = true; for (int i = 1; i < s.length()+1; ++i) { for (int j = i - 1; j >= 0; j--) { if (flag[j] && dict.count(s.substr(j, i-j)) != 0) { flag[i] = true; break; } } } return flag[s.length()]; }
简单讲一下这里是怎么利用动态规划的。
首先
vector<bool> flag(s.length()+1,false);
具体怎么做的等会再说。
这里的第一层 i 循环,其实就是我们刚才讲的1,也就是子问题。
你可以理解为 i 从1一直到s.length() ,分别表示从下标0开始,长度为1的子问题,长度为2的子问题,……,长度为s.length()的子问题。
而长度为s.length()的子问题的解,其实也就是原问题的解了。
现在再来解释flag数组。
if (flag[j] && dict.count(s.substr(j, i-j)) != 0) { flag[i] = true; break; }
并且有s.substr(j,i-1) 这个子串包含在 dict中。
这样,是不是就利用了前面 子问题i 的计算结果避免了重复计算 子问题i呢。
至此,整个动态规划的思想就讲完了。
我自己又写了一个递归版本的答案,如果你对上面的似懂非懂的话,可以参考一下我的答案。
vector<bool> *flag; bool fun(string s, unordered_set<string> &dict, int i) { if (i == 0 && (*flag)[0] == false) return dict.find(s) != dict.end(); while (i > 0 && (dict.find(s.substr(i, s.length() - i)) == dict.end()||(*flag)[i]==true)) { i--; } if (i == 0 && dict.find(s.substr(i, s.length() - i)) == dict.end()) return false; else if (i == 0) return true; (*flag)[i] = true; return fun(s.substr(0, i), dict, i-1) || fun(s, dict, i); } bool wordBreak(string s, unordered_set<string> &dict) { if (s.length() == 0) return false; vector<bool> mflag(s.length(), false); flag = &mflag; return fun(s, dict, s.length()-1); }
简单给点提示,因为要递归,所以flag 数组需要放在全局变量。
最简单的解释就是看它的return语句。返回两个递归的结果。分别表示从后往前 s.length()-i长度的这个子串是答案的解或者它不是答案的解。(有时候s的子串包含在dict中但不是答案的解,见上面的 aaa aaa a 这种分法)
是解,或者不是解,包含了所有可能的情况。因此分成了两种子问题。再加上全局变量flag避免重复计算。
因此满足了动态规划的所有步骤。得到的也是正确答案。