Задача о наибольшей общей подпоследовательности — различия между версиями
| Строка 4: | Строка 4: | ||
{{Определение | {{Определение | ||
|definition= | |definition= | ||
| − | Последовательность <tex> | + | Последовательность <tex> z = \left \langle z_1, z_2, ..., z_k \right \rangle </tex> является '''подпоследовательностью''' (subsequence) последовательности <tex> x = \left \langle x_1, x_2, ..., x_m \right \rangle </tex>, если существует строго возрастающая последовательность <tex> \left \langle i_1, i_2, ..., i_k \right \rangle </tex> индексов <tex> x </tex> таких, что для всех <tex> j = 1, 2, ..., k </tex> выполняется соотношение <tex> x_{i_j} = z_j </tex>. |
}} | }} | ||
| − | Другими словами, подпоследовательность данной последовательности — это последовательность, из которой удалили ноль или больше элементов. Например, <tex> | + | Другими словами, подпоследовательность данной последовательности — это последовательность, из которой удалили ноль или больше элементов. Например, <tex> z = \left \langle B, C, D, B \right \rangle </tex> является подпоследовательностью последовательности <tex> x = \left \langle A, B, C, B, D, A, B \right \rangle </tex>, а соответствующая последовательность индексов имеет вид <tex> \left \langle 2, 3, 5, 7 \right \rangle </tex>. |
{{Определение | {{Определение | ||
|definition= | |definition= | ||
| − | Последовательность <tex> | + | Последовательность <tex> z </tex> является '''общей подпоследовательностью''' (common subsequence) последовательностей <tex> x </tex> и <tex> y </tex>, если <tex> z </tex> является подпоследовательностью как <tex> x </tex>, так и <tex> y </tex>. |
}} | }} | ||
== Постановка задачи == | == Постановка задачи == | ||
| − | Даны две последовательности: <tex> | + | Даны две последовательности: <tex> x = \left \langle x_1, x_2, ..., x_m \right \rangle </tex> и <tex> y = \left \langle y_1, y_2, ..., y_n \right \rangle </tex>. Требуется найти общую подпоследовательность <tex> x </tex> и <tex> y </tex> максимальной длины. Заметим, что таких подпоследовательностей может быть несколько. |
== Наивная идея решения == | == Наивная идея решения == | ||
| Строка 19: | Строка 19: | ||
== Динамическое программирование == | == Динамическое программирование == | ||
=== Решение === | === Решение === | ||
| − | Обозначим как <tex> | + | Обозначим как <tex> lcs[i][j] </tex> НОП префиксов данных последовательностей, заканчивающихся в элементах с номерами <tex> i </tex> и <tex> j </tex> соответственно. Получаем следующее рекуррентное соотношение: |
<tex> | <tex> | ||
| − | + | lcs[i][j] = | |
\begin{cases} | \begin{cases} | ||
0, & i = 0\text{ or }j = 0 \\ | 0, & i = 0\text{ or }j = 0 \\ | ||
| − | + | lcs[i - 1][j - 1] + 1, & x[i] = y[j] \\ | |
| − | max( | + | max(lcs[i][j - 1], lcs[i - 1][j]), & x[i] \neq y[j] |
\end{cases} | \end{cases} | ||
</tex> | </tex> | ||
| Строка 35: | Строка 35: | ||
База: при <tex> i = 0 </tex> или <tex> j = 0 </tex> длина одной из последовательностей равна нулю, поэтому и их НОП тоже нулевой длины. | База: при <tex> i = 0 </tex> или <tex> j = 0 </tex> длина одной из последовательностей равна нулю, поэтому и их НОП тоже нулевой длины. | ||
| − | Переходы: предположим, что некоторое значение <tex> | + | Переходы: предположим, что некоторое значение <tex> lcs[i][j] </tex> посчитано неверно. Однако, в случае различия соответствующих символов, они не могут одновременно участвовать в НОП, а значит ответ действительно равен формуле для случая с различными символами. В случае же равенства, ответ не может быть больше, чем <tex> lcs[i - 1][j - 1] + 1 </tex>, так как тогда неверно посчитано значение <tex> lcs[i - 1][j - 1] + 1 </tex>. |
=== Построение подпоследовательности === | === Построение подпоследовательности === | ||
| Строка 41: | Строка 41: | ||
=== Псевдокод === | === Псевдокод === | ||
| − | <tex> | + | <tex> x </tex>, <tex> y </tex> — данные последовательности; <tex> lcs[i][j] </tex> — НОП для префикса длины <tex> i </tex> последовательности <tex> x </tex> и префикса длины <tex> j </tex> последовательности <tex> y </tex>; <tex> prev[i][j] </tex> — пара индексов элемента таблицы, соответствующего оптимальному решению вспомогательной задачи, выбранной при вычислении <tex> lcs[i][j] </tex>. |
// подсчёт таблиц | // подсчёт таблиц | ||
| − | LCS( | + | LCS(x, y) |
| − | m = length( | + | m = length(x) |
| − | n = length( | + | n = length(y) |
for i = 1 to m | for i = 1 to m | ||
| − | + | lcs[i][0] = 0 | |
for j = 0 to n | for j = 0 to n | ||
| − | + | lcs[0][j] = 0 | |
for i = 1 to m | for i = 1 to m | ||
for j = 1 to n | for j = 1 to n | ||
| − | if x[i] = y[i] | + | if x[i] == y[i] |
| − | + | lcs[i][j] = lcs[i - 1][j - 1] + 1 | |
| − | + | prev[i][j] = pair(i - 1, j - 1) | |
else | else | ||
if a[i - 1][j] >= a[i][j - 1] | if a[i - 1][j] >= a[i][j - 1] | ||
| − | + | lcs[i][j] = lcs[i - 1][j] | |
| − | + | prev[i][j] = pair(i - 1, j) | |
else | else | ||
| − | + | lcs[i][j] = lcs[i][j - 1] | |
| − | + | prev[i][j] = pair(i, j - 1) | |
| − | // вывод НОП | + | // вывод НОП, вызывается как PrintLCS(prev, x, m, n) |
| − | PrintLCS( | + | PrintLCS(prev, x, i, j) |
| − | if i = 0 or j = 0 // пришли к началу НОП | + | if i == 0 or j == 0 // пришли к началу НОП |
return | return | ||
| − | if | + | if prev[i][j] == pair(i - 1, j - 1) // если пришли в lcs[i][j] из lcs[i - 1][j - 1], то x[i] == y[j], надо вывести этот элемент |
| − | PrintLCS( | + | PrintLCS(prev, x, i - 1, j - 1) |
| − | print | + | print x[i] |
else | else | ||
| − | if | + | if prev[i][j] == pair(i - 1, j) |
| − | PrintLCS( | + | PrintLCS(prev, x, i - 1, j) |
else | else | ||
| − | PrintLCS( | + | PrintLCS(prev, x, i, j - 1) |
== Оптимизация для вычисления только длины НОП == | == Оптимизация для вычисления только длины НОП == | ||
| − | Заметим, что для вычисления <tex> | + | Заметим, что для вычисления <tex> lcs[i][j] </tex> нужны только <tex> i </tex>-ая и <tex> (i-1) </tex>-ая строчки матрицы <tex> lcs </tex>. Тогда можно использовать лишь <tex> 2 \cdot min(m, n) </tex> элементов таблицы: |
| − | LCS2( | + | LCS2(x, y) |
| − | if length( | + | if length(x) < length(y) // в таблице будет length(y) столбцов, и если length(x) меньше, выгоднее поменять местами x и y |
| − | swap( | + | swap(x, y) |
| − | m = length( | + | m = length(x) |
| − | n = length( | + | n = length(y) |
for j = 0 to n | for j = 0 to n | ||
| − | + | lcs[0][j] = 0 | |
| − | + | lcs[1][j] = 0 | |
for i = 1 to m | for i = 1 to m | ||
| − | + | lcs[1][0] = 0 | |
for j = 1 to n | for j = 1 to n | ||
| − | + | lcs[0][j] = lcs[1][j] // элемент, который был в a[1][j], теперь в предыдущей строчке | |
| − | if x[i] = y[i] | + | if x[i] == y[i] |
| − | + | lcs[1][j] = lcs[0][j - 1] + 1 | |
else | else | ||
| − | if | + | if lcs[0][j] >= lcs[1][j - 1] |
| − | + | lcs[1][j] = lcs[0][j] | |
else | else | ||
| − | + | lcs[1][j] = lcs[1][j - 1] | |
| − | // ответ — | + | // ответ — lcs[1][n] |
Приглядевшись повнимательнее, заметим, что от <tex> (i - 1) </tex>-ой строчки нам нужны только элементы с <tex> (j - 1) </tex>-го столбца. В этом случае можно использовать лишь <tex> min(m, n) </tex> элементов таблицы: | Приглядевшись повнимательнее, заметим, что от <tex> (i - 1) </tex>-ой строчки нам нужны только элементы с <tex> (j - 1) </tex>-го столбца. В этом случае можно использовать лишь <tex> min(m, n) </tex> элементов таблицы: | ||
| − | LCS3( | + | LCS3(x, y) |
| − | if length( | + | if length(x) < length(y) // в таблице будет length(y) столбцов, и если length(x) меньше, выгоднее поменять местами x и y |
| − | swap( | + | swap(x, y) |
| − | m = length( | + | m = length(x) |
| − | n = length( | + | n = length(y) |
for j = 0 to n | for j = 0 to n | ||
| − | + | lcs[j] = 0 | |
| − | d = 0 // d — дополнительная переменная, в ней хранится | + | 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 i = 1 to m | ||
for j = 1 to n | for j = 1 to n | ||
| − | tmp = | + | tmp = lcs[j] |
| − | if x[i] = y[i] | + | if x[i] == y[i] |
| − | + | lcs[j] = d + 1 | |
else | else | ||
| − | if | + | if lcs[j] >= lcs[j - 1] |
| − | + | lcs[j] = lcs[j] // в lcs[j] и так хранится lcs[i - 1][j] | |
else | else | ||
| − | + | lcs[j] = lcs[j - 1] | |
d = tmp | d = tmp | ||
| − | // ответ — | + | // ответ — lcs[n] |
== Список литературы == | == Список литературы == | ||
Т. Кормен, Ч. Лейзерсон, Р. Риверст, К. Штайн, «Алгоритмы: построение и анализ», 2-е изд., стр 418—425 | Т. Кормен, Ч. Лейзерсон, Р. Риверст, К. Штайн, «Алгоритмы: построение и анализ», 2-е изд., стр 418—425 | ||
Версия 07:48, 23 ноября 2011
Задача нахождения наибольшей общей подпоследовательности (longest common subsequence, LCS) — это задача поиска последовательности, которая является самой длинной подпоследовательностью нескольких последовательностей (обычно двух).
Содержание
Определения
| Определение: |
| Последовательность является подпоследовательностью (subsequence) последовательности , если существует строго возрастающая последовательность индексов таких, что для всех выполняется соотношение . |
Другими словами, подпоследовательность данной последовательности — это последовательность, из которой удалили ноль или больше элементов. Например, является подпоследовательностью последовательности , а соответствующая последовательность индексов имеет вид .
| Определение: |
| Последовательность является общей подпоследовательностью (common subsequence) последовательностей и , если является подпоследовательностью как , так и . |
Постановка задачи
Даны две последовательности: и . Требуется найти общую подпоследовательность и максимальной длины. Заметим, что таких подпоследовательностей может быть несколько.
Наивная идея решения
Переберем все различные подпоследовательности обеих строк и сравним их. Мы гарантированно найдем искомую НОП, однако время работы алгоритма будет экспоненциально зависеть от длины исходных последовательностей.
Динамическое программирование
Решение
Обозначим как НОП префиксов данных последовательностей, заканчивающихся в элементах с номерами и соответственно. Получаем следующее рекуррентное соотношение:
Очевидно, что сложность алгоритма составит , где и — длины последовательностей.
Доказательство оптимальности
База: при или длина одной из последовательностей равна нулю, поэтому и их НОП тоже нулевой длины.
Переходы: предположим, что некоторое значение посчитано неверно. Однако, в случае различия соответствующих символов, они не могут одновременно участвовать в НОП, а значит ответ действительно равен формуле для случая с различными символами. В случае же равенства, ответ не может быть больше, чем , так как тогда неверно посчитано значение .
Построение подпоследовательности
Для каждой пары элементов будем хранить не только длину НОП соответствующих префиксов, но и номера последних элементов, участвующих в этой НОП.Таким образом, посчитав ответ, мы сможем восстановить всю наибольшую общую подпоследовательность.
Псевдокод
, — данные последовательности; — НОП для префикса длины последовательности и префикса длины последовательности ; — пара индексов элемента таблицы, соответствующего оптимальному решению вспомогательной задачи, выбранной при вычислении .
// подсчёт таблиц
LCS(x, y)
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[i]
lcs[i][j] = lcs[i - 1][j - 1] + 1
prev[i][j] = pair(i - 1, j - 1)
else
if a[i - 1][j] >= a[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)
// вывод НОП, вызывается как PrintLCS(prev, x, m, n)
PrintLCS(prev, x, i, j)
if i == 0 or j == 0 // пришли к началу НОП
return
if prev[i][j] == pair(i - 1, j - 1) // если пришли в lcs[i][j] из lcs[i - 1][j - 1], то x[i] == y[j], надо вывести этот элемент
PrintLCS(prev, x, i - 1, j - 1)
print x[i]
else
if prev[i][j] == pair(i - 1, j)
PrintLCS(prev, x, i - 1, j)
else
PrintLCS(prev, x, i, j - 1)
Оптимизация для вычисления только длины НОП
Заметим, что для вычисления нужны только -ая и -ая строчки матрицы . Тогда можно использовать лишь элементов таблицы:
LCS2(x, y)
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[i]
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]
Приглядевшись повнимательнее, заметим, что от -ой строчки нам нужны только элементы с -го столбца. В этом случае можно использовать лишь элементов таблицы:
LCS3(x, y)
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[i]
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]
Список литературы
Т. Кормен, Ч. Лейзерсон, Р. Риверст, К. Штайн, «Алгоритмы: построение и анализ», 2-е изд., стр 418—425