Greedy-Algorithmus mit Beispiel: Was ist, Methode und Ansatz
โก Intelligente Zusammenfassung
Der Greedy-Algorithmus baut eine optimale Lรถsung auf, indem er in jedem Schritt die beste lokale Wahl trifft. Dabei werden Rekursion, geordnete Ressourcen und eine Abbruchbedingung verwendet, um Scheduling-, Spannbaum-, Kรผrzeste-Wege- und Netzwerkoptimierungsprobleme effizient zu lรถsen.
Was ist ein Greedy-Algorithmus?
A Gieriger Algorithmus Teilt eine Menge von Ressourcen rekursiv auf der Grundlage der maximalen unmittelbaren Verfรผgbarkeit dieser Ressource in jeder Phase der Ausfรผhrung auf.
Die Lรถsung eines Problems mit dem Greedy-Ansatz umfasst zwei Phasen:
- Scannen der Artikelliste
- Optimierung
Beide Phasen laufen parallel ab, wรคhrend das Eingabe-Array schrittweise unterteilt wird.
Um dem Greedy-Ansatz zu folgen, sind Grundkenntnisse in Rekursion und Kontextwechsel hilfreich. tracBetrachten wir den Code. Das Greedy-Paradigma lรคsst sich mit einem Paar notwendiger und hinreichender Aussagen beschreiben.
Zwei Bedingungen definieren das Greedy-Paradigma.
- Jeder einzelne Entscheidungsschritt muss das Problem in Richtung der bestmรถglichen Lรถsung lenken.
- Die Problemstruktur muss nach einer endlichen Anzahl von gierigen Schritten zum Stillstand kommen.
Nachdem wir die Theorie geklรคrt haben, wollen wir uns nun die Geschichte des Greedy-Search-Ansatzes ansehen.
Geschichte von Greedy Algorithms
Hier sind die wichtigsten Meilensteine โโin der Geschichte der Greedy-Algorithmen:
- Greedy-Algorithmen wurden erstmals in den 1950er Jahren fรผr Graph-Walk-Algorithmen konzipiert.
- Edsger Dijkstra entwickelte seinen Algorithmus zur Berechnung des kรผrzesten Weges, um Routen durch die niederlรคndische Hauptstadt Amsterdam zu verkรผrzen.
- Im selben Jahrzehnt entwickelten Prim und Kruskal Optimierungsstrategien, die die Pfadkosten entlang gewichteter Routen minimieren, um minimale Spannbรคume zu konstruieren.
- In den 70er Jahren beschrieben die amerikanischen Forscher Cormen, Leiserson, Rivest und Stein in ihrem klassischen Werk die rekursive Substrukturierung von Greedy-Lรถsungen. Introduction to Algorithms Lehrbuch.
- Das gierige Suchparadigma wurde 2005 in den NIST-Aufzeichnungen als eigenstรคndige Optimierungsstrategie katalogisiert.
- Bis heute verwenden Webprotokolle wie Open Shortest Path First (OSPF) und viele Paketvermittlungsprotokolle die Greedy-Strategie, um die Transitzeit in einem Netzwerk zu minimieren.
Gierige Strategien und Entscheidungen
Die Logik reduziert sich in jeder Phase auf eine binรคre Entscheidung โ โgierigโ oder โnicht gierigโ โ basierend auf der Richtung, die der Algorithmus zum Fortschreiten einschlรคgt.
Der Dijkstra-Algorithmus identifiziert beispielsweise Hosts im Internet, indem er in jedem Schritt eine Kostenfunktion auswertet. Der von der Kostenfunktion zurรผckgegebene Wert entscheidet darรผber, ob der nรคchste Pfad โgierigโ oder โnicht-gierigโ ist.
Kurz gesagt, ein Algorithmus hรถrt auf, gierig zu sein, sobald er einen Schritt unternimmt, der nicht lokal optimal ist, und gierige Probleme enden, wenn kein weiterer gieriger Schritt mehr mรถglich ist.
Eigenschaften des Greedy-Algorithmus
Die wichtigen Merkmale eines Greedy-Algorithmus sind:
- Eine geordnete Liste von Ressourcen enthรคlt Kosten- oder Wertzuweisungen, die die Beschrรคnkungen des Systems quantifizieren.
- Der Algorithmus nutzt die maximale Menge an Ressourcen innerhalb der vorgegebenen Zeit.
- Bei einem Aktivitรคtsplanungsproblem werden beispielsweise die Ressourcenkosten in Stunden gemessen und die Aktivitรคten mรผssen der Reihe nach ausgefรผhrt werden.
Warum den gierigen Ansatz wรคhlen?
Hier sind die Grรผnde fรผr die Verwendung des Greedy-Ansatzes:
- Der gierige Ansatz birgt Kompromisse, die ihn fรผr die Optimierung gut geeignet machen.
- Der offensichtlichste Grund ist, umgehend eine realisierbare Lรถsung zu finden. Im unten beschriebenen Problem der Aktivitรคtsauswahl kรถnnen, falls weitere Aktivitรคten vor Abschluss der aktuellen Aktivitรคt mรถglich sind, diese im selben Zeitfenster eingeplant werden.
- Ein weiterer Grund ist, dass es ein Problem rekursiv anhand einer Bedingung aufteilt, ohne dass Teillรถsungen zusammengefรผhrt werden mรผssen.
- Bei dem Problem der Aktivitรคtsauswahl wird der rekursive Teilungsschritt dadurch erreicht, dass die Liste einmal durchsucht und nur die in Frage kommenden Aktivitรคten berรผcksichtigt werden.
Wie man das Problem der Aktivitรคtsauswahl lรถst
Im Beispiel der Aktivitรคtsplanung hat jede Aktivitรคt eine Start- und Endzeit und ist zur besseren รbersicht mit einer Nummer gekennzeichnet. Es gibt zwei Aktivitรคtskategorien:
- Berรผcksichtigung der Aktivitรคt: Die Referenzaktivitรคt, anhand derer die Fรคhigkeit gemessen wird, weitere verbleibende Aktivitรคten einzufรผgen.
- Verbleibende Aktivitรคten: Aktivitรคten an einem oder mehreren Indizes vor der betrachteten Aktivitรคt.
Die Kosten fรผr die Durchfรผhrung einer Aktivitรคt entsprechen ihrer Dauer, berechnet als (Ende โ Anfang).
Der gierige Umfang ist einfach die Anzahl der verbleibenden Aktivitรคten, die innerhalb der Zeit einer betrachteten Aktivitรคt durchgefรผhrt werden kรถnnen.
ArchiStruktur des Greedy Approach
Schritt 1) Scannen Sie die Liste der Aktivitรคtskosten, beginnend mit dem Index 0 als Ausgangspunkt.
Schritt 2) Wenn bis zum Ende der betrachteten Aktivitรคt weitere Aktivitรคten abgeschlossen werden kรถnnen, suchen Sie nach diesen verbleibenden Aktivitรคten.
Schritt 3) Wenn keine weiteren Aktivitรคten geplant werden kรถnnen, wird die aktuell verbleibende Aktivitรคt als nรคchste berรผcksichtigte Aktivitรคt herangezogen. Wiederholen Sie Schritt 1 und Schritt 2 mit der neuen Aktivitรคt. Falls keine Aktivitรคten mehr verfรผgbar sind, fahren Sie mit Schritt 4 fort.
Schritt 4) Gib die Vereinigung der betrachteten Indizes zurรผck โ dies sind die Aktivitรคtsindizes, die den Durchsatz maximieren.
ArchiStruktur des Greedy Approach
Code Erlรคuterung
#include<iostream> #include<stdio.h> #include<stdlib.h> #define MAX_ACTIVITIES 12
Erklรคrung des Codes:
- Enthaltene Header-Dateien/Klassen
- Die maximale Anzahl der vom Benutzer konfigurierbaren Aktivitรคten.
using namespace std; class TIME { public: int hours; public: TIME() { hours = 0; } };
Erklรคrung des Codes:
- Deklariert den Standard-Namensraum fรผr Streaming-Operationen.
- Eine Klassendefinition fรผr TIME
- Ein Stunden-Zeitstempel.
- Ein TIME-Standardkonstruktor
- Die Stundenvariable.
class Activity { public: int index; TIME start; TIME finish; public: Activity() { start = finish = TIME(); } };
Erklรคrung des Codes:
- Eine Klassendefinition fรผr Activity.
- Zeitstempel, die zusammen eine Dauer definieren.
- Im Standardkonstruktor werden alle Zeitstempel auf 0 initialisiert.
class Scheduler { public: int considered_index,init_index; Activity *current_activities = new Activity[MAX_ACTIVITIES]; Activity *scheduled;
Erklรคrung des Codes:
- Teil 1 der Definition der Scheduler-Klasse.
- considered_index ist der Startpunkt fรผr das Scannen des Arrays.
- init_index wird verwendet, um wรคhrend der Einrichtung zufรคllige Zeitstempel zuzuweisen.
- Mit dem new-Operator wird dynamisch ein Array von Activity-Objekten erstellt.
- Der geplante Zeiger enthรคlt das aktuelle Ergebnis der Greedy-Operation.
Scheduler()
{
considered_index = 0;
scheduled = NULL;
...
...
Erklรคrung des Codes:
- Der Scheduler-Konstruktor โ Teil 2 der Klassendefinition.
- considered_index markiert den Beginn des aktuellen Scans.
- Das Ausmaร der Gier ist zu Beginn undefiniert.
for(init_index = 0; init_index < MAX_ACTIVITIES; init_index++) { current_activities[init_index].start.hours = rand() % 12; current_activities[init_index].finish.hours = current_activities[init_index].start.hours + (rand() % 2); printf("\nSTART:%d END %d\n", current_activities[init_index].start.hours ,current_activities[init_index].finish.hours); } … …
Erklรคrung des Codes:
- Eine For-Schleife initialisiert die Start- und Endzeiten jeder geplanten Aktivitรคt.
- Legt die Startzeit fest.
- Legt als Endzeitpunkt fest, dass er mindestens zur Startzeit liegt.
- Eine Debug-Anweisung gibt die zugewiesenen Zeitdauern aus.
public: Activity * activity_select(int); };
Erklรคrung des Codes:
- Teil 4 โ der letzte Teil der Scheduler-Klassendefinition.
- activity_select() nimmt einen Startindex als Basis und teilt die gierige Suche in Teilprobleme auf.
Activity * Scheduler :: activity_select(int considered_index) { this->considered_index = considered_index; int greedy_extent = this->considered_index + 1; … …
- Der Bereichsauflรถsungsoperator (::) verknรผpft die Funktionsdefinition mit der Klasse Scheduler.
- considered_index wird als Wert รผbergeben, und greedy_extent wird auf den unmittelbar darauf folgenden Index initialisiert.
Activity * Scheduler :: activity_select(int considered_index) { while( (greedy_extent < MAX_ACTIVITIES ) && ((this->current_activities[greedy_extent]).start.hours < (this->current_activities[considered_index]).finish.hours )) { printf("\nSchedule start:%d \nfinish%d\n activity:%d\n", (this->current_activities[greedy_extent]).start.hours, (this->current_activities[greedy_extent]).finish.hours, greedy_extent + 1); greedy_extent++; } … ...
Erklรคrung des Codes:
- Die Kernlogik โ die gierige Ausdehnung ist auf MAX_ACTIVITIES begrenzt.
- Die Startzeit der aktuellen Aktivitรคt wird mit der Endzeit der betrachteten Aktivitรคt verglichen.
- Solange die Bedingung erfรผllt ist, wird optional eine Debug-Meldung ausgegeben.
- Der gierige Extent geht dann zum nรคchsten Index im Aktivitรคtsarray รผber.
... if ( greedy_extent <= MAX_ACTIVITIES ) { return activity_select(greedy_extent); } else { return NULL; } }
Erklรคrung des Codes:
- Die Bedingungsprรผfung stellt sicher, dass alle Aktivitรคten abgedeckt sind.
- Andernfalls startet der Algorithmus die gierige Suche vom aktuellen Index aus neu โ ein rekursiver Schritt, der das Problem gierig aufteilt.
- Falls ja, wird die Kontrolle an den Aufrufer zurรผckgegeben, ohne dass die Mรถglichkeit besteht, Greed zu erweitern.
int main() { Scheduler *activity_sched = new Scheduler(); activity_sched->scheduled = activity_sched->activity_select( activity_sched->considered_index); return 0; }
Erklรคrung des Codes:
- Die Hauptfunktion ruft den Scheduler auf.
- Ein neues Scheduler-Objekt wird instanziiert.
- Die Funktion activity_select() gibt nach Beendigung der gierigen Quest einen Activity-Zeiger an den Aufrufer zurรผck.
Ausgang:
START:7 END 7 START:9 END 10 START:5 END 6 START:10 END 10 START:9 END 10 Schedule start:5 finish6 activity:3 Schedule start:9 finish10 activity:5
Einschrรคnkungen der Greedy-Technik
Der Greedy-Ansatz eignet sich nicht fรผr Probleme, die fรผr jedes Teilproblem eine optimale Lรถsung erfordern, wie beispielsweise das Sortieren.
In solchen Fรคllen kann die Greedy-Methode zu Fehlern fรผhren โ im schlimmsten Fall liefert sie eine nicht optimale Lรถsung.
Der grรถรte Nachteil gieriger Algorithmen besteht darin, dass sie Entscheidungen treffen, ohne zu wissen, was nach dem aktuellen gierigen Zustand kommt.
Das folgende Diagramm veranschaulicht diesen Nachteil der Greedy-Methode.
Bei dem hier als Baum dargestellten Greedy-Scan (hรถherer Wert bedeutet hรถhere Gier) wรผrde ein Algorithmus mit dem Wert 40 als nรคchstes 29 auswรคhlen, dann bei 12 enden, was insgesamt 41 ergibt.
Im Gegensatz dazu wรผrde eine Divide-and-Conquer-Strategie 25 mit 40 kombinieren, was insgesamt 65 ergibt und 24 Punkte hรถher ist als bei der lokal gierigen Wahl.
Beispiele fรผr Gierig Algorithms
Die meisten Netzwerkalgorithmen basieren auf einem Greedy-Ansatz. Gรคngige Beispiele fรผr Greedy-Algorithmen sind:
- Prims Algorithmus fรผr minimale Spannbรคume
- Problem des Handlungsreisenden (ungefรคhr)
- Graphische Kartenfรคrbung
- Kruskals Algorithmus fรผr minimale Spannbรคume
- Dijkstras Algorithmus fรผr den kรผrzesten Pfad
- Knotenรผberdeckung des Graphen
- Rucksackproblem
- Jobsequenzierung mit Fristen















