Skriptum zur Vorlesung Algorithmen und Datenstrukturen 2 Univ.-Prof. Dr. Petra Mutzel Ass.-Prof. Dr. Günther Raidl I NSTITUT W INTERSEMESTER 2001/2002 F ÜR C OMPUTERGRAPHIK UND A LGORITHMEN T ECHNISCHE U NIVERSIT ÄT W IEN c A LLE R ECHTE VORBEHALTEN 2 Inhaltsverzeichnis 1 Graphenalgorithmen 1.1 Definition von Graphen . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Darstellung von Graphen im Rechner . . . . . . . . . . . . . . . . 1.3 Durchlaufen von Graphen . . . . . . . . . . . . . . . . . . . . . . . 1.3.1 Tiefensuche (Depth-First-Search, DFS) . . . . . . . . . . . 1.4 Minimale aufspannende Bäume . . . . . . . . . . . . . . . . . . . 1.4.1 Der Algorithmus von Kruskal . . . . . . . . . . . . . . . . 1.4.2 Implementierung von Kruskal mittels Union-Find . . . . . . 1.4.3 Abstrakter Datentyp: Dynamische Disjunkte Menge (DDM) 1.4.4 Der Algorithmus von Prim . . . . . . . . . . . . . . . . . . 2 Optimierungsalgorithmen 2.1 Exakte Algorithmen für schwierige Optimierungsprobleme 2.1.1 Enumerationsverfahren . . . . . . . . . . . . . . . 2.1.2 Dynamische Programmierung . . . . . . . . . . . 2.1.3 Beschränkte Enumeration . . . . . . . . . . . . . 2.1.4 Branch-and-Bound . . . . . . . . . . . . . . . . . 2.2 Approximative Algorithmen und Gütegarantien . . . . . . 2.2.1 Konstruktive Heuristiken mit Gütegarantien . . . . 2.2.2 -Approximative Algorithmen . . . . . . . . . . . 2.3 Verbesserungsheuristiken . . . . . . . . . . . . . . . . . . 2.3.1 Einfache Austauschverfahren . . . . . . . . . . . 2.3.2 Simulated Annealing . . . . . . . . . . . . . . . . 2.3.3 Evolutionäre Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Geometrische Algorithmen 3.1 Scan-Line Prinzip . . . . . . . . . . . . . . . . . . . . . . . . 3.1.1 Das Schnittproblem für Iso-orientierte Liniensegmente 3.1.2 Schnitt von Allgemeinen Liniensegmenten . . . . . . 3.2 Mehrdimensionale Bereichssuche . . . . . . . . . . . . . . . 3.2.1 Zweidimensionale Bäume . . . . . . . . . . . . . . . 3.2.2 Höhere Dimensionen . . . . . . . . . . . . . . . . . . 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 12 13 15 15 20 20 21 23 26 . . . . . . . . . . . . 29 30 30 32 37 41 48 48 55 56 57 58 61 . . . . . . 67 67 68 71 74 75 79 INHALTSVERZEICHNIS 4 4 Suchen in Texten 4.1 Naives Verfahren . . . . . . . . . 4.2 Verfahren von Knuth-Morris-Pratt 4.3 Verfahren von Boyer-Moore . . . 4.3.1 Berechnung von last[] . . 4.3.2 Berechnung von suffix[] . 4.3.3 Analyse von Boyer-Moore 4.4 Tries . . . . . . . . . . . . . . . . 4.4.1 Radix Trie . . . . . . . . 4.4.2 Indexed Trie . . . . . . . 4.4.3 Linked Trie . . . . . . . . 4.4.4 Suffix Compression . . . . 4.4.5 Packed Trie . . . . . . . . 4.4.6 Suchen . . . . . . . . . . . . . . . . . . . . . . . 81 82 83 85 86 87 88 89 89 92 95 95 96 98 5 Randomisierte Algorithmen 5.1 Randomisierter Primzahltest . . . . . . . . . . . . . . . . . . . . . . . . . 5.1.1 Algorithmus von Miller-Rabin . . . . . . . . . . . . . . . . . . . . 101 102 102 6 Parallele Algorithmen 6.1 Ein Modell einer parallelen Maschine . . . . . . . 6.2 Parallele Minimum-Berechnung . . . . . . . . . . 6.3 Parallele Präfixsummen-Berechnung . . . . . . . . 6.4 Anwendungen der Präfixsummen-Berechnung . . . 6.4.1 Komprimieren eines dünn besetzten Arrays 6.4.2 Simulation eines endlichen Automaten . . 6.4.3 Addier-Schaltung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 . 108 . 108 . 112 . 115 . 115 . 115 . 116 INHALTSVERZEICHNIS 5 Vorwort Die Vorlesung Algorithmen und Datenstrukturen 2 ist die Fortsetzung der VO Algorithmen und Datenstrukturen 1 und wendet sich an interessierte Studierende der Fachrichtungen Informatik, Wirtschaftsinformatik und Mathematik. Sie ist Pflichtvorlesung der Fachrichtung Informatik. Während im vorangegangenen Semester in der Vorlesung Algorithmen und Datenstrukturen 1 die Grundlagen im Design und in der Analyse von Algorithmen sowie grundlegende Datenstrukturen (balancierte Suchbäume, Heaps, . . . ) mit Schwerpunkt Sortier- und Suchverfahren behandelt wurden, untersuchen wir nun Algorithmen und Datenstrukturen in vielfältigen Anwendungsbereichen. Im Kapitel über Graphenalgorithmen werden wir sehen, wie die Laufzeit des Greedy-Algorithmus zur Berechnung minimaler aufspannender Bäume mit Hilfe der Union-FindDatenstruktur deutlich verbessert werden kann. Dabei sind sowohl der Greedy-Algorithmus als auch die Union-Find-Datenstruktur allgemeine, in vielfältigen Anwendungen und Szenarien einsetzbare Konstrukte im Algorithmendesign. Optimierungsprobleme treten in vielen Bereichen in Industrie und Wirtschaft auf und ihre erfolgreiche Behandlung wird zunehmend wichtiger. So lassen sich zum Beispiel das Stückgut-Transportproblem oder die Auswahl von Werbekampagnen als Rucksackproblem formulieren (s. Kapitel 2). Ein Verschnittproblem bei Bilderrahmen oder Fensterbau lässt sich als Bin-Packing-Problem formulieren. Das TSP-Problem taucht in vielfältigen Fragestellungen im Bereich der Routenplanung (Lieferwagen, Müllabfuhr, Spedition, Außendienstmitarbeiter) und im Bereich der Produktion (Steuerung von NC-Maschinen, wie Bohren, Löten, Schweißen, Verdrahtung von Leiterplatten) auf. Ein großer Teil der Vorlesung widmet sich den grundlegenden Konzepten zu Optimierungsalgorithmen. Wichtige Konzepte hierbei sind Dynamische Programmierung und Branch-and-Bound-Algorithmen, die oft zur exakten Lösung NP-schwieriger Optimierungsaufgaben eingesetzt werden. Eine Alternative zu exakten Optimierungsverfahren sind approximative Algorithmen. Hierbei unterscheidet man zwischen Konstruktionsheuristiken und Verbesserungsheuristiken. Seit wenigen Jahren spielt der sogenannte Approximationsfaktor bzw. das Gütemaß einer Heuristik eine zunehmende Rolle in der Algorithmentheorie. Kapitel 3 befasst sich mit Algorithmen ganz anderer Art, nämlich den geometrischen Algorithmen. Ein wichtiges Prinzip für vielfältige Fragestellungen in dem Fall, dass sich die Daten auf geometrische Daten abbilden lassen, ist das Scan-Line-Prinzip. Eine wichtige Aufgabenstellung ist die ein- oder mehrdimensionale Bereichssuche. In Kapitel 4 behandeln wir zwei klassische Algorithmen zur effizienten Suche in Texten, nämlich den Knuth-Morris-Pratt- und den Boyer-Moore-Algorithmus. Außerdem wird die 6 INHALTSVERZEICHNIS Datenstruktur des Tries besprochen, die für die Textsuche und -analyse große Bedeutung hat. In Kapitel 5 erhalten wir einen Einblick in die randomisierten (d.h. zufallsbasierten) Algorithmen. Wir verstehen darunter deterministische Algorithmen, die als zusätzliche Elementaroperation Zufallsexperimente durchführen können. Kapitel 6 schließlich befasst sich mit Algorithmen für parallele Hardwarearchitekturen. Natürlich können wir in der 3-stündigen Vorlesung nicht alle möglichen Anwendungsbereiche für Algorithmen abdecken. Ziel der Vorlesung ist es, exemplarisch verschiedene Bereiche aufzuzeigen, die häufig in der Praxis auftreten und Ihnen verdeutlichen sollen, dass für die meisten auftretenden Probleme bereits effiziente Algorithmen, Approximationsalgorithmen oder zumindest generelle Methoden in der Literatur bekannt sind. Sie sollten also nicht jedes Mal “das Rad neu erfinden”. Die in der Vorlesung behandelten Themen zeigen auch wie vielseitig die Algorithmentheorie ist. Vielleicht hat Ihnen ja ein Thema, wie z.B. die Optimierungs- oder die geometrischen Algorithmen, besonders viel Spaß gemacht, und Sie wollen sich eventuell darin vertiefen. Dann melden Sie sich einfach bei uns und/oder Sie besuchen weiterführende Veranstaltungen zu diesem Themenbereich. Wir bieten durchgehend Proseminare, Seminare und Praktika zu den in der Vorlesung und Übung behandelten Themenbereichen an. Außerdem bieten wir regelmäßig Vorlesungen über Optimierung (z.B. Evolutionäre Algorithmen, das Handlungsreisendenproblem und seine Verwandten (TSP)) und Graphenalgorithmen (z.B. Algorithmen zum Zeichnen von Graphen) an. Wir würden uns freuen, Sie wiederzusehen, und wünschen Ihnen viel Erfolg für Ihre Zukunft. Wien, im Jänner 2002 Petra Mutzel, Günther Raidl und die Assistent/inn/en von 186/1. INHALTSVERZEICHNIS 7 Organisatorisches Organisatorisches zur Vorlesung Die Vorlesung findet im WS 2001/2002 ab dem 3. Oktober 2001 Mittwochs von 14:45–15:52 Uhr und Donnerstags von 14:45–15:52 Uhr im Auditorium Maximum statt. Sie wird von Frau Univ.-Prof. Dr. Petra Mutzel und Herrn Ass.-Prof. Dr. Günther Raidl vom Institut für Computergraphik und Algorithmen gehalten. Der Haupttermin für die Vorlesungsprüfung ist Freitag, der 1. Februar 2002 ab 16:15 Uhr. Beachten Sie auch aktuelle Hinweise unter: http://www.ads.tuwien.ac.at/teaching/ LVA/186648.html Das vorliegende Skript wurde gerade vollkommen neu erstellt. Natürlich werden Sie einige Fehler finden, die sich eingeschlichen haben. Ein Skript steht nie von heute auf morgen korrekt da, sondern verbessert sich und wächst im Laufe der Zeit. Wir hoffen, dass Sie Verständnis dafür aufbringen. Das vorliegende Skript ist nicht dazu da, um ausschließlich daraus den Stoff zu lernen, sondern um festzustellen, welche Themengebiete in der Vorlesung behandelt werden und worauf der Schwerpunkt gelegt wird. Darauf basierend sollen Sie, die Studierenden, sich mit dem Lehrstoff aus den Büchern weitergehend beschäftigen. Dazu empfehlen wir ganz besonders die Bücher aus der Literaturliste (s. Abschnitt Literaturliste). Organisatorisches zur Übung Die Übung soll dazu dienen, die Inhalte der Vorlesung anzuwenden, zu verstehen und zu vertiefen. Wichtig: Zur Teilnahme an der Laborübung ist eine Anmeldung zu einer konkreten Übungsgruppe über unser Web-Formular http://www.ads.tuwien.ac.at/teaching/Anmeldung/ erforderlich. Diese Anmeldung ist ab Donnerstag, den 4. Oktober 2001 bis spätestens Mittwoch, den 17. Oktober 2001 möglich! Die Übungen finden in Kleingruppen unter Anleitung Ihres/Ihrer Übungsleiters/-leiterin an vier Tagen über das Semester verteilt statt. An den drei ersten Terminen werden Aufgaben von Übungsblättern durchgenommen, zum vierten Termin findet ein Übungstest statt. Jedes Übungsblatt beinhaltet 10 Beispiele. Das erste Übungsblatt erhalten Sie in einer der ersten Vorlesungseinheiten, die weiteren dann immer in der Übungsgruppe. Außerdem 8 INHALTSVERZEICHNIS werden diese Übungsblätter auch auf unserer Webseite zur Laborübung oder im Sekretariat unserer Abteilung erhältlich sein. Am Beginn einer derartigen Übungstunde tragen alle Teilnehmer in eine Liste Ihre Anwesenheit ein und vermerken jene Beispiele, die Sie Ihrer Meinung nach richtig gelöst haben. Der/die Leiter/in der Übungsgruppe wählt nun entsprechend diesen Angaben Teilnehmer aus, die ihre vorbereiteten Lösungen mit entsprechenden Erläuterungen an der Tafel präsentieren. Diese Präsentationen fließen auch in die Beurteilung ein. Den Schwerpunkt der Beurteilung bildet jedoch der Übungstest. Dieser Test dauert 60 Minuten. Hierbei sind keine Unterlagen oder andere Hilfsmittel erlaubt. Die Beispiele in diesem Test umfassen prinzipiell den bis dahin in der Vorlesung durchgenommenen Stoff, sind aber vor allem an die davor in den Übungsstunden durchgenommenen Beispiele angelehnt. Arbeiten Sie daher in den Übungsstunden mit — es lohnt sich spätestens beim Test! Sollten Sie bei diesem Test nicht teilnehmen können oder einfach ,,einen schlechten Tag“ erwischt haben, so ist dies auch kein prinzipielles Problem, da die Vorlesungsprüfung am 1. Februar 2002 auf Wunsch auch gleichzeitig als Ersatzübungstest gewertet werden kann. Weiters erhalten Sie Mitte November in der Übungsgruppe Angaben zu einer Programmieraufgabe. Diese ist eigenständig zu lösen. Das fertige Programm stellen Sie Ihrem/r Übungsgruppenleiter/in bei einem Abgabegespräch in der zweiten Jännerwoche vor. Dabei müssen Sie die Funktionsweise auch im Detail erklären können. Nur wenn Sie bei der Abgabe davon überzeugen können, dass Sie der/die Autor/in des Programmes sind, wird dieses positiv gewertet. Zum Lösen der Programmieraufgaben stehen Ihnen im Informatiklabor, Favoritenstraße 9-11, EG, zahlreiche PCs mit Linux bzw. Microsoft Windows 2000 und verschiedenen Programmiersprachen zur Verfügung. Den notwendigen Account erhalten Sie mit der Ausgabe der Aufgabenstellungen bei Ihrem/Ihrer Übungsgruppenleiter/in. Für eine positive Note im Übungszeugnis der Laborübung muss die Hälfte der Übungsaufgaben erfolgreich bearbeitet werden, der Übungstest bestanden werden (Hälfte der Punkte) und die Programmieraufgabe erfolgreich abgegeben werden. Ferner gibt es für die aktive Teilnahme in den Übungsgruppen, d.h. gute Präsentationen von Übungsbeispielen, eine bestimmte Punkteanzahl. Details zur Bewertung werden im Übungsverlauf auf der Webseite zur Laborübung bekannt gegeben. Die Übung wird hauptsächlich von den Universitätsassistent/inn/en Gabriele Kodydek, Gunnar Klau, René Weiskircher und unserem Studienassistenten Georg Kraml organisiert. Wir möchten uns an dieser Stelle für die Mitarbeit der zahlreichen Tutorinnen und Tutoren bedanken, ohne deren Engagement die Laborübung in Kleingruppen nicht möglich wäre. INHALTSVERZEICHNIS 9 Mit allen Fragen, Problemen, Anregungen und Beschwerden, die vom Leiter bzw. von der Leiterin Ihrer Übungsgruppen nicht behandelt werden können, wenden Sie sich bitte an uns. Wir haben für Sie dazu die email-Hotline [email protected] eingerichtet. Natürlich stehen wir Ihnen auch persönlich zu Verfügung. Kommen Sie aber bitte ausschließlich in den Sprechstundenzeiten oder nach vorheriger Vereinbarung. Die aktuellen Sprechstundenzeiten erfahren Sie auf unserer Homepage (http://www.ads.tuwien.ac.at). Aktuelle Informationen zur Laborübung (Termine, Übungsblätter, etc.) finden Sie auf unserer Webseite unter: http://www.ads.tuwien.ac.at/teaching/LVA/186659.html Wir wünschen Ihnen VIEL ERFOLG! 10 INHALTSVERZEICHNIS Literaturliste Die Bücher (1)-(3) beinhalten für viele der hier behandelten Themenbereiche ein einführendes Kapitel. Ein interessantes multimediales Buch ist (4), das interaktive Vorlesungen zu weiterführenden Algorithmen enthält. (5) ist ein sehr gutes und interessantes Buch zum Themenbereich der Optimierung. Interessieren Sie sich eher für Heuristiken und Lokale Suchverfahren, so empfehlen wir Ihnen, auch kurz in (6) zu blicken. (7) ist ein immer noch aktueller Klassiker im Algorithmen-Bereich. Daneben finden Sie am Ende jedes Kapitels weitere Literaturhinweise, die sich speziell auf die dort behandelten Kapitel beziehen. Darüber hinaus steht es Ihnen frei, jedes andere Buch, das den Stoff behandelt, auszuwählen. (1) T.H. Cormen, C.E. Leiserson und R.L. Rivest: “Introduction to Algorithms”, MIT Press, Cambridge, 1990 (2) R. Sedgewick: “Algorithmen in C++”, Addison-Wesley, 1992 (3) T. Ottmann und P. Widmayer: “Algorithmen und Datenstrukturen”, B.I. Wissenschaftsverlag, Mannheim, 1990 (4) T. Ottman (Hrsg.): “Prinzipien des Algorithmenentwurfs”, Spektrum Akademischer Verlag, Berlin 1998 (5) C.H. Papadimitriou und K. Steiglitz: “Combinatorial Optimization: Algorithms and Complexity”, Dover Publications, 1998 (6) E. Aarts und J.K. Lenstra: “Local Search in Combinatorial Optimization”, John Wiley & Sons, Chichester, 1997 (7) A.V. Aho, J.E. Hopcroft und J.D. Ullman: “Data Structures and Algorithms”, AddisonWesley, 1982 Dankesworte An dieser Stelle sei allen gedankt, die zum Entstehen dieses Skriptums beigetragen haben: Verantwortlich für den Inhalt sind Prof. Dr. Petra Mutzel und Dr. Günther Raidl. Dipl.Inf. René Weiskircher, Dr. Gunnar Klau, Dr. Gabriele Kodydek, Dr. Martin Schönhacker, Mag. Ivana Ljubić, und Herr Georg Kraml haben zur Verbesserung des Skriptums entscheidend beigetragen. Wir danken Martin Gruber für seine unermüdliche Soft- und HardwareUnterstützung, vor allem aber auch Barbara Hufnagel für die Umsetzung zahlreicher handschriftlicher Notizen in LaTeX. Kapitel 1 Graphenalgorithmen Viele Probleme lassen sich mit Hilfe graphentheoretischer Konzepte modellieren. Graphen bestehen aus Objekten und deren Beziehungen zueinander. Sie modellieren also diskrete Strukturen, wie z.B. Netzwerke oder Prozesse. Betrachten wir als Beispiel das Bahnnetz von Österreich. Die Objekte (sogenannte Knoten) können hier die Bahnhöfe (und eventuell Weichen) sein, die Beziehungen (sogenannte Kanten) die Schienen des Bahnnetzes. Belegt man die Kanten mit zusätzlichen Attributen (bzw. Gewichten), so kann man damit verschiedene Probleme formulieren. Bedeutet das Attribut einer Kante zwischen und die Fahrzeit eines Zuges von nach , so besteht das Problem der kürzesten Wege darin, die minimale Fahrzeit von einer beliebigen Stadt zu einer beliebigen Stadt zu ermitteln. Andere Beispiele, die mit Hilfe von graphentheoretischen Konzepten gelöst werden können, sind Routenplanungsprobleme. Hier sollen z.B. Güter von mehreren Produzenten zu den Verbrauchern transportiert werden. Im Bereich Produktionsplanung geht es u.a. darum, die auftretenden Teilaufgaben auf bestehende Maschinen zuzuordnen, so dass die Herstellungszeit minimiert wird. Da hierbei die Reihenfolge der Abarbeitung der Teilaufgaben zentral ist, werden diese Probleme auch Scheduling-Probleme genannt. Ein klassisches Graphenproblem ist das Königsberger Brückenproblem“, das Euler 1736 ” gelöst und damit die Theorie der Durchlaufbarkeit von Graphen gegründet hat. Die topologische Graphentheorie hat sehr von den Forschungen am Vierfarbenproblem“ profitiert: Die ” ursprüngliche Aufgabe ist, eine Landkarte so zu färben, dass jeweils benachbarte Länder unterschiedliche Farben bekommen. Dabei sollen möglichst wenige Farben verwendet werden. Während ein Beweis, dass dies immer mit maximal fünf Farben möglich ist, relativ schnell gefunden wurde, existiert bis heute nur ein sogenannter Computerbeweis (erstmals 1976 von Appel und Haken) dafür, dass auch vier Farben ausreichen. Im Bereich der chemischen Isomere ist es heutzutage von großer Bedeutung für die Industrie (im Rahmen der Entwicklung neuer künstlicher Stoffe und Materialien), folgende be11 KAPITEL 1. GRAPHENALGORITHMEN 12 v2 e1 v1 v1 e2 e3 e2 e3 e4 v3 v2 e1 v3 (a) e4 (b) Abbildung 1.1: Beispiele eines (a) gerichteten und (b) ungerichteten Graphen reits 1890 von Caley gestellte Frage zu beantworten: Wie viele verschiedene Isomere einer bestimmten Zusammensetzung existieren? Caley hat damit die Abzähltheorie von Graphen begründet. Oftmals werden auch Beziehungsstrukturen bzw. Netzwerke mit Hilfe von Graphen dargestellt. Bei der Umorganisation von Betrieben kann man damit z.B. relativ leicht feststellen, wer die wichtigsten“ bzw. einflussreichsten“ Mitarbeiter sind. Zunehmend wer” ” den Betriebsabläufe und Geschäftsprozesse als Graphen modelliert (z.B. UML-Darstellung). 1.1 Definition von Graphen Ein Graph ist ein Tupel , wobei eine endliche Menge ist, deren Elemente Knoten (engl. vertices, nodes) genannt werden. Die Menge besteht aus Paaren von Knoten. Sind diese geordnet , spricht man von gerichteten Graphen, sind die Paare ungeordnet, spricht man von ungerichteten Graphen. Die Elemente in heißen gerichtete bzw. ungerichtete Kanten (engl. edges), gerichtete Kanten nennt man auch B ögen (engl. arcs). Eine Kante heißt Schleife (engl. loop). Abbildung 1.1(a) und 1.1(b) zeigen je ein Beispiel eines gerichteten und ungerichteten Graphen. Ist eine Kante in , dann sagen wir: und sind adjazent (bzw. ) und sind inzident und sind Nachbarn Die eingehende Nachbarmenge eines Knotens ist definiert als "!# $ %&(' und die ausgehende Nachbarmenge als ) Graphen gilt: 0 102 *) +,-"!# ./.' . Für ungerichtete . Der (Ausgangs- bzw. Eingangs-)Knotengrad 1.2. DARSTELLUNG VON GRAPHEN IM RECHNER 13 von ist definiert als 1 und bezeichnet die Anzahl seiner (ausgehenden bzw. eingehenden) inzidenten Kanten. Dabei wird eine Schleife an Knoten als 2 gezählt. Für ungerichtete Graphen gilt folgendes Lemma. Lemma: In einem ungerichteten Graphen ungeradem Grad gerade. ist die Anzahl der Knoten mit Beweis: Summiert man über alle Knotengrade, so zählt man jede Kante genau zwei Mal: ! " ! Da die rechte Seite gerade ist, folgt daraus, dass auch die linke Seite gerade sein muss. Also ist die Anzahl der ungeraden Summanden auch gerade. 1.2 Darstellung von Graphen im Rechner Wir stellen im folgenden zwei verschiedene Möglichkeiten vor, Graphen im Rechner zu speichern. Sie unterscheiden sich in Hinblick auf den benötigten Speicherplatz und die benötigte Laufzeit für die bei manchen Algorithmen auftretenden, immer wiederkehrenden Abfragen. Wir nehmen in der Folge an, dass die Knotenmenge ! "#"#" %$ ' und & ! !. (A) Speicherung in einer Adjazenzmatrix Eine Möglichkeit ist, einen Graphen ohne Mehrfachkanten als Adjazenzmatrix zu speichern. Das sieht dann folgendermaßen aus: -. ' '*) falls $ %& ,+ sei eine &(& -Matrix mit Einträgen / sonst Für die Beispiele aus Abb. 1.1(a) und 1.1(b) ergeben sich die folgenden Matrizen: , , 20 21 0 0 1 %0 1 0 0 %1 , %0 , 20 0 1 1 21 0 1 1 1 0 1 %1 1 1 1 Analyse typischer Abfragen: Abfrage: Existiert die Kante ? Iteration über alle Nachbarn eines Knotens Platzverbrauch zum Speichern . 3( 3(4& 3.5& 0 selbst wenn der Graph dünn ist 0 (d.h. viel weniger als & Kanten enthält) KAPITEL 1. GRAPHENALGORITHMEN 14 (B) Speicherung mit Hilfe von Adjazenzlisten Eine Alternative (die am meisten verwendete) ist die Speicherung mittels Adjazenzlisten. Für jeden Knoten , existiert eine Liste der ausgehenden Kanten. Man kann dies folgendermaßen als einfach verkettete Listenstruktur umsetzen. Für die Beispiele aus Abb. 1.1(a) und 1.1(b) ergeben sich hier folgende Listen: 1:2 2:3 3:1,3 1:2,3 2:1,3 3:1,2,3 gerichtet ungerichtet Als Realisierung hierfür bieten sich z.B. einfach verkettete lineare Listen an: 1 2 . 1 2 3 . 2 3 . 2 1 3 . 3 1 3 3 1 2 3 . (a) gerichtet . (b) ungerichtet Als alternative Umsetzung bieten sich Felder (Arrays) an: Hierbei gibt es einen Eintrag & für jeden Knoten, der angibt, an welcher Stelle in der Kantenliste (edgelist) die Nachbarliste von beginnt. Für das Beispiel aus Abb.1(b) ungerichtet ergibt sich: ) index + 1 3 5 ) edgelist + 2 3! 1 3! 1 2 3 Analyse typischer Abfragen: Abfrage: Existiert die Kante ? Iteration über alle Nachbarn eines Knotens Platzverbrauch 3( 1 3( 1 3. ! ! ! ! Für die Effizienz von Graphenalgorithmen ist es wichtig, jeweils die richtige Datenstruktur zur Speicherung von Graphen zu wählen. Ist der Graph dünn“, d.h. enthält er relativ ” wenige Kanten, also 3( ! ! -viele Kanten, so sind Adjazenzlisten meist besser geeignet. Ist 0 der Graph sehr dicht“, d.h. enthält er 3( ! ! -viele Kanten und sind viele Anfragen der ” Art Existiert die Kante $ ?“ zu erwarten, dann ist die Darstellung als Adjazenzmatrix ” vorzuziehen. 1.3. DURCHLAUFEN VON GRAPHEN 15 1.3 Durchlaufen von Graphen Die wichtigsten Verfahren zum Durchlaufen von Graphen sind Tiefensuche und Breitensuche. Wir konzentrieren uns auf die Tiefensuche. 1.3.1 Tiefensuche (Depth-First-Search, DFS) Tiefensuche ist für folgende Graphenprobleme gut geeignet: (1) Finde alle Knoten, die von aus erreichbar sind. Gegeben sei ein Knoten . Bestimme alle Knoten , für die es einen Pfad von nach gibt. Dabei ist ein Pfad der Länge eine Folge 2 #"#"" ) ) mit & . Ein Weg ist ein Pfad, bei dem alle Knoten und Kanten verschieden sind. Ein Pfad ist ein Kreis, wenn . (2) Bestimme die Zusammenhangskomponenten von . Ein ungerichteter Graph heißt zusammenhängend, wenn für alle $ * gilt: Es existiert ein Pfad von nach . Ein gerichteter Graph heißt stark zusammenhängend, wenn für alle $ gilt: Es existiert ein (gerichteter) Pfad von nach . Daneben gibt es für gerichtete Graphen auch den Begriff des schwachen Zusammenhangs: ein solcher Graph heißt schwach zusammenhängend (oder auch nur zusammenhängend), wenn der zugrundeliegende ungerichtete Graph zusammenhängend ist. Eine Zusammenhangskomponente ist ein maximal zusammenhängender Untergraph Graphen, sodass und , so von . Sind und heißt Untergraph von . (3) Enthält einen Kreis? (4) Ist 2-zusammenhängend? Ein zusammenhängender Graph ist 2-zusammenhängend, wenn ' (das ist nach Löschen des Knotens und seiner inzidenten Kanten) für alle zusammenhängend ist. (2-Zusammenhang ist z.B. bei Telefonnetzen wichtig; Stichwort: Survivability“.) ” Die Idee des DFS-Algorithmus ist die folgende: Verlasse jeden Knoten so schnell wie möglich, um tiefer“ in den Graphen zu laufen. ” 1. sei der letzte besuchte Knoten: markiere 2. Sei eine Kante und der Knoten ist noch unmarkiert, dann: markiere , setze und gehe zu 1. 2 / 3. Sonst: wenn kein unmarkierter Nachbar von gänger von . mehr existiert, gehe zurück zum Vor- KAPITEL 1. GRAPHENALGORITHMEN 16 3 2 1 4 5 2 1 3 3 1 7 7 6 4 6 5 4 6 7 5 2 (a) (b) Abbildung 1.2: Ein Graph und ein möglicher DFS-Baum Wir betrachten als Beispiel den Graphen in Abb. 1.2(a). Abb. 1.2(b) zeigt eine mögliche Abarbeitungsreihenfolge des DFS-Algorithmus. Die kleinen Nummern neben den Knoten geben die Reihenfolge an, in der diese markiert wurden. Die gerichteten Kanten geben für jeden besuchten Knoten dessen eindeutigen Vorgänger und seine direkten Nachfolger an. Der Graph, der durch diese gerichteten Kanten definiert wird, ist der zu dem DFS-Lauf gehörige DFS-Baum. Im folgenden präsentieren wir mögliche Lösungen zu den oben genannten Problemen (1)– (3). Wir nehmen dabei an, dass der gegebene Graph ungerichtet ist. Wir überlassen es Ihnen, die dazugehörigen Varianten für gerichtete Graphen zu entwickeln. Basisalgorithmus zur Tiefensuche Algorithmus 1/2 realisiert die Grundidee der Tiefensuche. Hier werden alle Knoten bestimmt, die von dem gegebenen Knoten durch einen Pfad erreichbar sind. Der Knoten ist von aus erreichbar, wenn entweder oder wenn es einen Knoten gibt mit und von aus erreichbar ist. Nach Ausführung von Depth-First-Search1 + . ) gilt für alle & : markiert + genau dann wenn es einen Pfad von nach gibt. Analyse der Laufzeit: Initialisierung: 3( ! ! DFS wird für jeden Knoten höchstens einmal aufgerufen und hat (ohne Folgekosten der dort gestarteten DFS-Aufrufe) Kosten Insgesamt ergibt dies Kosten von: ! ! Dies ist asymptotisch optimal. ! ! ! "! ! ! ! "! " 1.3. DURCHLAUFEN VON GRAPHEN 17 Algorithmus 1 Depth-First-Search1 + Input: Graph , Knoten Output: Ausgabe aller Knoten, die von erreichbar sind ) / 1: Initialisierung: setze markiert + für alle ; 2: DFS1 + ; 3: für alle Knoten & . ) 4: falls (markiert + ) dann 5: Ausgabe von ; ' 6: 7: ' Algorithmus 2 DFS1 . ) 1: markiert ,+ ; 2: für alle Knoten aus ) 3: falls (markiert + 4: DFS1 ; / dann ' 5: 6: ' Bestimmung der Zusammenhangskomponenten Algorithmus 3/4 realisiert einen Tiefensuche-Algorithmus zur Lösung von Problem (2) für ungerichtete Graphen. Ein Aufruf von DFS2 besucht und markiert alle Knoten, die in der gleichen Zusammenhangskomponente wie liegen, mit der aktuellen Komponentennummer. Bleiben nach dem Lauf DFS2 noch unmarkierte Knoten übrig, erhöht sich die Komponentennummer compnum um eins, und DFS2 wird aufgerufen; jeder in diesem Lauf entdeckte Knoten wird der neuen Komponente mit Nummer compnum zugeordnet. Algorithmus 3 Depth-First-Search2 Input: Graph ) Output: markiert + enthält für jeden Knoten 0 die ID der ihm zugeordneten Zusammenhangskomponente, compnum enthält am Ende die Anzahl der Komponenten. ) / 1: Initialisierung: setze markiert + für alle ; / 2: compnum ; 3: für alle aus ) / 4: falls (markiert + ) dann 5: compnum ; 6: DFS2 ; 7: 8: ' ' KAPITEL 1. GRAPHENALGORITHMEN 18 Algorithmus 4 DFS2 1 ) 1: markiert + compnum; 2: für alle Knoten aus ) / 3: falls (markiert + ) dann 4: DFS2 ; 5: 6: ' ' Analyse der Laufzeit: Mit Hilfe der gleichen Argumentation wie für Algorithmus 1 folgt eine Laufzeit von 3( ! ! ! "! . Kreissuche in einem Graphen Algorithmus 5/6 zeigt einen Tiefensuche-Algorithmus zur Lösung von Problem (3) für ungerichtete Graphen. Falls der betrachtete Nachbarknoten von in Algorithmus 6 noch unmarkiert ist, dann wird die Kante in den DFS-Baum aufgenommen. Um am Ende leichter den Kreis zu erhalten, wird als direkter Vorgänger von in father gespeichert. Die weitere Durchsuchung wird bei mit dem Aufruf DFS3 fortgesetzt. Sobald auf einen Nachbarn trifft, der bereits markiert ist (und nicht sein direkter Vorgänger ist), wird ein Kreis gefunden. Der Graph enthält also genau dann keinen Kreis, wenn ist. Für jeden im DFS-Lauf entdeckten Kreis wird eine Kante in der Menge gespeichert. (Achtung: Sie können sich leicht überlegen, dass nicht alle Kreise in dadurch gefunden werden. Machen Sie sich klar, dass dies kein Widerspruch zur obigen Aussage ist.) Mit Hilfe des DFS-Baumes (der hier sowohl in als auch in father() gespeichert ist) kann man nun ganz einfach die Menge der gefundenen Kreise abrufen. Für jede Kante in erhält man den dazugehörigen Kreis, indem man so lange den eindeutigen Vorfahren (father) von im DFS-Baum folgt, bis man auf trifft. Dazu ist es notwendig, die Menge als gerichtete Kantenmenge abzuspeichern, damit man Anfangsknoten und Endknoten unterscheiden kann. (Anmerkung: zu diesem Zweck wäre es eigentlich nicht nötig gewesen, die Menge zu speichern. Wir haben Sie nur zur besseren Veranschaulichung eingeführt.) Analyse der Laufzeit: Die gleiche Argumentation wie für Algorithmus 1 liefert die Laufzeit 3( ! ! ! "! . 1.3. DURCHLAUFEN VON GRAPHEN 19 Algorithmus 5 Depth-First-Search3 + Input: Graph Output: Findet einen Kreis in , falls einer existiert ) / 1: Initialisierung: setze markiert + für alle ; 2: Setze ; ; 3: für alle aus ) / 4: falls (markiert + ) dann 5: DFS3 ; 6: 7: 8: 9: 10: ' ' falls ( ) dann Ausgabe Kreis gefunden“; ” ' Algorithmus 6 DFS3 . ) 1: markiert ,+ ; 2: für alle Knoten aus . ) 3: falls ((markiert + ) und (father 1 4: Setze ; ' sonst 5: 6: Setze ; 7: father ; 8: DFS3 ; 9: 10: ' ' )) dann KAPITEL 1. GRAPHENALGORITHMEN 20 8 b 4 11 a 7 c d 2 8 9 6 7 h 14 4 i 1 e 10 g 2 f Abbildung 1.3: Die durchgezogenen Linien stellen einen MST des gezeigten Graphen dar. 1.4 Minimale aufspannende Bäume Ein Graph wird Baum genannt, wenn er kreisfrei . (im ungerichteten Sinne) und zusammenhängend ist. Für einen Baum gilt: ! ! ! "! . Ferner gibt es in einem Baum jeweils genau einen ungerichteten Weg zwischen je zwei Knoten. Entfernt man eine Kante aus einem Baum, so zerfällt dieser in genau zwei Komponenten. Bei Wegnahme eines Knotens mit Grad und seiner inzidenten Kanten zerfällt ein Baum in Komponenten. Man kann sich überlegen, dass die obigen Aussagen jeweils äquivalente Definitionen eines Baumes sind. Das MST-Problem Gegeben ist ein zusammenhängender und gewichteter (ungerichteter) Graph mit Kantengewichten für $ % . Gesucht ist ein minimaler aufspannender Baum (Minimum Spanning Tree, MST) in , d.h. ein zusammenhängender, zyklenfreier Untergraph mit , dessen Kanten alle Knoten aufspannen und für den so klein wie möglich ist. Abbildung 1.3 zeigt einen gewichteten Graphen und einen minimalen aufspannenden Baum von . Das Gesamtgewicht des MST ist (bzw. dessen Kosten sind) 37. Beachten Sie, dass der MST nicht eindeutig sein muss. So kann z.B. in Abb. 1.3 die Kante + durch ersetzt werden, ohne dass sich das Gewicht ändert. Eindeutigkeit des MST ist jedoch dann gegeben, wenn die Gewichte aller Kanten unterschiedlich sind. Das MST-Problem taucht in der Praxis z.B. als Unterproblem beim Design von Kommunikationsnetzwerken auf. 1.4.1 Der Algorithmus von Kruskal Kruskal schlug 1956 einen Algorithmus zur exakten Lösung des MST-Problems vor. Dieser Algorithmus fällt unter das allgemein anwendbare Greedy-Prinzip. Greedy-Algorithmen sind gierige Verfahren zur exakten oder approximativen Lösung von Optimierungsaufgaben, welche die Lösung iterativ aufbauen (z.B. durch schrittweise Hinzunahme von Objekten) und sich in jedem Schritt für die jeweils lokal beste Lösung entscheiden. Eine getroffene 1.4. MINIMALE AUFSPANNENDE BÄUME 21 Entscheidung wird hierbei niemals wieder geändert. Daher sind Greedy-Verfahren bezogen auf die Laufzeit meist effizient, jedoch führen sie nur in seltenen Fällen – wie z.B. beim MST-Problem – zur optimalen Lösung. Die Idee des Kruskal-Algorithmus lautet wie folgt: Sortiere die Kanten von nach ihren Gewichten: Für . #"#"" : Falls ' kreisfrei ist, dann setze "#"#" ' . Der Algorithmus ist greedy, weil er in jedem Schritt die jeweils billigste“ noch hinzufügba” re Kante aufnimmt. Korrektheit: kreisfrei? zusammenhängend? aufspannend? minimal? Indirekte Annahme: wäre nicht minimal. Dann existiert ein aufspannender Baum mit . Außerdem existiert eine Kante mit . Wir bauen nun schrittweise zu um; wenn wir dies schaffen, ohne eine teurere Kante durch eine billigere zu ersetzen, haben wir den erwünschten Widerspruch erreicht. ' enthält einen Kreis . In existiert eine Kante mit & und . , denn sonst wäre vor angeschaut worden und zu Behauptung: + hinzugefügt worden. Denn das Hinzufügen von zu dem aktuellen Unterbaum von hätte keinen Kreis erzeugen können, da sonst und somit einen Kreis ' ' . ist weiterhin ein aufspannender enthalten würde. Wir setzen also 1 . Baum und es gilt Wir bauen weiter um bis erreicht ist. 1.4.2 Implementierung von Kruskal mittels Union-Find Möchte man den Kruskal-Algorithmus implementieren, so stellt sich die Frage, wie man effizient das folgende Problem löst: Teste, ob ' einen Kreis enthält. Eine naive Lösung wäre es, für jede Abfrage den DFS-Algorithmus für ' aufzurufen. Dies würde eine Gesamtlaufzeit von ! ! ! "! ergeben, da sich in bis zu ! ! Kanten befinden können, was einer Laufzeit von ! ! für einen DFS-Aufruf entspricht. KAPITEL 1. GRAPHENALGORITHMEN 22 Im folgenden werden wir sehen, wie wir für den zweiten Teil des Kruskal-Algorithmus, d.h. alles nach dem Sortieren der Kanten, mit Hilfe der Datenstruktur Union-Find eine fast lineare Laufzeit erhalten können und damit der Gesamtaufwand durch das Kantensortieren bestimmt wird ( ! !! "! ). Die Idee besteht darin, eine Menge von Bäumen, die der aktuellen Kantenmenge entspricht, iterativ zu einem Baum zusammenwachsen zu lassen ( Wir lassen Wälder ” wachsen“). Man beginnt mit Bäumen, die jeweils nur aus einem Knoten des gegebenen Graphen bestehen. Dann werden nach jeder Hinzunahme einer Kante $ zur anfangs leeren Menge die Bäume, in denen und liegen, zu einem Baum verbunden (union). Eine Hilfsfunktion liefert hierzu jeweils die Information, in welchem Baum ein Knoten liegt. Liegen und bereits im gleichen Baum, dann würde die Hinzunahme der Kante ($ ) zu einem Kreis führen. Wir betrachten das folgende Beispiel zum Graphen aus Abb. 1.3: Komponenten (Bäume) ' ' ' ' ' ' ' + ' ' ' + ' ' ' + ' ' ' ' + ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' nächste betrachtete Kante ' ' ' ' 1 + ' ' Hinzunahme zu nein! wird nicht hinzugefügt, weil und bereits im gleichen Baum liegen, d.h. verbunden sind. Die Hinzunahme der Kante würde also einen (verbotenen) Kreis erzeugen. Weiter geht es dann z.B. wie folgt: Komponenten (Bäume) ' + ' + ' + + + ' nächste betrachtete Kante Hinzunahme zu ' ' + ' ' ' ' ' ' nein! ' ' + nein! In diesem Schritt wurde ein kompletter MST erzeugt, daher werden die restlichen Kanten abgelehnt. Man kann also den Algorithmus bereits an dieser Stelle abbrechen, weil ohnehin nur mehr Kreise entstehen würden, wie die probeweise Weiterführung zeigt: Komponenten (Bäume) + + + ' ' ' nächste betrachtete Kante Hinzunahme zu nein! nein! nein! Im folgenden betrachten wir die Union-Find Datenstruktur genauer. 1.4. MINIMALE AUFSPANNENDE BÄUME 23 1.4.3 Abstrakter Datentyp: Dynamische Disjunkte Menge (DDM) Wir betrachten den abstrakten Datentyp DDM, der die Union-Find Datenstruktur beschreibt. Die Objekte sind eine Familie 0 "#"#" ' disjunkter Teilmengen einer endlichen ' Menge . Jede Teilmenge hat einen Repräsentanten $ . ' Die Operationen sind die folgenden: Seien , makeset : erzeugt eine. neue Menge für alle #"#"#" . dass ' 0 ' ; hierbei gilt die Annahme, union : vereinigt die Mengen und , deren Repräsentanten und sind, zu einer neuen Menge . Der Repräsentant von ist ein beliebiges Element aus ; $' ' . findset : liefert den Repräsentanten der (eindeutigen) Menge, die enthält. Mit Hilfe dieses abstrakten Datentyps können wir nun den Kruskal-Algorithmus genauer formulieren: Algorithmus 7 Kruskal-Minimal-Spanning-Tree Input: Graph mit Gewichten für alle Output: Minimum Spanning Tree von . 1: ; ; ; 2: Sortiere Kanten: #0 3: für alle Knoten aus 4: makeset ; & 5: 6: 7: 8: 9: 10: 11: 12: 13: ' solange mehr als eine Menge enthält /* Sei die nächste Kante */ falls (findset findset ) dann * ; union(findset ,findset ); ' i++; ' Man beachte, dass nicht unbedingt jede Kante von betrachtet werden muss, sondern abgebrochen wird, wenn ein vollständiger Spannbaum erreicht wurde. An der Ordnung des Aufwands ändert dies jedoch nichts, da im worst case gerade die teuerste Kante benötigt wird. Realisierung des Datentyps DDM: Jede Menge wird durch einen gerichteten Baum repräsentiert. Die Knoten des Baumes sind die Elemente der Menge. Jeder Knoten im Baum enthält einen Zeiger auf seinen Vater. KAPITEL 1. GRAPHENALGORITHMEN 24 g i g i f f c c h h (a) (b) Abbildung 1.4: Dynamische disjunkte Mengen: (a) zwei gerichtete Bäume, die die Mengen + ' und ' repräsentieren; (b) nach der Vereinigung der Mengen. Die Wurzel markieren wir dadurch, dass sie auf sich selbst zeigt. So besitzt jeder Knoten genau eine ausgehende Kante, und zur Speicherung der Bäume genügt ein einfaches Feld ) father + . Abbildung 1.4 zeigt die Situation unseres Beispiels vor und nach dem Hinzufügen der Kante + . ) father + sieht danach folgendermaßen aus: ) father + Die Realisierung der Operationen der Union-Find Datenstruktur DDM ist nun nicht nur offensichtlich, sondern vor allem auch kurz und elegant, wie man an den Implementierungen Algorithmus 8, Algorithmus 9, und Algorithmus 10 sieht. Analyse der Laufzeit: . makeset: . union: findset: (Baum)) nach Union-Operationen Problem: Da degenerierte Bäume die Höhe 3( besitzen können, würde jeder Schritt im dauern. Wir hätten also bezüglich worst-case Gesamtlaufzeit von ! ! ! "! mer noch nichts gewonnen. besitzen. (Sie können sich leicht selbst eine Operationen-Folge überlegen, bei der ein degenerierter Baum der Höhe 3. erzeugt wird.) Verbesserung 1: Vereinigung nach Größe oder Höhe: Idee: Um zwei Bäume mit Wurzeln und zu vereinigen, macht man die Wurzel des Baumes mit kleinerer Größe (bzw. Höhe) zum direkten weiteren Sohn des Baumes mit 1.4. MINIMALE AUFSPANNENDE BÄUME Algorithmus 8 makeset ) 1: father + 25 Algorithmus 9 union ) 1: father + Algorithmus 10 findset 1: ) 2: solange (father !+ ) 3: father !+ ; 4: 5: ' ) Return ; größerer Größe (bzw. Höhe). Der Repräsentant des größeren bzw. höheren Baumes wird somit zum Repräsentanten der neuen Menge. Hierzu muss man sich zusätzlich zu jedem Baum dessen Größe bzw. Höhe in der Wurzel merken. Lemma: Dieses Verfahren liefert Bäume mit Höhe Beweis) Folgerung: Die findset-Operation benötigt im Worst-Case 3( nach Operationen. (ohne Schritte. Verbesserung 2: Methode der Pfadverkürzung (Kompressionsmethode): Idee: Man erhält eine weitere Verkürzung der Pfade, und somit eine Laufzeiteinsparung der findset-Operation, wenn man beim Durchlauf jeder findset-Operation alle durchlaufenen Knoten direkt an die Wurzel anhängt. Bei der Ausführung von findset durchläuft man also den gleichen Pfad zweimal: das erste Mal, um die Wurzel zu finden und das zweite Mal, um den Vater aller Knoten auf dem durchlaufenen Pfad auf die Wurzel zu setzen. Dies verteuert zwar die findset-Operation geringfügig, führt aber zu einer drastischen Reduktion der Pfadlängen, denn man kann das folgende Theorem zeigen: ' Theorem (Amortisierte Analyse): Sei die Anzahl aller union-, makeset-, findset ' Operationen und die Anzahl der Elemente in allen Mengen, : Die Ausführung ' ' ' der Operationen benötigt 3( Schritte. ' ' heißt die inverse Ackermannfunktion. Ihr Wert erhöht sich mit wachsendem ' bzw. nur extrem langsam. So ist für alle praktisch auftretenden Werte von . ' ' / und (solange ). Damit ist die Ausführung einer findset-Operation durchschnittlich (gemittelt über alle ' KAPITEL 1. GRAPHENALGORITHMEN 26 Operationen) innerhalb des Kruskal-Algorithmus in praktisch konstanter Zeit möglich. Dies führt zu einer praktisch linearen Laufzeit des zweiten Teils des Kruskal-Algorithmus, nachdem die Kanten sortiert worden sind. Der Gesamtaufwand wird nun durch das Kantensortieren bestimmt und ist somit ! !! "! . 1.4.4 Der Algorithmus von Prim Ein weiterer, exakter Algorithmus zur Bestimmung eines MST wurde von Prim 1957 vorgeschlagen. Dieser Ansatz arbeitet ebenfalls nach dem Greedy-Prinzip. Wir wollen diesen Algorithmus hier nur kurz zusammenfassen. Im Gegensatz zu Kruskals Algorithmus wird von einem Startknoten ausgegangen, und neue Kanten werden immer nur zu einem bestehenden Baum hinzugefügt: Algorithmus 11 Prim-MST Input: Graph mit Gewichten für alle Output: Minimum Spanning Tree von 1: Wähle einen beliebigen Startknoten 2: ' 3: 4: solange ! ! ! "! 5: Ermittle eine Kante $ mit & & ' ; 6: ' ; 7: & und minimalem Gewicht 8: ' Schritt (5), das Ermitteln einer günstigsten Kante zwischen dem bestehenden Baum und einem noch freien Knoten, kann effizient gelöst werden, indem alle noch freien Knoten in einem Heap verwaltet werden. Die Sortierung erfolgt dabei nach dem Gewicht der jeweils günstigsten Kante, die einen freien Knoten mit dem aktuellen Baum verbindet. Der Heap muss nach dem Hinzufügen jeder Kante aktualisiert werden. Konkret wird für jeden verbleibenden freien Knoten überprüft, ob es eine Kante zum gerade angehängten Knoten gibt und diese günstiger ist als die bisher bestmögliche Verbindung zum Baum. Unter Verwendung des beschriebenen Heaps ist der Gesamtaufwand des Algorithmus von 0 0 Prim ! ! . Damit ist dieser Ansatz für dichte Graphen ( ! "!% 3( ! ! ) etwas besser geeignet als jener von Kruskal. Umgekehrt ist Kruskals Algorithmus für dünne Graphen ( ! "! 3( ! ! ) effizienter. 1.4. MINIMALE AUFSPANNENDE BÄUME 27 Weiterführende Literatur Das Minimum Spanning Tree Problem wird z.B. in den Büchern von Sedgewick und Cormen, Leiserson und Rivest (siehe Literaturliste (2) und (1)) ausführlich beschrieben. In letzterem Buch finden sich auch eine weitergehende Beschreibung zu Greedy-Algorithmen, eine genaue Definition und Behandlung der Ackermann-Funktion und ihrer Inversen, sowie Informationen zur amortisierten Analyse. 28 KAPITEL 1. GRAPHENALGORITHMEN Kapitel 2 Optimierungsalgorithmen Optimierungsprobleme sind Probleme, die im Allgemeinen viele zulässige Lösungen besitzen. Jeder Lösung ist ein bestimmter Wert (Zielfunktionswert, Kosten) zugeordnet. Optimierungsalgorithmen suchen in der Menge aller zulässigen Lösungen diejenigen mit dem besten, dem optimalen, Wert. Beispiele für Optimierungsprobleme sind: Minimal aufspannender Baum Kürzeste Wege in Graphen Längste Wege in Graphen Handelsreisendenproblem Rucksackproblem Scheduling (z.B. Maschinen, Crew) Zuschneideprobleme (z.B. Bilderrahmen, Anzüge) Packungsprobleme Wir unterscheiden zwischen Optimierungsproblemen, für die wir Algorithmen mit polynomiellem Aufwand kennen (“in P”), und solchen, für die noch kein polynomieller Algorithmus bekannt ist. Darunter fällt auch die Klasse der NP-schwierigen Optimierungsprobleme. Die Optimierungsprobleme dieser Klasse besitzen die Eigenschaft, dass das Finden eines polynomiellen Algorithmus für eines dieser Optimierungsprobleme auch polynomielle Algorithmen für alle anderen Optimierungsprobleme dieser Klasse nach sich ziehen würde. Die meisten Informatiker glauben, dass für NP-schwierige Optimierungsprobleme keine polynomiellen Algorithmen existieren. Ein Algorithmus hat polynomielle Laufzeit, wenn die Laufzeitfunktion 5& durch ein Polynom in & beschränkt ist. Dabei steht & für die Eingabegröße der Instanz. Z.B. ist das Kürzeste-Wegeproblem polynomiell lösbar, während das Längste-Wegeproblem im 29 30 KAPITEL 2. OPTIMIERUNGSALGORITHMEN Allgemeinen NP-schwierig ist. (Transformation vom Handlungsreisendenproblem: Wäre ein polynomieller Algorithmus für das Längste–Wegeproblem bekannt, dann würde dies direkt zu einem polynomiellen Algorithmus für das Handlungsreisendenproblem führen.) Dies scheint auf den ersten Blick ein Widerspruch zu sein: wenn man minimieren kann, dann sollte man nach einer Kostentransformation (“mal -1”) auch maximieren können. Allerdings ist für das Kürzeste–Wegeproblem nur dann ein polynomieller Algorithmus bekannt, wenn die Instanz keine Kreise mit negativen Gesamtkosten enthält. Und genau das ist nach der Kostentransformation nicht mehr gewährleistet. Für polynomielle Optimierungsaufgaben existieren verschiedene Strategien zur Lösung. Die meisten davon sind speziell für das jeweilige Problem entwickelte Algorithmen. Manchmal jedoch greifen auch allgemeine Strategien, wie z.B. das Greedy-Prinzip oder die Dynamische Programmierung. In diesem Abschnitt wollen wir uns auf Lösungsansätze für NP-schwierige Optimierungsprobleme konzentrieren, da diese in der Praxis weitaus häufiger auftauchen als polynomielle Optimierungsprobleme. Zunächst werden wir typische exakte Lösungsverfahren betrachten. Dies sind auf Enumeration beruhende Verfahren, Branch-and-Bound Verfahren und das Verfahren der Dynamischen Programmierung. Danach konzentrieren wir uns auf die approximativen Algorithmen (Heuristiken), die meist die optimale Lösung nur annähern. Man unterscheidet hier zwischen Algorithmen mit und ohne Gütegarantie, sowie zwischen Konstruktionsheuristiken und Verbesserungsheuristiken. 2.1 Exakte Algorithmen für schwierige Optimierungsprobleme 2.1.1 Enumerationsverfahren Exakte Verfahren, die auf einer vollständigen Enumeration beruhen, eignen sich für NP-schwierige Optimierungsprobleme diskreter Natur. Für solche kombinatorische Optimierungsprobleme ist es immer möglich, die Menge aller zulässigen Lösungen aufzuzählen und mit der Kostenfunktion zu bewerten. Die am besten bewertete Lösung ist die optimale Lösung. Natürlich ist die Laufzeit dieses Verfahrens nicht durch ein Polynom in der Eingabegröße beschränkt. Im Folgenden betrachten wir ein Enumerationsverfahren für das 0/1-Rucksackproblem. Das 0/1-Rucksack-Problem Gegeben: Gegenstände mit Gewicht (Größe) , und Wert (Kosten) , und ein Rucksack der Größe . Gesucht: Menge der in den Rucksack gepackten Gegenstände mit maximalem Gesamtwert; 2.1. EXAKTE ALGORITHMEN FÜR SCHWIERIGE OPTIMIERUNGSPROBLEME 31 dabei darf das Gesamtgewicht den Wert nicht überschreiten. Wir führen 0/1-Entscheidungsvariablen für die Wahl der Gegenstände ein: - #"#"" wobei / falls Element nicht gewählt wird falls Element gewählt wird . Das Rucksackproblem lässt sich nun formal folgendermaßen formulieren: ! * / . ' . Beispiel: Das Rucksack-Problem mit Gegenstand a Gewicht 3 Wert 3 Nutzen 1 ' und folgenden Daten: b c d e f g h 4 4 6 6 8 8 9 5 5 10 10 11 11 13 1,25 1,25 1,66 1,66 1,375 1,375 1,44 Der Nutzen eines Gegenstands errechnet sich aus . Das Packen der Gegenstände a, b, c und d führt zu einem Gesamtwert von 23 bei dem maximal zulässigen Gesamtgewicht von 17. Entscheidet man sich für die Gegenstände c, d und e, dann ist das Gewicht sogar nur 16 bei einem Gesamtwert von 25. Ein Enumerationsalgorithmus für das 0/1-Rucksackproblem Eine Enumeration aller zulässigen Lösungen für das 0/1-Rucksackproblem entspricht der Aufzählung aller Teilmengen einer -elementigen Menge (bis auf diejenigen Teilmengen, die nicht in den Rucksack passen). Der Algorithmus Enum() (s. Algorithmus 12) basiert auf genau dieser Idee. Zu jedem Lösungsvektor gehört ein Zielfunktionswert (Gesamtkosten von ) und ein Gesamtgewichtswert . Die bisher beste gefundene Lösung wird in globalen Vektor und der zugehörige Lösungswert in der globalen Variablen / / / gespeichert. Der Algorithmus wird mit dem Aufruf & gestartet, wobei Anfang der Nullvektor ist. dem am Analyse: Korrektheit: Zeilen (6)-(9) enumerieren über alle möglichen Teilmengen einer elementigen Menge. Zeilen (1)-(5) sorgen dafür, dass nur zulässige Lösungen betrachtet werden und die optimale Lösung gefunden wird. KAPITEL 2. OPTIMIERUNGSALGORITHMEN 32 Algorithmus 12 Enum 1 ; 1: falls ( ) dann 2: falls ( ) dann 3: ; 4: " ; 5: 6: 7: 8: 9: 10: 11: ' für ) . + . " "" ; Enum ( , ) / + ; ) +, ) +, ); ' ' Laufzeit: . Wegen der exponentiellen Laufzeit ist das Enumerationsverfahren i.A. nur für kleine / Instanzen des 0/1-Rucksackproblems geeignet. Bereits für ist das Verfahren nicht mehr praktikabel. Deutlich besser geeignet für die Praxis ist das Verfahren der Dynamischen Programmierung. 2.1.2 Dynamische Programmierung Idee: Zerlege das Problem in kleinere Teilprobleme ähnlich wie bei “Divide & Conquer”. Allerdings: während die bei Divide & Conquer unabhängig sind, sind sie hier voneinander abhängig. Dazu: Löse jedes Teilproblem und speichere das Ergebnis ab, so dass zur Lösung größerer Probleme verwendet werden kann. Allgemeines Vorgehen: 1. Wie ist das Problem zerlegbar? Definiere den Wert einer optimalen Lösung rekursiv. 2. Bestimme den Wert der optimalen Lösung bottom-up“. ” Wir betrachten im Folgenden wieder das 0/1-Rucksackproblem. Dynamische Programmierung für das 0/1-Rucksackproblem Eine Möglichkeit, das Problem in Teilprobleme zu zerlegen, ist die folgende: Wir betrachten zunächst die Situation nach der Entscheidung über die ersten Gegenstände und merken uns für alle möglichen Gesamtwerte diejenigen Lösungen, die jeweils das kleinste Gewicht erhalten. Wir definieren: 2.1. EXAKTE ALGORITHMEN FÜR SCHWIERIGE OPTIMIERUNGSPROBLEME 33 . Für jedes #" Die Funktion "" ! . * / ' ' erhalten wir somit ein Teilproblem. kann rekursiv folgendermaßen bestimmt werden: und ) ) # 0 ' "#"" falls falls sonst / für / In Worten bedeutet dies: Wir können aus den bereits bekannten Funktionswerten für kleinere folgendermaßen berechnen: Im Entscheidungsschritt wird entweder der Gegenstand nicht in den Rucksack gepackt; in diesem Fall wird der Wert von ) einfach übernommen. Oder, Gegenstand wird in den Rucksack gepackt. Das minimale Gewicht des neu gepackten Rucksacks mit Gegenstand und Wert erhalten wir dann als Summe aus dem minimalen Gewicht des Rucksacks vor dem Packen von mit Wert gleich und dem Gewicht von Gegenstand . / Problem: Effiziente Bestimmung aller #""#" Lösung: Entscheide iterativ über alle Gegenstände ? . #" "" . In unserem Beispiel sieht das folgendermaßen aus: 1. Entscheidung über den ersten Gegenstand a: - / / . 2 2 2. Entscheidung über den zweiten Gegenstand b: Man sieht hier, dass nicht für alle möglichen wirklich berechnet wird, sondern - 0 / / 0 . 2 2 0 / / 0 0 nur für die relevanten Fälle. Man kann sich das sehr gut mit einem Entscheidungsbaum veranschaulichen (s. Abb. 2.1). Entscheidend ist, dass bei zwei Rucksäcken mit gleichen -Werten derjenige mit dem kleinsten Gewicht behalten wird und der andere verworfen wird. Einträge mit sind irrelevant und werden nicht betrachtet. KAPITEL 2. OPTIMIERUNGSALGORITHMEN 34 (0.0) 0 (0.0) 1 (3.3) 1 (5.4) 0 (0.0) 0 (0.0) 0 (5.4) 1 (5.4) 0 (3.3) 0 1 0 1 (0.0) (10.6) (5.4) (15.10) 1 (10.8) 0 (3.3) 1 (8.7) 1 (8.7) 0 (8.7) 1 (13.11) 0 1 0 1 0 1 (10.8) (20.14) (3.3) (13.9) (8.7) (18.13) Abbildung 2.1: Teil des Entscheidungsbaumes bis Programmierung 0 1 (13.11) (23.17) unseres Beispiels zur Dynamischen Dynamischer Programmierungsalgorithmus für das Rucksackproblem ' Sei Menge aller Elemente in Stufe ' , wobei die Menge der jeweils eingepackten Gegenstände ist. Der Algorithmus Dynamische Programmierung 0/1-Knapsack . ' " " " . berechnet iterativ für alle Algorithmus 13 Dynamische Programmierung 0/1-Knapsack Input: Gegenstände mit Gewicht und Wert , sowie Rucksack der Größe Output: Eingepackte Gegenstände mit maximalem Wert unter allen Rucksäcken mit Gewicht kleiner gleich ' / / 1: Sei . ' ; 2: für #" " " ' ) 3: für jedes + ' 10: 11: * falls 0 ' ' 4: 5: 6: 7: 8: 9: ' ' ' dann ' 0 ' Untersuche auf Paare mit gleichem und behalte für jedes solche Paar das Element mit kleinstem Gewicht ' Optimale Lösung ist dasjenige Element in ' mit maximalem . Analyse: . Korrektheit: Es wird jeweils in Schritt ( ) unter allen möglichen Lösungen mit " " " Wert die “leichteste” behalten. Nach Schritten muss die optimale Lösung dabei sein. Laufzeit: Die Laufzeit hängt von der Realisierung der Schritte (3)-(6) sowie (9) ab. Um Schritt (9) effizient durchzuführen, führen wir ein Array ' ein; dabei ist der größte auftretende -Wert in . )/ . " "" + von Tripeln 2.1. EXAKTE ALGORITHMEN FÜR SCHWIERIGE OPTIMIERUNGSPROBLEME 35 Algorithmus 14 Beispiel für Realisierung von Schritt 9 ' 1: Bestimme unter den Elementen in ' nach aufsteigenden -Werten + in 2: Sortiere . die Elemente #" " " 3: für ) + 4: ; ' 5: für alle Tripel + in : ) 6: falls + dann 7: Übernehme das Tripel 8: ' sonst ' 9: Entferne Tripel + aus 10: 11: 12: ' ' ' ' Jede Menge enthält höchstens Elemente, wobei den maximalen Lösungswert bezeichnet. Die Laufzeit der Schritte (5)-(7) hängt davon ab, wie man die Kopien der Menge realisiert; kopiert man sie jedesmal, dann ergibt sich insgesamt eine Gesamtlaufzeit von 0 , wobei den optimalen Lösungwert bezeichnet. Gibt man sich mehr Mühe bei der Organisation der Menge , dann kann man auch Laufzeit erreichen. Der folgende Algorithmus 0/1-Rucksack Dynamische Programmierung Tabelle zeigt eine alternative Möglichkeit für eine Implementierung der Idee der Dynamischen Programmierung. Wir verwenden die folgende Datenstruktur: )/ / " " + , wobei hier eine obere Schranke für Wir speichern #""#" im Feld ist. Für die Berechnung von gibt es mehrere Möglichkeiten: 1. Wir sortieren die Gegenstände absteigend nach ihrem Nutzen. Der Nutzen eines Gegenstands ist definiert als der relative Wert im Vergleich zum Gewicht : Intuitiv sind Gegenstände mit einem größeren Nutzen wertvoller als welche mit geringerem Nutzen, und sind deshalb vorzuziehen. (Jedoch führt ein einfacher GreedyAlgorithmus hier nicht zur Optimallösung.) Wir packen nun zunächst den Gegenstand mit größtem Nutzen, dann denjenigen mit zweitgrößtem Nutzen, etc. bis das Gesamtgewicht ist. 2. Wir nehmen den Gegenstand mit dem größten Nutzen Mal, solange bis 3. Wir multiplizieren den größten Nutzen mit . . KAPITEL 2. OPTIMIERUNGSALGORITHMEN 36 Algorithmus 15 . Rucksack Dynamische Programmierung Tabelle / 1: für #" " " ) ) )/ / 2: + + ; + 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: ' . für #" " " . / " " " für ) + falls ) /* Fall: + ) + falls ) + ' / ) ) dann */ ) + dann + . /* Fall: + */ ) ) falls + 0 + ) ' ) ++ ) ) + 0 ) ) + + . ) + 0 + ) + dann ' ' für ' ) . / + ) #" + "" ) + ' Korrektheit: Auch hier wird jeweils die kleinste Lösung unter allen Werten gleich nach Schritten berechnet. Für ist also die Lösung des Rucksack-Problems korrekt. Laufzeit: . Oberflächlich betrachtet sehen die Laufzeiten der beiden Dynamischen Programmierungsal polynomiell aus. Allerdings kann (bzw. bzw. gorithmen von im allgemeinen sehr groß sein. Nur in dem Fall dass durch ein Polynom in beschränkt ist, besitzt der Algorithmus polynomielle Laufzeit. Da die Gesamtlaufzeit nicht nur von der Eingabegröße , sondern auch von den Werten , und abhängt, nennt man die Laufzeit pseudopolynomielle Laufzeit. Zusammenfassend können wir festhalten, dass das Rucksackproblem mit Hilfe von Dynamischer Programmierung in pseudopolynomieller Zeit exakt lösbar ist. Da in der Praxis meist durch ein Polynom beschränkt ist, führt dies dann zu einer polynomiellen Laufzeit (obwohl das Problem im Allgemeinen NP-schwierig ist). 2.1. EXAKTE ALGORITHMEN FÜR SCHWIERIGE OPTIMIERUNGSPROBLEME 37 2.1.3 Beschränkte Enumeration Das Acht-Damen Problem Wir wollen nun ein weiteres kombinatorisches Problem betrachten, das wir mit einer Enumeration aller Möglichkeiten lösen wollen. Das Acht-Damen Problem ist ein klassisches Schachproblem, das von Bezzel 1845 formuliert wurde. Die Aufgabe verlangt, acht Damen auf einem Schachbrett so aufzustellen, dass keine Dame von einer anderen bedroht wird. Bedrohungen gehen dabei gemäß den Schachregeln in horizontaler, vertikaler und diagonaler Richtung von jeder Dame aus, wie Abbildung 2.2 zeigt. Bei diesem Problem geht es daher im Gegensatz zu vielen anderen nicht darum, aus einer großen Menge gültiger Lösungen eine zu finden, die eine Zielfunktion minimiert, sondern nur darum, überhaupt eine gültige Lösung zu finden ( Constraint satisfaction“ Problem). ” Wir wollen hier dieses Problem verallgemeinern und gehen von & Damen und einem & & Spielfeld aus. Die einzelnen Spalten bezeichnen wir anstatt mit Buchstaben nun wie die . Zeilen mit den Zahlen #"#"" & . Außerdem wollen wir nicht nur eine richtige Lösung, sondern alle erzeugen. Ein naiver Ansatz, alle Möglichkeiten der Damenaufstellungen zu enumerieren ist in ) + ein Vektor, der die Position für jede Dame Algorithmus 16 beschrieben. Dabei ist . "#"#" & speichert. Ein Aufruf dieses rekursiven Algorithmus mit Naive-Enumeration(D,1) erzeugt alle Kombinationen von allen möglichen Platzierungen jeder einzelnen Dame auf eines 0 $ 0$ der & & Felder. Insgesamt werden daher 5& & komplette Stellungen erzeugt und auf Korrektheit überprüft. Das Überprüfen auf Korrektheit erfordert aber nochmals einen 0 0$ 0 ist! Aufwand von 3.5& , womit der Gesamtaufwand 3(4& 8 7 6 5 4 3 2 1 A B C D E F G H Abbildung 2.2: Bedrohte Felder einer Dame im Acht-Damen Problem KAPITEL 2. OPTIMIERUNGSALGORITHMEN 38 Algorithmus 16 . Naive-Enumeration . ) 1: für + bis 4& & 2: falls & dann . 3: Naive-Enumeration 4: ' sonst 5: Überprüfe und gib es aus, falls korrekt /* Komplette Platzierung erreicht */ 6: 7: ' ' . / Für & sind ca. Stellungen zu erzeugen. Selbst wenn 1 Million Stellungen pro Sekunde überprüft werden könnten, würde das ca. 9 Jahre dauern. Wir sehen also, dass ein derart naives Vorgehen sehr teuer kommen würde. Durch Berücksichtigung folgender Beobachtungen kann das Verfahren entscheidend verbessert werden: 1. In einer korrekten Lösung muss in jeder Zeile genau eine Dame stehen. 2. Ebenso muss in jeder Spalte genau eine Dame stehen. 3. Ein paarweises Vertauschen von Damen ändert nichts an einer Lösung. Wir fixieren daher zunächst, dass Dame immer in Spalte zu stehen kommt. Die Zeilen aller Damen müssen immer unterschiedlich sein. Daher können wir nun alle möglichen Stellungen, welche. die oberen Bedingungen erfüllen, enumerieren, indem wir alle Permuta) tionen der Werte #""#" & ' erzeugen. Wenn eine solche Permutation ist, stellt dann + die Zeile für die Dame in der -ten Spalte dar. Mit Algorithmus 17 können wir alle Permutationen erzeugen und die entsprechenden Stellungen auf Richtigkeit überprüfen. ist dabei die Menge aller bisher . noch nicht verwendeten Zeilen. Der Algorithmus wird mit Alle-Permutationen( , #""#" & ' ,1) gestartet, wobei nicht initialisiert zu sein braucht. / / Nun werden nur“ mehr & Stellungen erzeugt und auf Korrektheit untersucht ( ). ” Die rekursiven Aufrufe können wie in Abbildung 2.3 gezeigt als Baum dargestellt werden. Wir können das Verfahren weiter beschleunigen, indem wir so früh wie möglich ganze Gruppen von Permutationen erkennen, die keine gültige Lösungen darstellen, und diese erst gar nicht vollständig erzeugen. In Abbildung 2.3 würde das bedeuten, dass wir bei Konflikten in bereits fixierten Knoten alle weiteren Unterbäume abschneiden“. ” 2.1. EXAKTE ALGORITHMEN FÜR SCHWIERIGE OPTIMIERUNGSPROBLEME 39 Algorithmus 17 Alle-Permutationen 1: für alle . ) 2: 3: 4: 5: 6: 7: 8: + falls & dann . ) Alle-Permutationen +' ' sonst Überprüfe auf diagonales Schlagen und gib Stellung aus, falls korrekt /* Komplette Stellung erreicht */ ' ' Man bezeichnet allgemein ein solches Vorgehen als begrenzte Enumeration im Gegensatz zur vollständigen Enumeration. Die folgende Variante des Algorithmus überprüft unmittelbar bei der Platzierung der -ten Dame, ob diese nicht von einer bereits vorher platzierten bedroht wird (s. Algorithmus 18). Nur wenn dies nicht der Fall ist, wird mit der Rekursion fortgesetzt. Der Überprüfung auf diagonales Schlagen zweier Damen liegt die Idee zu Grunde, dass bei einer Bedrohung der Absolutbetrag der Spaltendifferenz gleich dem Absolutbetrag der Zeilendifferenz sein muss. Die exakte Berechnung des Aufwands ist nun schwierig, in der Praxis aber kommt es bei nun nur mehr zu 2057 rekursiven Aufrufen im Vergleich zu 69281 beim Enumerieren & aller Permutationen. Eine richtige Lösung ist in Abbildung 2.4 dargestellt. Eine weitere Verbesserung ist die Ausnutzung von Symmetrien.. So können wir uns in der Enumeration bei der Platzierung der ersten Dame auf die Zeilen #"#"#" & beschränken. Zu jeder dann gefundenen Lösung generieren wir automatisch gleich die um die Horizontale gespiegelte Lösung: Alle−Permutationen( Π,{1,...,8},1) Π[1] fixiert Π[2] fixiert 2 3 4 5 6 7 8 3 1 4 5 2 6 7 3 4 5 6 8 Π[3] fixiert Abbildung 2.3: Rekursive Aufrufe in Alle-Permutationen. 7 8 KAPITEL 2. OPTIMIERUNGSALGORITHMEN 40 Algorithmus 18 Begrenzte-Enumeration 1: für alle ( ) 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: + . solange ' . und ! ) + ) + ! falls dann /* keine Bedrohung */ falls & dann ) +' Begrenzte-Enumeration ' sonst /* Komplette, gültige Stellung erreicht */ Gib Stellung aus . ' ' ' 8 7 6 5 4 3 2 1 A B C D E F G H Abbildung 2.4: Eine Lösung zum Acht-Damen Problem. 2.1. EXAKTE ALGORITHMEN FÜR SCHWIERIGE OPTIMIERUNGSPROBLEME 41 ) + & ) + . . #""#" & " Der Aufwand der Enumeration sinkt dadurch um den Faktor 2. Auch horizontale Spiegelungen und Rotationen können ausgenutzt werden, um den Aufwand der Enumeration weiter zu senken. 2.1.4 Branch-and-Bound Eine spezielle Form der beschränkten Enumeration ist das Branch-and-Bound Verfahren. Dabei werden durch Benutzung von Primal- und Dualheuristiken möglichst große Gruppen von Lösungen als nicht optimal bzw. gültig erkannt und von der vollständigen Enumeration ausgeschlossen. Die Idee von Branch-and-Bound ist einfach. Wir gehen hier von einem Problem aus, bei dem eine Zielfunktion minimiert werden soll. (Ein Maximierungsproblem kann durch Vorzeichenumkehr in ein Minimierungsproblem überführt werden.) Zunächst berechnen wir z.B. mit einer Heuristik eine zulässige (im Allgemeinen nicht optimale) Startlösung mit Wert und eine untere Schranke für alle möglichen Lösungswerte (ein Verfahren hierzu nennt man auch Dualheuristiken). Falls / sein sollte, ist man fertig, denn die gefundene Lösung muss optimal sein. Ansonsten wird die Lösungsmenge partitioniert (Branching) und die Heuristiken werden auf die Teilmengen angewandt (Bounding). Ist für eine (oder mehrere) dieser Teilmengen die für sie berechnete untere Schranke (lower bound) nicht kleiner als die beste überhaupt bisher gefundene obere Schranke (upper bound = eine Lösung des Problems), braucht man die Lösungen in dieser Teilmenge nicht mehr beachten. Man erspart sich also die weitere Enumeration. Ist die untere Schranke kleiner als die beste gegenwärtige obere Schranke, muss man die Teilmengen weiter zerkleinern. Man fährt solange mit der Zerteilung fort, bis für alle Lösungsteilmengen die untere Schranke mindestens so groß ist wie die (global) beste obere Schranke. Die Hauptschwierigkeit besteht darin, “gute” Zerlegungstechniken und einfache Datenstrukturen zu finden, die eine effiziente Abarbeitung und Durchsuchung der einzelnen Teilmengen ermöglichen. Außerdem sollten die Heuristiken möglichst gute Schranken, damit möglichst große Teilprobleme ausgeschlossen werden können. KAPITEL 2. OPTIMIERUNGSALGORITHMEN 42 Branch-and-Bound für das asymmetrische TSP Der Großvater“ aller Branch-and-Bound Algorithmen in der kombinatorischen Optimie” rung ist das Verfahren von Little, Murty, Sweeny und Karel zur Lösung asymmetrischer Travelling-Salesman-Probleme, das 1963 veröffentlicht wurde. Weil dieses Verfahren so einfach ist, wollen wir es hier vorstellen. Asymmetrisches Travelling Salesman Problem (ATSP) ! ! Knoten, die Städten entGegeben: Gerichteter vollständiger Graph mit / sprechen, und eine Distanzmatrix mit und . Gesucht: Rundtour durch alle Städte, die jede Stadt genau einmal besucht und minimalen Distanzwert aufweist. Branch-and-Bound Verfahren von Little et al. Idee: Zeilen- und Spaltenreduktion der Matrix und sukzessive Erzeugung von Teilproble men beschrieben durch 1 . Dabei bedeutet: : Menge der bisher auf 1 fixierten Kanten : Menge der bisher auf 0 fixierten Kanten : Noch relevante Zeilenindizes von : Noch relevante Spaltenindizes von : Distanzmatrix des Teilproblems : Untere Schranke für das Teilproblem : Obere Schranke für das globale Problem Vorgangsweise: 1. Das Anfangsproblem ist . . #"#"" & ' "#"#" & ' / wobei die Ausgangsmatrix ist. Setze dieses Problem auf eine globale Liste der zu lösenden Probleme. 2. Sind alle Teilprobleme gelöst STOP. 2.1. EXAKTE ALGORITHMEN FÜR SCHWIERIGE OPTIMIERUNGSPROBLEME 43 Andernfalls wähle ein ungelöstes Teilproblem aus der Problemliste. 3. Bounding: a) Zeilenreduktion: Für alle : Berechne das Minimum der -ten Zeile von Setze und b) Spaltenreduktion: Für alle . : Berechne das Minimum der -ten Spalte von und Setze c) Ist , so entferne das gegenwärtige Teilproblem aus der Problemliste und gehe zu 2. Die Schritte (d) und (e) dienen dazu, eine neue (globale) Lösung des Gesamtproblems zu finden. Dabei verwenden wir die Information der in diesem Teilproblem bereits festgesetzten Entscheidungen. d) Definiere den “Nulldigraphen” / 2 0! ' mit e) Versuche, mittels einer Heuristik, eine Rundtour in zu finden. Wurde keine Tour gefunden, so gehe zu 4: Branching. Sonst hat die gefundene Tour die Länge . In diesem Fall: 2 #0 #1 Enferne alle Teilprobleme aus der Problemliste, deren lokale, untere Schranke größer gleich ist. Setze in allen noch nicht gelösten Teilproblemen . Gehe zu 2. KAPITEL 2. OPTIMIERUNGSALGORITHMEN 44 4. Branching: a) Wähle nach einem Plausibilitätskriterium eine Kante gesetzt wird. b) Definiere die neuen Teilprobleme # ' ' ' / . , die oder ' wobei aus durch Streichen der Zeile und der Spalte entsteht – von darf nicht noch einmal hinaus- und nach nicht noch einmal hineingelaufen werden. 0 ' wobei aus entsteht durch 2 , " Füge diese Teilprobleme zur Problemliste hinzu und gehe zu 2. Bemerkungen zum Algorithmus: zu 3. Bounding: Hinter der Berechnung des Zeilenminimums steckt die folgende Überlegung: Jede Stadt muss in einer Tour genau einmal verlassen werden. Zeile der Matrix enthält jeweils die Kosten für das Verlassen von Stadt . Und das geht auf keinen Fall billiger als durch das Zeilenminimum . Das gleiche Argument gilt für das Spaltenminimum, denn jede Stadt muss genau einmal betreten werden, und die Kosten dafür sind in Spalte gegeben. Schritte (a) und (b) dienen nur dazu, eine untere Schranke zu berechnen (Bounding des Teilproblems). Offensichtlich erhält man eine untere Schranke dieses Teilproblems durch das Aufsummieren des Zeilen- und Spaltenminimums. Für die Berechnung einer globalen oberen Schranke können Sie statt (d) und (e) auch jede beliebige TSP-Heuristik verwenden. zu 4. Branching: Ein Plausibilitätskriterium ist beispielsweise das folgende: Seien ! ' ' ! die minimalen Zusatzkosten, die entstehen, wenn wir von Kante zu benutzen. Wir wählen eine Kante / und ' ' nach gehen, ohne die , so dass ! / '," 2.1. EXAKTE ALGORITHMEN FÜR SCHWIERIGE OPTIMIERUNGSPROBLEME 45 Dahinter steckt die Überlegung: Wenn die Zusatzkosten sehr hoch sind, sollten wir lieber direkt von nach gehen. Damit werden gute“ Kanten beim Enumerieren erst ”/ einmal bevorzugt. Wichtig dabei ist, dass sein muss, sonst stimmen die Berechnungen von und nicht mehr. Bei der Definition eines neuen Teilproblems kann es vorkommen, dass die dort (in EINS oder NULL) fixierten Kanten zu keiner zulässigen Lösung mehr führen können (weil z.B. die zu EINS fixierten Kanten bereits einen Kurzzyklus enthalten). Es ist sicher von Vorteil, den Algorithmus so zu erweitern, dass solche Teilprobleme erst gar nicht zu der Problemliste hinzugefügt werden (denn in diesem Zweig des Enumerationsbaumes kann sich keine zulässige Lösung mehr befinden). Durchführung des Branch & Bound Algorithmus an einem ATSP Beispiel Die Instanz des Problems ist gegeben durch die Distanzmatrix : . . . . . . Das Ausgangsproblem: . #" "" . " ' "" ' / Bounding: Die Zeilenreduktion ergibt: / . . / . / / / / . / . . . / . / / . . / Die Spaltenreduktion ändert hier nichts, da bereits in jeder Spalte das Minimum 0 beträgt. Nun wird der Nulldigraph aufgestellt (siehe Abbildung 2.5). In diesem Nulldigraphen existiert keine Tour, da 6 nur über 2 erreicht werden kann. KAPITEL 2. OPTIMIERUNGSALGORITHMEN 46 6 1 5 2 3 4 Abbildung 2.5: Der Nulldigraph in Branch & Bound Branching: Wähle Kante (2,6) und setze diese einmal gleich 1 und einmal gleich 0. Dies ergibt zwei neue Teilprobleme. Das erste neue Teilproblem: ' . / 1#" "" . #" / / "" . / . / . / / ' . / / . ' Die untere Schranke für dieses Teilproblem nach Zeilen- und Spaltenreduktion ist da nur die Zeile mit Index (6) um 2 reduziert werden muss. Das zweite neue Teilproblem: . ' 1 entsteht aus nach Setzen von 0 auf ebenfalls 12. . ' 1 . ' / . , . Die untere Schranke für dieses Problem ist Abbildung 2.6 zeigt einen möglichen Enumerationsbaum für dieses Beispiel. (Dabei wird nicht das vorgestellte Plausibilitätskriterium genommen.) Hier wird als nächstes die Kante im Branching betrachtet, was Teilprobleme (4) und (5) erzeugt. Danach wird die Kante 1 . fixiert, was die Teilprobleme (6) und (7) begründet. Nach dem Fixieren der Kante wird zum ersten Mal eine globale obere Schranke von 13 gefunden (d.h. eine Tour im Nulldigraphen). Nun können alle Teilprobleme, deren untere Schranke gleich 13 ist, aus 2.1. EXAKTE ALGORITHMEN FÜR SCHWIERIGE OPTIMIERUNGSPROBLEME 47 1 L 1 L=10 Problemnummmer untere Schranke (2,6) (2,6) 2 12 (4,2) (4,2) (2,4) (2,4) 4 12 5 15 9 12 10 15 (3,4) nach 8 (6,2) (3,4) 6 13 7 12 (5,1) nach 8 (3,1) 8 13 3 12 nach 8 11 15 (6,2) 12 18 13 13 (3,1) 14 13 Abbildung 2.6: Möglicher Enumerationsbaum für das Beispiel der Problemliste entfernt werden. In unserem Beispiel wird als nächstes das Teilproblem (3) gewählt, wobei dann über die Kante gebrancht wird, was Teilprobleme (9) und (10) erzeugt. Weil wir mit Teilproblem (10) niemals eine bessere Lösung als 15 erhalten können (untere Schranke), können wir diesen Teilbaum beenden. Wir machen mit Teilproblem (9) weiter und erzeugen die Probleme (11) und (12), die auch sofort beendet werden . können. Es bleibt Teilproblem (7), das jedoch auch mit dem Branchen auf der Kante 1 zu “toten” Teilproblemen führt. Analyse der Laufzeit: Die Laufzeit ist im Worst-Case exponentiell, denn jeder Branching-Schritt verdoppelt die Anzahl der neuen Teilprobleme. Dies ergibt im schlimmsten Fall bei Städten eine Laufzeit von . Allerdings greift das Bounding i.A. recht gut, so dass man deutlich weniger / Städten mit Hilfe Teilprobleme erhält. Doch bereits das Berechnen einer Instanz mit dieses Branch-and-Bound Verfahrens dauert schon zu lange. Das vorgestellte Verfahren ist für TSPs mit bis zu 50 Städten anwendbar. In der Praxis hat sich für die exakte Lösung von TSPs die Kombination von Branchand-Bound Methoden mit linearer Programmierung bewährt. Solche Verfahren, die als Branch-and-Cut Verfahren bezeichnet werden, sind heute in der Lage in relativ kurzer Zeit TSPs mit ca. 500 Städten exakt zu lösen. Die größten nicht-trivialen bisher exakt gelösten TSPs wurden mit Hilfe von Branch-and-Cut Verfahren gelöst und umfassen ca. 15.000 Städte. KAPITEL 2. OPTIMIERUNGSALGORITHMEN 48 Typische Anwendungen von Branch-and-Bound Verfahren liegen in Brettspielen, wie z.B. Schach oder Springer-Probleme. Die dort erfolgreichsten intelligenten Branch-and-Bound Verfahren funktionieren in Kombination mit parallelen Berechnungen. 2.2 Approximative Algorithmen und Gütegarantien In diesem Abschnitt behandeln wir Heuristiken bzw. approximative Algorithmen für Optimierungsprobleme. Eine Heuristik ist ein Verfahren, das eine zulässige Lösung für ein Optimierungsproblem findet, die nicht notwendigerweise optimal ist. Man nennt eine Heuristik auch approximativer Algorithmus, wenn man etwas über die Qualität (Güte) der errechneten Lösungen aussagen kann. Es gibt konstruktive Heuristiken, die eine zulässige Lösung konstruieren, und Verbesserungsheuristiken, die bereits als Input eine zulässige Lösung erhalten und versuchen diese zu verbessern (s. Abschnitt 2.3). 2.2.1 Konstruktive Heuristiken mit Gütegarantien Wir unterscheiden zwischen Algorithmen mit und ohne Gütegarantie. Die “Güte” eines Algorithmus sagt etwas über seine Fähigkeiten aus, optimale Lösungen gut oder schlecht anzunähern. Die formale Definition lautet wie folgt: Sei ein Algorithmus, der für jede Probleminstanz eines Optimierungsproblems eine zulässige Lösung mit positivem Wert liefert. Dann definieren wir als den Wert der Lösung des Algorithmus für Probleminstanz ; sei der optimale Wert für . Für Minimierungsprobleme gilt: Falls / für alle Probleminstanzen und , dann heißt die Zahl heißt Gütegarantie von Algorithmus . ein -approximativer Algorithmus und Für Maximierungsprobleme gilt: Falls / für alle Probleminstanzen und , dann heißt die Zahl heißt Gütegarantie von Algorithmus . ein -approximativer Algorithmus und 2.2. APPROXIMATIVE ALGORITHMEN UND GÜTEGARANTIEN 49 Wir betrachten als Beispiel ein Minimierungsproblem und jeweils die Lösungen einer Heuristik und die optimale Lösung zu verschiedenen Probleminstanzen : . 0 / / und und / 0 / / Die Information, die wir daraus erhalten, ist lediglich, dass die Güte der Heuristik besser als 2 sein kann. Nur, falls nicht für alle möglichen Probleminstanzen gilt, ist ein 2-approximativer Algorithmus. Bemerkung: Es gilt . für Minimierungsprobleme: für Maximierungsprobleme: , . . % ist exakter Algorithmus Im folgenden betrachten wir verschiedene approximative Algorithmen für das Bin-PackingProblem und für das Travelling Salesman Problem. Beispiel 1: Packen von Kisten (Bin-Packing) . Gegeben: Gegenstände #""#" der Größe ; und beliebig viele Kisten der Größe Gesucht: Finde die kleinste Anzahl von Kisten, die alle Gegenstände aufnehmen. . / . . Beispiel: Gegeben sind beliebig viele Kisten der Größe und 37 Gegenstände der folgenden Größe: 7 Größe 6, 7 Größe 10, 3 Größe 16, 10 Größe 34, und 10 Größe 51. Die optimale Lösung sieht folgendermaßen aus: 3 7 51 34 16 51 34 10 6 . . . / / . Die dargestellte Lösung benötigt nur 10 Kisten. Offensichtlich ist diese auch die optimale Lösung, da jede Kiste auch tatsächlich ganz voll gepackt ist. First-Fit-Heuristik (FF): Die Gegenstände werden in beliebiger Reihenfolge behandelt. Jeder Gegenstand wird in die Kiste gelegt, in die er passt. KAPITEL 2. OPTIMIERUNGSALGORITHMEN 50 Unser Beispiel: 1 1 5 10 (7 (2 (2 (1 )6 (5 )10 )10 (3 )16 )34 )51 . Das ergibt insgesamt 17 Kisten. Daraus ergibt sich . . / für diese Probleminstanz . Dies lässt bisher nur den Rückschluss zu, dass die Gütegarantie der FF-Heuristik auf keinen Fall besser als sein kann. Analyse der Gütegarantie: Wir beweisen zunächst eine Güte von 2 für die First-Fit Heuristik. . Theorem: Es gilt: für alle . Beweis: Offensichtlich gilt: Jede FF-Lösung füllt alle bis auf eine der belegten Kisten mindestens bis zur Hälfte. Daraus folgt: . . für alle Man kann sogar noch eine schärfere Güte zeigen. Es gilt (ohne Beweis): . . / Weiterhin kann man zeigen, dass der asymptotisch beste Approximationsfaktor für die FF-Heuristik ist. Für interessierte Studierende ist der Beweis in Johnson, Demers, Ullman, Garey, Graham in SIAM Journal on Computing, vol. 3 no. 4, S. 299-325, 1974, zu finden. Beispiel 2: Symmetrisches TSP Gegeben: ungerichteter vollständiger Graph $ mit Distanzmatrix . Gesucht: Rundtour kleinsten Gewichts, die alle Städte genau einmal besucht. Eine beliebte Heuristik für das symmetrische TSP ist die Spanning Tree Heuristik. Sie basiert auf der Idee einen minimal aufspannenden Baum zu generieren und daraus eine Tour zu basteln. 2.2. APPROXIMATIVE ALGORITHMEN UND GÜTEGARANTIEN 51 Spanning-Tree-Heurisik (ST) (1) Bestimme einen minimalen aufspannenden Baum (2) Verdopple alle Kanten aus Graph von $ . 0 (3) Bestimme eine Eulertour im Graphen 0 . Eine Eulertour ist eine Tour, die jede Kante des Graphen genau einmal enthält. Man kann zeigen, dass ein Graph, in dem jeder Knoten geraden Grad hat, immer eine Eulertour besitzt. Gib dieser Tour eine Orientierung, wähle einen Knoten , markiere , setze 2 & (4) Sind alle Knoten markiert, setze -2 STOP; ' ist die Ergebnis-Tour (5) Laufe von entlang der Orientierung von bis ein unmarkierter Knoten ist. Setze -2 ' , markiere , setze "2 gehe zu (4) erreicht Diskussion der Gütegarantie: . und einen polynomiellen Algorithmus , der für jedes Man kann zeigen: Gibt es ein . symmetrische TSP eine Tour liefert mit , dann ist Theorem: Das -Approximationsproblem des symmetrischen TSP ist NP-vollständig (ohne Beweis). Doch es gibt auch positive Nachrichten. Wenn die gegebene TSP-Instanz gewisse Eigenschaften aufweist, dann ist es doch approximierbar: Ein TSP heißt euklidisch wenn für die Distanzmatrix die Dreiecks-Ungleichung gilt, d.h. für alle Knoten gilt: . Das heißt: es kann nie kürzer sein, von nach nicht direkt, sondern über eine andere Stadt zu laufen. Dies ist meistens in der Praxis erfüllt. (Denkt man beispielsweise an Wegstrecken, so gilt hier meist die Dreiecksgleichung.) Gute Nachrichten bringt das folgende Theorem: Theorem: Für das euklidische TSP und die ST-Heuristik gilt: Beweis: Es gilt: für alle " " Die erste Abschätzung gilt wegen der Dreiecksungleichung, die letzte, da ein Minimum Spanning Tree die billigste Möglichkeit darstellt, in einem Graphen alle Knoten zu verbinden. KAPITEL 2. OPTIMIERUNGSALGORITHMEN 52 W W D A D A K K B B F F (a) MST (b) Verdopplung der Kanten W W D A D A K K B B F F (c) Orientierung (d) Tour mittels ST-Heuristik Abbildung 2.7: ST-Heuristik Abbildung 2.7 zeigt eine Visualisierung der Spanning-Tree Heuristik anhand eines Beispiels (Städte in Nord-Rhein-Westfalen und Hessen: Aachen, Köln, Bonn, Düsseldorf, Frankfurt, Wuppertal). Christophides-Heuristik (CH) Diese 1976 publizierte Heuristik funktioniert ähnlich wie die Spanning-Tree-Heuristik, jedoch wird Schritt (2) durch folgenden Schritt (2’) ersetzt. Dadurch erspart man sich die Verdopplung der Kanten. (2’) Sei die Menge der Knoten in a) Bestimme im von kleinsten Gewichts b) Setze 0 ' mit ungeradem Grad; induzierten Untergraphen von $ ein perfektes Matching ' 2.2. APPROXIMATIVE ALGORITHMEN UND GÜTEGARANTIEN 53 Anmerkungen: Das Matching “geht auf”, denn wir wissen: ! ! ist immer gerade (s. Abschnitt 1.1). Ein perfektes Matching in einem Graphen ist eine Kantenmenge, die jeden Knoten genau einmal enthält. Sie ordnet jedem Knoten einen eindeutigen Partnerknoten zu ( Alle Knoten werden zu Paaren zusammengefasst). Ein solches perfektes Matching kann in polynomieller Zeit gefunden werden. Da eine Eulertour existiert, wenn die Knoten eines Graphen alle geraden Grad haben, kann nun in den weiteren Schritten des Algorithmus wiederum sicher eine Rundreise gefunden werden. Abbildung 2.8 zeigt die Konstruktion an unserem Beispiel. Dabei betrachten wir in Schritt (2’)a) den vollständigen Untergraphen, der von den Knoten induziert wird. Analyse der Gütegarantie: Theorem: Für das euklidische TSP und die Christophides-Heuristik gilt: für alle Beweis: Seien 0 #"#"" 0 die Knoten von mit ungeradem Grad und zwar so numme riert, wie sie in einer optimalen Tour vorkommen, d.h. #"#"#" 0 #"#"#" 0"#"#" ' . ' ' 0 + 1 #"#"#"#' und 0 0 1 "#"#" 0 ' . Es gilt: Sei Die erste Abschätzung gilt wegen der Dreiecks-Ungleichung (Euklidisches TSP). Die zweite ' Abschätzung gilt, weil das Matching kleinsten Gewichts ist. Weiterhin gilt: % . " Bemerkungen: Die Christophides-Heuristik (1976) war lange Zeit die Heuristik mit der besten Gütegarantie. Vor kurzem zeigte Arora (1996), dass das euklidische TSP beliebig nah ap. / proximiert werden kann: die Gütegarantie , kann mit Laufzeit approximiert werden. Eine solche Familie von Approximationsalgorithmen wird als polynomial time approximation scheme (PTAS) bezeichnet (s. Abschnitt 2.2.2). Konstruktionsheuristiken für das symmetrische TSP erreichen in der Praxis meistens eine Güte von ca. 10-15% Abweichung von der optimalen Lösung. Die ChristophidesHeuristik liegt bei ca. 14%. KAPITEL 2. OPTIMIERUNGSALGORITHMEN 54 W W D A D A K K B B F F (b) (a) MST W W D A D A K B K B F (c) Orientierung F (d) Tour Abbildung 2.8: Die CH-Heuristik an einem Beispiel 2.2. APPROXIMATIVE ALGORITHMEN UND GÜTEGARANTIEN 2.2.2 55 -Approximative Algorithmen Definition: Ein Algorithmus ist ein PTAS (polynomial-time approximation scheme) für ein Optimierungsproblem , falls er für jede Instanz in polynomieller Zeit (für fixiert) eine -approximative Lösung berechnet. Definition: Ein PTAS-Algorithmus heißt FPTAS (fully polynomial-time approximation scheme) wenn die Laufzeit zusätzlich zur Eingabegröße der Instanz polynomiell in ist. Beispiel: 0/1-Rucksackproblem Wir betrachten noch einmal den bereits bekannten Algorithmus Dynamische Programmierung 0/1-Knapsack (s. Algorithmus13). Seine Laufzeit um den optimalen Lösungswert 0 zu berechnen ist . Wir wenden den Algorithmus auf das folgende Beispiel an, wobei und jeweils das Gewicht und die Kosten des Gegenstands bezeichnen. . / / . . . / . / . . . / . . / . Die Durchführung des Algorithmus ergibt: / 1 ' mit . Der Algorithmus musste 91 Paare konstruieren. Der Algorithmus müsste deutlich weniger Paare berücksichtigen, wenn wir jeweils die letzte Stelle der -Werte ignoriert hätten (nach Streichung erhalten wir Kosten ). In diesem . / Fall hätte der Algorithmus die Lösung (ca. 5% weg von Optimum) berechnet und 1 ' mit Wert hätte dabei nur noch 36 Paare berücksichtigen müssen. Nun stellt sich natürlich die Frage, wie groß der Fehler maximal werden kann, wenn wir die letzte Stelle “vernachlässigen´´ um eine Beschleunigung der Laufzeit des Algorithmus zu erhalten. Es gilt: . / . . / / Allgemein gilt: Beim Abschneiden der letzten Dezimalstellen entsteht ein Fehler von . / maximal . KAPITEL 2. OPTIMIERUNGSALGORITHMEN 56 Als nächstes betrachten wir die neue Laufzeit. Sei der größte Wert aller ’s, dann gilt: 1 . Daraus ergibt sich eine bisherige Laufzeit von , sowie nach dem 1 Abschneiden der letzten Stellen, eine neue Laufzeit von . Behauptung: Für jede Wahl von ergibt der Algorithmus Dynamische Programmierung . einen -approximativen Algorithmus mit . . Beweis: Sei rung gilt: zeigen: . Laut Definition eines -approximativen Algorithmus bei Maximie. . . Und genau das können wir , was gleichbedeutend ist mit . / . / Theorem: Für jeden Wert existiert ein 0/1-Rucksackproblem, der in Zeit läuft. -approximativer Algorithmus für das . Beweis: Um aus Algorithmus 13 einen ( man lediglich die letzten )-approximativen Algorithmus zu machen, muß Stellen vernachlässigen. Dies ergibt einen FPTAS für das 0/1-Rucksackproblem mit Laufzeit . . Algorithmus 19 faßt den PTAS noch einmal zusammen. Algorithmus 19 FPTAS für das 0/1-Rucksackproblem 1: Sei der Koeffizient und . größte #" " " . 2: für / 3: 4: 5: ' Wende DynProg auf an. 2.3 Verbesserungsheuristiken Verbesserungsheuristiken liegt die Idee zugrunde, eine gegebene zulässige Lösung durch lokale Änderungen zu verbessern. 2.3. VERBESSERUNGSHEURISTIKEN 57 Wir unterscheiden zwischen einfache Austauschverfahren (s. Abschnitt 2.3.1) und lokalen Suchverfahren bzw. Meta-Heuristiken, wie Simulated Annealing (s. Abschnitt 2.3.2) oder evolutionäre Algorithmen (s. Abschnitt 2.3.3). 2.3.1 Einfache Austauschverfahren Idee: Entferne einige Elemente aus der gegenwärtigen Lösung um eine Menge zu erhalten. Nun versuchen wir, alle möglichen zulässigen Lösungen, die enthalten, zu erzeugen. Davon wählen wir die beste. Beispiel: Zweier-Austausch für das Symmetrische TSP (2-OPT): (1) Wähle eine beliebige Anfangstour (2) Setze + '! . 0 + 0 1 #"#"#" + $ ' 1 . . & ' (3) Für alle Kantenpaare + ' aus : Ist : setzte ' ' gehe zu (2) (4) ist das Ergebnis. Beispiel: r-Austausch für das Symmetrische TSP (r-OPT) (1) Wähle eine beliebige Anfangstour (2) Sei 0 + 0 1 #"#"#" + $1 ' die Menge aller -elementigen Teilmenge von (3) Für alle : Setze und konstruiere alle Touren, die enthalten. Gibt es eine unter diesen, die besser als ist, wird diese das aktuelle und gehe zu (2). (4) ist das Ergebnis. Eine Tour, die durch einen -Austausch nicht mehr verbessert werden kann, heißt in diesem Zusammenhang -optimal; -optimale Touren sind sogenannte “lokale Optima”. Bemerkungen: Austauschverfahren werden sehr oft in der Praxis verwendet, um bereits vorhandene Lösungen zu verbessern. Für das TSP kommt 3-Opt meist sehr nah an die optimale Lösung (ca. 3-4%). Jedoch / / erhält man bereits für Städte sehr hohe Rechenzeiten, da der Aufwand bei 1 liegt. KAPITEL 2. OPTIMIERUNGSALGORITHMEN 58 In der Praxis hat sich für das TSP als beste Heuristik die Heuristik von Lin und Kernighan (1973) herausgestellt, die oft bis zu 1-2% nahe an die Optimallösung herankommt. Diese Heuristik ist im Wesentlichen eine Mischung aus 2- und 3-OptVerfahren. 2.3.2 Simulated Annealing Einfachen Austauschheuristiken wie 2-Opt ist es oft nicht möglich, mitunter schlechten lokalen Optima zu entkommen“. Bei aufwändigeren Austauschheuristiken (z.B. -Opt, ” ) steigt aber der Aufwand meist allzu stark mit der Problemgröße an. Simulated Annealing ist eine allgemeine Verbesserungsstrategie (eine Meta-Heuristik), die es bei skalierbarem Zeitaufwand erlaubt, lokalen Optima grundsätzlich auch zu entkommen. Abgeleitet wurde dieses Verfahren von dem physikalischen Prozess, ein Metall in einen Zustand möglichst geringer innerer Energie zu bringen, in dem die Teilchen möglichst strukturiert angeordnet sind. Dabei wird das Metall erhitzt und sehr langsam abgekühlt. Die Teilchen verändern anfangs ihre Positionen und damit ihre Energieniveaus sehr stark, mit sinkender Temperatur werden die Bewegungen von lokal niedrigen Energieniveaus weg aber immer geringer. Der von Kirkpatrick et al. (1983) vorgeschlagene Algorithmus des Simulated Annealing (s. Algorithmus 20). Algorithmus 20 Simulated Annealing / 1: 2: Tinit 3: Ausgangslösung 4: Wiederhole 5: /* leite eine Nachbarlösung ab 6: falls besser als dann 7: 8: ' sonst ) ) 9: falls dann 10: ' 11: 12: . 13: 14: ' 15: Bis Abbruchkriterium erfüllt ist hierbei eine Zufallszahl */ )/ . , / die Bewertung der Lösung . 2.3. VERBESSERUNGSHEURISTIKEN 59 Die Ausgangslösung kann entweder zufällig oder – meist sinnvoller – mit einer Konstruktionsheuristik generiert werden. In jeder Iteration wird dann ausgehend von der aktuellen Lösung eine ähnliche Lösung durch eine kleine zufällige Änderung abgeleitet. Ist besser als , so wird in jedem Fall als neue aktuelle Lösung akzeptiert. Ansonsten, wenn also eineschlechtere Lösung darstellt, wird diese mit einer Wahrscheinlichkeit ) ) akzeptiert. Dadurch ist die Möglichkeit gegeben, lokalen Optima zu entkommen. Die Wahrscheinlichkeit hängt von der Differenz der Bewertungen von und ab – nur geringfügig schlechtere Lösungen werden mit höherer Wahrscheinlichkeit akzeptiert als viel schlechtere. Außerdem spielt die Temperatur eine Rolle, die über die Zeit hinweg kleiner wird: Anfangs werden Änderungen mit größerer Wahrscheinlichkeit erlaubt als später. Als Abbruchkriterium sind unterschiedliche Bedingungen vorstellbar: Ablauf einer bestimmten Zeit; eine gefundene Lösung ist gut genug“; oder keine Verbesse ” rung in den letzten Iterationen. Wie initialisiert und in Abhängigkeit der Zeit vermindert wird, beschreibt das CoolingSchema. Geometrisches Cooling: $ 2 z.B. Sind bzw. verwendet. nicht bekannt, so werden Schranken bzw. Schätzungen hierfür . (z.B. 0,999) Adaptives Cooling: Es wird der Anteil der Verbesserungen an den letzten erzeugten Lösungen gemessen und auf Grund dessen stärker oder schwächer reduziert. Generierung von Nachbarlösungen Die Nachbarschaftsfunktion Bedingungen erfüllen muss: ist eine stochastische Funktion, die die zwei folgenden Erreichbarkeit eines Optimums: Ausgehend von jeder möglichen Lösung muss eine optimale Lösung durch wiederholte Anwendung der Nachbarschaftsfunktion mit einer Wahrscheinlichkeit größer Null erreichbar sein. KAPITEL 2. OPTIMIERUNGSALGORITHMEN 60 Lokalitätsbedingung: Eine Nachbarlösung muss aus ihrer Ausgangslösung durch ein kleine“ Änderung ” abgeleitet werden können. Dieser kleine Unterschied muss im Allgemeinen (d.h. mit Ausnahmen) auch eine kleine Änderung der Bewertung bewirken. Sind diese Voraussetzungen erfüllt, spricht man von hoher Lokalität – eine effiziente Optimierung ist möglich. Haben eine Ausgangslösung und ihre abgeleitete Nachbarlösung im Allgemeinen große Unterschiede in der Bewertung, ist die Lokalität schwach. Die Optimierung ähnelt dann der Suche einer Nadel im Heuhaufen“ bzw. einer reinen ” Zufallssuche und ist nicht effizient. Es folgen Beispiele für sinnvolle Nachbarschaftsfunktionen. Beispiel: TSP Inversion: Ähnlich wie im 2-Opt werden zwei nicht benachbarte Kanten einer aktuellen Tour zufällig ausgewählt und entfernt. Die so verbleibenden zwei Pfade werden mit zwei neuen Kanten zu einer neuen Tour zusammengefügt. Anmerkung: Dass dieser Operator keinerlei Problemwissen wie etwa Kantenkosten ausnutzt, ist einerseits eine Schwäche; andererseits ist das Verfahren aber so auch für schwierigere Varianten des TSPs, wie dem blinden TSP, einsetzbar. Bei dieser Variante sind die Kosten einer Tour nicht einfach die Summe fixer Kantenkosten, sondern im allgemeinen eine nicht näher bekannte oder komplizierte, oft nicht-lineare Funktion. Beispielsweise können die Kosten einer Verbindung davon abhängen, wann diese verwendet wird. (Wenn Sie mit einem PKW einerseits in der Stoßzeit, andererseits bei Nacht durch die Stadt fahren, wissen Sie, was gemeint ist.) Beispiel: Mehrdimensionales Rucksack-Problem Das einfache 0/1-Rucksack-Problem, wie es in Abschnitt 2.1.1 beschrieben wurde, kann mit exakten Verfahren auch für sehr große Instanzen im Allgemeinen gut gelöst werden. Hier ist der Einsatz einer Heuristik wie Simulated Annealing nicht sinnvoll. Es gibt jedoch auch hier deutlich schwierigere Varianten, wie das mehrdimensionale Rucksackproblem: / Gegeben: & Gegenstände mit Werten und Resourcen der. Größe #"#"#" . Jeder . #""#" & #""#" Gegenstand verbraucht von jeder Resource eine bestimmte / Menge . Gesucht: Teilmenge von Gegenständen, beschrieben durch einen Vektor maximalem Gesamtwert $ / . ' $ , mit 2.3. VERBESSERUNGSHEURISTIKEN 61 wobei die vorhandenen Resourcen nicht überschritten werden dürfen: $ . #""#" " Eine sinnvolle Nachbarschaftsfunktion, die nur zulässige Lösungen erzeugt, wäre beispielsweise: Ein zufällig ausgewählter, bisher nicht eingepackter Gegenstand wird zur Lösung hinzugenommen. Überschreitet diese Lösung nun irgendeine Resource, wird so lange ein eingepackter Gegenstand entfernt, bis die Lösung wieder gültig ist. Beispiel: Quadratic Assignment Problem und & Standorte eines Betriebs. Gegeben: & Abteilungen . / , . #"#"" & , seien die Distanzen zwischen den Standorten. / , "#"#" & , sei das Verkehrsaufkommen zwischen den Abteilungen und . . . Gesucht: Eineindeutige Zuordnung *2 "#"#" & ' orten mit minimalem Gesamtverkehrsaufkommen $ $ #" "" & ' von Abteilungen zu Stand- . Vergleichbar mit dem TSP beschreiben auch hier alle Permutationen von "#"#" & ' gültige Lösungen. Die Nachbarschaftsfunktion vom TSP ist hier jedoch nicht geeignet, da sie hier geringe Lokalität besitzt: Lösungen sind nun nicht rotationsinvariant; es kommt auf absolute“ Positionen und nicht auf Kanten an. ” Besser: Vertausche zwei zufällig gewählte Abteilungen. (Diese Nachbarschaftsfunktion hat umgekehrt für das TSP geringere Lokalität.) 2.3.3 Evolutionäre Algorithmen Unter dem Begriff evolutionäre Algorithmen werden eine Reihe von Meta-Heuristiken (genetische Algorithmen, Evolutionsstrategien, evolutionary programming, genetic programming, etc.) zusammengefasst, die Grundprinzipien der natürlichen Evolution in einfacher Weise nachahmen. Konkret sind diese Mechanismen vor allem die Selektion (natürliche Auslese, survival of the fittest“), die Rekombination (Kreuzung) und die ” Mutation (kleine, zufällige Änderungen). KAPITEL 2. OPTIMIERUNGSALGORITHMEN 62 Ein wesentlicher Unterschied zu Simulated Annealing ist, dass nun nicht mehr mit nur einer aktuelle Lösung, sondern einer ganze Menge (= Population) gearbeitet wird. Durch diese größere Vielfalt ist die Suche nach einer optimalen Lösung breiter“ und robuster, d.h. es ” wird nicht so leicht bei einem lokalem Optimum hängengeblieben. Algorithmus 21 zeigt den Basisablauf eines evolutionären Algorithmus. Algorithmus 21 Grundprinzip eines evolutionären Algorithmus 1: = Menge von Ausgangslösungen 2: bewerte( ) 3: wiederhole 4: Qs = Selektion( ) 5: Qr = Rekombination(Qs) 6: P = Mutation(Qr) 7: bewerte( ) 8: bis Abbruchkriterium erfüllt Ausgangslösungen können wiederum entweder zufällig oder mit einfachen Erzeugungsheuristiken generiert werden. Wichtig ist jedoch, dass sich diese Lösungen unterscheiden und so Vielfalt gegeben ist. Selektion Die Selektion kopiert aus der aktuellen Population Lösungen, die in den weiteren Schritten neue Nachkommen produzieren werden. Dabei werden grundsätzlich bessere Lösungen mit höherer Wahrscheinlichkeit gewählt. Schlechtere Lösungen haben aber im Allgemeinen auch Chancen, selektiert zu werden, damit lokalen Optima entkommen werden kann. Fitness-proportionale Selektion: / Sei die Bewertung (= Fitness) jeder Lösung wir davon aus, dass diese Funktion maximiert werden soll. #"#"#" ' und gehen Wir ordnen jeder Lösung eine Selektionswahrscheinlichkeit zu. Die Auswahl erfolgt nun zufällig entsprechend den Wahrscheinlichkeiten . Zu achten ist auf die Verhältnisse zwischen den Selektionswahrscheinlichkeiten der Lösun. #"#"#" ' und * gen in . Sei ! ! . Als Selektionsdruck 2.3. VERBESSERUNGSHEURISTIKEN 63 wird das Verhältnis bezeichnet, d.h. der Faktor, um den die beste Lösung erwartungsgemäß häufiger selektiert wird als eine durchschnittliche. Ist der Selektionsdruck zu niedrig (alle Lösungen haben große und sehr ähnliche Bewertungen), werden gute Lösungen zu wenig bevorzugt und der evolutionäre Algorithmus ähnelt einer Zufallssuche. Ist umgekehrt der Selektionsdruck zu hoch, werden gute Lösungen zu stark bevorzugt, die Vielfalt in der Population verringert sich rasch und der evolutionäre Algorithmus konvergiert zu einem lokalen Optimum. Um den Selektionsdruck steuern zu können, skaliert man die Bewertungsfunktion beispielsweise linear über mit geeigneten Werten und und verwendet dann diese skalierten Fitnesswerte für die Selektion. Skalierung ist auch notwendig, wenn ein / Minimierungsproblem vorliegt ( ist dann negativ) oder sein kann. Tournament Selektion: Eine andere häufig eingesetzte Variante zur Selektion einer Lösung funktioniert wie folgt: (1) Wähle aus der Population Lösungen gleichverteilt zufällig aus (Mehrfachauswahl ist üblicherweise erlaubt). (2) Die beste der Lösungen ist die selektierte. Für die Selektionswahrscheinlichkeit einer Lösung spielen die relativen Unterschiede der Fitnesswerte keine Rolle mehr, sondern nur mehr der fitness-basierte Rang. Eine Skalierung ist deshalb nicht erforderlich. Der Selektionsdruck kann über die Gruppengröße gesteuert werden. Rekombination Die Aufgabe der Rekombination ist, aus zwei selektierten Elternlösungen eine neue Lösung zu generieren. Vergleichbar mit der bereits beim Simulated Annealing beschriebenen Lokalitätsbedingung ist hier wichtig, dass die neue Lösung möglichst ausschließlich aus Merkmalen der Eltern aufgebaut wird. Mutation Die Mutation entspricht der Nachbarschaftsfunktion beim Simulated-Annealing. Sie dient meist dazu, neue oder verlorengegangene Merkmale in die Population hineinzubringen. Häufig werden die Rekombination und Mutation nicht immer, sondern nur mit einer bestimmten Wahrscheinlichkeit ausgeführt, sodass vor allem gute Lösungen manchmal auch KAPITEL 2. OPTIMIERUNGSALGORITHMEN 64 unverändert in die Nachfolgegeneration übernommen werden. Wir sehen uns im Folgenden konkrete Beispiele an. Beispiel: TSP Edge-recombination (ERX): 0 Ziel: Aus zwei Elterntouren und soll eine neue Tour nur aus Kanten der Eltern aufgebaut ist. (1) Beginne bei einem beliebigen Startknoten (2) Sei (3) Ist ; erstellt werden, die möglichst ' die Menge der noch unbesuchten Knoten, die in ' , wähle einen Nachfolgeknoten 0 adjazent zu sind. zufällig aus. (4) Ansonsten wähle einen zufälligen noch nicht besuchten Nachfolgeknoten . (5) Setze ' ' und . (6) Gibt es noch unbesuchte Knoten, gehe zu Schritt 2. (7) Schließe die Tour: ' ' Hier werden neue Kanten, die nicht von den Eltern stammen, nur dann zu hinzugefügt, wenn leer ist. Um die Wahrscheinlichkeit, dass es zu diesen Situationen kommt, möglichst gering zu halten, kann die Auswahl in Schritt 3 wie folgt verbessert werden: (3’) Ermittle für jeden Knoten die Anzahl der gültigen Knoten, die von diesem dann weiter unter Verwendung von Elternkanten angelaufen werden können, d.h. 0 ! ! + ' noch nicht besucht in ' !" Wähle ein für das minimal ist. Mutation: Inversion Beispiel: Quadratic Assignment Problem Rekombination: Um größtmögliche Lokalität zu gewährleisten, wollen wir hier aus zwei Elternlösungen 0 (Abteilungen/Standorte-Zuordnungen) und eine neue Lösung ableiten, in welcher 0 der Standort einer jeden Abteilung immer entweder von oder von übernommen wird. Das Ergebnis dabei muss wiederum eine gültige Permutation sein. Man bezeichnet die folgende Rekombination als cycle crossover: 2.3. VERBESSERUNGSHEURISTIKEN (1) Initialisiere alle (2) leer für . #""#" & . (3) Setze $ (4) Sei . 0 ; ermittle "! & (6) Wenn (5) Setze (7) 65 leer, gehe zu Schritt 3. . #"#"#" : Wenn & leer, dann setze 0 Mutation: Vertauschen der Standorte zweier Abteilungen. Beispiel: Mehrdimensionales Rucksackproblem Initialisierung: Zufallszahl / . . ' & Rekombination: Seien und 0 die Elternlösungen, die neue Lösung. 1-point crossover: . Wähle einen crossover-point * # ""#" & . #"#"#" & & 2 . ' zufällig. 0 für sonst oder uniform crossover: . #""#" Mutation: Wähle ein & setze per Zufallsentscheidung 2 . * #""#" & ' zufällig und setze . oder 0 " . Problem: Ungültige Lösungen, die Resourcen überschreiten! Lösungsmöglichkeiten: Repair“-Algorithmus auf jede generierte (ungültige) Lösung anwenden. Hier z.B. so lan” ge einen zufälligen Gegenstand entfernen, bis die Lösung alle Resourcenbeschränkungen erfüllt. KAPITEL 2. OPTIMIERUNGSALGORITHMEN 66 “Bestrafung” zur Bewertung hinzufügen (Lagrange’scher Ansatz) $ / $ Adaptive Einstellung der Lagrange-Faktoren . / Anfang: Alle Generationen: $ Für alle , für die " 0 $ / 0 0 ""#" : "" : setze ( / ). Abschließend sei zu evolutionären Algorithmen noch angemerkt, dass sie mit anderen Heuristiken und lokalen Verbesserungstechniken sehr effektiv kombiniert werden können. So ist es möglich . . . Ausgangslösungen mit anderen Heuristiken zu erzeugen. . . . in der Rekombination und Mutation Heuristiken einzusetzen (z.B. können beim TSP kostengünstige Kanten mit höherer Wahrscheinlichkeit verwendet werden). . . . alle (oder einige) erzeugte Lösungen mit einem anderen Verfahren (z.B. 2-Opt) noch lokal weiter zu verbessern. Auch können die Vorteile paralleler Hardware gut genutzt werden – die Evolution ist ja quasi von Natur aus“ parallel. ” Weiterführende Literatur W.J. Cook, W.H. Cunningham, W.R. Pulleyblank und A. Schrijver: “Combinatorial Optimization”, John Wiley & Sons, Chichester, 1998 C. H. Papadimitriou und K. Steiglitz: “Combinatorial Optimization: Algorithms and Complexity”, Prentice-Hall, Inc., Englewood Cliffs, New Jersey, 1982 E. Aarts und J.K. Lenstra: “Local Search in Combinatorial Optimization”, John Wiley & Sons, Chichester, 1997 Z. Michalewicz: “Genetic Algorithms + Data Structures = Evolution Programs”, Springer, 1996 Kapitel 3 Geometrische Algorithmen In der Praxis kommt es immer wieder vor, dass geometrische Daten gegeben sind, d.h. Daten, die gewisse Koordinaten im -dimensionalen Raum besitzen. Denken Sie z.B. an Städte auf einer Landkarte oder auch an Komponenten im Chip-Design. Geometrische Algorithmen, auch “Algorithmische Geometrie” oder “Computational Geometry” genannt, nutzen die geometrischen Eigenschaften der Daten aus, um effiziente Algorithmen zu erhalten. Anwendungsgebiete liegen in der Bildverarbeitung, Computergraphik, Geographie, CAD, oder auch im Chip-Layout. Ein grundlegendes Prinzip im Bereich der geometrischen Algorithmen ist das Scan-Line Prinzip, das wir im folgenden Abschnitt betrachten. Ein weiteres wichtiges Problemfeld ist die Bereichssuche, die im darauffolgenden Abschnitt behandelt wird. 3.1 Scan-Line Prinzip Das Scan-Line Prinzip ist ein wichtiges Paradigma im Bereich der geometrischen Algorithmen. Es ist immer dann anwendbar, wenn eine Menge von Objekten auf einer 2dimensionalen Fläche gegeben ist. Die grundlegende Idee ist, eine vertikale Linie von links nach rechts über die Objektmenge zu führen, um dadurch das zweidimensionale (statische) Problem in eine dynamische Folge von eindimensionalen Problemen umzuwandeln. Die Scan-Line teilt zu jeder Zeit die Objekte ein in tote Objekte, die vollständig links von aktive Objekte, die gegenwärtig von inaktive Objekte, die künftig von liegen geschnitten werden geschnitten werden Wir betrachten im folgenden das Schnittproblem für Liniensegmente, in dem es darum geht, einen oder alle Schnittpunkte von Liniensegmenten zu bestimmen. Zunächst diskutieren wir den einfacheren Fall der iso-orientierten Liniensegmente. 67 KAPITEL 3. GEOMETRISCHE ALGORITHMEN 68 B C A D E A A AAADD 0 EDDE EE (A,B) (A,C) (C,D) Abbildung 3.1: Eine Probleminstanz für den Schnitt von iso-orientierten Liniensegmenten 3.1.1 Das Schnittproblem für Iso-orientierte Liniensegmente Bei diesem Problem geht es darum, alle Schnittpunkte einer Menge von senkrechten und horizontalen Geradensegmenten zu bestimmen. Gegeben: Gesucht: Menge von insgesamt & vertikalen und horizontalen Liniensegmenten in der Ebene #""#" #$ ' alle Paare sich schneidender Segmente Ein Beispiel für eine solche Problemstellung finden Sie in Abbildung 3.1 (die Buchstaben unter der Zeichnung werden später erklärt). Eine naive Methode zur Lösung des Problems wäre es, alle Paare von Segmenten auf Schnitt zu testen. Dies ergäbe eine Gesamtlaufzeit 0 3(4& . Tatsächlich gibt es Probleminstanzen, für die es keine schnellere Lösung gibt, da es 0 3(4& Schnittpunkte gibt. Wenn allerdings die Anzahl der Schnittpunkte klein ist und nicht quadratisch in & wächst, dann liefert der Scan-Line-Algorithmus eine bessere Laufzeit. Wir machen eine vereinfachende Annahme: Alle Anfangs- und Endpunkte horizontaler Segmente und alle vertikalen Segmente haben paarweise verschiedene -Koordinaten. Algorithmus 22 verwendet das Scan-Line Prinzip für die Lösung unseres Problems. Er nutzt dabei folgende Beobachtungen aus: Trifft man mit der Scan-Line auf ein vertikales Segment , so kann punkte mit den gerade aktiven horizontalen Segmenten haben. nur Schnitt- 3.1. SCAN-LINE PRINZIP 69 Man muss die Scan-Line nicht kontinuierlich über die Fläche führen. Es genügt, sie jeweils an Haltepunkten zu beobachten. Haltepunkte sind die -Koordinaten der Anfangs- und Endpunkte horizontaler Segmente und die -Koordinaten vertikaler Segmente. Algorithmus 22 Scan-Line für Schnitt iso-orientierter Liniensegmente 1: : Menge der Haltepunkte in aufsteigender -Reihenfolge; 2: ; /* Menge der aktiven Segmente, aufsteigend nach sortiert */ 3: solange : nächster Haltepunkt von ; 4: 5: falls ( ist linker Endpunkt eines horizontalen Segments ) dann 6: Füge in ein; ' sonst 7: 8: falls ( ist rechter Endpunkt eines horizontalen Segments ) dann 9: Entferne aus ; 10: ' sonst 11: /* ist -Wert eines vertikalen Segments mit unterem Endpunkt ( ) und oberem Endpunkt ( ) */ 12: Bestimme alle horizontalen Segmente aus , deren -Koordinaten im Be) reich + liegen und gib als Paar sich schneidender Segmente aus; 13: 14: 15: ' ' ' Die Spalten von Buchstaben unter den Haltepunkten in Abbildung 3.1 geben jeweils den Inhalt der Menge an. Darunter finden Sie die von dem Algorithmus berechneten Paare von sich kreuzenden Segmenten mit Pfeilen zu den Haltepunkten, bei denen sie ausgegeben werden. offen. Allerdings lässt der Algorithmus noch die Realisierung der geordneten Menge Hierfür wird eine Datenstruktur benötigt, die die folgenden Operationen effizient ausführen kann. Dies sind: Einfügen eines neuen Elements Entfernen eines Elements Bestimmen aller Elemente, die in einen gegebenen Bereich frage) ) + fallen (Bereichsab- Eine mögliche Lösung bieten z.B. balancierte Binärbäume an (z.B. AVL-Bäume). Damit können die Einfüge-Operationen und die Entfernungs-Operationen in Zeit ! ! durchgeführt werden. Die Bereichsabfrage in Zeile 12 von Algorithmus 22 könnte dann folgendermaßen realisiert werden: KAPITEL 3. GEOMETRISCHE ALGORITHMEN 70 1. Suche in den Knoten mit kleinstem Schlüssel falls sein Schlüssel . und gib als Segment aus, 2. Laufe ab Knoten in Inorder-Reihenfolge durch, bis Knoten mit Schlüssel erreicht wird, gib alle dabei durchlaufenen Segmente außer aus. Algorithmus 23 zeigt die Realisierung des ersten Schritts. Die Bezeichnungen für die balancierten Binärbaüme wurden vom SS01 Skript Algorithmen und Datenstrukturen 1 übernommen. Die Realisierung des zweiten Schritts wird hier nicht ausgeführt, weil es sich um eine schlichte Inorder-Durchmusterung handelt. Algorithmus 23 Suche Knoten mit kleinstem Schlüssel 1: root; NULL; 2: solange (( NULL) (p.key )) ; 3: 4: falls ( p.key) dann p.leftson; 5: ' sonst 6: p.rightson; 7: 8: 9: 10: 11: 12: ' ' falls ( NULL) dann Return /* Schlüssel gefunden */ falls (root NULL) dann Return NULL /* leerer Baum */ falls ( NULL) dann Return NULL bzw. root abhängig von root.key besteht aus einem Knoten */ 13: falls ( " ) dann Return sonst Return Successor(q) /* Baum Analyse der Laufzeit Wir betrachten zunächst die Laufzeit der Bereichssuche: Ist die Anzahl der Elemente im ) Bereich + , so ist die Laufzeit für eine Bereichsabfrage & ( & für den ersten Schritt der Bereichssuche und für den zweiten). Somit ergibt sich für den ScanLine Algorithmus eine Gesamtlaufzeit von 4& & , wobei die Anzahl der sich schneidenden Segmente ist. Der Platzverbrauch ist hingegen nur linear: 5& . Bemerkungen: 1. Das Scan-Line Verfahren ist dem naiven Verfahren überlegen, wenn quadratisch mit der Anzahl der Segmente wächst. schwächer als 2. Man kann zeigen, dass im schlechtesten Fall 5& & Schritte erforderlich sind, um das RSS-Problem zu lösen. Somit ist das hier beschriebene Verfahren worst case optimal. 3.1. SCAN-LINE PRINZIP 71 y x Abbildung 3.2: Eine Menge von Geradensegmenten in allgemeiner Lage 3.1.2 Schnitt von Allgemeinen Liniensegmenten Bei diesem Problem ist eine Menge von Liniensegmenten in allgemeiner Lage gegeben und wieder sollen die Paare sich schneidender Segmente berechnet werden. Im Gegensatz zum vorigen Problem sind nun nicht nur waagerechte und senkrechte Segmente erlaubt Gegeben: Gesucht: Menge von & Liniensegmenten #"#"" $ in der Ebene Menge echter Schnittpunkte (echte Schnittpunkte sind Kreuzungen von Segmenten, die keine Endpunkte sind) Um den Algorithmus nicht durch viele Fallunterscheidungen zu komplizieren, machen wir auch hier wieder vereinfachende Annahmen, die man auch zusammengefaßt Allgemeine Lage der Segmente nennt: 1. Kein Segment ist senkrecht. 2. Der Schnitt zweier Segmente ist stets leer oder genau ein Punkt. 3. Es schneiden sich nie mehr als zwei Segmente in einem Punkt. 4. Alle Endpunkte und Schnittpunkte haben paarweise verschiedene -Koordinaten. Abbildung 3.2 zeigt eine Menge von Geradensegmenten in allgemeiner Lage. Wir benutzen auch hier wieder einen Sweepline-Algorithmus mit einer senkrechten Sweepline, die sich von links nach rechts über die Fläche bewegt. Beobachtung 1: Wenn sich das Segment mit dem Segment bei -Koordinate schneidet, dann sind und bei -Koordinate Nachbarn auf der Sweepline für ein hinreichend kleines aber positives . Daraus ergibt sich, dass es genügt, auf der Sweepline benachbarte Segmente auf Schnitt zu testen. KAPITEL 3. GEOMETRISCHE ALGORITHMEN 72 Beobachtung 2: Am Schnittpunkt zweier Segmente vertauscht sich die Reihenfolge der betroffenen Segmente auf der Sweepline. Daraus ergeben sich folgende Konsequenzen: 1. Auch Schnittpunkte müssen Ereignisse sein. 2. Schnittpunkte müssen während des Sweeps in die Ereignisstruktur eingefügt werden. Wir definieren ein Ereignis als einen Haltepunkt der Sweepline. Es gibt 3 Typen von Ereignissen: 1. Anfang eines Segments 2. Ende eines Segments 3. Schnitt zweier Segmente Zusammen mit den jeweiligen -Koordinaten merken wir uns auch die IDs der betroffenen Segmente. Wir führen eine Ereignisdatenstruktur (ES) mit den folgenden Operationen ein: 1. “NächstesEreignis()”: liefert das Ereignis mit der kleinsten -Koordinate aus ES und löscht es. 2. “FügeEin(Ereignis)”: fügt ein neues Ereignis in ES ein. Diese Datenstruktur kann durch einen balancierten Baum oder durch einen Heap implementiert werden. Bei beiden Implementierungen ist die Laufzeit der Operationen logarithmisch in der Größe von ES. Weiterhin benötigen wir eine Sweep-Status-Struktur (SSS). Diese speichert Segmente geordnet nach der -Koordinate ihres momentanen Schnittpunkts mit der Sweepline. Die Datenstruktur stellt folgende Operationen zur Verfügung: 1. “FügeEin(Segment)”: Fügt ein Segment in SSS ein 2. “Entferne(Segment)”: Entfernt ein Segment aus SSS 3. “Vorg(Segment)”: Liefert den Vorgänger eines Segments auf der Sweepline 4. “Nachf(Segment)”: Liefert den Nachfolger eines Segments auf der Sweepline 5. “Vertausche(Segment1, Segment2)”: Vertauscht die Reihenfolge zweier Segmente auf der Sweepline Diese Struktur kann durch einen blattorientierten balancierten binären Suchbaum mit Verkettung der Blätter (z.B. B-Baum) realisiert werden. Algorithmus 24 zeigt unseren Sweep-Line Algorithmus zur Lösung des Problems. 3.1. SCAN-LINE PRINZIP Algorithmus 24 Algorithmus für das Segment-Schnitt Problem in allgemeiner Lage Initialisiere ES und SSS; Sortiere die 2& Endpunkte aufsteigend nach -Koordinate; Speichere resultierende Ereignisse in ; solange ES nicht leer ES.NächstesEreignis(); falls ist Segment Anfang dann SSS.FügeEin(E.Segment); VS SSS.Vorg(E.Segment); TesteSchnittErzeugeEreignis(E,VS); falls dann ES.FügeEin(E’); ' NS SSS.Nachf(E.Segment); TesteSchnittErzeugeEreignis(E,NS); ' falls ist Segment Ende dann VS SSS.Vorg(E.Segment); NS SSS.Nachf(E.Segment); SSS.Entferne(E.Segment); TesteSchnittErzeugeEreignis(VS,NS) falls dann ES.FügeEin(E’); ' ' falls ist Schnittpunkt dann Gib E.SegmentO und E.SegmentU aus; SSS.Vertausche(E.SegmentO,E.SegmentU); VS SSS.Vorg(E.SegmentU); TesteSchnittErzeugeEreignis(E.SegmentU,VS); falls dann ES.FügeEin(E’); ' NS SSS.Nachf(E.SegmentO); TesteSchnittErzeugeEreignis(E.SegmentO,VS); falls dann ES.FügeEin(E’); ' ' ' 73 KAPITEL 3. GEOMETRISCHE ALGORITHMEN 74 Die Funktion TesteSchnittErzeugeEreignis(Segment1,Segment2) berechnet die -Koordinate des Schnittpunkts zweier Segmente und erzeugt ein entsprechendes Ereignis, falls sich die Segmente schneiden. Ansonsten wird ein leeres Ereignis erzeugt. Ein Schnitt-Ereignis speichert die IDs der beiden betroffenen Segmente als SegmentU (Segment das vor dem Schnitt unterhalb des zweiten war) und SegmentO (Segment das vor dem Schnitt oben war). Analyse der Laufzeit implementiert werden, wobei Der Schnitt von Liniensegmenten kann in Zeit 5& & & die Anzahl der Segmente und die Anzahl der Schnittpunkte bezeichnet. Der benötigte 0 Speicherplatz ist 5& . Das Sortieren der 2& Endpunkte nach ihrer -Koordinate erfolgt in Zeit 5& & . Insge 0 samt gibt es 2& verschiedene Ereignisse, von denen sich nie mehr als 4& Ereignisse 0 in ES befinden (weil es maximal 4& Schnittpunkte gibt). Der Zugriff auf ES ist also in 0 Zeit & % & möglich. In SSS befinden sich nie mehr als & Elemente, weshalb ein Zugriff auf diese Datenstruktur auch nur Zeit & benötigt. Die Schleife wird maximal 2& mal durchlaufen und bei jedem Durchlauf werden nie mehr als fünf Operationen auf ES und SSS durchgeführt. Der Speicherplatzbedarf ist im schlimmsten Fall quadratisch, weil die Kreuzungen in der Ereignisstruktur abgelegt werden und es quadratisch viele Kreuzungen geben kann. 3.2 Mehrdimensionale Bereichssuche Bei der mehrdimensionalen Bereichssuche geht es darum, effizient die Menge all der Punkte in einem mehrdimensionalen Raum zu finden, die innerhalb eines Suchbereichs liegen. Die Menge aller Punkte ist dabei schon im Vorhinein bekannt und es geht darum, mehrere Bereichsanfragen möglichst schnell zu beantworten. Die zentrale Idee ist dabei, die Menge aller Punkte in einer Datenstruktur abzulegen, die das Ausführen einer Bereichssabfrage beschleunigt. Dies zahlt sich aus, wenn auf der gleichen Punktmenge mehrere Anfragen gestellt werden mit unterschiedlichen Suchbereichen. Die Problemstellung ist die folgende: Gegeben: Gesucht: Beispiele: . -dimensionaler rechteckiger Bereich Raum. Finde alle Punkte, die in liegen. und & Punkte im -dimensionalen : Aufzählen aller Elemente in einer Folge mit Schlüssel zwischen und 3.2. MEHRDIMENSIONALE BEREICHSSUCHE Wien 75 : Aufzählen aller Städte im Quadrat mit 100 km Seitenlänge und Mittelpunkt : Datenbankabfragen, Aufzählen aller Personen, die – Informatik studiert haben – zwischen 25 und 39 Jahre alt sind – mit Einkommen zwischen 30.000 EUR und 50.000 EUR – kein Handy besitzen Satz: Eindimensionale Bereichssuche kann mit 4& & Schritten für die Vorverarbeitung und & Schritten für die Bereichssuche ausgeführt werde, wobei die Anzahl der Punkte ist, die tatsächlich im Bereich liegen. Beweis: Man legt die Folge in einem balancierten Suchbaum ab, z.B. in einem AVL-Baum. Wir wollen für höhere Dimensionen nun ein ähnlich gutes Verfahren entwickeln. Eine naive Lösung des Problems für höhere Dimensionen ist das Sequentielle Durchlaufen aller Punkte wobei man jeweils testet, ob der gerade betrachtete Punkt im Bereich liegt. Dies bedeutet einen Aufwand von 4& Schritten für jede Bereichsabfrage. Liegt bei jeder Anfrage mindestens ein konstanter Anteil aller Punkte im Suchbereich , so ist dieses naive Verfahren asymptotisch optimal. Liefert aber jede Suchanfrage nur eine kleine konstante Anzahl von Punkten als Ergebnis, so ist das Verfahren sehr ineffizient. Wir wollen einen output sensitiven Algorithmus, bei dem die Laufzeit von der Anzahl der im Suchbereich liegenden Punkte und somit von der Größe der Ausgabe abhängig ist. Man könnte nun ein regelmäßiges Gitter über die Menge aller Punkte legen, um die Anfragen schneller zu bearbeiten. Dies liefert gute Ergebnisse, wenn die Punkte gleichmäßigverteilt sind. Sind die Punkte aber in bestimmten Bereichen des Raumes konzentriert, so gibt es viele leere Gitterzellen während andere Gitterzellen sehr viele Punkte enthalten. Das folgende Verfahren umgeht dieses Problem. 3.2.1 Zweidimensionale Bäume Die Idee ist, den zweidimensionalen Raum aufzuteilen, ähnlich wie wir den eindimensionalen Raum mittels binäre Suchbäume aufgeteilt haben. Anders als bei den binären Suchbäumen verwenden wir hier alternierend die - und -Koordinaten als Schlüssel. Wir konstruieren also einen binären Baum, der eine Aufteilung der Ebene repräsentiert. Dabei enthält jeder Knoten des Baumes einen der & Punkte. Verwenden wir an einem Knoten der den Punkt repräsentiert die -Koordinate zur Bestimmung des linken und rechten Teilbaums, so enthält der Linke Teilbaum von die Punkte die unterhalb von KAPITEL 3. GEOMETRISCHE ALGORITHMEN 76 liegen und der rechte Teilbaum die Knoten, die oberhalb von liegen. Verwenden wir aber die -Koordinate, so enthält der linke Teilbaum von die Knoten links von und der rechte Teilbaum die Knoten rechts von . Sind die Teilbäume von durch die -Koordinate bestimmt worden, so werden die Teilbäume der Kinder von durch die -Koordinate bestimmt. Sind aber die Teilbäume von durch die -Koordinate bestimmt worden, so werden die Teilbäume der Kinder von durch die -Koordinate bestimmt. Es liegt also jeder Punkt der gegebenen Punktmenge auf einem vertikalen oder horizontalen Segment, welches die Zerlegung definiert die an dem entsprechenden Knoten im binären Baum vorgenommen wurde. Abbildung 3.3 zeigt ein Beispiel für diese Aufteilung der Ebene. A D C B B A E G E D G F C F Abbildung 3.3: Ein 2-D-Suchbaum und die dadurch definierte Zerlegung der Ebene Die Suche in einem 2-D-Baum funktioniert ganz ähnlich wie die Suche in einem binären Suchbaum. Allerdings muß man bei jedem durchlaufenen Knoten darauf achten, ob die Teilbäume des Knotens durch die - oder -Koordinate bestimmt sind. Außerdem kann es sein, dass die Suche nach den Punkten in einem Bereich in beiden Teilbäumen eines Knotens durchgeführt werden muss. Algorithmus 25 stellt die Bereichssuche in einem 2-D-Baum als Funktion dar. Der Aufruf erfolgt in der Form + , wobei die Wurzel des Baums ist und wir davon ausgehen, dass die Kinder der Wurzel durch die -Koordinate bestimmt sind. Es stellt sich nun noch die Frage wie man einen solchen Baum effizient aufbauen kann, damit eine schnelle Abarbeitung der Suchanfragen möglich ist. Es muss sich um einen balancierten Baum handeln, damit für kleine Ergebnismengen logarithmische Laufzeit in & erreicht werden kann. Wir erreichen dies, indem wir stets am Median Punkt der aktuellen Folge aufteilen. Dadurch wird garantiert, dass in den neu entstehenden Teilen jeweils gleich viele 3.2. MEHRDIMENSIONALE BEREICHSSUCHE 77 Algorithmus 25 Bereichssuche(Knoten p, Richtung d,Bereich D) falls NULL dann falls vert dann (" (" 20 ; coord " ; rNeu horiz; ' sonst (" (" 0 ; coord " ; rNeu vert; ' falls dann Ausgabe von ; falls coord dann Bereichssuche(p.left,rNeu,D); falls coord dann Bereichssuche(p.right,rNeu,D); ' Punkte enthalten sind (wobei es zu einem maximalen Unterschied von einem Punkt kommen kann). Wir gehen dazu wie folgt vor: 1. Wir sortieren alle Punkte sowohl nach der - als auch nach der -Koordinate. Dadurch erhalten wir zwei sortierte Folgen und . 2. Wir teilen die Folge am Median Element und machen dieses zur Wurzel des Baums. Es entstehen zwei neue Folgen und 0 . Nun teilen wir auch die Folge in zwei Teile, so dass Folge die selben Punkte wie enthält und 0 die selben Punkte wie 0 (natürlich jeweils nach -Koordinate sortiert). . 3. Wir führen nun dasselbe rekursiv mit den Folgen und für ' durch, wobei wir nun aber die -Folgen am Median Element aufteilen. Wir teilen immer weiter abwechselnd am Median der - und -Folgen auf, bis die Folgen nur noch aus einem Punkt bestehen. Diese Punkte sind dann die Blätter des Baums. Am Beispiel der Punkte in der Punktmenge von Abbildung 3.3 geht der Aufbau des Baums wie folgt von Statten. Sortiert man erst alle Punkte in den Folgen und , so erhält man: : : Das Median-Element der Folgen so aus: : G : F G D F G A C C E B A F B E D -Folge ist fett gedruckt. Nach der ersten Unterteilung sehen die C F G C 0 : D A 0 : A B B D KAPITEL 3. GEOMETRISCHE ALGORITHMEN 78 Im nächsten Schritt wir dann an den fett gedruckten Median Elementen der -Folgen aufgeteilt. Es ist klar, dass dieser Aufteilungsschritt in Zeit durchgeführt werden kann, wenn die Länge der Folge vor der Teilung ist. Algorithmus 26 zeigt eine Funktion, die einen balancierten 2-D-Baum in Zeit 5& & aufbaut. Es wird davon ausgegangen, dass das Feld die Menge aller Punkte sortiert nach -Koordinate enthält und Feld die Menge aller Punkte sortiert nach -Koordinate. Algorithmus 26 2D-Aufbau(l,r,knoten,Xdir) Output: Konstruktion eines balancierten 2-D-Baums 1: falls dann 2: 0 ; 3: falls Xdir == true dann ) +; 4: punkt ' sonst 5: ) +; 6: punkt 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: ' knoten.eintrag punkt; falls Xdir false dann Partitioniere Feld +" ; ' sonst Partitioniere Feld +" ; ' lsohn neuer linker Sohn( & #& ); rsohn neuer rechter Sohn( & #& ); . 2D-Aufbau . lsohn !Xdir ; + rsohn !Xdir ; 2D-Aufbau ' Da das Aufteilen einer Folge in linearer Zeit möglich ist. ergibt sich für die Laufzeit des Algorithmus die folgende Rekursionsformel: 4& & & Diese Formel hatten wir schon bei der Funktion Mergesort gesehen und wie schon dort erläutert ist die Lösung der Gleichung 3(4& & . Der Aufwand für eine Bereichsabfrage ist & , wobei die Anzahl der Punkte im Bereich ist. Der Beweis wird hier wegen seiner Komplexität nicht ausgeführt. Es ist aber deutlich, dass der Aufwand der Bereichsabfrage für kleine deutlich geringer ist als bei der naiven Methode, bei der einfach alle Punkte geprüft werden. 3.2. MEHRDIMENSIONALE BEREICHSSUCHE 79 3.2.2 Höhere Dimensionen Man kann die obigen Algorithmen recht einfach an höhere Dimensionen anpassen, indem man reihum nach den Dimensionen aufteilt (beim Baumaufbau) b.z.w. entscheidet, welchen Teilbaum man betrachtet (bei der Bereichssuche). Man kann dann zeigen, dass der Aufbau des entsprechenden Baumes Zeit 3. & & benötigt und die Bereichssuche Zeit ) & , wobei die Dimension ist. Weiterführende Literatur Die beiden hier aufgeführten Problemfelder (Scan-Line-Algorithmen und mehrdimensionale Bereichssuche) sind besonders gut beschrieben in dem Buch “Algorithms” von Robert Sedgewick (s. Literaturliste (2)). 80 KAPITEL 3. GEOMETRISCHE ALGORITHMEN Kapitel 4 Suchen in Texten Dieser Abschnitt beschäftigt sich mit dem Suchen einer gegebenen Zeichenfolge (Muster bzw. “pattern”) in einem Text . Dieses Problem (Pattern Matching, String Matching) tritt häufig in der Textverarbeitung auf, etwa in Form der “Suchfunktion” in Texteditoren. Auch in der Molekularbiologie wird häufig nach Mustern in einer gegebenen DNS-Sequenz ' gesucht. Meist ist dabei die Länge des Musters relativ klein im Vergleich zur Länge des zu durchsuchenden Textes. Wir betrachten in diesem Abschnitt das folgende Problem: Gegeben ist eine Text ' Zeichenfolge der Länge und eine Muster-Zeichenfolge (“Pattern”) der Länge jeweils aus einem endlichen Alphabet . . Gesucht sind alle Vorkommen von innerhalb . ' von , d.h. jeweils die Anfangs-Indizes ( . ). Wir nehmen dabei an, dass ) ). ' der Text und das Muster in jeweils einem Feld von "#"#" + bzw. "#"#" + . Formal ist das Problem folgendermaßen definiert. Pattern Matching Problem "#"#" von Zeichen aus einem endlichen Alphabet Gegeben: eine Zeichenkette (Text) . ' und eine Zeichenkette (Muster/Pattern) "#"#" , ebenfalls mit $ . ""#" Gesucht: ein oder alle Vorkommen. von ""#" in , d.h. jene Indizes mit . . ' ' , so dass #"#"" 2 ) . Wir setzen voraus: ' . In der Regel gilt: Beispiel: Oxford English Dictionary mit / / / / / " Definitionen Dynamischer Fall: Text und Pattern ändern sich häufig (Wäre der Text immer der gleiche, würde es sinnvoll sein, eine Index-Datenstruktur aufzubauen.) 81 KAPITEL 4. SUCHEN IN TEXTEN 82 4.1 Naives Verfahren Die offensichtliche Methode besteht darin, das Muster der Reihe nach (von links nach rechts) ' an jeden Teilstring des Textes mit Länge anzulegen und dann zu prüfen, ob tatsächlich Übereinstimmung an der momentanen Stelle vorliegt. Algorithmus 27 Naive-Search ). ). ' Input: Text " " + und Muster "" + Output: Ausgabe aller Vorkommen von in / /* Position vor der Anlegestelle von */ 1: ' 2: solange. 3: ) ) ' . + + 4: solange . 5: 6: 7: 8: 9: 10: 11: ' ' . falls dann . Ausgabe: “ an Stelle im Text gefunden”; ' . ' Das Programm verwendet einen Textzeiger , der jeweils auf den Index vor der Anlegestelle des Musters zeigt. Stimmen die Zeichen an der Stelle überein (“match”) wird hochgesetzt und das nächste Zeichen wird verglichen. Andernfalls (“mismatch”) wird das ' Muster um eine Stelle weiter rechts angesetzt. Wurde -Mal hintereinander hochgezählt, wurde das Muster gefunden. ' . Analyse der Laufzeit: Das Muster wird genau Mal an den Text angelegt. ' Im schlimmsten Fall müssen bei jedem solchen Anlegen 3( Vergleiche durchgeführt werden (was in der Praxis nur selten vorkommt). Insgesamt ergibt dies eine Worst-Case ' Laufzeit von . Das Problem des naiven Algorithmus ist es, dass die Information, die während der Vergleiche erhalten wird, nicht benutzt wird, sondern vergessen wird. Man nennt dies auch einen Algorithmus “ohne Gedächtnis”. Die folgenden beiden Abschnitte behandeln Algorithmen mit Gedächtnis. 4.2. VERFAHREN VON KNUTH-MORRIS-PRATT 83 4.2 Verfahren von Knuth-Morris-Pratt Die Idee hierbei ist es, die Informationen, die wir jeweils bis zu einem “Mismatch” über den Text an den jeweiligen Stellen erhalten haben, auszunutzen. Dabei wird versucht, das Muster nach jedem Mismatch um mehr als nur eine Position nach rechts zu rücken. Ziel ist es, zu verhindern, dass die Vergleiche noch einmal über bereits bekannte Zeichen geführt werden müssen. Beispiel: Position: 123456789... Text 1: NANANA DAS IST Muster (erstes Anlegen): ANANAS Muster (zweites Anlegen): ANANAS Beim ersten Anlegen des Musters “ANANAS” an den Text “NANANA DAS IST” erfolgt sogleich ein Mismatch. Jedoch beim zweiten Anlegen (an Position 2) erfolgen fünf Übereinstimmungen bevor der erste Mismatch an Position 7 auftaucht. Wegen der Übereinstimmungen kennen wir bereits die fünf Zeichen des Textes vor der aktuellen Position, d.h. diese brauchen wir nicht noch einmal anzuschauen. Doch wie weit müssen wir das Muster nach rechts schieben? Es ist folgendes zu beachten: (1) Nach der Verschiebung muss garantiert sein, dass links von der aktuellen Position im Muster nur Zeichen stehen, die alle mit dem jeweiligen Zeichen im Text übereinstimmen. (2) Dabei müssen wir beachten, dass wir das Muster keinesfalls zu weit nach rechts schieben dürfen. Die erste Eigenschaft (1) ist in unserem Beispiel bei Position 4 im Text sowie bei Position 6 im Text erfüllt. Würden wir das Muster auf Anlegeposition 6 legen, dann könnte uns eventuell ein Vorkommen des Musters im Text (Pattern Matching) verloren gehen. Es wäre also in diesem Fall richtig, das Muster um 2 Positionen nach rechts zu schieben. Wir nehmen an: Die letzten gelesenen Zeichen im Text stimmen mit den ersten Zeichen des Musters ). ) + bis %+ . überein. Es sei das Teilmuster von Das gerade gelesene -te Zeichen im Text ist verschieden vom Muster. . -ten Zeichen im Wir bestimmen vom Anfangsstück des Musters mit Länge (dem gematchten Teil des Musters) ein Endstück maximaler Länge , das ebenfalls Anfangsstück des Musters ist. . Das Muster kann dann um Stellen nach rechts geschoben werden. Position ist dann im Muster die erste Stelle, die man mit dem -ten Zeichen im Text als nächstes vergleichen muss. KAPITEL 4. SUCHEN IN TEXTEN 84 Beispiel: ababababca 1 a# 2 ab# 3 aba# 4 abab# 5 ababa# 6 ababab# 7 abababa# 8 abababab# 9 ababababc# 10 ababababca 0 0 1 2 3 4 5 6 0 1 Shift nach rechts ( 1 2 2 2 2 2 2 2 9 9 (kompletter Match) ) Algorithmus 28 zeigt eine Realisierung der Knuth-Morris-Pratt Idee. Die Werte von für . ' "#"#" werden dabei. vom Algorithmus InitNext( ) (s. Algorithmus 29) vorausberechnet ) ' "" + abgespeichert. Im Wesentlichen geschieht die Berechnung von und in dem Feld InitNext( ) sehr ähnlich wie die eigentliche Idee des Knuth-Morris-Pratt Algorithmus: das Muster wird mit sich selbst verglichen. Algorithmus 28 Knuth-Morris-Pratt ). ). ' " " + und Muster "" + Input: Text Output: Ausgabe aller Vorkommen von in 1: InitNext( ); /* Initialisiere das Feld next[] */ / 2: ; . 3: für #""#" . ) ) / 4: solange + ) + 5: +; 6: 7: 8: 9: 10: 11: 12: 13: ' falls ) ' falls . . + ) + ; ' dann gefunden: Ausgabe; dann ) + ; ' ' Korrektheit: Die Korrektheit des Knuth-Morris-Pratt Algorithmus ergibt sich aus der obigen Argumentation und der korrekten Berechnung des Feldes next[] in InitNext( ), die wir näher diskutieren wollen. . ) ) Fall 1: + + in Zeile (3). In diesem Fall ist das Endstück des Musters , das ) Anfangsstück von ist, das bisherige (von ) ) vereinigt mit %+ . . ) ) Fall 2: + %+ in Zeile (3). Dann geht man am besten genauso vor wie im 4.3. VERFAHREN VON BOYER-MOORE 85 Algorithmus 29 Prozedur InitNext ). ' Input: Muster "" + Output: .Initialisiert das Feld next[] für Muster / / ) 1: + ; ; ' 2: für "#"#" . ) ) / 3: solange . + %+ ) +; 4: 5: 6: 7: 8: 9: 10: ' ) falls ( ' ) %+ . . + ) + dann ; ; ' Knuth-Morris-Pratt Algorithmus, nämlich man vergleicht der Reihe nach ) ) + + , . . . , bis man bei 0 angekommen ist. ) + mit ) + , Mal und der Index Analyse der Laufzeit: Im Algorithmus wird der Index genau maximal Mal erhöht. Der Index kann allerdings höchstens so oft zurückgesetzt werden, wie er erhöht wurde, also insgesamt höchstens Mal. Das gleiche Argument gilt auch für Algorithmus InitNext( ). Dort kann höchstens so oft zurückgesetzt werden, wie es ' insgesamt erhöht wurde, und das ist maximal . Wir erhalten also als Gesamtlaufzeit des ' Knuth-Morris-Pratt Algorithmus 3( . Bemerkung: Bei Knuth-Morris-Pratt wird der Text ausschließlich sequentiell durchlaufen, da der Index niemals zurückgesetzt wird. Dies ist in manchen Anwendungen von Vorteil, z.B., wenn der Text auf externen Medien gespeichert ist, die nur sequentiell in einer Richtung durchlaufen werden können (z.B. Bänder). 4.3 Verfahren von Boyer-Moore Die Grundidee von Boyer-Moore ist die folgende: Eine Verschiebung des Musters bei Mismatch ist hier davon abhängig, welches Zeichen im Text für den Mismatch verantwortlich ist und wo dieses im Muster auftritt. Dazu wird das Muster von links nach rechts angelegt, aber die Zeichen werden von rechts nach links gelesen. Die Motivation hierfür liegt in der Beobachtung, dass die meisten Textzeichen im Muster nicht auftauchen (da die Musterlänge im Verhältnis zum Alphabet bzw. Text sehr klein ist), und so das Muster sehr oft um die ganze Länge verschoben werden kann. Wenn man allerdings nur diese Idee (Last-Verschiebung) implementiert, dann kann es im ' schlimmsten Fall zu einer Laufzeit von kommen. Deswegen verwendet man noch KAPITEL 4. SUCHEN IN TEXTEN 86 ein zweites Verschiebungskriterium, das so weit verschiebt, bis die letzten gematchten Zeichen im Text mit den ersten Zeichen in übereinstimmen (Suffix-Verschiebung). Es gibt also zwei verschiedene Verschiebungskriterien, die jeweils unabhängig voneinander berechnet werden und von denen dann die größere Verschiebung durchgeführt wird. Algorithmus Boyer-Moore( ) (s. Algorithmus 30) gibt den Algorithmus an. Dabei wird zunächst die Berechnung der Verschiebungstabellen last[] und suffix[] . aufgerufen. Die Ersetzung von Zeile (11) und (13) durch den Befehl ergibt einen naiven ' . Algorithmus mit Laufzeit 3( Algorithmus 30 Boyer-Moore ). ). ' Input: Text " " + und Muster "" + Output: Ausgabe aller Vorkommen von in 1: InitLast ; 2: InitSuffix ; / 3: ; /* Position vor der Anlegestelle von */ ' 4: solange ' 5: ; ) ) / 6: solange . + + 7: ; 8: 9: 10: 11: 12: 13: 14: 15: ' falls / dann gefunden: Ausgabe; . )' +; sonst ' ' ) + ) ) ++ ; ' 4.3.1 Berechnung von last[] Wir betrachten die Situation, . dass die erste Nichtübereinstimmung bei auftrat, ) ) ' + für ein . Das Zeichen an dieser Stelle im Text sei d.h. + . In diesem Fall verschieben wir so weit nach rechts, bis im Text über dem rechtesten Zeichen gleich in ist. Falls nicht in vorkommt, dann kann bis hinter die Position verschoben werden. . ' ) Lemma: Sei der größte Index aus , für den gilt / zu erhöhen. vorhanden, sonst . Dann ist es korrekt, um Beweis: Fall 1: / . Dann sind alle anderen Zeichen links von ungleich ) + + ) + , falls 4.3. VERFAHREN VON BOYER-MOORE 87 Fall 2: . Das rechteste Erscheinen des Mismatch-Zeichens in liegt links von in / . In diesem Fall ist eine Verschiebung um nach rechts korrekt. / Fall 3: . In diesem Fall ist und es wird keine Verschiebung (wegen last) durchgeführt. Der Algorithmus InitLast( ) (s. Algorithmus 31) berechnet jeweils die Position des rechtesten Vorkommens für alle Zeichen im Alphabet . Algorithmus 31 Prozedur InitLast 1: für alle aus dem Alphabet / 2: 3: 4: 5: 6: ' für ' ) . ) #"#"#" ++ ' 4.3.2 Berechnung von suffix[] Verschiebe soweit nach rechts, bis die bisher untersuchten gematchten Zeichen im Text mit den ersten Zeichen im Muster übereinstimmen. Falls keinerlei Übereinstimmung herrscht, dann kann das Muster bis ganz hinter die Position geschoben werden. Wir betrachten wieder die Situation, dass der erste Mismatch bei aufgetaucht ist, ) ) d.h. + +. Wir definieren den Suffix als die kleinste Verschiebung , so dass ) . )' + ) .. . + .. . )' ) . + + ) . +. Eine alternative Sichtweise ist die folgende: . ) )' +#"#"#" + Suffix von ist, wobei + ""#" ' ) + , / ' , so dass . Zur Berechnung unterscheiden wir zwei Fälle. 1. Fall: Der Suffix befindet sich nur am Anfang von . ) ' )' )' Dann gilt + + , wobei + die Länge des längsten Präfixes von ist, das echter Suffix von ist. KAPITEL 4. SUCHEN IN TEXTEN 88 2. Fall: Der Suffix befindet sich nicht nur am Anfang von . ) In diesem Fall kann der Suffix im invertierten Muster Hilfe von Ab. next[] (s. ) ) ' mit schnitt 4.2) ausgerechnet werden. Es gilt + + für alle und ' ) %+ . Intuitiv suchen wir alle diejenigen Längen , deren Endungen mit dem ge' ) + . suchten Präfix übereinstimmen. Und das sind genau diejenigen, für die gilt Der Suffix kann berechnet werden als ) suffix + ' )' +' ) next +$2 . ' und ' ) next +' Der Algorithmus InitSuffix( ) (s. Algorithmus 32) bestimmt zunächst im gegebenen Muster den Suffix, der Fall 1 entspricht (d.h. am Anfang des Musters). Danach bestimmt er im ) invertierten Muster den Suffix, der Fall 2 entspricht. Dazu berechnet er im invertierten Muster das Feld []. Algorithmus 32 Prozedur InitSuffix ) 1: Berechne InitNext + ) ) 2: Berechne InitNext + ' / 3: für #"#"#" ) ' )' 4: + +; 5: 6: 7: 8: 9: 10: 11: ' für . ' falls #"#"#" ) ) + ' + ) + ; ) ) %+ ; %+ dann ' ' 4.3.3 Analyse von Boyer-Moore Man kann zeigen, dass die Laufzeit des Boyer-Moore Verfahrens in der Ordnung von ' 3( liegt. Dies gilt aber nur, wenn beide Verschiebeverfahren verwendet werden. Die Verschiebung mittels last ist sehr einfach, die Suffix-Verschiebung etwas aufwendiger zu programmieren. Aus diesem Grund wird der Algorithmus in der Praxis oft ohne die SuffixVerschiebung implementiert. Das Verfahren ist in der Praxis in beiden Fällen sehr schnell. 4.4. TRIES 89 4.4 Tries Für verschiedene Aufgaben in der Textverarbeitung ist der Trie eine wichtige Datenstruktur, die wir deshalb genauer behandeln wollen. Der Name Trie“ kommt vom englischen ” retrieve“ (wiederauffinden). ” 4.4.1 Radix Trie Grundsätzlich ist ein Radix Trie ein binärer Baum, bei dem jedoch nur die Blätter Datensätze beinhalten, die nach einem Schlüssel geordnet sind. )/ ). + und + , die entweder auf NachfolgeJeder innere Knoten besitzt zwei Zeiger knoten – den linken bzw. rechten Teilbaum – verweisen oder den Wert NULL besitzen. Ein Blattknoten schließlich, beinhaltet den Schlüssel und eventuelle Anwenderdaten. 0 #"#"#" einer fixen, vorgegebenen Der Schlüssel ist hier immer ein binärer String Länge . Andere Datentypen müssen gegebenenfalls in eine solche Binärrepräsentation fixer Länge umgewandelt werden. Für den Radix Trie gilt Folgendes: In einem inneren Knoten sind nie beide Verweise gleich NULL. Hat ein innerer Knoten nur einen Nachfolger, so ist dies immer ein weiterer innerer Knoten, keinesfalls ein Blatt. Diese und die vorige Bedingung stellen sicher, dass der Radix Trie keine unnötigen“ inneren Knoten besitzt. ” . Jeder Radix Trie hat immer maximal Ebenen und kann Datensätze aufnehmen. Er kann somit nicht entarten, wie dies etwa beim einfachen binären Suchbaum möglich ist. wurzel wurzel 1 0 E I N U V 00101 01001 01110 10101 10110 0 1 1 0 V 1 0 0 E E 0 1 0 1 I N I N 1 0 1 U V Abbildung 4.1: Ein Radix Trie vor und nach dem Einfügen von ‘U’. KAPITEL 4. SUCHEN IN TEXTEN 90 . Für jeden inneren Knoten der Ebene #"#"#" ' des Trie gilt, dass in seinem linken Teilbaum nur Datensätze gespeichert sind, deren (das -te Bit des Schlüssels). gleich 0 ist. Umgekehrt beinhaltet der rechte Teilbaum nur jene Datensätze mit . Der leere Baum wird lediglich durch eine Verweis-Variable tiert. repräsen- Abbildung 4.1 zeigt einen Radix Trie vor und nach dem Einfügen von ‘U’ (=10101). Im Folgenden betrachten wir die Such-, Einfüge- und Löschoperationen genauer. Einfügen Randbedingung: Wir gehen davon aus, dass zwei Datensätze niemals gleiche Schlüssel besitzen. Treten in einer Anwendung dennoch Datensätze mit gleichen Schlüsseln auf, so kann dies beispielsweise durch eine zusätzliche äußere Verkettung gelöst werden, d.h. die Blätter sind jeweils lineare Listen, die alle Datensätze mit identen Schlüsseln beinhalten. Algorithmus 33 Einfügen ( , , ) 1: falls NULL dann 2: = Erzeuge Blattknoten für Datensatz mit Schlüssel ; 3: ' sonst falls ist innerer Knoten . dann ) + , , ); 4: Einfügen ( " 5: ' sonst 6: /* ist ein Blatt; ein neuer innerer Knoten wird eingefügt: */ ; 7: 8: = neuer innerer Knoten; ) 9: ; " . " + ) 10: ; . " " + ) 11: Einfügen ( " + , , ); 12: ' Diese rekursive Prozedur wird für einen gegebenen Schlüssel aufgerufen: und Trie wurzel wie folgt Einfügen ( , , 1); Stößt die Prozedur auf einen Blattknoten, so werden in den weiteren Schritten so lange innere Knoten erzeugt, bis sich dieser Datensatz und der neu einzufügende in einem Bit unterscheiden. Im Worst-Case wird in einem anfangs aus einem Datensatz bestehender Trie ein zweiter Datensatz eingefügt, der sich vom ersten nur im letzten Bit unterscheidet. Vergleiche auch Abbildung 4.1. Aufwand: Da der Baum maximal Tiefe . hat, ist der Aufwand des Einfügens . 4.4. TRIES 91 Suchen Algorithmus 34 Suchen ( , , ) 1: falls NULL dann 2: Datensatz nicht enthalten; 3: ' sonst falls ist innerer Knoten dann . ) 4: Suchen ( " + , , ); 5: ' sonst 6: /* ist ein Blatt: */ #"#"" dann 7: falls " "#"#" " 8: Datensatz gefunden; ' sonst 9: 10: Datensatz nicht enthalten; ' 11: 12: ' Aufwand: Wiederum ist durch die beschränkte Tiefe des Baumes der Aufwand Prozedur kommt sogar mit einem einzigen Schlüsselvergleich aus. Entfernen Algorithmus 35 Entfernen ( , , ) 1: falls NULL dann 2: Datensatz nicht in Trie; 3: ' sonst falls ist innerer Knoten. dann ) 4: Entfernen ( " , , ); + )/ ). + . + ist Blatt dann 5: falls " && " ) " + ; /* . Knoten löschen */ 6: ) )/ ' sonst falls " + + ist Blatt dann 7: && " )/ " + ; /* Knoten löschen */ 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: ' sonst /* ist Blatt: */ falls " "#"#" " #"#"" dann /* Zu löschenden Datensatz gefunden */ ; /* Datensatz löschen */ ' sonst Datensatz nicht enthalten; ' ' ' . Die KAPITEL 4. SUCHEN IN TEXTEN 92 Diese Prozedur entfernt auch alle inneren Knoten, die durch das Entfernen eines Unterknotens nur mehr ein einzelnes Blatt haben würden. Dadurch ist auch sichergestellt, dass es keine inneren Knoten ohne Nachfolger geben kann. Aufwand: . Eine Eigenschaft aller Tries, die sie von den meisten anderen Baumstrukturen unterscheidet ist, dass es für eine gegebene Menge von Datensätzen immer nur genau einen bestimmten Trie gibt, unabhängig davon, in welcher Reihenfolge die Datensätze eingefügt wurden. In vielen Anwendungen ist ein Nachteil des Radix Trie jedoch, dass die maximale Tiefe durch eine große Schlüssellänge sehr hoch werden kann. Beispiel: Strings, die aus maximal . / / / / 200 Zeichen kodiert durch je 8 Bit bestehen, haben eine Schlüssellänge . % In diesem Fall ist es dann sinnvoll, bei jedem inneren Knoten mehr als nur zwei mögliche Nachfolger zu verwalten. Beispielsweise kann ein innerer Knoten so viele Nachfolger besitzen, wie es unterschiedliche Zeichen für jede Position eines String-Schlüssels gibt. Dieses Vorgehen führt uns weiter zum Indexed Trie. 4.4.2 Indexed Trie Der Indexed Trie dient primär zur Speicherung von Worten, zum Beispiel für eine Rechtschreibprüfung. Die Aufgabe besteht beim Suchen lediglich darin, festzustellen, ob ein Wort gültig“, d.h. in der Datenstruktur enthalten ist oder nicht. ” Wir gehen hier als Beispiel davon aus, dass jedes Wort 0 "#"#" aus beliebig vielen Zeichen des Alphabets ‘a’, ‘b’,. . . ,‘z’ ' besteht. Im Unterschied zum Radix Trie ist nun jeder Knoten ein Feld der Größe ! ! , das durch die Elemente des Alphabets indiziert ist und folgende beiden Variablen für jedes beinhaltet: einen Verweis next auf einen Nachfolgeknoten und eine Boolsche Variable end, die angibt, ob ein gültiges Wort mit dem entsprechenden Buchstaben (= Index) an dem Knoten endet. 4.4. TRIES 93 wurzel (1) a b c d end: T F F F next: ... ... ... z F a b c d (2) end: F T F F next: ... ... ... z F a b c d (3) end: T F T F next: ... ... ... z F a b c d (4) end: F F F F next: ... ... ... z F a b c d (5) end: F F F F next: ... ... ... z F a b c d (7) end: T F F F next: ... ... ... z F a b c d (8) end: T F F F next: ... ... ... z F a b c d (6) end: T F F T next: ... ... ... z F Abbildung 4.2: Ein Indexed Trie der die Worte a, ab, abda, da, dc, dcda, dda, ddd ' beinhaltet. Einfügen Da nun keine speziellen Blattknoten mehr verwendet werden, muss beim Einfügen eines Wortes 0 "#"#" $ für jedes Zeichen ein Knoten erzeugt werden, falls dieser nicht bereits existiert. Abbildung 4.2 zeigt ein Beispiel eines Indexed Trie. Algorithmus 36 Einfügen ( , 1: falls NULL dann = Erzeuge Knoten; 2: 3: für alle ) + " 4: ; ) ; + " 5: 6: 7: 8: 9: 10: 11: 12: ' ' falls ) & dann ; +" sonst ) Einfügen ( +" , ' , ) , . ); ' Die maximale Tiefe des Trie ist immer gleich der Länge des längsten darin enthaltenen Wortes. KAPITEL 4. SUCHEN IN TEXTEN 94 Aufwand: 3(4& ! ! Suchen Algorithmus 37 Suchen ( , , ) 1: falls NULL dann 2: Wort nicht enthalten; 3: 4: 5: 6: 7: 8: 9: 10: ' falls & dann ) Suchen ( + " , , ) ' sonst falls + " Wort gefunden; ' sonst Wort nicht enthalten; . ); dann ' 5& Aufwand: (& . . . Länge des zu suchenden Wortes) Entfernen Algorithmus 38 Entfernen ( , , ) 1: falls NULL dann 2: Wort nicht enthalten; Abbruch 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: ' falls & dann . ) Entfernen ( +" , , ); ) ' sonst falls + " dann /* Wort gefunden */ ) ; +" ' sonst Wort nicht enthalten; Abbruch ' falls 2 ) 5& ! ) + " +" ; /* Knoten entfernen */ ' Aufwand: ! dann 4.4. TRIES 95 Weitere Anwendungen des Indexed Trie Suche in einem (dynamischen) Text gleichzeitig nach mehreren Worten bzw. unterschiedlichen Wortformen. Fortgeschrittenere Varianten des Trie können auch Wortfamilien, wie sie durch reguläre Ausdrücke gegeben sind, repräsentieren. Hier besteht auch ein gewisser Zusammenhang zu endlichen Automaten. #"#"#" Ist ein fixer Text gegeben, für den sehr oft überprüft werden soll, ob ein Wort darin vorkommt, kann ein Trie aus allen möglichen Suffixen von , d.h. aus "#"#" , 0 #"#"#" , . . . , ' aufgebaut werden. Das Überprüfen, ob ein Wort der Länge & Substring von ist, kann dann in 5& durchgeführt werden. Die Variablen end sind in diesem Fall nicht notwendig. Ein derartiger Trie wird auch als Suffix-Tree bezeichnet. 4.4.3 Linked Trie In den Knoten eines Indexed Trie gibt es oft sehr viele Einträge mit NULL False. Wir nennen solche Einträge leere“ Einträge. Dadurch ” bedingt ist die Datenstruktur nicht sehr kompakt. Beim Linked Trie wird das Feld für jeden Knoten durch eine lineare Liste ersetzt, die nur next- und end-Variablen für jene beinhaltet, für die NULL ! ! False. D.h. alle leeren Einträge werden nicht gespeichert. Dafür muss nun zusätzlich immer das jeweilige vermerkt werden. Aufwand: Bei größerem ! ! und dichten Tries kann durch das notwendige Iterieren durch die linearen Listen der Aufwand für das Einfügen, Suchen und Entfernen deutlich höher als beim Indexed Trie sein: 5& ! ! . 4.4.4 Suffix Compression Haben mehrere Worte genau die gleiche Endung (Suffix), so kann man einen Indexed oder Linked Trie durch eine Suffix Compression kompaktieren. Dabei speichert man den Subtrie für die gemeinsame Endung nur mehr einmal und verweist darauf mehrfach. Abbildung 4.3 zeigt den Trie aus Abbildung 4.2 nach einer erfolgten Suffix Compression. Die idente Endung da der Worte abda und dcda wird nunmehr durch einen gemeinsamen Subtrie repräsentiert. Der Trie hat damit keine Baumstruktur mehr. Achtung: Führt man Suffix Compression durch, können die betroffenen Worte nicht mehr individuell verändert werden. Würde man im gezeigten Beispiel das Wort abdd einfügen, so würde automatisch auch das Wort dcdd im Trie enthalten sein. Suffix Compression ist daher ein Schritt, der erst durchgeführt werden sollte, wenn im Trie keine Änderungen mehr gemacht werden. KAPITEL 4. SUCHEN IN TEXTEN 96 wurzel (1) a b c d end: T F F F next: a b c d (2) end: F T F F next: ... ... ... z F ... ... ... z F a b c d (3) end: T F T F next: ... ... ... z F (5) a b c d end: F F F F next: ... ... ... z F a b c d (8) end: T F F F next: ... ... ... z F a b c d (6) end: T F F T next: ... ... ... z F Abbildung 4.3: Der Indexed Trie für die Worte a, ab, abda, da, dc, dcda, dda, ddd ' nach einer Suffix Compression. Ein Algorithmus zur Suffix Compression auf einem Indexed oder Linked Trie sieht abstrakt wie folgt aus: Algorithmus 39 Suffix Compression () 1: solange zwei Knoten und existieren, die gleich sind (sowohl alle end-Flags als auch alle next-Verweise!) 2: Lösche Knoten ; 3: Ersetze im gesamten Trie alle Verweise auf durch Verweise auf ; 4: ' 4.4.5 Packed Trie Für den Indexed Trie gibt es noch eine weitere Möglichkeit der Komprimierung. Die Umwandlung eines Indexed Trie in einen Packed Trie ist ein Schritt, der im Allgemeinen eine sehr große Menge an Speicher befreit. Fast alle Felder, die im Indexed Trie leer sind (end==False next==NULL), werden eliminiert und es muss dennoch nicht auf die Geschwindigkeitsvorteile des Index-basierten Zugriffs verzichtet werden. Der Packed Trie wird nicht mehr durch eine Menge von Feldern mit Verweisen dargestellt, sondern durch ein einziges, größeres Feld , in welchem alle Knoten teilweise ineinander verzahnt gespeichert sind. ist mit ganzen Zahlen indiziert, und jedes Feldelement besteht 4.4. TRIES Index: (2) ... (5) 2 #$ ... 7 ... ... 1 (3) (8) a b c d e ... z (1) (6) 97 ... ... 6565 6565 ... !! "" :9:9 --. &%&% )*+, *) ''(( 87870/0/ 8787 34 >=<;2121 >=<; 4 ... 6 ... ... Knoten des Indexed Tries Index: 1 2 3 4 5 6 7 ... 10 ... 8 9 10 11 12 13 14 ... Packed Trie: Index: 1 2 3 c: a a c end: T T T next: 0 7 4 4 5 6 7 8 9 10 d d a d b d a F F T F T T T 6 Index der Wurzel: 2 1 0 10 4 0 0 Abbildung 4.4: Der Indexed Trie nach der Suffix Compression wird weiter in einen Packed Trie umgewandelt. aus folgenden drei Komponenten: c – das Zeichen des Alphabets (= der Index eines Knotenelements im Indexed Trie) end – die Wortende-Markierung next – der Verweis auf den Beginn des Nachfolgeknotens im Feld (= der Index des ersten Buchstaben des Nachfolgeknotens). Anstatt NULL“ wird hier z.B. der Wert 0 ” verwendet, um anzuzeigen, dass es keinen Nachfolger gibt. Beim Aufbau des Packed Trie ist wichtig, dass Elemente, für die im Indexed Trie end False oder next NULL gilt, niemals kollidieren, da sonst Information verloren gehen würde. Weiters dürfen niemals zwei Knoten den gleichen Anfangsindex haben, da sonst nicht zwischen ihren Elementen unterschieden werden kann. Zum Packed Trie gehört auch noch die (globale) Variable wurzel, die den Anfangsindex des Wurzelknotens angibt. Der Wurzelknoten muss nämlich nicht unbedingt bei Index 1 beginnen. Abbildung 4.4 zeigt als Beispiel, wie unser Indexed Trie aus Abbildung 4.3 weiter in einen Packed Trie kodiert werden kann. Die leeren Felder am Ende der zusammengefügten Knoten brauchen dabei – wie in der Abbildung ersichtlich – nicht gespeichert zu werden, sofern KAPITEL 4. SUCHEN IN TEXTEN 98 bei der Suche eine entsprechende Bereichsüberprüfung der Indizes durchgeführt wird. In diesem Beispiel weist der Packed Trie keine einzige Lücke auf. Das ist aber nicht immer so. Eine große Menge von Knoten eines Indexed Trie tatsächlich optimal zu komprimieren, d.h. in ein Feld minimaler Länge mit möglichst wenigen Lücken, ist im Allgemeinen ein schwieriges Problem, das praktisch nicht exakt gelöst werden kann. Man erreicht jedoch mit relativ einfachen greedy-Heuristiken zumeist sehr gute Resultate, da in der Praxis viele Knoten sehr dünn besetzt sind, oft sogar nur ein einziges belegtes Element besitzen und so als Lückenfüller“ verwendet werden können. ” Eine einfache Heuristik ist beispielsweise, die Knoten nach ihrer Anzahl belegter Elemente absteigend zu sortieren und dann in dieser Reihenfolge einen Knoten nach dem anderen an der ersten geeigneten Stelle im Packed Trie einzuordnen. Das Auffinden dieser Stelle ist eine Pattern-Matching Aufgabe, die mit Varianten der Algorithmen zur Textsuche (z.B. BoyerMoore) effizient gelöst werden kann. 4.4.6 Suchen Abschließend hier noch der Algorithmus zur Suche eines Wortes Algorithmus 40 Suchen ( , , wurzel) . 1: ; 2: ; 3: wiederhole . $ ; 4: 5: falls $ dann 6: /* Index außerhalb von : */ 7: Wort nicht enthalten; Abbruch; 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: ' ) + " dann falls Wort nicht enthalten; Abbruch; ' ) falls & +" #& Wort gefunden; Abbruch; ' ) . +" ; ; / bis &! ! ; Wort nicht enthalten; Aufwand: 5& dann im Packed Trie : 4.4. TRIES 99 Weiterführende Literatur Suchen in Texten wird z.B. in den Büchern von Sedgewick und Cormen, Leiserson und Rivest (s. Literaturliste (2) und (3)) ausführlich beschrieben. 100 KAPITEL 4. SUCHEN IN TEXTEN Kapitel 5 Randomisierte Algorithmen Unter einem Randomisierten Algorithmus (bzw. Probabilistischen Algorithmus) verstehen wir einen deterministischen Algorithmus, der als zusätzliche Elementaroperationen Zufallsexperimente durchführen kann. Man unterscheidet grundsätzlich zwei Klassen von randomisierten Algorithmen, nämlich Las-Vegas-Verfahren, die stets ein korrektes Ergebnis berechnen und Monte-CarloVerfahren, die ein Ergebnis berechnen, das mit einer gewissen Fehlerwahrscheinlichkeit behaftet ist. Ein wesentlicher Unterschied zwischen randomisierten und deterministischen Algorithmen besteht in der Analyse. Während man bei deterministischen Algorithmen in erster Linie die worst-case Komplexität betrachtet, analysiert man bei randomisierten Algorithmen die sogenannte expected-case Komplexität, die das Verhalten des Algorithmus über alle möglichen Ausgänge der durchgeführten Zufallsexperimente mittelt. Tabelle 5.1 zeigt weitere Unterschiede. Während der Aufbau deterministischer Algorithmen sehr komplex sein kann, ist der Aufbau randomisierter Algorithmen meist sehr einfach. Durch die expected-case Analyse ist jedoch die Analyse relativ komplex im Vergleich zu deterministischen Algorithmen. Die Laufzeit randomisierter Algorithmen kann garantiert sein, oft hängt diese jedoch vom Ausgang der Zufallsexperimente ab. Algorithmus Zufallsexperimente Aufbau Analyse Laufzeit Korrektheit Deterministisch nein komplex einfach garantiert garantiert Randomisiert ja einfach komplex variabel garantiert/probabilistisch Tabelle 5.1: Unterschiede zwischen deterministischen und randomisierten Algorithmen 101 KAPITEL 5. RANDOMISIERTE ALGORITHMEN 102 5.1 Randomisierter Primzahltest Im folgenden wenden wir ein Monte-Carlo Verfahren, nämlich den Algorithmus von Miller-Rabin, auf das Primzahlproblem an. Primzahltest Gegeben: Ganze Zahl & (üblicherweise sehr groß) Gesucht: Antwort auf die Frage: Ist & eine Primzahl? Dieses Problem ist in der Kryptographie (z.B. für das RSA-Verfahren) eines der zentralen Probleme überhaupt. Die Zahlen & , um die es dort geht, sind Teile von Schl üsseln, die bereits jetzt eine Länge von über 100 Stellen besitzen. Mit steigender Rechnerkapazität wird diese Länge weiter ansteigen. Weil man solche langen Zahlen nicht mehr in konstanter Zeit addieren kann, gehen wir hier zur sogenannten Bitkomplexität über. D.h., wir stellen & als Binärzahl mit genau . 4& Bits dar und zählen die Bitoperationen einzeln. Das folgende Lemma zeigt, dass etwa jede Lemma: Sei 5& & & 5& & & . Für & . $ gilt: & & . 5& & . Für den Grenzwert gilt: . die Anzahl der Primzahlen -te Zahl eine Primzahl ist. & Zunächst untersuchen wir die naive Divisionsmethode. Diese testet nacheinander, ob & durch 2 oder durch eine ungerade Zahl aus dem Bereich 1#""#" & ' teilbar ist. Die Anzahl der Divisionen ist in diesem Fall in & , wobei 3( & (Bitkomplexität). Somit ist das naive Verfahren für große Zahlen nicht praktikabel. Bis heute ist kein deterministischer Algorithmus bekannt, der das Problem Primzahltest in polynomialer Laufzeit lösen kann. 5.1.1 Algorithmus von Miller-Rabin Der Algorithmus von Miller-Rabin ist ein randomisiertes Verfahren zum Primzahltest und gehört der Klasse der Monte-Carlo Verfahren an. D.h., das Ergebnis ist mit einer gewissen Fehlerwahrscheinlichkeit behaftet. Die Idee beruht auf dem folgenden wichtigen Satz von Fermat. 5.1. RANDOMISIERTER PRIMZAHLTEST 103 Satz von Fermat . . Ist & eine Primzahl, so gilt für alle * "#"#" & $ ) . ' & " (5.1) Der Satz von Fermat liefert ein nützliches Kriterium zum Test der Primalität von & . Fixieren wir z.B. eine Basis , und liefert unser Test $ ) . & so ist & sicherlich keine Primzahl, und heißt Zeuge für die nicht-Primheit von & . Tatsächlich gibt es sogenannte Pseudoprimzahlen, die die Gleichung für ein zwar erfüllen, aber keine Primzahlen sind. Jedoch liefert bereits .dieser einfache Test für 100-stellige Zahlen & und / ) 41 eine sehr kleine Fehlerrate von etwa . Weiterhin gibt es die sogenannten Carmichael $ die Gleichung 5.1 erfüllen, aber keine Primzahlen sind. Dabei Zahlen, die für alle ist $ der Restklassenring bezüglich & definiert als $ . #"#"#" * & . ' ! ggt . & '," . Carmichael-Zahlen sind allerdings sehr selten: es gibt nur 255 von ihnen, die kleiner als sind. / Diese Überlegungen führen uns direkt zur Idee des Algorithmus von Miller-Rabin . (s. Algo. rithmus 41). Wir wählen eine Basis zufällig und gleich verteilt aus der Menge #" " " & ' aus und machen den Test . ) $ & " Wenn die Gleichung nicht erüllt ist, dann haben wir einen Zeugen für die Nicht-Primheit von & , andernfalls wählen wir eine weitere Basis. Dieser Test wird insgesamt Mal wiederholt. Das zugrundeliegende Prinzip nennt man Random Search, da ein Raum mit unbekannter Struktur zufällig auf Elemente mit einer gewünschten Eigenschaft durchsucht wird. Algorithmus. 41 Primzahltest von Miller-Rabin #" " " 1: für . . 2: falls Zeuge (Random ( & & ) dann 3: Return “nicht prim” 4: 5: 6: ' ' Return “prim” Es fehlt nun noch ein effizienter Algorithmus für die Berechnung von rithmus Zeuge (siehe 42) löst das Problem durch Repeated Squaring. Sei $ ) ) #" & . Algo" " die KAPITEL 5. RANDOMISIERTE ALGORITHMEN 104 Algorithmus 42 Zeuge ( & ) . / 1: /* . / 2: für #"#"" . & /* 3: 4: falls dann & /* 5: 6: 7: 8: ' ' Return NOT . . . Binärdarstellung von & . Die Funktion Witness() vermeidet große Zahlen, indem sie die Vertauschbarkeit von Multiplikation und Modulo-Berechnung ausnutzt: 0 & & 0 & & & (5.2) (5.3) & Dabei gilt in jedem Schleifendurchlauf die Invariante & . wobei durch Verdoppeln und Inkrementieren von 0 auf & erhöht wird. Dabei gilt in ) #"#"#" . Somit gilt nach dem letzten Schleifenjedem Schleifendurchlauf , dass / ) #"#"#" & . durchlauf , dass Analyse der Laufzeit Die Multiplikation zweier -Bit Zahlen (Zeile (3)) hat Bitkomplexität 1 für jeden der Aufrufe von Witness(). 0 . Dies sind Satz: Der Monte-Carlo Algorithmus von Miller-Rabin zum Test der Primalität von & mit 1 Binärdarstellung ) #"#"" hat eine polynomielle Laufzeit von . Analyse der Fehlerrate Man kann das folgende Theorem mit Hilfe von gruppentheoretischen Überlegungen bewei .) sen. Unter anderem gehören alle Nicht-Zeugen zur Menge $ Satz: Für & , ungerade und nicht prim, gibt es mindestens 0 Zeugen für die Zusammengesetztheit. Daraus folgt, dass die . zufällige Auswahl von in jeder Iteration mit einer Wahrscheinlichkeit von mindestens einen Zeugen für die Zusammengesetztheit einer nicht-primen Zahl & liefert. Da der Algorithmus Miller-Rabin nur dann ein falsches Testergebnis ausgibt, wenn nach Iterationen immer noch kein Zeuge gefunden worden ist und die Zahl & tatsächlich nicht prim ist, erhalten wir insgesamt eine Fehlerwahrscheinlichkeit von höchstens 0 . Tatsächlich ist die Fehlerrate in der Praxis um einige Größenordnungen kleiner. 5.1. RANDOMISIERTER PRIMZAHLTEST 105 Weiterführende Literatur Eine kurze Einführung in das Themengebiet randomisierte Algorithmen finden Sie in dem Artikel von T. Roos, “Randomisierte Algorithmen” (in dem Buch von T. Ottmann, “Prinzipien des Algorithmenentwurfs”, Spektrum Akademischer Verlag, 1998), das auch als Vorlage für diesen Teil des Skripts war. Für weitergehende Literatur empfehlen wir das Buch von R. Motwani und P. Raghavan,“Randomized Algorithms”, Cambridge University Press, 1995. 106 KAPITEL 5. RANDOMISIERTE ALGORITHMEN Kapitel 6 Parallele Algorithmen Beim Parallelen Rechnen geht es um das schnelle Lösen algorithmischer Probleme durch den Einsatz vieler Prozessoren (Rechner). Man untersucht die Frage, wann und unter welchen Bedingungen sich der Einsatz vieler Prozessoren zur schnelleren Berechnung eines gegebenen Problems lohnt. Dabei spielen auch verschiedene Rechnermodelle eine wichtige Rolle. ). Wir nehmen an, wir wollen & Zahlen aufsummieren, die sich in einem Array ""& + befinden. Die sequentielle Addition ist in Abbildung 6.1(a) visualisiert. Wir berechnen zunächst die Summe zweier Zahlen, und nehmen dieses Ergebnis, um es zur dritten Zahl zu addieren, u.s.w. Diese Operationen sind abhängig voneinander. Es kann nur jeweils ein Prozessor sinnvoll eingesetzt werden. + + + + A(1) + A(4) + A(3) A(2) + A(1) + A(2) (a) A(3) + A(4) A(5) + A(6) A(7) A(8) (b) Abbildung 6.1: Verlauf einer sequentiellen und einer parallelen Summation Ein alternativer paralleler Verlauf ist in Abbildung 6.1(b) visualisiert. Hier werden jeweils Paare gebildet und diese unabhängig voneinander aufsummiert. Dies ist mit Hilfe von & Prozessoren parallel durchführbar. Im nächsten Schritt werden diese Paare wieder paarweise 107 KAPITEL 6. PARALLELE ALGORITHMEN 108 aufsummiert, u.s.w. Insgesamt ist die Summation für & Zahlen hier mit & Schritten mit Hilfe von & Prozessoren durchführbar. Diese Methode ist also die günstigere Methode. Nach dieser kurzen Einführung stellen wir das von uns im folgenden betrachtete parallele Rechnermodell vor. 6.1 Ein Modell einer parallelen Maschine Es gibt viele Möglichkeiten für parallele Maschinen. Wir betrachten als paralleles Rechnermodell eine PRAM (Parallel Random Access Machine). Abbildung 6.2 zeigt den Aufbau einer PRAM. P1 Pp P2 viele Prozessoren einige lokale Speicherplaetze gemeinsamer Speicher Abbildung 6.2: Modell einer PRAM Jeder Prozessor kann auf den gemeinsamen Speicher direkt zugreifen. Jeder Prozessor besitzt daneben auch selbst noch lokale Speicherplätze. Die PRAM wird von einem einzigen Programm gesteuert. Ein Schritt in einer PRAM besteht aus drei Teilen: Lesephase, d.i. die Übernahme eines Speicherwertes auf dem gemeinsamen Speicher in den lokalen Speicher. Rechenphase, d.h. jeder Prozessor kann konstant viele Operationen mit Werten in seinem Speicher durchführen. Schreibphase, d.h. jeder Prozessor kann einen Speicherwert von seinem lokalen in den gemeinsamen Speicher schreiben. 6.2 Parallele Minimum-Berechnung In diesem Abschnitt diskutieren wir zwei parallele Verfahren, die jeweils mit & und & Prozessoren eine parallele Minimum-Berechnung auf der PRAM durchführen. Gegeben: & Zahlen in dem Array Gesucht: Minimum der Zahlen in 0 6.2. PARALLELE MINIMUM-BERECHNUNG 109 Min Min Min Min Min Min Min Abbildung 6.3: Parallele Minimum-Berechnung mit & Prozessoren Parallele Minimum-Berechnung mit & Prozessoren Abbildung 6.3 zeigt das Berechnungsschema des Verfahrens. Eine Realisierung ist in Algorithmus 43 beschrieben. Der Algorithmus gibt für jeden Prozessor an, was dieser in welchem Schritt der Rechnung zu tun hat. Man sagt auch, dass ein solche Algorithmus mit der Prozessornummer parametrisiert ist. Die beiden Arrays und befinden sich im gemeinsamen Speicher. Die Speicherplätze , , und befinden sich jeweils im lokalen Speicher von Prozessor . Der Algorithmus durchläuft für & Eingabewerte die Schleife & Mal. Beim ersten Schleifendurchlauf sind & Prozessoren beteiligt, beim zweiten Durchlauf nur noch & , u.s.w. Zu Beginn eines jeden Schleifendurchlaufs überprüft jeder Prozessor , ob er noch beteiligt ist. Das ist beim -ten Durchlauf der Fall, wenn seine Nummer. kleiner gleich & ist. Ist die Schleife. & Mal durchlaufen, dann legt ) Prozessor das Minimum in der Speicherzelle + ab. Siehe auch Abbildung 6.4. Algorithmus 43 Parallele Minimum-Berechnung für & Prozessoren 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: ) + . ) + für bis & $ . dann falls ) 0 + ) ) + $1 + ' ' . falls dann ) Ausgabe von + ' Der beschriebene Algorithmus ist nur dann korrekt, wenn wir annehmen, dass die Prozesso- KAPITEL 6. PARALLELE ALGORITHMEN 110 Pi’s B: h=1: h=2: h=3: i=1 Abbildung 6.4: Visualisierung der Minimum-Berechnung mit Algorithmus 43 ren zeitlich synchron arbeiten können, d.h., dass alle Prozessoren dieselben Arbeitsschritte zu exakt der gleichen Zeit ausführen. 0 Parallele Minimum-Berechnung mit & Prozessoren Der alternative Algorithmus (s. Algorithmus 44) berechnet das Minimum mit Hilfe von qua. & , in konstant vielen Schritten. Die Arrays und dratisch vielen Prozessoren , ) stehen im gemeinsamen Speicher. Im Element + vermerken wir, ob die Zahl noch als Minimum in Frage kommt oder nicht. Dieses Feld wird im ersten Schritt mit 1 initialisiert. . Alle Prozessoren , & , schreiben im ersten Schritt jeweils eine 1 an die Stelle ) + . Im zweiten Schritt passiert die eigentliche Minimum-Berechnung. Jeder Prozessor ) ) ) ) ) vergleicht jeweils die Werte + und + . Ist + + , dann scheidet + als Mini) mum aus, und der Prozessor schreibt eine 0 an die Stelle + . Wenn wir annehmen, dass das Minimum an . Stelle im Feld steht, dann wird allein durch die Berechnungen der & ) Prozessoren , & , an allen Stellen , die größer als das Minimum sind, + mit 0 beschrieben. 0 Algorithmus 44 Parallele Minimum-Berechnung mit & Prozessoren . ) 1: Für Prozessoren : + ) ) ) / 2: Für Prozessoren : Falls + + . + dann ) ) / 3: Für Prozessoren : Falls + . dann + ) ) + dann Ausgabe von + 4: Für Prozessoren : Falls Abbildung 6.5 zeigt eine (sequentielle) Visualisierung anhand eines Beispiels, die verdeutlicht was in Schritt 2 genau geschieht. Die. erste Zeile des Blocks ist zu lesen als das Ergebnis der Berechnungen aller Prozessoren , . & , wenn diese zuerst rechnen würden. Die nächste Zeile betrifft die Prozessoren 0 , & , u.s.w. (Das Bild ist leicht irreführend hinsichtlich der Parallelität. Natürlich arbeiten alle Prozessoren gleichzeitig.) Ist das Minimum eindeutig, dann genügt dieser Schritt. Schritt 3 sorgt nur noch dafür, dass von allen Minimumwerten derjenige mit dem kleinsten Index übrigbleibt. Im letzten Schritt wird das Minimum ausgegeben. 6.2. PARALLELE MINIMUM-BERECHNUNG 2 8 6 3 h[i] 1 1 1 1: 2,3,4 ... 1 1 1 2: 1,3,4 ... 3: 0 1 4: 0 0 111 2 4 5 7 1 1 1 1 0 1 1 0 0 0 1 1 1 0 1 0 0 1 0 1 0 0 0 0 1 0 1 0 0 0 Abbildung 6.5: Visualisierung der Minimum-Berechnung mit Algorithmus 2 Für diesen Algorithmus wird eine PRAM benötigt, die gleichzeitige Lesezugriffe mehrerer Prozessoren gestattet. Eine solche Maschine nennt man auch CR-PRAM für Concurrent Read. Ausserdem ist ein gleichzeitiger Schreibzugriff nötig. Eine solche Maschine nennt man CW-PRAM für Concurrent Write. Bei unserem Algorithmus tritt dieses Problem allerdings nur in einer besonderen Form auf: die Prozessoren schreiben zwar in die gleiche Speicherzelle, aber sie schreiben jeweils denselben Wert. Eine PRAM, die erlaubt den gleichen Wert zu schreiben, nennt man eine weak CW-PRAM. Zusammenfassend kann man sagen, dass der Algorithmus eine CRCW-PRAM benötigt (im Gegensatz zu einer EREW-PRAM: exclusive Read, exclusive Write). Erstaunlicherweise kann das obige Verfahren das Minimum immer in vier Schritten (konstant!) berechnen — und dies unabhängig von der Anzahl der Ememente & . Allerdings benötigen wir für die Berechnung eine große Anzahl von Prozessoren, die natürlich abhängig von & ist. Effizienzmaße für parallele Algorithmen Nun kann man sich fragen, welcher von beiden Algorithmen der bessere ist. Für die Effizienz von Algorithmen gibt es veschiedene Maße. Zu diesen gehören die Anzahl der benötigten Prozessoren 4& , die parallele Laufzeit 4& , die Arbeit, die Effizienz, sowie der Speedup. Die Arbeit ist definiert als die Effizienz als und der Speedup als 5& 5& 4& Arbeit sequentiell Arbeit parallel Laufzeit sequentiell " Laufzeit parallel Tabelle 6.1 gibt eine Übersicht über die verschiedenen Werte für die beiden Algorithmen. KAPITEL 6. PARALLELE ALGORITHMEN 112 Algorithmus 43: Algorithmus 44: Prozessoren Laufzeit Arbeit Effizienz 3(4& 0 3(4& 3( . & 3( 3(5& & 0 3 5& ( 3( %$ 3 ( $ Speedup $ 3. 2$ 3.5& Tabelle 6.1: Übersicht über die Maße zu den beiden Algorithmen zur Minimum-Berechnung & für Wir nennen einen parallelen Algorithmus optimal, wenn er Laufzeit 5& 4& , wobei 4& die sequentielle Laufzeit ist. Ein parein besitzt und Arbeit 4& alleler Algorithmus heißt effizient, wenn 5& & und 4& 4& & . Der Algorithmus 43 ist also effizient, währenddessen Algorithmus 44 nicht effizient ist. Das liegt an dem Einsatz der sehr großen Prozessorenanzahl, die die Arbeit im Vergleich zum sequentiellen Fall sehr vergrößert. 6.3 Parallele Präfixsummen-Berechnung In diesem Abschnitt untersuchen wir einen parallelen Algorithmus für das folgende Problem: Das Präfixsummen-Problem Gegeben: Feld von & ganzen Zahlen Gesucht: Feld , das die Präfixsummen zu ) Für das. Beispiel . . / + enthält, also für jedes Anfangsstück die Summe . ) + ist die gewünschte Präfixsummenfolge 1 1 . Wie gehen wir vor, wenn wir diese Folge parallel berechnen wollen? Die Idee hierzu basiert auf der rekursiven Paarbildung. Zunächst werden Paare gebildet, die dann unabhängig werden. In unserem Beispiel. wären . voneinander . addiert . das . z.B. . Die neue Folge ist nur. noch halb so lang wie die Originalfolge. Die Präfixsummenfolge davon ist . / . Wenn wir daraus nun die Präfixsummenfolge unserer Originalfolge 1 berechnen wollen, geht das folgendermaßen. Für jedes gerade Element ( gerade), können ) ) wir das Ergebnis + direkt aus der errechneten Präfixsummenfolge %+ übernehmen. Dies ist nicht möglich für) die Elemente mit ungeradem Index. Für diese Elemente muss ) ) noch der Term + zu 0 + hinzuaddiert werden. 1 1 Algorithmus 45 realisiert diese Idee. Dabei wird davon ausgegangen, dass & für ein ganzzahliges gilt. Falls das nicht der Fall ist, kann um entsprechend viele Stellen mit 6.3. PARALLELE PRÄFIXSUMMEN-BERECHNUNG 113 Null-Einträgen verlängert werden. Schritt 1 legt das Rekursionsende fest. In Schritt 2 wird jeweils die Paarbildung durchgeführt. Schritt 3 startet jeweils einen neuen rekursiven Aufruf für alle Prozessoren gemeinsam. Und Schritt 4 setzt die Ergebnisse der PräfixsummenBerechnung der Paare um in die korrekten Präfixsummen der gegebenen Originalfolge. Algorithmus 45 . Parallel Präfixsummen Rekursiv ). ). + + ; STOP 1: Falls & dann . ) ) + 2: Für alle : Falls. gerade dann 0 + ) $ #" " " 0 : Berechne rekursiv + 3: Für alle , ) ) 0 + sonst 4: Für alle : Falls gerade dann + ) .) + ) + + """ ) )) ) + 0 + + ) + Das benötigte Rechnermodell ist hier die CREW, denn in Schritt (4) wird jeweils das gleiche Feld von verschiedenen Prozessoren gelesen. Allerdings kann dieser Algorithmus leicht modifiziert werden, sodass er bereits auf einer EREW-PRAM läuft: Es genügt z.B. aus Schritt (4) zwei Schritte zu machen, die jeweils nur die geraden bzw. die ungeraden abarbeiten. Analyse der Effizienz: Schritte (1)-(4) benötigen jeweils bis zu 5& & Prozessoren und $ konstante Zeit 4& . Schritt (3) führt eine Rekursion auf 0 Werten durch und benötigt hierfür $ bis zu 0 Prozessoren bei einer Laufzeit von 5& & . Dies ergibt die parallele Arbeit 5& 4& & . Die serielle Arbeit der Präfixsummen-Berechnung ist 4& 5& . Daraus folgt, dass Algorithmus 45 ein effizienter Algorithmus ist, jedoch kein optimaler. Im folgenden werden wir sehen, wie wir den Algorithmus durch leichte Modifikation in einen optimalen Algorithmus umwandeln können. Die Idee dabei ist, die parallele Arbeit durch Einsatz von weniger Prozessoren zu verringern. Sie beruht auf der Beobachtung, dass die & Prozessoren nur in der ersten Rekursionsstufe benötigt werden. In der zweiten $ Rekursionsstufe genügen bereits 0 , u.s.w. Über alle & Stufen fällt also nicht wirklich $ $ $ & & Arbeit an, sondern nur & 0 5& . Brents Lemma beschreibt, wie man im Allgemeinen einen parallelen Algorithmus, der viele Prozessoren benötigt, durch einen Algorithmus mit wenigen Prozessoren simulieren kann. Genauer besagt es das folgende: Brents Lemma Gegeben sei eine parallele Rechnung mit Schritten, die in jedem Schritt parallele Operationen ausführt. Somit ist die Anzahl der verwendeten Prozessoren . Sei — mit . Man kann diese Rechnung — unter gewissen Voraussetzungen nur & Prozessoren durchführen, indem man jeden einzelnen Schritt durch Schritte KAPITEL 6. PARALLELE ALGORITHMEN 114 ersetzt. Die neue parallele Laufzeit ist dann . " Wir wenden Brent’s Lemma auf unser Beispiel an. Hier ist 5& Um einen optimalen Algorithmus zu erreichen, muss gelten 4& 4& & 4& 5& und & . und 5& Dies erreichen wir durch die Wahl von $4& & & Konkrete Umsetzung $ Wir zeigen im folgenden, wie wir die 5& $ Prozessoren genau einsetzen. Jedem 2$ Prozessor ist nun in Schritt (2) nicht mehr nur ein Zahlenpaar, sondern 0 Zahlenpaare zugeordnet. Diese arbeitet er sequentiell ab. Prozessor 1 übernimmt das erste & starke Zahlenpaket, Prozessor 2 das nächste, u.s.w. In Schritt (3) erfolgt der erste Rekursionsaufruf, von dem wiederum jeder Prozessor statt $ eine 0 Berechnungen auszuführen hat. Mit jeder weiteren Rekursion halbiert sich die “Überlast”, die jeweils sequentiell ausgerechnet wird. Und zwar solange, bis die Länge der aktuellen Folge kleiner gleich der Anzahl der Prozessoren ist. Danach ist jeder Prozessor wieder nur für eine Zahl zuständig (wie in unserem Originalalgorithmus 45). Insgesamt ergibt sich eine neue Gesamtlaufzeit von 4& & & & . . . & " $ Daraus folgt, dass die Prozessorallokation bei der Präfixsumme mit nur $ Prozessoren keinerlei Probleme bereitet. Somit haben wir einen optimalen Algorithmus, denn die Arbeit ist 4& 4& & & & 5& " 6.4. ANWENDUNGEN DER PRÄFIXSUMMEN-BERECHNUNG 115 6.4 Anwendungen der Präfixsummen-Berechnung 6.4.1 Komprimieren eines dünn besetzten Arrays Wir wollen das folgende Problem mit Hilfe der Präfixsummen-Berechnung lösen. Array-Komprimierung Gegeben: Array mit vielen Nullen und wenigen anderen Zeichen Gesucht: Array , das aus den Nicht-Null-Zeichen von besteht; die Reihenfolge der Zeichen muss erhalten bleiben. / / . / / / / Zum Beispiel ist das Ergebnis der Folge die Folge . . Dies können wir nun sehr einfach mit Hilfe von Algorithmus 45 berechnen, siehe Algorithmus 46. Algorithmus 46 Komprimierung eines Arrays ) ) / / 1: Für alle : Falls dann sonst + + ) 2: Für alle : Berechne die Präfixsumme für + ) / ) ) ) 3: Für alle : Falls dann + ++ + . ) ) + + Die Analyse ist einfach. Schritte (1) und (3) laufen in konstanter Zeit bei linear vielen Pro$ zessoren, Schritt (2) benötigt Zeit & bei $ Prozessoren. Beachten Sie, dass in Schritt (2) alle Prozessoren zusammen genau einmal Algorithmus 45 aufrufen. Der Algo$ rithmus läuft auf einer EREW-PRAM. Die Arbeit ist 4& & & $ & 5& . Somit ist der parallele Algorithmus optimal. 6.4.2 Simulation eines endlichen Automaten Ein endlicher Automat besteht aus einer endlichen Menge von Zuständen und Zustandsübergängen. Er liest Zeichen für Zeichen ein und verarbeitet diese. Ein endlicher Automat lässt sich als Graph darstellen. Abbildung 6.6 zeigt ein Beispiel eines endlichen Automaten. Der Anfangszustand sei 1. Bei Eingabe eines Zeichens ‘ ’ geht der Automat in den Zustand 3 über, während er bei Eingabe von ‘ ’ vom Anfangszustand in den Endzustand 4 übergeht. Unser Ziel ist es nun, für einen beliebigen gegebenen Eingabestring den Endzustand eines endlichen Automaten zu berechnen. Wir lösen diese Aufgabe mit Hilfe der Präfixsummen-Berechnung. Dazu ersetzen wir zunächst jedes Eingabezeichen durch seine Übergangsfunktion: . 2 . 2 . 1 1 . Mit jedem Eingabestring verbinden wir nun eine zusammengesetzte Übergangsfunktion, z.B. KAPITEL 6. PARALLELE ALGORITHMEN 116 b b 2 a a b 1 3 a a 4 b Abbildung 6.6: Beispiel eines endlichen Automaten . . 2 Zwei Übergangsfunktionen werden zusammengesetzt, indem sie nacheinander ausgeführt werden. Der Präfixsummen-Algorithmus kann hierfür benutzt werden, indem die Addition durch die Zusammensetzungsfunktion ersetzt wird. Damit sind die Präfixsummen gerade die Übergangsfunktionen aller Anfangsstücke der Eingabe. Endliche Automaten können sehr vielseitig in der Informatik und in Anwendungen eingesetzt werden. Im folgenden sehen wir, wie man die Addition zweier Zahlen auf der Ebene der digitalen Schaltung als endlichen Automaten darstellen kann. 6.4.3 Addier-Schaltung Gegeben sind zwei Zahlen $ in Binärdarstellung. in Binärdarstellung mit & Bits. Gesucht ist die Summe Beispiel: 1 0 1 0 1 1 1 0 0 1 0 0 1 1 Übertrag 0 0 1 0 1 1 1 Summe 1 1 0 1 0 1 0 Bei der dualen Addition von und bilden sich an gewissen Stellen Überträge, die sich jeweils ein Bit weiter nach links fortpflanzen. Mit einem Prozessor kann man die Addition von und in & Schritten (unter Berücksichtigung der Überträge) von rechts nach links durchführen. Das Propagieren der Überträge kann durch den in Abbildung 6.7 dargestellten endlichen Automaten modelliert werden. Ein Zustand entspricht dabei dem Übertrag in die nächste Stelle bei der Addition. Befindet sich z.B. der Automat nach dem -ten Übergang im 6.4. ANWENDUNGEN DER PRÄFIXSUMMEN-BERECHNUNG 117 2 0 1 0,1 1,2 0 Abbildung 6.7: Endlicher Automat für Übertragsbit . Zustand 0, so ist der Übertrag an die -te Stelle 0. Die Übergangsfunktion hängt von der Summe der beiden -ten Bits von und ab. Sind, z.B. beide Bits 1, d.h. die Summe ist 2, und ist der Zustand 0, so geht der Automat in den Zustand 1 über; er merkt also eine 1 als Übertrag vor. Algorithmus 47 Addier-Schaltung ) + für alle Input: Eingabe der bitweisen Summe in 1: Für alle : Simulation des endlichen Automaten durch Präfixsumme ) ) 2: Für alle : Addiere Ausgabezustand + zum dazugehörigen Input 3: Für alle : Falls Summe ungerade dann setze Bit=1 sonst Bit=0 ) .+ + Algorithmus 47 berechnet die Summe mit Hilfe des endlichen Automaten. Als Eingabe des Algorithmus genügen die bitweisen Summen der beiden Zahlen und beginnend bei . . / / . den niederwertigsten Bits. Dies ist in unserem Beispiel der Vektor $ Die Eingabe kann mit & Prozessoren in konstanter Zeit, und mit $ Prozessoren in logarithmischer Zeit berechnet werden. Danach simulieren wir den endlichen Automaten mit Hilfe der Präfixsummen-Berechnung in Zeit & . Nun wird für jedes Bit der jeweilige Zustand des Übertragungsbits zum dazugehörigen Input addiert. Falls die Summe ungerade ist, wird das Bit gleich eins gesetzt und sonst 0. Auch dieser Schritt kann mit $ 2$ Prozessoren in logarithmischer Zeit berechnet werden. Insgesamt berechnet der $ Algorithmus die Summe mit Hilfe von $ Prozessoren in Zeit & und ist somit optimal. Dieser Algorithmus kann direkt in eine digitale Schaltung übertragen werden. Man erhält eine Schaltung aus linear vielen Bausteinen, die eine duale Addition in logarithmischer Zeit ausführt. Weiterführende Literatur Eine kurze Einführung, die auch als Vorlage für diesen Abschnitt genommen wurde, ist der Artikel Widmayer: “Parallele Präfix-Summation” in dem Buch von T. Ottmann: “Prinzipien des Algorithmenentwurfs” (s. Literaturhinweise (4)). Eine ausführliche Einführung und Behandlung bietet das Buch von J. Jaja: “An Introduction to Parallel Algorithms”, Addison Wesley, 1992.