LCP-Array
Das LCP-Array ist eine Datenstruktur aus der Informatik, welche meist in Kombination mit dem Suffixarray verwendet wird. Die Bezeichnung „LCP“ ist eine Abkürzung für „Skriptfehler: Ein solches Modul „Vorlage:lang“ ist nicht vorhanden.“ (dt. längstes gemeinsames Präfix). Das Array selbst enthält die Länge des längsten gemeinsamen Präfixes von je zwei lexikographisch aufeinanderfolgenden Suffixen.
Für das LCP-Array gibt es zahlreiche Anwendungen aus dem Bereich der Textsuche und -indizierung, wie beispielsweise die Konstruktion des Suffixbaums oder eine effiziente Suche aller Vorkommen eines Suchmusters in einem Text. Der benötigte Speicherplatz des LCP-Arrays ist linear im Vergleich zur Textgröße und es gibt Algorithmen, die das LCP-Array in linearer Zeit mit Hilfe des Suffixarrays konstruieren. Es wurde erstmals in einer Veröffentlichung von Manber und Myers (1993)<ref name="10.1137/0222058">Skriptfehler: Ein solches Modul „Vorlage:Literatur“ ist nicht vorhanden.Vorlage:Cite book/URLVorlage:Cite book/MeldungVorlage:Cite book/MeldungVorlage:Cite book/MeldungVorlage:Cite book/MeldungVorlage:Cite book/MeldungVorlage:Cite book/MeldungVorlage:Cite book/Meldung2</ref> benutzt, in der es als Hgt-Array bezeichnet wurde.
Definition[Bearbeiten]
Sei <math>T=t_1,t_2,...,t_{n}</math> ein Text der Länge <math>n</math> und sei <math>A</math> das Suffixarray über dem Text <math>T</math>. Außerdem bezeichne <math>T^{i}</math> das Suffix <math>t_i,t_{i+1},...,t_n</math> und <math>lcp(s,t)</math> die Länge des längsten gemeinsamen Präfixes der beiden Strings <math>s</math> und <math>t</math>.
Das LCP-Array <math>H</math> ist ein Array der Größe <math>n</math>, wobei die einzelnen Felder wie folgt definiert sind:
<math>H[i] = \begin{cases} \text{undefiniert}, & \text{für }i = 1 \\ lcp(T^{A[i-1]},T^{A[i]}), & \text{für }2 \leq i \leq n \end{cases}</math>
Das Suffixarray <math>A</math> enthält eine lexikographische Sortierung aller Suffixe von <math>T</math>. Etwas informeller ausgedrückt bezieht sich ein Eintrag das LCP-Array immer auf zwei lexikographisch aufeinanderfolgende Suffixe von <math>T</math> und beschreibt die Länge des längsten gemeinsamen Präfixes der betrachteten Suffixe. Der Wert von <math>H[1]</math> ist undefiniert, weil <math>T^{A[1]}</math> bereits das lexikographisch kleinste Suffix von <math>T</math> ist und keinen Vorgänger besitzt.
Beispiel[Bearbeiten]
Betrachte den Text <math>T=\text{mississippi}\$</math>.
| i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| T[i] | m | i | s | s | i | s | s | i | p | p | i | $ |
Das Suffixarray von <math>T</math> repräsentiert die Sortierung der Suffixe des Textes, wobei im Array jeweils der Index des ersten Buchstabens des jeweiligen Suffixes gespeichert wird. Für den Text <math>T</math> sieht die Suffixsortierung wie folgt aus:
| i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| A[i] | 12 | 11 | 8 | 5 | 2 | 1 | 10 | 9 | 7 | 4 | 6 | 3 |
| 1 | $ | i | i | i | i | m | p | p | s | s | s | s |
| 2 | $ | p | s | s | i | i | p | i | i | s | s | |
| 3 | p | s | s | s | $ | i | p | s | i | i | ||
| 4 | i | i | i | s | $ | p | s | p | s | |||
| 5 | $ | p | s | i | i | i | p | s | ||||
| 6 | p | s | s | $ | p | i | i | |||||
| 7 | i | i | s | p | $ | p | ||||||
| 8 | $ | p | i | i | p | |||||||
| 9 | p | p | $ | i | ||||||||
| 10 | i | p | $ | |||||||||
| 11 | $ | i | ||||||||||
| 12 | $ |
Das LCP-Array <math>H</math> enthält die Länge des längsten gemeinsamen Präfixes zweier aufeinanderfolgender Suffixe. Es kann konstruiert werden, indem die Suffixe in der Suffixsortierung zeichenweise miteinander verglichen werden. Beispielsweise werden für die Berechnung des Wertes <math>H[10]</math> die bei <math>A[10]</math> und <math>A[9]</math> beginnenden Suffixe miteinander verglichen: Das längste gemeinsame Präfix von <math>T^{A[10]}=T^4=\text{sissippi}\$</math> und <math>T^{A[9]} = T^7 = \text{sippi}\$</math> ist <math>\text{si}</math> und hat damit eine Länge von 2. Dementsprechend ist <math>H[10] = 2</math>. Die restlichen Werte des LCP-Arrays können auf die gleiche Weise berechnet werden. Der Wert von <math>H[1]</math> bleibt dabei undefiniert, da es kein Suffix gibt, das lexikographisch vor <math>T^{A[1]} = T^{12} = \$</math> liegt. Das LCP-Array für den Text <math>T</math> sieht wie folgt aus:
| i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| H[i] | <math>\bot</math> | 0 | 1 | 1 | 4 | 0 | 0 | 1 | 0 | 2 | 1 | 3 |
Berechnung[Bearbeiten]
Die einfachste Art, das LCP-Array zu berechnen, wäre (so wie im obigen Beispiel) mit Hilfe des Suffixarrays die lexikographisch aufeinanderfolgenden Suffixe zeichenweise zu vergleichen, um so die Länge des längsten gemeinsamen Präfixes zu berechnen. Allerdings ergibt sich mit diesem Verfahren im schlimmsten Fall eine Laufzeit von <math>O(n^2)</math>. Enthält ein Text beispielsweise <math>n</math> gleiche Zeichen, so müssten insgesamt <math>1 + 2 + 3 + ... + (n-1) = O(n^2)</math> Vergleiche getätigt werden.
Ein effizienterer Ansatz basiert auf der Grundidee, die LCP-Einträge in Textreihenfolge zu berechnen. Angenommen <math>i</math> und <math>j</math> sind aufeinanderfolgende Werte im Suffixarray und <math>T^i</math> und <math>T^j</math> haben ein gemeinsames Präfix von <math>h</math> Zeichen. Die Suffixe <math>T^{i+1}</math> und <math>T^{j+1}</math> besitzen dann <math>h-1</math> gemeinsame Zeichen. Allerdings müssen <math>i+1</math> und <math>j+1</math> im Suffixarray nicht nebeneinander stehen; es kann durchaus Suffixe geben, die lexikographisch zwischen <math>T^{i+1}</math> und <math>T^{j+1}</math> liegen. Angenommen <math>k</math> sei der Wert, der vor dem Wert <math>i+1</math> im Suffixarray steht. Dann müssen <math>T^{i+1}</math> und <math>T^k</math> wegen der lexikographischen Sortierung der Suffixe mindestens <math>h-1</math> gemeinsame Zeichen haben (denn <math>k</math> liegt im Suffixarray zwischen <math>i+1</math> und <math>j+1</math>), das heißt, es würde reichen die beiden Suffixe ab dem <math>h</math>-ten Zeichen miteinander zu vergleichen.
Für diesen Ansatz wird das inverse Suffixarray <math>A^{-1}</math> benötigt, um herauszufinden, an welcher Stelle ein bestimmter Wert in <math>A</math> vorkommt. Bei <math>A^{-1}</math> handelt es sich um die inverse Permutation von <math>A</math>, das heißt <math>A^{-1}[i]</math> gibt an, an welcher Stelle im Suffixarray <math>A</math> der Wert <math>i</math> steht.
Es ergibt sich folgender Algorithmus: <syntaxhighlight lang="c" line> LCP-Array(T, A, n)
for (i=1 to n) {
Ainv[A[i]] = i;
}
H[1] = 0;
h = 0;
for (i=1 to n) {
if (Ainv[i] ≠ 1) {
j = A[Ainv[i] - 1]
while T[i+h] = T[j+h]
h = h + 1
H[Ainv[i]] = h
h = max(0, h - 1)
}
}
</syntaxhighlight>
Die Laufzeit ist linear, da <math>h</math> maximal den Wert <math>n</math> annehmen kann. Da <math>h</math> in jedem Schritt nur um 1 dekrementiert wird, wird die innere While-Schleife höchstens <math>O(n)</math> mal ausgeführt. Es ergibt sich somit eine Gesamtlaufzeit von <math>O(n)</math>.
Der oben vorgestellte Ansatz stammt von Kasai et al. (2001)<ref>Kasai, T. et al.: Linear-Time Longest-Common-Prefix Computation in Suffix. In: Proceedings of the 12th Annual Symposium on Combinatorial Pattern Matching, 2001.</ref> und ist der erste veröffentlichte Algorithmus, der das LCP-Array in linearer Zeit berechnet. Manzini (2004)<ref>Manzini, Giovanni: Two Space Saving Tricks for Linear Time LCP Array Computation. In: Lecture Notes in Computer Science, Volume 3111, Seite 372, 2004.</ref> hat eine verbesserte Version entwickelt, die neben dem eigentlichen Text, dem Suffix-Array und dem LCP-Array keinen zusätzlichen Speicher benötigt. Eine weitere Verbesserung ist der φ-Algorithmus von Kärkkäinen, Manzini und Puglisi:<ref>Kärkkäinen, Juha; Manzini, Giovanni; Puglisi, Simon J.: Permuted Longest-Common-Prefix Array. In: Lecture Notes in Computer Science, Volume 5577, Seite 181, 2009.</ref> Während der ursprüngliche Algorithmus durch nicht sequentielle Zugriffe auf die Arrays für entsprechend lange Texte bis zu <math>4n</math> Cache-Misses verursacht (nämlich in den Zeilen 3, 9, 10 und 12), kommt der φ-Algorithmus mit nur <math>3n</math> Cache-Misses aus.
Die oben genannten Algorithmen benutzen für die Berechnung des LCP-Arrays ein bereits vorhandenes Suffixarray. Es gibt Algorithmen, die das LCP-Array gleichzeitig mit dem Suffix-Array berechnen. Der zur Zeit schnellste Linearzeit-Algorithmus stammt von Fischer (2011).<ref>Fischer, Johannes: Inducing the LCP-Array. In: Lecture Notes in Computer Science, Volume 6844, Seite 374–385, 2011.</ref>
Gog & Ohlebusch (2011)<ref>Gog, Simon; Ohlebusch, Enno: Fast and Lightweight LCP-Array Construction Algorithms. In: Proceedings of the Workshop on Algorithm Engineering and Experiments, ALENEX, Seite 25–34, 2011.</ref> haben zwei Algorithmen veröffentlicht, die im Worst-Case eine quadratische Laufzeit haben, aber in der Praxis schneller sind, als die oben erwähnten Linearzeit-Algorithmen.
Beschleunigung der Textsuche mit Hilfe des LCP-Arrays[Bearbeiten]
Mit dem Suffixarray kann das Vorkommen eines Musters <math>P</math> der Länge <math>m</math> in einem Text <math>T</math> der Länge <math>n</math> mit Hilfe von binärer Suche bestimmt werden. Da die Suffixe im Suffixarray lexikographisch sortiert sind, genügen <math>O(\log{n})</math> Vergleiche, um das lexikographisch kleinste (bzw. größte) Suffix zu finden, welches <math>P</math> enthält. Für jeden Vergleich müssen dabei bis zu <math>m</math> Zeichen verglichen werden, wodurch dieses Verfahren eine Laufzeit von <math>O(m \log{n})</math> besitzt.
Mit Hilfe des LCP-Arrays lässt sich die Laufzeit auf <math>O(m+\log{n})</math> verbessern, indem verhindert wird, dass bei jedem Schritt der binären Suche das Muster <math>P</math> erneut von Beginn an gelesen werden muss.
Bei jedem Schritt der binären Suche liegt eine linke Intervallgrenze <math>l</math>, eine rechte Intervallgrenze <math>r</math> und ein mittleres Element <math>m</math> vor. Dabei wird <math>P</math> mit dem lexikographisch <math>A[m]</math>-ten Suffix (also mit <math>T^{A[m]}</math>) verglichen. Stimmen beide Strings überein, ist die binäre Suche fertig, ansonsten muss entweder in der linken oder rechten Intervallhälfte weitergesucht werden. Wir schauen uns den Fall an, dass <math>P</math> lexikographisch kleiner als <math>T^{A[m]}</math> ist – der andere Fall ist analog. Sei <math>k=lcp(P, T^{A[m]})</math> die Länge des längsten gemeinsamen Präfixes der beiden Strings. Das heißt, dass das <math>k+1</math>-te Zeichen von <math>P</math> lexikographisch kleiner als das von <math>T^{A[m]}</math> ist.
Das neue Intervall besitzt die Grenzen <math>l</math> und <math>m</math> und das mittlere Element <math>m'</math>. Sei <math>k'=lcp(T^{A[m]}, T^{A[m']})</math> die Länge des längsten gemeinsamen Präfixes zwischen dem alten und dem neuen mittleren Element. Es folgt eine Fallunterscheidung:
- <math>k < k'</math>: Das <math>k+1</math>-te Zeichen der Suffixe <math>T^{A[m]}</math> und <math>T^{A[m']}</math> ist gleich. Daher muss <math>P</math> auch lexikographisch kleiner als <math>T^{A[m']}</math> sein und die binäre Suche kann ohne weitere Vergleiche in der linken Intervallhälfte fortgesetzt werden.
- <math>k > k'</math>: Wegen <math>m' < m</math> ist das Suffix <math>T^{A[m']}</math> lexikographisch kleiner als das Suffix <math>T^{A[m]}</math>. Das bedeutet, dass das <math>k'+1</math>-te Zeichen von Suffix <math>T^{A[m']}</math> kleiner als das <math>k'+1</math>-te Zeichen von Suffix <math>T^{A[m]}</math> sein muss. Da <math>T^{A[m]}</math> und <math>P</math> ein gemeinsames Präfix von mindestens <math>k'+1</math> Zeichen haben, folgt hier, dass <math>T^{A[m']}</math> lexikographisch kleiner ist als P und die binäre Suche wird in der rechten Hälfte fortgesetzt.
- <math>k = k'</math>: <math>P</math> und <math>T^{A[m']}</math> haben ein gemeinsames Präfix von mindestens <math>k</math> Zeichen. Hier müssen beide Strings verglichen werden, wobei der Vergleich erst beim <math>k+1</math>-ten Zeichen beginnen muss.
Durch das beschriebene Verfahren ist es bei der binären Suche niemals notwendig bereits gelesene Zeichen von <math>P</math> nochmals zu lesen. Das Verfahren stammt von Manber und Myers<ref name="10.1137/0222058"/>. Da sich im LCP-Array nur die <math>lcp</math>-Werte für lexikographisch aufeinanderfolgende Suffixe befinden, wird im Folgenden noch gezeigt, wie sich beliebige <math>lcp</math>-Werte effizient berechnen lassen.
Im Allgemeinen lässt sich das längste gemeinsame Präfix von zwei Suffixen mit Hilfe einer Range Minimum Query über dem LCP-Array finden<ref name="10.1007/11780441_5">Skriptfehler: Ein solches Modul „Vorlage:Literatur“ ist nicht vorhanden.Vorlage:Cite book/URLVorlage:Cite book/MeldungVorlage:Cite book/MeldungVorlage:Cite book/MeldungVorlage:Cite book/MeldungVorlage:Cite book/MeldungVorlage:Cite book/MeldungVorlage:Cite book/Meldung2</ref>. Für zwei Suffixe <math>T^{A[i]}, T^{A[j]}</math>, betrachtet man alle Suffixe, die lexikographisch dazwischen liegen: Falls zwei aufeinanderfolgende Suffixe ein längstes gemeinsames Präfix der Länge <math>k</math> besitzen, so können <math>T^{A[i]}</math> und <math>T^{A[j]}</math> wegen der lexikographischen Sortierung kein längeres gemeinsames Präfix haben. Der Wert <math>lcp(T^{A[i]}, T^{A[j]})</math> entspricht daher genau dem Minimum über den LCP-Einträgen <math>H[i+1], ... , H[j]</math>. Dieses Minimum lässt sich mit geeigneten Verfahren für Range Minimum Queries in konstanter Zeit finden.<ref name="10.1137/090779759">Skriptfehler: Ein solches Modul „Vorlage:Literatur“ ist nicht vorhanden.Vorlage:Cite book/URLVorlage:Cite book/MeldungVorlage:Cite book/MeldungVorlage:Cite book/MeldungVorlage:Cite book/MeldungVorlage:Cite book/MeldungVorlage:Cite book/MeldungVorlage:Cite book/Meldung2</ref>
Literatur[Bearbeiten]
- Enno Ohlebusch Bioinformatics Algorithms: Sequence Analysis, Genome Rearrangements, and Phylogenetic Reconstruction. Oldenbusch Verlag, 2013, Kapitel 4.
Einzelnachweise[Bearbeiten]
<references />