Најдужи заједнички подниз

Проблем проналаска најдужег заједничког подниза или најдуже заједничке подниске (енгл. longest common subsequence problem) представља алгоритамски проблем у којем је потребно одредити најдужу подниску која је заједничка свим нискама у скупу ниски (који чине најчешће две ниске). Подниска неке ниске симбола се добија уклањањем нула или више симбола из дате ниске, при чему неуклоњени симболи задржавају свој оригинални међусобни редослед. На пример, за две ниске "бтматмац" и "мтамтаб", најдуже заједничке подниске су, између осталих, "мата" и "мама". Проблем проналаска најдуже заједничке подниске има примену у програмима који се баве компресијом података, као и у биоинформатици.

Увод уреди

Подниз датог низа је једноставно дати низ са нула или више изостављених елемената. Формално, уколико имамо низ   тада је низ   подниз низа   ако постоји строго растући низ индекса   низа   такав да за свако   важи  . На пример, низ   је подниз низа   са одговарајућим низом индекса  .

Уколико имамо два низа   и  , низ   називамо заједничким поднизом низова   и   уколико је низ   уједно подниз низа   и подниз низа  . На пример, ако је   и   тада је низ   заједнички подниз низова   и  . Међутим, низ   није најдужи заједнички подниз низова   и  . Низ   је најдужи заједнички подниз[1] низова   и  , као што је и низ   јер низови   и   немају зајеничких поднизова дужине 5 или веће.

Проблем најдужи заједнички подниз (НЗП) представља проналажење заједничког подниза максималне дужине унапред задатих низова   и  .

Карактеризација најдужег заједничког подниза уреди

У покушају да решимо НЗП проблем грубом силом, прво бисмо нумерисали све поднизове низа   и онда бисмо проверили да ли је сваки од њих уједно и подниз низа  , памтећи најдужи који смо до тог тренутка пронашли. Сваком поднизу низа   одговара подскуп индекса   низа  . Обзиром да низ   има   поднизова, овакав приступ захтева експоненцијално време што га чини непрактичним за дугачке низове.

НЗП проблем има својство оптималног потпроблема, што доказује следећа теорема. Природне класе потпроблема одговорају паровима префикса полазних низова. Прецизније, ако имамо низ   дефинишимо  -ти префикс од   за свако   као  . На пример, ако је   тада је   и   је празан низ.

Теорема

Нека су дати низови   и  , и нека је   било који најдужи заједнички подниз (НЗП) низова   и  . Тада:

  1. Ако  , онда   и   је НЗП низова   и  .
  2. Ако  , онда   и   је НЗП низова   и  .
  3. Ако  , онда   и   је НЗП низова   и  .
Доказ
(1) Ako je   онда можемо да надовежемо   на   чиме бисмо добили заједнички подниз низова   и   дужине  , што је у контрадикцији са претпоставком да је   најдужи заједнички подниз. Стога, мора да важи  . Префикс   је заједнички подниз низова   и   дужине  , и још остаје да се покаже да је НЗП. Претпоставимо да постоји заједнички подниз   низова   и   са дужином већом од  . Тада, надовезујући   на   добијамо заједнички подниз дужине веће од  , што је у контрадикцији са претпоставком да је   НЗП.
(2) Ако  , онда је   је заједнички подниз низова   и  . Ако би постојао заједнички подниз   низова   и   дужине веће од  , онда би   био заједнички подниз низова   и  , што је у контрадикцији са претпоставком да је   НЗП низова   и  .
(3) Симетрично другом случају.

Начин на који теорема карактерише најдужи заједнички подниз говори нам да НЗП два низа у себи садржи и НЗП префикса та два низа. Стога, прблем НЗП има својство оптималног потпроблема.

Рекурзивно решење уреди

Теорема нам говори да треба да испитамо или једно или два потпроблема када проналазимо најдужи заједнички подниз низова   и  . Ако је   онда морамо да пронађемо НЗП низова   и  . Надовезујући   на пронађени НЗП добићемо најдужи заједнички подниз низова   и  . Ако  , онда морамо да решимо два потпроблема: морамо да пронађемо НЗП низова   и   и НЗП низова   и  . Дужи од два пронађена најмања заједничка подниза је НЗП полазних низова   и  .

Лако се уочава да се потпроблеми проблема НЗП међусобно преплићу. Како бисмо пронашли НЗП низова   и  , можда треба да пронађемо и најдуже заједничке поднизове низова   и   и низова   и  . Али, сваки од ова два потпроблема за потпроблем има проналазак најмањег заједничког подниза низова   и  . Многи други потпроблеми деле своје потпроблеме.

Рекурзивно решење НЗП проблема обухвата и креирање рекурентне релације за проналажење оптималног решења. Дефинишимо   као дужину НЗП низова   и  . Ако је   или  , онда један од низова има дужину  , чиме је и НЗП дужине  . Опитимални потпроблем НЗП проблема даје следећу рекурентну релацију:

 

Приметимо да у рекурентној релацији услов у проблему одређује који потпроблем ћемо да разматрамо. Када је  , тада треба да разматрамо потпроблем који представња тражење најдужег заједничког подниза низова   и  . У супротном, разматраћемо два потпроблема, један је проналажење НЗП низова   и   и НЗП низова   и  .

Израчунавање дужине најдужег заједничког подниза уреди

Уз помоћ горе наведене рекурентне релације лако можемо да напишемо рекурзивни алгоритам који ће израчунавати дужину најдужег заједничког подниза два низа у експоненцијалном времену. Обзиром да НЗП проблем има само   различитих потпроблема можемо да искористимо динамичко програмирање за израчунавање решења одоздо нагоре.

Процедура LCS_length као улазне аргументе има два низа   и  . Процедура чува вредности   у матрици  , тако што редом по врстама слева надесно израчунава дужине најдужих заједничких поднизова низова   и  . Процедура такође одржава и помоћну матрицу   коју користимо при конструкцији најдужег заједничког подниза. Интуитивно,   показује на запис у матрици који одговара оптималном решењу изабраног потпроблема приликом израчунавања  . Процедура враћа матрице   и  , при чему   садржи дужину најдужег заједничког подниза низова   и  .

Дефинишимо следеће симболе:

 
 
 
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

Време извршавања приказане процедуре је  , јер сваки унос у матрицу захтева   корака приликом извршавања.

Конструисање најдужег заједничког подниза уреди

Матрица   коју враћа процедура LCS_length омогућава нам да брзо конструишемо НЗП низова   и  . Једноставно кренемо од   и само се крећемо кроз матрицу пратећи стрелице. Када год наиђемо на стрелицу   на пољу   то значи да је   елемент најдужег заједничког подниза низова   и   који је процедура LCS_length пронашла. Овим приступом откривамо елементе НЗП низова   и   у обрнутом поретку. Следећа рекурзивна процедура штампа елементе НЗП низова   и   у оригиналном поретку. Позив процедуре је следећи 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)

Процедури LCS_print је потребно   корака за извршавање, јер се у сваком рекурзивном позиву смањује барем један од индекса   и  .

Унапређења уреди

У НЗП алгоритму могуће је потпуно елиминисати матрицу  . Сваки   зависи само од још три записа у матрици  :  ,  и  . Уколико имамо вредност  , онда можемо у константном времену да утврдимо која од ове три вредности је била искоришћена приликом израчунавања   без потребе да приступамо елементима матрице  . Стога можемо да реконструишемо НЗП у времену  . Иако ћемо на овај начин уштедети   простора, додатни простор за израчунавање најдужег заједничког подниза не опада асимптотски, јер нам и даље треба   простора за складиштење матрице  .

Ипак, можемо асимптотски да смањимо потребан простор за израчунавање дужине најдужег заједничког подниза, јер су нам у сваком тренутку потребне само две врсте матрице  : врста која се израчунава и претходно израчуната врста. Ово унапређење је корисно само ако нам треба дужина најдужег заједничког подниза, јер смањена матрица не садржи довољно информација за реконструкцију самог НЗП-а у времену  .

Примене уреди

Апликације у биологији често треба да пореде ДНК ланце два или више различитих организама. ДНК ланци могу да се представе као коначне речи над језиком  . Један од разлога за поређење ДНК ланаца различитих организама је утврђивање њихове међусобне сличности, што може да буде мера сродности различитих организама. Сличност два ланца може да се мери на различите начине. Један од начина како можемо да измеримо сличност два ДНК ланца   и   је да пронађемо трећи ланац   у коме се симболи из језика   појављују и у ланцима   и  ; симболи морају да се појављују у истом редоследу али не и узастопно. Што је дужи пронађени ланац  , то су ланци   и   сличнији.

Опис решења за две ниске уреди

За проналазак најдуже заједничке подниске две ниске x и y, израчунавају се дужине најдужих заједничких подниски свих могућих парова префикса, једног из ниске x и другог из ниске y. Нека је x = x1...xm и y = y1...yn. За свако i = 0,1,...,m и j = 0,1,...,n, најдужа заједничка подниска префикса x1...xi ниске x и префикса y1...yj ниске y се може одредити на следећи начин. Пре свега, уколико је i или j једнако 0, тада један од ових префикса мора бити празна реч, па је једина могућа заједничка подниска таква два префикса празна реч.

Уколико су i и j оба већи од 0, посматрају се два случаја у зависности од тога да ли су последњи знакови две ниске једнаки:

  1. Ако је xiyj, тада најдужа заједничка подниска два префикса не може укључити xi и yj, већ је она једнака дужој од две најдуже заједничке подниске - за префиксе x1...xi-1 и y1...yj, односно x1...xi и y1...yj-1.
  2. Ако је xi = yj, тада најдужа заједничка подниска може укључити xi и yj, који неће реметити друга потенцијална везивања симбола у два префикса. То значи да је најдужа заједничка подниска за префиксе x1...xi и y1...yj заправо најдужа заједничка подниска за префиксе x1...xi-1 и y1...yj-1 на које је надовезан последњи симбол који је исти у два префикса - xi, односно yj.

На основу ових запажања може се извести рекурзивна дефиниција за дужину најдуже заједничке подниске префикса x1...xi и y1...yj. Ако ту дужину означимо са L(i,j), тада је:

 

Дужина најдуже заједничке подниске за две целе ниске x и y је L(m,n), што се може израчунати једноставним рекурзивним алгоритмом на основу претходне формуле. Међутим, време извршавања таквог алгоритма би било експоненцијално, а разлог за то је чињеница да алгоритам више пута рачуна дужину најдуже заједничке подниске за исте парове префикса.

Рекурзивни алгоритам се може знатно убрзати ако се примени приступ динамичког програмирања и конструише табела L вредности дужина L(i,j) за i=0,1,...,m и j=0,1,...,n. Пошто у израчунавању дужине L(i,j) учествују вредности L(i,j-1), L(i-1,j) и L(i-1,j-1), табела се може попуњавати ред по ред и тако обезбедити да се потребне вредности за L(i,j) налазе у табели. Ово је илустровано на следећој слици:

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

Табела L се може искористити за одређивање симбола који чине најдужу заједничку подниску. Пошто табела L садржи дужине најдужих заједничких подниски свих парова префикса две дате ниске, од доњег десног угла табеле се може формирати пут кроз ову табелу и на том путу узимати одређени симболи. Тако одабрани симболи у обрнутом редоследу представљају најдужу заједничку подниску за две дате ниске. Ако претпоставимо да на путу од доњег десног угла табеле L долазимо до тачке пресека i-те врсте и j-те колоне која одговара пару знакова xi и yj. Ако је xi=yj, онда знакове xi и yj сматрамо упареним и знак xi=yj додајемо испред свих знакова најдуже заједничке подниске који су до тада нађени. На путу у наредном кораку се прелази дијагонално лево на горњи ред, тј. у ред i-1 и колону j-1. Ако је xiyj, вредност L(i,j) мора бити једнака једној од дужина L(i-1,j) или L(i,j-1) (или обема). У првом случају, потребно је прећи на позицију (i-1,j), а у другом случају на позицију (i,j-1). У случају једнаких вредности, на произвољан начин се бира једна од две позиције.

Пример уреди

На слици је приказан резултат верзије алгоритма која користи динамичко програмирање за ниске "бтматмац" и "мтамтаб". Вредности у матрици које су означене жутом бојом представљају пут којим се одређује најдужа заједничка подниска, па је једно од могућих решења за овај пример подниска "мата".

б т м а т м а ц
j\i 0 1 2 3 4 5 6 7 8
0 0 0 0 0 0 0 0 0 0
м 1 0 0 0 1 1 1 1 1 1
т 2 0 0 1 1 1 2 2 2 2
а 3 0 0 1 1 2 2 3 3 3
м 4 0 0 1 2 2 2 3 3 3
т 5 0 0 1 2 2 3 3 3 3
а 6 0 0 1 2 3 3 3 4 4
б 7 0 1 1 2 3 3 3 4 4

Имплементација алгоритма у програмском језику Јава уреди

Рекурзивни алгоритам уреди

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;
	 }
}

Алгоритам заснован на динамичком програмирању уреди

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();
}

Имплементација алгоритма у програмском језику C уреди

Имплементација је у програмском језику C уз претпоставку о максималној дужини низова и уз претпоставку да се ради о низовима који садрже целобројне вредности.

#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);    
}

Сложеност алгоритама уреди

Рекурзивни алгоритам којим се одређује најдужа заједничка подниска две ниске има експоненцијалну сложеност. Ако две ниске, за које је потребно одредити најдужу заједничку подниску, имају дужине m, односно n и нека је k = m + n, време извршавања таквог алгоритма је θ(2k).

Сложеност алгоритма заснованом на динамичком програмирању за одређивање најдуже заједничке подниске две ниске износи Ο(mn).

Референце уреди

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

Литература уреди

  • Дејан Живковић, (2007). Основе дизајна и анализе алгоритама. ЦЕТ. ISBN 978-86-7991-327-2.  Поглавље 7.

Спољашње везе уреди