320 likes | 493 Views
Grundlagen der Informatik 1 Thema 8: Akkumulation von Wissen. Prof. Dr. Max Mühlhäuser Dr. Guido Rößling. Rekursive Funktionen. Die rekursiven Funktionen, die wir bisher gesehen haben, sind kontextfrei Sie kümmern sich um ihr Subproblem, ohne etwas von dem Gesamtproblem zu wissen
E N D
Grundlagen der Informatik 1Thema 8: Akkumulation von Wissen Prof. Dr. Max Mühlhäuser Dr. Guido Rößling
Rekursive Funktionen • Die rekursiven Funktionen, die wir bisher gesehen haben, sind kontextfrei • Sie kümmern sich um ihr Subproblem, ohne etwas von dem Gesamtproblem zu wissen • Vorteil: Solche Funktionen sind einfach zu entwickeln, einfach wartbar etc. • Nachteil: Manche Funktionen werden dadurch ineffizient oder kompliziert
0 50 40 70 30 30 220 0 50 90 160 190 Beispiel • Gegeben: Liste von Punkten mit relativer Distanz zwischen Punkten • Aufgabe: Absolute Distanzen vom Ursprung berechnen
Lösung mit struktureller Rekursion ;; relative-2-absolute : (listof number) -> (listof number) ;; to convert a list of relative distances to a ;; list of absolute distances; the first item on the list ;; represents the distance to the origin (define (relative-2-absolute alon) (cond [(empty? alon) empty] [else (cons (first alon) (add-to-each (first alon) (relative-2-absolute (rest alon))))])) ;; add-to-each : number (listof number) -> (listof number) ;; to add n to each number on alon (define (add-to-each n alon) …)
Lösung mit struktureller Rekursion ;; add-to-each : number (listof number) -> (listof number) ;; to add n to each number in alon (define (add-to-each n alon) (cond [(empty? alon) empty] [else (cons (+ (first alon) n) (add-to-each n (rest alon)))])) Bevor wir die Nachteile diskutieren, wollen wir kurz unser Wissen über Funktionen höherer Ordnung anwenden • (add-to-each n alon)->(map (lambda (m) (+ n m)) alon)) • (relative-2-absolutealon) ->(foldr (lambda (x xs) (cons x (add-to-each x xs))) emptyalon)) • Beachte: die Funktion, die gefaltet wird, ist nicht assoziativ • foldl anstelle von foldr ergibt ein anderes Ergebnis! • Wie kommt man auf die letzte Gleichung?
Zur Erinnerung: foldr ;; foldr : (X Y -> Y) Y (listof X) -> Y ;; (foldr f base (list x-1 ... x-n)) = ;; (f x-1 ... (f x-n base)) (define (foldr f base alox) (cond [(empty? alox) base] [else (f (first alox) (foldr f base (rest alox)))])) • Wie kann man foldr spezialisieren, um relative-2-absolute zu erhalten? • f=(lambda (x xs) (cons x (add-to-each x xs))) (define (relative-2-absolute alon) (cond [(empty? alon) empty] [else (cons (first alon) (add-to-each (first alon) (relative-2-absolute (rest alon))))]))
Zurück zum Thema… • Was ist die Zeitkomplexität von relative-2-absolute in Abhängigkeit von der Länge der Liste? • Sei T(n) Zeitkomplexität von relative-2-absolute,sei U(n) Zeitkomplexität von add-to-each • U(n) = 1 + U(n-1) U(n) = n O (n) • T(n) = 1 + T(n-1) + U(n-1) = 1 + T(n-1) + n-1 = n + T(n-1) = n + (n-1) + … + 1 = n*(n+1) / 2 = (n2+n)/2 O (n²) • In der Version mit foldr, map kann man die Zeitkomplexität leicht aus der Zeitkomplexität von foldr, map folgern • foldr f basealox T(n) = n*(n), • wobei n = Länge von alox, • (n) = Zeitkomplexität von f
Was ist das Problem? • Das Problem ist die “Kontextfreiheit” von relative-2-absolute • Der rekursive Aufruf für L in der Liste (cons N L) macht genau dasselbe wie bei einer anderen Liste (cons K L) • Idee: Zusätzlicher Parameter akkumuliert das Wissen über den Kontext, in diesem Fall die akkumulierte Distanz (define (rel-2-abs alon accu-dist) (cond [(empty? alon) empty] [else (cons (+ (first alon) accu-dist) (rel-2-abs (rest alon) (+ (first alon) accu-dist)))]))
Lösung • Funktion ist noch nicht äquivalent zu relative-2-absolute • Es gibt einen zusätzlichen Parameter • Dieser ist am Anfang 0 (define (relative-2-absolute2 alon) (local ((define (rel-2-abs alon accu-dist) (cond [(empty? alon) empty] [else (cons (+ (first alon) accu-dist) (rel-2-abs (rest alon) (+ (first alon) accu-dist)))]))) (rel-2-abs alon 0)))
Beispiel mit generativer Rekursion • Das Problem, in bestimmten Situationen Wissen zu akkumulieren, existiert nicht nur bei struktureller, sondern auch bei generativer Rekursion • Beispiel: Finden von Pfaden in einem einfachen Graphen, in dem jeder Knoten nur eine ausgehende Kante hat (Zyklen sind erlaubt!). A B C D E F • Ein Knoten ist ein Symbol. • Ein Paar ist eine Liste mit zwei Knoten (mit S, T Symbole): (list S T) • Ein simple-graph ist eine Liste von Paaren: (listof pair). (define SimpleG '((A B) (B C) (C E) (D E) (E B) (F F)))
Beispiel mit generativer Rekursion • Der Kopf der Funktion ist einfach • Für die Implementierung des Körpers brauchen wir Antworten zu den 4 Basisfragen bei generativer Rekursion: • Was ist ein trivial zu lösendes Problem? • Das Problem ist trivial, wenn die Knoten orig und dest den selben Knoten bezeichnen • Was ist eine dazugehörige Lösung? • Einfach: true. • Wie erzeugen wir neue Probleme? • Wenn orig nicht denselben Knoten wie dest bezeichnet, können wir folgendes tun: gehe zu den Knoten, mit denen orig verbunden ist und stelle fest, ob diese eine Verbindung mit dest haben. • Wie verbinden wir die Lösungen? • Wir müssen nichts mehr tun, wenn wir das neue Problem gelöst haben. Wenn origsNachbar mit dest verbunden ist, gilt das auch für orig. ;; route-exists? : node node simple-graph -> boolean ;; to determine whether there is a route from ;; orig to dest in sg (define (route-exists? origdestsg) ...)
Die Lösung ist nun einfach… • …funktioniert aber leider nicht richtig! • route-exists? kann nie false zurückgeben ;; route-exists? : node node simple-graph -> boolean ;; to determine whether there is a route from orig to dest in sg (define (route-exists? orig dest sg) (cond [(symbol=? orig dest) true] [else (route-exists? (neighbor orig sg) dest sg)])) ;; neighbor : node simple-graph -> node ;; to determine the node that is connected to a-node in sg (define (neighbor a-node sg) (cond [(empty? sg) (error "neighbor: impossible")] [else (cond [(symbol=? (first (first sg)) a-node) (second (first sg))] [else (neighbor a-node (rest sg))])]))
A B C D E F Ursache des Fehlers • Betrachten wir einen Fall, in dem “false” zurückgegeben werden müsste: • Die Funktion ruft sich nach ein paar Aufrufen wieder mit denselben Parametern auf • Endlosschleife • Ursache: Die Funktion “vergisst”, mit welchen Parametern sie schon aufgerufen wurde (route-exists? 'C 'D '((A B) (B C) (C E) (D E) (E B) (F F))) = (route-exists? 'E 'D '((A B) (B C) (C E) (D E) (E B) (F F))) = (route-exists? 'B 'D '((A B) (B C) (C E) (D E) (E B) (F F))) = (route-exists? 'C 'D '((A B) (B C) (C E) (D E) (E B) (F F)))= …
Lösung • Akkumulation der besuchten Knoten • Akkumulation alleine reicht natürlich nicht aus, wir müssen das akkumulierte Wissen auch nutzen! ;; route-exists-accu? : ;; node node simple-graph (listof node) -> boolean ;; to determine whether there is a route from orig to dest in sg, ;; assuming the nodes in accu-seen have already been inspected ;; and failed to deliver a solution (define (route-exists-accu? origdestsgaccu-seen) (cond [(symbol=? origdest) true] [else (route-exists-accu? (neighbor origsg) destsg (cons origaccu-seen))]))
Lösung • Check hinzufügen, spezialisieren… ;; route-exists2? : node node simple-graph -> boolean ;; to determine whether there is a route from orig->destin sg (define (route-exists2? origdestsg) (local ((define (re-accu? origdestsgaccu-seen) (cond [(symbol=? origdest) true] [(contains? origaccu-seen) false] [else (re-accu? (neighbor origsg) dest sg (cons origaccu-seen))]))) (re-accu? origdestsg empty)))
Vorläufige Zusammenfassung • Sowohl strukturelle als auch generative Rekursion können von dem Problem betroffen sein, dass man einen Akkumulator benötigt • Bei dem Beispiel mit struktureller Rekursion haben wir die Performanz erheblich verbessert • Das Beispiel mit generativer Rekursion funktionierte ohne Akkumulator nicht • Betrachten wir nun im Allgemeinen, wann man einen Akkumulator benötigt.
Entwurf von Funktionen mit Akkumulatoren • Oft erkennt man, dass man einen Akkumulator benötigt, erst nachdem man bereits eine Version der Funktion implementiert hat… • Der Schlüssel zur Entwicklung einer Funktion im Akkumulator-Stil ist: • Erkenne, dass die Funktion einen Akkumulator benötigt, oder von dessen Verwendung profitiert; • Verstehe, was den Akkumulator ausmacht.
Erkennen, dass man einen Akkumulator benötigt • Wir haben zwei Gründe gesehen: • Performanz • Die Funktion funktioniert ohne Akkumulator nicht korrekt. • Das sind die wichtigsten Gründe um Akkumulator-Parameter hinzuzufügen. • In beiden Fällen ist es entscheidend, dass wir zunächst eine vollständige Funktion auf Basis eines Design Musters entwerfen. • Dann studieren wir diese Funktion und schauen nach den folgenden Eigenschaften…
Potentieller Kandidat für Akkumulator Heuristik: • Die Funktion ist strukturell rekursiv • Ergebnis einer rekursiven Anwendung wird durch eine zusätzliche, rekursive Funktion bearbeitet ;; invert : (listof X) -> (listof X) ;; to construct the reverse of alox ;; structural recursion (define (invert alox) (cond [(empty? alox) empty] [else (make-last-item (first alox) (invert (rest alox)))])) ;; make-last-item : X (listof X) -> (listof X) ;; to add an-x to the end of alox ;; structural recursion (define (make-last-item an-x alox) (cond [(empty? alox) (list an-x)] [else (cons (first alox) (make-last-item an-x (rest alox)))]))
Funktionen im Akkumulator-Stil Wenn wir entschieden haben, eine Funktion im Akkumulator-Stil zu schreiben, führen wir den Akkumulator in zwei Schritten hinzu • Definition und Bereitstellung des Akkumulators • Welches Wissen über die Parameter muss im Akkumulator gespeichert werden? • Fürrelative-2-absolute reichte es aus, die gesamte bisherige Distanz zu akkumulieren (Akkumulator = eine Zahl) • Für das Routenproblem mussten wir jeden Knoten, der bisher besucht wurde, speichern (Akkumulator = eine Liste von Knoten) • Ausnutzung des Akkumulators
Definition und Bereitstellung des Akkumulators • Füge ein Template der Akkumulatorfunktion als lokale Funktion ein. • Benenne die Parameter der Hauptfunktion und der Hilfsfunktion unterschiedlich. ;; invert : (listof X) -> (listof X) ;; to construct the reverse of alox (define (invert alox0) (local (;; accumulator ... (define (rev alox accumulator) (cond [(empty? alox) ...] [else (rev (rest alox)... (first alox)... accumulator)... ]))) (rev alox0 ...)))
Analyse invert kann nichts vergessen, da es nur die Reihenfolge von Listenelementen ändert. Daher könnten wir einfach alle Elemente akkumulieren denen rev begegnet. Das bedeutet: • accumulator steht für eine Liste und • accumulator steht für alle Elemente in alox, die dem alox- Argument von rev vorangehen. • Der Anfangswert von accumulator ist leer. • Wenn rev rekursiv aufgerufen wird, hat es nur ein Element bearbeitet: (firstalox). Um uns an dieses Element zu erinnern, können wir es mittels cons an die aktuellen Liste accumulator einfügen.
Erweiterte Version von invert ;; invert : (listof X) -> (listof X) ;; to construct the reverse of alox (define (invert alox0) (local (;; accumulator is the reversed list of all those items ;; on alox0 that precede alox (define (rev alox accumulator) (cond [(empty? alox) …] [else …(rev (rest alox) (cons (first alox) accumulator)) …]))) (rev alox0 empty))) • accumulator enthält nicht einfach die Elemente von alox0 vor alox, sondern eine Liste dieser Elemente in umgekehrter Reihenfolge.
Ausnutzen des Akkumulators • accumulator ist die Liste aller Elemente von alox0 vor alox in umgekehrter Reihenfolge… • Sobald alox leer ist, enthält accumulator die Umkehrung von alox0. ;; invert : (listof X) -> (listof X) ;; toconstructthereverseofalox (define (invert alox0) (local (;; accumulatoristhereversedlistof all thoseitems ;; on alox0 thatprecedealox (define (revaloxaccumulator) (cond [(empty? alox) accumulator] [else (rev (restalox) (cons (firstalox) accumulator))]))) (rev alox0 empty)))
[Direkte Implementierung von invert] • Variante ohne Akkumulator… ;; invert : (listof X) -> (listof X) ;; toconstructthereverseofalox (define (invertalox) (cond [(empty? alox) empty] [else (append (invert (restalox)) (list (firstalox)))]))
Definition von Akkumulator-Invarianten • Im Allgemeinen beschreibt eine Akkumulator-Invariante einen Beziehung zwischen • dem eigentlichen Argument der Funktion, • dem aktuellen Argument der Hilfsfunktion, und • dem Akkumulator • Wir betrachten die Definition von Akkumulator-Invarianten an einem Beispiel, welches keinen Akkumulator benötigt • Ermöglicht genauen Vergleich der beiden Varianten • Beispiel: Addition aller Zahlen einer Liste ;; sum : (listof number) -> number ;; to compute the sum of the numbers in alon ;; structural recursion (define (sum alon) (cond [(empty? alon) 0] [else (+ (first alon) (sum (rest alon)))]))
Definition von Akkumulator-Invarianten • Erster Schritt zu einer Akkumulator-Variante: Template • Ziel von sum ist die Summation von Zahlen • Intuitive Akkumulator-Invariante: Akkumulator speichert die Summe der Zahlen, die schon bearbeitet wurden ;; sum : (listof number) -> number ;; to compute the sum of the numbers on alon0 (define (sum alon0) (local (;; accumulator ... (define (sum-a alon accumulator) (cond [(empty? alon) ...] [else ... (sum-a (rest alon) ... (first alon) ... accumulator) ... ]))) (sum-a alon0 ...)))
Definition von Akkumulator-Invarianten • Erster Schritt zu einer Akkumulator-Variante: Template ;; sum : (listof number) -> number ;; to compute the sum of the numbers in alon0 (define (sum alon0) (local (;; accumulator is the sum of the numbers ;; in alon0 that preced those in alon (define (sum-a alon accumulator) (cond [(empty? alon) ...] [else ... (sum-a (rest alon) (+ (first alon) accumulator)) ... ]))) (sum-a alon0 0)))
Definition von Akkumulator-Invarianten • Der Rest ist nun einfach • Der Schlüssel ist die präzise Definition der Akkumulator-Invariante, der Rest folgt dann von selbst. ;; sum : (listof number) -> number ;; to compute the sum of the numbers on alon0 (define (sum alon0) (local (;; accumulator is the sum of the numbers ;; in alon0 that preceededthose in alon (define (sum-a alon accumulator) (cond [(empty? alon) accumulator] [else (sum-a (rest alon) (+ (first alon) accumulator))]))) (sum-a alon0 0)))
Vergleich • Ursprüngliche Version • Akkumulator-Version (sum (list 10.23 4.50 5.27)) = (+ 10.23 (sum (list 4.50 5.27))) = (+ 10.23 (+ 4.50 (sum (list 5.27)))) = (+ 10.23 (+ 4.50 (+ 5.27 (sum empty)))) = (+ 10.23 (+ 4.50 (+ 5.27 0))) = (+ 10.23 (+ 4.50 5.27)) = (+ 10.23 9.77) = 20.0 (sum (list 10.23 4.50 5.27)) = (sum-a (list 10.23 4.50 5.27) 0) = (sum-a (list 4.50 5.27) 10.23) = (sum-a (list 5.27) 14.73) = (sum-a empty 20.0) = 20.0
Zusammenfassung • Manche Funktionen sind nur im Akkumulator-Stil korrekt zu implementieren • Beispiel: route-exists? • In anderen Fällen führt der Akkumulator-Stil zu einem Effizienzgewinn • Beispiel: relative-2-absolute • Es ist jedoch nicht so, dass Funktionen im Akkumulator-Stil immer schneller sind • Beispiel: sum • Wir werden später sehen, dass Funktionen im Akkumulator-Stil sehr ähnlich zu Schleifen in Sprachen wie Java, Pascal etc. sind