Suffix Trees: Simple Algorithm and Applications Valerio Lupperger Suffix Trees: Simple Algorithm and Applications Proseminar Bioinformatik Wintersemester 2010/11 Valerio Lupperger 1 Suffix Trees: Simple Algorithm and Applications Valerio Lupperger Inhaltsverzeichnis 1. Einleitung 2. Geschichte 3. Definitionen und Eigenschaften 4. Naive Konstruktion 5. Generalisierter Suffix Tree 6. Implementationsfragen 6.1 Array 6.2 Linked List 7. Anwendungsbeispiele für Suffix Trees 7.1 Exaktes String Matching 7.2 Datenbank von Pattern 2 Suffix Trees: Simple Algorithm and Applications Valerio Lupperger 1. Einleitung Um mit DNA/RNA bioinformatisch zu arbeiten, reichen meist die Sequenzen der Nukleobasen, die als String dargestellt werden. Dasselbe gilt auch für Proteine, mit denen mithilfe von AminosäurenStrings ebenfalls abstrakt gearbeitet werden kann. Diese, zum Arbeiten benötigte, Algorithmen wiederum benötigt spezielle „Werkzeuge“, um effizient durchgeführt werden zu können. Ein Beispiel hierfür stellen die Suffix Trees dar, die einen gegebenen String so vorbereiten, dass im Anschluss mit linearem Zeitaufwand festgestellt werden kann, ob ein beliebiges Pattern in diesem String auftritt, und wenn dies der Fall sein sollte, auch angibt, an welcher Stelle dieses Pattern im String zu finden ist. 2. Geschichte Zum ersten Mal wurde 1973 ein Algorithmus zur Erstellung eines sog. Positionsbaums von Weiner veröffentlicht, der als Vorgänger des heutigen Suffix Trees gesehen werden kann. Ein paar Jahre später entwickelte McCreight einen anderen Algorithmus, der ebenfalls eine lineare Laufzeit aufwies, aber weniger Speicherplatz benötigte. Es dauerte noch 20 Jahre bis ein weiterer, bis heute angewandter Algorithmus von Ukkonen entwickelt wurde. Dieser kann als eine Abwandlung des McCreight Algorithmus gesehen werden, die allerdings leichter verständlich ist. Da die beiden erst genannten Algorithmen allerdings den Ruf haben schwer verständlich zu sein, finden die Suffix Trees bis heute eher selten Anwendung. 3 Suffix Trees: Simple Algorithm and Applications Valerio Lupperger 3. Definitionen und Eigenschaften Vor der Konstruktion eines Suffix Trees müssen ein paar grundlegende Definitionen festgehalten werden. Der zu verarbeitende String sei der String S [1,...,m] mit der Länge |String| = m über dem abgeschlossenen Alphabet A. Ein Suffix Tree ST wird festgelegt durch folgende Definitionen: 1. Der ST ist ein gerichteter Wurzelbaum. 2. Die Blätter des ST sind durchnummeriert von 1 bis m. 3. Jeder innere Knoten (außer der Wurzel) besitzt mindestens 2 Kinder. 4. Jede Kante ist mit einem nicht-leeren Substring von S beschriftet. 5. Kanten die am selben Knoten beginnen, dürfen nicht mit demselben Buchstaben starten. 6. Die Verknüpfung aller Kanten, die in der Wurzel beginnen und zu einem mit Nummer i Blatt führen, ergeben genau einen Substring S[i,...,m]. Es kann passieren, dass ein Substring nicht in einem Blatt endet, wenn das Suffix zugleich ein Präfix darstellt. Um sicherzustellen, dass jedes Suffix in einem Blatt endet, wird am Ende jedes Strings und somit auch jedes Substrings ein Zeichen angehängt, das nicht in A existiert. Im Folgenden wird das Symbol $ angehängt, welches nicht in einem Alphabet vorkommt, das aus Buchstaben besteht. Somit besitzt der ST für den String S$ folgende Eigenschaften: 1. Durch das Endsymbol besitzt der ST nun m+1 Blätter, die je ein Suffix von S$ darstellen. 2. Es gibt maximal m innere Knoten. 3. Folglich gibt es höchstens 2m+1 Knoten. 4. Jeder Knoten kann bis zu |Alphabet| Kinder haben. 5. Die max. |Kanten| = |Knoten| -1 = 2m (da außer zur Wurzel, jede Kante zu einem Knoten führt) 4 Suffix Trees: Simple Algorithm and Applications Valerio Lupperger 4. Naive Konstruktion Die eigentlich in linearer Zeit durchführbare Konstruktion wird hier nun zum Verständnis des Aufbaus eines ST in naiver bzw. intuitiver Form durchgeführt. Zunächst wird der komplette String S[1,...m]$ als erste Kante an die Wurzel angefügt. Diese wird mit S$ beschriftet und an das Kantenende, das Blatt, wird eine „1“ geschrieben. Anschließend werden noch die Substrings S[i,...,m]$ (in aufsteigender Reihenfolge von 2 - m) betrachtet, deren erste Symbole mit den ersten Symbolen der bereits bestehenden Kanten verglichen werden. Sollten diese Symbole nicht identisch sein, wird von der Wurzel aus eine komplett neue Kante gebildet, die logischerweise die Beschriftung S[i,...,m]$ erhält. Am Blatt dieses Pfads wird folglich das „i“ angetragen. Bei Übereinstimmung der ersten Symbole werden die folgenden Symbole verglichen. Dann wird die bereits vorhandene Kante solange verfolgt, bis sich die Symbole unterscheiden. Dort wird ein Knoten gesetzt, aus dem eine neue Kante entspringt. Diese wird mit den restlichen Symbolen (inklusive des sich unterscheidenden Symbols) des i-ten Strings beschriftet. Das Blatt wird, wie im ersten Fall, mit einem „i“ versehen. Um den kompletten ST zu erzeugen, wird dieses Verfahren nun solange wiederholt, bis das Suffix nur noch das Terminalsymbol $ enthält, welches dann zum Schluss noch eine eigene Kante von der Wurzel aus erhält. Zur besseren Vorstellung siehe Bild 4.1. k i $ 2 i w i w i $ 4 $ 5 w $ i $ 1 3 Bild 4.1 Ein ST zum String „kiwi$“ 5 Suffix Trees: Simple Algorithm and Applications Valerio Lupperger 5. Generalisierter Suffix Tree Ein generalisierter ST ist ein ST, der sich nicht nur aus einem String zusammensetzt, sondern einer ganzen Reihe von Strings (S1,...,Sn) . Die einfachste Methode einen solchen ST zu erzeugen, besteht darin, an jeden String ein anderes Terminalsymbol anzuhängen, welches natürlich nicht in A vorkommt, die modifizierten Strings hintereinander zu hängen und einen ST der neu entstandenen Stringkette zu bilden. Die Blätter müssten dann als Informationen sowohl den dort endenden String als auch die passende Startposition enthalten. Dasselbe Ziel lässt sich auch erreichen, indem in einen bereits erstellten ST für den String S1 ein weiterer String S2 eingefügt wird. Dies passiert nach demselben Prinzip wie beim Erstellen eines ST für einen String. Es wird nach Übereinstimmungen mit bereits angetragenen Symbolen an Kantenanfängen an der Wurzel gesucht und bei Übereinstimmung die Kante verfolgt bis ein Mismatch auftritt etc. Dabei ist darauf zu achten, dass bei selbem Terminalsymbol in den Blättern evtl. mehr Informationen gespeichert werden müssen, da unterschiedliche Strings die gleichen Suffixe enthalten können und dann alle im selben Blatt enden. 6. Implementationsfragen Bei typischen Anwendungen, z. B. im bioinformatischen Bereich, geht die Stringgröße bis in die Millionen oder sogar Milliarden und deswegen ist es wichtig, eine praktische und effiziente Implementation zu finden. Die wesentlichen Fragen, um die es bei der Implementation eines ST geht, sind die Wahl der Datenstruktur für die Knoten und die dann von dort abgehenden Kanten. Hierbei ist die wichtigste Entscheidung, die Entscheidung zwischen schnellem Zugriff auf die Kanten und benötigtem bzw. vorhandenem Speicherplatz für den ST. 6 Suffix Trees: Simple Algorithm and Applications Valerio Lupperger 6.1 Array Die wohl simpelste Art einen Knoten darzustellen gelingt mithilfe eines Arrays. Jeder innere Knoten bekommt ein Array der Länge |A|, wobei die einzeln Zellen des Arrays jeweils den möglichen Anfangssymbolen der folgenden Kanten entsprechen. Die Zellen zeigen dann jeweils auf die folgenden Kinder des Knotens. Dieses Array erlaubt einen Zugriff auf Knoten und Kanten in konstanter Zeit. Allerdings wird der benötigte Speicherplatz für wachsende |String| bzw. |Alphabet| unpraktisch groß. 6.2 Linked List Eine weitere Möglichkeit den Knoten zu implementieren bietet die Linked List. Für jeden neuen Knoten wird eine Liste von Symbolen angelegt, die dann jeweils um die hinzukommenden Startsymbole der aus dem Knoten hervorgehenden Kanten erweitert wird. Beim Zugriff auf den Knoten wird dann jedes Mal die Liste nach dem entsprechenden Symbol durchsucht. Durch das Einhalten einer z. B. alphabetischen Reihenfolge kann sowohl die Konstruktions- als auch die Suchzeit verbessert werden. Im Falle einer erfolglosen Suche würde der Algorithmus im Durchschnitt früher abbrechen. 7. Anwendungsbeispiele für Suffix Trees Ist ein ST erst einmal in linearer Zeit erstellt, so können damit viele „Matching-Probleme“ effizienter gelöst werden als mit vielen anderen Methoden. Folgende Beispiele lassen sich in linearem Zeitaufwand lösen. 7.1 Exaktes String Matching Das exakte String Matching ist ein weit verbreitetes Problem, das nicht nur in der Bioinformatik anzutreffen ist. Dabei geht es darum, 7 Suffix Trees: Simple Algorithm and Applications Valerio Lupperger ein gegebenes Pattern P der Länge n in einem Text der Länge m zu finden. Dies ist in O(n+m) Zeit möglich, da der ST in O(m) Zeit aufgebaut werden kann und die anschließende Suche einen Zeitaufwand von O(n) benötigt. Die Suche nach einem Pattern in einem ST erfolgt durch das Verfolgen der passenden Kante des ST, d. h. an der Wurzel wird die Kante mit dem passenden ersten Buchstaben ausgewählt und dann solange verfolgt bis kein übereinstimmendes Zeichen mehr gefunden wird. Dann ist das Pattern nicht im Text. Andernfalls wird das Pattern komplett gefunden. In diesem Fall werden die restlichen Unterbäume nach dem gefundenen Pattern bis zu den Blättern verfolgt, um herauszufinden, wie oft und wo das Pattern im Text auftritt. Dies kann durch z. B. den depth-first Algorithmus in linearer Laufzeit in Abhängigkeit von der Anzahl der Vorkommen von P geschafft werden. Diese Gesamtlaufzeit wäre dann O(n+m+k) bei k-fachem Vorkommen. 7.2 Datenbank von Pattern In einer großen Datenbank nach einem Pattern der Länge n zu suchen wirft mehrere Probleme auf. Die Vorbereitung als auch die Suche müssen schnell gehen, aber auch der Speicherbedarf sollte möglichst gering gehalten werden. Dies alles bietet die Datenstruktur des ST. Hierfür wird der generalisierte ST benutzt, welcher die Datenbank der Länge m in linearer Zeit O(m) vorbereiten kann und dafür auch nur O(m) Platz benötigt. Ebenso kann die Suche nach dem P in linearer Zeit O(n) durchgeführt werden. 8. Fazit Zusammenfassend kann gesagt werden, dass sich mit den STAlgorithmen viele Suchalgorithmen auf Texten enorm beschleunigen lassen, da die Probleme sich in linearer Abhängigkeit zum Suchstrings vorbereiten lassen und dann in linearer Abhängigkeit zum gesuchten Pattern lösen lassen. Somit sind sie vielen anderen Suchalgorithmen überlegen. 8 Suffix Trees: Simple Algorithm and Applications Valerio Lupperger Literatur [1] D. Gusfield: Algorithms on Strings, Trees, and Sequences – Computer Science and Computional Biology, CambridgeUniversity Press, 1997; Kapitel 5, Abschnitt 6.4, Abschnitte 7.1 und 7.3 – 7.6. [2] http://de.wikipedia.org/wiki/Suffixbaum (Internetenzyklopädie) vom 02.06.2010; zuletzt besucht am 07.11.2010 [3] F. Schüle: Suffix Trees, 2006. Ausarbeitung in Form eines Seminars in der Bioinformatik an der Universität Ulm [4] S. Telejnikov, Suffixbäume, 2006. Ausarbeitung in Form eines Seminars für Algorithmen an der Universität Stuttgart 9