Leetcode OJ: Word Break I/II
又是专题二重奏的节奏。这回的题目是word break,其实就是自然语言处理中的分词,不过说实话,从自然语言处理课学来的方法是前向最大匹配或者是后向最大匹配,又或者是两者结合啊,像这个貌似不一定按着语言规则走的,所以不能用自然语言处理中通过统计的方法去进行剪技,那怎么办呢?
Given a string s and a dictionary of words dict, determine if s can be segmented into a space-separated sequence of one or more dictionary words.
For example, given
s ="leetcode"
,
dict =["leet", "code"]
.Return true because
"leetcode"
can be segmented as"leet code"
.
第一反应,递归实现的穷举。果不其然,Time Limit Exceed。贴下懒人的代码:
1 class Solution { 2 public: 3 bool wordBreak(string s, unordered_set<string> &dict) { 4 string::iterator is = s.begin(); 5 int count = 0; 6 while (is != s.end()) { 7 is++; 8 count++; 9 if (dict.count(s.substr(0, count)) > 0 && wordBreak(s.substr(count), dict)) 10 return true; 11 } 12 return false; 13 } 14 };
然后,想想自然语言处理课都讲过什么了,发现当年貌似是要统计字典最大长度什么的,这样比较就可以少一些,于是就在前面加了段统计单词最大长度的代码,然后满心欢喜地submit了,然后了果断又TLE了。再贴下代码:
1 class Solution { 2 public: 3 bool wordBreak(string s, unordered_set<string> &dict) { 4 string::iterator is = s.begin(); 5 int count = 0; 6 int maxSize = 0; 7 for (auto it = dict.begin(); it != dict.end(); ++it) { 8 if ((*it).size() > maxSize) 9 maxSize = (*it).size(); 10 } 11 while (is != s.end() && count < maxSize) { 12 is++; 13 count++; 14 if (dict.count(s.substr(0, count)) > 0 && wordBreak(s.substr(count), dict)) 15 return true; 16 } 17 return false; 18 } 19 };
LZ痛定思痛,休息了两天,然后跟同学讨论了下这题,是不是可以用动态规划的方法剪技?嗯~ 终于有思路了。
LZ从数组尾部开始比较啊,其实从头部也是可以的,思路差不多。
我们先定义一个数组flag[0..n]用于表示以位置i (0..n-1)为起点,到结尾是否可达,即能成功分词。
若我们从尾部开始考虑,则当在位置i时,i后存在一个j,满足
1) i, j间的子串在字典中
2) 位置j到结尾是可达的
那么,在位置i点则可达,否则不可达,即
flag[i] = exists(dict.exists(substr(i, j)) && flag[j] ),其中i < j <= i + maxSize
从尾部一直记录到头部的时候就知道头部是否可达了~
代码如下:
1 class Solution { 2 public: 3 bool wordBreak(string s, unordered_set<string> &dict) { 4 if (s.empty() || dict.empty()) 5 return false; 6 int maxSize = 0; 7 for (auto it = dict.begin(); it != dict.end(); ++it) { 8 if ((*it).size() > maxSize) 9 maxSize = (*it).size(); 10 } 11 int size = s.size(); 12 // 注意是size+1的长度,最后一个设为true 13 vector<bool> flag(size + 1, false); 14 flag[size] = true; 15 16 for (int i = size - 1; i >= 0; --i) { 17 bool tmp = false; 18 for (int j = 1; j <= maxSize && i + j <= size; ++j) { 19 // 有些小trick,先判断可达,再判断是否在字典中,会快些 20 if (flag[i + j] && dict.find(s.substr(i, j)) != dict.end()) { 21 tmp = true; 22 break; 23 } 24 } 25 flag[i] = tmp; 26 } 27 28 return flag[0]; 29 } 30 };
时间复杂度为O(maxSize * n),空间复杂度为n
这还仅仅是判断是否存在,那如果是需要找出所有路径呢?这才是我们分词的最终目的啊。
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"]
.
有了上面的经验,当然做起来就容易很多了,从原来的存是否可达变成存放路径就可以了~ 直接上代码吧~
1 class Solution { 2 public: 3 vector<string> wordBreak(string s, unordered_set<string> &dict) { 4 if (s.empty() || dict.empty()) 5 return vector<string>(); 6 int maxSize = 0; 7 for (auto id = dict.begin(); id != dict.end(); ++id) { 8 if ((*id).size() > maxSize) 9 maxSize = (*id).size(); 10 } 11 12 int size = s.size(); 13 // 存之前的组合,最后一个设为空串 14 vector< vector<string> > ret(size + 1, vector<string>()); 15 ret[size] = vector<string>(1, ""); 16 17 for (int i = size - 1; i >= 0; --i) { 18 for (int j = 1; j <= maxSize && i + j <= size; ++j) { 19 string sub = s.substr(i, j); 20 if (!ret[i + j].empty() && dict.find(sub) != dict.end()) { 21 for (vector<string>::iterator isv = ret[i + j].begin(); 22 isv != ret[i + j].end(); ++isv) 23 { 24 // 处理非空串时 25 if (!(*isv).empty()) 26 ret[i].push_back(sub + " " + (*isv)); 27 else // 空串时也需要push 28 ret[i].push_back(sub); 29 } 30 } 31 } 32 } 33 return ret[0]; 34 } 35 };
这里就A了,但这里其实是有问题的,时间复杂度是O(maxSize*n),空间复杂度是O(n!)。
当我们的字符串很长的时候,如果我们把一路的组合都记录下来了,那会占用很大的内存,有没有优化的方法?
这里LZ想到的方法是用循环队列。我们发现当前位置i,进行比较的范围最大只到其前面的maxSize个偏移的位置,再后面的是不需要比较的。
那么用一个定长的循环队列就能很好地完成这个事情了,空间复杂度也能降不少。代码如下:
1 class Solution { 2 public: 3 vector<string> wordBreak(string s, unordered_set<string> &dict) { 4 if (s.empty() || dict.empty()) 5 return vector<string>(); 6 int maxSize = 0; 7 for (auto id = dict.begin(); id != dict.end(); ++id) { 8 if ((*id).size() > maxSize) 9 maxSize = (*id).size(); 10 } 11 12 int size = s.size(); 13 // 大小只有maxSize 14 vector< vector<string> > ret(maxSize, vector<string>()); 15 // 从第0个开始 16 ret[0] = vector<string>(1, ""); 17 int pos = 0; 18 for (int i = size - 1; i >= 0; --i) { 19 // 存i位置的组合 20 vector<string> tmp; 21 for (int j = 1; j <= maxSize && i + j <= size; ++j) { 22 string sub = s.substr(i, j); 23 // 简易的循环队列 24 int cur = (pos + j - 1) % maxSize; 25 if (!ret[cur].empty() && dict.find(sub) != dict.end()) { 26 for (vector<string>::iterator isv = ret[cur].begin(); 27 isv != ret[cur].end(); ++isv) 28 { 29 if (!(*isv).empty()) 30 tmp.push_back(sub + " " + (*isv)); 31 else 32 tmp.push_back(sub); 33 } 34 } 35 } 36 // 把得到的组合置于开始 37 if (--pos < 0) { 38 pos += maxSize; 39 } 40 ret[pos] = tmp; 41 } 42 return ret[pos]; 43 } 44 };
总的来说,空间占用是有所减少的,但还是不少。跟同学再讨论了一翻,提供了个方案:
1. 利用以上word break I中的flag数组,再存一个用于表示每一个位置上可达的话需要向前移的步数step数组
2. 利用step数组,进行深搜,通过回溯的方法得出最后的路径。
这一方案LZ没实现,有兴趣的同学们可以自己试下。