Längsta vanliga följdsekvens: Python, C++ Exempelvis

⚡ Smart sammanfattning

Längsta gemensamma delsekvens identifierar det längsta ordnade elementmönstret som delas av två strängar utan att kräva sammanhängande tecken. Denna dynamiska programmeringsklassiker ligger till grund för diff-verktyg, DNA-justering och versionskontroll genom att effektivt jämföra sekvenser i polynomtid.

  • 📘 Kärnkoncept: Längsta gemensamma delsekvens returnerar den längsta ordnade uppsättningen tecken som visas i båda inmatningssträngarna samtidigt som deras ursprungliga relativa ordning bevaras.
  • ???? Naivt tillvägagångssätt: Brute force räknar upp varje delsekvens av den första strängen och kontrollerar den mot den andra, i exponentiell O(n·2^m) tid.
  • 🔁 Rekursiv metod: En rekursiv regel matchar de sista tecknen eller rekurserar på mindre delsträngar, men beräknar om överlappningping delproblem upprepade gånger.
  • 🧮 Dynamisk programmering: En tvådimensionell dp-tabell cachar delproblemresultat, vilket ger en ren O(m·n)-lösning med O(m·n) hjälprum.
  • 🐍 Språktäckning: Komplett Python och C++ Implementeringar demonstrerar både den rekursiva baslinjen och den memoiserade dp-tabellen för praktisk användning.
  • 🌐 Verkliga tillämpningar: Longest Common Subsequence driver diff-verktyg, plagiatkontroll, stavningskorrigerare och bioinformatisk sekvensjustering över DNA och proteiner.

Längsta vanliga följd

Vad är den längsta vanliga följden?

Längsta gemensamma delsekvens (LCS) innebär att du får två strängar, mönster eller sekvenser av objekt. Bland dessa två sekvenser eller strängar måste du hitta den längsta delsekvensen av element i samma ordning som finns i båda strängarna eller mönstren.

Exempelvis

Till exempel finns det två strängar. Låt oss anta att:

Pattern_1 = "RGBGARGA"
Pattern_2 = "BGRARG"

  • Från pattern_1 kan sekvenser som "RGB", "RGGA", "RGAR" skapas. För att skapa en sekvens måste du bibehålla den relativa positionen för varje tecken i strängen.
  • Från pattern_2 kan vi producera sekvenser som "BGR", "BRAG", "RARG". Sekvenser kan produceras så länge de bibehåller den ursprungliga strängens relativa position.

Termen relativ position betyder ordning.

Till exempel är ”BRG” en giltig sekvens eftersom ”B” dök upp först, sedan ”R” och sedan ”G” i den ursprungliga strängen pattern_2. Men om en sekvens är ”RBRG” är den inte giltig, eftersom ”B” kommer först i den ursprungliga strängen (pattern_2).

Exempelsträngar för längsta gemensamma delsekvenser

Vi har två alternativ för att hitta den längsta vanliga undersekvensen från de givna två sekvenserna eller arrayerna.

  • Naiv metod
  • Dynamisk programmeringslösning: Längsta vanliga delsekvensen kallas även LCS.

En naiv lösning har större tidskomplexitet och är inte den optimala lösningen. Med hjälp av dynamisk programmeringslösning (DP) övervinner vi komplexitetsproblemet.

Naiv metod

Den naiva metoden är en enkel metod för att lösa problemet, oavsett tidskomplexitet och andra optimeringsfaktorer. Den består av "brute force", flera loopar och rekursiva anrop i de flesta fall. Termen brute force betyder att man går igenom alla möjliga mönster för ett givet problem.

Exempelvis

Från ovanstående exempel på mönster1 och mönster2, låt oss anta att mönster1 har längden m och mönster2 har längden n. För att kontrollera alla möjliga fall måste vi utvärdera alla möjliga följder av mönster1 med mönster2.

Här är en enkel sträng med fyra bokstäver, ”ABCD”. Till exempel behöver vi skapa en sekvens från ”ABCD”. Antingen kan vi ta ett tecken eller inte. Det betyder att vi för varje tecken har två val:

  • Tecknet kommer att läggas till i efterföljden.
  • Tecknet kommer inte att läggas till i efterföljden.

Här visar bilderna alla sekvenser som vi kan göra från strängen "ABCD".

Naiva metodsekvenser av ABCD

Sekvens med 1 tecken:

Naive Method-sekvenser med enstaka tecken

Sekvenser med 2 tecken:

Naive Method två teckensekvenser

Sekvenser med 3 tecken:

Naive Method tre teckensekvenser

Enligt diagrammet ovan finns det 14 sekvenser. Om vi ​​inte tar några bokstäver, i princip en tom sträng, blir det totala antalet sekvenser 15. Dessutom är själva strängen "ABCD" en sekvens. Så det totala antalet sekvenser är 16.

Så det är möjligt att generera 2^4 eller 16 delsekvenser från "ABCD". Sedan skapas en sträng med en längd på m kommer att ha en total delsekvens på 2^m.

För varje delsekvens behöver vi kontrollera den för hela mönstret2. Det tar O(n) tid. O(n) är komplexitetsfunktionen som beräknar tiden det tar för exekvering.

Så total tidskomplexitet blir O(n*2^m). För exemplet vi har sett ovan är värdet på m=8 och n=5.

Här är stegen i den naiva metoden:

Steg 1) Ta en sekvens från mönster1.
Steg 2) Matcha sekvensen från steg 1 med mönster 2.
Steg 3) Om det matchar, spara sedan undersekvensen.
Steg 4) Om fler sekvenser finns kvar i mönster 1, gå sedan till steg 1 igen.
Steg 5) Skriv ut den längsta efterföljden.

Optimal underlag

Termen optimal delstruktur betyder att en optimal lösning kan hittas genom att lösa delproblemen. Till exempel, i exemplet ovan har vi mönster1 och mönster2.

Steg 1) Ta de två första tecknen från varje mönster.

Steg 2) Ta det tredje till femte tecknet från varje mönster.

Steg 3) Fortsätt på samma sätt med de återstående tecknen.

Rekursiv struktur för LCS-problem

Rekursiv struktur för LCS-problem

Vi hittar LCS på delsträngen (en sträng genererad från en originalsträng). Sedan registrerar vi längden på LCS för delsträngarna.

Nu, här är en annan intressant egenskap överlappningping delproblemEtt problem sägs ha överlappningping delproblem om problemformuleringen kan delas upp i små delproblem och användas flera gånger i programmet.

Diagrammet nedan visar att den rekursiva algoritmen anropade funktionen med samma parameter flera gånger.

Optimal överlappning av underkonstruktionenping delproblem

Titta till exempel på rekursionsträdet. I den mörka rutan kan du se överlappningping delproblem. (”RG”, ”RA”), (”RG”, ”R”) och andra anropas flera gånger.

För att optimera detta har vi tillvägagångssättet att Dynamisk programmering (DP).

Rekursiv metod för längsta gemensamma delföljd

Grafen som visas ovan är den rekursiva metoden. Varje rekursiv funktion har ett basfall för att bryta rekursionen eller börja återgå från sin stack.

För denna implementering kommer vi att använda ett basfall. Så, algoritm är som följande:

  • Om alla element före det sista elementet har en matchning, öka längden med ett och returnera.
  • Skicka två mönster till funktionen och ta maxvärdet av returvärdet.
  • Om ett mönster har noll längd, så har vi ingen följd att jämföra. Returnera 0 i detta fall. Detta är basfallet för rekursionen.

Pseudo 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))

Genomförande i 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;
}

Produktion:

Length of LCS is: 5

Genomförande i 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)))

Produktion:

Length of LCS is:  5

Dynamisk programmeringsmetod för längsta gemensamma delsekvens (LCS)

Dynamisk programmering innebär att optimera den rekursiva metoden. Om vi ​​till exempel tittar på grafen för rekursiva eller naiva metoder kan vi se att det finns flera identiska funktionsanrop. Den dynamiska programmeringsmetoden registrerar alla beräkningar i en array och återanvänder dem vid behov.

Vi kommer att använda en 2D-matris med dimensionerna mxn, där m och n är längderna på mönster1 och mönster2. För en 2D-array, kan vi använda List-datastrukturer i Python eller vektor-/matrisdatastrukturer i C++.

Pseudo Code för LCS med 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]

Här är tabellen LCS som används som en 2D-matrisdatastruktur för den dynamiska programmeringsmetoden.

Dynamisk programmeringsmetod för LCS 2D-tabell

Låt oss diskutera logiken vi använde här. Stegen är:

Steg 1) Om i eller j är noll, tar vi en tom sträng från de givna två strängarna och försöker hitta de gemensamma delsekvenserna. Men eftersom delsträngen vi tar är tom, är delsekvensens längd 0.

Steg 2) Om två tecken matchar, tilldelar vi värdet till (i,j)-indexet genom att öka den tidigare beräknade LCS, som finns i (i-1,j-1)-indexet (från föregående rad).

Steg 3) Om det inte matchar tar vi den maximala LCS-gränsen för de två intilliggande indexen. Och på så sätt behöver vi fylla alla värden i 2D-matrisen.

Steg 4) Slutligen kommer vi att returnera värdet för den sista cellen i 2D-matrisen.

I grund och botten innehåller alla värden i 2D-matrisen längden på gemensamma delsekvenser. Bland dessa innehåller den sista cellen längden på den längsta gemensamma delsekvensen.

Genomförande i 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;
}

Produktion:

Length of LCS: 5

Genomförande i 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))

Produktion:

Length of LCS: 5

Så båda strängarna har den längsta gemensamma följden av längd 5.

Kort sagt beräknar vi helt enkelt varje uppgift en gång i DP-metoden. I den rekursiva metoden kan vi ha överlappningping delproblem.

I denna dynamiska programmeringsalgoritm använder vi en 2D-matris. Det kommer att finnas två strängar (antag att båda har längden n). Då är utrymmet som behövs i arrayen nx n. Om strängarna är tillräckligt stora behöver vi en minnesoptimerad version av DP-lösningen.

Den förenklade logiken som togs i koden är:

  • Deklarera en 2D Array DP[m][n].
  • Fyll den första raden och den första kolumnen i DP-matrisen med 0.
  • Ta i och j för iterationen.
  • Om mönster1[i] är lika med mönster2[j], uppdatera då DP[i][j] = DP[i-1][j-1] + 1.
  • Om mönster1[i] inte är lika med mönster2[j], så kommer DP[i][j] att vara det maximala värdet mellan DP[i-1][j] och DP[i][j-1].
  • Fortsätt tills i och j når m och n.
  • Det sista elementet, DP[m-1][n-1], kommer att innehålla längden.

Här adresseras den som DP[m-1][n-1] eftersom arrayindexet börjar från 0.

Vanliga frågor

Maskininlärningspipelines använder LCS som en likhetsfunktion i textklassificering, sekvens-till-sekvens-utvärdering och kodplagiatdetektorer. Det ligger också till grund för BLEU- och ROUGE-liknande mätvärden som poängsätter genererad text mot referensutdata.

Ja. AI-kodningsassistenter som GitHub Copilot och GPT kan producera rekursiva och dynamiska programmeringsversioner av LCS i Python, C++, eller JavaDe kan också lägga till memoisering, skriva ut den faktiska delsekvensen eller konvertera koden till iterativ form på begäran.

En delsträng måste vara sammanhängande, medan en delsekvens bara behöver behålla ordningen. För "ABCDE" är "ACD" en giltig delsekvens men inte en delsträng, medan "BCD" är både en delsträng och en delsekvens.

Den dynamiska programmeringsversionen körs i O(m·n) tid och rum, där m och n är längderna på de två ingångssekvenserna. Den rekursiva versionen körs i värsta fall i exponentiell O(2^(m+n)) tid.

LCS driver fildiff-verktyg, Git-sammanslagningar, DNA- och proteinsekvensjustering inom bioinformatik, plagiatdetektering, stavningskontroll och datasynkroniseringsverktyg som måste bevara den delade ordningen för poster.

Standardtabellen kräver O(m·n) utrymme. En rullande tvåradsoptimering minskar utrymmet till O(min(m, n)) när man bara behöver längden, men rekonstruktionen av den faktiska delsekvensen kräver fortfarande hela tabellen.

Ja, ren rekursion fungerar för korta strängar men beräknar om samma delproblem många gånger och blir opraktiskt efter 20 till 25 tecken. Att lägga till memoisering eller DP-tabellen återställer tracbordets prestanda.

Ja. DP-idén sträcker sig till k sekvenser med hjälp av en k-dimensionell tabell med O(n^k) tid och rum. Denna variant förekommer i multifilsdiffverktyg och multisekvensjustering inom bioinformatik.

Sammanfatta detta inlägg med: