Самая длинная общая подпоследовательность ( LCS ) — это самая длинная подпоследовательность , общая для всех последовательностей в наборе последовательностей (часто всего двух последовательностей). Она отличается от самой длинной общей подстроки : в отличие от подстрок, подпоследовательности не обязаны занимать последовательные позиции в исходных последовательностях. Задача вычисления самой длинной общей подпоследовательности — это классическая задача компьютерной науки , основа программ сравнения данных , таких как diff
утилита , и имеет приложения в вычислительной лингвистике и биоинформатике . Она также широко используется системами контроля версий, такими как Git, для согласования множественных изменений, внесенных в контролируемую ревизией коллекцию файлов.
Например, рассмотрим последовательности (ABCD) и (ACBAD). У них есть пять общих подпоследовательностей длины 2: (AB), (AC), (AD), (BD) и (CD); две общие подпоследовательности длины 3: (ABD) и (ACD); и больше нет общих подпоследовательностей. Таким образом, (ABD) и (ACD) являются их самыми длинными общими подпоследовательностями.
Для общего случая произвольного числа входных последовательностей задача является NP-трудной . [1] Когда число последовательностей постоянно, задача решается за полиномиальное время с помощью динамического программирования .
При наличии последовательностей длин наивный поиск будет проверять каждую из подпоследовательностей первой последовательности, чтобы определить, являются ли они также подпоследовательностями оставшихся последовательностей; каждая подпоследовательность может быть проверена за время, линейно зависящее от длин оставшихся последовательностей, поэтому время для этого алгоритма будет
Для случая двух последовательностей из n и m элементов время выполнения подхода динамического программирования составляет O ( n × m ). [2] Для произвольного числа входных последовательностей подход динамического программирования дает решение за
Существуют методы с меньшей сложностью [3], которые часто зависят от длины LCS, размера алфавита или и того, и другого.
LCS не обязательно уникален; в худшем случае число общих подпоследовательностей экспоненциально зависит от длины входных данных, поэтому алгоритмическая сложность должна быть как минимум экспоненциальной. [4]
Задача LCS имеет оптимальную подструктуру : задача может быть разбита на более мелкие, более простые подзадачи, которые, в свою очередь, могут быть разбиты на более простые подзадачи, и так далее, пока, наконец, решение не станет тривиальным. В частности, LCS имеет перекрывающиеся подзадачи : решения высокоуровневых подзадач часто повторно используют решения низкоуровневых подзадач. Задачи с этими двумя свойствами поддаются подходам динамического программирования , в которых решения подзадач запоминаются , то есть решения подзадач сохраняются для повторного использования.
Префикс S n слова S определяется как первые n символов слова S. [ 5] Например, префиксы слова S = (AGCA) следующие:
Пусть LCS ( X , Y ) — функция, которая вычисляет самую длинную подпоследовательность, общую для X и Y. Такая функция обладает двумя интересными свойствами.
LCS ( X ^ A , Y ^ A ) = LCS ( X , Y )^ A , для всех строк X , Y и всех символов A , где ^ обозначает конкатенацию строк. Это позволяет упростить вычисление LCS для двух последовательностей, заканчивающихся одним и тем же символом. Например, LCS ("BANANA","ATANA") = LCS ("BANAN","ATAN")^"A", Продолжая для оставшихся общих символов, LCS ("BANANA","ATANA") = LCS ("BAN","AT")^"ANA".
Если A и B — различные символы ( A ≠ B ), то LCS (X^A,Y^B) — одна из строк максимальной длины в наборе { LCS ( X ^ A , Y ), LCS ( X , Y ^ B ) } для всех строк X , Y .
Например, LCS ("ABCDEFG","BCDGK") является самой длинной строкой среди LCS ("ABCDEFG","BCDG") и LCS ("ABCDEF","BCDGK"); если бы обе имели одинаковую длину, одну из них можно было бы выбрать произвольно.
Для реализации имущества различают два случая:
Пусть две последовательности определены следующим образом: и . Префиксами являются ; префиксами являются . Пусть представляет собой набор наиболее длинных общих подпоследовательностей префиксов и . Этот набор последовательностей задается следующим образом.
Чтобы найти НКП и , сравните и . Если они равны, то последовательность расширяется на этот элемент, . Если они не равны, то сохраняется самая длинная из двух последовательностей, , и . (Если они имеют одинаковую длину, но не идентичны, то сохраняются обе.) Базовым случаем, когда либо или пусто, является пустая строка , .
Будет найдена самая длинная подпоследовательность, общая для R = (GAC) и C = (AGCAT). Поскольку функция LCS использует «нулевой» элемент, удобно определить нулевые префиксы, которые являются пустыми для этих последовательностей: R 0 = ε; и C 0 = ε. Все префиксы помещаются в таблицу с C в первой строке (делая ее заголовком столбца ) и R в первом столбце (делая ее заголовком строки ).
ε | А | Г | С | А | Т | |
---|---|---|---|---|---|---|
ε | ε | ε | ε | ε | ε | ε |
Г | ε | |||||
А | ε | |||||
С | ε |
Эта таблица используется для хранения последовательности LCS для каждого шага расчета. Второй столбец и вторая строка были заполнены ε, поскольку при сравнении пустой последовательности с непустой последовательностью самая длинная общая подпоследовательность всегда является пустой последовательностью.
LCS ( R 1 , C 1 ) определяется путем сравнения первых элементов в каждой последовательности. G и A не одинаковы, поэтому эта LCS получает (используя «второе свойство») самую длинную из двух последовательностей, LCS ( R 1 , C 0 ) и LCS ( R 0 , C 1 ). Согласно таблице, обе они пусты, поэтому LCS ( R 1 , C 1 ) также пуста, как показано в таблице ниже. Стрелки указывают, что последовательность исходит как из ячейки выше, LCS ( R 0 , C 1 ), так и из ячейки слева, LCS ( R 1 , C 0 ).
LCS ( R 1 , C 2 ) определяется путем сравнения G и G. Они совпадают, поэтому G добавляется к верхней левой последовательности, LCS ( R 0 , C 1 ), которая является (ε), давая (εG), которая является (G).
Для LCS ( R 1 , C 3 ), G и C не совпадают. Последовательность выше пуста; та, что слева, содержит один элемент, G. Выбираем самую длинную из них, LCS ( R 1 , C 3 ) — это (G). Стрелка указывает влево, так как это самая длинная из двух последовательностей.
LCS ( R 1 , C 4 ) также является (G).
LCS ( R 1 , C 5 ) также является (G).
ε | А | Г | С | А | Т | |
---|---|---|---|---|---|---|
ε | ε | ε | ε | ε | ε | ε |
Г | ε | ε | (Г) | (Г) | (Г) | (Г) |
А | ε | |||||
С | ε |
Для LCS ( R 2 , C 1 ) A сравнивается с A. Два элемента совпадают, поэтому A добавляется к ε, что дает (A).
Для LCS ( R 2 , C 2 ) A и G не совпадают, поэтому используется самая длинная из LCS ( R 1 , C 2 ), которая является (G), и LCS ( R 2 , C 1 ), которая является (A). В этом случае каждая из них содержит один элемент, поэтому этой LCS даются две подпоследовательности: (A) и (G).
Для LCS ( R 2 , C 3 ) A не соответствует C. LCS ( R 2 , C 2 ) содержит последовательности (A) и (G); LCS( R 1 , C 3 ) — это (G), которая уже содержится в LCS ( R 2 , C 2 ). Результатом является то, что LCS ( R 2 , C 3 ) также содержит две подпоследовательности, (A) и (G).
Для LCS ( R 2 , C 4 ) A соответствует A, который добавляется к верхней левой ячейке, давая (GA).
Для LCS ( R 2 , C 5 ) A не соответствует T. Сравнивая две последовательности, (GA) и (G), самая длинная — (GA), поэтому LCS ( R 2 , C 5 ) — это (GA).
ε | А | Г | С | А | Т | |
---|---|---|---|---|---|---|
ε | ε | ε | ε | ε | ε | ε |
Г | ε | ε | (Г) | (Г) | (Г) | (Г) |
А | ε | (А) | (А) и (Г) | (А) и (Г) | (ГА) | (ГА) |
С | ε |
Для LCS ( R 3 , C 1 ) C и A не совпадают, поэтому LCS ( R 3 , C 1 ) получает самую длинную из двух последовательностей, (A).
Для LCS ( R 3 , C 2 ), C и G не совпадают. Как LCS ( R 3 , C 1 ), так и LCS ( R 2 , C 2 ) имеют по одному элементу. Результатом является то, что LCS ( R 3 , C 2 ) содержит две подпоследовательности, (A) и (G).
Для LCS ( R 3 , C 3 ) C и C совпадают, поэтому C добавляется к LCS ( R 2 , C 2 ), которая содержит две подпоследовательности, (A) и (G), что дает (AC) и (GC).
Для LCS ( R 3 , C 4 ) не совпадают C и A. Объединение LCS ( R 3 , C 3 ), который содержит (AC) и (GC), и LCS ( R 2 , C 4 ), который содержит (GA), дает в общей сложности три последовательности: (AC), (GC) и (GA).
Наконец, для LCS ( R 3 , C 5 ) не совпадают C и T. В результате LCS ( R 3 , C 5 ) также содержит три последовательности: (AC), (GC) и (GA).
ε | А | Г | С | А | Т | |
---|---|---|---|---|---|---|
ε | ε | ε | ε | ε | ε | ε |
Г | ε | ε | (Г) | (Г) | (Г) | (Г) |
А | ε | (А) | (А) и (Г) | (А) и (Г) | (ГА) | (ГА) |
С | ε | (А) | (А) и (Г) | (AC) и (GC) | (AC) и (GC) и (GA) | (AC) и (GC) и (GA) |
Конечный результат заключается в том, что последняя ячейка содержит все самые длинные подпоследовательности, общие для (AGCAT) и (GAC); это (AC), (GC) и (GA). Таблица также показывает самые длинные общие подпоследовательности для каждой возможной пары префиксов. Например, для (AGC) и (GA) самые длинные общие подпоследовательности — это (A) и (G).
Расчет LCS строки таблицы LCS требует только решений для текущей строки и предыдущей строки. Тем не менее, для длинных последовательностей эти последовательности могут стать многочисленными и длинными, требуя много места для хранения. Место для хранения можно сэкономить, сохраняя не сами подпоследовательности, а длину подпоследовательности и направление стрелок, как в таблице ниже.
ε | А | Г | С | А | Т | |
---|---|---|---|---|---|---|
ε | 0 | 0 | 0 | 0 | 0 | 0 |
Г | 0 | 0 | 1 | 1 | 1 | 1 |
А | 0 | 1 | 1 | 1 | 2 | 2 |
С | 0 | 1 | 1 | 2 | 2 | 2 |
Фактические подпоследовательности выводятся в процедуре «обратного отслеживания», которая следует за стрелками в обратном направлении, начиная с последней ячейки в таблице. Когда длина уменьшается, последовательности должны иметь общий элемент. Возможны несколько путей, когда в ячейке показаны две стрелки. Ниже приведена таблица для такого анализа с числами, окрашенными в ячейки, где длина собирается уменьшаться. Жирные числа прослеживают последовательность, (GA). [6]
ε | А | Г | С | А | Т | |
---|---|---|---|---|---|---|
ε | 0 | 0 | 0 | 0 | 0 | 0 |
Г | 0 | 0 | 1 | 1 | 1 | 1 |
А | 0 | 1 | 1 | 1 | 2 | 2 |
С | 0 | 1 | 1 | 2 | 2 | 2 |
Для двух строк и длина кратчайшей общей суперпоследовательности связана с длиной LCS соотношением [3]
Расстояние редактирования , когда разрешены только вставка и удаление (без замены) или когда стоимость замены в два раза превышает стоимость вставки или удаления, составляет:
Функция ниже принимает в качестве входных данных последовательности X[1..m]
и Y[1..n]
, вычисляет НКП между X[1..i]
и Y[1..j]
для всех 1 ≤ i ≤ m
и 1 ≤ j ≤ n
и сохраняет их в C[i,j]
. C[m,n]
будет содержать длину НКП X
и Y
. [7]
функция LCSLength(X[1..m], Y[1..n]) C = массив(0..m, 0..n) для i := 0..m С[я,0] = 0 для j := 0..n С[0,j] = 0 для i := 1..m для j := 1..n если X[i] = Y[j] С[i,j] := С[i-1,j-1] + 1 еще C[i,j] := макс(C[i,j-1], C[i-1,j]) вернуть C[м,н]
В качестве альтернативы можно использовать мемоизацию .
Следующая функция отслеживает выборы, сделанные при вычислении C
таблицы. Если последние символы в префиксах равны, они должны быть в LCS. Если нет, проверьте, что дало наибольшее LCS, оставив и , и сделайте тот же выбор. Просто выберите один, если они были одинаковой длины. Вызовите функцию с и .i=m
j=n
функция backtrack(C[0..m,0..n], X[1..m], Y[1..n], i, j) если i = 0 или j = 0 вернуть "" если X[i] = Y[j] вернуть backtrack(C, X, Y, i-1, j-1) + X[i] если C[i,j-1] > C[i-1,j] вернуть backtrack(C, X, Y, i, j-1) вернуть backtrack(C, X, Y, i-1, j)
Если выбор и даст одинаково длинный результат, зачитайте обе полученные подпоследовательности. Это возвращается как набор этой функцией. Обратите внимание, что эта функция не является полиномиальной, так как она может разветвляться почти на каждом шаге, если строки похожи.
функция backtrackAll(C[0..m,0..n], X[1..m], Y[1..n], i, j) если i = 0 или j = 0 вернуть {""} если X[i] = Y[j] вернуть {Z + X[i] для всех Z в backtrackAll(C, X, Y, i-1, j-1)} Р := {} если C[i,j-1] ≥ C[i-1,j] R := backtrackAll(C, X, Y, i, j-1) если C[i-1,j] ≥ C[i,j-1] R := R ∪ backtrackAll(C, X, Y, i-1, j) возврат R
Эта функция будет возвращаться по матрице C и выводить разницу между двумя последовательностями. Обратите внимание, что вы получите другой ответ, если поменяете местами ≥
и <
, с >
и ≤
ниже.
функция printDiff(C[0..m,0..n], X[1..m], Y[1..n], i, j) если i >= 0 и j >= 0 и X[i] = Y[j] printDiff(C, X, Y, i-1, j-1) распечатать " " + X[i] иначе, если j > 0 и (i = 0 или C[i,j-1] ≥ C[i-1,j]) printDiff(C, X, Y, i, j-1) распечатать "+ " + Y[j] иначе, если i > 0 и (j = 0 или C[i,j-1] < C[i-1,j]) printDiff(C, X, Y, i-1, j) распечатать "- " + X[i] еще печать ""
Пусть будет « » и будет « ». Самая длинная общая подпоследовательность между и — это « ». Таблица , показанная ниже, которая сгенерирована функцией , показывает длины самых длинных общих подпоследовательностей между префиксами и . Строка th и столбец th показывают длину LCS между и .XMJYAUZ
MZJAWXU
MJAU
C
LCSLength
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ||
---|---|---|---|---|---|---|---|---|---|
ε | М | З | Дж. | А | Вт | Х | У | ||
0 | ε | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | Х | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
2 | М | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
3 | Дж. | 0 | 1 | 1 | 2 | 2 | 2 | 2 | 2 |
4 | И | 0 | 1 | 1 | 2 | 2 | 2 | 2 | 2 |
5 | А | 0 | 1 | 1 | 2 | 3 | 3 | 3 | 3 |
6 | У | 0 | 1 | 1 | 2 | 3 | 3 | 3 | 4 |
7 | З | 0 | 1 | 2 | 2 | 3 | 3 | 3 | 4 |
Выделенные числа показывают путь, по которому функция будет следовать из нижнего правого вbacktrack
верхний левый угол при считывании LCS. Если текущие символы в и равны, они являются частью LCS, и мы идем как вверх, так и влево (выделено жирным шрифтом ). Если нет, мы идем вверх или влево, в зависимости от того, какая ячейка имеет большее число. Это соответствует либо взятию LCS между и , либо и .
В приведенный выше алгоритм можно внести ряд оптимизаций, чтобы ускорить его работу в реальных ситуациях.
Матрица C в наивном алгоритме растет квадратично с длиной последовательностей. Для двух последовательностей из 100 элементов потребуется матрица из 10 000 элементов и потребуется выполнить 10 000 сравнений. В большинстве реальных случаев, особенно при diff-сравнениях и исправлениях исходного кода, начало и конец файлов редко меняются, и почти наверняка не оба одновременно. Если в середине последовательности изменилось всего несколько элементов, начало и конец можно исключить. Это снижает не только требования к памяти для матрицы, но и количество сравнений, которые необходимо выполнить.
функция LCS(X[1..m], Y[1..n]) начало := 1 m_end := m n_end := n обрезать соответствующие элементы в начале , пока начало ≤ m_end и начало ≤ n_end и X[start] = Y[start] начало := начало + 1 обрезать соответствующие элементы в конце , пока начало ≤ m_end и начало ≤ n_end и X[m_end] = Y[n_end] m_end := m_end - 1 n_end := n_end - 1 C = массив(начало-1..m_конец, начало-1..n_конец) цикл только по измененным элементам для i := start..m_end для j := start..n_end алгоритм продолжается, как и прежде ...
В лучшем случае, если последовательность не изменяется, эта оптимизация устранит необходимость в матрице C. В худшем случае, если изменяются самые первый и последний элементы последовательности, выполняются только два дополнительных сравнения.
Большая часть времени, затрачиваемого наивным алгоритмом, тратится на выполнение сравнений между элементами последовательностей. Для текстовых последовательностей, таких как исходный код, вы хотите рассматривать строки как элементы последовательности, а не отдельные символы. Это может означать сравнение относительно длинных строк для каждого шага алгоритма. Можно выполнить две оптимизации, которые помогут сократить время, затрачиваемое на эти сравнения.
Для уменьшения размера строк в последовательностях можно использовать хэш -функцию или контрольную сумму . То есть, для исходного кода, где средняя длина строки составляет 60 или более символов, хэш или контрольная сумма для этой строки может быть длиной всего от 8 до 40 символов. Кроме того, рандомизированная природа хэшей и контрольных сумм гарантирует, что сравнения будут выполняться быстрее, поскольку строки исходного кода редко будут изменяться в начале.
У этой оптимизации есть три основных недостатка. Во-первых, необходимо потратить некоторое время заранее на предварительный расчет хэшей для двух последовательностей. Во-вторых, необходимо выделить дополнительную память для новых хэшированных последовательностей. Однако, по сравнению с наивным алгоритмом, используемым здесь, оба этих недостатка относительно минимальны.
Третий недостаток — коллизии . Поскольку контрольная сумма или хэш не гарантированно будут уникальными, есть небольшая вероятность того, что два разных элемента могут быть сведены к одному и тому же хешу. Это маловероятно в исходном коде, но это возможно. Поэтому криптографический хэш будет гораздо лучше подходить для этой оптимизации, так как его энтропия будет значительно больше, чем у простой контрольной суммы. Однако преимущества могут не стоить настройки и вычислительных требований криптографического хеша для небольших длин последовательностей.
Если требуется только длина LCS, матрицу можно свести к матрице или к вектору, поскольку подход динамического программирования требует только текущий и предыдущий столбцы матрицы. Алгоритм Хиршберга позволяет построить оптимальную последовательность за то же квадратичное время и линейные границы пространства. [8]
Чоудхури и Рамачандран разработали квадратичный по времени линейно-пространственный алгоритм [9] [10] для нахождения длины LCS вместе с оптимальной последовательностью, который на практике работает быстрее, чем алгоритм Хиршберга, благодаря своей превосходной производительности кэша. [9] Алгоритм имеет асимптотически оптимальную сложность кэша в рамках идеальной модели кэша . [11] Интересно, что сам алгоритм не обращает внимания на кэш [11], что означает, что он не делает никаких выборов на основе параметров кэша (например, размера кэша и размера строки кэша) машины.
Существует несколько алгоритмов, которые работают быстрее, чем представленный подход динамического программирования. Одним из них является алгоритм Ханта–Шиманского , который обычно работает за время (для ), где — количество совпадений между двумя последовательностями. [12] Для задач с ограниченным размером алфавита можно использовать метод четырех русских , чтобы сократить время работы алгоритма динамического программирования на логарифмический множитель. [13]
Начиная с Chvátal & Sankoff (1975), [14] ряд исследователей изучали поведение длины самой длинной общей подпоследовательности, когда две заданные строки выбираются случайным образом из одного и того же алфавита. Когда размер алфавита постоянен, ожидаемая длина LCS пропорциональна длине двух строк, а константы пропорциональности (зависящие от размера алфавита) известны как константы Chvátal–Sankoff . Их точные значения неизвестны, но были доказаны верхние и нижние границы их значений, [15] и известно, что они растут обратно пропорционально квадратному корню из размера алфавита. [16] Было показано, что упрощенные математические модели задачи о самой длинной общей подпоследовательности контролируются распределением Трейси–Уидома . [17]
{{cite book}}
: CS1 maint: multiple names: authors list (link)