Anwendungen von Dynamischer Programmierung

Werbung
Dynamische Programmierung
Simon Philippi - 53577
Khaled Ahmed - 53558
HTW Aalen
HTW Aalen
Jasmin Ratajczyk - 57135
HTW Aalen
25. Januar 2017
1
Inhaltsverzeichnis
1 Einleitung
3
2 Definition
2.1 Top-Down . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2 Bottom-Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5
6
6
3 Schneiden von Eisenstangen
3.1 Rekursive Implementierung . . . . . . . . . .
3.2 Top-Down Implementierung . . . . . . . . .
3.3 Bottom-Up Implementierung . . . . . . . . .
3.4 Vergleich der Laufzeiten von Top-Down mit
. . . . . . . .
. . . . . . . .
. . . . . . . .
Bottom-Up
.
.
.
.
.
.
.
.
.
.
.
.
7
8
10
11
12
4 Dynamische Programmierung am Beispiel von Traveling Salesperson
13
5 Elemente der dynamischen Programmierung
5.1 Die optimale- Teilstruktur- Eigenschaft . . . . . . .
5.2 Überlappende Teilprobleme . . . . . . . . . . . . . . .
5.3 Unabhängigkeit der Teilprobleme . . . . . . . . . . .
5.4 Erstellen der optimalen Lösung aus den Elementen
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
15
15
16
17
19
6 Die längste gemeinsame Teilsequenz LCS
20
7 Teilproblem-Graphen
24
8 Anwendungen von Dynamischer Programmierung
26
8.1 Optimale binäre Suchbäume . . . . . . . . . . . . . . . . . . . . 26
8.2 Matrix-Kettenmultiplikation . . . . . . . . . . . . . . . . . . . . 29
8.3 Biologie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2
1
Einleitung
Gesucht wird ein effizienter Algorithmus für die Ermittlung der FibonacciFolge. Die Fibonacci-Folge wird folgendermaßen definiert:
F0 = 0, F1 = 1, Fn = Fn−1 + Fn−2
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144...
n = 0 und n = 1 stellen die Trivial-werte dar und haben einen festen Wert,
während alle darauffolgenden Zahlen mit Fn = Fn−1 + Fn−2 ermittelt werden.
Zuerst wird ein simpler Algorithmus geschrieben:
Algorithm 1 fib
1: procedure fib(n)
2:
if n == 0 then return 0
3:
if n == 1 then return 1
return fib(n-1) + fib(n-2)
[?]
Abbildung 1: Aufrufe der Fibonacci-Funktion am Beispiel von F6 . [?]
Das Problem wird rekursiv gelöst. Die Methode ist aufgrund der wiederholten Errechnung von bereits gelösten Problemen sehr langsam. Die
Laufzeit beträgt O(2n ).
Der effektivere Ansatz wäre, wenn unser Algorithmus die Teilprobleme sortiert und das Problem iterativ löst. Wir berechnen also das letzte Teilproblem aus und mit dessen Ergebnis werden die darauffolgenden Teilprobleme
3
gelöst, bis man zur Lösung des gewünschten Problems kommt.
[?]
Algorithm 2 fib-bottom-up
1: procedure fib-bottom-up(n)
2:
sei table[0..n+2] eine neue Tabelle
3:
table[0] = 0
4:
table[1] = 1
5:
for i = 2 to n do
6:
textittable[i] = table[i − 1] + table[i − 2]
return table[n]
Zunächst wird eine Tabelle initialisiert und die Trivial-Werte eingespeichert.
Diese sind F0 = 0 und F1 = 1. Mithilfe einer for-Schleife werden die restlichen
Ergebnisse ergänzt. Aus einer Rekursion wird eine iterative Lösung. Um ein
Teilproblem zu lösen, schlagen wir die bekannten Teilprobleme nach und
verwenden diese, um das neue Teilproblem zu lösen. Die Laufzeit beträgt
hierbei nur O(n). Man opfert Speicher für Zeit, da hierfür ein Feld nötig ist.
4
2
Definition
Die dynamische Programmierung stellt eine Programmiertechnik dar, die
durch Aufteilung in disjunkte Teilprobleme, ein Problem rekursiv löst. Im
Gegensatz zu herkömmlichen Teile-und-herrsche-Algorithmen werden sich
bereits gelöste Teilprobleme gemerkt. Daher müssen diese nicht erneut gelöst
werden. Dadurch ist eine gewisse Effizienz gegeben. Je mehr Überschneidungen beim Bilden von Teilproblemen entstehen, desto effizienter wird der
Algorithmus.
Der Begriff wurde erstmals 1940 von Richard Bellman eingeführt. Dynamische Programmierung wurde im Gebiet der Regelungstheorie angewandt.
1957 wurde das Optimalitätsprinzip von Richard Bellman beschrieben. Dieses besagt, dass jede Teillösung einer optimalen Lösung selbst eine optimale
Lösung darstellt.
Das volle Potential von dynamischer Programmierung entfaltet sich beim
Anwenden auf ein Optimierungsproblem. Optimierungsprobleme haben mehrere mögliche optimale Lösungen. Aus diesem Grund wird ein ermittelter
Lösungsweg als ein optimaler Lösungsweg bezeichnet. Ermittelt wird immer ein Maximum oder Minimum.
Um einen Algorithmus, der auf dynamischer Programmierung basiert, zu
entwickeln, wird nach den folgenden 4 schritten gehandelt:
1. Charakterisierung der Struktur
2. Definition des Wertes einer optimalen Lösung rekursiv
3. Berechnen des Wertes einer optimalen Lösung
4. Konstruktion der optimalen Lösung aus berechneten Informationen
Zunächst wird die Struktur näher beschrieben und die wichtigsten Merkmale gebündelt. Anschließend entwickelt man einen gewöhnlichen rekursiven
Algorithmus. Anschließend werden die Zwischenergebnisse vermerkt, sodass
man bei Bedarf immer darauf zugreifen kann.
Es gibt 2 verschiedene Varianten, um eine dynamische Programmierung zu
implementieren:
ˆ Top-Down-Methode
ˆ Bottom-Up-Methode
Die Top-Down-Methode setzt auf Rekursion und Speichern in einer Tabelle.
Die Bottom-Up-Methode setzt auf Iteration und das Speichern der Zwischenergebnisse in einer Tabelle. Diese Methode arbeitet von unten nach
5
oben und verwendet lediglich die ermittelten Zwischenergebnisse, um die
nächsten Probleme zu lösen. Beide Ansätze verfügen über die gleiche asymptotische Laufzeit.
2.1
Top-Down
Bei der Top-Down-Variante wird weiterhin eine Rekursion verwendet. Zusätzlich wird die Memoisation verwendet und stellt den einzigen Unterschied zur
herkömmlichen Rekursion dar. Unter bestimmten Umständen muss der TopDown-Algorithmus nicht alle Teilprobleme betrachen. Jedoch wird oft eine
Hilfsfunktion benötigt, um beispielsweise ein Feld zu initialisieren deklarieren.
2.2
Bottom-Up
Bei diesem Ansatz werden die Teilprobleme sortiert und die kleineren Teilprobleme zuerst gelöst. Die größeren Teilprobleme hängen von diesen ab. Jedes Teilproblem wird genau einmal gelöst. Die Teilprobleme eines größeren
Teilproblems wurden zum Zeitpunkt der Betrachtung gelöst und vermerkt.
Der Bottom-Up-Algorithmus hat im Vergleich zum Top-Down-Algorithmus
bessere konstante Faktoren, da der Overhead, der durch Prozeduraufrufe
anfällt, fehlt.
6
3
Schneiden von Eisenstangen
Gegeben ist eine Eisenstange mit variabler Länge i = 1,2..., die so geschnitten werden soll, dass sie zu einem möglichst hohen Preis verkauft werden
kann. Die Preise zu den Längen sind vorgegeben. Die einzelnen Teilstäbe
haben eine Länge von 1 bis i − 1. Wenn die Länge i bereits die optimale
Lösung darstellt, muss der Stab nicht geteilt werden.
Als Beispiel wird der Fall n = 4 betrachtet. Abbildung 2 zeigt alle 8 Möglichkeiten, wie die Stange geteilt werden kann. Eine optimale Lösung wäre
p2 + p2 = 10.
Es gibt 2(n−1) Möglichkeiten, um eine Stange mit der Länge n zu zerlegen.
rn ist der maximale Erlös wenn f eine Stange mit der Länge n aufgeteilt
wird. Er kann für n ≥ 1 folgendermaßen berechnet werden:
rn = max(pn , r1 + rn−1 , r2 + rn−2 , ..., rn−1 + r1 )
pn : stellt den Preis für einen Stab der Länge n dar.
rn−i : stellt einen rekursiven Aufruf dar.
rn lässt sich mit folgender Formel einfacher darstellen:
rn = max(pi + rn−i )
1≤i≤n
Die optimalen Teilstäbe werden in einer additiven Notation angegeben. Ein
Beispiel wäre 7 = 2 + 2 + 3. Ein Stab der Länge 7 wird hier in 3 Teilstäbe mit
der Länge 2, 2, 3 geteilt. Wenn eine optimale Lösung den gegebenen Stab
in k Teilstäbe für 1 ≤ k ≤ n schneidet, dann sieht eine optimale Lösung wie
folgt aus:
n = i1 + i2 + ... + ik
Abbildung 2: Das Schaubild zeigt alle Möglichkeiten auf, um die Stange zu
teilen. [?]
7
Demnach wird der Erlös der Teilstäbe folgendermaßen angegeben:
rn = pi1 + pi2 + ... + pik
In unserem Beispiel wäre das also r7 = 5 + 5 + 8. Die optimale Lösung beträgt
demnach also 18, da r7 = p2 + p2 + p3 .
In den folgenden Abschnitten werden mehrere Möglichkeiten zur Implementierung aufgezeigt. Zunächst wird die herkömmliche Implementierung gezeigt und anschließend beide der Top-Down-Algorithmus so wie die BottomUp-Variante. vergleiche [?]
Länge i
Preis i
1 2 3 4 5 6 7 8 9 10
1 5 8 9 10 17 17 20 24 30
Tabelle 1: Preisliste
3.1
Rekursive Implementierung
Algorithm 3 CUT-ROD
1: procedure CUT-ROD(p,n)
2:
if n == 0 then return 0
3:
q = -∞
4:
for i = 1 to n do
5:
q = max(q, p[i] + CUT-ROD(p,n-i))
return q
[?]
p[1..n] ist ein Feld, das die Preise für die Stangen der Länge 1 bis n enthält.
Dieses wird zusammen mit einer Ganzzahl n übergeben, die die Länge des
auszurechnenden Stabes ist.
n = 0 ist ein Trivialwert und somit wird standardmäßig eine 0 zurückgegeben
(Zeile 1-2). Dies ist auch unsere Abbruchbedingung in der Rekursion.
q ist der maximale Erlös. Dieser wird zuerst auf −∞ gesetzt (Zeile 3) und
anschließend durch mehrfache rekursive Aufrufe in einer Schleife von 1 bis
n der Funktion neu berechnet(Zeile 4-5).
Mithilfe einer Funktion zur Berechnung des Maximums, wird festgestellt,
ob der aktuelle Wert von q größer ist als der Erlös von p[i] + der Erlös von
(n-i).
In Zeile 6 wird das Ergebnis zurückgegeben.
Die Laufzeit dieser Methode steigt rasant an und verdoppelt sich jedes mal,
wenn n um 1 inkrementiert wird. Dies passiert, da gleiche Teilprobleme
8
Abbildung 3: Rekursionsbaum am Beispiel von n = 4. [?, S.368]
mehrmals neu berechnet werden. Man kann anhand von Abbildung 3 entnehmen, wie sich dies auf die Laufzeit am Beispiel von n = 4 auswirkt.
9
Bei der Analyse der Laufzeit wird das Ergebnis mit T(n) angegeben.
T(n) zählt die Prozeduraufrufe der Methode CUT-ROD(p,n). Diese Zahl
entspricht der Anzahl der Teilbäume des Rekursionsbaumes einer Methode CUT-ROD(p,n). Dessen Wurzel wird mit n markiert. Die Wurzel wird
mitgezählt. Also ist demnach T(0) = 1.
n−1
T (n) = 1 + ∑ T (j)
j=0
Die 1 im Ausdruck stellt die Wurzel dar. T(j) ist die Anzahl der Aufrufe
mit ihren rekursiven Aufrufen der Funktion CUT-ROD(p, n-1) mit j = n-1.
Daraus folgt:
T (n) = 2n
Es handelt sich hierbei um eine exponentielle Laufzeit in n.
Das lässt sich damit begründen, dass es 2n−1 Möglichkeiten gibt, um die
Stange zu zerlegen und CUT-ROD jede einzelne Möglichkeit explizit überprüft.
3.2
Top-Down Implementierung
Algorithm 4 MEMOIZED-CUT-ROD
1: procedure MEMOIZED-CUT-ROD(p,n)
2:
sei r[0..n] ein neues Feld
3:
for i = 0 to n do
4:
r[i] = -∞
return MEMOIZED-CUT-ROD-AUX(p,n,r)
[?]
Algorithm 5 MEMOIZED-CUT-ROD-AUX
1: procedure MEMOIZED-CUT-ROD-AUX(p,n,r)
2:
if r[n] ≥ 0 then return r[n]
3:
if n == 0 then
4:
q=0
5:
else
6:
q = -∞
7:
for i = 1 to n do
8:
q = max(q, p[i] + MEMOIZED-CUT-ROD-AUX(p,n-i,r))
9:
r[n]=q return q
10
[?] Die Lösung besteht aus 2 Methoden. Die erste initialisiert ein Feld
r[0..n]. Alle Werte werden auf −∞ gesetzt. Es handelt sich hierbei um die
Erlöse. Anschließend wird die 2. Methode aufgerufen und dessen Rückgabe
ausgegeben
Die 2. Methode erhält zusätzlich zu der länge n und den Preisen p das erstellte Feld r. r speichert die maximalen Erlöse aller einzelnen Teilprobleme. Dadurch kann man ein bekanntes Teilproblem nachschlagen und gegebenenfalls
zurückgeben. Es handelt sich bei dieser Funktion um eine rekursive Funktion. Die Abbruchbedingung ist gegeben, wenn r[n] ≥ 0. Demnach wurde
also der Wert mit dem optimalen Wert überschrieben. Dieser wird zurückgegeben. Die 2. Abbruchbedingung ist gegeben, wenn n den Wert 0 hat. In
diesem Fall wird 0 in das Feld r[n] geschrieben und 0 zurückgegeben. Ist die
Bedingung jedoch falsch, wird q auf −∞ gesetzt. Mithilfe einer Schleife wird
das Maximum von q dann berechnet. die Methode ruft sich rekursiv auf und
bestimmt dann immer die noch nicht berechneten optimalen Teilprobleme
des Hauptproblems.
3.3
Bottom-Up Implementierung
Algorithm 6 BOTTOM-UP-CUT-ROD
1: procedure BOTTOM-UP-CUT-ROD(p,n)
2:
sei r[0..n] ein neues Feld
3:
r[0] = 0
4:
for j = 1 to n do
5:
q = -∞
6:
for i=1 to j do
7:
q = max(q,p[i] + r[j-1])
8:
r[j] = q
[?]
Die Bottom-Up-Methode ist simpler. Sie Besteht nur aus einer Funktion und
spart sich dadurch einen Overhead. Sie erhählt ebenfalls das Feld p, das die
Preise enthält sowie die Länge n. In Zeile 1 wird ein Feld r[0..n] initialisiert.
In Zeile 2 wird r[0] auf 0 gesetzt.
Ab Zeile 3 wird eine verschachtelte for-Schleife verwendet. Der Startwert
beginnt bei 1, da r[0] bereits ein Wert zugewiesen wurde. Zu Beginn jedes
Durchlaufs der äußeren Schleife wird q auf −∞ gesetzt. Die innere Schleife
läuft von 1 bis j. Die Teilprobleme werden von unten nach oben bearbeitet.
Dadurch wird eine Rekursion vermieden, da man mithilfe der bekannten
Teilprobleme das Hauptproblem lösen kann. Anschließend wird q im Feld r
gespeichert und r[n] zurückgegeben.
11
3.4
Vergleich der Laufzeiten von Top-Down mit Bottom-Up
Beide Algorithmen haben eine asymptotische Laufzeit von Θ(n2 ).
Die Bottom-Up-Funktion hat eine doppelt verschachtelte Schleife. Die Iterationen der inneren Schleife stellen eine arithmetische Reihe dar.
Bei der Top-Down-Methode wird jedes Teilproblem genau einmal gelöst. Jedoch werden alle Fälle betrachtet und bei einem rekursiven Aufruf geprüft,
ob r[n] bereits berechnet wurde. Ein Problem der Größe n wird gelöst, indem
die Teilprobleme der Größe 0,1...n-1 zuerst gelöst werden. Demnach werden
die Zeilen 6 und 7 n mal iteriert. Auch hier entsteht eine arithmetische Reihe.
12
4
Dynamische Programmierung am Beispiel von
Traveling Salesperson
Man kann mithilfe der dynamischen Programmierung das TSP effizient
lösen.
Eingegeben wird ein vollständiger, gerichteter Graph mit Kantengewichten
ai,j ≥ 0. Es ist also eine Matrix ((ai,j )1<=i,j<=n .)
Gesucht is eine günstigste ”Rundreise”, bei der jede Stadt (also jeder Knoten) genau einmal besucht wird und zum Startpunkt. In diesem Beispiel ist
der Startknoten 1.
Formal: Gesucht ist eine Permutation von π {1,....,n}, so dass
c(π) = ∑ aπ(i−1),π(i) + aπ(n),π(1)
2≤i≤n
minimal unter allen möglichen Permutationen
Der Anfangspunkt wird fest gewählt. In unserem Fall startet die Tour bei
Knoten 1. Der naive Ansatz wäre das Durchprobieren aller Möglichkeiten
((n-1)!). Wie sehen sinnvolle Teilprobleme aus?
Zunächst wird Folgendes betrachtet P(S,`) mit S ⊆ {1,...,n}, 1 ≤ ` ≤ n,
wobei 1, ` ∈ S.
P(S,`) fragt nach dem kürzesten, einfachen Weg von 1 durch alle Knoten
in S mit Ende `.
Basisfälle: P({1}, 1) = 0,P(S,1) = ∞ für ∣S∣ > 1.
Länge der kürzesten Rundtour:
min(P (1, ..., n, l) + al, 1∣ < l < n).
Für ∣S∣ > 1 gilt:
P (S, j) = min(P (S − {j}, i) + ai,j ∣i ∈ S − {j}).
Das iterative Programm sieht folgendermaßen aus:
P({1},1) ← 0
for s ← 2 to n do
foreach S ⊆ {1,2,...,n} with ∣S∣ = s and 1 ∈ S do
P(s,1) ← ∞
13
foreach j ∈ S,j ≠ 1 do
P(S,j) ← min(P(S-{j},i)+ ai,j ∣i ∈ S − {j})
return min(P({1,...,}, `)+a`,1 ∣1 ≤ ` ≤ n)
Die Laufzeit beträgt O(2n ⋅n2 ). Der naive Ansatz hingegen hat eine Laufzeit
von O(n!).
Beweis
ˆ Die Anzahl aller Teilmengen {1,...,n} beträgt 2n
ˆ Zu jeder solcher Teilmenge gibt es maximal n Teilprobleme.
ˆ Jedes Teilproblem kann in Zeit O(n) gelöst werden.
[?, S.10]
Abbildung 4: Beispielgraph. [?]
14
5
Elemente der dynamischen Programmierung
Die dynamische Programmierung verwendet die optimale- Teilstruktur- Eigenschaft mit Bottom- up Ansatz. Das bedeutet, dass als Erstes die optimale
Lösung für die Teilproleme berechnet wird und danach die daraus resultierende Lösung. Damit die dynamische Programmierung für die Lösung eines
Problems angewendet werden kann, muss der Weg, wie das Problem gelöst
wird bestimmte Eigenschaften besitzen.
Im Folgenden werden nun die Eigenschaften erklärt, die ein Problem aufweisen muss, damit es für eine Lösung durch dynamische Programmierung
in Frage kommt.
5.1
Die optimale- Teilstruktur- Eigenschaft
“Ein Problem besitzt die optimale- Teilstruktur- Eigenschaft, wenn eine optimale Lösung selbst optimale Lösungen für Teilprobleme hat.“[?, S.382] Das
bedeutet, dass die Lösungen der Teilprobleme eines Problemes eine optimale
Lösung haben müssen, damit die Lösung des Problems optimal sein kann.
Als Beispiel soll noch einmal die Matrizenmultiplikation betrachtet werden.
Die Teilprobleme bestanden daraus, für einen bestimmten Teil der Matrizen
die optimale Klammerung zu finden. Nun werden für die Lösungen Klammerungen verwendet, die nicht optimal sind. Das Resultat wäre, dass für die
Multiplikation der Matrizen im schlechtesten Fall viel mehr Zeit als benötigt
gebraucht wird. Das kommt daher, dass die optimale Klammerungen eine
Klammerungen bestimmt, die dafür sorgen soll, dass so wenig Multiplikationen wie möglich gemacht werden müssen. Aus der Verwendung nichtoptimaler Teilprobleme würde also keine optimale Lösung entstehen.
Es gibt ein allgemeines Schema, mit welchem herausgefunden werden kann,
ob ein Problem die optimale Teilstruktur- Eigenschaft besitzt.
1. Zuerst muss bewiesen werden, dass das Problem durch Teilprobleme
gelöst werden muss. Beim Lösen eines Problems muss also eine Entscheidung getroffen werden. Bei der Matrizenmultiplikation muss beispielsweise entschieden werden, an welcher Stelle die Matrizenketten
aufgebrochen werden müssen. Dadurch enstehen Teilprobleme, die zuerst einzeln gelöst werden müssen.
2. Als Zweites muss eine Annahme getroffen werden, dass die Entscheidung bekannt ist, wobei noch nicht wichtig ist, wie diese getroffen
wurde.
3. Basierend auf dieser Entscheidung muss nun der Raum der Teilprobleme bestimmt werden. Der Teilproblemraum beschreibt, wie viele
Teilprobleme benötigt werden, um die Lösung des Problems zu berechnen. Zusätzlich muss die Anzahl der Möglichkeiten mit einberechnet
15
werden, wie ein Problem ausgerechnet werden kann. Aus dem Teilproblemraum lässt sich die Laufzeit errechnen. Diese besteht aus dem
Produkt der genannten Faktoren. Beispielsweise hat ein Problem n
Teilprobleme und maximal n Möglichkeiten diese auszuwählen, wäre
die Laufzeit von θ(n ⋅ n) = O(n2 ).
4. Nun muss bewiesen werden, dass eine optimale Lösung eines Problems
aus optimalen Lösungen für Teilprobleme bestehen muss. (Dazu wird
ein Widerspruchsbeweis durchgeführt. Es wird angenommen, dass keine optimalen Lösungen der Teilprobleme für eine optimale Lösung
gebraucht werden. Das diese Aussage falsch ist, kann man mit einem
Austauschargument beweisen. Bei einer optimalen Lösung mit optimal
gelösten Teilproblemen, wird die Lösung eines Teilproblems durch eine nicht optimale Lösung ausgestauscht. An der Lösung erkennt man
nun, dass sie durch den Austausch schlechter als vorher geworden ist.
Man nennt so einen Widerspruchsbeweis auch cut- and- paste, da
eine Teillösung durch eine andere Teilösung ausgetauscht wird.
Ein Beispiel wird im Kapitel zur optimalen gemeinsamen Teilsequenz gezeigt.
5.2
Überlappende Teilprobleme
Teilprobleme überlappen, wenn sie in mehreren Teilproblemen als Problem
wieder auftauchen. Überlappen sie also nicht würde jedes Problem nur einmal vorkommen. Dadurch können die Teilprobleme nicht mehr rekursiv
gelöst werden. In diesem Fall findet die dynamische Programmierung keine Anwendung, da die Probleme nicht mehrmals aufgerufen werden und
deshalb keine Laufzeiteinsparung entsteht, indem Teilergebnisse gespeichert
werden.
16
Abbildung 5: Aufrufbaum der Funktion zur Berechnung der optimalen Klammerung bei der Matrizenmultiplikation Die orangenen
Felder sind diejenigen Teilprobleme, die mehrmals aufgerufen werden.[?, S.5]
5.3
Unabhängigkeit der Teilprobleme
Teilprobleme sind unabhängig, wenn “die Lösung eines Teilproblems nicht
die Lösung eines anderen Teilproblems desselben Problems“ beeinflusst [?,
S.3]. Dürften beispielsweise Elemente, die in einem Teilprobleme verwendet
werden, in einem anderen Teilproblem nicht verwendet werden, dann sind
diese Teilprobleme voneinander abhängig. Der Vorteil den die dynamische
Programmierung hat, liegt in der Optimierung von rekursiven Aufrufen.
Sind Teilprobleme nicht unabhängig voneinander, könnte eine Rekursion
nicht zum richtigen Ergebniss führen.
Anmerkung:
Dass die Teilprobleme unabhängig sein müssen widerspricht sich nicht damit,
das die Teilprobleme überlappend sein müssen. Dies sind zwei voneinander
unabhängige Eigenschaften. Während es bei überlappenden Teilproblemen
darum geht, dass Probleme mehrmals in Teilproblemen vorkommen (hier
geht es um die Anwendungsmöglichkeit der Rekursion), geht es bei überlappenden Teilproblemen um die Ressourcen, die in den Problemen verwendet
werden.
Ein Beispiel:
Sei ein Graph G=(V,E) mit den Knoten u,v ∈ V gegeben. Es werden die
Probleme betrachtet, welche bei der Suche eines ungewichteten kürzesten Pfades und bei der Suche eines ungewichteten längsten einfachen
Pfades auftreten. Bei einem ungewichteten kürzesten Pfad soll der Pfad
u→v mit einer minimalen Anzahl an besuchten Knoten gegeben sein. Dabei
dürfen keine Zyklen übersprungen werden. Bei einem ungewichteten längsten einfachen soll der Pfad u→v mit einer maximalen Anzahl an besuchten
Knoten gegeben sein. Dabei dürfen keine Zyklen wiederholt werden um be-
17
liebig viele Knoten zu besuchen. Knoten u muss ≠ v sein, damit das Problem
nichttrivial ist. Gibt es einen Knoten w zwischen u und v, wobei w auch u
p
p1
p2
oder v sein kann, kann der Pfad u↝v in die Teilpfade u↝w und w↝v zerlegt
werden. Dabei sind die Anzahl der Kanten p=p1 +p2 . Wenn p der kürzeste
Pfad zwischen u und v ist, dann ist p1 der kürzeste Pfad von u nach w und
p2 der kürzeste Pfad von v nach w.
Die Suche nach dem ungewichteten längsten einfachen Pfad von q nach t
besitzt jedoch nicht wie die Suche nach dem ungewichteten kürzesten Pfades
p
eine optimale-Teilstruktur-Eigenschaft. Soll der Pfad q↝t in zwei Teilpfade
zerlegt werden, also in q→r und r→t ergibt sich ein Problem. Der längste Pfad
p lautet q→r→t. Wird jedoch der Teilpfad p1 betrachtet, lautet der längste
Pfad von q→r: q→s→t→r. Der Pfad ist jedoch ungültig, wird er mit p2 von
r→t (r→q→s→t) zusammengesetzt, da die Einfachheit nicht mehr gegeben
ist. Dieses Problem ist NP-vollständig und es gibt keine effiziente Lösung
durch die dynamische Programmierung. Dies liegt daran, dass die Teilprobleme beim Finden des ungewichteten längsten Pfad nicht unabhängig sind:
Die Teilprobleme beeinfließen sich gegeseitig. Man sagt, dass die Ressourcen die ein Teilproblem verwendet dem anderen Teilproblem nicht mehr zur
Verfügung stehen. Die Ressourcen sind in diesem Beispiel die Knoten, welche aufgrund der Einfachheit des Pfades nur einmal benutzt werden dürfen.
Abbildung 6: Ein Graph mit vier Knoten. [?, S.385]
18
5.4
Erstellen der optimalen Lösung aus den Elementen
Da die Lösungen der Teilprobleme während der Laufzeit in eine Tabelle
eingetragen werden, kann daraus am Ende eine optimale Lösung erstellt
werden. Hierbei ist es wichtig, die richtigen Daten abzuspeichern, um die
Lösung rekonstruieren zu können. Im Beispiel der Eisenstangen wurde nach
den optimalen Kosten gefragt. Würden nun in jedem Schritt, in welchem die
optimalen Teillösungen berechnet wurden, die optimalen Kosten in die Tabelle eingetragen, wären die optimalen Gesamtkosten schnell zu berechnen.
Wie die Stangen zerlegt wurden, muss dann durch diese Werte erst rekonstruiert werden. Eine bessere Möglichkeit ist es, die Stelle zu speichern, in
welcher das optimale Ergebnis berechnet wurde. Das heißt, dass der Index
des Entscheidungspunkt gespeichert wird. Dadurch kann jede Entscheidung
durch einen Aufruf rekonstruiert werden. Die Laufzeit wäre dann pro Entscheidung O(1).
19
6
Die längste gemeinsame Teilsequenz LCS
Dieser Begriff beschreibt eine Sequenz, welche nur diese Elemente enthält,
die zwei Sequenzen S1 und S2 gemeinsam in dieser Reihenfolge haben. Um so
länger diese Sequenz ist, desto mehr Elemente haben S1 und S2 gemeinsam.
Mit der längsten gemeinsamen Teilsequenz wird also die Ähnlichkeit von
Elementen bestimmt. Dies wird beispielsweise beim Vergleich zweier DNAStränge angewendet. Diese bestehen aus den vier Basen Adenin, Guanin,
Cytosin und Tymin, welche in einem DNA- Strang in einer bestimmten
Sequenz vorkommen. Durch den Vergleich zweier DNA- Stränge von zwei
unterschiedlichen Organismen, kann man bestimmen, wie ähnlich sich diese sind. Beispielsweise stimmt die DNA des Menschen zu ca. 99% mit der
des Affen überein. Die DNA-Sränge der Organismen haben also eine lange
gemeinsame Teilsequenz. Auch im Vergleich von Textdateien, beispielsweise
bei zwei Versionen von Programmcode wird diese Methode angewendet.
Formalisierung:
Eine Sequenz Z=(z1 , z2 , ..., zk ) ist eine Teilsequenz von X=(x1 , x2 , ..., xm ),
“wenn es eine streng steigende Sequenz (i1 , i2 , ..., ik ) von Indizes von X gibt,
sodass für alle j=1, 2, ..., k die Gleichung xij = zj gilt.“[?, S.394 Z.9ff]
Beispiel: X= (A, B, C, A, B, C) und Z= (B, A, B, C). Mit der Indexsequenz (2,4,5,6) ist Z eine Teilsequenz von X.
Eine Sequenz Z ist eine gemeinsame Teilsequenz von X und Y, wenn
Z eine Teilsequenz von Beiden ist.
Beispiel: X=(A, B, C, B, D, A, B) und Y=(B, D, C, A, B, A). Z= (A,
C, B) ist dann eine gemeinsame Teilsequenz von X und Y. Diese muss von
der längsten gemeinsamen Teilsequenz abgegrenzt werden. Die Teilsequenz Z hat die Länge 3. Die Sequenzen (B, C, B, A) und (B, D, A, B) sind
auch gemeinsame Teilsequenzen von X und Y und haben die Länge 4. Beide
bilden eine längste gemeinsame Teilsequenz von X und Y.
Die längste gemeinsame Teilsequenz im Englischen LCS (longest path sequence) soll nun mithilfe der dynamischen Programmierung effizient berechnet werden. Dazu wird wieder nach den vier Schritten zur Entwicklung
eines auf dynamischer Programmierung basierenden Algorithmus gehandelt.
Schritt 1: Charakterisierung der Struktur einer LCS
Naiv würden die Teilsequenzen von X aufgezählt werden und mit Teilsequenzen von Y verglichen werden (Laufzeit ist O(n)). “Jede Teilsequenz von X
20
entspricht einer Untermenge der Indizes 1,2,...,m von X.“ [?, S.394 Z.31 ff].
X besitzt also 2m Teilsequenzen, weshalb durch diese Methode exponentielle
Zeit benötigt werden würde. Für lange Sequenzen wäre die Methode deshalb
ungeeignet. Die Laufzeit beträgt dann insgesamt O(2n ∗ m).
Damit für das LCS- Problem durch dynamische Programmierung eine optimale Lösung bestimmt werden kann, muss das Problem die optimale TeilstrukturEigenschaft besitzen. Mit folgendem Theorem soll bewiesen werden, dass das
LCS- Problem diese Eigenschaft besitzt.
Theorem für die optimale Teilstruktur einer LCS
Seien X= (x1 , x2 , ..., xm ) und Y = (y1 , y2 , ..., yn ) zwei Sequenzen und sei Z=
(z1 , z2 , ..., zk ) eine LCS von X und Y.
1. Ist xm = yn , so gilt zk = xm = yn und Zk−1 ist eine LCS von Xm−1 undYn−1 .
2. Ist xm ≠ yn , so folgt aus zk ≠ xm , dass Z eine LCS von Xm−1 und Y
ist.
3. Ist xm ≠ yn , so folgt aus zk ≠ yn , dass Z eine LCS von X und Yn−1 ist.
(vergleiche [?, S.394 Theorem 15.1])
Beweis. Zu (1): Angenommen, zk =xm , dann würde xm in Z fehlen und
deshalb an Z angehängt werden. Damit entsteht eine neue Teilsequenz für
X und Y mit Länge k+1 und nicht mehr k. Die ist ein Widerspruch zu der
Annahme, dass Z eine LCS ist. Dies beweist den ersten Teil der Aussage.
Die zweite Aussage war, dass Zk−1 eine LCS von Xm−1 und Yn−1 ist und
die Länge k-1 hat. Angenommen, eine Teilsequenz W von Xm−1 und Yn−1
habe eine Länge größer als k-1. Dann könnte man xm =yn an W anhängen,
wodurch die Länge der Teilsequenz größer als k werden wäre. Widerspruch.
Zu (2): Gäbe es eine gemeinsame Teilsequenz W von Xm−1 und Y, deren
Länge größer als k sei. Dann ”wäre W auch eine gemeinsame Teilsequenz von
Xm und Y’”[?, S.395 Beweis (2) Z.3]. Dies widerspricht der Voraussetzung,
’”dass Z eine LCS von X und Y ist.’”[?, S.395 Beweis (2) Z.5]
Zu (3): Wird analog zu (2) bewiesen.
Damit besitzt LCS also die optimale -Teilstruktur -Eigenschaft, da eine LCS
die LCS ihrer Teilsequenzen enthält.
Im Falle, dass (1) zutrifft, muss ein Teilproblem gelöst werden. Nämlich
die LCS von Xm−1 und von Yn−1 . Im Falle, dass (2) oder (3) zutreffen,
müssen zwei Teilprobleme gelöst werden. Nämlich eine LCS von X und Yn−1
und von Xm−1 und Y. Die größere LCS wird dann ausgewählt.
Erkennbar ist außerdem, dass die Teilprobleme unabhängig voneinander
21
sind, da die Teilsequenzen Elemente verwenden dürfen, ohne, dass sie bei
Anderen dann fehlen würden. Eine weitere wichtige Eigenschaft, welche LCS
besitzen muss, ist die der überlappenden Teilprobleme. Eine LCS von X und
Y findet man über die LCS von X und Yn−1 und die LCS von Xm−1 und Y.
Diese haben dann jeweils das Teilproblem Xm−1 und Yn−1 . Sie haben also
gemeinsame Teilprobleme, wodurch die Teilprobleme überlappend sind.
Schritt 2: Rekursive Bestimmung des Wertes einer optimalen
Lösung
Bei der rekursiven Lösung muss nun eine Rekursionsgleichung bestimmt werden, mit welcher die optimale Lösung berechnet werden kann. Es wird also
“c[i,j] als die Länge einer LCS der Sequenzen Xi und Yj .“[?, S.396 Z.1ff].
Haben X und Y beide Länge 0, dann hat auch die LCS die Länge 0. Folgende
Gleichung ergibt sich:
⎧
0
wenn i=0 oder j=0,
⎪
⎪
⎪
⎪
c[i,j]= ⎨c[i − 1, j − 1] + 1
wenn i, j > 0 und xi = yi , [?, S.396
⎪
⎪
⎪
⎪
⎩max(c[i, j − 1], c[i − 1, j]) wenn i, j > 0 und xi ≠ yi
(15.9)]
Hier wird erkennbar, dass auch unterschiedlichen Teilprobleme ausgewählt
werden können, je nach dem wie die Probleme beschaffen sind. Dieses Charakteristikum findet auch bei einigen anderen dynamischen Programmen
Anwendung.
Schritt 3: Berechnung des Wertes einer optimalen Lösung
Der Wert der optimalen Lösung ist in diesem Fall die Länge der LCS, da
die ”langste gemeinsame Teilsequenz gefunden werden soll.
Aufgrund der relativ geringen Teilproblemanzahl von Θ(mn) wird die Lösung
bottom- up mit dynamischer Programmierung gelöst.
Es wird der Algorithmus LCS-Length verwendet. Die Sequenzen X=(x1 , x2 , ..., xn )
und Y=(y1 , y2 , ..., yn ) werden eingegeben. In zwei Tabellen werden Werte eingetragen. Die Werte c[i,j] werden in die Tabelle c[0..m,0..n] eingetragen. In
diese wird die optimale Lösung des Teilproblems eingetragen. In eine zweite
Tabelle b[i,j] wird eingetragen, wo die Einträge für die optimale Lösung in
der Tabelle stehen. Zurückgegeben werden die Tabellen b und c. Die Länge
der LCS der Eingabesequenzen steht in c[m,n].
[?, S.397] Die Bedeutung der Pfeile wird in Schritt 4 deutlich. Die Pfeile werden in b eingetragen um anzuzeigen, welches der Teilprobleme ausgewählt werden muss. Der Pfeil “ ↖ “ bedeutet beispielsweise, dass xi =
yi .
22
Algorithm 7 LCS LENGTH
1: procedure LCS-LENGTH(X,Y)
2:
m = X.length
3:
n = Y.length
4:
for i=1 to m do
5:
c[i,0]= 0
6:
for j=0 to n do
7:
c[0,j]=0
8:
for i=1 to m do
9:
for j=1 to n do
10:
if xi == yi then
11:
c[i,j]=c[i-1,j-1]+1
12:
b[i,j]=“↖“
13:
ElseIf c[i − 1, j] ≥ c[i, j − 1]
14:
c[i,j]=c[i-1,j]
15:
b[i,j]=“↑“
16:
else
17:
c[i,j]=c[i,j-1]
18:
b[i,j]=“←“
return c und b
Schritt 4: Konstruieren einer optimalen Lösung
Um nun die LCS zu bestimmen wird bei b[m,n] begonnen und je nach Pfeil
wird zu der jeweiligen Stelle in der Tabelle gesprungen. Da die LCS durch
LCS-LENGTH in der umgekehrten Reihenfolge eingetragen wurde, muss
ein weiterer Algorithmus verwendet werden, welcher die LCS in der richtigen Reihenfolge ausgibt.
[?, S.396]
Algorithm 8 PRINT- LCS(b,X, X.length, Y.length)
1: if i==0 or j==0 then return
2: if b[i,j] == “ ↖ ’” then
3:
PRINT-LCS(b,X,i-1,j)
4:
print xi
5:
ElseIf b[i,j]==“ ↑ “
6:
PRINT-LCS(b,X,i-1,j)
7: elsePRINT-LCS(b,X,i,j-1)
23
Abbildung 7: Tabelle zu c [?, S.398 Abbildung 15.8]
In der Tabelle wird rechts unten bei c[m,n] begonnen und dann wird den
Pfeilen gefolgt. Für die eigegebene Sequenz, wird der Pfad über die grauen
Kästen gebildet. Der Pfad “ ↖ “ steht dafür, dass xi =yj . Die Elemente, die
am Index dieser Kästen stehen, werden ausgewählt. Dadurch ergibt sich die
Sequenz (B, C, B, A).
Die Laufzeit beträgt =(m + n), da ”bei jedem rekursiven Aufruf wenigstens
eine der beiden Variablen i und j dekrementiert’”.[?, S.398 Z.11ff]
7
Teilproblem-Graphen
Wichtige Fragen bei der dynamischen Programmierung sind: in wie viele
Teilprobleme wurde das Problem zerlegt und in welcher Abhängikeit stehen
diese? Diese Informationen werden in einem Teilproblem-Graphen dargestellt. In dem Graphen steht jeder Knoten für ein Teilproblem und jede
gerichtete Kante, von Knoten x zu Knoten y, für eine Abhängigkeit.
Die Methode ”bottom-up” arbeitet genau diesen Teilproblem-Graphen ab,
indem sie das Teilproblem y löst. Dieser ist später zum lösen von Teilproblem
x notwendig. Somit arbeitet die Methode alle Teilprobleme in der Reihen-
24
folge der ”umgekehrter Topologischer Sortierung” ab. Es werden die
Teilprobleme, welche zum lösen anderer Teilprobleme erforderlich sind, zuerst gelöst.
”Top-down” macht im Gegensatz zur ”bottom-up” eine Art Tiefensuche und
arbeitet die Probleme ”von oben nach unten ab”.
Zusätzlich kann durch den Teilproblem-Graphen die Laufzeit des Problems
ermittelt werden, da jedes Teilproblem nur einmal gelöst werden muss und
sich somit eine lineare Laufzeit aus der Summe alle Teilprobleme ergibt.
Diese ist meistens proportional zum Grad der Teilprobleme.
Abbildung 8: Ein Teilproblemgraph mit fünf Teilproblemen. [?, S.371]
25
8
Anwendungen von Dynamischer Programmierung
Es gibt eine Vielzahl von Anwendungsbereichen der dynamischen Programmierung. Aufzuzählen wären dabei beispielsweise:
ˆ Optimale binäre Suchbäume
ˆ Matrix-Kettenmultiplikation
ˆ Die Biologie
8.1
Optimale binäre Suchbäume
Ein Beispiel für die Nutzung eines optimalen binären Suchbaums ist die
Übersetzung eines Textes in eine andere Sprache. Dabei werden die übersetzten Wörter in einem binären Baum gespeichert, in dem die am häufigst
genutzten Wörter weiter oben stehen und die seltener benutzen Wörter weiter unten in der Hierarchie des Baumes. Dabei ist der Aufbau dieses Baumes
wichtig für die Laufzeit des Programmes. Ist der Baum optimal strukturiert, wird von einem optimalen binären Suchbaum gesprochen. Die Knoten
innerhalb des Baumes funktionieren als Schlüssel, also in dem Fall der Übersetzung als passende Wörter. Die Blätter des Baumes sind die sogenannten
Dummyschlüssel, welche erreicht werden, wenn keine passende Übersetzung
gefunden wurde. Die Wahrscheinlichkeit wird pro Schlüssel ermittelt und gespeichert, daher lassen sich die Kosten für jeden Knoten berechnen. Das Ziel
ist es die Gesamtkosten des Baumes minimal zu halten, damit ein optimaler
binärer Suchbaum gegeben ist. Die Gesamtkosten eines Baumes hängen dabei nicht direkt von der Gesamthöhe eines Baumes ab oder ob der Schlüssel
mit der höchsten Wahrscheinlichkeit die Wurzel des Baumes ist. Ein Teilbaum mit dem Bereich ki ,...,kj , 1≤i≤j≤n muss die Knoten ki ,...,kj sowie die
Dummy-Schlüssel di−1 ,...,dj , mit den Wahrscheinlichkeiten qi−1 ,...,qj , besitzen. Der Teilbaum muss zudem auch eigenständig die Eigenschaften eines
optimalen binären Suchbaums besitzen. Zu jedem Schlüssel ki wird die zugehörige Wahrscheinlichkeit pi und für jeden Dummy-Schlüssel di die zugehörige Wahrscheinlichkeit qi gespeichert.
Die optimale Lösung kann rekursiv ermittelt werden. Nun wird der Teilproblembaum mit den Schlüsseln ki ,...,kj (i≤1, j≤n und j≤i-1) betrachtet. Sollte
j=i-1 sein, besitzt der Teilbaum nur den Dummy-Schlüssel di−1 . Die Funktion e[i,j] ermittelt die Kosten, welche der optimale binäre Suchbaum mit den
Schlüsseln ki ,...,kj hat. Unser Ziel ist es schlussendlich e[1,n] zu finden. Ist
der Teilbaum mit den Schlüsseln ki ,...,kj und der Wurzel kr gegeben so ist der
linke Teilbaum ein optimaler Suchbaum mit den Schlüsseln ki ,...,kr−1 und der
rechte Teilbaum ein optimaler Suchbaum mit den Schlüsseln kr+1 ,...,kj . Ist
26
der gegebene Teilbaum erneut ein Teilbaum eines weiteren Knoten, erhöht
sich die Tiefe jedes Knotens um 1. Nach der Gleichung
n
n
E[Suchkosten in T ] = 1 + ∑ tief eT (ki ) ⋅ pi + ∑ tief eT (di ) ⋅ qi
i=1
i=0
[?, S. 401]
erhöhen sich die erwarteten Kosten um die Summe aller Wahrscheinlichkeiten in dem Teilbaum.
Die Summe der Wahrscheinlichkeiten eines Teilbaums lässt sich als
j
j
w(i, j) = ∑ pl + ∑ ql
l=i
l=i−1
[?, S. 403]
bezeichnen.
Es gilt also für einen Teilbaum mit der Wurzel kr und den Schlüsseln ki ,...,kj
e[i, j] = pr + (e[i, r − 1] + w(i, r − 1)) + (e[r + 1, j] + w(r + 1, j)).
[?, S. 403]
Da jedoch
w(i, j) = w(i, r − 1)) + pr + w(r + 1, j)
[?, S. 403]
gilt, kann e[i,j] in die Form
e[i, j] = pr + e[i, r − 1] + e[r + 1, j] + w(i, j)
[?, S. 403]
gebracht werden.
Wenn vorausgesetzt wird, dass der Knoten kr bekannt ist und er als Wurzel
mit den niedrigsten erwarteten Suchkosten genommen wird ergibt sich diese
rekursive Formel:
⎧
qi−1
falls j = i - 1
⎪
⎪
⎪
⎪ min {e[i, r − 1] + e[r + 1, j] + w(i, j)}
falls i ≤ j
e[i, j] = ⎨
⎪
´¹¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹¸ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¶
⎪
⎪
⎪
i≤r≤j
⎩
⎫
⎪
⎪
⎪
⎪
⎬
⎪
⎪
⎪
⎪
⎭
[?, S. 404]
Für die Funktion werden nun die Werte von ei,j] in einer Tabelle e mit den
Dimensionen [1...n+1,0...n] speichern. Die erste Dimension ist 1...n+1, da
für den Dummyschlüssel dn e[n+1,n] aufgerufen werden muss. Die zweite
Dimension ist 0...n, da für den Dummyschlußsel d0 e[1,0] aufgerufen werden
muss. Eine weitere Tabelle wurzel[i,j] um die Wurzeln des Teilbaumes ki ...kj
zu speichern. Um die Performance des Programmes zu erhöhen wird eine
dritte Tabelle w[1...n+1,0...n] benutzt, damit der Wurzelknoten nicht bei
27
jeder Berechnung von e[i,j] erneut ermitteln muss. Als Basisfall ist w[i,i-1]
= qi−1 definiert für 1 ≤ i ≤ n+1.
Jedes j≥i wird mit
w[i, j] = w[i, j − 1] + pj + qj
[?, S. 404]
berechnet.
28
Pseudecode:
Algorithm 9 OPTIMAL-BST
1: procedure OPTIMAL-BST(p,q,n)
2:
seien e[1..n+1,0..n],w[1..n+1,0..n] und wurzel[1..n,1..n] neue Tabellen
3:
for i=1 to n+1 do
4:
e[i,i-1]=qi−1
5:
w[i,i-1]=qi−1
6:
for l=1 to n do
7:
for i=1 to n-l+1 do
8:
j=i+l-1
9:
e[i,j] = ∞
10:
w[i,j]=w[i,j-1] + pj + qj
11:
for r=i to j do
12:
t = e[i,r-1] + r[r+1,j] + w[i,j]
13:
if t¡e[i,j] then
14:
e[i,j]=t
15:
wurzel[i,j]=r
return e und wurzel.
[?, S. 405]
8.2
Matrix-Kettenmultiplikation
Bei der Matrix-Kettenmultiplikation wird eine Reihe von Matrizen miteinander multipliziert. Zur Berechnung dieser Multiplikation wird ein Standartalgorithmus als Unterroutine benutzt. Um diese Multiplikation durchzuführen,
müssen alle Matrizen vollständig geklammert werden um alle Mehrdeutigkeiten zu entfernen. Auch wenn die Matrixmultiplikation assoziativ ist, dass
heißt das alle Klammerungen zum selben Ergebnis führen, kann die Klammerung erhebliche Folgen für die Kosten der Multiplikation haben.
Algorithm 10 MATRIX-MULTIPLY
1: procedure MATRIX-MULTIPLY(A,B)
2:
if A.spalten ≠ B.zeilen then
3:
error ”inkompatible Dimensionen”
4:
else sei C eine neue A.zeile × B.spalten-Matrix
5:
for i=1 to A.zeilen do
6:
for j=1 to B.spalten do
7:
cij =0
8:
for j=1 to B.spalten do
9:
cij = cij + aik ⋅ bkj
return e und wurzel.
29
[?, S. 374]
Damit zwei Matrizen miteinander Multipliziert werden können, müssen
die Spalten der Matrix mit den Zeilen der Matrix B übereinstimmen.
p×q⋅q×r =p×r
Dabei beträgt die Anzahl der skalaren Multiplikationen p⋅q⋅r.
Es wird als Beispiel die Kette ⟨A1 , A2 , A3 ⟩ mit den Dimensionen 10×100, 100×
5, 5×50 betrachtet. Bei der Klammerung ((A1 A2 )A3 ) ergibt sich eine Anzahl
von 10⋅100⋅5 = 5000 skalaren Multiplikationen für A1 ⋅A2 , plus 10⋅5⋅50 = 2500
skalare Multiplikation um das Ergbnis mit A3 zu multiplizieren. Wird nun
die Klammerung (A1 (A2 A3 )) gewählt, werden 100 ⋅ 5 ⋅ 50 = 25000 skalaren
Multiplikationen für A2 ⋅ A3 und 10 ⋅ 100 ⋅ 50 = 50000 skalare Multiplikation
um das Ergbnis mit A1 zu multiplizieren benötigt. Dadurch werden 10 mal
so viele skalare Multiplikation aufgrund der geänderten Klammerung angewendet.
Das Problem wird folgendermaßen definiert: Eine Kette ⟨A1 , ..., An ⟩ von n
Matrizen mit den Dimensionen für i=1,2,...,n pi−1 × pi soll so geklammert
werden, dass die Anzahl der skalaren Multiplikationen zur Berechnung minimal sind. Die Matrizen werden dabei jedoch nicht wirklich multipliziert,
es wird lediglich die optimale Klammerung zurückgegeben. Die Anzahl der
möglichen Klammerungen von n Matrizen wird als P(n) definiert. Für n=1
ergibt sich lediglich eine mögliche Klammerung. Ist n≥2 kann ein Schnitt in
zwei Teilprodukte an der Stelle k ∈ {1,2,...,n-1} liegen.
P (n) = {
1
falls n = 1
}
n−1
P
(k)P
(n
−
k)
falls n ≥ 2
∑k=1
[?, S. 375]
Die Matrix-Kette kann in Teile Ai ...Aj (i≤j) zerlegt werden. Um eine nicht
trivial Matrixkette Ai ...Aj (i¡j) zu klammern, müssen die Kette an einer
Stelle k zwischen Ak und Ak+1 gespalten werden (i≤k≤j). Danach können
die Kosten für die Matrixketten Ai ...Ak , Ak+1 ...Aj und die der anschließende Multiplikation dieser beiden Matrizen berechnet werden. Somit kann das
Problem in Teilprobleme zerlegt werden.
Das Ziel ist eine Klammerung von Ai ...Aj (1≤i≤j≤n) mit minimalen Kosten.
m[i,j] ist eine Funktion eine Klammerung von Ai ...Aj zu finden, dann ist
das Gesamtproblem eine Multiplikation mit minimalen Kosten von A1 ...An
m[1,n] zu finden. Ist i=j, dann ist die Berechnung trivial, da deine Klammerung notwendig ist (m[i,j]=0). Sobald i¡j ist, kann auf die Zerlegung der Teilprobleme zurückgegriffen. Dabei wird Ai ...Aj in die zwei Teilketten Ai ...Ak
und Ak+1 ...Aj aufgeteilt. Nun werden die Anzahl der Multiplikationen der
30
optimalen Klammerung für die beiden Teilketten gesucht und mit den Anzahl der Multiplikation der beiden Teilketten-Matrizen addiert.
m[i, j] = m[i, k] + m[k + 1, j] + pi−1 pk pj
[?, S. 377]
Diese Gleichung benötigt den Wert K, welcher jedoch unbekannt ist. Daher
müssen alle Werte die für k in Frage kommen (i,i+1,...,j) in Betracht gezogen
werden.
⎧
0
falls i = j
⎪
⎪
⎪
⎪ min {m[i, k] + m[k + 1, j] + p p p } falls i ≤ j
i−1
j
k
m[i, j] = ⎨
⎪
´¹¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¸ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¹ ¶
⎪
⎪
⎪
i<j
⎩
⎫
⎪
⎪
⎪
⎪
⎬
⎪
⎪
⎪
⎪
⎭
[?, S. 377]
s[i,j] soll nun den gesuchten Wert k finden und die optimale Spaltung der
Matrixkette Ai ...Aj angeben damit = m[i,k] + m[k+1,j] + pi−1 pk pj gilt. Es
wird nun ein bottom-up Ansatz in eine MATRIX-CHAIN-ORDER implementiert. Die Hilfstabellen m[1...n,1...n] werden zum Speichern der Kosten
m[i,j] und s[1...n,1...n] zum Speichern des optimalen Index k zum Teilen der
Matrixketten verwendet.
Algorithm 11 MATRIX-CHAIN-ORDER
1: procedure MATRIX-CHAIN-ORDER(p)
2:
n=p.länge -1string
3:
seien m[1..n, 1..n] und s[1..n-1,2..n] neue Tabellen
4:
for i=1 to n do
5:
m[i,i]=0
6:
for l=2 to n do
7:
for i=1 to n-l+1 do
8:
j = i - l +1
9:
m[i,j] = ∞
10:
for k=i to j-1 do
11:
q = m[i,k] + m[k+1,j] + pi−1 pk pj
12:
if q < m[i,j] then
13:
m[i,j]=q
14:
s[i,j]=k
return m und s.
[?, S. 379]
Nach Durchlauf der MATRIX-CHAIN-ORDER kannmit Hilfe der Hilfstabelle s eine optimale Lösung konstruiert werden. Die optimale Lösung lässt
sich mit folgendem Pseudocode anzeigen.
[?, S. 380]
31
Algorithm 12 PRINT-OPTIMAL-PARENTS
1: procedure PRINT-OPTIMAL-PARENTS(s,i,j)
2:
if i==j then
3:
print ”A”i
4:
s[i,j]=k
5:
else print ”(”
6:
PRINT-OPTIMAL-PARENTS(s,i,j,s[i,j])
7:
PRINT-OPTIMAL-PARENTS(s,s[i,j]+1,j)
8:
print ”)”
8.3
Biologie
Als Anwendungsfall in der Biologie, gäbe es als Beispiel das Finden der wahrscheinlichsten Sekundärstruktur eines RNS Moleküls. Dabei hat die RNS im
Gegensatz zur DNS nur ein Strang, welcher dafür jedoch eine Sekundärstruktur bildet. Die RNS besteht aus den vier Basen Adenin, Guanin, Cytzosin
und Uracil. Dabei bildet Adenin mit Uracil und Cytosin mit Guanin Paare.
Die Sekundärstruktur hat keine Überkreuzungen und keine scharfen Knicke.
Ein Paar (i,j), ist dabei i<j-4. Der Algorithmus ermittelt die wahrscheinlichste Sekundärstruktur, indem er ein Matching findet, welches die Regeln
einhält und die größte Anzahl an Basenpaaren hat. Zur Lösung des Problems
gibt es folgende Idee:
Idee
Die Idee ist, dass OPT(j) die maximale Anzahl von Basenpaaren in der Sekundärstruktur von b1 ,...,bj sein soll.
Gesucht ist OPT(n). OPT soll 0 sein für n≤5. Dabei gibt es zwei Möglichkeiten für j:
ˆ Wenn j kein Teil eines Paares ist, wird j nicht mehr weiter betrachtet
und es kann mit OPT(j-1) weiter gearbeitet werden, da die Lösung
von OPT(j-1) identisch mit der Lösung von OPT(j) ist.
ˆ Wenn j ein Teil eines Paares mit t<j-4 ist, wird das Problem in zwei
Teilprobleme zerlegt:
Dafür darf OPT jedoch nicht mit einem Parameter aufgerufen werden, da alle Teilprobleme bi ,...,bj für i≤j in Betracht gezogen werden
müssen. Daher wird OPT(i,j) benutzt, welches die größte Anzahl an
Basenpaaren bi ,...,bj ermittelt.
Algorithmus
Zunächst werden alle Paare bi ,...,bj , i<j-4 mit OPT(i,j)=0 initalisiert. Sollte
j nun kein Teil eines Paares sein wird wieder OPT(i,j-1) aufgerufen. Ist j je32
Abbildung 9: Beispiel einer Basensequenz. [?, S. 9]
doch Teil eines Basenpaares wird OPT(i,t-1) und OPT(t+1,j-1) aufgerufen,
da sich die Basenpaare nicht überkreuzen dürfen.
Rekursive Funktion:
Abbildung 10: Rekursiver Aufruf der OPT(i,j) Funktion. [?, S. 11]
33
Herunterladen