Algorithmen und Datenstrukturen
Werner Struckmann
Wintersemester 2005/06
9. Entwurf von Algorithmen
9.1 Einführung
9.2 Teile-und-Beherrsche-Algorithmen 9.3 Gierige Algorithmen
9.4 Backtracking-Algorithmen
9.5 Dynamische Programmierung
Entwurf von Algorithmen
In diesem Kapitel stellen wir anhand von Beispielen einige typische Prinzipien für den Entwurf von Algorithmen vor.
Die folgenden Techniken haben wir (implizit oder explizit) bereits kennen gelernt.
◮ Schrittweise Verfeinerung des Problems
◮ Reduzierung der Problemgröße durch Rekursion
◮ Einsatz von Algorithmenmustern
9.1 Einführung 9-1
Schrittweise Verfeinerung des Problems
◮ Die erste Formulierung des Problems erfolgt in einem sehr abstrakten Pseudocode.
◮ Die schrittweise Verfeinerung basiert auf dem Ersetzen von Pseudocode durch verfeinerten Pseudocode
◮ und letztlich durch konkrete Algorithmenschritte.
9.1 Einführung 9-2
Problemreduzierung durch Rekursion
◮ Diese Technik kann angewendet werden, wenn das Problem auf ein gleichartiges, aber kleineres Problem zurückgeführt werden kann.
◮ Die Rekursion muss schließlich auf ein oder mehrere kleine Probleme führen, die sich direkt lösen lassen.
◮ Rekursion bietet sich an, wenn die Problemstruktur rekursiv aufgebaut ist. Beispiele: Listen, Bäume.
◮ Zu rekursiven Lösungen gibt es iterative Entsprechungen
(zum Beispiel durch Einsatz eines Kellers, s. Aufgabe 23). Bei der Auswahl zwischen iterativer und rekursiver Lösung ist die Effizienz der Realisierung zu berücksichtigen.
9.1 Einführung 9-3
Einsatz von Algorithmenmustern
Beispiele für Algorithmenmuster:
◮ Inkrementelle Vorgehensweise
◮ Teile-und-Beherrsche-Algorithmen
◮ Gierige Algorithmen (Greedy Algorithmen)
◮ Backtracking-Algorithmen
◮ Dynamische Programmierung
Die Zuordnung eines Musters zu einem Algorithmus ist nicht immer eindeutig – und manchmal sogar unmöglich.
Beispielsweise kann der Algorithmus von Kruskal als
inkrementeller und als gieriger Algorithmus gesehen werden.
Es gibt weitere Algorithmenmuster.
9.1 Einführung 9-4
Inkrementelle Vorgehensweise
Beispiel: Sortieren durch Einfügen benutzt eine inkrementelle Herangehensweise. Nachdem das Teilfeld a
[
1..j − 1]
sortiert wurde, wird das Element a[
j]
an der richtigen Stelle eingefügt, woraus sich das sortierte Teilfeld a[
1..j]
ergibt.Weitere Beispiele:
◮ Algorithmus von Kruskal
◮ Algorithmus von Prim
Beide Algorithmen bauen schrittweise einen minimalen Spannbaum auf.
9.1 Einführung 9-5
Teile-und-Beherrsche-Algorithmen
◮ Teile das Problem in eine Anzahl von Teilproblemen auf.
◮ Beherrsche die Teilprobleme durch rekusives Lösen. Wenn die Teilprobleme hinreichend klein sind, dann löse sie auf direktem Wege.
◮ Verbinde die Lösungen der Teilprobleme zur Lösung des Ausgangsproblems.
9.2 Teile-und-Beherrsche-Algorithmen 9-6
Beispiel: Sortieren durch Mischen
Sortieren durch Mischen (Mergesort, vgl. Abschnitt 3.2) arbeitet rekursiv nach folgendem Schema:
1. Teile die Folge in zwei Teilfolgen auf.
2. Sortiere die beiden Teilfolgen.
3. Mische die sortierten Teilfolgen.
4 2 9 5 8 2 1 6
4 2 9 5 8 2 1 6
2 4 5 9 1 2 6 8
1 2 2 4 5 6 8 9
9.2 Teile-und-Beherrsche-Algorithmen 9-7
Beispiel: Sortieren durch Mischen
◮ Alternativ könnte man die Liste auch in mehr als zwei Listen aufteilen, hätte dann aber in der Mischphase größeren
Aufwand.
◮ Allgemein: Die Rekursionstiefe kann durch stärkere Spaltung verringert werden. Dies bedingt allerdings einen größeren Aufwand in der Teile- und der Zusammenführungsphase.
9.2 Teile-und-Beherrsche-Algorithmen 9-8
Beispiel: Türme von Hanoi
◮ n Scheiben verschiedener Größe sind aufeinandergestapelt.
Es liegen stets nur kleinere Scheiben auf größeren.
◮ Der gesamte Stapel soll Scheibe für Scheibe umgestapelt werden.
◮ Ein dritter Stapel darf zur Zwischenlagerung benutzt werden, ansonsten dürfen die Scheiben nirgendwo anders abgelegt werden.
◮ Auch in jedem Zwischenzustand dürfen nur kleinere Scheiben auf größeren liegen.
9.2 Teile-und-Beherrsche-Algorithmen 9-9
Beispiel: Türme von Hanoi
Gesucht ist ein Algorithmus, der dieses Problem löst. Dazu muss der Algorithmus angeben, in welcher Reihenfolge die Scheiben zu bewegen sind.
1. Bringe die obersten n − 1 Scheiben von Turm 1 zu Turm 3.
2. Bewege die unterste Scheibe von Turm 1 zu Turm 2.
3. Bringe die n − 1 Scheiben von Turm 3 zu Turm 2.
1
2 3
9.2 Teile-und-Beherrsche-Algorithmen 9-10
Beispiel: Türme von Hanoi
1. Bringe die obersten n − 1 Scheiben von Turm 1 zu Turm 3.
2. Bewege die unterste Scheibe von Turm 1 zu Turm 2.
3. Bringe die n − 1 Scheiben von Turm 3 zu Turm 2.
proc hanoi(n: int; t1, t2, t3: Turm) begin if n > 1 then
hanoi(n-1, t1, t3, t2); fi;
<bewege die Scheibe von t1 nach t2>;
if n > 1 then
hanoi(n-1, t3, t2, t1); fi;
end
9.2 Teile-und-Beherrsche-Algorithmen 9-11
Beispiel: Türme von Hanoi
◮ Diese Lösung besteht aus einer rekursiven Prozedur
hanoi(n, t1, t2, t3)
.◮ Die Problemgröße ist n.
◮ Der Aufruf
hanoi(n, t1, t2, t3)
bewegt den Stapel vont1
nacht2
und verwendett3
als Hilfsstapel.◮ Für die Anzahl T
(
n)
der notwendigen Schritte gilt T(
n) =
1 für n
=
1,2T
(
n − 1) +
1 für n > 1.◮ Explizit ergibt sich T
(
n) =
2n − 1.9.2 Teile-und-Beherrsche-Algorithmen 9-12
Komplexität von Teile-und-Beherrsche-Algorithmen
Die Problemgröße sei durch eine natürliche Zahl n gegeben. Die Berechnung der Komplexität führt häufig auf
Rekurrenzgleichungen der Form
T
(
n) =
Θ(
1)
, falls n klein ist,aT
(
n/b) +
f(
n)
, falls n groß genug ist, oderT
(
n) =
f(
T(
n − 1)
, . . . ,T(
n − k))
mit gegebenen Anfangswerten T(
0)
,. . . ,T(
k − 1)
. Beispiel: Sortieren durch Mischen, Türme von Hanoi.Wir wiederholen jetzt das Mastertheorem und das Verfahren zur Lösung linearer Rekurrenzgleichungen mit konstanten
Koeffizienten.
9.2 Teile-und-Beherrsche-Algorithmen 9-13
Beispiel: Die Multiplikation nach Karatsuba
◮ Wie groß ist die Komplexität des klassischen Verfahrens zur Multiplikation natürlicher Zahlen?
◮ Wenn der erste Faktor n-stellig und der zweite m-stellig ist, dann müssen zuerst n · m Einzelmultiplikationen durchgeführt werden. Anschließend sind m Zahlen der Maximallänge
n
+
m zu addieren. Das Ergebnis ist im Allgemeinen eine (n+
m)-stellige Zahl.◮ Den größten Anteil trägt offenbar das Produkt n · m bei.
◮ Die Komplexität des Verfahrens liegt daher in
Θ(
n · m)
bzw. inΘ(
n2)
, wenn beide Zahlen die Länge n besitzen.9.2 Teile-und-Beherrsche-Algorithmen 9-14
Beispiel: Die Multiplikation nach Karatsuba
◮ Im Jahre 1962 stellte A. Karatsuba ein schnelleres Verfahren zur Multiplikation vor.
◮ Die Idee besteht darin, die Zahlen x und y der Länge ≤ n in Stücke der Länge ≤ n/2 aufzuteilen, sodass
x
=
a · 10n/2+
b y=
c · 10n/2+
dgilt.
◮ Beispiel: n
=
4, x=
3141=
31 · 102+
419.2 Teile-und-Beherrsche-Algorithmen 9-15
Beispiel: Die Multiplikation nach Karatsuba
◮ Wir erhalten:
x · y
= (
a · 10n/2+
b)(
c · 10n/2+
d)
=
ac · 10n+ (
ad+
bc)
· 10n/2+
bd=
ac · 10n+ ((
a+
b)(
c+
d)
− ac − bd)
· 10n/2+
bd◮ Die Berechnung des Produkts zweier Zahlen x und y der Länge ≤ n wird zurückgeführt auf die Berechnung der drei Produkte ac, bd und (a + b)(c + d) der Länge ≤ n/2.
◮ Dann wird dasselbe Verfahren rekursiv auf diese drei Produkte angewendet.
9.2 Teile-und-Beherrsche-Algorithmen 9-16
Beispiel: Die Multiplikation nach Karatsuba
Beispiel: x
=
3141,y=
5927x · y
=
3141 · 5927=
31 · 59 · 104+
((
31+
41)(
59+
27)
− 31 · 59 − 41 · 27)
· 102+
41 · 27=
31 · 59 · 104+
(
72 · 86 − 31 · 59 − 41 · 27)
· 102+
41 · 27=
1829 · 104+ (
6192 − 1829 − 1107)
· 102+
1107=
1829 · 104+
3256 · 102+
1107=
186167079.2 Teile-und-Beherrsche-Algorithmen 9-17
Beispiel: Die Multiplikation nach Karatsuba
◮ Für die Komplexität T
(
n)
des Verfahrens gilt:T
(
n) =
k, n
=
13 · T n
2
+
kn, n > 1◮ Diese Rekurrenzgleichung besitzt die Lösung T
(
n) =
3knlog2(3) − 2kn= Θ
nlog2(3)
= Θ
n1,585 .
◮ Das ist deutlich günstiger als
Θ
n2. Allerdings wirken sich die Verbesserungen erst bei großen Zahlen aus.
9.2 Teile-und-Beherrsche-Algorithmen 9-18
Beispiel: Die Multiplikation nach Karatsuba
◮ Wir haben oben die Faktoren x und y in je zwei Teile zerlegt.
Durch Aufspalten in noch mehr Teile können wir die Laufzeit weiter verbessern:
Für jedes ε > 0 gibt es ein Multiplikationsverfahren, das höchstens c
(
ε)
n1+ε Schritte benötigt. Die Konstante c(
ε)
hängt nicht von n ab.◮ In den 1970er Jahren wurde diese Schranke auf O
(
n log(
n)
log(
log(
n)))
verbessert.
9.2 Teile-und-Beherrsche-Algorithmen 9-19
Gierige Algorithmen
Annahmen:
◮ Es gibt eine endliche Menge von Eingabewerten.
◮ Es gibt eine Menge von Teillösungen, die aus den Eingabewerten berechnet werden können.
◮ Es gibt eine Bewertungsfunktion für Teillösungen.
◮ Die Lösungen lassen sich schrittweise aus Teillösungen, beginnend bei der leeren Lösung, durch Hinzunahme von Eingabewerten ermitteln.
◮ Gesucht wird eine/die optimale Lösung.
Vorgehensweise:
◮ Nimm (gierig) immer das am besten bewertete Stück.
9.3 Gierige Algorithmen 9-20
Beispiel: Algorithmus zum Geldwechseln
◮ Münzwerte: 1, 2, 5, 10, 20, 50 Cent und 1, 2 Euro.
◮ Wechselgeld soll mit möglichst wenig Münzen ausgezahlt werden. 1,42 €: 1 € + 20 Cent + 20 Cent + 2 Cent
Allgemein: Wähle im nächsten Schritt die größtmögliche Münze.
In unserem Münzsystem gibt diese Vorgehensweise immer die optimale Lösung. Im Allgemeinen gilt dies nicht. Angenommen, es stünden 1, 5 und 11 Cent Münzen zur Verfügung. Um 15 Cent
herauszugeben, ergäbe sich:
◮ gierig: 11 + 1 + 1 + 1 + 1,
◮ optimal: 5 + 5 + 5.
9.3 Gierige Algorithmen 9-21
Gierige Algorithmen
func greedy(E: Eingabemenge): Ergebnis begin var L: Ergebnismenge;
var x: Element;
E.sort();
while! E.empty() do x
←E.first();
E.remove(x);
if valid(
L ∪ {x}) then L.add(x); fi;
od;
return L;
end
9.3 Gierige Algorithmen 9-22
Beispiel: Bedienreihenfolge im Supermarkt
◮ n Kunden warten vor einer Kasse.
◮ Der Bezahlvorgang von Kunde i dauere ci Zeiteinheiten.
◮ Welche Reihenfolge der Bedienung der Kunden führt zur Minimierung der mittleren Verweilzeit (über alle Kunden)?
Die Gesamtbedienzeit Tges
=
Pni=1 ci ist konstant.
Die mittlere Verweilzeit ist T
=
1n
(
c1+ (
c1+
c2) +
· · ·+ (
c1+
· · ·+
cn))
=
1n
(
nc1+ (
n − 1)
c2+ (
n − 2)
c3+
· · ·+
2cn−1+
cn)
=
1 nn
X
k=1
(
n − k+
1)
ck9.3 Gierige Algorithmen 9-23
Beispiel: Bedienreihenfolge im Supermarkt
Die mittlere Verweilzeit pro Kunde
◮ steigt, wenn Kunden mit langer Bedienzeit vorgezogen werden.
◮ sinkt, wenn Kunden mit kurzer Bedienzeit zuerst bedient werden.
◮ wird minimal, wenn die Kunden nach ci aufsteigend sortiert werden.
Konsequenzen:
◮ Greedy-Algorithmus ist geeignet.
◮ Die Funktion zur Bestimmung des nächsten Kandidaten wählt den Kunden mit minimaler Bedienzeit.
Frage: Ist dies eine geeignete Strategie für die Prozessorvergabe?
9.3 Gierige Algorithmen 9-24
Beispiel: Algorithmus von Kruskal
◮ Selektiere fortwährend eine verbleibende Kante mit
geringstem Gewicht, die keinen Zyklus erzeugt, bis alle Knoten verbunden sind (Kruskal, 1956).
◮ Eine eindeutige Lösung ist immer dann vorhanden, wenn alle Gewichte verschieden sind.
4
8 6
2
5 7 6
3 2
3 3
5 4
6
Nach Wahl der Kanten 2, 2, 3 und 3 darf die verbleibende 3 nicht gewählt werden, da sonst ein Zyklus entstünde.
9.3 Gierige Algorithmen 9-25
Matroide
◮ Greedy-Algorithmen liefern nicht immer eine optimale Lösung.
◮ Mithilfe der Theorie der gewichteten Matroide kann bestimmt werden, wann ein Greedy-Algorithmus eine optimale Lösung liefert.
◮ Die Theorie der bewerteten Matroide deckt nicht alle Fälle ab.
9.3 Gierige Algorithmen 9-26
Matroide
Ein Matroid ist ein Paar M
= (
S, I)
mit folgenden Eigenschaften:◮ S ist eine endliche Menge.
◮ I ist eine nichtleere Familie von Teilmengen von S.
◮ Vererbungseigenschaft: Sind A ⊆ B und B ∈ I, so ist A ∈ I.
◮ Austauscheigenschaft: Sind A, B ∈ I und |A| < |B|, so gibt es ein x ∈
(
B \ A)
mit A ∪ {x} ∈ I.Die Mengen in I heißen unabhängig.
Eine unabhängige Menge A ∈ I heißt maximal, wenn es keine Erweiterung x mit A ∪ {x} ∈ I gibt.
9.3 Gierige Algorithmen 9-27
Matroide
◮ Ein Matroid M
= (
S, I)
heißt gewichtet, wenn es eine Gewichtsfunktion w:
S → R+ gibt.◮ Die Gewichtsfunktion lässt sich auf Teilmengen A ⊆ S durch w
(
a) =
Xx∈A
w
(
A)
erweitern.◮ Eine Menge A ∈ I mit maximalem Gewicht heißt optimal.
9.3 Gierige Algorithmen 9-28
Matroide
Satz: Es sei M
= (
S,I)
ein gewichtetes Matroid mit derGewichtsfunktion w
:
S → R+. Der folgende gierige Algorithmus gibt eine optimale Teilmenge zurück.func greedy(M, w):
Ibegin A
← ∅;
sortiere S in monoton fallender Reihenfolge nach dem Gewicht w;
foreach x
∈S do if A
∪{x}
∈ Ithen A
←A
∪{x};
return A;
end
9.3 Gierige Algorithmen 9-29
Matroide
Satz: Die Komplexität des gierigen Algorithmus liegt in O
(
n logn+
n · f(
n))
,wobei
◮ n log n der Aufwand für das Sortieren und
◮ f
(
n)
der Aufwand für den Test A ∪ {x}ist. n ist die Anzahl der Elemente von S, d. h. |S|
=
n.9.3 Gierige Algorithmen 9-30
Matroide
Beispiel: Es sei G
= (
V,E)
ein ungerichteter Graph. Dann ist MG= (
SG, IG)
ein Matroid, dabei gilt:◮ SG
=
E,◮ A ⊆ E: A ∈ IG ⇔ A azyklisch.
Eine Menge A von Kanten ist genau dann unabhängig, wenn der Graph GA
= (
V, A)
einen Wald bildet.Der Algorithmus von Kruskal ist ein Beispiel für das obige allgemeine Verfahren.
9.3 Gierige Algorithmen 9-31
Backtracking-Algorithmen
◮ Das Backtracking realisiert eine systematische Suchtechnik, die die Menge aller möglichen Lösungen eines Problems vollständig durchsucht.
◮ Führt die Lösung auf einem Weg nicht zum Ziel, wird zur letzten Entscheidung zurückgegangen und dort eine
Alternative untersucht.
◮ Da alle möglichen Lösungen untersucht werden, wird eine Lösung – wenn sie existiert – stets gefunden.
9.4 Backtracking-Algorithmen 9-32
Beispiel: Wegsuche in einem Labyrinth
Gesucht ist ein Weg in einem Labyrinth von einem Start- zu einem Zielpunkt.
◮ Gehe zur ersten Kreuzung und schlage dort einen Weg ein.
◮ Markiere an jeder Kreuzung den eingeschlagenen Weg.
◮ Falls eine Sackgasse gefunden wird, gehe zurück zur letzten Kreuzung, die einen noch nicht untersuchten Weg aufweist und gehe diesen Weg.
9.4 Backtracking-Algorithmen 9-33
Beispiel: Wegsuche in einem Labyrinth
◮ Die besuchten Kreuzungspunkte werden als Knoten eines Baumes aufgefasst.
◮ Der Startpunkt bildet die Wurzel dieses Baumes. Blätter sind die Sackgassen und der Zielpunkt.
◮ Der Baum wird beginnend mit der Wurzel systematisch aufgebaut.
◮ Wegpunkte sind Koordinatentupel
(
x, y)
. Im Beispiel ist(
1, 1)
der Startpunkt und(
1, 3)
der Zielpunkt.(1,1) (2,1)
(2,2) (3,1)
(1,2) (2,3) (3,2)
(3,3) (1,3)
9.4 Backtracking-Algorithmen 9-34
Backtracking-Algorithmen
◮ Es gibt eine endliche Menge K von Konfigurationen.
◮ K ist hierarchisch strukturiert:
◮ Es gibt eine Ausgangskonfiguration k0 ∈ K .
◮ Zu jeder Konfiguration kx ∈ K gibt es eine Menge kx 1, . . . ,kx nx von direkt erreichbaren Folgekonfigurationen.
◮ Für jede Konfiguration ist entscheidbar, ob sie eine Lösung ist.
◮ Gesucht werden Lösungen, die von k0 aus erreichbar sind.
9.4 Backtracking-Algorithmen 9-35
Backtracking-Algorithmen
proc backtrack(k: Konfiguration) Konfiguration begin
if k ist Lösung then print(k); fi;
foreach Folgekonfiguration k’ von k do backtrack(k’); od;
end
◮ Dieses Schema terminiert nur, wenn der Lösungsraum
endlich und wiederholte Bearbeitung einer bereits getesteten Konfiguration ausgeschlossen ist (keine Zyklen).
◮ Kritisch ist ggf. der Aufwand. Er ist häufig exponentiell.
9.4 Backtracking-Algorithmen 9-36
Backtracking-Algorithmen (Varianten)
◮ Lösungen werden bewertet. Zuletzt wird die beste ausgewählt.
◮ Das angegebene Schema findet alle Lösungen. Oft genügt es, den Algorithmus nach der ersten Lösung zu beenden.
◮ Aus Komplexitätsgründen wird eine maximale Rekursionstiefe vorgegeben. Als Lösung dient dann beispielsweise die am besten bewertete Lösung aller bisher gefundenen.
◮ Branch-and-Bound-Algorithmen.
9.4 Backtracking-Algorithmen 9-37
Branch-and-Bound-Algorithmen
◮ Das angegebene Schema untersucht jeden Konfigurationsteilbaum.
◮ Oft kann man schon im Voraus entscheiden, dass es sich nicht lohnt, einen bestimmten Teilbaum zu besuchen.
◮ Dies ist zum Beispiel bei einer Sackgasse der Fall oder wenn man weiß, dass die zu erwartende Lösung auf jeden Fall
schlechter sein wird, als eine bisher gefundene.
◮ In diesem Fall kann auf die Bearbeitung des Teilbaums verzichtet werden.
9.4 Backtracking-Algorithmen 9-38
Branch-and-Bound-Algorithmen
Beispiele:
◮ Spiele (insbesondere rundenbasierte Strategiespiele), zum Beispiel Schach.
Konfigurationen entsprechen den Stellungen,
Nachfolgekonfigurationen sind durch die möglichen Spielzüge bestimmt. Nachweisbar schlechte Züge müssen nicht
untersucht werden.
◮ Erfüllbarkeitstests von logischen Aussagen.
◮ Planungsprobleme.
◮ Optimierungsprobleme.
9.4 Backtracking-Algorithmen 9-39
Beispiel: Das N-Damen-Problem
Es sollen alle Stellungen von n Damen auf einem
n × n-Schachbrett ermittelt werden, bei denen keine Dame eine andere bedroht. Es dürfen also nicht zwei Damen in der gleichen Zeile, Spalte oder Diagonale stehen.
1 2 3 4 5 6 7 8 1
2 3 4 5 6 7 8
1 2 3 4 5 6 7 8 1
2 3 4 5 6 7 8
9.4 Backtracking-Algorithmen 9-40
Beispiel: Das N-Damen-Problem
◮ K sei die Menge aller Stellungen mit einer Dame in jeder der ersten m Zeilen, 0 ≤ m ≤ n, sodass je zwei Damen sich nicht bedrohen.
◮ K enthält alle Lösungen. Nicht jede Stellung lässt sich allerdings zu einer Lösung erweitern. So ist zum Beispiel unten jedes Feld in der 7. Zeile bereits bedroht, sodass dort keine Dame mehr gesetzt werden kann.
◮ Durch Ausnutzung von Symmetrien lässt sich der Aufwand verringern.
1 2 3 4 5 6 7 8 1
2 3 4 5 6 7 8
9.4 Backtracking-Algorithmen 9-41
Beispiel: Das N-Damen-Problem
proc platziere(zeile: int) begin var i: int;
for i
←1 to n do
if <feld (zeile, i) nicht bedroht> then
<setze Dame auf (zeile, i)>;
if zeile = n then
<gib Konfiguration aus>;
else platziere(zeile + 1); fi;
fi;
od;
end
9.4 Backtracking-Algorithmen 9-42
Beispiel: Das N-Damen-Problem
1 2 3 4 1
2 3 4
1 2 3 4 1
2 3 4
1 2 3 4 1
2 3 4
1 2 3 4 1
2 3 4
1 2 3 4 1
2 3 4 1 2 3 4
1 2 3 4
1 2 3 4 1
2 3 4
1 2 3 4 1
2 3 4 1 2 3 4
1 2 3 4
Sackgasse
Sackgasse
L¨osung
9.4 Backtracking-Algorithmen 9-43
Beispiel: Das N-Damen-Problem
◮ Das N-Damen-Problem ist für n ≥ 4 lösbar. Wenn die erste Dame nicht richtig gesetzt ist, werden allerdings bis zu
(
n − 1)!
Schritte benötigt, um dies herauszufinden. Nach der stirlingschen Formel istn
!
≈ nn · e−n √2πn, der Aufwand also exponentiell.
◮ Für jedes n ≥ 4 ist ein Verfahren bekannt (Ahrens, 1912), das in linearer Zeit eine Lösung findet (nur eine, nicht alle). Es basiert auf der Beobachtung, dass in Lösungsmustern häufig Rösselsprung-Sequenzen auftreten.
◮ Im Jahre 1990 ist ein schneller probabilistischer Algorithmus veröffentlicht worden, dessen Laufzeit in O
(
n3)
liegt.9.4 Backtracking-Algorithmen 9-44
Beispiel: Problem des Handlungsreisenden
◮ Gegeben seien n durch Straßen verbundene Städte mit Reisekosten c
(
i,j)
zwischen je zwei Städten i und j, 1 ≤ i, j ≤ n.◮ Gesucht ist die billigste Rundreise, die jede Stadt genau einmal besucht (Traveling Salesman Problem, TSP).
◮ Eine solche Kantenfolge heißt hamiltonscher Zyklus.
◮ Dieser Graph ist vollständig.
Erfurt Celle
Frankfurt Braunschweig
Darmstadt Augsburg
8 3 2
6 1
3 2 2 3
9
9
9 9
8 4
Die billigste Rundreise kostet 13 Einheiten.
9.4 Backtracking-Algorithmen 9-45
Beispiel: Problem des Handlungsreisenden
◮ Ein naiver Algorithmus beginnt bei einem Startknoten und sucht dann alle Wege ab.
◮ Die Komplexität dieses Verfahrens liegt in O
(
n!)
.◮ Das folgende Verfahren führt zu einer Näherungslösung:
1. Die Kanten werden nach ihren Kosten sortiert.
2. Man wählt die billigste Kante unter den beiden folgenden Nebenbedingungen:
◮ Es darf kein Zyklus entstehen (außer am Ende der Rundreise).
◮ Kein Knoten darf zu mehr als 2 Kanten adjazent sein.
9.4 Backtracking-Algorithmen 9-46
Beispiel: Problem des Handlungsreisenden
◮ Die Laufzeit dieses gierigen Algorithmus liegt in O
(
n2 log n2)
.◮ Das Verfahren führt nicht immer zu einer optimalen Lösung.
Trotzdem wird es in der Praxis erfolgreich eingesetzt.
◮ Branch-and-Bound: Wenn man weiß, dass eine Lösung mit Kosten k existiert (zum Beispiel durch obigen Algorithmus), dann kann ein Backtrack-Algorithmus alle Teillösungen
abschneiden, die bereits teurer als k sind.
9.4 Backtracking-Algorithmen 9-47
Dynamische Programmierung
Rekursive Problemstruktur:
1. Aufteilung in abhängige Teilprobleme.
2. Berechnen und Zwischenspeichern wiederbenötigter Teillösungen.
3. Bestimmung des Gesamtergebnisses unter Verwendung der Teillösungen.
Die dynamische Programmierung ist mit der
Teile-und-Beherrsche-Methode verwandt. Die Teilprobleme sind aber abhängig. Einmal berechnete Teillösungen werden
wiederverwendet.
Die dynamische Programmierung wird häufig bei Optimierungsproblemen angewendet.
9.5 Dynamische Programmierung 9-48
Beispiel: Fibonacci-Zahlen
fib
(
n) =
0 n
=
01 n
=
1fib
(
n − 1) +
fib(
n − 2)
n ≥ 2 0,1, 1,2, 3, 5,8, 13, 21,34, . . .func fib(n: int): int begin if n < 2 then
return n;
fi;
return fib(n-1) + fib(n-2);
end
9.5 Dynamische Programmierung 9-49
Beispiel: Fibonacci-Zahlen
Berechnung von
fib(5)
:fib 2 fib 1 fib 0 fib 4
fib 3
fib 1 fib 2
fib 1 fib 0
fib 3
fib 1 fib 2
fib 1 fib 0 fib 5
1
1 1 1
1 0
0 0
9.5 Dynamische Programmierung 9-50
Beispiel: Fibonacci-Zahlen
Die Berechnung von
fib(5)
führt zweimal auf die Berechnung vonfib(3)
. Die zugehörigen Teilbäume werden zweimal ausgewertet.Aufwandsabschätzung:
T
(
n) =
Anzahl der Funktionsaufrufe=
1 n
=
0,n=
11
+
T(
n − 1) +
T(
n − 2)
n ≥ 2T
(
n)
wächst exponentiell. Wir haben in der Übung gezeigt:T
(
n) =
1+
1 5√5
!
1
+
√ 5 2
n
+
1 − 15 √ 5!
1 − √ 5 2
n
− 1
≈ 1,45 · 1,62n.
9.5 Dynamische Programmierung 9-51
Beispiel: Fibonacci-Zahlen
Iterative dynamische Lösung (vgl. Abschnitt 2.1):
func fibDyn(n: int): int begin
var i, result, minus1, minus2: int;
if n < 2 then return n; fi;
minus2
←0;
minus1
←1;
for i
←2 to n do
result
←minus1 + minus2;
minus2
←minus1;
minus1
←result;
od;
return result;
end
9.5 Dynamische Programmierung 9-52
Beispiel: Das Rucksackproblem
Das Rucksackproblem (knapsack problem):
Ein Wanderer findet einen Schatz aus Edelsteinen.
◮ Jeder Edelstein hat ein bestimmtes Gewicht und einen bestimmten Wert.
◮ Er hat nur einen Rucksack, dessen Kapazität durch ein maximales Gewicht begrenzt ist.
Gesucht ist ein Algorithmus, der diejenige Befüllung des
Rucksacks ermittelt, die einen maximalen Wert hat, ohne die Gewichtsbeschränkung zu verletzen.
9.5 Dynamische Programmierung 9-53
Beispiel: Das Rucksackproblem
Gegeben:
◮ Kapazität c ∈ N,
◮ Menge O mit n ∈ N Objekten o1, . . . , on,
◮ Gewichtsfunktion g
:
O → N mit Pj∈O g
(
j)
> c,◮ Bewertungsfunktion w
:
O → N. Gesucht ist eine Menge O′ ⊆ O mitX
j∈O′
g
(
j)
≤ c und Xj∈O′
w
(
j)
maximal.Da Gegenstände nur vollständig oder gar nicht eingepackt werden, spricht man auch vom 0-1-Rucksackproblem. Beim fraktionalen Rucksackproblem können auch Teile eines Gegenstands
ausgewählt werden.
9.5 Dynamische Programmierung 9-54
Beispiel: Das Rucksackproblem
Der gierige Algorithmus führt nicht zur Lösung:
O
=
{o1,o2, o3}, Kapazität c=
5.Gewichte: g
(
o1) =
1,g(
o2) =
2, g(
o3) =
3.Werte: w
(
o1) =
6, w(
o2) =
10,w(
o3) =
12.Ein gieriger Algorithmus wählt das Objekt mit dem größten relativen Wert.
r
(
o) =
w(
o)
g
(
o)
,r(
o1) =
6,r(
o2) =
5,r(
o3) =
4. O′=
{o1,o2}, Xj∈O′
g
(
j) =
3 ≤ 5=
c, Xj∈O′
w
(
j) =
16. Die optimale Lösung istO′′
=
{o2,o3}, Xj∈O′′
g
(
j) =
5=
c, Xj∈O′′
w
(
j) =
22.9.5 Dynamische Programmierung 9-55
Beispiel: Das Rucksackproblem
Backtracking liefert die korrekte Lösung, ist aber ineffizient.
O
=
{o1,o2, o3,o4},c=
10.Gewichte: g
(
o1) =
2,g(
o2) =
2,g(
o3) =
6,g(
o4) =
5.Werte: w
(
o1) =
6,w(
o2) =
3, w(
o3) =
5,w(
o4) =
4.10
10 8
10 8 8 6
10 4 8 2 8 2 6 0
10 5 4 8 3 2 8 3 2 6 1 0
nein ja
zur Disposition o : (g, w)
o1 : (2,6) o2 : (2,3) o3 : (6,5) o4 : (5,4)
(0/0),(5/4),(6/5),(2/3),(7/7),(8/8),(2/6),(7/10),(8/11),(4/9),(9/13),(10/14), jeweils (Gewicht/Wert).
Es ist O′
=
{o1, o2,o3} mit g(
O′) =
10 und w(
O′) =
14.9.5 Dynamische Programmierung 9-56
Beispiel: Das Rucksackproblem
Rückgabewert ist der Wert der optimalen Füllung.
Aufruf: btKnapsack(1,c).
func btKnapsack(i, rest: int): int begin if i = n then
if g(i) > rest then return 0;
else return w(i); fi;
else if g(i) > rest then
return btKnapsack(i+1,rest);
else
return max(btKnapsack(i+1,rest),
btKnapsack(i+1,rest-g(i))+w(i));
fi; fi;
end
Das Optimierungspotential durch Vermeidung wiederkehrender Berechnungen wird nicht genutzt.
9.5 Dynamische Programmierung 9-57
Beispiel: Das Rucksackproblem
Dynamische Programmierung:
O
=
{o1, . . . ,o5}, c=
10.Gewichte: g
(
o1) =
2,g(
o2) =
2,g(
o3) =
6,g(
o4) =
5,g(
o5) =
4.Werte: w
(
o1) =
6,w(
o2) =
3, w(
o3) =
5,w(
o4) =
4, w(
o5) =
6. Es wird ein zweidimensionalen Feld f[
i, r]
berechnet:r→
i↓ 0 1 2 3 4 5 6 7 8 9 10
5 0 0 0 0 6 6 6 6 6 6 6
4 0 0 0 0 6 6 6 6 6 10 10
3 0 0 0 0 6 6 6 6 6 10 11
2 0 0 3 3 6 6 9 9 9 10 11
f
[
4, 9] =
10: Wenn o4 und o5 bei der Restkapazität 9 zurDisposition stehen, beträgt der Wert der zusätzlichen Füllung 10.
9.5 Dynamische Programmierung 9-58
Beispiel: Das Rucksackproblem
Die zentrale Anweisung in btKnapsack:
return max(btKnapsack(i+1,rest),
btKnapsack(i+1,rest-g(i))+w(i));
r→
i↓ 0 1 2 3 4 5 6 7 8 9 10
5 0 0 0 0 6 6 6 6 6 6 6
4 0 0 0 0 6 6 6 6 6 10 10
3 0 0 0 0 6 6 6 6 6 10 11
2 0 0 3 3 6 6 9 9 9 10 11
f
[
3, 8] =
max(
f[
4,8]
, f[
4, 2] +
5) =
max(
6,0+
5) =
69.5 Dynamische Programmierung 9-59
Beispiel: Das Rucksackproblem
Der Algorithmus arbeitet folgendermaßen:
1. Berechne zunächst die Werte f
[
n, 0]
,. . . ,f[
n,c]
.2. Berechne anschließend f
[
i, 0]
,. . . ,f[
i, c]
für i=
n − 1 bis i=
2 unter Rückgriff auf die bereits berechneten Werte der Zeile i+
1.3. Das Gesamtergebnis f
[
1,c]
ergibt sich dann ausf
[
1, 10] =
max(
f[
2,10]
,f[
2, 8] +
6) =
max(
11, 9+
6) =
15.9.5 Dynamische Programmierung 9-60
Beispiel: Das Rucksackproblem
func dynKnapsack(n, c: int): int begin var f[2..n, 0..c]: int;
var i, r: int;
for r ← 0 to c do
if g(n) > r then f[n,r] ← 0;
else f[n,r] ← w(n); fi;
for i ← n - 1 downto 2 do for r ← 0 to c do
if g(i) > r then
f[i,r] ← f[i+1,r];
else f[i, r] ←
max(f[i+1,r],f[i+1,r-g(i)]+w(i)); fi;
od; od;
if g(1) > c then return f[2, c];
else return max(f[2,c],f[2,c-g(1)]+w(1));
end
9.5 Dynamische Programmierung 9-61
Beispiel: Suche in einem Text
Im Folgenden werden Zeichenketten als Felder behandelt.
Gegeben:
◮ Feld t
[
1..n]
von Zeichen, der Text,◮ Feld p
[
1..m]
von Zeichen, das Muster (pattern).Es sei m ≤ n. In der Regel ist sogar m << n.
Gesucht: Vorkommen von p in t, d. h. Indices s, 0 ≤ s ≤ n − m, mit t
[
s+
1..s+
m] =
p[
1..m]
.9.5 Dynamische Programmierung 9-62
Beispiel: Suche in einem Text
Naive Lösung: Vergleiche für alle s
=
0..n − m und für alle i=
0..m die Zeichen p[
i] =
t[
s+
i]
.proc naiv(t, p): begin var i,s: int;
for s
←0 to n - m do i
←1;
while
i ≤ m&& p[i] = t[s+i] do i
←i+1; od;
if i = m+1 then print(s); fi;
od;
end
9.5 Dynamische Programmierung 9-63
Beispiel: Suche in einem Text
◮ Als Maß für die Laufzeit nehmen wir die Anzahl der ausgeführten Tests der inneren Schleife.
◮ Der schlimmste Fall tritt ein, wenn für jeden Wert s die Zeichenkette p bis zum letzten Zeichen mit t verglichen werden muss. Beispiel: t
=
"aaaaaaaaaaaaaaaaab", p=
"aaab".◮ Die Laufzeit liegt in O
((
n − m)
m) =
O(
nm)
.◮ Gesucht ist ein effizienterer Algorithmus.
9.5 Dynamische Programmierung 9-64
Beispiel: Suche in einem Text
s
=
4,q=
5:t: bacbababaabcbab p: ababaca
s′
=
s+
2:t: bacbababaabcbab p: ababaca
Die nächste möglicherweise erfolgreiche Verschiebung ist s′
=
s+ (
q − d[
q])
.9.5 Dynamische Programmierung 9-65
Beispiel: Suche in einem Text
Es seien die Musterzeichen p
[
1..q]
gegeben, die mit denTextzeichen t
[
s+
1..s+
q]
übereinstimmen. Wie groß ist die geringste Verschiebung s′ > s für diep
[
1..k] =
t[
s′+
1..s′+
k]
mit s′+
k=
s+
q gilt?9.5 Dynamische Programmierung 9-66
Beispiel: Suche in einem Text
Die Präfixfunktion für das Muster
ababababca
:i 1 2 3 4 5 6 7 8 9 10
p[i] a b a b a b a b c a
d[i] 0 0 1 2 3 4 5 6 0 1
9.5 Dynamische Programmierung 9-67
Beispiel: Suche in einem Text
proc Berechnung der Präfixfunktion d begin d(1)
←0;
k
←0;
for q
←2 to m do
while k > 0
∧p[k+1]
,p[q]; do k
←d[k]; od;
if p[k+1] = p[q]
then k
←k+1; fi;
d[q]
←k;
od;
return d;
end
9.5 Dynamische Programmierung 9-68
Beispiel: Suche in einem Text
Der Knuth-Morris-Pratt-Algorithmus:
proc kmp(t, p): begin berechne d;
q
←0;
for i
←1 to n do
while q > 0
∧p[q+1]
,t[i] do q
←d[q]; od;
if p[q+1] = t[i]
then q
←q+1; fi;
if q = m
then print(i-m);
q
←d[q]; fi;
od;
end
Die Laufzeit dieses Algorithmus liegt in O
(
n+
m)
.9.5 Dynamische Programmierung 9-69
Weitere Algorithmenmuster
◮ Zufallsgesteuerte Algorithmen
◮ Verteilte und parallele Algorithmen
◮ Lokale Suche
◮ Amortisierte Analyse
◮ Approximative Algorithmen
◮ Genetische Algorithmen
◮ Schwarmbasierte Algorithmen und Koloniealgorithmen
◮ . . .
9.5 Dynamische Programmierung 9-70
Weitere Gebiete
◮ Mathematische Algorithmen
◮ Geometrische Algorithmen
◮ Algorithmen für Texte
◮ Lineare Programmierung
◮ . . .
Große Bedeutung für die Theorie der Algorithmen besitzt die
Komplexitätstheorie. Hierzu zählen zum Beispiel die Untersuchung von Komplexitätsklassen wie P und NP und die so genannte
NP-Vollständigkeit. Die Komplexitätstheorie wird in der Vorlesung
„Theoretische Informatik II“ behandelt.
9.5 Dynamische Programmierung 9-71