Sunteți pe pagina 1din 4

Căutarea într-un șir de caractere

Problema pe care vrem să o rezolvăm presupune determinarea tuturor aparițiilor unui șir
de caractere P într-un șir de caractere S. Considerăm lungimea lui S = n și lungimea lui P =m
1. Algoritmul naiv

Încercăm pe rând toate shiftările lui P în S. Există n-m+1 shiftări posibile. Verificarea
pentru fiecare shiftare se face în O(m). Practic, pentru fiecare shiftare comparăm
caracter cu caracter până la găsirea primei perechi de caractere distincte, caz in care ne
oprim. Problema acestui algoritm este că ignoră toate informațiile despre P.
Complexitatea este O(m*(n-m+1)), dar dacă toate caracterele sunt distincte algoritmul
rulează în O(n) .

Exemplu :
S= ”aaabaabaaab”
P=”aaab”
Se observă ca pe poziția 0 se găsește o potrivire a șirului P în S, dar se observă și ca pe
niciuna din pozițiile 1,2,3 nu va putea exista o potrivire, fapt pe care algoritmul naiv îl
omite.

2. Algoritmul Knuth-Morris-Prat(KMP)
Optimizarea pe care acest algoritm o aduce constă în a nu mai verifica poziții despre care
putem ști deja că nu sunt potriviri. În acest sens, vom precalcula pentru fiecare poziție din P
cel mai lung prefix care este și sufix, astfel ca atunci cănd știm ca o parte din litere se
potrivesc deja, sa nu le mai verificăm.
Considerăm exemplul :
S=”abcxabcdabxabcdabcdabcy”
P=”abcdabcy”
În primă fază, parcurgem cele două șiruri în paralel, iar pe primele 3 poziții avem o
potrivire. Prima nepotrivire o găsim pe a 4-a poziție( x != d) . Scopul este ca acum să
minimizăm numărul de caractere pe care le vom verifica în continuare, deci căutăm pe
pozițiile anterioare nepotrivirii caractere care sa se repete la începutul și sfărșitul cuvântului,
pentru ca daca acestea s-au potrivit deja, nu este necesară o reverificare a lor. În acest caz, nu
există nicio potrivire, deci se va relua comparația lui P de la poziția a 4-a. Cum a și x nu se
potrivesc ne mutăm la urmatoarea poziție. Acum, de la poziția 5 avem o potrivire pentru
primele 6 caractere. Cum următorul caracter nu mai reprezintă o potrivire, se vor căuta
caracterele care se repetă.
În ”abcdab” observăm că există un prefix de lungime 2 care este și sufix ( ”ab”). Deci, la
următorul pas, nu va fi nevoie să comparăm vectorul P de la început, ci de la poziția 3, pentru
că știm deja că primele 2 caractere se potrivesc. Algoritmul continuă în acest mod, și se
găsește în final o potrivire.
Observăm că pentru fiecare poziție din P, avem nevoie de lungimea prefixului care este și
sufix până la acea poziție. Este util deci să precalculăm aceste prefixe. Vom folosi un vector
L, cu semnificația că L[i] reprezintă lungimea prefixului maxim care este și sufix pentru șirul
P, până la poziția i.
P[i] a b c d a b c a
i 0 1 2 3 4 5 6 7
L[i] 0 0 0 0 1 2 3 1

În cazul primului element situația este evidentă, nu avem niciun prefix care să fie și sufix.
În continuare vom parcurge cuvântul în paralel cu 2 indici, și putem întălni unul din
următoarele 2 cazuri. Cele două caractere comparate să nu se potrivească, caz în care L[i] va
fi 0, sau să se potrivească. În acest caz L[i]=j+1. Explicația constă în faptul că dacă până
acum s-au potrivit j caractere, acesta este încă o potrivire, și vom incrementa ambii indici. La
întâlnirea primei nepotriviri, dăm de o situație puțin mai complicată. Deoarece j-ul crescuse,
înseamă ca pe această poziție nu există un prefix mai lung decăt anteriorul, dar poate exista
totuși un prefix. Se trece deci la valoarea din L[j-1] pe care o primește j, și se compară din nou
P[i] cu P[j], dând lui L[i] valoarea j+1 sau 0, după cum stabilisem anterior.
Calculul prefixelor se face în O(n).
void prefix()
{
    int i=0,j=-1;
    L[i]=j;
    while(i<m)
    {
        while(j>=0 && P[i]!=P[j])
            j=L[j];
        i++; j++;
        L[i]=j;
    }
}

Având lungimea prefixelor calculată, să vedem cum realizează algoritmul KMP


potrivirea. Considerăm exemplul :
S=”abxabcabcaby”
P=”abcaby”
Atunci, vectorul L va fi (0,0,0,1,2,0).
Comparăm cei 2 vectori caracter cu caracter: a se potrivește cu a, b se potrivește cu b, c nu
se potrivește cu x. Deoarece în vectorul L, pe poziția 2 avem valoarea 0, înseamnă ca niciun
caracter din prefix nu se va mai potrivi pe pozițiile anterioare acestei nepotriviri, și se va relua
comparația cu a(P[0]) si a(S[3]). Acum avem potrivirea abcab, după care c și y nu se mai
potrivesc. L[4]=2, ceea ce inseamnă ca anterior acestei nepotriviri, există o potrivire pentru
primele 2 caractere din P, deci acestea nu mai trebuiesc verificate. (Într-adevăr, înainte de c în
S se gasește secvența ab, adică exact primele caractere din P). În continuare se va verifica deci
doar de la poziția 2 din P, cu poziția la care s-a rămas în S, găsindu-se aici potrivirea.
Complexitatea acestei parcurgeri este O(m).
void KMP()
{
   int i=0,j=0;
   while(i<n)
   {
        while(j>=0 && S[i]!=P[j])
            j=L[j];
        i++;
        j++;
       if(j==m)
       {
        //s-a gasit o potrivire.
        j=L[j];
        }
 
}
}

3. Algoritmul Rabin-Karp

Dacă algoritmul KMP s-a focusat pe a sări peste anumite caractere despre care
știa cu siguranță că se potrivesc, algoritmul Rabin-Karp va încerca să minimizeze
numărul de poziții la care să încerce o potrivire. În acest sens, se va folosi o functie
hash. Algoritmul va calcula apoi valoarea functiei hash(P), și pentru fiecare poziție i
din S, va verifica mai întâi valoarea funcției hash pentru secvența i...i+m-1. Dacă
această valoare va coincide cu hash(P), atunci avem o potrivire posibilă și încercăm
caracter cu caracter.
Puterea acestui algoritm constă în alegerea funcției hash.
Să consderăm o funcție hash care adună valorile literelor din secventa
respectivă( vom număra a=1,b=2,...z=26). Fie exemplul:
S=”abdabc”
P=”abc”
Hash(abc)=6.
Acum, pentru fiecare poziție din S calculăm valoarea funcției :
Hash(abd)=7, hash(bda)=7,hash(dab)=7,hash(abc)=6. Deci singura potrivire
posibilă este începând cu poziția 3.
Această funcție hash nu este o alegere bună, deoarece pentru multe șiruri
diferite va produce aceeași valoare. Trebuie să avem în vedere că vom calcula
valoarea funcției des, deci avem nevoie de o functie pentru care să putem calcula ușor(
în număr constant de pași) valoarea curentă, cunoscând valoarea anterioară.
Se folosește uzual o funcție ce va simula reprezentarea stringului ca număr într-
o bază mare (256) calculată modulo un număr prim ( de obicei 101). Funcția va arăta
astfel :
Hash(abc) = (a*2562+b*256+c)%101.
Totuși, pentru a nu depăși valorile cuprinse în int, mai ales pentru șiruri de
lungime mare, este recomandat să aplicăm %101 pe fiecare operație. Funcția hash
astfel aleasă permite calculul in număr constant de operații pentru secvența următoare,
avand valoarea secvenței curente (demonstrați! ).
Deși pentru căutarea unui singur subșir, algoritmul KMP este superior, în cazul
în care căutam mai multe subșiruri de lungime egală într-un șir, este recomandată
folosirea algoritmului Rabin-Karp, pentru care nu se va modifica complexitatea.
Un pseudocod orientativ pentru algoritmul Rabin-Karp:
function RabinKarp(string s[1..n], string pattern[1..m])
hpattern := hash(pattern[1..m]);
for i from 1 to n-m+1
hs := hash(s[i..i+m-1])
if hs = hpattern
if s[i..i+m-1] = pattern[1..m]
return i
return not found

Probleme:
1. Potrivirea șirurilor (infoarena) – cu KMP și Rabin-Karp
2. Prefix( infoarena)
3. Seti(campion)
4. Pstring(campion)
5. Sablon(campion)
6. Map(infoarena)
7. ADN(infoarena)