Theoretische Informatik 2 Vorlesung im Rahmen der Lehrerfortbildung Technische Universität Chemnitz Wintersemester 2000/2001 Prof. Dr. Werner Dilger Theoretische Informatik 2 Seite 2 Inhalt Seite 1. Einleitung 4 2. Effizienz von Algorithmen 2.1. Motivation 2.2. Definition (-Notation) 2.3. Definition (O-Notation) 2.4. Definition (-Notation) 2.5. Bestimmung der worst case-Laufzeit eines Programms 2.6. Beispiele 4 4 5 6 6 6 6 3. Graphentheoretische Grundlagen für Graphalgorithmen 3.1. Definition (Gerichteter Graph) 3.2. Definition (Ungerichteter Graph) 3.3. Definition (Teilgraph) 3.4. Definition (Wege in Graphen) 3.5. Definition (Kreise in gerichteten Graphen) 3.6. Definition (Kreise in ungerichteten Graphen) 3.7. Definition (Grad von Knoten und Adjazenz) 3.8. Definition (Darstellung durch Adjazenzlisten und Adjazenzmatrizen) 7 7 7 7 7 8 8 8 8 4. Breitensuche in Graphen 4.1. Definition (Datenstruktur Schlange oder Queue) 4.2. Algorithmus (Breitensuche, breadth first search) 4.3. Satz (Laufzeit von BFS) 4.4 Definition (zusammenhängend) 4.5 Folgerung 4.6 Definition (Zusammenhangskomponente) 4.7. Algorithmus (Finden von Zusammenhangskomponenten) 8 8 9 11 11 11 11 11 5. Tiefensuche in Graphen 5.1. Algorithmus (Tiefensuche, depth first search) 5.2. Laufzeitbestimmung von DFS 5.3. Satz (Laufzeitbestimmung von DFS) 5.4. Definition (Kantenarten) 12 12 14 15 15 6. Anwendung der Tiefensuche: Starke Zusammenhangskomponenten 6.1. Definition (Starke Zusammenhangskomponente) 6.2. Definition (Umkehrung eines gerichteten Graphen) 6.3. Algorithmus (Starke Zusammenhangskomponenten) 15 15 15 16 7. Sortieren 7.1. Beispiel 7.2. Beispiel 7.3. Algorithmus (Counting Sort) 7.4. Algorithmus (Radix Sort) 16 16 17 17 17 8. Divide-and-Conquer: Multiplikation großer Zahlen 8.1. Algorithmen (Multiplikation in O(n2)) 8.2. Satz (Laufzeit der Divide-and-Conquer-Multiplikation) 8.3. Satz (Laufzeit der Divide-and-Conquer-Multiplikation bei Dreifachunterteilung) 18 18 19 19 8.4. Algorithmus (Multiplikation in Technische Universität Chemnitz O n log 3 ) Wintersemester 2000/2001 19 Prof. Dr. Werner Dilger Theoretische Informatik 2 Seite 3 Literatur T.H. Cormen, C.E. Leiserson, R.L. Rivest: Introduction to Algorithms. MIT Press, Cambridge, MA, 1994. A. Goerdt: Skriptum Theoretische Informatik I, TU Chemnitz J.H. Kingston: Algorithms and Data Structures. Design, Correctness, Analysis. Harlow, 1998 Technische Universität Chemnitz Wintersemester 2000/2001 2 Addison-Wesley, Prof. Dr. Werner Dilger Theoretische Informatik 2 1. Seite 4 Einleitung In dieser Vorlesung werden algorithmische Lösungen für verschiedene Standardprobleme behandelt. Relativ breiten Raum nehmen die Graphalgorithmen ein. Bei ihnen geht es um das systematische Aufsuchen aller Knoten eines beliebigen, gerichteten oder ungerichteten Graphen. Als die beiden hauptsächlichen Lösungswege werden die Breitensuche und die Tiefensuche behandelt. Vor allem mit der Tiefensuche ist es möglich, verschiedene Strukturen in Graphen zu bestimmen, hier wird die Bestimmung starker Zusammenhangskomponenten behandelt. Außer den Graphalgorithmen wird noch das Problem des Sortierens bei geordneten Mengen von Daten betrachtet. Für die Aufgabe des Multiplizierens großer Zahlen wird eine Divide-and-conquerStrategie untersucht. Die Algorithmen werden dabei jeweils auf ihre Effizienz hin untersucht. Damit will man feststellen, wie groß der Zeitaufwand für die Abarbeitung eines Algorithmus ist, unabhängig von einem konkreten Rechner. Natürlich kann man bei einer solch abstrakten Abschätzung keinen genauen Zeitbedarf angeben. Man kann ihn aber größenordnungsmäßig in Abhängigkeit von der Art des Problems bestimmen und kann auf dieser Basis verschiedene Algorithmen für das selbe Problem miteinander vergleichen. 2. 2.1. Effizienz von Algorithmen Motivation Die Effizienz eines Algorithmus oder eines Programms wird durch zwei Größen bestimmt: 1. Laufzeit 2. Benötigter Speicherplatz Beispiel Bestimme die Laufzeit des folgenden Programms: Var A: array [1..amax] of integer amax, zaehler: integer 1. read(amax) 2. for i = 1 to amax 3. do read A[i] 4. zaehler := 0 5. for i = 1 to amax 6. do if A[i] = 7 then zaehler := zaehler +1 7. write(zaehler) Laufzeit für einzelne Befehle: Für jeden einfachen Befehl (Zuweisungsbefehl, read-Befehl, write-Befehl) wird die Laufzeit 1 angesetzt, ebenso für jeden Test, für das Erhöhen des Schleifenzählers und für die Schleifenverwaltung, d.h. für den Test, ob das Ende der Schleife erreicht ist. if-Befehle enthalten implizit einen Sprungbefehl. Für diesen wird die Laufzeit 1 angesetzt. Technische Universität Chemnitz Wintersemester 2000/2001 Prof. Dr. Werner Dilger Theoretische Informatik 2 Seite 5 Die gesamte Laufzeit des Programms hängt vom Inhalt von A und von amax ab. Die Laufzeit kann als Funktion T: Eingabe IN Hier ist speziell T(A[1..amax], amax) = t IN mit 1 amax. 3 + 7amax t 3 + 8 Das Problem bei der Bestimmung von T ist 1 beliebige Programme, dass T eine etwas 4amax oder 5amax schwer zu ermittelnde Funktion ist. Um trotzdem finden, betrachtet man alle Eingaben einer festen 1 schlechtesten Fall aller Eingaben dieser Größe. allgemein, d.h. für undurchsichtige und einen Wert für T zu "Größe" und den 3amax Im Folgenden wird die schlechteste Zeit Twc genannt (wc für worst case). Es ist Twc: IN IN Die Größe entspricht der Anzahl der Bits für die Eingabe (alternativ Anzahl der Speicherplätze), es ist nicht die Größe der Eingabewerte als Zahlen gemeint, denn das Programm arbeitet mit Bitstrings und kennt nicht den Wert einer Zahl. Im Beispiel ist deshalb die Größe amax. Wir setzen abkürzend M(n) := {T(A[1], A[2], ..., A[amax], amax) | amax = n}. Twc wird definiert als das Maximum von M(n), also Twc(n) = max M(n). Verfeinerung der Laufzeitabschätzung Sei z eine elementare Programmzeile (kein Prozeduraufruf). Zu z gibt es eine Konstante cz, so dass die Ausführung der Zeile z auf einem Rechner eine Anzahl m cz von Maschinenbefehlen benötigt. Sei C = max{cz | z ist Programmzeile}. Dann ist die Gesamtlaufzeit eines Programms gemessen nach der Zahl der Maschinenbefehle t CTwc(n). Die (worst case-) Laufzeit eines Programms wird nur bis auf einen konstanten Faktor spezifiziert. Das heißt, bis auf einen konstanten Faktor ist Twc(n) = n. Genauer bedeutet dies, dass es Konstanten D und C gibt, die nur vom Programm abhängen, so dass gilt: Dn Twc(n) Cn Weitere Gründe für die Vernachlässigung konstanter Faktoren bei der Bestimmung der Laufzeit: Bei neuen Rechnergenerationen verkürzt sich die Ausführungszeit von Maschinenbefehlen, entsprechend ändert sich der konstante Faktor. Die Laufzeitbestimmung wird theoretisch besser handhabbar. 2.2. Definition (-Notation) Ist f: IN IN, so wird definiert: Eine Funktion g: IN IN ist (von) (f(n)) genau dann, wenn es zwei Konstanten C, D > 0 und ein n0 IN gibt, so dass für alle n n0 gilt: Df(n) g(n) Cf(n) Technische Universität Chemnitz Wintersemester 2000/2001 Prof. Dr. Werner Dilger Theoretische Informatik 2 2.3. Seite 6 Definition (O-Notation) Ist f: IN IN, so ist eine Funktion g: IN IN von O(f) genau dann, wenn es C > 0, n0 IN gibt, so dass für alle n n0 gilt: g(n) Cf(n) Beachte: Ist g von (f), dann ist g auch von O(f). Die Umkehrung gilt nicht! 2.4. Definition (-Notation) Ist f: IN IN, so ist eine Funktion g: IN IN von (f) genau dann, wenn es D > 0, n0 IN gibt, so dass für alle n n0 gilt: g(n) Df(n) Beachte: Ist g von (f), dann ist auch g von (f). 2.5. Bestimmung der worst case-Laufzeit eines Programms Einmalige Ausführung einer Programmzeile z: (1). Da ein Programm nur aus endlich vielen Programmzeilen besteht, gibt es Konstanten D' und C', die nur vom Programm abhängig sind, so dass für jede Zeile gilt: D' Laufzeit der Zeile C'. Es reicht also zu zählen, wie oft jede Programmzeile im worst case ausgeführt wird (in Abhängigkeit von der Größe der Eingabe, z.B. n). Diese Anzahl sei eine Funktion von n, etwa f(n). Dann gilt für die worst case-Laufzeit Twc des Programms Df(n) Twc(n) Cf(n) also ist Twc(n) von (f(n)). 2.6. Beispiele (a) Betrachte die Schleife S for i = 1 to 2n do A Sei n die Größe der Eingabe und sei die worst case-Laufzeit von A O(n2). Daher gibt es eine Funktion g(n) von O(n2) und eine Konstante C von (1), so dass gilt: g (n) C g (n) . . . C g (n) = 2nC + 2ng(n) worst case-Laufzeit von S C 1. Durchlauf 2. Durchlauf 2 n. Durchlauf Die worst case-Laufzeit ist also von O(n) + O(ng(n)). Da g(n) von O(n2) ist, überwiegt O(n g(n)), deshalb ist die worst case-Laufzeit von O(n3). (b) Wie groß ist die Laufzeit eines Programms der folgenden Art: Technische Universität Chemnitz Wintersemester 2000/2001 Prof. Dr. Werner Dilger Theoretische Informatik 2 1. 2. 3. x1 x2 x3 100. x100 3. Seite 7 := e1 := e2 := e3 . . . := e100 Graphentheoretische Grundlagen für Graphalgorithmen 3.1. Definition (Gerichteter Graph) Ein gerichteter Graph ist ein Paar G = (V, E) mit den folgenden Komponenten: V ist eine Menge von Knoten E {(u, v) | u, v V, u v} ist eine Menge von Kanten 3.2. Definition (Ungerichteter Graph) Ein ungerichteter Graph ist ein Paar G = (V, E) mit den folgenden Komponenten: V ist eine Menge von Knoten E {{u, v} | u, v V, u v} ist eine Menge von Kanten 3.3. Definition (Teilgraph) Sei G = (V, E) ein gerichteter oder ungerichteter Graph. G' = (V', E') ist Teilgraph von G genau dann, wenn V' V und E' E. 3.4. Definition (Wege in Graphen) Sei G = (V, E) ein ungerichteter Graph. Ein Weg (oder Pfad) der Länge k (k 0) von u nach u' ist eine Folge von k+1 Knoten (v0, v1, ..., vk) = (vi)0ik mit (i) v0 = u (ii) vk = u' (iii) (vi-1, vi) E für alle i {1, ..., k} Ein Weg heißt einfach genau dann, wenn vi vj für alle i j. u' heißt von u aus erreichbar genau dann, wenn es einen Weg von u nach u' gibt. Die Distanz zwischen u und u' ist definiert durch falls u u ' 0 Dist (u, u ' ) min{ k | es gibt einen Weg der Länge k von u nach u '} falls u von u ' aus erreichbar falls es keinen Weg von u nach u ' gibt Ein Weg in einem gerichteten Graph ist entsprechend definiert. Technische Universität Chemnitz Wintersemester 2000/2001 Prof. Dr. Werner Dilger Theoretische Informatik 2 3.5. Seite 8 Definition (Kreise in gerichteten Graphen) Sei G = (V, E) ein gerichteter Graph und sei k 2. Ein Weg (v0, v1, ..., vk) in G ist ein Kreis genau dann, wenn v0 = vk. Ein Kreis (v0, v1, ..., vk) heißt einfach genau dann, wenn der Weg (v0, v1, ..., vk) einfach ist. 3.6. Definition (Kreise in ungerichteten Graphen) Sei G = (V, E) ein ungerichteter Graph und sei k 3. Ein Weg (v0, v1, ..., vk) in G ist ein Kreis genau dann, wenn der Weg (v0, v1, ..., vk-1) einfach ist v0 = vk. 3.7. Definition (Grad von Knoten und Adjazenz) Sei G = (V, E) ein ungerichteter Graph und v V. Der Grad von v ist definiert durch Grad(v) = {(v, u) | (v, u) E} = {(u, v) | (u, v) E} Die Nachbarn von v oder die zu v adjazenten Knoten sind definiert durch A(v) = {u | (v, u) E} Sei G = (V, E) ein gerichteter Graph und v V. Dann ist Ausgangsgrad(v) = {(v, u) | (v, u) E} Eingangsgrad(v) = {(u, v) | (u, v) E} Die Nachbarn von v oder die zu v adjazenten Knoten sind ebenso definiert wie beim ungerichteten Graph. 3.8. Definition (Darstellung durch Adjazenzlisten und Adjazenzmatrizen) Sei G = (V, E) ein (gerichteter oder ungerichteter) Graph. Die Darstellung von G durch Adjazenzlisten ist ein Array Adj[1..|V|] bestehend aus |V| Adjazenzlisten. Der Array enthält für jeden Knoten u eine Adjazenzliste, nämlich Adj[u], die genau die zu dem Knoten adjazenten Knoten enthält. Statt durch Adjazenzlisten lassen sich Graphen auch durch Adjazenzmatrizen darstellen. Die Adjazenzmatrix eines Graphen G = (V, E) ist eine |V||V|-Matrix. Die Position (vi, vj) der Matrix enthält eine 1, falls (vi, vj) E, sonst eine 0. 4. 4.1. Breitensuche in Graphen Definition (Datenstruktur Schlange oder Queue) Array: Variablen: Prozedur: Funktion: Q head, tail Enqueue(Q, x) Dequeue(Q) Enqueue(Q, x) 1. Q[tail] := x Technische Universität Chemnitz Wintersemester 2000/2001 Prof. Dr. Werner Dilger Theoretische Informatik 2 2. 3. 4. Seite 9 if tail = length[Q] then tail := 1 else tail := tail + 1 Die Laufzeit von Enqueue(Q, x) ist O(1). Dequeue(Q) 1. 2. 3. 4. 5. x := Q[head] if head = length[Q] then head := 1 else head := head + 1 return x Die Laufzeit von Dequeue(Q) ist O(1). Eine Schlange ist eine Datenstruktur zur Verwaltung von Mengen. Das Einfügen und Löschen erfolgt nach dem FIFO-Prinzip in Zeit O(1). Die Suche, ob ein Element in der Schlange enthalten ist, erfordert die Zeit O(n), wobei n die Länge der Schlange ist. 4.2. Algorithmus (Breitensuche, breadth first search) Zur Motivation des Problems der Suche in Graphen wird das folgende Beispiel betrachtet. Gegeben sei der ungerichtete Graph G = (V, E) mit V = {r, s, t, u, v, w, x, y} und E = {(r, s), (r, v), (s, w), (w, t), (w, x), (t, u), (t, x), (u, y), (x, y)}. Er ist in Abbildung 4.1 dargestellt. r s t u v w x y Abbildung 4.1 Darstellung des Prozesses der Durchsuchung des Graphen von Abbildung 4.1 in Abbildung 4.2. Sei G = (V, E) ein (gerichteter oder ungerichteter) Graph und s V (Startknoten). Der Array d[1..|V|] wird definiert durch d[i] := Dist(s, i) für alle i V. Der Array [1..|V|] wird definiert durch [i] ist der Vorgängerknoten bei der Durchsuchung des Graphen G, d.h. derjenige Knoten, von dem aus i gefunden wurde. Das heißt, ([i], i) E. Technische Universität Chemnitz Wintersemester 2000/2001 Prof. Dr. Werner Dilger Theoretische Informatik 2 Seite 10 Abbildung 4.2 Prozedur BFS(G, s) 1. 2. 3. 4. for each u V\{s} ; Inititalisierungsphase, alle Elemente von V werden in do color[u] := weiß unspezifizierter Reihenfolge durchgegangen d[u] := ; es sind noch keine Distanzen bekannt [u] := nil ; es sind noch keine Knoten aufgesucht worden 5. 6. color[s] := grau d[s] := 0 Technische Universität Chemnitz Wintersemester 2000/2001 Prof. Dr. Werner Dilger Theoretische Informatik 2 7. 8. Seite 11 [s] := nil Q := {s} ; s ist aufgesucht, aber noch keine Nachbarn von s ; Q ist die Menge der grau gefärbten Knoten (als Schlange verwaltet) 9. while Q 10. do u := head 11. for each v Adj[u] 12. do if color[v] = weiß 13. then color[v] := grau 14. d[v] := d[u] + 1 15. [v] := u 16. Enqueue(Q, v) 17. Dequeue(Q) 18. color[u] := schwarz 4.3. ; durch tail head testbar ; die Adjazenzliste von u wird abgearbeitet ; hier ist noch d[v] = , v ist offen ; d[v] wird nur einmal gesetzt ; v wird in Q eingetragen ; u wird aus Q gelöscht ; u ist abgeschlossen Satz (Laufzeit von BFS) Sei Twc(n) = max{Laufzeit von BFS(G, s) | G = (V, E), s V und |V| + |E| = n}. Dann gilt: Twc(n) = O(n) 4.4 Definition (zusammenhängend) Sei G = (V, E) ein ungerichteter Graph. G heißt zusammenhängend genau dann, wenn es für alle u, v V einen Weg von u nach v gibt. 4.5 Folgerung (a) G als ungerichteter Graph betrachtet ist ein kreisfreier, zusammenhängender Graph, d.h. ein Baum. s ist die Wurzel des Baums. (b) In G gibt es für jedes v V genau einen einfachen Weg von s nach v. Dieser ist zugleich ein kürzester Weg von s nach v in G. (Beachte, dass es in G mehrere kürzeste Wege geben kann, da G nicht kreisfrei sein muss.) 4.6 Definition (Zusammenhangskomponente) Sei G = (V, E) ein ungerichteter Graph. Ein Teilgraph von G, H = (W, F) (d.h. W V, F E) ist eine (Zusammenhangs-) Komponente genau dann, wenn H ein maximaler zusammenhängender Teilgraph von G ist. 4.7. Algorithmus (Finden von Zusammenhangskomponenten) Eingabe: Ein ungerichteter Graph G = (V, E). Ausgabe: Array A[1..|V|] mit A[u] = A[v] u, v liegen in derselben Komponente. 1. 2. 3. 4. 5. Initialisierung von A mit nil Initialisierung von BSF for each w V if color[w] = weiß then BFS(G, w), aber so modifiziert, dass keine Initialisierung erfolgt, also A[u] = w, falls u im Verlauf aufgesucht wird Technische Universität Chemnitz Wintersemester 2000/2001 Prof. Dr. Werner Dilger Theoretische Informatik 2 5. 5.1. Seite 12 Tiefensuche in Graphen Algorithmus (Tiefensuche, depth first search) Gegeben sei der gerichtete Graph G = (V, E) mit V = {u, v, w, x, y, z} und E = {(u, v), (u, x), (v, y), (w, y), (w, z), (x, v), (y, x)}. Er ist in Abbildung 5.1 dargestellt. u v w x y z Abbildung 5.1 Die Tiefensuche im Beispielgraph von Abbildung 5.1 ist in Abbildung 5.2 dargestellt. Sei G = (V, E) ein (gerichteter oder ungerichteter) Graph. Es werden folgende Arrays definiert: d[1..|V|] - Entdeckzeit (Discovery time), d.h. Zeitpunkt des ersten Aufsuchens eines Knotens. Der Knoten wird hellgrau eingefärbt. f[1..|V|] - Beendezeit (Finishing time), d.h. Zeitpunkt des letzten Aufsuchens eines Knotens. Der Knoten wird dunkelgrau eingefärbt. [1..|V|] - Liste der Vorgängerknoten (wie bei der Breitensuche). Prozedur DFS(G) 1. 2. 3. 4. for each u V do color[u] := weiß [u] := nil time := 0 5. 6. 7. for each u V do if color[u] = weiß then DFS-visit[u] ; Initialisierung ; man fängt also mit irgendeinem Knoten an Prozedur DFS-visit(u) 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. color[u] := grau d[u] := time time := time + 1 for each v Adj[u] do if color[v] = weiß then [v] := u DFS-visit(v) color[u] := schwarz f[u] := time time := time + 1 Technische Universität Chemnitz ; G und time werden als globale Parameter betrachtet ; die Kante (u, v) wird untersucht ; rekursiver Aufruf von DFS-visit Wintersemester 2000/2001 Prof. Dr. Werner Dilger Theoretische Informatik 2 Seite 13 Abbildung 5.2 Es gilt: {f[u] | u V} {d[u] | u V} = {1, 2, ..., 2|V|}. Zwischen Zeiten und Färbung besteht folgender Zusammenhang: Technische Universität Chemnitz Wintersemester 2000/2001 Prof. Dr. Werner Dilger Theoretische Informatik 2 Seite 14 Farbe des Knotens u weiß grau schwarz Zeitverhältnis time < d[u] d[u] time < f[u] f[u] time Beobachtungen 1. Wird DFS-visit(v) direkt oder vermittelt über weitere Aufrufe von DFS-visit(u) aus aufgerufen, d.h. wird v über u aufgesucht, dann gibt es zum Zeitpunkt d[u] einen Weg über lauter weiße Knoten von u nach v. Der Grund dafür ist, dass DFS-visit(v) nur aufgerufen wird, wenn color[v] = weiß. 2. Im Allgemeinen werden nicht alle von einem Knoten u aus in G erreichbaren Knoten über u aufgesucht. 3. Im Allgemeinen werden nicht alle von einem Knoten u aus in G erreichbaren Knoten, die zum Zeitpunkt d[u] weiß sind, über u aufgesucht. Aufgrund der Adjazenzlisten-Darstellung der Knoten in dem Graphen von Abbildung 5.1 (s.u.) erhält man die in Abbildung 5.b dargestellte Struktur von rekursiven Funktionsaufrufen von DFSvisit. Sie entspricht genau dem Tiefensuchwald. uvx vy wyz xv yx 5.2. Laufzeitbestimmung von DFS Organisation des Hauptspeichers bei rekursiven Prozeduraufrufen, vgl. Abbildung 5.3. Befehl hinter DFS-visit(u) Befehl hinter DFS-visit(y) Stack-frame activiation record Speicher für DFS(G) Speicher für DFS-visit(u) Speicher für DFS-visit(y) Programm Globale Daten eines Programms Rücksprungadressen Abbildung 5.3 Für jeden Aufruf der Prozedur wird ein eigenes Stück des Hauptspeichers, genannt Frame, reserviert. Dort werden alle Variablen für den jeweiligen Aufruf samt ihren Werten abgelegt. Am Ende Technische Universität Chemnitz Wintersemester 2000/2001 Prof. Dr. Werner Dilger Theoretische Informatik 2 Seite 15 jedes dieser Stücke steht jeweils eine Rücksprungadresse ins Programm. Der Hauptspeicher wird stackartig verwaltet, d.h. man arbeitet jeweils am Ende des Speicherabschnitts. Der Verwaltungsaufwand für das Einrichten und Löschen eines Frames ist (1). Der Zeitaufwand für DFS und DFS-visit wird folgendermaßen berechnet: Der Durchlauf durch eine Programmzeile, inklusive Prozeduraufruf, erfordert die Zeit (1). Die gesamte Zeit für das einmalige Durchlaufen von DFS-visit ist dann Zeit bei Aufruf + Zeit im Prozedurrumpf 5.3. Satz (Laufzeitbestimmung von DFS) Sei G = (V, E). Die Laufzeit von DFS(G) ist in (|V| + |E|), also linear in der Größe der Adjazenzlistendarstellung. 5.4. Definition (Kantenarten) Sei G = (V, E) ein gerichteter Graph und sei G = (V, E) der depth first-Wald, der sich bei der Ausführung von DFS(G) ergibt. Mittels G werden die Kanten von G in verschiedener Weise klassifiziert. (a) (u, v) heißt Baumkante genau dann, wenn (u, v) E, d.h. v wird von u aus aufgesucht. (b) (u, v) heißt Rückwärtskante genau dann, wenn (u, v) E und v ist (direkter oder indirekter) Vorgänger von u im depth first-Wald. (c) (u, v) heißt Vorwärtskante genau dann, wenn (u, v) E und v ist (direkter oder indirekter) Nachfolger von u im depth first-Wald. (d) (u, v) heißt Kreuzkante genau dann, wenn (u, v) E und u ist weder Nachfolger noch Vorgänger von v im depth first-Wald. u und v können im selben oder in verschiedenen Bäumen des Waldes liegen. Diese Kantenarten sind im gerichteten Graphen wohldefiniert, da jede Kante nur einmal überquert wird. Im ungerichteten Graphen ist die Richtung des ersten Überquerens einer Kante maßgebend. Deshalb gibt es im ungerichteten Graphen keine Kreuz- und keine Vorwärtskanten. 6. 6.1. Anwendung der Tiefensuche: Starke Zusammenhangskomponenten Definition (Starke Zusammenhangskomponente) (a) Sei G = (V, E) ein gerichteter Graph. G heißt stark zusammenhängend genau dann, wenn es für alle u, v V einen Weg von u nach v und einen Weg von v nach u gibt. (b) Sei G = (V, E) ein gerichteter Graph. Ein Teilgraph H = (W, F) von G ist eine starke Zusammenhangskomponente genau dann, wenn H ein maximaler Teilgraph und stark zusammenhängend ist. 6.2. Definition (Umkehrung eines gerichteten Graphen) Sei G = (V, E) ein gerichteter Graph. Die Umkehrung von G, GU = (V, EU) ist definiert durch Technische Universität Chemnitz Wintersemester 2000/2001 Prof. Dr. Werner Dilger Theoretische Informatik 2 Seite 16 EU = {(u, v) | (v, u) E} Die Adjazenzlistendarstellung von GU kann in Zeit (|V| + |E|) aus der von G ermittelt werden. Dies geschieht dadurch, dass man für alle Elemente k1, k2, ..., kn von Adj[i] in G in die Adjazenzlisten AdjU[k1], AdjU[k2], ..., AdjU[kn] den Wert i einträgt. Beispiel Abbildung 6.1 zeigt einen gerichteten Graphen und seine Umkehrung. Die Knoten bleiben also gleich, die Kanten ändern ihre Richtung. A A B B E E F C G D H F C G D H I I Abbildung 6.1 6.3. Algorithmus (Starke Zusammenhangskomponenten) Eingabe ist ein gerichteter Graph G = (V, E). Prozedur Komp(G) 1. Berechne DFS(G) und speichere alle Knoten in einer Liste (v1, v2, ..., vn), so dass f[vi] > f[vi+1] für alle i = 1, ..., n-1. vn ist also der Knoten, der zuerst schwarz gefärbt wird, v1 zuletzt. 2. Ermittle GU. 3. Berechne DFS(GU) und besuche dabei die Knoten in der Hauptschleife von DFS nach absteigenden Werten f[u] (gemäß der Liste aus 1.). Notiere die Knoten jedes Baums in einer Liste. 4. Gib die Listen aus 3. als Knoten der starken Zusammenhangskomponenten aus. Die Laufzeit von Komp(G) ergibt sich im Wesentlichen aus dem zweimaligen Aufruf von DFS, einmal für G und einmal für GU, und ist damit (|V| + |E|). 7. 7.1. Sortieren Beispiel Betrachte den folgenden Sortieralgorithmus, genannt selection sort: Sei A ein Array, der n Zahlen enthält und B ein zweites Array der Länge n. Führe folgende Schritte aus: Technische Universität Chemnitz Wintersemester 2000/2001 Prof. Dr. Werner Dilger Theoretische Informatik 2 Seite 17 1. Suche das größte Element und trage es an die letzte Stelle von B ein. 2. Suche das zweitgrößte Element und trage es an die zweitletzte Stelle von B ein. . . . Die Laufzeit dieses Algorithmus ist O(n2), denn für jedes Element müssen maximal n Suchschritte durchgeführt werden. 7.2. Beispiel Ein anderer Sortieralgorithmus. 1. 2. 3. 4. 5. 6. 7. i := 1 t := true while i < n 1 do if A[i] > A[i+1] then t := false ; t = false genau dann, wenn A nicht sortiert i := i + 1 if A = true then return A else sortiere A mit selection sort 7.3. Algorithmus (Counting Sort) Prozedur Counting Sort(A, B, k) 1. 2. 3. 4. 5. 6. 7. 8. 9. for i = 1 to k ; die zu sortierenden Elemente sind aus {1, ..., k} do C[i] := 0 for j = 1 to length(A) do C[A[j]] := C[A[j]] + 1 for i = 2 to k do C[i] := C[i] + C[i1] for j = length(A) downto 1 do B[C[A[j]]] := A[j] C[A[j]] := C[A[j]] 1 Der Zeitbedarf für den Algorithmus Counting Sort wird wie folgt bestimmt: Sei n = length(A). 1. + 2.: 3. + 4.: 5. + 6.: 7. – 9.: O(k) O(n) O(k) O(n) also insgesamt O(n + k). Ist k = O(n), dann erhält man als Zeitbedarf O(n). 7.4. Algorithmus (Radix Sort) Eingabe: Ausgabe: Ein Array A von zu sortierenden Elementen und die Stellenanzahl d. Das sortierte Array A. 1. for i = 1 to d 2. do sortiere A auf Stelle i mit einem stabilen Sortieralgorithmus Technische Universität Chemnitz Wintersemester 2000/2001 Prof. Dr. Werner Dilger Theoretische Informatik 2 8. 8.1. Seite 18 Divide-and-Conquer: Multiplikation großer Zahlen Algorithmen (Multiplikation in O(n2)) (a) Schulmethode (a1...an)(b1...bn) (a1...an)b1 (a1...an)b2 (a1...an)b3 . . . (a1...an)bn O(n) O(n) O(n) . . . O(n) gleiche Konstanten nO(n) = O(n2) für Multiplikationen O(n)O(n) ..... O(n) O(n2) für Addition Die gesamte Laufzeit ist also O(n2). (b) Multiplikation mit Divide-and-Conquer. Sei n eine Zweierpotenz. Zerlege die Zahl a1...an in a‘ = a1 ...a n und a“ = a n 1 ...a n und die Zahl 2 2 b1...bn in b‘ = b1 ...b n und b“ = b n 1 ...bn . Alle vier Teilstücke haben 2 2 n 2 Stellen. Handelt es sich um Zahlen zur Basis 10, dann ist n n a1 ...a n a '10 2 a" und b1 ...bn b'10 2 b" n n n n Damit folgt: a b a'10 2 a" b'10 2 b" a' b'10 n a' b"10 2 a" b'10 2 a" b" . Mit den Produkten a‘b‘,a‘b“,a“b‘ und a“b“ fährt man in analoger Weise fort. Prozedur Mult(X, Y, n) X und Y sind Zahlen zur Basis 2, 0 X, Y < 2n, n ist eine Zweierpotenz. 1.a 1.b 1.c 2.a 2.b 3.a 3.b 4. 5. 6. 7. if n = 1then if X = 1 and Y = 1 8. return m1 2 n m2 2 2 m3 2 2 m4 then return 1 else return 0 A‘ := Bit 1, ..., Bit n2 von A A“ := Bit ( n2 +1), ..., Bit n von A B‘ := Bit 1, ..., Bit n2 von B B“ := Bit ( n2 +1), ..., Bit n von B m1 := Mult(A‘, B‘, n2 ) m2 := Mult(A‘, B“, n2 ) m3 := Mult(A“, B‘, n2 ) m4 := Mult(A“, B“, n2 ) n n Divide Conquer Technische Universität Chemnitz Wintersemester 2000/2001 Prof. Dr. Werner Dilger Theoretische Informatik 2 8.2. Seite 19 Satz (Laufzeit der Divide-and-Conquer-Multiplikation) Die Laufzeit der Prozedur Mult(X, Y, n) ist O(n2). 8.3. Ist Satz (Laufzeit der Divide-and-Conquer-Multiplikation bei Dreifachunterteilung) T(1) = d T(n) = 3T( n2 ) + dn so ist T(n) von O(nlog 3), dies ist von O(n1,59). 8.4. Algorithmus (Multiplikation in O n log 3 ) Das Aufteilen des Multiplikationsproblems in drei Teile erfolgt in folgender Weise: Zunächst werden die Zahlen a und b wie gehabt in zwei Teile zerlegt. Es ist also a = a1... an, a‘ = a1... an/2, a“ = an/2 + 1... an, b = b1... bn, b‘ = b1... bn/2, b“ = bn/2 + 1... bn. Für Zahlen zur Basis 10 wird folgende Zerlegung vorgenommen: a ' b' 1.Teilproblem 2.Teilproblem 3.Teilproblem 1.Teilproble m 3.Teilproble m n 2 10 ((a'a" )(b"b' ) a' b' a" b" ) 10 a" b" n n = a' b'10 n (a' b"a' b'a" b"a" b'a' b'a" b" ) 10 2 a" b" n n = a' b'10 n a' b"10 2 a" b'10 2 a" b" = ab Beispiel: a = 25, b = 16. Die Berechnung nach obigem Schema ergibt: 21100 + 2610 + 5110 + 56 = 200 + 120 + 50 + 30 = 400 Programm für Zahlen zur Basis 2: Prozedur Mult(X, Y, n) X, Y sind ganze Zahlen mit Vorzeichen, 0 |X|, |Y| < 2n, n ist eine Zweierpotenz. ; sgn(X) = +1 – positives Vorzeichen, sgn(X) = -1 – negatives Vorzeichen 1. s := sgn(X)sgn(Y) 2. X := |X| 3. Y := |Y| 4. if n = 1 then 5. if X = 1 and Y = 1 6. then return s 7. else return 0 8. A‘ := Bit 1 ... Bit n2 von A 9. A“ := Bit n2 +1 ... Bit n von A 10. B‘ := Bit 1 ... Bit n2 von B 11. B“ := Bit n2 +1 ... Bit n von B 12. m1 := Mult(A‘, B‘, n2 ) Technische Universität Chemnitz Wintersemester 2000/2001 Prof. Dr. Werner Dilger Theoretische Informatik 2 Seite 20 13. m2 := Mult(A‘A“, B‘B“, n2 ) 14. m3 := Mult(A“, B“, n2 ) 15. return(s(m12n + (m1 + m2 + m3) 2n/2 + m3)) Die Prozedur erfüllt die Bedingungen von Satz 14.3, daher ist die Laufzeit O n log 3 . Technische Universität Chemnitz Wintersemester 2000/2001 Prof. Dr. Werner Dilger