查找字符串中所有重复的子字符串以及它们出现的频率

时间:2016-05-28 14:07:37

标签: python string algorithm suffix-tree

问题:

我需要符合以下条件的所有字符序列:

  1. 字符序列必须多次出现((LE,1)因此无效)。
  2. 字符序列必须长于一个字符((M,2)因此无效)。
  3. 字符序列不能是存在次数相同的较长现有序列的一部分(如果(LIO,2)存在,则(LI,2)无效)。
  4. 所以,如果输入字符串是:KAKAMNENENELIOLELIONEM$ 输出将是:

    (KA, 2)
    (NE, 4)
    (LIO, 2)
    

    它还需要快速,它应该能够在合理的时间内解决1000个字符长的字符串。

    我尝试过:

    从后缀树获取分支数量:

    编辑this后缀树 - 创建librabry(Python-Suffix-Tree),我制作了一个程序,给出了一些错误的结果。

    我将此函数添加到suffix_tree.py中的SuffixTree类:

    def get_repeated_substrings(self):
        curr_index = self.N
        values = self.edges.values()
        values = sorted(values, key=lambda x: x.dest_node_index)
        data = []  # index = edge.dest_node_index - 1
        for edge in values:
            if edge.source_node_index == -1:
                continue
            top = min(curr_index, edge.last_char_index)
            data.append([edge.source_node_index,
                    self.string[edge.first_char_index:top+1]])
        repeated_substrings = {}
        source_node_indexes = [i[0] for i in data]
        nodes_pointed_to = set(source_node_indexes)
        nodes_pointed_to.remove(0)
        for node_pointed_to in nodes_pointed_to:
            presence = source_node_indexes.count(node_pointed_to)
            key = data[node_pointed_to-1][1]
            if key not in repeated_substrings:
                repeated_substrings[key] = 0
            repeated_substrings[key] += presence
        for key in repeated_substrings:
            if len(key) > 1:
                print(key, repeated_substrings[key])
    

    然后使用它和库的其余部分:

    from lib.suffix_tree import SuffixTree
    
    st = SuffixTree("KAKANENENELIOLELIONE$")
    print(st.get_repeated_substrings())
    

    输出:

    KA 2
    NE 7
    LIO 2
    IO 4
    

    get_repeated_substrings()基本上遍历节点之间的所有连接(在此库中称为边缘)并保存它指向的节点有多少连接(它将其保存到repeated_substrings),然后它打印更多的已保存值比一个字符长。

    它会将连接数添加到该序列已有的数量,这大部分时间都有效,但正如您在上面的输出中所看到的那样,它导致了' NE&#39的值不正确; (7,应该是4)。解决了这个问题后,我意识到这种方法不会检测到由相同字符(AA,BB)和其他故障构成的模式。我的结论是:要么没有办法用后缀树来解决它,要么我做了一些非常错误的事情。

    其他方法:

    我也尝试了一些更简单的方法,包括循环浏览东西,但这也没有取得成功:

    import copy
    
    string = 'kakabaliosie'
    
    for main_char in set(string):
        indices = []
        for char_i, char in enumerate(string):
            if main_char == char:
                indices.append(char_i)
        relative = 1
        while True
            for index in indices:
                other_indices = copy.deepcopy(indices)
                other_indices.remove(index)
                for other_index in other_indices:
    

    (无法完成它)

    问题:

    我怎样才能制作出我想要的程序?

1 个答案:

答案 0 :(得分:4)

您的后缀树方法是正确的方法。

获取匹配项及其出现次数

基本上你需要做的是以BFS方式遍历树。从root的子节点开始,您将递归计算每个节点可到达的叶子数。这将导致您在根上调用Node的方法。这是一个可能的实现:

def count_leaves(self, stree):
    leaves_count = 0
    for child in [stree.nodes[x.dest_node_index] for x in self.edges.values()]:
        child_leaves_count = child.count_leaves(stree)
        if 0 == child_leaves_count:
            # The child node is a leaf...
            leaves_count = leaves_count + 1
        else:
            # The child node is an internal node, we add up the number of leaves it can reach
            leaves_count = leaves_count + child_leaves_count
    self.leaves_count = leaves_count
    return leaves_count

现在,每个节点都标有可以到达的叶数。

然后,后缀树的有趣属性将帮助您自动过滤掉与您的某些要求不匹配的子字符串:

  • 如果一个字符串只出现一次,那么你必然最终转换到叶子节点(所述转换至少有两个字符,因为我们不计算结束标记$)。这意味着它不是一个明确的状态,因此我们甚至不考虑它。
  • 如果我们有(LI,2)和(LIO,2),则(LI,2)是后缀树的隐式状态,这意味着它位于边缘的中间。由于我们只考虑具有显式状态的子串(即最终在节点中),我们将永远不会找到(LI,2)。
  • 内部节点至少有2个子节点,否则它们将是一个叶子而没有。

现在迭代内部节点将获得子串列表及其输入字符串中出现的次数(您需要过滤掉表示1个字符子串的节点)。

您将在下面找到输入字符串后缀树的字符串表示形式。这将帮助您可视化哪些子字符串匹配。

- O - N E M $ - ##  
    - L E L I O N E M $ - ##  
- I O - N E M $ - ##  
      - L E L I O N E M $ - ##  
- $ - ##  
- E - M $ - ##  
      L I O - N E M $ - ##
              L E L I O N E M $ - ##
      N E - L I O L E L I O N E M $ - ##
            N E L I O L E L I O N E M $ - ##
- K A - M N E N E N E L I O L E L I O N E M $ - ##
        K A M N E N E N E L I O L E L I O N E M $ - ##
- L - E L I O N E M $ - ##
      I O - N E M $ - ##
            L E L I O N E M $ - ##
- A - M N E N E N E L I O L E L I O N E M $ - ##
      K A M N E N E N E L I O L E L I O N E M $ - ##
- M - $ - ##
      N E N E N E L I O L E L I O N E M $ - ##
- N E - M $ - ##
        L I O L E L I O N E M $ - ##
        N E - L I O L E L I O N E M $ - ##
              N E L I O L E L I O N E M $ - ##

这导致以下输出:

  

(IO,2)
  (ELIO,2)
  (ENE,2)
  (KA,2)
  (LIO,2)
  (NE,4)
  (NENE,2)

排除固定出现次数(N)的冗余匹配

我们现在假设LIOIO应该被过滤掉,因为就像ELIO一样,它们有两个匹配。这种较大匹配的子串称为“冗余匹配”。以下谜题仍未解决:假设所有匹配的集合恰好发生N次,即 N匹配(其中N是固定整数),我们如何过滤掉“冗余”?

我们首先从通过减少长度排序的N匹配集合中建立优先级队列。然后,我们将迭代地构建这些匹配的广义后缀树 GST ),以识别冗余匹配。为此,算法如下:

  1. 对于堆中的每个元素(在顶部),测试此元素是否是已在 GST 中注册的元素之一的子字符串
    • 如果不是:将其插入 GST 并将其附加到“良好匹配”列表中。
    • 其他:跳过它,因为已经注册了另一个更大的匹配...并尝试使用下一个元素
  2. 一旦堆为空,好匹配列表包含所有非冗余N匹配
  3. 这导致以下伪Python代码:

    match_heap = heapify(set_of_matches)
    good_matches = []
    match_gst = generalized_suffix_tree()
    while (not match_heap.empty()):
        top_match = match_heap.top()
        if (not match_gst.is_substring(top_match.string)):
            gst_match.insert(top_match.string)
            good_matches.append(top_match)
        else:
            # The given match is a substring of an already registered, bigger match
            # We skip it
    return good_matches
    

    过滤所有冗余匹配

    现在我们可以过滤 N-matches 的冗余匹配,很容易从我们的全局匹配中过滤掉所有这些匹配。我们使用它们的出现次数在桶中收集匹配,然后我们在每个桶上应用上一节的算法。

    备注

    要实现上述算法,您需要具有通用后缀树实现,这与常规后缀树略有不同。如果你找不到Python实现,你可以随时调整你的实现。请参阅this question以获取有关如何操作的提示。