Längsta vanliga undersekvensproblem - Longest common subsequence problem
Den längsta gemensamma delsekvens ( LCS ) problem är problemet att hitta den längsta delsekvensen gemensam för alla sekvenser i en uppsättning av sekvenser (ofta bara två sekvenser). Det skiljer sig från det längsta vanliga delsträngsproblemet : till skillnad från substrat krävs inte delsekvenser för att inta på varandra följande positioner inom de ursprungliga sekvenserna. Det längsta vanliga undersekvensproblemet är ett klassiskt datavetenskapsproblem , grunden för dataparametrar som diff -verktyget , och har tillämpningar inom beräkningsspråkig lingvistik och bioinformatik . Det används också i stor utsträckning avrevisionskontrollsystem som Git för att förena flera ändringar som gjorts i en revisionsstyrd samling filer.
Tänk till exempel på sekvenserna (ABCD) och (ACBAD). De har 5 längd-2 vanliga undersekvenser: (AB), (AC), (AD), (BD) och (CD); 2 längd-3 vanliga undersekvenser: (ABD) och (ACD); och inte längre vanliga undersekvenser. Så (ABD) och (ACD) är deras längsta vanliga undersekvenser.
Komplexitet
För det allmänna fallet med ett godtyckligt antal ingångssekvenser är problemet NP-hårt . När antalet sekvenser är konstant är problemet löst i polynomtid genom dynamisk programmering .
Med tanke på längdesekvenser skulle en naiv sökning testa var och en av undersekvenserna i den första sekvensen för att avgöra om de också är undersekvenser av de återstående sekvenserna; varje undersekvens kan testas linjärt i längden på de återstående sekvenserna, så tiden för denna algoritm skulle vara
För två sekvenser av n- och m -element är den dynamiska programmeringsmetodens drifttid O ( n × m ). För ett godtyckligt antal ingångssekvenser ger den dynamiska programmeringsmetoden en lösning
Det finns metoder med lägre komplexitet, som ofta beror på LCS -längden, alfabetets storlek eller båda.
LCS är inte nödvändigtvis unik; i värsta fall är antalet vanliga undersekvenser exponentiellt i ingångarnas längd, så den algoritmiska komplexiteten måste vara åtminstone exponentiell.
Lösning för två sekvenser
LCS -problemet har en optimal understruktur : problemet kan delas upp i mindre, enklare delproblem, som i sin tur kan delas upp i enklare delproblem, och så vidare, tills slutligen lösningen blir trivial. LCS har i synnerhet överlappande delproblem : lösningarna på delproblem på hög nivå återanvänder ofta lösningar till lägre nivåproblem. Problem med dessa två egenskaper är mottagliga för dynamiska programmeringsmetoder , där delproblemlösningar memoreras , det vill säga lösningarna för delproblem sparas för återanvändning.
Prefix
Prefixet S n av S definieras som de första n tecknen i S . Till exempel är prefixen för S = (AGCA)
- S 0 = ()
- S 1 = (A)
- S 2 = (AG)
- S 3 = (AGC)
- S 4 = (AGCA).
Låt LCS ( X , Y ) vara en funktion som beräknar en längsta delsekvens gemensam för X och Y . En sådan funktion har två intressanta egenskaper.
Första egendomen
LCS ( X ^ A , Y ^ A ) = LCS ( X , Y ) ^ A , för alla strängar X , Y och alla symboler A , där ^ betecknar stränganslutning. Detta gör att man kan förenkla LCS -beräkningen för två sekvenser som slutar med samma symbol. Till exempel LCS ("BANANA", "ATANA") = LCS ("BANAN", "ATAN")^"A", fortsätter för de återstående vanliga symbolerna, LCS ("BANANA", "ATANA") = LCS (" BAN "," AT ")^" ANA ".
Andra fastigheten
Om A och B är distinkta symboler ( A ≠ B ) är LCS (X^A, Y^B) en av strängarna med maximal längd i uppsättningen { LCS ( X ^ A , Y ), LCS ( X , Y ^ B )}, för alla strängar X , Y .
Till exempel är LCS ("ABCDEFG", "BCDGK") den längsta strängen bland LCS ("ABCDEFG", "BCDG") och LCS ("ABCDEF", "BCDGK"); om båda råkade vara lika långa, kunde en av dem väljas godtyckligt.
För att realisera egendomen, särskilja två fall:
- Om LCS ("ABCDEFG", "BCDGK") slutar med ett "G", kan det sista "K" inte vara i LCS, därför LCS ("ABCDEFG", "BCDGK") = LCS ("ABCDEFG", "BCDG ").
- Om LCS ("ABCDEFG", "BCDGK") inte slutar med ett "G", kan det sista "G" inte vara i LCS, därför LCS ("ABCDEFG", "BCDGK") = LCS ("ABCDEF", "BCDGK").
LCS -funktion definierad
Låt två sekvenser definieras enligt följande: och . Prefixen för är ; prefixen till är . Låt representera uppsättningen av längsta vanliga undersekvens av prefix och . Denna uppsättning sekvenser ges av följande.
För att hitta LCS för och , jämföra och . Om de är lika, då sekvensen förlängs med det elementet, . Om de inte är lika, behålls den längsta bland de två sekvenserna,, och ,. (Om de är lika långa, men inte identiska, behålls båda.)
Utarbetat exempel
Den längsta undersekvensen som är vanlig för R = (GAC) och C = (AGCAT) kommer att hittas. Eftersom LCS -funktionen använder ett "zeroth" -element är det bekvämt att definiera nollprefix som är tomma för dessa sekvenser: R 0 = Ø; och C 0 = Ø. Alla prefix placeras i en tabell med C i den första raden (vilket gör det en c olumn header) och R i den första spalten (vilket gör det en r ow header).
O | A | G | C | A | T | |
---|---|---|---|---|---|---|
O | O | O | O | O | O | O |
G | O | |||||
A | O | |||||
C | O |
Denna tabell används för att lagra LCS -sekvensen för varje steg i beräkningen. Den andra kolumnen och den andra raden har fyllts i med Ø, för när en tom sekvens jämförs med en icke-tom sekvens är den längsta vanliga undersekvensen alltid en tom sekvens.
LCS ( R 1 , C 1 ) bestäms genom att jämföra de första elementen i varje sekvens. G och A är inte desamma, så denna LCS får (med hjälp av den "andra egenskapen") den längsta av de två sekvenserna, LCS ( R 1 , C 0 ) och LCS ( R 0 , C 1 ). Enligt tabellen är båda dessa tomma, så LCS ( R 1 , C 1 ) är också tom, som visas i tabellen nedan. Pilarna indikerar att sekvensen kommer från både cellen ovan, LCS ( R 0 , C 1 ) och cellen till vänster, LCS ( R 1 , C 0 ).
LCS ( R 1 , C 2 ) bestäms genom att jämföra G och G. De matchar, så G läggs till i den övre vänstra sekvensen, LCS ( R 0 , C 1 ), vilket är (Ø), vilket ger (ØG), vilket är (G).
För LCS ( R 1 , C 3 ) matchar inte G och C. Sekvensen ovan är tom; den till vänster innehåller ett element, G. Om du väljer den längsta av dessa är LCS ( R 1 , C 3 ) (G). Pilen pekar till vänster, eftersom det är den längsta av de två sekvenserna.
LCS ( R 1 , C 4 ), likaledes, är (G).
LCS ( R 1 , C 5 ), likaså är (G).
O | A | G | C | A | T | |
---|---|---|---|---|---|---|
O | O | O | O | O | O | O |
G | O | O | (G) | (G) | (G) | (G) |
A | O | |||||
C | O |
För LCS ( R 2 , C 1 ) jämförs A med A. De två elementen matchar, så A läggs till Ø, vilket ger (A).
För LCS ( R 2 , C 2 ) stämmer inte A och G, så den längsta av LCS ( R 1 , C 2 ), som är (G) och LCS ( R 2 , C 1 ), vilket är (A ), är använd. I detta fall innehåller de var och en ett element, så detta LCS ges två undersekvenser: (A) och (G).
För LCS ( R 2 , C 3 ), inte A matchar inte C. LCS ( R 2 , C 2 ) innehåller sekvenser (A) och (G); LCS ( R 1 , C 3 ) är (G), som redan finns i LCS ( R 2 , C 2 ). Resultatet är att LCS ( R 2 , C 3 ) innehåller också de två undersekvenser, (A) och (G).
För LCS ( R 2 , C 4 ), överensstämmer med A A, vilken är fäst till den övre vänstra cellen, vilket ger (GA).
För LCS ( R 2 , C 5 ), betyder A inte matchar T. Jämföra de två sekvenserna, (GA) och (G), är den längsta (GA), så LCS ( R 2 , C 5 ) är (GA).
O | A | G | C | A | T | |
---|---|---|---|---|---|---|
O | O | O | O | O | O | O |
G | O | O | (G) | (G) | (G) | (G) |
A | O | (A) | (A) & (G) | (A) & (G) | (GA) | (GA) |
C | O |
För LCS ( R 3 , C 1 ) stämmer inte C och A, så LCS ( R 3 , C 1 ) får den längsta av de två sekvenserna, (A).
För LCS ( R 3 , C 2 ) matchar inte C och G. Båda LCS ( R 3 , C 1 ) och LCS ( R 2 , C 2 ) har ett element. Resultatet är att LCS ( R 3 , C 2 ) innehåller de två undersekvenserna, (A) och (G).
För LCS ( R 3 , C 3 ) matchar C och C, så C läggs till LCS ( R 2 , C 2 ), som innehåller de två undersekvenserna, (A) och (G), vilket ger (AC) och (GC ).
För LCS ( R 3 , C 4 ) matchar inte C och A. Att kombinera LCS ( R 3 , C 3 ), som innehåller (AC) och (GC), och LCS ( R 2 , C 4 ), som innehåller (GA), ger totalt tre sekvenser: (AC), (GC) och (GA).
Slutligen, för LCS ( R 3 , C 5 ), matchar inte C och T. Resultatet är att LCS ( R 3 , C 5 ) innehåller även de tre sekvenserna, (AC), (GC), och (GA).
O | A | G | C | A | T | |
---|---|---|---|---|---|---|
O | O | O | O | O | O | O |
G | O | O | (G) | (G) | (G) | (G) |
A | O | (A) | (A) & (G) | (A) & (G) | (GA) | (GA) |
C | O | (A) | (A) & (G) | (AC) och (GC) | (AC) & (GC) & (GA) | (AC) & (GC) & (GA) |
Slutresultatet är att den sista cellen innehåller alla de längsta undersekvenserna som är gemensamma för (AGCAT) och (GAC); dessa är (AC), (GC) och (GA). Tabellen visar också de längsta gemensamma undersekvenserna för alla möjliga prefix. Till exempel för (AGC) och (GA) är den längsta vanliga undersekvensen (A) och (G).
Traceback -tillvägagångssätt
Att beräkna LCS för en rad i LCS -tabellen kräver endast lösningarna för den aktuella raden och den föregående raden. Men för långa sekvenser kan dessa sekvenser bli många och långa, vilket kräver mycket lagringsutrymme. Lagringsutrymme kan sparas genom att inte spara de faktiska undersekvenserna, utan längden på undersekvensen och pilarnas riktning, som i tabellen nedan.
O | A | G | C | A | T | |
---|---|---|---|---|---|---|
O | 0 | 0 | 0 | 0 | 0 | 0 |
G | 0 | 0 | 1 | 1 | 1 | 1 |
A | 0 | 1 | 1 | 1 | 2 | 2 |
C | 0 | 1 | 1 | 2 | 2 | 2 |
De faktiska undersekvenserna härleds i ett "spår tillbaka" -förfarande som följer pilarna bakåt, med början från den sista cellen i tabellen. När längden minskar måste sekvenserna ha haft ett gemensamt element. Flera vägar är möjliga när två pilar visas i en cell. Nedan är tabellen för en sådan analys, med siffror färgade i celler där längden är på väg att minska. De djärva siffrorna spårar sekvensen, (GA).
O | A | G | C | A | T | |
---|---|---|---|---|---|---|
O | 0 | 0 | 0 | 0 | 0 | 0 |
G | 0 | 0 | 1 | 1 | 1 | 1 |
A | 0 | 1 | 1 | 1 | 2 | 2 |
C | 0 | 1 | 1 | 2 | 2 | 2 |
Förhållande till andra problem
För två strängar och är längden på den kortaste vanliga supersekvensen relaterad till längden på LCS med
Den redigeringsdistansen när endast insättning och deletion är tillåtet (ingen substitution), eller när kostnaden för substitutionen är den dubbla av kostnaden för en insertion eller deletion, är:
Kod för den dynamiska programmeringslösningen
Beräkna längden på LCS
Funktionen nedan tar som inmatningssekvenser X[1..m]
och Y[1..n]
beräknar LCS mellan X[1..i]
och Y[1..j]
för alla 1 ≤ i ≤ m
och 1 ≤ j ≤ n
, och lagrar den i C[i,j]
. C[m,n]
kommer att innehålla längden på LCS för X
och Y
.
function LCSLength(X[1..m], Y[1..n]) C = array(0..m, 0..n) for i := 0..m C[i,0] = 0 for j := 0..n C[0,j] = 0 for i := 1..m for j := 1..n if X[i] = Y[j] C[i,j] := C[i-1,j-1] + 1 else C[i,j] := max(C[i,j-1], C[i-1,j]) return C[m,n]
Alternativt kan memoization användas.
Exempel i C#
static int[,] LcsLength(string a, string b)
{
int[,] C = new int[a.Length + 1, b.Length + 1]; // (a, b).Length + 1
for (int i = 0; i < a.Length; i++)
C[i, 0] = 0;
for (int j = 0; j < b.Length; j++)
C[0, j] = 0;
for (int i = 1; i <= a.Length; i++)
for (int j = 1; j <= b.Length; j++)
{
if (a[i - 1] == b[j - 1])//i-1,j-1
C[i, j] = C[i - 1, j - 1] + 1;
else
C[i, j] = Math.Max(C[i, j - 1], C[i - 1, j]);
}
return C;
}
Läser upp en LCS
Följande funktion spårar de val som tas vid beräkningen av C
tabellen. Om de sista tecknen i prefixen är lika måste de vara i en LCS. Om inte, kolla vad som gav den största LCS för att behålla och , och gör samma val. Välj bara en om de var lika långa. Ring funktionen med och .
i=m
j=n
function backtrack(C[0..m,0..n], X[1..m], Y[1..n], i, j) if i = 0 or j = 0 return "" if X[i] = Y[j] return backtrack(C, X, Y, i-1, j-1) + X[i] if C[i,j-1] > C[i-1,j] return backtrack(C, X, Y, i, j-1) return backtrack(C, X, Y, i-1, j)
Exempel i C#
string Backtrack(int[,] C, char[] aStr, char[] bStr, int x, int y)
{
if (x == 0 | y == 0)
return "";
if (aStr[x - 1] == bStr[y - 1]) // x-1, y-1
return backtrack(C, aStr, bStr, x - 1, y - 1) + aStr[x - 1]; // x-1
if (C[x, y - 1] > C[x - 1, y])
return backtrack(C, aStr, bStr, x, y - 1);
return backtrack(C, aStr, bStr, x - 1, y);
}
Läser upp alla LCS
Om du väljer och skulle ge ett lika långt resultat, läs upp båda resulterande undersekvenserna. Detta returneras som en uppsättning av denna funktion. Lägg märke till att denna funktion inte är polynom, eftersom den kan förgrena sig i nästan varje steg om strängarna är lika.
function backtrackAll(C[0..m,0..n], X[1..m], Y[1..n], i, j) if i = 0 or j = 0 return {""} if X[i] = Y[j] return {Z + X[i] for all Z in backtrackAll(C, X, Y, i-1, j-1)} R := {} if C[i,j-1] ≥ C[i-1,j] R := backtrackAll(C, X, Y, i, j-1) if C[i-1,j] ≥ C[i,j-1] R := R ∪ backtrackAll(C, X, Y, i-1, j) return R
Skriv ut diff
Denna funktion går tillbaka genom C -matrisen och skriver ut skillnaden mellan de två sekvenserna. Lägg märke till att du får ett annat svar om du byter ≥
och <
, med >
och ≤
nedan.
function printDiff(C[0..m,0..n], X[1..m], Y[1..n], i, j) if i >= 0 and j >= 0 and X[i] = Y[j] printDiff(C, X, Y, i-1, j-1) print " " + X[i] else if j > 0 and (i = 0 or C[i,j-1] ≥ C[i-1,j]) printDiff(C, X, Y, i, j-1) print "+ " + Y[j] else if i > 0 and (j = 0 or C[i,j-1] < C[i-1,j]) printDiff(C, X, Y, i-1, j) print "- " + X[i] else print ""
Exempel
Låt vara " " och " ". Den längsta vanliga undersekvensen mellan och är “ ”. Tabellen som visas nedan, som genereras av funktionen , visar längden på de längsta gemensamma undersekvenserna mellan prefixen för och . Den th raden och th kolumnen visar längden på LCS mellan och .
XMJYAUZ
MZJAWXU
MJAU
C
LCSLength
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ||
---|---|---|---|---|---|---|---|---|---|
O | M | Z | J | A | W | X | U | ||
0 | O | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | X | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
2 | M | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
3 | J | 0 | 1 | 1 | 2 | 2 | 2 | 2 | 2 |
4 | Y | 0 | 1 | 1 | 2 | 2 | 2 | 2 | 2 |
5 | A | 0 | 1 | 1 | 2 | 3 | 3 | 3 | 3 |
6 | U | 0 | 1 | 1 | 2 | 3 | 3 | 3 | 4 |
7 | Z | 0 | 1 | 2 | 2 | 3 | 3 | 3 | 4 |
De markerade siffrorna visar den väg som funktionen backtrack
skulle följa från nedre högra till övre vänstra hörnet när man läser upp en LCS. Om de nuvarande symbolerna i och är lika är de en del av LCS, och vi går både upp och åt vänster (visas med fet stil ). Om inte, går vi upp eller åt vänster, beroende på vilken cell som har ett högre tal. Detta motsvarar antingen att ta LCS mellan och , eller och .
Kodoptimering
Flera optimeringar kan göras till algoritmen ovan för att påskynda den för verkliga fall.
Minska problemuppsättningen
C -matrisen i den naiva algoritmen växer kvadratiskt med sekvensernas längd. För två sekvenser med 100 artiklar skulle en matris på 10 000 artiklar behövas och 10 000 jämförelser skulle behöva göras. I de flesta verkliga fall, särskilt källkoddifferenser och patchar, ändras början och slutet av filer sällan, och nästan säkert inte båda samtidigt. Om bara några få objekt har ändrats i mitten av sekvensen kan början och slutet elimineras. Detta minskar inte bara minneskraven för matrisen, utan också antalet jämförelser som måste göras.
function LCS(X[1..m], Y[1..n]) start := 1 m_end := m n_end := n trim off the matching items at the beginning while start ≤ m_end and start ≤ n_end and X[start] = Y[start] start := start + 1 trim off the matching items at the end while start ≤ m_end and start ≤ n_end and X[m_end] = Y[n_end] m_end := m_end - 1 n_end := n_end - 1 C = array(start-1..m_end, start-1..n_end) only loop over the items that have changed for i := start..m_end for j := start..n_end the algorithm continues as before ...
I bästa fall, en sekvens utan förändringar, skulle denna optimering helt eliminera behovet av C-matrisen. I värsta fall, en ändring av de allra första och sista objekten i sekvensen, görs bara ytterligare två jämförelser.
Minska jämförelsetiden
Det mesta av den tid som den naiva algoritmen tar tar åt att utföra jämförelser mellan objekt i sekvenserna. För textsekvenser som källkod vill du se rader som sekvenselement istället för enstaka tecken. Detta kan innebära jämförelser av relativt långa strängar för varje steg i algoritmen. Två optimeringar kan göras som kan bidra till att minska tiden som dessa jämförelser tar.
Minska strängarna till hash
En hash -funktion eller kontrollsumma kan användas för att minska storleken på strängarna i sekvenserna. Det vill säga, för källkod där den genomsnittliga raden är 60 eller fler tecken lång, kan hash eller kontrollsumma för den raden vara endast 8 till 40 tecken lång. Dessutom skulle haschers och kontrollsummars randomiserade karaktär garantera att jämförelser skulle kortsluta snabbare, eftersom rader med källkod sällan kommer att ändras i början.
Det finns tre primära nackdelar med denna optimering. Först måste en tid läggas ned i förväg för att beräkna hasharna för de två sekvenserna. För det andra måste ytterligare minne tilldelas för de nya hashade sekvenserna. I jämförelse med den naiva algoritmen som används här är emellertid båda dessa nackdelar relativt minimala.
Den tredje nackdelen är kollisioner . Eftersom kontrollsumman eller hash inte garanteras vara unik är det en liten chans att två olika objekt kan reduceras till samma hash. Detta är osannolikt i källkoden, men det är möjligt. En kryptografisk hash skulle därför vara mycket bättre lämpad för denna optimering, eftersom dess entropi kommer att bli betydligt större än för en enkel kontrollsumma. Fördelarna kanske dock inte är värda installationen och beräkningskraven för en kryptografisk hash för små sekvenslängder.
Minska det nödvändiga utrymmet
Om bara längden på LCS krävs kan matrisen enkelt reduceras till en matris eller till en vektor (smartare) eftersom den dynamiska programmeringsmetoden bara behöver matrisens nuvarande och tidigare kolumner. Hirschbergs algoritm gör det möjligt att konstruera själva den optimala sekvensen i samma kvadratiska tid och linjära rymdgränser.
Ytterligare optimerade algoritmer
Det finns flera algoritmer som körs snabbare än den presenterade dynamiska programmeringsmetoden. En av dem är Hunt – Szymanski -algoritmen , som vanligtvis körs i tid (för ), där är antalet matchningar mellan de två sekvenserna. För problem med en begränsad alfabetstorlek kan metoden för fyra ryssar användas för att minska den dynamiska programmeringsalgoritmens drifttid med en logaritmisk faktor.
Beteende på slumpmässiga strängar
Från och med Chvátal & Sankoff (1975) har ett antal forskare undersökt beteendet hos den längsta vanliga undersekvenslängden när de två givna strängarna slumpmässigt dras från samma alfabet. När alfabetets storlek är konstant är LCS: s förväntade längd proportionell mot längden på de två strängarna, och proportionalitetskonstanterna (beroende på alfabetets storlek) är kända som Chvátal – Sankoff -konstanterna . Deras exakta värden är inte kända, men övre och nedre gränser för deras värden har bevisats, och det är känt att de växer omvänt proportionellt mot kvadratroten i alfabetets storlek. Förenklade matematiska modeller av det längsta vanliga undersekvensproblemet har visat sig styras av Tracy -Widom -distributionen .
Se även
Referenser
externa länkar
- Dictionary of Algorithms and Data Structures: längsta gemensamma undersekvens
- En samling implementeringar av den längsta gemensamma undersekvensen på många programmeringsspråk
- Hitta längsta vanliga följd i Python