• Keine Ergebnisse gefunden

Binäre Suche im Suffix Array mit LCP-Array

Im Dokument Institut für Informatik (Seite 28-35)

4. Das String-Matching-Problem

4.3 Effiziente String-Matching-Algorithmen

4.3.2 Binäre Suche im Suffix Array mit LCP-Array

Die Lösung des String-Matching-Problems mit der binären Suche im Suffix Array wurde auch von Manber und Myers [8] vorgestellt. Sie zeigten aber auch, dass die in

{

Kapitel 4.3.1 genannte Laufzeit weiter verbessert werden kann. Dies geschieht durch die Nutzung des LCP-Arrays. Auch hier gehen wir davon aus, dass das Suffix Array und LCP-Array bereits vorliegen und dem Algorithmus als Eingabe übergeben werden. Daher wird deren Laufzeit bei der Konstruktion nicht berücksichtigt.

Algorithmus 4.5: FINDLEFTMOST - Diese Implementierung benötigt weniger Zeichenvergleiche

verwendet, wie der vorherige Algorithmus 4.3, die Funktion CMP, um Zeichenvergleiche ab einer bestimmten Position durchzuführen. Außerdem wird die Funktion lcp(u,v) verwendet, die den längsten gemeinsamen Präfix der Strings u und v angibt. Im folgenden betrachten wir nur FINDLEFTMOST, da FINDRIGHTMOST

(Algorithmus 4.6) analog dazu ist. Wir sehen, dass zunächst (wie bei Algorithmus 4.3) die Variablen n, l und r bestimmt werden. Die Zeiger l und r befinden sich an den äußersten Suffixen im Suffix Array. Zu Beginn wird überprüft, ob das gesuchte Pattern überhaupt in S vorkommt. Dazu werden die äußersten Suffixe mit P verglichen. Ist P lexikographisch kleiner als oder gleich (fl ≤ 0) die ersten m Zeichen des ersten Suffixes oder lexikographisch größer, als die ersten m Zeichen des letzten Suffixes, kommt P in S nicht vor. Daher wird für r der Wert 1 oder n+1 ausgegeben.

Falls dieser Fall nicht eintritt, wird die while-Schleife aufgerufen, in der die eigentliche binäre Suche im Suffix Array stattfindet. Dazu wird vor jedem Durchlauf überprüft, ob die Zeiger sich überholt haben oder nicht (r-l>1). Solange dies nicht geschieht, wird mit der binären Suche fortgefahren. Zunächst wird der mittlere Wert mid bestimmt. Falls hr ≥ hr ist, wird in der linken Hälfte des Suffix Arrays nach dem Pattern gesucht, ansonsten in der rechten. hl gibt die Länge des längsten gemeinsamen Präfix von SSA[l] und P an, d.h. hl = |lcp(SSA[l], P)| und hr = |lcp(SSA[r], P)|. Somit nähert der Algorithmus sich dem Pattern an und verwirft immer eine Hälfte des Suffix Arrays. Nach diesem Schritt wird der längste gemeinsame Präfix lcp(SSA[l], SSA[mid]) berechnet (bzw. lcp(SSA[r], SSA[mid])).

Lemma 4.2: Gegeben ist ein String S der Länge n und dessen LCP-Array, sodass

|lcp(SSA[i], SSA[j])| = LCP[RMQLCP(i+1,j)] = mini<k≤j{LCP[k]} für alle 1 ≤ i < j ≤ n gilt.16

Das Lemma 4.2 besagt, dass die Berechnung des lcp von zwei Suffixen SSA[i] und SSA[j] genau der kleinste Wert im LCP-Array in dem Intervall LCP[i+1, ... ,j] ist. Sei l = |lcp(SSA[i], SSA[j])|, dann sind die ersten l Zeichen jedes Suffix zwischen SSA[i] und SSA[j] identisch. Dies ist nur möglich, wenn auch jeweils die benachbarten Suffixe einen lcp-Wert von mindestens l haben. d.h. l ist der kleinste Wert im LCP-Array im Intervall i+1 bis j.

Beispiel 4.4: Seien SSA[i], SSA[j] und m=|SSA[i]|, n=|SSA[j]|. Wenn lcp(SSA[i],SSA[j])=l, dann müssen alle Suffixe, die dazwischen liegen, denselben Präfix haben. D.h. die Werte im LCP-Array in dem Intervall i+1 bis j haben ebenfalls einen Wert von

Als Beispiel nehmen wir den String S = aaabaaacaa$ der Länge 11. Wir berechnen lcp(SSA[3], SSA[7]) = 2. Man sieht, dass alle Suffixe, die zwischen den beiden Suffixen SSA[3] und SSA[7] liegen einen gemeinsamen Präfix „aa“ der Länge 2 haben. Die LCP-Werte im Intervall 4 (=i+1) bis 7 (=j) sind größer oder gleich l.

i SA[i] LCP[i] SSA[i]

1 11 -1 $

2 10 0 a $

3 9 1 a a$

4 1 2 a aa b a a a c a a $

5 5 3 a aa c a a $

6 2 2 a ab a a a c a a $

7 6 2 a ac a a $

8 3 1 a b a a a c a a $

9 7 1 a c a a $

10 4 0 b a a a c a a $

11 8 0 c a a $

12 -1

Um nun den kleinsten Wert in einem Intervall zu suchen, kann die Funktion RMQ verwendet werden (|lcp(SSA[i], SSA[j])| = LCP[RMQLCP(i+1,j)]).

Theorem 4.1: Gegeben ist ein Array A. Nach einem Vorverarbeitungsschritt, der lineare Zeit und linearen Speicher benötigt, kann die Anfrage RMQ(i,j) in konstanter Zeit beantwortet werden.17

Das Theorem 4.1 besagt, dass eine RMQ-Abfrage in konstanter Zeit möglich ist, wenn das LCP-Array dementsprechend vorher bearbeitet wurde. Diese kann direkt nach der Konstruktion des LCP-Array einmalig erfolgen und abgespeichert werden, da der Speicherbedarf sich nicht erhöht. Dieses bearbeitete LCP-Array wird anschließend dem String-Matching-Algorithmus übergeben. Der Algorithmus 4.5 spart somit weitere Schritte, wodurch sich die Laufzeit verbessert.

Nachdem nun die Werte von hl und lcpl (bzw. hr und lcpr) bestimmt wurden, wird das Pattern P mit dem mittleren Suffix SSA[mid] verglichen. Der Algorithmus 4.5 verwendet aber folgendes Lemma 4.4. Dieses Lemma zeigt, dass es in manchen Fällen möglich ist zu bestimmen, ob ein bestimmter String lexikographisch kleiner als ein anderer ist, ohne weitere Zeichen miteinander vergleichen zu müssen.

Lemma 4.3: Seien u,v und w Strings, wobei u lexikographisch kleiner ist als v.

Wenn |lcp(u,v)| < |lcp(u,w)|, dann ist w auch lexikographisch kleiner als v.18

17 [9] Seite 53.

18 [9] Seite 123.

Beweis Sei k = |lcp(u,v)|. Da u < v gilt, dass das (k+1)-te Zeichen von u kleiner als das (k+1)-te Zeichen von v ist. Zudem folgt die Ungleichung |lcp(u,v)| < |lcp(u,w)|, da das (k+1)-te Zeichen von u mit dem (k+1)-ten Zeichen von w übereinstimmt. D.h.

wir haben u[1, ... , k] = v[1, ... , k] = w[1, ... , k] und u[k+1] = w[k+1] < v[k+1].

Beispiel 4.5: Gegeben ist das folgende LCP-Array des Strings S = aaabaaacaa$. Es gilt |lcp(SSA[4],SSA[6])|<|lcp(SSA[4],SSA[5])| und u ist lexikographisch kleiner als v, wenn entsprechend dem Lemma 4.3 u=SSA[4]=aaabaaacaa$, v=SSA[6]=aabaaacaa$ und w=SSA[5]=aaacaa$ gewählt sind. Wir berechnen k =|lcp(u,v)|=2, d.h. dass die ersten beiden Zeichen von u und v sind identisch. Da u lexikographisch kleiner als v ist, ist das 3.(=k+1) Zeichen von u lexikographisch kleiner als das 3. Zeichen von v (u[2+1]=a < b= v[2+1]). Damit die Ungleichung |lcp(u,v)|<|lcp(u,w)| gilt, muss

|lcp(u,w)| mindestens k+1 ergeben. D.h. das 3. Zeichen von v muss mit dem 3.

Zeichen von w übereinstimmen (u[2+1]=a=w[2+1]). Damit ergibt sich, dass w auch lexikographisch kleiner als v ist.

Dieses Lemma wird im Algorithmus verwendet, um zu entscheiden, ob das Pattern P lexikographisch kleiner oder größer als der Suffix SSA[mid] ist, um im nächsten Schritt entweder im nächsten Intervall weiterzusuchen, oder P mit dem aktuellen Suffix zu vergleichen. Im folgenden gehen wir alle Möglichkeiten durch für hl ≥ hr, da der Fall hl < hr analog ist.

Fall 1: lcpl = |lcp(SSA[l], SSA[mid])| > |lcp(SSA[l], P)| = hl

Es gilt SSA[l] < P. Wenn wir gemäß dem Lemma 4.3 u = SSA[l], v = P und w = SSA[mid]

wählen folgt, dass SSA[mid] < P ist. Zudem gilt auch |lcp(SSA[mid],P)| = |lcp(SSA[l],P)|.

Daher wird l auf den Wert mid gesetzt und hl bleibt unverändert.

Fall 2: lcpl = |lcp(SSA[l], SSA[mid])| = |lcp(SSA[l], P)| = hl

In diesem Fall wissen wir, dass die ersten hl Zeichen von P und SSA[mid]

übereinstimmen. Um aber die nachfolgenden Zeichen miteinander zu vergleichen, wird die Funktion CMP(P,SSA[mid],hl) aufgerufen.

= u

= w

= v

Fall 3: lcpl = |lcp(SSA[l], SSA[mid])| < |lcp(SSA[l], P)| = hl

Es gilt SSA[l]<SSA[mid]. Wenn wir gemäß dem Lemma 4.3 u = SSA[l], v = SSA[mid] und w = P wählen folgt, dass P < SSA[mid] ist. Zudem gilt auch |lcp(SSA[mid],P)| = |lcp(SSA [l],P)|. Daher wird r auf den Wert mid und hr auf lcpl gesetzt. (Der neue hr-Wert ist immer größer als der vorherige.)

Der Algorithmus 4.5 spart viele Zeichenvergleiche. Die Werte von hr und hl werden während der binären Suche nicht kleiner, sondern nur größer, je mehr Zeichen mit dem gesuchten Pattern übereinstimmen. Die Funktion CMP überspringt immer die ersten hl bzw. hr Zeichen, sodass in der binären Suche höchsten m=|P|

Zeichenvergleiche durchgeführt werden. Die binäre Suche benötigt O(log(n)) Zeit, wodurch sich eine Gesamtlaufzeit von O(m+log(n)) im Worst-Case ergibt.19

Wir sehen, dass das String-Matching-Problem in O(m+log(n)) Zeit gelöst werden kann, wenn wir erweiterte Suffix Arrays verwenden. Diese müssen aber zunächst konstruiert, vorbearbeitet und gespeichert werden. Dieser Aufwand ist jedoch gering, wenn nachfolgend sehr oft String-Matching durchgeführt wird.

19 [9] Seite 123.

Algorithmus 4.6: FINDRIGHTTMOST -benötigt weniger Zeichenvergleiche

FINDRIGHTMOST(S, P, SA, LCP) n ← length(S)

l ← 1 r ← n

(hl, fl) ← CMP(P, SSA[l], 0) if fl < 0 then

return 0

(hr, fr) ← CMP(P, SSA[r], 0) if fr ≥ 0 then

return n while r–l>1 do

mid ← ⎣(l+r)/2⎦

(c, fc) ← CMP(P, SSA[mid], min{hl,hr}) if hl ≥ hr then

lcpl ← |lcp(SSA[l],SSA[mid])|

if lcpl > hl then

(c, fc) ← (hl, 1) else if lcpl = hl then

(c, fc) ← cmp(P, SSA[mid], hl) else

(c, fc) ← (lcpl, –1) else

lcpr ← |lcp(SSA[mid],SSA[r])|

if lcpr > hr then

(c, fc) ← (hr, –1) else if lcpr = hr then

(c, fc) ← cmp(P, SSA[mid], hr) else

(c, fc) ← (lcpr, 1) if fc ≥ 0 then

(hl, l) ← (c, mid) else

(hr, r) ← (c, mid) return l

Im Dokument Institut für Informatik (Seite 28-35)