Задача о наибольшей общей подпоследовательности — различия между версиями
Gostkin (обсуждение | вклад) м (LCS3 дает WA если сравнивать x[i] и y[i], нужно сравнивать x[i] и y[j]) |
Taras ska (обсуждение | вклад) (Добавлена постановка задачи алгоритма Хиршберга и его описание) |
||
| Строка 187: | Строка 187: | ||
Аналогичным образом задача решается для <tex>k</tex> строк. Заполняется <tex>k</tex>-мерная динамика. | Аналогичным образом задача решается для <tex>k</tex> строк. Заполняется <tex>k</tex>-мерная динамика. | ||
| + | |||
| + | == Алгоритм Хиршберга == | ||
| + | |||
| + | {{Задача | ||
| + | |definition = Пусть имеются последовательности <tex> X = \left \langle x_1, x_2, \dots, x_m \right \rangle </tex> и <tex> Y = \left \langle y_1, y_2, \dots, y_n \right \rangle </tex>. Необходимо найти <tex>LCS(X,Y)</tex> за время <tex> O(nm) </tex> и <tex> O(\min (n, m)) </tex> памяти. | ||
| + | }} | ||
| + | |||
| + | === Алгоритм === | ||
| + | |||
| + | Не теряя общности, будем считать, что <tex> m \ge n </tex>. Тогда разобьем последовательность <tex> X </tex> на две равные части <tex> x^1 \left [0 \dots \frac {m} {2} \right ] </tex> и <tex> x^2 \left [\frac {m} {2} + 1 \dots m \right ] </tex>. Найдем <tex> LCS </tex> для <tex> x^1 </tex> и всех префиксов | ||
| + | последовательности <tex>Y</tex>, аналогично - для развернутых последовательностей <tex> x^2 </tex> и <tex> Y </tex>. Стоит отметить, что для нахождения <tex> LCS </tex> на всех префиксах достаточно одного квадратичного прохода, так как <tex>i</tex>-ый элемент последней строки результирующей матрицы есть не что иное, как <tex> LCS </tex> первой последовательности и префикса второй длины <tex>i</tex>. Затем выберем такой индекс <tex> j </tex>, что <tex> \left | LCS(x^1, y \left [0 \dots j \right ]) + LCS(reverse(x^2), reverse(y \left [j \dots n \right ])) \right | = max </tex> . Запустим алгоритм рекурсивно для пар | ||
| + | <tex>(x^1, y \left [0 \dots j \right ])</tex> и <tex>(x^2, y \left [j \dots n \right ])</tex>. Будем продолжать до тех пор, пока в <tex>X</tex> не останется ровно 1 элемент, тогда достаточно проверить, входит ли он текущую часть <tex>Y</tex>, если входит, то вернем этот символ, если нет - вернем пустую строку. | ||
| + | Таким образом ответом будет конкатенация всех вызовов функции. | ||
== См. также == | == См. также == | ||
Версия 21:00, 7 мая 2019
| Определение: |
| Последовательность является подпоследовательностью (англ. subsequence) последовательности , если существует строго возрастающая последовательность индексов таких, что для всех выполняется соотношение . |
Другими словами, подпоследовательность данной последовательности — это последовательность, из которой удалили ноль или больше элементов. Например, является подпоследовательностью последовательности , а соответствующая последовательность индексов имеет вид .
| Определение: |
| Последовательность является общей подпоследовательностью (англ. common subsequence) последовательностей и , если является подпоследовательностью как , так и . |
| Задача: |
| Пусть имеются последовательности и . Необходимо найти |
Содержание
Наивное решение
Переберем все различные подпоследовательности обеих строк и сравним их. Тогда искомая гарантированно найдётся, однако время работы алгоритма будет экспоненциально зависеть от длины исходных последовательностей.
Динамическое программирование
Для решения данной задачи используем Принцип оптимальности на префиксе.
Доказательство оптимальности
| Теорема: |
Пусть имеются последовательности и , а — их .
|
| Доказательство: |
|
Решение
Обозначим как префиксов данных последовательностей, заканчивающихся в элементах с номерами и соответственно. Получается следующее рекуррентное соотношение:
Очевидно, что сложность алгоритма составит , где и — длины последовательностей.
Построение подпоследовательности
Для каждой пары элементов помимо длины соответствующих префиксов хранятся и номера последних элементов, участвующих в этой .Таким образом, посчитав ответ, можно восстановить всю наибольшую общую подпоследовательность.
Псевдокод
, — данные последовательности; — для префикса длины последовательности и префикса длины последовательности ; — пара индексов элемента таблицы, соответствующего оптимальному решению вспомогательной задачи, выбранной при вычислении .
// подсчёт таблиц
int LCS(x: int, y: int):
m = length(x)
n = length(y)
for i = 1 to m
lcs[i][0] = 0
for j = 0 to n
lcs[0][j] = 0
for i = 1 to m
for j = 1 to n
if x[i] == y[j]
lcs[i][j] = lcs[i - 1][j - 1] + 1
prev[i][j] = pair(i - 1, j - 1)
else
if lcs[i - 1][j] >= lcs[i][j - 1]
lcs[i][j] = lcs[i - 1][j]
prev[i][j] = pair(i - 1, j)
else
lcs[i][j] = lcs[i][j - 1]
prev[i][j] = pair(i, j - 1)
// вывод LCS, вызывается как printLCS(m, n)
int printLCS(i: int, j: int):
if i == 0 or j == 0 // пришли к началу LCS
return
if prev[i][j] == pair(i - 1, j - 1) // если пришли в lcs[i][j] из lcs[i - 1][j - 1], то x[i] == y[j], надо вывести этот элемент
printLCS(i - 1, j - 1)
print x[i]
else
if prev[i][j] == pair(i - 1, j)
printLCS(i - 1, j)
else
printLCS(i, j - 1)
Оптимизация для вычисления только длины
Заметим, что для вычисления нужны только -ая и -ая строчки матрицы . Тогда можно использовать лишь элементов таблицы:
int LCS2(x: int, y: int):
if length(x) < length(y) // в таблице будет length(y) столбцов, и если length(x) меньше, выгоднее поменять местами x и y
swap(x, y)
m = length(x)
n = length(y)
for j = 0 to n
lcs[0][j] = 0
lcs[1][j] = 0
for i = 1 to m
lcs[1][0] = 0
for j = 1 to n
lcs[0][j] = lcs[1][j] // элемент, который был в a[1][j], теперь в предыдущей строчке
if x[i] == y[j]
lcs[1][j] = lcs[0][j - 1] + 1
else
if lcs[0][j] >= lcs[1][j - 1]
lcs[1][j] = lcs[0][j]
else
lcs[1][j] = lcs[1][j - 1]
// ответ — lcs[1][n]
Также можно заметить, что от -ой строчки нужны только элементы с -го столбца. В этом случае можно использовать лишь элементов таблицы:
int LCS3(x: int, y: int):
if length(x) < length(y) // в таблице будет length(y) столбцов, и если length(x) меньше, выгоднее поменять местами x и y
swap(x, y)
m = length(x)
n = length(y)
for j = 0 to n
lcs[j] = 0
d = 0 // d — дополнительная переменная, в ней хранится lcs[i - 1][j - 1]
// в lcs[j], lcs[j + 1], …, lcs[n] хранятся lcs[i - 1][j], lcs[i - 1][j + 1], …, lcs[i - 1][n]
// в lcs[0], lcs[1], …, lcs[j - 1] хранятся lcs[i][0], lcs[i][1], …, lcs[i][j - 1]
for i = 1 to m
for j = 1 to n
tmp = lcs[j]
if x[i] == y[j]
lcs[j] = d + 1
else
if lcs[j] >= lcs[j - 1]
lcs[j] = lcs[j] // в lcs[j] и так хранится lcs[i - 1][j]
else
lcs[j] = lcs[j - 1]
d = tmp
// ответ — lcs[n]
Длина кратчайшей общей суперпоследовательности
Для двух подпоследовательностей и длина кратчайшей общей суперпоследовательности равна [1]
Решение для случая k строк
Найдем решение для 3 строк.
| Задача: |
| Пусть имеются последовательности , и . Необходимо найти |
Наивное решение подсчёта нескольких строк при помощи последовательного нахождения двух строк и уменьшением набора строк на единицу, не срабатывает. Это доказывается следующим контрпримером. Даны три последовательности:
Подсчитаем
Это неверно, так как
| Теорема: |
Пусть имеются последовательности , и , а — их .
|
| Доказательство: |
| Доказательство аналогично доказательству для двух последовательностей. |
Решение
Обозначим как наибольшую общую подпоследовательность префиксов данных последовательностей, заканчивающихся в элементах с номерами , и соответственно. Получается следующее рекуррентное соотношение:
Очевидно, что сложность алгоритма составит , где , и — длины последовательностей.
Аналогичным образом задача решается для строк. Заполняется -мерная динамика.
Алгоритм Хиршберга
| Задача: |
| Пусть имеются последовательности и . Необходимо найти за время и памяти. |
Алгоритм
Не теряя общности, будем считать, что . Тогда разобьем последовательность на две равные части и . Найдем для и всех префиксов последовательности , аналогично - для развернутых последовательностей и . Стоит отметить, что для нахождения на всех префиксах достаточно одного квадратичного прохода, так как -ый элемент последней строки результирующей матрицы есть не что иное, как первой последовательности и префикса второй длины . Затем выберем такой индекс , что . Запустим алгоритм рекурсивно для пар и . Будем продолжать до тех пор, пока в не останется ровно 1 элемент, тогда достаточно проверить, входит ли он текущую часть , если входит, то вернем этот символ, если нет - вернем пустую строку. Таким образом ответом будет конкатенация всех вызовов функции.
См. также
- Задача о наибольшей возрастающей подпоследовательности
- Наибольшая общая возрастающая подпоследовательность
- Задача о наибольшей общей палиндромной подпоследовательности
Примечания
Список литературы
- Томас Х. Кормен, Чарльз И. Лейзерсон, Рональд Л. Ривест, Клиффорд Штайн Алгоритмы: построение и анализ — 2-е изд. — М.: «Вильямс», 2007. — с. 459. — ISBN 5-8489-0857-4