zoukankan      html  css  js  c++  java
  • 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没实现,有兴趣的同学们可以自己试下。

     
     
  • 相关阅读:
    ubuntu右上角时间不显示
    树、森林和二叉树之间的转换(转)
    机器学习——支持向量机(SVM)之拉格朗日乘子法,KKT条件以及简化版SMO算法分析
    Java数据结构——平衡二叉树的平衡因子(转自牛客网)
    机器学习——梯度下降算法
    ubuntu下gedit闪退,遇到问题:ERROR:../../gi/pygi-argument.c:1583:_pygi_argument_to_object: code should not be reached 已放弃 (核心已转储)
    机器学习——Logistic回归
    机器学习——基于概率论的分类方法:朴素贝叶斯
    ubuntu安装simplejson模块
    Java数据结构——树的三种存储结构
  • 原文地址:https://www.cnblogs.com/flowerkzj/p/3630779.html
Copyright © 2011-2022 走看看