【问题标题】:Find smallest subset prefixes查找最小子集前缀
【发布时间】:2016-12-30 01:30:23
【问题描述】:

这是我解决这个问题的代码。我正在为这个解决方案使用 Trie 树,并想知道在更好的时间复杂度或空间复杂度方面是否还有其他更好的想法。也感谢任何错误和代码风格的建议。

问题:

给定一组字符串,返回给定输入词集的最小子集 --- 包含给定输入词集中每个输入词的前缀。前缀应该是给定输入集中的完整输入词,而不是给定词的前缀,对于没有前缀的词, 返回自己。

如果列表是 ['foo', 'foog', 'food', 'asdf'] 返回 ['foo', 'asdf']

返回是foo,因为foofoo(本身)的前缀,foog 的前缀和food 的前缀(换句话说,foo 可以“表示”更长的字符串,例如@ 987654327@ 和 food)。输出还包含asdf,因为它不是输入列表中任何其他单词的前缀,所以输出本身。

空集不是正确答案,因为它不包含可能的最长前缀。

源代码:

from collections import defaultdict
class TrieNode:
    def __init__(self):
        self.children = defaultdict(TrieNode)
        self.isEnd = False
    def insert(self, word):
        node = self
        for w in word:
            node = node.children[w]
        node.isEnd = True
    def find_prefix(self, prefix, result):
        if self.isEnd:
            result.append(prefix[:])
            return
        for k,v in self.children.items():
            prefix.append(k)
            v.find_prefix(prefix, result)
            prefix.pop(-1)

if __name__ == "__main__":
    words = ['foo', 'foog', 'food', 'asdf']
    root = TrieNode()
    for w in words:
        root.insert(w)
    result = []
    root.find_prefix([], result)
    print result

【问题讨论】:

  • 我发现您的问题描述不清楚。为什么不只返回整个输入集——其中一个是任何给定单词的最长前缀。
  • 问题表述不当:例如,[fab, fabc, fbc] 的预期输出是什么?是[fab, fbc] 还是[f]? (“通用前缀”应该有多“通用”?仅由 2 个元素共享就足够了,或者如果它由两个以上共享,那么“通用性”优先于“最大长度”?)
  • @RoryDaulton,我需要返回最小的集合,它是所有输入单词的前缀。我也编辑了问题描述,如果仍然不清楚,请随时纠正。非常感谢您对原始问题的建议。
  • @AdrianColomitchi,感谢 cmets,我已编辑问题以使其更清楚。在您的示例中,应该返回 [fab, fbc],因为 f 不是输入集中的单独单词。如果仍有任何不清楚的地方,请随时提出建议。非常感谢您对原始问题的建议。
  • 啊,所以不仅是通用前缀,而且是其他人通用前缀的整个单词?

标签: python algorithm python-2.7


【解决方案1】:

我认为这个问题是明确的。 (也许在编辑之前更难)。答案是 trie 似乎完全正确。

从输入的单词构建一个 trie,然后先深度遍历它。每次在输入集中找到一个节点(内部节点或叶子)时,将该节点处的单词添加到输出中并停止搜索它的子节点。

【讨论】:

  • 感谢 danh,想知道其他基于字符串的算法(如 KMP)是否有助于您提高时间复杂度?
  • 这是个好主意,但是将每个单词与所有其他单词进行比较很复杂。我太生疏了,无法证明,但你目前的想法是 O(n * average word length) 其中 n 是字数。使用一个聪明的字符串前缀测试器,即使它是 O(1) 给你 O(n^2),你可以将每个单词与其他单词进行比较,以确定它是否可以在输出中代替它们
  • “我认为这个问题是明确的。”真的吗? [f, fa, fab] 呢?毕竟fafab的前缀,所以请引用规范中禁止fa作为前缀词列出的规则。
  • @AdrianColomitchi,您在 cmets 中做得对。因为它是所有其他单词的输入单词和前缀,所以“f”是该集合的唯一输出。这是我们所依赖的规范的“最小子集”部分
  • 你是对的,最小子集胜过最大前缀长度。
【解决方案2】:

我更喜欢简单的while-loop 方法,开头有一个排序:

minimal = []
words = ['foo', 'foog', 'food', 'asdf']
words.sort(key=lambda x: (len(x), x))
while words:
    word = words[0]
    minimal.append(word)
    words = [ x for x in words[1:] if not x.startswith(word) ]
print minimal

当没有字符串是任何其他字符串的前缀时,这是一个相当有效的实现,在最坏的情况下运行 O(n**2)。


附言 #1:您可以通过仅按单词的长度而不是长度和字母顺序排序来稍微提高排序效率。例如,改变这一行:

    words.sort(key=lambda x: (len(x), x))

到:

    words.sort(key=lambda x: len(x))

当然,排序是 O(n(log n)),它是运行时间/复杂度的下限。


后记 #2:

如果您更喜欢定义的内存特性,您可以使用标记而不是在words 列表上进行过滤。该算法的标记版本如下所示:

    words = [ 'foo', 'foog', 'food', 'asdf' ]
    words.sort(key=lambda x: len(x))
    marked = [ False for _ in words ]
    for i in range(0, len(words)):
        is_marked = marked[i]
        if is_marked: continue 
        word = words[i]

        for j in range(i + 1, len(words)):
            if not marked[j] and words[j].startswith(word):
                marked[j] = True
    minimal = [ word for word, is_marked in zip(words, marked) if not is_marked ]

它比我喜欢的过滤版本稍微冗长,但它的好处是不会在循环的每次连续传递中不断地创建/销毁单词数组。

【讨论】:

  • 谢谢 2ps,为什么你认为你的代码是最坏的情况O(n**2)?我认为它总是O(n**2),因为您比较了每个单词对?如果我读错了你的逻辑,请随时纠正我。
  • 另外一个 cmets 是,您正在使用 words 来控制 while 循环,并且在循环本身中,您更改了循环控制变量 words 的值,不确定它是否是一个好的主意?我来自 C++/Java,通常的理解是不将循环控制变量作为一种好习惯。谢谢。
  • @LinMa:看来您没有正确遵循while 循环的逻辑。该算法不会比较每个单词对。在 while 循环的第一遍之后,'foo' 与所有其他单词进行比较,'foog''food' 被从单词列表中过滤掉。因此,在while 循环的第二次(也是最后一次)通过时,'asdf' 被添加到最小集合中,但不与任何其他单词进行比较。 QED。
  • @LinMa:wrt 循环控制变量,即使是 Java 和 C++ 中非常基本的 for 循环,循环控制变量也会发生变化。例如:for (x = 0; x < 10; ++x) {} 有一个循环控制变量,它在循环过程中发生变化。从数组中消耗项目的while 循环是标准模式。在这种情况下,while 循环肯定会结束,因为在循环的每次迭代中,我们总是从单词列表中消耗至少一个单词。
猜你喜欢
  • 1970-01-01
  • 2013-10-31
  • 2012-07-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多