【问题标题】:Need suggestion to improve speed for word break (dynamic programming)需要提高分词速度的建议(动态编程)
【发布时间】:2015-05-12 13:40:43
【问题描述】:

问题是:给定一个字符串 s 和一个单词字典 dict,确定 s 是否可以分割成一个或多个字典单词的空格分隔序列。

例如,给定 s = "这里", dict = ["hi", "there"].

返回true,因为“hithere”可以被分割为“leet code”。

我的实现如下。此代码适用于正常情况。但是,它会因以下输入而受到很大影响:

s = "aaaaaaaaaaaaaaaaaaaaaaab", dict = {"aa", "aaaaaa", "aaaaaaaa"}。

我想记住处理过的子字符串,但是我做错了。关于如何改进的任何建议?非常感谢!

class Solution {
public:
    bool wordBreak(string s, unordered_set<string>& wordDict) {
        int len = s.size();
        if(len<1) return true;
        for(int i(0); i<len; i++) {
            string tmp = s.substr(0, i+1);
            if((wordDict.find(tmp)!=wordDict.end()) 
               && (wordBreak(s.substr(i+1), wordDict)) )
                return true;
        }
        return false;
    }
};

【问题讨论】:

  • 您没有使用动态规划。您已经实现了天真的蛮力解决方案。
  • 请注意,无论具体实现如何,您始终可以通过消除复合词来修剪字典。由于aaaaaa 是复合词aa-aa-aaaaaaaaaa 也是复合词,您可以将字典缩减为仅aa。在一个非常聪明的方法中,您可以动态发现这样的复合词(即当复合词第一次出现在输入字符串中时)。

标签: c++ dynamic-programming word-break


【解决方案1】:

从逻辑上讲,这是一个两步过程。查找输入中的所有字典单词,考虑找到的位置(开始/结束对),然后查看这些单词是否覆盖整个输入。

所以你会得到你的例子

aa:       {0,2}, {1,3}, {2,4}, ... {20,22}
aaaaaa:   {0,6}, {1,7}, ... {16,22}
aaaaaaaa: {0,8}, {1,9} ... {14,22}

这是一个图,有节点 0-23 和一堆边。但是节点 23 b 完全无法访问 - 没有传入边缘。现在这是一个简单的图论问题

如果您的字典以 trie 形式组织,则查找字典单词出现的所有位置非常容易。但即使是std::map 也是可用的,这要归功于它的equal_range 方法。对于开始和结束位置,您似乎有一个 O(N*N) 嵌套循环,每个单词都有 O(log N) 查找。但是您可以快速确定s.substr(begin,end) 是否仍然是一个可行的前缀,以及该前缀保留了哪些字典单词。

还请注意,您可以懒惰地构建图表。盯着begin=0 你会发现边缘{0,2}, {0,6} and {0,8}。 (没有其他人)。您现在可以搜索节点 2、6 和 8。您甚至有一个很好的算法 - A* - 建议您先尝试节点 8(仅在 1 个边缘即可到达)。因此,您会发现节点 {8,10}{8,14}{8,16} 等。如您所见,您永远不需要构建包含 {1,3} 的图形部分,因为它根本无法访问。

使用图论,很容易看出您的蛮力方法失败的原因。您反复到达节点 8 (aaaaaaaa.aaaaaaaaaaaaaab),每次都从那里搜索子图。

进一步的优化是运行双向 A*。这会给你一个非常快速的解决方案。在第一步的后半部分,您寻找通往23, b 的边。由于不存在,您立即知道节点{23} 是隔离的。

【讨论】:

  • 感谢您的解释。我看到懒惰地构建图的方法更像是图中的 BFS。我可以用动态规划做 DFS 吗?哪个更好?
  • 我不确定这是否是正式的“动态编程”; BFS 和 DFS 是图遍历算法。我不确定您是否使用 DFS 获胜,因为您需要递归地进行字典遍历。
【解决方案2】:

在您的代码中,您没有使用动态编程,因为您不记得已解决的子问题。

您可以启用这种记忆功能,例如,通过基于字符串 s 在原始字符串中的起始位置存储结果,甚至基于其长度(因为无论如何您正在使用的字符串都是后缀原始字符串,因此它的长度唯一地标识它)。然后,在您的 wordBreak 函数的开头,只需检查是否已经处理了这样的长度,如果已经处理,则不要重新运行计算,只需返回存储的值。否则,运行计算并存储结果。

另请注意,您使用unordered_set 的方法不会让您获得最快的解决方案。我能想到的最快解决方案是 O(N^2),通过将所有单词存储在一个 trie 中(而不是在地图中!)并在沿着给定字符串行走时遵循这个 trie。这在不计算递归调用的情况下实现了每次循环迭代 O(1)。

【讨论】:

  • 一旦你记住结果,我认为字符串副本并不重要。请记住,无论如何您都需要创建这些字符串才能将它们用作std::map 键。
  • 你说得对,我需要以某种方式存储以前的结果。但是,当我的方法试图找到匹配项时,然后从输入字符串中删除匹配部分并继续对较小的输入进行测试。我无法按结束位置存储已处理的部分,但存储整个子字符串的成本很高......
  • @MSalters,最快的解决方案还需要将字符串存储在 trie 中,而不是 std::map。在这种情况下,您可以沿着字符串运行,将当前位置保留在 trie 中,并使用O(1) 进行循环迭代,使用O(N^2) 进行整个解决方案。当前的方法是O(N^3) 最好的,因为在每次迭代中都需要O(N) 复制和/或查找。我会更新评论以显示这一点。
  • @Sarah,我已经更新了答案,请检查一下。您可以按起始位置存储字符串,甚至可以按长度存储。
  • @MSalters,在我之前的评论中,我混合了来自wordDict 的单词和已完成记忆的字符串。所以有点更新版本:为了记忆,你实际上不需要std::map,因为字符串长度足以作为动态编程键。就单词而言,trie 将是一个更好的结构,因为它允许每次循环迭代 O(1)。
【解决方案3】:

感谢所有 cmets。我将以前的解决方案更改为下面的实现。在这一点上,我没有探索对字典进行优化,但这些见解非常有价值,非常感谢。

对于目前的实现,您认为还可以进一步改进吗?谢谢!

class Solution {
public:
    bool wordBreak(string s, unordered_set<string>& wordDict) {
        int len = s.size();
        if(len<1) return true;
        if(wordDict.size()==0) return false;

        vector<bool> dq (len+1,false);
        dq[0] = true;
        for(int i(0); i<len; i++) {// start point
            if(dq[i]) {
                for(int j(1); j<=len-i; j++) {// length of substring, 1:len
                    if(!dq[i+j]) {
                        auto pos = wordDict.find(s.substr(i, j));
                        dq[i+j] = dq[i+j] || (pos!=wordDict.end());
                    }
                }
            }
            if(dq[len]) return true;
        }
        return false;
    }
};

【讨论】:

  • “能否进一步改进”不是一个好问题,它太宽泛了。此外,您将此作为答案发布,因此不会有很多人发现这个问题。 (可以发布您的最终解决方案作为答案)。也就是说,由于vector&lt;bool&gt; 的实现,dq[i+j] 的设置效率相当低。只需使用 if 语句即可。
  • 感谢您的评论。我很感激。我确实注意到 vector 很昂贵,但我不太清楚为什么。你能详细说明一下吗?
【解决方案4】:

尝试以下方法:

class Solution {
public:
    bool wordBreak(string s, unordered_set<string>& wordDict) 
    {
        for (auto w : wordDict)
        {
            auto pos = s.find(w);
            if (pos != string::npos)
            {
                if (wordBreak(s.substr(0, pos), wordDict) && 
                    wordBreak(s.substr(pos + w.size()), wordDict))
                    return true;
            }
        }
        return false;
    }
};

基本上你找到一个匹配从输入字符串中删除匹配的部分,然后继续测试一个较小的输入。

【讨论】:

  • 而且也是错误的......必须分别检查字符串的两个部分,否则可能会找到不存在的匹配项。例如给定字符串 aba 与字典 aa,b 可以找到加工 b 然后寻找匹配的 aa...
猜你喜欢
  • 2016-09-01
  • 2021-11-05
  • 2017-11-03
  • 1970-01-01
  • 2019-09-27
  • 1970-01-01
  • 1970-01-01
  • 2015-03-21
  • 1970-01-01
相关资源
最近更新 更多