2 - Technische Universität Chemnitz

Werbung
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 + 7amax  t  3 + 8
Das Problem bei der Bestimmung von T ist 1
beliebige Programme, dass T eine etwas
4amax oder 5amax
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
 3amax

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  CTwc(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:
Dn  Twc(n)  Cn
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:
Df(n)  g(n)  Cf(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)  Cf(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)  Df(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
Df(n)  Twc(n)  Cf(n)
also ist Twc(n) von (f(n)).
2.6.
Beispiele
(a) Betrachte die Schleife
S  for i = 1 to 2n 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) = 2nC + 2ng(n)
worst case-Laufzeit von S  C


 





1. Durchlauf
2. Durchlauf
2 n. Durchlauf
Die worst case-Laufzeit ist also von O(n) + O(ng(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)0ik
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.
uvx
vy
wyz
xv
yx
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[i1]
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
nO(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) = 3T( n2 ) + dn
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:
21100 + 2610 + 5110 + 56 = 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(m12n + (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
Herunterladen