Приблизительное сопоставление строк с использованием поиска с возвратом

Я хотел бы использовать поиск с возвратом для поиска всех подстрок в длинной строке, допускающей совпадения переменной длины, то есть совпадения, допускающие максимально заданное количество несоответствий, вставок и удалений. Я не смог найти каких-либо полезных примеров. Самое близкое, что я нашел, это эта статья здесь, но это ужасно сложный. Любой?

Ваше здоровье,

Мартин


person maasha    schedule 26.09.2011    source источник
comment
Примечание. Спасибо за альтернативные предложения, но, пожалуйста, покажите мне еще какой-нибудь элегантный код для поиска с возвратом!   -  person maasha    schedule 27.09.2011


Ответы (3)


Алгоритм

Функция ff() ниже использует рекурсию (т.е. возврат) для решения вашей проблемы. Основная идея состоит в том, что в начале любого вызова f() мы пытаемся сопоставить суффикс t исходной строки "иглы" с суффиксом s строки "стог сена", допуская при этом только определенное количество каждого типа операция редактирования.

// ss is the start of the haystack, used only for reporting the match endpoints.
void f(char* ss, char* s, char* t, int mm, int ins, int del) {
    while (*s && *s == *t) ++s, ++t;    // OK to always match longest segment
    if (!*t) printf("%d\n", s - ss);    // Matched; print endpoint of match
    if (mm && *s && *t) f(ss, s + 1, t + 1, mm - 1, ins, del);
    if (ins && *s) f(ss, s + 1, t, mm, ins - 1, del);
    if (del && *t) f(ss, s, t + 1, mm, ins, del - 1);
}

// Find all occurrences of t starting at any position in s, with at most
// mm mismatches, ins insertions and del deletions.
void ff(char* s, char* t, int mm, int ins, int del) {
    for (char* ss = s; *s; ++s) {
//      printf("Starting from offset %d...\n", s - ss);
        f(ss, s, t, mm, ins, del);
    }
}

Пример вызова:

ff("xxabcydef", "abcdefg", 1, 1, 1);

Это выводит:

9
9

потому что есть два способа найти "abcdefg" в "xxabcydef" с не более чем одним изменением каждого типа, и оба эти способа заканчиваются на позиции 9:

Haystack: xxabcydef-
Needle:     abc-defg

который имеет 1 вставку (из y) и 1 удаление (из g) и

Haystack: xxabcyde-f
Needle:     abc-defg

который имеет 1 вставку (из y), 1 удаление (из f) и 1 замену g на f.

Отношения доминирования

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

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

Haystack: abbbbc
Needle:   a-b-bc

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

Haystack: abbbbc
Needle:   ab--bc

Если вы сделаете это несколько раз, пока не станет невозможно шунтировать символы без замены, вы получите совпадение, в котором наибольший начальный сегмент одинаковых символов из обеих строк выровнен относительно друг друга:

Haystack: abbbbc
Needle:   abb--c

Мой алгоритм найдет все такие совпадения, поэтому он не упустит ни одной позиции совпадения.

Экспоненциальное время

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

person j_random_hacker    schedule 27.09.2011
comment
Нет, только на нескольких примерах, пока это не сработало. f() достаточно прост, чтобы можно было доказать, что он работает, обдумав все возможные комбинации входных данных s и t: (a) s пусто, t нет; (б) t пусто, s нет; (c) оба пусты; (d) ни один из них не пуст и начинается с 1 или более одинаковых символов; (e) ни один из них не пуст и начинается с разных символов. Обратите внимание, что случай (d) преобразуется в один из других случаев с помощью цикла while, что еще больше упрощает ситуацию. - person j_random_hacker; 27.09.2011
comment
Вы продолжаете говорить, что scan_for_matches быстрее, чем другие вещи, но вы не приводите примеры (иголка, стог сена, #mm, #ins, #del), на которых вы фактически тестируете - понимаете ли вы, что скорость полностью зависит от этого? ? - person j_random_hacker; 29.09.2011
comment
Я могу придумать несколько способов ускорить его, не теряя ни одного важного матча. Во-первых, вставка, за которой следует удаление (или наоборот), всегда может быть заменена одной заменой, если mm > 0, поскольку, если нам потребуется эта замена позже в том же совпадении, мы можем просто использовать вместо этого комбинацию вставки + удаления (поскольку , сэкономив по 1 из каждого, у нас еще есть как минимум по одному из каждого, чтобы потратить). Это означает запрет на ins после del и наоборот всякий раз, когда mm > 0. - person j_random_hacker; 29.09.2011
comment
Также мы можем потребовать, чтобы любой непрерывный блок вставок и удалений сначала содержал (скажем) все вставки - это означает запрет вставки после удаления, точка. Наконец, поскольку ff() начинается с каждой позиции по очереди, нас не очень интересуют совпадения, начинающиеся с одной или нескольких вставок (поскольку все они будут найдены ff() с помощью последующих вызовов f()), поэтому f() может принимать дополнительный параметр, allow_ins, говоря ему, стоит ли пробовать прошивки. ff() будет передавать 0 для этого, в то время как все рекурсивные вызовы f() будут передавать 1 для обычного поведения. - person j_random_hacker; 29.09.2011
comment
pastebin.com/hr7y3gz1 исправляет вашу ошибку. Проблема заключалась в том, что в строках 14-16 вы возвращались сразу же, поэтому, например, если можно было попробовать мутацию в заданной позиции (т.е. если условие if строки 14 было выполнено), то ни вставка, ни удаление не были бы предприняты в этой позиции. позиция. Если позже мутация привела к несоответствию, но, скажем, удаление позволило бы найти совпадение, это совпадение было бы упущено. - person j_random_hacker; 06.11.2011
comment
Я плохой (снова) - я пытался поменять местами значения ins и del и облажался. Я написал модульные тесты для всех крайних случаев, и это выглядит хорошо. Кроме того, я готовил здесь свои собственные комментарии, так как это не дискуссионный форум — я предлагаю вам сделать то же самое. Еще раз спасибо за помощь! Моя реализация находится здесь: code.google .com/p/biopieces/source/browse/trunk/code_ruby/lib/ - person maasha; 17.11.2011
comment
Нет проблем :) Но из интереса я подумал, что вас интересует скорость, а Ruby — медленно интерпретируемый язык...? - person j_random_hacker; 17.11.2011
comment
Это компромисс. Ruby для гибкости и Inline-C для скорости. - person maasha; 18.11.2011
comment
А, хорошая идея. Спасибо, что упомянули меня в комментариях; было бы еще лучше, если бы вы дали URL-адрес этого вопроса SO. - person j_random_hacker; 19.11.2011

Я бы начал с алгоритма расстояния Левенштейна, который является стандартным подходом при проверке сходства строк через несоответствие, вставка и удаление.

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

person DaveFar    schedule 26.09.2011
comment
Спасибо, но меня явно интересует, как использовать для этого откат. - person maasha; 26.09.2011
comment
Ок, ик. Но если вас интересуют расстояния String, я бы придерживался динамического программирования. Если вы заинтересованы в откате, я бы нашел более подходящую задачу. Просто мои 2 цента... - person DaveFar; 26.09.2011
comment
Что касается сопоставления шаблонов биологических последовательностей, scan_for_matches blog.theseed.org/ server/2010/07/scan-for-matches.html превосходит все программное обеспечение для динамического программирования, насколько мне известно. scan_for_matches основан на поиске с возвратом. - person maasha; 26.09.2011
comment
Спасибо за разъяснение - я бы не догадался. - person DaveFar; 26.09.2011
comment
@maasha: приблизительный поиск строки на основе возврата занимает время, экспоненциальное по длине шаблона в худшем случае - например, если вы попытаетесь сопоставить шаблон abcd с текстом aabbcce с не более чем 3 вставками или удалениями, каждый из 2 ^ 3 = 8 способов удаления каждого из a, b и c должен быть опробован до конца c != сравнение, даже если это окончательное сравнение означает, что окончательный ответ не совпадает. сканирование на совпадения может быть быстрее в непатологических случаях, которые вы пробовали, и это нормально, но в худшем случае оно медленнее, чем алгоритмы DP. - person j_random_hacker; 27.09.2011
comment
Реализации DP я пробовал logic.at/people/bruno/ В документе Papers/2007-GATE-ESSLLI.pdf (стр. 200 и далее) возникают проблемы, если вы хотите указать несоответствия/вставки/удаления, поскольку некоторые их комбинации никогда не будут учитываться, и вы не получите исчерпывающего результата. - person maasha; 27.09.2011

Самый лучший из известных мне алгоритмов для этого — A Fast Bit- Векторный алгоритм для приблизительного сопоставления строк на основе динамического программирования Джина Майерса. Учитывая текст для поиска длины n, строку шаблона для поиска длиной m и максимальное количество несоответствий/вставок/удалений k, этот алгоритм требует времени O(mn/w), где w — размер слова вашего компьютера (32 или 64). Если вы хорошо разбираетесь в алгоритмах работы со строками, на самом деле невероятно, что существует алгоритм, который требует времени, не зависящего от k — ​​долгое время это казалось недостижимой целью.

Я не знаю о существующей реализации вышеуказанного алгоритма. Если вам нужен инструмент, agrep может быть именно тем, что вам нужно. Он использует более ранний алгоритм, который требует времени O(mnk/w), но он достаточно быстр для низких k, т. е. в милях впереди поиска с возвратом в худшем случае.

agrep основан на алгоритме Shift-Or (или "Bitap"), который является очень умным алгоритм динамического программирования, который может представить свое состояние в виде битов целого числа и заставить двоичное сложение выполнять большую часть работы по обновлению состояния, что ускоряет алгоритм в 32 или 64 раза. над более типичной реализацией. :) Алгоритм Майерса также использует эту идею, чтобы получить коэффициент скорости 1/w.

person j_random_hacker    schedule 26.09.2011
comment
Я протестировал nrgrep, который частично является реализацией битапа, против scan_for_mathches, и победа scan_for_matches (см. другой комментарий). Кроме того, различные реализации (agrep, nrgrep и т. д.) не позволяют указать количество несоответствий, вставок и удалений, а позволяют указать расстояние редактирования. - person maasha; 27.09.2011
comment
Эта статья Джина Майерса также ужасно сложна. Мне действительно нужно увидеть пошаговый пример и простой код прототипа. - person maasha; 27.09.2011