Grundlagen der Informatik 2 – Algorithmik Vorlesung 1 und 5 Prof. Dr. Wolfram Conen [email protected] Raum: P -1.08 Version 0.9β , 1. Mai 2005 c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Inhalte • Dijkstra: Korrektheit, Datenstrukturen zur effizienten Implementierung (Priority Queues, Exkurs: Heap Sort, Radix Sort, Bucketsort); Minimal spannende Bäume (Union-Find) • Greedy-Algorithmen, Huffman-Codierung, Hashing • Algorithmen für uninformierte und informierte Suche nach Problemlösungen • NP-Vollständigkeit: TSP und andere schwere Probleme; Approximation • Strings und Suffix-Trees c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 2 Was wir nicht mehr tun können . . . • (Effiziente) randomisierte Algorithmen zur (exakten oder approximativen) Problemlösung • Andere Modelle der Berechenbarkeit (Termersetzungssysteme, DNA-Computer etc.) • Logik und formal nicht-erkennbare Wahrheiten (Gödel) • Moderne Logik und das Semantic Web (Description Logic, Temporal and Action Logics) • Weitere Grundlegende Algorithmen und Methoden der KI, z.B. zum maschinellen Lernen, zur symbolischen Wissensverarbeitung, zum Data Mining, zur Sprachverarbeitung etc. • . . . und viel, viel mehr, das auf den Inhalten unserer GIN1a/1b/2-Veranstaltungen aufbaut. c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 3 Ziele aus der Modulbeschreibung . . . • Erkennen der grundlegenden Bedeutung von mathematischen/theoretischen Instrumenten für das Finden und die Analyse von Problemlösungen. • Beherrschen der grundlegenden Begrifflichkeiten, der wichtigsten Resultate und der wesentlichen Beweisverfahren. • Überblicksartige Kenntnisse der grundlegenden theoretischen Resultate und Methoden, die wichtigen Einfluss auf weitere Felder der Informatik haben (z.B. auf Algorithmik, Sprachen, künstliche Intelligenz). c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 4 Literatur... Kaufen Sie erstmal nichts! Ich nenne Literatur bei den einzelnen Inhalten! Für die Jäger und Sammler unter ihnen (zum Nachschlagen für den Schrank) hier das meist gelobte Buch zu Algorithmen und Datenstrukturen: Cormen, Leierson, Rivest, Stein Introduction to Algorithms, MIT Press, 2001 (2. Auflage, 1202 Seiten, ca. 60 Euro) Für den Blick über den Tellerrand dieser Vorlesung: • Schöning: Ideen der Informatik, Oldenbourg, 2002 • Gritzmann, Brandenberg: Das Geheimnis des kürzesten Weges, Springer, 2. Aufl., 2003 c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 5 Unterlagen... Für Skriptversionen, Übungsblätter, aktuelle Nachrichten usw. guckst du wie gehabt hier: http://www.informatik.fh-gelsenkirchen.de/conen c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 6 Algorithmik: Dijkstra Dijkstra revisited (1) Input: Gewichteter Digraph G = (V, E) mit nicht-negativen Gewichten Output: Kürzeste s, v -Wege und deren Längen Distanz(v ) für alle v ∈ V BEGIN S ← {s}, Distanz(s) ← 0 /* Bogenlänge(s, v) = ∞, wenn es keinen Bogen von s nach v gibt */ (1) FOR ALL v ∈ V /{s} DO Distanz(v ) ← Bogenlänge(s, v ); Vorgänger(v ) ← s; END FOR WHILE S 6= V DO /* ⇐= Hier auf alle Knoten erweitert! */ (2) finde v ∗ ∈ V \S mit Distanz(v ∗ ) = min{Distanz(v ): v ∈ V \S}; (3) S ← S ∪ {v ∗ } (4) FOR ALL v ∈ V \S DO IF Distanz(v ∗ )+Bogenlänge(v ∗ , v ) < Distanz(v ) THEN Distanz(v ) ← Distanz(v ∗ )+Bogenlänge(v ∗ , v ); Vorgänger(v ) ← v ∗ Satz 1. D IJKSTRAS Algorithmus arbeitet korrekt. Beweis: Wir zeigen, dass die folgenden Zusicherungen vor jeder Ausführung von (2) und am Ende gelten: (i) Für alle Knoten v ∈ S und alle Knoten w ∈ V \S gilt Distanz(v ) ≤ Distanz(w). (ii) Für alle Knoten v ∈ S gilt: Distanz(v ) ist die Länge eines kürzesten s-v -Weges in G. Falls Distanz(v ) < ∞, dann gibt es einen s-v -Weges der Länge Distanz(v ), dessen letzter Bogen (Vorgänger(v ),v ) ist (außer für v = s) und dessen Knoten alle in S liegen. (iii) Für alle Knoten w ∈ V \S ist Distanz(w) die Länge eines kürzesten s-w-Weges im Untergraph W mit den Knoten VW = S ∪ {w}. Falls Distanz(v ) < ∞, dann Vorgänger(w) ∈ S und Distanz(w) = Distanz(Vorgänger(w) + Bogenlänge(Vorgänger(w,v )). c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 7 Algorithmik: Dijkstra Dijkstra revisited (2) Input: Gewichteter Digraph G = (V, E) mit nicht-negativen Gewichten Output: Kürzeste s, v -Wege und deren Längen Distanz(v ) für alle v ∈ V BEGIN S ← {s}, Distanz(s) ← 0 /* Bogenlänge(s, v) = ∞, wenn es keinen Bogen von s nach v gibt */ (1) FOR ALL v ∈ V /{s} DO Distanz(v ) ← Bogenlänge(s, v ); Vorgänger(v ) ← s; END FOR WHILE S 6= V DO /* ⇐= Hier auf alle Knoten erweitert! */ (2) finde v ∗ ∈ V \S mit Distanz(v ∗ ) = min{Distanz(v ): v ∈ V \S}; (3) S ← S ∪ {v ∗ } (4) FOR ALL v ∈ V \S DO IF Distanz(v ∗ )+Bogenlänge(v ∗ , v ) < Distanz(v ) THEN Distanz(v ) ← Distanz(v ∗ )+Bogenlänge(v ∗ , v ); Vorgänger(v ) ← v ∗ Beweis (Forts.): (Per Induktion, hier abgekürzt) Klarerweise gelten alle Bedingungen nach der Initialisierung (Induktionsanfang). Wir müssen also noch beweisen, dass (2),(3) und (4) die Gültigkeit der Bedingungen nicht verletzen. Anmerkung: Der Algorithmus terminiert natürlich: in jeder Runde wird ein Knoten aus V \S zu S hinzugefügt. Deshalb ist klar, dass die Gültigkeit von (ii) nach Abschluß des Algorithmus garantiert, dass alle kürzesten Wege gefunden wurden (denn das genau sicher (ii) zu jedem Zeitpunkt für alle Knoten aus S zu – und in S sind am Ende eben alle Knoten enthalten!) c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 8 Algorithmik: Dijkstra Dijkstra revisited (3) Input: Gewichteter Digraph G = (V, E) mit nicht-negativen Gewichten Output: Kürzeste s, v -Wege und deren Längen Distanz(v ) für alle v ∈ V BEGIN S ← {s}, Distanz(s) ← 0 /* Bogenlänge(s, v) = ∞, wenn es keinen Bogen von s nach v gibt */ (1) FOR ALL v ∈ V /{s} DO Distanz(v ) ← Bogenlänge(s, v ); Vorgänger(v ) ← s; END FOR WHILE S 6= V DO /* ⇐= Hier auf alle Knoten erweitert! */ (2) finde v ∗ ∈ V \S mit Distanz(v ∗ ) = min{Distanz(v ): v ∈ V \S}; (3) S ← S ∪ {v ∗ } (4) FOR ALL v ∈ V \S DO IF Distanz(v ∗ )+Bogenlänge(v ∗ , v ) < Distanz(v ) THEN Distanz(v ) ← Distanz(v ∗ )+Bogenlänge(v ∗ , v ); Vorgänger(v ) ← v ∗ (Wir nehmen nun an, dass die Bedingungen vor der Ausführung von (2) gegolten haben und zeigen, dass sie dann auch nach (2),(3) und (4) gelten!) Sei nun v ∗der Knoten, der im Schritt (2) ausgewählt wird (das gilt für den ganzen Rest des Beweises!) Zu (i): Für jedes v ∈ S und jedes w ∈ V \S gilt Distanz(v ) ≤ Distanz(v ∗) ≤ Distanz(w) wg. (i) und der Art der Auswahl in (2), also gilt (i) nach (3) und (4) weiterhin. c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 9 Algorithmik: Dijkstra Dijkstra revisited (4) Input: Gewichteter Digraph G = (V, E) mit nicht-negativen Gewichten Output: Kürzeste s, v -Wege und deren Längen Distanz(v ) für alle v ∈ V BEGIN S ← {s}, Distanz(s) ← 0 /* Bogenlänge(s, v) = ∞, wenn es keinen Bogen von s nach v gibt */ (1) FOR ALL v ∈ V /{s} DO Distanz(v ) ← Bogenlänge(s, v ); Vorgänger(v ) ← s; END FOR WHILE S 6= V DO /* ⇐= Hier auf alle Knoten erweitert! */ (2) finde v ∗ ∈ V \S mit Distanz(v ∗ ) = min{Distanz(v ): v ∈ V \S}; (3) S ← S ∪ {v ∗ } (4) FOR ALL v ∈ V \S DO IF Distanz(v ∗ )+Bogenlänge(v ∗ , v ) < Distanz(v ) THEN Distanz(v ) ← Distanz(v ∗ )+Bogenlänge(v ∗ , v ); Vorgänger(v ) ← v ∗ Hält (ii) nach (3) (und damit auch nach (4), denn dort wird nichts mehr an S verändert)? Um das zu prüfen, reicht es wg. der Gültigkeit von (iii) vor (3) zu zeigen, dass kein s − v ∗ -Pfad, der einen beliebigen Knoten w aus V \S enthält, kürzer sein kann, als Distanz(v ∗ ). Nehmen wir also an, dass ein solcher Pfad P mit w doch existieren würde und sei w der erste Knoten ausserhalb von S , auf den wir bei der Reise von s nach v ∗ treffen. Weil (iii) vor (3) galt, ist Distanz(w) ≤ Pfadkosten(P[s,w] ).1 Weil die Bogengewichte nicht-negativ sind, gilt Pfadkosten(P[s,w] ) ≤ Pfadkosten(P ) < Distanz(v ∗ ). Das impliziert aber Distanz(w) < Distanz(v ∗ ), im Widerspruch zur Wahl von v ∗ in (2) (hier wird versteckt nochmal (iii) verwendet: die Distanz von v ∗ war vor (2) minimal unter allen Knoten aus V \S ). 1P [s,w] ist der Teil des Weges P , der von s nach w führt, s. auch Übungen zu GIN1b (eindeutig, weil P ein Weg ist). c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 10 Algorithmik: Dijkstra Dijkstra revisited (5) Input: Gewichteter Digraph G = (V, E) mit nicht-negativen Gewichten Output: Kürzeste s, v -Wege und deren Längen Distanz(v ) für alle v ∈ V BEGIN S ← {s}, Distanz(s) ← 0 /* Bogenlänge(s, v) = ∞, wenn es keinen Bogen von s nach v gibt */ (1) FOR ALL v ∈ V /{s} DO Distanz(v ) ← Bogenlänge(s, v ); Vorgänger(v ) ← s; END FOR WHILE S 6= V DO /* ⇐= Hier auf alle Knoten erweitert! */ (2) finde v ∗ ∈ V \S mit Distanz(v ∗ ) = min{Distanz(v ): v ∈ V \S}; (3) S ← S ∪ {v ∗ } (4) FOR ALL v ∈ V \S DO IF Distanz(v ∗ )+Bogenlänge(v ∗ , v ) < Distanz(v ) THEN Distanz(v ) ← Distanz(v ∗ )+Bogenlänge(v ∗ , v ); Vorgänger(v ) ← v ∗ Noch zu zeigen ist, dass (iii) nach (2),(3) und (4) gilt: Falls für ein w aus V \S in (4) der Vorgänger auf v ∗ und die Distanz auf Distanz(v ∗ ) + Bogenlänge(v ∗ ,w) gesetzt wird (also ein Update erfolgt), dann existiert ein s-w-Weg im Teilgraph H mit den Knoten VH = S ∪ {w} der Länge Distanz(v ∗ ) + Bogenlänge(v ∗ ,w) mit dem letzten Bogen (v ∗ , w) (achten sie wieder darauf, dass (iii) für v ∗ galt). Gilt nun nach dem Update (iii) weiter für alle w aus V \S ? Nehmen Sie an, dass es nach (3) und (4) ein w und einen s-w-Weg P im Teilgraph H mit VH = S ∪ {w} gibt, der kürzer ist, als Distanz(w) (also (iii) verletzt). P muß nun v ∗ enthalten (nur dieser Knoten wurde zu S hinzugefügt), sonst wäre (iii) bereits vor (3) verletzt gewesen (achten sie darauf, dass Distanz(w) niemals ansteigt)! c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 11 Algorithmik: Dijkstra Dijkstra revisited (6) Input: Gewichteter Digraph G = (V, E) mit nicht-negativen Gewichten Output: Kürzeste s, v -Wege und deren Längen Distanz(v ) für alle v ∈ V BEGIN S ← {s}, Distanz(s) ← 0 /* Bogenlänge(s, v) = ∞, wenn es keinen Bogen von s nach v gibt */ (1) FOR ALL v ∈ V /{s} DO Distanz(v ) ← Bogenlänge(s, v ); Vorgänger(v ) ← s; END FOR WHILE S 6= V DO /* ⇐= Hier auf alle Knoten erweitert! */ (2) finde v ∗ ∈ V \S mit Distanz(v ∗ ) = min{Distanz(v ): v ∈ V \S}; (3) S ← S ∪ {v ∗ } (4) FOR ALL v ∈ V \S DO IF Distanz(v ∗ )+Bogenlänge(v ∗ , v ) < Distanz(v ) THEN Distanz(v ) ← Distanz(v ∗ )+Bogenlänge(v ∗ , v ); Vorgänger(v ) ← v ∗ Sei nun v der Vorgänger von w in P . Da v ∈ S ist, wissen wir wg. (i), dass Distanz(v ) ≤ Distanz(v ∗ ) (v ∗ wurde zuletzt ausgewählt, war also vor (2) in V \S und da galt ja (i) auch schon mit v ∈ V !) und dass Distanz(w) ≤ Distanz(v ) + Bogenlänge(v, w) (wäre es anders hätte (4) bereits in früheren Runden zu einem Update führen müssen!). Wir schließen: Distanz(w) ≤ Distanz(v ) + Bogenlänge(v, w) ≤ Distanz(v ∗ ) + Bogenlänge(v, w) ≤ Pfadkosten(P). Die letzte Ungleichung gilt, weil wg. (ii) Distanz(v ∗ ) die Länge einen kürzesten s-v ∗ -Weges in S ist und weil P einen sv ∗ -Weg und den Bogen (v, w) enthält. Aber natürlich steht Distanz(w) ≤ Pfadkosten(P ) im Widerspruch zur Annahme, dass P kürzer ist, als Distanz(w) – also kann es einen solchen Weg P nicht geben (also gilt (iii) nach (3) und (4)!). q.e.d. c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 12 Algorithmik: Heap und Co. (für Dijkstra) Bucket/Radix Sort, PQueues, Heaps [s. PPT-Präsentationen bzw. die zugehörigen PDF-Files] c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 13 GIN2 – 2. Vorlesung, SS04 Prof. Dr. Wolfram Conen 29.3.2005 Rund um Dijkstra: - Count/Bucket/Radix-Sort - Priority Queues/Heap 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 1 Schnelles Sortieren 6 12 5 9 10 11 1 2 4 8 3 7 1 2 3 4 5 6 7 8 9 10 11 12 Situation: n (Schlüssel-)Werte aus [1,n] Keine Duplikate. Kosten? O(n) 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 2 Schnelles Sortieren: (einfacher) BucketSort 6 12 5 9 6 10 11 1 Töpfe: Ergebnis: 1 6 5 6 9 10 11 12 1 5 6 6 9 10 11 12 Situation: m Töpfe, Schlüsselwerte aus [1,m], Duplikate 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 3 Schneller Sortieren: BucketSort in mehreren Phasen (Radixsort) Situation: n Werte aus [0,L,mk-1], Duplikate möglich Kosten normaler Bucketsort: O(n+mk) Idee: Wir wenden ihn mehrfach an! Beispiel: n Werte aus [0,L,m2-1] 1. Phase: Werti einfügen in Bk mit k = Werti MOD m 2. Phase: Ergebnisliste durchlaufen, Werti nach Bk mit k = Werti DIV m, dort am ENDE anfügen (allgemein: 1. MOD m, 2. DIV m, 3. DIV m2 usw.) 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 4 Schneller Sortieren: BucketSort in mehreren Phasen Beispiel: n = m = 10, Werte aus [0,L,99], 1. Phase 3 18 24 6 47 7 56 34 98 60 3 MOD 10 = 3 18 MOD 10 = 8 Töpfe: 60 24 3 34 6 47 18 56 7 98 Ergebnis 1. Phase: 60 3 24 34 6 5 47 7 18 98 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 5 Schneller Sortieren: BucketSort in mehreren Phasen Beispiel: n = 10, Werte aus [0,L,99], 2. Phase 60 3 24 34 6 56 47 7 18 98 60 DIV 10 = 6 18 DIV 10 = 1 Töpfe: Ergebnis: 26.04.2005 3 6 7 18 24 34 47 56 60 98 3 6 7 18 24 34 47 56 60 98 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 6 Radixsort aus anderer Sicht... Zahlen werden nach Zifferpositionen sortiert Zuerst nach der Ziffer ganz rechts (der am wenigsten signifikanten Ziffer) Dann rücken wir nach links... In den Phasen wird „stabil“ sortiert, d.h. die relative Reihenfolge bei Gleichstand ändert sich nicht (sonst geht es nicht!) 26.04.2005 329 457 657 839 436 720 355 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 720 355 436 457 657 839 329 720 329 436 839 355 457 657 329 355 436 457 657 720 839 7 Bucket/Radix Sort: Review Wenn wir die Größe des Schlüsselbereichs als Konstante ansehen, dann sortieren wir zu Kosten von O(n) (mit möglicherweise großen Konstanten!) Aus einer sortieren Folge können wir zu Kosten von O(1) das minimale Element finden und entnehmen Aber Dijkstra verändert ja auch noch die Distanz-Werte der Knoten... 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 8 Priority Queues INSERT: Warteschlangen, in die Elemente gemäß einer Priorität eingeordnet werden DELETE MIN: Es wird jeweils das Element höchster Priorität entnommen (das soll das Element mit dem minimalen Wert sein) Für uns noch wichtig: DECREASE KEY – der Wert eines Knotens verringert sich! Das sind genau die Operationen, die wir im Dijkstra brauchen: Initialer Aufbau des Queues (INSERT), Updates der Knotenwerte (DECREASE KEY), Entnahme des „besten“ Knotens (DELETE MIN) 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 9 Priority Queues Q ← INSERT(Q,v): Füge Knoten v mit Wert Wert(v) in Priority-Queue Q ein (Q,v*) ← DELETE MIN(Q): Liefere den Knoten mit dem minimalen Wert und lösche ihn aus dem Priority-Queue Q, liefere Q zurück Q ← DECREASE KEY(Q,v,Wert): Verringere den Wert des Knotens v auf Wert. Anmerkung: Normalerweise geben wir Q niccht explizit zurück, weil wir nur auf einer Queue arbeiten 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 10 Priority Queues: Implementierung Wie kann man das effizient implementieren? Z.B. mittels eines sogenannte Heaps! (wir betrachten zunächst nur die Operationen INSERT und DELETE MIN) Was ist ein Heap (=Haufen)? Das ist ein partiell-geordneter Baum: Definition: Ein partiell-geordneter (binärer) Baum ist ein binärer Wurzelbaum T, in dem für jeden Teilbaum T´ mit Wurzel w gilt: ∀ y ∈ T´: Wert(w) · Wert(y) 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 11 Partiell-geordneter Baum (Schlüssel-)Werte: 4 6 6 7 10 10 12 13 13 19 4 6 10 12 13 6 19 13 10 7 Alle Wurzeln erfüllen die Bedingung! Ist der Baum eindeutig? Nein. 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 12 Partiell-geordneter Baum (Schlüssel-)Werte: 4 6 6 7 10 10 12 13 13 19 4 6 10 12 13 6 19 10 7 13 Alle Wurzeln erfüllen die Bedingung! Aber der Baum ist nicht mehr „balanciert“! 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 13 Heap: INSERT Wir betrachten linksvollständige partiell geordnete Bäume: alle Ebenen bis auf die letzte sind voll besetzt auf der letzten Ebene sitzen die Knoten soweit links wie möglich 26.04.2005 Algorithm INSERT(Q,v) Füge v auf der ersten freien Position der untersten Ebene ein (wenn voll, neue Ebene beginnen) p ← Vater(v) Solange p existiert und Wert(v) < Wert(p) tue Vertausche die Werte von p und v; v ← p; p ← Vater(p) (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 14 Heap: INSERT Algorithm INSERT(Q,v) Füge v auf der ersten freien Position der untersten Ebene ein (wenn voll, neue Ebene beginnen) p ← Vater(v) Solange p existiert und Wert(v) < Wert(p) tue Vertausche die Werte von p und v; v ← p; p ← Vater(p) Einfügen von 5 4 6 p→ 6 12 13 10 19 7 13 5 10 ←v Wert(v) < Wert(p)? Klar! Also: Vertauschen! 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 15 Heap: INSERT Algorithm INSERT(Q,v) Füge v auf der ersten freien Position der untersten Ebene ein (wenn voll, neue Ebene beginnen) p ← Vater(v) Solange p existiert und Wert(v) < Wert(p) tue Vertausche die Werte von p und v; v ← p; p ← Vater(p) Einfügen von 5 4 6 ←p v→ 5 12 13 10 19 7 13 10 6 Wert(v) < Wert(p)? Klar! Also: Vertauschen! 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 16 Heap: INSERT Algorithm INSERT(Q,v) Füge v auf der ersten freien Position der untersten Ebene ein (wenn voll, neue Ebene beginnen) p ← Vater(v) Solange p existiert und Wert(v) < Wert(p) tue Vertausche die Werte von p und v; v ← p; p ← Vater(p) Einfügen von 5 p→ 4 5 ←v 12 13 10 6 19 7 13 10 6 Wert(v) < Wert(p)? Nein! Also: Fertig! 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 17 Heap: INSERT Algorithm INSERT(Q,v) Füge v auf der ersten freien Position der untersten Ebene ein (wenn voll, neue Ebene beginnen) p ← Vater(v) Solange p existiert und Wert(v) < Wert(p) tue Vertausche die Werte von p und v; v ← p; p ← Vater(p) 26.04.2005 Ist INSERT korrekt? Wir betrachten eine einzelne Vertauschung der Werte von v und p, es gilt also Wert(v) < Wert(p). Wert(p) ist minimal bzgl. aller Unterbäume von p (und damit aller Unterbäume von v – das gilt auch nach dem Positionswechsel!) Wg. Wert(v) < Wert(p) ist dann auchWert(v) nach Vertauschung minimal für alle Unterbäume, also ist der neue Baum partiell geordnet (unter der Annahme, dass der Ausgangsbaum partiell geordnet war). (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 18 Heap: DELETE MIN Algorithm DELETE MIN (Q): v* Sei w die Wurzel des Heaps mit den Söhnen sl, sr; v* ← w; Sei k der letzte Knoten (unten, rechts) Wert(w) ← Wert(k); Lösche k; Solange sl oder sr existieren und (Wert(w) > Wert(sl) oder Wert(w) > Wert(sr)) tue Vertausche den Wert von w mit dem Wert des Sohn mit dem kleineren Wert, dieser Sohn sei s; w ← s; sl ← Linker_Sohn(w); sr ← Rechter_Sohn(w) Gib Q und Min zurück. 26.04.2005 Entfernen des Minimums: w→ 4 5 10 12 13 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 6 19 7 13 10 6 ←k 19 Heap: DELETE MIN Algorithm DELETE MIN (Q): v* Entfernen des Minimums: Sei w die Wurzel des Heaps mit den Söhnen sl, sr; v* ← w; w→ 6 Sei k der letzte Knoten (unten, rechts) Wert(w) ← Wert(k); Lösche k; s= Solange sl oder sr existieren und sr → 10 sl → 5 (Wert(w) > Wert(sl) oder Wert(w) > Wert(sr)) tue Vertausche den Wert von w mit 12 6 13 10 dem Wert des Sohn mit dem kleineren Wert, dieser Sohn sei s; w ← s; 13 19 7 sl ← Linker_Sohn(w); sr ← Rechter_Sohn(w) Bedingung für s = sl erfüllt! Gib Q und Min zurück. Also: Tauschen 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 20 Heap: DELETE MIN Algorithm DELETE MIN (Q): (Q, Min) Entfernen des Minimums: Sei w die Wurzel des Heaps mit den Söhnen sl, sr; v* ← w; 5 Sei k der letzte Knoten (unten, rechts) Wert(w) ← Wert(k); Lösche k; Solange sl oder sr existieren und w→ 6 10 (Wert(w) > Wert(sl) oder Wert(w) > Wert(sr)) tue Vertausche den Wert von w mit sl → 12 sr → 6 13 10 dem Wert des Sohn mit dem kleineren Wert, dieser Sohn sei s; w ← s; 13 19 7 sl ← Linker_Sohn(w); sr ← Rechter_Sohn(w) Bedingung nicht erfüllt! Gib Q und Min zurück. Also: Fertig! 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 21 Heap: DELETE MIN Algorithm DELETE MIN (Q): (Q, Min) Ist DELETE MIN korrekt? Sei w die Wurzel des Heaps mit den Wir betrachten eine einzelne Söhnen sl, sr; vertauschung der Werte von w und s, es gilt also Wert(s) < Wert(w). v* ← w; Sei k der letzte Knoten (unten, rechts) Wert(s) ist minimal bzgl. aller Wert(w) ← Wert(k); Lösche k; Unterbäume von s. Es wurde Solange sl oder sr existieren und ausgewählt, also ist es auch (Wert(w) > Wert(sl) oder Wert(w) > minimal im Vergleich zum anderen Wert(sr)) tue Kind-Baum – das gilt auch nach Vertausche den Wert von w mit dem Positionswechsel!) dem Wert des Sohn mit dem kleineren Wert, dieser Sohn sei s; w ist möglicherweise nicht minimal w ← s; für seinen Unterbaum. Das wird sl ← Linker_Sohn(w); aber weiterbehandelt (w sinkt dann sr ← Rechter_Sohn(w) weiter!) bis schließlich Wert(w) · Wert(sl) und Wert(w) · Wert(sr). Gib Q und Min zurück. 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 22 Priority Queue Mit dem Heap lassen sich INSERT und DELETE MIN mit Aufwand O(log n) realisieren! Das gleiche gilt für Updates, also DECREASE KEY-Operationen (analog zu INSERT plus schnelles Auffinden, kommt noch)! Damit können wir (für sparsam besetzte Graphen) Dijkstra verbessern! (Wenn die Graphen Kanten in der Größenordnung von O(n2) haben, also z.B. jeder Knoten eine Kante zu jedem anderen Knoten hat, dann hilft alles nichts im Worst-Case, weil dann das Update bzw. die Anzahl der Vergleiche die Kosten dominiert) Wie es noch besser geht: spätere Veranstaltung 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 23 (Allgemeiner) Bucket Sort Was ist, wenn wir viel mehr mögliche Werte, als zur Verfügung stehende Töpfe haben? Situation: Idee: Wir teilen [0..k-1] in m Töpfe auf. z.b. k = 100, m = 5, Breite k/m, also 20 Jeder Topf steht für einen bestimmten Bereich n INTEGER aus dem Wertebereich [0,..,k-1], Platz für m Töpfe, k >> m (d.h. k ist deutlich größer als m). T1: [0..19], T2: [20..39], ... Dann füllen wir die Töpfe und sortieren diese! 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 24 Schnelles Sortieren: BucketSort Töpfe: 5, Werte 10, Wertebereich [0..99] 60 16 48 91 12 25 36 89 23 21 [0..19] Töpfe: 26.04.2005 [20..39] [40..59] [60..79] [80..99] 16 12 25 36 23 21 48 60 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 91 89 25 (Allgemeiner) Bucket Sort: Aufwand Finden des richtigen Buckets für einen Wert v durch Vergleich mit den Bucketgrenzen: Naiv: Laufe mit i von Bucket 1 bis m, Vergleiche den Wert v mit der oberen Bucketgrenze UB(Bi) Falls v <= UB(Bi), dann gehört v in Bi, stop; sonst weitersuchen Worstcase für einen einzelnen Wert: m Vergleiche (bzw. m-1, denn man muß nur bis m-1 laufen) Maximal also n*m Vergleiche, bis alle Werte ihren Platz gefunden haben (im letzten oder vorletzen Bucket) 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 26 (Allgemeiner) Bucket Sort: Aufwand Finden des richtigen Buckets für einen Wert v durch Vergleich mit den Bucketgrenzen: Besser: Binäre Suche 1: Setze aktuelles Bucketintervall [l..r] auf [1..m] 2: Falls l == r, stoppe (richtiges Intervall gefunden!) 3: Bestimme den mittleren Bucket, Bm, m = d (r+1-l)/2 e+(l-1) des Intervalls 3: Falls v · UB(Bm), dann suche weiter in [l..m] „unten“ sonst in [m+1..r] „oben“ (Update der Grenzen, zurück zu 2) Aufwand für einen Wert: O(log2(m)), für alle n*O(log2(m)) Allerdings immer! (Best case oben: alle im ersten Intervall, O(n)) 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 27 (Allgemeiner) Bucket Sort: Aufwand Binäre Suche am Beispiel: 10 Buckets [0..9],[10..19], ..., [90..99] m = 10, k = 100, Breite k/m = 10, Wert v = 27 Bucketintervall [1..10], Mittlerer Bucket: d (10+1-1) / 2 e = d 5 e = 5 Upper Bound von Bucket 5 = UB(B5) = 49, 27 <= 49? JA Bucketintervall [1..5], Mittlerer Bucket: d (5+1-1) / 2 e = d 3 e = 3 Upper Bound von Bucket 3 = UB(B3) = 29, 27 <= 29? JA Bucketintervall [1..3], Mittlerer Bucket: d (3+1-1) / 2 e = d 2 e = 2 Upper Bound von Bucket 2 = UB(B2) = 19, 27 <= 19? NEIN Bucketintervall [3..3], FERTIG. Aufwand: 3 Vergleiche von v mit Bucketgrenzen, O(log m) 3 4 Zweierlogarithmus von 10 liegt zwischen 3 und 4, 2 = 8, 2 = 16 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 28 (Allgemeiner) Bucket Sort: Aufwand Noch besser: Wir können den Bucket, in den v gehört, direkt ausrechnen! Bucketindex i = (v DIV m)+1, also ganzzahlige Division +1, weil wir hier von 1 bis m zählen, bei 0..m nicht +1. z.B. (27 DIV 10)+1 = 2+1 = 3 Kosten konstant, O(1) Man kann die Bucketaufteilung auch anders gestalten, z.B. mehr Buckets zwischen 0..49, wenn dort die meisten Werte landen, usw. (sofern man etwas über die VERTEILUNG der Werte weiß) Je nach Anzahl der Buckets und der Wertverteilung können auch andere Möglichkeiten, den Bucket für einen Wert v zu bestimmen, sinnvoll sein (s. Hashfunktionen, späteres Kapitel) Eine genaue Analyse der Durchschnittskosten hängt i.A. von der Wertverteilung und dem Verteilen der Werte auf die Buckets ab Um einen einzelnen Bucket „online“ zu sortieren, eignet sich ein HEAP ganz hervorragend! 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 29 (Allgemeiner) Bucket Sort: Aufwand Zum Platzfinden kommt der Aufwand für das Speichern in den Buckets, O(m+n) Speicher, O(m+n) Zeit m für die Zeiger/Update auf die Listen, n Einträge in den Listen (konstante Kosten für Anfügen am Beginn oder am Ende, wenn man zwei Verweise auf Anfang und Ende vorhält). Und die Kosten für das Sortieren der einzelnen Buckets! Worst-Case: alle Werte in einem Bucket, O(n log n) (z.B. mit Quicksort oder Heapsort) Best-Case: n <= m, n gleichmäßig verteilt: Sortieren kostet nichts! Average Case: hängt von der Verteilung der Werte auf die Buckets ab, wenn wir eine Gleichverteilung (Best Case!) annehmen, dann: ist d n / m e = A die Anzahl der Elemente in einem Bucket, Sortierkosten dann m * (A log A) HINWEIS: Wenn wir log A schreiben, dann meinen wir Logarithmus zur Basis 2, es sei denn, wir geben etwas anderes an Gesamt O(n)+O(m+n)+Sortierkosten, wird durch Maximum von O(m+n) und Sortierkosten (O(n log n) im Worst Case) dominiert 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 30 Weitere Alternative: Counting Sort n INTEGER, Werte aus [0..k], k = O(n) Idee: Wir zählen zu jedem Wert i aus 0..k, wie oft er in der Eingabe vorkommt, das halten wir in einem Array C fest. Dann bestimmen wir zu jedem Wert i aus 0..k, wieviele Werte in der Eingabe kleiner oder gleich i sind, wir verändern C entsprechend Wenn es keine Duplikate gibt, ist das Overkill, dann geht auch das direkte sortierte Ablegen auf der ersten Folie! Um auch mit Duplikaten umgehen zu können, dekrementieren wir C[v] um eins, sobald v abgelegt wurde Kosten Θ(k) Wenn alle Werte aus der Eingabe verschieden sind, dann gibt C[v] für jeden Wert v die Position im Ergebnisarray an Kosten Θ(k) für Initialisierung auf 0, Θ(n) für das Zählen Kosten Θ(n) Gesamtkosten Θ(k + n), for k = O(n) also Θ(n) 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 31 Counting Sort n = 8, k = 5 Eingabe: Eingabe- und Ausgabearray laufen von 1 bis n. C: Counting Sort: C auf 0 initialisieren Vorkommen zählen Anzahl <= bestimmen Eingabe sortieren Resultat: 26.04.2005 2 5 3 0 2 3 0 3 0 0 2 2 01 1 0 0 2 0 0 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 2 0 2 3 42 2 3 0 3 7 46 4 0 0 7 2 3 3 5 0 1 8 77 3 5 32 Counting Sort ist STABIL Stabiles Sortieren: Eingabezahlen mit dem gleichen Wert behalten in der Ausgabe die relative Reihenfolge ihres Auftretens in der Eingabe bei. (Counting Sort ist genau „umgekehrt“ stabil...man kann aber auch von rechts die Eingabe in das Ergebnisarray einsortieren, dann bleibt die relative Reihenfolge, z.B. der 2er, „stabil“) Normalerweise nur wichtig, wenn an dem Schlüssel noch weitere Daten hängen und diese nach einem weiteren Kriterium „vorsortiert“ sind, z.B. Adressensortieren zuerst nach Hausnummer, dann nach Strassennamen 26.04.2005 macht man natürlich schlauer durch Gruppierung: zuerst nach Strassennamen, dann Gruppenweise nach Hausnummer – mit einem stabilen Sortierverfahren geht es aber so wie oben direkt ohne Gruppenbildung Für uns aber eine wichtige Eigenschaft beim Radixsort! (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 33 Übungen Abgabe der Übungszettel Pflicht für Studenten des ZWEITEN Semesters Regeln: 3*nicht abgegeben: Gespräch, ggfs. keine Zulassung, 4*nicht abgegeben: keine Prüfung Verspätetes Nachreichen möglich für 2 Blätter Diplom-Studenten sollten diese Veranstaltung nicht hören! Erste Prüfungsmöglichkeit wäre das nächste WS (solange werden für Diplomer noch die alten Inhalte geprüft) 26.04.2005 (c) W. Conen, FH GE, GIN2, SS05, Vorlesung 2, Version 1.0alpha 34 GIN2 – 4. Vorlesung, SS05 Prof. Dr. Wolfram Conen 19.4.2005 Rund um Dijkstra: - Kosten, Dials Variante 26.04.2005 (c) W. Conen, FH GE, GIN2 1 GIN2 – 3. Vorlesung, SS05 Prof. Dr. Wolfram Conen 12.4.2005 Rund um Dijkstra: - Heap-Implementierung mit Arrays - Bottom-Up-Heaps 01.04.2004 (c) W. Conen, FH GE, GIN2 1 Heap-Implementierung mit Arrays Zur Erinnerung: Was ist ein Heap (=Haufen)? Das ist ein partiell-geordneter Baum: Definition: Ein partiell-geordneter (binärer) Baum ist ein binärer Wurzelbaum T, in dem für jeden Teilbaum T´ mit Wurzel w gilt: ∀ y ∈ T´: Wert(w) · Wert(y) Dies ist ein Min-Heap, auch ·-Heap. In Max-Heaps bzw. ≥-Heap gilt Wert(w) ≥ Wert(y), d.h. der Wert jeder Wurzel eines Teilbaums ist größergleich den Werten unter ihr. Ein Heap kann Priority-Queues unmittelbar implementieren! Aber wie implementiert man einen Heap? 01.04.2004 (c) W. Conen, FH GE, GIN2 2 Partiell-geordneter Baum (Schlüssel-)Werte: 4 6 6 7 10 10 12 13 13 19 4 6 10 12 13 6 19 13 10 7 Hier unser Heap aus der letzen Vorlesung als Baum... 01.04.2004 (c) W. Conen, FH GE, GIN2 3 Partiell-geordneter Baum (Schlüssel-)Werte: 4 6 6 7 10 10 12 13 13 19 4 6 12 13 19 Idee: Kinder von Position i sind an Pos. 2i und Pos. 2i+1 10 6 13 10 7 Und hier als Array: 4 6 10 12 6 13 10 13 19 7 Pos. 1 01.04.2004 2 3 4 (c) W. Conen, FH GE, GIN2 5 6 7 8 9 10 4 Heap: INSERT Wir betrachten links-vollständige partiell geordnete Bäume: alle Ebenen bis auf die letzte sind voll besetzt auf der letzten Ebene sitzen die Knoten soweit links wie möglich Algorithm INSERT(Q,v) Füge v auf der ersten freien Position der untersten Ebene ein (wenn voll, neue Ebene beginnen) p ← Vater(v) Solange p existiert und Wert(v) < Wert(p) tue Vertausche die Werte von p und v; v ← p; p ← Vater(p) 01.04.2004 Nun mit Array, nennen wir es H. ⇐ Das ist immer eins mehr, als das Ende des Arrays (also: erweitern!) Für gerade Pos. v ist p = i/2, sonst (i1)/2 für i>1 bzw. nichts für Wurzel ⇐ Falls H[i].wert < H[p].wert tue hilf←H[i].wert; H[i].wert←H[p].wert; H[p].wert←hilf; ⇐ Genauso, Vater wie oben finden. (c) W. Conen, FH GE, GIN2 5 Heap: INSERT Algorithm INSERT(Q,v) Füge v auf der ersten freien Position der untersten Ebene ein (wenn voll, neue Ebene beginnen) p ← Vater(v) Solange p existiert und Wert(v) < Wert(p) tue Vertausche die Werte von p und v; v ← p; p ← Vater(p) Einfügen von 5 4 6 10 12 6 13 10 13 19 7 5 p=5⇐ v = 11 Wert(v) < Wert(p)? Klar! Vertauschen! 4 6 10 12 5 13 10 13 19 7 6 pNeu= 2 vNeu= pAlt= 5 vAlt Wert(v) < Wert(p)? Klar! Vertauschen! 01.04.2004 (c) W. Conen, FH GE, GIN2 6 Heap: INSERT Algorithm INSERT(Q,v) Füge v auf der ersten freien Position der untersten Ebene ein (wenn voll, neue Ebene beginnen) p ← Vater(v) Solange p existiert und Wert(v) < Wert(p) tue Vertausche die Werte von p und v; v ← p; p ← Vater(p) 01.04.2004 Einfügen von 5 4 5 10 12 6 13 10 13 19 7 6 vNeu= 2 vAlt= 5 pNeu=1 Wert(v) < Wert(p)? Nein! Fertig! (c) W. Conen, FH GE, GIN2 7 Heap: INSERT Nach dem Einfügen von 5: 4 5 10 12 6 13 10 13 19 7 6 4 Und als Baum: 5 10 12 13 01.04.2004 6 19 (c) W. Conen, FH GE, GIN2 7 13 10 6 8 Heap: INSERT Oder „andersherum“: 4 5 10 12 6 13 10 13 19 7 6 13 19 7 12 6 6 13 5 10 10 4 01.04.2004 (c) W. Conen, FH GE, GIN2 9 Heap: INSERT Oder „andersherum“ und etwas verzerrt: 4 5 10 12 6 13 10 13 19 7 6 13 19 7 12 6 5 6 13 10 10 4 01.04.2004 (c) W. Conen, FH GE, GIN2 10 Heap mit Array DELETE MIN völlig analog Kleines Randproblem: dynamisch wachsende Arrays in manchen Programmiersprachen kein Problem Oft weiß man auch die Anzahl Objekte vorab und will diese „nur“ sortieren (oder sortiert ausgeben, wie beim Dijkstra) Initiales Einfügen aller Elemente per Insert ist dann nicht sehr effizient, soll heißen: es geht besser! 01.04.2004 (c) W. Conen, FH GE, GIN2 11 Heap mit Array: Aufbau Annahme: Wir kennen n vorab. Dann können wir alle Blattpositionen füllen, ohne Vergleiche/Tauschoperationen ausführen zu müssen! Warum? Das kann die partielle Ordnung nicht verletzen! Wie geht das? Wir füllen das Array „von hinten nach vorn“ und beginnen mit dem Einfügen per INSERT erst ab Position (n DIV 2) (für n=10 also Position 5) 01.04.2004 (c) W. Conen, FH GE, GIN2 12 Heap mit Array: Aufbau (1) Naives Einfügen-von-vorn (mit INSERT) kostet log1 + log 2 + ... + log n = Ω(n log n) Jetzt haben wir Kosten für das Füllen von O(n): 01.04.2004 Sei a ein Array mit n Elementen. Zahl der Vergleiche, um eine partielle Ordnung zu erzeugen, ist höchstens 2mal so groß, wie die Summe der Entfernungen aller Knoten bis zur Blattebene! (Jeder Knoten sinkt von ganz oben bis ganz unten) Fortsetzung nächste Folie... (c) W. Conen, FH GE, GIN2 13 Heap mit Array: Aufbau (2) Die Summe dieser Abstände übersteigt die Zahl n NICHT: Etwas die Hälfte der Knoten sind Blätter, etwa 1/4 hat den Abstand 1, etwa 1/8 den Abstand 2 usw. Beispiel: n = 31 (Tiefe 4, also 5 Schichten, vollbesetzt) Abstandssumme: 26, allgemein für vollständige Binärbäume der Tiefe k (mit n = 2k+1 – 1 Knoten): n - k -1 (best case) Beispiel: n=32 (also Tiefe 5, Schicht 6 hat nur einen Knoten!) Abstandssumme: 31, allgemein mit n = 2k+1 n-1 (worst case) Abstandssumme also insgesamt immer kleiner als n. 01.04.2004 (c) W. Conen, FH GE, GIN2 14 Heap „verkehrt“ „Normale“ Heapanwendung: Heapsort Ohne weiteren nennenswerten Platz zu beanspruchen, wollen wir „in situ“ (also direkt „am Ort“) sortieren. Das geht leichter, wenn wir einen Heap bauen, der eine „umgekehrte“ partielle Ordnungsbedingung erfüllt: die Wurzel ist größergleich als die Söhne (also ein Max- bzw. ≥-Heap) Dann wenden wir ein DELETE MAX an und tauschen die Wurzel (n-1)-mal gegen das letzte Element des Heaps (und verkürzen diesen dann „von hinten“ und sammeln so dahinter die geordneten Elemente ein! (s. Applet)) 01.04.2004 (c) W. Conen, FH GE, GIN2 15 Heapsort improved „Normaler“ Heapsort (wie gerade beschrieben) hat Worst- und average-case-Kosten von 2n log n + O(n) Zur Erinnerung: Quicksort im average-case: 1,386 n log n + O(n), worst-case: O(n2) Aber Heap-Sort kann es noch besser! 01.04.2004 (c) W. Conen, FH GE, GIN2 16 Bottom-Up Heapsort Bisher haben wir beim Einsinken gleich Ordnung geschaffen: Der kleinere Sohn wurde ausgewählt und zur neuen Wurzel gemacht. Das sind 2 Vergleiche: Söhne miteinander, Wurzel gegen kleineren (bei Min-Heap) bzw. größeren Sohn (bei MaxHeap). Jetzt stellen wir uns vor, wir hätten beispielsweise einen MinHeap bis zum Element a[2] bereits gefüllt und organisiert und fügen nun die Wurzel a[1] hinzu. Jetzt suchen wir zunächst eine „virtuellen“ Einsinkepfad bis ganz nach unten... indem wir nur zwischen den Söhnen vergleichen und in Richtung des kleineren Sohn weitergehen... 01.04.2004 (c) W. Conen, FH GE, GIN2 17 Bottom-Up Heapsort ... und folgen dann dem eben beschrittenen Pfad solange wieder nach oben, bis wir den Wert von a[1] an die Stelle des momentan betrachteten Elements q schreiben können dort ist erstmals a[1] > q. Dann lassen wir den Wert des momentan betrachteten Elements und alle anderen Werte auf die Position ihrer Väter rutschen (die Wurzel ist ja gerade leer, die wird zuletzt gefüllt, dann stoppen wir natürlich) Auf den nächsten Folien findet sich ein Beispiel! [Bottom-Up-Heapsort ist eine Idee von Prof. Wegener, U DO] 01.04.2004 (c) W. Conen, FH GE, GIN2 18 Bottom-Up Heapsort: Down-Phase ·-Heap, Wurzel mit Wert 12 wird einsortiert. 12 4 6 Min? 12 Min? 11 13 01.04.2004 19 Min? 17 Min? 15 17 13 10 12 13 20 11 16 17 11 : „Virtueller“ Einfügepfad (c) W. Conen, FH GE, GIN2 19 Bottom-Up Heapsort: Up-Phase (1) ·-Heap, Wurzel mit Wert 12 wird einsortiert. 12 4 10 6 12 13 15 12 > 11? 19 17 17 12 13 20 11 16 17 11 13 <12? : Suche nach Einfügepunkt 01.04.2004 (c) W. Conen, FH GE, GIN2 20 Bottom-Up Heapsort: Up-Phase (2) ·-Heap, Wurzel mit Wert 12 wird einsortiert. 12 12 4 4 6 10 11 6 12 13 15 11 12 19 17 17 12 13 20 11 16 17 11 13 : Ringtausch 01.04.2004 (c) W. Conen, FH GE, GIN2 21 Bottom-Up Heapsort: Performance Geht der Ringtausch bis zur Tiefe t (t · log n), erfordert das log n + (log n – t) = 2 log n – t Vergleiche. Gerade haben wir 4 + 2 Vergleiche benötigt (die grünen Ovale)! „Normaler Heapsort“ hätte 8 Vergleiche benötigt (warum?) Das benötigt im average case („Durchschnittsfall“) sehr nahe an 1*n*log n + O(n) – und besser geht es auch theoretisch für Sortierverfahren mit paarweisen Schlüsselvergleichen kaum! 01.04.2004 (c) W. Conen, FH GE, GIN2 22 Bottom-Up Heapsort: Performance In Experimenten war Bottom-Up-Heapsort etwa ab n > 400 besser, als Quicksort ... ... und ab n > 16000 besser, als Clever-Quicksort bei Quicksort sind die Konstanten im Faktor O(n) also günstiger, so dass er für kleine n, trotz der schlechteren Konstante vorne, besser ist. Für größere n ist aber Bottom-Up-Heapsort besser (mit sich vergrößerndem Vorsprung)! (genaue Analyse in Güting/Dieker) 01.04.2004 (c) W. Conen, FH GE, GIN2 23 Literatur Allgemein zur Algorithmik: Cormen, Leierson, Rivest: Introduction to Algorithms, MIT Press, 2001, 1184 Seiten, knapp über 60 Euro (DAS Standardwerk, sehr präzise, schön gesetzte Darstellung, Englisch, leider teuer, aber etwas, das man immer wieder in die Hand nehmen kann – allerdings ohne Bottom-Up-Heapsort!) – gibt es seit Oktober 2004 auch übersetzt für knapp 70 € vom Oldenbourg-Verlag (zur Qualität der Übersetzung kann ich nich nichts sagen) Bernd Owsnicki-Klewe: Algorithmen und Datenstrukturen, 2002, Wißner-Verlag, sehr gut lesbarer, hinreichend präziser „Standard“-Streifzug durch die Algorithmik, recht knappe Darstellung, aber nette Auswahl, orientiert sich u.a. auch an Cormen et. al (hat deshalb auch nichts zu Bottom-Up-Heaps), 15,80 € Ergänzend 01.04.2004 Güting, Dieker: Datenstrukturen und Algorithmen, Teubner, 2. Aufl., 2003 (krumme Grafiken und nicht sehr übersichtlich gesetzt, sonst sehr nett, Reihenfolge nicht immer glücklich gewählt – vom Problem zur Datenstruktur ist meist besser als umgekehrt, aber insgesamt les- und brauchbar, gibt es jetzt in der 3. Auflage für 29,90 – mit Bottom-Up-Heaps) Uwe Schöning: Algorithmik, Spektrum, 2001 (runder, tiefer, aber auch knapp und nicht leicht) Ottmann, Widmayer: Algorithmen und Datenstrukturen, Spektrum, 2002, dick und teuer (noch teuer derzeit, als Cormen, Leierson, Rivest, und nicht ganz so schön, aber umfassend und erprobt. (c) W. Conen, FH GE, GIN2 24 Literatur Speziell zu Heaps (für angehende Heap-Fans) z.B. Stefan Edelkamps (Juniorprof. an der Uni DO) Diplomarbeit: Weak-Heapsort: Ein schnelles Sortierverfahren ... und jede Menge Forschungspapiere, z.B. On the Performance of Weak-Heapsort (Edelkamp, Wegener, 1999) etc. (das brauchen sie natürlich nicht zu lesen, aber hier finden Sie Startpunkte, wenn sie ein „Sortier“-Spezialist werden wollen ;-) 01.04.2004 (c) W. Conen, FH GE, GIN2 25 Kosten für Dijkstra mit Priority-Queues (kurz: PQueues) Zur Erinnerung: gerichteter Graph G = (V,E) mit nicht-negativen Gewichten w(·) (Bogenlänge in unserem Algorithmus), Startknoten s ∈ V; zur einfacheren Darstellung: keine Schleifen, jeder Knoten ist von s aus erreichbar n = |V| = Anzahl Knoten, m = |E| = Anzahl Bögen Dijkstra liefert alle kürzesten Wege von s zu den anderen Knoten; in S werden die Knoten gesammelt, zu denen bereits kürzeste Wege bekannt sind In D(v) = Distanz(v) wird die bisher bekannt gewordene kürzeste Länge von s über Knoten in S zu v registriert; V(v) = Vorgänger(v) vermerkt den Vorgänger auf dem kürzesten bisher gefundenen Weg von s zu v (bei mehreren Wegen gleicher Länge wird hier der Vorgänger im ersten gefunden Weg vermerkt). Dijkstra-Ablauf: Initialisieren der Distanzen aller Knoten (INIT-Phase), S ← {s} (INSERT) Iteratives Hinzufügen aller Knoten aus V \ S zu S: 26.04.2005 jeweils Auswahl des Knotens v* mit der minimalen Distanz zu s (DELETE MIN) Anschauen aller von Bögen, die von v* ausgehen, ggfs. Updates der Distanzen (und Vorgängerbeziehung) (DECREASE KEY) (c) W. Conen, FH GE, GIN2 2 Kosten für Dijkstra mit PQueues Dijkstra-Ablauf: Initialisieren der Distanzen aller Knoten (INIT-Phase), S ← {s} Iteratives Hinzufügen aller Knoten aus V \ S zu S: jeweils Auswahl des Knotens v* mit der minimalen Distanz zu s Anschauen aller von Bögen, die von v* ausgehen, ggfs. Updates der Distanzen (und Vorgängerbeziehung) wir vernachlässigen hier, dass nur noch Kanten nach V/S geprüft werden Dijkstra Kosten: n * INSERTs n * Hinzufügen; jeweils mit 26.04.2005 1* DELETE MIN Gradout(v*) Vergleiche Insgesamt also Anzahl der aus einem Knoten herausführenden Bögen, die Summe dieser Grade über alle Knoten ist natürlich die Anzahl der Kanten, m n * INSERT n * DELETE MIN jeder Bogen wird angepackt, d.h. z.B. m Vergleiche max. DECREASE KEY-Aufrufe (maximale Anzahl Updates), s. nächste Folie (c) W. Conen, FH GE, GIN2 3 Kosten für Dijkstra mit PQueues Wieviele Updates gibt es maximal? Beispiel: Graphen für 5 Knoten 5 2 1 1 s 0 7 5 5 3 4 2 9 9 8 1 5 7 6 7 6 7 26.04.2005 Insgesamt 10 Updates und 10 Kanten Davon kann man 4 per INSERT erledigen Also 6 DECREASE KEY Wenn man eine beliebige Kante entfernt, hat man weniger Updates! Wenn man eine Kante hinzufügt, kann man nicht mehr Updates bekommen! ⇒ Maximale Anzahl Updates für 5 Knoten: 5 * 2 = 5 * (51) / 2 = 10. Maximale Anzahl allgemein für n Knoten: n*(n-1) / 2 Maximales Verhältnis Updates pro Kante: 1 Minimales Verhältnis: (n-1) Updates bei n*(n-1) Kanten = 1/n Updates: 4 + 3 + 2 + 1 (c) W. Conen, FH GE, GIN2 4 Kosten für Dijkstra mit PQueues Wie läßt sich das abhängig von m ausdrücken? Maximal kann es m = n*(n-1) / 2 Kanten geben, die zu einem Update führen können. Gibt es mehr Kanten, dann sind mindestens m – n*(n-1)/2 hiervon irrelevant. Mindestens muss es m = n-1 Kanten geben (der Graph ist zusammenhängend), und die müssen alle zu Updates führen (Annahme: jeder Knoten ist erreichbar von s, maximales Kantengewicht nicht vorher bekannt) Ausgehend von m gibt es also maximal m Updates, aber nie mehr als n*(n-1)/2 insgesamt Hier sind die Updates in der Initialisierungsrunde mitgezählt, diese führen aber nicht zu einem DECREASE KEY, da man in dieser Runde das initiale INSERT mit dem Kantengewicht von s zu diesem Knoten ausführen würde, es ist also die Anzahl der von s ausgehenden Kanten jeweils abzuziehen: Also maximal (m-1)-DECREASE KEY, aber nie mehr als (n1)*(n-2)/2. 26.04.2005 (c) W. Conen, FH GE, GIN2 5 Kosten für Dijkstra mit PQueues Von Interesse: Kosten für INSERT, DELETE MIN, DECREASE KEY Alle anderen Operationen betrachten wir als elementar zu konstanten Kosten, also Kosten in der Größenordnung O(1) Zwei Fragen: Wieviel Aufwand verursachen die Operationen für eine konkrete PQueue-Realisierung bzw. Implementierung Wie groß ist der Aufwand mindestens, unabhängig von einer Implementierung (hierzu braucht man Annahmen über die Art der Realisierung, z.B. Sortieren per Vergleich zwischen je zwei Schlüsseln)? Welche Aufwände sind interessant: Worst case (schlimmster Fall), Best case (bester Fall), Average Case („durchschnittlicher Fall“) Wenn man Operationen wiederholt ausführt, dann kann z.B. die erste Ausführung teuer sein (Einfügen des ersten Elements in eine neu zu errichtende Datenbank), weitere Folgeoperationen werden aber deutlich günstiger. Dann analysiert man die amortisierten Kosten. 26.04.2005 (c) W. Conen, FH GE, GIN2 6 Kosten für Dijkstra mit PQueues Simple Realisierung von PQueues: Annahme: alle Knoten sind eindeutig von 1 bis n nummeriert Array [1..n] speichert die Distanzen D(v) Kosten: Insgesamt also O(n2): INSERT: O(1) DELETE MIN: Suche über das ganze Array, also O(n) DECREASE KEY: O(1) n*O(INSERT) + n*O(DELETE MIN) + O(m)*O(DECREASE KEY) = n*O(1) + n*O(n) + O(m)*O(1) = O(n) + O(n2) + O(m) = O(n2), weil m < n2. Die ersten beiden Faktoren sind unabhängig von der Kantenanzahl, ihre Kosten fallen IMMER an. Der zweite Faktor dominiert alle anderen Kosten, d.h.: 26.04.2005 die Kosten fallen im worst, im average und im best case an! (c) W. Conen, FH GE, GIN2 7 Kosten für Dijkstra mit PQueues Realisierung von PQueues mit „normalem“ Heap: Annahme: alle Knoten sind eindeutig von 1 bis n nummeriert Ein Heap speichert die Knoten mit ihren Distanzen D(v) als Key Kosten: INSERT, naiver Aufbau: ein Insert pro Knoten): log1+log2+...+log n-1=Ω(n log n)=O(n log n) DELETE MIN: Minimum entnehmen: O(1) + Heap reorganisieren maximal bis zur Tiefe log n: O(log n) = O(log n) DECREASE KEY: Key finden (direkt über Verweis auf Heapelement vom Knoten aus einem Knotenarray [1..n]), also O(1) + Reorg: O(log n) = O(log n) Insgesamt also O(n): (n log n) + n*O(DELETE MIN) + O(m)*O(DECREASE KEY) = O(n log n) + n*O(log n) + O(m)*O(log n) = O(n*log n) + O(m*log n) = O((n +m) * log n) = O(m * log n) [weil alle Knoten von s erreichbar sind nach Annahme und daher m zwischen O(n) und O(n2) liegt, also nicht von n dominiert wird, dieses aber ggfs. dominiert – im ersten Fall ergibt sich O(n + n) = O(n), im zweiten O(n2 + n) = O(n2), also jeweils O(m), ebenso für alle „Zwischenfälle“!] 26.04.2005 (c) W. Conen, FH GE, GIN2 8 Kosten für Dijkstra mit PQueues Mit einer „normalen“ Heap-Implementierung von Priorityqueues erreichen wir also Kosten für Dijkstra von O(m* log n). Das ist jetzt zunächst eine Worst-Case-Abschätzung (wenn es gar keine Updates gibt, haben wir im Best case, z.B. nur O(n log n + m) an Kosten) Es ist aber auch eine average case Abschätzung, Amortisierung spielt hier keine Rolle (es wird nichts durch mehrfaches Verwenden billiger. Insgesamt ist das besser, als die O(n2)-Implementierung bei der einfachen ArrayImplementierung, falls gilt m = o(n2/log n) (sonst lohnt der Aufwand nicht!) Typischerweise gibt es wesentlich mehr DECREASE KEY-Operationen, als EXTRACT MIN. Heapvarianten, die günstigere (amortisierte) Kosten für DECREASE KEY-Operationen haben (z.B. Fibonacci-Heap, Radix-Heap, beide gemeinsam) verbessern das Laufzeitverhalten weiter. Mit Fibonacci-Heaps erreichen wir O(n log n + m), weil die amortisierten Kosten für DECREASE KEY nur bei O(1) liegen! 26.04.2005 (c) W. Conen, FH GE, GIN2 9 Kosten für Dijkstra mit PQueues Problem unserer Analysen: Wir bleiben ein wenig „unscharf“: ist ihre konkrete Implementierung effizient (in Sinne des Größenordnungmäßig erreichbaren)? Entspricht ihre Implementierung tatsächlich den Algorithmen für Heapbasierte Pqueues? Sind die Annahmen über die Kosten „elementarer“ Operation für ihre konkret verwendete Rechnerarchitektur und für ihre spezielle Maschine gerechtfertigt? Daraus ergibt sich auch, ob die „idealisierten“ Kostenanalysen für die PQueue-Operationen gerechtfertigt sind und sie diese übernehmen können. Wenn ja, ist sie auch im Hinblick auf die „Konstanten“ (die im OKalkül ausgeblendet werden) „gut“? 26.04.2005 Ist das überhaupt interessant? Ja! Denn n*d1 = n*d2 = O(n), auch wenn d1=5 und d2 = 500.000! (c) W. Conen, FH GE, GIN2 10 Kostenanalysen generell „Exaktere“ Analysen: Unterstellen sehr genau eine bestimmte (abstrakte) Rechnerarchitektur (mit Details zur Darstellung von Zahlen, exakten Kosten zu einzelnen Klassen von Operationen, etc.), z.B. verwendet Knuth in The Art of Computer Programming eine Architektur mit einer eigenen (Assembler-)Sprache, deren Kosten (und Effekte!) sehr exakt analysierbar sind und in der er die Algorithmen darstellt Überlegungen zum Speicherbedarf haben wir noch gar nicht angestellt, ab einer bestimmten Größe für die Laufzeit wichtig, wo gespeichert wird: Cache, primär (Hauptspeicher), sekundär (Platte), tertiär Speicher (Netz, CD-RW, Band) Weitere Probleme: Was genau sind „average cases“? Wie bestimmt man amortisierte Kosten? etc. Details hierzu (generell und zu vielen Algorithmen und Datenstrukturen) z.B. in Cormen, Leierson, Rivest, Stein: Introduction to Algortithms, 2nd Edition, MIT Press, 2001. Für diese und ähnliche Analysen (und für vieles mehr ;-) braucht man häufig Zählargumente (Kombinatorik) und Wahrscheinlichkeitsanalysen (Probabilistik) – Prof. Engels kann das sicher gut erklären, ansonsten kann es auch nicht schaden, mal ein passendes Buch hierzu aufzuschlagen, z.B. Steger: Diskrete Strukturen (Band 1 und 2) 26.04.2005 (c) W. Conen, FH GE, GIN2 11 Kosten für Dijkstra mit PQueues Jetzt schauen wir noch ein paar Spezialfälle an! Angenommen, alle Bogengewichte sind ganzzahlig und aus dem Intervall [1..C]. Für diesen Fall haben wir bereits eine Idee gesehen, die auf Dial (1969) zurückgeht: verwende ein Array [0..C*(n-1)] von Töpfen (Buckets), um die Knoten in Töpfe einzusortieren, die ihrem Gewicht entsprechen. Beginne vorn und schreite auf der Suche nach Knoten mit minimalem Abstand durch die Töpfe 26.04.2005 (c) W. Conen, FH GE, GIN2 12 Kosten für Dijkstra mit PQueues Der längste kürzeste Weg von s zu anderen Knoten kann höchstens (n-1) Bögen lang sein: 1 s 2 n-1 z Maximales Gewicht eines kürzesten Wegs: (n-1)*C Wir haben Erreichbarkeit unterstellt, also können wir alle Knoten ungleich s mit D(v) = (n-1)*C initialisieren. Die Knoten-IDs sind weiter aus [1..n]. Wir merken uns für jeden Knoten den Topf, in dem er gerade steckt. Machen wir das an unserem Beispielgraphen durch! 26.04.2005 (c) W. Conen, FH GE, GIN2 13 Dials simple Vorgehensweise 5 C = 9, maximales Bogengewicht C*(n-1) = 35, maximale kürzeste Weglänge 1 1 1 s 0 7 5 2 2 5 4 2 9 7 0 Init: 1 2 3 4 5 6 7 8 9 10 9 1 7 4 3 C*(n-1) s 1 2 3 4 v*=s 26.04.2005 1 2 3 (c) W. Conen, FH GE, GIN2 4 14 Kosten für Dijkstra mit PQueues 5 C = 9, maximales Bogengewicht C*(n-1) = 35, maximale kürzeste Weglänge 1 1 1 s 0 7 5 2 2 5 3 4 2 9 7 0 1 2 3 4 11 v*=4 v*=2 v*=1 v*=3 26.04.2005 5 6 3 8 9 3 2 2 7 43 4 9 8 1 5 7 6 7 6 3 4 C*(n-1) 10 4 4 (c) W. Conen, FH GE, GIN2 D(1) = 1 D(2) = 3 D(3) = 5 D(4) = 6 15 Kosten von Dials Variante Im schlimmsten Fall erstreckt sich die Suche über alle Buckets, also von 1 bis C*(n-1) = O(nC). Das ist linear: C ist eine Konstante! Aber stören kann es schon...z.B. wenn für ihre Anwendungen n im Vergleich zu C eher klein ist ... Zudem führt eine direkte, naive Implementierung natürlich zu zusätzlichem Speicherbedarf von O(nC). Gesamte Kosten: INSERT (gesamt): O(n) DELETE MIN (gesamt): maximal O(nC) DECREASE KEY: Finden (mittels Hilfsarray): O(1), Löschen/Einfügen in Bucket: O(1) Insgesamt also: O(n) + O(nC) + O(m) = O(m + nC) 26.04.2005 (c) W. Conen, FH GE, GIN2 16 Verbesserungen zu Dial? Nur ¼ des Bucket-Arrays wird im Beispiel genutzt! Wenn alle Knoten von s erreichbar sind: dann kann der nächste Knoten minimaler Distanz höchstens C Felder entfernt sein Wenn eine Distanz kleiner als C ist, z.B. C-x, und das momentane Array-Ende bei Ende liegt, dann werden die Felder [Ende-x+1..Ende] niemals besucht Ideal wäre die Suche, wenn man nur gefüllte Buckets besucht, diese nur Knoten mit gleicher Distanz enthalten und Verwaltung/Anlage/Update der Buckets billig ist. Das wird nicht gehen...aber wenig suchen und wenig und billig sortieren/finden (bei Knoten ungleichen Werts in den Buckets) ist schon ein gutes Ziel! Das versuchen Radix-Heaps, s. Literatur (im letzten Jahr haben wir die noch angeschaut, ist aber doch eine reichlich recht Datenstruktur, deshalb sei sie hier nur erwähnt). 26.04.2005 (c) W. Conen, FH GE, GIN2 17 Literatur Allgemein zur Algorithmik, nochmals erinnert sei an: Cormen, Leierson, Rivest, Stein: Introduction to Algorithms, MIT Press, 2nd Edition, 2001 (ein inhaltlich sehr gutes und optisch sehr schönes Buch, zum Nachschlagen für den Schrank, zum Lernen nicht so doll, weil ohne Lösungshinweise; zum Verstehen aber gut!) Speziell zu Datenstrukturen für Dijkstra (only for the *very* brave ones): Ahuja, Mehlhorn, Orlin, Tarjan: Faster Algorithms for the Shortest Path Problem. Verwendet Radix-Heaps, Präsentation hierzu z.B. auf James Orlins Webseite am MIT. Mikkel Thorup: Integer Priority Queues with Decrease Key in Constant Time and the Single Source Shortest Paths Problem, Proc. STOC’03, ACM, 2003 (s. diesen Link) 26.04.2005 eine abstrakt beschriebene deterministische Fibonacci-Heap-Variante als Priority-Queue für Dijkstra mit insgesamt O(m + n log log C) für ganzzahlige Gewichte aus [0..C] – das löst ein offenes Problem aus dem obigen Paper – und verbessert die bekannten Grenzen! Ohne Gewichtsgrenze C erhält er O(m + n log log n), das größenordnungsmässig nur verbessert werden kann, wenn die bisher beste bekannte Lösung für deterministisches Sortieren von Han (eben mit O(n log log n) verbesserbar wäre. Leider nicht so leicht praktisch umsetzbar... Es nutzt die Idee, dass man nicht immer das Minimum auswählen muß, sondern nur einen Knoten, der nicht mehr verbessert werden kann (darunter ist auch immer das Minimum) ...und eine große Anzahl weiterer Arbeiten, u.a. zu Fibonacci-Heaps etc. im Zusammenhang mit Dikstra (c) W. Conen, FH GE, GIN2 18 Algorithmik: K RUSKAL Spannende Bäume, Teil 2 Zur Erinnnerung: Sei G = (V, E) ein ungerichteter Graph mit Kantenbewertung w. • G heißt zusammenhängend (oder verbunden), wenn jeder Knoten von jedem anderen Knoten erreichbar ist (bei gerichteten Graphen hieße das dann stark zusammenhängend). • Ein Graph ohne Kreise (oder Zyklen) heißt kreisfrei (oder azyklisch). • Ein zusammenhängender Graph ohne Zyklen heißt auch freier Baum (frei, weil es keine ausgezeichnete Wurzel gibt). Anmerkung: Wenn der Graph nicht zusammenhängend, aber kreisfrei war, dann kann man ihn auch als Wald von freien Bäumen ansehen. Übrigens: Eine Komponente ist ein maximal zusammenhängender Teilgraph von G, d.h. es gibt keine Kante in G, die einen Knoten, der nicht im Teilgraph ist, mit diesem Teilgraph verbindet. Mit anderen Worten: andere Knoten, als die die im Teilgraph enthalten sind, sind von Knoten dieses Teilgraphs aus nicht erreichbar. c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 14 Algorithmik: K RUSKAL Die einzelnen Komponenten eines Graphs induzieren eine Zerlegung der Knotenmenge (s. GIN1b-Folien). Wenn der Graph zusammenhängend ist, dann besteht er natürlich nur aus einer Komponente. c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 15 Algorithmik: K RUSKAL Spannende Bäume, Teil 2 Freie Bäume haben z.B. die folgenden interessanten Eigenschaften: (i) Ein freier Baum mit n ≥ 1 hat genau n − 1 Kanten. (ii) Wenn man einem freien Baum eine beliebige Kante hinzufügt, entsteht ein Zyklus Machen sie sich insbesondere die letzte Eigenschaft klar! Abstrakt ist es klar: • zwischen jedem Paar von Knoten, z.B. x und y , aus V gibt es bereits einen Weg (der Baum ist ja ein zusammenhängender Graph). • Wenn sie jetzt einen Kante einfügen, die x und y miteinander verbindet (unter der Annahme, dass es zwischen diesen beiden Knoten keine direkte Verbindung gab, ein solches Paar gibt es in einem Baum immer, wenn n > 2 ist), dann gibt es einen weiteren Weg von x nach y , also haben sie einen Kreis! c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 16 Algorithmik: K RUSKAL Spannende Bäume, Teil 2 Weiteres zur Erinnerung: • Ein Spannbaum zu einem zusammenhängenden Graphen G = (V, E) mit Kantenbewertungen bzw. Gewichten w ist ein freier Baum, der alle Knoten aus V enthält und dessen Kanten eine Teilmenge von E bilden. • Das Gewicht eines Spannbaum ist die Summe der Gewichte seiner Kanten. • Spannbäume mit einem Gewicht, das im Vergleich zu allen Spannbäumen von G minimal ist (d.h. es gibt keinen Spannbaum zu G mit kleinerem Gewicht) heißen minimale Spannbäume (natürlich kann es mehrere Spannbäume mit dieser Eigenschaft geben). c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 17 Algorithmik: K RUSKAL Spannende Bäume, Teil 2 Wir kennen bereits den Algorithmus von P RIM, der einen minimalen Spannbaum, ausgehend von einem ausgezeichneten Knoten s, findet. • Der Ablauf entspricht dem D IJKSTRA-Algorithmus • Als Distanzen werden aber die Gewichte von einzelnen Kanten mitgeführt, und nicht die Gewichte von Wegen! • Diese Gewichte für einen Knoten v aus V S sind die Gewichte der jeweils besten Kante, die aus S heraus direkt zum Knoten v führt [Beispiel in ihrem Mitschrieb] c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 18 Algorithmik: K RUSKAL Spannende Bäume, Teil 2 Der P RIM-Algorithmus konstruiert also nach und nach einen minimalen Spannbaum, der in jedem Schritt um eine Kante erweitert wird. Es geht aber auch anders . . . Definition 2. [Cut] Ein Cut eines Graphen ist eine Zerlegung (alternativ: Partition) der Knoten (zum Zerlegungsbegriff s. Übungsaufgabe 13 zu GIN1b) in zwei Mengen. Eine kreuzende Kante ist eine Kante, die einen Knoten der einen Menge mit einem Knoten der anderen Menge verbindet. Satz 3. [Cut Eigenschaft] Sei für G = (V, E) die Menge Z = {U, W } ein Cut der Knotenmenge V . Sei uw eine kreuzende Kante in G mit minimalen Kosten unter allen kreuzenden Kanten, also aus der Menge {u0w0|u0 ∈ U, w0 ∈ W }. Dann gibt es mindestens einen minimalen Spannbaum zu G, der uw enthält (i) und jeder minimal spannende Baum enthält eine minimale kreuzende Kante. (ii) [Beweise s. Übung, dieser Beweis (ebenso wie der nächste) findet sich z.B. im Sedgewick, Part 5] c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 19 Algorithmik: K RUSKAL Spannende Bäume, Teil 2 Satz 4. [Cycle Eigenschaft] Gegeben sei ein Graph G und ein Graph G0 der aus G durch Hinzufügen einer Kante e entsteht. Fügt man e zu einem minimalen spannenden Baum für G hinzu und löscht eine maximale Kante auf dem resultierenden Kreis, dann erhält man einen minimalen spannenden Baum für G0. c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 20 Algorithmik: K RUSKAL Spannende Bäume, Teil 2 Dieser Satz liefert die Grundlage für den Algorithmus von K RUSKAL, der für G = (V, E) (kleine) minimale Spannbäume nach und nach zusammenfügt, und zwar wie folgt: • Zu Beginn sei T ein Graph, der genau die Knoten V enthält, aber keine Kanten. • Die Kanten aus E werden nach Gewicht sortiert. • In jeder Runde eine Kante mit minimalem Gewicht ausgewählt und aus der Menge der noch nicht betrachteten Kanten entfernt. • Wenn die Kante zwei bisher getrennte Komponenten miteinander verbindet, dann wird sie in den Graphen eingefügt (und die Komponenten werden dadurch verschmolzen). • Ansonsten wird die Kante ignoriert (sie würde zu einem Kreis führen! Warum?) • Fertig ist man, wenn T nur noch aus einer einzigen Komponente besteht. c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 21 Algorithmik: K RUSKAL Kruskal Implementierung Nach und nach die Minima aus einer Menge zu entnehmen können wir schon! (PriorityQueue) Aber wie können wir geschickt feststellen, ob eine ausgewählte Kante zu einem Kreis führt oder zwei bisher getrennte Komponenten verbindet? • Jede ungerichtete Kante kann man als zwei gerichtete Bögen darstellen. • Die Bögen sind nichts anderes als geordnete Paare. • Wir können diese geordneten Paare als Teil einer Verbunden-mit-Relation betrachten. • Wir nehmen zudem an, dass jeder Knoten mit sich selbst verbunden ist. • ... und ergänzen die Relation um Paare, die aus der Transitivität der Relation entstehen. Wir betrachten die Kanten ja nach und nach. Zu jedem Betrachtungszeitpunkt bestimmt die Kantenmenge eine Relation, die die Knoten V in Äquivalenzklassen zerlegt. c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 22 Algorithmik: K RUSKAL Spannende Bäume, Teil 2 • Jede Prüfung, ob eine Kante uv zu einem Kreis führen würde, können wir nun in die Frage übersetzen, ob u und v in der gleichen Äquivalenzklasse sind (also schon verbunden sind). • Wenn das nicht der Fall ist, dann fügen wir die Kante uv hinzu. Für unsere “Verbunden-mit”-Relation bedeutet dies, dass wir (u, v) und (v, u) hinzufügen – und alle Paare, die aus der Transitivität der Relation folgen. • Das brauchen wir aber gar nicht wirklich zu tun, denn u ist in einer Äquivalenzklasse U und v in einer Äquivalenzklasse V mit U 6= V . Durch Schaffen einer Verbindung zwischen U und V folgt mit der Transitivität, dass sich einfach eine neue Äquivalenzklasse U ∪ V bildet, die u und v enthält. • Dies entspricht genau dem Zusammenfügen zweier Komponten in T ! c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 23 Algorithmik: K RUSKAL Spannende Bäume, Teil 2 Wir können das Problem also auch mit Hilfe von Äquivalenzklassen betrachten: • Test, ob uv zu einem Kreis führt: sind u und v in der gleichen Äquivalenzklasse? • Verschmelzen von Komponenten: Vereinigung von Äquivalenzklassen Das kann man als Operationen auf Zerlegungen beschreiben (zur Erinnerung: eine Zerlegung P einer Menge M besteht aus Mengen, die überschneidungsfrei und nichtleer sind und vereinigt M ergeben) • z = find(P ,k): Liefert das Element der Zerlegung P , in dem sich k befindet • P = union(P ,z1,z2): Vereinigt z1 und z2 aus P und liefert das “neue” P zurück Beachten Sie, dass P ein Mengensystem ist, Elemente aus P also Mengen sind. Die folgende Implementierung verwendet allerdings Elemente dieser Mengen als Repräsentanten für die Mengen (so, wie jedes Element einer Äquivalenzklasse als Repräsentant der ganzen Klasse gewählt werden kann). c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 24 Algorithmik: K RUSKAL Union-Find Init(P) For i ← 0 to n do P[i] ← i Union(P,i,j) Random z in [0,1] if z = 0 then P[i] ← j else P[j ] ← i Find(P,i) if i =P[i] then return i else j ← Find(P,P[i]) P[i] ← j return j [s. auch die Informationen zu TARJAN] c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 25 Algorithmik: K RUSKAL Eine Verbesserung • Die obige Implementierung verwendet bereits die sogenannte Pfadkompression (s. Mitschrieb)2 • Sie wählt allerdings zufällig aus, ob zwei Komponenten i und j durch j oder durch i repräsentiert werden • Diese Wahl kann man auch bewußt treffen: – In einem weiteren Array wird gespeichert, wieviele Elemente sich hinter einem repräsentierenden Element verbergen – Dann wird die kleinere Menge zur größeren hinzugefügt (d.h. jedes Element zeigt dann auf den Repräsentanten der größeren Menge) – Natürlich muß man dann auch noch die Information zur Elementzahl updaten – Noch simpler (aber fast genau so gut und leichter zu analysieren) ist ein Ranking: für jeden Knoten gibt der Rank ein obere Grenze für die Höhe des Knotens an 2 die Elemente einer Äquivalenzklasse zeigen also direkt auf den Repräsentanten der Klasse (sonst könnte aus dem nach und nach erfolgenden Verschmelzen von Klassen eine “tiefe” Baumstruktur folgen, die es teurer macht, die Frage nach dem Repräsentanten der Klasse, in der ein gegebenes Element ist, zu beantworten, s. Übung.) c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 26 Algorithmik: K RUSKAL Init(P) For i ← 0 to n do P[i] ← i; rank[x] ← 0 Union(P,i,j) if rank[i] < rank[j ] then P[i] ← j else P[j ] ← i if rank[i] = rank[j ] then rank[i] ← rank[i] + 1. Find-Worst-Case: O(log n), Amortisationsanalyse für eine beliebige Folge von O(n) Union und Find Operationen führt zu O(n log∗ n) (das ist praktisch linear – s. Mitschrieb, Details s. z.B. Cormen et. al) c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 27 Algorithmik: K RUSKAL Spannende Bäume, Teil 2 Algorithmus K RUSKAL: Input: Zusammenhängender, ungerichteter Graph G = (V, E) mit Gewichten w und n Knoten Output: Minimaler Spannbaum T zu G Init(P ); Sei T ← (V, ∅); Füge alle Kanten aus E in die PQueue Q ein; kantenanzahl ← 0; while kantenanzahl < n − 1 do vw ← deleteMin(Q); a ← find(P ,v ); b ← find(P ,w); if a 6= b then insert(T ,vw); P ← union(P ,a,b); kantenanzahl ← kantenanzahl+1; end if end while c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 28 Algorithmik: K RUSKAL Spannende Bäume, Teil 2 Analyse des K RUSKAL-Algorithmus (n = Knotenanzahl, m = Kantenanzahl): • Initialisierung von P und T : O(n) • Initialisierung von Q: O(m log m) (geht natürlich auch in O(m)) • Schleife: – maximal m deleteMin O(m log m) – maximal m find und maximal n union Je nach Implementierung O(n log n + m) oder O(m log n) Da wir angenommen haben, dass G zusammenhängend ist, gilt m ≥ n − 1, insgesamt folgt also ein Aufwand von O(m log m). c 2005, Dr. W. Conen — Nutzung nur an der FH Gelsenkirchen Version 0.9β , 1. Mai 2005, Seite 29 GIN2 Vorlesung SS05 Prof. Dr. W. Conen, 2. Mai 2005, FH Gelsenkirchen -Minimal spannende Bäume mit dem Boruvka-Algorithmus V1.0b Es fehlt noch die älteste Variante... Laut Tarjan von Boruvka, 1926! Idee wie folgt: In jeder Runde werden Teilbäume (eines MST) mit ihren nächsten Nachbarn verschmolzen (wir nennen die Teilbäume im folgenden Komponenten) Die Kanten, die zu Verschmelzungen führen, werden in den MST aufgenommen Durch die Verschmelzungen werden viele Kanten irrelevant Das sind die Kanten, die Knoten innerhalb einer bereits verbundenen Komponente verbinden (also in der Komponente zu Kreisen führen würden) Zu Beginn steht jeder Knoten für eine Komponente, die nur ihn enthält V1.0b Boruvkas Algorithmus Annahme: wir haben n Knoten, m Kanten, indiziert von [0..n-1], der (ungerichtete) Graph G = (V,E) ist zusammenhängend, Ausgabe: MST Hier der Ablauf des noch folgenden Algorithmus Solange es noch mehr als eine Komponente gibt (Runde/Iteration) Setze für jede Komponente i die kürzeste in dieser Runde bisher gefundene Kante aus i heraus auf null mit einer Länge unendlich Laufe über alle Kanten xy, die noch in E sind (Phase 1) Finde die Repräsentanten der Komponenten, in denen x und y sich befinden, i für x, j für y. Entferne xy aus E, falls i = j, also x und y in einer Komponente sind Sonst prüfe, ob xy eine neue kürzeste Kante aus i bzw. j heraus ist, und, falls ja, dann vermerke dies Laufe über alle Komponenten i (Phase 2) Sei xy die kürzeste Kante, die aus i herausführt mit eine Länge D[i] Wenn x und y nicht mittlerweile in der gleichen Komponente liegen, dann nimm xy in den MST auf und vereine die Komponenten Entferne xy aus E V1.0b Boruvka 3 b 4 d 3 1 a 4 c 3 b 1 4 1 2 e d 3 1 a 4 c 3 b 1 4 1 2 e d 3 1 a 4 2 f 3 2 f 3 2 f 2 c 1 e Der Ausgangsgraph, es werden alle Kanten nach und nach betrachtet. Der „naive“ Algorithmus schreibt keine Reihenfolge vor. 3 Runde 1, Ergebnis Phase 1: Jeder Knoten steht noch für seine eigene Komponente. Die roten Kanten sind die gefundenen minimalen Kanten (die Wahl, z.B. für c ist nicht eindeutig), die aus den Komponenten hinausführen. Runde 1, Ergebnis Phase 2: Nach Betrachtung von b und c wird Kante eb überflüssig. Die grünen Kanten werden in den MST übernommen und aus E entfernt. In E sind noch die schwarzen Kanten. Es gibt jetzt nur noch ZWEI Komponenten. V1.0b Boruvka 3 b 4 d 3 1 a b 1 4 e d 3 a f 2 c 3 2 3 2 2 1 c 1 e f 3 Runde 2, Ergebnis der Phase 1: Jetzt ist das Ergebnis übrigens eindeutig. Denken Sie daran: im Prinzip wird jede schwarze Kante „angepackt“, die Kante ac wird entfernt, weil sie Knoten aus der gleichen Komponente verbindet Runde 2, Ergebnis der Phase 2: In E sind noch die schwarzen Kanten enthalten. Wir haben n-1 Kanten aufgenommen (die grünen), der MST ist komplett, es gibt nur noch eine Komponente. V1.0b Boruvkas Algorithmus: Implementierung Um die zusammenwachsenden Komponenten zu verwalten, verwenden wir Union-Find auf Arraybasis. In jeder „Solange“-Runde verwenden wir zwei Arrays, um die Informationen zu den kürzesten Kanten, die aus den Komponenten hinausführen, abzulegen: D[i] gibt die Länge der bisher gefundenen kürzesten Kante min_edge[i] an; zu Beginn jeder Runde werden alle auf ∞ bzw. null initialisiert In der Phase 1 der „Solange“-Runde werden alle Kanten, die noch in E verblieben sind, angeschaut: Die Daten werden in einem Array P mit n Einträgen abgelegt, nummeriert von 0..n-1. Zu einem gegebenen Knoten x findet Find(P,x) den Repräsentanten der Komponente, in der x liegt, ebenso verwenden wir wieder Union und Init. Falls die Kante innerhalb einer bereits gefundenen Komponente liegt, wird sie aus E entfernt Sonst wird geschaut, ob sie vielleicht kürzer ist, als die bisher gefundenen „kürzesten“ Kanten, die aus den beiden beteiligten Komponenten hinausführen – wenn ja, dann werden die Informationen in D und min_edge upgedated, und zwar für den Knoten, der die jeweilge Komponente repräsentiert, in der der jeweilie Endpunkt der betrachteten Kante liegt (wenn nein, dann geschieht nichts) In der Phase 2 der „Solange“-Runde laufen wir über alle Knoten. Wenn diese Komponenten repräsentieren (also P[i] = i ist), dann nehmen wir die min_edge zu dieser Komponente in den MST auf und vereinen die beiden Komponenten, falls die beiden Knoten, die sie verbindet, nicht bereits (durch andere in dieser Phase aufgenommene Kanten) in einer Komponente liegen. Zuletzt entfernen wir noch die Kante aus E V1.0b Boruvkas Algorithmus Boruvka(G): int P[n], Init(P), MST ← {}, int D[n], Edge min_edge[n]; while Anzahl(P) > 1 do . D[i] ← ∞, min_edge[i] ← null, jeweils für alle i . for each xy ∈ E do . i ← Find(P,x), j ← Find(P,y) . if (i = j) then E ← E - {ij} . else if w(ij) < D[i] then D[i] ← w(ij), min_edge[i] ← xy . if w(ij) < D[j] then D[j] ← w(ij), min_edge[j] ← xy . for each i in [0..n-1] do . if P[i] = i then xy ← min_edge[i] . if Find(P,x) != Find(P,y) then . MST ← MST ∪ {xy}, Union(P,Find(x),Find(y)) E ← E – {xy} V1.0b Boruvkas Algorithmus Kosten: Die Union-Find-Operation sind annähernd linear Die Anzahl von Komponenten (also bisher unverbundenen Teilen des MST) verringert sich in jeder Runde mind. um den Faktor 2 Eine nennenswerte Zahl von Kanten wird in jeder Runde entfernt Eine nicht sehr präzise, „konservative“ Abschätzung (ohne Einsparungen durch Kantenentfernungen): O(|E| log |V| log* |E|) = O (m log n log* m) Anzahl der Runden höchsten log |V|, pro Runde höchstens m Find, mit Kosten weniger als m log* m. mit Modifikationen O(m log n) erreichbar, In empirischen Tests (s. Sedgewick) Kruskal für spärlich besetzte Graphen und Prim-Varianten für stärker bis stark gefüllte Graphen unterlegen V1.0b Boruvkas Algorithmus Trotzdem ist der Algorithmus interessant, z.B. für Parallelisierungen – aber auch für Randomisierung („Verzufallisierung“) Dort werden Kanten entfernt, die sicher nicht im MST sein können... Zunächst 3 modifizierte Boruvka-Iterationen: Die Knoten der Teil-MST werden je Teil-MST zu einem einzigen Superknoten verschmolzen die Kanten werden entsprechend umnummeriert, bei parallelen wird nur die kürzeste vermerkt, Schleifen werden entfernt („Kontraktion“) Das kann man übrigens auch in den normalen Boruvka einbauen und damit Aufwand vermeiden (effektivere Reduktion der Kantenzahl) Weitere Kanten werden zufällig gewählt und geprüft, dies geschieht eingebettet in einen bestimmten Ablauf, den wir nicht im Detail anschauen werden, der aber zu zu erwartenden Kosten von O(m+n) führt – also linear! Wir schauen uns das heute immer wichtiger werdende Instrument der Randomisierung von Algorithmen noch an einem anderen Beispiel genauer an (haben wir leider nicht geschafft, ein nettes Beispiel ist ein randomisierter Algo von Uwe Schöning zur Erfüllbarkeitsprüfung von aussagenlogischen Formeln) V1.0b GIN2 – Vorlesung, SS04 Prof. Dr. Wolfram Conen 2.+3.5.2004 Inhalte: - Greedy-Algorithmen - Matroide - Greedy-Beispiel Huffman-Codierung 05.05.2005 (c) W. Conen, FH GE, GIN2 1 Greedy-Optimierung Nehmen Sie an, sie brechen nachts in eine Villa ein und wollen soviel an Werten mitnehmen, wie sie tragen können so schnell wie möglich wieder raus Es gelten folgende Nebenbedingungen 05.05.2005 Die Bewohner sind verreist, wir haben im Prinzip „reichlich Zeit“ Wir kennen den Wert aller Wertgegenstände Wir können nicht alles mitnehmen, nur ein bestimmtes Gewicht (sonst wäre „Alles mitnehmen“ optimal) Die Gewichte kennen wir auch (Handwaage ist immer dabei!) (c) W. Conen, FH GE, GIN2 2 Greedy-Optimierung Dieses Problem ist (in krimineller Verkleidung) das klassische Knapsack Problem (Rucksack-Problem), formal: Gegeben ist eine Menge M = {1,..,n} von Gegenständen mit den Werten vi und den Gewichten wi. Gesucht ist M‘ ⊆ M mit ∑i ∈ M‘ wi · wmax d.h. die Summe der Gewichte der Gegenstände in M‘ überschreitet ein vorgegebenes Maximalgewicht nicht. Der Wert der Gegenstände soll so groß wie möglich sein: MaxM‘ ⊆ M ← ∑i ∈ M‘ vi 05.05.2005 (c) W. Conen, FH GE, GIN2 3 Greedy-Optimierung Lösungsidee: Gegenstände nach dem Verhältnis von Wert zu Gewicht („Wert pro Kilo“) absteigend sortieren (der mit dem größten Wert pro Gewichtseinheit steht also vorne), z.B. in einer PQueue Dann nehmen wir solange Gegenstände aus der PQueue, wie wir sie noch tragen können Diese Strategie ist „gierig“! (English: Greedy) – man nimmt das „Vielversprechendste“ zuerst usw. Ist das immer sinnvoll? 05.05.2005 (c) W. Conen, FH GE, GIN2 4 Greedy-Optimierung Natürlich nicht (würde ich sonst so fragen...) Wir können maximal 50kg tragen, es gibt die folgenden Gegenstände Lehrbuch über Algorithmen und Datenstrukturen, Gewicht w1 = 10kg, Wert v1 = 60 Euro, d.h. 6 Euro pro kg Taschenrechner mit eingebautem Dijkstra, Gewicht w2 = 20kg, Wert v2 = 100 Euro, d.h. 5 Euro pro kg Allegorische Statue, die die ewige Schönheit von (bottom-up) Heapsort symbolisiert, Gewicht w3 = 30kg, Wert v3 = 120 Euro, d.h. 4 Euro pro kg Was liefert die Greedy-Strategie als Resultat? Wie sieht das optimale Resultat aus? 05.05.2005 (c) W. Conen, FH GE, GIN2 5 Greedy-Optimierung Aber manchmal geht es auch...sogar immer, wenn die Problemstellung entsprechend ist. Andere Gegenstände: Ein Haufen Schnipsel mit Klausurlösungen, Gewicht 10kg, Wert 60 Euro, 6 Euro pro kg Ein Haufen Goldstaub, Gewicht 20kg, Wert 100 Euro, 5 Euro pro kg Ein Haufen Silberstaub, Gewicht 30kg, Wert 120 Euro, 4 Euro pro kg Jetzt können wir die Gegenstände beliebig teilen: Also nehmen wir die Klausurschnipsel, den Goldstaub, und füllen unseren Rucksack mit Silberstaub auf (zu einem Gesamtwert von 60+100+80 = 240 Euro) Besser geht es natürlich nicht! 05.05.2005 (c) W. Conen, FH GE, GIN2 6 Greedy-Optimierung Probleme des ersten Typs (mit unteilbaren Gegenständen) heißen 0-1 Knapsack Probleme Probleme des zweiten Typs (mit beliebig teilbaren Gegenständen) heißen Fractional Knapsack 05.05.2005 Fractional Knapsack Probleme lassen sich immer „greedy“ lösen! 0-1 Knapsacks nur ab und an „zufällig“! (c) W. Conen, FH GE, GIN2 7 Greedy-Optimierung Unser „kürzeste-Wege-Problem“ lässt sich auch „Greedy“ lösen – und genau das tut der Dijkstra auch! Das gleiche gilt für das „Minimum Spanning Tree“-Problem, und auch Kruskal bzw. Prim lösen das Problem „greedy“ Ein guter Hinweis darauf ist immer die Verwendung einer PQueue... 05.05.2005 (c) W. Conen, FH GE, GIN2 8 Arbeitsweise von Greedy-Algorithmen Wir haben folgendes zur Verfügung 05.05.2005 Eine Menge von Kandidaten C, aus denen wir die Lösung konstruieren wollen (z.B. Kanten beim MST) Eine Teilmenge S ⊆ C, die bereits ausgewählt wurde Boole‘sche Funktion solution, die sagt, ob eine Menge von Kandidaten eine legale Lösung des Problems darstellt (unabhängig davon, ob die Lösung optimal ist) Eine Testfunktion feasible, die sagt, ob eine Teillösung unter Umständen zu einer kompletten legalen Lösung erweitert werden kann Eine Auswahlfunktion select, die den nächsten „vielversprechendsten“ Kandidaten liefert Eine Zielfunktion value, die uns den Wert einer Lösung angibt (c) W. Conen, FH GE, GIN2 9 Arbeitsweise von Greedy-Algorithmen Function greedy(c) S←∅ while not solution(S) and C ≠ ∅ do x ← select(C) C ← C - {x} if feasible(S ∪ {x}) then S ← S ∪ {x} if solution(S) then return S else return „There_is_no_solution“ Mit der Funktion value(S) kann man am Ende den Wert der gefundenen Lösung bestimmen (falls es eine gab...) 05.05.2005 (c) W. Conen, FH GE, GIN2 10 Welche Probleme sind „greedy“ lösbar? Wenn das Problem sich als Matroid modellieren läßt, dann kann man es „Greedy“ lösen! Aber was ist ein Matroid? [s. Übung] Es gilt sogar: ein Problem lässt sich genau dann „greedy“ lösen (allgemein für jede Gewichtungsfunktion der Elemente in die positiven reellen Zahlen), wenn es eine Matroid-Struktur aufweist (s. auch „Das Geheimnis des kürzesten Weges“, Literaturhinweis zu Gin1b) 05.05.2005 (c) W. Conen, FH GE, GIN2 11 Noch ein wichtiges Problem, das man „greedy“ angehen kann: Datenkompression Ist ihre Festplatte ständig zu klein? ...oder ihre Internet-Anbindung zu langsam? Dann ist Datenkompression ein Thema für Sie! Ziele: möglichst platzsparende Datenspeicherung bzw. Übertragung entpacken möglich, ohne Fehler in den Daten zu hinterlassen 05.05.2005 (c) W. Conen, FH GE, GIN2 12 Datenkompression Zwei Teilprozesse: Beispielfälle für „Original“-Daten: Kompression: Ein Prozess, der Daten in einer komprimierte, also „kleinere“, Form überführt Expansion: Ein Prozess, der aus der komprimierten Form die Ausgangsdaten rekonstruiert Ein Text aus 256000 Zeichen, jedes der 256 möglichen (ASCII-)Zeichen tritt genau 1000-mal auf Der Text besteht aus 256000-mal dem Zeichen ‚a‘ Beide Texte nehmen 256000*8 = 2.048.000 Bit Plattenplatz ein. 05.05.2005 (c) W. Conen, FH GE, GIN2 13 Datenkompression Betrachten wir einmal die Wahrscheinlichkeit, dass an einer bestimmten Stelle der Files ein bestimmtes Zeichen auftaucht: Im ersten File ist die Wahrscheinlichkeit für das Auftauchen jedes Zeichens an der ersten betrachteten Position gleich, nämlich 1/256, z.B. für ‚a‘ 05.05.2005 An später betrachteten Positionen verschieben sich die Wahrscheinlichkeiten abhängig von den vorher bereits betrachteten Zeichen, aber „ungefähr“ bleibt es bei der Wahrscheinlichkeit 1/256 auch an den anderen Positionen Im zweiten Fall ist die Wahrscheinlichkeit für das Auftauchen von ‚a‘ an jeder Position 1. (c) W. Conen, FH GE, GIN2 14 Datenkompression Im ersten Fall besteht eine hohe Unsicherheit darüber, welches Zeichen an der betrachteten Position auftritt. Um zwischen den verschiedenen möglichen „Ereignissen“ (das Auftreten eines bestimmten der 256 Zeichen) zu unterscheiden, müssen wir den aufgetretenen Fall genau angeben Um 256 Ereignisse zu unterscheiden, brauchen wir (log2 256) Bit (also 8 ;-) 05.05.2005 (c) W. Conen, FH GE, GIN2 15 Datenkompression Im zweiten Fall wissen wir mit Sicherheit, welches Zeichen an der betrachteten Position auftritt (nämlich „a“) Um zwischen den verschiedenen möglichen „Ereignissen“ zu unterscheiden, brauchen wir gar keine Information Wir müssen nur wissen, wieviele „a“ auftreten Insgesamt können wir das File durch „Jetzt kommen 256.000 ‚a‘“ vollständig beschreiben (also mit ungefähr 150 Bit) Intuitiv ist klar, dass man im ersten Fall nicht sehr viel komprimieren kann, im zweiten aber schon! 05.05.2005 (c) W. Conen, FH GE, GIN2 16 Datenkompression Zum Komprimieren muss man den „Eingabetext“ sinnvoll kodieren. Elementare Idee: Zeichen, die häufig vorkommen, erhalten vergleichsweise kurze Codes Das nennt man „variable Kodierung“ Sie soll den folgenden Ausdruck minimieren ∑ l(ci)*f(ci), 1 · i · n 05.05.2005 n = Anzahl der Zeichen l(ci) = Länge der Codierung des Zeichens ci f(ci) = Häufigkeit von ci im Text (c) W. Conen, FH GE, GIN2 17 Datenkompression a Häufigkeit 45 b c d e f 13 12 16 9 5 ASCII 01100001 01100010 01100011 01100100 01100101 01100110 Code 1 0 011 100 101 0011 1100 Code 2 0 101 100 111 1101 1100 Was ist schlecht am Code 1? Probieren Sie mal, 001100101 zu expandieren! Man könnte natürlich die Länge des nächsten Codes abspeichern, aber so recht macht das keinen Sinn...bei Code 2 geht das auch so! 05.05.2005 (c) W. Conen, FH GE, GIN2 18 Datenkompression Definition: Eine Codierung heißt präfix-frei, wenn kein Code Anfangsstück (=Präfix) eines anderen Codes ist Einen solchen Code kann man mit Hilfe von binären Entscheidungsbäumen finden: 05.05.2005 Die Blätter sind die zu kodierenden Zeichen Die Codes ergeben sich aus den Wegen im Baum, die von der Wurzel zu den Zeichen führen Eine Zweig nach links steht für ein 0, ein Zweig nach rechts für eine 1 [s. Mitschrieb] Eine solche Kodierung ist immer präfix-frei (warum?) (c) W. Conen, FH GE, GIN2 19 Datenkompression Unsere „neue“ Aufgabe ist also: Gegeben ist eine Datei mit zu komprimierenden Daten Man konstruiere einen binären Entscheidungsbaum, der zu einer optimalen präfix-freien Codierung des Dateiinhalts führt. Eine sehr bekannte Lösung dieser Aufgabe ist die so genannte Huffman-Codierung 05.05.2005 (c) W. Conen, FH GE, GIN2 20 Huffman-Codierung Baum zur Kodierung auf Basis der Zeichenhäufigkeit optimal aufbauen Infos über die gewählte Codierung in der erzeugten Datei mit der Komprimierung speichern Führt je nach Daten zu Komprimierungen ca. zwischen 20%-70% (im worst-case spart man nix...im Gegenteil, die Codetabelle kostet ja auch etwas) 05.05.2005 (c) W. Conen, FH GE, GIN2 21 Huffman-Codierung: Ablauf 1. 2. 3. 4. 5. Ein Durchlauf zur Bestimmung der vorkommenden Zeichen und zur Ermittlung ihrer Vorkommenshäufigkeit Aufbau des optimalen Codebaums Ableiten der Codes und Codelängen aus dem Baum Abspeichern der Codeinformationen in der Ausgabedatei Zweiter Durchlauf, um die Zeichen zu codieren, nebst Ablage in der Ausgabedatei 05.05.2005 (c) W. Conen, FH GE, GIN2 22 Huffman-Codierung: Ablauf Häufigkeitsermittlung ist klar Aufbau des Codebaums: 05.05.2005 „Gierige“ Suche nach einem optimalen Baum (Gierig, weil die Zeichen in der Reihenfolge „absteigende Häufigkeit“ genau einmal angepackt werden) (c) W. Conen, FH GE, GIN2 23 Huffman-Codierung: Baumaufbau STUDENTEN SCHLAFEN NIE (na ja, ungefähr...) Zeichen Häufigkeit blank ‚_‚ D E F N O P S T U 2 1 4 1 5 1 1 1 3 1 Erstes Ziel: die vorkommenden Zeichen in zwei Gruppen zerlegen, die möglichst gleich häufig sind 05.05.2005 Warum? Mit einem Bit können sie perfekt eine „SchwarzWeiss“-Entscheidung wiedergeben. Wenn die beiden Ereignisse „Schwarz“ und „Weiss“ gleichwahrscheinlich sind, dann brauchen sie tatsächlich ein ganzes Bit zu ihrer Unterscheidung Wenn sie ungleich verteilt sind, dann kämen sie „auf lange Sicht“ auch mit weniger aus (warum?) Wir möchten den möglichen Informationsgehalt in einem Bit optimal ausschöpfen (und nichts verschenken!) (c) W. Conen, FH GE, GIN2 24 Huffman-Codierung: Baumaufbau Der Text ist 20 Zeichen lang, wir haben 10 verschiedene Zeichen. 05.05.2005 Gruppe 1 EDFOP 8 Gruppe 2 N T ‚_‚ S U 12 Das führt zur ersten Ebene des „Konstruktions“-Baumes (s. Mitschrieb) Zerlegt man die Gruppe 1 weiter, erhält man bereits ein einzelnes Zeichen als Blatt (s. Mitschrieb) Insgesamt ergibt sich der Ergebnisbaum des Mitschriebs (c) W. Conen, FH GE, GIN2 25 Huffman-Codierung: Codierung Das führt zu folgender Codierung (Codetabelle): Zeichen blank ‚_‚ D E F N O P S T U 2 1 4 1 5 1 1 1 3 1 Code 1110 0100 00 0101 0111 11110 110 11111 Länge 4 4 2 4 4 5 3 5 Häufigkeit 05.05.2005 10 0110 2 4 Unsere Eingabe war: STUDENTEN PENNEN OFT (sorry!) Wie sieht die Kodierung aus? 11110110111110100001011000101110011100101000101 11001100101110 Diese Bitfolge kann man natürlich bei gegebener Codetabelle in eindeutiger Weise wieder expandieren – probieren sie es! (c) W. Conen, FH GE, GIN2 26 Huffman-Codierung: Implementierung Implementieren kann man das leichter „von unten“: 1. 2. 3. 4. 5. 05.05.2005 Starten mit den Blättern (eines je vorkommendem Zeichen) und ihren Häufigkeiten Suchen der beiden Blätter mit den niedrigsten Häufigkeiten, z.B. S und U Konstruktion eines Knoten, dessen linker Nachfolger der eine (hier: S) und dessen rechter Nachfolger der andere Knoten (hier: U) ist, die zugehörige Häufigkeit ergibt sich als Summe der Häufigkeiten der Kindknoten Entfernen der beiden „alten“ Knoten aus der Menge „vaterloser“ Knoten, fügen den neuen Knotens hinzu Wiederhole 2-4, bis nur noch ein Knoten übrig ist (c) W. Conen, FH GE, GIN2 27 Huffman-Codierung: Implementierung Die Implementierung mit einer PQueue ist „straightforward“ (Strickmuster: 2 raus, einen rein) Aus dem entstandenen Baum läßt sich die Codetabelle, wie bereits beschrieben, unmittelbar generieren (wie?) Komplexität des PQueue-Handlings ist für k verschiedene Zeichen wie gehabt O(k log k) 05.05.2005 Initial k Knoten einfügen dann jeweils 2 entnehmen und einen Hinzufügen, insgesamt also n-1 neue Inserts (mit steigenden Häufigkeiten) Für „längere“ Texte mit n Zeichen(vorkommen), n >> k, dominiert der (zu n lineare) Aufwand für das Einsammeln der Häufigkeiten (c) W. Conen, FH GE, GIN2 28 Huffman-Codierung: Expansion Die Codetabelle wird in der Ausgabedatei abgespeichert Aus der Codetabelle kann man direkt den Baum rekonstruieren Die Expansion läßt dann den binären „Codestring“ durch den Baum rieseln: 1. 2. 3. 05.05.2005 0 = links, 1 = rechts, die Tiefe eines Zeichens entspricht der Länge des Codes für ein Zeichen Beginn mit dem ersten Zeichen des Codestrings Wegwahl entsprechend der binären Ziffern entlang des Baumes, beginnend mit der Wurzel, bis ein Blatt erreicht wird Zum Blatt gehöriges Zeichen ausgeben und auf die Wurzel zurückgehen, wenn noch nicht alles expandiert ist, zum Schritt 2 zurückkehren (c) W. Conen, FH GE, GIN2 29 Literatur Zum generellen Greedy-Ablauf und zur HuffmanCodierung: B. Owsnicki-Klewe: Algorithmen und Datenstrukturen, 4. Auflage, Wißner-Verlag, Augsburg, 2002 (gut lesbares Buch, launig-nett geschrieben, kann beim Verstehen sicher helfen, kaum/keine Beweise, wenig Aufgaben, keine Lösungen, aber dafür nicht sehr teuer, ca. 15 Euro, und berührt viele unserer Themen) Ansonsten finden sie praktisch in allen genannten Büchern Informationen zu Greedy-Algorithmen und Codierungen Zu Matroiden: In allen guten Büchern zu Optimierung (z.B. dem von Korte, Nguyen, das sie schon aus GIN1b kennen) oder auch in Cormen, Rivest, Leierson oder bei Schöning (s. frühere Literaturangaben). 05.05.2005 (c) W. Conen, FH GE, GIN2 30 GIN 2 – Vorlesung zu Hashing, 31. Mai 2005 Prof. Dr. W. Conen FH Gelsenkirchen SS 2005 Hashing - Ausgangssituation [Einstieg: s. auch ihr Mitschrieb aus der letzten Woche] Datenstruktur „Dictionary“: Insert, Delete, Member/Search Bisher: Der Schlüssel selbst konnte „unmittelbar“ als Index in einem Array zur Speicherung der Daten verwendet werden T U 4 0 6 7 K 2K 5 Nutzlasten 1 1 9 0 8 3 2 3 4 5 6 7 8 9 U = Universum möglicher Keys (=Schlüssel); K = Tatsächlich auftretende Keys; T = Direkt-addressierte Tabelle Hashing - Ausgangssituation Datenstruktur „Dictionary“: Insert, Delete, Member/Search Jetzt: U ist sehr groß (z.B. Strings!) und |K| << |U|, d.h. die Anzahl tatsächlich genutzter Schlüssel ist eher klein. Selbst, wenn unser T genug Platz für die möglichen Schlüssel hätte, wäre das unschön: es würde sehr viel Platz nicht verwendet! Also Annahme: |U| >> |T| ≥ |K| T 0 1 U 2 = h(k1) 3 K kK 1 k3 k2 k4 4 5 = h(k2) = h(k3) 6 7 = h(k4) 8 h = Hashfunktion: U → {0,...,m-1} m-1 Kollision! Hashing - Ausgangssituation Annahme: |U| >> |T| ≥ |K| K kann auch größer, als T werden, dann gibt es aber natürlich auf jeden Fall gewisse Probleme... Lösung: Wir bilden mit einer Hashfunktion h : U → {0,...,m-1} die (möglichen und tatsächlichen) Schlüssel auf die Einträge in T ab Hauptproblem: es können Kollisionen auftreten Worst-Case: bei „ungünstiger“ Hashfunktion können alle tatsächlichen Werte auf einen Index abgebildet werden Und das selbst dann, wenn die Hashfunktion „im Prinzip“, also gemessen an U, z.B. bei Annahme einer Gleichverteilung der Auftretenswahrscheinlichkeit, „gut“ zu sein scheint! Hashing Was hätten wir gern? Eine Hashfunktion, die wenigstens im Prinzip alle Werte in [0,...,m-1] treffen kann (also surjektiv ist) Eine Hashfunktion, die die tatsächlich auftretenden Werte (also K) möglichst gut über T „streut“ also Kollisionen so weit wie möglich vermeidet Bei der Konstruktion von h weiß man eventuell bereits etwas über die Auftretenswahrscheinlichkeit der möglichen Schlüssel Wenn es um z.B. um Namen geht, dann sind manche Buchstabenkombination häufiger („Schmidt“, „Weber“), manche eher selten („Xyzmick“) Die Hashfunktion sollte „Schmidt“ und „Weber“ möglichst auf verschiedene Indices abbilden Wo „Xyzmick“ landet, ist eher nicht so wichtig... Hashing Ursache von Kollisionen: „Unvermeidbar 1“: Wenn Keywerte mehrfach auftreten können z.B. mehrere Personen mit Namen „Weber“ Randbemerkung: manchmal hilft dann natürlich, die Schlüssel zu „vergrößern“, z.B. „Vorname Nachname“ zu verwenden „Unvermeidbar 2“: Wenn K größer als T ist (aber selbst dann ist eine Minimierung von Kollisionen hilfreich!) „Vermeidbar“: Wenn |K| <= |T|, dann könnte h | K ( also „h eingeschränkt auf K“, s. GIN1b) im Prinzip injektiv sein... wenn es aber dann nicht injektiv ist, dann gibt es „unnötige“ Kollisionen! Für uns ist vor allem der „vermeidbare“ Fall interessant! Hashing Wichtige Fragen: Frage 1: Wie „designed“ man eine Hashfunktion so, dass sie „möglichst“ injektiv ist, also „vermeidbare“ Kollisionen vermeidet? Frage 2: Wie geht man mit Kollisionen um, wenn sie denn auftreten? Wir schauen uns zunächst Frage 2 an. Antworten zur Frage 1 finden Sie in ihrem Mitschrieb. Hashing: Umgang mit Kollisionen (1) Kollisionen treten auf Die Daten jeder „Kollisionsklasse“ werden in einer Liste hinter dem berechneten Indexwert für ihren Schlüssel abgelegt. Das nennt man „Chaining“, also Verkettung T 0 U k1 K k1K k3 k2 k2 k4 k4 h m-1 k3 Hashing: Umgang mit Kollisionen (2) Kollisionen treten auf Für jeden Datensatz wird ein Platz direkt in T gesucht Verschiedene Strategien möglich (z.B. Re-Hashing, Sondieren) Nebenproblem: Was passiert, wenn T überläuft T 0 Nutzlasten U k1 K kK 1 k3 „Sondieren“ in der Nachbarschaft oder zweites (drittes, ...) Hashing k3 k2 k2 k4 k4 h [s. auch Übungsaufgaben zu Hashing] m-1 Hashing: Kollisionsbehandlung (3) „Chaining“ Insert(d), Datensatz d hat den Schlüssel k Kosten: O(1) (+ Kosten für die Berechnung von h(k) – h sollte also möglichst effizient berechenbar sein! Wir nehmen O(1) an) Delete(d): O(1) Member(d): Best Case: O(1) Worst Case: O(|K|)...Aua! Mittlerer Fall, s. Mitschrieb „Platz in T suchen“ Insert(d): Kosten je nach Kollisionsbehandlung Best Case: O(1) Worst Case: O(min(|T|,|K|)) Delete(d): wie Insert Member(d): wie Insert weitere Details s. Mitschrieb Unsere Übungsaufgabe... stammt aus Knuth: The Art of Computer Programming, Volume 3 Wir wollen 31 Strings auf den Bereich [0..40] bijektiv abbilden (im Original auf [-10..30]) Es gibt 4131, ungefähr also 1050 Funktionen, die 31 Werte auf 41 Werte verteilen Es gibt 41*40*...*11 = 41!/10!, ungefähr also 1043 injektive Funktion Das sieht nach vielen aus...aber es ist nur 1 aus ungefähr 10 Millionen der „möglichen“ Funktionen! Aber sie sehen: sie hatten eine große Auswahl bei der Lösung der Übungsaufgabe! ;-) Knuths Lösung Ein MIX-Programm LD1N K(1:1) LD2 K(2:2) INC1 -8,2 J1P *+2 INC1 16,2 LD2 K(3:3) J2Z 9F INC1 -28,2 J1P 9F INC1 11,2 LDA K(4:4) JAZ 9F DEC1 -5,2 J1N 9F INC1 10 9F: LDA K CMPA TABLE,1 JNE FAILURE Beispiel-Ablauf für BE -2 (in rI1) 5 (in rI2) -2-8+5 = -5 Kommentar Wert(B)=2,Position(B)=1 Sprung, wenn 1 positiv -5+16+5 = 16 Sprung, wenn 2 leer/null Sprung, wenn Accu leer (Sprungziel eigentlich 9H) (versteht aber niemand... ;) Wenn Sie noch kein h gefunden haben, dann suchen sie noch ein wenig weiter! Literatur Hashing z.B. in Cormen et al. „Introduction to Algorithms“ oder Owsnicki-Klewe „Algorithmen und Datenstrukturen“ (s. auch frühere Literaturempfehlungen) Zum MIX-Computer von Knuth können sie direkt bei Knuth schauen („The Art of Computer Programming“) oder einen der Emulatoren ausprobieren: http://www.recreationalmath.com/mixal/ (Emulator in HTML mit Javaskript realisiert) http://www.gnu.org/software/mdk/mdk.html, der GNU-MIXDevelopment-Kit (mit Doku: http://www.gnu.org/software/mdk/manual/mdk.html) GIN2 SS05 Prof. Dr. W. Conen, 10.5.05 - Nullsummen-Spiele - Min-Max-Suche - Alpha-Beta-Pruning (späterer Termin) „Abwechselnde“ Suche Heute spielen wir TicTacToe... Bei TicTacToe gibt es drei mögliche Resultate: X gewinnt (O verliert) O gewinnt (X verliert) - unentschieden Es ist ein Null-SummenSpiel: was der eine gewinnt, verliert der andere (wenn man Sieg und Niederlage mit individuellem Nutzen bewerten würde). XOX OXO OOX „Abwechselnde“ Suche Am Zug ist O: Eine Spielsituation... O ist am Zug O kann „im Prinzip“ auf 4 Positionen setzen usw. O O X: X X X X O X O O X O O X X O X X X O O O: X X O O O X O O X X X X X O X O O X X X: X O O X O X X O X O X O O X O O X O O X O X X X O X X O X X O X O X X X X O O X O O X X O X X O X O X O O X O O X O O X O O X O X X O X X O X X O X X O X O X X O X O O X X O O X O O X O O O O X O O X O X X X X X X X O X O X O O O X X O X X X O O O O O X O X X O -- „Abwechselnde“ Suche o x ox x ox o ooo ox ox o - oo x x O Am Zug ist O: Wenn O und X optimal spielen, dann gewinnt O auf jeden Fall! O X: X X X X O X O O X O O X X O X X X O O O: X X O O O X O O X X X X X O X O O X X X: X O O X O X X O X O X O O X O O X O O X O X X X O X X O X X O X O X X X X O O X O O X X O X X O X O X O O X O O X O O X O O X O X X O X X O X X O X X O X O X X O X O O X X O O X O O X O O O O X O O X O X X X X X X X O X O X O O O X X O X X X O O O O O X O X X O -- „Ausrechnen“ eines Spiels „Im Prinzip“ kann man jedes deterministische Spiel so „ausrechnen“ Man bestimmt den Baum bis zu den Blättern, die für entschiedene Spiele (also „Endzustände“ stehen) Das Resultat gibt man nach oben weiter Bei inneren Knoten gibt man dann von unten nach oben das für den jeweiligen Spieler beste erreichbare Resultat nach weiter 3x3-TicTacToe ist immer unentschieden, wenn beide Spieler optimal spielen: Der „Max“-Spieler will das Ergebnis des anderen Spielers minimieren bzw. sein Ergebnis maximieren Der „Min“-Spieler verhält sich analog Wenn man einen Sieg des Max-Spielers mit 1 bewertet und einen Sieg des Min-Spielers mit -1, dann wählt der Max-Spieler immer den maximal bewerteten Zug, der Min-Spieler immer den minimal bewerteten Zug Deshalb spricht man auch von MinMax-Bewertung bzw. MinMax-Algo „Ausrechnen“ eines Spiels Grob kalkuliert muß man sich 1 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1 = 362880 Knoten anschauen und diese bewerten früheres Erreichen von Endzuständen wird hier vernachlässigt Wenn n die (maximale) Anzahl der Zugmöglichkeiten ist, dann entspricht das n! Es gilt: n! ∈ O(2n) (Abschätzung hierzu in der nächsten Vorlesung) Zeitaufwändig, naiv implementiert auch speicheraufwändig (geht aber gut mit Tiefensuche, dann „nur“ Zeitproblem) Übrigens: es gibt genau 39 = 19683 verschiedene Brettstellungen. Wenn man festlegt, dass immer X beginnt, dann sind damit auch die Spielsituationen eindeutig bestimmt. nicht alle sind erreichbar (weil vorher Ende wäre) Wie kommt es zur Differenz? Weil es zu praktisch jeder Stellung viele Wege gibt! „Ausrechnen“ eines Spiels Ein Computer könnte ein „ausgerechnetes“ Spiel perfekt spielen – er wüßte immer die bestmögliche Antwort! Im allgemeinen ist das „Ausrechnen“ aber viel zu teuer! Für fortgeschrittene Spielen „im Endspiel“ geht es allerdings oft auch bei „komplexeren“ Spielen (wie z.B. Dame) Für ein 6x6-TicTacToe mit 4-gewinnt Regel gibt es z.B. 336 h 1.5*1017 Stellungen ... und 36! h 3,72*1041 Knoten (1080 ist die geschätzte Zahl der Atome im sichtbaren Universium) (auch wieder ohne frühe Endzustände) Das ist ein bisschen viel für 220k-Speicher... Wie man aber dennoch erfolgreich ein Programm zum Spielen von 6x6/4TicTacToe schreiben kann – sogar für Handys –, erzählen uns jetzt 3 ihrer Kommilitonen: die Herren Cevani, Schramma und Wengler Legen Sie los! ;-) [Alpha-Beta haben wir in einer späteren Veranstaltung auf Overhead geschrieben] GIN2 – Vorlesung, SS05 Prof. Dr. Wolfram Conen 7. Mai 2005 Inhalte: - Repräsentation von Problemen - Problemlösung durch Suche SS - V1.0 (c) W. Conen, FH GE, GIN 2 1 Künstliche Intelligenz (KI) „KI: Teilgebiet der Informatik, welches versucht, menschliche Vorgehensweisen der Problemlösung auf Computern nachzubilden, um auf diesem Wege neue oder effizientere Aufgabenlösungen zu erreichen“, aus: Lämmel, Cleve: Künstliche Intelligenz, 2. Aufl. 2004, Hanser Verlag SS - V1.0 (c) W. Conen, FH GE, GIN 2 2 Problem 1 – Missionare und Kannibalen Drei Missionare und drei Kannibalen sind auf der selben Seite eines Flusses. Es gibt auf dieser Seite auch ein Boot, das ein oder zwei Leute aufnehmen kann. Problem: Finden Sie nun einen Weg, alle so auf die andere Seite zu bekommen, dass die Zahl der Kannibalen die Zahl der Missionare auf irgendeiner Seite des Flusses niemals überschreitet (dann würden die Missionare nämlich gefressen...) SS - V1.0 (c) W. Conen, FH GE, GIN 2 3 Wie löst man solch ein Problem? Man „sucht“ nach einer Lösung Aber zunächst mal muß man sich klar machen, WAS genau die Aufgabe ist Dann beginnt die Suche nach einer Lösungsmethode. Man sucht nach einer günstigen Repäsentation des Problems „Günstig“ ist sie, wenn man mit dieser Repräsentation „leicht“ eine Lösung finden kann (für das „umformulierte“, repräsentierte Problem) ... und diese Lösung sich zurück übertragen lässt auf die ursprüngliche Problemstellung – also das „tatsächliche“ Problem löst! Und natürlich sollte die Repräsentation „beherrschbar“ sein, also möglichst klein, verständlich („wartbar“), präzise, usw. Oft hat man bereits eine Methode im Hinterkopf und schaut, ob man das Problem passend repräsentieren kann (z.B. Graph-basierte Suche, Constraint Optimization, Genetic Algorithms, Neuronale Netze, sehen sie, wenn sie mögen, in INT im Master) Wenn man die Methode (ev. auch mehrere) ausgewählt hat, dann beginnt die tatsächliche Suche nach einer Lösung, und zwar „auf“ der gewählten Problemrepräsentation SS - V1.0 (c) W. Conen, FH GE, GIN 2 4 Kannibalen haben Hunger... Repräsentation des „Zustandes“ als Vektor (m,k,b) m = Anzahl Missionare 3,3,1 k = Anzahl Kannibalen b = Position des Bootes Wir brauchen nur eine Seite des Flußes darstellen, wir nehmen die linke Seite (b = 1) 3,2,1 Es gilt immer: Missionare rechts = 3-m, Kannibalen rechts = 3-k Ev. mögliche Folgezustände (naiv) zu (m,k,1): 1m,1k (m-1,k-1,0), (m-2,k,0), (m,k-2,0),(m1,k,0),(m,k-1,0) Ev. mögliche Folgezustände (naiv) zu (m,k,0): 1k 2,2,1 (m+1,k+1,1), (m+2,k,1), (m,k+2,1), (m+1,k,1),(m,k+1,1) Startzustand: (3,3,1), Zielzustand: (0,0,0) Suche nach eine Zustandsfolge vom Start zum Ziel! Problem: wir merken noch nicht, dass wir ungültige Zustände verwenden... SS - V1.0 (c) W. Conen, FH GE, GIN 2 1m,1k im Boot 2,2,0 1m 2,1,0 1m, 2k rechts...AUA! 5 Kannibalen haben Hunger... Ungültige Zustände feststellen...z.B. durch Aufzählen: 2,2,0 (m,k,b) mit m < k ∧ m > 0 oder k < m ∧ k < 3 ist ungültig Zugfolgen sind nur legal, wenn sie nicht über ungültige Zustände führen Wenn wir eine Funktion haben, die zu einem Zustnd die gültige Folgezustände ausspuckt, dann können wir direkt die Lösung finden! Wenn wir eine haben, die nur alle „möglichen“ Zustände ausspuckt, dann müssen wir noch die Gültigkeit prüfen Beides läßt sich als Graph visualisieren! (nächste Folie) SS - V1.0 1m,1k im Boot 3,3,1 Möglicherweise sind nicht alle hiervon erreichbar (überhaupt oder nur über gültige Zustände) Man könnte das auch abstrakt angeben: (2,3,1),(1,3,1),(1,2,1) (2,1,0),(2,0,0),(1,0,0) (1,0,1),(2,0,1),(2,1,1) (2,3,0),(1,3,0),(1,2,0) 3,2,1 1m 1m,1k 2,1,0 1k 1m, 2k rechts...AUA! 2,2,1 (c) W. Conen, FH GE, GIN 2 6 Die Rettung der Missionare (1) 3,3,1 2,2,0 2,3,0 2,3,1 3,2,0 1,3,0 3,1,0 3,2,1 1,2,0 2,1,0 3,0,0 3,1,1 1,1,0 SS - V1.0 (c) W. Conen, FH GE, GIN 2 7 Rettung der Missionare (2) (3,1,1) ist der Vorgänger 1,1,0 2m, 2k rechts mit Boot 2,1,1 2,2,1 1,2,1 1,3,1 2,1,0 2,0,0 1,2,0 0,2,0 (gab‘s bereits) 0,3,1 0,1,0 0,2,1 1,1,1 2,1,1 1,2,1 (gab‘s bereits) SS - V1.0 (c) W. Conen, FH GE, GIN 2 8 Rettung der Missionare (3) (0,1,0) ist Vorgänger SS - V1.0 0,2,1 1,1,1 0,0,0 1,0,0 (c) W. Conen, FH GE, GIN 2 9 Rettung...Kontrolle (1) 3,3,1 2,2,0 2,3,0 3,2,0 1,3,0 3,1,0 2,3,1 3,2,1 1,2,0 2,1,0 3,0,0 3,1,1 1,1,0 SS - V1.0 (c) W. Conen, FH GE, GIN 2 Gefundener Weg: 2 Kannibalen nach rechts: (3,1,-,0,2,B) oder 1M+1K nach rechts: (2,2,-,1,1,B) 1 Kannibale zurück oder 1M zurück, je nach Wahl oben (3,2,B,0,1,-) 2 K nach rechts: (3,0,-,0,3,B) 1 K zurück: (3,1,B,0,2,-) 2M nach rechts: (1,1,-,2,2,B) 10 Rettung...Kontrolle (2) 1,1,0 2,1,1 2,2,1 1,2,1 1,3,1 2,1,0 2,0,0 1,2,0 0,2,0 0,3,1 0,1,0 0,2,1 SS - V1.0 1,1,1 2,1,1 (c) W. Conen, FH GE, GIN 2 1,2,1 Gefundener Weg: 1M+1K nach links: (2,2,B,1,1,-) 2 M nach rechts: (0,2,-,3,1,B) 1 K nach links: (0,3,B,3,0,-) 2 K nach rechts: (0,1,-,3,2,B) 1K nach links: (0,2,B,3,1,-) oder 1M nach links: (1,1,B,2,2,-) 11 Rettung...Kontrolle (3) 0,2,1 1,1,1 0,0,0 1,0,0 Gefundener Weg: 2K nach rechts oder 1M+1K nach rechts: (0,0,-,3,3,B) Lösungen: (1) 2K, 1K, 2K, 1K, 2M, 1M+1K, 2M, 1K, 2K, 1K, 2K (2) 2K, 1K, 2K, 1K, 2M, 1M+1K, 2M, 1K, 2K, 1M, 1M+1K (3) 1M+1K, 1M, 2K, 1K, 2M, 1M+1K, 2M, 1K, 2K, 1K, 2K (4) 1M+1K, 1M, 2K, 1K, 2M, 1M+1K, 2M, 1K, 2K, 1M, 1M+1K SS - V1.0 (c) W. Conen, FH GE, GIN 2 12 Welche Probleme können auftreten? Man findet keine „griffige“ Repräsentation, weil z.B. Informationen fehlen oder „unscharf“ sind Informationen unsicher/unglaubhaft sind Man hat ein Problem vor sich, dass im allgemeinen unlösbar ist (Halteproblem) im allgemeinen „hart“ zu lösen ist (NP-komplett, EXP) Man kennt keine sinnvolle Problemlösungsmethode für die gefundene Repräsentation Im Master werden Sie einige Methoden für verschiedene Repräsentationen kennenlernen ... ... und wenn die richtige nicht dabei ist, dann können Sie mit ihrem Wissen und ihrer Cleverness vielleicht einen (Er-)Finden! SS - V1.0 (c) W. Conen, FH GE, GIN 2 13 Suche...nochmal generell Angenommen, sie wollen ein Problem lösen ... ... dann suchen sie also nach einer Lösung Viele Probleme lassen sich als Graph-Probleme modellieren manchmal ist das unmittelbar klar (MST, kürzeste Wege, TSP) manchmal braucht man einen „abstrakten“ Umweg: SS - V1.0 Das Problem spielt sich in einem bestimmten „Realwelt“-Ausschnitt ab, den man durch eine Menge von „Dingen“ und (regelhaften) Beziehungen zwischen diesen Dingen beschreiben kann Diese Dinge (und damit der Ausschnitt) befinden sich zu jedem Betrachtungszeitpunkt in einem bestimmten Zustand Modellieren kann man das z.B. durch Parameter/Variablen, denen Wertebereiche zugeordnet sind und zwischen denen Relationen bestehen. Ein Zustand entspricht dann einer konkreten Belegung der Parameter mit Werten (c) W. Conen, FH GE, GIN 2 14 Suche (Forts. Problemlösen als Suche) Aus den Wertebereichen und Beziehungen/Regeln ergeben sich die möglichen Zustände des Realweltausschnitts Es steht eine Menge an Operatoren zur Verfügung, um einen Zustand in einen Folgezustand zu überführen Ein Problem sieht dann wie folgt aus: Gesucht ist eine clevere Sequenz von Operatoranwendungen, die uns von einem gegebenen Ausgangszustand in einen gewünschten Zielzustand führt. Regelmäßig wollen wir zudem eine besonders „gute“ Operatorsequenz finden (z.B. eine kostengünstige, wenn wir Kosteninformationen zu den Operatoren haben) SS - V1.0 (c) W. Conen, FH GE, GIN 2 15 Suche (Forts. Problemlösen als Suche) o1 z1 o2 o3 z2 z5 z8 z3 z6 z9 z4 z7 z10 z11 Ausgangszustand z1, Zielzustand z11 Es gibt viele mögliche Pfade inkl. Sackgassen (z7,z8) und unerreichbare Zustände (z10) SS - V1.0 (c) W. Conen, FH GE, GIN 2 16 Suche (Forts. Problemlösen als Suche) o1 z1 o2 o3 z2 z5 z8 z3 z6 z9 z4 z7 z10 z11 Schauen wir uns noch eine der Sackgassen an Um einen Weg zum Ziel zu finden, müssen wir einfach eine Entscheidung für einen Operator zurücknehmen und ändern Das nennt man „Backtracking“! SS - V1.0 (c) W. Conen, FH GE, GIN 2 17 Suche (Forts. Problemlösen als Suche) o1 z2 z5 z8 z3 z6 z11 z9 z4 z7 z10 o2 z1 o3 z11 Es gibt viele verschiedene Wege in diesem Zustandsgraphen (wieviele?), manche dieser Wege führen zum Ziel, andere nicht Um garantieren zu können, dass wir das Ziel erreichen (oder sicher sein können, dass es nicht erreichbar ist), müssen wir ggfs. alle von z1 aus begehbaren Wege anschauen Wie können wir das systematisch tun? SS - V1.0 (c) W. Conen, FH GE, GIN 2 18 Suche (Forts. Problemlösen als Suche) o1 z1 o2 o3 z2 z5 z8 z8 z3 z4 z6 z9 z9 z7 z11 z9 z5 z8 z6 z9 z2 z5 z8 z3 z6 z10 Tiefensuche z1 z4 z7 z9 z11 z11 z11 z8 z9 z11 z7 SS - V1.0 (c) W. Conen, FH GE, GIN 2 19 Suche (Forts. Problemlösen als Suche) 1 2 o1 z1 o2 o3 z2 z5 1 4 3 2 z8 1 2 z3 z6 1 4 3 2 z9 1 4 3 2 z11 z7 z9 z5 z8 z6 z9 z2 z5 z8 z3 z6 1 2 z4 z10 Breitensuche z1 z8 z4 z7 z9 z11 z11 z11 z8 z9 z11 z7 SS - V1.0 (c) W. Conen, FH GE, GIN 2 20 Tiefensuche für Zustandsbäume Hilfsdatenstruktur: Knoten k im Zustandsgraph sind mit einem Zustand beschriftet, erhältlich über k.zustand Genereller Ablauf für Tiefensuche in einem Zustandsgraphen mit Baumform: Algorithm tiefensuche(Knoten start) for each k ∈ Kinder(start) do if k.zustand = zielzustand then print „Ziel gefunden!“; return true; else if tiefensuche(k) then return true; return false; Liefert sicher eine Lösung, wenn es eine gibt! Achtung: Die Reihenfolge der Kinderbesuche ist nicht vorgeschrieben, sie können frei wählen! SS - V1.0 (c) W. Conen, FH GE, GIN 2 21 Breitensuche für Zustandsbäume Wir verwenden eine FIFO-Queue queue (also eine Liste, an die hinten angefügt und vorne entnommen wird, FIFO steht für first-in-first-out) Genereller Ablauf für Breitensuche in einem Zustandsgraphen mit Baumform: Algorithm breitensuche(Knoten start) queue.append(start); // queue leer vor Beginn while (not queue.empty()) do k ← queue.deleteFirst(); // Knoten k besuchen if k.zustand = zielzustand then print „Ziel gefunden!“; return true; for each c ∈ Kinder(k) do // Knoten k expandieren queue.append(c); print „Kein Ziel gefunden!“; return false; Anmerkung: Man kann auch vor dem Einstellen der Kinder prüfen, ob ein Zielzustand erreicht ist. Wir können zeigen, dass das generell Speicher und Tests spart. Trotzdem verwenden wir aus Gründen der Einheitlich (s. BestFirst) für die Aufgaben diese Variante. Größenordnungsmäßig macht es keinen Unterschied...exponentiellen Zeit- und Speicheraufwand erfordern beide Algorithmen in Best- und Worstcase (DFS nur im Worst Case!) SS - V1.0 (c) W. Conen, FH GE, GIN 2 22 Breitensuche für Zustandsbäume Liefert sicher eine Lösung, wenn es eine gibt! Achtung: Die Reihenfolge der Kinderbesuche ist nicht vorgeschrieben, sie können frei wählen! Bisher haben wir Zustandsräume in Baumform betrachtet Da funktionieren beide Verfahren gut: beide sind „komplett“, d.h. sie finden einen Zielzustand, wenn er existiert und erreichbar ist Wenn die maximale Tiefe des Baumes d ist und der „flachste“ Zielzustand sich auf der Ebene m befindet und wir einen „gleichmäßigen“ Verzweigungsfaktor b unterstellen, dann Tiefensuche: best-case O(m), worst-case O(bd), average case: hängt von der Verteilung der Zielzustände über die Tiefen zwischen m und d ab Breitensuche: best-case = average case = worst-case O(bm), falls eine Lösung vorhanden ist, sonst best-case = average case = worst-case = O(bd) SS - V1.0 (c) W. Conen, FH GE, GIN 2 23 Suche für allgemeine Zustandsgraphen Problem: ein Graph, der kein Baum ist, enthält einen Kreis, d.h. gleiche Zustände können bei der Reise durch den Graphen mehrfach auftreten! (s. Missionare) Was passiert, wenn wir unsere Algorithmen auf einen Graphen mit Kreis loslassen? Die Tiefensuche läuft immer weiter „geradeaus“ und kann sich in einer endlosen Schleife „aufhängen“ Wenn es eine Lösung gibt, findet die Tiefensuche sie dann nicht! Wenn es keine Lösung gibt, merkt sie es nicht! Die Breitensuche expandiert gleiche Knoten mehrfach Kein „prinzipielles“ Problem, wenn es eine Lösung gibt – dann wird diese auch gefunden (und zwar weiterhin die „flachste“) – die Breitensuche ist also auch im „Wiederholungsfall“ komplett! Wenn es allerdings keine Lösung gibt, dann merkt unser einfaches Verfahren zur Breitensuche das nicht! SS - V1.0 (c) W. Conen, FH GE, GIN 2 24 Beispiel: Suche in Kreisen mit Tiefensuche GE Startzustand OB OB E GE GE MH D OB E E DUI GE DUI MH D Zielzustand SS - V1.0 E Unendliche Zweige können in dem Baum entstehen, der die Wege durch den Zustandsgraphen darstellt (also die Suche beschreibt)! (c) W. Conen, FH GE, GIN 2 25 Suche Kann man beide Verfahren noch „retten“? Idee: wir können kontrollieren, ob es zu Zustandswiederholungen kommt Knoten markieren bzw. die durch sie repräsentierten Zustände in einer globalen „CLOSED“-Liste registrieren und nur einmal besuchen Erweiterung der Algorithmen ist einfach: besuchte Zustände werden in eine CLOSED-LISTE aufgenommen Suchkosten: linear zur Anzahl der Zustände in der Liste mit einem Bitfeld und nummerierten (oder „gut“ gehashten) Zuständen kann man die (Zeit-)Kosten konstant und den Speicher „erträglich“ halten (es sei denn, es gäbe sehr viele Zustände) Dann zwei Alternativen (zunächst nur für unsere Breitensuche relevant) 1. Nur Knoten in queue einstellen, die nicht in CLOSED sind 2. Nur Knoten besuchen/expandieren, die nicht in CLOSED sind SS - V1.0 (c) W. Conen, FH GE, GIN 2 26 Suche Übrigens kann das zweite Verfahren besser sein, wenn der Test deutlich teurer ist, als ein Einstellen sein sollte...denken sie an folgendes: Nehmen Sie an, die Lösung auf Tiefe m wird dort als letzter Knoten „angepackt“ Dann wurden vorher bereits bm-1 Knoten expandiert, also bm+1-b Knoten in die queue gestellt und, bei Variante 1, auch getestet Wenn Tests im Vergleich zum Einstellen teuer sind (wie in unserem Fall), dann sollte man unnötige Tests vermeiden In Variante 2 werden die Kinder von Knoten der Tiefe m zwar eingestellt, aber nicht mehr getestet, das spart einen Zeitaufwand von O(bm)*O(n)! (O(n) bei naiver Suche in CLOSED) Allerdings kostet es mehr Speicher – und wenn man beim Grundablauf die Kinder vor dem Einstellen auf die Zieleigenschaft testen würde, sähe die Situation wieder anders aus...uns interessiert aber wieder vorrangig die Größenordnung des Aufwands, und die kennen wir bereits: exponentiell für Speicher und Zeit! Sie sollten beide Varianten beherrschen! SS - V1.0 (c) W. Conen, FH GE, GIN 2 27 Suche Problem mit dem Markieren von Zuständen im Zustandsgraphen: der ist häufig gar nicht explizit gegeben (und muß dann auch nicht explizit repräsentiert werden), sondern wird nur durch einen Startzustand und eine Zustandsübergangsfunktion beschrieben (vor allem empfehlenswert bei unendlichen Zustandsräumen) Schwerwiegender: Speichereffizienzüberlegungen! es kann sehr viele (besuchte) Zustände geben, die muß man sich dann ev. alle merken SS - V1.0 in der Tiefensuche braucht man sonst nur alle Knoten entlang eines Weges, also O(d) bei der Breitensuche ohnehin jeweils komplette Ebenen, also max. O(bm) bei Tiefensuche kann man sich manchmal auch durch „einfache“ Abbruchkriterien behelfen, um unendliche Zweige zu vermeiden, z.B. wenn man weiß, dass es nur max. C Zustände gibt (dann macht man immer noch Arbeit ggfs. doppelt, aber man braucht keine Liste) Ähnliches geht auch mit Breitensuche. Wenn man sogar weiß, dass eine Lösung existiert, dann kann man auch auf die Kontrolle von Wiederholungen verzichten und ist dennoch komplett (macht aber ggfs. mehr Aufwand, als erforderlich – abwägen: wie oft kommen Wiederholungen vor?) (c) W. Conen, FH GE, GIN 2 28 Breitensuche für Zustandsgraphen Vermeiden von Wiederholungen für die Variante 2: Genereller Ablauf für Breitensuche in einem Zustandsgraphen (queue und closed zu Beginn leer), es wird nur ein Ziel gefunden (um alle zu finden, schmeißen sie einfach das „return true“ raus und geben nur false zurück, wenn sie gar keins finden, also zählen sie die gefundenen Ziele am besten mit – so können sie natürlich auch die normale Breitensuche modifizieren) Algorithm breitensuche(Knoten start) queue.append(start); while (not queue.empty()) do k ← queue.deleteFirst(); // Knoten k besuchen if (not closed.in(k.zustand)) then // Ist k in closed? if k.zustand = zielzustand then print „Ziel gefunden!“; return true; closed.append(k.zustand); for each c ∈ Kinder(k) do // Knoten k expandieren queue.append(c); print „Ziel nicht gefunden“; return false; SS - V1.0 (c) W. Conen, FH GE, GIN 2 29 Beispiel: Suche mit Pfadkosten OB 10 20 Startzustand Wir wollen weiterhin von Gelsenkirchen nach D‘dorf Aber jetzt wollen wir nicht nur einen Weg finden, sondern einen guten Weg! Genauer: einen Weg durch den Zustandsgraphen mit minimalen Kosten (also einen „kürzesten Weg“) GE DUI 13 15 12 MH 14 35 D E Zielzustand SS - V1.0 (c) W. Conen, FH GE, GIN 2 30 Suche mit Pfadkosten Was können wir tun? SS - V1.0 Weiterhin Tiefen- oder Breitensuche verwenden und dort einfach nach allen Lösungen suchen (Lösungen „enumerieren“) und die beste auswählen! (ggfs. sehr teuer) wir können auch mit Tiefensuche nur nach einer Lösung suchen und dann hoffen, dass es die richtige ist... manchmal wissen wir auch, dass die flachste Lösung die beste ist, z.B. wenn alle Schrittkosten konstant und positiv sind oder gleichmässig und einheitlich mit der Entfernung vom Startzustand zunehmen (dann geht die normale Breitensuche, die nur die flachste Lösung findet) Wenn wir Wiederholungen vermeiden wollen, dann geht das nicht ohne „Nachdenken“ wir müssen uns die bisher besten Kosten zu den Zuständen merken und im Wiederholungsfall die Erkundung eines Zweigs stoppen, wenn die neuen Kosten zum wiederholten Zustand nicht kleiner sind (c) W. Conen, FH GE, GIN 2 31 Suche mit Pfadkosten Und sonst? SS - V1.0 Wir verwenden die Kosteninformationen, um nach und nach die vielversprechendsten Wege zu erkunden (Russell/Norvig nennen das „Uniform cost“-Suche, kein sehr passender Name) Im Grunde ist das ein klassischer „Best-First“-Ansatz: der Knoten mit den niedrigsten aufgelaufenen Kosten wird zuerst expandiert Wenn man weiß, dass die Kosten mit der Entfernung vom Startknoten nicht abnehmen, dann kann man mit der ersten gefundenen Lösung aufhören – sie muß optimal sein! (c) W. Conen, FH GE, GIN 2 32 Suche mit Pfadkosten – Best-First Uniform Cost Ablauf für Zustandsgraphen mit Vermeidung von Wiederholungen nach Variante 2 (bei Uniform Cost kann auch Variante 1 lohnenswert sein, je nach Probleminstanz) Die Min-PQueue pqueue und die Closed List sind leer zu Beginn: Algorithm bestFirst(Knoten start) start.cost ← 0; pqueue.insert(start); closed.append(start); while (not pqueue.empty()) do k ← pqueue.deleteMin(); // Knoten k besuchen if (not closed.in(k.zustand)) then // Ist k in closed? if k.zustand = zielzustand then print „Ziel gefunden!“; return true; closed.append(k.zustand); for each c ∈ Kinder(k) do // Knoten k expandieren c.cost ← k.cost + kante(k,c).cost; pqueue.insert(c); // Knoten c in PQueue ablegen Der Wert der Knoten wird im Feld cost abgelegt. Der Wert einer Kante wird ebenso abgelegt. Ist fast genau Dijkstra, nur ein bisschen „blöder“, weil mehrfaches Einstellen statt Update (kann zu spektakulär höherem Speicheraufwand führen!) SS - V1.0 (c) W. Conen, FH GE, GIN 2 33 Uniform-Cost-Ablauf OB 10 Startzustand 20 GE pqueue (und closed in Klammern dahinter): DUI 13 15 12 MH 14 35 D SS - V1.0 E GE/0 E/15, OB/20 (GE) OB/20, MH/27, GE/30, D/50 (GE,E) MH/27, GE/30, DUI/30, GE/40, D/50 (GE,E,OB) GE/30, DUI/30, E/39, GE/40, DUI/40, D/50 (GE,E,OB,MH) GE in Closed! DUI/30, E/39, GE/40, DUI/40, D/50 (GE,E,OB,MH) E/39, GE/40, DUI/40, OB/40, MH/43, D/44, D/50 (GE,E,OB,MH,DUI) E,GE,DUI,OB, MH in Closed! D/44, D/50, OB/50, MH/53 (GE,E,OB,MH,DUI) D/44 gefunden! (c) W. Conen, FH GE, GIN 2 34 Was geht noch „uninformiert“? Simples „Greedy“: Wenn wir nicht auf Wiederholungen achten, kann das zu endlosem Pendeln zwischen zwei Zuständen führen Verwende von deinem Knoten aus jeweils den günstigsten nächsten Schritt. Im Beispiel würde er sich zwischen E und MH einpendeln Also achten wir auf Wiederholungen (von Zuständen) Das gibt aber noch keine Garantie, dass wir auch einen Zielzustand finden (wir enden ggfs. in einer Sackgasse, die auch erst entstanden sein kann, weil wir die Nachbarn bereits besucht haben) Im Beispiel würde er in OB hängen bleiben Also verwenden wir Backtracking („Zurückspringen“) und führen eine CLOSED-List bereits verwendeter Kanten! SS - V1.0 Im Beispiel besuchen wir dann folgende Kanten (und damit die Knoten): {GE,E}, {E,MH},{MH,DUI},{DUI,OB},Backtrack,{DUI,D} Also finden wir in diesem Beispiel nicht die optimale Lösung (aber immerhin, wir finden jetzt sicher eine Lösung – das kann auch mal die Beste sein!) (c) W. Conen, FH GE, GIN 2 35 Was geht noch „uninformiert“? Wir können auch noch mittels Tiefensuche (depth-first search oder kurz: DFS) die Breitensuche simulieren (mit oder ohne Schrittkosten) dann brauchen wir nicht auf Wiederholungen zu achten und haben trotzdem ein vollständiges Verfahren für endliche Zustandsräume Das geht, indem wir ein Tiefenlimit einführen Setze das Limit zu Beginn auf 0 (dann wird nur der Startzustand angeschaut) Erhöhe das Limit in jeder Runde um eins und beginne immer wieder oben mit Tiefensuche, wiederhole das solange bis die erste Lösung gefunden wurde Dieses Verfahren nennt sich Iterative Deepening und ist für den Fall ohne Schrittkosten die sinnvollste Wahl SS - V1.0 (c) W. Conen, FH GE, GIN 2 36 Iterative Deepening Die Implementierung ist simpel: Wie der Algorithmus Tiefensuche, aber mit Abbruch des Abstiegs, wenn das Tiefenlimit erreicht ist (also einfach ein Limit vorgeben und beim Aufruf von Tiefensuche einen Parameter Tiefe, der schrittweise erhöht wird, hinzufügen – bei Erreichen des Limits nicht mehr expandieren!) Das Verfahren ist besser, als DFS, weil es sich nicht in endlose Zweige verlaufen kann Im Vergleich zur Breitensuche wiederholt es zwar eine Menge, aber es muss sich wesentlich weniger merken (linear zur Lösungstiefe) und es expandiert vor allem die Knoten auf Tiefe m nicht mehr! (es wird ab einer gewissen Tiefe dramatisch günstiger als Breitensuche) Es findet allerdings die beste Lösung nur, wenn es die flachste ist (wie Breitensuche). Man kann das leicht zu einem optimalen Verfahren machen, wenn man sich die Kosten der besten bisher gefundenen Lösung merkt: SS - V1.0 Solange es auf der Limitebene noch Knoten mit niedrigeren Kosten gibt, wird weiter iteriert ... ...und dabei Knoten nicht expandiert, wenn sie nicht günstiger als die beste bisherige Lösung sind. (c) W. Conen, FH GE, GIN 2 37 Und was ist „informierte“ Suche? Wenn wir zu den Zuständen z noch heuristische Informationen h haben, die es uns erlauben, die Entfernung zum nächsten/besten Zielknoten zu schätzen, also h(z) Für einen gegebenen Knoten k mit Zustand k.zustand = z können wir die bisherigen tatsächlichen Kosten des Wegs zu k, angegeben durch g(k) und die noch zu erwartenden Kosten, h(k) = h(k.zustand) = h(z) addieren Diese Summe f(k) = g(k)+h(k) verwenden wir dann als „Distanzwert“ in unserer PQueue für den Best-First-Algorithmus von vorn Dieses berühmte Verfahren nennt sich A* (Beispiel nächste Folie), gesprochen „ä-star“ SS - V1.0 (c) W. Conen, FH GE, GIN 2 38 A*-Ablauf (auf Wiederholungen wird nicht geachtet) OB 10 Startzustand 20 GE DUI 13 15 pqueue: 12 MH 14 35 D SS - V1.0 Heuristische Informationen: h(GE) = 30, h(OB) = 24, h(DUI) = 14, h(MH) = 16, h(E) = 20, h(D) = 0 E GE/0+30 E/15+20, OB/20+24 MH/27+16,OB/20+24, D/50+0,GE/30+30 OB/20+24, D/50+0, DUI/40+14,E/39+20, GE/30+30 DUI/30+14,D/50+0, DUI/40+14,E/39+20, GE/30+30, GE/40+30 D/44+0, D/50+0, DUI/40+14,E/39+20, MH/43+16, GE/30+30, GE/40+30, MH/43+16 (fertig) (c) W. Conen, FH GE, GIN 2 39 Informierte Suche mit A* Wenn die verwendete Heuristik „admissible“ ist – das ist sie, wenn sie die tatsächlichen Kosten unterschätzt, dann ist A* optimal für endliche Zustandsräume (bei nicht-negativen Pfadkosten, wie wir generell annehmen) A* ist außerdem auch noch optimal effizient relativ zur Klasse der Algortihmen, die einen solchen Suchbaum explorieren. Das Argument ist einfach: A* untersucht alle Knoten mit niedrigeren tatsächlichen Kosten, als der optimale Zielknoten wenn ein anderer Algo einen dieser Knoten ausläßt, dann kann er nicht sicher sein, das Optimum gefunden zu haben Manchmal kann ein anderer Algo „zufällig mal“ besser sein, aber nicht immer! (A* expandiert auch Knoten mit dem gleichen Gewicht wie der optimale Zielknoten, die muss man aber nicht unbedingt anschauen!) Das Vermeiden von Wiederholungen spielt auch wieder eine Rolle für die Effizienz (und die Vollständigkeit des Algo) – hier helfen konsistente Heuristiken (sie erfüllen die Dreiecksungleichung und sind admissible) Natürlich können wir auch h verwenden, um „greedy“ loszulaufen (diesmal stürzen wir uns nicht „greedy“ auf Kanten, sondern auf Nachfolger) – mit ähnlichen Problemen und Resultaten, wie oben ...und einiges mehr (Praktisch relevant: Speicherbeschränkte Varianten von A*!) SS - V1.0 (c) W. Conen, FH GE, GIN 2 40 Und sonst noch? Man kann auch noch anders modellieren – man verwendet nur komplette Lösungen und versucht dann durch Operatoren von einer Lösung zur nächsten zu gelangen Kann z.B. beim TSP sinnvoll sein: Zustände sind dann komplette Rundtouren, man sucht die beste. Man kann auch „partielle“ oder ungültige „Lösungsvorschläge“ zulassen und dann nach der besten gültigen Lösung in diesem erweiterten Zustandsraum suchen Und vieles mehr...wie wir noch sehen werden (aber leider erst im Master) SS - V1.0 (c) W. Conen, FH GE, GIN 2 41 Literatur zur Suche in Zustandsräumen Russell, Norvig: Artificial Intelligence – the Intelligent Agent Approach, Prentice-Hall, 2nd Edition (unbedingt die zweite Auflage verwenden mit einem aktuellen Printing), International Edition (billiger als das amerikanisch/kanadische Original), 2003 Russell ist Professor in Berkeley, eine der öffentlichen Top-Unis (eine/die andere öffentliche Top-Uni in Informatik ist die UMICH in Ann Arbor) Norvig ist Director of Search Quality bei Google Das Buch ist das „Standardwerk“ zu KI (=künstlicher Intelligenz), es hat ein paar kleine Schwächen, z.B. wenn es um Optimierung geht oder wenn man sehr präzise Details braucht, es gibt aber einen exzellenten Überblick über viele Teilgebiete der KI (und fast alles spannende gehört da „irgendwie“ zu...zumindest sehen das die KI‘ler so...stimmt natürlich nicht so ganz, oder doch... ;-) SS - V1.0 (c) W. Conen, FH GE, GIN 2 42 Ein kurzer Ausflug in die Welt „harter“ Probleme... [der 2. Teil zur ersten Vorlesung in GIN1B] Prof. Dr. Wolfram Conen 21. Juni 2005 Vorlesung zu GIN2 Alan Turing – Mini-Rückblick auf die erste GIN1b-Vorlesung 23. Juni 1912, London 1936: On computable numbers ... (es gibt keine „definite“ Methode, die für jede mathematischlogische Aussage entscheiden kann, ob sie beweisbar ist oder nicht.) 1939-40: Entwicklung der Bombe (zur Entschlüsselung der deutschen EnigmaCodes) 7. Juni 1954: Selbstmord Von der Intuition zur Exaktheit Hilbert suchte nach einem Verfahren, das für jede mathematischlogische Aussage einen Beweis oder eine Widerlegung liefert: „Das Entscheidungsproblem ist gelöst, wenn man ein Verfahren kennt, das bei einem vorgelegten logischen Ausdruck durch endlich viele Operationen die Entscheidung über die Allgemeingültigkeit bzw. Erfüllbarkeit erlaubt. (...) Das Entscheidungsproblem muss als das Hauptproblem der mathematischen Logik bezeichnet werden“, Hilbert, Ackermann, Grundzüge der theoretischen Logik, 1928 Um zu zeigen, dass es so ein Verfahren gibt, könnte man einfach eines angeben. Turing wollte zeigen, das es ein solches Verfahren nicht geben kann! Er mußte etwas finden, dass Hilberts Intuition eines Verfahrens entsprach, diese präzisierte und wesentliche Eigenschaften aller anderen „möglichen“ Verfahren beinhaltete und so als „Prototyp“ dienen konnte: Wenn der Prototyp die gewünschte Antwort nicht geben kann, dann kann es auch kein anderes Verfahren! Die Turing-Maschine: Ein Modell der Berechenbarkeit ... 0 0 0 0 0 0 1 Zustand: z1 „Programm“ ... Falls Zustand == z1 und Zeichen unter Schreib/Lesekopf == 0, dann schreibe 1, gehe in den Zustand z1 und bewege dich ein Feld nach rechts. Kurz: (z1,0) Æ (1,z1,R) (z1,1) Æ (0,z1,R) (z1,_) Æ (_,ende,L) Was macht dieses Programm? Die Turing-Maschine: Eine Berechnung ... 1 0 0 0 0 0 1 Zustand: z1 „Programm“ (z1,0) Æ (1,z1,R) (z1,1) Æ (0,z1,R) (z1,_) Æ (_,ende,L) ... Die Turing-Maschine: Eine Berechnung ... 1 1 1 1 1 1 1 (z1,0) Æ (1,z1,R) (z1,1) Æ (0,z1,R) (z1,_) Æ (_,ende,L) Zustand: z1 „Programm“ ... Die Turing-Maschine: Eine Berechnung ... 1 1 1 1 1 1 0 0 (z1,0) Æ (1,z1,R) (z1,1) Æ (0,z1,R) (z1,_) Æ (_,ende,L) Zustand: z1 „Programm“ ... Die Turing-Maschine: Eine Berechnung ... 1 1 1 1 1 1 0 (z1,0) Æ (1,z1,R) (z1,1) Æ (0,z1,R) (z1,_) Æ (_,ende,L) ... Zustand: ende „Programm“ Die Berechnung hält an! Noch wichtig: Programm, Eingabe und Zeichensatz sind endlich. Universelle TM (1) Turing führt eine Turing-Maschine UTM ein, die andere Turingmaschinen simulieren kann Bisher: Jetzt: Eingabestring x → Turingmaschine TM → Ausgabestring y; kurz: TM(x) = y Kodieren des Anweisungen der Turingmaschine als String z Eingabestring zx → Turingmaschine UTM simuliert TM und wendet die Simulation auf x an → Ausgabe y; kurz also: UTM(zx) = y bzw. UTM(TM,x) = y Die UTM von Turing kann alle Turingmaschinen simulieren...sie ist in einem bestimmten Sinn universell Universelle TM (2) Eine „binäre“ UTM von Stephen V. Gunhouse mit 176 Zuständen B0.UTM=10000001011101000000010101101000000100101101000010101101000001000101101010011101000001010101110100001110100001001 0101110100100110100001010010110101000110100010001010110101010011101001010101001011101000001110100000000011101000000101011 0100000100101101001000110100010101001011010010001000110100101010001110101000011101001010010010111010010001001011010100010 1001110100100000011101010100101110100100101001110101010101011101000000111010010101010101110100001001101000110100010100001 1010000010111010100101000101110100010101011101001000011010101001010010110101001011010010010101101010001010110100101001011 1010101000010011101001010101011101000101010011101010000101101010010001101010001010110100010000101110101001011010010010011 0101001010101101001001000101110101010001011101010101000110101010010101101001000101101010101001011001110100010010111010010 0000111010001000101110100100011010000010101101001001001001110100001001011010010011010000101010110101001001011101001101000 0000111010001001010110100100010101110100010100101110101001000101110100100001011101001010001010111010100100010111010010010 0101110100101000101110100100100011101001010000111010001000110100101001001110100100010011101000011101001010011101010010011 1010010100111010010101011101000000111010100010001011101010001010010111010100000101110100100000111010100001010110100010011 0101000100101110101001001110101001010111010010101001010111010100010010101110101010100010111010100100101011101000010101101 0101000101011001110101010101010010110100010101101010100001011101010100100101101010000001110100010000011101000100011101000 0111010101010010101110101000001001110100101000001110100001110101010010111010010000010111010000000011010010011010000101010 1011010100000010110100001010101101000100110100001000011010001010110100000100110101001110100000010101011101001011010000100 0101101000011101010100101010111010010011010101010101010110100100110100010010101101001001010111010101011101010001101010100 0101010110101000010010110101010010111010000000111010001010010101101001000010011101000101000101101000000011101000101001011 1010010001101000101010101011010010001101001000101010110100010000110101010101000111010101010100100111010010000100111010000 0010011010000010001101001000110100001001011010001010111010100100100111010100000111010111010101010101010101101001001010101 0110101000010011010001010011101010101000011101010101010000111010000100101110101000101110101000011101001010010101011101010 0001110100101010001011101010000111010100010010111010001000101110101010101110101010011101001010101010101110101010011101010 0001010101110100000000110100000001011010100001000110101000000011010001001000110100010010010110101010011101010100001011101 0000001110101001000010111010100000011101010010010101011101000110101000101010101110100011010100100010101110100000011101010 1000001011101000110101001001001011101000110101010101011101000101001110101001110100110101001010010101110100110101001010100 1011101001101010010101010101110100110100101010101110100101000111010010011010001001011010101000100101110101010010011010100 1010011010100011010101001000101101010001101010100100101011010000101110101001010101011010010000110101010010101010110100100 0011010101010000101101001000011010000000001110100001011101010101001001011101000010111010101010010101011101000010111010010 0100010111010100000101011101001010000101110110111001110101000010110011101010101010101101000010111010101010001010111010000 0101011010101101000100101010110100100101001011010100111010000010010101110100001010110101001010010110101001110100000101001 0111010101000101110100001 Was kann die Turing-Maschine? „Alles Machbare!“ Genauer formuliert dies die Church-Turing-These: „Genau das, was wir intuitiv für „effektiv berechenbar“ halten, kann durch die Turing-Maschine tatsächlich berechnet werden.“ Wir nennen ab jetzt etwas berechenbar, wenn es eine Turingmaschine gibt (geben kann), die mit dem richtigen Ergebnis anhält! Es wurden noch viele andere Modelle der Berechenbarkeit „erfunden“ (generelle rekursive Funktionen, λ-Definierbarkeit, Kombinatorische Systeme, Termersetzungssysteme, Registermaschinen, ...), aber alle (zumindest alle physikalisch realisierbaren) erwiesen sich als „äquivalent“, sie können die gleichen Probleme lösen! Zeigen kann man dies, in dem man ein Modell durch ein anderes simuliert (und vice versa). Merke: Modelle der Berechenbarkeit präzisieren den Begriff Algorithmus / Lösungsverfahren (F1) Noch ein Beispiel...fleissige Bieber Bieber bauen Dämme aus Baumstämmen... Als Turingmaschine sieht das z.B. so aus: _/1,R 1/1,R z1 z2 z3 1/1,R _/1,L _/1,L 1/1,L _ Satz: halt _ 1 _ 1 _ 1 _ 1 _ 1 _ 1 _ _ _ _ Die Rado-Funktion kann nicht durch eine Turing-Maschine berechnet werden Luftholen... [Ende des Rückblicks] Wahre Titanen des Geistes (auch, wenn manche von Ihnen ein unglückliches Ende gefunden haben...) legten die Grundlagen. Wir wissen jetzt: es gibt Probleme, die sich nicht lösen lassen, obwohl sie „mathematisch genau“ beschrieben sind! Modelle der Berechenbarkeit erlauben eine Analyse der Berechenbarkeit/Lösbarkeit eines Problems! (F2) Aber: wie schwer kann es werden, ein konkretes Problem tatsächlich zu lösen? Komplexität Gegeben: ein berechenbares Problem P und ein bestimmtes Modell der Berechenbarkeit (z.B. Turing-Maschinen oder RAMs) Zentrale Frage: Erfordert die Lösung des Problems einen Zeit oder Speicheraufwand, der polynomial oder (möglicherweise) exponentiell zur Größe der Eingabe ist? Zur Größenordnung: Annahme: Jede Operation benötigt eine Mikrosekunde Eingabegröße n = 10 =100 =1000 =1000000 Zeitaufwand als Funktion von n: n 10us 0.1ms 1ms 1s n2 0.1ms 10ms 1s 2W n5 0.1s 3h 30J 3*1016J 2n • 1ms 3*1016 J 3*10288J groß 3*1016 J ist übrigens mehr als eine Millionen mal die geschätzte bisherige Dauer des Universums... TSP - Traveling Salesman Problem Gegeben ist eine Menge von „Städten“, so dass jede von jeder direkt erreichbar ist (z.B. per Hubschrauber ;-) Mit den Reisen sind Kosten verbunden (wir nehmen mal an, dass die Dreiecksungleichung erfüllt ist, d.h. kein „Umweg“ ist kürzer, als die direkte Verbindung) Gesucht ist eine Rundtour durch alle Städte, die minimale Kosten über alle möglichen Rundtouren hat (jede Stadt wird nur einmal besucht) Das ist das „metrische“ TSP TSP TSP TSP Wir können das TSP als Graphproblem auffassen n Knoten (die Städte), n*n-1 ungerichtete Kanten (die Wege) Der Graph ist also vollständig (er kann natürlich auch unvollständig sein...) Wenn Hin- und Rückfahrt zwischen je zwei Städten gleich teuer sind, dann ist es ein symmetrisches TSP Naiver Lösungsalgo: Suche zufällig einen Startpunkt aus Generiere nach und nach alle möglichen Rundtouren Vergleiche die Länge jeder Rundtour mit der Länge der bisher besten Merke dir ggfs. die neue „beste“ Tour Aufwand? n-1*...*2*1 = (n-1)! im symmetrischen Fall (n-1)!/2 TSP Für ein symmetrisches TSP gibt es also eine Menge Lösungskandidaten: n = 5: n = 10: n = 20: 12 Touren 181.440 Touren 60.822.550.204.416.000 Touren (~ 61 Billiarden Touren) Das heißt: Die Anzahl von Lösungskandidaten explodiert exponentiell mit steigendem n TSP Das geht natürlich besser! Aber wenn wir wirklich das Optimum haben wollen, dann erfordert jedes exakte Lösungsverfahren im worst case exponentiellen Aufwand es sei denn, NP = P (was das ist, sehen wir gleich) Man kann metrische TSPs aber zu polynomialen Kosten approximieren (wie gut?) Approximationen metrischer TSPs Jetzt kommt unser alter MST zu Ehren: Gegeben: 8 Knoten, vollständiger, ungerichteter Graph v MST „Doppelter“ MST: jede Rundtour mit Kante wird verdoppelt und die „Abkürzungen“ Kanten eines jeden Paares werden in gegensätzliche Richtungen orientiert Approximationen metrischer TSPs Algo APP_TSP Bestimme einen MST T Wähle zufällig einen Knoten v und laufe komplett um den Baum (=verdopple alle Kanten und mache eine Eulertour) Vermerke dabei die Besuche der Knoten in einer Liste L Entferne alle bis auf das jeweils erste Vorkommen der Knoten, zurück bleibt der Kreis C (oder kürze bei der Tour gleich ab) [C ist ein Preorder-Traversal von T] Kosten für Prim: O(m + n log n) = O(n2) für kompletten Graph O(1) (Auswahl) O(n) (n-1 Kanten * 2) O(n) Speicher O(n) Dominiert von MST-Kosten, insgesamt O(n2) Approximationen metrischer TSPs Satz: C ist eine 2-Approximation der kürzesten Tour (Beweis gleich) Aber: was ist eine 2-Approximation? Gegeben ist ein Optimierungsproblem OPT mit der optimalen Lösung C* Gegeben ist außerdem ein Approximationsalgorithmus, der eine Näherungslösung zu OPT bestimmt, seine Lösung hat den Wert C Approximationen metrischer TSPs C* = optimale Lösung, C = approximative Lösung Approximationsfaktor für eine bestimmte Instanz bei einem Minimierungsproblem: C/C* Approximationsfaktor des Algorithmus: p = sup C/C* Hier wird das Supremum (also die Schranke nach oben) über alle Instanzen bestimmt (also eine Schranke für die schlechteste Approximation, die der Algo liefert) p ist hier immer größer als 1 (klar, C kann ja nicht besser als C* sein) Approximationen metrischer TSPs Relativer Fehler ε des Approximationsalgorithmus: ε = sup |C-C*|/C* Für Minimierungsprobleme äquivalent zu C · (1 + ε) C*, d.h. p · (1 + ε) Approximationsschema: Eingabe: eine Probleminstanz I und ein ε > 0 Ausgabe: eine (1+ε)-Approximation des Optimums (für Minimierungsprobleme) D.h. einem Approximationschema können sie sagen, welche Qualität es liefern soll! (sie geben also den maximal erlaubten relativen Fehler ε vor) Approximationen metrischer TSPs 2-Approximation: ε = 1, d.h der relative Fehler ist schlechtestenfalls 1, der Approximationsfaktor also nicht schlechter als (1+ε)=2, d.h. C ist höchsten doppelt so teuer, wie C* Zurück zum Satz, dass APP_TSP eine 2-Approximation liefert (zur Erinnerung: T ist unser MST, C unser Kreis) Wenn man aus der optimalen Tour eine Kante entfernt, hat meinen einen Spannbaum, d.h. C* ≥ Cost(T) In L wird jede Kante aus T zweimal aufgenommen, also Cost(L) = 2*Cost(T) C entsteht aus L durch Hinzufügen von Abkürzungen. Weil die Dreiecksungleichung gilt, kann C nicht länger als L sein, also C · Cost(L) Insgesamt also: Cost(T) · C* · C · Cost(L) = 2*Cost(T) Approximationen metrischer TSPs Christofides hat 1976 einen verbesserten polynomiales 3/2Approximations-algorithmus gefunden (mittels MST und minimum weight matching) Engebretsen hat 1999 gezeigt, dass kein Approximationsfaktor 5381/5380 - ε für ε > 0 möglich ist Spannend wird es, wenn man gute polynomiale Approximationsschemata (AS) findet: PTAS sind AS, deren Laufzeit polynomial zu n bei gegebenem ε ist (PT = polynomial time) FPTAS sind AS, deren Laufzeit polynomial zu n und 1/ε (FPT = fully polynomial time) O(n1/ε), O(n/ε): Beides PTAS, nur das zweite FPTAS Approximationen metrischer TSPs Mit einem solchen Schema können sie in der Praxis wie folgt arbeiten: Sie haben 2 Wochen Zeit, bis sie ein möglichst gute Lösung brauchen: sie bestimmen mit der Abschätzung ε und lassen das PTAS loslaufen, nach ungefähr 2 Wochen wird es in der entsprechenden Güte antworten – dann brauchen sie aber natürlich eine Abschätzung, in der auch die Konstanten enthalten sind und sie müssen wissen, wie lang elementare Operation (im Mittel) wirklich brauchen. Sie können also den Trade-Off zwischen Aufwand und Güte bewusst steuern – was will man mehr! (wenn dann die zur Verfügung stehende Zeit auch noch für „gute“ Lösungen reicht, dann haben sie wirklich Glück ;-) Für euklidische TSP (=metrisch und in der Ebene, also ohne Berg-auf, Bergab) hat Arora 1996 ein gutes PTAS gefunden (und ist damit berühmt geworden)! Uneingeschränktes TSP ist in der Klasse der schwierigsten Optimierungsprobleme (NPO-complete), d.h. es gibt keine 2n^ε-Approximation für kleine ε (Orponen, Mannila, 1987) Wie schwer ist TSP „wirklich“? TSP lässt sich mit einer nicht-deterministischen Turingmaschine, die den richtigen Abarbeitungsweg „raten“ kann, in polynomialer Zeit lösen Man sagt: TSP ist in der Komplexitätsklasse NP Es ist aber keine deterministische Turingmaschine bekannt, die alle TSPs in polynomialer Laufzeit lösen könnte Sonst wäre TSP in der Komplexitätsklasse P. Zu Problemen in P (z.B. Kürzeste-Wege-mit-nicht-negativen-Kanten) sagt man auch: Problem dieser Problemklasse sind effizient lösbar! Offene Frage: Ist P=NP? TSP und Komplexität TSP gehört zu den „schwersten“ Problemen in NP: wenn man dieses polynomiell lösen könnte, dann ginge das auch mit allen anderen Problemen aus NP! TSP ist deshalb ein NP-vollständiges Problem Als erstes NP-vollständiges Problem wurde 1971 von Cook das SAT-Problem identifiziert: Erfüllbarkeit von aussagenlogischen Formeln! (kennen sie aus GIN1b) – alle anderen Probleme in NP lassen sich auf dieses Problem „reduzieren“. Es gibt noch viele andere NP-harte Problem: Matching-Probleme („Stabile Heirat“), Graph-Färbe-Probleme, div. Scheduling/Planungsprobleme (Stundenplan! ;-), ganzzahlige Programmierung, usw. – einen Überblick finden sie auch im Web: Es gibt auch noch sogenannte NP-harte-Probleme, diese sind „mindestens so schwer, wie Probleme aus NP“ und ihre Lösung würde zur Lösung aller Probleme aus NP führen (alle NP-Probleme lassen sich auf sie reduzieren), aber es ist nicht sicher, ob sie wirklich in NP liegen (oder aber in EXP, also sicher exponentiellen Aufwand erfordern, auch im nichteterministischen Fall!) Mögliche Auswege für den Informatiker (1) Nicht alle Probleminstanzen sind schwer! Vielleicht gehören „unsere“ Probleminstanzen dazu? manchmal kann man das generell sagen (wenn das Problem nicht „stark NP-schwer“ ist), manchmal helfen Experimente! manchmal findet man auch „langsam wachsende“ exponentielle Algorithmen Mögliche Auswege für den Informatiker (2) Alternativ kann man vielleicht approximieren! Man sucht nach polynomialen Algorithmen, die eine bestimmte Güte der Lösung garantieren können (etwa immer mindestens halb so gut wie das Optimum) Manche Probleme lassen sich nicht sinnvoll approximieren, auch das kann man formal untersuchen! Mögliche Auswege für den Informatiker (3) Oder man wendet eine Heuristik an, die versucht mit Daumenregeln oder beispielsweise in Anlehnung an natürliche Prozesse „schnell“ „gute“ Lösungen zu finden Das kann natürlich grandios schief gehen! Auch hier kann man einiges analysieren! Bei allem hilft die Theorie zu Komplexität und Approximierbarkeit, die aus den Modellen zur Berechenbarkeit hervorgegangen ist: Die Analysen von Komplexität und Approximierbarkeit stecken den Rahmen für die erreichbare Lösungsqualität ab! (F3) Sie weisen den Weg zu verwandten Problemen und bereits bekannten exakten, approximativen oder heuristischen Problemlösungen! (F4) Rückblick: Bedeutung für den Informatiker Zentrale Aufgaben: Problemanalyse und Problemlösung. (F1) Wie lassen sich mein Problem und eine ev. Lösung präzise beschreiben? Präzision ist eine wesentlich Voraussetzung für Analyse und Lösung von Problemen (Übergang zur Logik) Modelle der Berechenbarkeit präzisieren den Begriff Algorithmus/Lösungsvorschrift (F2) Ist das Problem überhaupt lösbar? Modelle der Berechenbarkeit erlauben eine Analyse der Berechenbarkeit/Lösbarkeit eines Problems! Rückblick: Bedeutung für die Anwendungsentwicklung (2) (F3) Wenn ja, kann ich es auch mit vertretbarem Aufwand lösen? Die Komplexitätstheorie hilft uns, die Schwere des Problems einzuschätzen! Die Analysen von Komplexität und Approximierbarkeit stecken den Rahmen für die erreichbare Lösungsqualität ab! (F4) Wie kann ich das Problem lösen? Die Komplexitätstheorie hilft auch, verwandte Probleme zu identifizieren! Die Analysen von Komplexität und Approximierbarkeit weisen den Weg zu verwandten Problemen und bereits bekannten exakten, approximativen oder heuristischen Problemlösungen! (Übergang zur Algorithmik) Rückblick: Bedeutung für die Anwendungsentwicklung Zentrale Aufgaben: Problemanalyse und Problemlösung Was genau ist mein Problem? Ist das Problem überhaupt lösbar? Historische Bedeutung: Entwicklung von Formalismen zur Beschreibung von Problemen und Lösungseigenschaften. Entwicklung der Modelle der Berechenbarkeit zur Präzisierung des Algorithmusbegriffe und zur Untersuchung der Lösbarkeit von Problemen Speziell die Turingmachine hat die Entwicklung von „mechanischen“ Computern und Programmiersprachen beeinflußt und Intuitives mit Formalem verbunden! Wenn ja, kann ich es auch mit vertretbarem Aufwand lösen? Turingmachine diente als Grundlage zur Entwicklung der Komplexitätstheorie – wie lange braucht eine deterministische bzw. eine nicht-deterministische Turingmachine für diese Problem? Komplexität und Approximierbarkeit sind wesentliche Problemeigenschaften – nach welchem Typ von Algorithmus soll ich suchen (exakte Lösung, approximative Lösung, heuristische Lösung) Die moderne Algorithmik ist eng mit der Forschung zu Komplexität und Approximierbarkeit verbunden - ganz wichtig für den Anwendungsentwickler: Problemlösen durch Nachschlagen!