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