Нахождение максимальной подстроки для двух строк

Popoff

popoff.donetsk.ua
Нахождение максимальной подстроки для двух строк

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

Сейчас у меня реализован алгоритм Левенштейна (ru.wikipedia.org/...) со слежением, что там как менялось (то есть, не просто число вычисляется, как в оригинале, а ещё отслеживается, как это число получилось и это позволяет сказать, что и как поменялось в статьях). Сложность алгоритма - О(m*n). То есть, если в статье 1000 строк (а это не самая большая статья) и там поменяли один символ, то, чтобы это определить, нужно, грубо говоря, выполнить миллион операций. Скорость работы такая, что реально за 30 секунд успевает выполниться сравнение статей объёмом максимум 700-800 строк.

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

Предполагаемое улучшение, которое можно было бы ввести в простейшем случае - это найти наибольшую общую подстроку (например, назовём её А) среди двух строк (назовём их Y и Z), вычеркнуть эту подстроку из этих двух строк (то есть рассматриваем эти две строки как конкатенацию Y==BAC и Z==DAE, где B,C,D и E - некоторые строки) и дальше отдельно сравнивать B c D и C c E. Это простое, я считаю, логичное улучшение.

Улучшение тут состоит не только в том, что из исходные строки становятся короче из-за удаления повтояющихся последовательностей, но также и в том, что задача разбивается на две независимые подзадачи, так что эти две задачи последовательно решаются быстрее, чем если бы решалась одна общая задача. Дальше, эти две подзадачи тоже можно решать с этим же улучшением - то есть, рекурсивно, снова искать в новых строках новую наибольшую общую часть, вычеркивать, разбивать на подзадачи и т.п. Это, я считаю, тоже просто.

В PEAR есть процедуры для поиска наибольшей подстроки, но сложность реализованных там процедур равна O(m*n), то есть, только чтобы найти наибольшую подстроку, уже нужно m*n операций сделать - не считая того, что дальше ещё потом те кусочки тоже сравнивать надо.

Если представить все возможные подстроки (в данном случае интересуют только все возможные суффиксы) второй строки (ещё лучше - той, которая длиннее) в виде дерева и проверять вхождение подстроки уже с использованием дерева (то есть, если в дереве найден суффикс, для которого искомая подстрока первой строки была бы префиксом, то значит эта подстрока является общей частью этих двух строк), а не прямым перебором во второй строке, то сложность алгоритма можно было бы уменьшить до O(m*log(n)). Не считая времени на построение того дерева; вместе с простейшим двоичным деревом без всяких штук, описанных ниже, получится что-то типа O((m+n)*log(n)) - тоже, в общем-то неплохо. Сравнивая те же две статьи по 1000 строк, уже нужно будет выполнить не миллион операций, а каких-то ~20 000.

Я начал копать в сторону суффиксных деревьев (ссылка 1, ссылка 2) - они, вроде как, обещают скорость поиска наибольшей подстроки, равной О(m+n). Я, в общем-то разобрался, как те деревья строятся, но в ступор меня вогнал третий снизу абзац по первой ссылке:
Аналогично можно определить самую длинную общую подстроку двух строк x и y, за время O(m + n) для фиксированного алфавита. В этом случае строится суффиксное дерево для строки x#y$, где символы # и $ не принадлежат алфавиту, строками над которым являются x и y. И снова, требуемый результат дается подстрокой максимальной длины, представляемой внутренним узлом суффиксного дерева.
Фактически, они предлагают найти самую большую повторяющуюся последовательность в строке x#y$ и говорят, что это - и будет наибольшая общая последовательность символов между x и y.

Мне кажется, это не совсем так. Если, к примеру, в y будет наибольшая повторяющаяся последовательность из 3 символов (например, y=abcdebcdf, наибольшая повторяющаяся последовательность = bcd), а строка х, к примеру, будет состоять всего из 2 символов (например, ab), то предложенный ими способ вернёт те три символа из y (наибольшая повторяющаяся последовательность для строки ab#abcdebcdf$ - всё та же bcd), которых в х и впомине нету.

Хотя, конечно, может, я не уловил какой-то не сказанный явно смысл или недостаточно внимательно изучил ту статью, или ещё чего-то не так понял. Я, конечно, найду решение и сам, или, на худой конец, удовлетворюсь O((m+n)*log(n)). Но если кому-то интересно помыслить над этой задачкой и решить её со сложностью O(m+n), то я был бы рад помощи. Ну, или, может, кто-то знает другие алгоритмы.
 

fixxxer

К.О.
Партнер клуба
ммм, а чем стандартный алгоритм поиска наибольшей общей последовательности из diff не подошел? я исходники давно смотрел, но насколько помню там достаточно много оптимизаций.
 

Popoff

popoff.donetsk.ua
На самом деле, мой вопрос - не в том, как решить мою задачу быстро, а в том, правильно ли я понял автора той статьи, и возможно ли вообще решить задачу поиска наибольшей общей подстроки за время O(m+n).

-~{}~ 13.11.07 02:47:

Там в диффе стоит просто ограничение на наибольшую неодинаковую последовательность, после встречи с которой алгоритм считает, что всё разное.
 

Pigmeich

Новичок
Popoff
Статью я еще посомтрю, но могу подсказать как уменьшают полином времени - бьют текст на осмысленные куски. Для кода - это строки, для статей - абзацы. Далее сравнивают не набор символов, а набор строк и уже потом, в отличающихся строках ищут различия (если это вообще надо).
 

Жигaн

Новичок
Аналогично можно определить самую длинную общую подстроку двух строк x и y, за время O(m + n) для фиксированного алфавита. В этом случае строится суффиксное дерево для строки x#y$, где символы # и $ не принадлежат алфавиту, строками над которым являются x и y. И снова, требуемый результат дается подстрокой максимальной длины, представляемой внутренним узлом суффиксного дерева.
тут они не сказали, что искомый узел должен иметь потомков с текстом *#* и *$ т.е. *#* - суффикс первой строки, *$ - второй.

За O(n+m) можно сделать(точнее там будет два шага по O(n+m), но все равно O(n+m)). Я делал для множества строк, для двух еще проще:

1) строим suffix tree для двух строк (s1, s2)
2) метим каждый узел у которого есть листья представляющие суффикс для s1 и s2(в листах надо хранить номер строки 1 или 2)
3) обходим дерево, ищем самый длинный префикс среди меченных узлов.
 

Popoff

popoff.donetsk.ua
Вот, если кому интересно, реализация суффиксных деревьев в РНР:
http://popoff.donetsk.ua/text/work/libs/a/suffix.html

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

дополнительно, в оригинале статьи я нашёл, что не нужно в дерево добавлять строку x#y$, а следует добавить две отдельные строки: x# и y$. это имеет значение, так как во втором случае в листе по последнему символу можно определить, там # или $, а в первом случае (то есть, если добавлять одну общую длинную строку) - нужно было бы либо хранить терминальный символ отдельно, либо как-то ещё по-особому его вычислять, так как во всех листьях последним символом был бы $.
 

dark-demon

d(^-^)b
можно попробовать пятиуровневое разбиение:
1. главы
2. абзацы
3. предложения
4. слова (разбиение по границам слов)
5. символы

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

phpdev2007

Новичок
если статьи будут большими то врядли получится сделать на php, для генной инженерии он не подходит :)
Хотя если без шуток, то попробовать переписать вашу реализацию с рhp на си, можно как расширения для php, хотя для детального анализа нужно использовать отладчик для чёткого выявления узких мест и анализа возможно ли его оптимизировать на си.
 

Popoff

popoff.donetsk.ua
ну, всё, может, не так просто, когда нужно сравнить не просто две статьи, а два HTML-документа. разве что на абзацы удаётся разбить, а абзацев бывает очень много. и там дополнительная проблема с тем, что похожие абзацы хорошо бы рядом друг с другом расположить, а не просто по признаку равны-не равны сравнивать...

-~{}~ 26.11.07 00:11:

если кому интересно - вот результаты некоторых тестов для реализованной мной операции сравнения:

1. скорость сравнения одинаковых и полностью абсолютно разных последовательностей - одинакова и линейна. последовательности из 40000 элементов сраниваются за 10 секунд. это, логично, так как эти два крайних случая определяются быстро по суффиксному дереву - в первом случае определяется быстро, что эти две строки полностью образуют наибольшую общую последовательность, а во втором - что длина наибольшей общей последовательности меньше 2.

2. если сравнивать между собой последовательности, в которых изменено от 1 до примерно 100 элементов, то при увеличении изменений время сравнения возрастает по (на глазок) логарифмическому закону. это, в общем-то и логично, так как задача делится на подзадачи, которые по очереди решаются быстрее, чем решалась бы одна общая задача. при 100 изменениях при сравнении последовательностей на 10000 элементов время - 15 секунд, 20000 - 30, 30000 - 45 секунд. в среднем.

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

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

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

-~{}~ 26.11.07 00:31:

вот тут несколько рисунков
http://popoff.donetsk.ua/text/work/libs/a/a.compare.html
 

Pigmeich

Новичок
Popoff
Спасибо за развертый пример.

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

dark-demon

d(^-^)b
дык если есть три абзаца, которые не равны двум исходным - нужно разбить их всех на предложения и сравнивать уже их. в изменившихся абзацах скорее всего останутся неизменившиеся предложения. думаю в большинстве случаев это даст хорошую экономию, если, конечно, пользователь не начнёт изменять по одной букве в каждом слове ;-)
 

phpdev2007

Новичок
Popoff
есть ли похожие системы, даже не написанные на php? например сравнения отличий на svn подходит под ваши требования?
 
Сверху