Greedy-Algorithmen SS09

Aus ProgrammingWiki

< AuK
Wechseln zu: Navigation, Suche

Loading

Inhaltsverzeichnis

Folien

Greedy.pdf (7.3 MB)

Einführung

Greedy (engl. gefräßig) - eine gefräßige Strategie für Algorithmen. Sie funktioniert nach der Methode:

"Nimm immer den besten Happen, den du kriegen kannst."

Das geschieht durch eine rechnerische Entscheidung. Die Berechnung erfolgt auf der Basis lokaler Information, also ohne Rückgriff auf vorher ermittelte Werte. Es wird also keine Tabelle, die alle Teilproblemlösungen bereithält, mitgeführt, wie dam beim dynamisches Programmieren der Fall ist.

Beim Greedy-Algorithmen gibt es kein Zurücknehmen und Erproben von Alternativen, wie bei systematischen Probieren mit Backtracking. Eine einmal getroffene Entscheidung wird nicht revidiert.

Der Aufwand eines Greedy-Verfahrens ist sicher nicht exponentiell. Allerdings ist die Anwendbarkeit dieses Superverfahrens sehr in Frage gestellt.

Greedy-Algorithmen brauchen um die beste Auswahl zu treffen eine vorsortierte Kandidatenliste. Die neu aufnehmende Elemente werden immer gleich an der richtigen Stelle eingefügt, so dass die Sortierung erhalten bleibt.

Der zugehörige Datentyp ist eine Prioritätswarteschlange (priority queue). Sie kann mit einem heap sehr effizient implementiert werden.


Bruchteilrucksack-Problem

Bruchteilrucksack-Problem unterscheidet sich von dem 0/1 Rucksackproblem. Man hat die Möglichkeit, Gegenstände auch teilweise einzupacken. Hinsichtlich der Gegenstandeswerte gehen wir von einer homogenen Verteilung aus. Das heißt, dass ein zu 80% eingepackter Gegenstand, einen Wertzuwachs von 80% seines vollen Wertes erbringt.

Die Kandidatenliste wird nach dem Quotient aus:

  • - Wert des -ten Gegenstandes
  • - Gewicht des -ten Gegenstandes

absteigend sortiert.



Der Gegenstand mit dem größten spezifischen Wert erhält die Nummer , danach folgen , , , in dieser Reihenfolge.

sei die Kapazität des Rucksacks.

Nach der Greedy-Strategie stecken wir nun die ersten Gegenstände in den Rucksack. ergibt sich, wenn Gegenstand der erste ist, der nicht vollständig in den Rucksack passt. Die eventuell vorhandene Restkapazität kann aber durch einen Teil mit des Gegenstandsgewichtes ausgefüllt werden.



Der Gesamtwert des so gefüllten Rucksacks beträgt



Kann man wirklich sicher sein, dass auf diese weise der größtmögliche Gesamtwert erreicht wird? Könnte es nicht besser sein, von einem der ursprünglich vollständig eingepackten Gegenstände, sagen wir (mit ) nur einen Teil, sagen wir (mit ) zu nehmen. Dann hat man so viel Platz, dass ein Teil eines Gegenstandes () mit kleinerem spezifischem Wert als der von eingepackt werden kann. Dies ergibt den folgenden Gesamtwert.



Da das Gewicht des -ten Teiles von gerade mit dem Restgewicht übereinstimmt, d.h. , gilt


.


Dann kann man für den Gesamtwert



schreiben. Eine einfache Umformung ergibt



Da ergibt sich wegen



kein Wertzuwachs. Ganz im gegenteil, es gilt sogar .


Wir können also sicher sein, dass die Greedy-Methode das Bruchteil-Rucksackproblem effizient löst.


Hier unten ist die Prozedur teil_rucksack zu sehen, die nimmt als Eingabe die Rucksack-Kapazität und liefert die beste Einpackungsmöglichkeit aus der vorgegebenen Kandidatenliste (Wert . Gewicht). Gesamtwert, Gesamtgewicht und Bruchteil des letztes Elements werden auch ausgegeben.


Aufbau-Template

Bestandteile des Algorithmus:

  • eine (sich verbrauchende) Kandidatenliste,
  • eine (sich aufbauende) Resultatliste,
  • Funktionen:
    • loesung?, die überprüft, ob die aktuelle Resultatliste eine Lösung des Problems darstellt,
    • okay?, die überprüft, ob die Erweiterung einer Resultatliste durch den nächsten Kandidaten zulässig ist,
    • naechster, die den besten Kandidaten aus der Liste auswählt,
    • modify+, die die Kandidatenliste im positiven Fall modifiziert,
    • modify-, die die Kandidatenliste im negativen Fall modifiziert,
    • ziel, die die Lösung angibt.
(define greedy
 (lambda (vorgabewert) ; z.B.: Kapazitaet der Rucksacks
  (let
      ((kandidatenliste '(......))
       (loesung? (lambda (ls) ... ))
       (okay? (lambda (ls) ... ))
       (naechster (lambda (ls) ... ))
       (modify+ (lambda (x ls) ... ))
       (modify- (lambda (x ls) ... ))
       (ziel (lambda (ls) ... ))
   (letrec
        ((helfer
           (lambda (kandidaten resultat)
            (cond
             ((loesung? resultat)(ziel resultat))
             ((and (null? kandidaten)(not (loesung? resultat)))
              (error 'greedy "Es gibt keine Loesung."))
             (else
              (let* ((neuer-kandidat (naechster kandidaten))
                     (neues-resultat (cons neuer-kandidat resultat)))
                 (if (okay? neues-resultat)
                     (helfer (modify+ neuer-kandidat kandidaten) neues-resultat)
                     (helfer (modify- neuer-kandidat kandidaten) resultat))))))))
        (helfer kandidatenliste '())))))


Beispiel (Geldwechsel)

Einem Kunden wird Wechselgeld herausgegeben, wobei möglichst wenige Münzen verwendet werden sollen.

  • Zur Verfügung haben wir alle Euro-Münzen: 1, 2, 5, 10, 20, 50, 100 (1 Euro), 200 (2 Euro).
  • Die Münzen werden mit absteigendem Cent-Wert vorsortiert.
  • Die Prozedur geldwechsel ergibt Resultatliste und Anzahl der Münzen.


Die Prozedur wird benutzt um 12,93 Euro (1293 Euro-Cent) herauszugeben.


Verschiedene Greedy-Algorithmen

Dijkstra

Bild 6.1: Dijkstras Algorithmus
(Quelle: Wikipedia)

Edsger W. Dijkstra auf Wikipedia

Kürzeste Wege in Graphen

Die Suche nach der kürzesten Rundreise, die n gegebene Städte genau einmal besucht, gehört zu den schwierigsten Problemen, für die bisher nur Lösungsverfahren mit exponentieller Rechenzeit bekannt sind (siehe: Verzweigen und beschränken: Rundreiseproblem).

Das folgende Problem klingt noch schwieriger. Gesucht ist der kürzeste Weg von Stadt A zu Stadt B, wobei für die n betrachteten Städte eine Entfernungsmatrix gegeben ist. Man muss alle Zwischenstädtefolgen (Wege) betrachten, auch solche, die nicht alle Städte enthalten.

Bild 6.2: Bewerteter ungerichteter Graph mit 7 Knoten

Dijkstra fand 1959 einen Algorithmus zur Lösung des Kürzeste-Wege-Problems dessen Rechenzeit in liegt. Ein nach Floyd und Warshall benanntes Verfahren löst eine etwas weiter gehende fragestellung in . Dabei werden die kürzesten Wege für alle Städtpaare bestimmt.

Das Problem ist, mathematisch gesehen, eine Sache der Graphentheorie.

Unter einem endlichen Graph G versteht man ein Paar (V, E), wobei V eine endliche Knotenmenge (Knoten = vertex) und E eine endliche Menge von Kanten (edges) bezeichnet. Eine Kante verbindet genau zwei Knoten. Es ist nicht verlangt, dass alle Knotenpaare durch je eine Kante verbunden sind. Es kann Mehrfachkanten geben.

Zur Speicherung von Graphen verwendet man Matrizen. Eine gängige Form sind sog. Adjazenmatrizen, die für jedes Knotenpaar vermerken, ob die durch die aktuelle Zeile bzw. Spalte bestimmten Knoten durch eine Kante verbunden sind.

Handelt es sich um einen gewichteten Graph, dessen Kanten z.B. mit je einer Zahl für eine bestimmte Länge, Masse oder einen Euro-Betrag markiert sind, werden diese Gewichte in der Adjazenmatrix angegeben. Nicht vorhandene Kanten werden durch einen vereinbarten Wert, meist 0, x oder eine sehr große Zahl, beschrieben.

Wir sprechen von einem gerichteten Graph (engl. directed graph, Digraph), wenn die Kanten einseitig gerichtete Pfeile sind.


Dijkstras Algorithmus

Gegeben sei der Bild in Bild 6.2 modellierter Stadplan mit 7 markanten Punkten A, B, C, D, E, F und G.
Gesucht ist die kürzeste Strecke von A (Airport) zum Stadthotel D.

Die Grundidee des Algorithmus ist, ab einem Startknoten die kürzest möglichen Wege weiter zu verfolgen und längere Wege beim Updaten auszuschließen. Er besteht aus diesen Schritten:

  1. Initialisiere eine Menge von Startknoten mit einem Element (A).
  2. Weise allen Knoten die beiden Eigenschaften "Distanz" und "Vorgänger" zu. Initialisiere die Distanz im Startknoten mit 0 und in allen anderen Knoten mit ∞.
  3. Solange es noch unbesuchte Knoten gibt wähle darunter denjenigen mit minimaler Distanz aus und
    1. Speichere, dass dieser Knoten schon besucht wurde
    2. Berechne für alle noch unbesuchten Nachbarknoten die Summe des jeweiligen Kantengewichtes und der Distanz im aktuellen Knoten
    3. Ist dieser Wert für einen Knoten kleiner als die dort gespeicherte Distanz, aktualisiere sie und setze den aktuellen Knoten als Vorgänger.
      Dieser Schritt wird auch als Update bezeichnet und ist die zentrale Idee von Dijkstra.

Naive Implementation: .

Heaps Implementation: , wo Knoten und Kanten sind.

Bild 6.3: Bild 6.2 bearbeitet von Dijkstra-Algorithmus (von A nach D)
S:={s};
Distanz(s):=0;
for all vV\{s} do {
    Distanz(v)=Kante(s,v);
    Vorgaenger(v)=s;
}
while zS do {
    Finde v'V\S mit Distanz(v')=min{Distanz(v) | vV\S};
    S:=S{v'};
    for all vV\S do {
        if Distanz(v')+Kante(v',v) < Distanz(v)
        then {
            Distanz(v):=Distanz(v')+Kante(v' ,v);
            Vorgaenger(v):=v'
        }
    }
}


Kruskal

Bild 6.4: Kruskal Algorithmus (minimaler Spannbaum)

Joseph Kruskal auf Wikipedia

Der Algorithmus von Kruskal ist ein Algorithmus der Graphentheorie zur Berechnung minimaler Spannbäume von ungerichteten Graphen. Der Graph muss dazu zusätzlich zusammenhängend, kantengewichtet und endlich sein.

Der Algorithmus stammt von Joseph Kruskal, der ihn 1956 in der Zeitschrift „Proceedings of the American Mathematical Society“ veröffentlichte. Er beschrieb ihn dort wie folgt:

Führe den folgenden Schritt so oft wie möglich aus: Wähle unter den noch nicht ausgewählten Kanten von G (dem Graphen) die kürzeste Kante, die mit den schon gewählten Kanten keinen Kreis bildet.

Die kürzeste Kante bezeichnet dabei jeweils die Kante mit dem kleinsten Kantengewicht. Nach Abschluss des Algorithmus bilden die ausgewählten Kanten einen minimalen Spannbaum des Graphen.

Wendet man den Algorithmus auf unzusammenhängende Graphen an, so berechnet er einen minimalen Wald, also minimal spannende Bäume für jede Zusammenhangskomponente des Graphen.


Best case: , bei

Average case:

Worse case: , bei

: Anzahl der Knoten.

: Anzahl der Kanten.


Prim

Minimaler Spannbaum Algorithmus, der eine Alternative zu Kruskal-Algorithmus ist. Prim stellt eine kleine Modifikation von Dijksrta-Algorithmus vor, die sich im vergleich zu Dijkstra, nur an 3 Stellen unterscheidet.

Der Prim-Algorithmus ist effizienter bei Graphen mit vielen Kanten als Kruskal-Algorithmus.

Aufwand:

S:={s};
Distanz(s):=0;
for all vV\{s} do {
    Distanz(v)=Kante(s,v);
    Vorgaenger(v)=s;
}
while S!=V do {
    Finde v'V\S mit Distanz(v')=min{Distanz(v) | vV\S};
    S:=S{v'};
    for all vV\S do {
        if Kante(v',v) < Distanz(v)
        then {
            Distanz(v):=Kante(v' ,v);
            Vorgaenger(v):=v'
        }
    }
}


Die folgende Scheme-Prozedur versucht die Zahlen in Primfaktoren zu zerlegen, die sich in der Kandidatenliste befinden. In der Lösung wird auch die Anzahl der Primfaktoren ausgegeben.


Übung

Teilrucksack-Problem

Gegeben sei:

  • Rucksackkapazität: 60kg
  • Material 1:
    • Wert: 200 000
    • Gewicht: 35
  • Material 2:
    • Wert: 103 000
    • Gewicht: 25
  • Material 3:
    • Wert: 92 543
    • Gewicht: 14

Berechnen Sie die absteigende Kandidatenliste (schriftlich). Benutzen Sie die Scheme-Prozedur um die beste Rucksackseinpackung zu finden.


Geldwechsel (Euro)

Berechnen Sie schriftlich, wieviele Münzen heraugegeben werden müssen, um den Betrag 15,61 Euro auszuzahlen.

001eur.png 002eur.png 005eur.png 010eur.gif 020eur.gif 050eur.gif 1eur.gif 2eur.png

Überprüfen Sie das Ergebnis mit hilfe von der Prozedur geldwechsel.


Geldwechsel (US Dollar)

Berechnen Sie schriftlich, wieviele Münzen heraugegeben werden müssen, um den Betrag 15,61 US Dollar auszuzahlen.

Beachten Sie darauf, dass US Dollars folgende Münzen haben:

150px-Formative Years in Indiana Reverse.jpg 150px-2006 Nickel Proof Rev.png 150px-2005 Dime Rev Unc P.png 150px-2006 Quarter Proof.png 150px-2005 Half Dollar Rev Unc P.png 150px-LineartPresRev.png

Überprüfen Sie das Ergebnis mit hilfe von der Prozedur geldwechsel.

Hinweis: die Kandidatenliste soll im Quellcode angepasst werden.


Geldwechsel (Polski Złoty)

Berechnen Sie schriftlich, wieviele Münzen heraugegeben werden müssen, um den Betrag 15,61 PLN auszuzahlen.

Beachten Sie darauf, dass Polski Złoty folgende Münzen hat:

1grosz 2.png 2grosze 2.png 5groszy 2.png 10groszy 2.png 20groszy 2.png 50groszy 2.png 1zloty 2.png 2zloty 2.png 5zlotych 2.png

Überprüfen Sie das Ergebnis mit hilfe von der Prozedur geldwechsel.

Hinweis: die Kandidatenliste soll im Quellcode angepasst werden.


Dijkstra-Algorithmus

Dijkstra-Algorithmus Aufgabe

Ein Beispiel für die Anwendung des Algorithmus von Dijkstra ist die Suche nach einem kürzesten Pfad auf einer Landkarte.

Im diese Aufgabe sollen Sie aus der Adjazenzmatrix einen Graphen erstellen (schriftlich) und den kürzesten Pfad von Frankfurt nach München finden. Die Zahlen auf den Verbindungen zwischen zwei Städten geben jeweils die Entfernung zwischen den beiden durch die Kante verbundenen Städten an.


Prim-Algorithmus

Prim-Algorithmus Aufgabe

Verwenden Sie den Bsp. aus der Vorlesung, wählen Sie jetzt als den Startknoten: D.

Ermitteln Sie eine Reihenfolgenliste für alle Knoten.


Prim-Algorithmus (Scheme)

Experimentieren Sie mit dem Scheme-Code für Prim-Algorithmus.

Versuchen Sie die folgenden Zahlen in Primfaktoren zu zerlegen:

  • 24
  • 76
  • 98
  • 437
  • 667

Hinweis: Denken Sie auch an Erweiterung der Kandidatenliste, wenn die Prozedur einen Fehler ausgibt.


Kommentare

 This site requires Flash Player 8.0 or greater
Please click here to download.
If you are sure you have the required version, press this link: bypass the detection.

Auswertung

Persönliche Werkzeuge