Самая длинная общая подстрока без вырезания слова-питона

Учитывая следующее, я могу найти самую длинную общую подстроку:

s1 = "this is a foo bar sentence ."
s2 = "what the foo bar blah blah black sheep is doing ?"

def longest_common_substring(s1, s2):
  m = [[0] * (1 + len(s2)) for i in xrange(1 + len(s1))]
  longest, x_longest = 0, 0
  for x in xrange(1, 1 + len(s1)):
    for y in xrange(1, 1 + len(s2)):
      if s1[x - 1] == s2[y - 1]:
        m[x][y] = m[x - 1][y - 1] + 1
        if m[x][y] > longest:
          longest = m[x][y]
          x_longest = x
      else:
        m[x][y] = 0
  return s1[x_longest - longest: x_longest]

print longest_common_substring(s1, s2)

[из]:

foo bar

Но как я могу гарантировать, что самая длинная общая подстрока уважает границу английского слова и не разрезает слово? Например, следующие предложения:

s1 = "this is a foo bar sentence ."
s2 = "what a kappa foo bar black sheep ?"
print longest_common_substring(s1, s2)

выводит следующее, что НЕ желательно, так как это разбивает слово kappa из с2:

a foo bar

Желаемый результат по-прежнему:

foo bar

Я пробовал также ngram способ получить самую длинную общую подстроку, относящуюся к границе слова, но есть ли другой способ, который имеет дело со строками без вычисления ngram? (см. ответ)

8 ответов

Решение

Это слишком просто для понимания. Я использовал твой код для выполнения 75% работы. Сначала я делю предложение на слова, затем передаю его вашей функции, чтобы получить наибольшую общую подстроку (в этом случае это будут самые длинные слова подряд), поэтому ваша функция дает мне ['foo', 'bar'], я присоединяюсь к элементы этого массива для получения желаемого результата.

Вот онлайн рабочая копия, которую вы можете протестировать, проверить и поиграть с ней.

http://repl.it/RU0/1

def longest_common_substring(s1, s2):
  m = [[0] * (1 + len(s2)) for i in xrange(1 + len(s1))]
  longest, x_longest = 0, 0
  for x in xrange(1, 1 + len(s1)):
    for y in xrange(1, 1 + len(s2)):
      if s1[x - 1] == s2[y - 1]:
        m[x][y] = m[x - 1][y - 1] + 1
        if m[x][y] > longest:
          longest = m[x][y]
          x_longest = x
      else:
        m[x][y] = 0
  return s1[x_longest - longest: x_longest]

def longest_common_sentence(s1, s2):
    s1_words = s1.split(' ')
    s2_words = s2.split(' ')  
    return ' '.join(longest_common_substring(s1_words, s2_words))


s1 = 'this is a foo bar sentence .'
s2 = 'what a kappa foo bar black sheep ?'
common_sentence = longest_common_sentence(s1, s2)
print common_sentence
>> 'foo bar'

Краевые случаи

  1. '' а также '?' также рассматриваются как допустимые слова, как в вашем случае, если между последним словом и знаком препинания есть пробел. Если вы не оставите пробел, они будут засчитаны как часть последнего слова. В этом случае "овцы" и "овцы?" не будет больше одних и тех же слов. Вам решать, что делать с такими символами, прежде чем вызывать такую ​​функцию. В таком случае

    import re
    s1 = re.sub('[.?]','', s1)
    s2 = re.sub('[.?]','', s2)

а затем продолжайте как обычно.

Мой ответ основан не на каких-либо официальных источниках, а просто на простом наблюдении: по крайней мере, в моей установке есть разница между выводом вашей функции LCS, как и в паре (s1, s2) и (s1, s3):

In [1]: s1 = "this is a foo bar sentence ."

In [3]: s2 = "what the foo bar blah blah black sheep is doing ?"

In [4]: s3 = "what a kappa foo bar black sheep ?"

In [12]: longest_common_substring(s1, s3)
Out[12]: 'a foo bar '

In [13]: longest_common_substring(s1, s2)
Out[13]: ' foo bar '

Как вы можете заметить, если совпадают полные слова, то также совпадают окружающие пробелы.

Затем вы можете изменить функцию до ее вывода, например так:

answer = s1[x_longest - longest: x_longest]
if not (answer.startswith(" ") and answer.endswith(" ")):
    return longest_common_substring(s1, answer[1:])
else:
    return answer

Я уверен, что есть другие крайние случаи, такие как подстрока, появляющаяся в конце строки, рекурсивно вызывающая функцию либо с s1 или же s2ли обрезать answer спереди или сзади и другие - но, по крайней мере, в тех случаях, которые вы показываете, эта простая модификация делает то, что вы хотите:

In [20]: longest_common_substring(s1, s3)
Out[20]: ' foo bar '

Как вы думаете, это направление стоит изучить?

Это более интересная проблема, чем я изначально считал. Когда вы думаете об этом, есть 4 возможных результата.

  1. Тривиальный случай, вся строка соответствует без границ (ваш первый пример)
  2. Пересеките границу слова в начале (ваш второй пример)
  3. Пересечь границу слова в конце
  4. Иметь слово границы на каждом конце

Теперь ваш код заботится о тривиальном случае, чтобы мы могли использовать это; Осталось только обернуть результаты в несколько проверок для других случаев. Так как должны выглядеть эти проверки? Давайте возьмем ваш случай неудачи:

string 1 = "this is a foo bar sentence ."
string 2 = "what a kappa foo bar black sheep ?"
output string = "a foo bar"

Так что из строки find в перспективе мы можем найти все эти буквы в этом порядке в обоих string1 а также string2, но если мы разделим все вокруг пробелов в списки, и искать списки только по порядку string1 будет соответствовать.

Теперь я в основном парень на C, поэтому я хочу написать это в функции:

def full_string(str1, str2, chkstr):
  l1 = str1.split()
  l2 = str2.split()
  chkl = chkstr.split()
  return (any(l1[i:i+len(chkl)]==chkl for i in xrange(len(l1)-len(chkl)+1)) and
          any(l2[i:i+len(chkl)]==chkl for i in xrange(len(l2)-len(chkl)+1)))

С помощью этой функции мы можем проверить, не содержит ли одна из двух строк все слова нашего результата из longest_common_substring(s1, s2) с целью. Отлично. Итак, последний шаг - объединить эти две функции и проверить для каждого из 4 случаев, перечисленных выше:

def longest_whole_substring(s1, s2):
  subs = longest_common_substring(s1, s2)
  if not full_string(s1, s2, subs):
    if full_string(s1, s2, ' '.join(subs.split()[1:])):
      subs = ' '.join(subs.split()[1:])
    elif full_string(s1, s2, ' '.join(subs.split()[:-1])):
      subs = ' '.join(subs.split()[:-1])
    else:
      subs = ' '.join(subs.split()[1:-1])
  return subs

Теперь функция longest_whole_substring(s1, s2) обеспечит самую длинную целую подстроку, не отрубая ни одного слова. Давайте просто проверим это в каждом из случаев:

Trivial:

>>> a = 'this is a foo bar bar foo string'
>>> b = 'foo bar'
>>> 
>>> longest_whole_substring(a,b)
'foo bar'

Граница слова в начале:

>>> b = 's a foo bar'
>>> 
>>> longest_whole_substring(a,b)
'a foo bar '

Слово ограничено в конце:

>>> b = 'foo bar f'
>>> 
>>> longest_whole_substring(a,b)
'foo bar'

И Слово ограничено на обоих концах:

>>> b = 's a foo bar f'
>>> 
>>> longest_whole_substring(a,b)
'a foo bar'

Выглядит хорошо!

Все, что вам нужно сделать, это добавить проверки начала и конца слова.

Затем вы обновите m только для действительного конца матча.

Вот так:

def longest_common_substring(s1, s2):
  m = [[0] * (1 + len(s2)) for i in xrange(1 + len(s1))]
  longest, x_longest = 0, 0
  for x in xrange(1, 1 + len(s1)):
    # current character in s1
    x_char = s1[x - 1]
    # we are at the beginning of a word in s1 if
    #   (we are at the beginning of s1) or 
    #   (previous character is a space)
    x_word_begin = (x == 1) or (s1[x - 2] == " ")
    # we are at the end of a word in s1 if
    #   (we are at the end of s1) or 
    #   (next character is a space)
    x_word_end = (x == len(s1)) or (s1[x] == " ")
    for y in xrange(1, 1 + len(s2)):
      # current character in s2
      y_char = s2[y - 1]
      # we are at the beginning of a word in s2 if
      #   (we are at the beginning of s2) or 
      #   (previous character is a space)
      y_word_begin = (y == 1) or (s2[y - 2] == " ")
      # we are at the end of a word in s2 if
      #   (we are at the end of s2) or 
      #   (next character is a space)
      y_word_end = (y == len(s2)) or (s2[y] == " ")
      if x_char == y_char:
        # no match starting with x_char
        if m[x - 1][y - 1] == 0:
          # a match can start only with a space
          #   or at the beginning of a word
          if x_char == " " or (x_word_begin and y_word_begin):
              m[x][y] = m[x - 1][y - 1] + 1
        else:
          m[x][y] = m[x - 1][y - 1] + 1
        if m[x][y] > longest:
          # the match can end only with a space
          #   or at the end of a word
          if x_char == " " or (x_word_end and y_word_end):
            longest = m[x][y]
            x_longest = x
      else:
        m[x][y] = 0
  return s1[x_longest - longest: x_longest]

Я сделал это рекурсивно:

def common_phrase(self, longer, shorter):
""" recursively find longest common substring, consists of whole words only and in the same order """
if shorter in longer:
    return shorter
elif len(shorter.split()) > 1:
    common_phrase_without_last_word = common_phrase(shorter.rsplit(' ', 1)[0], longer)
    common_phrase_without_first_word = common_phrase(shorter.split(' ', 1)[1], longer)
    without_first_is_longer = len(common_phrase_without_last_word) < len(common_phrase_without_first_word)

    return ((not without_first_is_longer) * common_phrase_without_last_word +
            without_first_is_longer * common_phrase_without_first_word)
else:
    return ''

Просто классифицируйте две строки как "короче" и "длиннее" перед применением:

if len(str1) > len(str2):
    longer, shorter = str1, str2 
else:
    longer, shorter = str2, str1

Просто добавьте условие принятия в ваш код:

s1 = "this is a foo bar sentence ."
s2 = "what the foo bar blah blah black sheep is doing ?"

def longest_common_substring(s1, s2):
  m = [[0] * (1 + len(s2)) for i in xrange(1 + len(s1))]
  longest, x_longest = 0, 0
  for x in xrange(1, 1 + len(s1)):
    for y in xrange(1, 1 + len(s2)):
      if s1[x - 1] == s2[y - 1]:
        m[x][y] = m[x - 1][y - 1] + 1
        if m[x][y] > longest and word_aligned(x, y, m[x][y]):  # acceptance condition
          longest = m[x][y]
          x_longest = x
      else:
        m[x][y] = 0
  return s1[x_longest - longest: x_longest]

def word_aligned(x, y, length):
    """check that a match starting at s1[x - 1] and s2[y - 1] is aligned on a word boundary"""
    # check start of match in s1
    if s1[x - 1].isspace():
        # match doesn't start with a character, reject
        return False
    if x - 2 > 0 and not s1[x - 2].isspace():
        # char before match is not start of line or space, reject
        return False
    # check start of match in s2
    ... same as above ...
    # check end of match in s1
    ... your code is a bit hard for me follow, what is end of match? ...
    # check end of match in s2
    ... same as above ...
    return True

print longest_common_substring(s1, s2)

Одним из эффективных методов поиска самых длинных общих подстрок является дерево суффиксов (см. http://en.wikipedia.org/wiki/Suffix_tree и http://en.wikipedia.org/wiki/Longest_common_substring_problem). Я не вижу причин, по которым вы не могли бы создать дерево суффиксов, используя слова вместо символов, и в этом случае самая длинная общая подпоследовательность, извлеченная из дерева, будет соответствовать границам токенов. Этот подход будет особенно эффективен, если вы хотите найти общие подстроки между одной фиксированной строкой и большим количеством других строк.

Смотрите принятый ответ для python: library для обобщенных деревьев суффиксов для списка реализаций дерева суффиксов Python.

from difflib import SequenceMatcher
def longest_substring(str1, str2):
    # initialize SequenceMatcher object with
    # input string
    # below logic is to make sure word does not get cut
    str1 = " " + str1.strip() + " "
    str2 = " " + str2.strip() + " "
    seq_match = SequenceMatcher(None, str1, str2)

    # find match of longest sub-string
    # output will be like Match(a=0, b=0, size=5)
    match = seq_match.find_longest_match(0, len(str1), 0, len(str2))

    # return longest substring
    if match.size != 0:
        lm = str1[match.a: match.a + match.size]
        # below logic is to make sure word does not get cut
        if not lm.startswith(" "):
            while not (lm.startswith(" ") or len(lm) == 0):
                lm = lm[1:]
        if not lm.endswith(" "):
            while not (lm.endswith(" ") or len(lm) == 0):
                lm = lm[:-1]
        return lm.strip()
    else:
        return ""

Вот способ ngram:

def ngrams(text, n):
  return [text[i:i+n] for i in xrange(len(text)-n)]

def longest_common_ngram(s1, s2):
  s1ngrams = list(chain(*[[" ".join(j) for j in ngrams(s1.split(), i)] 
                          for i in range(1, len(s1.split()))]))
  s2ngrams = list(chain(*[[" ".join(j) for j in ngrams(s2.split(), i)]
                          for i in range(1, len(s2.split()))]))

  return set(s1ngrams).intersection(set(s2ngrams))
Другие вопросы по тегам