Najduži zajednički podniz

S Vikipedije, slobodne enciklopedije

Problem pronalaska najdužeg zajedničkog podniza ili najduže zajedničke podniske (engl. longest common subsequence problem) predstavlja algoritamski problem u kojem je potrebno odrediti najdužu podnisku koja je zajednička svim niskama u skupu niski (koji čine najčešće dve niske). Podniska neke niske simbola se dobija uklanjanjem nula ili više simbola iz date niske, pri čemu neuklonjeni simboli zadržavaju svoj originalni međusobni redosled. Na primer, za dve niske "btmatmac" i "mtamtab", najduže zajedničke podniske su, između ostalih, "mata" i "mama". Problem pronalaska najduže zajedničke podniske ima primenu u programima koji se bave kompresijom podataka, kao i u bioinformatici.

Uvod[uredi | uredi izvor]

Podniz datog niza je jednostavno dati niz sa nula ili više izostavljenih elemenata. Formalno, ukoliko imamo niz tada je niz podniz niza ako postoji strogo rastući niz indeksa niza takav da za svako važi . Na primer, niz je podniz niza sa odgovarajućim nizom indeksa .

Ukoliko imamo dva niza i , niz nazivamo zajedničkim podnizom nizova i ukoliko je niz ujedno podniz niza i podniz niza . Na primer, ako je i tada je niz zajednički podniz nizova i . Međutim, niz nije najduži zajednički podniz nizova i . Niz je najduži zajednički podniz[1] nizova i , kao što je i niz jer nizovi i nemaju zajeničkih podnizova dužine 5 ili veće.

Problem najduži zajednički podniz (NZP) predstavlja pronalaženje zajedničkog podniza maksimalne dužine unapred zadatih nizova i .

Karakterizacija najdužeg zajedničkog podniza[uredi | uredi izvor]

U pokušaju da rešimo NZP problem grubom silom, prvo bismo numerisali sve podnizove niza i onda bismo proverili da li je svaki od njih ujedno i podniz niza , pamteći najduži koji smo do tog trenutka pronašli. Svakom podnizu niza odgovara podskup indeksa niza . Obzirom da niz ima podnizova, ovakav pristup zahteva eksponencijalno vreme što ga čini nepraktičnim za dugačke nizove.

NZP problem ima svojstvo optimalnog potproblema, što dokazuje sledeća teorema. Prirodne klase potproblema odgovoraju parovima prefiksa polaznih nizova. Preciznije, ako imamo niz definišimo -ti prefiks od za svako kao . Na primer, ako je tada je i je prazan niz.

Teorema

Neka su dati nizovi i , i neka je bilo koji najduži zajednički podniz (NZP) nizova i . Tada:

  1. Ako , onda i je NZP nizova i .
  2. Ako , onda i je NZP nizova i .
  3. Ako , onda i je NZP nizova i .
Dokaz
(1) Ako je onda možemo da nadovežemo na čime bismo dobili zajednički podniz nizova i dužine , što je u kontradikciji sa pretpostavkom da je najduži zajednički podniz. Stoga, mora da važi . Prefiks je zajednički podniz nizova i dužine , i još ostaje da se pokaže da je NZP. Pretpostavimo da postoji zajednički podniz nizova i sa dužinom većom od . Tada, nadovezujući na dobijamo zajednički podniz dužine veće od , što je u kontradikciji sa pretpostavkom da je NZP.
(2) Ako , onda je je zajednički podniz nizova i . Ako bi postojao zajednički podniz nizova i dužine veće od , onda bi bio zajednički podniz nizova i , što je u kontradikciji sa pretpostavkom da je NZP nizova i .
(3) Simetrično drugom slučaju.

Način na koji teorema karakteriše najduži zajednički podniz govori nam da NZP dva niza u sebi sadrži i NZP prefiksa ta dva niza. Stoga, prblem NZP ima svojstvo optimalnog potproblema.

Rekurzivno rešenje[uredi | uredi izvor]

Teorema nam govori da treba da ispitamo ili jedno ili dva potproblema kada pronalazimo najduži zajednički podniz nizova i . Ako je onda moramo da pronađemo NZP nizova i . Nadovezujući na pronađeni NZP dobićemo najduži zajednički podniz nizova i . Ako , onda moramo da rešimo dva potproblema: moramo da pronađemo NZP nizova i i NZP nizova i . Duži od dva pronađena najmanja zajednička podniza je NZP polaznih nizova i .

Lako se uočava da se potproblemi problema NZP međusobno prepliću. Kako bismo pronašli NZP nizova i , možda treba da pronađemo i najduže zajedničke podnizove nizova i i nizova i . Ali, svaki od ova dva potproblema za potproblem ima pronalazak najmanjeg zajedničkog podniza nizova i . Mnogi drugi potproblemi dele svoje potprobleme.

Rekurzivno rešenje NZP problema obuhvata i kreiranje rekurentne relacije za pronalaženje optimalnog rešenja. Definišimo kao dužinu NZP nizova i . Ako je ili , onda jedan od nizova ima dužinu , čime je i NZP dužine . Opitimalni potproblem NZP problema daje sledeću rekurentnu relaciju:

Primetimo da u rekurentnoj relaciji uslov u problemu određuje koji potproblem ćemo da razmatramo. Kada je , tada treba da razmatramo potproblem koji predstavnja traženje najdužeg zajedničkog podniza nizova i . U suprotnom, razmatraćemo dva potproblema, jedan je pronalaženje NZP nizova i i NZP nizova i .

Izračunavanje dužine najdužeg zajedničkog podniza[uredi | uredi izvor]

Uz pomoć gore navedene rekurentne relacije lako možemo da napišemo rekurzivni algoritam koji će izračunavati dužinu najdužeg zajedničkog podniza dva niza u eksponencijalnom vremenu. Obzirom da NZP problem ima samo različitih potproblema možemo da iskoristimo dinamičko programiranje za izračunavanje rešenja odozdo nagore.

Procedura LCS_length kao ulazne argumente ima dva niza i . Procedura čuva vrednosti u matrici , tako što redom po vrstama sleva nadesno izračunava dužine najdužih zajedničkih podnizova nizova i . Procedura takođe održava i pomoćnu matricu koju koristimo pri konstrukciji najdužeg zajedničkog podniza. Intuitivno, pokazuje na zapis u matrici koji odgovara optimalnom rešenju izabranog potproblema prilikom izračunavanja . Procedura vraća matrice i , pri čemu sadrži dužinu najdužeg zajedničkog podniza nizova i .

Definišimo sledeće simbole:

LCS_length(X, Y)

m = X.dužina
n = Y.dužina
neka su b[1 .. m, 1 .. n] i c[0 .. m, 1 .. n] prazne matrice

for i = 1 do m
   c[i,0] = 0
for j = 0 do n
   c[0,j] = 0

for i = 1 do m
   for j = 1 do n
      if x[i] == y[j]
         c[i,j] = c[i-1,j-1] + 1
         b[i,j] = GORELEVO
      else if c[i-1,j] >= c[i, j-1]
         c[i,j] = c[i-1, j]
         b[i,j] = GORE
      else
         c[i,j] = c[i, j-1]
         b[i,j] = LEVO

return b i c

Vreme izvršavanja prikazane procedure je , jer svaki unos u matricu zahteva koraka prilikom izvršavanja.

Konstruisanje najdužeg zajedničkog podniza[uredi | uredi izvor]

Matrica koju vraća procedura LCS_length omogućava nam da brzo konstruišemo NZP nizova i . Jednostavno krenemo od i samo se krećemo kroz matricu prateći strelice. Kada god naiđemo na strelicu na polju to znači da je element najdužeg zajedničkog podniza nizova i koji je procedura LCS_length pronašla. Ovim pristupom otkrivamo elemente NZP nizova i u obrnutom poretku. Sledeća rekurzivna procedura štampa elemente NZP nizova i u originalnom poretku. Poziv procedure je sledeći LCS_print(b, X, X.dužina, Y.dužina).

LCS_print(b,X,i,j)

if i == 0 ili j == 0
   return
if b[i,j] == GORELEVO
   LCS_print(b, X, i-1, j-1)
   print X[i]
else if b[i,j] == GORE
   LCS_print(b, X, i-1, j)
else
   LCS_print(b, X, i, j-1)

Proceduri LCS_print je potrebno koraka za izvršavanje, jer se u svakom rekurzivnom pozivu smanjuje barem jedan od indeksa i .

Unapređenja[uredi | uredi izvor]

U NZP algoritmu moguće je potpuno eliminisati matricu . Svaki zavisi samo od još tri zapisa u matrici : , i . Ukoliko imamo vrednost , onda možemo u konstantnom vremenu da utvrdimo koja od ove tri vrednosti je bila iskorišćena prilikom izračunavanja bez potrebe da pristupamo elementima matrice . Stoga možemo da rekonstruišemo NZP u vremenu . Iako ćemo na ovaj način uštedeti prostora, dodatni prostor za izračunavanje najdužeg zajedničkog podniza ne opada asimptotski, jer nam i dalje treba prostora za skladištenje matrice .

Ipak, možemo asimptotski da smanjimo potreban prostor za izračunavanje dužine najdužeg zajedničkog podniza, jer su nam u svakom trenutku potrebne samo dve vrste matrice : vrsta koja se izračunava i prethodno izračunata vrsta. Ovo unapređenje je korisno samo ako nam treba dužina najdužeg zajedničkog podniza, jer smanjena matrica ne sadrži dovoljno informacija za rekonstrukciju samog NZP-a u vremenu .

Primene[uredi | uredi izvor]

Aplikacije u biologiji često treba da porede DNK lance dva ili više različitih organizama. DNK lanci mogu da se predstave kao konačne reči nad jezikom . Jedan od razloga za poređenje DNK lanaca različitih organizama je utvrđivanje njihove međusobne sličnosti, što može da bude mera srodnosti različitih organizama. Sličnost dva lanca može da se meri na različite načine. Jedan od načina kako možemo da izmerimo sličnost dva DNK lanca i je da pronađemo treći lanac u kome se simboli iz jezika pojavljuju i u lancima i ; simboli moraju da se pojavljuju u istom redosledu ali ne i uzastopno. Što je duži pronađeni lanac , to su lanci i sličniji.

Opis rešenja za dve niske[uredi | uredi izvor]

Za pronalazak najduže zajedničke podniske dve niske x i y, izračunavaju se dužine najdužih zajedničkih podniski svih mogućih parova prefiksa, jednog iz niske x i drugog iz niske y. Neka je x = x1...xm i y = y1...yn. Za svako i = 0,1,...,m i j = 0,1,...,n, najduža zajednička podniska prefiksa x1...xi niske x i prefiksa y1...yj niske y se može odrediti na sledeći način. Pre svega, ukoliko je i ili j jednako 0, tada jedan od ovih prefiksa mora biti prazna reč, pa je jedina moguća zajednička podniska takva dva prefiksa prazna reč.

Ukoliko su i i j oba veći od 0, posmatraju se dva slučaja u zavisnosti od toga da li su poslednji znakovi dve niske jednaki:

  1. Ako je xiyj, tada najduža zajednička podniska dva prefiksa ne može uključiti xi i yj, već je ona jednaka dužoj od dve najduže zajedničke podniske - za prefikse x1...xi-1 i y1...yj, odnosno x1...xi i y1...yj-1.
  2. Ako je xi = yj, tada najduža zajednička podniska može uključiti xi i yj, koji neće remetiti druga potencijalna vezivanja simbola u dva prefiksa. To znači da je najduža zajednička podniska za prefikse x1...xi i y1...yj zapravo najduža zajednička podniska za prefikse x1...xi-1 i y1...yj-1 na koje je nadovezan poslednji simbol koji je isti u dva prefiksa - xi, odnosno yj.

Na osnovu ovih zapažanja može se izvesti rekurzivna definicija za dužinu najduže zajedničke podniske prefiksa x1...xi i y1...yj. Ako tu dužinu označimo sa L(i,j), tada je:

Dužina najduže zajedničke podniske za dve cele niske x i y je L(m,n), što se može izračunati jednostavnim rekurzivnim algoritmom na osnovu prethodne formule. Međutim, vreme izvršavanja takvog algoritma bi bilo eksponencijalno, a razlog za to je činjenica da algoritam više puta računa dužinu najduže zajedničke podniske za iste parove prefiksa.

Rekurzivni algoritam se može znatno ubrzati ako se primeni pristup dinamičkog programiranja i konstruiše tabela L vrednosti dužina L(i,j) za i=0,1,...,m i j=0,1,...,n. Pošto u izračunavanju dužine L(i,j) učestvuju vrednosti L(i,j-1), L(i-1,j) i L(i-1,j-1), tabela se može popunjavati red po red i tako obezbediti da se potrebne vrednosti za L(i,j) nalaze u tabeli. Ovo je ilustrovano na sledećoj slici:

L(i-1,j-1) L(i-1,j)
L(i,j-1) L(i,j)

Tabela L se može iskoristiti za određivanje simbola koji čine najdužu zajedničku podnisku. Pošto tabela L sadrži dužine najdužih zajedničkih podniski svih parova prefiksa dve date niske, od donjeg desnog ugla tabele se može formirati put kroz ovu tabelu i na tom putu uzimati određeni simboli. Tako odabrani simboli u obrnutom redosledu predstavljaju najdužu zajedničku podnisku za dve date niske. Ako pretpostavimo da na putu od donjeg desnog ugla tabele L dolazimo do tačke preseka i-te vrste i j-te kolone koja odgovara paru znakova xi i yj. Ako je xi=yj, onda znakove xi i yj smatramo uparenim i znak xi=yj dodajemo ispred svih znakova najduže zajedničke podniske koji su do tada nađeni. Na putu u narednom koraku se prelazi dijagonalno levo na gornji red, tj. u red i-1 i kolonu j-1. Ako je xiyj, vrednost L(i,j) mora biti jednaka jednoj od dužina L(i-1,j) ili L(i,j-1) (ili obema). U prvom slučaju, potrebno je preći na poziciju (i-1,j), a u drugom slučaju na poziciju (i,j-1). U slučaju jednakih vrednosti, na proizvoljan način se bira jedna od dve pozicije.

Primer[uredi | uredi izvor]

Na slici je prikazan rezultat verzije algoritma koja koristi dinamičko programiranje za niske "btmatmac" i "mtamtab". Vrednosti u matrici koje su označene žutom bojom predstavljaju put kojim se određuje najduža zajednička podniska, pa je jedno od mogućih rešenja za ovaj primer podniska "mata".

b t m a t m a c
j\i 0 1 2 3 4 5 6 7 8
0 0 0 0 0 0 0 0 0 0
m 1 0 0 0 1 1 1 1 1 1
t 2 0 0 1 1 1 2 2 2 2
a 3 0 0 1 1 2 2 3 3 3
m 4 0 0 1 2 2 2 3 3 3
t 5 0 0 1 2 2 3 3 3 3
a 6 0 0 1 2 3 3 3 4 4
b 7 0 1 1 2 3 3 3 4 4

Implementacija algoritma u programskom jeziku Java[uredi | uredi izvor]

Rekurzivni algoritam[uredi | uredi izvor]

public static String najduzaZajednickaPodniska(String a, String b){
	 int duzinaA = a.length();
	 int duzinaB = b.length();
	 if(duzinaA == 0 || duzinaB == 0){
	 return "";
	 }else if(a.charAt(duzinaA-1) == b.charAt(duzinaB-1)){
	 return najduzaZajednickaPodniska(a.substring(0,duzinaA-1),b.substring(0,duzinaB-1))
	 + a.charAt(duzinaA-1);
	 }else{
	 String x = najduzaZajednickaPodniska(a, b.substring(0,duzinaB-1));
	 String y = najduzaZajednickaPodniska(a.substring(0,duzinaA-1), b);
	 return (x.length() > y.length()) ? x : y;
	 }
}

Algoritam zasnovan na dinamičkom programiranju[uredi | uredi izvor]

public static String najduzaZajednickaPodniska(String a, String b) {
	 int[][] duzine = new int[a.length()+1][b.length()+1];
	 
	 // red 0 and kolona 0 su vec inicijalizovane na 0
	 
	 for (int i = 0; i < a.length(); i++)
	 for (int j = 0; j < b.length(); j++)
	 if (a.charAt(i) == b.charAt(j))
	 	duzine[i+1][j+1] = duzine[i][j] + 1;
	 else
	 	duzine[i+1][j+1] =
	 Math.max(duzine[i+1][j], duzine[i][j+1]);
	 
	 // citamo simbole koji cine najduzu zajednicku podnisku
	 StringBuffer sb = new StringBuffer();
	 for (int x = a.length(), y = b.length();
	 x != 0 && y != 0; ) {
	 if (duzine[x][y] == duzine[x-1][y])
	 x--;
	 else if (duzine[x][y] == duzine[x][y-1])
	 y--;
	 else {
	 assert a.charAt(x-1) == b.charAt(y-1);
	 sb.append(a.charAt(x-1));
	 x--;
	 y--;
	 }
	 }
	 
	 return sb.reverse().toString();
}

Implementacija algoritma u programskom jeziku C[uredi | uredi izvor]

Implementacija je u programskom jeziku C uz pretpostavku o maksimalnoj dužini nizova i uz pretpostavku da se radi o nizovima koji sadrže celobrojne vrednosti.

#include <stdio.h>
#include <stdlib.h>
#define UP 1
#define LEFT 2
#define UPLEFT 3
#define MAX_LENGTH 10000

int c[MAX_LENGTH][MAX_LENGTH];
int b[MAX_LENGTH][MAX_LENGTH];
int n;
int m;

void LCS_length(int* x, int *y, int m, int n){
     int i,j;
     
     for (i=0; i<=m; i++){
         for (j=0; j<=n; j++){
             c[i][j] = b[i][j] = 0;    
         }
     }     
     
     for (i=1; i<=m; i++){
         c[i][0] = 0;
         b[i][0] = UP;
     }
         
     for (j=0; j<=n; j++){
         c[0][j] = 0;
         b[0][j] = LEFT;    
     }
         
     for (i=1; i<=m; i++){
         for (j=1; j<=n; j++){
         
             if (x[i-1] == y[j-1]){
                c[i][j] = c[i-1][j-1] + 1;
                b[i][j] = UPLEFT;         
             }
             else if (c[i-1][j] >= c[i][j-1]){
                  c[i][j] = c[i-1][j];
                  b[i][j] = UP;     
             }
             else {
                  c[i][j] = c[i][j-1];
                  b[i][j] = LEFT;     
             }
         }
     }
}

void LCS_print(int b[][MAX_LENGTH], int* x, int i, int j){
     
     if (i==0 || j == 0)
        return;
     
     if (b[i][j] == UPLEFT){
        LCS_print(b, x, i-1, j-1);
        printf("%d ", x[i]);            
     }
     else if (b[i][j] == UP)
          LCS_print(b, x, i-1, j);
     else
         LCS_print(b, x, i, j-1);
}

int main(int argc, char** argv){
    
    int *x;
    int *y;
    int i;
    
    scanf("%d", &m);
    
    if (m > MAX_LENGTH - 1)
    	m = MAX_LENGTH-1;
    
    if ((x = (int*)malloc(m*sizeof(int))) == NULL)
       exit(EXIT_FAILURE);
       
    for (i=0; i<m; i++)
        scanf("%d", &x[i]);
    
    scanf("%d", &n);
    
    if (n > MAX_LENGTH - 1)
    	n = MAX_LENGTH-1;
    
    if ((y = (int*)malloc(n*sizeof(int))) == NULL)
       exit(EXIT_FAILURE);
       
    for (i=0; i<n; i++)
        scanf("%d", &y[i]);
        
    LCS_length(x,y,m,n);
    LCS_print(b, x, m, n);
    
    free(x);
    free(y);
    
    exit(EXIT_SUCCESS);    
}

Složenost algoritama[uredi | uredi izvor]

Rekurzivni algoritam kojim se određuje najduža zajednička podniska dve niske ima eksponencijalnu složenost. Ako dve niske, za koje je potrebno odrediti najdužu zajedničku podnisku, imaju dužine m, odnosno n i neka je k = m + n, vreme izvršavanja takvog algoritma je θ(2k).

Složenost algoritma zasnovanom na dinamičkom programiranju za određivanje najduže zajedničke podniske dve niske iznosi Ο(mn).

Reference[uredi | uredi izvor]

  1. ^ Cormen, Leiserson, Rivest: Introduction to algorithms

Literatura[uredi | uredi izvor]

  • Dejan Živković, (2007). Osnove dizajna i analize algoritama. CET. ISBN 978-86-7991-327-2.  Poglavlje 7.

Spoljašnje veze[uredi | uredi izvor]