【问题标题】:Optimizing construction of a trie over all substrings优化所有子字符串的 trie 构造
【发布时间】:2015-09-18 22:04:14
【问题描述】:

我正在解决一个与 trie 相关的问题。有一组字符串S。我必须为 S 中的每个字符串的所有子字符串创建一个 trie。我正在使用以下例程:

String strings[] = { ... }; // array containing all strings
for(int i = 0; i < strings.length; i++) {
    String w = strings[i];
    for (int j = 0; j < w.length(); j++) {
        for (int k = j + 1; k <= w.length(); k++) {
            trie.insert(w.substring(j, k));
        }
    }
}

我正在使用here 提供的 trie 实现。但是,我想知道是否可以进行某些优化以降低在所有子字符串上创建 trie 的复杂性?

我为什么需要这个?因为我正在尝试解决this problem

【问题讨论】:

    标签: algorithm optimization data-structures trie suffix-tree


    【解决方案1】:

    首先,请注意只添加后缀到 trie 就足够了,并且每个子字符串的节点都会沿途添加。

    其次,你必须compress the trie,否则它不符合 HackerRank 施加的内存限制。这也将使您的解决方案更快。

    我刚刚提交了实施这些建议的解决方案,它是was accepted。 (最大执行时间为 0.08 秒。)

    但是您可以通过实施线性时间算法来构造suffix tree,从而使您的解决方案更快。您可以阅读有关线性时间后缀树构造算法herehere 的信息。 StackOverflowhere上也有Ukkonen算法的解释。

    【讨论】:

      【解决方案2】:

      您需要的可能是suffix automaton。它只花费 O(n) 时间并且可以识别所有子字符串。

      Suffix array也可以解决这个问题。

      这两种算法可以解决大部分字符串问题,而且真的很难学。学完这些你就解决了。

      【讨论】:

        【解决方案3】:

        如果我们有N 单词,每个单词的最大长度为L,你的算法将采用O(N*L^3)(假设添加到trie 与添加单词的长度成线性关系)。但是,生成的 trie 的大小(节点数)最多为 O(N*L^2),因此您似乎在浪费时间,您可以做得更好。

        你确实可以,但你必须从你的袖子里拿出一些技巧。此外,您将不再需要 trie。

        1. .substring() 在恒定时间内

        在 Java 7 中,每个 String 都有一个支持 char[] 数组以及起始位置和长度。这允许.substring() 方法在恒定时间内运行,因为String 是不可变类。创建了具有相同支持 char[] 数组的新 String 对象,只是开始位置和长度不同。

        您需要稍微扩展一下,通过增加长度来支持在字符串末尾添加。始终创建一个新的字符串对象,但保持后备数组不变。

        1. 添加单个字符后在恒定时间内重新计算哈希

        再次,让我使用 Java 的 hashCode() 函数来处理 String

        int hash = 0;
        for (int i = 0; i < data.length; i++) {
            hash = 31 * hash + data[i];
        } // data is the backing array
        

        现在,在单词末尾添加单个字符后,哈希值将如何变化?很简单,只需将它的值(ASCII 码)乘以31^length。您可以将 31 的幂保存在单独的表格中,也可以使用其他素数。

        1. 将所有子字符串存储在单个HashMap

        通过使用技巧1和2,您可以及时生成所有子字符串O(N*L^2),这是子字符串的总数。总是从长度为 1 的字符串开始,一次添加一个字符。将所有字符串放入一个 HashMap 中,以减少重复。

        (您可以在排序时/之后跳过 2 和 3 并丢弃重复项,也许它会更快。)

        1. 对子字符串进行排序,一切顺利。

        好吧,当我到达第 4 点时,我意识到我的计划行不通,因为在排序时您需要比较字符串,这可能需要 O(L) 时间。我想出了几个尝试来解决它,其中包括桶排序,但没有一个比原来的 O(N*L^3)

        我会在这里回答,以防它启发某人。


        如果您不知道Aho-Corasic algorithm,请仔细研究一下,它可能对您的问题有所帮助。

        【讨论】:

        • 算法我听说过,看完会回复你。我不确定它是否适合这个问题。
        • 我才意识到这个答案是错误的,等我重写它。
        【解决方案4】:

        您可以考虑以下优化:

        • 维护已处理子字符串的列表。插入子字符串时,检查处理后的集合是否包含该特定子字符串,如果是,则跳过在 trie 中插入该子字符串。

        但是,在 trie 中插入所有子字符串的最坏情况复杂度将是 n^2 的数量级,其中 n 是字符串数组的大小。从问题页面来看,这在 trie 中大约是 10^8 次插入操作。因此,即使每次插入平均需要 10 次操作,您总共会有 10^9 次操作,这会使您超过时间限制。

        问题页面将 LCP 阵列作为问题的相关主题。你应该考虑改变方法。

        【讨论】:

        • 我不是在居高临下,而是考虑到你的处理方式,这个答案很讽刺。 :)
        • @Bhoot:哈哈!没有冒犯。
        • 我建议将添加的子字符串集实现为某种HashSet,因为您可以在恒定时间内添加或删除单个字母时重新计算字符串的哈希值。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2019-12-08
        • 2020-08-20
        • 1970-01-01
        • 1970-01-01
        • 2012-03-18
        • 2015-02-13
        • 2019-05-16
        相关资源
        最近更新 更多