• Keine Ergebnisse gefunden

Entwurf von Algorithmen

N/A
N/A
Protected

Academic year: 2022

Aktie "Entwurf von Algorithmen"

Copied!
73
0
0

Wird geladen.... (Jetzt Volltext ansehen)

Volltext

(1)

Algorithmen und Datenstrukturen

Werner Struckmann

Wintersemester 2005/06

(2)

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

(3)

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

(4)

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

(5)

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

(6)

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

(7)

Inkrementelle Vorgehensweise

Beispiel: Sortieren durch Einfügen benutzt eine inkrementelle Herangehensweise. Nachdem das Teilfeld a

[

1..j1

]

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

(8)

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

(9)

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

(10)

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

(11)

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

(12)

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

(13)

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

(14)

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 von

t1

nach

t2

und verwendet

t3

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

) =

2n1.

9.2 Teile-und-Beherrsche-Algorithmen 9-12

(15)

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, oder

T

(

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

(16)

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

(17)

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

+

d

gilt.

Beispiel: n

=

4, x

=

3141

=

31 · 102

+

41

9.2 Teile-und-Beherrsche-Algorithmen 9-15

(18)

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

)

acbd

)

· 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

(19)

Beispiel: Die Multiplikation nach Karatsuba

Beispiel: x

=

3141,y

=

5927

x · y

=

3141 · 5927

=

31 · 59 · 104

+

((

31

+

41

)(

59

+

27

)

31 · 5941 · 27

)

· 102

+

41 · 27

=

31 · 59 · 104

+

(

72 · 8631 · 5941 · 27

)

· 102

+

41 · 27

=

1829 · 104

+ (

6192 − 18291107

)

· 102

+

1107

=

1829 · 104

+

3256 · 102

+

1107

=

18616707

9.2 Teile-und-Beherrsche-Algorithmen 9-17

(20)

Beispiel: Die Multiplikation nach Karatsuba

Für die Komplexität T

(

n

)

des Verfahrens gilt:

T

(

n

) =





k, n

=

1

3 · 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

(21)

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

(22)

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

(23)

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

(24)

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

(25)

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

=

Pn

i=1 ci ist konstant.

Die mittlere Verweilzeit ist T

=

1

n

(

c1

+ (

c1

+

c2

) +

· · ·

+ (

c1

+

· · ·

+

cn

))

=

1

n

(

nc1

+ (

n − 1

)

c2

+ (

n − 2

)

c3

+

· · ·

+

2cn1

+

cn

)

=

1 n

n

X

k=1

(

n − k

+

1

)

ck

9.3 Gierige Algorithmen 9-23

(26)

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

(27)

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

(28)

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

(29)

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

(30)

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

) =

X

xA

w

(

A

)

erweitern.

Eine Menge A ∈ I mit maximalem Gewicht heißt optimal.

9.3 Gierige Algorithmen 9-28

(31)

Matroide

Satz: Es sei M

= (

S,I

)

ein gewichtetes Matroid mit der

Gewichtsfunktion w

:

S → R+. Der folgende gierige Algorithmus gibt eine optimale Teilmenge zurück.

func greedy(M, w):

I

begin A

← ∅

;

sortiere S in monoton fallender Reihenfolge nach dem Gewicht w;

foreach x

S do if A

{x}

∈ I

then A

A

{x};

return A;

end

9.3 Gierige Algorithmen 9-29

(32)

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

(33)

Matroide

Beispiel: Es sei G

= (

V,E

)

ein ungerichteter Graph. Dann ist MG

= (

SG, IG

)

ein Matroid, dabei gilt:

SG

=

E,

A ⊆ E: A ∈ IGA 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

(34)

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

(35)

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

(36)

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

(37)

Backtracking-Algorithmen

Es gibt eine endliche Menge K von Konfigurationen.

K ist hierarchisch strukturiert:

Es gibt eine Ausgangskonfiguration k0K .

Zu jeder Konfiguration kxK 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

(38)

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

(39)

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

(40)

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

(41)

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

(42)

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

(43)

Beispiel: Das N-Damen-Problem

K sei die Menge aller Stellungen mit einer Dame in jeder der ersten m Zeilen, 0 ≤ mn, 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

(44)

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

(45)

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

(46)

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 ist

n

!

nn · en

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

(47)

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, jn.

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

(48)

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

(49)

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

(50)

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

(51)

Beispiel: Fibonacci-Zahlen

fib

(

n

) =













0 n

=

0

1 n

=

1

fib

(

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

(52)

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

(53)

Beispiel: Fibonacci-Zahlen

Die Berechnung von

fib(5)

führt zweimal auf die Berechnung von

fib(3)

. Die zugehörigen Teilbäume werden zweimal ausgewertet.

Aufwandsabschätzung:

T

(

n

) =

Anzahl der Funktionsaufrufe

=





1 n

=

0,n

=

1

1

+

T

(

n − 1

) +

T

(

n − 2

)

n ≥ 2

T

(

n

)

wächst exponentiell. Wir haben in der Übung gezeigt:

T

(

n

) =

1

+

1 5

√5

! 





1

+

√ 5 2





n

+

115 √ 5

! 





1 − √ 5 2





n

1

1,45 · 1,62n.

9.5 Dynamische Programmierung 9-51

(54)

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

(55)

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

(56)

Beispiel: Das Rucksackproblem

Gegeben:

Kapazität c ∈ N,

Menge O mit n ∈ N Objekten o1, . . . , on,

Gewichtsfunktion g

:

O → N mit P

jO g

(

j

)

> c,

Bewertungsfunktion w

:

O → N. Gesucht ist eine Menge OO mit

X

jO

g

(

j

)

c und X

jO

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

(57)

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}, X

jO

g

(

j

) =

3 ≤ 5

=

c, X

jO

w

(

j

) =

16. Die optimale Lösung ist

O′′

=

{o2,o3}, X

jO′′

g

(

j

) =

5

=

c, X

jO′′

w

(

j

) =

22.

9.5 Dynamische Programmierung 9-55

(58)

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

(59)

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

(60)

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 zur

Disposition stehen, beträgt der Wert der zusätzlichen Füllung 10.

9.5 Dynamische Programmierung 9-58

(61)

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

) =

6

9.5 Dynamische Programmierung 9-59

(62)

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 aus

f

[

1, 10

] =

max

(

f

[

2,10

]

,f

[

2, 8

] +

6

) =

max

(

11, 9

+

6

) =

15.

9.5 Dynamische Programmierung 9-60

(63)

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

(64)

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 ≤ snm, mit t

[

s

+

1..s

+

m

] =

p

[

1..m

]

.

9.5 Dynamische Programmierung 9-62

(65)

Beispiel: Suche in einem Text

Naive Lösung: Vergleiche für alle s

=

0..nm 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

(66)

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

(67)

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

(68)

Beispiel: Suche in einem Text

Es seien die Musterzeichen p

[

1..q

]

gegeben, die mit den

Textzeichen t

[

s

+

1..s

+

q

]

übereinstimmen. Wie groß ist die geringste Verschiebung s > s für die

p

[

1..k

] =

t

[

s

+

1..s

+

k

]

mit s

+

k

=

s

+

q gilt?

9.5 Dynamische Programmierung 9-66

(69)

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

(70)

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

(71)

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

(72)

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

(73)

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

Referenzen

ÄHNLICHE DOKUMENTE

(3) Ein (roter) Knoten, der über alle seine Kanten einen Explorer oder ein Echo erhalten hat, wird grün und sendet ein (grünes) Echo über seine &#34;erste&#34; Kante. Auf

I Bei Deklaration muss Typ des Pointers angegeben werden, da *ptr eine Variable sein soll. • int* ptr; deklariert ptr als Pointer auf int I Wie üblich gleichzeitige

• Am besten wäre es, wenn wir erst auswerten, dann auf das Ergebnis zugreifen .... Helmut Seidl, TU München ;-).. dem Wert für stop ). → Es werden automatisch alle

Auch über die Pädagogische Hochschule Thurgau PHTG, die Partnerhochschule der Universität Konstanz mit gemeinsamen Studiengängen, kommt Konstanzer Studierenden die persönliche

Schreibe eine funktionale Spezifikation für das Problem. Zerlege das Problem in Unterprobleme und schreibe für diese

Zwei Personen A und B spielen auf dem vollständigen Graphen K n ein Spiel: In jeder Runde markiert erst Spieler A eine noch nicht gefärbte Kante rot; danach markiert Spieler B eine

Wir müssen noch zeigen, dass beide Rekursionen dieselben

◮ Lokale Variablen: Innerhalb eines Blocks können Variablen deklariert werden, die nur in diesem Block verfügbar