Suffix Trees und Suffix Arrays Stefka Penkova Angel Tchorbadjiiski 30.Juni 2005 1 Problembeschreibung und Motivation Objekt diesem Artikels ist der Substringproblem. Eingabe des Problems P ist eine Zeichenkette S. Ziel ist alle Vorkommen einer gegebenen willkürlichen Zeichenkette q zu finden, wo q Teilkette von S ist. Es wird also eine Datenstruktur D gesucht, die P so repräsentiert, dass: (1) D lineare Platzkomplexität hat, (2) D in linearer Zeit hergestellt werden kann, (3) D die Suche von q in S in Zeit O(|m|), wobei m die Länge von q, erlaubt. 2 Suffix Trees 2.1 Definition, Vorstellung des Begriffs Der Suffix-Baum ist eine Datenstruktur, mit deren Hilfe der Stringmatching-Problem in O(m) Zeit zu l ösen ist. Was ist aber ein Suffix-Baum? Für ein Wort der Länge m ist das den Baum T mit genau m Blättern (1 bis m). Jeder innere Knoten von T ausser der Wurzel hat mindestens 2 Nachfolger und jede Kante hat eine nicht leere Beschriftung. Es kann keine Zwei Kanten vom selben Knoten mit dem selben Beschriftung geben. Die wichtigste Eigenschaft der Suffix-Bäume ist, dass jeder Pfad vom Wurzel bis zu einem Blatt einen Suffix von dem vorgegebenen Wort entspricht. 2.2 Vorgeschichte Es gibt drei Algorithmen, mit deren Hilfe man einen Suffix-Baum im O(m) Zeit erstellen kann. Der erste Algorithmus wurde im Jahre 1973 vom Weiner erfunden. Einige Jahre sp äter wurde vom McCreigth einen platzeffizienteren Algorithmus entwickelt. Vor 10 Jahren (1995) entwickelte Ukkonen ein Algorithmus, der konzeptuell unterschiedlich von den 2 obigen ist, aber die Vorteile von McCreight’s Algorithmus hat. Der ist aber auch viel leichter zu erkl ären/verstehen. 2.3 Strukturaufbau und naiver Algorithmus Es gibt eine wichtige Kleinigkeit - die Definition von Suffix-Baum garantiert nicht, dass f ür jeden String S der Länge m ein Baum existiert. Man muss noch hinzufügen, dass wenn ein Suffix von S auch ein Präfix von einem anderen Suffix von S ist, gibt es keinen Suffix-Baum, der von der obigen Definition herzuleiten ist. Deswegen nehmen wir an, dass das letzte Zeichen von S, nur einmal in dem Wort vorkommt. Wenn das nicht der Fall ist, wird am Ende ein Zeichen (z.B. $) hinzugefügt, das nicht im Wort vorkommt. Der naive Algorithmus zur Konstruktion von einem Suffix-Baum ist der Folgende: Wir nehmen an, dass das Wort S in einem Array gespeichert vorliegt. Schritt 1. Man fügt zuerst das ganze Wort in den Baum, also von Index 1 bis Index m. Im Shritt i. (i = 2 . . . m) fügt man die Folge i bis m in den Baum. Es ist dabei nur folgende Regel zu beachten: 1 Abbildung 1: Beispiel - Aachen - Falls die Indexfolge (der Suffix) [i . . . m] nicht mit einem Pr äfix von einem schon im Baum eingefügten Suffix beginnt, wird der ganze Suffix [i . . . m], so wie der ist, einfach von der Wurzel in den Baum eingef ügt. - Falls ein Präfix von [i . . . m] auch ein Präfix von einem anderen Suffix (schon im Baum eingefügt) ist, d.h. dass eine oder mehrere Anfangsbuchstaben von beiden Suffixen übereinstimmen, wird der übereinstimmende Pfad von der Wurzel verfolgt, bis man das erste Zeichen erreicht, das beide Suffixe unterscheidet. An der Stelle vor diesem Zeichen wird ein neuer Knoten eingefügt (wenn man sich gerade auf einer Kante befindet - sonst ist man schon in einem Knoten) und an der Stelle wird eine Kante von dem Knoten zu einem neu hinzugef ügten Blatt mit Nummer i eingefügt. Die Beschriftung dieser Kante ist der Suffix [i . . . m] ohne den oben genannten Präfix. So wird Schritt i so lange iteriert, bis man i = m erreicht hat. Wenn das passiert ist, haben wir den Suffix-Baum f ür das Wort S. Der Baum wurde in Zeit O(m2 ) gebildet, die nicht linear ist. Das Ziel war aber nur ein naiver Algorithmus vorzustellen, damit die Idee, die hinter den Suffix-Bäumen steckt, klar wird. 2.4 Ukkonen’s Algorithmus zur Konstruktion - Grundidee und Implementierungsdetails Der Algorithmus von Ukkonen beruht auf den impliziten Suffix-B äumen. Der Unterschied zwischen dieser Art und den normalen Suffix-Bäumen (wie oben beschrieben) ist, dass hier alle Vorkommen des zus ätzlich eingefügten Zeichens (z.B. $) entfernt werden. Dann werden zuerst alle Kanten ohne Beschriftung, danach auch alle Knoten, die weniger als 2 Nachfolger haben, vom Baum entfernt. Ukkonen’s Idee ist folgende: Man f ängt bei 1 an und bildet den impliziten Suffix-Baum T (1). Dieser Suffix-Baum besteht nur aus dem ersten Zeichen im String S. Danach macht man weiter, indem man im Schritt i + 1, i ≥ 1 den impliziten Suffix-Baum T (i + 1) von seinem Vorgänger T (i) konstruiert.Das geschieht indem man die Suffixe mit dem n ächsten Zeichen (an Position i + 1) verlängert. So hat man nach m Schritten, mit Hilfe der impliziten Suffix-B äume, den Suffix-Baum für String S konstruiert. - Beschleunigungstipps Es gibt einige Verbesserungen, die auf den oben erl äuterten Algorithmus angewandt werden können. 1. Die Erste davon ist die Benutzung von interne Links. Ein Link vom Knoten v zum Knoten s(v) w äre vorhanden, falls der Pfad von v mit x@ und der Pfad zu s(v) mit @ beschriftet ist. Dabei ist x ein einziges Zeichen und @ ist eine Kette von Zeichen, die auch leer sein kann. Mit den Suffix-Links kann man sich sehr viel Mühe bei der Konstruktion von T (i + 1) von T (i) sparen , wenn man die Links verfolgt. 2. Kompression der Kantenbeschriftungen gibt die Möglichkeit eine Platzkomplexität von O(m) zu erreichen. Diese Methode ist eigentlich sehr simpel, sogar intuitiv. Man spart Platz, indem man die Indizes der Anfangs- und Endbuchstaben des Substrings als Beschriftung der Kante benutzt und nicht der Substring selbst. Da der Algorithmus (oder besser das Programm, das den Algorithmus anwendet) eine Kopie des Substrings hat, stellt die Kompression keine Komplikationen dar. 2 Abbildung 2: Suffix-Links - Kurze Zusammenfassug , was erreicht wurde Es werden also m Phasen gebraucht, um den impliziten Suffix-Baum T (m) zu konstruiieren. Der Platzkomplexität ist maximal 2(2m − 1), da maximal 2m − 1 Kanten im Baum vorhanden sein k önnen. Es wird eine zusätzliche Umwandlung des impliziten Suffix-Baums in einem normalen Suffix-Baum, d.h. man muss nur noch den $-Zeichen im Baum einfügen. Wie wir schon wissen, ist das eine zusätzliche Phase, also haben wir nicht mehr m Phasen, sondern m + 1. Dann ist die Komplexität also O(m) + O(m + 1) = O(m), also linear bezüglich |S|. 2.5 Vergleich von Weiner’s, McCreight’s und Ukkonen’s Algorithmen Die Implementierungsideen, die hinter den drei Algorithmen stehen, sind sehr unterschiedlich, das Ziel ist aber das gleiche - der Suffix-Baum für ein bestimmtes Wort S in O(|S|) Komplexität zu erzeugen. Wenn man Weiner’s Algorithmus mit dem von Ukkonen vergleicht, kann man sehr viele Ähnlichkeiten feststellen. Sie fangen beide unterschiedlich an - der eine mit dem ersten Buchstaben, der andere mit dem ganzen String, es gibt aber eine Änlichkeit - beide fügen jedes Mal einen neuen Suffix ein. Die Kompression der Kantenbeschriftungen erweist sich als notwendig in beiden Fällen, um ein O(m) Komplexität zu erreichen. Weiner benutzt aber noch zwei Vektoren, die er ”indicator und link vectors” nennt. Das grösste Teil der Implementierung ist fast so effizient wie die von Ukkonen, Weiner braucht aber mehr Platz für diese zusätzliche (verglichen mit Ukkonen) Vektoren ist nicht so effizient. Hier stellt man fest, das der oben genannte Beschleunigungstipp immer anwendbar ist, da es immer so ein Knoten s(v) gibt, der mit @ beschriftet ist, wenn ein Knoten v mit Beschriftung x@ im Baum ist. McCreight’s und Ukkonen’s Algorithmen sind gleich effizient. Die Implementierungen sehen sich fast gar nicht ähnlich, aber wenn man sich bisschen Gedanken macht, kann man die Verbindung sehen. Eine Verbindung wurde von Ukkonen vorgeschlagen und wurde von Giegerich und Kurtz erklärt. 2.6 Generalisierte Suffix Bäume Dieser Art von Suffix-Bäume repräsentieren mehrere Strings im selben Baum. Es gibt zwei M öglichkeiten so einen Baum zu konstruieren. Hier wird die effektivere Methode von beiden vorgestellt. Nehmen wir o.B.d.A. an, dass wir n Strings haben. Wir fangen mit dem ersten (S1) an und bilden dessen Suffix-Baum. Wenn wir mit dem String S1 fertig sind, fangen wir mit dem nächsten (S2) an. Es kann sein, dass manche Suffixe von S1 und S2 übereinstimmen - die werden nur einmal eingefügt. Man macht weiter, bis alle Suffixe von S2 im Baum sind. So macht man weiter, bis alle Strings im Baum sind. Das Ergebnis ist den Suffix-Baum, in dem alle Suffixe von den gegebenen Strings S1,. . . ,Sn drin sind. 2.7 Anwendungsbeispiele Die Suffix-Bäume (inclusiv generalisierte Suffix-Bäume) , wie auch die Suffix-Array, können bei vielen Probleme sehr nützlich sein. Das betrifft aber nicht nur solche aus der Informatik, sondern auch biologisch oder chemisch bezogene 3 Probleme. Hier sind einige davon, die mit Hilfe von den oben genannten zwei Strukturen zu l ösen sind: Aus der Biologie: - genome alignment - signature selection - Finding and Representing all Tandem Repeats in a String - recognizing DNA contamination Aus der Chemie: - circular string linearization Aus der Informatik: - exact string matching - substring problem for a database of patterns - building a smaller DAG (directed acyclic graph) for exact matching - all-pair suffix-prefix matching - ziv-lempel compression (wird auch zur Kompression von DNA-Sequenz benutzt) - exact matching with wildcards - common substrings of more than two strings 3 Suffix Arrays 3.1 Vorstellung des Begriffs Ein Suffix Array für einen String S ist ein Array pos, in dem die Anfangspositionen von den Suffixen aus S gespeichert werden. Die Zahlen, die die Anfangspositionen repr äsentieren, befinden sich in dem Array bezüglich der Suffixen in lexikographischer Reihenfolge, z.B. enth ält pos[2] das Suffix, das lexikographisch an 2. Stelle steht. Suffix Arrays können in O(m) Zeit hergestellt werden und haben Platzkomplexit ät von O(m+1). Die Binäre Suche in dieser Datenstruktur benötigt lineare Zeit. Beispiel 1. Sei t = M ISSISSIP P I, dann sieht das Array pos so aus: pos[0] = 12 → $ pos[1] = 11 → I$ pos[2] = 08 → IP P I$ pos[3] = 05 → ISSIP I$ pos[4] = 02 → ISSISSIP P I$ pos[5] = 01 → M ISSISSIP P I$ pos[6] = 10 → P I$ pos[7] = 09 → P P I$ pos[8] = 07 → SIP P I$ pos[9] = 04 → SISSIP I$ pos[10] = 06 → SSIP P I$ pos[11] = 03 → SSISSIP P I$ 4 3.2 Kurz zur Geschichte Die Idee für Suffix Array wurde zuerst von U. Manber und G. Myers im Jahr 1993 [7] vorgestellt. Sie haben eine neue Datenstruktur vorgeschlagen, die platzeffizienter als die Suffix Trees war. Eine ähnliche Idee genannt PAT Trees ist im Jahr 1991 im ”Handbook of Algorithms and Data Structures” von Gonnet und Baeza-Yates erschienen. 3.3 Konstruktion von Suffix Array durch Suffix Tree in linearer Zeit Angenommen das Suffix Tree ST zu dem String S ist gegeben. Das Suffix Array kann aus dem ST durch Anwenden des Tiefensuchealgorithmus erstellt werden. Dafür muss vereinbart werden, wie die Kanten lexikographisch anordnen lassen. Eine Kante (v, u) in einem Suffix Tree ist lexikographisch kleiner als eine Kante (v, w) genau dann, wenn das erste Zeichen der Kante (v, u) in der lexikographischen Ordnung vor dem ersten Zeichen der Kante (v, w) steht. (Dabei wird angenommen, dass $ ≤ c, ∀c ∈ Σ, wobei $ das Ende der Zeichenkette bezeichnet). Jetzt kann der Tiefendurchlaufalgorithmus anwendet werden. Die Reihenfolge, in der die Bl ätter des Suffix Tree besucht werden, gibt die Reihenfolge der Einträge in dem Suffix Array. 3.4 Direkte Konstruktion von Suffix Array Eine direkte Konstruktion von Suffix Arrays erfolgt in O(n log n) Zeit. Dabei wird auf die Idee des Bucket-Sorts zurückgegriffen. Im ersten Schritt wird das Array pos mit allen Suffixen absteigend nach deren L änge initialisiert. Dann werden die Zeichenketten nach dem ersten Zeichen sortiert, wobei eine Menge von Buckets entsteht. Jeder Bucket enthält alle Suffixe, die mit dem selben Buchstaben anfangen. Abbildung 3: Das Array pos nach der ersten Phase. Im zweiten Schritt muss nach dem zweiten Buchstaben sortiert werden, aber nur innerhalb der Buckets. Seien t = ti . . . tn $ und tj = tj . . . tn $ zwei Suffixen in einem Bucket. D.h. ti = tj und es muss jeweils nach dem zweiten Zeichen sortiert werden-ti+1 und tj+1 . Der Vergleich hat das selbe Ergebnis, wie der Vergleich von t i+1 = ti+1 . . . tn $ mit ti+1 = ti+1 . . . tn $ von der erste Phase des Algorithmus. Also werden in diesem Schritt in jedem Bucket die Suffixen ti so sortiert, wie ti+1 nach dem ersten Zeichen. Dabei wird das Array pos neu sortiert, wobei neue Buckets entstehen, in den je die ersten zwei Buchstaben der Suffixen übereinstimmen. Im dritten Schritt wird gleichzeitig nach dem dritten und vierten Zeichen sortiert, d.h. es werden t i+2 ti+3 und tj+2 tj+3 verglichen. Der Vergleich wurde aber bereits implizit gemacht, es liefert das Ergebnis wie in Phase zwei, wo ti+2 = ti+2 . . . tn $ und tj+2 . . . tn $ sortiert wurden. Es werden nun also die Suffixen ti genau so sortiert, wie ti+2 nach den ersten zwei Buhstaben. Es wird analog weiter verfahren, in dem in jedem Schritt die Anzahl der verglichenen Symbolen sich verdoppelt. Es werden maximal log(n) Phasen durchgeführt. Am Ende besteht das Suffix Array aus genau n + 1 Buckets, der je einen Suffix enthält. Insgesamt braucht der Algorithmus von Manber und Myers f ür direkte Konstruktion O(n log n) Zeit. i 3.5 Auffinden von Mustern im Suffix Array Angenommen das Suffix Array für den Text t ist schon gegeben durch pos. Die gesuchte Teilzeichenkette sei q = q1 . . . q h . Wenn q in t enhalten ist, dann müssen alle Vorkommen von q in dem Array pos lexikographisch angeordnet sein. Da lexikographisch änlichen Suffixen nebeneinander im Array stehen, ist es sinnvoll bin äre Suche zu verwenden. Bei 5 dieser Art Suche werden O(log n) Vergleiche durchgef ührt und jeder Vergleich muss in O(h) Positionen suchen. Also benötigt dieser Algorithmus O(h log n) Zeit. - Erster Speed-up Sei L die linke Grenze und R die rechte Grenze des aktuellen Suchintervalls. Dabei seien t = t1 . . . tn ∈ Σ und pos das zugehörige Suffix Array. Der längste gemeinsame Präfix (LGP engl. longest common prefix) von den Positionen i < j ∈ 0, . . . , n der Wert lcp(i, j) = k, wenn t A[i] . . . tA[i]+k−1 = tA[j] . . . tA[j]+k−1 und tA[i]+k < tA[j]+k . Sei nun l die Länge des LGP von q und spos[L] (Suffix gestartet aus der Position pos[L]), r die Länge des LGP von q und spos[R] und mlr = min(l, r). Da die Einträge von pos lexikographisch angeordnet sind, sind die ersten mlr Zeichen von spos[i] für alle i ∈ [L, R] gleich. Daher können die bei dem Vergleich von q mit spos[M ] übersprungen werden. So haben wir eine Beschleunigung erreicht, die in der Praxis die Zeitkomplexit ät von O(h + log n) hat. Worst-case ist aber immer noch O(h log n). - Zweiter Speed-up Das Überprüfen eines Zeichens aus q wird redundant/überflüssig genannt, wenn dieses Zeichen schon überprüft wurde. Man kann diese redundante Kontrollen zu maximal eine per Iteration der Bin äre Suche reduzieren, also zu O(log n). Das beschleunigt den Algorithmus zu O(h + log n). 3.6 Anwendung Suffix Arrays können die selben Aufgaben wie Suffix Trees erfüllen, mit dem Unterschied, dass sie wesentlich weniger Platz verbrauchen. Diese Datenstruktur ist besonders f ür kleinere Alphabete geeignet. Literatur [1] Suffix Trees in Computational Biology. (http://homepage.usask.ca/∼ctl271/857/suffix tree.shtml). [2] Wally Bitar. Introduction to Bioinformatics (http://www.msci.memphis.edu/∼giri/compbio/f00/Wally/Wally.html). Generalized Suffix Trees. [3] M. Crochemore and W. Rytter. Jewels of Stringology. World Scientific, 2002. [4] D. Gusfield. Algorithms on Strings,Trees, and Sequences. Cambridge University Press, 1997. [5] V. Heun. Algorithmen auf Sequenzen. Skriptum zur Vorlesung, 2004. [6] D. Huson. Algorithms in Bioinformatiks. Lecture, May 2005. [7] U. Manber and G.Myers. Suffix Arrays: a new method for on-line string searching. SIAM Journal on Computing, 1993. 6