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没实现,有兴趣的同学们可以自己试下。

 

 

 

 
posted @ 2014-03-28 15:29  flowerkzj  阅读(173)  评论(0编辑  收藏  举报