Задача о наибольшей общей подпоследовательности — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
Строка 4: Строка 4:
 
{{Определение
 
{{Определение
 
|definition=
 
|definition=
Последовательность <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> 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> 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>.
+
Другими словами, подпоследовательность данной последовательности — это последовательность, из которой удалили ноль или больше элементов. Например, <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> Z </tex> является '''общей подпоследовательностью''' (common subsequence) последовательностей <tex> X </tex> и <tex> Y </tex>, если <tex> Z </tex> является подпоследовательностью как <tex> X </tex>, так и <tex> Y </tex>.
+
Последовательность <tex> z </tex> является '''общей подпоследовательностью''' (common subsequence) последовательностей <tex> x </tex> и <tex> y </tex>, если <tex> z </tex> является подпоследовательностью как <tex> x </tex>, так и <tex> y </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> максимальной длины. Заметим, что таких подпоследовательностей может быть несколько.
+
Даны две последовательности: <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> a[i][j] </tex> НОП префиксов данных последовательностей, заканчивающихся в элементах с номерами <tex> i </tex> и <tex> j </tex> соответственно. Получаем следующее рекуррентное соотношение:
+
Обозначим как <tex> lcs[i][j] </tex> НОП префиксов данных последовательностей, заканчивающихся в элементах с номерами <tex> i </tex> и <tex> j </tex> соответственно. Получаем следующее рекуррентное соотношение:
  
 
<tex>
 
<tex>
a[i][j] =
+
lcs[i][j] =
 
\begin{cases}
 
\begin{cases}
 
  0, & i = 0\text{ or }j = 0 \\
 
  0, & i = 0\text{ or }j = 0 \\
  a[i - 1][j - 1] + 1, & x[i] = y[j] \\
+
  lcs[i - 1][j - 1] + 1, & x[i] = y[j] \\
  max(a[i][j - 1], a[i - 1][j]), & x[i] \neq y[j]
+
  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> a[i][j] </tex> посчитано неверно. Однако, в случае различия соответствующих символов, они не могут одновременно участвовать в НОП, а значит ответ действительно равен формуле для случая с различными символами. В случае же равенства, ответ не может быть больше, чем <tex> a[i - 1][j - 1] + 1 </tex>, так как тогда неверно посчитано значение <tex> a[i - 1][j - 1] + 1 </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> X </tex>, <tex> Y </tex> — данные последовательности; <tex> a[i][j] </tex> — НОП для префикса длины <tex> i </tex> последовательности <tex> X </tex> и префикса длины <tex> j </tex> последовательности <tex> Y </tex>; <tex> b[i][j] </tex> — пара индексов элемента таблицы, соответствующего оптимальному решению вспомогательной задачи, выбранной при вычислении <tex> a[i][j] </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(X, Y)
+
   LCS(x, y)
     m = length(X)
+
     m = length(x)
     n = length(Y)
+
     n = length(y)
 
     for i = 1 to m
 
     for i = 1 to m
       a[i][0] = 0
+
       lcs[i][0] = 0
 
     for j = 0 to n
 
     for j = 0 to n
       a[0][j] = 0
+
       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]
           a[i][j] = a[i - 1][j - 1] + 1
+
           lcs[i][j] = lcs[i - 1][j - 1] + 1
           b[i][j] = pair(i - 1, j - 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]
             a[i][j] = a[i - 1][j]
+
             lcs[i][j] = lcs[i - 1][j]
             b[i][j] = pair(i - 1, j)
+
             prev[i][j] = pair(i - 1, j)
 
           else
 
           else
             a[i][j] = a[i][j - 1]
+
             lcs[i][j] = lcs[i][j - 1]
             b[i][j] = pair(i, j - 1)
+
             prev[i][j] = pair(i, j - 1)
 
    
 
    
   // вывод НОП
+
   // вывод НОП, вызывается как PrintLCS(prev, x, m, n)
   PrintLCS(b, X, i, j)
+
   PrintLCS(prev, x, i, j)
     if i = 0 or j = 0 // пришли к началу НОП
+
     if i == 0 or j == 0 // пришли к началу НОП
 
       return
 
       return
     if b[i][j] = pair(i - 1, j - 1) // если пришли в a[i][j] из a[i - 1][j - 1], то X[i] = Y[j], надо вывести этот элемент
+
     if prev[i][j] == pair(i - 1, j - 1) // если пришли в lcs[i][j] из lcs[i - 1][j - 1], то x[i] == y[j], надо вывести этот элемент
       PrintLCS(b, X, i - 1, j - 1)
+
       PrintLCS(prev, x, i - 1, j - 1)
       print X[i]
+
       print x[i]
 
     else
 
     else
       if b[i][j] = pair(i - 1, j)
+
       if prev[i][j] == pair(i - 1, j)
         PrintLCS(b, X, i - 1, j)
+
         PrintLCS(prev, x, i - 1, j)
 
       else
 
       else
         PrintLCS(b, X, i, j - 1)
+
         PrintLCS(prev, x, i, j - 1)
  
 
== Оптимизация для вычисления только длины НОП ==
 
== Оптимизация для вычисления только длины НОП ==
Заметим, что для вычисления <tex> a[i][j] </tex> нужны только <tex> i </tex>-ая и <tex> (i-1) </tex>-ая строчки матрицы <tex> a </tex>. Тогда можно использовать лишь <tex> 2 \cdot min(m, n) </tex> элементов таблицы:
+
Заметим, что для вычисления <tex> lcs[i][j] </tex> нужны только <tex> i </tex>-ая и <tex> (i-1) </tex>-ая строчки матрицы <tex> lcs </tex>. Тогда можно использовать лишь <tex> 2 \cdot min(m, n) </tex> элементов таблицы:
  
   LCS2(X, Y)
+
   LCS2(x, y)
     if length(X) < length(Y) // в таблице будет length(Y) столбцов, и если length(X) меньше, выгоднее поменять местами X и Y
+
     if length(x) < length(y) // в таблице будет length(y) столбцов, и если length(x) меньше, выгоднее поменять местами x и y
       swap(X, Y)
+
       swap(x, y)
     m = length(X)
+
     m = length(x)
     n = length(Y)
+
     n = length(y)
 
     for j = 0 to n
 
     for j = 0 to n
       a[0][j] = 0
+
       lcs[0][j] = 0
       a[1][j] = 0
+
       lcs[1][j] = 0
 
     for i = 1 to m
 
     for i = 1 to m
       a[1][0] = 0
+
       lcs[1][0] = 0
 
       for j = 1 to n
 
       for j = 1 to n
         a[0][j] = a[1][j] // элемент, который был в a[1][j], теперь в предыдущей строчке
+
         lcs[0][j] = lcs[1][j] // элемент, который был в a[1][j], теперь в предыдущей строчке
         if x[i] = y[i]
+
         if x[i] == y[i]
           a[1][j] = a[0][j - 1] + 1
+
           lcs[1][j] = lcs[0][j - 1] + 1
 
         else
 
         else
           if a[0][j] >= a[1][j - 1]
+
           if lcs[0][j] >= lcs[1][j - 1]
             a[1][j] = a[0][j]
+
             lcs[1][j] = lcs[0][j]
 
           else
 
           else
             a[1][j] = a[1][j - 1]
+
             lcs[1][j] = lcs[1][j - 1]
     // ответ — a[1][n]
+
     // ответ — 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(X, Y)
+
   LCS3(x, y)
     if length(X) < length(Y) // в таблице будет length(Y) столбцов, и если length(X) меньше, выгоднее поменять местами X и Y
+
     if length(x) < length(y) // в таблице будет length(y) столбцов, и если length(x) меньше, выгоднее поменять местами x и y
       swap(X, Y)
+
       swap(x, y)
     m = length(X)
+
     m = length(x)
     n = length(Y)
+
     n = length(y)
 
     for j = 0 to n
 
     for j = 0 to n
       a[j] = 0
+
       lcs[j] = 0
     d = 0 // d — дополнительная переменная, в ней хранится a[i - 1][j - 1]
+
     d = 0 // d — дополнительная переменная, в ней хранится lcs[i - 1][j - 1]
     // в a[j], a[j + 1], …, a[n] хранятся a[i - 1][j], a[i - 1][j + 1], …, a[i - 1][n]
+
     // в lcs[j], lcs[j + 1], …, lcs[n] хранятся lcs[i - 1][j], lcs[i - 1][j + 1], …, lcs[i - 1][n]
     // в a[0], a[1], …, a[j - 1] хранятся a[i][0], a[i][1], …, a[i][j - 1]
+
     // в 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 = a[j]
+
         tmp = lcs[j]
         if x[i] = y[i]
+
         if x[i] == y[i]
           a[j] = d + 1
+
           lcs[j] = d + 1
 
         else
 
         else
           if a[j] >= a[j - 1]
+
           if lcs[j] >= lcs[j - 1]
             a[j] = a[j] // в a[j] и так хранится a[i - 1][j]
+
             lcs[j] = lcs[j] // в lcs[j] и так хранится lcs[i - 1][j]
 
           else
 
           else
             a[j] = a[j - 1]
+
             lcs[j] = lcs[j - 1]
 
         d = tmp
 
         d = tmp
     // ответ — a[n]
+
     // ответ — lcs[n]
  
 
== Список литературы ==
 
== Список литературы ==
 
Т. Кормен, Ч. Лейзерсон, Р. Риверст, К. Штайн, «Алгоритмы: построение и анализ», 2-е изд., стр 418—425
 
Т. Кормен, Ч. Лейзерсон, Р. Риверст, К. Штайн, «Алгоритмы: построение и анализ», 2-е изд., стр 418—425

Версия 07:48, 23 ноября 2011

Задача нахождения наибольшей общей подпоследовательности (longest common subsequence, LCS) — это задача поиска последовательности, которая является самой длинной подпоследовательностью нескольких последовательностей (обычно двух).

Определения

Определение:
Последовательность [math] z = \left \langle z_1, z_2, ..., z_k \right \rangle [/math] является подпоследовательностью (subsequence) последовательности [math] x = \left \langle x_1, x_2, ..., x_m \right \rangle [/math], если существует строго возрастающая последовательность [math] \left \langle i_1, i_2, ..., i_k \right \rangle [/math] индексов [math] x [/math] таких, что для всех [math] j = 1, 2, ..., k [/math] выполняется соотношение [math] x_{i_j} = z_j [/math].

Другими словами, подпоследовательность данной последовательности — это последовательность, из которой удалили ноль или больше элементов. Например, [math] z = \left \langle B, C, D, B \right \rangle [/math] является подпоследовательностью последовательности [math] x = \left \langle A, B, C, B, D, A, B \right \rangle [/math], а соответствующая последовательность индексов имеет вид [math] \left \langle 2, 3, 5, 7 \right \rangle [/math].

Определение:
Последовательность [math] z [/math] является общей подпоследовательностью (common subsequence) последовательностей [math] x [/math] и [math] y [/math], если [math] z [/math] является подпоследовательностью как [math] x [/math], так и [math] y [/math].

Постановка задачи

Даны две последовательности: [math] x = \left \langle x_1, x_2, ..., x_m \right \rangle [/math] и [math] y = \left \langle y_1, y_2, ..., y_n \right \rangle [/math]. Требуется найти общую подпоследовательность [math] x [/math] и [math] y [/math] максимальной длины. Заметим, что таких подпоследовательностей может быть несколько.

Наивная идея решения

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

Динамическое программирование

Решение

Обозначим как [math] lcs[i][j] [/math] НОП префиксов данных последовательностей, заканчивающихся в элементах с номерами [math] i [/math] и [math] j [/math] соответственно. Получаем следующее рекуррентное соотношение:

[math] lcs[i][j] = \begin{cases} 0, & i = 0\text{ or }j = 0 \\ lcs[i - 1][j - 1] + 1, & x[i] = y[j] \\ max(lcs[i][j - 1], lcs[i - 1][j]), & x[i] \neq y[j] \end{cases} [/math]

Очевидно, что сложность алгоритма составит [math] O(mn) [/math], где [math] m [/math] и [math] n [/math] — длины последовательностей.

Доказательство оптимальности

База: при [math] i = 0 [/math] или [math] j = 0 [/math] длина одной из последовательностей равна нулю, поэтому и их НОП тоже нулевой длины.

Переходы: предположим, что некоторое значение [math] lcs[i][j] [/math] посчитано неверно. Однако, в случае различия соответствующих символов, они не могут одновременно участвовать в НОП, а значит ответ действительно равен формуле для случая с различными символами. В случае же равенства, ответ не может быть больше, чем [math] lcs[i - 1][j - 1] + 1 [/math], так как тогда неверно посчитано значение [math] lcs[i - 1][j - 1] + 1 [/math].

Построение подпоследовательности

Для каждой пары элементов будем хранить не только длину НОП соответствующих префиксов, но и номера последних элементов, участвующих в этой НОП.Таким образом, посчитав ответ, мы сможем восстановить всю наибольшую общую подпоследовательность.

Псевдокод

[math] x [/math], [math] y [/math] — данные последовательности; [math] lcs[i][j] [/math] — НОП для префикса длины [math] i [/math] последовательности [math] x [/math] и префикса длины [math] j [/math] последовательности [math] y [/math]; [math] prev[i][j] [/math] — пара индексов элемента таблицы, соответствующего оптимальному решению вспомогательной задачи, выбранной при вычислении [math] lcs[i][j] [/math].

 // подсчёт таблиц
 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)

Оптимизация для вычисления только длины НОП

Заметим, что для вычисления [math] lcs[i][j] [/math] нужны только [math] i [/math]-ая и [math] (i-1) [/math]-ая строчки матрицы [math] lcs [/math]. Тогда можно использовать лишь [math] 2 \cdot min(m, n) [/math] элементов таблицы:

 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]

Приглядевшись повнимательнее, заметим, что от [math] (i - 1) [/math]-ой строчки нам нужны только элементы с [math] (j - 1) [/math]-го столбца. В этом случае можно использовать лишь [math] min(m, n) [/math] элементов таблицы:

 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