Самая длинная общая подпоследовательность (LCS) — классическая задача в информатике. Подходы к решению проблемы LCS часто появляются на собеседованиях по программированию и курсах по алгоритмам.
- Какова самая длинная общая задача последовательности?
- Каков практический пример проблемы LCS?
- Как работают подходы к решению самой длинной общей задачи последовательности?
- Рекурсивно определить самую длинную общую подпоследовательность
- Оптимизация рекурсивного подхода с помощью мемоизации
- Динамическое программирование для самой длинной общей подпоследовательности
Какова самая длинная общая задача последовательности?
Цель задачи — найти самую длинную общую подпоследовательность двух последовательностей. Подпоследовательность получается из исходной последовательности. Подпоследовательность имеет тот же порядок элементов, но некоторые элементы могли быть удалены. Проиллюстрируем принцип несколькими примерами:
| последовательность X | последовательность Y | LCS(X, Y) |
| FATHER | VATER | ATER |
| MOTHER | MUTTER | MTER |
| DAVID | DANIEL | DAI |
Каков практический пример проблемы LCS?
Проблема самой длинной общей подпоследовательности важна во всех областях, где используются последовательности, производные друг от друга. Подходы к поиску ЛСК используются, например, для проверки текстов на сходства и различия — так можно отследить плагиат. Известная diffутилита, показывающая изменения в файлах исходного кода, также основана на проблеме LCS.
В биоинформатике проблема самой длинной общей подпоследовательности возникает при анализе последовательностей ДНК. Мутации меняют основания ДНК в отдельных позициях с течением времени. Наличие длинной общей подпоследовательности между двумя последовательностями указывает на высокое генетическое родство. Таким образом, генетическое развитие между видами можно проследить на протяжении всей эволюции.
Лингвисты используют проблему самой длинной общей последовательности для изучения эволюции языков на протяжении веков. Если есть два слова из разных языков, которые имеют одинаковое значение и имеют длинную общую последовательность, это указывает на общее происхождение. Таким образом, историческое развитие может быть выведено из соответствия между буквами.
Как работают подходы к решению самой длинной общей задачи последовательности?
Во-первых, проблема LCS может быть решена с помощью «наивного» подхода. Это очевидный подход без какой-либо оптимизации или специального подхода. Один определяет все подпоследовательности двух последовательностей и находит самую длинную подпоследовательность, которая является общей для обеих последовательностей. К сожалению, этот подход неэффективен и поэтому подходит только для коротких последовательностей.
Ниже мы покажем три эффективных подхода к решению проблемы LCS:
- Рекурсивный подход
- Оптимизация с помощью мемоизации
- Динамическое программирование
Общим для всех подходов является то, что они различают три случая в отношении двух последовательностей:
- Последнее письмо такое же.
- Последнее письмо не то.
- Длина одной из последовательностей равна нулю.
Подходы различаются по временной сложности (асимптотическое время выполнения) и пространственной сложности (требования к памяти):
| Подход | Продолжительность | Требования к памяти |
| Наивный подход | O(n * n²) | O(n) |
| Рекурсивный подход | O(n²) | O(1) |
| Оптимизация с помощью мемоизации | O(n * m) | O(n * m) |
| Динамическое программирование | O(n * m) | O(n * m) |
Рекурсивно определить самую длинную общую подпоследовательность
Взгляд на проблему LCS показывает, что она имеет «оптимальную подструктуру». Это означает, что проблема может быть сведена к подзадачам. Для решения этой проблемы можно использовать рекурсивный подход. Мы показываем реализацию алгоритма на трех популярных языках программирования.
Python
def lcs(X, Y, m, n): # Base case if m == 0 or n == 0: return 0 # Last elements are equal elif X[m-1] == Y[n-1]: return 1 + lcs(X, Y, m-1, n-1) # Last elements differ else: return max(lcs(X, Y, m, n-1), lcs(X, Y, m-1, n)) # Let's test X, Y = "DANIEL", "DAVID" lcs_len = lcs(X, Y, len(X), len(Y)) print(f"Length of LCS is: {lcs_len}")
Java
import java.io.*; class LCS { public static int lcs(String A, String B, int m, int n) { // Base case if (m == 0 || n == 0) return 0; // Last elements are equal if (A.charAt(m - 1) == B.charAt(n - 1)) return 1 + lcs(A, B, m - 1, n - 1); // Last elements differ else return Math.max(lcs(A, B, m, n - 1), lcs(A, B, m - 1, n)); } // Let's test public static void main(String[] args) { String X = "DAVID"; String Y = "DANIEL"; int lcsLength = LCS.lcs(X, Y, X.length(), Y.length()); System.out.println("Length of LCS is: " + lcsLength); } }
С++
#include <iostream> using namespace std; int lcs(string X, string Y, int m, int n) { // Base case if (m == 0 || n == 0) { return 0; } // Last elements are equal if (X[m - 1] == Y[n - 1]) { return 1 + lcs(X, Y, m - 1, n - 1); } // Last elements differ else { return max(lcs(X, Y, m, n - 1), lcs(X, Y, m - 1, n)); } } // Let's test int main() { // Initialize variables string X = "DAVID"; string Y = "DANIEL"; // Compute and output length of LCS cout << "Length of LCS is " << lcs(X, Y, X.size(), Y.size()); return 0; }
Оптимизация рекурсивного подхода с помощью мемоизации
Дальнейшее рассмотрение рекурсивного подхода показывает, что вычисляются перекрывающиеся подпоследовательности. Это свойство, называемое «перекрывающимися подзадачами», известно из последовательности Фибоначчи. Здесь тоже части рекурсивно вычисляются снова и снова на пути к решению. Чтобы сделать процесс более эффективным, имеет смысл использовать мемоизацию. Другими словами, мы кэшируем уже вычисленные подзадачи в двумерной матрице.
Python
def lcs(X, Y, m, n, table): # Base case if (m == 0 or n == 0): return 0 # Already computed value at given position if (table[m][n] != -1): return table[m][n] # Last elements are equal if X[m - 1] == Y[n - 1]: table[m][n] = 1 + lcs(X, Y, m - 1, n - 1, table) # Last elements differ else: table[m][n] = max(lcs(X, Y, m, n - 1, table), lcs(X, Y, m - 1, n, table)) return table[m][n] # Let's test X = "DAVID" Y = "DANIEL" m, n = len(X), len(Y) # Initialize table fields to `-1` table = [[-1 for i in range(n + 1)] for j in range(m + 1)] // Compute and output length of LCS print(f"Length of LCS is: {lcs(X, Y, m, n, table)}")
Java
import java.io.*; class LCS { public static int lcs(String X, String Y, int m, int n, int[][] table) { // Base case if (m == 0 || n == 0) { return 0; } // Already computed value at given position if (table[m][n] != -1) { return table[m][n]; } // Last elements are equal if(X.charAt(m - 1) == Y.charAt(n - 1)) { table[m][n] = 1 + lcs(X, Y, m - 1, n - 1, table); return table[m][n]; } // Last elements differ else { table[m][n] = Math.max(lcs(X, Y, m, n - 1, table),lcs(X, Y, m - 1, n, table)); return table[m][n]; } } // Let's test public static void main(String args[]){ // Initialize variables String X = "DAVID"; String Y = "DANIEL"; int m = X.length(); int n = Y.length(); int[][] table = new int[m + 1][n + 1]; // Initialize table fields to `-1` for(int i=0; i < m + 1; i++) { for(int j=0; j < n + 1; j++) { table[i][j] = -1; } } // Compute and output length of LCS int lcsLength = lcs(X, Y, m, n, table); System.out.println("Length of LCS is: " + lcsLength); } }
С++
#include <bits/stdc++.h> using namespace std; int lcs(char* X, char* Y, int m, int n, vector<vector<int>>& table) { // Base case if (m == 0 || n == 0) return 0; // Already computed value at given position if (table[m][n] != -1) { return table[m][n]; } // Last elements are equal if (X[m - 1] == Y[n - 1]) { table[m][n] = 1 + lcs(X, Y, m - 1, n - 1, table); return table[m][n]; } // Last elements differ else { table[m][n] = max(lcs(X, Y, m, n - 1, table), lcs(X, Y, m - 1, n, table)); return table; } } // Let's test int main() { // Initialize variables char X[] = "DAVID"; char Y[] = "DANIEL"; int m = strlen(X); int n = strlen(Y); // Initialize table with `-1` vector<vector<int>> table(m + 1, vector<int>(n + 1, -1)); // Compute and output length of LCS cout << "Length of LCS is: " << lcs(X, Y, m, n, table); return 0; }
Динамическое программирование для самой длинной общей подпоследовательности
Динамическое программирование — это нерекурсивный метод решения задач оптимизации путем их разбиения на более мелкие подзадачи и последующего их решения снизу вверх. Оно используется, среди прочего, как решение для алгоритмов поиска пути. Проблема самой длинной общей подпоследовательности также может быть решена с помощью динамического программирования с использованием двумерной матрицы.
Python
def lcs(X , Y, m, n): # Initialize dynamic programming table fields to `None` table = [[None] * (n + 1) for _ in range(m + 1)] # Compute table values for i in range(m + 1): for j in range(n + 1): # Base case if i == 0 or j == 0 : table[i][j] = 0 # Last elements are equal elif X[i - 1] == Y[j - 1]: table[i][j] = table[i - 1][j - 1]+ 1 # Last elements differ else: table[i][j] = max(table[i - 1][j] , table[i][j - 1]) return table[m][n] # Let's test X = "DAVID" Y = "DANIEL" # Compute and output length of LCS lcs_len = lcs(X, Y, len(X), len(Y)) print(f"Length of LCS is: {lcs_len}")
Java
import java.io.*; class LCS { public static int lcs(String X, String Y, int m, int n) { // Initialize dynamic programming table fields int table[][] = new int[m + 1][n + 1]; for (int i = 0; i <= m; i++) { for (int j = 0; j <= n; j++) { // Base case if (i == 0 || j == 0) table[i][j] = 0; // Last elements are equal else if (X.charAt(i - 1) == Y.charAt(j - 1)) table[i][j] = table[i - 1][j - 1] + 1; // Last elements differ else table[i][j] = Math.max(table[i - 1][j], table[i][j - 1]); } } return table[m][n]; } // Let's test public static void main(String args[]){ // Initialize variables String X = "DAVID"; String Y = "DANIEL"; int m = X.length(); int n = Y.length(); // Compute and output length of LCS int lcsLength = lcs(X, Y, m, n); System.out.println("Length of LCS is: " + lcsLength); } }
С++
#include <bits/stdc++.h> using namespace std; int lcs(string X, string Y, int m, int n) { // Initialize dynamic programming table int table[m + 1][n + 1]; // Compute table values for (int i = 0; i <= m; i++) { for (int j = 0; j <= n; j++) { // Base case if (i == 0 || j == 0) table[i][j] = 0; // Last elements are equal else if (X[i - 1] == Y[j - 1]) table[i][j] = table[i - 1][j - 1] + 1; // Last elements differ else table[i][j] = max(table[i - 1][j], table[i][j - 1]); } } return table[m][n]; } // Let's test int main() { // Initialize variables string X = "DAVID"; string Y = "DANIEL"; int m = X.size(); int n = Y.size(); // Compute and output length of LCS cout << "Length of LCS is " << lcs(X, Y, m, n); return 0; }








