самая длинная общая подстрока в R, находящая несмежные совпадения между двумя строками

У меня есть вопрос о поиске самой длинной общей подстроки в R. Просматривая несколько сообщений в StackOverflow, я узнал о пакете qualV. Однако я вижу, что функция LCS в этом пакете фактически находит все символы из строки 1, которые присутствуют в строке 2, даже если они не являются смежными.

Чтобы объяснить, если строки представляют собой string1 : "hello" string2 : "hel12345lo" я ожидаю, что вывод будет hel, однако я получить вывод как привет. Должно быть, я делаю что-то не так. Пожалуйста, смотрите мой код ниже.

library(qualV)
a= "hello"
b="hel123l5678o" 
sapply(seq_along(a), function(i)
    paste(LCS(substring(a[i], seq(1, nchar(a[i])), seq(1, nchar(a[i]))),
              substring(b[i], seq(1, nchar(b[i])), seq(1, nchar(b[i]))))$LCS,
          collapse = ""))

Я также пробовал метод Rlibstree, но все равно получаю подстроки, которые не являются смежными. Кроме того, длина подстроки также не соответствует моим ожиданиям. См. ниже.

> a = "hello"
> b = "h1e2l3l4o5"

> ll <- list(a,b)
> lapply(data.frame(do.call(rbind, ll), stringsAsFactors=FALSE), function(x) getLongestCommonSubstring(x))
$do.call.rbind..ll.
[1] "h" "e" "l" "o"

> nchar(lapply(data.frame(do.call(rbind, ll), stringsAsFactors=FALSE), function(x) getLongestCommonSubstring(x)))
do.call.rbind..ll.
                21

r lcs
person IAMTubby    schedule 01.02.2015    source источник
comment
Связанный вопрос: stackoverflow.com/q/16196327/602276   -  person Andrie    schedule 01.02.2015
comment
@Andrie, я попробовал метод Rlibstree по ссылке. Однако я все еще получаю подстроки, которые не являются смежными. Также отключена длина соответствующей подстроки. Добавил информацию как РЕДАКТИРОВАТЬ мой исходный пост выше. Пожалуйста, посмотрите.   -  person IAMTubby    schedule 01.02.2015
comment
Чтобы уточнить: функция LCS qualV находит не самую длинную общую подстроку, она находит самую длинную общую подпоследовательность — отсюда и результат, который вы получаете. Это определение подпоследовательности. Эти проблемы связаны, но имеют совершенно разные решения, а самая длинная общая задача subsequence является более классической задачей в информатике и, следовательно, чаще реализуется.   -  person Konrad Rudolph    schedule 01.02.2015


Ответы (5)


Вот три возможных решения.

library(stringi)
library(stringdist)

a <- "hello"
b <- "hel123l5678o"

## get all forward substrings of 'b'
sb <- stri_sub(b, 1, 1:nchar(b))
## extract them from 'a' if they exist
sstr <- na.omit(stri_extract_all_coll(a, sb, simplify=TRUE))
## match the longest one
sstr[which.max(nchar(sstr))]
# [1] "hel"

В базе R также есть adist() и agrep(), а в пакете stringdist есть несколько функций, запускающих метод LCS. Вот посмотрите на stringsidt. Возвращает количество непарных символов.

stringdist(a, b, method="lcs")
# [1] 7

Filter("!", mapply(
    stringdist, 
    stri_sub(b, 1, 1:nchar(b)),
    stri_sub(a, 1, 1:nchar(b)),
    MoreArgs = list(method = "lcs")
))
#  h  he hel 
#  0   0   0 

Теперь, когда я изучил это немного больше, я думаю, что adist() может быть подходящим способом. Если мы установим counts=TRUE, мы получим последовательность совпадений, вставок и т. д. Поэтому, если вы укажете это на stri_locate(), мы сможем использовать эту матрицу для получения совпадений от a до b.

ta <- drop(attr(adist(a, b, counts=TRUE), "trafos")))
# [1] "MMMIIIMIIIIM"

Таким образом, значения M обозначают прямые совпадения. Мы можем пойти и получить подстроки с помощью stri_sub()

stri_sub(b, stri_locate_all_regex(ta, "M+")[[1]])
# [1] "hel" "l"   "o" 

Извините, я не очень хорошо это объяснил, так как я плохо разбираюсь в алгоритмах расстояния между строками.

person Rich Scriven    schedule 01.02.2015
comment
Хотя это работает для коротких строк, это довольно неэффективно (я даже не знаю асимптотической производительности… может быть, O(n^3)?), и есть гораздо более эффективные решения этой проблемы. - person Konrad Rudolph; 01.02.2015
comment
Ну, я не уверен в производительности. Я получил комментарий от OP на один из моих других ответов с просьбой о помощи здесь, поэтому я решил попытаться помочь. - person Rich Scriven; 01.02.2015
comment
@KonradRudolph - Я играл с adist(). Кажется, это, вероятно, путь сюда - person Rich Scriven; 01.02.2015
comment
Для справки, identical(stri_sub(a, 1, 1:nchar(a)), substring(a,1,1:nchar(a))) - person The Red Pea; 28.10.2015
comment
@KonradRudolph Не могли бы вы указать мне более эффективный метод, у меня похожая проблема, но огромный набор данных для ее запуска. - person Vaibhav; 29.08.2018
comment
@Vaibhav Эффективное решение описано на странице en.wikipedia.org/wiki/Longest_common_substring_problem — к сожалению, я не думаю, что реализация для R существует. - person Konrad Rudolph; 29.08.2018

Использование информации @RichardScriven о том, что adist можно использовать ( он вычисляет приблизительное расстояние между строками. Я сделал функцию более полной. Обратите внимание, что "trafos" означает преобразования, используемые для определения расстояния между двумя строками (пример внизу)

ИЗМЕНИТЬ Этот ответ может привести к неправильным/неожиданным результатам; как указано @wdkrnls:

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

Смотрите объяснение неправильного результата ниже. Начнем с функции для получения longest_string в списке:

longest_string <- function(s){return(s[which.max(nchar(s))])}

Затем мы можем использовать работу @RichardSriven и библиотеку stringi:

library(stringi)
lcsbstr <- function(a,b) { 
  sbstr_locations<- stri_locate_all_regex(drop(attr(adist(a, b, counts=TRUE), "trafos")), "M+")[[1]]
  cmn_sbstr<-stri_sub(longest_string(c(a,b)), sbstr_locations)
  longest_cmn_sbstr <- longest_string(cmn_sbstr)
   return(longest_cmn_sbstr) 
}

Или мы можем переписать наш код, чтобы избежать использования каких-либо внешних библиотек (по-прежнему используя родную функцию R adist):

lcsbstr_no_lib <- function(a,b) { 
    matches <- gregexpr("M+", drop(attr(adist(a, b, counts=TRUE), "trafos")))[[1]];
    lengths<- attr(matches, 'match.length')
    which_longest <- which.max(lengths)
    index_longest <- matches[which_longest]
    length_longest <- lengths[which_longest]
    longest_cmn_sbstr  <- substring(longest_string(c(a,b)), index_longest , index_longest + length_longest - 1)
    return(longest_cmn_sbstr ) 
}

Обе приведенные выше функции идентифицируют только 'hello ' как самую длинную общую подстроку вместо 'hello r' (независимо от того, какой аргумент длиннее из двух):

identical('hello',
    lcsbstr_no_lib('hello', 'hello there'), 
    lcsbstr(       'hello', 'hello there'),
    lcsbstr_no_lib('hello there', 'hello'), 
    lcsbstr(       'hello there', 'hello'))

ПОСЛЕДНИЕ ИЗМЕНЕНИЯ Обратите внимание на странное поведение с этим результатом:

lcsbstr('hello world', 'hello')
#[1] 'hell'

Я ожидал 'hello', но поскольку преобразование фактически перемещает (через удаление) o в миреorld, чтобы стать o в адуo -- только ад часть считается совпадением в соответствии с M:

drop(attr(adist('hello world', 'hello', counts=TRUE), "trafos"))
#[1] "MMMMDDDMDDD"
#[1]  vvvv   v
#[1] "hello world"

Такое поведение наблюдается с помощью инструмента Левенштейна — он дает два возможных решения: эквивалентно этим двум преобразованиям

#[1] "MMMMDDDMDDD"
#[1] "MMMMMDDDDDD"

Я не знаю, можем ли мы настроить adist так, чтобы одно решение предпочиталось другому? (преобразования имеют одинаковый вес - одинаковое количество M и D - не знаю, как предпочесть преобразования с большим числом последовательных M)

Наконец, не забывайте, что adist позволяет передавать ignore.case = TRUE (по умолчанию FALSE).

  • Ключ к свойству "trafos" объекта adist; преобразования для перехода от одной строки к другой:

последовательности преобразования возвращаются как атрибут trafos возвращаемого значения, как строки символов с элементами M, I, D и S, указывающими совпадение, вставку, удаление и замену

person The Red Pea    schedule 28.10.2015
comment
Чтобы добавить к вашему решению, если вы знаете, из какой строки - a или b вы хотите выбрать LCS, вы можете добавить grep внутри своей функции с аргументом longest_cmn_sbstr для возврата полной строки. - person Vaibhav; 30.08.2018
comment
Я запустил вашу функцию против яблока и больших яблочных рогаликов, и она вернула appl. Я бы ожидал яблоко. - person wdkrnls; 29.05.2021
comment
Да @wdkrnls, я согласен, что мое решение дольше всего неверно - оно полагается на Левенштейна, который может определить другое решение, включающее УДАЛЕНИЯ (см. Редактирование моего ответа). Это причина, по которой вы получаете appl; по той же причине я получаю этот результат: lcsbstr('hello world', 'hello') #[1] 'hell' Может быть, я могу изменить свое регулярное выражение, чтобы я не искал только последовательные M, но также проверял M (совпадения), охватывающие D (удаления) - person The Red Pea; 30.05.2021

Я не уверен, что вы сделали, чтобы получить "привет". Основываясь на приведенных ниже экспериментах методом проб и ошибок, кажется, что функция LCS (а) не будет рассматривать строку как LCS, если символ следует за тем, что в противном случае было бы LCS; (b) найти несколько LCS одинаковой длины (в отличие от функции sub(), которая находит только первую); (c) порядок элементов в строках не имеет значения, что не имеет иллюстраций ниже; и (b) порядок строки в вызове LCS не имеет значения — также не показано.

Итак, ваше «привет» a не имело LCS в b, так как за «hel» b следовал символ. Ну, это моя текущая гипотеза.

Пункт А выше:

a= c("hello", "hel", "abcd")
b= c("hello123l5678o", "abcd") 
print(LCS(a, b)[4]) # "abcd" - perhaps because it has nothing afterwards, unlike hello123...

a= c("hello", "hel", "abcd1") # added 1 to abcd
b= c("hello123l5678o", "abcd") 
print(LCS(a, b)[4]) # no LCS!, as if anything beyond an otherwise LCS invalidates it

a= c("hello", "hel", "abcd") 
b= c("hello1", "abcd") # added 1 to hello
print(LCS(a, b)[4]) # abcd only, since the b hello1 has a character

Пункт Б выше:

a= c("hello", "hel", "abcd") 
b= c("hello", "abcd") 
print(LCS(a, b)[4]) # found both, so not like sub vs gsub of finding first or all
person lawyeR    schedule 01.02.2015
comment
Извините, lawyeR, я не смог до конца понять. Я ищу функцию, которая принимает две строки в качестве аргументов и возвращает подстроку максимальной длины, которая является общей для них двух. Я немного запутался, читая сообщение выше. - person IAMTubby; 01.02.2015
comment
Я объяснял, что LCS может и не может делать. - person lawyeR; 01.02.2015
comment
LawyeR, О, хорошо! Но просто чтобы уточнить, есть ли лучший способ найти самую длинную общую подстроку между ними? - person IAMTubby; 01.02.2015

Использование биострок:

library(Biostrings)
a= "hello"
b="hel123l5678o"
astr= BString(a)
bstr=BString(b)

pmatchPattern(astr, bstr)

возвращает:

  Views on a 12-letter BString subject
Subject: hel123l5678o
views:
      start end width
  [1]     1   3     3 [hel]
  Views on a 5-letter BString pattern
Pattern: hello
views:
      start end width
  [1]     1   3     3 [hel]

Итак, я провел тест, и хотя мой ответ действительно помогает и дает вам гораздо больше информации, он примерно в 500 раз медленнее, чем @Rich Scriven, лол.

system.time({
a= "hello"
b="123hell5678o"
rounds=100
for (i in 1:rounds) {
astr= BString(a)
bstr=BString(b)
pmatchPattern(astr, bstr)
}
})

system.time({
  c= "hello"
  d="123hell5678o"
  rounds=100
  for (i in 1:rounds) {
ta <- drop(attr(adist(c, d, counts=TRUE), "trafos"))
stri_sub(d, stri_locate_all_regex(ta, "M+")[[1]])
}
})

   user  system elapsed 
  2.476   0.027   2.510 

   user  system elapsed 
  0.006   0.000   0.005 
person kana    schedule 31.05.2021

person    schedule
comment
Это будет работать в случаях, когда подстрока начинается с начала обеих строк, однако, если подстрока находится между какой-либо строкой, это не удастся. - person Vaibhav; 30.08.2018
comment
Да, ты прав. Но если вы считаете, что подстрока находится между какой-то строкой, вы можете получить несколько выходов для каждой пары. И можно адаптировать код для получения первой строки, совпадающей с некоторой строкой. - person Juan Antonio Roldán Díaz; 30.08.2018