Най-дълга обща подпоследователност: Python, C++ Пример

⚡ Умно обобщение

Най-дългата обща подпоследователност идентифицира най-дългия подреден шаблон от елементи, споделен от два низа, без да изисква съседни символи. Тази класическа методология за динамично програмиране е в основата на помощните програми за диференциални последователности, подравняването на ДНК и контрола на версиите, като сравнява последователности ефективно за полиномиално време.

  • 📘 Основна концепция: Най-дългата обща подпоследователност връща най-дългия подреден набор от символи, който се появява и в двата входни низа, като същевременно запазва първоначалния им относителен ред.
  • ???? Наивен подход: Brute force изброява всяка подпоследователност от първия низ и я сравнява с втория, като се изпълнява експоненциално време O(n·2^m).
  • 🔁 Рекурсивен метод: Рекурсивното правило съпоставя последните символи или рекурсира върху по-малки поднизове, но преизчислява припокриванетоping подпроблеми многократно.
  • 🧮 Динамично програмиране: Двумерна dp таблица кешира резултатите от подзадачата, давайки чисто O(m·n) решение с O(m·n) помощно пространство.
  • 🐍 Езиково покритие: Пълен Python намлява C++ Имплементациите демонстрират както рекурсивната базова линия, така и мемоизираната dp таблица за практическо приложение.
  • 🌐 Реални приложения: Най-дългата обща подпоследователност захранва инструменти за разграничаване, проверка за плагиатство, корекция на правопис и биоинформатично подравняване на последователности в ДНК и протеини.

Най-дългата обща подпоследователност

Какво е най-дългата обща подпоследователност?

Най-дългата обща подпоследователност (LCS) означава, че ще ви бъдат дадени два низа, шаблона или поредици от обекти. Сред тези две поредици или низове трябва да намерите най-дългата подпоследователност от елементи в един и същи ред, присъстващи и в двата низа или шаблона.

Пример

Например, са предоставени два низа. Да предположим, че:

Образец_1 = „RGBGARGA“
Pattern_2 = „BGRARG“

  • От pattern_1 могат да се генерират последователности като „RGB“, „RGGA“, „RGAR“. За да създадете последователност, трябва да запазите относителната позиция на всеки символ в низа.
  • От pattern_2 можем да генерираме последователности като „BGR“, „BRAG“, „RARG“. Последователности могат да бъдат генерирани, стига да запазят относителната позиция на оригиналния низ.

Терминът относителна позиция означава ред.

Например, „BRG“ е валидна последователност, защото „B“ се е появило първо, след това „R“ и накрая „G“ в оригиналния низ pattern_2. Ако обаче последователността е „RBRG“, тя не е валидна, защото в оригиналния низ (pattern_2) „B“ е първо.

Примерни низове за най-дългата обща подпоследователност

Имаме две опции за намиране на най-дългата обща подпоследователност от дадените две последователности или масиви.

  • Наивен метод
  • Решение за динамично програмиране: Най-дългата обща подпоследователност е известна още като LCS.

Наивното решение има по-голяма времева сложност и не е оптималното решение. Използвайки решението за динамично програмиране (DP), преодоляваме проблема със сложността.

Наивен метод

Наивният метод е прост подход към проблема, независимо от времевата сложност и други фактори за оптимизация. В повечето случаи той се състои от „груба сила“, множество цикли и рекурсивни извиквания. Терминът „груба сила“ означава преминаване през всички възможни модели за даден проблем.

Пример

От горния пример за pattern1 и pattern2, нека приемем, че pattern1 има дължина m, а pattern2 има дължина n. За да проверим всеки възможен случай, трябва да оценим всяка възможна подпоследователност от pattern1 с pattern2.

Ето един прост низ от 4 букви „ABCD“. Например, трябва да създадем поредица от „ABCD“. Можем да вземем или даден символ, или не. Това означава, че за всеки символ имаме два варианта:

  • Символът ще бъде добавен към подпоследователността.
  • Символът няма да бъде добавен към подпоследователността.

Тук изображенията показват всички последователности, които можем да направим от низа „ABCD“.

Последователности на наивния метод на ABCD

Последователност с 1 символ:

Наивен метод - последователности от единични символи

Поредици с 2 знака:

Наивен метод - двусимволни последователности

Поредици с 3 знака:

Наивен метод - трисимволни последователности

От горната диаграма се виждат 14 поредици. Ако не вземем никакви букви, т.е. празен низ, общият брой поредици ще бъде 15. Освен това, самият низ „ABCD“ е поредица. Така че общият брой поредици е 16.

Така че е възможно да се генерират 2^4 или 16 подпоследователности от „ABCD“. След това, низ с дължина m ще има обща подпоследователност от 2^m.

За всяка подпоследователност трябва да я проверим за целия шаблон2. Това ще отнеме O(n) време. O(n) означава функцията на сложност, която изчислява времето, необходимо за изпълнение.

Така общата времева сложност става O(n*2^m). За примера, който видяхме по-горе, стойността на m=8 и n=5.

Ето стъпките на наивния метод:

Стъпка 1) Вземете последователност от шаблон1.
Стъпка 2) Съпоставете последователността от стъпка 1 с шаблон 2.
Стъпка 3) Ако съвпада, запазете подпоследователността.
Стъпка 4) Ако в шаблон 1 са останали още последователности, тогава преминете отново към стъпка 1.
Стъпка 5) Отпечатайте най-дългата подпоследователност.

Оптимална подструктура

Терминът „оптимална подструктура“ означава, че оптимално решение може да се намери чрез решаване на подзадачите. Например, в горния пример имаме модел1 и модел2.

Стъпка 1) Вземете първите два знака от всеки шаблон.

Стъпка 2) Вземете третия до петия знак от всеки модел.

Стъпка 3) Продължете по същия начин с останалите знаци.

Рекурсивна структура на LCS проблем

Рекурсивна структура на LCS проблем

Намираме НСК на подниз (низ, генериран от оригинален низ). След това записваме дължината на НСК на поднизовете.

Ето още един интересен имот застъпванеping подпроблемиКазва се, че проблемът има припокриванеping подзадачи, ако формулировката на проблема може да бъде разделена на малки подзадачи и използвана няколко пъти в програмата.

Диаграмата по-долу показва, че рекурсивният алгоритъм е извикал функцията с един и същи параметър няколко пъти.

Оптимално припокриване на подструктуратаping подзадачи

Например, разгледайте дървото на рекурсията. В тъмно оцветената кутия можете да забележите припокриванеping подзадачи. („RG“, „RA“), („RG“, „R“) и други се извикват няколко пъти.

За да оптимизираме това, имаме подхода на Динамично програмиране (DP).

Рекурсивен метод на най-дългата обща подпоследователност

Графиката, показана по-горе, е рекурсивен метод. Всяка рекурсивна функция има базов случай, за да прекъсне рекурсията или да започне връщане от стека си.

За тази имплементация ще използваме базов случай. Така че, алгоритъм е като следното:

  • Ако всички елементи преди последния елемент съвпадат, тогава увеличете дължината с едно и върнете резултата.
  • Предайте два шаблона на функцията и вземете максималната стойност на връщането.
  • Ако един шаблон има нулева дължина, тогава нямаме подпоследователност за сравнение. Върнете 0 в този случай. Това е основният случай на рекурсията.

Прякор Code:

def lcs:
    input: pattern_1, pattern_2, len_1, len_2
    if len_1 or len_2 is zero:
        return 0
    if pattern_1[len_1 - 1] equals pattern_2[len_2 - 1]:
        return 1 + lcs(pattern_1, pattern_2, len_1 - 1, len_2 - 1)
    else:
        return max(lcs(pattern_1, pattern_2, len_1 - 1, len_2),
                   lcs(pattern_1, pattern_2, len_1, len_2 - 1))

Изпълнение в C++

#include<iostream>
#include<bits/stdc++.h>
using namespace std;
int lcs(string pattern_1, string pattern_2, int len_1, int len_2) {
  if (len_1 == 0 || len_2 == 0)
    return 0;
  if (pattern_1[len_1 - 1] == pattern_2[len_2 - 1]) {
    return 1 + lcs(pattern_1, pattern_2, len_1 - 1, len_2 - 1);
  } else {
    return max(lcs(pattern_1, pattern_2, len_1 - 1, len_2), lcs(pattern_1, pattern_2, len_1, len_2 - 1));
  }
}
int main() {
  string pattern_1, pattern_2;
  pattern_1 = "RGBGARGA";
  pattern_2 = "BGRARG";
  cout<<"Length of LCS is: "<<lcs(pattern_1, pattern_2, pattern_1.size(), pattern_2.size())<<endl;
}

Изход:

Length of LCS is: 5

Изпълнение в Python

def lcs(pattern_1, pattern_2, len_1, len_2):
    if len_1 == 0 or len_2 == 0:
        return 0
    if pattern_1[len_1 - 1] == pattern_2[len_2 - 1]:
        return 1 + lcs(pattern_1, pattern_2, len_1 - 1, len_2 - 1)
    else:
        return max(lcs(pattern_1, pattern_2, len_1 - 1, len_2),
                   lcs(pattern_1, pattern_2, len_1, len_2 - 1))

pattern_1 = "RGBGARGA"
pattern_2 = "BGRARG"
print("Length of LCS is: ", lcs(pattern_1, pattern_2, len(pattern_1), len(pattern_2)))

Изход:

Length of LCS is:  5

Метод на динамично програмиране на най-дългата обща подпоследователност (LCS)

Динамичното програмиране означава оптимизиране на обикновения рекурсивен метод. Например, ако разгледаме графиката на рекурсивния или наивен подход, можем да видим, че има няколко еднакви извиквания на функции. Методът на динамично програмиране записва всички изчисления в масив и ги използва повторно, когато е необходимо.

Ще използваме 2D масив с размери mxn, където m и n са дължините на pattern1 и pattern2. За a 2D масив, можем да използваме структури от данни List в Python или векторни/масивни структури от данни в C++.

Прякор Code за LCS, използващ DP:

LCS(pattern_1, pattern_2):
    m = length of pattern_1 + 1
    n = length of pattern_2 + 1
    dp[n][m]
    for i in range 0 to n + 1:
        for j in range 0 to m + 1:
            if i or j equals to 0:
                dp[i][j] = 0
            else if pattern_1[i] == pattern_2[j]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[n][m]

Ето таблицата LCS, която се използва като 2D масивна структура от данни за подхода на динамично програмиране.

Метод за динамично програмиране на LCS 2D таблица

Нека обсъдим логиката, която използвахме тук. Стъпките са:

Стъпка 1) Ако i или j е нула, вземаме празен низ от дадените два низа и се опитваме да намерим общите подпоследователности. Тъй като обаче поднизът, който вземаме, е празен, дължината на подпоследователността е 0.

Стъпка 2) Ако два символа съвпадат, ще присвоим стойността на индекса (i,j), като увеличим предварително изчислената НСК, която присъства в индекса (i-1,j-1) (от предишния ред).

Стъпка 3) Ако не съвпада, тогава ще вземем максималната НСК на двата съседни индекса. И по този начин трябва да запълним всички стойности в 2D масива.

Стъпка 4) Накрая ще върнем стойността на последната клетка от 2D масива.

По принцип всички стойности в 2D масива съдържат дължината на общите подпоследователности. Сред тях последната клетка съдържа дължината на най-дългата обща подпоследователност.

Изпълнение в C++

#include<iostream>
using namespace std;
int lcs(string pattern_1, string pattern_2) {
  int m = pattern_1.size();
  int n = pattern_2.size();
  // dp will store solutions as the iteration goes on
  int dp[n + 1][m + 1];
  for (int i = 0; i < n + 1; i++) {
    for (int j = 0; j < m + 1; j++) {
      if (i == 0 || j == 0) {
        dp[i][j] = 0;
      } else if (pattern_2[i - 1] == pattern_1[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1;
      } else {
        dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
  }
  return dp[n][m];
}
int main() {
  string pattern_1 = "RGBGARGA";
  string pattern_2 = "BGRARG";
  cout<<"Length of LCS: "<<lcs(pattern_1, pattern_2)<<endl;
}

Изход:

Length of LCS: 5

Изпълнение в Python

def lcs(pattern_1, pattern_2):
    m = len(pattern_1)
    n = len(pattern_2)
    # dp will store solutions as the iteration goes on
    dp = [[None] * (n + 1) for item in range(m + 1)]
    for i in range(m + 1):
        for j in range(n + 1):
            if i == 0 or j == 0:
                dp[i][j] = 0
            elif pattern_1[i - 1] == pattern_2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[m][n]

pattern_1 = "RGBGARGA"
pattern_2 = "BGRARG"
print("Length of LCS: ", lcs(pattern_1, pattern_2))

Изход:

Length of LCS: 5

И така, и двата низа имат най-дългата обща подпоследователност с дължина 5.

Накратко, ние просто изчисляваме всяка задача веднъж в метода DP. В рекурсивния метод може да имаме припокриване.ping подпроблеми.

В този алгоритъм за динамично програмиране ние използваме 2D матрица. Ще бъдат дадени два низа (да приемем, че и двата имат дължина n). Тогава необходимото пространство в масива е nx n. Ако низовете са достатъчно големи, ще ни трябва оптимизирана за памет версия на DP решението.

Опростената логика, която беше взета в кода, е:

  • Декларирайте 2D масив DP[m][n].
  • Попълнете първия ред и първата колона на DP масива с 0.
  • Вземете i и j за итерацията.
  • Ако pattern1[i] е равно на pattern2[j], тогава актуализирайте DP[i][j] = DP[i-1][j-1] + 1.
  • Ако pattern1[i] не е равно на pattern2[j], тогава DP[i][j] ще бъде максималната стойност между DP[i-1][j] и DP[i][j-1].
  • Продължете, докато i и j достигнат m и n.
  • Последният елемент, DP[m-1][n-1], ще съдържа дължината.

Тук е адресиран като DP[m-1][n-1], защото индексът на масива започва от 0.

Въпроси и Отговори

Конвейерите за машинно обучение използват LCS като функция за сходство при класификация на текст, оценка на последователност към последователност и детектори за плагиатство в кода. Той е в основата и на показатели в стил BLEU и ROUGE, които оценяват генерирания текст спрямо референтните изходи.

Да. Асистенти за AI кодиране като GitHub Copilot и GPT могат да създават рекурсивни и динамични версии за програмиране на LCS в Python, C++ или JavaТе могат също да добавят мемоизация, да отпечатат действителната подпоследователност или да преобразуват кода в итеративен вид при поискване.

Поднизът трябва да е съседен, докато подпоследователността трябва само да запазва реда. За „ABCDE“, „ACD“ е валидна подпоследователност, но не е подниз, докато „BCD“ е едновременно подниз и подпоследователност.

Версията за динамично програмиране се изпълнява за време и пространство O(m·n), където m и n са дължините на двете входни последователности. Версията с обикновен рекурсивен характер се изпълнява за експоненциално време O(2^(m+n)) в най-лошия случай.

LCS захранва помощни програми за разграничаване на файлове, сливания в Git, подравняване на ДНК и протеинови последователности в биоинформатиката, откриване на плагиатство, проверка на правописа и инструменти за синхронизиране на данни, които трябва да запазват споделения ред на записите.

Стандартната таблица изисква O(m·n) пространство. Подвижната двуредова оптимизация намалява пространството до O(min(m, n)), когато е необходима само дължината, въпреки че реконструкцията на действителната подпоследователност все още изисква пълната таблица.

Да, чистата рекурсия работи за кратки низове, но преизчислява едни и същи подзадачи многократно и става непрактична след 20 до 25 символа. Добавянето на мемоизация или DP таблицата възстановява tracпроизводителност на масата.

Да. Идеята за DP се простира до k последователности, използвайки k-мерна таблица с O(n^k) време и пространство. Този вариант се появява в инструментите за многофайлово сравнение и множествено подравняване на последователности в биоинформатиката.

Обобщете тази публикация с: