Поиск всех общих непересекающихся подстрок

Учитывая две строки, я хотел бы определить все общие подстроки от самой длинной до самой короткой.

Я хочу удалить любые «подстроки». Например, любые подстроки «1234» не будут включены в соответствие между «12345» и «51234».

string1 = '51234'

string2 = '12345'

result = ['1234', '5']

Я думал найти самую длинную общую подстроку, а затем рекурсивно найти самые длинные подстроки для левый/правый. Однако я не хочу удалять общую подстроку после ее обнаружения. Например, в приведенном ниже результате разделяет цифру 6 посередине:

string1 = '12345623456'

string2 = '623456'

result = ['623456', '23456']

Наконец, мне нужно сравнить одну строку с фиксированным списком из тысяч строк. Я не уверен, есть ли умный шаг, который я мог бы предпринять для хеширования всех подстрок в этих строках.

Предыдущие ответы:

В этом потоке найдено решение для динамического программирования это занимает O(nm) времени, где n и m — длины строк. Меня интересует более эффективный подход, который будет использовать деревья суффиксов.

Предыстория:

Я сочиняю мелодии песен из фрагментов мелодий. Иногда комбинации удается создать мелодию, соответствующую слишком большому количеству нот в ряду существующей.

Я могу использовать меру схожести строк, такую ​​как Edit Distance, но считаю, что мелодии с очень небольшими отличиями от мелодий уникальны и интересны. К сожалению, эти мелодии будут иметь такой же уровень сходства с песнями, которые копируют множество нот мелодии подряд.


person Andy Toulis    schedule 27.05.2016    source источник


Ответы (1)


Начнем с Дерева

from collections import defaultdict
def identity(x):
    return x


class TreeReprMixin(object):
    def __repr__(self):
        base = dict(self)
        return repr(base)


class PrefixTree(TreeReprMixin, defaultdict):
    '''
    A hash-based Prefix or Suffix Tree for testing for
    sequence inclusion. This implementation works for any
    slice-able sequence of hashable objects, not just strings.
    '''
    def __init__(self):
        defaultdict.__init__(self, PrefixTree)
        self.labels = set()

    def add(self, sequence, label=None):
        layer = self
        if label is None:
            label = sequence
        if label:
            layer.labels.add(label)
        for i in range(len(sequence)):
            layer = layer[sequence[i]]
            if label:
                layer.labels.add(label)

        return self

    def add_ngram(self, sequence, label=None):
        if label is None:
            label = sequence
        for i in range(1, len(sequence) + 1):
            self.add(sequence[:i], label)

    def __contains__(self, sequence):
        layer = self
        j = 0
        for i in sequence:
            j += 1
            if not dict.__contains__(layer, i):
                break
            layer = layer[i]
        return len(sequence) == j

    def depth_in(self, sequence):
        layer = self
        count = 0
        for i in sequence:
            if not dict.__contains__(layer, i):
                print "Breaking"
                break
            else:
                layer = layer[i]
            count += 1
        return count

    def subsequences_of(self, sequence):
        layer = self
        for i in sequence:
            layer = layer[i]
        return layer.labels

    def __iter__(self):
        return iter(self.labels)


class SuffixTree(PrefixTree):
    '''
    A hash-based Prefix or Suffix Tree for testing for
    sequence inclusion. This implementation works for any
    slice-able sequence of hashable objects, not just strings.
    '''
    def __init__(self):
        defaultdict.__init__(self, SuffixTree)
        self.labels = set()

    def add_ngram(self, sequence, label=None):
        if label is None:
            label = sequence
        for i in range(len(sequence)):
            self.add(sequence[i:], label=label)

Чтобы заполнить дерево, вы должны использовать метод .add_ngram.

Следующая часть немного сложнее, так как вы ищете параллельный обход строк, отслеживая координаты дерева. Чтобы все это реализовать, нам нужны некоторые функции, работающие с деревом, и строка запроса.

def overlapping_substrings(string, tree, solved=None):
    if solved is None:
        solved = PrefixTree()
    i = 1
    last = 0
    matching = True
    solutions = []
    while i < len(string) + 1:
        if string[last:i] in tree:
            if not matching:
                matching = True
            else:
                i += 1
                continue
        else:
            if matching:
                matching = False
                solutions.append(string[last:i - 1])
                last = i - 1
                i -= 1
        i += 1
    if matching:
        solutions.append(string[last:i])
    for solution in solutions:
        if solution in solved:
            continue
        else:
            solved.add_ngram(solution)
            yield solution

def slide_start(string):
    for i in range(len(string)):
        yield string[i:]

def seek_subtree(tree, sequence):
    # Find the node of the search tree which
    # is found by this sequence of items
    node = tree
    for i in sequence:
        if i in node:
            node = node[i]
        else:
            raise KeyError(i)
    return node

def find_all_common_spans(string, tree):
    # We can keep track of solutions to avoid duplicates
    # and incomplete prefixes using a Prefix Tree
    seen = PrefixTree()
    for substring in slide_start(string):
        # Drive generator forward
        list(overlapping_substrings(substring, tree, seen))
    # Some substrings are suffixes of other substrings which you do not
    # want
    compress = SuffixTree()
    for solution in sorted(seen.labels, key=len, reverse=True):
        # A substrings may be a suffix of another substrings, but that substrings
        # is actually a repeating pattern. If a solution is
        # a repeating pattern, `not solution in seek_subtree(tree, solution)` will tell us.
        # Otherwise, discard the solution
        if solution in compress and not solution in seek_subtree(tree, solution):
            continue
        else:
            compress.add_ngram(solution)
    return compress.labels

def search(query, corpus):
    tree = SuffixTree()
    if isinstance(corpus, SuffixTree):
        tree = corpus
    else:
        for elem in corpus:
            tree.add_ngram(elem)
    return list(find_all_common_spans(query, tree))

Итак, теперь, чтобы сделать то, что вы хотели, сделайте следующее:

search("12345", ["51234"])
search("623456", ["12345623456"])

Если что-то неясно, пожалуйста, дайте мне знать, и я постараюсь уточнить.

person mobiusklein    schedule 28.05.2016
comment
Кажется, что он добавляет решения по одному: search('abc', 'b') ›› ['bc'] Также пытается выяснить, почему он сначала находит кратчайшие решения: search('test', 'tests ') >> ['тестовое задание'] - person Andy Toulis; 30.05.2016
comment
Вероятно, в overlapping_substrings есть ошибка "один на один", так как управление индексами все еще было немного небрежным. Также обратите внимание, что в моем примере использования корпус представляет собой список строк, а не одну строку. - person mobiusklein; 30.05.2016
comment
Спасибо. На самом деле это было в процедуре contains, j=j+1 увеличивается до выполнения проверки, поэтому 'bc' находится в n_gram '‹anything›b'. Также обнаружил, что не хочу добавлять какие-либо уже добавленные n_grams, так как это позволит избежать двойной проверки/странных результатов. - person Andy Toulis; 04.06.2016