Найти максимальная длина всех N-слово-длина подстроки разделяют две строки

Я работаю над созданием скрипта Python, который может найти (самую длинную возможную) длину всех подстрок длиной n слов, разделяемых двумя строками, без учета конечной пунктуации. Даны две строки:

"это пример строки"

"это также пример строки"

Я хочу, чтобы скрипт определил, что эти строки имеют последовательность из 2 общих слов ("это"), а затем последовательность из 3 общих слов ("образец строка.)" Вот мой текущий подход:

a = "this is a sample string"
b = "this is also a sample string"

aWords = a.split()
bWords = b.split()

#create counters to keep track of position in string
currentA = 0
currentB = 0

#create counter to keep track of longest sequence of matching words
matchStreak = 0

#create a list that contains all of the matchstreaks found
matchStreakList = []

#create binary switch to control the use of while loop
continueWhileLoop = 1

for word in aWords:
    currentA += 1

    if word == bWords[currentB]:
        matchStreak += 1

        #to avoid index errors, check to make sure we can move forward one unit in the b string before doing so
        if currentB + 1 < len(bWords):
            currentB += 1

        #in case we have two identical strings, check to see if we're at the end of string a. If we are, append value of match streak to list of match streaks
        if currentA == len(aWords):
            matchStreakList.append(matchStreak)

    elif word != bWords[currentB]:

        #because the streak is broken, check to see if the streak is >= 1. If it is, append the streak counter to out list of streaks and then reset the counter
        if matchStreak >= 1:
            matchStreakList.append(matchStreak)
        matchStreak = 0

        while word != bWords[currentB]:

            #the two words don't match. If you can move b forward one word, do so, then check for another match
            if currentB + 1 < len(bWords):
                currentB += 1

            #if you have advanced b all the way to the end of string b, then rewind to the beginning of string b and advance a, looking for more matches
            elif currentB + 1 == len(bWords):
                currentB = 0
                break

        if word == bWords[currentB]:
            matchStreak += 1

            #now that you have a match, check to see if you can advance b. If you can, do so. Else, rewind b to the beginning
            if currentB + 1 < len(bWords):
                currentB += 1
            elif currentB + 1 == len(bWords):

                #we're at the end of string b. If we are also at the end of string a, check to see if the value of matchStreak >= 1. If so, add matchStreak to matchStreakList
                if currentA == len(aWords):
                    matchStreakList.append(matchStreak)
                currentB = 0
                break

print matchStreakList

этот скрипт правильно выводит (максимальные) длины подстрок общей длины слова (2, 3) и сделал это для всех тестов до сих пор. Мой вопрос: есть ли пара двух строк, для которых подход выше не будет работать? Более того: существуют ли существующие библиотеки Python или известные подходы, которые можно использовать для поиска максимальной длины всех подстрок длиной n слов, которые разделяют две строки?

[этот вопрос отличается от самой длинной общей проблемы подстроки, которая является только частным случаем того, что я ищу (поскольку я хочу найти все общие подстроки, а не только самую длинную общую подстроку). это так пост предполагает, что такие методы, как 1) кластерный анализ, 2) редактирование подпрограмм расстояния и 3) самые длинные алгоритмы общей последовательности могут быть подходящими подходами, но я не нашел никаких рабочих решений, и моя проблема, возможно, немного проще, что упомянуто в ссылке потому что я имею дело со словами, ограниченная пробелами.]

EDIT:

Я начинаю щедрость по этому вопросу. В случае, если это поможет другим, я хотел бы уточнить несколько ключевых моментов. Во-первых, полезный ответ, предложенный ниже @DhruvPathak, не находит всех максимально длинных подстрок длиной n слов, разделяемых двумя строками. Например, предположим, что мы анализируем две строки:

" они все белые лист безупречной бумаги, когда они рождаются первыми. но они должны быть нацарапаны и стерты каждым гусиным пером.--5-->

и

4 ответов


я написал в Python difflib.SequenceMatcher и много времени нахождения ожидаемого случая быстрые способы найти самые длинные общие подстроки. Теоретически это должно быть сделано с "деревьями суффиксов" или связанными "массивами суффиксов", дополненными "самыми длинными общими массивами префиксов" (фразы в кавычках-это поисковые термины, если вы хотите Google для более.) Они могут решить проблему в худшем случае линейного времени. Но, как это иногда бывает, наихудшие алгоритмы линейного времени мучительно сложны и деликатны и страдают большими постоянными факторами - они все еще могут окупиться очень сильно, если данный корпус будет искать много раз, но это не типичный случай для Python difflib и это тоже не похоже на ваше дело.

в любом случае, мой вклад здесь-переписать SequenceMatcher ' s find_longest_match() способ вернуться все (локально) максимальные совпадения, которые он находит по пути. Примечания:

  1. я собираюсь использовать to_words() функция Raymond Hettinger дал вам, но без преобразования в нижний регистр. Преобразование в нижний регистр приводит к выходу, который не совсем похож на то, что вы сказали, что хотите.

  2. тем не менее, как я уже отметил в комментарии, это выводит "перо", которого нет в вашем списке желаемых результатов. У меня нет идея, почему это не так, так как "перо" появляется в обоих входах.

вот код:

import re
def to_words(text):
    'Break text into a list of words without punctuation'
    return re.findall(r"[a-zA-Z']+", text)

def match(a, b):
    # Make b the longer list.
    if len(a) > len(b):
        a, b = b, a
    # Map each word of b to a list of indices it occupies.
    b2j = {}
    for j, word in enumerate(b):
        b2j.setdefault(word, []).append(j)
    j2len = {}
    nothing = []
    unique = set() # set of all results
    def local_max_at_j(j):
        # maximum match ends with b[j], with length j2len[j]
        length = j2len[j]
        unique.add(" ".join(b[j-length+1: j+1]))
    # during an iteration of the loop, j2len[j] = length of longest
    # match ending with b[j] and the previous word in a
    for word in a:
        # look at all instances of word in b
        j2lenget = j2len.get
        newj2len = {}
        for j in b2j.get(word, nothing):
            newj2len[j] = j2lenget(j-1, 0) + 1
        # which indices have not been extended?  those are
        # (local) maximums
        for j in j2len:
            if j+1 not in newj2len:
                local_max_at_j(j)
        j2len = newj2len
    # and we may also have local maximums ending at the last word
    for j in j2len:
        local_max_at_j(j)
    return unique

затем:

a = "They all are white a sheet of spotless paper " \
    "when they first are born but they are to be " \
    "scrawled upon and blotted by every goose quill"
b = "You are all white, a sheet of lovely, spotless " \
    "paper, when you first are born; but you are to " \
    "be scrawled and blotted by every goose's quill"

print match(to_words(a), to_words(b))

отображает:

set(['all',
     'and blotted by every',
     'first are born but',
     'are to be scrawled',
     'are',
     'spotless paper when',
     'white a sheet of',
     'quill'])

EDIT-как это работает

многие алгоритмы согласования и выравнивания последовательности лучше всего понимать как работу над 2-мерной матрицей с правилами вычисления записей матрицы и последующей интерпретации записей значение.

для ввода последовательности a и b, картина матрицы M С len(a) строк и len(b) столбцы. В этом приложении, мы хотим M[i, j] чтобы содержать длину самой длинной общей смежной подпоследовательности, заканчивающейся на a[i] и b[j], и вычислительные правила очень просто:

  1. M[i, j] = 0 если a[i] != b[j].
  2. M[i, j] = M[i-1, j-1] + 1 если a[i] == b[j] (где мы предполагаем ссылку на матрицу вне границ молча возвращает 0).

интерпретация также очень проста в этом случае: существует локально максимум непустой матч заканчивается в a[i] и b[j], длиной M[i, j], если и только если M[i, j] является ненулевым, но M[i+1, j+1] либо 0, либо вне пределов.

вы можете использовать эти правила для написания очень простого и компактного кода с двумя циклами, который вычисляет M правильно для этой проблемы. Недостатком является то, что код будет принимать (лучшие, средние и в худших случаях)O(len(a) * len(b)) времени и пространство.

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

  • вместо того, чтобы делать один проход для вычисления M, затем еще один проход для интерпретации результатов, вычисления и интерпретации чередуются за один проход a.

  • из-за этого не нужно хранить всю матрицу. Вместо этого только текущая строка (newj2len) и предыдущая строка (j2len) одновременно присутствуют.

  • и потому, что матрица в этом проблема обычно в основном нули, строка здесь представлена редко, через индексы столбцов отображения dict к ненулевым значениям. Нулевые записи являются "свободными", поскольку они никогда не хранятся явно.

  • при обработке строки нет необходимости перебирать каждый столбец: предварительно вычисленный b2j dict сообщает нам точно интересные индексы столбцов в текущей строке (те столбцы, которые соответствуют текущему word С a).

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

EDIT-the dirt простая версия

вот код, который реализует 2D-матрицу напрямую, без попыток оптимизации (кроме этого Counter часто можно избежать явного хранения 0 записей). Это очень просто, коротко и легко:

def match(a, b):
    from collections import Counter
    M = Counter()
    for i in range(len(a)):
        for j in range(len(b)):
            if a[i] == b[j]:
                M[i, j] = M[i-1, j-1] + 1
    unique = set()
    for i in range(len(a)):
        for j in range(len(b)):
            if M[i, j] and not M[i+1, j+1]:
                length = M[i, j]
                unique.add(" ".join(a[i+1-length: i+1]))
    return unique

конечно ; -), который возвращает те же результаты, что и оптимизированный match() я опубликовал сначала.

EDIT - и еще без диктата!--68-->

просто для удовольствия : -) если у вас есть матрица модель вниз ПЭТ, этот код будет легко следовать. Примечательно, что значение ячейки матрицы зависит только от значений, расположенных по диагонали к северо-западу от ячейки. Поэтому "достаточно хорошо" просто пересечь все основные диагонали, двигаясь на юго-восток от всех ячеек на Западной и Северной границах. Таким образом, требуется только небольшое постоянное пространство, независимо от входных данных длина:

def match(a, b):
    from itertools import chain
    m, n = len(a), len(b)
    unique = set()
    for i, j in chain(((i, 0) for i in xrange(m)),
                      ((0, j) for j in xrange(1, n))):
        k = 0
        while i < m and j < n:
            if a[i] == b[j]:
                k += 1
            elif k:
                unique.add(" ".join(a[i-k: i]))
                k = 0
            i += 1
            j += 1
        if k:
            unique.add(" ".join(a[i-k: i]))
    return unique

есть действительно четыре вопроса, встроенные в ваш пост.

1) Как вы разделяете текст на слова?

есть много способов сделать это в зависимости от того, что вы считаете словом, заботитесь ли вы о случае, разрешены ли сокращения и т. д. Регулярное выражение позволяет реализовать выбор правил разбиения слов. Обычно я использую r"[a-z'\-]+". Схватки, как don't и разрешить перенос слов, таких как mother-in-law.

2) Какая структура данных может ускорить поиск общих подпоследовательностей?

создайте карту местоположения, показывающую для каждого слова. Например, в предложении you should do what you like отображение для for you is {"you": [0, 4]} потому что он появляется дважды, один раз в нулевой позиции и один раз в позиции четыре.

С картой местоположения в руке, это простой вопрос, чтобы зациклить начальные точки для сравнения подпоследовательностей n-длины.

3) Как найти общие подпоследовательности n-длины?

перебрать все слова в одном из предложений. Для каждого такого слова найдите места, где оно встречается в другой последовательности (используя карту местоположения), и проверьте, равны ли два среза n-длины.

4) Как найти самую длинную общую подпоследовательность?

на max () функция находит максимальное значение. Он принимает ключевую функцию, такую как len () to определите основу для сравнения.

вот некоторый рабочий код, который вы можете настроить для своей собственной интерпретации проблемы:

import re

def to_words(text):
    'Break text into a list of lowercase words without punctuation'
    return re.findall(r"[a-z']+", text.lower())

def starting_points(wordlist):
    'Map each word to a list of indicies where the word appears'
    d = {}
    for i, word in enumerate(wordlist):
        d.setdefault(word, []).append(i)
    return d

def sequences_in_common(wordlist1, wordlist2, n=1):
    'Generate all n-length word groups shared by two word lists'
    starts = starting_points(wordlist2)
    for i, word in enumerate(wordlist1):
        seq1 = wordlist1[i: i+n]
        for j in starts.get(word, []):
            seq2 = wordlist2[j: j+n]
            if seq1 == seq2 and len(seq1) == n:
                yield ' '.join(seq1)

if __name__ == '__main__':

    t1 = "They all are white a sheet of spotless paper when they first are " \
         "born but they are to be scrawled upon and blotted by every goose quill"

    t2 = "You are all white, a sheet of lovely, spotless paper, when you first " \
         "are born; but you are to be scrawled and blotted by every goose's quill"

    w1 = to_words(t1)
    w2 = to_words(t2)

    for n in range(1,10):
        matches = list(sequences_in_common(w1, w2, n))
        if matches:
            print(n, '-->', max(matches, key=len))

модуль difflib будет хорошим кандидатом для этого случая, см. get_matching_blocks :

import difflib

def matches(first_string,second_string):
    s = difflib.SequenceMatcher(None, first_string,second_string)
    match = [first_string[i:i+n] for i, j, n in s.get_matching_blocks() if n > 0]
    return match

first_string = "this is a sample string"
second_string = "this is also a sample string"
print matches(second_string, first_string )

демо: http://ideone.com/Ca3h8Z


небольшая модификация, с совпадением не символов, а слов, я полагаю, будет делать:

def matche_words(first_string,second_string):
    l1 = first_string.split()
    l2 = second_string.split()
    s = difflib.SequenceMatcher(None, l1, l2)
    match = [l1[i:i+n] for i, j, n in s.get_matching_blocks() if n > 0]
    return match

демо:

>>> print '\n'.join(map(' '.join, matches(a,b)))
all
white a sheet of
spotless paper when
first are born but
are to be scrawled
and blotted by every
quill