Suffix Trees: Simple Algorithm and Applications Melanie Jakob 10. Mai 2010 1 Einleitung Ein Suffix Tree ist eine Datenstruktur, die die interne Struktur eines Strings verdeutlicht und alle Teilstrings eines Strings S enthält. Ihr Vorteil liegt darin, Lösungen für viele String Probleme, wie zum Beispiel das Substring-Problem, in linearer Zeit zu finden. Dabei schlagen Suffix Trees Brücken zwischen exaktem und unexaktem Matching und finden unter anderem in der Genomanalyse Verwendung. Bei einem gegebenen Text T der Länge m kann der Suffix Tree in einer Vorbereitungszeit von O(m) erstellt werden und ein beliebiger String der Länge n kann in O(n) in T gefunden werden, oder dessen Auftreten wird ausgeschlossen. Die lineare Zeitgrenze der Suche ist essentiell für typische Anwendungen der Suffix Trees, bei denen eine große Menge zu suchender Strings eingegeben wird. Das Erstellen eines Suffix Trees in O(m) ist durch den Algorithmus von Ukkonen möglich, auf den im Folgenden noch näher eingegangen wird. 2 Geschichte 1973 entwickelte Peter Weiner einen Algorithmus zur Konstruktion von Positionsbäumen. Diese Positionsbäume sind die Vorgänger der heutigen Suffix Trees, benötigten jedoch sehr viel Speicherplatz. Edward M. McCreight erfand 1976 einen platzsparenderen Algorithmus zur Konstruktion von Suffix Trees. Weitere 20 Jahre später wurde ein anders konzipierter Algorithmus von Esko Ukkonen erfunden. Dieser Algorithmus enthält alle ursprünglichen Ideen von McCreights, lässt sich aber wesentlich einfacher erklären. Da die Vorgehensweise und Implementierungstricks sehr ähnlich sind, kann man Ukkonens Algorithmus als eine Suffix Trees: Simple Algorithm and Applications Melanie Jakob Variante von McCreights Algorithmus betrachten. Ein grundlegende Idee des Ukkonen-Algorithmus ist, dass zuerst von einem naiven Implementationsansatz ausgegangen wird, dessen worst-case-Laufzeit durch leicht zu verstehende Implementationstricks verbessert wird. Dadurch kommt der Algorithmus auf eine Laufzeit von O(m) in Abhängigkeit zur Länge des Strings. Obwohl mehr als 30 Jahre seit Weiners Original vergangen sind, werden Suffix Trees nur sehr selten in der Informatik unterrichtet. Sie erhalten generell weniger Aufmerksamkeit und Anwendung als erwartet, vermutlich aufgrund der schlechten Verständlichkeit der beiden originalen Paper aus den 70er Jahren. Trotzdem sind die Algorithmen, obwohl sie nicht simpel sind, nicht komplizierter als andere viel gelehrte Methoden. Wenn sie gut implementiert sind, sind die Algorithmen praktisch anwendbar und ermöglichen effiziente Lösungen für viele komplexe String Probleme. 3 Grundlegende Definitionen Ein Suffix Tree T für einen String S der Länge m ist ein gerichteter Wurzelbaum mit genau m Blättern nummeriert von 1 bis m. Jeder innere Knoten, außer der Wurzel, hat mindestens zwei Kinder und jede Kante ist mit einem nicht-leeren Substring von S beschriftet. Die Beschriftung zweier ausgehender Kanten eines Knotens darf nicht mit demselben Buchstaben beginnen. S[i..m] entspricht der Verknüpfung der Kanten-Beschriftungen auf dem Weg von der Wurzel bis zum Blatt i und stellt exakt den Suffix von S beginnend bei der Position i dar. In bestimmten Fällen können Probleme bei der Konstruktion eines Suffix Trees auftauchen. Nämlich genau dann, wenn ein Suffix von S mit einem Präfix eines anderen Suffixes von S übereinstimmt. Dann kann kein Suffix Tree nach den obigen Bestimmungen erstellt werden, weil der Pfad des ersten Suffixes nicht in einem Blatt enden würde. Die Lösung dieses Problems ist denkbar einfach. Um dafür zu sorgen, dass kein Suffix ein Präfix eines anderen Suffixes ist, hängt man am Ende jedes Strings ein Zeichen an, welches nicht aus demselben Alphabet stammt wie S. Wenn man die Terminierung durch ein solches Zeichen, z.B. $, nachdrücklich betonen möchte, kann man den String als S$ bezeichnen. Meistens ist diese Hilfestellung jedoch nicht nötig und ohne es explizit zu erwähnen, ist jeder String durch das Symbol $ abgeschlossen, auch wenn das Symbol selbst nicht zu sehen ist. Seite 2 von 8 Suffix Trees: Simple Algorithm and Applications 4 Melanie Jakob Naiver Algorithmus zur Aufstellung eines Suffix Trees In diesem Abschnitt wird ein einfaches Verfahren vorgestellt, für einen String S einen Suffix Tree zu konstruieren. Zuerst wird für das Suffix S[1..m]$, das den gesamten Eingabestring darstellt, eine Kante von der Wurzel aus geschaffen. Diese Kante wird mit S$ beschriftet und das neu hinzugefügte Blatt am Ende der Kante wird mit „1“ beschriftet. Im zweiten Schritt wird jedes Suffix S[i..m] betrachtet, wobei i von 2 bis m inkrementiert wird. Für dieses Suffix muss beginnend bei der Wurzel der längste Pfad gefunden werden, bei welchem die Beschriftung mit einem Präfix von S[i..m] übereinstimmt. Dieser Pfad ist einzigartig, weil die Beschriftungen ausgehender Kanten eines Knotens nie mit denselben Buchstaben beginnen. An einem gewissen Punkt sind keine weiteren Übereinstimmungen mehr möglich, weil kein Suffix von S$ ein Präfix von irgendeinem anderen Suffix von S$ sein kann. Wenn der Algorithmus so in der Mitte einer Kante angekommen ist und kein neues Zeichen, welches zu einer Verlängerung des gefundenen Präfixes von S[i..m] passt, mehr vorhanden ist, wird an dieser Stelle die Kante aufgebrochen und ein neuer Knoten mit einer neuen Kante angefügt. Die neue Kante wird mit den verbleibenden Buchstaben von S[i..m] beschriftet, die nicht mehr mit dem Pfad übereinstimmen. Das Blatt bekommt den Index i. Sollte kein passender Pfad im vorhandenen Suffix Tree existieren, wird eine neue Kante von der Wurzel aus angelegt, beschriftet mit S[i..m]. Das dazugehörige Blatt wäre natürlich i. Der Verlauf der Konstruktion lässt sich mit Hilfe der Abbildung leicht nachvollziehen. [7] CO 1 O$ OC 2 $ CO $ 1 O$ OC CO C O$ CO C O$ 1 usw. 3 2 Da für einen String der Länge n gilt, dass die Anzahl der Zeichen aller Suffixe des Strings m1 gleich ist, beträgt die Laufzeit des naivem Algorithmus im worst-case O(m2). 2 Seite 3 von 8 Suffix Trees: Simple Algorithm and Applications 5 Melanie Jakob Generalisierter Suffix Tree für eine Menge von Strings Ein Generalisierter Suffix Tree ist eine Baum, der für eine Menge von Strings (S 1 … Sk) steht. Der einfachste Weg diesen Generalisierten Suffix Tree zu bilden, ist am Ende jedes Strings ein unterschiedliches Kennzeichen zu setzen und anschließend alle Strings aneinander zu reihen. Die Kennzeichen dürften dabei selbstverständlich nicht aus demselben Alphabet stammen, wie die Strings. Aus dieser Reihe kann nun ein Suffix Tree in linearer Zeit in Abhängigkeit zur Summe der einzelnen Stringlängen konstruiert werden. Die Nummerierung der Blätter bestünde dabei jeweils aus der Nummer für den String S i und der Startnummer in Si. Ein Generalisierter Suffix Tree lässt sich auch ohne eine vorherige Verknüpfung der Strings aufstellen. Dabei könnten die zusammengefassten Beschriftungen auf unterschiedlichen Kanten möglicherweise auf unterschiedliche Strings zurückzuführen sein. Folglich würde die Anzahl der Indizes pro Kante von zwei auf drei erhöht werden. Außerdem könnten die Suffixe zweier Strings identisch sein können, in diesem Fall muss ein Blatt alle Strings und Startpositionen der beinhalteten Strings indizieren. 6 Praktische Implementationsfragen Die wesentliche Herausforderung bei der Implementierung von Bäumen ist die Darstellung der Zweige der Knoten. Die beste Lösung sollte dabei das Gleichgewicht zwischen dem Speicherbedarf und der Geschwindigkeit halten, sowohl bei der Konstruktion des Suffix Trees, als auch bei der Anwendung. 6.1 Array Die einfachste Art die Zweige darzustellen, ist ein Array für jeden Knoten v, der kein Blatt ist. Das Array hat verschiedene Zellen, die jeweils mit dem Anfangsbuchstaben der Kantenbeschriftung eines Kindes indiziert sind. Diese Zellen enthalten dann einen Zeiger vom Knoten zum Kind. Dieses Array ermöglicht direkten Zugriff und Updates in konstanter Zeit und ist einfach zu programmieren. Doch der Speicherbedarf macht diese Implementierung unbrauchbar, wenn die Anzahl der Elemente im Alphabet und die Länge m des Strings S größer werden. Seite 4 von 8 Suffix Trees: Simple Algorithm and Applications Melanie Jakob 6.2 Linked List Alternativ zum Array kann man ebenso eine Linked List dazu verwenden, Knoten und deren Zweige zu implementieren. Die Linked List enthält dabei alle Anfangsbuchstaben der Kantenbeschriftungen von Zweigen eines Knotens. Wird eine neue Kante aus demselben Knoten zum Baum hinzugefügt wird, wird auch ein neuer Buchstabe zur Liste hinzugefügt. Die Traversierung des Suffix Trees ist durch die sequentielle Suche nach dem entsprechenden Buchstaben umgesetzt. Die durchschnittliche Laufzeit einer Suche kann reduziert werden, indem die Liste sortiert gespeichert wird. Der springende Punkt dabei ist, dass ein schnellerer Abbruch der Suche ermöglicht wird, falls ein Zeichen nicht in der Liste vorkommt. 7 Erste Anwendung der Suffix Trees Warum sind Suffix Trees so wichtig? Man kann sie dazu benutzen, das exakte Matching Problem in linearer Zeit zu lösen, und zwar in Abhängigkeit zur Länge des Suchmusters. Außerdem ermöglichen Suffix Trees auch die Lösung von weiteren komplexeren Stringverarbeitungsproblemen. Diese Probleme tauchen überall dort auf, wo man suchen muss, nämlich in den Gebieten von Information Retrieval - Internet, datenbankbasierten Anwendungen, Texteditoren, Bibliothekssystemen, www-Verzeichnissen, usw. Des Weiteren ist die Bioinformatik ein wesentliches Einsatzgebiet für Suffix Trees, da Gensequenzen als String darstellbar sind. 7.1 Exact String Matching Gegeben sei der Pattern P der Länge n und ein Text T der Länge m. Mithilfe eines Suffix Tree's kann man jedes Auftreten von P in T in O(n+m) Zeit finden. Anleitung: „Bilde einen Suffix Tree T für den Text T in O(m) Zeit. Dann finde eine Übereinstimmung der Buchstaben des Patterns P mit einem eindeutigen Pfad in T bis P entweder aufgebraucht ist oder keine Übereinstimmungen mehr möglich sind. Im letzten Fall kommt P nicht in T vor. Im ersten Fall jedoch ist jedes Blatt im Unterbaum unter der letzten Zusammensetzung mit der Startposition von P in T nummeriert und jede Startposition von P in T nummeriert so ein Blatt.“ [1] Seite 5 von 8 Suffix Trees: Simple Algorithm and Applications Melanie Jakob Nachdem der Suffix Tree für einen feststehenden Text fertiggestellt worden ist, kann eine lange Reihe von Patterns eingegeben werden und die Suche nach den Vorkommen jedes dieser Pattern kann dann möglichst schnell durchgeführt werden. Die Länge der Pattern sei n und die Anzahl der Vorkommen sei k. Benutzt man einen Suffix Tree können alle Vorkommen in O(n+k) Zeit gefunden werden, absolut unabhängig von der Länge des Textes. Wenn nur ein einziges Aufkommen eines Patterns P gefordert ist und die Vorbereitung ein bisschen erweitert wird, dann kann die Suchzeit von O(n+k) auf O(n) reduziert werden. Die Idee dabei ist jeden Knoten mit der kleinsten Zahl eines Blattes in seinem Unterbaum zu beschriften. Bei der Suche kann man also auf diese Beschriftung der Knoten zurückgreifen und hat dadurch sofort eine Startposition für ein Vorkommen der Pattern im Text. 7.2 Exact Set Matching Das Problem des Exakten Set Matchings besteht darin, dass alle Vorkommen von einer Reihe von Strings P in einem Text T gefunden werden sollen, wobei das komplette Set von , Strings auf einmal eingegeben wird. Tatsächlich können alle Vorkommen eines speziellen P's der Länge n in O(n+k p) Zeit gefunden werden, wobei k die Anzahl der Vorkommen von P ist, wenn T erst bekannt ist und festgehalten wird und die Pattern variieren. Also ist das Exakte Set Matching Problem eigentlich ein einfacherer Fall, weil das Set P eingegeben wird, zur gleichen Zeit in der T bekannt ist. Um es zu lösen bilden wir einen Suffix Tree T für den Text T in Zeit O(m) und nutzen dann diesen Baum zur sukzessiven Suche nach jedem Auftreten eines Patterns in P. Die totale benötige Zeit bei dieser Vorgehensweise ist O(n+m+k). 7.3 Substring Problem einer Datenbank von Patterns Eine Reihe von Strings oder eine Datenbank ist gegeben und steht fest. Später wird eine Reihe von Strings gebildet und für jeden dieser dargestellten Strings müssen alle Strings der Datenbank herausgesucht werden, die den String als Substring enthalten. Das ist die Umkehrung des Exakten Set Matching Problems, in dem gefragt wird, welcher der fixen Patterns in einem Substring des eingegebenen Strings enthalten ist. Im Zusammenhang mit Datenbanken für Genom DNA Daten ist das Problem Substrings zu finden, real und kann nicht mit dem Exakten Set Matching gelöst werden. Die DNA Datenbank beinhaltet eine Sammlung früher sequenzierter DNA Strings. Wenn ein ein neuer DNA String sequenziert wird, könnte er in einem bereits sequenzierten String enthalten sein, deshalb ist eine effiziente Methode dies herauszufinden nötig. Seite 6 von 8 Suffix Trees: Simple Algorithm and Applications Melanie Jakob Die totale Länge m aller Strings in der Datenbank ist vermutlich sehr hoch. Was macht also eine gute Datenstruktur und Suchalgorithmus für das Substring-Problem aus? Die zwei Hemmnisse sind, dass die Datenbank wenig Speicherplatz benötigen soll und dass jede Suche sowie die Vorbereitung der Datenbank zur Suche schnell durchgeführt werden soll. Suffix Trees bieten eine attraktive Lösung zu diesem Datenbank Problem. Ein generalisierter Suffix Tree T für die Strings der Datenbank wird in O(m) Zeit gebildet und, noch wichtiger, dieser braucht nur O(m) Platz. Ein einzelner String S der Länge n wird in O(n) Zeit gefunden, oder das Vorkommen wird ausgeschlossen. Wie gewöhnlich wird das durch Zusammenführen des Strings mit dem Pfad im Baum beginnend bei der Wurzel bewerkstelligt. 7.4 Longest Common Substring Ein klassisches Problem der String-Analyse ist den längsten gemeinsamen Substring zweier gegebener Strings S1 und S2 zu finden. Das nennt man das Longest Common Substring Problem. Ein effizienter und konzeptionell einfacher Weg den längsten gemeinsamen Substring zu finden, ist einen Generalisierten Suffix Tree für S 1 und S2 zu bilden. Jedes Blatt in diesem Baum stellt entweder ein Suffix einer der beiden Strings dar, oder ein Suffix, das in beiden Strings enthalten ist. Markiert man nun jeden inneren Knoten mit einer 1 bzw. 2, falls ein Blatt im Unterbaum ist, dass S1 bzw. S2 abbildet, dann ist jeder Pfad im Suffix Tree, dessen innere Knoten mit 1 und 2 beschriftet sind, ein gemeinsamer Substring. Der längste dieser Pfade ist der Longest Common Substring. Also muss der Algorithmus nur den Knoten mit der maximalen String-Tiefe (größte Anzahl Buchstaben auf dem Pfad zum Knoten), welcher mit 1 und 2 markiert ist. Der Suffix Tree kann in linearer Zeit in Abhängigkeit zur Länge von S 1 und S2 aufgestellt werden. Die Knoten zu markieren und die Berechnung der String-Tiefe kann mithilfe von Standard Baum-Traversierungen ebenfalls in linearer Zeit durchgeführt werden. Der längste gemeinsame Substring zweier String kann also durch die Verwendung eines Generalisierten Suffix Trees in linearer Zeit gefunden werden. Seite 7 von 8 Suffix Trees: Simple Algorithm and Applications 8 Melanie Jakob Fazit In dieser Arbeit wurden Suffixbäume und die naive Konstruktion solcher Datenstrukturen dargestellt und wie man in ihnen suchen kann. Außerdem wurde ein kurzer Überblick über die Anwendungsmöglichkeiten von Suffix Trees gegeben, die in Wirklichkeit vielzählig sind. Den größten Nutzen erzielen Suffix Trees wohl bei der Suche nach unterschiedlichen Strings oder Patterns in einem feststehenden Text oder einer Datenbank. Literatur [1] D. Gusfield: Algorithms on Strings, Trees, and Sequences – Computer Science and Computional Biology, Cambridge University Press, 1997; Kapitel 5, Abschnitt 6.4, Abschnitte 7.1 und 7.3 – 7.6. [2] Wikipedia: Suffixbaum, Wikipedia, Die freie Enzyklopädie, Stand 30.12.2009, aufgerufen am 10.05.2010. [3] Wikipedia: Regulärer Ausdruck, Wikipedia, Die freie Enzeklopädie, Stand 23.04.2010, aufgerufen am 10.05.2010. [4] J. Harzer: Der Suffix-Tree, Stand 11.06.2001, aufgerufen am 10.05.2010. [5] S. Penkova, A. Tchorbadjiiski: Suffix Trees und Suffix Arrays, Ausarbeitung Proseminar Algorithmen und Datenstrukturen (Sommersemester 2005), RheinischWestfälische Technische Hochschule, Fakultät für Informatik, 2005, zugegriffen am 10.05.2010. [6] F. Schüle: Suffix Trees, Vortragsfolien und Ausarbeitung Hauptseminar Bioinformatik (Sommersemester 2006), Universität Ulm, Fakultät für Informatik, 2006, zugegriffen am 10.05.2010. [7] S. Telejnikov: Suffixbäume, Ausarbeitung Hauptseminar (Sommersemester 2006), Universität Stuttgart, Institut für formale Methoden der Informatik, 2006, zugegriffen am 10.05.2010. Seite 8 von 8