【问题标题】:Using a trie for string segmentation - time complexity?使用 trie 进行字符串分割 - 时间复杂度?
【发布时间】:2017-07-19 19:44:03
【问题描述】:

待解决的问题:

给定一个非空字符串 s 和一个包含列表的字符串数组 wordArr 非空词,判断 s 是否可以分割成 一个或多个字典单词的空格分隔序列。你可以 假设字典不包含重复的单词。

例如,给定 s = "leetcode", wordArr = ["leet", "code"]。

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

在上述问题中,构建一个包含wordArr 中每个字符串的trie 是否可行。然后,对于给定字符串s 中的每个字符,继续尝试。如果一个 trie 分支终止,那么这个子字符串是完整的,所以将剩余的字符串传递给根并递归地做完全相同的事情。

这应该是 O(N) 时间和 O(N) 空间对吗?我问是因为我正在处理的问题说这将以最佳方式是 O(N^2) 时间,我不确定我的方法有什么问题。

例如,如果s = "hello"wordArr = ["he", "ll", "ee", "zz", "o"],那么"he"将在trie的第一个分支中完成,"llo"将递归地传递给根。然后,"ll" 将完成,所以"o" 被传递到 trie 的根。那么"o"就完成了,也就是s的结束,所以返回true。如果s 的结尾未完成,则返回 false。

这对吗?

【问题讨论】:

  • 如果wordArr 不包含不相交的词,则可能涉及回溯。假设wordArr = ["lee", "leet", "code"]。你会先匹配lee,然后浪费大量时间来寻找tcode的匹配。

标签: string algorithm time-complexity trie substring


【解决方案1】:

让我们从将 trie 转换为 nfa 开始。我们在根节点上创建一个接受节点,并添加一条边,该边从特里中字典的每个单词末尾移动到空字符的根节点。

时间复杂度:由于 trie 中的每一步,我们只能移动到表示输入字符串中当前字符和根的一条边。 T(n) = 2×T (n-1)+c 这给了我们 O(2^n)

确实不是 O(n),但是使用动态编程可以做得更好。

  • 我们将使用自上而下的方法。
  • 在我们解决任何字符串之前检查我们是否已经解决了它。
  • 我们可以使用另一个 HashMap 来存储已经解析的字符串的结果。
  • 当任何递归调用返回 false 时,将该字符串存储在 HashMap 中。

这个想法是只计算单词的每个后缀一次。我们只有 n 个后缀,它会以 O(n^2) 结束。

代码形式算法.tutorialhorizo​​n.com:

Map<String, String> memoized;
Set<String> dict;

String SegmentString(String input) {
  if (dict.contains(input)) return input;
  if (memoized.containsKey(input) {
    return memoized.get(input);
  }
  int len = input.length();
  for (int i = 1; i < len; i++) {
    String prefix = input.substring(0, i);
    if (dict.contains(prefix)) {
      String suffix = input.substring(i, len);
      String segSuffix = SegmentString(suffix);
      if (segSuffix != null) {
        memoized.put(input, prefix + " " + segSuffix);
        return prefix + " " + segSuffix;
    }
}

而且你可以做得更好!

Map<String, String> memoized;
Trie<String> dict;

String SegmentString(String input) 
{
    if (dict.contains(input)) 
        return input;
    if (memoized.containsKey(input) 
        return memoized.get(input);

    int len = input.length();
    foreach (StringBuilder word in dict.GetAll(input)) 
    {
        String prefix = input.substring(0, word.length);
        String suffix = input.substring(word.length, len);
        String segSuffix = SegmentString(suffix);
        if (segSuffix != null) 
        {
            memoized.put(input, word.ToString()  + " " + segSuffix);
            return prefix + " " + segSuffix;
        }
    }
    retrun null;
}

使用 Trieto 查找递归调用,仅当 Trie 到达一个词尾时,您将得到 o (z×n),其中 z 是 Trie 的长度。

【讨论】:

    【解决方案2】:

    您的示例确实表明了线性时间复杂度,但请看以下示例:

     s = "hello" 
     wordArr = ["hell", "he", "e", "ll", "lo", "l", "h"]
    

    现在,首先尝试“地狱”,但在下一个递归循环中,没有找到解决方案(没有“o”),因此算法需要回溯并假设“地狱”不合适(双关语不是故意的),因此您尝试“he”,然后在下一个级别中找到“ll”,但又失败了,因为没有“o”。再次需要回溯。现在从“h”开始,然后是“e”,然后又出现了失败:您尝试“ll”但没有成功,所以回溯到使用“l”:现在可以使用解决方案:“hel lo”。

    所以,不,这没有 O(n) 时间复杂度。

    【讨论】:

    • 我在 cxw 的回答上写了这个,但是回溯部分的时间复杂度是多少?
    【解决方案3】:

    我怀疑问题是回溯。如果这个词不能基于特定的字典进行分词,或者如果有多个可能的子字符串具有一个共同的前缀怎么办?例如,假设字典包含 hellenicllo。沿着 trie 的一个分支失败需要回溯,时间复杂度相应增加。

    这类似于一个正则表达式匹配问题:你给出的例子就像测试一个输入单词

    ^(he|ll|ee|zz|o)+$
    

    (任意数量的字典成员,以任意顺序,仅此而已)。我不知道正则表达式匹配器的时间复杂度,但我知道回溯可以让你进入serious time trouble

    我确实找到了this answer,上面写着:

    对字符串运行 DFA 编译的正则表达式确实是 O(n),但可能需要高达 O(2^m) 的构造时间/空间(其中 m = 正则表达式大小)。

    所以可能是 O(n^2) 减少了施工工作量。

    【讨论】:

    • 在 trie 中回溯的时间复杂度是多少?我无法分析。
    • @Sunny 你已经超出了我的理论CS知识范围,很抱歉! :) 我猜你会有类似 O(n log m) 的字符串长度 n 和 trie 深度 m,但这只是一个猜想。我是从每个字符位置的 trie 搜索(记录时间,如果 trie 适度平衡或者如果您可以将其转换为 BST)中得到的。我想说查看链接的其他答案中提到的 NFA 算法。祝你好运!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-01-06
    • 1970-01-01
    相关资源
    最近更新 更多