<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
		<id>https://alda.iwr.uni-heidelberg.de/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Ukoethe</id>
		<title>Alda - User contributions [en]</title>
		<link rel="self" type="application/atom+xml" href="https://alda.iwr.uni-heidelberg.de/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Ukoethe"/>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php/Special:Contributions/Ukoethe"/>
		<updated>2026-05-08T19:56:59Z</updated>
		<subtitle>User contributions</subtitle>
		<generator>MediaWiki 1.30.0</generator>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Main_Page&amp;diff=5455</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Main_Page&amp;diff=5455"/>
				<updated>2012-10-04T16:14:37Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Übungsaufgaben */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Vorlesung Algorithmen und Datenstrukturen ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Dr. Ullrich Köthe, Universität Heidelberg, Sommersemester 2012&lt;br /&gt;
&lt;br /&gt;
Die Vorlesung findet '''dienstags''' und '''donnerstags''' jeweils um 14:15 Uhr in INF 227 (KIP), HS 2 statt. &lt;br /&gt;
&lt;br /&gt;
=== Klausur und Nachprüfung ===&lt;br /&gt;
&lt;br /&gt;
Die '''Abschlussklausur''' findet am Dienstag, dem 31.7.2012 von 10:00 bis 12:00 Uhr im HS 1 in INF 306 statt. Zur Klausur wird zugelassen, wer mindestens 50% der Übungspunkte erreicht. (Hinweis: Sie benötigen einen Lichtbildausweis, um sich bei der Klausur zu indentifizieren!) Die Nachklausur finet am 4.10.2012, 10:00 bis 12:00 im großen Seminarraum des HCI, Speyerer Str. 6 statt.&lt;br /&gt;
* '''[[Media:2012-Klausur-1.pdf|Ergebnis der Klausur vom 31.7.2012]]''' (anonymisiert)&lt;br /&gt;
* '''[[Media:2012-Klausur-2.pdf|Ergebnis der 2. Klausur vom 4.10.2012]]''' (anonymisiert)&lt;br /&gt;
&amp;lt;!---&lt;br /&gt;
* '''[[Media:Prüfungsteilnehmer.pdf|Liste der Studenten]], die sich verbindlich zur Klausur angemeldet und die notwendige Übungspunktzahl erreicht haben.'''&lt;br /&gt;
* '''Scheine''' können ab 1.9.2008 im Sekretariat Informatik bei Frau Tenschert abgeholt werden.&lt;br /&gt;
* Die '''Wiederholungsklausur''' findet am 1.10.2008 um 9:00 Uhr im Seminarraum des [http://hci.iwr.uni-heidelberg.de/contact.php HCI, Speyerer Str. 4], statt.&lt;br /&gt;
* '''[[Media:Ergebnis-Klausur-01-10-2008.pdf|Ergebnis der Wiederholungsklausur vom 1.10.2008]]''' (anonymisiert)&lt;br /&gt;
---&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Leistungsnachweise ===&lt;br /&gt;
Für alle Leistungsnachweise ist die erfolgreiche Teilnahme an den Übungen erforderlich. Für Leistungspunkte bzw. den Klausurschein muss außerdem die schriftliche Prüfung bestanden werden. Einzelheiten werden noch bekanntgegeben.&lt;br /&gt;
&amp;lt;!-------&lt;br /&gt;
Im einzelnen können erworben werden:&lt;br /&gt;
* ein unbenoteter Übungsschein, falls jemand nicht an der Klausur teilnimmt bzw. die Klausur nicht bestanden wurde.&lt;br /&gt;
* ein benoteter Übungsschein (Magister mit Computerlinguistik im ''Nebenfach'', Physik Diplom)&lt;br /&gt;
* ein Klausurschein (Magister mit Computerlinguistik im ''Hauptfach'')&lt;br /&gt;
* ein Leistungsnachweis über 9 Leistungspunkte (B.A. Computerlinguistik - alte Studienordnung) &lt;br /&gt;
* ein Leistungsnachweis über 8 Leistungspunkte (B.Sc. Informatik, B.A. Computerlinguistik - neue Studienordnung) &lt;br /&gt;
* ein Leistungsnachweis über 7 Leistungspunkte (B.Sc. Physik).&lt;br /&gt;
---------&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Übungsbetrieb ===&lt;br /&gt;
* Termine und Räume: &lt;br /&gt;
** Mo 14:00 - 16:00 Uhr, INF 227 (KIP), Seminarraum 2.402 (Tutor: Sven Ebser [mailto:sven@ebsers.de sven AT ebsers.de])&lt;br /&gt;
** Di  9:00 - 11:00 Uhr, INF 227 (KIP), Seminarraum 2.403 (Tutor: Christoph Koke [mailto:koke@kip.uni-heidelberg.de koke AT kip.uni-heidelberg.de])&lt;br /&gt;
** Di 11:00 - 13:00 Uhr, INF 227 (KIP), Seminarraum 2.403 (Tutor: Kai Karius [mailto:kai.karius@googlemail.com kai.karius AT googlemail.com])&lt;br /&gt;
** Mi 14:00 - 16:00 Uhr, INF 227 (KIP), Seminarraum 2.401 (Tutor: Stephan Meister [mailto:stephan.meister@iwr.uni-heidelberg.de stephan.meister AT iwr.uni-heidelberg.de])  &lt;br /&gt;
* Die Übungsgruppen werden über &amp;lt;b&amp;gt;[https://www.mathi.uni-heidelberg.de/muesli/lecture/view/169 MÜSLI]&amp;lt;/b&amp;gt; verwaltet. Dort erfolgt auch die &amp;lt;b&amp;gt;Anmeldung&amp;lt;/b&amp;gt;.&lt;br /&gt;
&amp;lt;!------&lt;br /&gt;
* [[Media:Punktestand.pdf|aktueller Punktestand]] (PDF, anonymisiert, so aktuell, wie von den Tutoren an mich übermittelt -- UK)&lt;br /&gt;
-------&amp;gt;&lt;br /&gt;
* &amp;lt;b&amp;gt;[[Main Page#Übungsaufgaben|Übungsaufgaben]]&amp;lt;/b&amp;gt; (Übungszettel mit Abgabetermin, Musterlösungen). Lösungen bitte per Email an den jeweiligen Übungsgruppenleiter.&lt;br /&gt;
* Zur Klausur wird zugelassen, wer mindestens 50% der Übungspunkte erreicht. Außerdem muss jeder Teilnehmer eine Lösung (bzw. einen Teil davon) in der Übungsgruppe vorrechnen. &lt;br /&gt;
* Durch das Lösen von Bonusaufgaben und gute Mitarbeit in den Übungen können Sie Zusatzpunkte erlangen. Zusatzpunkte werden auch vergeben, wenn Sie größere Verbesserungen an diesem Wiki vornehmen. Damit solche Verbesserungen der richtigen Person zugeordnet werden, sollten Sie dafür ein eigenes Wiki-Login verwenden, das Ihnen Stephan Meister oder Ullrich Köthe auf Anfrage gerne einrichten.&lt;br /&gt;
&lt;br /&gt;
=== Prüfungsvorbereitung ===&lt;br /&gt;
&lt;br /&gt;
Zur Hilfe bei der Prüfungsvorbereitung hat Andreas Fay [http://de.neemoy.com/quizcategories/31/ Quizfragen] erstellt.&lt;br /&gt;
&lt;br /&gt;
=== Literatur ===&lt;br /&gt;
&lt;br /&gt;
* R. Sedgewick: Algorithmen (empfohlen für den ersten Teil, bis einschließlich Graphenalgorithmen)&lt;br /&gt;
* J. Kleinberg, E.Tardos: Algorithm Design (empfohlen für den zweiten Teil, einschließlich Graphenalgorithmen)&lt;br /&gt;
* T. Cormen, C. Leiserson, R.Rivest: Algorithmen - eine Einführung (empfohlen zum Thema Komplexität)&lt;br /&gt;
* Wikipedia und andere Internetseiten (sehr gute Seiten über viele Algorithmen und Datenstrukturen)&lt;br /&gt;
&lt;br /&gt;
=== Gliederung der Vorlesung ===&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Einführung]] (17.4.2012) &lt;br /&gt;
#* Definition von Algorithmen und Datenstrukturen, Geschichte&lt;br /&gt;
#* Fundamentale Algorithmen: create, assign, copy, swap, compare etc.&lt;br /&gt;
#* Fundamentale Datenstrukturen: Zahlen, Container, Handles&lt;br /&gt;
#* Python-Grundlagen&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Container]] (19.4.2012)&lt;br /&gt;
#* Anforderungen von Algorithmen an Container&lt;br /&gt;
#* Einteilung der Container&lt;br /&gt;
#* Grundlegende Container: Array, verkettete Liste, Stack und Queue&lt;br /&gt;
#* Sequenzen und Intervalle (Ranges)&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Sortieren]] (24. und 26.4.2012)&lt;br /&gt;
#* Spezifikation des Sortierproblems&lt;br /&gt;
#* Selection Sort und Insertion Sort&lt;br /&gt;
#* Merge Sort&lt;br /&gt;
#* Quick Sort und seine Varianten&lt;br /&gt;
#* Vergleich der Anzahl der benötigten Schritte&lt;br /&gt;
#* Laufzeitmessung in Python&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Korrektheit]] (3. und 8.5.2012)&lt;br /&gt;
#* Definition von Korrektheit, Algorithmen-Spezifikation&lt;br /&gt;
#* Korrektheitsbeweise versus Testen&lt;br /&gt;
#* Vor- und Nachbedingungen, Invarianten, Programming by contract&lt;br /&gt;
#* Testen, Execution paths, Unit Tests in Python&lt;br /&gt;
#* Ausnahmen (exceptions) und Ausnahmebehandlung in Python&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Effizienz]] (10. und 15.5.2012)&lt;br /&gt;
#* Laufzeit und Optimierung: Innere Schleife, Caches, locality of reference&lt;br /&gt;
#* Laufzeit versus Komplexität&lt;br /&gt;
#* Landausymbole (O-Notation, &amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;-Notation, &amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;-Notation), Komplexitätsklassen&lt;br /&gt;
#* Bester, schlechtester, durchschnittlicher Fall&lt;br /&gt;
#* Amortisierte Komplexität&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Suchen]] (22. und 24.5.2012)&lt;br /&gt;
#* Sequentielle Suche&lt;br /&gt;
#* Binäre Suche in sortierten Arrays, Medianproblem&lt;br /&gt;
#* Suchbäume, balancierte Bäume&lt;br /&gt;
#* selbst-balancierende Bäume, Rotationen&lt;br /&gt;
#* Komplexität der Suche&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Sortieren in linearer Zeit]] (29.5.2012)&lt;br /&gt;
#* Permutationen&lt;br /&gt;
#* Sortieren als Suchproblem&lt;br /&gt;
#* Bucket Prinzip, Bucket Sort&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Prioritätswarteschlangen]] (31.5.2012)&lt;br /&gt;
#* Heap-Datenstruktur&lt;br /&gt;
#* Einfüge- und Löschoperationen&lt;br /&gt;
#* Heapsort&lt;br /&gt;
#* Komplexität des Heaps&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Assoziative Arrays]] (5.6.2012)&lt;br /&gt;
#* Datenstruktur-Dreieck für assoziative Arrays&lt;br /&gt;
#* Definition des abstrakten Datentyps&lt;br /&gt;
#* JSON-Datenformat&lt;br /&gt;
#* Realisierung durch sequentielle Suche und durch Suchbäume&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Hashing und Hashtabellen]] (5.6.und 12.6.2012)&lt;br /&gt;
#* Implementation assoziativer Arrays mit Bäumen&lt;br /&gt;
#* Hashing und Hashfunktionen&lt;br /&gt;
#* Implementation assoziativer Arrays als Hashtabelle mit linearer Verkettung bzw. mit offener Adressierung&lt;br /&gt;
#* Anwendung des Hashing zur String-Suche: Rabin-Karp-Algorithmus&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Iteration versus Rekursion]] (14.6.2012)&lt;br /&gt;
#* Typen der Rekursion und ihre Umwandlung in Iteration&lt;br /&gt;
#* Auflösung rekursiver Formeln mittels Master-Methode und Substitutionsmethode&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Generizität]] (19.6.2012)&lt;br /&gt;
#* Abstrakte Datentypen, Typspezifikation&lt;br /&gt;
#* Required Interface versus Offered Interface&lt;br /&gt;
#* Adapter und Typattribute, Funktoren&lt;br /&gt;
#* Beispiel: Algebraische Konzepte und Zahlendatentypen&lt;br /&gt;
#* Operator overloading in Python&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Graphen und Graphenalgorithmen]] (21.6. bis 5.7.2012)&lt;br /&gt;
#* Einführung&lt;br /&gt;
#* Graphendatenstrukturen, Adjazenzlisten und Adjazenzmatrizen&lt;br /&gt;
#* Gerichtete und ungerichtete Graphen&lt;br /&gt;
#* Vollständige Graphen&lt;br /&gt;
#* Planare Graphen, duale Graphen&lt;br /&gt;
#* Pfade, Zyklen&lt;br /&gt;
#* Tiefensuche und Breitensuche&lt;br /&gt;
#* Zusammenhang, Komponenten&lt;br /&gt;
#* Gewichtete Graphen&lt;br /&gt;
#* Minimaler Spannbaum&lt;br /&gt;
#* Kürzeste Wege, Best-first search (Dijkstra)&lt;br /&gt;
#* Most-Promising-first search (A*)&lt;br /&gt;
#* Problem des Handlungsreisenden, exakte Algorithmen (erschöpfende Suche, Branch-and-Bound-Methode) und Approximationen&lt;br /&gt;
#* Erfüllbarkeitsproblem, Darstellung des 2-SAT-Problems durch gerichtete Graphen, stark zusammenhängende Komponenten&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Repetition---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Orthogonale Zerlegung des Problems---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Hierarchische Zerlegung der Daten (Divide and Conquer)---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Randomisierung---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Optimierung, Zielfunktionen---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Systematisierung von Algorithmen aus der bisherigen Vorlesung---&amp;gt;&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
&amp;lt;!---# [[Analytische Optimierung]] (25.6.2008)---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Methode der kleinsten Quadrate---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Approximation von Geraden---&amp;gt;&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Randomisierte Algorithmen]] (10. und 12.7.2012)&lt;br /&gt;
#* Zufallszahlen, Zyklenlänge, Pitfalls&lt;br /&gt;
#* Zufallszahlengeneratoren: linear congruential generator, Mersenne Twister&lt;br /&gt;
#* Randomisierte vs. deterministische Algorithmen&lt;br /&gt;
#* Las Vegas vs. Monte Carlo Algorithmen&lt;br /&gt;
#* Beispiel für Las Vegas: Randomisiertes Quicksort&lt;br /&gt;
#* Beispiele für Monte Carlo: Randomisierte Lösung des k-SAT Problems &lt;br /&gt;
#* RANSAC-Algorithmus, Erfolgswahrscheinlichkeit, Vergleich mit analytischer Optimierung (Methode der kleinsten Quadrate)&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Greedy-Algorithmen und Dynamische Programmierung]] (17.7.2012)&lt;br /&gt;
#* Prinzipien, Aufwandsreduktion in Entscheidungsbäumen&lt;br /&gt;
#* bereits bekannte Algorithmen: minimale Spannbäume nach Kruskal, kürzeste Wege nach Dijkstra&lt;br /&gt;
#* Beispiel: Interval Scheduling Problem und Weighted Interval Scheduling Problem&lt;br /&gt;
#* Beweis der Optimalität beim Scheduling Problem: &amp;quot;greedy stays ahead&amp;quot;-Prinzip, Directed Acyclic Graph bei dynamischer Programmierung&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[NP-Vollständigkeit]] (19.7.2012)&lt;br /&gt;
#* die Klassen P und NP&lt;br /&gt;
#* NP-Vollständigkeit und Problemreduktion&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# Reserve und/oder Wiederholung (24. und 26.7.2012)&lt;br /&gt;
&lt;br /&gt;
== Übungsaufgaben ==&lt;br /&gt;
&lt;br /&gt;
zur Zeit nicht freigeschaltet.&lt;br /&gt;
&amp;lt;!-----&lt;br /&gt;
(im PDF Format). Die Abgabe erfolgt am angegebenen Tag bis 14:00 Uhr per Email an den jeweiligen Übungsgruppenleiter. Bei Abgabe bis zum folgenden Montag 11:00 Uhr werden noch 50% der erreichten Punkte angerechnet. Danach wird die Musterlösung freigeschaltet. Erreichbare Punkte (ohne Bonusaufgaben): 466.&lt;br /&gt;
&lt;br /&gt;
# [[Media:Übung-1.pdf|Übung]] (Abgabe 24.4.2012) und [[Media:Uebung-1-Musterloesung.pdf|Musterlösung]]&lt;br /&gt;
#* Python-Tutorial&lt;br /&gt;
#* Sieb des Eratosthenes&lt;br /&gt;
#* Wert- und Referenzsemantik&lt;br /&gt;
#* Dynamisches Array&lt;br /&gt;
# [[Media:Uebung-2.pdf|Übung]] (Abgabe 3.5.2012) und [[Media:Uebung-2-Musterloesung.pdf|Musterlösung]]&lt;br /&gt;
#* Sortieren: Implementation und Geschwindigkeitsvergleich (Diagramme in Abhängigkeit von der Problemgröße)&lt;br /&gt;
#* Entwicklung eines Gewinnalgorithmus für ein Spiel&lt;br /&gt;
#* Bonus: Dynamisches Array mit verringertem Speicherverbrauch&lt;br /&gt;
# [[Media:Uebung-3.pdf|Übung]] (Abgabe 10.5.2012) und [[Media:Uebung-3-Musterlösung.pdf|Musterlösung]]&lt;br /&gt;
#* Experimente zur Effektivität von Unit Tests&lt;br /&gt;
#* Bestimmung von Pi mit dem Algorithmus von Archimedes&lt;br /&gt;
#* Deque-Datenstruktur: Vor- und Nachbedingungen der Operationen, Implementation und Unit Tests&lt;br /&gt;
# [[Media:Uebung-4.pdf|Übung]] (Abgabe '''Montag''' 21.5.2012) und [[Media:muster_blatt4.pdf|Musterlösung]]&lt;br /&gt;
#* Theoretische Aufgaben zur Komplexität&lt;br /&gt;
#* Amortisierte Komplexität von array.append()&lt;br /&gt;
#* Optimierung der Matrizenmultiplikation&lt;br /&gt;
# [[Media:Uebung-5.pdf|Übung]] (31.5.2012) und [[Media:muster_blatt5.pdf|Musterlösung]]&lt;br /&gt;
#* Implementation und Analyse eines Binärbaumes&lt;br /&gt;
#* Anwendung: einfacher Taschenrechner&lt;br /&gt;
# [[Media:Uebung-6.pdf|Übung]] (Abgabe '''Freitag''' 8.6.2012) und [[Media:muster_blatt6.pdf|Musterlösung]]&lt;br /&gt;
#* Treap-Datenstruktur: Verbindung von Suchbaum und Heap&lt;br /&gt;
#* Anwendung: Worthäufigkeiten (Dazu benötigen Sie das File  [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/die-drei-musketiere.txt die-drei-musketiere.txt]. Die Zeichenkodierung in diesem File ist Latin-1.)&lt;br /&gt;
#* BucketSort&lt;br /&gt;
# [[Media:Uebung-7.pdf|Übung]] (Abgabe 14.6.2012) und [[Media:muster_blatt07.pdf|Musterlösung]]&lt;br /&gt;
#* Absichtliche Konstruktion von Kollisionen für eine Hashfunktion&lt;br /&gt;
#* Übungen zum Assoziativen Array und zum JSON-Format: Cocktail-Datenbank (Dazu benötigen Sie das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/cocktails.json cocktails.json]. Die Zeichenkodierung in diesem File ist UTF-8.)&lt;br /&gt;
# [[Media:Uebung-8.pdf|Übung]] (Abgabe 21.6.2012) und [[Media:muster_blatt8.pdf|Musterlösung]]&lt;br /&gt;
#* Übungen zu Rekursion und Iteration: Fibonaccizahlen, Koch-Schneeflocke, Komplexität rekursiver Algorithmen, Umwandlung von Rekursion in Iteration&lt;br /&gt;
# [[Media:Uebung-9.pdf|Übung]] (Abgabe 28.6.2012) und [[Media:muster_blatt9.pdf|Musterlösung]]&lt;br /&gt;
#* Planare Graphen: Aufstellen von Adjazenzmatrizen und Adjazenzlisten, obere Schranke für die Zahl der Kanten&lt;br /&gt;
#* Übungen zur Generizität: Sortieren mit veränderter Ordnung, Iterator für Tiefensuche&lt;br /&gt;
# [[Media:Uebung-10.pdf|Übung]] (Abgabe 5.7.2012) und [[Media:muster_blatt10.pdf|Musterlösung]]&lt;br /&gt;
#* Fortgeschrittene Graphenaufgaben: Erzeugen einer perfekten Hashfunktion, Routenplaner (Dazu benötigen Sie das File  [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/entfernungen.json entfernungen.json]. Die Zeichenkodierung in diesem File ist UTF-8.)&lt;br /&gt;
# [[Media:Uebung-11.pdf|Übung]] (Abgabe 12.7.2012) und [[Media:muster_blatt11.pdf|Musterlösung]] sowie schöne [[Media:ballungsgebiete.pdf|Visualisierung der Ballungsgebiete]] von Thorben Kröger&lt;br /&gt;
#* Fortgeschrittene Graphenaufgaben 2: Clusterung mittels minimaler Spannbäume, Bildverarbeitung mit Graphen (Dazu benötigen Sie wieder das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/entfernungen.json entfernungen.json] sowie die Files [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/cells.pgm cells.pgm] und [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/pgm.py pgm.py].)&lt;br /&gt;
# [[Media:Uebung-12.pdf|Übung]] (Abgabe 19.7.2012) und [[Media:muster_blatt12.pdf|Musterlösung]]&lt;br /&gt;
#* Erfüllbarkeitsproblem, Anwendung: Heim- und Auswärtsspiele im Fussball (Dazu benötigen sie das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/bundesliga-paarungen-12-13.json bundesliga-paarungen-12-13.json].)&lt;br /&gt;
#* Randomisierte Algorithmen: RANSAC für Kreise (Dazu benötigen sie das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/noisy-circles.txt noisy-circles.txt].)&lt;br /&gt;
# [[Media:Bonusuebung.pdf|Übung (Bonus)]] (&amp;lt;font color=red&amp;gt;Achtung: Abgabe bereits am Dienstag, 24.7.2012&amp;lt;/font&amp;gt;)&lt;br /&gt;
#* Greedy-Algorithmus&lt;br /&gt;
#* Weg durch einen Graphen&lt;br /&gt;
#* Wiederholungsaufgaben für die Klausur&lt;br /&gt;
---!&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Sonstiges ==&lt;br /&gt;
* [[Gnuplot| Gnuplot Kurztutorial]]&lt;br /&gt;
* [[Git Kurztutorial]]&lt;br /&gt;
* [[neue Startseite|mögliche neue Startseite]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Main_Page&amp;diff=5454</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Main_Page&amp;diff=5454"/>
				<updated>2012-10-04T16:04:57Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Klausur und Nachprüfung */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Vorlesung Algorithmen und Datenstrukturen ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Dr. Ullrich Köthe, Universität Heidelberg, Sommersemester 2012&lt;br /&gt;
&lt;br /&gt;
Die Vorlesung findet '''dienstags''' und '''donnerstags''' jeweils um 14:15 Uhr in INF 227 (KIP), HS 2 statt. &lt;br /&gt;
&lt;br /&gt;
=== Klausur und Nachprüfung ===&lt;br /&gt;
&lt;br /&gt;
Die '''Abschlussklausur''' findet am Dienstag, dem 31.7.2012 von 10:00 bis 12:00 Uhr im HS 1 in INF 306 statt. Zur Klausur wird zugelassen, wer mindestens 50% der Übungspunkte erreicht. (Hinweis: Sie benötigen einen Lichtbildausweis, um sich bei der Klausur zu indentifizieren!) Die Nachklausur finet am 4.10.2012, 10:00 bis 12:00 im großen Seminarraum des HCI, Speyerer Str. 6 statt.&lt;br /&gt;
* '''[[Media:2012-Klausur-1.pdf|Ergebnis der Klausur vom 31.7.2012]]''' (anonymisiert)&lt;br /&gt;
* '''[[Media:2012-Klausur-2.pdf|Ergebnis der 2. Klausur vom 4.10.2012]]''' (anonymisiert)&lt;br /&gt;
&amp;lt;!---&lt;br /&gt;
* '''[[Media:Prüfungsteilnehmer.pdf|Liste der Studenten]], die sich verbindlich zur Klausur angemeldet und die notwendige Übungspunktzahl erreicht haben.'''&lt;br /&gt;
* '''Scheine''' können ab 1.9.2008 im Sekretariat Informatik bei Frau Tenschert abgeholt werden.&lt;br /&gt;
* Die '''Wiederholungsklausur''' findet am 1.10.2008 um 9:00 Uhr im Seminarraum des [http://hci.iwr.uni-heidelberg.de/contact.php HCI, Speyerer Str. 4], statt.&lt;br /&gt;
* '''[[Media:Ergebnis-Klausur-01-10-2008.pdf|Ergebnis der Wiederholungsklausur vom 1.10.2008]]''' (anonymisiert)&lt;br /&gt;
---&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Leistungsnachweise ===&lt;br /&gt;
Für alle Leistungsnachweise ist die erfolgreiche Teilnahme an den Übungen erforderlich. Für Leistungspunkte bzw. den Klausurschein muss außerdem die schriftliche Prüfung bestanden werden. Einzelheiten werden noch bekanntgegeben.&lt;br /&gt;
&amp;lt;!-------&lt;br /&gt;
Im einzelnen können erworben werden:&lt;br /&gt;
* ein unbenoteter Übungsschein, falls jemand nicht an der Klausur teilnimmt bzw. die Klausur nicht bestanden wurde.&lt;br /&gt;
* ein benoteter Übungsschein (Magister mit Computerlinguistik im ''Nebenfach'', Physik Diplom)&lt;br /&gt;
* ein Klausurschein (Magister mit Computerlinguistik im ''Hauptfach'')&lt;br /&gt;
* ein Leistungsnachweis über 9 Leistungspunkte (B.A. Computerlinguistik - alte Studienordnung) &lt;br /&gt;
* ein Leistungsnachweis über 8 Leistungspunkte (B.Sc. Informatik, B.A. Computerlinguistik - neue Studienordnung) &lt;br /&gt;
* ein Leistungsnachweis über 7 Leistungspunkte (B.Sc. Physik).&lt;br /&gt;
---------&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Übungsbetrieb ===&lt;br /&gt;
* Termine und Räume: &lt;br /&gt;
** Mo 14:00 - 16:00 Uhr, INF 227 (KIP), Seminarraum 2.402 (Tutor: Sven Ebser [mailto:sven@ebsers.de sven AT ebsers.de])&lt;br /&gt;
** Di  9:00 - 11:00 Uhr, INF 227 (KIP), Seminarraum 2.403 (Tutor: Christoph Koke [mailto:koke@kip.uni-heidelberg.de koke AT kip.uni-heidelberg.de])&lt;br /&gt;
** Di 11:00 - 13:00 Uhr, INF 227 (KIP), Seminarraum 2.403 (Tutor: Kai Karius [mailto:kai.karius@googlemail.com kai.karius AT googlemail.com])&lt;br /&gt;
** Mi 14:00 - 16:00 Uhr, INF 227 (KIP), Seminarraum 2.401 (Tutor: Stephan Meister [mailto:stephan.meister@iwr.uni-heidelberg.de stephan.meister AT iwr.uni-heidelberg.de])  &lt;br /&gt;
* Die Übungsgruppen werden über &amp;lt;b&amp;gt;[https://www.mathi.uni-heidelberg.de/muesli/lecture/view/169 MÜSLI]&amp;lt;/b&amp;gt; verwaltet. Dort erfolgt auch die &amp;lt;b&amp;gt;Anmeldung&amp;lt;/b&amp;gt;.&lt;br /&gt;
&amp;lt;!------&lt;br /&gt;
* [[Media:Punktestand.pdf|aktueller Punktestand]] (PDF, anonymisiert, so aktuell, wie von den Tutoren an mich übermittelt -- UK)&lt;br /&gt;
-------&amp;gt;&lt;br /&gt;
* &amp;lt;b&amp;gt;[[Main Page#Übungsaufgaben|Übungsaufgaben]]&amp;lt;/b&amp;gt; (Übungszettel mit Abgabetermin, Musterlösungen). Lösungen bitte per Email an den jeweiligen Übungsgruppenleiter.&lt;br /&gt;
* Zur Klausur wird zugelassen, wer mindestens 50% der Übungspunkte erreicht. Außerdem muss jeder Teilnehmer eine Lösung (bzw. einen Teil davon) in der Übungsgruppe vorrechnen. &lt;br /&gt;
* Durch das Lösen von Bonusaufgaben und gute Mitarbeit in den Übungen können Sie Zusatzpunkte erlangen. Zusatzpunkte werden auch vergeben, wenn Sie größere Verbesserungen an diesem Wiki vornehmen. Damit solche Verbesserungen der richtigen Person zugeordnet werden, sollten Sie dafür ein eigenes Wiki-Login verwenden, das Ihnen Stephan Meister oder Ullrich Köthe auf Anfrage gerne einrichten.&lt;br /&gt;
&lt;br /&gt;
=== Prüfungsvorbereitung ===&lt;br /&gt;
&lt;br /&gt;
Zur Hilfe bei der Prüfungsvorbereitung hat Andreas Fay [http://de.neemoy.com/quizcategories/31/ Quizfragen] erstellt.&lt;br /&gt;
&lt;br /&gt;
=== Literatur ===&lt;br /&gt;
&lt;br /&gt;
* R. Sedgewick: Algorithmen (empfohlen für den ersten Teil, bis einschließlich Graphenalgorithmen)&lt;br /&gt;
* J. Kleinberg, E.Tardos: Algorithm Design (empfohlen für den zweiten Teil, einschließlich Graphenalgorithmen)&lt;br /&gt;
* T. Cormen, C. Leiserson, R.Rivest: Algorithmen - eine Einführung (empfohlen zum Thema Komplexität)&lt;br /&gt;
* Wikipedia und andere Internetseiten (sehr gute Seiten über viele Algorithmen und Datenstrukturen)&lt;br /&gt;
&lt;br /&gt;
=== Gliederung der Vorlesung ===&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Einführung]] (17.4.2012) &lt;br /&gt;
#* Definition von Algorithmen und Datenstrukturen, Geschichte&lt;br /&gt;
#* Fundamentale Algorithmen: create, assign, copy, swap, compare etc.&lt;br /&gt;
#* Fundamentale Datenstrukturen: Zahlen, Container, Handles&lt;br /&gt;
#* Python-Grundlagen&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Container]] (19.4.2012)&lt;br /&gt;
#* Anforderungen von Algorithmen an Container&lt;br /&gt;
#* Einteilung der Container&lt;br /&gt;
#* Grundlegende Container: Array, verkettete Liste, Stack und Queue&lt;br /&gt;
#* Sequenzen und Intervalle (Ranges)&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Sortieren]] (24. und 26.4.2012)&lt;br /&gt;
#* Spezifikation des Sortierproblems&lt;br /&gt;
#* Selection Sort und Insertion Sort&lt;br /&gt;
#* Merge Sort&lt;br /&gt;
#* Quick Sort und seine Varianten&lt;br /&gt;
#* Vergleich der Anzahl der benötigten Schritte&lt;br /&gt;
#* Laufzeitmessung in Python&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Korrektheit]] (3. und 8.5.2012)&lt;br /&gt;
#* Definition von Korrektheit, Algorithmen-Spezifikation&lt;br /&gt;
#* Korrektheitsbeweise versus Testen&lt;br /&gt;
#* Vor- und Nachbedingungen, Invarianten, Programming by contract&lt;br /&gt;
#* Testen, Execution paths, Unit Tests in Python&lt;br /&gt;
#* Ausnahmen (exceptions) und Ausnahmebehandlung in Python&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Effizienz]] (10. und 15.5.2012)&lt;br /&gt;
#* Laufzeit und Optimierung: Innere Schleife, Caches, locality of reference&lt;br /&gt;
#* Laufzeit versus Komplexität&lt;br /&gt;
#* Landausymbole (O-Notation, &amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;-Notation, &amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;-Notation), Komplexitätsklassen&lt;br /&gt;
#* Bester, schlechtester, durchschnittlicher Fall&lt;br /&gt;
#* Amortisierte Komplexität&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Suchen]] (22. und 24.5.2012)&lt;br /&gt;
#* Sequentielle Suche&lt;br /&gt;
#* Binäre Suche in sortierten Arrays, Medianproblem&lt;br /&gt;
#* Suchbäume, balancierte Bäume&lt;br /&gt;
#* selbst-balancierende Bäume, Rotationen&lt;br /&gt;
#* Komplexität der Suche&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Sortieren in linearer Zeit]] (29.5.2012)&lt;br /&gt;
#* Permutationen&lt;br /&gt;
#* Sortieren als Suchproblem&lt;br /&gt;
#* Bucket Prinzip, Bucket Sort&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Prioritätswarteschlangen]] (31.5.2012)&lt;br /&gt;
#* Heap-Datenstruktur&lt;br /&gt;
#* Einfüge- und Löschoperationen&lt;br /&gt;
#* Heapsort&lt;br /&gt;
#* Komplexität des Heaps&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Assoziative Arrays]] (5.6.2012)&lt;br /&gt;
#* Datenstruktur-Dreieck für assoziative Arrays&lt;br /&gt;
#* Definition des abstrakten Datentyps&lt;br /&gt;
#* JSON-Datenformat&lt;br /&gt;
#* Realisierung durch sequentielle Suche und durch Suchbäume&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Hashing und Hashtabellen]] (5.6.und 12.6.2012)&lt;br /&gt;
#* Implementation assoziativer Arrays mit Bäumen&lt;br /&gt;
#* Hashing und Hashfunktionen&lt;br /&gt;
#* Implementation assoziativer Arrays als Hashtabelle mit linearer Verkettung bzw. mit offener Adressierung&lt;br /&gt;
#* Anwendung des Hashing zur String-Suche: Rabin-Karp-Algorithmus&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Iteration versus Rekursion]] (14.6.2012)&lt;br /&gt;
#* Typen der Rekursion und ihre Umwandlung in Iteration&lt;br /&gt;
#* Auflösung rekursiver Formeln mittels Master-Methode und Substitutionsmethode&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Generizität]] (19.6.2012)&lt;br /&gt;
#* Abstrakte Datentypen, Typspezifikation&lt;br /&gt;
#* Required Interface versus Offered Interface&lt;br /&gt;
#* Adapter und Typattribute, Funktoren&lt;br /&gt;
#* Beispiel: Algebraische Konzepte und Zahlendatentypen&lt;br /&gt;
#* Operator overloading in Python&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Graphen und Graphenalgorithmen]] (21.6. bis 5.7.2012)&lt;br /&gt;
#* Einführung&lt;br /&gt;
#* Graphendatenstrukturen, Adjazenzlisten und Adjazenzmatrizen&lt;br /&gt;
#* Gerichtete und ungerichtete Graphen&lt;br /&gt;
#* Vollständige Graphen&lt;br /&gt;
#* Planare Graphen, duale Graphen&lt;br /&gt;
#* Pfade, Zyklen&lt;br /&gt;
#* Tiefensuche und Breitensuche&lt;br /&gt;
#* Zusammenhang, Komponenten&lt;br /&gt;
#* Gewichtete Graphen&lt;br /&gt;
#* Minimaler Spannbaum&lt;br /&gt;
#* Kürzeste Wege, Best-first search (Dijkstra)&lt;br /&gt;
#* Most-Promising-first search (A*)&lt;br /&gt;
#* Problem des Handlungsreisenden, exakte Algorithmen (erschöpfende Suche, Branch-and-Bound-Methode) und Approximationen&lt;br /&gt;
#* Erfüllbarkeitsproblem, Darstellung des 2-SAT-Problems durch gerichtete Graphen, stark zusammenhängende Komponenten&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Repetition---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Orthogonale Zerlegung des Problems---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Hierarchische Zerlegung der Daten (Divide and Conquer)---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Randomisierung---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Optimierung, Zielfunktionen---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Systematisierung von Algorithmen aus der bisherigen Vorlesung---&amp;gt;&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
&amp;lt;!---# [[Analytische Optimierung]] (25.6.2008)---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Methode der kleinsten Quadrate---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Approximation von Geraden---&amp;gt;&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Randomisierte Algorithmen]] (10. und 12.7.2012)&lt;br /&gt;
#* Zufallszahlen, Zyklenlänge, Pitfalls&lt;br /&gt;
#* Zufallszahlengeneratoren: linear congruential generator, Mersenne Twister&lt;br /&gt;
#* Randomisierte vs. deterministische Algorithmen&lt;br /&gt;
#* Las Vegas vs. Monte Carlo Algorithmen&lt;br /&gt;
#* Beispiel für Las Vegas: Randomisiertes Quicksort&lt;br /&gt;
#* Beispiele für Monte Carlo: Randomisierte Lösung des k-SAT Problems &lt;br /&gt;
#* RANSAC-Algorithmus, Erfolgswahrscheinlichkeit, Vergleich mit analytischer Optimierung (Methode der kleinsten Quadrate)&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Greedy-Algorithmen und Dynamische Programmierung]] (17.7.2012)&lt;br /&gt;
#* Prinzipien, Aufwandsreduktion in Entscheidungsbäumen&lt;br /&gt;
#* bereits bekannte Algorithmen: minimale Spannbäume nach Kruskal, kürzeste Wege nach Dijkstra&lt;br /&gt;
#* Beispiel: Interval Scheduling Problem und Weighted Interval Scheduling Problem&lt;br /&gt;
#* Beweis der Optimalität beim Scheduling Problem: &amp;quot;greedy stays ahead&amp;quot;-Prinzip, Directed Acyclic Graph bei dynamischer Programmierung&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[NP-Vollständigkeit]] (19.7.2012)&lt;br /&gt;
#* die Klassen P und NP&lt;br /&gt;
#* NP-Vollständigkeit und Problemreduktion&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# Reserve und/oder Wiederholung (24. und 26.7.2012)&lt;br /&gt;
&lt;br /&gt;
== Übungsaufgaben ==&lt;br /&gt;
&lt;br /&gt;
(im PDF Format). Die Abgabe erfolgt am angegebenen Tag bis 14:00 Uhr per Email an den jeweiligen Übungsgruppenleiter. Bei Abgabe bis zum folgenden Montag 11:00 Uhr werden noch 50% der erreichten Punkte angerechnet. Danach wird die Musterlösung freigeschaltet. Erreichbare Punkte (ohne Bonusaufgaben): 466.&lt;br /&gt;
&lt;br /&gt;
# [[Media:Übung-1.pdf|Übung]] (Abgabe 24.4.2012) und [[Media:Uebung-1-Musterloesung.pdf|Musterlösung]]&lt;br /&gt;
#* Python-Tutorial&lt;br /&gt;
#* Sieb des Eratosthenes&lt;br /&gt;
#* Wert- und Referenzsemantik&lt;br /&gt;
#* Dynamisches Array&lt;br /&gt;
# [[Media:Uebung-2.pdf|Übung]] (Abgabe 3.5.2012) und [[Media:Uebung-2-Musterloesung.pdf|Musterlösung]]&lt;br /&gt;
#* Sortieren: Implementation und Geschwindigkeitsvergleich (Diagramme in Abhängigkeit von der Problemgröße)&lt;br /&gt;
#* Entwicklung eines Gewinnalgorithmus für ein Spiel&lt;br /&gt;
#* Bonus: Dynamisches Array mit verringertem Speicherverbrauch&lt;br /&gt;
# [[Media:Uebung-3.pdf|Übung]] (Abgabe 10.5.2012) und [[Media:Uebung-3-Musterlösung.pdf|Musterlösung]]&lt;br /&gt;
#* Experimente zur Effektivität von Unit Tests&lt;br /&gt;
#* Bestimmung von Pi mit dem Algorithmus von Archimedes&lt;br /&gt;
#* Deque-Datenstruktur: Vor- und Nachbedingungen der Operationen, Implementation und Unit Tests&lt;br /&gt;
# [[Media:Uebung-4.pdf|Übung]] (Abgabe '''Montag''' 21.5.2012) und [[Media:muster_blatt4.pdf|Musterlösung]]&lt;br /&gt;
#* Theoretische Aufgaben zur Komplexität&lt;br /&gt;
#* Amortisierte Komplexität von array.append()&lt;br /&gt;
#* Optimierung der Matrizenmultiplikation&lt;br /&gt;
# [[Media:Uebung-5.pdf|Übung]] (31.5.2012) und [[Media:muster_blatt5.pdf|Musterlösung]]&lt;br /&gt;
#* Implementation und Analyse eines Binärbaumes&lt;br /&gt;
#* Anwendung: einfacher Taschenrechner&lt;br /&gt;
# [[Media:Uebung-6.pdf|Übung]] (Abgabe '''Freitag''' 8.6.2012) und [[Media:muster_blatt6.pdf|Musterlösung]]&lt;br /&gt;
#* Treap-Datenstruktur: Verbindung von Suchbaum und Heap&lt;br /&gt;
#* Anwendung: Worthäufigkeiten (Dazu benötigen Sie das File  [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/die-drei-musketiere.txt die-drei-musketiere.txt]. Die Zeichenkodierung in diesem File ist Latin-1.)&lt;br /&gt;
#* BucketSort&lt;br /&gt;
# [[Media:Uebung-7.pdf|Übung]] (Abgabe 14.6.2012) und [[Media:muster_blatt07.pdf|Musterlösung]]&lt;br /&gt;
#* Absichtliche Konstruktion von Kollisionen für eine Hashfunktion&lt;br /&gt;
#* Übungen zum Assoziativen Array und zum JSON-Format: Cocktail-Datenbank (Dazu benötigen Sie das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/cocktails.json cocktails.json]. Die Zeichenkodierung in diesem File ist UTF-8.)&lt;br /&gt;
# [[Media:Uebung-8.pdf|Übung]] (Abgabe 21.6.2012) und [[Media:muster_blatt8.pdf|Musterlösung]]&lt;br /&gt;
#* Übungen zu Rekursion und Iteration: Fibonaccizahlen, Koch-Schneeflocke, Komplexität rekursiver Algorithmen, Umwandlung von Rekursion in Iteration&lt;br /&gt;
# [[Media:Uebung-9.pdf|Übung]] (Abgabe 28.6.2012) und [[Media:muster_blatt9.pdf|Musterlösung]]&lt;br /&gt;
#* Planare Graphen: Aufstellen von Adjazenzmatrizen und Adjazenzlisten, obere Schranke für die Zahl der Kanten&lt;br /&gt;
#* Übungen zur Generizität: Sortieren mit veränderter Ordnung, Iterator für Tiefensuche&lt;br /&gt;
# [[Media:Uebung-10.pdf|Übung]] (Abgabe 5.7.2012) und [[Media:muster_blatt10.pdf|Musterlösung]]&lt;br /&gt;
#* Fortgeschrittene Graphenaufgaben: Erzeugen einer perfekten Hashfunktion, Routenplaner (Dazu benötigen Sie das File  [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/entfernungen.json entfernungen.json]. Die Zeichenkodierung in diesem File ist UTF-8.)&lt;br /&gt;
# [[Media:Uebung-11.pdf|Übung]] (Abgabe 12.7.2012) und [[Media:muster_blatt11.pdf|Musterlösung]] sowie schöne [[Media:ballungsgebiete.pdf|Visualisierung der Ballungsgebiete]] von Thorben Kröger&lt;br /&gt;
#* Fortgeschrittene Graphenaufgaben 2: Clusterung mittels minimaler Spannbäume, Bildverarbeitung mit Graphen (Dazu benötigen Sie wieder das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/entfernungen.json entfernungen.json] sowie die Files [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/cells.pgm cells.pgm] und [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/pgm.py pgm.py].)&lt;br /&gt;
# [[Media:Uebung-12.pdf|Übung]] (Abgabe 19.7.2012) und [[Media:muster_blatt12.pdf|Musterlösung]]&lt;br /&gt;
#* Erfüllbarkeitsproblem, Anwendung: Heim- und Auswärtsspiele im Fussball (Dazu benötigen sie das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/bundesliga-paarungen-12-13.json bundesliga-paarungen-12-13.json].)&lt;br /&gt;
#* Randomisierte Algorithmen: RANSAC für Kreise (Dazu benötigen sie das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/noisy-circles.txt noisy-circles.txt].)&lt;br /&gt;
# [[Media:Bonusuebung.pdf|Übung (Bonus)]] (&amp;lt;font color=red&amp;gt;Achtung: Abgabe bereits am Dienstag, 24.7.2012&amp;lt;/font&amp;gt;)&lt;br /&gt;
#* Greedy-Algorithmus&lt;br /&gt;
#* Weg durch einen Graphen&lt;br /&gt;
#* Wiederholungsaufgaben für die Klausur&lt;br /&gt;
&lt;br /&gt;
== Sonstiges ==&lt;br /&gt;
* [[Gnuplot| Gnuplot Kurztutorial]]&lt;br /&gt;
* [[Git Kurztutorial]]&lt;br /&gt;
* [[neue Startseite|mögliche neue Startseite]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Main_Page&amp;diff=5453</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Main_Page&amp;diff=5453"/>
				<updated>2012-10-04T16:04:34Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Klausur und Nachprüfung */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Vorlesung Algorithmen und Datenstrukturen ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Dr. Ullrich Köthe, Universität Heidelberg, Sommersemester 2012&lt;br /&gt;
&lt;br /&gt;
Die Vorlesung findet '''dienstags''' und '''donnerstags''' jeweils um 14:15 Uhr in INF 227 (KIP), HS 2 statt. &lt;br /&gt;
&lt;br /&gt;
=== Klausur und Nachprüfung ===&lt;br /&gt;
&lt;br /&gt;
Die '''Abschlussklausur''' findet am Dienstag, dem 31.7.2012 von 10:00 bis 12:00 Uhr im HS 1 in INF 306 statt. Zur Klausur wird zugelassen, wer mindestens 50% der Übungspunkte erreicht. (Hinweis: Sie benötigen einen Lichtbildausweis, um sich bei der Klausur zu indentifizieren!). Die Nachklausur finet am 4.10.2012, 10:00 bis 12:00 im großen Seminarraum des HCI, Speyerer Str. 6 statt.&lt;br /&gt;
* '''[[Media:2012-Klausur-1.pdf|Ergebnis der Klausur vom 31.7.2012]]''' (anonymisiert)&lt;br /&gt;
* '''[[Media:2012-Klausur-2.pdf|Ergebnis der 2. Klausur vom 4.10.2012]]''' (anonymisiert)&lt;br /&gt;
&amp;lt;!---&lt;br /&gt;
* '''[[Media:Prüfungsteilnehmer.pdf|Liste der Studenten]], die sich verbindlich zur Klausur angemeldet und die notwendige Übungspunktzahl erreicht haben.'''&lt;br /&gt;
* '''Scheine''' können ab 1.9.2008 im Sekretariat Informatik bei Frau Tenschert abgeholt werden.&lt;br /&gt;
* Die '''Wiederholungsklausur''' findet am 1.10.2008 um 9:00 Uhr im Seminarraum des [http://hci.iwr.uni-heidelberg.de/contact.php HCI, Speyerer Str. 4], statt.&lt;br /&gt;
* '''[[Media:Ergebnis-Klausur-01-10-2008.pdf|Ergebnis der Wiederholungsklausur vom 1.10.2008]]''' (anonymisiert)&lt;br /&gt;
---&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Leistungsnachweise ===&lt;br /&gt;
Für alle Leistungsnachweise ist die erfolgreiche Teilnahme an den Übungen erforderlich. Für Leistungspunkte bzw. den Klausurschein muss außerdem die schriftliche Prüfung bestanden werden. Einzelheiten werden noch bekanntgegeben.&lt;br /&gt;
&amp;lt;!-------&lt;br /&gt;
Im einzelnen können erworben werden:&lt;br /&gt;
* ein unbenoteter Übungsschein, falls jemand nicht an der Klausur teilnimmt bzw. die Klausur nicht bestanden wurde.&lt;br /&gt;
* ein benoteter Übungsschein (Magister mit Computerlinguistik im ''Nebenfach'', Physik Diplom)&lt;br /&gt;
* ein Klausurschein (Magister mit Computerlinguistik im ''Hauptfach'')&lt;br /&gt;
* ein Leistungsnachweis über 9 Leistungspunkte (B.A. Computerlinguistik - alte Studienordnung) &lt;br /&gt;
* ein Leistungsnachweis über 8 Leistungspunkte (B.Sc. Informatik, B.A. Computerlinguistik - neue Studienordnung) &lt;br /&gt;
* ein Leistungsnachweis über 7 Leistungspunkte (B.Sc. Physik).&lt;br /&gt;
---------&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Übungsbetrieb ===&lt;br /&gt;
* Termine und Räume: &lt;br /&gt;
** Mo 14:00 - 16:00 Uhr, INF 227 (KIP), Seminarraum 2.402 (Tutor: Sven Ebser [mailto:sven@ebsers.de sven AT ebsers.de])&lt;br /&gt;
** Di  9:00 - 11:00 Uhr, INF 227 (KIP), Seminarraum 2.403 (Tutor: Christoph Koke [mailto:koke@kip.uni-heidelberg.de koke AT kip.uni-heidelberg.de])&lt;br /&gt;
** Di 11:00 - 13:00 Uhr, INF 227 (KIP), Seminarraum 2.403 (Tutor: Kai Karius [mailto:kai.karius@googlemail.com kai.karius AT googlemail.com])&lt;br /&gt;
** Mi 14:00 - 16:00 Uhr, INF 227 (KIP), Seminarraum 2.401 (Tutor: Stephan Meister [mailto:stephan.meister@iwr.uni-heidelberg.de stephan.meister AT iwr.uni-heidelberg.de])  &lt;br /&gt;
* Die Übungsgruppen werden über &amp;lt;b&amp;gt;[https://www.mathi.uni-heidelberg.de/muesli/lecture/view/169 MÜSLI]&amp;lt;/b&amp;gt; verwaltet. Dort erfolgt auch die &amp;lt;b&amp;gt;Anmeldung&amp;lt;/b&amp;gt;.&lt;br /&gt;
&amp;lt;!------&lt;br /&gt;
* [[Media:Punktestand.pdf|aktueller Punktestand]] (PDF, anonymisiert, so aktuell, wie von den Tutoren an mich übermittelt -- UK)&lt;br /&gt;
-------&amp;gt;&lt;br /&gt;
* &amp;lt;b&amp;gt;[[Main Page#Übungsaufgaben|Übungsaufgaben]]&amp;lt;/b&amp;gt; (Übungszettel mit Abgabetermin, Musterlösungen). Lösungen bitte per Email an den jeweiligen Übungsgruppenleiter.&lt;br /&gt;
* Zur Klausur wird zugelassen, wer mindestens 50% der Übungspunkte erreicht. Außerdem muss jeder Teilnehmer eine Lösung (bzw. einen Teil davon) in der Übungsgruppe vorrechnen. &lt;br /&gt;
* Durch das Lösen von Bonusaufgaben und gute Mitarbeit in den Übungen können Sie Zusatzpunkte erlangen. Zusatzpunkte werden auch vergeben, wenn Sie größere Verbesserungen an diesem Wiki vornehmen. Damit solche Verbesserungen der richtigen Person zugeordnet werden, sollten Sie dafür ein eigenes Wiki-Login verwenden, das Ihnen Stephan Meister oder Ullrich Köthe auf Anfrage gerne einrichten.&lt;br /&gt;
&lt;br /&gt;
=== Prüfungsvorbereitung ===&lt;br /&gt;
&lt;br /&gt;
Zur Hilfe bei der Prüfungsvorbereitung hat Andreas Fay [http://de.neemoy.com/quizcategories/31/ Quizfragen] erstellt.&lt;br /&gt;
&lt;br /&gt;
=== Literatur ===&lt;br /&gt;
&lt;br /&gt;
* R. Sedgewick: Algorithmen (empfohlen für den ersten Teil, bis einschließlich Graphenalgorithmen)&lt;br /&gt;
* J. Kleinberg, E.Tardos: Algorithm Design (empfohlen für den zweiten Teil, einschließlich Graphenalgorithmen)&lt;br /&gt;
* T. Cormen, C. Leiserson, R.Rivest: Algorithmen - eine Einführung (empfohlen zum Thema Komplexität)&lt;br /&gt;
* Wikipedia und andere Internetseiten (sehr gute Seiten über viele Algorithmen und Datenstrukturen)&lt;br /&gt;
&lt;br /&gt;
=== Gliederung der Vorlesung ===&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Einführung]] (17.4.2012) &lt;br /&gt;
#* Definition von Algorithmen und Datenstrukturen, Geschichte&lt;br /&gt;
#* Fundamentale Algorithmen: create, assign, copy, swap, compare etc.&lt;br /&gt;
#* Fundamentale Datenstrukturen: Zahlen, Container, Handles&lt;br /&gt;
#* Python-Grundlagen&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Container]] (19.4.2012)&lt;br /&gt;
#* Anforderungen von Algorithmen an Container&lt;br /&gt;
#* Einteilung der Container&lt;br /&gt;
#* Grundlegende Container: Array, verkettete Liste, Stack und Queue&lt;br /&gt;
#* Sequenzen und Intervalle (Ranges)&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Sortieren]] (24. und 26.4.2012)&lt;br /&gt;
#* Spezifikation des Sortierproblems&lt;br /&gt;
#* Selection Sort und Insertion Sort&lt;br /&gt;
#* Merge Sort&lt;br /&gt;
#* Quick Sort und seine Varianten&lt;br /&gt;
#* Vergleich der Anzahl der benötigten Schritte&lt;br /&gt;
#* Laufzeitmessung in Python&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Korrektheit]] (3. und 8.5.2012)&lt;br /&gt;
#* Definition von Korrektheit, Algorithmen-Spezifikation&lt;br /&gt;
#* Korrektheitsbeweise versus Testen&lt;br /&gt;
#* Vor- und Nachbedingungen, Invarianten, Programming by contract&lt;br /&gt;
#* Testen, Execution paths, Unit Tests in Python&lt;br /&gt;
#* Ausnahmen (exceptions) und Ausnahmebehandlung in Python&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Effizienz]] (10. und 15.5.2012)&lt;br /&gt;
#* Laufzeit und Optimierung: Innere Schleife, Caches, locality of reference&lt;br /&gt;
#* Laufzeit versus Komplexität&lt;br /&gt;
#* Landausymbole (O-Notation, &amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;-Notation, &amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;-Notation), Komplexitätsklassen&lt;br /&gt;
#* Bester, schlechtester, durchschnittlicher Fall&lt;br /&gt;
#* Amortisierte Komplexität&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Suchen]] (22. und 24.5.2012)&lt;br /&gt;
#* Sequentielle Suche&lt;br /&gt;
#* Binäre Suche in sortierten Arrays, Medianproblem&lt;br /&gt;
#* Suchbäume, balancierte Bäume&lt;br /&gt;
#* selbst-balancierende Bäume, Rotationen&lt;br /&gt;
#* Komplexität der Suche&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Sortieren in linearer Zeit]] (29.5.2012)&lt;br /&gt;
#* Permutationen&lt;br /&gt;
#* Sortieren als Suchproblem&lt;br /&gt;
#* Bucket Prinzip, Bucket Sort&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Prioritätswarteschlangen]] (31.5.2012)&lt;br /&gt;
#* Heap-Datenstruktur&lt;br /&gt;
#* Einfüge- und Löschoperationen&lt;br /&gt;
#* Heapsort&lt;br /&gt;
#* Komplexität des Heaps&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Assoziative Arrays]] (5.6.2012)&lt;br /&gt;
#* Datenstruktur-Dreieck für assoziative Arrays&lt;br /&gt;
#* Definition des abstrakten Datentyps&lt;br /&gt;
#* JSON-Datenformat&lt;br /&gt;
#* Realisierung durch sequentielle Suche und durch Suchbäume&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Hashing und Hashtabellen]] (5.6.und 12.6.2012)&lt;br /&gt;
#* Implementation assoziativer Arrays mit Bäumen&lt;br /&gt;
#* Hashing und Hashfunktionen&lt;br /&gt;
#* Implementation assoziativer Arrays als Hashtabelle mit linearer Verkettung bzw. mit offener Adressierung&lt;br /&gt;
#* Anwendung des Hashing zur String-Suche: Rabin-Karp-Algorithmus&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Iteration versus Rekursion]] (14.6.2012)&lt;br /&gt;
#* Typen der Rekursion und ihre Umwandlung in Iteration&lt;br /&gt;
#* Auflösung rekursiver Formeln mittels Master-Methode und Substitutionsmethode&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Generizität]] (19.6.2012)&lt;br /&gt;
#* Abstrakte Datentypen, Typspezifikation&lt;br /&gt;
#* Required Interface versus Offered Interface&lt;br /&gt;
#* Adapter und Typattribute, Funktoren&lt;br /&gt;
#* Beispiel: Algebraische Konzepte und Zahlendatentypen&lt;br /&gt;
#* Operator overloading in Python&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Graphen und Graphenalgorithmen]] (21.6. bis 5.7.2012)&lt;br /&gt;
#* Einführung&lt;br /&gt;
#* Graphendatenstrukturen, Adjazenzlisten und Adjazenzmatrizen&lt;br /&gt;
#* Gerichtete und ungerichtete Graphen&lt;br /&gt;
#* Vollständige Graphen&lt;br /&gt;
#* Planare Graphen, duale Graphen&lt;br /&gt;
#* Pfade, Zyklen&lt;br /&gt;
#* Tiefensuche und Breitensuche&lt;br /&gt;
#* Zusammenhang, Komponenten&lt;br /&gt;
#* Gewichtete Graphen&lt;br /&gt;
#* Minimaler Spannbaum&lt;br /&gt;
#* Kürzeste Wege, Best-first search (Dijkstra)&lt;br /&gt;
#* Most-Promising-first search (A*)&lt;br /&gt;
#* Problem des Handlungsreisenden, exakte Algorithmen (erschöpfende Suche, Branch-and-Bound-Methode) und Approximationen&lt;br /&gt;
#* Erfüllbarkeitsproblem, Darstellung des 2-SAT-Problems durch gerichtete Graphen, stark zusammenhängende Komponenten&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Repetition---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Orthogonale Zerlegung des Problems---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Hierarchische Zerlegung der Daten (Divide and Conquer)---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Randomisierung---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Optimierung, Zielfunktionen---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Systematisierung von Algorithmen aus der bisherigen Vorlesung---&amp;gt;&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
&amp;lt;!---# [[Analytische Optimierung]] (25.6.2008)---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Methode der kleinsten Quadrate---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Approximation von Geraden---&amp;gt;&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Randomisierte Algorithmen]] (10. und 12.7.2012)&lt;br /&gt;
#* Zufallszahlen, Zyklenlänge, Pitfalls&lt;br /&gt;
#* Zufallszahlengeneratoren: linear congruential generator, Mersenne Twister&lt;br /&gt;
#* Randomisierte vs. deterministische Algorithmen&lt;br /&gt;
#* Las Vegas vs. Monte Carlo Algorithmen&lt;br /&gt;
#* Beispiel für Las Vegas: Randomisiertes Quicksort&lt;br /&gt;
#* Beispiele für Monte Carlo: Randomisierte Lösung des k-SAT Problems &lt;br /&gt;
#* RANSAC-Algorithmus, Erfolgswahrscheinlichkeit, Vergleich mit analytischer Optimierung (Methode der kleinsten Quadrate)&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Greedy-Algorithmen und Dynamische Programmierung]] (17.7.2012)&lt;br /&gt;
#* Prinzipien, Aufwandsreduktion in Entscheidungsbäumen&lt;br /&gt;
#* bereits bekannte Algorithmen: minimale Spannbäume nach Kruskal, kürzeste Wege nach Dijkstra&lt;br /&gt;
#* Beispiel: Interval Scheduling Problem und Weighted Interval Scheduling Problem&lt;br /&gt;
#* Beweis der Optimalität beim Scheduling Problem: &amp;quot;greedy stays ahead&amp;quot;-Prinzip, Directed Acyclic Graph bei dynamischer Programmierung&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[NP-Vollständigkeit]] (19.7.2012)&lt;br /&gt;
#* die Klassen P und NP&lt;br /&gt;
#* NP-Vollständigkeit und Problemreduktion&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# Reserve und/oder Wiederholung (24. und 26.7.2012)&lt;br /&gt;
&lt;br /&gt;
== Übungsaufgaben ==&lt;br /&gt;
&lt;br /&gt;
(im PDF Format). Die Abgabe erfolgt am angegebenen Tag bis 14:00 Uhr per Email an den jeweiligen Übungsgruppenleiter. Bei Abgabe bis zum folgenden Montag 11:00 Uhr werden noch 50% der erreichten Punkte angerechnet. Danach wird die Musterlösung freigeschaltet. Erreichbare Punkte (ohne Bonusaufgaben): 466.&lt;br /&gt;
&lt;br /&gt;
# [[Media:Übung-1.pdf|Übung]] (Abgabe 24.4.2012) und [[Media:Uebung-1-Musterloesung.pdf|Musterlösung]]&lt;br /&gt;
#* Python-Tutorial&lt;br /&gt;
#* Sieb des Eratosthenes&lt;br /&gt;
#* Wert- und Referenzsemantik&lt;br /&gt;
#* Dynamisches Array&lt;br /&gt;
# [[Media:Uebung-2.pdf|Übung]] (Abgabe 3.5.2012) und [[Media:Uebung-2-Musterloesung.pdf|Musterlösung]]&lt;br /&gt;
#* Sortieren: Implementation und Geschwindigkeitsvergleich (Diagramme in Abhängigkeit von der Problemgröße)&lt;br /&gt;
#* Entwicklung eines Gewinnalgorithmus für ein Spiel&lt;br /&gt;
#* Bonus: Dynamisches Array mit verringertem Speicherverbrauch&lt;br /&gt;
# [[Media:Uebung-3.pdf|Übung]] (Abgabe 10.5.2012) und [[Media:Uebung-3-Musterlösung.pdf|Musterlösung]]&lt;br /&gt;
#* Experimente zur Effektivität von Unit Tests&lt;br /&gt;
#* Bestimmung von Pi mit dem Algorithmus von Archimedes&lt;br /&gt;
#* Deque-Datenstruktur: Vor- und Nachbedingungen der Operationen, Implementation und Unit Tests&lt;br /&gt;
# [[Media:Uebung-4.pdf|Übung]] (Abgabe '''Montag''' 21.5.2012) und [[Media:muster_blatt4.pdf|Musterlösung]]&lt;br /&gt;
#* Theoretische Aufgaben zur Komplexität&lt;br /&gt;
#* Amortisierte Komplexität von array.append()&lt;br /&gt;
#* Optimierung der Matrizenmultiplikation&lt;br /&gt;
# [[Media:Uebung-5.pdf|Übung]] (31.5.2012) und [[Media:muster_blatt5.pdf|Musterlösung]]&lt;br /&gt;
#* Implementation und Analyse eines Binärbaumes&lt;br /&gt;
#* Anwendung: einfacher Taschenrechner&lt;br /&gt;
# [[Media:Uebung-6.pdf|Übung]] (Abgabe '''Freitag''' 8.6.2012) und [[Media:muster_blatt6.pdf|Musterlösung]]&lt;br /&gt;
#* Treap-Datenstruktur: Verbindung von Suchbaum und Heap&lt;br /&gt;
#* Anwendung: Worthäufigkeiten (Dazu benötigen Sie das File  [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/die-drei-musketiere.txt die-drei-musketiere.txt]. Die Zeichenkodierung in diesem File ist Latin-1.)&lt;br /&gt;
#* BucketSort&lt;br /&gt;
# [[Media:Uebung-7.pdf|Übung]] (Abgabe 14.6.2012) und [[Media:muster_blatt07.pdf|Musterlösung]]&lt;br /&gt;
#* Absichtliche Konstruktion von Kollisionen für eine Hashfunktion&lt;br /&gt;
#* Übungen zum Assoziativen Array und zum JSON-Format: Cocktail-Datenbank (Dazu benötigen Sie das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/cocktails.json cocktails.json]. Die Zeichenkodierung in diesem File ist UTF-8.)&lt;br /&gt;
# [[Media:Uebung-8.pdf|Übung]] (Abgabe 21.6.2012) und [[Media:muster_blatt8.pdf|Musterlösung]]&lt;br /&gt;
#* Übungen zu Rekursion und Iteration: Fibonaccizahlen, Koch-Schneeflocke, Komplexität rekursiver Algorithmen, Umwandlung von Rekursion in Iteration&lt;br /&gt;
# [[Media:Uebung-9.pdf|Übung]] (Abgabe 28.6.2012) und [[Media:muster_blatt9.pdf|Musterlösung]]&lt;br /&gt;
#* Planare Graphen: Aufstellen von Adjazenzmatrizen und Adjazenzlisten, obere Schranke für die Zahl der Kanten&lt;br /&gt;
#* Übungen zur Generizität: Sortieren mit veränderter Ordnung, Iterator für Tiefensuche&lt;br /&gt;
# [[Media:Uebung-10.pdf|Übung]] (Abgabe 5.7.2012) und [[Media:muster_blatt10.pdf|Musterlösung]]&lt;br /&gt;
#* Fortgeschrittene Graphenaufgaben: Erzeugen einer perfekten Hashfunktion, Routenplaner (Dazu benötigen Sie das File  [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/entfernungen.json entfernungen.json]. Die Zeichenkodierung in diesem File ist UTF-8.)&lt;br /&gt;
# [[Media:Uebung-11.pdf|Übung]] (Abgabe 12.7.2012) und [[Media:muster_blatt11.pdf|Musterlösung]] sowie schöne [[Media:ballungsgebiete.pdf|Visualisierung der Ballungsgebiete]] von Thorben Kröger&lt;br /&gt;
#* Fortgeschrittene Graphenaufgaben 2: Clusterung mittels minimaler Spannbäume, Bildverarbeitung mit Graphen (Dazu benötigen Sie wieder das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/entfernungen.json entfernungen.json] sowie die Files [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/cells.pgm cells.pgm] und [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/pgm.py pgm.py].)&lt;br /&gt;
# [[Media:Uebung-12.pdf|Übung]] (Abgabe 19.7.2012) und [[Media:muster_blatt12.pdf|Musterlösung]]&lt;br /&gt;
#* Erfüllbarkeitsproblem, Anwendung: Heim- und Auswärtsspiele im Fussball (Dazu benötigen sie das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/bundesliga-paarungen-12-13.json bundesliga-paarungen-12-13.json].)&lt;br /&gt;
#* Randomisierte Algorithmen: RANSAC für Kreise (Dazu benötigen sie das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/noisy-circles.txt noisy-circles.txt].)&lt;br /&gt;
# [[Media:Bonusuebung.pdf|Übung (Bonus)]] (&amp;lt;font color=red&amp;gt;Achtung: Abgabe bereits am Dienstag, 24.7.2012&amp;lt;/font&amp;gt;)&lt;br /&gt;
#* Greedy-Algorithmus&lt;br /&gt;
#* Weg durch einen Graphen&lt;br /&gt;
#* Wiederholungsaufgaben für die Klausur&lt;br /&gt;
&lt;br /&gt;
== Sonstiges ==&lt;br /&gt;
* [[Gnuplot| Gnuplot Kurztutorial]]&lt;br /&gt;
* [[Git Kurztutorial]]&lt;br /&gt;
* [[neue Startseite|mögliche neue Startseite]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=File:2012-Klausur-2.pdf&amp;diff=5452</id>
		<title>File:2012-Klausur-2.pdf</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=File:2012-Klausur-2.pdf&amp;diff=5452"/>
				<updated>2012-10-04T16:02:49Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=File:2012-Klausur-1.pdf&amp;diff=5451</id>
		<title>File:2012-Klausur-1.pdf</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=File:2012-Klausur-1.pdf&amp;diff=5451"/>
				<updated>2012-08-08T11:01:47Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: uploaded a new version of &amp;quot;File:2012-Klausur-1.pdf&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=File:2012-Klausur-1.pdf&amp;diff=5450</id>
		<title>File:2012-Klausur-1.pdf</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=File:2012-Klausur-1.pdf&amp;diff=5450"/>
				<updated>2012-08-06T12:53:34Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: uploaded a new version of &amp;quot;File:2012-Klausur-1.pdf&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=File:2012-Klausur-1.pdf&amp;diff=5449</id>
		<title>File:2012-Klausur-1.pdf</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=File:2012-Klausur-1.pdf&amp;diff=5449"/>
				<updated>2012-08-01T14:18:22Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: uploaded a new version of &amp;quot;File:2012-Klausur-1.pdf&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=File:2012-Klausur-1.pdf&amp;diff=5448</id>
		<title>File:2012-Klausur-1.pdf</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=File:2012-Klausur-1.pdf&amp;diff=5448"/>
				<updated>2012-08-01T13:23:36Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: uploaded a new version of &amp;quot;File:2012-Klausur-1.pdf&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Main_Page&amp;diff=5447</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Main_Page&amp;diff=5447"/>
				<updated>2012-08-01T11:29:21Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Vorlesung Algorithmen und Datenstrukturen */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Vorlesung Algorithmen und Datenstrukturen ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Dr. Ullrich Köthe, Universität Heidelberg, Sommersemester 2012&lt;br /&gt;
&lt;br /&gt;
Die Vorlesung findet '''dienstags''' und '''donnerstags''' jeweils um 14:15 Uhr in INF 227 (KIP), HS 2 statt. &lt;br /&gt;
&lt;br /&gt;
=== Klausur und Nachprüfung ===&lt;br /&gt;
&lt;br /&gt;
Die '''Abschlussklausur''' findet am Dienstag, dem 31.7.2012 von 10:00 bis 12:00 Uhr im HS 1 in INF 306 statt. Zur Klausur wird zugelassen, wer mindestens 50% der Übungspunkte erreicht. (Hinweis: Sie benötigen einen Lichtbildausweis, um sich bei der Klausur zu indentifizieren!) Falls notwendig, wird eine Nachklausur kurz vor Beginn des neuen Semesters stattfinden, näheres wird noch bekanntgegeben.&lt;br /&gt;
* '''[[Media:2012-Klausur-1.pdf|Ergebnis der Klausur vom 31.7.2012]]''' (anonymisiert)&lt;br /&gt;
&amp;lt;!---&lt;br /&gt;
* '''[[Media:Prüfungsteilnehmer.pdf|Liste der Studenten]], die sich verbindlich zur Klausur angemeldet und die notwendige Übungspunktzahl erreicht haben.'''&lt;br /&gt;
* '''Scheine''' können ab 1.9.2008 im Sekretariat Informatik bei Frau Tenschert abgeholt werden.&lt;br /&gt;
* Die '''Wiederholungsklausur''' findet am 1.10.2008 um 9:00 Uhr im Seminarraum des [http://hci.iwr.uni-heidelberg.de/contact.php HCI, Speyerer Str. 4], statt.&lt;br /&gt;
* '''[[Media:Ergebnis-Klausur-01-10-2008.pdf|Ergebnis der Wiederholungsklausur vom 1.10.2008]]''' (anonymisiert)&lt;br /&gt;
---&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Leistungsnachweise ===&lt;br /&gt;
Für alle Leistungsnachweise ist die erfolgreiche Teilnahme an den Übungen erforderlich. Für Leistungspunkte bzw. den Klausurschein muss außerdem die schriftliche Prüfung bestanden werden. Einzelheiten werden noch bekanntgegeben.&lt;br /&gt;
&amp;lt;!-------&lt;br /&gt;
Im einzelnen können erworben werden:&lt;br /&gt;
* ein unbenoteter Übungsschein, falls jemand nicht an der Klausur teilnimmt bzw. die Klausur nicht bestanden wurde.&lt;br /&gt;
* ein benoteter Übungsschein (Magister mit Computerlinguistik im ''Nebenfach'', Physik Diplom)&lt;br /&gt;
* ein Klausurschein (Magister mit Computerlinguistik im ''Hauptfach'')&lt;br /&gt;
* ein Leistungsnachweis über 9 Leistungspunkte (B.A. Computerlinguistik - alte Studienordnung) &lt;br /&gt;
* ein Leistungsnachweis über 8 Leistungspunkte (B.Sc. Informatik, B.A. Computerlinguistik - neue Studienordnung) &lt;br /&gt;
* ein Leistungsnachweis über 7 Leistungspunkte (B.Sc. Physik).&lt;br /&gt;
---------&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Übungsbetrieb ===&lt;br /&gt;
* Termine und Räume: &lt;br /&gt;
** Mo 14:00 - 16:00 Uhr, INF 227 (KIP), Seminarraum 2.402 (Tutor: Sven Ebser [mailto:sven@ebsers.de sven AT ebsers.de])&lt;br /&gt;
** Di  9:00 - 11:00 Uhr, INF 227 (KIP), Seminarraum 2.403 (Tutor: Christoph Koke [mailto:koke@kip.uni-heidelberg.de koke AT kip.uni-heidelberg.de])&lt;br /&gt;
** Di 11:00 - 13:00 Uhr, INF 227 (KIP), Seminarraum 2.403 (Tutor: Kai Karius [mailto:kai.karius@googlemail.com kai.karius AT googlemail.com])&lt;br /&gt;
** Mi 14:00 - 16:00 Uhr, INF 227 (KIP), Seminarraum 2.401 (Tutor: Stephan Meister [mailto:stephan.meister@iwr.uni-heidelberg.de stephan.meister AT iwr.uni-heidelberg.de])  &lt;br /&gt;
* Die Übungsgruppen werden über &amp;lt;b&amp;gt;[https://www.mathi.uni-heidelberg.de/muesli/lecture/view/169 MÜSLI]&amp;lt;/b&amp;gt; verwaltet. Dort erfolgt auch die &amp;lt;b&amp;gt;Anmeldung&amp;lt;/b&amp;gt;.&lt;br /&gt;
&amp;lt;!------&lt;br /&gt;
* [[Media:Punktestand.pdf|aktueller Punktestand]] (PDF, anonymisiert, so aktuell, wie von den Tutoren an mich übermittelt -- UK)&lt;br /&gt;
-------&amp;gt;&lt;br /&gt;
* &amp;lt;b&amp;gt;[[Main Page#Übungsaufgaben|Übungsaufgaben]]&amp;lt;/b&amp;gt; (Übungszettel mit Abgabetermin, Musterlösungen). Lösungen bitte per Email an den jeweiligen Übungsgruppenleiter.&lt;br /&gt;
* Zur Klausur wird zugelassen, wer mindestens 50% der Übungspunkte erreicht. Außerdem muss jeder Teilnehmer eine Lösung (bzw. einen Teil davon) in der Übungsgruppe vorrechnen. &lt;br /&gt;
* Durch das Lösen von Bonusaufgaben und gute Mitarbeit in den Übungen können Sie Zusatzpunkte erlangen. Zusatzpunkte werden auch vergeben, wenn Sie größere Verbesserungen an diesem Wiki vornehmen. Damit solche Verbesserungen der richtigen Person zugeordnet werden, sollten Sie dafür ein eigenes Wiki-Login verwenden, das Ihnen Stephan Meister oder Ullrich Köthe auf Anfrage gerne einrichten.&lt;br /&gt;
&lt;br /&gt;
=== Prüfungsvorbereitung ===&lt;br /&gt;
&lt;br /&gt;
Zur Hilfe bei der Prüfungsvorbereitung hat Andreas Fay [http://de.neemoy.com/quizcategories/31/ Quizfragen] erstellt.&lt;br /&gt;
&lt;br /&gt;
=== Literatur ===&lt;br /&gt;
&lt;br /&gt;
* R. Sedgewick: Algorithmen (empfohlen für den ersten Teil, bis einschließlich Graphenalgorithmen)&lt;br /&gt;
* J. Kleinberg, E.Tardos: Algorithm Design (empfohlen für den zweiten Teil, einschließlich Graphenalgorithmen)&lt;br /&gt;
* T. Cormen, C. Leiserson, R.Rivest: Algorithmen - eine Einführung (empfohlen zum Thema Komplexität)&lt;br /&gt;
* Wikipedia und andere Internetseiten (sehr gute Seiten über viele Algorithmen und Datenstrukturen)&lt;br /&gt;
&lt;br /&gt;
=== Gliederung der Vorlesung ===&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Einführung]] (17.4.2012) &lt;br /&gt;
#* Definition von Algorithmen und Datenstrukturen, Geschichte&lt;br /&gt;
#* Fundamentale Algorithmen: create, assign, copy, swap, compare etc.&lt;br /&gt;
#* Fundamentale Datenstrukturen: Zahlen, Container, Handles&lt;br /&gt;
#* Python-Grundlagen&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Container]] (19.4.2012)&lt;br /&gt;
#* Anforderungen von Algorithmen an Container&lt;br /&gt;
#* Einteilung der Container&lt;br /&gt;
#* Grundlegende Container: Array, verkettete Liste, Stack und Queue&lt;br /&gt;
#* Sequenzen und Intervalle (Ranges)&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Sortieren]] (24. und 26.4.2012)&lt;br /&gt;
#* Spezifikation des Sortierproblems&lt;br /&gt;
#* Selection Sort und Insertion Sort&lt;br /&gt;
#* Merge Sort&lt;br /&gt;
#* Quick Sort und seine Varianten&lt;br /&gt;
#* Vergleich der Anzahl der benötigten Schritte&lt;br /&gt;
#* Laufzeitmessung in Python&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Korrektheit]] (3. und 8.5.2012)&lt;br /&gt;
#* Definition von Korrektheit, Algorithmen-Spezifikation&lt;br /&gt;
#* Korrektheitsbeweise versus Testen&lt;br /&gt;
#* Vor- und Nachbedingungen, Invarianten, Programming by contract&lt;br /&gt;
#* Testen, Execution paths, Unit Tests in Python&lt;br /&gt;
#* Ausnahmen (exceptions) und Ausnahmebehandlung in Python&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Effizienz]] (10. und 15.5.2012)&lt;br /&gt;
#* Laufzeit und Optimierung: Innere Schleife, Caches, locality of reference&lt;br /&gt;
#* Laufzeit versus Komplexität&lt;br /&gt;
#* Landausymbole (O-Notation, &amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;-Notation, &amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;-Notation), Komplexitätsklassen&lt;br /&gt;
#* Bester, schlechtester, durchschnittlicher Fall&lt;br /&gt;
#* Amortisierte Komplexität&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Suchen]] (22. und 24.5.2012)&lt;br /&gt;
#* Sequentielle Suche&lt;br /&gt;
#* Binäre Suche in sortierten Arrays, Medianproblem&lt;br /&gt;
#* Suchbäume, balancierte Bäume&lt;br /&gt;
#* selbst-balancierende Bäume, Rotationen&lt;br /&gt;
#* Komplexität der Suche&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Sortieren in linearer Zeit]] (29.5.2012)&lt;br /&gt;
#* Permutationen&lt;br /&gt;
#* Sortieren als Suchproblem&lt;br /&gt;
#* Bucket Prinzip, Bucket Sort&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Prioritätswarteschlangen]] (31.5.2012)&lt;br /&gt;
#* Heap-Datenstruktur&lt;br /&gt;
#* Einfüge- und Löschoperationen&lt;br /&gt;
#* Heapsort&lt;br /&gt;
#* Komplexität des Heaps&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Assoziative Arrays]] (5.6.2012)&lt;br /&gt;
#* Datenstruktur-Dreieck für assoziative Arrays&lt;br /&gt;
#* Definition des abstrakten Datentyps&lt;br /&gt;
#* JSON-Datenformat&lt;br /&gt;
#* Realisierung durch sequentielle Suche und durch Suchbäume&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Hashing und Hashtabellen]] (5.6.und 12.6.2012)&lt;br /&gt;
#* Implementation assoziativer Arrays mit Bäumen&lt;br /&gt;
#* Hashing und Hashfunktionen&lt;br /&gt;
#* Implementation assoziativer Arrays als Hashtabelle mit linearer Verkettung bzw. mit offener Adressierung&lt;br /&gt;
#* Anwendung des Hashing zur String-Suche: Rabin-Karp-Algorithmus&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Iteration versus Rekursion]] (14.6.2012)&lt;br /&gt;
#* Typen der Rekursion und ihre Umwandlung in Iteration&lt;br /&gt;
#* Auflösung rekursiver Formeln mittels Master-Methode und Substitutionsmethode&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Generizität]] (19.6.2012)&lt;br /&gt;
#* Abstrakte Datentypen, Typspezifikation&lt;br /&gt;
#* Required Interface versus Offered Interface&lt;br /&gt;
#* Adapter und Typattribute, Funktoren&lt;br /&gt;
#* Beispiel: Algebraische Konzepte und Zahlendatentypen&lt;br /&gt;
#* Operator overloading in Python&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Graphen und Graphenalgorithmen]] (21.6. bis 5.7.2012)&lt;br /&gt;
#* Einführung&lt;br /&gt;
#* Graphendatenstrukturen, Adjazenzlisten und Adjazenzmatrizen&lt;br /&gt;
#* Gerichtete und ungerichtete Graphen&lt;br /&gt;
#* Vollständige Graphen&lt;br /&gt;
#* Planare Graphen, duale Graphen&lt;br /&gt;
#* Pfade, Zyklen&lt;br /&gt;
#* Tiefensuche und Breitensuche&lt;br /&gt;
#* Zusammenhang, Komponenten&lt;br /&gt;
#* Gewichtete Graphen&lt;br /&gt;
#* Minimaler Spannbaum&lt;br /&gt;
#* Kürzeste Wege, Best-first search (Dijkstra)&lt;br /&gt;
#* Most-Promising-first search (A*)&lt;br /&gt;
#* Problem des Handlungsreisenden, exakte Algorithmen (erschöpfende Suche, Branch-and-Bound-Methode) und Approximationen&lt;br /&gt;
#* Erfüllbarkeitsproblem, Darstellung des 2-SAT-Problems durch gerichtete Graphen, stark zusammenhängende Komponenten&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Repetition---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Orthogonale Zerlegung des Problems---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Hierarchische Zerlegung der Daten (Divide and Conquer)---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Randomisierung---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Optimierung, Zielfunktionen---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Systematisierung von Algorithmen aus der bisherigen Vorlesung---&amp;gt;&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
&amp;lt;!---# [[Analytische Optimierung]] (25.6.2008)---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Methode der kleinsten Quadrate---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Approximation von Geraden---&amp;gt;&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Randomisierte Algorithmen]] (10. und 12.7.2012)&lt;br /&gt;
#* Zufallszahlen, Zyklenlänge, Pitfalls&lt;br /&gt;
#* Zufallszahlengeneratoren: linear congruential generator, Mersenne Twister&lt;br /&gt;
#* Randomisierte vs. deterministische Algorithmen&lt;br /&gt;
#* Las Vegas vs. Monte Carlo Algorithmen&lt;br /&gt;
#* Beispiel für Las Vegas: Randomisiertes Quicksort&lt;br /&gt;
#* Beispiele für Monte Carlo: Randomisierte Lösung des k-SAT Problems &lt;br /&gt;
#* RANSAC-Algorithmus, Erfolgswahrscheinlichkeit, Vergleich mit analytischer Optimierung (Methode der kleinsten Quadrate)&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Greedy-Algorithmen und Dynamische Programmierung]] (17.7.2012)&lt;br /&gt;
#* Prinzipien, Aufwandsreduktion in Entscheidungsbäumen&lt;br /&gt;
#* bereits bekannte Algorithmen: minimale Spannbäume nach Kruskal, kürzeste Wege nach Dijkstra&lt;br /&gt;
#* Beispiel: Interval Scheduling Problem und Weighted Interval Scheduling Problem&lt;br /&gt;
#* Beweis der Optimalität beim Scheduling Problem: &amp;quot;greedy stays ahead&amp;quot;-Prinzip, Directed Acyclic Graph bei dynamischer Programmierung&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[NP-Vollständigkeit]] (19.7.2012)&lt;br /&gt;
#* die Klassen P und NP&lt;br /&gt;
#* NP-Vollständigkeit und Problemreduktion&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# Reserve und/oder Wiederholung (24. und 26.7.2012)&lt;br /&gt;
&lt;br /&gt;
== Übungsaufgaben ==&lt;br /&gt;
&lt;br /&gt;
(im PDF Format). Die Abgabe erfolgt am angegebenen Tag bis 14:00 Uhr per Email an den jeweiligen Übungsgruppenleiter. Bei Abgabe bis zum folgenden Montag 11:00 Uhr werden noch 50% der erreichten Punkte angerechnet. Danach wird die Musterlösung freigeschaltet. Erreichbare Punkte (ohne Bonusaufgaben): 466.&lt;br /&gt;
&lt;br /&gt;
# [[Media:Übung-1.pdf|Übung]] (Abgabe 24.4.2012) und [[Media:Uebung-1-Musterloesung.pdf|Musterlösung]]&lt;br /&gt;
#* Python-Tutorial&lt;br /&gt;
#* Sieb des Eratosthenes&lt;br /&gt;
#* Wert- und Referenzsemantik&lt;br /&gt;
#* Dynamisches Array&lt;br /&gt;
# [[Media:Uebung-2.pdf|Übung]] (Abgabe 3.5.2012) und [[Media:Uebung-2-Musterloesung.pdf|Musterlösung]]&lt;br /&gt;
#* Sortieren: Implementation und Geschwindigkeitsvergleich (Diagramme in Abhängigkeit von der Problemgröße)&lt;br /&gt;
#* Entwicklung eines Gewinnalgorithmus für ein Spiel&lt;br /&gt;
#* Bonus: Dynamisches Array mit verringertem Speicherverbrauch&lt;br /&gt;
# [[Media:Uebung-3.pdf|Übung]] (Abgabe 10.5.2012) und [[Media:Uebung-3-Musterlösung.pdf|Musterlösung]]&lt;br /&gt;
#* Experimente zur Effektivität von Unit Tests&lt;br /&gt;
#* Bestimmung von Pi mit dem Algorithmus von Archimedes&lt;br /&gt;
#* Deque-Datenstruktur: Vor- und Nachbedingungen der Operationen, Implementation und Unit Tests&lt;br /&gt;
# [[Media:Uebung-4.pdf|Übung]] (Abgabe '''Montag''' 21.5.2012) und [[Media:muster_blatt4.pdf|Musterlösung]]&lt;br /&gt;
#* Theoretische Aufgaben zur Komplexität&lt;br /&gt;
#* Amortisierte Komplexität von array.append()&lt;br /&gt;
#* Optimierung der Matrizenmultiplikation&lt;br /&gt;
# [[Media:Uebung-5.pdf|Übung]] (31.5.2012) und [[Media:muster_blatt5.pdf|Musterlösung]]&lt;br /&gt;
#* Implementation und Analyse eines Binärbaumes&lt;br /&gt;
#* Anwendung: einfacher Taschenrechner&lt;br /&gt;
# [[Media:Uebung-6.pdf|Übung]] (Abgabe '''Freitag''' 8.6.2012) und [[Media:muster_blatt6.pdf|Musterlösung]]&lt;br /&gt;
#* Treap-Datenstruktur: Verbindung von Suchbaum und Heap&lt;br /&gt;
#* Anwendung: Worthäufigkeiten (Dazu benötigen Sie das File  [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/die-drei-musketiere.txt die-drei-musketiere.txt]. Die Zeichenkodierung in diesem File ist Latin-1.)&lt;br /&gt;
#* BucketSort&lt;br /&gt;
# [[Media:Uebung-7.pdf|Übung]] (Abgabe 14.6.2012) und [[Media:muster_blatt07.pdf|Musterlösung]]&lt;br /&gt;
#* Absichtliche Konstruktion von Kollisionen für eine Hashfunktion&lt;br /&gt;
#* Übungen zum Assoziativen Array und zum JSON-Format: Cocktail-Datenbank (Dazu benötigen Sie das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/cocktails.json cocktails.json]. Die Zeichenkodierung in diesem File ist UTF-8.)&lt;br /&gt;
# [[Media:Uebung-8.pdf|Übung]] (Abgabe 21.6.2012) und [[Media:muster_blatt8.pdf|Musterlösung]]&lt;br /&gt;
#* Übungen zu Rekursion und Iteration: Fibonaccizahlen, Koch-Schneeflocke, Komplexität rekursiver Algorithmen, Umwandlung von Rekursion in Iteration&lt;br /&gt;
# [[Media:Uebung-9.pdf|Übung]] (Abgabe 28.6.2012) und [[Media:muster_blatt9.pdf|Musterlösung]]&lt;br /&gt;
#* Planare Graphen: Aufstellen von Adjazenzmatrizen und Adjazenzlisten, obere Schranke für die Zahl der Kanten&lt;br /&gt;
#* Übungen zur Generizität: Sortieren mit veränderter Ordnung, Iterator für Tiefensuche&lt;br /&gt;
# [[Media:Uebung-10.pdf|Übung]] (Abgabe 5.7.2012) und [[Media:muster_blatt10.pdf|Musterlösung]]&lt;br /&gt;
#* Fortgeschrittene Graphenaufgaben: Erzeugen einer perfekten Hashfunktion, Routenplaner (Dazu benötigen Sie das File  [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/entfernungen.json entfernungen.json]. Die Zeichenkodierung in diesem File ist UTF-8.)&lt;br /&gt;
# [[Media:Uebung-11.pdf|Übung]] (Abgabe 12.7.2012) und [[Media:muster_blatt11.pdf|Musterlösung]] sowie schöne [[Media:ballungsgebiete.pdf|Visualisierung der Ballungsgebiete]] von Thorben Kröger&lt;br /&gt;
#* Fortgeschrittene Graphenaufgaben 2: Clusterung mittels minimaler Spannbäume, Bildverarbeitung mit Graphen (Dazu benötigen Sie wieder das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/entfernungen.json entfernungen.json] sowie die Files [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/cells.pgm cells.pgm] und [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/pgm.py pgm.py].)&lt;br /&gt;
# [[Media:Uebung-12.pdf|Übung]] (Abgabe 19.7.2012) und [[Media:muster_blatt12.pdf|Musterlösung]]&lt;br /&gt;
#* Erfüllbarkeitsproblem, Anwendung: Heim- und Auswärtsspiele im Fussball (Dazu benötigen sie das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/bundesliga-paarungen-12-13.json bundesliga-paarungen-12-13.json].)&lt;br /&gt;
#* Randomisierte Algorithmen: RANSAC für Kreise (Dazu benötigen sie das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/noisy-circles.txt noisy-circles.txt].)&lt;br /&gt;
# [[Media:Bonusuebung.pdf|Übung (Bonus)]] (&amp;lt;font color=red&amp;gt;Achtung: Abgabe bereits am Dienstag, 24.7.2012&amp;lt;/font&amp;gt;)&lt;br /&gt;
#* Greedy-Algorithmus&lt;br /&gt;
#* Weg durch einen Graphen&lt;br /&gt;
#* Wiederholungsaufgaben für die Klausur&lt;br /&gt;
&lt;br /&gt;
== Sonstiges ==&lt;br /&gt;
* [[Gnuplot| Gnuplot Kurztutorial]]&lt;br /&gt;
* [[Git Kurztutorial]]&lt;br /&gt;
* [[neue Startseite|mögliche neue Startseite]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=File:2012-Klausur-1.pdf&amp;diff=5446</id>
		<title>File:2012-Klausur-1.pdf</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=File:2012-Klausur-1.pdf&amp;diff=5446"/>
				<updated>2012-08-01T11:28:48Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Suchen&amp;diff=5445</id>
		<title>Suchen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Suchen&amp;diff=5445"/>
				<updated>2012-07-31T14:01:54Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Suchbäume */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Das Suchen ist eine grundlegende Operation in der Informatik. Viele Probleme in der Informatik können auf Suchaufgaben zurückgeführt werden.&lt;br /&gt;
&lt;br /&gt;
Gemeint ist mit Suchen das Wiederauffinden eines Datensatzes aus einer Menge von früher gespeicherten Datensätzen, oder das Auffinden einer bestimmten Lösung in einem (potentiell großen) Suchraum möglicher Lösungen. Ein paar einleitende Worte zum Suchproblem findet man [http://de.wikipedia.org/wiki/Suche hier].&lt;br /&gt;
&lt;br /&gt;
== Überblick über verschiedene Suchmethoden ==&lt;br /&gt;
&lt;br /&gt;
Um sich der Vielseitigkeit des Suchproblems bewusst zu werden, ist es sinnvoll, sich einen Überblick über verschiedene Suchmethoden zu verschaffen. &lt;br /&gt;
&lt;br /&gt;
Hier sei auch auf einen bereits existierenden Wikipedia-Artikel zu [http://de.wikipedia.org/wiki/Suchverfahren Suchverfahren] verwiesen.&lt;br /&gt;
&lt;br /&gt;
Allen gemeinsam ist die grundlegende Aufgabe, ein Datenelement mit bestimmten Eigenschaften aus einer großen Menge von Datenelementen zu selektieren.&lt;br /&gt;
Dies kann, natürlich ohne jeden Anspruch auf Vollständigkeit, nach einer der jetzt diskutierten Methoden geschehen:&lt;br /&gt;
&lt;br /&gt;
* '''Schlüsselsuche''': meint das Suchen von Elementen mit bestimmtem Schlüssel; ein klassisches Beispiel wäre das Suchen in einem Wörterbuch,  die Schlüssel entsprechen hier den Wörtern, die Datensätze wären die zu den Wörtern gehörigen Eintragungen.&lt;br /&gt;
&lt;br /&gt;
* '''Bereichssuche''': Im Allgemeinen meint die Bereichssuche in n-Dimensionen die Selektion von Elementen mit Eigenschaften aus einem bestimmten n-dimensionalen Volumen. Im eindimensionalen Fall will man alle Elemente finden, deren Eigenschaft(en) in einem bestimmten Intervall liegen. Die Verallgemeinerung auf n-Dimensionen ist offensichtlich. Ein Beispiel für die Bereichssuche in einer 3D-Kugel wäre ein Handy mit Geolokalisierung, welches alle Restaurants in einem Umkreis von 500m findet. Lineare Ungleichungen werden graphisch durch [http://de.wikipedia.org/wiki/Hyperebene Hyperebenen] repräsentiert. In 2D sind diese Hyperebenen Geraden. Die Ungleichungen können dann den Lösungsraum in irgendeiner Form begrenzen.&lt;br /&gt;
&lt;br /&gt;
* '''Ähnlichkeitssuche''': Finde Elemente, die gegebenen Eigenschaften möglichst ähnlich sind. Ein prominentes Beispiel ist Google (=Ähnlichkeit zwischen Suchbegriffen und Dokumenten) oder das Suchen des nächstengelegenen Restaurants (Ähnlichkeit zwischen eigener Position und Position des Restaurants). Ein wichtiger Spezialfall ist die ''nächste-nachbar Suche''.&lt;br /&gt;
&lt;br /&gt;
* '''Graphensuche''': Hier wäre beispielsweise das Problem optimaler Wege zu nennen (Navigationssuche). Dieser Punkt wird später im Verlauf der Vorlesung noch einmal aufgegriffen werden.&lt;br /&gt;
&lt;br /&gt;
Im jetzt folgenden wird nur noch die ''Schlüsselsuche'' betrachtet werden.&lt;br /&gt;
&lt;br /&gt;
==Sequentielle Suche==&lt;br /&gt;
&lt;br /&gt;
Die ''sequentielle'' oder ''lineare'' Suche ist die einfachste Methode, einen Datensatz zu durchsuchen. Hierbei wird ein Array beispielsweise sequentiell von vorne nach hinten durchsucht. Ein prinzipieller Vorteil der Methode ist, dass auf der Eigenschaft der Datenelemente, nach denen das Array durchsucht wird, keine Ordnung im Sinne von &amp;gt; oder &amp;lt; definiert zu sein braucht, lediglich die Identität (==) muss feststellbar sein. Der folgende Python-Code zeigt, wie man sequentielle Suche einsetzen kann:&lt;br /&gt;
&lt;br /&gt;
 a = ... # array mit den zu durchsuchenden Elementen&lt;br /&gt;
 &lt;br /&gt;
 foundIndex = sequentialSearch(a, key) &lt;br /&gt;
 # foundIndex == -1 wenn nichts gefunden, 0 &amp;lt;math&amp;gt;\leq &amp;lt;/math&amp;gt; foundIndex &amp;lt; len(a) wenn key gefunden (erster Eintrag mit diesem Wert)&lt;br /&gt;
&lt;br /&gt;
Wir verwenden hier die Konvention, dass der zugehörige Arrayindex zurückgegeben wird, falls ein Element mit dem Schlüssel &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; gefunden wird (falls es mehrere solche Elemente gibt, wird das erste zurückgegeben). Das Ergebnis &amp;lt;tt&amp;gt;-1&amp;lt;/tt&amp;gt; signalisiert hingegen, dass kein solches Element gefunden wurde. Die Funktion &amp;lt;tt&amp;gt;sequentialSearch&amp;lt;/tt&amp;gt; kann folgendermaßen implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def sequentialSearch(a, key):&lt;br /&gt;
    for i in range(len(a)):&lt;br /&gt;
        if a[i] == key:  # bzw. allgemeiner a[i].key == key &lt;br /&gt;
            return i&lt;br /&gt;
    return -1&lt;br /&gt;
&lt;br /&gt;
Wir wollen jetzt die Komplexität dieses Algorithmus bestimmen, wobei die Problemgröße durch &amp;lt;tt&amp;gt;N = len(a)&amp;lt;/tt&amp;gt; gegeben ist. &lt;br /&gt;
&lt;br /&gt;
Dabei nimmt man an, dass der Vergleich in der inneren Schleife (&amp;lt;tt&amp;gt;a[i] == key&amp;lt;/tt&amp;gt;) jeweils &amp;lt;math&amp;gt; \mathcal{O}(1)&amp;lt;/math&amp;gt; ist (diese Annahme könnte verletzt sein, wenn der Vergleichsoperator eine komplizierte Berechnung mit höherer Komplexität ausführen muss). Bei einer erfolglosen Suche wird dieser Vergleich in der for-Schleife N-mal durchgeführt (&amp;lt;math&amp;gt; \mathcal{O}(N)&amp;lt;/math&amp;gt;), bei einer erfolgreichen Suche im Mittel (N/2)-mal (ebenfalls &amp;lt;math&amp;gt; \mathcal{O}(N)&amp;lt;/math&amp;gt;). Nach der Verschachtelungsregel erhält man also eine gesamte Komplexität von &amp;lt;math&amp;gt; \mathcal{O}(N)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Der Name ''lineare'' Suche rührt von diesem linearen Anwachsen der Komplexität mit der Arraygröße her.&lt;br /&gt;
&lt;br /&gt;
==Binäre Suche==&lt;br /&gt;
&lt;br /&gt;
Wie wir weiter unten zeigen werden, gestattet es diese Suchmethode, die Gesamtdauer der Suche in großen Datensätzen beträchtlich zu verringern. Die Methode beruht auf dem [http://de.wikipedia.org/wiki/Divide_and_Conquer Divide and Conquer-Prinzip], wobei die Suche in jedem Schritt rekursiv auf eine Hälfte des Datensatzes eingeschränkt wird. Weitere Details zur Methode sind [http://de.wikipedia.org/wiki/Bin%C3%A4re_Suche hier] zu finden. &lt;br /&gt;
&lt;br /&gt;
Die Methode ist nur dann anwendbar beziehungsweise effektiv, wenn folgendes gilt:&lt;br /&gt;
&lt;br /&gt;
# Auf der Eigenschaft der Daten, die zur Suche verwendet wird, ist eine Ordnung im Sinne von &amp;lt; oder &amp;gt; definiert.&lt;br /&gt;
# Wir wollen uns auf Datensätze beschränken, die schon fertig aufgebaut sind, in die also keine neuen Elemente mehr eingefügt werden, wenn man mit dem Suchen beginnt. Ist dies nicht der Fall, müsste nach jeder Einfügung das Array neu sortiert werden (unter diesen Umständen wäre die Verwendung eines [[Suchen#Suchb.C3.A4ume|Suchbaumes]] geschickter). &lt;br /&gt;
&lt;br /&gt;
Im Unterschied zur sequenziellen Suche müssen wir jetzt das Array sortieren bevor die Suchfunktion aufgerufen werden kann:&lt;br /&gt;
&lt;br /&gt;
 a = [...,...]     # array&lt;br /&gt;
 a.sort()   # sortiere über Ordnung des Schlüssels&lt;br /&gt;
 foundIndex = binSearch(a, key, 0, len(a))  # (Array, Schlüssel, von wo bis wo suchen im Array)&lt;br /&gt;
 # foundIndex == -1 wenn nichts gefunden, 0 &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt;  foundIndex &amp;lt; len(a) wenn key gefunden (erster Eintrag mit diesem Wert)&lt;br /&gt;
&lt;br /&gt;
Der folgende Algorithmus zeigt eine beispielhafte Implementierung der Methode:&lt;br /&gt;
&lt;br /&gt;
 def binSearch(a, key, start, end):  # start ist 1. Index, end ist letzter Index + 1&lt;br /&gt;
    size = end - start   # &amp;lt;math&amp;gt; \mathcal{O}(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
    if size &amp;lt;= 0:   # Bereich leer?  &amp;lt;math&amp;gt; \mathcal{O}(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
        return -1   # also nichts gefunden, &amp;lt;math&amp;gt; \mathcal{O}(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
    center = (start + end)/2   # Integer Division (d.h. Ergebnis wird abgerundet, wichtig für ganzzahlige Indizes) &amp;lt;math&amp;gt; \mathcal{O}(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
    if a[center] == key:  # &amp;lt;math&amp;gt; \mathcal{O}(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
        return center  # Schlüssel gefunden, &amp;lt;math&amp;gt; \mathcal{O}(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
    elif a[center] &amp;lt; key:  &amp;lt;math&amp;gt; \mathcal{O}(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
        return binSearch(a, key, center + 1, end)  # Rekursion in die rechte Teilliste&lt;br /&gt;
    else:&lt;br /&gt;
        return binSearch(a, key, start, center)  # Rekursion in die linke Teilliste&lt;br /&gt;
&lt;br /&gt;
Zur Berechnung der Komplexität dieses Algorithmus vernachlässigen wir zunächst den Aufwand, den die Sortierung verursacht (wir diskutieren unten, wann dies nicht zulässig ist). Wir setzen &amp;lt;tt&amp;gt;N = len(a)&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Im obigen Code ist zu erkennen, dass fast alle Anweisungen des Algorithmus die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;. Nach der Sequenzregel hat auch deren Hintereinanderausführung die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;.  Es bleibt die Komplexität der Rekursion zu berechnen. Die gesamte Komplexität des Algorithmus (jetzt als Funktion f bezeichnet) setzt sich zusammen aus den oben erwähnten &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;-Anweisungen sowie der Rekursion auf einem Teilarray der halben Größe &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;f(N) = \mathcal{O}(1) + f(N/2) = \mathcal{O}(1) + \mathcal{O}(1) + f(N/4) = ... = \underbrace{\mathcal{O}(1) + ... + \mathcal{O}(1) + \underbrace{f(0)}_{\mathcal{O}(1)\, \rightarrow \,\mathrm{size-Abfrage}}}_{n+1 \,\mathrm{Terme}} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Zur Vereinfachung nehmen wir an &amp;lt;math&amp;gt; N = 2^n &amp;lt;/math&amp;gt;, so dass gilt&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \rightarrow f(N) = \mathcal{O}(1) \cdot \mathcal{O}(n+1) = \mathcal{O}(n) = \mathcal{O}(\lg N) &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für große Datenmengen ist die ''binäre Suche'' also weit effizienter als die ''lineare Suche''. Verdoppelt sich beispielsweise die zu durchsuchende Datenmenge, so verdoppelt sich der Aufwand für die ''sequentielle Suche'' - bei der ''binären Suche'' hingegen benötigt man lediglich eine zusätzliche Vergleichsoperation. &lt;br /&gt;
&lt;br /&gt;
Für kleine Daten (&amp;lt;math&amp;gt; N = 4,\, 5 &amp;lt;/math&amp;gt;) ist die ''sequentielle Suche'' jedoch schneller als die ''binäre Suche'', da hier die rekursiven Funktionsaufrufe teurer als das Mehr an Vergleichen sind. Ein anderer ungünstiger Fall ist gegeben, wenn nur sehr wenige Suchanfragen erfolgen (weniger als &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt; viele). Dann wird der Aufwand durch das Sortieren des Arrays dominiert, ist also &amp;lt;math&amp;gt;\mathcal{O}(N \lg N) &amp;lt;/math&amp;gt;. Auch dann ist sequentielle Suche vorzuziehen.&lt;br /&gt;
&lt;br /&gt;
Eine relativ einfache Möglichkeit, die ''binäre Suche'' zu verbessern, ist die sogenannte ''Interpolationssuche''. Hierbei wird die neue Position für die Suche, also die Mitte des Arrays, durch eine Schätzung ersetzt, die angibt, wo sich der Schlüssel innerhalb des Arrays befinden könnte. Bei der Suche in einem Telefonbuch nach dem Namen Zebra würde man ja auch nicht in der Mitte anfangen. Näheres hierzu im Buch von ''Sedgewick''.&lt;br /&gt;
&lt;br /&gt;
Um sich den Algorithmus der ''binären Suche'' klar zu machen, ist es instruktiv, sich die folgende Tabelle genauer anzusehen, die die sukzessive Belegung der Variablen bei verschiedenen Anfragen beschreibt. Die Testfälle wurden nach dem Prinzip des ''domain partitioning'' gewählt. Das zugehörige Array hat die Einträge&lt;br /&gt;
&lt;br /&gt;
 a = [2, 3, 4, 5, 6]&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:center&amp;quot; border=&amp;quot;1&amp;quot; cellpadding=&amp;quot;5&amp;quot; cellspacing=&amp;quot;0&amp;quot; &lt;br /&gt;
! gesuchter key   !!  start      !! end  !! size !! center !! return &amp;lt;br/&amp;gt; (-1 oder index)  !! Kommentare  &lt;br /&gt;
|- bgcolor=&amp;quot;#e0e0e0&amp;quot;&lt;br /&gt;
| 4     ||0            || 5    ||  5   || 2   ||  2         || gefunden&lt;br /&gt;
|-&lt;br /&gt;
| 2     || 0           || 5    ||  5   || 2   ||            || linker Randfall&lt;br /&gt;
|-&lt;br /&gt;
|       ||0            || 2    ||  2   || 1   ||            ||           &lt;br /&gt;
|-&lt;br /&gt;
|       ||  0          || 1    ||  1   || 0   ||  0         || gefunden&lt;br /&gt;
|- bgcolor=&amp;quot;#e0e0e0&amp;quot;&lt;br /&gt;
| 1     ||0            || 5    ||  5   || 2   ||            || links außerhalb&lt;br /&gt;
|- bgcolor=&amp;quot;#e0e0e0&amp;quot;&lt;br /&gt;
|       ||0            || 2    ||  2   || 1   ||            ||&lt;br /&gt;
|- bgcolor=&amp;quot;#e0e0e0&amp;quot;&lt;br /&gt;
|       ||0            || 1    ||  1   || 0   ||            ||&lt;br /&gt;
|- bgcolor=&amp;quot;#e0e0e0&amp;quot;&lt;br /&gt;
|       ||0            || 0    ||  0   ||     || -1         || nichts gefunden&lt;br /&gt;
|-&lt;br /&gt;
| 6     ||0            || 5    ||  5   || 2   ||            || rechter Randfall&lt;br /&gt;
|-&lt;br /&gt;
|       ||  3          || 5    || 2    || 4   || 4          || gefunden&lt;br /&gt;
|- bgcolor=&amp;quot;#e0e0e0&amp;quot;&lt;br /&gt;
| 5     ||0            || 5    ||  5   || 2   ||            || typischer Fall&lt;br /&gt;
|- bgcolor=&amp;quot;#e0e0e0&amp;quot;&lt;br /&gt;
|       ||3            || 5    ||  2   || 4   ||            ||  &lt;br /&gt;
|- bgcolor=&amp;quot;#e0e0e0&amp;quot;&lt;br /&gt;
|       || 3           || 4    ||  1   || 3   || 3          || gefunden&lt;br /&gt;
|- &lt;br /&gt;
| 7     ||0            || 5    ||  5   || 2   ||            || rechts außerhalb        &lt;br /&gt;
|-&lt;br /&gt;
|       ||  3          || 5    ||  2   || 4   ||            ||&lt;br /&gt;
|-&lt;br /&gt;
|       ||5            || 5    ||  0   ||     || -1         || nichts gefunden&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Suchbäume ==&lt;br /&gt;
&lt;br /&gt;
Effiziente Suchalgorithmen kann man elegent mit Hilfe von Binärbäumen realisieren. Eine kurze Einführung in Binärbäume findet man [http://de.wikipedia.org/wiki/Bin%C3%A4rbaum hier]. Die Skizze erläutert wichtige Begriffe:&lt;br /&gt;
&lt;br /&gt;
[[Image:Baum.png|text-top|300x300px|Zur Illustration von Bäumen]]&lt;br /&gt;
&lt;br /&gt;
Bäume sind zweidimensional verkettete Strukturen. Sie gehören zu den fundamentalen Datenstrukturen in der Informatik. Da man in Bäumen nicht nur Daten speichern kann, sondern auch relevante Beziehungen der Daten untereinander, festgelegt über eine Ordnung auf der vergleichenden Dateneigenschaft (''Schlüssel''), eignen sich Bäume also insbesondere, um gesuchte Daten schnell wieder auffinden zu können.&lt;br /&gt;
&lt;br /&gt;
Ein ''Binärbaum'' wie oben skizziert besteht aus einer Menge von ''Knoten'', die untereinander durch ''Kanten'' verbunden sind. Jeder Knoten hat einen linken und einen rechten Unterbaum, der auch leer sein kann (in Python ließe sich dies mit ''None'' implementieren). Führt eine Kante von Knoten A zu Knoten B, so heißt A Vater von B und B Kind von A. Es gibt genau einen Knoten ohne Vater, den man ''Wurzel'' nennt. Knoten ohne Kinder heißen ''Blätter''.&lt;br /&gt;
&lt;br /&gt;
Ein ''Suchbaum'' hat zusätzlich die Eigenschaft, dass die Schlüssel jedes Knotens sortiert sind: &lt;br /&gt;
;Suchbaumbedingung: Für jeden Knoten des Binärbaumes gilt: &amp;lt;b&amp;gt;Alle&amp;lt;/b&amp;gt; Schlüssel im linken Unterbaum sind kleiner als der Schlüssel des gegebenen Knotens, &amp;lt;b&amp;gt;alle&amp;lt;/b&amp;gt; Schlüssel im rechten Unterbaum sind größer. Wir wollen hierbei annehmen, dass jeder Schlüssel pro Datensatz nur einmal vorkommt, da sich sonst die &amp;gt;- oder &amp;lt;-Relation nicht mehr strikt erfüllen ließe.&lt;br /&gt;
Mit anderen Worten: der maximale Schlüssel des linken Unterbaums, der Schlüssel des gegebenen Knotens, sowie der minimale Schlüssel des rechten Unterbaums sind in dieser Reihenfolge sortiert, und dies muss für alle Knoten und deren Unterbäume (falls sie existieren) gelten.&lt;br /&gt;
&lt;br /&gt;
Um die Verwendung eines Suchbaums zu motivieren, wollen wir von zwei Annahmen ausgehen:&lt;br /&gt;
# Einfügen und Suchen im Baum wechseln sich ab. (Wenn das Suchen erst beginnt, nachdem alle Einfügungen erfolgt sind, wäre ein dynamisches Array mit [[Suchen#Bin.C3.A4re_Suche|binärer Suche]] wesentlich einfacher.)&lt;br /&gt;
# Der Schlüssel, der die Anordnung bestimmt, kennt eine [http://de.wikipedia.org/wiki/Ordnungsrelation Ordnung] (&amp;lt;-Relation oder &amp;gt;-Relation).&lt;br /&gt;
&lt;br /&gt;
Zunächst definieren wir eine Knotenklasse für den Suchbaum:&lt;br /&gt;
 &lt;br /&gt;
 class Node:&lt;br /&gt;
     def __init__(self, key):&lt;br /&gt;
         self.key = key&lt;br /&gt;
         self.left = self.right = None&lt;br /&gt;
&lt;br /&gt;
=== Suche in einem Binärbaum ===&lt;br /&gt;
&lt;br /&gt;
Wir nehmen nun an, dass der Baum durch eine Referenz auf den Wurzelknoten &amp;lt;tt&amp;gt;root&amp;lt;/tt&amp;gt; gegeben ist. Dann kann man folgendermassen suchen:&lt;br /&gt;
  &lt;br /&gt;
 root = ...    # Wurzel des Suchbaums&lt;br /&gt;
 nodeFound = treeSearch(root, key)   # None, falls nichts gefunden&lt;br /&gt;
 &lt;br /&gt;
Hier verwenden wir die Konvention, dass der passende Knoten zurückgegeben wird, falls &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; gefunden wurde, oder &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; andernfalls. Die Suchfunktion wird rekursiv implementiert:&lt;br /&gt;
&lt;br /&gt;
 def treeSearch(node, key):&lt;br /&gt;
     if node is None:&lt;br /&gt;
         return None&lt;br /&gt;
     elif node.key == key: # gefunden&lt;br /&gt;
         return node       # =&amp;gt; Knoten zurückgeben&lt;br /&gt;
     elif key &amp;lt; node.key:  # gesuchter Schlüssel ist kleiner&lt;br /&gt;
         return treeSearch(node.left, key)  # =&amp;gt; im linken Unterbaum weitersuchen&lt;br /&gt;
     else:                 # andernfalls &lt;br /&gt;
         return treeSearch(node.right, key) # =&amp;gt; im rechten Unterbaum weitersuchen&lt;br /&gt;
&lt;br /&gt;
=== Einfügen in einen Binärbaum ===&lt;br /&gt;
&lt;br /&gt;
Bevor wir den Einfügealgorithmus implementieren, müssen wir festlegen, was passieren soll, wenn der einzufügende Schlüssel schon vorhanden ist. Mehrere Möglichkeiten bieten sich an:&lt;br /&gt;
* Fehler signalisieren (exception auslösen)&lt;br /&gt;
* nichts einfügen&lt;br /&gt;
* nichts einfügen, aber einen boolean zurückgeben (false wenn nichts eingefügt wurde, true wenn etwas einfügt wurde)&lt;br /&gt;
* nochmals einfügen (z.B. kann man die Klasse Node oben durch einen Zähler erweitern, der angibt, wie oft der betreffende Schlüssel bereits eingefügt wurde)&lt;br /&gt;
&lt;br /&gt;
Die ersten 3 Punkte realisieren eine Mengensemantik, der letzte eine Multimenge. Wir entscheiden uns hier für Möglichkeit 2 (nichts einfügen). Das Prinzip des Einfügens besteht darin, im Baum dorthin abzusteigen, wo der Schlüssel sich befinden müsste (wie bei &amp;lt;tt&amp;gt;treeSearch&amp;lt;/tt&amp;gt;), und dann an der betreffenden Stelle einen neuen Blattknoten zu erzeugen. Die Funktion gibt ein Knotenobjekt zurück, damit die Verkettungen im Elternknoten entsprechend angepasst werden können:&lt;br /&gt;
&lt;br /&gt;
 def treeInsert(node, key):&lt;br /&gt;
     if node is None:      # richtiger Platz gefunden&lt;br /&gt;
         return Node(key)  # =&amp;gt; neuen Knoten einfügen&lt;br /&gt;
     if node.key == key:   # schon vorhanden&lt;br /&gt;
         return node       # =&amp;gt; nichts tun&lt;br /&gt;
     elif key &amp;lt; node.key:     &lt;br /&gt;
         node.left = treeInsert(node.left, key) # im linken Teilbaum einfügen&lt;br /&gt;
     else:&lt;br /&gt;
         node.right = treeInsert(node.right, key) # im rechten Teilbaum einfügen&lt;br /&gt;
     return node&lt;br /&gt;
&lt;br /&gt;
Ein Binärbaum wird aufgebaut, indem &amp;lt;tt&amp;gt;treeInsert&amp;lt;/tt&amp;gt; für jeden Schlüssel aufgerufen wird. Wir verwenden hier ganze Zahlen als Schlüssel. Am Anfang ist der Baum leer:&lt;br /&gt;
&lt;br /&gt;
 root = None&lt;br /&gt;
 root = treeInsert(root, 4)&lt;br /&gt;
 root = treeInsert(root, 2)&lt;br /&gt;
 root = treeInsert(root, 3)&lt;br /&gt;
 root = treeInsert(root, 6)&lt;br /&gt;
&lt;br /&gt;
=== Entfernen aus einem Binärbaum ===&lt;br /&gt;
Wir legen wiederum zuerst fest, was im Fehlerfall passieren soll, d.h. wenn der Schlüssel nicht vorhanden ist:&lt;br /&gt;
* Auslösen einer Exception (KeyError)&lt;br /&gt;
* nichts löschen&lt;br /&gt;
* nichts löschen, aber ein boolean zurückgeben, das dies signalisiert.&lt;br /&gt;
&lt;br /&gt;
Wir entscheiden uns wieder für Möglichkeit 2. Beim Entfernen eines Knotens unterscheiden wir nun 3 Fälle:&lt;br /&gt;
# node, welcher &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; enthält, ist ein Blatt =&amp;gt; kann einfach gelöscht werden&lt;br /&gt;
# node hat &amp;lt;u&amp;gt;nur&amp;lt;/u&amp;gt; linken Unterbaum oder &amp;lt;u&amp;gt;nur&amp;lt;/u&amp;gt; rechten Unterbaum =&amp;gt; durch Unterbaum ersetzen&lt;br /&gt;
# node hat beide Unterbäume:&lt;br /&gt;
#* Suche Vorgänger: &amp;lt;math&amp;gt;\max_{k &amp;lt; key} (k \in keys)&amp;lt;/math&amp;gt; =&amp;gt; ersetze node durch seinen Vorgänger und entferne Vorgänger. (Dies führt zu einem effizienten Algorithmus, weil der Vorgänger immer zu Fall 1 oder Fall 2 gehört. Wenn er nämlich einen rechten Unterbaum hätte, könnte er nicht der Vorgänger sein.)&lt;br /&gt;
&lt;br /&gt;
Die Funktion, die den Vorgänger sucht, muss den größten Knoten im lnken Unterbaum suchen. Da diese Funktion nur in Fall 3 aufgerufen wird, gibt es den linken Unterbaum immer.&lt;br /&gt;
 def treePredecessor(node):&lt;br /&gt;
     node = node.left&lt;br /&gt;
     while node.right is not None:&lt;br /&gt;
         node = node.right&lt;br /&gt;
     return node&lt;br /&gt;
&lt;br /&gt;
Die oben angegebenen Fälle werden durch folgende Funktion realisiert:&lt;br /&gt;
&lt;br /&gt;
 def treeRemove(node, key):&lt;br /&gt;
     if node is None:   # key nicht vorhanden&lt;br /&gt;
         return node    # =&amp;gt; nichts tun&lt;br /&gt;
     if key &amp;lt; node.key: &lt;br /&gt;
         node.left = treeRemove(node.left, key)&lt;br /&gt;
     elif key &amp;gt; node.key:&lt;br /&gt;
         node.right = treeRemove(node.right, key)&lt;br /&gt;
     else:              # key gefunden&lt;br /&gt;
         if node.left is None and node.right is None:     # Fall 1&lt;br /&gt;
             node = None            &lt;br /&gt;
         elif node.left is None:     # Fall 2&lt;br /&gt;
             node = node.right       # +&lt;br /&gt;
         elif node.right is None:    # Fall 2&lt;br /&gt;
             node = node.left&lt;br /&gt;
         else:                       # Fall 3&lt;br /&gt;
             pred = treePredecessor(node)&lt;br /&gt;
             node.key = pred.key&lt;br /&gt;
             node.left = treeRemove(node.left, pred.key)&lt;br /&gt;
     return node&lt;br /&gt;
&lt;br /&gt;
=== Komplexitätsanalyse ===&lt;br /&gt;
&lt;br /&gt;
Um die Komplexität der Operationen auf einem Binärbaum zu bestimmen, müssen wir zunächst einige weitere Begriffe einführen:&lt;br /&gt;
;Pfad: Ein Pfad zwischen zwei Knoten node&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; und node&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; ist eine Folge von Knoten node&amp;lt;sub&amp;gt;k1&amp;lt;/sub&amp;gt;,...,node&amp;lt;sub&amp;gt;kn&amp;lt;/sub&amp;gt;, so dass:&lt;br /&gt;
:* node&amp;lt;sub&amp;gt;k1&amp;lt;/sub&amp;gt; == node&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:* node&amp;lt;sub&amp;gt;kn&amp;lt;/sub&amp;gt; == node&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;&lt;br /&gt;
:* node&amp;lt;sub&amp;gt;ki&amp;lt;/sub&amp;gt; und node&amp;lt;sub&amp;gt;ki+1&amp;lt;/sub&amp;gt; haben eine gemeinsame Kante.&lt;br /&gt;
[[Image:Baum_Pfad.png]]&lt;br /&gt;
Ein Baum ist definiert als ein Graph, in dem es zwischen beliebigen Knoten stets genau einen Pfad gibt.&lt;br /&gt;
&lt;br /&gt;
;Länge eines Pfades: Anzahl der Kanten im Pfad (= Anzahl der Knoten - 1)&lt;br /&gt;
;Tiefe eines Knotens: Pfadlänge vom Knoten zur Wurzel des Baumes (die Wurzel hat also die Tiefe 0)&lt;br /&gt;
;Tiefe des Baumes: maximale Tiefe eines Knotens&lt;br /&gt;
&lt;br /&gt;
Allen Baumoperationen ist gemeinsam, dass sie entlang genau eines Pfades im Baum absteigen (welcher Pfad dies ist ergibt sich aus der Ordnung der Schlüssel). Der Abstieg endet, wenn entweder der gesuchte Schlüssel gefunden wird, oder wenn erkannt wird, dass der Schlüssel nicht vorhanden ist (wenn das Kind, wo der Schlüssel sein müsste, den Wert &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; hat). Während des Abstiegs werden in jedem Knoten nur Anweisungen ausgeführt, die konstante Zeit benötigen (1 Vergleich, wenn die Suche in dem Knoten erfolglos beendet wird, 2 Vergleiche, wenn der Schlüssel gefunden wird, und 3 Vergleiche, wenn im rechten oder linken Teilbaun weiter abgestiegen werden muss). Daraus folgt, dass die Suche im ungünstigsten Fall die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(T)&amp;lt;/math&amp;gt; hat, wobei T die Tiefe des Baumes (= längster Pfad, der durchlaufen werden kann) ist.&lt;br /&gt;
&lt;br /&gt;
==== Ungünstigster Fall für die Baumoperationen ====&lt;br /&gt;
&lt;br /&gt;
Um den ungünstigsten Fall für die Baumoperationen zu finden, müssen wir offensichtlich herausfinden, wie groß die Tiefe maximal werden kann. Es ist leicht zu erkennen, dass die Tiefe maximiert wird, wenn man sortierte Daten in den Baum einfügt:&lt;br /&gt;
* Fügt man [1,2,3,4,5] in dieser Reihenfolge ein, muss man bei &amp;lt;tt&amp;gt;treeInsert&amp;lt;/tt&amp;gt; stets in den rechten Teilbaum absteigen (weil der nächste Schlüssel immer größer als der größte bisherige Schlüssel ist) und dort ein rechtes Kind einfügen. Es ergibt sich folgender Baum:&amp;lt;br /&amp;gt; [[Image:Balance.png]]&lt;br /&gt;
: Dieser Baum hat die Tiefe 4. Die Funktion &amp;lt;tt&amp;gt;treeSerach&amp;lt;/tt&amp;gt; verhält sich dann wie sequentielle Suche, man hat also durch die Verwendung des Suchbaums nichts gewonnen. &lt;br /&gt;
Allgemein gilt: Alle Operationen eine binären Suchbaums haben im ungünstigsten Fall die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;, wo N die Anzahl der Elemente im Baum bezeichnet. Eine offensichtliche Lösung der Problems besteht darin, die Elemente nicht in einer so ungünstigen Reihenfolge einzufügen (siehe Übungsaufgabe 5.1.c). Allerdings ist dies nicht immer möglich. Abhilfe schaffen dann selbst-balancierende Bäume.&lt;br /&gt;
&lt;br /&gt;
==Selbst-balancierende Suchbäume==&lt;br /&gt;
&lt;br /&gt;
=== Balance eines Suchbaumes ===&lt;br /&gt;
&lt;br /&gt;
Um die Komplexität der Suchbaum-Operationen zu minimieren, müssen wir die Höhe des Baumes minimieren. Wir wollen also die Länge des längsten Pfades verkürzen, ohne dass ein anderer Pfad dadurch unnötig lang wird. Mit anderen Worten wollen wir erreichen, dass alle Pfade von der Wurzel zu den Blättern ungefährt die gleiche Länge haben. Diese Idee kann man formal durch den Begriff der ''Balance'' eines Suchbaums fassen. Um die Balance zu definieren, betrachten wir &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; als zusätzlichen Knoten, als sogenannten '''Sentinel''' (engl. für ''Wächter''). Der sentinel-Knoten wird als rechter oder linker Nachfolger verlinkt, wenn der entsprechende Nachfolger nicht durch einen echten Knoten belegt ist:&lt;br /&gt;
&lt;br /&gt;
[[Image:sentinel.png|400px|right]]&lt;br /&gt;
&lt;br /&gt;
Wir definieren nun:&lt;br /&gt;
;RS-Pfade: Pfad von ''root'' &amp;amp;rarr; ''sentinel''. In jedem Binärbaum gibt es mehrere RS-Pfade.&lt;br /&gt;
;Balance eines Baumes: Differenz zwischen der Länge des längsten und kürzesten RS-Pfads:&lt;br /&gt;
:::&amp;lt;math&amp;gt; B = \max_{P\in\{RS\}} |P| - \min_{P\in\{RS\}} |P|&amp;lt;/math&amp;gt;&lt;br /&gt;
:wobei &amp;lt;math&amp;gt;\{RS\}&amp;lt;/math&amp;gt; die Menge aller RS-Pfade bezeichnet, und |P| die Länge des Pfades P.&lt;br /&gt;
;vollständiger Baum:  Balance &amp;lt;math&amp;gt;B=0&amp;lt;/math&amp;gt;&lt;br /&gt;
:Daraus folgt, dass alle Knoten (außer den Blättern) 2 Kinder haben müssen.&lt;br /&gt;
;perfekt balancierter Baum:  Balance  &amp;lt;math&amp;gt;B \le 1&amp;lt;/math&amp;gt;&lt;br /&gt;
::alternative Definition für perfekt balancierte Bäume: Für jeden Knoten gilt, dass der rechte und linke Unterbaum ebenfalls perfekt balancierte Bäume sind und ihre Höhe sich höchstens um '''1''' unterscheidet. Leere Unterbäume sind per Definition perfekt balanciert und haben die Höhe Null.&lt;br /&gt;
&lt;br /&gt;
====Größe eines Baumes in Abhängigkeit von Balance und Tiefe====&lt;br /&gt;
[[Image:Baum_voll.png|400px|right]]&lt;br /&gt;
;vollständiger Baum:&lt;br /&gt;
Aus der Abbildung erkennt man, dass Ebene k eines vollständigen Baumes stets 2&amp;lt;sup&amp;gt;k&amp;lt;/sup&amp;gt; Knoten enthält (der grüne Knoten gehört nicht zum vollständigen Baum). Hat der Baum die Tiefe d, dann enthält er &lt;br /&gt;
&lt;br /&gt;
::N = 2&amp;lt;sup&amp;gt;0&amp;lt;/sup&amp;gt; + 2&amp;lt;sup&amp;gt;1&amp;lt;/sup&amp;gt;.....+ 2&amp;lt;sup&amp;gt;d&amp;lt;/sup&amp;gt; = 2&amp;lt;sup&amp;gt;d+1&amp;lt;/sup&amp;gt; - 1 &lt;br /&gt;
&lt;br /&gt;
Knoten (und damit ebensoviele Datenelemente).&lt;br /&gt;
&lt;br /&gt;
;perfekt balancierter Baum:&lt;br /&gt;
Für eine gegebene Tiefe d kann kein Baum mehr Elemente enthalten als der entsprechende vollständige Baum. Also gilt für jeden perfekt balancierten Baum der Größe N:&lt;br /&gt;
:::&amp;lt;math&amp;gt; N \le 2^{d+1} - 1&amp;lt;/math&amp;gt;&lt;br /&gt;
Der kleinste perfekt balancierte Baum der Tiefe d ist ein vollständiger Baum der Tiefe d-1 (mit &amp;lt;math&amp;gt;2^{(d-1)+1} - 1&amp;lt;/math&amp;gt; Knoten), wo an einem einzigen Knoten noch ein weiteres Datenelement angehängt wurde (grüner Knoten in der Abbildung). Dieser Baum enthält&lt;br /&gt;
:::&amp;lt;math&amp;gt;N = \left(2^{(d-1)+1} - 1\right) + 1 = 2^d&amp;lt;/math&amp;gt;&lt;br /&gt;
Datenelemente. Folglich gilt für perfekt balancierte Bäume die Ungleichung&lt;br /&gt;
:::&amp;lt;math&amp;gt;2^d \le N \le 2^{d+1} - 1&amp;lt;/math&amp;gt;&lt;br /&gt;
und demzufolge auch&lt;br /&gt;
:::&amp;lt;math&amp;gt;\log_2(2^d) \le \log_2(N) \le \log_2(2^{d+1} - 1) &amp;lt; \log_2(2^{d+1})&amp;lt;/math&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;d \le \log_2(N) &amp;lt; d+1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Da die Baumoperationen im ungünstigsten Fall die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(d)&amp;lt;/math&amp;gt; haben, gilt für perfekt balancierte Bäume, dass alle Operationen im schlechtesten Fall die Komplexität&lt;br /&gt;
:::&amp;lt;math&amp;gt;\mathcal{O}(\log(N))&amp;lt;/math&amp;gt;&lt;br /&gt;
haben, das ist ''logarithmische Komplexität''. Ein perfekt balancierter Baum wird z.B. durch die Datenstruktur des [http://en.wikipedia.org/wiki/AVL_tree AVL-Baums] realisiert. Die Implementation eines AVL-Baums ist jedoch kompliziert, und es zeigt sich, dass die Eigenschaft der perfekten Balance gar nicht notwendig ist, um logarithmische Komplexität zu garantieren. Wir definieren:&lt;br /&gt;
;balancierter Baum: Für die Tiefe d(N) eines balancierten Baumes mit N Knoten gilt&lt;br /&gt;
:::&amp;lt;math&amp;gt;\forall  N:d(N)\le c \cdot d_{PB}(N)&amp;lt;/math&amp;gt; mit &amp;lt;math&amp;gt;1 \le c &amp;lt; \infty&amp;lt;/math&amp;gt;&lt;br /&gt;
:wobei d&amp;lt;sub&amp;gt;PB&amp;lt;/sub&amp;gt;(N) die Tiefe eines perfekt balancierten Baumes mit N Knoten ist. Für die Komplexität der Operationen in einem balancierten Baum gilt dann:&lt;br /&gt;
:::&amp;lt;math&amp;gt;f(N) \le  c\cdot f_{PB}(N) = c\, \mathcal{O}(\log(N)) = \mathcal{O}(\log(N))&amp;lt;/math&amp;gt;&lt;br /&gt;
d.h. die Komplexität ändert sich nicht. Balancierte Bäume sind fast genauso schnell wie perfekt balancierte Bäume (bis auf den Faktor c), aber ihr Aufbau ist algorithmisch einfacher.&lt;br /&gt;
&lt;br /&gt;
===Idee selbst-balancierende Bäume===&lt;br /&gt;
&lt;br /&gt;
Die grundlegende Idee der selbst-balancierenden Bäume besteht darin, nach jeder Einfügung die Balance des Baumes zu optimieren. Dies geschieht am zweckmäßigsten im aufsteigenden Zweig der Rekursion, also nach der Rückkehr von den rekursiven Aufrufen der Funktion &amp;lt;tt&amp;gt;treeInsert&amp;lt;/tt&amp;gt;. Dies entspricht folgendem Pseudo-Code:&lt;br /&gt;
&lt;br /&gt;
  def insertTree(node,key):&lt;br /&gt;
      if node is None: &lt;br /&gt;
          return Node(key)       &lt;br /&gt;
      if node.key == key:&lt;br /&gt;
          return node&lt;br /&gt;
      if key &amp;lt; node.key:&lt;br /&gt;
          node.left  = insertTree(node.left, key)&lt;br /&gt;
      else:&lt;br /&gt;
          node.right = insertTree(node.right, key)    &lt;br /&gt;
      &amp;lt;font color=&amp;quot;red&amp;quot;&amp;gt;optimiere die Balance hier&amp;lt;/font&amp;gt;&lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Dabei muss man beachten, dass bei den Optimierungen die Suchbaumbedingung (Definition siehe oben) erhalten bleibt. Dies ist garantiert, wenn alle Umstrukturierungen durch die elementare Operation der ''Rotation'' implementiert werden. Eine ''Rechtsrotation'' ersetzt die Wurzel &amp;lt;tt&amp;gt;n&amp;lt;/tt&amp;gt; eines Teilbaumes durch sein linkes Kind, und fügt die alte Wurzel als rechtes Kind der neuen Wurzel ein. Die ''Linksrotation'' ist die Inverse dieser Operation. Die Abbildung verdeutlicht die Umstrukturierungen:&lt;br /&gt;
&lt;br /&gt;
[[Image:Baum_Rotation.png]]&lt;br /&gt;
&lt;br /&gt;
Die Rotationen werden wie folgt implementiert:&lt;br /&gt;
&lt;br /&gt;
 def rotateRight(node):&lt;br /&gt;
     newRoot = node.left&lt;br /&gt;
     node.left = newRoot.right&lt;br /&gt;
     newRoot.right = node&lt;br /&gt;
     return newRoot&lt;br /&gt;
&lt;br /&gt;
 def rotateLeft(node):&lt;br /&gt;
     newRoot = node.right&lt;br /&gt;
     node.right = newRoot.left&lt;br /&gt;
     newRoot.left = node&lt;br /&gt;
     return newRoot&lt;br /&gt;
&lt;br /&gt;
Man erkennt leicht, dass die Suchbaumbedingung erhalten bleibt. Wir erläutern dies für die Rechtsrotation, bei der Linksrotation gilt die Erklärung entsprechend. Knoten ''n'' hat einen größeren Schlüssel als Knoten ''L'', denn ''L'' ist vor der Rechtsrotation das linke Kind von ''n''. Nach der Rotation ist ''n'' deshalb korrekterweise das rechte Kind von ''L''. Weiter gilt für den Teilbaum mit der Wurzel ''LR'', dass er größer als ''L'' ist (denn er ist das rechte Kind von ''L''), aber kleiner als ''n'' (denn er liegt im linken Teilbaum von ''n''). Nach der Rechtsrotation ist diese Bedingung immer noch erfüllt, denn ''LR'' ist jetzt linker Teilbaum von ''n'', welches wiederum rechter Teilbaum von ''L'' geworden ist. Alle anderen Teilbäume sind von der Rotation nicht betroffen.&lt;br /&gt;
&lt;br /&gt;
Verschiedene Arten von selbst-balancierenden Bäumen unterscheiden sich im Wesentlichen dadurch, wann welche Rotation ausgeführt wird. Wichtige Beispiele sind&lt;br /&gt;
* [http://en.wikipedia.org/wiki/AVL_tree AVL-Bäume] (älteste Variante)&lt;br /&gt;
* [http://en.wikipedia.org/wiki/Red_black_tree Rot-Schwarz-Bäume] (verbreitetste Variante)&lt;br /&gt;
* [http://en.wikipedia.org/wiki/Treap Treaps] (flexibelste Variante, siehe Übung 6.1)&lt;br /&gt;
* [http://en.wikipedia.org/wiki/Splay_tree Splay trees]&lt;br /&gt;
* [http://en.wikipedia.org/wiki/AA_tree Andersson-Bäume] (einfachste Variante, siehe unten)&lt;br /&gt;
&lt;br /&gt;
Daneben wird gern die [http://en.wikipedia.org/wiki/Skip_list Skip List] verwendet, die aber kein Binärbaum ist, sondern auf einem anderen Prinzip beruht.&lt;br /&gt;
&lt;br /&gt;
===Andersson-Bäume===&lt;br /&gt;
&lt;br /&gt;
Jeder selbst-balancierende Baum benötigt Zusatzinformationen, die die augenblickliche Balance beschreiben, so dass diese gegebenenfalls optimiert werden kann. Der Andersson-Baum fügt zu diesem Zweck in jedem Knoten ein neues Feld ''level'' ein, welches mit 1 initialisiert wird:&lt;br /&gt;
&lt;br /&gt;
  class AnderssonNode:&lt;br /&gt;
    def__init__(self, key):&lt;br /&gt;
        self.key = key&lt;br /&gt;
        self.left = self.right = None&lt;br /&gt;
        self.level = 1&lt;br /&gt;
&lt;br /&gt;
Grob gesprochen kodiert das ''level''-Feld den Abstand des Knotens vom Sentinel. Genauer gelten folgende&lt;br /&gt;
&lt;br /&gt;
====Regeln====&lt;br /&gt;
&lt;br /&gt;
* Es gibt vertikale Kanten (parent.level == child.level + 1 ) und horizontale Kanten (parent.level == child.level). &lt;br /&gt;
* Die ''reduzierte Länge'' eines Pfades zwischen zwei Knoten wird berechnet, indem nur die vertikalen Kanten im Pfad gezählt werden.&lt;br /&gt;
* Das Sentinel hat ''level = 0''. Alle Kanten zum Sentinel sind vertikal.&lt;br /&gt;
* Die ''reduzierte Höhe'' eines Knotens entspricht der reduzierten Länge des Pfades von diesem Knoten zum Sentinel. Das ''level''-Feld jedes Knotens speichert die reduzierte Höhe dieses Knotens. Folglich gilt für alle Knoten, die direkt mit dem Sentinel verbunden sind, ''level = 1''. Insbesondere gilt dies auch für neu eingefügte Knoten (siehe obige Initialisierung).&lt;br /&gt;
&lt;br /&gt;
Die nächsten zwei Regeln sichern die Balance:&lt;br /&gt;
* Alle RS-Pfade haben die gleiche reduzierte Länge. Dies ist äquivalent zu der Bedingung, dass die Wurzel des Andersson-Baumes über alle möglichen RS-Pfade auf dem gleichen Level erreicht wird.&lt;br /&gt;
* Kein Pfad hat 2 aufeinander folgende horizontale Kanten. &lt;br /&gt;
&lt;br /&gt;
Die letzte Regel führt zu starken algorithmischen Vereinfachungen gegenüber den konzeptionell sehr ähnlichen Rot-Schwarz-Bäumen:&lt;br /&gt;
* Nur Kanten zum rechten Kind dürfen horizontal sein.&lt;br /&gt;
&lt;br /&gt;
Das folgende Bild zeigt einen Andersson-Baum, bei dem allerdings nicht alle Verbindungen zum Sentinel eingezeichnet sind:&lt;br /&gt;
&lt;br /&gt;
[[Image:Abild.png]]&lt;br /&gt;
&lt;br /&gt;
Es gilt folgender&lt;br /&gt;
;Satz: Jeder Andersson-Baum ist balanciert. Beweis:&lt;br /&gt;
:1. Sei ''h&amp;lt;sub&amp;gt;r&amp;lt;/sub&amp;gt;'' die reduzierte Höhe des Andersson-Baumes. Die Eigenschaft, dass alle RS-Pfade die reduzierte Länge ''h&amp;lt;sub&amp;gt;r&amp;lt;/sub&amp;gt;'' (also die ''gleiche'' reduzierte Länge) haben, hat eine wichtige Folge: Hat der Andersson-Baum ''keine'' horizontalen Kanten, so muss er ein vollständiger Baum der Tiefe ''d&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; = h&amp;lt;sub&amp;gt;r&amp;lt;/sub&amp;gt; - 1'' sein, denn nur ein vollständiger Baum hat die Eigenschaft, dass alle RS-Pfade die gleiche Länge besitzen. Gibt es hingegen horizontale Kanten, muss der Andersson-Baum ''mehr'' Elemente enthalten als der vollständige Baum der Tiefe ''d&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt;''. Folglich gilt für die Anzahl der Knoten eines Andersson-Baumes:&lt;br /&gt;
:::&amp;lt;math&amp;gt;N \ge 2^{d_v+1} - 1 = 2^{h_r} - 1&amp;lt;/math&amp;gt;&lt;br /&gt;
:2. Da niemals zwei aufeinenderfolgende Kanten horizontal sein dürfen, ist in jedem RS-Pfad höchstens die Hälfte aller Kanten horizontal. Daher gilt für die Tiefe ''d'' eines Andersson-Baumes&lt;br /&gt;
:::&amp;lt;math&amp;gt;d \le 2 h_r&amp;lt;/math&amp;gt;&lt;br /&gt;
:3. Fasst man 1. und 2. zusammen, erhält man:&lt;br /&gt;
:::&amp;lt;math&amp;gt;N \ge 2^{h_r} - 1 \ge 2^{d/2} - 1&amp;lt;/math&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;N + 1 \ge 2^{d/2}&amp;lt;/math&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;\log_2(N + 1) \ge d/2&amp;lt;/math&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;d \le 2 \log_2(N + 1)&amp;lt;/math&amp;gt;.&lt;br /&gt;
::Da die Komplexität der Baumoperationen &amp;lt;math&amp;gt;f(N) = \mathcal{O}(d)&amp;lt;/math&amp;gt; ist, gilt für den Andersson-Baum:&lt;br /&gt;
:::&amp;lt;math&amp;gt;f(N) = \mathcal{O}(2 \log_2(N + 1)) = \mathcal{O}(\log(N))&amp;lt;/math&amp;gt; &lt;br /&gt;
::q.e.d.&lt;br /&gt;
&lt;br /&gt;
====Wie erreicht man die Balance?====&lt;br /&gt;
&lt;br /&gt;
Der Baum ist nicht mehr balanciert, wenn obige Regeln verletzt sind. Dies kann durch Einfügen eines neuen Knotens oder durch Löschen eines Knotens passieren. Nach jeder Einfügung haben sowohl der neue Knoten als auch sein Vater das Level 1 (denn der Vater war vorher direkt mit dem Sentinel verbunden). Kanten zu neu eingefügten Knoten sind deshalb immer horizontal. Dies kann die Regeln verletzen, indem entweder&lt;br /&gt;
* eine horizontale Kante zum linken Kind enstanden ist (falls der neue Knoten ein linkes Kind ist), oder&lt;br /&gt;
* zwei aufeinander folgende horizontale Kanten zu rechten Kindern entstanden sind (falls der neue Knoten ein rechtes Kind ist, und sein Vater bereits ein horizontales rechtes Kind war).&lt;br /&gt;
Diese Fehler können durch Rotation leicht behoben werden:&lt;br /&gt;
* Linke horizontale Kanten werden durch Rechtsrotation in rechte horizontale Kanten verwandelt.&lt;br /&gt;
* Bei zwei aufeinander folgenden rechten horizontalen Kanten wird der mittlere Knoten um eine Ebene angehoben.&lt;br /&gt;
Dabei ist zu beachten, dass die erste Reparatur einen neuen Fehler erzeugen kann: Es können zwei aufeinanderfolgende rechte horizontale Kanten enstehen. Daher muss die zweite Operation stets nach der ersten ausgeführt werden. Das Anheben des Levels in der zweiten Operation kann wiederum dazu führen, dass auf der nächsthöheren Ebene verbotene horizontale Kanten entstehen. Deshalb müssen die Reparaturoperationen auf der nächsten Ebene rekursiv wiederholt werden. Dies führt uns zu folgender Implementation des Insert-Algorithmus&lt;br /&gt;
&lt;br /&gt;
  def anderssonTreeInsert(node,key):&lt;br /&gt;
      if node is None: &lt;br /&gt;
          return AnderssonNode(key)       &lt;br /&gt;
      if node.key == key:&lt;br /&gt;
          return node&lt;br /&gt;
      if key &amp;lt; node.key:&lt;br /&gt;
          node.left  = anderssonTreeInsert(node.left, key)&lt;br /&gt;
      else:&lt;br /&gt;
          node.right = anderssonTreeInsert(node.right, key)    &lt;br /&gt;
      &amp;lt;font color=&amp;quot;red&amp;quot;&amp;gt;if node.left is not None and node.level == node.left.level: # linke horizontale Kante&lt;br /&gt;
            node = rotateRight(node)  # wird zu rechter horizontaler Kante gemacht&lt;br /&gt;
      if node.right is not None and node.right.right is not None and node.level==node.right.right.level:  # aufeinanderfolgende horizontale Kanten&lt;br /&gt;
            node = rotateLeft(node)   # mache den mittleren Knoten zur Wurzel des Teilbaums&lt;br /&gt;
            node.level += 1           # und hebe die Wurzel um ein level an&amp;lt;/font&amp;gt;    &lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Da die Reparaturoperationen auf dem Rückweg von der Rekursion ausgeführt werden, ist gewährleistet, dass sie auf der nächsten Ebene des Baumes ebenfalls ausgeführt werden, falls nötig. Die folgende Skizze verdeutlicht die Anwendung der Reparaturen, wenn Knoten ''c'' über eine linke horizontale Kante an Knoten ''b'' angefügt wurde. Im oberen Beispiel genügt die erste Operation zur Reparatur, beim unteren Beispiel muss hingegen auch noch die zweite Operation angewendet werden.&lt;br /&gt;
&lt;br /&gt;
[[Image:rotate.jpg|text-top]]&lt;br /&gt;
&lt;br /&gt;
Die folgende Illustration verdeutlicht das Verhalten des Andersson-Baumes, wenn die Schlüssel in der Folge [5,4,3,2,1] eingefügt werden. Beim einfachen Binärbaum sind solche vorsortierten Daten sehr ungünstig und führen zu entarteten Bäumen mit linearer Zugriffzeit. Die Umstrukturierungen beim Andersson-Baum stellen hingegen sicher, dass die Balance immer gewahrt bleibt. Wir stellen die Knoten hier als Paare &amp;lt;tt&amp;gt;(key, level)&amp;lt;/tt&amp;gt; dar, Pfeile markieren die Richtung von horizontalen Kanten. Wie oben beschrieben, werden neue Knoten zunächst normal in den Baum eingefügt und ihr Level mit 1 initialisiert. Wenn dadurch Bedingungen verletzt werden, werden die notwendigen Umstrukturierungen durchgeführt.&lt;br /&gt;
&lt;br /&gt;
Beim Einfügen des ersten Knotens (Schlüssel 5) gibt es noch keine Probleme:&lt;br /&gt;
&lt;br /&gt;
 (5,1)&lt;br /&gt;
&lt;br /&gt;
Der zweite Knoten (Schlüssel 4) wird zum linken Kind des ersten. Da beide Knoten sich auf Level 1 befinden, ensteht dadurch eine verbotene horizontale Kante nach links, die durch eine Rechtsrotation (RR) in eine erlaubte horizontale Kante nach rechts umgewandelt wird. Danach ist Knoten 4 die neue Wurzel des Baumes:&lt;br /&gt;
&lt;br /&gt;
   (4,1) &amp;lt;-- (5,1)   ==RR==&amp;gt;   (4,1) --&amp;gt; (5,1)&lt;br /&gt;
 &lt;br /&gt;
Das Einfügen von Schlüssel 3 verursacht wieder eine horizontale linke Kante, die in eine rechte umgewandelt wird:&lt;br /&gt;
&lt;br /&gt;
   (3,1) &amp;lt;-- (4,1) --&amp;gt; (5,1)   ==RR==&amp;gt;   (3,1) --&amp;gt; (4,1) --&amp;gt; (5,1)&lt;br /&gt;
 &lt;br /&gt;
Nun gibt es aber zwei horizontale Kanten hintereinander. Wir führen deshalb eine Linksrotation (LR) durch und heben das Level des mittleren Knotens um 1 an:&lt;br /&gt;
&lt;br /&gt;
                                                                                  (4,2)&lt;br /&gt;
                                                                                   /   \&lt;br /&gt;
   (3,1) --&amp;gt; (4,1) --&amp;gt; (5,1)   ==LR==&amp;gt;   (3,1) &amp;lt;-- (4,1) --&amp;gt; (5,1)  ==Lift==&amp;gt;  (3,1)   (5,1)&lt;br /&gt;
&lt;br /&gt;
Damit ist der Baum wieder korrekt. Das Einfügen des Schlüssels 2 führt wieder zu einer verbotenen linken Kante, die durch Rechtsrotation beseitigt wird:&lt;br /&gt;
&lt;br /&gt;
                                                 (4,2)&lt;br /&gt;
                  (4,2)                         /     \&lt;br /&gt;
                  /   \       ==RR==&amp;gt;          /       \&lt;br /&gt;
    (2,1) &amp;lt;-- (3,1)   (5,1)                   /         \&lt;br /&gt;
                                          (2,1)--&amp;gt;(3,1) (5,1)&lt;br /&gt;
&lt;br /&gt;
Nun fügen wir Schlüssel 1 ein, der ebenfalls zu einer verbotenen linken Kante führt, aber die Reparatur des Fehlers durch Rechstsrotation würde zwei aufeinanderfolgende horizontale Kanten erzeugen. Knoten 2 muss deshalb angehoben werden:&lt;br /&gt;
&lt;br /&gt;
                      (4,2)                      (2,2) &amp;lt;-- (4,2)    &lt;br /&gt;
                     /     \                     /   \         \&lt;br /&gt;
                    /       \        ===&amp;gt;       /     \         \&lt;br /&gt;
                   /         \                 /       \         \&lt;br /&gt;
     (1,1) &amp;lt;-- (2,1)--&amp;gt;(3,1) (5,1)         (1,1)       (3,1)     (5,1)&lt;br /&gt;
&lt;br /&gt;
Jetzt ist aber bei Level 2 eine verbotene linke horizontale Kante entstanden, die wir wieder durch Rechtsrotation in eine erlaubte rechte horizontale Kante verwandeln, so dass Knoten 2 nun die Wurzel des Baumes bildet:&lt;br /&gt;
&lt;br /&gt;
           (2,2) &amp;lt;-- (4,2)                       (2,2) --&amp;gt; (4,2)   &lt;br /&gt;
           /   \         \                       /         /   \&lt;br /&gt;
          /     \         \          ===&amp;gt;       /         /     \&lt;br /&gt;
         /       \         \                   /         /       \&lt;br /&gt;
     (1,1)       (3,1)     (5,1)           (1,1)     (3,1)       (5,1)&lt;br /&gt;
&lt;br /&gt;
Jetzt sind alle Bedingungen erfüllt. Man erkennt, dass alle reduzierten RS-Pfade die gleiche Länge, nämlich 2, haben (dies entspricht gerade dem Level der Wurzel des Baumes). Die tatsächliche Tiefe des Baumes (längster Pfad von der Wurzel zu einem Blatt, wobei horizontale Kanten mitgezählt werden) beträgt 2. Für einen Binärbaum mit 5 Knoten ist die Tiefe 2 gerade der beste erreichbare Wert, der Andersson-Baum verhält sich hier also optimal.&lt;br /&gt;
 &lt;br /&gt;
Die Löschoperation &amp;lt;tt&amp;gt;anderssonTreeRemove&amp;lt;/tt&amp;gt; benötigt in jedem Knoten bis zu 5 Rotationen. Wegen der Einzelheiten verweisen wir auf Anderssons [http://user.it.uu.se/~arnea/abs/simp.html Originalartikel].&lt;br /&gt;
&lt;br /&gt;
==Beziehungen zwischen dem Suchproblem und dem Sortierproblem==&lt;br /&gt;
&lt;br /&gt;
===Sortieren mit Hilfe eines selbst-balancierenden Suchbaums===&lt;br /&gt;
&lt;br /&gt;
Mit Hilfe eines selbst-balancierenden Suchbaums kann ein effizienter Sortieralgorithmus implementiert werden, indem man zunächst die Daten in beliebiger Reihenfolge in einen Baum einfügt, und dann in der richtigen Sortierung wieder ausliest.&lt;br /&gt;
&lt;br /&gt;
   a = ...   # unsortiertes Array&lt;br /&gt;
   t = None  # leerer Andersson-Baum&lt;br /&gt;
   for e in a:&lt;br /&gt;
       t = anderssonTreeInsert(t, e) # Baum erzeugen&lt;br /&gt;
   r = []    # leeres dynamisches Array&lt;br /&gt;
   treeSort(t, r) &lt;br /&gt;
   # r enthält jetzt die Daten aus a in sortierter Reihenfolge&lt;br /&gt;
&lt;br /&gt;
Die Funktion &amp;lt;tt&amp;gt;treeSort&amp;lt;/tt&amp;gt; navigiert im Sinne eines sogenannten ''in-order traversals'' durch den Baum und fügt die Datenelemente in der richtigen Reihenfolge an des Array an:&lt;br /&gt;
&lt;br /&gt;
 def treeSort(node,array):          # dynamisches Array als 2. Argument&lt;br /&gt;
     if node is None:               # &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
         return&lt;br /&gt;
     treeSort(node.left, array)     # rekursiv&lt;br /&gt;
     array.append(node.key)         # amortisiert &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
     treeSort(node.right, array)    # rekursiv&lt;br /&gt;
&lt;br /&gt;
;Komplexität:&lt;br /&gt;
&lt;br /&gt;
* Jede Einfügeoperation in den Baum hat logarithmische Komplexität. Der Aufbau eines Baumes aus N Elementen hat daher Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N \log(N))&amp;lt;/math&amp;gt;.&lt;br /&gt;
* &amp;lt;tt&amp;gt;treeSort&amp;lt;/tt&amp;gt; führt in jedem Knoten eine oder zwei Operationen mit Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt; sowie zwei rekursive Aufrufe aus. Die Auflösung der Rekursion ergibt&lt;br /&gt;
 &amp;lt;math&amp;gt;&lt;br /&gt;
 f(N)=\mathcal{O}(1)+f(N_\mathrm{left})+f(N_\mathrm{right})=\mathcal{O}(1)+\mathcal{O}(1)+f(N_\mathrm{left.left})+f(N_\mathrm{left.right})+\mathcal{O}(1)+f(N_\mathrm{right.left})&lt;br /&gt;
 +f(N_\mathrm{left.right})=N\cdot\mathcal{O}(1)=\mathcal{O}(N)&lt;br /&gt;
 &amp;lt;/math&amp;gt;&lt;br /&gt;
* Insgesamt erhalten wir also Komplexität &amp;lt;math&amp;gt;\mathcal{O}(\max(N \log(N), N)) = \mathcal{O}(N \log(N))&amp;lt;/math&amp;gt; wie bei Merge Sort. Allerdings sind der konstante Faktor sowie der Speicherverbrauch größer, so dass diese Sortiermethode in der Praxis kaum angewendet wird.&lt;br /&gt;
&lt;br /&gt;
===Sortieren als Suchproblem===&lt;br /&gt;
&lt;br /&gt;
Diesem Thema ist jetzt ein eigenes Kapitel [[Sortieren in linearer Zeit]] gewidmet.&lt;br /&gt;
&lt;br /&gt;
[[Sortieren in linearer Zeit|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Suchen&amp;diff=5444</id>
		<title>Suchen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Suchen&amp;diff=5444"/>
				<updated>2012-07-31T14:00:49Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Suchbäume */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Das Suchen ist eine grundlegende Operation in der Informatik. Viele Probleme in der Informatik können auf Suchaufgaben zurückgeführt werden.&lt;br /&gt;
&lt;br /&gt;
Gemeint ist mit Suchen das Wiederauffinden eines Datensatzes aus einer Menge von früher gespeicherten Datensätzen, oder das Auffinden einer bestimmten Lösung in einem (potentiell großen) Suchraum möglicher Lösungen. Ein paar einleitende Worte zum Suchproblem findet man [http://de.wikipedia.org/wiki/Suche hier].&lt;br /&gt;
&lt;br /&gt;
== Überblick über verschiedene Suchmethoden ==&lt;br /&gt;
&lt;br /&gt;
Um sich der Vielseitigkeit des Suchproblems bewusst zu werden, ist es sinnvoll, sich einen Überblick über verschiedene Suchmethoden zu verschaffen. &lt;br /&gt;
&lt;br /&gt;
Hier sei auch auf einen bereits existierenden Wikipedia-Artikel zu [http://de.wikipedia.org/wiki/Suchverfahren Suchverfahren] verwiesen.&lt;br /&gt;
&lt;br /&gt;
Allen gemeinsam ist die grundlegende Aufgabe, ein Datenelement mit bestimmten Eigenschaften aus einer großen Menge von Datenelementen zu selektieren.&lt;br /&gt;
Dies kann, natürlich ohne jeden Anspruch auf Vollständigkeit, nach einer der jetzt diskutierten Methoden geschehen:&lt;br /&gt;
&lt;br /&gt;
* '''Schlüsselsuche''': meint das Suchen von Elementen mit bestimmtem Schlüssel; ein klassisches Beispiel wäre das Suchen in einem Wörterbuch,  die Schlüssel entsprechen hier den Wörtern, die Datensätze wären die zu den Wörtern gehörigen Eintragungen.&lt;br /&gt;
&lt;br /&gt;
* '''Bereichssuche''': Im Allgemeinen meint die Bereichssuche in n-Dimensionen die Selektion von Elementen mit Eigenschaften aus einem bestimmten n-dimensionalen Volumen. Im eindimensionalen Fall will man alle Elemente finden, deren Eigenschaft(en) in einem bestimmten Intervall liegen. Die Verallgemeinerung auf n-Dimensionen ist offensichtlich. Ein Beispiel für die Bereichssuche in einer 3D-Kugel wäre ein Handy mit Geolokalisierung, welches alle Restaurants in einem Umkreis von 500m findet. Lineare Ungleichungen werden graphisch durch [http://de.wikipedia.org/wiki/Hyperebene Hyperebenen] repräsentiert. In 2D sind diese Hyperebenen Geraden. Die Ungleichungen können dann den Lösungsraum in irgendeiner Form begrenzen.&lt;br /&gt;
&lt;br /&gt;
* '''Ähnlichkeitssuche''': Finde Elemente, die gegebenen Eigenschaften möglichst ähnlich sind. Ein prominentes Beispiel ist Google (=Ähnlichkeit zwischen Suchbegriffen und Dokumenten) oder das Suchen des nächstengelegenen Restaurants (Ähnlichkeit zwischen eigener Position und Position des Restaurants). Ein wichtiger Spezialfall ist die ''nächste-nachbar Suche''.&lt;br /&gt;
&lt;br /&gt;
* '''Graphensuche''': Hier wäre beispielsweise das Problem optimaler Wege zu nennen (Navigationssuche). Dieser Punkt wird später im Verlauf der Vorlesung noch einmal aufgegriffen werden.&lt;br /&gt;
&lt;br /&gt;
Im jetzt folgenden wird nur noch die ''Schlüsselsuche'' betrachtet werden.&lt;br /&gt;
&lt;br /&gt;
==Sequentielle Suche==&lt;br /&gt;
&lt;br /&gt;
Die ''sequentielle'' oder ''lineare'' Suche ist die einfachste Methode, einen Datensatz zu durchsuchen. Hierbei wird ein Array beispielsweise sequentiell von vorne nach hinten durchsucht. Ein prinzipieller Vorteil der Methode ist, dass auf der Eigenschaft der Datenelemente, nach denen das Array durchsucht wird, keine Ordnung im Sinne von &amp;gt; oder &amp;lt; definiert zu sein braucht, lediglich die Identität (==) muss feststellbar sein. Der folgende Python-Code zeigt, wie man sequentielle Suche einsetzen kann:&lt;br /&gt;
&lt;br /&gt;
 a = ... # array mit den zu durchsuchenden Elementen&lt;br /&gt;
 &lt;br /&gt;
 foundIndex = sequentialSearch(a, key) &lt;br /&gt;
 # foundIndex == -1 wenn nichts gefunden, 0 &amp;lt;math&amp;gt;\leq &amp;lt;/math&amp;gt; foundIndex &amp;lt; len(a) wenn key gefunden (erster Eintrag mit diesem Wert)&lt;br /&gt;
&lt;br /&gt;
Wir verwenden hier die Konvention, dass der zugehörige Arrayindex zurückgegeben wird, falls ein Element mit dem Schlüssel &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; gefunden wird (falls es mehrere solche Elemente gibt, wird das erste zurückgegeben). Das Ergebnis &amp;lt;tt&amp;gt;-1&amp;lt;/tt&amp;gt; signalisiert hingegen, dass kein solches Element gefunden wurde. Die Funktion &amp;lt;tt&amp;gt;sequentialSearch&amp;lt;/tt&amp;gt; kann folgendermaßen implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def sequentialSearch(a, key):&lt;br /&gt;
    for i in range(len(a)):&lt;br /&gt;
        if a[i] == key:  # bzw. allgemeiner a[i].key == key &lt;br /&gt;
            return i&lt;br /&gt;
    return -1&lt;br /&gt;
&lt;br /&gt;
Wir wollen jetzt die Komplexität dieses Algorithmus bestimmen, wobei die Problemgröße durch &amp;lt;tt&amp;gt;N = len(a)&amp;lt;/tt&amp;gt; gegeben ist. &lt;br /&gt;
&lt;br /&gt;
Dabei nimmt man an, dass der Vergleich in der inneren Schleife (&amp;lt;tt&amp;gt;a[i] == key&amp;lt;/tt&amp;gt;) jeweils &amp;lt;math&amp;gt; \mathcal{O}(1)&amp;lt;/math&amp;gt; ist (diese Annahme könnte verletzt sein, wenn der Vergleichsoperator eine komplizierte Berechnung mit höherer Komplexität ausführen muss). Bei einer erfolglosen Suche wird dieser Vergleich in der for-Schleife N-mal durchgeführt (&amp;lt;math&amp;gt; \mathcal{O}(N)&amp;lt;/math&amp;gt;), bei einer erfolgreichen Suche im Mittel (N/2)-mal (ebenfalls &amp;lt;math&amp;gt; \mathcal{O}(N)&amp;lt;/math&amp;gt;). Nach der Verschachtelungsregel erhält man also eine gesamte Komplexität von &amp;lt;math&amp;gt; \mathcal{O}(N)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Der Name ''lineare'' Suche rührt von diesem linearen Anwachsen der Komplexität mit der Arraygröße her.&lt;br /&gt;
&lt;br /&gt;
==Binäre Suche==&lt;br /&gt;
&lt;br /&gt;
Wie wir weiter unten zeigen werden, gestattet es diese Suchmethode, die Gesamtdauer der Suche in großen Datensätzen beträchtlich zu verringern. Die Methode beruht auf dem [http://de.wikipedia.org/wiki/Divide_and_Conquer Divide and Conquer-Prinzip], wobei die Suche in jedem Schritt rekursiv auf eine Hälfte des Datensatzes eingeschränkt wird. Weitere Details zur Methode sind [http://de.wikipedia.org/wiki/Bin%C3%A4re_Suche hier] zu finden. &lt;br /&gt;
&lt;br /&gt;
Die Methode ist nur dann anwendbar beziehungsweise effektiv, wenn folgendes gilt:&lt;br /&gt;
&lt;br /&gt;
# Auf der Eigenschaft der Daten, die zur Suche verwendet wird, ist eine Ordnung im Sinne von &amp;lt; oder &amp;gt; definiert.&lt;br /&gt;
# Wir wollen uns auf Datensätze beschränken, die schon fertig aufgebaut sind, in die also keine neuen Elemente mehr eingefügt werden, wenn man mit dem Suchen beginnt. Ist dies nicht der Fall, müsste nach jeder Einfügung das Array neu sortiert werden (unter diesen Umständen wäre die Verwendung eines [[Suchen#Suchb.C3.A4ume|Suchbaumes]] geschickter). &lt;br /&gt;
&lt;br /&gt;
Im Unterschied zur sequenziellen Suche müssen wir jetzt das Array sortieren bevor die Suchfunktion aufgerufen werden kann:&lt;br /&gt;
&lt;br /&gt;
 a = [...,...]     # array&lt;br /&gt;
 a.sort()   # sortiere über Ordnung des Schlüssels&lt;br /&gt;
 foundIndex = binSearch(a, key, 0, len(a))  # (Array, Schlüssel, von wo bis wo suchen im Array)&lt;br /&gt;
 # foundIndex == -1 wenn nichts gefunden, 0 &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt;  foundIndex &amp;lt; len(a) wenn key gefunden (erster Eintrag mit diesem Wert)&lt;br /&gt;
&lt;br /&gt;
Der folgende Algorithmus zeigt eine beispielhafte Implementierung der Methode:&lt;br /&gt;
&lt;br /&gt;
 def binSearch(a, key, start, end):  # start ist 1. Index, end ist letzter Index + 1&lt;br /&gt;
    size = end - start   # &amp;lt;math&amp;gt; \mathcal{O}(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
    if size &amp;lt;= 0:   # Bereich leer?  &amp;lt;math&amp;gt; \mathcal{O}(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
        return -1   # also nichts gefunden, &amp;lt;math&amp;gt; \mathcal{O}(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
    center = (start + end)/2   # Integer Division (d.h. Ergebnis wird abgerundet, wichtig für ganzzahlige Indizes) &amp;lt;math&amp;gt; \mathcal{O}(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
    if a[center] == key:  # &amp;lt;math&amp;gt; \mathcal{O}(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
        return center  # Schlüssel gefunden, &amp;lt;math&amp;gt; \mathcal{O}(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
    elif a[center] &amp;lt; key:  &amp;lt;math&amp;gt; \mathcal{O}(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
        return binSearch(a, key, center + 1, end)  # Rekursion in die rechte Teilliste&lt;br /&gt;
    else:&lt;br /&gt;
        return binSearch(a, key, start, center)  # Rekursion in die linke Teilliste&lt;br /&gt;
&lt;br /&gt;
Zur Berechnung der Komplexität dieses Algorithmus vernachlässigen wir zunächst den Aufwand, den die Sortierung verursacht (wir diskutieren unten, wann dies nicht zulässig ist). Wir setzen &amp;lt;tt&amp;gt;N = len(a)&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Im obigen Code ist zu erkennen, dass fast alle Anweisungen des Algorithmus die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;. Nach der Sequenzregel hat auch deren Hintereinanderausführung die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;.  Es bleibt die Komplexität der Rekursion zu berechnen. Die gesamte Komplexität des Algorithmus (jetzt als Funktion f bezeichnet) setzt sich zusammen aus den oben erwähnten &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;-Anweisungen sowie der Rekursion auf einem Teilarray der halben Größe &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;f(N) = \mathcal{O}(1) + f(N/2) = \mathcal{O}(1) + \mathcal{O}(1) + f(N/4) = ... = \underbrace{\mathcal{O}(1) + ... + \mathcal{O}(1) + \underbrace{f(0)}_{\mathcal{O}(1)\, \rightarrow \,\mathrm{size-Abfrage}}}_{n+1 \,\mathrm{Terme}} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Zur Vereinfachung nehmen wir an &amp;lt;math&amp;gt; N = 2^n &amp;lt;/math&amp;gt;, so dass gilt&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \rightarrow f(N) = \mathcal{O}(1) \cdot \mathcal{O}(n+1) = \mathcal{O}(n) = \mathcal{O}(\lg N) &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für große Datenmengen ist die ''binäre Suche'' also weit effizienter als die ''lineare Suche''. Verdoppelt sich beispielsweise die zu durchsuchende Datenmenge, so verdoppelt sich der Aufwand für die ''sequentielle Suche'' - bei der ''binären Suche'' hingegen benötigt man lediglich eine zusätzliche Vergleichsoperation. &lt;br /&gt;
&lt;br /&gt;
Für kleine Daten (&amp;lt;math&amp;gt; N = 4,\, 5 &amp;lt;/math&amp;gt;) ist die ''sequentielle Suche'' jedoch schneller als die ''binäre Suche'', da hier die rekursiven Funktionsaufrufe teurer als das Mehr an Vergleichen sind. Ein anderer ungünstiger Fall ist gegeben, wenn nur sehr wenige Suchanfragen erfolgen (weniger als &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt; viele). Dann wird der Aufwand durch das Sortieren des Arrays dominiert, ist also &amp;lt;math&amp;gt;\mathcal{O}(N \lg N) &amp;lt;/math&amp;gt;. Auch dann ist sequentielle Suche vorzuziehen.&lt;br /&gt;
&lt;br /&gt;
Eine relativ einfache Möglichkeit, die ''binäre Suche'' zu verbessern, ist die sogenannte ''Interpolationssuche''. Hierbei wird die neue Position für die Suche, also die Mitte des Arrays, durch eine Schätzung ersetzt, die angibt, wo sich der Schlüssel innerhalb des Arrays befinden könnte. Bei der Suche in einem Telefonbuch nach dem Namen Zebra würde man ja auch nicht in der Mitte anfangen. Näheres hierzu im Buch von ''Sedgewick''.&lt;br /&gt;
&lt;br /&gt;
Um sich den Algorithmus der ''binären Suche'' klar zu machen, ist es instruktiv, sich die folgende Tabelle genauer anzusehen, die die sukzessive Belegung der Variablen bei verschiedenen Anfragen beschreibt. Die Testfälle wurden nach dem Prinzip des ''domain partitioning'' gewählt. Das zugehörige Array hat die Einträge&lt;br /&gt;
&lt;br /&gt;
 a = [2, 3, 4, 5, 6]&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:center&amp;quot; border=&amp;quot;1&amp;quot; cellpadding=&amp;quot;5&amp;quot; cellspacing=&amp;quot;0&amp;quot; &lt;br /&gt;
! gesuchter key   !!  start      !! end  !! size !! center !! return &amp;lt;br/&amp;gt; (-1 oder index)  !! Kommentare  &lt;br /&gt;
|- bgcolor=&amp;quot;#e0e0e0&amp;quot;&lt;br /&gt;
| 4     ||0            || 5    ||  5   || 2   ||  2         || gefunden&lt;br /&gt;
|-&lt;br /&gt;
| 2     || 0           || 5    ||  5   || 2   ||            || linker Randfall&lt;br /&gt;
|-&lt;br /&gt;
|       ||0            || 2    ||  2   || 1   ||            ||           &lt;br /&gt;
|-&lt;br /&gt;
|       ||  0          || 1    ||  1   || 0   ||  0         || gefunden&lt;br /&gt;
|- bgcolor=&amp;quot;#e0e0e0&amp;quot;&lt;br /&gt;
| 1     ||0            || 5    ||  5   || 2   ||            || links außerhalb&lt;br /&gt;
|- bgcolor=&amp;quot;#e0e0e0&amp;quot;&lt;br /&gt;
|       ||0            || 2    ||  2   || 1   ||            ||&lt;br /&gt;
|- bgcolor=&amp;quot;#e0e0e0&amp;quot;&lt;br /&gt;
|       ||0            || 1    ||  1   || 0   ||            ||&lt;br /&gt;
|- bgcolor=&amp;quot;#e0e0e0&amp;quot;&lt;br /&gt;
|       ||0            || 0    ||  0   ||     || -1         || nichts gefunden&lt;br /&gt;
|-&lt;br /&gt;
| 6     ||0            || 5    ||  5   || 2   ||            || rechter Randfall&lt;br /&gt;
|-&lt;br /&gt;
|       ||  3          || 5    || 2    || 4   || 4          || gefunden&lt;br /&gt;
|- bgcolor=&amp;quot;#e0e0e0&amp;quot;&lt;br /&gt;
| 5     ||0            || 5    ||  5   || 2   ||            || typischer Fall&lt;br /&gt;
|- bgcolor=&amp;quot;#e0e0e0&amp;quot;&lt;br /&gt;
|       ||3            || 5    ||  2   || 4   ||            ||  &lt;br /&gt;
|- bgcolor=&amp;quot;#e0e0e0&amp;quot;&lt;br /&gt;
|       || 3           || 4    ||  1   || 3   || 3          || gefunden&lt;br /&gt;
|- &lt;br /&gt;
| 7     ||0            || 5    ||  5   || 2   ||            || rechts außerhalb        &lt;br /&gt;
|-&lt;br /&gt;
|       ||  3          || 5    ||  2   || 4   ||            ||&lt;br /&gt;
|-&lt;br /&gt;
|       ||5            || 5    ||  0   ||     || -1         || nichts gefunden&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Suchbäume ==&lt;br /&gt;
&lt;br /&gt;
Effiziente Suchalgorithmen kann man elegent mit Hilfe von Binärbäumen realisieren. Eine kurze Einführung in Binärbäume findet man [http://de.wikipedia.org/wiki/Bin%C3%A4rbaum hier]. Die Skizze erläutert wichtige Begriffe:&lt;br /&gt;
&lt;br /&gt;
[[Image:Baum.png|text-top|300x300px|Zur Illustration von Bäumen]]&lt;br /&gt;
&lt;br /&gt;
Bäume sind zweidimensional verkettete Strukturen. Sie gehören zu den fundamentalen Datenstrukturen in der Informatik. Da man in Bäumen nicht nur Daten speichern kann, sondern auch relevante Beziehungen der Daten untereinander, festgelegt über eine Ordnung auf der vergleichenden Dateneigenschaft (''Schlüssel''), eignen sich Bäume also insbesondere, um gesuchte Daten schnell wieder auffinden zu können.&lt;br /&gt;
&lt;br /&gt;
Ein ''Binärbaum'' wie oben skizziert besteht aus einer Menge von ''Knoten'', die untereinander durch ''Kanten'' verbunden sind. Jeder Knoten hat einen linken und einen rechten Unterbaum, der auch leer sein kann (in Python ließe sich dies mit ''None'' implementieren). Führt eine Kante von Knoten A zu Knoten B, so heißt A Vater von B und B Kind von A. Es gibt genau einen Knoten ohne Vater, den man ''Wurzel'' nennt. Knoten ohne Kinder heißen ''Blätter''.&lt;br /&gt;
&lt;br /&gt;
Ein ''Suchbaum'' hat zusätzlich die Eigenschaft, dass die Schlüssel jedes Knotens sortiert sind: &lt;br /&gt;
;Suchbaumbedingung: Für jeden Knoten des Binärbaumes gilt: &amp;lt;b&amp;gt;Alle&amp;lt;/b&amp;gt; Schlüssel im linken Unterbaum sind kleiner als der Schlüssel des gegebenen Knotens, &amp;lt;b&amp;gt;alle&amp;lt;/b&amp;gt; Schlüssel im rechten Unterbaum sind größer. Wir wollen hierbei annehmen, dass jeder Schlüssel pro Datensatz nur einmal vorkommt, da sich sonst die &amp;gt;- oder &amp;lt;-Relation nicht mehr strikt erfüllen ließe.&lt;br /&gt;
Mit anderen Worten: der maximale Schlüssel des linken Teilbaums, der Schlüssel des gegebenen Knotens, sowie der minimale Schlüssel des rechten Teilbaums sind in dieser Reihenfolge sortiert, und dies muss für alle Knoten und deren Teilbäume (falls sie existieren) gelten.&lt;br /&gt;
&lt;br /&gt;
Um die Verwendung eines Suchbaums zu motivieren, wollen wir von zwei Annahmen ausgehen:&lt;br /&gt;
# Einfügen und Suchen im Baum wechseln sich ab. (Wenn das Suchen erst beginnt, nachdem alle Einfügungen erfolgt sind, wäre ein dynamisches Array mit [[Suchen#Bin.C3.A4re_Suche|binärer Suche]] wesentlich einfacher.)&lt;br /&gt;
# Der Schlüssel, der die Anordnung bestimmt, kennt eine [http://de.wikipedia.org/wiki/Ordnungsrelation Ordnung] (&amp;lt;-Relation oder &amp;gt;-Relation).&lt;br /&gt;
&lt;br /&gt;
Zunächst definieren wir eine Knotenklasse für den Suchbaum:&lt;br /&gt;
 &lt;br /&gt;
 class Node:&lt;br /&gt;
     def __init__(self, key):&lt;br /&gt;
         self.key = key&lt;br /&gt;
         self.left = self.right = None&lt;br /&gt;
&lt;br /&gt;
=== Suche in einem Binärbaum ===&lt;br /&gt;
&lt;br /&gt;
Wir nehmen nun an, dass der Baum durch eine Referenz auf den Wurzelknoten &amp;lt;tt&amp;gt;root&amp;lt;/tt&amp;gt; gegeben ist. Dann kann man folgendermassen suchen:&lt;br /&gt;
  &lt;br /&gt;
 root = ...    # Wurzel des Suchbaums&lt;br /&gt;
 nodeFound = treeSearch(root, key)   # None, falls nichts gefunden&lt;br /&gt;
 &lt;br /&gt;
Hier verwenden wir die Konvention, dass der passende Knoten zurückgegeben wird, falls &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; gefunden wurde, oder &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; andernfalls. Die Suchfunktion wird rekursiv implementiert:&lt;br /&gt;
&lt;br /&gt;
 def treeSearch(node, key):&lt;br /&gt;
     if node is None:&lt;br /&gt;
         return None&lt;br /&gt;
     elif node.key == key: # gefunden&lt;br /&gt;
         return node       # =&amp;gt; Knoten zurückgeben&lt;br /&gt;
     elif key &amp;lt; node.key:  # gesuchter Schlüssel ist kleiner&lt;br /&gt;
         return treeSearch(node.left, key)  # =&amp;gt; im linken Unterbaum weitersuchen&lt;br /&gt;
     else:                 # andernfalls &lt;br /&gt;
         return treeSearch(node.right, key) # =&amp;gt; im rechten Unterbaum weitersuchen&lt;br /&gt;
&lt;br /&gt;
=== Einfügen in einen Binärbaum ===&lt;br /&gt;
&lt;br /&gt;
Bevor wir den Einfügealgorithmus implementieren, müssen wir festlegen, was passieren soll, wenn der einzufügende Schlüssel schon vorhanden ist. Mehrere Möglichkeiten bieten sich an:&lt;br /&gt;
* Fehler signalisieren (exception auslösen)&lt;br /&gt;
* nichts einfügen&lt;br /&gt;
* nichts einfügen, aber einen boolean zurückgeben (false wenn nichts eingefügt wurde, true wenn etwas einfügt wurde)&lt;br /&gt;
* nochmals einfügen (z.B. kann man die Klasse Node oben durch einen Zähler erweitern, der angibt, wie oft der betreffende Schlüssel bereits eingefügt wurde)&lt;br /&gt;
&lt;br /&gt;
Die ersten 3 Punkte realisieren eine Mengensemantik, der letzte eine Multimenge. Wir entscheiden uns hier für Möglichkeit 2 (nichts einfügen). Das Prinzip des Einfügens besteht darin, im Baum dorthin abzusteigen, wo der Schlüssel sich befinden müsste (wie bei &amp;lt;tt&amp;gt;treeSearch&amp;lt;/tt&amp;gt;), und dann an der betreffenden Stelle einen neuen Blattknoten zu erzeugen. Die Funktion gibt ein Knotenobjekt zurück, damit die Verkettungen im Elternknoten entsprechend angepasst werden können:&lt;br /&gt;
&lt;br /&gt;
 def treeInsert(node, key):&lt;br /&gt;
     if node is None:      # richtiger Platz gefunden&lt;br /&gt;
         return Node(key)  # =&amp;gt; neuen Knoten einfügen&lt;br /&gt;
     if node.key == key:   # schon vorhanden&lt;br /&gt;
         return node       # =&amp;gt; nichts tun&lt;br /&gt;
     elif key &amp;lt; node.key:     &lt;br /&gt;
         node.left = treeInsert(node.left, key) # im linken Teilbaum einfügen&lt;br /&gt;
     else:&lt;br /&gt;
         node.right = treeInsert(node.right, key) # im rechten Teilbaum einfügen&lt;br /&gt;
     return node&lt;br /&gt;
&lt;br /&gt;
Ein Binärbaum wird aufgebaut, indem &amp;lt;tt&amp;gt;treeInsert&amp;lt;/tt&amp;gt; für jeden Schlüssel aufgerufen wird. Wir verwenden hier ganze Zahlen als Schlüssel. Am Anfang ist der Baum leer:&lt;br /&gt;
&lt;br /&gt;
 root = None&lt;br /&gt;
 root = treeInsert(root, 4)&lt;br /&gt;
 root = treeInsert(root, 2)&lt;br /&gt;
 root = treeInsert(root, 3)&lt;br /&gt;
 root = treeInsert(root, 6)&lt;br /&gt;
&lt;br /&gt;
=== Entfernen aus einem Binärbaum ===&lt;br /&gt;
Wir legen wiederum zuerst fest, was im Fehlerfall passieren soll, d.h. wenn der Schlüssel nicht vorhanden ist:&lt;br /&gt;
* Auslösen einer Exception (KeyError)&lt;br /&gt;
* nichts löschen&lt;br /&gt;
* nichts löschen, aber ein boolean zurückgeben, das dies signalisiert.&lt;br /&gt;
&lt;br /&gt;
Wir entscheiden uns wieder für Möglichkeit 2. Beim Entfernen eines Knotens unterscheiden wir nun 3 Fälle:&lt;br /&gt;
# node, welcher &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; enthält, ist ein Blatt =&amp;gt; kann einfach gelöscht werden&lt;br /&gt;
# node hat &amp;lt;u&amp;gt;nur&amp;lt;/u&amp;gt; linken Unterbaum oder &amp;lt;u&amp;gt;nur&amp;lt;/u&amp;gt; rechten Unterbaum =&amp;gt; durch Unterbaum ersetzen&lt;br /&gt;
# node hat beide Unterbäume:&lt;br /&gt;
#* Suche Vorgänger: &amp;lt;math&amp;gt;\max_{k &amp;lt; key} (k \in keys)&amp;lt;/math&amp;gt; =&amp;gt; ersetze node durch seinen Vorgänger und entferne Vorgänger. (Dies führt zu einem effizienten Algorithmus, weil der Vorgänger immer zu Fall 1 oder Fall 2 gehört. Wenn er nämlich einen rechten Unterbaum hätte, könnte er nicht der Vorgänger sein.)&lt;br /&gt;
&lt;br /&gt;
Die Funktion, die den Vorgänger sucht, muss den größten Knoten im lnken Unterbaum suchen. Da diese Funktion nur in Fall 3 aufgerufen wird, gibt es den linken Unterbaum immer.&lt;br /&gt;
 def treePredecessor(node):&lt;br /&gt;
     node = node.left&lt;br /&gt;
     while node.right is not None:&lt;br /&gt;
         node = node.right&lt;br /&gt;
     return node&lt;br /&gt;
&lt;br /&gt;
Die oben angegebenen Fälle werden durch folgende Funktion realisiert:&lt;br /&gt;
&lt;br /&gt;
 def treeRemove(node, key):&lt;br /&gt;
     if node is None:   # key nicht vorhanden&lt;br /&gt;
         return node    # =&amp;gt; nichts tun&lt;br /&gt;
     if key &amp;lt; node.key: &lt;br /&gt;
         node.left = treeRemove(node.left, key)&lt;br /&gt;
     elif key &amp;gt; node.key:&lt;br /&gt;
         node.right = treeRemove(node.right, key)&lt;br /&gt;
     else:              # key gefunden&lt;br /&gt;
         if node.left is None and node.right is None:     # Fall 1&lt;br /&gt;
             node = None            &lt;br /&gt;
         elif node.left is None:     # Fall 2&lt;br /&gt;
             node = node.right       # +&lt;br /&gt;
         elif node.right is None:    # Fall 2&lt;br /&gt;
             node = node.left&lt;br /&gt;
         else:                       # Fall 3&lt;br /&gt;
             pred = treePredecessor(node)&lt;br /&gt;
             node.key = pred.key&lt;br /&gt;
             node.left = treeRemove(node.left, pred.key)&lt;br /&gt;
     return node&lt;br /&gt;
&lt;br /&gt;
=== Komplexitätsanalyse ===&lt;br /&gt;
&lt;br /&gt;
Um die Komplexität der Operationen auf einem Binärbaum zu bestimmen, müssen wir zunächst einige weitere Begriffe einführen:&lt;br /&gt;
;Pfad: Ein Pfad zwischen zwei Knoten node&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; und node&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; ist eine Folge von Knoten node&amp;lt;sub&amp;gt;k1&amp;lt;/sub&amp;gt;,...,node&amp;lt;sub&amp;gt;kn&amp;lt;/sub&amp;gt;, so dass:&lt;br /&gt;
:* node&amp;lt;sub&amp;gt;k1&amp;lt;/sub&amp;gt; == node&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:* node&amp;lt;sub&amp;gt;kn&amp;lt;/sub&amp;gt; == node&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;&lt;br /&gt;
:* node&amp;lt;sub&amp;gt;ki&amp;lt;/sub&amp;gt; und node&amp;lt;sub&amp;gt;ki+1&amp;lt;/sub&amp;gt; haben eine gemeinsame Kante.&lt;br /&gt;
[[Image:Baum_Pfad.png]]&lt;br /&gt;
Ein Baum ist definiert als ein Graph, in dem es zwischen beliebigen Knoten stets genau einen Pfad gibt.&lt;br /&gt;
&lt;br /&gt;
;Länge eines Pfades: Anzahl der Kanten im Pfad (= Anzahl der Knoten - 1)&lt;br /&gt;
;Tiefe eines Knotens: Pfadlänge vom Knoten zur Wurzel des Baumes (die Wurzel hat also die Tiefe 0)&lt;br /&gt;
;Tiefe des Baumes: maximale Tiefe eines Knotens&lt;br /&gt;
&lt;br /&gt;
Allen Baumoperationen ist gemeinsam, dass sie entlang genau eines Pfades im Baum absteigen (welcher Pfad dies ist ergibt sich aus der Ordnung der Schlüssel). Der Abstieg endet, wenn entweder der gesuchte Schlüssel gefunden wird, oder wenn erkannt wird, dass der Schlüssel nicht vorhanden ist (wenn das Kind, wo der Schlüssel sein müsste, den Wert &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; hat). Während des Abstiegs werden in jedem Knoten nur Anweisungen ausgeführt, die konstante Zeit benötigen (1 Vergleich, wenn die Suche in dem Knoten erfolglos beendet wird, 2 Vergleiche, wenn der Schlüssel gefunden wird, und 3 Vergleiche, wenn im rechten oder linken Teilbaun weiter abgestiegen werden muss). Daraus folgt, dass die Suche im ungünstigsten Fall die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(T)&amp;lt;/math&amp;gt; hat, wobei T die Tiefe des Baumes (= längster Pfad, der durchlaufen werden kann) ist.&lt;br /&gt;
&lt;br /&gt;
==== Ungünstigster Fall für die Baumoperationen ====&lt;br /&gt;
&lt;br /&gt;
Um den ungünstigsten Fall für die Baumoperationen zu finden, müssen wir offensichtlich herausfinden, wie groß die Tiefe maximal werden kann. Es ist leicht zu erkennen, dass die Tiefe maximiert wird, wenn man sortierte Daten in den Baum einfügt:&lt;br /&gt;
* Fügt man [1,2,3,4,5] in dieser Reihenfolge ein, muss man bei &amp;lt;tt&amp;gt;treeInsert&amp;lt;/tt&amp;gt; stets in den rechten Teilbaum absteigen (weil der nächste Schlüssel immer größer als der größte bisherige Schlüssel ist) und dort ein rechtes Kind einfügen. Es ergibt sich folgender Baum:&amp;lt;br /&amp;gt; [[Image:Balance.png]]&lt;br /&gt;
: Dieser Baum hat die Tiefe 4. Die Funktion &amp;lt;tt&amp;gt;treeSerach&amp;lt;/tt&amp;gt; verhält sich dann wie sequentielle Suche, man hat also durch die Verwendung des Suchbaums nichts gewonnen. &lt;br /&gt;
Allgemein gilt: Alle Operationen eine binären Suchbaums haben im ungünstigsten Fall die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;, wo N die Anzahl der Elemente im Baum bezeichnet. Eine offensichtliche Lösung der Problems besteht darin, die Elemente nicht in einer so ungünstigen Reihenfolge einzufügen (siehe Übungsaufgabe 5.1.c). Allerdings ist dies nicht immer möglich. Abhilfe schaffen dann selbst-balancierende Bäume.&lt;br /&gt;
&lt;br /&gt;
==Selbst-balancierende Suchbäume==&lt;br /&gt;
&lt;br /&gt;
=== Balance eines Suchbaumes ===&lt;br /&gt;
&lt;br /&gt;
Um die Komplexität der Suchbaum-Operationen zu minimieren, müssen wir die Höhe des Baumes minimieren. Wir wollen also die Länge des längsten Pfades verkürzen, ohne dass ein anderer Pfad dadurch unnötig lang wird. Mit anderen Worten wollen wir erreichen, dass alle Pfade von der Wurzel zu den Blättern ungefährt die gleiche Länge haben. Diese Idee kann man formal durch den Begriff der ''Balance'' eines Suchbaums fassen. Um die Balance zu definieren, betrachten wir &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; als zusätzlichen Knoten, als sogenannten '''Sentinel''' (engl. für ''Wächter''). Der sentinel-Knoten wird als rechter oder linker Nachfolger verlinkt, wenn der entsprechende Nachfolger nicht durch einen echten Knoten belegt ist:&lt;br /&gt;
&lt;br /&gt;
[[Image:sentinel.png|400px|right]]&lt;br /&gt;
&lt;br /&gt;
Wir definieren nun:&lt;br /&gt;
;RS-Pfade: Pfad von ''root'' &amp;amp;rarr; ''sentinel''. In jedem Binärbaum gibt es mehrere RS-Pfade.&lt;br /&gt;
;Balance eines Baumes: Differenz zwischen der Länge des längsten und kürzesten RS-Pfads:&lt;br /&gt;
:::&amp;lt;math&amp;gt; B = \max_{P\in\{RS\}} |P| - \min_{P\in\{RS\}} |P|&amp;lt;/math&amp;gt;&lt;br /&gt;
:wobei &amp;lt;math&amp;gt;\{RS\}&amp;lt;/math&amp;gt; die Menge aller RS-Pfade bezeichnet, und |P| die Länge des Pfades P.&lt;br /&gt;
;vollständiger Baum:  Balance &amp;lt;math&amp;gt;B=0&amp;lt;/math&amp;gt;&lt;br /&gt;
:Daraus folgt, dass alle Knoten (außer den Blättern) 2 Kinder haben müssen.&lt;br /&gt;
;perfekt balancierter Baum:  Balance  &amp;lt;math&amp;gt;B \le 1&amp;lt;/math&amp;gt;&lt;br /&gt;
::alternative Definition für perfekt balancierte Bäume: Für jeden Knoten gilt, dass der rechte und linke Unterbaum ebenfalls perfekt balancierte Bäume sind und ihre Höhe sich höchstens um '''1''' unterscheidet. Leere Unterbäume sind per Definition perfekt balanciert und haben die Höhe Null.&lt;br /&gt;
&lt;br /&gt;
====Größe eines Baumes in Abhängigkeit von Balance und Tiefe====&lt;br /&gt;
[[Image:Baum_voll.png|400px|right]]&lt;br /&gt;
;vollständiger Baum:&lt;br /&gt;
Aus der Abbildung erkennt man, dass Ebene k eines vollständigen Baumes stets 2&amp;lt;sup&amp;gt;k&amp;lt;/sup&amp;gt; Knoten enthält (der grüne Knoten gehört nicht zum vollständigen Baum). Hat der Baum die Tiefe d, dann enthält er &lt;br /&gt;
&lt;br /&gt;
::N = 2&amp;lt;sup&amp;gt;0&amp;lt;/sup&amp;gt; + 2&amp;lt;sup&amp;gt;1&amp;lt;/sup&amp;gt;.....+ 2&amp;lt;sup&amp;gt;d&amp;lt;/sup&amp;gt; = 2&amp;lt;sup&amp;gt;d+1&amp;lt;/sup&amp;gt; - 1 &lt;br /&gt;
&lt;br /&gt;
Knoten (und damit ebensoviele Datenelemente).&lt;br /&gt;
&lt;br /&gt;
;perfekt balancierter Baum:&lt;br /&gt;
Für eine gegebene Tiefe d kann kein Baum mehr Elemente enthalten als der entsprechende vollständige Baum. Also gilt für jeden perfekt balancierten Baum der Größe N:&lt;br /&gt;
:::&amp;lt;math&amp;gt; N \le 2^{d+1} - 1&amp;lt;/math&amp;gt;&lt;br /&gt;
Der kleinste perfekt balancierte Baum der Tiefe d ist ein vollständiger Baum der Tiefe d-1 (mit &amp;lt;math&amp;gt;2^{(d-1)+1} - 1&amp;lt;/math&amp;gt; Knoten), wo an einem einzigen Knoten noch ein weiteres Datenelement angehängt wurde (grüner Knoten in der Abbildung). Dieser Baum enthält&lt;br /&gt;
:::&amp;lt;math&amp;gt;N = \left(2^{(d-1)+1} - 1\right) + 1 = 2^d&amp;lt;/math&amp;gt;&lt;br /&gt;
Datenelemente. Folglich gilt für perfekt balancierte Bäume die Ungleichung&lt;br /&gt;
:::&amp;lt;math&amp;gt;2^d \le N \le 2^{d+1} - 1&amp;lt;/math&amp;gt;&lt;br /&gt;
und demzufolge auch&lt;br /&gt;
:::&amp;lt;math&amp;gt;\log_2(2^d) \le \log_2(N) \le \log_2(2^{d+1} - 1) &amp;lt; \log_2(2^{d+1})&amp;lt;/math&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;d \le \log_2(N) &amp;lt; d+1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Da die Baumoperationen im ungünstigsten Fall die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(d)&amp;lt;/math&amp;gt; haben, gilt für perfekt balancierte Bäume, dass alle Operationen im schlechtesten Fall die Komplexität&lt;br /&gt;
:::&amp;lt;math&amp;gt;\mathcal{O}(\log(N))&amp;lt;/math&amp;gt;&lt;br /&gt;
haben, das ist ''logarithmische Komplexität''. Ein perfekt balancierter Baum wird z.B. durch die Datenstruktur des [http://en.wikipedia.org/wiki/AVL_tree AVL-Baums] realisiert. Die Implementation eines AVL-Baums ist jedoch kompliziert, und es zeigt sich, dass die Eigenschaft der perfekten Balance gar nicht notwendig ist, um logarithmische Komplexität zu garantieren. Wir definieren:&lt;br /&gt;
;balancierter Baum: Für die Tiefe d(N) eines balancierten Baumes mit N Knoten gilt&lt;br /&gt;
:::&amp;lt;math&amp;gt;\forall  N:d(N)\le c \cdot d_{PB}(N)&amp;lt;/math&amp;gt; mit &amp;lt;math&amp;gt;1 \le c &amp;lt; \infty&amp;lt;/math&amp;gt;&lt;br /&gt;
:wobei d&amp;lt;sub&amp;gt;PB&amp;lt;/sub&amp;gt;(N) die Tiefe eines perfekt balancierten Baumes mit N Knoten ist. Für die Komplexität der Operationen in einem balancierten Baum gilt dann:&lt;br /&gt;
:::&amp;lt;math&amp;gt;f(N) \le  c\cdot f_{PB}(N) = c\, \mathcal{O}(\log(N)) = \mathcal{O}(\log(N))&amp;lt;/math&amp;gt;&lt;br /&gt;
d.h. die Komplexität ändert sich nicht. Balancierte Bäume sind fast genauso schnell wie perfekt balancierte Bäume (bis auf den Faktor c), aber ihr Aufbau ist algorithmisch einfacher.&lt;br /&gt;
&lt;br /&gt;
===Idee selbst-balancierende Bäume===&lt;br /&gt;
&lt;br /&gt;
Die grundlegende Idee der selbst-balancierenden Bäume besteht darin, nach jeder Einfügung die Balance des Baumes zu optimieren. Dies geschieht am zweckmäßigsten im aufsteigenden Zweig der Rekursion, also nach der Rückkehr von den rekursiven Aufrufen der Funktion &amp;lt;tt&amp;gt;treeInsert&amp;lt;/tt&amp;gt;. Dies entspricht folgendem Pseudo-Code:&lt;br /&gt;
&lt;br /&gt;
  def insertTree(node,key):&lt;br /&gt;
      if node is None: &lt;br /&gt;
          return Node(key)       &lt;br /&gt;
      if node.key == key:&lt;br /&gt;
          return node&lt;br /&gt;
      if key &amp;lt; node.key:&lt;br /&gt;
          node.left  = insertTree(node.left, key)&lt;br /&gt;
      else:&lt;br /&gt;
          node.right = insertTree(node.right, key)    &lt;br /&gt;
      &amp;lt;font color=&amp;quot;red&amp;quot;&amp;gt;optimiere die Balance hier&amp;lt;/font&amp;gt;&lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Dabei muss man beachten, dass bei den Optimierungen die Suchbaumbedingung (Definition siehe oben) erhalten bleibt. Dies ist garantiert, wenn alle Umstrukturierungen durch die elementare Operation der ''Rotation'' implementiert werden. Eine ''Rechtsrotation'' ersetzt die Wurzel &amp;lt;tt&amp;gt;n&amp;lt;/tt&amp;gt; eines Teilbaumes durch sein linkes Kind, und fügt die alte Wurzel als rechtes Kind der neuen Wurzel ein. Die ''Linksrotation'' ist die Inverse dieser Operation. Die Abbildung verdeutlicht die Umstrukturierungen:&lt;br /&gt;
&lt;br /&gt;
[[Image:Baum_Rotation.png]]&lt;br /&gt;
&lt;br /&gt;
Die Rotationen werden wie folgt implementiert:&lt;br /&gt;
&lt;br /&gt;
 def rotateRight(node):&lt;br /&gt;
     newRoot = node.left&lt;br /&gt;
     node.left = newRoot.right&lt;br /&gt;
     newRoot.right = node&lt;br /&gt;
     return newRoot&lt;br /&gt;
&lt;br /&gt;
 def rotateLeft(node):&lt;br /&gt;
     newRoot = node.right&lt;br /&gt;
     node.right = newRoot.left&lt;br /&gt;
     newRoot.left = node&lt;br /&gt;
     return newRoot&lt;br /&gt;
&lt;br /&gt;
Man erkennt leicht, dass die Suchbaumbedingung erhalten bleibt. Wir erläutern dies für die Rechtsrotation, bei der Linksrotation gilt die Erklärung entsprechend. Knoten ''n'' hat einen größeren Schlüssel als Knoten ''L'', denn ''L'' ist vor der Rechtsrotation das linke Kind von ''n''. Nach der Rotation ist ''n'' deshalb korrekterweise das rechte Kind von ''L''. Weiter gilt für den Teilbaum mit der Wurzel ''LR'', dass er größer als ''L'' ist (denn er ist das rechte Kind von ''L''), aber kleiner als ''n'' (denn er liegt im linken Teilbaum von ''n''). Nach der Rechtsrotation ist diese Bedingung immer noch erfüllt, denn ''LR'' ist jetzt linker Teilbaum von ''n'', welches wiederum rechter Teilbaum von ''L'' geworden ist. Alle anderen Teilbäume sind von der Rotation nicht betroffen.&lt;br /&gt;
&lt;br /&gt;
Verschiedene Arten von selbst-balancierenden Bäumen unterscheiden sich im Wesentlichen dadurch, wann welche Rotation ausgeführt wird. Wichtige Beispiele sind&lt;br /&gt;
* [http://en.wikipedia.org/wiki/AVL_tree AVL-Bäume] (älteste Variante)&lt;br /&gt;
* [http://en.wikipedia.org/wiki/Red_black_tree Rot-Schwarz-Bäume] (verbreitetste Variante)&lt;br /&gt;
* [http://en.wikipedia.org/wiki/Treap Treaps] (flexibelste Variante, siehe Übung 6.1)&lt;br /&gt;
* [http://en.wikipedia.org/wiki/Splay_tree Splay trees]&lt;br /&gt;
* [http://en.wikipedia.org/wiki/AA_tree Andersson-Bäume] (einfachste Variante, siehe unten)&lt;br /&gt;
&lt;br /&gt;
Daneben wird gern die [http://en.wikipedia.org/wiki/Skip_list Skip List] verwendet, die aber kein Binärbaum ist, sondern auf einem anderen Prinzip beruht.&lt;br /&gt;
&lt;br /&gt;
===Andersson-Bäume===&lt;br /&gt;
&lt;br /&gt;
Jeder selbst-balancierende Baum benötigt Zusatzinformationen, die die augenblickliche Balance beschreiben, so dass diese gegebenenfalls optimiert werden kann. Der Andersson-Baum fügt zu diesem Zweck in jedem Knoten ein neues Feld ''level'' ein, welches mit 1 initialisiert wird:&lt;br /&gt;
&lt;br /&gt;
  class AnderssonNode:&lt;br /&gt;
    def__init__(self, key):&lt;br /&gt;
        self.key = key&lt;br /&gt;
        self.left = self.right = None&lt;br /&gt;
        self.level = 1&lt;br /&gt;
&lt;br /&gt;
Grob gesprochen kodiert das ''level''-Feld den Abstand des Knotens vom Sentinel. Genauer gelten folgende&lt;br /&gt;
&lt;br /&gt;
====Regeln====&lt;br /&gt;
&lt;br /&gt;
* Es gibt vertikale Kanten (parent.level == child.level + 1 ) und horizontale Kanten (parent.level == child.level). &lt;br /&gt;
* Die ''reduzierte Länge'' eines Pfades zwischen zwei Knoten wird berechnet, indem nur die vertikalen Kanten im Pfad gezählt werden.&lt;br /&gt;
* Das Sentinel hat ''level = 0''. Alle Kanten zum Sentinel sind vertikal.&lt;br /&gt;
* Die ''reduzierte Höhe'' eines Knotens entspricht der reduzierten Länge des Pfades von diesem Knoten zum Sentinel. Das ''level''-Feld jedes Knotens speichert die reduzierte Höhe dieses Knotens. Folglich gilt für alle Knoten, die direkt mit dem Sentinel verbunden sind, ''level = 1''. Insbesondere gilt dies auch für neu eingefügte Knoten (siehe obige Initialisierung).&lt;br /&gt;
&lt;br /&gt;
Die nächsten zwei Regeln sichern die Balance:&lt;br /&gt;
* Alle RS-Pfade haben die gleiche reduzierte Länge. Dies ist äquivalent zu der Bedingung, dass die Wurzel des Andersson-Baumes über alle möglichen RS-Pfade auf dem gleichen Level erreicht wird.&lt;br /&gt;
* Kein Pfad hat 2 aufeinander folgende horizontale Kanten. &lt;br /&gt;
&lt;br /&gt;
Die letzte Regel führt zu starken algorithmischen Vereinfachungen gegenüber den konzeptionell sehr ähnlichen Rot-Schwarz-Bäumen:&lt;br /&gt;
* Nur Kanten zum rechten Kind dürfen horizontal sein.&lt;br /&gt;
&lt;br /&gt;
Das folgende Bild zeigt einen Andersson-Baum, bei dem allerdings nicht alle Verbindungen zum Sentinel eingezeichnet sind:&lt;br /&gt;
&lt;br /&gt;
[[Image:Abild.png]]&lt;br /&gt;
&lt;br /&gt;
Es gilt folgender&lt;br /&gt;
;Satz: Jeder Andersson-Baum ist balanciert. Beweis:&lt;br /&gt;
:1. Sei ''h&amp;lt;sub&amp;gt;r&amp;lt;/sub&amp;gt;'' die reduzierte Höhe des Andersson-Baumes. Die Eigenschaft, dass alle RS-Pfade die reduzierte Länge ''h&amp;lt;sub&amp;gt;r&amp;lt;/sub&amp;gt;'' (also die ''gleiche'' reduzierte Länge) haben, hat eine wichtige Folge: Hat der Andersson-Baum ''keine'' horizontalen Kanten, so muss er ein vollständiger Baum der Tiefe ''d&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; = h&amp;lt;sub&amp;gt;r&amp;lt;/sub&amp;gt; - 1'' sein, denn nur ein vollständiger Baum hat die Eigenschaft, dass alle RS-Pfade die gleiche Länge besitzen. Gibt es hingegen horizontale Kanten, muss der Andersson-Baum ''mehr'' Elemente enthalten als der vollständige Baum der Tiefe ''d&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt;''. Folglich gilt für die Anzahl der Knoten eines Andersson-Baumes:&lt;br /&gt;
:::&amp;lt;math&amp;gt;N \ge 2^{d_v+1} - 1 = 2^{h_r} - 1&amp;lt;/math&amp;gt;&lt;br /&gt;
:2. Da niemals zwei aufeinenderfolgende Kanten horizontal sein dürfen, ist in jedem RS-Pfad höchstens die Hälfte aller Kanten horizontal. Daher gilt für die Tiefe ''d'' eines Andersson-Baumes&lt;br /&gt;
:::&amp;lt;math&amp;gt;d \le 2 h_r&amp;lt;/math&amp;gt;&lt;br /&gt;
:3. Fasst man 1. und 2. zusammen, erhält man:&lt;br /&gt;
:::&amp;lt;math&amp;gt;N \ge 2^{h_r} - 1 \ge 2^{d/2} - 1&amp;lt;/math&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;N + 1 \ge 2^{d/2}&amp;lt;/math&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;\log_2(N + 1) \ge d/2&amp;lt;/math&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;d \le 2 \log_2(N + 1)&amp;lt;/math&amp;gt;.&lt;br /&gt;
::Da die Komplexität der Baumoperationen &amp;lt;math&amp;gt;f(N) = \mathcal{O}(d)&amp;lt;/math&amp;gt; ist, gilt für den Andersson-Baum:&lt;br /&gt;
:::&amp;lt;math&amp;gt;f(N) = \mathcal{O}(2 \log_2(N + 1)) = \mathcal{O}(\log(N))&amp;lt;/math&amp;gt; &lt;br /&gt;
::q.e.d.&lt;br /&gt;
&lt;br /&gt;
====Wie erreicht man die Balance?====&lt;br /&gt;
&lt;br /&gt;
Der Baum ist nicht mehr balanciert, wenn obige Regeln verletzt sind. Dies kann durch Einfügen eines neuen Knotens oder durch Löschen eines Knotens passieren. Nach jeder Einfügung haben sowohl der neue Knoten als auch sein Vater das Level 1 (denn der Vater war vorher direkt mit dem Sentinel verbunden). Kanten zu neu eingefügten Knoten sind deshalb immer horizontal. Dies kann die Regeln verletzen, indem entweder&lt;br /&gt;
* eine horizontale Kante zum linken Kind enstanden ist (falls der neue Knoten ein linkes Kind ist), oder&lt;br /&gt;
* zwei aufeinander folgende horizontale Kanten zu rechten Kindern entstanden sind (falls der neue Knoten ein rechtes Kind ist, und sein Vater bereits ein horizontales rechtes Kind war).&lt;br /&gt;
Diese Fehler können durch Rotation leicht behoben werden:&lt;br /&gt;
* Linke horizontale Kanten werden durch Rechtsrotation in rechte horizontale Kanten verwandelt.&lt;br /&gt;
* Bei zwei aufeinander folgenden rechten horizontalen Kanten wird der mittlere Knoten um eine Ebene angehoben.&lt;br /&gt;
Dabei ist zu beachten, dass die erste Reparatur einen neuen Fehler erzeugen kann: Es können zwei aufeinanderfolgende rechte horizontale Kanten enstehen. Daher muss die zweite Operation stets nach der ersten ausgeführt werden. Das Anheben des Levels in der zweiten Operation kann wiederum dazu führen, dass auf der nächsthöheren Ebene verbotene horizontale Kanten entstehen. Deshalb müssen die Reparaturoperationen auf der nächsten Ebene rekursiv wiederholt werden. Dies führt uns zu folgender Implementation des Insert-Algorithmus&lt;br /&gt;
&lt;br /&gt;
  def anderssonTreeInsert(node,key):&lt;br /&gt;
      if node is None: &lt;br /&gt;
          return AnderssonNode(key)       &lt;br /&gt;
      if node.key == key:&lt;br /&gt;
          return node&lt;br /&gt;
      if key &amp;lt; node.key:&lt;br /&gt;
          node.left  = anderssonTreeInsert(node.left, key)&lt;br /&gt;
      else:&lt;br /&gt;
          node.right = anderssonTreeInsert(node.right, key)    &lt;br /&gt;
      &amp;lt;font color=&amp;quot;red&amp;quot;&amp;gt;if node.left is not None and node.level == node.left.level: # linke horizontale Kante&lt;br /&gt;
            node = rotateRight(node)  # wird zu rechter horizontaler Kante gemacht&lt;br /&gt;
      if node.right is not None and node.right.right is not None and node.level==node.right.right.level:  # aufeinanderfolgende horizontale Kanten&lt;br /&gt;
            node = rotateLeft(node)   # mache den mittleren Knoten zur Wurzel des Teilbaums&lt;br /&gt;
            node.level += 1           # und hebe die Wurzel um ein level an&amp;lt;/font&amp;gt;    &lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Da die Reparaturoperationen auf dem Rückweg von der Rekursion ausgeführt werden, ist gewährleistet, dass sie auf der nächsten Ebene des Baumes ebenfalls ausgeführt werden, falls nötig. Die folgende Skizze verdeutlicht die Anwendung der Reparaturen, wenn Knoten ''c'' über eine linke horizontale Kante an Knoten ''b'' angefügt wurde. Im oberen Beispiel genügt die erste Operation zur Reparatur, beim unteren Beispiel muss hingegen auch noch die zweite Operation angewendet werden.&lt;br /&gt;
&lt;br /&gt;
[[Image:rotate.jpg|text-top]]&lt;br /&gt;
&lt;br /&gt;
Die folgende Illustration verdeutlicht das Verhalten des Andersson-Baumes, wenn die Schlüssel in der Folge [5,4,3,2,1] eingefügt werden. Beim einfachen Binärbaum sind solche vorsortierten Daten sehr ungünstig und führen zu entarteten Bäumen mit linearer Zugriffzeit. Die Umstrukturierungen beim Andersson-Baum stellen hingegen sicher, dass die Balance immer gewahrt bleibt. Wir stellen die Knoten hier als Paare &amp;lt;tt&amp;gt;(key, level)&amp;lt;/tt&amp;gt; dar, Pfeile markieren die Richtung von horizontalen Kanten. Wie oben beschrieben, werden neue Knoten zunächst normal in den Baum eingefügt und ihr Level mit 1 initialisiert. Wenn dadurch Bedingungen verletzt werden, werden die notwendigen Umstrukturierungen durchgeführt.&lt;br /&gt;
&lt;br /&gt;
Beim Einfügen des ersten Knotens (Schlüssel 5) gibt es noch keine Probleme:&lt;br /&gt;
&lt;br /&gt;
 (5,1)&lt;br /&gt;
&lt;br /&gt;
Der zweite Knoten (Schlüssel 4) wird zum linken Kind des ersten. Da beide Knoten sich auf Level 1 befinden, ensteht dadurch eine verbotene horizontale Kante nach links, die durch eine Rechtsrotation (RR) in eine erlaubte horizontale Kante nach rechts umgewandelt wird. Danach ist Knoten 4 die neue Wurzel des Baumes:&lt;br /&gt;
&lt;br /&gt;
   (4,1) &amp;lt;-- (5,1)   ==RR==&amp;gt;   (4,1) --&amp;gt; (5,1)&lt;br /&gt;
 &lt;br /&gt;
Das Einfügen von Schlüssel 3 verursacht wieder eine horizontale linke Kante, die in eine rechte umgewandelt wird:&lt;br /&gt;
&lt;br /&gt;
   (3,1) &amp;lt;-- (4,1) --&amp;gt; (5,1)   ==RR==&amp;gt;   (3,1) --&amp;gt; (4,1) --&amp;gt; (5,1)&lt;br /&gt;
 &lt;br /&gt;
Nun gibt es aber zwei horizontale Kanten hintereinander. Wir führen deshalb eine Linksrotation (LR) durch und heben das Level des mittleren Knotens um 1 an:&lt;br /&gt;
&lt;br /&gt;
                                                                                  (4,2)&lt;br /&gt;
                                                                                   /   \&lt;br /&gt;
   (3,1) --&amp;gt; (4,1) --&amp;gt; (5,1)   ==LR==&amp;gt;   (3,1) &amp;lt;-- (4,1) --&amp;gt; (5,1)  ==Lift==&amp;gt;  (3,1)   (5,1)&lt;br /&gt;
&lt;br /&gt;
Damit ist der Baum wieder korrekt. Das Einfügen des Schlüssels 2 führt wieder zu einer verbotenen linken Kante, die durch Rechtsrotation beseitigt wird:&lt;br /&gt;
&lt;br /&gt;
                                                 (4,2)&lt;br /&gt;
                  (4,2)                         /     \&lt;br /&gt;
                  /   \       ==RR==&amp;gt;          /       \&lt;br /&gt;
    (2,1) &amp;lt;-- (3,1)   (5,1)                   /         \&lt;br /&gt;
                                          (2,1)--&amp;gt;(3,1) (5,1)&lt;br /&gt;
&lt;br /&gt;
Nun fügen wir Schlüssel 1 ein, der ebenfalls zu einer verbotenen linken Kante führt, aber die Reparatur des Fehlers durch Rechstsrotation würde zwei aufeinanderfolgende horizontale Kanten erzeugen. Knoten 2 muss deshalb angehoben werden:&lt;br /&gt;
&lt;br /&gt;
                      (4,2)                      (2,2) &amp;lt;-- (4,2)    &lt;br /&gt;
                     /     \                     /   \         \&lt;br /&gt;
                    /       \        ===&amp;gt;       /     \         \&lt;br /&gt;
                   /         \                 /       \         \&lt;br /&gt;
     (1,1) &amp;lt;-- (2,1)--&amp;gt;(3,1) (5,1)         (1,1)       (3,1)     (5,1)&lt;br /&gt;
&lt;br /&gt;
Jetzt ist aber bei Level 2 eine verbotene linke horizontale Kante entstanden, die wir wieder durch Rechtsrotation in eine erlaubte rechte horizontale Kante verwandeln, so dass Knoten 2 nun die Wurzel des Baumes bildet:&lt;br /&gt;
&lt;br /&gt;
           (2,2) &amp;lt;-- (4,2)                       (2,2) --&amp;gt; (4,2)   &lt;br /&gt;
           /   \         \                       /         /   \&lt;br /&gt;
          /     \         \          ===&amp;gt;       /         /     \&lt;br /&gt;
         /       \         \                   /         /       \&lt;br /&gt;
     (1,1)       (3,1)     (5,1)           (1,1)     (3,1)       (5,1)&lt;br /&gt;
&lt;br /&gt;
Jetzt sind alle Bedingungen erfüllt. Man erkennt, dass alle reduzierten RS-Pfade die gleiche Länge, nämlich 2, haben (dies entspricht gerade dem Level der Wurzel des Baumes). Die tatsächliche Tiefe des Baumes (längster Pfad von der Wurzel zu einem Blatt, wobei horizontale Kanten mitgezählt werden) beträgt 2. Für einen Binärbaum mit 5 Knoten ist die Tiefe 2 gerade der beste erreichbare Wert, der Andersson-Baum verhält sich hier also optimal.&lt;br /&gt;
 &lt;br /&gt;
Die Löschoperation &amp;lt;tt&amp;gt;anderssonTreeRemove&amp;lt;/tt&amp;gt; benötigt in jedem Knoten bis zu 5 Rotationen. Wegen der Einzelheiten verweisen wir auf Anderssons [http://user.it.uu.se/~arnea/abs/simp.html Originalartikel].&lt;br /&gt;
&lt;br /&gt;
==Beziehungen zwischen dem Suchproblem und dem Sortierproblem==&lt;br /&gt;
&lt;br /&gt;
===Sortieren mit Hilfe eines selbst-balancierenden Suchbaums===&lt;br /&gt;
&lt;br /&gt;
Mit Hilfe eines selbst-balancierenden Suchbaums kann ein effizienter Sortieralgorithmus implementiert werden, indem man zunächst die Daten in beliebiger Reihenfolge in einen Baum einfügt, und dann in der richtigen Sortierung wieder ausliest.&lt;br /&gt;
&lt;br /&gt;
   a = ...   # unsortiertes Array&lt;br /&gt;
   t = None  # leerer Andersson-Baum&lt;br /&gt;
   for e in a:&lt;br /&gt;
       t = anderssonTreeInsert(t, e) # Baum erzeugen&lt;br /&gt;
   r = []    # leeres dynamisches Array&lt;br /&gt;
   treeSort(t, r) &lt;br /&gt;
   # r enthält jetzt die Daten aus a in sortierter Reihenfolge&lt;br /&gt;
&lt;br /&gt;
Die Funktion &amp;lt;tt&amp;gt;treeSort&amp;lt;/tt&amp;gt; navigiert im Sinne eines sogenannten ''in-order traversals'' durch den Baum und fügt die Datenelemente in der richtigen Reihenfolge an des Array an:&lt;br /&gt;
&lt;br /&gt;
 def treeSort(node,array):          # dynamisches Array als 2. Argument&lt;br /&gt;
     if node is None:               # &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
         return&lt;br /&gt;
     treeSort(node.left, array)     # rekursiv&lt;br /&gt;
     array.append(node.key)         # amortisiert &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
     treeSort(node.right, array)    # rekursiv&lt;br /&gt;
&lt;br /&gt;
;Komplexität:&lt;br /&gt;
&lt;br /&gt;
* Jede Einfügeoperation in den Baum hat logarithmische Komplexität. Der Aufbau eines Baumes aus N Elementen hat daher Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N \log(N))&amp;lt;/math&amp;gt;.&lt;br /&gt;
* &amp;lt;tt&amp;gt;treeSort&amp;lt;/tt&amp;gt; führt in jedem Knoten eine oder zwei Operationen mit Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt; sowie zwei rekursive Aufrufe aus. Die Auflösung der Rekursion ergibt&lt;br /&gt;
 &amp;lt;math&amp;gt;&lt;br /&gt;
 f(N)=\mathcal{O}(1)+f(N_\mathrm{left})+f(N_\mathrm{right})=\mathcal{O}(1)+\mathcal{O}(1)+f(N_\mathrm{left.left})+f(N_\mathrm{left.right})+\mathcal{O}(1)+f(N_\mathrm{right.left})&lt;br /&gt;
 +f(N_\mathrm{left.right})=N\cdot\mathcal{O}(1)=\mathcal{O}(N)&lt;br /&gt;
 &amp;lt;/math&amp;gt;&lt;br /&gt;
* Insgesamt erhalten wir also Komplexität &amp;lt;math&amp;gt;\mathcal{O}(\max(N \log(N), N)) = \mathcal{O}(N \log(N))&amp;lt;/math&amp;gt; wie bei Merge Sort. Allerdings sind der konstante Faktor sowie der Speicherverbrauch größer, so dass diese Sortiermethode in der Praxis kaum angewendet wird.&lt;br /&gt;
&lt;br /&gt;
===Sortieren als Suchproblem===&lt;br /&gt;
&lt;br /&gt;
Diesem Thema ist jetzt ein eigenes Kapitel [[Sortieren in linearer Zeit]] gewidmet.&lt;br /&gt;
&lt;br /&gt;
[[Sortieren in linearer Zeit|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5442</id>
		<title>Randomisierte Algorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5442"/>
				<updated>2012-07-30T11:56:17Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* 2. RANSAC-ALGORITHMUS (Random Sample Consensus) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Randomisierte Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
;Definition: Randomisierte Algorithmen sind Algorithmen, die bei Entscheidungen über ihr weiteres Vorgehen oder bei der Wahl ihrer Parameter Zufallszahlen benutzen.&lt;br /&gt;
&lt;br /&gt;
Anschaulich gesprochen, wersucht man bei randomisierten Algorithmen, einen Teil der Lösung zu &amp;lt;i&amp;gt;raten&amp;lt;/i&amp;gt;. Auf den ersten Blick würde man vermuten, dass dabei nicht viel Sinnvolles herauskommen kann. Diese Kapitel wird jedoch zeigen, dass man durch geschicktes Raten tatsächlich zu sehr eleganten Algorithmen gelangen kann.&lt;br /&gt;
&lt;br /&gt;
Grundsätzlich unterscheidet man zwei Arten von randomisierten Algorithmen:&lt;br /&gt;
;Las Vegas - Algorithmen: Das Ergebnis des Algorithmus ist immer korrekt, und die Berechnung erfolgt mit hoher Wahrscheinlichkeit effizient.&lt;br /&gt;
;Monte Carlo - Algorithmen: Die Berechnung ist immer effizient, und das Ergebnis ist mit hoher Wahrscheinlichkeit korrekt.&lt;br /&gt;
Las Vegas-Algorithmen verwendet man, wenn der Algorithmus im ungünstigen Fall eine schlechte Laufzeit hat, und der ungünstige Fall kann durch die Randomisierung sehr unwahrscheinlich gemacht werden. Wir haben in der Vorlesung schon mehrere Las Vegas-Algorithmen kennen gelernt:&lt;br /&gt;
* Quick Sort mit zufälliger Wahl des Pivot-Elements: Die Randomisierung verhindert, dass das Array immer wieder in Subarrays von sehr unterschiedlicher Größe aufgeteilt wird.&lt;br /&gt;
* Treap mit zufälligen Prioritäten: Die Randomisierung verhindert, dass der Baum schlecht balanciert ist.&lt;br /&gt;
* Universelles Hashing: Die zufällige Wahl der Hashfunktion verhindert, dass ein Angreifer eine Schlüsselmenge mit sehr vielen Kollisionen konstruieren kann.&lt;br /&gt;
* Erzeugung einer perfekten Hashfunktion: Durch die Randomisierung entsteht mit nach wenigen Versuchen ein zyklenfreier Graph, der zur Definition der Hashfunktion geeignet ist.&lt;br /&gt;
Monte Carlo-Algorithmen verwendet man dagegen, wenn kein effizienter deterministischer Algorithmus für ein Problem bekannt ist. Man gibt sich dann damit zufrieden, dass der randomisierte Algorithmus die korrekte Lösung nur mit hoher Wahrscheinlichkeit findet, wenn dies dafür sehr effizient geschieht. Bei manchen Problemen ist auch dies unerreichbar - man muss dann bereits zufrieden sein, wenn der Algorithmus mit hoher Wahrscheinlichkeit eine sehr gute Näherungslösung findet. Beliebte Anwendungsgebiete für Monte Carlo-Algorithmen sind beispielsweise&lt;br /&gt;
* Randomisierte Primzahl-Tests: Moderne Verschlüsselungsverfahren benötigen zahlreiche Primzahlen, aber exakte Primzahltests sind teuer. Der [http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin-Test] findet effizient Zahlen, die mit sehr hoher Wahrscheinlichkeit tatsächlich Primzahlen sind.&lt;br /&gt;
* Randomisiertes Testen: Wie jeder Test kann auch eine randomisierter Test nicht die Abwesenheit von Programmierfehlern garantieren, aber man kann durch die Randomisierung viel mehr Testfälle generieren und erhöht so die Erfolgswarscheinlichkeit. Wir haben als Beispiel dafür den [[Korrektheit#Beispiel_f.C3.BCr_das_Testen:_Freivalds_Algorithmus|Algorithmus von Freivald]] behandelt.&lt;br /&gt;
* Lösung schwieriger Optimierungsprobleme: Wir zeigen unten, dass ein randomisierter Algorithmus effizient eine Lösung für das 2-SAT-Problem aus dem vorherigen Kapitel findet (für k-SAT mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; liefert der Algorithmus immer noch mit einer gewissen Wahrscheinlichkeit das richtige Ergebnis, ist aber nicht mehr effizient). Einen effizienten Approximationsalgorithmus für des Problem des Handelsreisenden behandlen wir im Kapitel [[NP-Vollständigkeit]]. Weitere wichtige Beispiele für diesen Bereich sind [http://en.wikipedia.org/wiki/Simulated_annealing simulated annealing] und das [http://de.wikipedia.org/wiki/MCMC-Verfahren Markov-Chain-Monte-Carlo-Verfahren].&lt;br /&gt;
* Robuste Statistik: Eine Grundaufgabe der Statistik ist das Anpassen (Fitten) von Modellen an gemessene Werte. Wenn die Messungen jedoch &amp;quot;Ausreißer&amp;quot; (einige völlig falsche Werte) enthalten, geht die Anpassung schief. Wir beschreiben unten den RANSAC-Algorithmus, der die Ausreißer identifizieren und beim Modellfitten ignorieren kann.&lt;br /&gt;
&lt;br /&gt;
Obwohl randomisierte Algorithmen oft einfach und elegant sind, ist ihre theoretische Analyse (also das Führen von Korrektheits- und Komplexitätsbeweisen) häufig sehr schwierig. Man muss fortgeschrittene Methoden der Wahrscheinlichkeitsrechnung und Statistik beherrschen, um die Wahrscheinlichkeit für das Versagen des Algorithmus zu berechnen und um zu zeigen, wie man den Algorithmus benutzt, damit diese Wahrscheinlichkeit unter einer akzeptablen Schranke bleibt. Die Algorithmen, die wir für diese Vorlesung ausgewählt haben, zeichnen sich dadurch aus, dass die Beweise hier einfach zu erbringen sind.&lt;br /&gt;
&lt;br /&gt;
== Anwendung: Lösen des K-SAT-Problems ==&lt;br /&gt;
&lt;br /&gt;
Der &amp;lt;b&amp;gt;Algorithmus von Schöning&amp;lt;/b&amp;gt; löst das [[Graphen_und_Graphenalgorithmen#Normalformen für logische Ausdrücke|k-SAT-Problem]] durch Raten: Wenn ein Ausdruck in k-CNF den Wert False hat, gibt es mindestens eine Klausel, die den Wert False hat. Alle Literale in dieser Klausel haben ebenfalls den Wert False, denn jede Klausel ist eine ODER-Verknüpfung, die nur dann False werden kann. Um den Ausdruck zu erfüllen, muss jede Klausel den Wert True annehmen, also müssen wir den Wert von mindestens einem Literal umdrehen. Wenn der Ausruck tatsächlich erfüllbar ist, gibt es immer ein geeignetes Literal, wir wissen nur nicht, welches. Deshalb drehen wir ein unter den k Literalen der betreffenden Klausel zufällig gewähltes. Liegen wir mit unserer Wahl richtig, sind wir der Lösung näher gekommen - im besten Fall sind jetzt alle Klauseln erfüllt. Wählen wir jedoch die falsche Variable, ist die aktuelle Klausel zwar jetzt True, aber dafür werden andere Klauseln zu False, die bisher True waren, und wir entfernen uns somit von der Lösung.&lt;br /&gt;
&lt;br /&gt;
    geg.: logischer Ausdruck in K-CNF (n Variablen, m Klauseln, k Variablen pro Klausel)&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;math&amp;gt;\underbrace {\underbrace {\left(x_1 \vee x_3 \vee...\right)}_{k\; Literale} \wedge \left( x_2 \vee x_4 \vee...\right)}_{m\;Klauseln}&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Algorithmus von Schöning lautet in Pseudocode:&lt;br /&gt;
    for i in range (trials):    #Anzahl der Versuche&lt;br /&gt;
         Bestimme eine Zufallsbelegung der Variablen &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
         for j in range (steps):&lt;br /&gt;
               if &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt; erfüllt alle Klauseln: &lt;br /&gt;
                   return &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
               wähle zufällig eine Klausel, die nicht erfüllt ist und negiere zufällig eine der Variablen in dieser Klausel &lt;br /&gt;
               # (die Klausel ist jetzt erfüllt)&lt;br /&gt;
    return None  # keine Lösung gefunden&lt;br /&gt;
&lt;br /&gt;
Findet der Algorithmus eine Lösung, wissen wir, dass der Ausdruck erfüllbar ist. Andernfalls könnte der Ausdruck unerfüllbar sein, oder wir haben nur Pech gehabt. Je mehr erfolglose Versuche wir machen, desto höher ist die Wahrscheinlichkeit, dass das erste zutrifft.&lt;br /&gt;
&lt;br /&gt;
Es ist sinnvoll, &amp;lt;tt&amp;gt;steps = k*n&amp;lt;/tt&amp;gt; zu wählen. Dann gilt der &lt;br /&gt;
;Satz: Wenn ein Ausdruck in k-CNF mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; erfüllbar ist, muss man im Mittel &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in O\left(\left(\frac{2(k-1)}{k}\right)^n \right)&amp;lt;/math&amp;gt; Versuche machen, um eine Lösung zu finden.&lt;br /&gt;
&lt;br /&gt;
Für &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; gilt stets &amp;lt;math&amp;gt;\frac{2(k-1)}{k} &amp;gt; 1&amp;lt;/math&amp;gt;, man benötigt also eine in n exponentielle Anzahl von Versuchen. Bei &amp;lt;math&amp;gt;k=3&amp;lt;/math&amp;gt; gilt z.B. &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt; \in O\left(\left(\frac{4}{3}\right)^n\right)&amp;lt;/math&amp;gt;. Dies ist zwar im Mittel effizienter also die erschöpfende Suche, die &amp;lt;math&amp;gt;O(2^n)&amp;lt;/math&amp;gt; Schritte benötigt, aber immer noch sehr langsam.&lt;br /&gt;
&lt;br /&gt;
Der Fall &amp;lt;b&amp;gt;&amp;lt;math&amp;gt;k=2&amp;lt;/math&amp;gt; ist jedoch ein Sonderfall&amp;lt;/b&amp;gt;: Hier kann man leicht beweisen, dass eine Lösung im Mittel bereits nach &amp;lt;math&amp;gt;O\left(n^2\right)&amp;lt;/math&amp;gt; Schritten gefunden wird. Wenn man schon weiss, dass der Ausdruck erfüllbar ist (was mit [[Graphen_und_Graphenalgorithmen#Lösung des 2-SAT-Problems mit Implikationgraphen|Implikationgraphen]] leicht geprüft werden kann), lässt man den randomisierten Algorithmus einfach so lange laufen, bis er eine Lösung findet. Man setzt also &amp;lt;tt&amp;gt;step = infinity&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;trials = 1&amp;lt;/tt&amp;gt; und verlässt sich darauf, dass das &amp;lt;tt&amp;gt;return&amp;lt;/tt&amp;gt; mit einer gültigen Lösung früher oder später ausgeführt wird. Dass man darauf im Mittel nur &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte warten muss, zeigen wir jetzt mit Hilfe eines &amp;lt;i&amp;gt;random walk&amp;lt;/i&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk===&lt;br /&gt;
&lt;br /&gt;
Um die Random Walk Analyse zu verstehen, betrachten wir folgendes Spiel:&lt;br /&gt;
&lt;br /&gt;
   geg.: eine Stuhlreihe mit N Stühlen. Wir nummerieren die Stühle so, dass links der Stuhl 0 und rechts der Stuhl N steht.&lt;br /&gt;
   &lt;br /&gt;
   * Eine Person setzt sich zufällig auf einen der Stühle.&lt;br /&gt;
   * Eine zweite Person wirft eine Münze.&lt;br /&gt;
   &lt;br /&gt;
         Wenn die Münze auf Zahl fällt, rückt die erste Person einen Stuhl nach links, andernfalls nach rechts.&lt;br /&gt;
         &amp;lt;--- Zahl                                                                                    Kopf ---&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
   * Frage: Wie oft muss man die Münze im Durchschnitt werfen, bis Person 1 zum ersten Mal auf Stuhl N sitzt?&lt;br /&gt;
&lt;br /&gt;
Da die erste Person sich anfangs zufällig hinsetzt, haben wir eine Chance von 1/N, dass sie gleich auf dem richtigen Stuhl landet und wir 0 Schritte benötigen. Mit der gleichen Wahrscheinlichkeit von 1/N setzt sie sich anfangs auf Stuhl Nummer (N-1), und wir haben eine fifty-fifty-Chance, mit nur einem Wurf durchzukommen. Wir können aber auch Pech haben und landen auf Stuhl Nummer (N-2). Das ist das Gleiche, als wenn Person 1 von Anfang an auf diesem Stuhl gesessen hätte, nur dass wir jetzt bereits einen Wurf verbraucht haben. Man sieht, dass man die Zahl der Restwürfe immer in dieser Art ausdrücken kann: Sitzt Person 1 auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, kann sie entweder nach rechts rücken und benötigt dann noch soviele Würfe, wie man typischerweise für Stuhl &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; benötigt, plus den Wurf von &amp;lt;tt&amp;gt;i =&amp;gt; i+1&amp;lt;/tt&amp;gt;. Oder sie kann nach links rücken und benötigt dann die typische Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; plus den Wurf &amp;lt;tt&amp;gt;i =&amp;gt; i-1&amp;lt;/tt&amp;gt;. Beide Möglichkeiten haben die Wahrscheinlichkeit 1/2. Mathematisch kann man dies elegant als Rekursionsformel schreiben, die die erwartete Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; als Funktion der entsprechenden Wurfzahlen für die Stühle &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; ausdrückt:&lt;br /&gt;
&lt;br /&gt;
* Wenn wir uns auf Stuhl N befinden, werfen wir gar nicht: &amp;lt;math&amp;gt;W\left(N\right)=0&amp;lt;/math&amp;gt;&lt;br /&gt;
* Von Stuhl 0 gehen wir immer zu Stuhl 1: &amp;lt;math&amp;gt;W\left(0\right)=1 + W\left(1\right)&amp;lt;/math&amp;gt;&lt;br /&gt;
* Allgemeiner Fall: &amp;lt;math&amp;gt;W\left(i\right)=\frac 1 2 \left(1 + W\left(i+1\right)\right) + \frac 1 2 \left(1 + W\left(i-1\right)\right) = \frac 1 2 W\left(i+1\right) + \frac 1 2 W\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
Diese Rekursion wird durch die explizite Formel&lt;br /&gt;
::&amp;lt;math&amp;gt;W\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
gelöst, wie man durch Einsetzen leicht nachprüft:&lt;br /&gt;
::&amp;lt;math&amp;gt;&lt;br /&gt;
       \begin{align} &lt;br /&gt;
             W\left(N\right) &amp;amp; = N^2-N^2=0 \\&lt;br /&gt;
                  &lt;br /&gt;
             W\left(0\right) &amp;amp;= W\left(1\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-1^2+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2 - 0^2\\&lt;br /&gt;
                 &lt;br /&gt;
              W\left(i\right) &amp;amp;= \frac 1 2 \left(N^2-\left(i-1\right)^2\right) + \frac 1 2 \left(N^2-\left(i+1\right)^2\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= \frac 1 2 N^2-\frac 1 2 \left( i^2-2i+1\right) + \frac 1 2 N^2-\frac 1 2 \left(i^2+2i+1\right) + 1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-i^2&lt;br /&gt;
       \end{align}&amp;lt;/math&amp;gt;&lt;br /&gt;
Insbesondere braucht man im ungünstigen Fall (Start auf Stuhl 0) im Durchschnitt &amp;lt;math&amp;gt;N^2&amp;lt;/math&amp;gt; Würfe, im typischen Fall (Start in der Mitte, also bei &amp;lt;math&amp;gt;i = N/2&amp;lt;/math&amp;gt;) im Durchschnitt &lt;br /&gt;
:&amp;lt;math&amp;gt;N^2 - (N/2)^2=\frac 3 4 N^2\in O(N^2)&amp;lt;/math&amp;gt; &lt;br /&gt;
Würfe. Die '''Beziehung zum randomisiertem 2-SAT-Algorithmus''' ist jetzt leicht zu erkennen. Sitzt die Person auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, interpretieren wir das als:&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Stuhl &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt;&amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert, &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
Wählt der Algorithmus eine Klausel, die nicht erfüllt ist, gibt es zwei Möglichkeiten:&lt;br /&gt;
# Beide Literale in der Klausel haben den falschen Wert: Die Lösung wird auf jeden Fall besser, egal welche der beiden wir umdrehen. Wir gehen also von Zustand &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
# Nur eins der Literale hat den falschen Wert: Beim Umdrehen haben wir eine fifty-fifty-Chance, das richtige Literal zu wählen und in den Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; zu gelangen. Mit der selben Wahrscheinlichkeit wählen wir das falsche Literal und landen im Zustand &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
Falls 2 ist der ungünstigere und entspricht unserem Spiel, dessen Analyse wir deshalb einfach auf das 2-SAT-Problem übertragen können: Ziel des Algorithmus ist es, in den Zustand N zu gelangen, und deshalb gilt genau wie beim Spiel der &lt;br /&gt;
;Satz: Der randomisierte 2-SAT-Algorithmus findet im Durchschnitt nach &amp;lt;math&amp;gt;O(N^2)&amp;lt;/math&amp;gt; Versuchen eine Lösung, wenn das Problem erfüllbar ist.&lt;br /&gt;
Damit ist der randomisierte Algorithmus für dieses Problem effizient, was Sie in Übung 12 experimentell nachprüfen sollen.&lt;br /&gt;
&lt;br /&gt;
== RANSAC-Algorithmus (Random Sample Consensus)==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''Aufgabe:''&amp;lt;/u&amp;gt; gegeben: Datenpunkte&lt;br /&gt;
::gesucht: Modell, das die Datenpunkte erklärt&lt;br /&gt;
&lt;br /&gt;
[[Image:Rubto.png|thumb|250px|none]]&lt;br /&gt;
&lt;br /&gt;
'''Messpunkte:'''&lt;br /&gt;
   &lt;br /&gt;
      übliche Lösung: Methode der kleinsten Quadrate&lt;br /&gt;
      &lt;br /&gt;
      &amp;lt;math&amp;gt;\min_{a,b} 	\sum_{i} \left(a x_i + b + y_i\right)^2&amp;lt;/math&amp;gt;&lt;br /&gt;
      &lt;br /&gt;
      Schulmathematik:      &amp;lt;math&amp;gt;Minimum\stackrel{\wedge}{=}Ableitung=0&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lineares Gleichungssystem'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{da}\sum{i} \left(ax_i+b-y_i\right)^2=\sum{i} \frac{d}{da} \left[ax_i+b-y_i\right)^2&amp;lt;/math&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(g\left(x\right)\right)&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(x\right)=x^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;y\left(a\right)=ax_i+b-y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;=\sum_{i}2\left(ax_i+b-y_i\right)\frac{d}{da} \underbrace {ax_i+b-y_i}_{x_i}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\underline {=2\sum_{i}\left(ax_i+b-y_i\right)x_i\stackrel{!}{=}0}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}{x_i}^2+b\sum_{i}x_i=\sum_{i}x_iy_i&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}x_i+b\sum_{i}1=\sum_{i}y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{db}\sum_{i}\left(ax_i+b-y_i\right)^2=2\sum_{i}\left(ax_i+b-y_i\right)*1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:Problem: &amp;lt;math&amp;gt;\epsilon  %&amp;lt;/math&amp;gt; der Datenpunkte sind Outlier&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\Longrightarrow&amp;lt;/math&amp;gt; Einfaches Anpassen des Modells an die Datenpunkte funktioniert nicht&lt;br /&gt;
&lt;br /&gt;
:Seien mindestens k Datenpunkte notwendig, um das Programm anpassen zu können&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
RANSAC-Algorithmus&lt;br /&gt;
&lt;br /&gt;
      for  l in range (trials):&lt;br /&gt;
           wähle zufällig k Punkte aus&lt;br /&gt;
           passe das Modell an die k Punkte an&lt;br /&gt;
           zähle, wieviele Punkte in der Nähe des Modells liegen (d.h. &amp;lt;math&amp;gt;d_i &amp;lt; d_max&amp;lt;/math&amp;gt; muss geschickt gewählt werden) &lt;br /&gt;
                                           #Bsp. Geradenfinden:-wähle a,b aus zwei Punkten&lt;br /&gt;
                                                               -berechne: &amp;lt;math&amp;gt;|ax_i+b-y_i|=d_i&amp;lt;/math&amp;gt;&lt;br /&gt;
                                                               -zähle Punkt i als Inlier, falls &amp;lt;math&amp;gt;d_i&amp;lt;d_ma&amp;lt;/math&amp;gt;&lt;br /&gt;
      return: Modell mit höchster Zahl der Inlier&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;trials= \frac{log\left(1-p\right)}{log\left(1-\left(1-\epsilon\right)^k\right)}&amp;lt;/math&amp;gt;  mit k=Anzahl der Datenpunkte und p=Erfolgswahrscheinlichkeit, &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;=Outlier-Anteil&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Erfolgswahrscheinlichkeit: p=99%'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{|c||c|c|c|c|c|}&lt;br /&gt;
         Beispiel &amp;amp; k &amp;amp; \epsilon=10% &amp;amp; 20% &amp;amp; 50% &amp;amp; 70%\\&lt;br /&gt;
         \hline&lt;br /&gt;
         Linie\;in\;2D &amp;amp; 2 &amp;amp; 3 &amp;amp;5 &amp;amp; 17 &amp;amp; 49\\&lt;br /&gt;
         Kreis\;in\;2D &amp;amp; 3 &amp;amp; 4 &amp;amp; 7 &amp;amp; 35 &amp;amp; 169\\&lt;br /&gt;
         Ebene\;in\;3D &amp;amp; 8 &amp;amp; 9 &amp;amp; 26 &amp;amp; 1172 &amp;amp; 70188\\&lt;br /&gt;
       \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== ''' Zufallszahlen ''' ==&lt;br /&gt;
&lt;br /&gt;
:- kann man nicht mit deterministischen Computern erzeugen&lt;br /&gt;
:- aber man kann Pseudo-Zufallszahlen erzeugen, die viele Eigenschaften von echten Zufallszahlen haben&lt;br /&gt;
::: * sehr ähnlich  zum Hash&lt;br /&gt;
&lt;br /&gt;
     ''&amp;quot;linear Conguential Random number generator&amp;quot;''&lt;br /&gt;
        &amp;lt;math&amp;gt;I_{i+1}= \left(a*I_i + c\right)\textrm{mod\ } m&amp;lt;/math&amp;gt;&lt;br /&gt;
        &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{=&amp;gt; } &amp;amp; I_i \in [0, m-1]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:-sorgfältige Wahl von  a, c, m notwendig&lt;br /&gt;
::'''Bsp.'''  m = 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;&lt;br /&gt;
::: a = 1664525, c = 1013904223&lt;br /&gt;
::: ''&amp;quot;quick and dirty generator&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
==='''Nachteile'''===&lt;br /&gt;
&lt;br /&gt;
* nicht zufällig genug für viele Anwendungen&lt;br /&gt;
::'''Bsp.''' wähle Punkt in R&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
      \mathrm{ } &amp;amp; p = (rand(), rand(), rand())\\&lt;br /&gt;
&lt;br /&gt;
      \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::gibt Zahl u, v, w so, dass &lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; u * p[0] + v * p[1] + w * p[3]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::stark geclustert ist.&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge ist zu kurz:&lt;br /&gt;
:: spätestens nach m Schritten wiederholt sich die Folge&lt;br /&gt;
&lt;br /&gt;
::'''allgemein''': falls der interne Zustand des Zufallsgenerators ''k'' bits hat, ist Periodenlänge:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; Periode &amp;lt; 2^k\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* ''lowbits'' sind weniger zufällig als die ''highbits''&lt;br /&gt;
----&lt;br /&gt;
=== ''Mersenne Twister''===&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
'''bester zur Zeit bekannter Zufallszahlengenerator (ZZG)'''&lt;br /&gt;
* innere Zustand: &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; 624*32 bit\ Integers  =&amp;gt; 19968 bits\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge: &amp;lt;math&amp;gt;2^ {19937} \approx 4 * 10^{6000}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Punkte aus aufeinanderfolgende Zufallszahlen in &amp;lt;math&amp;gt;\mathbb{R}^n&amp;lt;/math&amp;gt; sind gleich verteilt bis &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; n = 623\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* alle Bits sind unabhängig voneinander zufällig (&amp;quot;Twister&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
* schnell&lt;br /&gt;
&lt;br /&gt;
    class MersenneTwister:&lt;br /&gt;
        &lt;br /&gt;
        def __init__(self, seed):&lt;br /&gt;
            self.N = 624  # Größe des inneren Zustands festlegen&lt;br /&gt;
            self.i = 0    # zählt mit in welchem Zustand wir uns gerade aufhalten&lt;br /&gt;
            &lt;br /&gt;
            self.state = [0]*self.N  # Speicher für den inneren Zustand reservieren&lt;br /&gt;
            &lt;br /&gt;
            self.state[0] = seed     # initiale Zufallszahl vom Benutzer&lt;br /&gt;
            # den Rest des inneren Zustands mit einfachem Zufallszahlengenerator initialisieren&lt;br /&gt;
            for i in xrange(1, self.N):&lt;br /&gt;
                self.state[i] = (1812433253 * (self.state[i-1] ^ (self.state[i-1] &amp;gt;&amp;gt; 30)) + i) % 4294967296&lt;br /&gt;
     &lt;br /&gt;
        def __call__(self):&lt;br /&gt;
            &amp;quot;&amp;quot;&amp;quot;gibt die nächste Zufallszahl im Bereich [0, 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;-1] aus&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
            N, M = self.N, 397&lt;br /&gt;
            &lt;br /&gt;
            # Zustand aktualisieren (neue Zufallszahl ausrechnen)&lt;br /&gt;
            i = self.i&lt;br /&gt;
            r = ((self.state[i] &amp;amp; 0x80000000) | (self.state[(i+1)%N] &amp;amp; 0x7FFFFFFF)) &amp;gt;&amp;gt; 1&lt;br /&gt;
            if self.state[(i+1)%N] &amp;amp; 1:&lt;br /&gt;
                r ^= 0x9908B0DF&lt;br /&gt;
            self.state[i] = self.state[(i+M)%N] ^ r&lt;br /&gt;
     &lt;br /&gt;
            # aktuelle Zufallszahl auslesen und ihre Zufälligkeit durch verwürfeln der Bits verbessern&lt;br /&gt;
            y = self.state[i]&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 11)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt;  7) &amp;amp; 0x9D2C5680)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt; 15) &amp;amp; 0xEFC60000)&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 18)&lt;br /&gt;
            &lt;br /&gt;
            # Zustand weitersetzen und endgültige Zufallszahl ausgeben&lt;br /&gt;
            self.i = (self.i + 1) % N&lt;br /&gt;
            return y&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''geg.:''' Zufallszahl &lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, \overbrace{2^{32}-1}^{m-1}]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ges.:''' Zufallszahl&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, k - 1]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''naive Lösung:'''  &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; rand()%k\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;  ist schlecht.&lt;br /&gt;
&lt;br /&gt;
'''Bsp.'''&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; \qquad m = 16\qquad k = 11\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
! rand() || 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15&lt;br /&gt;
|-&lt;br /&gt;
! rand()%k&lt;br /&gt;
! 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 0 || 1 || 2 || 3 || 4 &lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; 0,...,4 kommt doppelt so häufig wie 5,...,10 &amp;quot;nicht zufällig&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lösung:'''  Zurückweisen des Rests der Zahlen (''rejektion sampling'')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; remainder = (m - 1 - (k - 1))% k = (m - k)%k\\&lt;br /&gt;
        \mathrm{ } &amp;amp; last\ Good\ Value = m-1-remainder\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  r = rand()&lt;br /&gt;
  while r &amp;gt; last.GoodValue:&lt;br /&gt;
        r = rand()&lt;br /&gt;
        return r%k&lt;br /&gt;
&lt;br /&gt;
[[Greedy-Algorithmen und Dynamische Programmierung|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5441</id>
		<title>Randomisierte Algorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5441"/>
				<updated>2012-07-30T11:55:43Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Las Vegas vs. Monte Carlo */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Randomisierte Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
;Definition: Randomisierte Algorithmen sind Algorithmen, die bei Entscheidungen über ihr weiteres Vorgehen oder bei der Wahl ihrer Parameter Zufallszahlen benutzen.&lt;br /&gt;
&lt;br /&gt;
Anschaulich gesprochen, wersucht man bei randomisierten Algorithmen, einen Teil der Lösung zu &amp;lt;i&amp;gt;raten&amp;lt;/i&amp;gt;. Auf den ersten Blick würde man vermuten, dass dabei nicht viel Sinnvolles herauskommen kann. Diese Kapitel wird jedoch zeigen, dass man durch geschicktes Raten tatsächlich zu sehr eleganten Algorithmen gelangen kann.&lt;br /&gt;
&lt;br /&gt;
Grundsätzlich unterscheidet man zwei Arten von randomisierten Algorithmen:&lt;br /&gt;
;Las Vegas - Algorithmen: Das Ergebnis des Algorithmus ist immer korrekt, und die Berechnung erfolgt mit hoher Wahrscheinlichkeit effizient.&lt;br /&gt;
;Monte Carlo - Algorithmen: Die Berechnung ist immer effizient, und das Ergebnis ist mit hoher Wahrscheinlichkeit korrekt.&lt;br /&gt;
Las Vegas-Algorithmen verwendet man, wenn der Algorithmus im ungünstigen Fall eine schlechte Laufzeit hat, und der ungünstige Fall kann durch die Randomisierung sehr unwahrscheinlich gemacht werden. Wir haben in der Vorlesung schon mehrere Las Vegas-Algorithmen kennen gelernt:&lt;br /&gt;
* Quick Sort mit zufälliger Wahl des Pivot-Elements: Die Randomisierung verhindert, dass das Array immer wieder in Subarrays von sehr unterschiedlicher Größe aufgeteilt wird.&lt;br /&gt;
* Treap mit zufälligen Prioritäten: Die Randomisierung verhindert, dass der Baum schlecht balanciert ist.&lt;br /&gt;
* Universelles Hashing: Die zufällige Wahl der Hashfunktion verhindert, dass ein Angreifer eine Schlüsselmenge mit sehr vielen Kollisionen konstruieren kann.&lt;br /&gt;
* Erzeugung einer perfekten Hashfunktion: Durch die Randomisierung entsteht mit nach wenigen Versuchen ein zyklenfreier Graph, der zur Definition der Hashfunktion geeignet ist.&lt;br /&gt;
Monte Carlo-Algorithmen verwendet man dagegen, wenn kein effizienter deterministischer Algorithmus für ein Problem bekannt ist. Man gibt sich dann damit zufrieden, dass der randomisierte Algorithmus die korrekte Lösung nur mit hoher Wahrscheinlichkeit findet, wenn dies dafür sehr effizient geschieht. Bei manchen Problemen ist auch dies unerreichbar - man muss dann bereits zufrieden sein, wenn der Algorithmus mit hoher Wahrscheinlichkeit eine sehr gute Näherungslösung findet. Beliebte Anwendungsgebiete für Monte Carlo-Algorithmen sind beispielsweise&lt;br /&gt;
* Randomisierte Primzahl-Tests: Moderne Verschlüsselungsverfahren benötigen zahlreiche Primzahlen, aber exakte Primzahltests sind teuer. Der [http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin-Test] findet effizient Zahlen, die mit sehr hoher Wahrscheinlichkeit tatsächlich Primzahlen sind.&lt;br /&gt;
* Randomisiertes Testen: Wie jeder Test kann auch eine randomisierter Test nicht die Abwesenheit von Programmierfehlern garantieren, aber man kann durch die Randomisierung viel mehr Testfälle generieren und erhöht so die Erfolgswarscheinlichkeit. Wir haben als Beispiel dafür den [[Korrektheit#Beispiel_f.C3.BCr_das_Testen:_Freivalds_Algorithmus|Algorithmus von Freivald]] behandelt.&lt;br /&gt;
* Lösung schwieriger Optimierungsprobleme: Wir zeigen unten, dass ein randomisierter Algorithmus effizient eine Lösung für das 2-SAT-Problem aus dem vorherigen Kapitel findet (für k-SAT mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; liefert der Algorithmus immer noch mit einer gewissen Wahrscheinlichkeit das richtige Ergebnis, ist aber nicht mehr effizient). Einen effizienten Approximationsalgorithmus für des Problem des Handelsreisenden behandlen wir im Kapitel [[NP-Vollständigkeit]]. Weitere wichtige Beispiele für diesen Bereich sind [http://en.wikipedia.org/wiki/Simulated_annealing simulated annealing] und das [http://de.wikipedia.org/wiki/MCMC-Verfahren Markov-Chain-Monte-Carlo-Verfahren].&lt;br /&gt;
* Robuste Statistik: Eine Grundaufgabe der Statistik ist das Anpassen (Fitten) von Modellen an gemessene Werte. Wenn die Messungen jedoch &amp;quot;Ausreißer&amp;quot; (einige völlig falsche Werte) enthalten, geht die Anpassung schief. Wir beschreiben unten den RANSAC-Algorithmus, der die Ausreißer identifizieren und beim Modellfitten ignorieren kann.&lt;br /&gt;
&lt;br /&gt;
Obwohl randomisierte Algorithmen oft einfach und elegant sind, ist ihre theoretische Analyse (also das Führen von Korrektheits- und Komplexitätsbeweisen) häufig sehr schwierig. Man muss fortgeschrittene Methoden der Wahrscheinlichkeitsrechnung und Statistik beherrschen, um die Wahrscheinlichkeit für das Versagen des Algorithmus zu berechnen und um zu zeigen, wie man den Algorithmus benutzt, damit diese Wahrscheinlichkeit unter einer akzeptablen Schranke bleibt. Die Algorithmen, die wir für diese Vorlesung ausgewählt haben, zeichnen sich dadurch aus, dass die Beweise hier einfach zu erbringen sind.&lt;br /&gt;
&lt;br /&gt;
== Anwendung: Lösen des K-SAT-Problems ==&lt;br /&gt;
&lt;br /&gt;
Der &amp;lt;b&amp;gt;Algorithmus von Schöning&amp;lt;/b&amp;gt; löst das [[Graphen_und_Graphenalgorithmen#Normalformen für logische Ausdrücke|k-SAT-Problem]] durch Raten: Wenn ein Ausdruck in k-CNF den Wert False hat, gibt es mindestens eine Klausel, die den Wert False hat. Alle Literale in dieser Klausel haben ebenfalls den Wert False, denn jede Klausel ist eine ODER-Verknüpfung, die nur dann False werden kann. Um den Ausdruck zu erfüllen, muss jede Klausel den Wert True annehmen, also müssen wir den Wert von mindestens einem Literal umdrehen. Wenn der Ausruck tatsächlich erfüllbar ist, gibt es immer ein geeignetes Literal, wir wissen nur nicht, welches. Deshalb drehen wir ein unter den k Literalen der betreffenden Klausel zufällig gewähltes. Liegen wir mit unserer Wahl richtig, sind wir der Lösung näher gekommen - im besten Fall sind jetzt alle Klauseln erfüllt. Wählen wir jedoch die falsche Variable, ist die aktuelle Klausel zwar jetzt True, aber dafür werden andere Klauseln zu False, die bisher True waren, und wir entfernen uns somit von der Lösung.&lt;br /&gt;
&lt;br /&gt;
    geg.: logischer Ausdruck in K-CNF (n Variablen, m Klauseln, k Variablen pro Klausel)&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;math&amp;gt;\underbrace {\underbrace {\left(x_1 \vee x_3 \vee...\right)}_{k\; Literale} \wedge \left( x_2 \vee x_4 \vee...\right)}_{m\;Klauseln}&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Algorithmus von Schöning lautet in Pseudocode:&lt;br /&gt;
    for i in range (trials):    #Anzahl der Versuche&lt;br /&gt;
         Bestimme eine Zufallsbelegung der Variablen &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
         for j in range (steps):&lt;br /&gt;
               if &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt; erfüllt alle Klauseln: &lt;br /&gt;
                   return &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
               wähle zufällig eine Klausel, die nicht erfüllt ist und negiere zufällig eine der Variablen in dieser Klausel &lt;br /&gt;
               # (die Klausel ist jetzt erfüllt)&lt;br /&gt;
    return None  # keine Lösung gefunden&lt;br /&gt;
&lt;br /&gt;
Findet der Algorithmus eine Lösung, wissen wir, dass der Ausdruck erfüllbar ist. Andernfalls könnte der Ausdruck unerfüllbar sein, oder wir haben nur Pech gehabt. Je mehr erfolglose Versuche wir machen, desto höher ist die Wahrscheinlichkeit, dass das erste zutrifft.&lt;br /&gt;
&lt;br /&gt;
Es ist sinnvoll, &amp;lt;tt&amp;gt;steps = k*n&amp;lt;/tt&amp;gt; zu wählen. Dann gilt der &lt;br /&gt;
;Satz: Wenn ein Ausdruck in k-CNF mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; erfüllbar ist, muss man im Mittel &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in O\left(\left(\frac{2(k-1)}{k}\right)^n \right)&amp;lt;/math&amp;gt; Versuche machen, um eine Lösung zu finden.&lt;br /&gt;
&lt;br /&gt;
Für &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; gilt stets &amp;lt;math&amp;gt;\frac{2(k-1)}{k} &amp;gt; 1&amp;lt;/math&amp;gt;, man benötigt also eine in n exponentielle Anzahl von Versuchen. Bei &amp;lt;math&amp;gt;k=3&amp;lt;/math&amp;gt; gilt z.B. &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt; \in O\left(\left(\frac{4}{3}\right)^n\right)&amp;lt;/math&amp;gt;. Dies ist zwar im Mittel effizienter also die erschöpfende Suche, die &amp;lt;math&amp;gt;O(2^n)&amp;lt;/math&amp;gt; Schritte benötigt, aber immer noch sehr langsam.&lt;br /&gt;
&lt;br /&gt;
Der Fall &amp;lt;b&amp;gt;&amp;lt;math&amp;gt;k=2&amp;lt;/math&amp;gt; ist jedoch ein Sonderfall&amp;lt;/b&amp;gt;: Hier kann man leicht beweisen, dass eine Lösung im Mittel bereits nach &amp;lt;math&amp;gt;O\left(n^2\right)&amp;lt;/math&amp;gt; Schritten gefunden wird. Wenn man schon weiss, dass der Ausdruck erfüllbar ist (was mit [[Graphen_und_Graphenalgorithmen#Lösung des 2-SAT-Problems mit Implikationgraphen|Implikationgraphen]] leicht geprüft werden kann), lässt man den randomisierten Algorithmus einfach so lange laufen, bis er eine Lösung findet. Man setzt also &amp;lt;tt&amp;gt;step = infinity&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;trials = 1&amp;lt;/tt&amp;gt; und verlässt sich darauf, dass das &amp;lt;tt&amp;gt;return&amp;lt;/tt&amp;gt; mit einer gültigen Lösung früher oder später ausgeführt wird. Dass man darauf im Mittel nur &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte warten muss, zeigen wir jetzt mit Hilfe eines &amp;lt;i&amp;gt;random walk&amp;lt;/i&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk===&lt;br /&gt;
&lt;br /&gt;
Um die Random Walk Analyse zu verstehen, betrachten wir folgendes Spiel:&lt;br /&gt;
&lt;br /&gt;
   geg.: eine Stuhlreihe mit N Stühlen. Wir nummerieren die Stühle so, dass links der Stuhl 0 und rechts der Stuhl N steht.&lt;br /&gt;
   &lt;br /&gt;
   * Eine Person setzt sich zufällig auf einen der Stühle.&lt;br /&gt;
   * Eine zweite Person wirft eine Münze.&lt;br /&gt;
   &lt;br /&gt;
         Wenn die Münze auf Zahl fällt, rückt die erste Person einen Stuhl nach links, andernfalls nach rechts.&lt;br /&gt;
         &amp;lt;--- Zahl                                                                                    Kopf ---&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
   * Frage: Wie oft muss man die Münze im Durchschnitt werfen, bis Person 1 zum ersten Mal auf Stuhl N sitzt?&lt;br /&gt;
&lt;br /&gt;
Da die erste Person sich anfangs zufällig hinsetzt, haben wir eine Chance von 1/N, dass sie gleich auf dem richtigen Stuhl landet und wir 0 Schritte benötigen. Mit der gleichen Wahrscheinlichkeit von 1/N setzt sie sich anfangs auf Stuhl Nummer (N-1), und wir haben eine fifty-fifty-Chance, mit nur einem Wurf durchzukommen. Wir können aber auch Pech haben und landen auf Stuhl Nummer (N-2). Das ist das Gleiche, als wenn Person 1 von Anfang an auf diesem Stuhl gesessen hätte, nur dass wir jetzt bereits einen Wurf verbraucht haben. Man sieht, dass man die Zahl der Restwürfe immer in dieser Art ausdrücken kann: Sitzt Person 1 auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, kann sie entweder nach rechts rücken und benötigt dann noch soviele Würfe, wie man typischerweise für Stuhl &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; benötigt, plus den Wurf von &amp;lt;tt&amp;gt;i =&amp;gt; i+1&amp;lt;/tt&amp;gt;. Oder sie kann nach links rücken und benötigt dann die typische Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; plus den Wurf &amp;lt;tt&amp;gt;i =&amp;gt; i-1&amp;lt;/tt&amp;gt;. Beide Möglichkeiten haben die Wahrscheinlichkeit 1/2. Mathematisch kann man dies elegant als Rekursionsformel schreiben, die die erwartete Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; als Funktion der entsprechenden Wurfzahlen für die Stühle &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; ausdrückt:&lt;br /&gt;
&lt;br /&gt;
* Wenn wir uns auf Stuhl N befinden, werfen wir gar nicht: &amp;lt;math&amp;gt;W\left(N\right)=0&amp;lt;/math&amp;gt;&lt;br /&gt;
* Von Stuhl 0 gehen wir immer zu Stuhl 1: &amp;lt;math&amp;gt;W\left(0\right)=1 + W\left(1\right)&amp;lt;/math&amp;gt;&lt;br /&gt;
* Allgemeiner Fall: &amp;lt;math&amp;gt;W\left(i\right)=\frac 1 2 \left(1 + W\left(i+1\right)\right) + \frac 1 2 \left(1 + W\left(i-1\right)\right) = \frac 1 2 W\left(i+1\right) + \frac 1 2 W\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
Diese Rekursion wird durch die explizite Formel&lt;br /&gt;
::&amp;lt;math&amp;gt;W\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
gelöst, wie man durch Einsetzen leicht nachprüft:&lt;br /&gt;
::&amp;lt;math&amp;gt;&lt;br /&gt;
       \begin{align} &lt;br /&gt;
             W\left(N\right) &amp;amp; = N^2-N^2=0 \\&lt;br /&gt;
                  &lt;br /&gt;
             W\left(0\right) &amp;amp;= W\left(1\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-1^2+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2 - 0^2\\&lt;br /&gt;
                 &lt;br /&gt;
              W\left(i\right) &amp;amp;= \frac 1 2 \left(N^2-\left(i-1\right)^2\right) + \frac 1 2 \left(N^2-\left(i+1\right)^2\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= \frac 1 2 N^2-\frac 1 2 \left( i^2-2i+1\right) + \frac 1 2 N^2-\frac 1 2 \left(i^2+2i+1\right) + 1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-i^2&lt;br /&gt;
       \end{align}&amp;lt;/math&amp;gt;&lt;br /&gt;
Insbesondere braucht man im ungünstigen Fall (Start auf Stuhl 0) im Durchschnitt &amp;lt;math&amp;gt;N^2&amp;lt;/math&amp;gt; Würfe, im typischen Fall (Start in der Mitte, also bei &amp;lt;math&amp;gt;i = N/2&amp;lt;/math&amp;gt;) im Durchschnitt &lt;br /&gt;
:&amp;lt;math&amp;gt;N^2 - (N/2)^2=\frac 3 4 N^2\in O(N^2)&amp;lt;/math&amp;gt; &lt;br /&gt;
Würfe. Die '''Beziehung zum randomisiertem 2-SAT-Algorithmus''' ist jetzt leicht zu erkennen. Sitzt die Person auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, interpretieren wir das als:&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Stuhl &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt;&amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert, &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
Wählt der Algorithmus eine Klausel, die nicht erfüllt ist, gibt es zwei Möglichkeiten:&lt;br /&gt;
# Beide Literale in der Klausel haben den falschen Wert: Die Lösung wird auf jeden Fall besser, egal welche der beiden wir umdrehen. Wir gehen also von Zustand &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
# Nur eins der Literale hat den falschen Wert: Beim Umdrehen haben wir eine fifty-fifty-Chance, das richtige Literal zu wählen und in den Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; zu gelangen. Mit der selben Wahrscheinlichkeit wählen wir das falsche Literal und landen im Zustand &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
Falls 2 ist der ungünstigere und entspricht unserem Spiel, dessen Analyse wir deshalb einfach auf das 2-SAT-Problem übertragen können: Ziel des Algorithmus ist es, in den Zustand N zu gelangen, und deshalb gilt genau wie beim Spiel der &lt;br /&gt;
;Satz: Der randomisierte 2-SAT-Algorithmus findet im Durchschnitt nach &amp;lt;math&amp;gt;O(N^2)&amp;lt;/math&amp;gt; Versuchen eine Lösung, wenn das Problem erfüllbar ist.&lt;br /&gt;
Damit ist der randomisierte Algorithmus für dieses Problem effizient, was Sie in Übung 12 experimentell nachprüfen sollen.&lt;br /&gt;
&lt;br /&gt;
== 2. RANSAC-ALGORITHMUS (Random Sample Consensus)==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''Aufgabe:''&amp;lt;/u&amp;gt; gegeben: Datenpunkte&lt;br /&gt;
::gesucht: Modell, das die Datenpunkte erklärt&lt;br /&gt;
&lt;br /&gt;
[[Image:Rubto.png|thumb|250px|none]]&lt;br /&gt;
&lt;br /&gt;
'''Messpunkte:'''&lt;br /&gt;
   &lt;br /&gt;
      übliche Lösung: Methode der kleinsten Quadrate&lt;br /&gt;
      &lt;br /&gt;
      &amp;lt;math&amp;gt;\min_{a,b} 	\sum_{i} \left(a x_i + b + y_i\right)^2&amp;lt;/math&amp;gt;&lt;br /&gt;
      &lt;br /&gt;
      Schulmathematik:      &amp;lt;math&amp;gt;Minimum\stackrel{\wedge}{=}Ableitung=0&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lineares Gleichungssystem'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{da}\sum{i} \left(ax_i+b-y_i\right)^2=\sum{i} \frac{d}{da} \left[ax_i+b-y_i\right)^2&amp;lt;/math&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(g\left(x\right)\right)&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(x\right)=x^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;y\left(a\right)=ax_i+b-y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;=\sum_{i}2\left(ax_i+b-y_i\right)\frac{d}{da} \underbrace {ax_i+b-y_i}_{x_i}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\underline {=2\sum_{i}\left(ax_i+b-y_i\right)x_i\stackrel{!}{=}0}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}{x_i}^2+b\sum_{i}x_i=\sum_{i}x_iy_i&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}x_i+b\sum_{i}1=\sum_{i}y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{db}\sum_{i}\left(ax_i+b-y_i\right)^2=2\sum_{i}\left(ax_i+b-y_i\right)*1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:Problem: &amp;lt;math&amp;gt;\epsilon  %&amp;lt;/math&amp;gt; der Datenpunkte sind Outlier&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\Longrightarrow&amp;lt;/math&amp;gt; Einfaches Anpassen des Modells an die Datenpunkte funktioniert nicht&lt;br /&gt;
&lt;br /&gt;
:Seien mindestens k Datenpunkte notwendig, um das Programm anpassen zu können&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
RANSAC-Algorithmus&lt;br /&gt;
&lt;br /&gt;
      for  l in range (trials):&lt;br /&gt;
           wähle zufällig k Punkte aus&lt;br /&gt;
           passe das Modell an die k Punkte an&lt;br /&gt;
           zähle, wieviele Punkte in der Nähe des Modells liegen (d.h. &amp;lt;math&amp;gt;d_i &amp;lt; d_max&amp;lt;/math&amp;gt; muss geschickt gewählt werden) &lt;br /&gt;
                                           #Bsp. Geradenfinden:-wähle a,b aus zwei Punkten&lt;br /&gt;
                                                               -berechne: &amp;lt;math&amp;gt;|ax_i+b-y_i|=d_i&amp;lt;/math&amp;gt;&lt;br /&gt;
                                                               -zähle Punkt i als Inlier, falls &amp;lt;math&amp;gt;d_i&amp;lt;d_ma&amp;lt;/math&amp;gt;&lt;br /&gt;
      return: Modell mit höchster Zahl der Inlier&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;trials= \frac{log\left(1-p\right)}{log\left(1-\left(1-\epsilon\right)^k\right)}&amp;lt;/math&amp;gt;  mit k=Anzahl der Datenpunkte und p=Erfolgswahrscheinlichkeit, &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;=Outlier-Anteil&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Erfolgswahrscheinlichkeit: p=99%'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{|c||c|c|c|c|c|}&lt;br /&gt;
         Beispiel &amp;amp; k &amp;amp; \epsilon=10% &amp;amp; 20% &amp;amp; 50% &amp;amp; 70%\\&lt;br /&gt;
         \hline&lt;br /&gt;
         Linie\;in\;2D &amp;amp; 2 &amp;amp; 3 &amp;amp;5 &amp;amp; 17 &amp;amp; 49\\&lt;br /&gt;
         Kreis\;in\;2D &amp;amp; 3 &amp;amp; 4 &amp;amp; 7 &amp;amp; 35 &amp;amp; 169\\&lt;br /&gt;
         Ebene\;in\;3D &amp;amp; 8 &amp;amp; 9 &amp;amp; 26 &amp;amp; 1172 &amp;amp; 70188\\&lt;br /&gt;
       \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== ''' Zufallszahlen ''' ==&lt;br /&gt;
&lt;br /&gt;
:- kann man nicht mit deterministischen Computern erzeugen&lt;br /&gt;
:- aber man kann Pseudo-Zufallszahlen erzeugen, die viele Eigenschaften von echten Zufallszahlen haben&lt;br /&gt;
::: * sehr ähnlich  zum Hash&lt;br /&gt;
&lt;br /&gt;
     ''&amp;quot;linear Conguential Random number generator&amp;quot;''&lt;br /&gt;
        &amp;lt;math&amp;gt;I_{i+1}= \left(a*I_i + c\right)\textrm{mod\ } m&amp;lt;/math&amp;gt;&lt;br /&gt;
        &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{=&amp;gt; } &amp;amp; I_i \in [0, m-1]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:-sorgfältige Wahl von  a, c, m notwendig&lt;br /&gt;
::'''Bsp.'''  m = 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;&lt;br /&gt;
::: a = 1664525, c = 1013904223&lt;br /&gt;
::: ''&amp;quot;quick and dirty generator&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
==='''Nachteile'''===&lt;br /&gt;
&lt;br /&gt;
* nicht zufällig genug für viele Anwendungen&lt;br /&gt;
::'''Bsp.''' wähle Punkt in R&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
      \mathrm{ } &amp;amp; p = (rand(), rand(), rand())\\&lt;br /&gt;
&lt;br /&gt;
      \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::gibt Zahl u, v, w so, dass &lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; u * p[0] + v * p[1] + w * p[3]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::stark geclustert ist.&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge ist zu kurz:&lt;br /&gt;
:: spätestens nach m Schritten wiederholt sich die Folge&lt;br /&gt;
&lt;br /&gt;
::'''allgemein''': falls der interne Zustand des Zufallsgenerators ''k'' bits hat, ist Periodenlänge:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; Periode &amp;lt; 2^k\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* ''lowbits'' sind weniger zufällig als die ''highbits''&lt;br /&gt;
----&lt;br /&gt;
=== ''Mersenne Twister''===&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
'''bester zur Zeit bekannter Zufallszahlengenerator (ZZG)'''&lt;br /&gt;
* innere Zustand: &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; 624*32 bit\ Integers  =&amp;gt; 19968 bits\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge: &amp;lt;math&amp;gt;2^ {19937} \approx 4 * 10^{6000}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Punkte aus aufeinanderfolgende Zufallszahlen in &amp;lt;math&amp;gt;\mathbb{R}^n&amp;lt;/math&amp;gt; sind gleich verteilt bis &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; n = 623\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* alle Bits sind unabhängig voneinander zufällig (&amp;quot;Twister&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
* schnell&lt;br /&gt;
&lt;br /&gt;
    class MersenneTwister:&lt;br /&gt;
        &lt;br /&gt;
        def __init__(self, seed):&lt;br /&gt;
            self.N = 624  # Größe des inneren Zustands festlegen&lt;br /&gt;
            self.i = 0    # zählt mit in welchem Zustand wir uns gerade aufhalten&lt;br /&gt;
            &lt;br /&gt;
            self.state = [0]*self.N  # Speicher für den inneren Zustand reservieren&lt;br /&gt;
            &lt;br /&gt;
            self.state[0] = seed     # initiale Zufallszahl vom Benutzer&lt;br /&gt;
            # den Rest des inneren Zustands mit einfachem Zufallszahlengenerator initialisieren&lt;br /&gt;
            for i in xrange(1, self.N):&lt;br /&gt;
                self.state[i] = (1812433253 * (self.state[i-1] ^ (self.state[i-1] &amp;gt;&amp;gt; 30)) + i) % 4294967296&lt;br /&gt;
     &lt;br /&gt;
        def __call__(self):&lt;br /&gt;
            &amp;quot;&amp;quot;&amp;quot;gibt die nächste Zufallszahl im Bereich [0, 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;-1] aus&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
            N, M = self.N, 397&lt;br /&gt;
            &lt;br /&gt;
            # Zustand aktualisieren (neue Zufallszahl ausrechnen)&lt;br /&gt;
            i = self.i&lt;br /&gt;
            r = ((self.state[i] &amp;amp; 0x80000000) | (self.state[(i+1)%N] &amp;amp; 0x7FFFFFFF)) &amp;gt;&amp;gt; 1&lt;br /&gt;
            if self.state[(i+1)%N] &amp;amp; 1:&lt;br /&gt;
                r ^= 0x9908B0DF&lt;br /&gt;
            self.state[i] = self.state[(i+M)%N] ^ r&lt;br /&gt;
     &lt;br /&gt;
            # aktuelle Zufallszahl auslesen und ihre Zufälligkeit durch verwürfeln der Bits verbessern&lt;br /&gt;
            y = self.state[i]&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 11)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt;  7) &amp;amp; 0x9D2C5680)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt; 15) &amp;amp; 0xEFC60000)&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 18)&lt;br /&gt;
            &lt;br /&gt;
            # Zustand weitersetzen und endgültige Zufallszahl ausgeben&lt;br /&gt;
            self.i = (self.i + 1) % N&lt;br /&gt;
            return y&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''geg.:''' Zufallszahl &lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, \overbrace{2^{32}-1}^{m-1}]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ges.:''' Zufallszahl&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, k - 1]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''naive Lösung:'''  &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; rand()%k\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;  ist schlecht.&lt;br /&gt;
&lt;br /&gt;
'''Bsp.'''&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; \qquad m = 16\qquad k = 11\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
! rand() || 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15&lt;br /&gt;
|-&lt;br /&gt;
! rand()%k&lt;br /&gt;
! 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 0 || 1 || 2 || 3 || 4 &lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; 0,...,4 kommt doppelt so häufig wie 5,...,10 &amp;quot;nicht zufällig&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lösung:'''  Zurückweisen des Rests der Zahlen (''rejektion sampling'')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; remainder = (m - 1 - (k - 1))% k = (m - k)%k\\&lt;br /&gt;
        \mathrm{ } &amp;amp; last\ Good\ Value = m-1-remainder\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  r = rand()&lt;br /&gt;
  while r &amp;gt; last.GoodValue:&lt;br /&gt;
        r = rand()&lt;br /&gt;
        return r%k&lt;br /&gt;
&lt;br /&gt;
[[Greedy-Algorithmen und Dynamische Programmierung|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5440</id>
		<title>Randomisierte Algorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5440"/>
				<updated>2012-07-30T11:55:15Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Anwendung: Lösen des K-SAT-Problems */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Randomisierte Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
;Definition: Randomisierte Algorithmen sind Algorithmen, die bei Entscheidungen über ihr weiteres Vorgehen oder bei der Wahl ihrer Parameter Zufallszahlen benutzen.&lt;br /&gt;
&lt;br /&gt;
Anschaulich gesprochen, wersucht man bei randomisierten Algorithmen, einen Teil der Lösung zu &amp;lt;i&amp;gt;raten&amp;lt;/i&amp;gt;. Auf den ersten Blick würde man vermuten, dass dabei nicht viel Sinnvolles herauskommen kann. Diese Kapitel wird jedoch zeigen, dass man durch geschicktes Raten tatsächlich zu sehr eleganten Algorithmen gelangen kann.&lt;br /&gt;
&lt;br /&gt;
Grundsätzlich unterscheidet man zwei Arten von randomisierten Algorithmen:&lt;br /&gt;
;Las Vegas - Algorithmen: Das Ergebnis des Algorithmus ist immer korrekt, und die Berechnung erfolgt mit hoher Wahrscheinlichkeit effizient.&lt;br /&gt;
;Monte Carlo - Algorithmen: Die Berechnung ist immer effizient, und das Ergebnis ist mit hoher Wahrscheinlichkeit korrekt.&lt;br /&gt;
Las Vegas-Algorithmen verwendet man, wenn der Algorithmus im ungünstigen Fall eine schlechte Laufzeit hat, und der ungünstige Fall kann durch die Randomisierung sehr unwahrscheinlich gemacht werden. Wir haben in der Vorlesung schon mehrere Las Vegas-Algorithmen kennen gelernt:&lt;br /&gt;
* Quick Sort mit zufälliger Wahl des Pivot-Elements: Die Randomisierung verhindert, dass das Array immer wieder in Subarrays von sehr unterschiedlicher Größe aufgeteilt wird.&lt;br /&gt;
* Treap mit zufälligen Prioritäten: Die Randomisierung verhindert, dass der Baum schlecht balanciert ist.&lt;br /&gt;
* Universelles Hashing: Die zufällige Wahl der Hashfunktion verhindert, dass ein Angreifer eine Schlüsselmenge mit sehr vielen Kollisionen konstruieren kann.&lt;br /&gt;
* Erzeugung einer perfekten Hashfunktion: Durch die Randomisierung entsteht mit nach wenigen Versuchen ein zyklenfreier Graph, der zur Definition der Hashfunktion geeignet ist.&lt;br /&gt;
Monte Carlo-Algorithmen verwendet man dagegen, wenn kein effizienter deterministischer Algorithmus für ein Problem bekannt ist. Man gibt sich dann damit zufrieden, dass der randomisierte Algorithmus die korrekte Lösung nur mit hoher Wahrscheinlichkeit findet, wenn dies dafür sehr effizient geschieht. Bei manchen Problemen ist auch dies unerreichbar - man muss dann bereits zufrieden sein, wenn der Algorithmus mit hoher Wahrscheinlichkeit eine sehr gute Näherungslösung findet. Beliebte Anwendungsgebiete für Monte Carlo-Algorithmen sind beispielsweise&lt;br /&gt;
* Randomisierte Primzahl-Tests: Moderne Verschlüsselungsverfahren benötigen zahlreiche Primzahlen, aber exakte Primzahltests sind teuer. Der [http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin-Test] findet effizient Zahlen, die mit sehr hoher Wahrscheinlichkeit tatsächlich Primzahlen sind.&lt;br /&gt;
* Randomisiertes Testen: Wie jeder Test kann auch eine randomisierter Test nicht die Abwesenheit von Programmierfehlern garantieren, aber man kann durch die Randomisierung viel mehr Testfälle generieren und erhöht so die Erfolgswarscheinlichkeit. Wir haben als Beispiel dafür den [[Korrektheit#Beispiel_f.C3.BCr_das_Testen:_Freivalds_Algorithmus|Algorithmus von Freivald]] behandelt.&lt;br /&gt;
* Lösung schwieriger Optimierungsprobleme: Wir zeigen unten, dass ein randomisierter Algorithmus effizient eine Lösung für das 2-SAT-Problem aus dem vorherigen Kapitel findet (für k-SAT mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; liefert der Algorithmus immer noch mit einer gewissen Wahrscheinlichkeit das richtige Ergebnis, ist aber nicht mehr effizient). Einen effizienten Approximationsalgorithmus für des Problem des Handelsreisenden behandlen wir im Kapitel [[NP-Vollständigkeit]]. Weitere wichtige Beispiele für diesen Bereich sind [http://en.wikipedia.org/wiki/Simulated_annealing simulated annealing] und das [http://de.wikipedia.org/wiki/MCMC-Verfahren Markov-Chain-Monte-Carlo-Verfahren].&lt;br /&gt;
* Robuste Statistik: Eine Grundaufgabe der Statistik ist das Anpassen (Fitten) von Modellen an gemessene Werte. Wenn die Messungen jedoch &amp;quot;Ausreißer&amp;quot; (einige völlig falsche Werte) enthalten, geht die Anpassung schief. Wir beschreiben unten den RANSAC-Algorithmus, der die Ausreißer identifizieren und beim Modellfitten ignorieren kann.&lt;br /&gt;
&lt;br /&gt;
Obwohl randomisierte Algorithmen oft einfach und elegant sind, ist ihre theoretische Analyse (also das Führen von Korrektheits- und Komplexitätsbeweisen) häufig sehr schwierig. Man muss fortgeschrittene Methoden der Wahrscheinlichkeitsrechnung und Statistik beherrschen, um die Wahrscheinlichkeit für das Versagen des Algorithmus zu berechnen und um zu zeigen, wie man den Algorithmus benutzt, damit diese Wahrscheinlichkeit unter einer akzeptablen Schranke bleibt. Die Algorithmen, die wir für diese Vorlesung ausgewählt haben, zeichnen sich dadurch aus, dass die Beweise hier einfach zu erbringen sind.&lt;br /&gt;
&lt;br /&gt;
== Anwendung: Lösen des K-SAT-Problems ==&lt;br /&gt;
&lt;br /&gt;
Der &amp;lt;b&amp;gt;Algorithmus von Schöning&amp;lt;/b&amp;gt; löst das [[Graphen_und_Graphenalgorithmen#Normalformen für logische Ausdrücke|k-SAT-Problem]] durch Raten: Wenn ein Ausdruck in k-CNF den Wert False hat, gibt es mindestens eine Klausel, die den Wert False hat. Alle Literale in dieser Klausel haben ebenfalls den Wert False, denn jede Klausel ist eine ODER-Verknüpfung, die nur dann False werden kann. Um den Ausdruck zu erfüllen, muss jede Klausel den Wert True annehmen, also müssen wir den Wert von mindestens einem Literal umdrehen. Wenn der Ausruck tatsächlich erfüllbar ist, gibt es immer ein geeignetes Literal, wir wissen nur nicht, welches. Deshalb drehen wir ein unter den k Literalen der betreffenden Klausel zufällig gewähltes. Liegen wir mit unserer Wahl richtig, sind wir der Lösung näher gekommen - im besten Fall sind jetzt alle Klauseln erfüllt. Wählen wir jedoch die falsche Variable, ist die aktuelle Klausel zwar jetzt True, aber dafür werden andere Klauseln zu False, die bisher True waren, und wir entfernen uns somit von der Lösung.&lt;br /&gt;
&lt;br /&gt;
    geg.: logischer Ausdruck in K-CNF (n Variablen, m Klauseln, k Variablen pro Klausel)&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;math&amp;gt;\underbrace {\underbrace {\left(x_1 \vee x_3 \vee...\right)}_{k\; Literale} \wedge \left( x_2 \vee x_4 \vee...\right)}_{m\;Klauseln}&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Algorithmus von Schöning lautet in Pseudocode:&lt;br /&gt;
    for i in range (trials):    #Anzahl der Versuche&lt;br /&gt;
         Bestimme eine Zufallsbelegung der Variablen &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
         for j in range (steps):&lt;br /&gt;
               if &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt; erfüllt alle Klauseln: &lt;br /&gt;
                   return &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
               wähle zufällig eine Klausel, die nicht erfüllt ist und negiere zufällig eine der Variablen in dieser Klausel &lt;br /&gt;
               # (die Klausel ist jetzt erfüllt)&lt;br /&gt;
    return None  # keine Lösung gefunden&lt;br /&gt;
&lt;br /&gt;
Findet der Algorithmus eine Lösung, wissen wir, dass der Ausdruck erfüllbar ist. Andernfalls könnte der Ausdruck unerfüllbar sein, oder wir haben nur Pech gehabt. Je mehr erfolglose Versuche wir machen, desto höher ist die Wahrscheinlichkeit, dass das erste zutrifft.&lt;br /&gt;
&lt;br /&gt;
Es ist sinnvoll, &amp;lt;tt&amp;gt;steps = k*n&amp;lt;/tt&amp;gt; zu wählen. Dann gilt der &lt;br /&gt;
;Satz: Wenn ein Ausdruck in k-CNF mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; erfüllbar ist, muss man im Mittel &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in O\left(\left(\frac{2(k-1)}{k}\right)^n \right)&amp;lt;/math&amp;gt; Versuche machen, um eine Lösung zu finden.&lt;br /&gt;
&lt;br /&gt;
Für &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; gilt stets &amp;lt;math&amp;gt;\frac{2(k-1)}{k} &amp;gt; 1&amp;lt;/math&amp;gt;, man benötigt also eine in n exponentielle Anzahl von Versuchen. Bei &amp;lt;math&amp;gt;k=3&amp;lt;/math&amp;gt; gilt z.B. &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt; \in O\left(\left(\frac{4}{3}\right)^n\right)&amp;lt;/math&amp;gt;. Dies ist zwar im Mittel effizienter also die erschöpfende Suche, die &amp;lt;math&amp;gt;O(2^n)&amp;lt;/math&amp;gt; Schritte benötigt, aber immer noch sehr langsam.&lt;br /&gt;
&lt;br /&gt;
Der Fall &amp;lt;b&amp;gt;&amp;lt;math&amp;gt;k=2&amp;lt;/math&amp;gt; ist jedoch ein Sonderfall&amp;lt;/b&amp;gt;: Hier kann man leicht beweisen, dass eine Lösung im Mittel bereits nach &amp;lt;math&amp;gt;O\left(n^2\right)&amp;lt;/math&amp;gt; Schritten gefunden wird. Wenn man schon weiss, dass der Ausdruck erfüllbar ist (was mit [[Graphen_und_Graphenalgorithmen#Lösung des 2-SAT-Problems mit Implikationgraphen|Implikationgraphen]] leicht geprüft werden kann), lässt man den randomisierten Algorithmus einfach so lange laufen, bis er eine Lösung findet. Man setzt also &amp;lt;tt&amp;gt;step = infinity&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;trials = 1&amp;lt;/tt&amp;gt; und verlässt sich darauf, dass das &amp;lt;tt&amp;gt;return&amp;lt;/tt&amp;gt; mit einer gültigen Lösung früher oder später ausgeführt wird. Dass man darauf im Mittel nur &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte warten muss, zeigen wir jetzt mit Hilfe eines &amp;lt;i&amp;gt;random walk&amp;lt;/i&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk===&lt;br /&gt;
&lt;br /&gt;
Um die Random Walk Analyse zu verstehen, betrachten wir folgendes Spiel:&lt;br /&gt;
&lt;br /&gt;
   geg.: eine Stuhlreihe mit N Stühlen. Wir nummerieren die Stühle so, dass links der Stuhl 0 und rechts der Stuhl N steht.&lt;br /&gt;
   &lt;br /&gt;
   * Eine Person setzt sich zufällig auf einen der Stühle.&lt;br /&gt;
   * Eine zweite Person wirft eine Münze.&lt;br /&gt;
   &lt;br /&gt;
         Wenn die Münze auf Zahl fällt, rückt die erste Person einen Stuhl nach links, andernfalls nach rechts.&lt;br /&gt;
         &amp;lt;--- Zahl                                                                                    Kopf ---&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
   * Frage: Wie oft muss man die Münze im Durchschnitt werfen, bis Person 1 zum ersten Mal auf Stuhl N sitzt?&lt;br /&gt;
&lt;br /&gt;
Da die erste Person sich anfangs zufällig hinsetzt, haben wir eine Chance von 1/N, dass sie gleich auf dem richtigen Stuhl landet und wir 0 Schritte benötigen. Mit der gleichen Wahrscheinlichkeit von 1/N setzt sie sich anfangs auf Stuhl Nummer (N-1), und wir haben eine fifty-fifty-Chance, mit nur einem Wurf durchzukommen. Wir können aber auch Pech haben und landen auf Stuhl Nummer (N-2). Das ist das Gleiche, als wenn Person 1 von Anfang an auf diesem Stuhl gesessen hätte, nur dass wir jetzt bereits einen Wurf verbraucht haben. Man sieht, dass man die Zahl der Restwürfe immer in dieser Art ausdrücken kann: Sitzt Person 1 auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, kann sie entweder nach rechts rücken und benötigt dann noch soviele Würfe, wie man typischerweise für Stuhl &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; benötigt, plus den Wurf von &amp;lt;tt&amp;gt;i =&amp;gt; i+1&amp;lt;/tt&amp;gt;. Oder sie kann nach links rücken und benötigt dann die typische Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; plus den Wurf &amp;lt;tt&amp;gt;i =&amp;gt; i-1&amp;lt;/tt&amp;gt;. Beide Möglichkeiten haben die Wahrscheinlichkeit 1/2. Mathematisch kann man dies elegant als Rekursionsformel schreiben, die die erwartete Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; als Funktion der entsprechenden Wurfzahlen für die Stühle &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; ausdrückt:&lt;br /&gt;
&lt;br /&gt;
* Wenn wir uns auf Stuhl N befinden, werfen wir gar nicht: &amp;lt;math&amp;gt;W\left(N\right)=0&amp;lt;/math&amp;gt;&lt;br /&gt;
* Von Stuhl 0 gehen wir immer zu Stuhl 1: &amp;lt;math&amp;gt;W\left(0\right)=1 + W\left(1\right)&amp;lt;/math&amp;gt;&lt;br /&gt;
* Allgemeiner Fall: &amp;lt;math&amp;gt;W\left(i\right)=\frac 1 2 \left(1 + W\left(i+1\right)\right) + \frac 1 2 \left(1 + W\left(i-1\right)\right) = \frac 1 2 W\left(i+1\right) + \frac 1 2 W\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
Diese Rekursion wird durch die explizite Formel&lt;br /&gt;
::&amp;lt;math&amp;gt;W\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
gelöst, wie man durch Einsetzen leicht nachprüft:&lt;br /&gt;
::&amp;lt;math&amp;gt;&lt;br /&gt;
       \begin{align} &lt;br /&gt;
             W\left(N\right) &amp;amp; = N^2-N^2=0 \\&lt;br /&gt;
                  &lt;br /&gt;
             W\left(0\right) &amp;amp;= W\left(1\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-1^2+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2 - 0^2\\&lt;br /&gt;
                 &lt;br /&gt;
              W\left(i\right) &amp;amp;= \frac 1 2 \left(N^2-\left(i-1\right)^2\right) + \frac 1 2 \left(N^2-\left(i+1\right)^2\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= \frac 1 2 N^2-\frac 1 2 \left( i^2-2i+1\right) + \frac 1 2 N^2-\frac 1 2 \left(i^2+2i+1\right) + 1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-i^2&lt;br /&gt;
       \end{align}&amp;lt;/math&amp;gt;&lt;br /&gt;
Insbesondere braucht man im ungünstigen Fall (Start auf Stuhl 0) im Durchschnitt &amp;lt;math&amp;gt;N^2&amp;lt;/math&amp;gt; Würfe, im typischen Fall (Start in der Mitte, also bei &amp;lt;math&amp;gt;i = N/2&amp;lt;/math&amp;gt;) im Durchschnitt &lt;br /&gt;
:&amp;lt;math&amp;gt;N^2 - (N/2)^2=\frac 3 4 N^2\in O(N^2)&amp;lt;/math&amp;gt; &lt;br /&gt;
Würfe. Die '''Beziehung zum randomisiertem 2-SAT-Algorithmus''' ist jetzt leicht zu erkennen. Sitzt die Person auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, interpretieren wir das als:&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Stuhl &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt;&amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert, &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
Wählt der Algorithmus eine Klausel, die nicht erfüllt ist, gibt es zwei Möglichkeiten:&lt;br /&gt;
# Beide Literale in der Klausel haben den falschen Wert: Die Lösung wird auf jeden Fall besser, egal welche der beiden wir umdrehen. Wir gehen also von Zustand &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
# Nur eins der Literale hat den falschen Wert: Beim Umdrehen haben wir eine fifty-fifty-Chance, das richtige Literal zu wählen und in den Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; zu gelangen. Mit der selben Wahrscheinlichkeit wählen wir das falsche Literal und landen im Zustand &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
Falls 2 ist der ungünstigere und entspricht unserem Spiel, dessen Analyse wir deshalb einfach auf das 2-SAT-Problem übertragen können: Ziel des Algorithmus ist es, in den Zustand N zu gelangen, und deshalb gilt genau wie beim Spiel der &lt;br /&gt;
;Satz: Der randomisierte 2-SAT-Algorithmus findet im Durchschnitt nach &amp;lt;math&amp;gt;O(N^2)&amp;lt;/math&amp;gt; Versuchen eine Lösung, wenn das Problem erfüllbar ist.&lt;br /&gt;
Damit ist der randomisierte Algorithmus für dieses Problem effizient, was Sie in Übung 12 experimentell nachprüfen sollen.&lt;br /&gt;
&lt;br /&gt;
== 2. RANSAC-ALGORITHMUS (Random Sample Consensus)==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''Aufgabe:''&amp;lt;/u&amp;gt; gegeben: Datenpunkte&lt;br /&gt;
::gesucht: Modell, das die Datenpunkte erklärt&lt;br /&gt;
&lt;br /&gt;
[[Image:Rubto.png|thumb|250px|none]]&lt;br /&gt;
&lt;br /&gt;
'''Messpunkte:'''&lt;br /&gt;
   &lt;br /&gt;
      übliche Lösung: Methode der kleinsten Quadrate&lt;br /&gt;
      &lt;br /&gt;
      &amp;lt;math&amp;gt;\min_{a,b} 	\sum_{i} \left(a x_i + b + y_i\right)^2&amp;lt;/math&amp;gt;&lt;br /&gt;
      &lt;br /&gt;
      Schulmathematik:      &amp;lt;math&amp;gt;Minimum\stackrel{\wedge}{=}Ableitung=0&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lineares Gleichungssystem'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{da}\sum{i} \left(ax_i+b-y_i\right)^2=\sum{i} \frac{d}{da} \left[ax_i+b-y_i\right)^2&amp;lt;/math&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(g\left(x\right)\right)&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(x\right)=x^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;y\left(a\right)=ax_i+b-y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;=\sum_{i}2\left(ax_i+b-y_i\right)\frac{d}{da} \underbrace {ax_i+b-y_i}_{x_i}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\underline {=2\sum_{i}\left(ax_i+b-y_i\right)x_i\stackrel{!}{=}0}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}{x_i}^2+b\sum_{i}x_i=\sum_{i}x_iy_i&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}x_i+b\sum_{i}1=\sum_{i}y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{db}\sum_{i}\left(ax_i+b-y_i\right)^2=2\sum_{i}\left(ax_i+b-y_i\right)*1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:Problem: &amp;lt;math&amp;gt;\epsilon  %&amp;lt;/math&amp;gt; der Datenpunkte sind Outlier&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\Longrightarrow&amp;lt;/math&amp;gt; Einfaches Anpassen des Modells an die Datenpunkte funktioniert nicht&lt;br /&gt;
&lt;br /&gt;
:Seien mindestens k Datenpunkte notwendig, um das Programm anpassen zu können&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
RANSAC-Algorithmus&lt;br /&gt;
&lt;br /&gt;
      for  l in range (trials):&lt;br /&gt;
           wähle zufällig k Punkte aus&lt;br /&gt;
           passe das Modell an die k Punkte an&lt;br /&gt;
           zähle, wieviele Punkte in der Nähe des Modells liegen (d.h. &amp;lt;math&amp;gt;d_i &amp;lt; d_max&amp;lt;/math&amp;gt; muss geschickt gewählt werden) &lt;br /&gt;
                                           #Bsp. Geradenfinden:-wähle a,b aus zwei Punkten&lt;br /&gt;
                                                               -berechne: &amp;lt;math&amp;gt;|ax_i+b-y_i|=d_i&amp;lt;/math&amp;gt;&lt;br /&gt;
                                                               -zähle Punkt i als Inlier, falls &amp;lt;math&amp;gt;d_i&amp;lt;d_ma&amp;lt;/math&amp;gt;&lt;br /&gt;
      return: Modell mit höchster Zahl der Inlier&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;trials= \frac{log\left(1-p\right)}{log\left(1-\left(1-\epsilon\right)^k\right)}&amp;lt;/math&amp;gt;  mit k=Anzahl der Datenpunkte und p=Erfolgswahrscheinlichkeit, &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;=Outlier-Anteil&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Erfolgswahrscheinlichkeit: p=99%'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{|c||c|c|c|c|c|}&lt;br /&gt;
         Beispiel &amp;amp; k &amp;amp; \epsilon=10% &amp;amp; 20% &amp;amp; 50% &amp;amp; 70%\\&lt;br /&gt;
         \hline&lt;br /&gt;
         Linie\;in\;2D &amp;amp; 2 &amp;amp; 3 &amp;amp;5 &amp;amp; 17 &amp;amp; 49\\&lt;br /&gt;
         Kreis\;in\;2D &amp;amp; 3 &amp;amp; 4 &amp;amp; 7 &amp;amp; 35 &amp;amp; 169\\&lt;br /&gt;
         Ebene\;in\;3D &amp;amp; 8 &amp;amp; 9 &amp;amp; 26 &amp;amp; 1172 &amp;amp; 70188\\&lt;br /&gt;
       \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== '''Las Vegas vs. Monte Carlo'''==&lt;br /&gt;
&lt;br /&gt;
   * ''Las Vegas - Algorithmen''&lt;br /&gt;
     - Ergebnis ist immer korrekt.&lt;br /&gt;
     - Berechnung ist mit hoher Wahrscheinlichkeit effizient (d.h. Randomisierung macht den ungünstigsten Fall unwahrscheinlich).&lt;br /&gt;
&lt;br /&gt;
   * ''Monte Carlo - Algorithmen''&lt;br /&gt;
     - Berechnung immer effizient.&lt;br /&gt;
     - Ergebnis mit hoher Wahrscheinlichkeit korrekt (falls kein effizienter Algorithmus bekannt, der immer die richtige Lösung liefert).&lt;br /&gt;
&lt;br /&gt;
{| border = &amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;center&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! Las Vegas&lt;br /&gt;
! Monte Carlo&lt;br /&gt;
|- &lt;br /&gt;
| - Erzeugen einer perfekten Hashfuktion &lt;br /&gt;
| - Algorithmus von Freiwald(Matrizenmultiplikation)&lt;br /&gt;
|-&lt;br /&gt;
| - universelles Hashing&lt;br /&gt;
| - RANSAC&lt;br /&gt;
|-&lt;br /&gt;
| - Quick Sort mit zufälliger Wahl des Pivot-Elements&lt;br /&gt;
| - randomisierte K-SAT(k&amp;gt;=3)(Alg. von Schöning)&lt;br /&gt;
|-&lt;br /&gt;
| - Treep mit zufälligen Prioritäten&lt;br /&gt;
| -&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== ''' Zufallszahlen ''' ==&lt;br /&gt;
&lt;br /&gt;
:- kann man nicht mit deterministischen Computern erzeugen&lt;br /&gt;
:- aber man kann Pseudo-Zufallszahlen erzeugen, die viele Eigenschaften von echten Zufallszahlen haben&lt;br /&gt;
::: * sehr ähnlich  zum Hash&lt;br /&gt;
&lt;br /&gt;
     ''&amp;quot;linear Conguential Random number generator&amp;quot;''&lt;br /&gt;
        &amp;lt;math&amp;gt;I_{i+1}= \left(a*I_i + c\right)\textrm{mod\ } m&amp;lt;/math&amp;gt;&lt;br /&gt;
        &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{=&amp;gt; } &amp;amp; I_i \in [0, m-1]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:-sorgfältige Wahl von  a, c, m notwendig&lt;br /&gt;
::'''Bsp.'''  m = 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;&lt;br /&gt;
::: a = 1664525, c = 1013904223&lt;br /&gt;
::: ''&amp;quot;quick and dirty generator&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
==='''Nachteile'''===&lt;br /&gt;
&lt;br /&gt;
* nicht zufällig genug für viele Anwendungen&lt;br /&gt;
::'''Bsp.''' wähle Punkt in R&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
      \mathrm{ } &amp;amp; p = (rand(), rand(), rand())\\&lt;br /&gt;
&lt;br /&gt;
      \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::gibt Zahl u, v, w so, dass &lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; u * p[0] + v * p[1] + w * p[3]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::stark geclustert ist.&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge ist zu kurz:&lt;br /&gt;
:: spätestens nach m Schritten wiederholt sich die Folge&lt;br /&gt;
&lt;br /&gt;
::'''allgemein''': falls der interne Zustand des Zufallsgenerators ''k'' bits hat, ist Periodenlänge:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; Periode &amp;lt; 2^k\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* ''lowbits'' sind weniger zufällig als die ''highbits''&lt;br /&gt;
----&lt;br /&gt;
=== ''Mersenne Twister''===&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
'''bester zur Zeit bekannter Zufallszahlengenerator (ZZG)'''&lt;br /&gt;
* innere Zustand: &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; 624*32 bit\ Integers  =&amp;gt; 19968 bits\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge: &amp;lt;math&amp;gt;2^ {19937} \approx 4 * 10^{6000}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Punkte aus aufeinanderfolgende Zufallszahlen in &amp;lt;math&amp;gt;\mathbb{R}^n&amp;lt;/math&amp;gt; sind gleich verteilt bis &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; n = 623\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* alle Bits sind unabhängig voneinander zufällig (&amp;quot;Twister&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
* schnell&lt;br /&gt;
&lt;br /&gt;
    class MersenneTwister:&lt;br /&gt;
        &lt;br /&gt;
        def __init__(self, seed):&lt;br /&gt;
            self.N = 624  # Größe des inneren Zustands festlegen&lt;br /&gt;
            self.i = 0    # zählt mit in welchem Zustand wir uns gerade aufhalten&lt;br /&gt;
            &lt;br /&gt;
            self.state = [0]*self.N  # Speicher für den inneren Zustand reservieren&lt;br /&gt;
            &lt;br /&gt;
            self.state[0] = seed     # initiale Zufallszahl vom Benutzer&lt;br /&gt;
            # den Rest des inneren Zustands mit einfachem Zufallszahlengenerator initialisieren&lt;br /&gt;
            for i in xrange(1, self.N):&lt;br /&gt;
                self.state[i] = (1812433253 * (self.state[i-1] ^ (self.state[i-1] &amp;gt;&amp;gt; 30)) + i) % 4294967296&lt;br /&gt;
     &lt;br /&gt;
        def __call__(self):&lt;br /&gt;
            &amp;quot;&amp;quot;&amp;quot;gibt die nächste Zufallszahl im Bereich [0, 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;-1] aus&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
            N, M = self.N, 397&lt;br /&gt;
            &lt;br /&gt;
            # Zustand aktualisieren (neue Zufallszahl ausrechnen)&lt;br /&gt;
            i = self.i&lt;br /&gt;
            r = ((self.state[i] &amp;amp; 0x80000000) | (self.state[(i+1)%N] &amp;amp; 0x7FFFFFFF)) &amp;gt;&amp;gt; 1&lt;br /&gt;
            if self.state[(i+1)%N] &amp;amp; 1:&lt;br /&gt;
                r ^= 0x9908B0DF&lt;br /&gt;
            self.state[i] = self.state[(i+M)%N] ^ r&lt;br /&gt;
     &lt;br /&gt;
            # aktuelle Zufallszahl auslesen und ihre Zufälligkeit durch verwürfeln der Bits verbessern&lt;br /&gt;
            y = self.state[i]&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 11)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt;  7) &amp;amp; 0x9D2C5680)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt; 15) &amp;amp; 0xEFC60000)&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 18)&lt;br /&gt;
            &lt;br /&gt;
            # Zustand weitersetzen und endgültige Zufallszahl ausgeben&lt;br /&gt;
            self.i = (self.i + 1) % N&lt;br /&gt;
            return y&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''geg.:''' Zufallszahl &lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, \overbrace{2^{32}-1}^{m-1}]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ges.:''' Zufallszahl&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, k - 1]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''naive Lösung:'''  &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; rand()%k\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;  ist schlecht.&lt;br /&gt;
&lt;br /&gt;
'''Bsp.'''&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; \qquad m = 16\qquad k = 11\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
! rand() || 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15&lt;br /&gt;
|-&lt;br /&gt;
! rand()%k&lt;br /&gt;
! 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 0 || 1 || 2 || 3 || 4 &lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; 0,...,4 kommt doppelt so häufig wie 5,...,10 &amp;quot;nicht zufällig&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lösung:'''  Zurückweisen des Rests der Zahlen (''rejektion sampling'')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; remainder = (m - 1 - (k - 1))% k = (m - k)%k\\&lt;br /&gt;
        \mathrm{ } &amp;amp; last\ Good\ Value = m-1-remainder\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  r = rand()&lt;br /&gt;
  while r &amp;gt; last.GoodValue:&lt;br /&gt;
        r = rand()&lt;br /&gt;
        return r%k&lt;br /&gt;
&lt;br /&gt;
[[Greedy-Algorithmen und Dynamische Programmierung|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5439</id>
		<title>Randomisierte Algorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5439"/>
				<updated>2012-07-30T11:54:03Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* 2. RANSAC-ALGORITHMUS (Random Sample Consensus) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Randomisierte Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
;Definition: Randomisierte Algorithmen sind Algorithmen, die bei Entscheidungen über ihr weiteres Vorgehen oder bei der Wahl ihrer Parameter Zufallszahlen benutzen.&lt;br /&gt;
&lt;br /&gt;
Anschaulich gesprochen, wersucht man bei randomisierten Algorithmen, einen Teil der Lösung zu &amp;lt;i&amp;gt;raten&amp;lt;/i&amp;gt;. Auf den ersten Blick würde man vermuten, dass dabei nicht viel Sinnvolles herauskommen kann. Diese Kapitel wird jedoch zeigen, dass man durch geschicktes Raten tatsächlich zu sehr eleganten Algorithmen gelangen kann.&lt;br /&gt;
&lt;br /&gt;
Grundsätzlich unterscheidet man zwei Arten von randomisierten Algorithmen:&lt;br /&gt;
;Las Vegas - Algorithmen: Das Ergebnis des Algorithmus ist immer korrekt, und die Berechnung erfolgt mit hoher Wahrscheinlichkeit effizient.&lt;br /&gt;
;Monte Carlo - Algorithmen: Die Berechnung ist immer effizient, und das Ergebnis ist mit hoher Wahrscheinlichkeit korrekt.&lt;br /&gt;
Las Vegas-Algorithmen verwendet man, wenn der Algorithmus im ungünstigen Fall eine schlechte Laufzeit hat, und der ungünstige Fall kann durch die Randomisierung sehr unwahrscheinlich gemacht werden. Wir haben in der Vorlesung schon mehrere Las Vegas-Algorithmen kennen gelernt:&lt;br /&gt;
* Quick Sort mit zufälliger Wahl des Pivot-Elements: Die Randomisierung verhindert, dass das Array immer wieder in Subarrays von sehr unterschiedlicher Größe aufgeteilt wird.&lt;br /&gt;
* Treap mit zufälligen Prioritäten: Die Randomisierung verhindert, dass der Baum schlecht balanciert ist.&lt;br /&gt;
* Universelles Hashing: Die zufällige Wahl der Hashfunktion verhindert, dass ein Angreifer eine Schlüsselmenge mit sehr vielen Kollisionen konstruieren kann.&lt;br /&gt;
* Erzeugung einer perfekten Hashfunktion: Durch die Randomisierung entsteht mit nach wenigen Versuchen ein zyklenfreier Graph, der zur Definition der Hashfunktion geeignet ist.&lt;br /&gt;
Monte Carlo-Algorithmen verwendet man dagegen, wenn kein effizienter deterministischer Algorithmus für ein Problem bekannt ist. Man gibt sich dann damit zufrieden, dass der randomisierte Algorithmus die korrekte Lösung nur mit hoher Wahrscheinlichkeit findet, wenn dies dafür sehr effizient geschieht. Bei manchen Problemen ist auch dies unerreichbar - man muss dann bereits zufrieden sein, wenn der Algorithmus mit hoher Wahrscheinlichkeit eine sehr gute Näherungslösung findet. Beliebte Anwendungsgebiete für Monte Carlo-Algorithmen sind beispielsweise&lt;br /&gt;
* Randomisierte Primzahl-Tests: Moderne Verschlüsselungsverfahren benötigen zahlreiche Primzahlen, aber exakte Primzahltests sind teuer. Der [http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin-Test] findet effizient Zahlen, die mit sehr hoher Wahrscheinlichkeit tatsächlich Primzahlen sind.&lt;br /&gt;
* Randomisiertes Testen: Wie jeder Test kann auch eine randomisierter Test nicht die Abwesenheit von Programmierfehlern garantieren, aber man kann durch die Randomisierung viel mehr Testfälle generieren und erhöht so die Erfolgswarscheinlichkeit. Wir haben als Beispiel dafür den [[Korrektheit#Beispiel_f.C3.BCr_das_Testen:_Freivalds_Algorithmus|Algorithmus von Freivald]] behandelt.&lt;br /&gt;
* Lösung schwieriger Optimierungsprobleme: Wir zeigen unten, dass ein randomisierter Algorithmus effizient eine Lösung für das 2-SAT-Problem aus dem vorherigen Kapitel findet (für k-SAT mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; liefert der Algorithmus immer noch mit einer gewissen Wahrscheinlichkeit das richtige Ergebnis, ist aber nicht mehr effizient). Einen effizienten Approximationsalgorithmus für des Problem des Handelsreisenden behandlen wir im Kapitel [[NP-Vollständigkeit]]. Weitere wichtige Beispiele für diesen Bereich sind [http://en.wikipedia.org/wiki/Simulated_annealing simulated annealing] und das [http://de.wikipedia.org/wiki/MCMC-Verfahren Markov-Chain-Monte-Carlo-Verfahren].&lt;br /&gt;
* Robuste Statistik: Eine Grundaufgabe der Statistik ist das Anpassen (Fitten) von Modellen an gemessene Werte. Wenn die Messungen jedoch &amp;quot;Ausreißer&amp;quot; (einige völlig falsche Werte) enthalten, geht die Anpassung schief. Wir beschreiben unten den RANSAC-Algorithmus, der die Ausreißer identifizieren und beim Modellfitten ignorieren kann.&lt;br /&gt;
&lt;br /&gt;
Obwohl randomisierte Algorithmen oft einfach und elegant sind, ist ihre theoretische Analyse (also das Führen von Korrektheits- und Komplexitätsbeweisen) häufig sehr schwierig. Man muss fortgeschrittene Methoden der Wahrscheinlichkeitsrechnung und Statistik beherrschen, um die Wahrscheinlichkeit für das Versagen des Algorithmus zu berechnen und um zu zeigen, wie man den Algorithmus benutzt, damit diese Wahrscheinlichkeit unter einer akzeptablen Schranke bleibt. Die Algorithmen, die wir für diese Vorlesung ausgewählt haben, zeichnen sich dadurch aus, dass die Beweise hier einfach zu erbringen sind.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Lösen des K-SAT-Problems ===&lt;br /&gt;
&lt;br /&gt;
Der &amp;lt;b&amp;gt;Algorithmus von Schöning&amp;lt;/b&amp;gt; löst das [[Graphen_und_Graphenalgorithmen#Normalformen für logische Ausdrücke|k-SAT-Problem]] durch Raten: Wenn ein Ausdruck in k-CNF den Wert False hat, gibt es mindestens eine Klausel, die den Wert False hat. Alle Literale in dieser Klausel haben ebenfalls den Wert False, denn jede Klausel ist eine ODER-Verknüpfung, die nur dann False werden kann. Um den Ausdruck zu erfüllen, muss jede Klausel den Wert True annehmen, also müssen wir den Wert von mindestens einem Literal umdrehen. Wenn der Ausruck tatsächlich erfüllbar ist, gibt es immer ein geeignetes Literal, wir wissen nur nicht, welches. Deshalb drehen wir ein unter den k Literalen der betreffenden Klausel zufällig gewähltes. Liegen wir mit unserer Wahl richtig, sind wir der Lösung näher gekommen - im besten Fall sind jetzt alle Klauseln erfüllt. Wählen wir jedoch die falsche Variable, ist die aktuelle Klausel zwar jetzt True, aber dafür werden andere Klauseln zu False, die bisher True waren, und wir entfernen uns somit von der Lösung.&lt;br /&gt;
&lt;br /&gt;
    geg.: logischer Ausdruck in K-CNF (n Variablen, m Klauseln, k Variablen pro Klausel)&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;math&amp;gt;\underbrace {\underbrace {\left(x_1 \vee x_3 \vee...\right)}_{k\; Literale} \wedge \left( x_2 \vee x_4 \vee...\right)}_{m\;Klauseln}&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Algorithmus von Schöning lautet in Pseudocode:&lt;br /&gt;
    for i in range (trials):    #Anzahl der Versuche&lt;br /&gt;
         Bestimme eine Zufallsbelegung der Variablen &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
         for j in range (steps):&lt;br /&gt;
               if &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt; erfüllt alle Klauseln: &lt;br /&gt;
                   return &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
               wähle zufällig eine Klausel, die nicht erfüllt ist und negiere zufällig eine der Variablen in dieser Klausel &lt;br /&gt;
               # (die Klausel ist jetzt erfüllt)&lt;br /&gt;
    return None  # keine Lösung gefunden&lt;br /&gt;
&lt;br /&gt;
Findet der Algorithmus eine Lösung, wissen wir, dass der Ausdruck erfüllbar ist. Andernfalls könnte der Ausdruck unerfüllbar sein, oder wir haben nur Pech gehabt. Je mehr erfolglose Versuche wir machen, desto höher ist die Wahrscheinlichkeit, dass das erste zutrifft.&lt;br /&gt;
&lt;br /&gt;
Es ist sinnvoll, &amp;lt;tt&amp;gt;steps = k*n&amp;lt;/tt&amp;gt; zu wählen. Dann gilt der &lt;br /&gt;
;Satz: Wenn ein Ausdruck in k-CNF mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; erfüllbar ist, muss man im Mittel &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in O\left(\left(\frac{2(k-1)}{k}\right)^n \right)&amp;lt;/math&amp;gt; Versuche machen, um eine Lösung zu finden.&lt;br /&gt;
&lt;br /&gt;
Für &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; gilt stets &amp;lt;math&amp;gt;\frac{2(k-1)}{k} &amp;gt; 1&amp;lt;/math&amp;gt;, man benötigt also eine in n exponentielle Anzahl von Versuchen. Bei &amp;lt;math&amp;gt;k=3&amp;lt;/math&amp;gt; gilt z.B. &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt; \in O\left(\left(\frac{4}{3}\right)^n\right)&amp;lt;/math&amp;gt;. Dies ist zwar im Mittel effizienter also die erschöpfende Suche, die &amp;lt;math&amp;gt;O(2^n)&amp;lt;/math&amp;gt; Schritte benötigt, aber immer noch sehr langsam.&lt;br /&gt;
&lt;br /&gt;
Der Fall &amp;lt;b&amp;gt;&amp;lt;math&amp;gt;k=2&amp;lt;/math&amp;gt; ist jedoch ein Sonderfall&amp;lt;/b&amp;gt;: Hier kann man leicht beweisen, dass eine Lösung im Mittel bereits nach &amp;lt;math&amp;gt;O\left(n^2\right)&amp;lt;/math&amp;gt; Schritten gefunden wird. Wenn man schon weiss, dass der Ausdruck erfüllbar ist (was mit [[Graphen_und_Graphenalgorithmen#Lösung des 2-SAT-Problems mit Implikationgraphen|Implikationgraphen]] leicht geprüft werden kann), lässt man den randomisierten Algorithmus einfach so lange laufen, bis er eine Lösung findet. Man setzt also &amp;lt;tt&amp;gt;step = infinity&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;trials = 1&amp;lt;/tt&amp;gt; und verlässt sich darauf, dass das &amp;lt;tt&amp;gt;return&amp;lt;/tt&amp;gt; mit einer gültigen Lösung früher oder später ausgeführt wird. Dass man darauf im Mittel nur &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte warten muss, zeigen wir jetzt mit Hilfe eines &amp;lt;i&amp;gt;random walk&amp;lt;/i&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk====&lt;br /&gt;
&lt;br /&gt;
Um die Random Walk Analyse zu verstehen, betrachten wir folgendes Spiel:&lt;br /&gt;
&lt;br /&gt;
   geg.: eine Stuhlreihe mit N Stühlen. Wir nummerieren die Stühle so, dass links der Stuhl 0 und rechts der Stuhl N steht.&lt;br /&gt;
   &lt;br /&gt;
   * Eine Person setzt sich zufällig auf einen der Stühle.&lt;br /&gt;
   * Eine zweite Person wirft eine Münze.&lt;br /&gt;
   &lt;br /&gt;
         Wenn die Münze auf Zahl fällt, rückt die erste Person einen Stuhl nach links, andernfalls nach rechts.&lt;br /&gt;
         &amp;lt;--- Zahl                                                                                    Kopf ---&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
   * Frage: Wie oft muss man die Münze im Durchschnitt werfen, bis Person 1 zum ersten Mal auf Stuhl N sitzt?&lt;br /&gt;
&lt;br /&gt;
Da die erste Person sich anfangs zufällig hinsetzt, haben wir eine Chance von 1/N, dass sie gleich auf dem richtigen Stuhl landet und wir 0 Schritte benötigen. Mit der gleichen Wahrscheinlichkeit von 1/N setzt sie sich anfangs auf Stuhl Nummer (N-1), und wir haben eine fifty-fifty-Chance, mit nur einem Wurf durchzukommen. Wir können aber auch Pech haben und landen auf Stuhl Nummer (N-2). Das ist das Gleiche, als wenn Person 1 von Anfang an auf diesem Stuhl gesessen hätte, nur dass wir jetzt bereits einen Wurf verbraucht haben. Man sieht, dass man die Zahl der Restwürfe immer in dieser Art ausdrücken kann: Sitzt Person 1 auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, kann sie entweder nach rechts rücken und benötigt dann noch soviele Würfe, wie man typischerweise für Stuhl &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; benötigt, plus den Wurf von &amp;lt;tt&amp;gt;i =&amp;gt; i+1&amp;lt;/tt&amp;gt;. Oder sie kann nach links rücken und benötigt dann die typische Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; plus den Wurf &amp;lt;tt&amp;gt;i =&amp;gt; i-1&amp;lt;/tt&amp;gt;. Beide Möglichkeiten haben die Wahrscheinlichkeit 1/2. Mathematisch kann man dies elegant als Rekursionsformel schreiben, die die erwartete Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; als Funktion der entsprechenden Wurfzahlen für die Stühle &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; ausdrückt:&lt;br /&gt;
&lt;br /&gt;
* Wenn wir uns auf Stuhl N befinden, werfen wir gar nicht: &amp;lt;math&amp;gt;W\left(N\right)=0&amp;lt;/math&amp;gt;&lt;br /&gt;
* Von Stuhl 0 gehen wir immer zu Stuhl 1: &amp;lt;math&amp;gt;W\left(0\right)=1 + W\left(1\right)&amp;lt;/math&amp;gt;&lt;br /&gt;
* Allgemeiner Fall: &amp;lt;math&amp;gt;W\left(i\right)=\frac 1 2 \left(1 + W\left(i+1\right)\right) + \frac 1 2 \left(1 + W\left(i-1\right)\right) = \frac 1 2 W\left(i+1\right) + \frac 1 2 W\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
Diese Rekursion wird durch die explizite Formel&lt;br /&gt;
::&amp;lt;math&amp;gt;W\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
gelöst, wie man durch Einsetzen leicht nachprüft:&lt;br /&gt;
::&amp;lt;math&amp;gt;&lt;br /&gt;
       \begin{align} &lt;br /&gt;
             W\left(N\right) &amp;amp; = N^2-N^2=0 \\&lt;br /&gt;
                  &lt;br /&gt;
             W\left(0\right) &amp;amp;= W\left(1\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-1^2+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2 - 0^2\\&lt;br /&gt;
                 &lt;br /&gt;
              W\left(i\right) &amp;amp;= \frac 1 2 \left(N^2-\left(i-1\right)^2\right) + \frac 1 2 \left(N^2-\left(i+1\right)^2\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= \frac 1 2 N^2-\frac 1 2 \left( i^2-2i+1\right) + \frac 1 2 N^2-\frac 1 2 \left(i^2+2i+1\right) + 1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-i^2&lt;br /&gt;
       \end{align}&amp;lt;/math&amp;gt;&lt;br /&gt;
Insbesondere braucht man im ungünstigen Fall (Start auf Stuhl 0) im Durchschnitt &amp;lt;math&amp;gt;N^2&amp;lt;/math&amp;gt; Würfe, im typischen Fall (Start in der Mitte, also bei &amp;lt;math&amp;gt;i = N/2&amp;lt;/math&amp;gt;) im Durchschnitt &lt;br /&gt;
:&amp;lt;math&amp;gt;N^2 - (N/2)^2=\frac 3 4 N^2\in O(N^2)&amp;lt;/math&amp;gt; &lt;br /&gt;
Würfe. Die '''Beziehung zum randomisiertem 2-SAT-Algorithmus''' ist jetzt leicht zu erkennen. Sitzt die Person auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, interpretieren wir das als:&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Stuhl &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt;&amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert, &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
Wählt der Algorithmus eine Klausel, die nicht erfüllt ist, gibt es zwei Möglichkeiten:&lt;br /&gt;
# Beide Literale in der Klausel haben den falschen Wert: Die Lösung wird auf jeden Fall besser, egal welche der beiden wir umdrehen. Wir gehen also von Zustand &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
# Nur eins der Literale hat den falschen Wert: Beim Umdrehen haben wir eine fifty-fifty-Chance, das richtige Literal zu wählen und in den Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; zu gelangen. Mit der selben Wahrscheinlichkeit wählen wir das falsche Literal und landen im Zustand &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
Falls 2 ist der ungünstigere und entspricht unserem Spiel, dessen Analyse wir deshalb einfach auf das 2-SAT-Problem übertragen können: Ziel des Algorithmus ist es, in den Zustand N zu gelangen, und deshalb gilt genau wie beim Spiel der &lt;br /&gt;
;Satz: Der randomisierte 2-SAT-Algorithmus findet im Durchschnitt nach &amp;lt;math&amp;gt;O(N^2)&amp;lt;/math&amp;gt; Versuchen eine Lösung, wenn das Problem erfüllbar ist.&lt;br /&gt;
Damit ist der randomisierte Algorithmus für dieses Problem effizient, was Sie in Übung 12 experimentell nachprüfen sollen.&lt;br /&gt;
&lt;br /&gt;
== 2. RANSAC-ALGORITHMUS (Random Sample Consensus)==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''Aufgabe:''&amp;lt;/u&amp;gt; gegeben: Datenpunkte&lt;br /&gt;
::gesucht: Modell, das die Datenpunkte erklärt&lt;br /&gt;
&lt;br /&gt;
[[Image:Rubto.png|thumb|250px|none]]&lt;br /&gt;
&lt;br /&gt;
'''Messpunkte:'''&lt;br /&gt;
   &lt;br /&gt;
      übliche Lösung: Methode der kleinsten Quadrate&lt;br /&gt;
      &lt;br /&gt;
      &amp;lt;math&amp;gt;\min_{a,b} 	\sum_{i} \left(a x_i + b + y_i\right)^2&amp;lt;/math&amp;gt;&lt;br /&gt;
      &lt;br /&gt;
      Schulmathematik:      &amp;lt;math&amp;gt;Minimum\stackrel{\wedge}{=}Ableitung=0&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lineares Gleichungssystem'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{da}\sum{i} \left(ax_i+b-y_i\right)^2=\sum{i} \frac{d}{da} \left[ax_i+b-y_i\right)^2&amp;lt;/math&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(g\left(x\right)\right)&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(x\right)=x^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;y\left(a\right)=ax_i+b-y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;=\sum_{i}2\left(ax_i+b-y_i\right)\frac{d}{da} \underbrace {ax_i+b-y_i}_{x_i}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\underline {=2\sum_{i}\left(ax_i+b-y_i\right)x_i\stackrel{!}{=}0}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}{x_i}^2+b\sum_{i}x_i=\sum_{i}x_iy_i&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}x_i+b\sum_{i}1=\sum_{i}y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{db}\sum_{i}\left(ax_i+b-y_i\right)^2=2\sum_{i}\left(ax_i+b-y_i\right)*1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:Problem: &amp;lt;math&amp;gt;\epsilon  %&amp;lt;/math&amp;gt; der Datenpunkte sind Outlier&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\Longrightarrow&amp;lt;/math&amp;gt; Einfaches Anpassen des Modells an die Datenpunkte funktioniert nicht&lt;br /&gt;
&lt;br /&gt;
:Seien mindestens k Datenpunkte notwendig, um das Programm anpassen zu können&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
RANSAC-Algorithmus&lt;br /&gt;
&lt;br /&gt;
      for  l in range (trials):&lt;br /&gt;
           wähle zufällig k Punkte aus&lt;br /&gt;
           passe das Modell an die k Punkte an&lt;br /&gt;
           zähle, wieviele Punkte in der Nähe des Modells liegen (d.h. &amp;lt;math&amp;gt;d_i &amp;lt; d_max&amp;lt;/math&amp;gt; muss geschickt gewählt werden) &lt;br /&gt;
                                           #Bsp. Geradenfinden:-wähle a,b aus zwei Punkten&lt;br /&gt;
                                                               -berechne: &amp;lt;math&amp;gt;|ax_i+b-y_i|=d_i&amp;lt;/math&amp;gt;&lt;br /&gt;
                                                               -zähle Punkt i als Inlier, falls &amp;lt;math&amp;gt;d_i&amp;lt;d_ma&amp;lt;/math&amp;gt;&lt;br /&gt;
      return: Modell mit höchster Zahl der Inlier&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;trials= \frac{log\left(1-p\right)}{log\left(1-\left(1-\epsilon\right)^k\right)}&amp;lt;/math&amp;gt;  mit k=Anzahl der Datenpunkte und p=Erfolgswahrscheinlichkeit, &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;=Outlier-Anteil&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Erfolgswahrscheinlichkeit: p=99%'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{|c||c|c|c|c|c|}&lt;br /&gt;
         Beispiel &amp;amp; k &amp;amp; \epsilon=10% &amp;amp; 20% &amp;amp; 50% &amp;amp; 70%\\&lt;br /&gt;
         \hline&lt;br /&gt;
         Linie\;in\;2D &amp;amp; 2 &amp;amp; 3 &amp;amp;5 &amp;amp; 17 &amp;amp; 49\\&lt;br /&gt;
         Kreis\;in\;2D &amp;amp; 3 &amp;amp; 4 &amp;amp; 7 &amp;amp; 35 &amp;amp; 169\\&lt;br /&gt;
         Ebene\;in\;3D &amp;amp; 8 &amp;amp; 9 &amp;amp; 26 &amp;amp; 1172 &amp;amp; 70188\\&lt;br /&gt;
       \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== '''Las Vegas vs. Monte Carlo'''==&lt;br /&gt;
&lt;br /&gt;
   * ''Las Vegas - Algorithmen''&lt;br /&gt;
     - Ergebnis ist immer korrekt.&lt;br /&gt;
     - Berechnung ist mit hoher Wahrscheinlichkeit effizient (d.h. Randomisierung macht den ungünstigsten Fall unwahrscheinlich).&lt;br /&gt;
&lt;br /&gt;
   * ''Monte Carlo - Algorithmen''&lt;br /&gt;
     - Berechnung immer effizient.&lt;br /&gt;
     - Ergebnis mit hoher Wahrscheinlichkeit korrekt (falls kein effizienter Algorithmus bekannt, der immer die richtige Lösung liefert).&lt;br /&gt;
&lt;br /&gt;
{| border = &amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;center&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! Las Vegas&lt;br /&gt;
! Monte Carlo&lt;br /&gt;
|- &lt;br /&gt;
| - Erzeugen einer perfekten Hashfuktion &lt;br /&gt;
| - Algorithmus von Freiwald(Matrizenmultiplikation)&lt;br /&gt;
|-&lt;br /&gt;
| - universelles Hashing&lt;br /&gt;
| - RANSAC&lt;br /&gt;
|-&lt;br /&gt;
| - Quick Sort mit zufälliger Wahl des Pivot-Elements&lt;br /&gt;
| - randomisierte K-SAT(k&amp;gt;=3)(Alg. von Schöning)&lt;br /&gt;
|-&lt;br /&gt;
| - Treep mit zufälligen Prioritäten&lt;br /&gt;
| -&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== ''' Zufallszahlen ''' ==&lt;br /&gt;
&lt;br /&gt;
:- kann man nicht mit deterministischen Computern erzeugen&lt;br /&gt;
:- aber man kann Pseudo-Zufallszahlen erzeugen, die viele Eigenschaften von echten Zufallszahlen haben&lt;br /&gt;
::: * sehr ähnlich  zum Hash&lt;br /&gt;
&lt;br /&gt;
     ''&amp;quot;linear Conguential Random number generator&amp;quot;''&lt;br /&gt;
        &amp;lt;math&amp;gt;I_{i+1}= \left(a*I_i + c\right)\textrm{mod\ } m&amp;lt;/math&amp;gt;&lt;br /&gt;
        &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{=&amp;gt; } &amp;amp; I_i \in [0, m-1]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:-sorgfältige Wahl von  a, c, m notwendig&lt;br /&gt;
::'''Bsp.'''  m = 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;&lt;br /&gt;
::: a = 1664525, c = 1013904223&lt;br /&gt;
::: ''&amp;quot;quick and dirty generator&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
==='''Nachteile'''===&lt;br /&gt;
&lt;br /&gt;
* nicht zufällig genug für viele Anwendungen&lt;br /&gt;
::'''Bsp.''' wähle Punkt in R&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
      \mathrm{ } &amp;amp; p = (rand(), rand(), rand())\\&lt;br /&gt;
&lt;br /&gt;
      \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::gibt Zahl u, v, w so, dass &lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; u * p[0] + v * p[1] + w * p[3]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::stark geclustert ist.&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge ist zu kurz:&lt;br /&gt;
:: spätestens nach m Schritten wiederholt sich die Folge&lt;br /&gt;
&lt;br /&gt;
::'''allgemein''': falls der interne Zustand des Zufallsgenerators ''k'' bits hat, ist Periodenlänge:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; Periode &amp;lt; 2^k\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* ''lowbits'' sind weniger zufällig als die ''highbits''&lt;br /&gt;
----&lt;br /&gt;
=== ''Mersenne Twister''===&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
'''bester zur Zeit bekannter Zufallszahlengenerator (ZZG)'''&lt;br /&gt;
* innere Zustand: &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; 624*32 bit\ Integers  =&amp;gt; 19968 bits\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge: &amp;lt;math&amp;gt;2^ {19937} \approx 4 * 10^{6000}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Punkte aus aufeinanderfolgende Zufallszahlen in &amp;lt;math&amp;gt;\mathbb{R}^n&amp;lt;/math&amp;gt; sind gleich verteilt bis &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; n = 623\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* alle Bits sind unabhängig voneinander zufällig (&amp;quot;Twister&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
* schnell&lt;br /&gt;
&lt;br /&gt;
    class MersenneTwister:&lt;br /&gt;
        &lt;br /&gt;
        def __init__(self, seed):&lt;br /&gt;
            self.N = 624  # Größe des inneren Zustands festlegen&lt;br /&gt;
            self.i = 0    # zählt mit in welchem Zustand wir uns gerade aufhalten&lt;br /&gt;
            &lt;br /&gt;
            self.state = [0]*self.N  # Speicher für den inneren Zustand reservieren&lt;br /&gt;
            &lt;br /&gt;
            self.state[0] = seed     # initiale Zufallszahl vom Benutzer&lt;br /&gt;
            # den Rest des inneren Zustands mit einfachem Zufallszahlengenerator initialisieren&lt;br /&gt;
            for i in xrange(1, self.N):&lt;br /&gt;
                self.state[i] = (1812433253 * (self.state[i-1] ^ (self.state[i-1] &amp;gt;&amp;gt; 30)) + i) % 4294967296&lt;br /&gt;
     &lt;br /&gt;
        def __call__(self):&lt;br /&gt;
            &amp;quot;&amp;quot;&amp;quot;gibt die nächste Zufallszahl im Bereich [0, 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;-1] aus&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
            N, M = self.N, 397&lt;br /&gt;
            &lt;br /&gt;
            # Zustand aktualisieren (neue Zufallszahl ausrechnen)&lt;br /&gt;
            i = self.i&lt;br /&gt;
            r = ((self.state[i] &amp;amp; 0x80000000) | (self.state[(i+1)%N] &amp;amp; 0x7FFFFFFF)) &amp;gt;&amp;gt; 1&lt;br /&gt;
            if self.state[(i+1)%N] &amp;amp; 1:&lt;br /&gt;
                r ^= 0x9908B0DF&lt;br /&gt;
            self.state[i] = self.state[(i+M)%N] ^ r&lt;br /&gt;
     &lt;br /&gt;
            # aktuelle Zufallszahl auslesen und ihre Zufälligkeit durch verwürfeln der Bits verbessern&lt;br /&gt;
            y = self.state[i]&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 11)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt;  7) &amp;amp; 0x9D2C5680)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt; 15) &amp;amp; 0xEFC60000)&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 18)&lt;br /&gt;
            &lt;br /&gt;
            # Zustand weitersetzen und endgültige Zufallszahl ausgeben&lt;br /&gt;
            self.i = (self.i + 1) % N&lt;br /&gt;
            return y&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''geg.:''' Zufallszahl &lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, \overbrace{2^{32}-1}^{m-1}]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ges.:''' Zufallszahl&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, k - 1]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''naive Lösung:'''  &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; rand()%k\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;  ist schlecht.&lt;br /&gt;
&lt;br /&gt;
'''Bsp.'''&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; \qquad m = 16\qquad k = 11\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
! rand() || 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15&lt;br /&gt;
|-&lt;br /&gt;
! rand()%k&lt;br /&gt;
! 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 0 || 1 || 2 || 3 || 4 &lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; 0,...,4 kommt doppelt so häufig wie 5,...,10 &amp;quot;nicht zufällig&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lösung:'''  Zurückweisen des Rests der Zahlen (''rejektion sampling'')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; remainder = (m - 1 - (k - 1))% k = (m - k)%k\\&lt;br /&gt;
        \mathrm{ } &amp;amp; last\ Good\ Value = m-1-remainder\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  r = rand()&lt;br /&gt;
  while r &amp;gt; last.GoodValue:&lt;br /&gt;
        r = rand()&lt;br /&gt;
        return r%k&lt;br /&gt;
&lt;br /&gt;
[[Greedy-Algorithmen und Dynamische Programmierung|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5438</id>
		<title>Randomisierte Algorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5438"/>
				<updated>2012-07-30T11:52:01Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Randomisierte Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
;Definition: Randomisierte Algorithmen sind Algorithmen, die bei Entscheidungen über ihr weiteres Vorgehen oder bei der Wahl ihrer Parameter Zufallszahlen benutzen.&lt;br /&gt;
&lt;br /&gt;
Anschaulich gesprochen, wersucht man bei randomisierten Algorithmen, einen Teil der Lösung zu &amp;lt;i&amp;gt;raten&amp;lt;/i&amp;gt;. Auf den ersten Blick würde man vermuten, dass dabei nicht viel Sinnvolles herauskommen kann. Diese Kapitel wird jedoch zeigen, dass man durch geschicktes Raten tatsächlich zu sehr eleganten Algorithmen gelangen kann.&lt;br /&gt;
&lt;br /&gt;
Grundsätzlich unterscheidet man zwei Arten von randomisierten Algorithmen:&lt;br /&gt;
;Las Vegas - Algorithmen: Das Ergebnis des Algorithmus ist immer korrekt, und die Berechnung erfolgt mit hoher Wahrscheinlichkeit effizient.&lt;br /&gt;
;Monte Carlo - Algorithmen: Die Berechnung ist immer effizient, und das Ergebnis ist mit hoher Wahrscheinlichkeit korrekt.&lt;br /&gt;
Las Vegas-Algorithmen verwendet man, wenn der Algorithmus im ungünstigen Fall eine schlechte Laufzeit hat, und der ungünstige Fall kann durch die Randomisierung sehr unwahrscheinlich gemacht werden. Wir haben in der Vorlesung schon mehrere Las Vegas-Algorithmen kennen gelernt:&lt;br /&gt;
* Quick Sort mit zufälliger Wahl des Pivot-Elements: Die Randomisierung verhindert, dass das Array immer wieder in Subarrays von sehr unterschiedlicher Größe aufgeteilt wird.&lt;br /&gt;
* Treap mit zufälligen Prioritäten: Die Randomisierung verhindert, dass der Baum schlecht balanciert ist.&lt;br /&gt;
* Universelles Hashing: Die zufällige Wahl der Hashfunktion verhindert, dass ein Angreifer eine Schlüsselmenge mit sehr vielen Kollisionen konstruieren kann.&lt;br /&gt;
* Erzeugung einer perfekten Hashfunktion: Durch die Randomisierung entsteht mit nach wenigen Versuchen ein zyklenfreier Graph, der zur Definition der Hashfunktion geeignet ist.&lt;br /&gt;
Monte Carlo-Algorithmen verwendet man dagegen, wenn kein effizienter deterministischer Algorithmus für ein Problem bekannt ist. Man gibt sich dann damit zufrieden, dass der randomisierte Algorithmus die korrekte Lösung nur mit hoher Wahrscheinlichkeit findet, wenn dies dafür sehr effizient geschieht. Bei manchen Problemen ist auch dies unerreichbar - man muss dann bereits zufrieden sein, wenn der Algorithmus mit hoher Wahrscheinlichkeit eine sehr gute Näherungslösung findet. Beliebte Anwendungsgebiete für Monte Carlo-Algorithmen sind beispielsweise&lt;br /&gt;
* Randomisierte Primzahl-Tests: Moderne Verschlüsselungsverfahren benötigen zahlreiche Primzahlen, aber exakte Primzahltests sind teuer. Der [http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin-Test] findet effizient Zahlen, die mit sehr hoher Wahrscheinlichkeit tatsächlich Primzahlen sind.&lt;br /&gt;
* Randomisiertes Testen: Wie jeder Test kann auch eine randomisierter Test nicht die Abwesenheit von Programmierfehlern garantieren, aber man kann durch die Randomisierung viel mehr Testfälle generieren und erhöht so die Erfolgswarscheinlichkeit. Wir haben als Beispiel dafür den [[Korrektheit#Beispiel_f.C3.BCr_das_Testen:_Freivalds_Algorithmus|Algorithmus von Freivald]] behandelt.&lt;br /&gt;
* Lösung schwieriger Optimierungsprobleme: Wir zeigen unten, dass ein randomisierter Algorithmus effizient eine Lösung für das 2-SAT-Problem aus dem vorherigen Kapitel findet (für k-SAT mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; liefert der Algorithmus immer noch mit einer gewissen Wahrscheinlichkeit das richtige Ergebnis, ist aber nicht mehr effizient). Einen effizienten Approximationsalgorithmus für des Problem des Handelsreisenden behandlen wir im Kapitel [[NP-Vollständigkeit]]. Weitere wichtige Beispiele für diesen Bereich sind [http://en.wikipedia.org/wiki/Simulated_annealing simulated annealing] und das [http://de.wikipedia.org/wiki/MCMC-Verfahren Markov-Chain-Monte-Carlo-Verfahren].&lt;br /&gt;
* Robuste Statistik: Eine Grundaufgabe der Statistik ist das Anpassen (Fitten) von Modellen an gemessene Werte. Wenn die Messungen jedoch &amp;quot;Ausreißer&amp;quot; (einige völlig falsche Werte) enthalten, geht die Anpassung schief. Wir beschreiben unten den RANSAC-Algorithmus, der die Ausreißer identifizieren und beim Modellfitten ignorieren kann.&lt;br /&gt;
&lt;br /&gt;
Obwohl randomisierte Algorithmen oft einfach und elegant sind, ist ihre theoretische Analyse (also das Führen von Korrektheits- und Komplexitätsbeweisen) häufig sehr schwierig. Man muss fortgeschrittene Methoden der Wahrscheinlichkeitsrechnung und Statistik beherrschen, um die Wahrscheinlichkeit für das Versagen des Algorithmus zu berechnen und um zu zeigen, wie man den Algorithmus benutzt, damit diese Wahrscheinlichkeit unter einer akzeptablen Schranke bleibt. Die Algorithmen, die wir für diese Vorlesung ausgewählt haben, zeichnen sich dadurch aus, dass die Beweise hier einfach zu erbringen sind.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Lösen des K-SAT-Problems ===&lt;br /&gt;
&lt;br /&gt;
Der &amp;lt;b&amp;gt;Algorithmus von Schöning&amp;lt;/b&amp;gt; löst das [[Graphen_und_Graphenalgorithmen#Normalformen für logische Ausdrücke|k-SAT-Problem]] durch Raten: Wenn ein Ausdruck in k-CNF den Wert False hat, gibt es mindestens eine Klausel, die den Wert False hat. Alle Literale in dieser Klausel haben ebenfalls den Wert False, denn jede Klausel ist eine ODER-Verknüpfung, die nur dann False werden kann. Um den Ausdruck zu erfüllen, muss jede Klausel den Wert True annehmen, also müssen wir den Wert von mindestens einem Literal umdrehen. Wenn der Ausruck tatsächlich erfüllbar ist, gibt es immer ein geeignetes Literal, wir wissen nur nicht, welches. Deshalb drehen wir ein unter den k Literalen der betreffenden Klausel zufällig gewähltes. Liegen wir mit unserer Wahl richtig, sind wir der Lösung näher gekommen - im besten Fall sind jetzt alle Klauseln erfüllt. Wählen wir jedoch die falsche Variable, ist die aktuelle Klausel zwar jetzt True, aber dafür werden andere Klauseln zu False, die bisher True waren, und wir entfernen uns somit von der Lösung.&lt;br /&gt;
&lt;br /&gt;
    geg.: logischer Ausdruck in K-CNF (n Variablen, m Klauseln, k Variablen pro Klausel)&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;math&amp;gt;\underbrace {\underbrace {\left(x_1 \vee x_3 \vee...\right)}_{k\; Literale} \wedge \left( x_2 \vee x_4 \vee...\right)}_{m\;Klauseln}&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Algorithmus von Schöning lautet in Pseudocode:&lt;br /&gt;
    for i in range (trials):    #Anzahl der Versuche&lt;br /&gt;
         Bestimme eine Zufallsbelegung der Variablen &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
         for j in range (steps):&lt;br /&gt;
               if &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt; erfüllt alle Klauseln: &lt;br /&gt;
                   return &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
               wähle zufällig eine Klausel, die nicht erfüllt ist und negiere zufällig eine der Variablen in dieser Klausel &lt;br /&gt;
               # (die Klausel ist jetzt erfüllt)&lt;br /&gt;
    return None  # keine Lösung gefunden&lt;br /&gt;
&lt;br /&gt;
Findet der Algorithmus eine Lösung, wissen wir, dass der Ausdruck erfüllbar ist. Andernfalls könnte der Ausdruck unerfüllbar sein, oder wir haben nur Pech gehabt. Je mehr erfolglose Versuche wir machen, desto höher ist die Wahrscheinlichkeit, dass das erste zutrifft.&lt;br /&gt;
&lt;br /&gt;
Es ist sinnvoll, &amp;lt;tt&amp;gt;steps = k*n&amp;lt;/tt&amp;gt; zu wählen. Dann gilt der &lt;br /&gt;
;Satz: Wenn ein Ausdruck in k-CNF mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; erfüllbar ist, muss man im Mittel &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in O\left(\left(\frac{2(k-1)}{k}\right)^n \right)&amp;lt;/math&amp;gt; Versuche machen, um eine Lösung zu finden.&lt;br /&gt;
&lt;br /&gt;
Für &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; gilt stets &amp;lt;math&amp;gt;\frac{2(k-1)}{k} &amp;gt; 1&amp;lt;/math&amp;gt;, man benötigt also eine in n exponentielle Anzahl von Versuchen. Bei &amp;lt;math&amp;gt;k=3&amp;lt;/math&amp;gt; gilt z.B. &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt; \in O\left(\left(\frac{4}{3}\right)^n\right)&amp;lt;/math&amp;gt;. Dies ist zwar im Mittel effizienter also die erschöpfende Suche, die &amp;lt;math&amp;gt;O(2^n)&amp;lt;/math&amp;gt; Schritte benötigt, aber immer noch sehr langsam.&lt;br /&gt;
&lt;br /&gt;
Der Fall &amp;lt;b&amp;gt;&amp;lt;math&amp;gt;k=2&amp;lt;/math&amp;gt; ist jedoch ein Sonderfall&amp;lt;/b&amp;gt;: Hier kann man leicht beweisen, dass eine Lösung im Mittel bereits nach &amp;lt;math&amp;gt;O\left(n^2\right)&amp;lt;/math&amp;gt; Schritten gefunden wird. Wenn man schon weiss, dass der Ausdruck erfüllbar ist (was mit [[Graphen_und_Graphenalgorithmen#Lösung des 2-SAT-Problems mit Implikationgraphen|Implikationgraphen]] leicht geprüft werden kann), lässt man den randomisierten Algorithmus einfach so lange laufen, bis er eine Lösung findet. Man setzt also &amp;lt;tt&amp;gt;step = infinity&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;trials = 1&amp;lt;/tt&amp;gt; und verlässt sich darauf, dass das &amp;lt;tt&amp;gt;return&amp;lt;/tt&amp;gt; mit einer gültigen Lösung früher oder später ausgeführt wird. Dass man darauf im Mittel nur &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte warten muss, zeigen wir jetzt mit Hilfe eines &amp;lt;i&amp;gt;random walk&amp;lt;/i&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk====&lt;br /&gt;
&lt;br /&gt;
Um die Random Walk Analyse zu verstehen, betrachten wir folgendes Spiel:&lt;br /&gt;
&lt;br /&gt;
   geg.: eine Stuhlreihe mit N Stühlen. Wir nummerieren die Stühle so, dass links der Stuhl 0 und rechts der Stuhl N steht.&lt;br /&gt;
   &lt;br /&gt;
   * Eine Person setzt sich zufällig auf einen der Stühle.&lt;br /&gt;
   * Eine zweite Person wirft eine Münze.&lt;br /&gt;
   &lt;br /&gt;
         Wenn die Münze auf Zahl fällt, rückt die erste Person einen Stuhl nach links, andernfalls nach rechts.&lt;br /&gt;
         &amp;lt;--- Zahl                                                                                    Kopf ---&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
   * Frage: Wie oft muss man die Münze im Durchschnitt werfen, bis Person 1 zum ersten Mal auf Stuhl N sitzt?&lt;br /&gt;
&lt;br /&gt;
Da die erste Person sich anfangs zufällig hinsetzt, haben wir eine Chance von 1/N, dass sie gleich auf dem richtigen Stuhl landet und wir 0 Schritte benötigen. Mit der gleichen Wahrscheinlichkeit von 1/N setzt sie sich anfangs auf Stuhl Nummer (N-1), und wir haben eine fifty-fifty-Chance, mit nur einem Wurf durchzukommen. Wir können aber auch Pech haben und landen auf Stuhl Nummer (N-2). Das ist das Gleiche, als wenn Person 1 von Anfang an auf diesem Stuhl gesessen hätte, nur dass wir jetzt bereits einen Wurf verbraucht haben. Man sieht, dass man die Zahl der Restwürfe immer in dieser Art ausdrücken kann: Sitzt Person 1 auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, kann sie entweder nach rechts rücken und benötigt dann noch soviele Würfe, wie man typischerweise für Stuhl &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; benötigt, plus den Wurf von &amp;lt;tt&amp;gt;i =&amp;gt; i+1&amp;lt;/tt&amp;gt;. Oder sie kann nach links rücken und benötigt dann die typische Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; plus den Wurf &amp;lt;tt&amp;gt;i =&amp;gt; i-1&amp;lt;/tt&amp;gt;. Beide Möglichkeiten haben die Wahrscheinlichkeit 1/2. Mathematisch kann man dies elegant als Rekursionsformel schreiben, die die erwartete Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; als Funktion der entsprechenden Wurfzahlen für die Stühle &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; ausdrückt:&lt;br /&gt;
&lt;br /&gt;
* Wenn wir uns auf Stuhl N befinden, werfen wir gar nicht: &amp;lt;math&amp;gt;W\left(N\right)=0&amp;lt;/math&amp;gt;&lt;br /&gt;
* Von Stuhl 0 gehen wir immer zu Stuhl 1: &amp;lt;math&amp;gt;W\left(0\right)=1 + W\left(1\right)&amp;lt;/math&amp;gt;&lt;br /&gt;
* Allgemeiner Fall: &amp;lt;math&amp;gt;W\left(i\right)=\frac 1 2 \left(1 + W\left(i+1\right)\right) + \frac 1 2 \left(1 + W\left(i-1\right)\right) = \frac 1 2 W\left(i+1\right) + \frac 1 2 W\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
Diese Rekursion wird durch die explizite Formel&lt;br /&gt;
::&amp;lt;math&amp;gt;W\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
gelöst, wie man durch Einsetzen leicht nachprüft:&lt;br /&gt;
::&amp;lt;math&amp;gt;&lt;br /&gt;
       \begin{align} &lt;br /&gt;
             W\left(N\right) &amp;amp; = N^2-N^2=0 \\&lt;br /&gt;
                  &lt;br /&gt;
             W\left(0\right) &amp;amp;= W\left(1\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-1^2+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2 - 0^2\\&lt;br /&gt;
                 &lt;br /&gt;
              W\left(i\right) &amp;amp;= \frac 1 2 \left(N^2-\left(i-1\right)^2\right) + \frac 1 2 \left(N^2-\left(i+1\right)^2\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= \frac 1 2 N^2-\frac 1 2 \left( i^2-2i+1\right) + \frac 1 2 N^2-\frac 1 2 \left(i^2+2i+1\right) + 1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-i^2&lt;br /&gt;
       \end{align}&amp;lt;/math&amp;gt;&lt;br /&gt;
Insbesondere braucht man im ungünstigen Fall (Start auf Stuhl 0) im Durchschnitt &amp;lt;math&amp;gt;N^2&amp;lt;/math&amp;gt; Würfe, im typischen Fall (Start in der Mitte, also bei &amp;lt;math&amp;gt;i = N/2&amp;lt;/math&amp;gt;) im Durchschnitt &lt;br /&gt;
:&amp;lt;math&amp;gt;N^2 - (N/2)^2=\frac 3 4 N^2\in O(N^2)&amp;lt;/math&amp;gt; &lt;br /&gt;
Würfe. Die '''Beziehung zum randomisiertem 2-SAT-Algorithmus''' ist jetzt leicht zu erkennen. Sitzt die Person auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, interpretieren wir das als:&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Stuhl &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt;&amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert, &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
Wählt der Algorithmus eine Klausel, die nicht erfüllt ist, gibt es zwei Möglichkeiten:&lt;br /&gt;
# Beide Literale in der Klausel haben den falschen Wert: Die Lösung wird auf jeden Fall besser, egal welche der beiden wir umdrehen. Wir gehen also von Zustand &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
# Nur eins der Literale hat den falschen Wert: Beim Umdrehen haben wir eine fifty-fifty-Chance, das richtige Literal zu wählen und in den Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; zu gelangen. Mit der selben Wahrscheinlichkeit wählen wir das falsche Literal und landen im Zustand &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
Falls 2 ist der ungünstigere und entspricht unserem Spiel, dessen Analyse wir deshalb einfach auf das 2-SAT-Problem übertragen können: Ziel des Algorithmus ist es, in den Zustand N zu gelangen, und deshalb gilt genau wie beim Spiel der &lt;br /&gt;
;Satz: Der randomisierte 2-SAT-Algorithmus findet im Durchschnitt nach &amp;lt;math&amp;gt;O(N^2)&amp;lt;/math&amp;gt; Versuchen eine Lösung, wenn das Problem erfüllbar ist.&lt;br /&gt;
Damit ist der randomisierte Algorithmus für dieses Problem effizient, was Sie in Übung 12 experimentell nachprüfen sollen.&lt;br /&gt;
&lt;br /&gt;
== 2. RANSAC-ALGORITHMUS (Random Sample Consensus)==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''Aufgabe:''&amp;lt;/u&amp;gt; gegeben: Datenpunkte&lt;br /&gt;
::gesucht: Modell, das die Datenpunkte erklärt&lt;br /&gt;
&lt;br /&gt;
[[Image:Rubto.png|thumb|250px|none]]&lt;br /&gt;
&lt;br /&gt;
'''Messpunkte:'''&lt;br /&gt;
   &lt;br /&gt;
      übliche Lösung: Methode der kleinsten Quadrate&lt;br /&gt;
      &lt;br /&gt;
      &amp;lt;math&amp;gt;\min_{a,b} 	\sum_{i} \left(a x_i + b + y_i\right)^2&amp;lt;/math&amp;gt;&lt;br /&gt;
      &lt;br /&gt;
      Schulmathematik:      &amp;lt;math&amp;gt;Minimum\stackrel{\wedge}{=}Ableitung=0&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lineares Gleichungssystem'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{da}\sum{i} \left(ax_i+b-y_i\right)^2=\sum{i} \frac{d}{da} \left[ax_i+b-y_i\right)^2&amp;lt;/math&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(g\left(x\right)\right)&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(x\right)=x^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;y\left(a\right)=ax_i+b-y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;=\sum_{i}2\left(ax_i+b-y_i\right)\frac{d}{da} \underbrace {ax_i+b-y_i}_{x_i}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\underline {=2\sum_{i}\left(ax_i+b-y_i\right)x_i\stackrel{!}{=}0}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}{x_i}^2+b\sum_{i}x_i=\sum_{i}x_iy_i&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}x_i+b\sum_{i}1=\sum_{i}y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{db}\sum_{i}\left(ax_i+b-y_i\right)^2=2\sum_{i}\left(ax_i+b-y_i\right)*1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:Problem: &amp;lt;math&amp;gt;\epsilon  %&amp;lt;/math&amp;gt; der Datenpunkte sind Outlier&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\Longrightarrow&amp;lt;/math&amp;gt; Einfaches Anpassen des Modells an die Datenpunkte funktioniert nicht&lt;br /&gt;
&lt;br /&gt;
:Seien mindestens k Datenpunkte notwendig, um das Programm anpassen zu können&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
RANSAC-Algorithmus&lt;br /&gt;
&lt;br /&gt;
      for  l in range (trials):&lt;br /&gt;
           wähle zufällig k Punkte aus&lt;br /&gt;
           passe das Modell an die k Punkte an&lt;br /&gt;
           zähle, wieviele Punkte in der Nähe des Modells liegen (d.h. &amp;lt;math&amp;gt;d_i &amp;lt; d_max&amp;lt;/math&amp;gt; muss geschickt gewählt werden) &lt;br /&gt;
                                           #Bsp. Geradenfinden:-wähle a,b aus zwei Punkten&lt;br /&gt;
                                                               -berechne: &amp;lt;math&amp;gt;|ax_i+b-y_i|=d_i&amp;lt;/math&amp;gt;&lt;br /&gt;
                                                               -zähle Punkt i als Inlier, falls &amp;lt;math&amp;gt;d_i&amp;lt;d_ma&amp;lt;/math&amp;gt;&lt;br /&gt;
      return: Modell mit höchster Zahl der Inlier&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;trials= \frac{log\left(1-p\right)}{log\left(1-\left(1-\epsilon\right)^k\right)}&amp;lt;/math&amp;gt;  mit k=Anzahl der Datenpunkte und p=Erfolgswahrscheinlichkeit, &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;=Outlier-Anteil&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Erfolgswahrscheinlichkeit: p=99%'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{|c||c|c|c|c|c|}&lt;br /&gt;
         Beispiel &amp;amp; k &amp;amp; \epsilon=10% &amp;amp; 20% &amp;amp; 50% &amp;amp; 70%\\&lt;br /&gt;
         \hline&lt;br /&gt;
         Linie\;in\;2D &amp;amp; 2 &amp;amp; 3 &amp;amp;5 &amp;amp; 17 &amp;amp; 49\\&lt;br /&gt;
         Kreis\;in\;2D &amp;amp; 3 &amp;amp; 4 &amp;amp; 7 &amp;amp; 35 &amp;amp; 169\\&lt;br /&gt;
         Ebene\;in\;3D &amp;amp; 8 &amp;amp; 9 &amp;amp; 26 &amp;amp; 1172 &amp;amp; 70188\\&lt;br /&gt;
       \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Ein Spiel: Wie viel Schritte braucht man im Mittel zum Ziel?'''&lt;br /&gt;
&lt;br /&gt;
   geg.: 5 Plätze, 2 Personen: eine Person rückt vom einem Platz zu dem enderen Platz;&lt;br /&gt;
         die zweite Person wirft die Münze.&lt;br /&gt;
         Wenn die Münze auf Kopf landet, rücke nach rechts und wenn die Münze auf Zahl landet, rücke nach links.&lt;br /&gt;
         &amp;lt;--- Zahl                                                         Kopf--&amp;gt;&lt;br /&gt;
         Kopf: /////&lt;br /&gt;
         Zahl: /// &lt;br /&gt;
&lt;br /&gt;
:: =&amp;gt; mit 8 Schritten bis zum Ziel&lt;br /&gt;
:im Mittel: bei N Plätzen braucht man N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritte&lt;br /&gt;
&lt;br /&gt;
: all: mit N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritten um N Plätze rücken&lt;br /&gt;
: Wie viel Schritte braucht man im Mittel zum Ziel?&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(N\right)=0&amp;lt;/math&amp;gt;    #wenn wir uns im Stuhl Nr.1 befinden&lt;br /&gt;
           &lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)=\frac 1 2 S\left(1 + S\left(i+1\right)\right) + \frac 1 2 S\left(1 + S\left(i-1\right)\right) = \frac 1 2 S\left(i+1\right) + \frac 1 2 S\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(0\right)=1 + S\left(1\right)&amp;lt;/math&amp;gt;    #bei 0.Platz&lt;br /&gt;
&lt;br /&gt;
:::*Lösung: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:::*speziell: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2&amp;lt;/math&amp;gt;           #wenn man am ungünstigsten Platz startet&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
'''Beziehung zu randomisiertem 2-SAT'''&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Platz &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; &amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert,  &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)=N^2 - \left(\frac N 2\right)^2 = N^2 - \frac N 4 ^2 = \frac 3 4 N^2 &amp;lt;/math&amp;gt;&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)&amp;lt;/math&amp;gt;     # Anfangszustand&lt;br /&gt;
----&lt;br /&gt;
== '''Las Vegas vs. Monte Carlo'''==&lt;br /&gt;
&lt;br /&gt;
   * ''Las Vegas - Algorithmen''&lt;br /&gt;
     - Ergebnis ist immer korrekt.&lt;br /&gt;
     - Berechnung ist mit hoher Wahrscheinlichkeit effizient (d.h. Randomisierung macht den ungünstigsten Fall unwahrscheinlich).&lt;br /&gt;
&lt;br /&gt;
   * ''Monte Carlo - Algorithmen''&lt;br /&gt;
     - Berechnung immer effizient.&lt;br /&gt;
     - Ergebnis mit hoher Wahrscheinlichkeit korrekt (falls kein effizienter Algorithmus bekannt, der immer die richtige Lösung liefert).&lt;br /&gt;
&lt;br /&gt;
{| border = &amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;center&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! Las Vegas&lt;br /&gt;
! Monte Carlo&lt;br /&gt;
|- &lt;br /&gt;
| - Erzeugen einer perfekten Hashfuktion &lt;br /&gt;
| - Algorithmus von Freiwald(Matrizenmultiplikation)&lt;br /&gt;
|-&lt;br /&gt;
| - universelles Hashing&lt;br /&gt;
| - RANSAC&lt;br /&gt;
|-&lt;br /&gt;
| - Quick Sort mit zufälliger Wahl des Pivot-Elements&lt;br /&gt;
| - randomisierte K-SAT(k&amp;gt;=3)(Alg. von Schöning)&lt;br /&gt;
|-&lt;br /&gt;
| - Treep mit zufälligen Prioritäten&lt;br /&gt;
| -&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== ''' Zufallszahlen ''' ==&lt;br /&gt;
&lt;br /&gt;
:- kann man nicht mit deterministischen Computern erzeugen&lt;br /&gt;
:- aber man kann Pseudo-Zufallszahlen erzeugen, die viele Eigenschaften von echten Zufallszahlen haben&lt;br /&gt;
::: * sehr ähnlich  zum Hash&lt;br /&gt;
&lt;br /&gt;
     ''&amp;quot;linear Conguential Random number generator&amp;quot;''&lt;br /&gt;
        &amp;lt;math&amp;gt;I_{i+1}= \left(a*I_i + c\right)\textrm{mod\ } m&amp;lt;/math&amp;gt;&lt;br /&gt;
        &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{=&amp;gt; } &amp;amp; I_i \in [0, m-1]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:-sorgfältige Wahl von  a, c, m notwendig&lt;br /&gt;
::'''Bsp.'''  m = 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;&lt;br /&gt;
::: a = 1664525, c = 1013904223&lt;br /&gt;
::: ''&amp;quot;quick and dirty generator&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
==='''Nachteile'''===&lt;br /&gt;
&lt;br /&gt;
* nicht zufällig genug für viele Anwendungen&lt;br /&gt;
::'''Bsp.''' wähle Punkt in R&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
      \mathrm{ } &amp;amp; p = (rand(), rand(), rand())\\&lt;br /&gt;
&lt;br /&gt;
      \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::gibt Zahl u, v, w so, dass &lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; u * p[0] + v * p[1] + w * p[3]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::stark geclustert ist.&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge ist zu kurz:&lt;br /&gt;
:: spätestens nach m Schritten wiederholt sich die Folge&lt;br /&gt;
&lt;br /&gt;
::'''allgemein''': falls der interne Zustand des Zufallsgenerators ''k'' bits hat, ist Periodenlänge:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; Periode &amp;lt; 2^k\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* ''lowbits'' sind weniger zufällig als die ''highbits''&lt;br /&gt;
----&lt;br /&gt;
=== ''Mersenne Twister''===&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
'''bester zur Zeit bekannter Zufallszahlengenerator (ZZG)'''&lt;br /&gt;
* innere Zustand: &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; 624*32 bit\ Integers  =&amp;gt; 19968 bits\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge: &amp;lt;math&amp;gt;2^ {19937} \approx 4 * 10^{6000}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Punkte aus aufeinanderfolgende Zufallszahlen in &amp;lt;math&amp;gt;\mathbb{R}^n&amp;lt;/math&amp;gt; sind gleich verteilt bis &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; n = 623\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* alle Bits sind unabhängig voneinander zufällig (&amp;quot;Twister&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
* schnell&lt;br /&gt;
&lt;br /&gt;
    class MersenneTwister:&lt;br /&gt;
        &lt;br /&gt;
        def __init__(self, seed):&lt;br /&gt;
            self.N = 624  # Größe des inneren Zustands festlegen&lt;br /&gt;
            self.i = 0    # zählt mit in welchem Zustand wir uns gerade aufhalten&lt;br /&gt;
            &lt;br /&gt;
            self.state = [0]*self.N  # Speicher für den inneren Zustand reservieren&lt;br /&gt;
            &lt;br /&gt;
            self.state[0] = seed     # initiale Zufallszahl vom Benutzer&lt;br /&gt;
            # den Rest des inneren Zustands mit einfachem Zufallszahlengenerator initialisieren&lt;br /&gt;
            for i in xrange(1, self.N):&lt;br /&gt;
                self.state[i] = (1812433253 * (self.state[i-1] ^ (self.state[i-1] &amp;gt;&amp;gt; 30)) + i) % 4294967296&lt;br /&gt;
     &lt;br /&gt;
        def __call__(self):&lt;br /&gt;
            &amp;quot;&amp;quot;&amp;quot;gibt die nächste Zufallszahl im Bereich [0, 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;-1] aus&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
            N, M = self.N, 397&lt;br /&gt;
            &lt;br /&gt;
            # Zustand aktualisieren (neue Zufallszahl ausrechnen)&lt;br /&gt;
            i = self.i&lt;br /&gt;
            r = ((self.state[i] &amp;amp; 0x80000000) | (self.state[(i+1)%N] &amp;amp; 0x7FFFFFFF)) &amp;gt;&amp;gt; 1&lt;br /&gt;
            if self.state[(i+1)%N] &amp;amp; 1:&lt;br /&gt;
                r ^= 0x9908B0DF&lt;br /&gt;
            self.state[i] = self.state[(i+M)%N] ^ r&lt;br /&gt;
     &lt;br /&gt;
            # aktuelle Zufallszahl auslesen und ihre Zufälligkeit durch verwürfeln der Bits verbessern&lt;br /&gt;
            y = self.state[i]&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 11)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt;  7) &amp;amp; 0x9D2C5680)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt; 15) &amp;amp; 0xEFC60000)&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 18)&lt;br /&gt;
            &lt;br /&gt;
            # Zustand weitersetzen und endgültige Zufallszahl ausgeben&lt;br /&gt;
            self.i = (self.i + 1) % N&lt;br /&gt;
            return y&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''geg.:''' Zufallszahl &lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, \overbrace{2^{32}-1}^{m-1}]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ges.:''' Zufallszahl&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, k - 1]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''naive Lösung:'''  &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; rand()%k\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;  ist schlecht.&lt;br /&gt;
&lt;br /&gt;
'''Bsp.'''&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; \qquad m = 16\qquad k = 11\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
! rand() || 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15&lt;br /&gt;
|-&lt;br /&gt;
! rand()%k&lt;br /&gt;
! 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 0 || 1 || 2 || 3 || 4 &lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; 0,...,4 kommt doppelt so häufig wie 5,...,10 &amp;quot;nicht zufällig&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lösung:'''  Zurückweisen des Rests der Zahlen (''rejektion sampling'')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; remainder = (m - 1 - (k - 1))% k = (m - k)%k\\&lt;br /&gt;
        \mathrm{ } &amp;amp; last\ Good\ Value = m-1-remainder\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  r = rand()&lt;br /&gt;
  while r &amp;gt; last.GoodValue:&lt;br /&gt;
        r = rand()&lt;br /&gt;
        return r%k&lt;br /&gt;
&lt;br /&gt;
[[Greedy-Algorithmen und Dynamische Programmierung|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5437</id>
		<title>Randomisierte Algorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5437"/>
				<updated>2012-07-30T11:50:40Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Randomisierte Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
;Definition: Randomisierte Algorithmen sind Algorithmen, die bei Entscheidungen über ihr weiteres Vorgehen oder bei der Wahl ihrer Parameter Zufallszahlen benutzen.&lt;br /&gt;
&lt;br /&gt;
Anschaulich gesprochen, wersucht man bei randomisierten Algorithmen, einen Teil der Lösung zu &amp;lt;i&amp;gt;raten&amp;lt;/i&amp;gt;. Auf den ersten Blick würde man vermuten, dass dabei nicht viel Sinnvolles herauskommen kann. Diese Kapitel wird jedoch zeigen, dass man durch geschicktes Raten tatsächlich zu sehr eleganten Algorithmen gelangen kann.&lt;br /&gt;
&lt;br /&gt;
Grundsätzlich unterscheidet man zwei Arten von randomisierten Algorithmen:&lt;br /&gt;
;Las Vegas - Algorithmen: Das Ergebnis des Algorithmus ist immer korrekt, und die Berechnung erfolgt mit hoher Wahrscheinlichkeit effizient.&lt;br /&gt;
;Monte Carlo - Algorithmen: Die Berechnung ist immer effizient, und das Ergebnis ist mit hoher Wahrscheinlichkeit korrekt.&lt;br /&gt;
Las Vegas-Algorithmen verwendet man, wenn der Algorithmus im ungünstigen Fall eine schlechte Laufzeit hat, und der ungünstige Fall kann durch die Randomisierung sehr unwahrscheinlich gemacht werden. Wir haben in der Vorlesung schon mehrere Las Vegas-Algorithmen kennen gelernt:&lt;br /&gt;
* Quick Sort mit zufälliger Wahl des Pivot-Elements: Die Randomisierung verhindert, dass das Array immer wieder in Subarrays von sehr unterschiedlicher Größe aufgeteilt wird.&lt;br /&gt;
* Treap mit zufälligen Prioritäten: Die Randomisierung verhindert, dass der Baum schlecht balanciert ist.&lt;br /&gt;
* Universelles Hashing: Die zufällige Wahl der Hashfunktion verhindert, dass ein Angreifer eine Schlüsselmenge mit sehr vielen Kollisionen konstruieren kann.&lt;br /&gt;
* Erzeugung einer perfekten Hashfunktion: Durch die Randomisierung entsteht mit nach wenigen Versuchen ein zyklenfreier Graph, der zur Definition der Hashfunktion geeignet ist.&lt;br /&gt;
Monte Carlo-Algorithmen verwendet man dagegen, wenn kein effizienter deterministischer Algorithmus für ein Problem bekannt ist. Man gibt sich dann damit zufrieden, dass der randomisierte Algorithmus die korrekte Lösung nur mit hoher Wahrscheinlichkeit findet, wenn dies dafür sehr effizient geschieht. Bei manchen Problemen ist auch dies unerreichbar - man muss dann bereits zufrieden sein, wenn der Algorithmus mit hoher Wahrscheinlichkeit eine sehr gute Näherungslösung findet. Beliebte Anwendungsgebiete für Monte Carlo-Algorithmen sind beispielsweise&lt;br /&gt;
* Randomisierte Primzahl-Tests: Moderne Verschlüsselungsverfahren benötigen zahlreiche Primzahlen, aber exakte Primzahltests sind teuer. Der [http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin-Test] findet effizient Zahlen, die mit sehr hoher Wahrscheinlichkeit tatsächlich Primzahlen sind.&lt;br /&gt;
* Randomisiertes Testen: Wie jeder Test kann auch eine randomisierter Test nicht die Abwesenheit von Programmierfehlern garantieren, aber man kann durch die Randomisierung viel mehr Testfälle generieren und erhöht so die Erfolgswarscheinlichkeit. Wir haben als Beispiel dafür den [[Korrektheit#Beispiel_f.C3.BCr_das_Testen:_Freivalds_Algorithmus|Algorithmus von Freivald]] behandelt.&lt;br /&gt;
* Lösung schwieriger Optimierungsprobleme: Wir zeigen unten, dass ein randomisierter Algorithmus effizient eine Lösung für das 2-SAT-Problem aus dem vorherigen Kapitel findet (für k-SAT mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; liefert der Algorithmus immer noch mit einer gewissen Wahrscheinlichkeit das richtige Ergebnis, ist aber nicht mehr effizient). Einen effizienten Approximationsalgorithmus für des Problem des Handelsreisenden behandlen wir im Kapitel [[NP-Vollständigkeit]]. Weitere wichtige Beispiele für diesen Bereich sind [http://en.wikipedia.org/wiki/Simulated_annealing simulated annealing] und das [http://de.wikipedia.org/wiki/MCMC-Verfahren Markov-Chain-Monte-Carlo-Verfahren].&lt;br /&gt;
* Robuste Statistik: Eine Grundaufgabe der Statistik ist das Anpassen (Fitten) von Modellen an gemessene Werte. Wenn die Messungen jedoch &amp;quot;Ausreißer&amp;quot; (einige völlig falsche Werte) enthalten, geht die Anpassung schief. Wir beschreiben unten den RANSAC-Algorithmus, der die Ausreißer identifizieren und beim Modellfitten ignorieren kann.&lt;br /&gt;
&lt;br /&gt;
Obwohl randomisierte Algorithmen oft einfach und elegant sind, ist ihre theoretische Analyse (also das Führen von Korrektheits- und Komplexitätsbeweisen) häufig sehr schwierig. Man muss fortgeschrittene Methoden der Wahrscheinlichkeitsrechnung und Statistik beherrschen, um die Wahrscheinlichkeit für das Versagen des Algorithmus zu berechnen und um zu zeigen, wie man den Algorithmus benutzt, damit diese Wahrscheinlichkeit unter einer akzeptablen Schranke bleibt. Die Algorithmen, die wir für diese Vorlesung ausgewählt haben, zeichnen sich dadurch aus, dass die Beweise hier einfach zu erbringen sind.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Lösen des K-SAT-Problems ===&lt;br /&gt;
&lt;br /&gt;
Der &amp;lt;b&amp;gt;Algorithmus von Schöning&amp;lt;/b&amp;gt; löst das [[Graphen_und_Graphenalgorithmen#Normalformen für logische Ausdrücke|k-SAT-Problem]] durch Raten: Wenn ein Ausdruck in k-CNF den Wert False hat, gibt es mindestens eine Klausel, die den Wert False hat. Alle Literale in dieser Klausel haben ebenfalls den Wert False, denn jede Klausel ist eine ODER-Verknüpfung, die nur dann False werden kann. Um den Ausdruck zu erfüllen, muss jede Klausel den Wert True annehmen, also müssen wir den Wert von mindestens einem Literal umdrehen. Wenn der Ausruck tatsächlich erfüllbar ist, gibt es immer ein geeignetes Literal, wir wissen nur nicht, welches. Deshalb drehen wir ein unter den k Literalen der betreffenden Klausel zufällig gewähltes. Liegen wir mit unserer Wahl richtig, sind wir der Lösung näher gekommen - im besten Fall sind jetzt alle Klauseln erfüllt. Wählen wir jedoch die falsche Variable, ist die aktuelle Klausel zwar jetzt True, aber dafür werden andere Klauseln zu False, die bisher True waren, und wir entfernen uns somit von der Lösung.&lt;br /&gt;
&lt;br /&gt;
    geg.: logischer Ausdruck in K-CNF (n Variablen, m Klauseln, k Variablen pro Klausel)&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;math&amp;gt;\underbrace {\underbrace {\left(x_1 \vee x_3 \vee...\right)}_{k\; Literale} \wedge \left( x_2 \vee x_4 \vee...\right)}_{m\;Klauseln}&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Algorithmus von Schöning lautet in Pseudocode:&lt;br /&gt;
    for i in range (trials):    #Anzahl der Versuche&lt;br /&gt;
         Bestimme eine Zufallsbelegung der Variablen &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
         for j in range (steps):&lt;br /&gt;
               if &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt; erfüllt alle Klauseln: &lt;br /&gt;
                   return &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
               wähle zufällig eine Klausel, die nicht erfüllt ist und negiere zufällig eine der Variablen in dieser Klausel &lt;br /&gt;
               # (die Klausel ist jetzt erfüllt)&lt;br /&gt;
    return None  # keine Lösung gefunden&lt;br /&gt;
&lt;br /&gt;
Findet der Algorithmus eine Lösung, wissen wir, dass der Ausdruck erfüllbar ist. Andernfalls könnte der Ausdruck unerfüllbar sein, oder wir haben nur Pech gehabt. Je mehr erfolglose Versuche wir machen, desto höher ist die Wahrscheinlichkeit, dass das erste zutrifft.&lt;br /&gt;
&lt;br /&gt;
Es ist sinnvoll, &amp;lt;tt&amp;gt;steps = k*n&amp;lt;/tt&amp;gt; zu wählen. Dann gilt der &lt;br /&gt;
;Satz: Wenn ein Ausdruck in k-CNF mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; erfüllbar ist, muss man im Mittel &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in O\left(\left(\frac{2(k-1)}{k}\right)^n \right)&amp;lt;/math&amp;gt; Versuche machen, um eine Lösung zu finden.&lt;br /&gt;
&lt;br /&gt;
Für &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; gilt stets &amp;lt;math&amp;gt;\frac{2(k-1)}{k} &amp;gt; 1&amp;lt;/math&amp;gt;, man benötigt also eine in n exponentielle Anzahl von Versuchen. Bei &amp;lt;math&amp;gt;k=3&amp;lt;/math&amp;gt; gilt z.B. &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt; \in O\left(\left(\frac{4}{3}\right)^n\right)&amp;lt;/math&amp;gt;. Dies ist zwar im Mittel effizienter also die erschöpfende Suche, die &amp;lt;math&amp;gt;O(2^n)&amp;lt;/math&amp;gt; Schritte benötigt, aber immer noch sehr langsam.&lt;br /&gt;
&lt;br /&gt;
Der Fall &amp;lt;b&amp;gt;&amp;lt;math&amp;gt;k=2&amp;lt;/math&amp;gt; ist jedoch ein Sonderfall&amp;lt;/b&amp;gt;: Hier kann man leicht beweisen, dass eine Lösung im Mittel bereits nach &amp;lt;math&amp;gt;O\left(n^2\right)&amp;lt;/math&amp;gt; Schritten gefunden wird. Wenn man schon weiss, dass der Ausdruck erfüllbar ist (was mit [[Graphen_und_Graphenalgorithmen#Lösung des 2-SAT-Problems mit Implikationgraphen|Implikationgraphen]] leicht geprüft werden kann), lässt man den randomisierten Algorithmus einfach so lange laufen, bis er eine Lösung findet. Man setzt also &amp;lt;tt&amp;gt;step = infinity&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;trials = 1&amp;lt;/tt&amp;gt; und verlässt sich darauf, dass das &amp;lt;tt&amp;gt;return&amp;lt;/tt&amp;gt; mit einer gültigen Lösung früher oder später ausgeführt wird. Dass man darauf im Mittel nur &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte warten muss, zeigen wir jetzt mit Hilfe eines &amp;lt;i&amp;gt;random walk&amp;lt;/i&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk====&lt;br /&gt;
&lt;br /&gt;
Um die Random Walk Analyse zu verstehen, betrachten wir folgendes Spiel:&lt;br /&gt;
&lt;br /&gt;
   geg.: eine Stuhlreihe mit N Stühlen. Wir nummerieren die Stühle so, dass links der Stuhl 0 und rechts der Stuhl N steht.&lt;br /&gt;
   &lt;br /&gt;
   * Eine Person setzt sich zufällig auf einen der Stühle.&lt;br /&gt;
   * Eine zweite Person wirft eine Münze.&lt;br /&gt;
   &lt;br /&gt;
         Wenn die Münze auf Zahl fällt, rückt die erste Person einen Stuhl nach links, andernfalls nach rechts.&lt;br /&gt;
         &amp;lt;--- Zahl                                                                                    Kopf ---&amp;gt;&lt;br /&gt;
   &lt;br /&gt;
   * Frage: Wie oft muss man die Münze im Durchschnitt werfen, bis Person 1 zum ersten Mal auf Stuhl N sitzt?&lt;br /&gt;
&lt;br /&gt;
Da die erste Person sich anfangs zufällig hinsetzt, haben wir eine Chance von 1/N, dass sie gleich auf dem richtigen Stuhl landet und wir 0 Schritte benötigen. Mit der gleichen Wahrscheinlichkeit von 1/N setzt sie sich anfangs auf Stuhl Nummer (N-1), und wir haben eine fifty-fifty-Chance, mit nur einem Wurf durchzukommen. Wir können aber auch Pech haben und landen auf Stuhl Nummer (N-2). Das ist das Gleiche, als wenn Person 1 von Anfang an auf diesem Stuhl gesessen hätte, nur dass wir jetzt bereits einen Wurf verbraucht haben. Man sieht, dass man die Zahl der Restwürfe immer in dieser Art ausdrücken kann: Sitzt Person 1 auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, kann sie entweder nach rechts rücken und benötigt dann noch soviele Würfe, wie man typischerweise für Stuhl &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; benötigt, plus den Wurf von &amp;lt;tt&amp;gt;i =&amp;gt; i+1&amp;lt;/tt&amp;gt;. Oder sie kann nach links rücken und benötigt dann die typische Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; plus den Wurf &amp;lt;tt&amp;gt;i =&amp;gt; i-1&amp;lt;/tt&amp;gt;. Beide Möglichkeiten haben die Wahrscheinlichkeit 1/2. Mathematisch kann man dies elegant als Rekursionsformel schreiben, die die erwartete Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; als Funktion der entsprechenden Wurfzahlen für die Stühle &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; ausdrückt:&lt;br /&gt;
&lt;br /&gt;
* Wenn wir uns auf Stuhl Nr.1 befinden, werfen wir gar nicht: &amp;lt;math&amp;gt;W\left(N\right)=0&amp;lt;/math&amp;gt;&lt;br /&gt;
* Von Stuhl 0 gehen wir immer zu Stuhl 1: &amp;lt;math&amp;gt;W\left(0\right)=1 + W\left(1\right)&amp;lt;/math&amp;gt;&lt;br /&gt;
* Allgemeiner Fall: &amp;lt;math&amp;gt;W\left(i\right)=\frac 1 2 \left(1 + W\left(i+1\right)\right) + \frac 1 2 \left(1 + W\left(i-1\right)\right) = \frac 1 2 W\left(i+1\right) + \frac 1 2 W\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
Diese Rekursion wird durch die explizite Formel&lt;br /&gt;
::&amp;lt;math&amp;gt;W\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
gelöst, wie man durch Einsetzen leicht nachprüft:&lt;br /&gt;
::&amp;lt;math&amp;gt;&lt;br /&gt;
       \begin{align} &lt;br /&gt;
             W\left(N\right) &amp;amp; = N^2-N^2=0 \\&lt;br /&gt;
                  &lt;br /&gt;
             W\left(0\right) &amp;amp;= W\left(1\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-1^2+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2 - 0^2\\&lt;br /&gt;
                 &lt;br /&gt;
              W\left(i\right) &amp;amp;= \frac 1 2 \left(N^2-\left(i-1\right)^2\right) + \frac 1 2 \left(N^2-\left(i+1\right)^2\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= \frac 1 2 N^2-\frac 1 2 \left( i^2-2i+1\right) + \frac 1 2 N^2-\frac 1 2 \left(i^2+2i+1\right) + 1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-i^2&lt;br /&gt;
       \end{align}&amp;lt;/math&amp;gt;&lt;br /&gt;
Insbesondere braucht man im ungünstigen Fall (Start auf Stuhl 0) im Durchschnitt &amp;lt;math&amp;gt;N^2&amp;lt;/math&amp;gt; Würfe, im typischen Fall (Start in der Mitte, also bei &amp;lt;math&amp;gt;i = N/2&amp;lt;/math&amp;gt;) im Durchschnitt &lt;br /&gt;
:&amp;lt;math&amp;gt;N^2 - (N/2)^2=\frac 3 4 N^2\in O(N^2)&amp;lt;/math&amp;gt; &lt;br /&gt;
Würfe. Die '''Beziehung zum randomisiertem 2-SAT-Algorithmus''' ist jetzt leicht zu erkennen. Sitzt die Person auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, interpretieren wir das als:&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Stuhl &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt;&amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert, &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
Wählt der Algorithmus eine Klausel, die nicht erfüllt ist, gibt es zwei Möglichkeiten:&lt;br /&gt;
# Beide Literale in der Klausel haben den falschen Wert: Die Lösung wird auf jeden Fall besser, egal welche der beiden wir umdrehen. Wir gehen also von Zustand &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
# Nur eins der Literale hat den falschen Wert: Beim Umdrehen haben wir eine fifty-fifty-Chance, das richtige Literal zu wählen und in den Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; zu gelangen. Mit der selben Wahrscheinlichkeit wählen wir das falsche Literal und landen im Zustand &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
Falls 2 ist der ungünstigere und entspricht unserem Spiel, dessen Analyse wir deshalb einfach auf das 2-SAT-Problem übertragen können: Ziel des Algorithmus ist es, in den Zustand N zu gelangen, und deshalb gilt genau wie beim Spiel der &lt;br /&gt;
;Satz: Der randomisierte 2-SAT-Algorithmus findet im Durchschnitt nach &amp;lt;math&amp;gt;O(N^2)&amp;lt;/math&amp;gt; Versuchen eine Lösung, wenn das Problem erfüllbar ist.&lt;br /&gt;
Damit ist der randomisierte Algorithmus für dieses Problem effizient, was Sie in Übung 12 experimentell nachprüfen sollen.&lt;br /&gt;
&lt;br /&gt;
== 2. RANSAC-ALGORITHMUS (Random Sample Consensus)==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''Aufgabe:''&amp;lt;/u&amp;gt; gegeben: Datenpunkte&lt;br /&gt;
::gesucht: Modell, das die Datenpunkte erklärt&lt;br /&gt;
&lt;br /&gt;
[[Image:Rubto.png|thumb|250px|none]]&lt;br /&gt;
&lt;br /&gt;
'''Messpunkte:'''&lt;br /&gt;
   &lt;br /&gt;
      übliche Lösung: Methode der kleinsten Quadrate&lt;br /&gt;
      &lt;br /&gt;
      &amp;lt;math&amp;gt;\min_{a,b} 	\sum_{i} \left(a x_i + b + y_i\right)^2&amp;lt;/math&amp;gt;&lt;br /&gt;
      &lt;br /&gt;
      Schulmathematik:      &amp;lt;math&amp;gt;Minimum\stackrel{\wedge}{=}Ableitung=0&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lineares Gleichungssystem'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{da}\sum{i} \left(ax_i+b-y_i\right)^2=\sum{i} \frac{d}{da} \left[ax_i+b-y_i\right)^2&amp;lt;/math&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(g\left(x\right)\right)&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(x\right)=x^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;y\left(a\right)=ax_i+b-y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;=\sum_{i}2\left(ax_i+b-y_i\right)\frac{d}{da} \underbrace {ax_i+b-y_i}_{x_i}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\underline {=2\sum_{i}\left(ax_i+b-y_i\right)x_i\stackrel{!}{=}0}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}{x_i}^2+b\sum_{i}x_i=\sum_{i}x_iy_i&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}x_i+b\sum_{i}1=\sum_{i}y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{db}\sum_{i}\left(ax_i+b-y_i\right)^2=2\sum_{i}\left(ax_i+b-y_i\right)*1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:Problem: &amp;lt;math&amp;gt;\epsilon  %&amp;lt;/math&amp;gt; der Datenpunkte sind Outlier&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\Longrightarrow&amp;lt;/math&amp;gt; Einfaches Anpassen des Modells an die Datenpunkte funktioniert nicht&lt;br /&gt;
&lt;br /&gt;
:Seien mindestens k Datenpunkte notwendig, um das Programm anpassen zu können&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
RANSAC-Algorithmus&lt;br /&gt;
&lt;br /&gt;
      for  l in range (trials):&lt;br /&gt;
           wähle zufällig k Punkte aus&lt;br /&gt;
           passe das Modell an die k Punkte an&lt;br /&gt;
           zähle, wieviele Punkte in der Nähe des Modells liegen (d.h. &amp;lt;math&amp;gt;d_i &amp;lt; d_max&amp;lt;/math&amp;gt; muss geschickt gewählt werden) &lt;br /&gt;
                                           #Bsp. Geradenfinden:-wähle a,b aus zwei Punkten&lt;br /&gt;
                                                               -berechne: &amp;lt;math&amp;gt;|ax_i+b-y_i|=d_i&amp;lt;/math&amp;gt;&lt;br /&gt;
                                                               -zähle Punkt i als Inlier, falls &amp;lt;math&amp;gt;d_i&amp;lt;d_ma&amp;lt;/math&amp;gt;&lt;br /&gt;
      return: Modell mit höchster Zahl der Inlier&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;trials= \frac{log\left(1-p\right)}{log\left(1-\left(1-\epsilon\right)^k\right)}&amp;lt;/math&amp;gt;  mit k=Anzahl der Datenpunkte und p=Erfolgswahrscheinlichkeit, &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;=Outlier-Anteil&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Erfolgswahrscheinlichkeit: p=99%'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{|c||c|c|c|c|c|}&lt;br /&gt;
         Beispiel &amp;amp; k &amp;amp; \epsilon=10% &amp;amp; 20% &amp;amp; 50% &amp;amp; 70%\\&lt;br /&gt;
         \hline&lt;br /&gt;
         Linie\;in\;2D &amp;amp; 2 &amp;amp; 3 &amp;amp;5 &amp;amp; 17 &amp;amp; 49\\&lt;br /&gt;
         Kreis\;in\;2D &amp;amp; 3 &amp;amp; 4 &amp;amp; 7 &amp;amp; 35 &amp;amp; 169\\&lt;br /&gt;
         Ebene\;in\;3D &amp;amp; 8 &amp;amp; 9 &amp;amp; 26 &amp;amp; 1172 &amp;amp; 70188\\&lt;br /&gt;
       \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Ein Spiel: Wie viel Schritte braucht man im Mittel zum Ziel?'''&lt;br /&gt;
&lt;br /&gt;
   geg.: 5 Plätze, 2 Personen: eine Person rückt vom einem Platz zu dem enderen Platz;&lt;br /&gt;
         die zweite Person wirft die Münze.&lt;br /&gt;
         Wenn die Münze auf Kopf landet, rücke nach rechts und wenn die Münze auf Zahl landet, rücke nach links.&lt;br /&gt;
         &amp;lt;--- Zahl                                                         Kopf--&amp;gt;&lt;br /&gt;
         Kopf: /////&lt;br /&gt;
         Zahl: /// &lt;br /&gt;
&lt;br /&gt;
:: =&amp;gt; mit 8 Schritten bis zum Ziel&lt;br /&gt;
:im Mittel: bei N Plätzen braucht man N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritte&lt;br /&gt;
&lt;br /&gt;
: all: mit N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritten um N Plätze rücken&lt;br /&gt;
: Wie viel Schritte braucht man im Mittel zum Ziel?&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(N\right)=0&amp;lt;/math&amp;gt;    #wenn wir uns im Stuhl Nr.1 befinden&lt;br /&gt;
           &lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)=\frac 1 2 S\left(1 + S\left(i+1\right)\right) + \frac 1 2 S\left(1 + S\left(i-1\right)\right) = \frac 1 2 S\left(i+1\right) + \frac 1 2 S\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(0\right)=1 + S\left(1\right)&amp;lt;/math&amp;gt;    #bei 0.Platz&lt;br /&gt;
&lt;br /&gt;
:::*Lösung: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:::*speziell: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2&amp;lt;/math&amp;gt;           #wenn man am ungünstigsten Platz startet&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
'''Beziehung zu randomisiertem 2-SAT'''&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Platz &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; &amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert,  &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)=N^2 - \left(\frac N 2\right)^2 = N^2 - \frac N 4 ^2 = \frac 3 4 N^2 &amp;lt;/math&amp;gt;&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)&amp;lt;/math&amp;gt;     # Anfangszustand&lt;br /&gt;
----&lt;br /&gt;
== '''Las Vegas vs. Monte Carlo'''==&lt;br /&gt;
&lt;br /&gt;
   * ''Las Vegas - Algorithmen''&lt;br /&gt;
     - Ergebnis ist immer korrekt.&lt;br /&gt;
     - Berechnung ist mit hoher Wahrscheinlichkeit effizient (d.h. Randomisierung macht den ungünstigsten Fall unwahrscheinlich).&lt;br /&gt;
&lt;br /&gt;
   * ''Monte Carlo - Algorithmen''&lt;br /&gt;
     - Berechnung immer effizient.&lt;br /&gt;
     - Ergebnis mit hoher Wahrscheinlichkeit korrekt (falls kein effizienter Algorithmus bekannt, der immer die richtige Lösung liefert).&lt;br /&gt;
&lt;br /&gt;
{| border = &amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;center&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! Las Vegas&lt;br /&gt;
! Monte Carlo&lt;br /&gt;
|- &lt;br /&gt;
| - Erzeugen einer perfekten Hashfuktion &lt;br /&gt;
| - Algorithmus von Freiwald(Matrizenmultiplikation)&lt;br /&gt;
|-&lt;br /&gt;
| - universelles Hashing&lt;br /&gt;
| - RANSAC&lt;br /&gt;
|-&lt;br /&gt;
| - Quick Sort mit zufälliger Wahl des Pivot-Elements&lt;br /&gt;
| - randomisierte K-SAT(k&amp;gt;=3)(Alg. von Schöning)&lt;br /&gt;
|-&lt;br /&gt;
| - Treep mit zufälligen Prioritäten&lt;br /&gt;
| -&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== ''' Zufallszahlen ''' ==&lt;br /&gt;
&lt;br /&gt;
:- kann man nicht mit deterministischen Computern erzeugen&lt;br /&gt;
:- aber man kann Pseudo-Zufallszahlen erzeugen, die viele Eigenschaften von echten Zufallszahlen haben&lt;br /&gt;
::: * sehr ähnlich  zum Hash&lt;br /&gt;
&lt;br /&gt;
     ''&amp;quot;linear Conguential Random number generator&amp;quot;''&lt;br /&gt;
        &amp;lt;math&amp;gt;I_{i+1}= \left(a*I_i + c\right)\textrm{mod\ } m&amp;lt;/math&amp;gt;&lt;br /&gt;
        &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{=&amp;gt; } &amp;amp; I_i \in [0, m-1]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:-sorgfältige Wahl von  a, c, m notwendig&lt;br /&gt;
::'''Bsp.'''  m = 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;&lt;br /&gt;
::: a = 1664525, c = 1013904223&lt;br /&gt;
::: ''&amp;quot;quick and dirty generator&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
==='''Nachteile'''===&lt;br /&gt;
&lt;br /&gt;
* nicht zufällig genug für viele Anwendungen&lt;br /&gt;
::'''Bsp.''' wähle Punkt in R&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
      \mathrm{ } &amp;amp; p = (rand(), rand(), rand())\\&lt;br /&gt;
&lt;br /&gt;
      \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::gibt Zahl u, v, w so, dass &lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; u * p[0] + v * p[1] + w * p[3]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::stark geclustert ist.&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge ist zu kurz:&lt;br /&gt;
:: spätestens nach m Schritten wiederholt sich die Folge&lt;br /&gt;
&lt;br /&gt;
::'''allgemein''': falls der interne Zustand des Zufallsgenerators ''k'' bits hat, ist Periodenlänge:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; Periode &amp;lt; 2^k\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* ''lowbits'' sind weniger zufällig als die ''highbits''&lt;br /&gt;
----&lt;br /&gt;
=== ''Mersenne Twister''===&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
'''bester zur Zeit bekannter Zufallszahlengenerator (ZZG)'''&lt;br /&gt;
* innere Zustand: &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; 624*32 bit\ Integers  =&amp;gt; 19968 bits\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge: &amp;lt;math&amp;gt;2^ {19937} \approx 4 * 10^{6000}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Punkte aus aufeinanderfolgende Zufallszahlen in &amp;lt;math&amp;gt;\mathbb{R}^n&amp;lt;/math&amp;gt; sind gleich verteilt bis &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; n = 623\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* alle Bits sind unabhängig voneinander zufällig (&amp;quot;Twister&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
* schnell&lt;br /&gt;
&lt;br /&gt;
    class MersenneTwister:&lt;br /&gt;
        &lt;br /&gt;
        def __init__(self, seed):&lt;br /&gt;
            self.N = 624  # Größe des inneren Zustands festlegen&lt;br /&gt;
            self.i = 0    # zählt mit in welchem Zustand wir uns gerade aufhalten&lt;br /&gt;
            &lt;br /&gt;
            self.state = [0]*self.N  # Speicher für den inneren Zustand reservieren&lt;br /&gt;
            &lt;br /&gt;
            self.state[0] = seed     # initiale Zufallszahl vom Benutzer&lt;br /&gt;
            # den Rest des inneren Zustands mit einfachem Zufallszahlengenerator initialisieren&lt;br /&gt;
            for i in xrange(1, self.N):&lt;br /&gt;
                self.state[i] = (1812433253 * (self.state[i-1] ^ (self.state[i-1] &amp;gt;&amp;gt; 30)) + i) % 4294967296&lt;br /&gt;
     &lt;br /&gt;
        def __call__(self):&lt;br /&gt;
            &amp;quot;&amp;quot;&amp;quot;gibt die nächste Zufallszahl im Bereich [0, 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;-1] aus&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
            N, M = self.N, 397&lt;br /&gt;
            &lt;br /&gt;
            # Zustand aktualisieren (neue Zufallszahl ausrechnen)&lt;br /&gt;
            i = self.i&lt;br /&gt;
            r = ((self.state[i] &amp;amp; 0x80000000) | (self.state[(i+1)%N] &amp;amp; 0x7FFFFFFF)) &amp;gt;&amp;gt; 1&lt;br /&gt;
            if self.state[(i+1)%N] &amp;amp; 1:&lt;br /&gt;
                r ^= 0x9908B0DF&lt;br /&gt;
            self.state[i] = self.state[(i+M)%N] ^ r&lt;br /&gt;
     &lt;br /&gt;
            # aktuelle Zufallszahl auslesen und ihre Zufälligkeit durch verwürfeln der Bits verbessern&lt;br /&gt;
            y = self.state[i]&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 11)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt;  7) &amp;amp; 0x9D2C5680)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt; 15) &amp;amp; 0xEFC60000)&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 18)&lt;br /&gt;
            &lt;br /&gt;
            # Zustand weitersetzen und endgültige Zufallszahl ausgeben&lt;br /&gt;
            self.i = (self.i + 1) % N&lt;br /&gt;
            return y&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''geg.:''' Zufallszahl &lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, \overbrace{2^{32}-1}^{m-1}]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ges.:''' Zufallszahl&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, k - 1]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''naive Lösung:'''  &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; rand()%k\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;  ist schlecht.&lt;br /&gt;
&lt;br /&gt;
'''Bsp.'''&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; \qquad m = 16\qquad k = 11\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
! rand() || 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15&lt;br /&gt;
|-&lt;br /&gt;
! rand()%k&lt;br /&gt;
! 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 0 || 1 || 2 || 3 || 4 &lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; 0,...,4 kommt doppelt so häufig wie 5,...,10 &amp;quot;nicht zufällig&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lösung:'''  Zurückweisen des Rests der Zahlen (''rejektion sampling'')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; remainder = (m - 1 - (k - 1))% k = (m - k)%k\\&lt;br /&gt;
        \mathrm{ } &amp;amp; last\ Good\ Value = m-1-remainder\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  r = rand()&lt;br /&gt;
  while r &amp;gt; last.GoodValue:&lt;br /&gt;
        r = rand()&lt;br /&gt;
        return r%k&lt;br /&gt;
&lt;br /&gt;
[[Greedy-Algorithmen und Dynamische Programmierung|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5436</id>
		<title>Randomisierte Algorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5436"/>
				<updated>2012-07-30T11:50:30Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Randomisierte Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
;Definition: Randomisierte Algorithmen sind Algorithmen, die bei Entscheidungen über ihr weiteres Vorgehen oder bei der Wahl ihrer Parameter Zufallszahlen benutzen.&lt;br /&gt;
&lt;br /&gt;
Anschaulich gesprochen, wersucht man bei randomisierten Algorithmen, einen Teil der Lösung zu &amp;lt;i&amp;gt;raten&amp;lt;/i&amp;gt;. Auf den ersten Blick würde man vermuten, dass dabei nicht viel Sinnvolles herauskommen kann. Diese Kapitel wird jedoch zeigen, dass man durch geschicktes Raten tatsächlich zu sehr eleganten Algorithmen gelangen kann.&lt;br /&gt;
&lt;br /&gt;
Grundsätzlich unterscheidet man zwei Arten von randomisierten Algorithmen:&lt;br /&gt;
;Las Vegas - Algorithmen: Das Ergebnis des Algorithmus ist immer korrekt, und die Berechnung erfolgt mit hoher Wahrscheinlichkeit effizient.&lt;br /&gt;
;Monte Carlo - Algorithmen: Die Berechnung ist immer effizient, und das Ergebnis ist mit hoher Wahrscheinlichkeit korrekt.&lt;br /&gt;
Las Vegas-Algorithmen verwendet man, wenn der Algorithmus im ungünstigen Fall eine schlechte Laufzeit hat, und der ungünstige Fall kann durch die Randomisierung sehr unwahrscheinlich gemacht werden. Wir haben in der Vorlesung schon mehrere Las Vegas-Algorithmen kennen gelernt:&lt;br /&gt;
* Quick Sort mit zufälliger Wahl des Pivot-Elements: Die Randomisierung verhindert, dass das Array immer wieder in Subarrays von sehr unterschiedlicher Größe aufgeteilt wird.&lt;br /&gt;
* Treap mit zufälligen Prioritäten: Die Randomisierung verhindert, dass der Baum schlecht balanciert ist.&lt;br /&gt;
* Universelles Hashing: Die zufällige Wahl der Hashfunktion verhindert, dass ein Angreifer eine Schlüsselmenge mit sehr vielen Kollisionen konstruieren kann.&lt;br /&gt;
* Erzeugung einer perfekten Hashfunktion: Durch die Randomisierung entsteht mit nach wenigen Versuchen ein zyklenfreier Graph, der zur Definition der Hashfunktion geeignet ist.&lt;br /&gt;
Monte Carlo-Algorithmen verwendet man dagegen, wenn kein effizienter deterministischer Algorithmus für ein Problem bekannt ist. Man gibt sich dann damit zufrieden, dass der randomisierte Algorithmus die korrekte Lösung nur mit hoher Wahrscheinlichkeit findet, wenn dies dafür sehr effizient geschieht. Bei manchen Problemen ist auch dies unerreichbar - man muss dann bereits zufrieden sein, wenn der Algorithmus mit hoher Wahrscheinlichkeit eine sehr gute Näherungslösung findet. Beliebte Anwendungsgebiete für Monte Carlo-Algorithmen sind beispielsweise&lt;br /&gt;
* Randomisierte Primzahl-Tests: Moderne Verschlüsselungsverfahren benötigen zahlreiche Primzahlen, aber exakte Primzahltests sind teuer. Der [http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin-Test] findet effizient Zahlen, die mit sehr hoher Wahrscheinlichkeit tatsächlich Primzahlen sind.&lt;br /&gt;
* Randomisiertes Testen: Wie jeder Test kann auch eine randomisierter Test nicht die Abwesenheit von Programmierfehlern garantieren, aber man kann durch die Randomisierung viel mehr Testfälle generieren und erhöht so die Erfolgswarscheinlichkeit. Wir haben als Beispiel dafür den [[Korrektheit#Beispiel_f.C3.BCr_das_Testen:_Freivalds_Algorithmus|Algorithmus von Freivald]] behandelt.&lt;br /&gt;
* Lösung schwieriger Optimierungsprobleme: Wir zeigen unten, dass ein randomisierter Algorithmus effizient eine Lösung für das 2-SAT-Problem aus dem vorherigen Kapitel findet (für k-SAT mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; liefert der Algorithmus immer noch mit einer gewissen Wahrscheinlichkeit das richtige Ergebnis, ist aber nicht mehr effizient). Einen effizienten Approximationsalgorithmus für des Problem des Handelsreisenden behandlen wir im Kapitel [[NP-Vollständigkeit]]. Weitere wichtige Beispiele für diesen Bereich sind [http://en.wikipedia.org/wiki/Simulated_annealing simulated annealing] und das [http://de.wikipedia.org/wiki/MCMC-Verfahren Markov-Chain-Monte-Carlo-Verfahren].&lt;br /&gt;
* Robuste Statistik: Eine Grundaufgabe der Statistik ist das Anpassen (Fitten) von Modellen an gemessene Werte. Wenn die Messungen jedoch &amp;quot;Ausreißer&amp;quot; (einige völlig falsche Werte) enthalten, geht die Anpassung schief. Wir beschreiben unten den RANSAC-Algorithmus, der die Ausreißer identifizieren und beim Modellfitten ignorieren kann.&lt;br /&gt;
&lt;br /&gt;
Obwohl randomisierte Algorithmen oft einfach und elegant sind, ist ihre theoretische Analyse (also das Führen von Korrektheits- und Komplexitätsbeweisen) häufig sehr schwierig. Man muss fortgeschrittene Methoden der Wahrscheinlichkeitsrechnung und Statistik beherrschen, um die Wahrscheinlichkeit für das Versagen des Algorithmus zu berechnen und um zu zeigen, wie man den Algorithmus benutzt, damit diese Wahrscheinlichkeit unter einer akzeptablen Schranke bleibt. Die Algorithmen, die wir für diese Vorlesung ausgewählt haben, zeichnen sich dadurch aus, dass die Beweise hier einfach zu erbringen sind.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Lösen des K-SAT-Problems ===&lt;br /&gt;
&lt;br /&gt;
Der &amp;lt;b&amp;gt;Algorithmus von Schöning&amp;lt;/b&amp;gt; löst das [[Graphen_und_Graphenalgorithmen#Normalformen für logische Ausdrücke|k-SAT-Problem]] durch Raten: Wenn ein Ausdruck in k-CNF den Wert False hat, gibt es mindestens eine Klausel, die den Wert False hat. Alle Literale in dieser Klausel haben ebenfalls den Wert False, denn jede Klausel ist eine ODER-Verknüpfung, die nur dann False werden kann. Um den Ausdruck zu erfüllen, muss jede Klausel den Wert True annehmen, also müssen wir den Wert von mindestens einem Literal umdrehen. Wenn der Ausruck tatsächlich erfüllbar ist, gibt es immer ein geeignetes Literal, wir wissen nur nicht, welches. Deshalb drehen wir ein unter den k Literalen der betreffenden Klausel zufällig gewähltes. Liegen wir mit unserer Wahl richtig, sind wir der Lösung näher gekommen - im besten Fall sind jetzt alle Klauseln erfüllt. Wählen wir jedoch die falsche Variable, ist die aktuelle Klausel zwar jetzt True, aber dafür werden andere Klauseln zu False, die bisher True waren, und wir entfernen uns somit von der Lösung.&lt;br /&gt;
&lt;br /&gt;
    geg.: logischer Ausdruck in K-CNF (n Variablen, m Klauseln, k Variablen pro Klausel)&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;math&amp;gt;\underbrace {\underbrace {\left(x_1 \vee x_3 \vee...\right)}_{k\; Literale} \wedge \left( x_2 \vee x_4 \vee...\right)}_{m\;Klauseln}&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Algorithmus von Schöning lautet in Pseudocode:&lt;br /&gt;
    for i in range (trials):    #Anzahl der Versuche&lt;br /&gt;
         Bestimme eine Zufallsbelegung der Variablen &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
         for j in range (steps):&lt;br /&gt;
               if &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt; erfüllt alle Klauseln: &lt;br /&gt;
                   return &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
               wähle zufällig eine Klausel, die nicht erfüllt ist und negiere zufällig eine der Variablen in dieser Klausel &lt;br /&gt;
               # (die Klausel ist jetzt erfüllt)&lt;br /&gt;
    return None  # keine Lösung gefunden&lt;br /&gt;
&lt;br /&gt;
Findet der Algorithmus eine Lösung, wissen wir, dass der Ausdruck erfüllbar ist. Andernfalls könnte der Ausdruck unerfüllbar sein, oder wir haben nur Pech gehabt. Je mehr erfolglose Versuche wir machen, desto höher ist die Wahrscheinlichkeit, dass das erste zutrifft.&lt;br /&gt;
&lt;br /&gt;
Es ist sinnvoll, &amp;lt;tt&amp;gt;steps = k*n&amp;lt;/tt&amp;gt; zu wählen. Dann gilt der &lt;br /&gt;
;Satz: Wenn ein Ausdruck in k-CNF mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; erfüllbar ist, muss man im Mittel &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in O\left(\left(\frac{2(k-1)}{k}\right)^n \right)&amp;lt;/math&amp;gt; Versuche machen, um eine Lösung zu finden.&lt;br /&gt;
&lt;br /&gt;
Für &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; gilt stets &amp;lt;math&amp;gt;\frac{2(k-1)}{k} &amp;gt; 1&amp;lt;/math&amp;gt;, man benötigt also eine in n exponentielle Anzahl von Versuchen. Bei &amp;lt;math&amp;gt;k=3&amp;lt;/math&amp;gt; gilt z.B. &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt; \in O\left(\left(\frac{4}{3}\right)^n\right)&amp;lt;/math&amp;gt;. Dies ist zwar im Mittel effizienter also die erschöpfende Suche, die &amp;lt;math&amp;gt;O(2^n)&amp;lt;/math&amp;gt; Schritte benötigt, aber immer noch sehr langsam.&lt;br /&gt;
&lt;br /&gt;
Der Fall &amp;lt;b&amp;gt;&amp;lt;math&amp;gt;k=2&amp;lt;/math&amp;gt; ist jedoch ein Sonderfall&amp;lt;/b&amp;gt;: Hier kann man leicht beweisen, dass eine Lösung im Mittel bereits nach &amp;lt;math&amp;gt;O\left(n^2\right)&amp;lt;/math&amp;gt; Schritten gefunden wird. Wenn man schon weiss, dass der Ausdruck erfüllbar ist (was mit [[Graphen_und_Graphenalgorithmen#Lösung des 2-SAT-Problems mit Implikationgraphen|Implikationgraphen]] leicht geprüft werden kann), lässt man den randomisierten Algorithmus einfach so lange laufen, bis er eine Lösung findet. Man setzt also &amp;lt;tt&amp;gt;step = infinity&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;trials = 1&amp;lt;/tt&amp;gt; und verlässt sich darauf, dass das &amp;lt;tt&amp;gt;return&amp;lt;/tt&amp;gt; mit einer gültigen Lösung früher oder später ausgeführt wird. Dass man darauf im Mittel nur &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte warten muss, zeigen wir jetzt mit Hilfe eines &amp;lt;i&amp;gt;random walk&amp;lt;/i&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk====&lt;br /&gt;
&lt;br /&gt;
Um die Random Walk Analyse zu verstehen, betrachten wir folgendes Spiel:&lt;br /&gt;
&lt;br /&gt;
   geg.: eine Stuhlreihe mit N Stühlen. Wir nummerieren die Stühle so, dass links der Stuhl 0 und rechts der Stuhl N steht.&lt;br /&gt;
   &lt;br /&gt;
   * Eine Person setzt sich zufällig auf einen der Stühle.&lt;br /&gt;
   * Eine zweite Person wirft eine Münze.&lt;br /&gt;
   &lt;br /&gt;
         Wenn die Münze auf Zahl fällt, rückt die erste Person einen Stuhl nach links, andernfalls nach rechts.&lt;br /&gt;
         &amp;lt;--- Zahl                                                                                    Kopf ---&amp;gt;&lt;br /&gt;
&lt;br /&gt;
   * Frage: Wie oft muss man die Münze im Durchschnitt werfen, bis Person 1 zum ersten Mal auf Stuhl N sitzt?&lt;br /&gt;
&lt;br /&gt;
Da die erste Person sich anfangs zufällig hinsetzt, haben wir eine Chance von 1/N, dass sie gleich auf dem richtigen Stuhl landet und wir 0 Schritte benötigen. Mit der gleichen Wahrscheinlichkeit von 1/N setzt sie sich anfangs auf Stuhl Nummer (N-1), und wir haben eine fifty-fifty-Chance, mit nur einem Wurf durchzukommen. Wir können aber auch Pech haben und landen auf Stuhl Nummer (N-2). Das ist das Gleiche, als wenn Person 1 von Anfang an auf diesem Stuhl gesessen hätte, nur dass wir jetzt bereits einen Wurf verbraucht haben. Man sieht, dass man die Zahl der Restwürfe immer in dieser Art ausdrücken kann: Sitzt Person 1 auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, kann sie entweder nach rechts rücken und benötigt dann noch soviele Würfe, wie man typischerweise für Stuhl &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; benötigt, plus den Wurf von &amp;lt;tt&amp;gt;i =&amp;gt; i+1&amp;lt;/tt&amp;gt;. Oder sie kann nach links rücken und benötigt dann die typische Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; plus den Wurf &amp;lt;tt&amp;gt;i =&amp;gt; i-1&amp;lt;/tt&amp;gt;. Beide Möglichkeiten haben die Wahrscheinlichkeit 1/2. Mathematisch kann man dies elegant als Rekursionsformel schreiben, die die erwartete Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; als Funktion der entsprechenden Wurfzahlen für die Stühle &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; ausdrückt:&lt;br /&gt;
&lt;br /&gt;
* Wenn wir uns auf Stuhl Nr.1 befinden, werfen wir gar nicht: &amp;lt;math&amp;gt;W\left(N\right)=0&amp;lt;/math&amp;gt;&lt;br /&gt;
* Von Stuhl 0 gehen wir immer zu Stuhl 1: &amp;lt;math&amp;gt;W\left(0\right)=1 + W\left(1\right)&amp;lt;/math&amp;gt;&lt;br /&gt;
* Allgemeiner Fall: &amp;lt;math&amp;gt;W\left(i\right)=\frac 1 2 \left(1 + W\left(i+1\right)\right) + \frac 1 2 \left(1 + W\left(i-1\right)\right) = \frac 1 2 W\left(i+1\right) + \frac 1 2 W\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
Diese Rekursion wird durch die explizite Formel&lt;br /&gt;
::&amp;lt;math&amp;gt;W\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
gelöst, wie man durch Einsetzen leicht nachprüft:&lt;br /&gt;
::&amp;lt;math&amp;gt;&lt;br /&gt;
       \begin{align} &lt;br /&gt;
             W\left(N\right) &amp;amp; = N^2-N^2=0 \\&lt;br /&gt;
                  &lt;br /&gt;
             W\left(0\right) &amp;amp;= W\left(1\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-1^2+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2 - 0^2\\&lt;br /&gt;
                 &lt;br /&gt;
              W\left(i\right) &amp;amp;= \frac 1 2 \left(N^2-\left(i-1\right)^2\right) + \frac 1 2 \left(N^2-\left(i+1\right)^2\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= \frac 1 2 N^2-\frac 1 2 \left( i^2-2i+1\right) + \frac 1 2 N^2-\frac 1 2 \left(i^2+2i+1\right) + 1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-i^2&lt;br /&gt;
       \end{align}&amp;lt;/math&amp;gt;&lt;br /&gt;
Insbesondere braucht man im ungünstigen Fall (Start auf Stuhl 0) im Durchschnitt &amp;lt;math&amp;gt;N^2&amp;lt;/math&amp;gt; Würfe, im typischen Fall (Start in der Mitte, also bei &amp;lt;math&amp;gt;i = N/2&amp;lt;/math&amp;gt;) im Durchschnitt &lt;br /&gt;
:&amp;lt;math&amp;gt;N^2 - (N/2)^2=\frac 3 4 N^2\in O(N^2)&amp;lt;/math&amp;gt; &lt;br /&gt;
Würfe. Die '''Beziehung zum randomisiertem 2-SAT-Algorithmus''' ist jetzt leicht zu erkennen. Sitzt die Person auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, interpretieren wir das als:&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Stuhl &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt;&amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert, &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
Wählt der Algorithmus eine Klausel, die nicht erfüllt ist, gibt es zwei Möglichkeiten:&lt;br /&gt;
# Beide Literale in der Klausel haben den falschen Wert: Die Lösung wird auf jeden Fall besser, egal welche der beiden wir umdrehen. Wir gehen also von Zustand &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
# Nur eins der Literale hat den falschen Wert: Beim Umdrehen haben wir eine fifty-fifty-Chance, das richtige Literal zu wählen und in den Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; zu gelangen. Mit der selben Wahrscheinlichkeit wählen wir das falsche Literal und landen im Zustand &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
Falls 2 ist der ungünstigere und entspricht unserem Spiel, dessen Analyse wir deshalb einfach auf das 2-SAT-Problem übertragen können: Ziel des Algorithmus ist es, in den Zustand N zu gelangen, und deshalb gilt genau wie beim Spiel der &lt;br /&gt;
;Satz: Der randomisierte 2-SAT-Algorithmus findet im Durchschnitt nach &amp;lt;math&amp;gt;O(N^2)&amp;lt;/math&amp;gt; Versuchen eine Lösung, wenn das Problem erfüllbar ist.&lt;br /&gt;
Damit ist der randomisierte Algorithmus für dieses Problem effizient, was Sie in Übung 12 experimentell nachprüfen sollen.&lt;br /&gt;
&lt;br /&gt;
== 2. RANSAC-ALGORITHMUS (Random Sample Consensus)==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''Aufgabe:''&amp;lt;/u&amp;gt; gegeben: Datenpunkte&lt;br /&gt;
::gesucht: Modell, das die Datenpunkte erklärt&lt;br /&gt;
&lt;br /&gt;
[[Image:Rubto.png|thumb|250px|none]]&lt;br /&gt;
&lt;br /&gt;
'''Messpunkte:'''&lt;br /&gt;
   &lt;br /&gt;
      übliche Lösung: Methode der kleinsten Quadrate&lt;br /&gt;
      &lt;br /&gt;
      &amp;lt;math&amp;gt;\min_{a,b} 	\sum_{i} \left(a x_i + b + y_i\right)^2&amp;lt;/math&amp;gt;&lt;br /&gt;
      &lt;br /&gt;
      Schulmathematik:      &amp;lt;math&amp;gt;Minimum\stackrel{\wedge}{=}Ableitung=0&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lineares Gleichungssystem'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{da}\sum{i} \left(ax_i+b-y_i\right)^2=\sum{i} \frac{d}{da} \left[ax_i+b-y_i\right)^2&amp;lt;/math&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(g\left(x\right)\right)&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(x\right)=x^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;y\left(a\right)=ax_i+b-y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;=\sum_{i}2\left(ax_i+b-y_i\right)\frac{d}{da} \underbrace {ax_i+b-y_i}_{x_i}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\underline {=2\sum_{i}\left(ax_i+b-y_i\right)x_i\stackrel{!}{=}0}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}{x_i}^2+b\sum_{i}x_i=\sum_{i}x_iy_i&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}x_i+b\sum_{i}1=\sum_{i}y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{db}\sum_{i}\left(ax_i+b-y_i\right)^2=2\sum_{i}\left(ax_i+b-y_i\right)*1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:Problem: &amp;lt;math&amp;gt;\epsilon  %&amp;lt;/math&amp;gt; der Datenpunkte sind Outlier&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\Longrightarrow&amp;lt;/math&amp;gt; Einfaches Anpassen des Modells an die Datenpunkte funktioniert nicht&lt;br /&gt;
&lt;br /&gt;
:Seien mindestens k Datenpunkte notwendig, um das Programm anpassen zu können&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
RANSAC-Algorithmus&lt;br /&gt;
&lt;br /&gt;
      for  l in range (trials):&lt;br /&gt;
           wähle zufällig k Punkte aus&lt;br /&gt;
           passe das Modell an die k Punkte an&lt;br /&gt;
           zähle, wieviele Punkte in der Nähe des Modells liegen (d.h. &amp;lt;math&amp;gt;d_i &amp;lt; d_max&amp;lt;/math&amp;gt; muss geschickt gewählt werden) &lt;br /&gt;
                                           #Bsp. Geradenfinden:-wähle a,b aus zwei Punkten&lt;br /&gt;
                                                               -berechne: &amp;lt;math&amp;gt;|ax_i+b-y_i|=d_i&amp;lt;/math&amp;gt;&lt;br /&gt;
                                                               -zähle Punkt i als Inlier, falls &amp;lt;math&amp;gt;d_i&amp;lt;d_ma&amp;lt;/math&amp;gt;&lt;br /&gt;
      return: Modell mit höchster Zahl der Inlier&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;trials= \frac{log\left(1-p\right)}{log\left(1-\left(1-\epsilon\right)^k\right)}&amp;lt;/math&amp;gt;  mit k=Anzahl der Datenpunkte und p=Erfolgswahrscheinlichkeit, &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;=Outlier-Anteil&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Erfolgswahrscheinlichkeit: p=99%'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{|c||c|c|c|c|c|}&lt;br /&gt;
         Beispiel &amp;amp; k &amp;amp; \epsilon=10% &amp;amp; 20% &amp;amp; 50% &amp;amp; 70%\\&lt;br /&gt;
         \hline&lt;br /&gt;
         Linie\;in\;2D &amp;amp; 2 &amp;amp; 3 &amp;amp;5 &amp;amp; 17 &amp;amp; 49\\&lt;br /&gt;
         Kreis\;in\;2D &amp;amp; 3 &amp;amp; 4 &amp;amp; 7 &amp;amp; 35 &amp;amp; 169\\&lt;br /&gt;
         Ebene\;in\;3D &amp;amp; 8 &amp;amp; 9 &amp;amp; 26 &amp;amp; 1172 &amp;amp; 70188\\&lt;br /&gt;
       \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Ein Spiel: Wie viel Schritte braucht man im Mittel zum Ziel?'''&lt;br /&gt;
&lt;br /&gt;
   geg.: 5 Plätze, 2 Personen: eine Person rückt vom einem Platz zu dem enderen Platz;&lt;br /&gt;
         die zweite Person wirft die Münze.&lt;br /&gt;
         Wenn die Münze auf Kopf landet, rücke nach rechts und wenn die Münze auf Zahl landet, rücke nach links.&lt;br /&gt;
         &amp;lt;--- Zahl                                                         Kopf--&amp;gt;&lt;br /&gt;
         Kopf: /////&lt;br /&gt;
         Zahl: /// &lt;br /&gt;
&lt;br /&gt;
:: =&amp;gt; mit 8 Schritten bis zum Ziel&lt;br /&gt;
:im Mittel: bei N Plätzen braucht man N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritte&lt;br /&gt;
&lt;br /&gt;
: all: mit N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritten um N Plätze rücken&lt;br /&gt;
: Wie viel Schritte braucht man im Mittel zum Ziel?&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(N\right)=0&amp;lt;/math&amp;gt;    #wenn wir uns im Stuhl Nr.1 befinden&lt;br /&gt;
           &lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)=\frac 1 2 S\left(1 + S\left(i+1\right)\right) + \frac 1 2 S\left(1 + S\left(i-1\right)\right) = \frac 1 2 S\left(i+1\right) + \frac 1 2 S\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(0\right)=1 + S\left(1\right)&amp;lt;/math&amp;gt;    #bei 0.Platz&lt;br /&gt;
&lt;br /&gt;
:::*Lösung: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:::*speziell: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2&amp;lt;/math&amp;gt;           #wenn man am ungünstigsten Platz startet&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
'''Beziehung zu randomisiertem 2-SAT'''&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Platz &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; &amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert,  &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)=N^2 - \left(\frac N 2\right)^2 = N^2 - \frac N 4 ^2 = \frac 3 4 N^2 &amp;lt;/math&amp;gt;&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)&amp;lt;/math&amp;gt;     # Anfangszustand&lt;br /&gt;
----&lt;br /&gt;
== '''Las Vegas vs. Monte Carlo'''==&lt;br /&gt;
&lt;br /&gt;
   * ''Las Vegas - Algorithmen''&lt;br /&gt;
     - Ergebnis ist immer korrekt.&lt;br /&gt;
     - Berechnung ist mit hoher Wahrscheinlichkeit effizient (d.h. Randomisierung macht den ungünstigsten Fall unwahrscheinlich).&lt;br /&gt;
&lt;br /&gt;
   * ''Monte Carlo - Algorithmen''&lt;br /&gt;
     - Berechnung immer effizient.&lt;br /&gt;
     - Ergebnis mit hoher Wahrscheinlichkeit korrekt (falls kein effizienter Algorithmus bekannt, der immer die richtige Lösung liefert).&lt;br /&gt;
&lt;br /&gt;
{| border = &amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;center&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! Las Vegas&lt;br /&gt;
! Monte Carlo&lt;br /&gt;
|- &lt;br /&gt;
| - Erzeugen einer perfekten Hashfuktion &lt;br /&gt;
| - Algorithmus von Freiwald(Matrizenmultiplikation)&lt;br /&gt;
|-&lt;br /&gt;
| - universelles Hashing&lt;br /&gt;
| - RANSAC&lt;br /&gt;
|-&lt;br /&gt;
| - Quick Sort mit zufälliger Wahl des Pivot-Elements&lt;br /&gt;
| - randomisierte K-SAT(k&amp;gt;=3)(Alg. von Schöning)&lt;br /&gt;
|-&lt;br /&gt;
| - Treep mit zufälligen Prioritäten&lt;br /&gt;
| -&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== ''' Zufallszahlen ''' ==&lt;br /&gt;
&lt;br /&gt;
:- kann man nicht mit deterministischen Computern erzeugen&lt;br /&gt;
:- aber man kann Pseudo-Zufallszahlen erzeugen, die viele Eigenschaften von echten Zufallszahlen haben&lt;br /&gt;
::: * sehr ähnlich  zum Hash&lt;br /&gt;
&lt;br /&gt;
     ''&amp;quot;linear Conguential Random number generator&amp;quot;''&lt;br /&gt;
        &amp;lt;math&amp;gt;I_{i+1}= \left(a*I_i + c\right)\textrm{mod\ } m&amp;lt;/math&amp;gt;&lt;br /&gt;
        &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{=&amp;gt; } &amp;amp; I_i \in [0, m-1]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:-sorgfältige Wahl von  a, c, m notwendig&lt;br /&gt;
::'''Bsp.'''  m = 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;&lt;br /&gt;
::: a = 1664525, c = 1013904223&lt;br /&gt;
::: ''&amp;quot;quick and dirty generator&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
==='''Nachteile'''===&lt;br /&gt;
&lt;br /&gt;
* nicht zufällig genug für viele Anwendungen&lt;br /&gt;
::'''Bsp.''' wähle Punkt in R&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
      \mathrm{ } &amp;amp; p = (rand(), rand(), rand())\\&lt;br /&gt;
&lt;br /&gt;
      \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::gibt Zahl u, v, w so, dass &lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; u * p[0] + v * p[1] + w * p[3]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::stark geclustert ist.&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge ist zu kurz:&lt;br /&gt;
:: spätestens nach m Schritten wiederholt sich die Folge&lt;br /&gt;
&lt;br /&gt;
::'''allgemein''': falls der interne Zustand des Zufallsgenerators ''k'' bits hat, ist Periodenlänge:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; Periode &amp;lt; 2^k\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* ''lowbits'' sind weniger zufällig als die ''highbits''&lt;br /&gt;
----&lt;br /&gt;
=== ''Mersenne Twister''===&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
'''bester zur Zeit bekannter Zufallszahlengenerator (ZZG)'''&lt;br /&gt;
* innere Zustand: &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; 624*32 bit\ Integers  =&amp;gt; 19968 bits\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge: &amp;lt;math&amp;gt;2^ {19937} \approx 4 * 10^{6000}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Punkte aus aufeinanderfolgende Zufallszahlen in &amp;lt;math&amp;gt;\mathbb{R}^n&amp;lt;/math&amp;gt; sind gleich verteilt bis &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; n = 623\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* alle Bits sind unabhängig voneinander zufällig (&amp;quot;Twister&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
* schnell&lt;br /&gt;
&lt;br /&gt;
    class MersenneTwister:&lt;br /&gt;
        &lt;br /&gt;
        def __init__(self, seed):&lt;br /&gt;
            self.N = 624  # Größe des inneren Zustands festlegen&lt;br /&gt;
            self.i = 0    # zählt mit in welchem Zustand wir uns gerade aufhalten&lt;br /&gt;
            &lt;br /&gt;
            self.state = [0]*self.N  # Speicher für den inneren Zustand reservieren&lt;br /&gt;
            &lt;br /&gt;
            self.state[0] = seed     # initiale Zufallszahl vom Benutzer&lt;br /&gt;
            # den Rest des inneren Zustands mit einfachem Zufallszahlengenerator initialisieren&lt;br /&gt;
            for i in xrange(1, self.N):&lt;br /&gt;
                self.state[i] = (1812433253 * (self.state[i-1] ^ (self.state[i-1] &amp;gt;&amp;gt; 30)) + i) % 4294967296&lt;br /&gt;
     &lt;br /&gt;
        def __call__(self):&lt;br /&gt;
            &amp;quot;&amp;quot;&amp;quot;gibt die nächste Zufallszahl im Bereich [0, 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;-1] aus&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
            N, M = self.N, 397&lt;br /&gt;
            &lt;br /&gt;
            # Zustand aktualisieren (neue Zufallszahl ausrechnen)&lt;br /&gt;
            i = self.i&lt;br /&gt;
            r = ((self.state[i] &amp;amp; 0x80000000) | (self.state[(i+1)%N] &amp;amp; 0x7FFFFFFF)) &amp;gt;&amp;gt; 1&lt;br /&gt;
            if self.state[(i+1)%N] &amp;amp; 1:&lt;br /&gt;
                r ^= 0x9908B0DF&lt;br /&gt;
            self.state[i] = self.state[(i+M)%N] ^ r&lt;br /&gt;
     &lt;br /&gt;
            # aktuelle Zufallszahl auslesen und ihre Zufälligkeit durch verwürfeln der Bits verbessern&lt;br /&gt;
            y = self.state[i]&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 11)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt;  7) &amp;amp; 0x9D2C5680)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt; 15) &amp;amp; 0xEFC60000)&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 18)&lt;br /&gt;
            &lt;br /&gt;
            # Zustand weitersetzen und endgültige Zufallszahl ausgeben&lt;br /&gt;
            self.i = (self.i + 1) % N&lt;br /&gt;
            return y&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''geg.:''' Zufallszahl &lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, \overbrace{2^{32}-1}^{m-1}]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ges.:''' Zufallszahl&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, k - 1]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''naive Lösung:'''  &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; rand()%k\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;  ist schlecht.&lt;br /&gt;
&lt;br /&gt;
'''Bsp.'''&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; \qquad m = 16\qquad k = 11\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
! rand() || 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15&lt;br /&gt;
|-&lt;br /&gt;
! rand()%k&lt;br /&gt;
! 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 0 || 1 || 2 || 3 || 4 &lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; 0,...,4 kommt doppelt so häufig wie 5,...,10 &amp;quot;nicht zufällig&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lösung:'''  Zurückweisen des Rests der Zahlen (''rejektion sampling'')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; remainder = (m - 1 - (k - 1))% k = (m - k)%k\\&lt;br /&gt;
        \mathrm{ } &amp;amp; last\ Good\ Value = m-1-remainder\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  r = rand()&lt;br /&gt;
  while r &amp;gt; last.GoodValue:&lt;br /&gt;
        r = rand()&lt;br /&gt;
        return r%k&lt;br /&gt;
&lt;br /&gt;
[[Greedy-Algorithmen und Dynamische Programmierung|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5435</id>
		<title>Randomisierte Algorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5435"/>
				<updated>2012-07-30T11:49:29Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Randomisierte Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
;Definition: Randomisierte Algorithmen sind Algorithmen, die bei Entscheidungen über ihr weiteres Vorgehen oder bei der Wahl ihrer Parameter Zufallszahlen benutzen.&lt;br /&gt;
&lt;br /&gt;
Anschaulich gesprochen, wersucht man bei randomisierten Algorithmen, einen Teil der Lösung zu &amp;lt;i&amp;gt;raten&amp;lt;/i&amp;gt;. Auf den ersten Blick würde man vermuten, dass dabei nicht viel Sinnvolles herauskommen kann. Diese Kapitel wird jedoch zeigen, dass man durch geschicktes Raten tatsächlich zu sehr eleganten Algorithmen gelangen kann.&lt;br /&gt;
&lt;br /&gt;
Grundsätzlich unterscheidet man zwei Arten von randomisierten Algorithmen:&lt;br /&gt;
;Las Vegas - Algorithmen: Das Ergebnis des Algorithmus ist immer korrekt, und die Berechnung erfolgt mit hoher Wahrscheinlichkeit effizient.&lt;br /&gt;
;Monte Carlo - Algorithmen: Die Berechnung ist immer effizient, und das Ergebnis ist mit hoher Wahrscheinlichkeit korrekt.&lt;br /&gt;
Las Vegas-Algorithmen verwendet man, wenn der Algorithmus im ungünstigen Fall eine schlechte Laufzeit hat, und der ungünstige Fall kann durch die Randomisierung sehr unwahrscheinlich gemacht werden. Wir haben in der Vorlesung schon mehrere Las Vegas-Algorithmen kennen gelernt:&lt;br /&gt;
* Quick Sort mit zufälliger Wahl des Pivot-Elements: Die Randomisierung verhindert, dass das Array immer wieder in Subarrays von sehr unterschiedlicher Größe aufgeteilt wird.&lt;br /&gt;
* Treap mit zufälligen Prioritäten: Die Randomisierung verhindert, dass der Baum schlecht balanciert ist.&lt;br /&gt;
* Universelles Hashing: Die zufällige Wahl der Hashfunktion verhindert, dass ein Angreifer eine Schlüsselmenge mit sehr vielen Kollisionen konstruieren kann.&lt;br /&gt;
* Erzeugung einer perfekten Hashfunktion: Durch die Randomisierung entsteht mit nach wenigen Versuchen ein zyklenfreier Graph, der zur Definition der Hashfunktion geeignet ist.&lt;br /&gt;
Monte Carlo-Algorithmen verwendet man dagegen, wenn kein effizienter deterministischer Algorithmus für ein Problem bekannt ist. Man gibt sich dann damit zufrieden, dass der randomisierte Algorithmus die korrekte Lösung nur mit hoher Wahrscheinlichkeit findet, wenn dies dafür sehr effizient geschieht. Bei manchen Problemen ist auch dies unerreichbar - man muss dann bereits zufrieden sein, wenn der Algorithmus mit hoher Wahrscheinlichkeit eine sehr gute Näherungslösung findet. Beliebte Anwendungsgebiete für Monte Carlo-Algorithmen sind beispielsweise&lt;br /&gt;
* Randomisierte Primzahl-Tests: Moderne Verschlüsselungsverfahren benötigen zahlreiche Primzahlen, aber exakte Primzahltests sind teuer. Der [http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin-Test] findet effizient Zahlen, die mit sehr hoher Wahrscheinlichkeit tatsächlich Primzahlen sind.&lt;br /&gt;
* Randomisiertes Testen: Wie jeder Test kann auch eine randomisierter Test nicht die Abwesenheit von Programmierfehlern garantieren, aber man kann durch die Randomisierung viel mehr Testfälle generieren und erhöht so die Erfolgswarscheinlichkeit. Wir haben als Beispiel dafür den [[Korrektheit#Beispiel_f.C3.BCr_das_Testen:_Freivalds_Algorithmus|Algorithmus von Freivald]] behandelt.&lt;br /&gt;
* Lösung schwieriger Optimierungsprobleme: Wir zeigen unten, dass ein randomisierter Algorithmus effizient eine Lösung für das 2-SAT-Problem aus dem vorherigen Kapitel findet (für k-SAT mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; liefert der Algorithmus immer noch mit einer gewissen Wahrscheinlichkeit das richtige Ergebnis, ist aber nicht mehr effizient). Einen effizienten Approximationsalgorithmus für des Problem des Handelsreisenden behandlen wir im Kapitel [[NP-Vollständigkeit]]. Weitere wichtige Beispiele für diesen Bereich sind [http://en.wikipedia.org/wiki/Simulated_annealing simulated annealing] und das [http://de.wikipedia.org/wiki/MCMC-Verfahren Markov-Chain-Monte-Carlo-Verfahren].&lt;br /&gt;
* Robuste Statistik: Eine Grundaufgabe der Statistik ist das Anpassen (Fitten) von Modellen an gemessene Werte. Wenn die Messungen jedoch &amp;quot;Ausreißer&amp;quot; (einige völlig falsche Werte) enthalten, geht die Anpassung schief. Wir beschreiben unten den RANSAC-Algorithmus, der die Ausreißer identifizieren und beim Modellfitten ignorieren kann.&lt;br /&gt;
&lt;br /&gt;
Obwohl randomisierte Algorithmen oft einfach und elegant sind, ist ihre theoretische Analyse (also das Führen von Korrektheits- und Komplexitätsbeweisen) häufig sehr schwierig. Man muss fortgeschrittene Methoden der Wahrscheinlichkeitsrechnung und Statistik beherrschen, um die Wahrscheinlichkeit für das Versagen des Algorithmus zu berechnen und um zu zeigen, wie man den Algorithmus benutzt, damit diese Wahrscheinlichkeit unter einer akzeptablen Schranke bleibt. Die Algorithmen, die wir für diese Vorlesung ausgewählt haben, zeichnen sich dadurch aus, dass die Beweise hier einfach zu erbringen sind.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Lösen des K-SAT-Problems ===&lt;br /&gt;
&lt;br /&gt;
Der &amp;lt;b&amp;gt;Algorithmus von Schöning&amp;lt;/b&amp;gt; löst das [[Graphen_und_Graphenalgorithmen#Normalformen für logische Ausdrücke|k-SAT-Problem]] durch Raten: Wenn ein Ausdruck in k-CNF den Wert False hat, gibt es mindestens eine Klausel, die den Wert False hat. Alle Literale in dieser Klausel haben ebenfalls den Wert False, denn jede Klausel ist eine ODER-Verknüpfung, die nur dann False werden kann. Um den Ausdruck zu erfüllen, muss jede Klausel den Wert True annehmen, also müssen wir den Wert von mindestens einem Literal umdrehen. Wenn der Ausruck tatsächlich erfüllbar ist, gibt es immer ein geeignetes Literal, wir wissen nur nicht, welches. Deshalb drehen wir ein unter den k Literalen der betreffenden Klausel zufällig gewähltes. Liegen wir mit unserer Wahl richtig, sind wir der Lösung näher gekommen - im besten Fall sind jetzt alle Klauseln erfüllt. Wählen wir jedoch die falsche Variable, ist die aktuelle Klausel zwar jetzt True, aber dafür werden andere Klauseln zu False, die bisher True waren, und wir entfernen uns somit von der Lösung.&lt;br /&gt;
&lt;br /&gt;
    geg.: logischer Ausdruck in K-CNF (n Variablen, m Klauseln, k Variablen pro Klausel)&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;math&amp;gt;\underbrace {\underbrace {\left(x_1 \vee x_3 \vee...\right)}_{k\; Literale} \wedge \left( x_2 \vee x_4 \vee...\right)}_{m\;Klauseln}&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Algorithmus von Schöning lautet in Pseudocode:&lt;br /&gt;
    for i in range (trials):    #Anzahl der Versuche&lt;br /&gt;
         Bestimme eine Zufallsbelegung der Variablen &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
         for j in range (steps):&lt;br /&gt;
               if &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt; erfüllt alle Klauseln: &lt;br /&gt;
                   return &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
               wähle zufällig eine Klausel, die nicht erfüllt ist und negiere zufällig eine der Variablen in dieser Klausel &lt;br /&gt;
               # (die Klausel ist jetzt erfüllt)&lt;br /&gt;
    return None  # keine Lösung gefunden&lt;br /&gt;
&lt;br /&gt;
Findet der Algorithmus eine Lösung, wissen wir, dass der Ausdruck erfüllbar ist. Andernfalls könnte der Ausdruck unerfüllbar sein, oder wir haben nur Pech gehabt. Je mehr erfolglose Versuche wir machen, desto höher ist die Wahrscheinlichkeit, dass das erste zutrifft.&lt;br /&gt;
&lt;br /&gt;
Es ist sinnvoll, &amp;lt;tt&amp;gt;steps = k*n&amp;lt;/tt&amp;gt; zu wählen. Dann gilt der &lt;br /&gt;
;Satz: Wenn ein Ausdruck in k-CNF mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; erfüllbar ist, muss man im Mittel &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in O\left(\left(\frac{2(k-1)}{k}\right)^n \right)&amp;lt;/math&amp;gt; Versuche machen, um eine Lösung zu finden.&lt;br /&gt;
&lt;br /&gt;
Für &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; gilt stets &amp;lt;math&amp;gt;\frac{2(k-1)}{k} &amp;gt; 1&amp;lt;/math&amp;gt;, man benötigt also eine in n exponentielle Anzahl von Versuchen. Bei &amp;lt;math&amp;gt;k=3&amp;lt;/math&amp;gt; gilt z.B. &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt; \in O\left(\left(\frac{4}{3}\right)^n\right)&amp;lt;/math&amp;gt;. Dies ist zwar im Mittel effizienter also die erschöpfende Suche, die &amp;lt;math&amp;gt;O(2^n)&amp;lt;/math&amp;gt; Schritte benötigt, aber immer noch sehr langsam.&lt;br /&gt;
&lt;br /&gt;
Der Fall &amp;lt;b&amp;gt;&amp;lt;math&amp;gt;k=2&amp;lt;/math&amp;gt; ist jedoch ein Sonderfall&amp;lt;/b&amp;gt;: Hier kann man leicht beweisen, dass eine Lösung im Mittel bereits nach &amp;lt;math&amp;gt;O\left(n^2\right)&amp;lt;/math&amp;gt; Schritten gefunden wird. Wenn man schon weiss, dass der Ausdruck erfüllbar ist (was mit [[Graphen_und_Graphenalgorithmen#Lösung des 2-SAT-Problems mit Implikationgraphen|Implikationgraphen]] leicht geprüft werden kann), lässt man den randomisierten Algorithmus einfach so lange laufen, bis er eine Lösung findet. Man setzt also &amp;lt;tt&amp;gt;step = infinity&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;trials = 1&amp;lt;/tt&amp;gt; und verlässt sich darauf, dass das &amp;lt;tt&amp;gt;return&amp;lt;/tt&amp;gt; mit einer gültigen Lösung früher oder später ausgeführt wird. Dass man darauf im Mittel nur &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte warten muss, zeigen wir jetzt mit Hilfe eines &amp;lt;i&amp;gt;random walk&amp;lt;/i&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk====&lt;br /&gt;
&lt;br /&gt;
Um die Random Walk Analyse zu verstehen, betrachten wir folgendes Spiel:&lt;br /&gt;
&lt;br /&gt;
   geg.: eine Stuhlreihe mit N Stühlen. Wir nummerieren die Stühle so, dass links der Stuhl 0 und rechts der Stuhl N steht.&lt;br /&gt;
   &lt;br /&gt;
   * Eine Person setzt sich zufällig auf einen der Stühle.&lt;br /&gt;
   * Eine zweite Person wirft eine Münze.&lt;br /&gt;
   &lt;br /&gt;
         Wenn die Münze auf Zahl fällt, rückt die erste Person einen Stuhl nach links, andernfalls nach rechts.&lt;br /&gt;
         &amp;lt;--- Zahl                                                                                    Kopf ---&amp;gt;&lt;br /&gt;
&lt;br /&gt;
   * Frage: Wie oft muss man die Münze im Durchschnitt werfen, bis Person 1 zum ersten Mal auf Stuhl N sitzt?&lt;br /&gt;
&lt;br /&gt;
Da die erste Person sich anfangs zufällig hinsetzt, haben wir eine Chance von 1/N, dass sie gleich auf dem richtigen Stuhl landet und wir 0 Schritte benötigen. Mit der gleichen Wahrscheinlichkeit von 1/N setzt sie sich anfangs auf Stuhl Nummer (N-1), und wir haben eine fifty-fifty-Chance, mit nur einem Wurf durchzukommen. Wir können aber auch Pech haben und landen auf Stuhl Nummer (N-2). Das ist das Gleiche, als wenn Person 1 von Anfang an auf diesem Stuhl gesessen hätte, nur dass wir jetzt bereits einen Wurf verbraucht haben. Man sieht, dass man die Zahl der Restwürfe immer in dieser Art ausdrücken kann: Sitzt Person 1 auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, kann sie entweder nach rechts rücken und benötigt dann noch soviele Würfe, wie man typischerweise für Stuhl &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; benötigt, plus den Wurf von &amp;lt;tt&amp;gt;i =&amp;gt; i+1&amp;lt;/tt&amp;gt;. Oder sie kann nach links rücken und benötigt dann die typische Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; plus den Wurf &amp;lt;tt&amp;gt;i =&amp;gt; i-1&amp;lt;/tt&amp;gt;. Beide Möglichkeiten haben die Wahrscheinlichkeit 1/2. Mathematisch kann man dies elegant als Rekursionsformel schreiben, die die erwartete Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; als Funktion der entsprechenden Wurfzahlen für die Stühle &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; ausdrückt:&lt;br /&gt;
&lt;br /&gt;
* Wenn wir uns auf Stuhl Nr.1 befinden, werfen wir gar nicht: &amp;lt;math&amp;gt;W\left(N\right)=0&amp;lt;/math&amp;gt;&lt;br /&gt;
* Von Stuhl 0 gehen wir immer zu Stuhl 1: &amp;lt;math&amp;gt;W\left(0\right)=1 + W\left(1\right)&amp;lt;/math&amp;gt;&lt;br /&gt;
* Allgemeiner Fall: &amp;lt;math&amp;gt;W\left(i\right)=\frac 1 2 \left(1 + W\left(i+1\right)\right) + \frac 1 2 \left(1 + W\left(i-1\right)\right) = \frac 1 2 W\left(i+1\right) + \frac 1 2 W\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
Diese Rekursion wird durch die explizite Formel&lt;br /&gt;
::&amp;lt;math&amp;gt;W\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
gelöst, wie man durch Einsetzen leicht nachprüft:&lt;br /&gt;
::&amp;lt;math&amp;gt;&lt;br /&gt;
       \begin{align} &lt;br /&gt;
             W\left(N\right) &amp;amp; = N^2-N^2=0 \\&lt;br /&gt;
                  &lt;br /&gt;
             W\left(0\right) &amp;amp;= W\left(1\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-1^2+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2 - 0^2\\&lt;br /&gt;
                 &lt;br /&gt;
              W\left(i\right) &amp;amp;= \frac 1 2 \left(N^2-\left(i-1\right)^2\right) + \frac 1 2 \left(N^2-\left(i+1\right)^2\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= \frac 1 2 N^2-\frac 1 2 \left( i^2-2i+1\right) + \frac 1 2 N^2-\frac 1 2 \left(i^2+2i+1\right) + 1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-i^2&lt;br /&gt;
       \end{align}&amp;lt;/math&amp;gt;&lt;br /&gt;
Insbesondere braucht man im ungünstigen Fall (Start auf Stuhl 0) im Durchschnitt &amp;lt;math&amp;gt;N^2&amp;lt;/math&amp;gt; Würfe, im typischen Fall (start in der Mitte, also bei &amp;lt;math&amp;gt;i = N/2&amp;lt;/math&amp;gt;) im Durchschnitt &lt;br /&gt;
:&amp;lt;math&amp;gt;N^2 - (N/2)^2=\frac 3 4 N^2\in O(N^2)&amp;lt;/math&amp;gt; &lt;br /&gt;
Würfe. Die '''Beziehung zum randomisiertem 2-SAT-Algorithmus''' ist jetzt leicht zu erkennen. Sitzt die Person auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, interpretieren wir das als:&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Stuhl &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt;&amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert, &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
Wählt der Algorithmus eine Klausel, die nicht erfüllt ist, gibt es zwei Möglichkeiten:&lt;br /&gt;
# Beide Literale in der Klausel haben den falschen Wert: Die Lösung wird auf jeden Fall besser, egal welche der beiden wir umdrehen. Wir gehen also von Zustand &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
# Nur eins der Literale hat den falschen Wert: Beim Umdrehen haben wir eine fifty-fifty-Chance, das richtige Literal zu wählen und in den Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; zu gelangen. Mit der selben Wahrscheinlichkeit wählen wir das falsche Literal und landen im Zustand &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
Falls 2 ist der ungünstigere und entspricht unserem Spiel, dessen Analyse wir deshalb einfach auf das 2-SAT-Problem übertragen können: Ziel des Algorithmus ist es, in den Zustand N zu gelangen, und deshalb gilt genau wie beim Spiel der &lt;br /&gt;
;Satz: Der randomisierte 2-SAT-Algorithmus findet im Durchschnitt nach &amp;lt;math&amp;gt;O(N^2)&amp;lt;/math&amp;gt; Versuchen eine Lösung, wenn das Problem erfüllbar ist.&lt;br /&gt;
Damit ist der randomisierte Algorithmus für dieses Problem effizient, was Sie in Übung 12 experimentell nachprüfen sollen.&lt;br /&gt;
&lt;br /&gt;
== 2. RANSAC-ALGORITHMUS (Random Sample Consensus)==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''Aufgabe:''&amp;lt;/u&amp;gt; gegeben: Datenpunkte&lt;br /&gt;
::gesucht: Modell, das die Datenpunkte erklärt&lt;br /&gt;
&lt;br /&gt;
[[Image:Rubto.png|thumb|250px|none]]&lt;br /&gt;
&lt;br /&gt;
'''Messpunkte:'''&lt;br /&gt;
   &lt;br /&gt;
      übliche Lösung: Methode der kleinsten Quadrate&lt;br /&gt;
      &lt;br /&gt;
      &amp;lt;math&amp;gt;\min_{a,b} 	\sum_{i} \left(a x_i + b + y_i\right)^2&amp;lt;/math&amp;gt;&lt;br /&gt;
      &lt;br /&gt;
      Schulmathematik:      &amp;lt;math&amp;gt;Minimum\stackrel{\wedge}{=}Ableitung=0&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lineares Gleichungssystem'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{da}\sum{i} \left(ax_i+b-y_i\right)^2=\sum{i} \frac{d}{da} \left[ax_i+b-y_i\right)^2&amp;lt;/math&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(g\left(x\right)\right)&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(x\right)=x^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;y\left(a\right)=ax_i+b-y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;=\sum_{i}2\left(ax_i+b-y_i\right)\frac{d}{da} \underbrace {ax_i+b-y_i}_{x_i}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\underline {=2\sum_{i}\left(ax_i+b-y_i\right)x_i\stackrel{!}{=}0}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}{x_i}^2+b\sum_{i}x_i=\sum_{i}x_iy_i&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}x_i+b\sum_{i}1=\sum_{i}y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{db}\sum_{i}\left(ax_i+b-y_i\right)^2=2\sum_{i}\left(ax_i+b-y_i\right)*1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:Problem: &amp;lt;math&amp;gt;\epsilon  %&amp;lt;/math&amp;gt; der Datenpunkte sind Outlier&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\Longrightarrow&amp;lt;/math&amp;gt; Einfaches Anpassen des Modells an die Datenpunkte funktioniert nicht&lt;br /&gt;
&lt;br /&gt;
:Seien mindestens k Datenpunkte notwendig, um das Programm anpassen zu können&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
RANSAC-Algorithmus&lt;br /&gt;
&lt;br /&gt;
      for  l in range (trials):&lt;br /&gt;
           wähle zufällig k Punkte aus&lt;br /&gt;
           passe das Modell an die k Punkte an&lt;br /&gt;
           zähle, wieviele Punkte in der Nähe des Modells liegen (d.h. &amp;lt;math&amp;gt;d_i &amp;lt; d_max&amp;lt;/math&amp;gt; muss geschickt gewählt werden) &lt;br /&gt;
                                           #Bsp. Geradenfinden:-wähle a,b aus zwei Punkten&lt;br /&gt;
                                                               -berechne: &amp;lt;math&amp;gt;|ax_i+b-y_i|=d_i&amp;lt;/math&amp;gt;&lt;br /&gt;
                                                               -zähle Punkt i als Inlier, falls &amp;lt;math&amp;gt;d_i&amp;lt;d_ma&amp;lt;/math&amp;gt;&lt;br /&gt;
      return: Modell mit höchster Zahl der Inlier&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;trials= \frac{log\left(1-p\right)}{log\left(1-\left(1-\epsilon\right)^k\right)}&amp;lt;/math&amp;gt;  mit k=Anzahl der Datenpunkte und p=Erfolgswahrscheinlichkeit, &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;=Outlier-Anteil&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Erfolgswahrscheinlichkeit: p=99%'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{|c||c|c|c|c|c|}&lt;br /&gt;
         Beispiel &amp;amp; k &amp;amp; \epsilon=10% &amp;amp; 20% &amp;amp; 50% &amp;amp; 70%\\&lt;br /&gt;
         \hline&lt;br /&gt;
         Linie\;in\;2D &amp;amp; 2 &amp;amp; 3 &amp;amp;5 &amp;amp; 17 &amp;amp; 49\\&lt;br /&gt;
         Kreis\;in\;2D &amp;amp; 3 &amp;amp; 4 &amp;amp; 7 &amp;amp; 35 &amp;amp; 169\\&lt;br /&gt;
         Ebene\;in\;3D &amp;amp; 8 &amp;amp; 9 &amp;amp; 26 &amp;amp; 1172 &amp;amp; 70188\\&lt;br /&gt;
       \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Ein Spiel: Wie viel Schritte braucht man im Mittel zum Ziel?'''&lt;br /&gt;
&lt;br /&gt;
   geg.: 5 Plätze, 2 Personen: eine Person rückt vom einem Platz zu dem enderen Platz;&lt;br /&gt;
         die zweite Person wirft die Münze.&lt;br /&gt;
         Wenn die Münze auf Kopf landet, rücke nach rechts und wenn die Münze auf Zahl landet, rücke nach links.&lt;br /&gt;
         &amp;lt;--- Zahl                                                         Kopf--&amp;gt;&lt;br /&gt;
         Kopf: /////&lt;br /&gt;
         Zahl: /// &lt;br /&gt;
&lt;br /&gt;
:: =&amp;gt; mit 8 Schritten bis zum Ziel&lt;br /&gt;
:im Mittel: bei N Plätzen braucht man N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritte&lt;br /&gt;
&lt;br /&gt;
: all: mit N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritten um N Plätze rücken&lt;br /&gt;
: Wie viel Schritte braucht man im Mittel zum Ziel?&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(N\right)=0&amp;lt;/math&amp;gt;    #wenn wir uns im Stuhl Nr.1 befinden&lt;br /&gt;
           &lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)=\frac 1 2 S\left(1 + S\left(i+1\right)\right) + \frac 1 2 S\left(1 + S\left(i-1\right)\right) = \frac 1 2 S\left(i+1\right) + \frac 1 2 S\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(0\right)=1 + S\left(1\right)&amp;lt;/math&amp;gt;    #bei 0.Platz&lt;br /&gt;
&lt;br /&gt;
:::*Lösung: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:::*speziell: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2&amp;lt;/math&amp;gt;           #wenn man am ungünstigsten Platz startet&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
'''Beziehung zu randomisiertem 2-SAT'''&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Platz &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; &amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert,  &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)=N^2 - \left(\frac N 2\right)^2 = N^2 - \frac N 4 ^2 = \frac 3 4 N^2 &amp;lt;/math&amp;gt;&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)&amp;lt;/math&amp;gt;     # Anfangszustand&lt;br /&gt;
----&lt;br /&gt;
== '''Las Vegas vs. Monte Carlo'''==&lt;br /&gt;
&lt;br /&gt;
   * ''Las Vegas - Algorithmen''&lt;br /&gt;
     - Ergebnis ist immer korrekt.&lt;br /&gt;
     - Berechnung ist mit hoher Wahrscheinlichkeit effizient (d.h. Randomisierung macht den ungünstigsten Fall unwahrscheinlich).&lt;br /&gt;
&lt;br /&gt;
   * ''Monte Carlo - Algorithmen''&lt;br /&gt;
     - Berechnung immer effizient.&lt;br /&gt;
     - Ergebnis mit hoher Wahrscheinlichkeit korrekt (falls kein effizienter Algorithmus bekannt, der immer die richtige Lösung liefert).&lt;br /&gt;
&lt;br /&gt;
{| border = &amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;center&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! Las Vegas&lt;br /&gt;
! Monte Carlo&lt;br /&gt;
|- &lt;br /&gt;
| - Erzeugen einer perfekten Hashfuktion &lt;br /&gt;
| - Algorithmus von Freiwald(Matrizenmultiplikation)&lt;br /&gt;
|-&lt;br /&gt;
| - universelles Hashing&lt;br /&gt;
| - RANSAC&lt;br /&gt;
|-&lt;br /&gt;
| - Quick Sort mit zufälliger Wahl des Pivot-Elements&lt;br /&gt;
| - randomisierte K-SAT(k&amp;gt;=3)(Alg. von Schöning)&lt;br /&gt;
|-&lt;br /&gt;
| - Treep mit zufälligen Prioritäten&lt;br /&gt;
| -&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== ''' Zufallszahlen ''' ==&lt;br /&gt;
&lt;br /&gt;
:- kann man nicht mit deterministischen Computern erzeugen&lt;br /&gt;
:- aber man kann Pseudo-Zufallszahlen erzeugen, die viele Eigenschaften von echten Zufallszahlen haben&lt;br /&gt;
::: * sehr ähnlich  zum Hash&lt;br /&gt;
&lt;br /&gt;
     ''&amp;quot;linear Conguential Random number generator&amp;quot;''&lt;br /&gt;
        &amp;lt;math&amp;gt;I_{i+1}= \left(a*I_i + c\right)\textrm{mod\ } m&amp;lt;/math&amp;gt;&lt;br /&gt;
        &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{=&amp;gt; } &amp;amp; I_i \in [0, m-1]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:-sorgfältige Wahl von  a, c, m notwendig&lt;br /&gt;
::'''Bsp.'''  m = 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;&lt;br /&gt;
::: a = 1664525, c = 1013904223&lt;br /&gt;
::: ''&amp;quot;quick and dirty generator&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
==='''Nachteile'''===&lt;br /&gt;
&lt;br /&gt;
* nicht zufällig genug für viele Anwendungen&lt;br /&gt;
::'''Bsp.''' wähle Punkt in R&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
      \mathrm{ } &amp;amp; p = (rand(), rand(), rand())\\&lt;br /&gt;
&lt;br /&gt;
      \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::gibt Zahl u, v, w so, dass &lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; u * p[0] + v * p[1] + w * p[3]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::stark geclustert ist.&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge ist zu kurz:&lt;br /&gt;
:: spätestens nach m Schritten wiederholt sich die Folge&lt;br /&gt;
&lt;br /&gt;
::'''allgemein''': falls der interne Zustand des Zufallsgenerators ''k'' bits hat, ist Periodenlänge:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; Periode &amp;lt; 2^k\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* ''lowbits'' sind weniger zufällig als die ''highbits''&lt;br /&gt;
----&lt;br /&gt;
=== ''Mersenne Twister''===&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
'''bester zur Zeit bekannter Zufallszahlengenerator (ZZG)'''&lt;br /&gt;
* innere Zustand: &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; 624*32 bit\ Integers  =&amp;gt; 19968 bits\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge: &amp;lt;math&amp;gt;2^ {19937} \approx 4 * 10^{6000}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Punkte aus aufeinanderfolgende Zufallszahlen in &amp;lt;math&amp;gt;\mathbb{R}^n&amp;lt;/math&amp;gt; sind gleich verteilt bis &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; n = 623\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* alle Bits sind unabhängig voneinander zufällig (&amp;quot;Twister&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
* schnell&lt;br /&gt;
&lt;br /&gt;
    class MersenneTwister:&lt;br /&gt;
        &lt;br /&gt;
        def __init__(self, seed):&lt;br /&gt;
            self.N = 624  # Größe des inneren Zustands festlegen&lt;br /&gt;
            self.i = 0    # zählt mit in welchem Zustand wir uns gerade aufhalten&lt;br /&gt;
            &lt;br /&gt;
            self.state = [0]*self.N  # Speicher für den inneren Zustand reservieren&lt;br /&gt;
            &lt;br /&gt;
            self.state[0] = seed     # initiale Zufallszahl vom Benutzer&lt;br /&gt;
            # den Rest des inneren Zustands mit einfachem Zufallszahlengenerator initialisieren&lt;br /&gt;
            for i in xrange(1, self.N):&lt;br /&gt;
                self.state[i] = (1812433253 * (self.state[i-1] ^ (self.state[i-1] &amp;gt;&amp;gt; 30)) + i) % 4294967296&lt;br /&gt;
     &lt;br /&gt;
        def __call__(self):&lt;br /&gt;
            &amp;quot;&amp;quot;&amp;quot;gibt die nächste Zufallszahl im Bereich [0, 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;-1] aus&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
            N, M = self.N, 397&lt;br /&gt;
            &lt;br /&gt;
            # Zustand aktualisieren (neue Zufallszahl ausrechnen)&lt;br /&gt;
            i = self.i&lt;br /&gt;
            r = ((self.state[i] &amp;amp; 0x80000000) | (self.state[(i+1)%N] &amp;amp; 0x7FFFFFFF)) &amp;gt;&amp;gt; 1&lt;br /&gt;
            if self.state[(i+1)%N] &amp;amp; 1:&lt;br /&gt;
                r ^= 0x9908B0DF&lt;br /&gt;
            self.state[i] = self.state[(i+M)%N] ^ r&lt;br /&gt;
     &lt;br /&gt;
            # aktuelle Zufallszahl auslesen und ihre Zufälligkeit durch verwürfeln der Bits verbessern&lt;br /&gt;
            y = self.state[i]&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 11)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt;  7) &amp;amp; 0x9D2C5680)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt; 15) &amp;amp; 0xEFC60000)&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 18)&lt;br /&gt;
            &lt;br /&gt;
            # Zustand weitersetzen und endgültige Zufallszahl ausgeben&lt;br /&gt;
            self.i = (self.i + 1) % N&lt;br /&gt;
            return y&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''geg.:''' Zufallszahl &lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, \overbrace{2^{32}-1}^{m-1}]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ges.:''' Zufallszahl&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, k - 1]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''naive Lösung:'''  &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; rand()%k\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;  ist schlecht.&lt;br /&gt;
&lt;br /&gt;
'''Bsp.'''&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; \qquad m = 16\qquad k = 11\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
! rand() || 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15&lt;br /&gt;
|-&lt;br /&gt;
! rand()%k&lt;br /&gt;
! 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 0 || 1 || 2 || 3 || 4 &lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; 0,...,4 kommt doppelt so häufig wie 5,...,10 &amp;quot;nicht zufällig&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lösung:'''  Zurückweisen des Rests der Zahlen (''rejektion sampling'')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; remainder = (m - 1 - (k - 1))% k = (m - k)%k\\&lt;br /&gt;
        \mathrm{ } &amp;amp; last\ Good\ Value = m-1-remainder\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  r = rand()&lt;br /&gt;
  while r &amp;gt; last.GoodValue:&lt;br /&gt;
        r = rand()&lt;br /&gt;
        return r%k&lt;br /&gt;
&lt;br /&gt;
[[Greedy-Algorithmen und Dynamische Programmierung|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5433</id>
		<title>Randomisierte Algorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5433"/>
				<updated>2012-07-27T18:53:28Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Randomisierte Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
;Definition: Randomisierte Algorithmen sind Algorithmen, die bei Entscheidungen über ihr weiteres Vorgehen oder bei der Wahl ihrer Parameter Zufallszahlen benutzen.&lt;br /&gt;
&lt;br /&gt;
Anschaulich gesprochen, wersucht man bei randomisierten Algorithmen, einen Teil der Lösung zu &amp;lt;i&amp;gt;raten&amp;lt;/i&amp;gt;. Auf den ersten Blick würde man vermuten, dass dabei nicht viel Sinnvolles herauskommen kann. Diese Kapitel wird jedoch zeigen, dass man durch geschicktes Raten tatsächlich zu sehr eleganten Algorithmen gelangen kann.&lt;br /&gt;
&lt;br /&gt;
Grundsätzlich unterscheidet man zwei Arten von randomisierten Algorithmen:&lt;br /&gt;
;Las Vegas - Algorithmen: Das Ergebnis des Algorithmus ist immer korrekt, und die Berechnung erfolgt mit hoher Wahrscheinlichkeit effizient.&lt;br /&gt;
;Monte Carlo - Algorithmen: Die Berechnung ist immer effizient, und das Ergebnis ist mit hoher Wahrscheinlichkeit korrekt.&lt;br /&gt;
Las Vegas-Algorithmen verwendet man, wenn der Algorithmus im ungünstigen Fall eine schlechte Laufzeit hat, und der ungünstige Fall kann durch die Randomisierung sehr unwahrscheinlich gemacht werden. Wir haben in der Vorlesung schon mehrere Las Vegas-Algorithmen kennen gelernt:&lt;br /&gt;
* Quick Sort mit zufälliger Wahl des Pivot-Elements: Die Randomisierung verhindert, dass das Array immer wieder in Subarrays von sehr unterschiedlicher Größe aufgeteilt wird.&lt;br /&gt;
* Treap mit zufälligen Prioritäten: Die Randomisierung verhindert, dass der Baum schlecht balanciert ist.&lt;br /&gt;
* Universelles Hashing: Die zufällige Wahl der Hashfunktion verhindert, dass ein Angreifer eine Schlüsselmenge mit sehr vielen Kollisionen konstruieren kann.&lt;br /&gt;
* Erzeugung einer perfekten Hashfunktion: Durch die Randomisierung entsteht mit nach wenigen Versuchen ein zyklenfreier Graph, der zur Definition der Hashfunktion geeignet ist.&lt;br /&gt;
Monte Carlo-Algorithmen verwendet man dagegen, wenn kein effizienter deterministischer Algorithmus für ein Problem bekannt ist. Man gibt sich dann damit zufrieden, dass der randomisierte Algorithmus die korrekte Lösung nur mit hoher Wahrscheinlichkeit findet, wenn dies dafür sehr effizient geschieht. Bei manchen Problemen ist auch dies unerreichbar - man muss dann bereits zufrieden sein, wenn der Algorithmus mit hoher Wahrscheinlichkeit eine sehr gute Näherungslösung findet. Beliebte Anwendungsgebiete für Monte Carlo-Algorithmen sind beispielsweise&lt;br /&gt;
* Randomisierte Primzahl-Tests: Moderne Verschlüsselungsverfahren benötigen zahlreiche Primzahlen, aber exakte Primzahltests sind teuer. Der [http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin-Test] findet effizient Zahlen, die mit sehr hoher Wahrscheinlichkeit tatsächlich Primzahlen sind.&lt;br /&gt;
* Randomisiertes Testen: Wie jeder Test kann auch eine randomisierter Test nicht die Abwesenheit von Programmierfehlern garantieren, aber man kann durch die Randomisierung viel mehr Testfälle generieren und erhöht so die Erfolgswarscheinlichkeit. Wir haben als Beispiel dafür den [[Korrektheit#Beispiel_f.C3.BCr_das_Testen:_Freivalds_Algorithmus|Algorithmus von Freivald]] behandelt.&lt;br /&gt;
* Lösung schwieriger Optimierungsprobleme: Wir zeigen unten, dass ein randomisierter Algorithmus effizient eine Lösung für das 2-SAT-Problem aus dem vorherigen Kapitel findet (für k-SAT mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; liefert der Algorithmus immer noch mit einer gewissen Wahrscheinlichkeit das richtige Ergebnis, ist aber nicht mehr effizient). Einen effizienten Approximationsalgorithmus für des Problem des Handelsreisenden behandlen wir im Kapitel [[NP-Vollständigkeit]]. Weitere wichtige Beispiele für diesen Bereich sind [http://en.wikipedia.org/wiki/Simulated_annealing simulated annealing] und das [http://de.wikipedia.org/wiki/MCMC-Verfahren Markov-Chain-Monte-Carlo-Verfahren].&lt;br /&gt;
* Robuste Statistik: Eine Grundaufgabe der Statistik ist das Anpassen (Fitten) von Modellen an gemessene Werte. Wenn die Messungen jedoch &amp;quot;Ausreißer&amp;quot; (einige völlig falsche Werte) enthalten, geht die Anpassung schief. Wir beschreiben unten den RANSAC-Algorithmus, der die Ausreißer identifizieren und beim Modellfitten ignorieren kann.&lt;br /&gt;
&lt;br /&gt;
Obwohl randomisierte Algorithmen oft einfach und elegant sind, ist ihre theoretische Analyse (also das Führen von Korrektheits- und Komplexitätsbeweisen) häufig sehr schwierig. Man muss fortgeschrittene Methoden der Wahrscheinlichkeitsrechnung und Statistik beherrschen, um die Wahrscheinlichkeit für das Versagen des Algorithmus zu berechnen und um zu zeigen, wie man den Algorithmus benutzt, damit diese Wahrscheinlichkeit unter einer akzeptablen Schranke bleibt. Die Algorithmen, die wir für diese Vorlesung ausgewählt haben, zeichnen sich dadurch aus, dass die Beweise hier einfach zu erbringen sind.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Lösen des K-SAT-Problems ===&lt;br /&gt;
&lt;br /&gt;
Der &amp;lt;b&amp;gt;Algorithmus von Schöning&amp;lt;/b&amp;gt; löst das [[Graphen_und_Graphenalgorithmen#Normalformen für logische Ausdrücke|k-SAT-Problem]] durch Raten: Wenn ein Ausdruck in k-CNF den Wert False hat, gibt es mindestens eine Klausel, die den Wert False hat. Alle Literale in dieser Klausel haben ebenfalls den Wert False, denn jede Klausel ist eine ODER-Verknüpfung, die nur dann False werden kann. Um den Ausdruck zu erfüllen, muss jede Klausel den Wert True annehmen, also müssen wir den Wert von mindestens einem Literal umdrehen. Wenn der Ausruck tatsächlich erfüllbar ist, gibt es immer ein geeignetes Literal, wir wissen nur nicht, welches. Deshalb drehen wir ein unter den k Literalen der betreffenden Klausel zufällig gewähltes. Liegen wir mit unserer Wahl richtig, sind wir der Lösung näher gekommen - im besten Fall sind jetzt alle Klauseln erfüllt. Wählen wir jedoch die falsche Variable, ist die aktuelle Klausel zwar jetzt True, aber dafür werden andere Klauseln zu False, die bisher True waren, und wir entfernen uns somit von der Lösung.&lt;br /&gt;
&lt;br /&gt;
    geg.: logischer Ausdruck in K-CNF (n Variablen, m Klauseln, k Variablen pro Klausel)&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;math&amp;gt;\underbrace {\underbrace {\left(x_1 \vee x_3 \vee...\right)}_{k\; Literale} \wedge \left( x_2 \vee x_4 \vee...\right)}_{m\;Klauseln}&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Algorithmus von Schöning lautet in Pseudocode:&lt;br /&gt;
    for i in range (trials):    #Anzahl der Versuche&lt;br /&gt;
         Bestimme eine Zufallsbelegung der Variablen &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
         for j in range (steps):&lt;br /&gt;
               if &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt; erfüllt alle Klauseln: &lt;br /&gt;
                   return &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
               wähle zufällig eine Klausel, die nicht erfüllt ist und negiere zufällig eine der Variablen in dieser Klausel &lt;br /&gt;
               # (die Klausel ist jetzt erfüllt)&lt;br /&gt;
    return None  # keine Lösung gefunden&lt;br /&gt;
&lt;br /&gt;
Findet der Algorithmus eine Lösung, wissen wir, dass der Ausdruck erfüllbar ist. Andernfalls könnte der Ausdruck unerfüllbar sein, oder wir haben nur Pech gehabt. Je mehr erfolglose Versuche wir machen, desto höher ist die Wahrscheinlichkeit, dass das erste zutrifft.&lt;br /&gt;
&lt;br /&gt;
Es ist sinnvoll, &amp;lt;tt&amp;gt;steps = k*n&amp;lt;/tt&amp;gt; zu wählen. Dann gilt der &lt;br /&gt;
;Satz: Wenn ein Ausdruck in k-CNF mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; erfüllbar ist, muss man im Mittel &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in O\left(\left(\frac{2(k-1)}{k}\right)^n \right)&amp;lt;/math&amp;gt; Versuche machen, um eine Lösung zu finden.&lt;br /&gt;
&lt;br /&gt;
Für &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; gilt stets &amp;lt;math&amp;gt;\frac{2(k-1)}{k} &amp;gt; 1&amp;lt;/math&amp;gt;, man benötigt also eine in n exponentielle Anzahl von Versuchen. Bei &amp;lt;math&amp;gt;k=3&amp;lt;/math&amp;gt; gilt z.B. &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt; \in O\left(\left(\frac{4}{3}\right)^n\right)&amp;lt;/math&amp;gt;. Dies ist zwar im Mittel effizienter also die erschöpfende Suche, die &amp;lt;math&amp;gt;O(2^n)&amp;lt;/math&amp;gt; Schritte benötigt, aber immer noch sehr langsam.&lt;br /&gt;
&lt;br /&gt;
Der Fall &amp;lt;b&amp;gt;&amp;lt;math&amp;gt;k=2&amp;lt;/math&amp;gt; ist jedoch ein Sonderfall&amp;lt;/b&amp;gt;: Hier kann man leicht beweisen, dass eine Lösung im Mittel bereits nach &amp;lt;math&amp;gt;O\left(n^2\right)&amp;lt;/math&amp;gt; Schritten gefunden wird. Wenn man schon weiss, dass der Ausdruck erfüllbar ist (was mit [[Graphen_und_Graphenalgorithmen#Lösung des 2-SAT-Problems mit Implikationgraphen|Implikationgraphen]] leicht geprüft werden kann), lässt man den randomisierten Algorithmus einfach so lange laufen, bis er eine Lösung findet. Man setzt also &amp;lt;tt&amp;gt;step = infinity&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;trials = 1&amp;lt;/tt&amp;gt; und verlässt sich darauf, dass das &amp;lt;tt&amp;gt;return&amp;lt;/tt&amp;gt; mit einer gültigen Lösung früher oder später ausgeführt wird. Dass man darauf im Mittel nur &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte warten muss, zeigen wir jetzt mit Hilfe eines &amp;lt;i&amp;gt;random walk&amp;lt;/i&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk====&lt;br /&gt;
&lt;br /&gt;
Um die Random Walk Analyse zu verstehen, betrachten wir folgendes Spiel:&lt;br /&gt;
&lt;br /&gt;
   geg.: eine Stuhlreihe mit N Stühlen. Wir nummerieren die Stühle so, dass links der Stuhl 0 und rechts der Stuhl N steht.&lt;br /&gt;
   &lt;br /&gt;
   * Eine Person setzt sich zufällig auf einen der Stühle.&lt;br /&gt;
   * Eine zweite Person wirft eine Münze.&lt;br /&gt;
   &lt;br /&gt;
         Wenn die Münze auf Zahl fällt, rückt die erste Person einen Stuhl nach links, andernfalls nach rechts.&lt;br /&gt;
         &amp;lt;--- Zahl                                                                                    Kopf ---&amp;gt;&lt;br /&gt;
&lt;br /&gt;
   * Frage: Wie oft muss man die Münze im Durchschnitt werfen, bis Person 1 zum ersten Mal auf Stuhl N sitzt?&lt;br /&gt;
&lt;br /&gt;
Da die erste Person sich anfangs zufällig hinsetzt, haben wir eine Chance von 1/N, dass sie gleich auf dem richtigen Stuhl landet und wir 0 Schritte benötigen. Mit der gleichen Wahrscheinlichkeit von 1/N setzt sie sich anfangs auf Stuhl Nummer (N-1), und wir haben eine fifty-fifty-Chance, mit nur einem Wurf durchzukommen. Wir können aber auch Pech haben und landen auf Stuhl Nummer (N-2). Das ist das Gleiche, als wenn Person 1 von Anfang an auf diesem Stuhl gesessen hätte, nur dass wir jetzt bereits einen Wurf verbraucht haben. Man sieht, dass man die Zahl der Restwürfe immer in dieser Art ausdrücken kann: Sitzt Person 1 auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, kann sie entweder nach rechts rücken und benötigt dann noch soviele Würfe, wie man typischerweise für Stuhl &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; benötigt, plus den Wurf von &amp;lt;tt&amp;gt;i =&amp;gt; i+1&amp;lt;/tt&amp;gt;. Oder sie kann nach links rücken und benötigt dann die typische Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; plus den Wurf &amp;lt;tt&amp;gt;i =&amp;gt; i-1&amp;lt;/tt&amp;gt;. Beide Möglichkeiten haben die Wahrscheinlichkeit 1/2. Mathematisch kann man dies elegant als Rekursionsformel schreiben, die die erwartete Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; als Funktion der entsprechenden Wurfzahlen für die Stühle &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; ausdrückt:&lt;br /&gt;
&lt;br /&gt;
* Wenn wir uns auf Stuhl Nr.1 befinden, werfen wir gar nicht: &amp;lt;math&amp;gt;W\left(N\right)=0&amp;lt;/math&amp;gt;&lt;br /&gt;
* Von Stuhl 0 gehen wir immer zu Stuhl 1: &amp;lt;math&amp;gt;W\left(0\right)=1 + W\left(1\right)&amp;lt;/math&amp;gt;&lt;br /&gt;
* Allgemeiner Fall: &amp;lt;math&amp;gt;W\left(i\right)=\frac 1 2 \left(1 + W\left(i+1\right)\right) + \frac 1 2 \left(1 + W\left(i-1\right)\right) = \frac 1 2 W\left(i+1\right) + \frac 1 2 W\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
Diese Rekursion wird durch die explizite Formel&lt;br /&gt;
::&amp;lt;math&amp;gt;W\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
gelöst, wie man durch Einsetzen leicht nachprüft:&lt;br /&gt;
::&amp;lt;math&amp;gt;&lt;br /&gt;
       \begin{align} &lt;br /&gt;
             W\left(N\right) &amp;amp; = N^2-N^2=0 \\&lt;br /&gt;
                  &lt;br /&gt;
             W\left(0\right) &amp;amp;= N^2-0^2 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= W\left(1\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-1^2+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2 \\&lt;br /&gt;
                 &lt;br /&gt;
              W\left(i\right) &amp;amp;= \frac 1 2 \left(N^2-\left(i-1\right)^2\right) + \frac 1 2 \left(N^2-\left(i+1\right)^2\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= \frac 1 2 N^2-\frac 1 2 \left( i^2-2i+1\right) + \frac 1 2 N^2-\frac 1 2 \left(i^2+2i+1\right) + 1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-i^2&lt;br /&gt;
       \end{align}&amp;lt;/math&amp;gt;&lt;br /&gt;
Insbesondere braucht man im ungünstigen Fall (Start auf Stuhl 0) im Durchschnitt &amp;lt;math&amp;gt;N^2&amp;lt;/math&amp;gt; Würfe, im typischen Fall (start in der Mitte, also bei &amp;lt;math&amp;gt;i = N/2&amp;lt;/math&amp;gt;) im Durchschnitt &lt;br /&gt;
:&amp;lt;math&amp;gt;N^2 - (N/2)^2=\frac 3 4 N^2\in O(N^2)&amp;lt;/math&amp;gt; &lt;br /&gt;
Würfe. Die '''Beziehung zum randomisiertem 2-SAT-Algorithmus''' ist jetzt leicht zu erkennen. Sitzt die Person auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, interpretieren wir das als:&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Stuhl &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt;&amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert, &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
Wählt der Algorithmus eine Klausel, die nicht erfüllt ist, gibt es zwei Möglichkeiten:&lt;br /&gt;
# Beide Literale in der Klausel haben den falschen Wert: Die Lösung wird auf jeden Fall besser, egal welche der beiden wir umdrehen. Wir gehen also von Zustand &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
# Nur eins der Literale hat den falschen Wert: Beim Umdrehen haben wir eine fifty-fifty-Chance, das richtige Literal zu wählen und in den Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; zu gelangen. Mit der selben Wahrscheinlichkeit wählen wir das falsche Literal und landen im Zustand &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
Falls 2 ist der ungünstigere und entspricht unserem Spiel. Ziel des Algorithmus ist es, in den Zustand N zu gelangen, und deshalb gilt genau wie beim Spiel der &lt;br /&gt;
;Satz: Der randomisierte 2-SAT-Algorithmus findet im Durchschnitt nach O(N^2) Versuchen eine Lösung, wenn das Problem erfüllbar ist.&lt;br /&gt;
Damit ist der randomisierte Algorithmus für dieses Problem effizient, was Sie in Übung 12 experimentell nachprüfen sollen.&lt;br /&gt;
&lt;br /&gt;
== 2. RANSAC-ALGORITHMUS (Random Sample Consensus)==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''Aufgabe:''&amp;lt;/u&amp;gt; gegeben: Datenpunkte&lt;br /&gt;
::gesucht: Modell, das die Datenpunkte erklärt&lt;br /&gt;
&lt;br /&gt;
[[Image:Rubto.png|thumb|250px|none]]&lt;br /&gt;
&lt;br /&gt;
'''Messpunkte:'''&lt;br /&gt;
   &lt;br /&gt;
      übliche Lösung: Methode der kleinsten Quadrate&lt;br /&gt;
      &lt;br /&gt;
      &amp;lt;math&amp;gt;\min_{a,b} 	\sum_{i} \left(a x_i + b + y_i\right)^2&amp;lt;/math&amp;gt;&lt;br /&gt;
      &lt;br /&gt;
      Schulmathematik:      &amp;lt;math&amp;gt;Minimum\stackrel{\wedge}{=}Ableitung=0&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lineares Gleichungssystem'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{da}\sum{i} \left(ax_i+b-y_i\right)^2=\sum{i} \frac{d}{da} \left[ax_i+b-y_i\right)^2&amp;lt;/math&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(g\left(x\right)\right)&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(x\right)=x^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;y\left(a\right)=ax_i+b-y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;=\sum_{i}2\left(ax_i+b-y_i\right)\frac{d}{da} \underbrace {ax_i+b-y_i}_{x_i}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\underline {=2\sum_{i}\left(ax_i+b-y_i\right)x_i\stackrel{!}{=}0}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}{x_i}^2+b\sum_{i}x_i=\sum_{i}x_iy_i&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}x_i+b\sum_{i}1=\sum_{i}y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{db}\sum_{i}\left(ax_i+b-y_i\right)^2=2\sum_{i}\left(ax_i+b-y_i\right)*1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:Problem: &amp;lt;math&amp;gt;\epsilon  %&amp;lt;/math&amp;gt; der Datenpunkte sind Outlier&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\Longrightarrow&amp;lt;/math&amp;gt; Einfaches Anpassen des Modells an die Datenpunkte funktioniert nicht&lt;br /&gt;
&lt;br /&gt;
:Seien mindestens k Datenpunkte notwendig, um das Programm anpassen zu können&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
RANSAC-Algorithmus&lt;br /&gt;
&lt;br /&gt;
      for  l in range (trials):&lt;br /&gt;
           wähle zufällig k Punkte aus&lt;br /&gt;
           passe das Modell an die k Punkte an&lt;br /&gt;
           zähle, wieviele Punkte in der Nähe des Modells liegen (d.h. &amp;lt;math&amp;gt;d_i &amp;lt; d_max&amp;lt;/math&amp;gt; muss geschickt gewählt werden) &lt;br /&gt;
                                           #Bsp. Geradenfinden:-wähle a,b aus zwei Punkten&lt;br /&gt;
                                                               -berechne: &amp;lt;math&amp;gt;|ax_i+b-y_i|=d_i&amp;lt;/math&amp;gt;&lt;br /&gt;
                                                               -zähle Punkt i als Inlier, falls &amp;lt;math&amp;gt;d_i&amp;lt;d_ma&amp;lt;/math&amp;gt;&lt;br /&gt;
      return: Modell mit höchster Zahl der Inlier&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;trials= \frac{log\left(1-p\right)}{log\left(1-\left(1-\epsilon\right)^k\right)}&amp;lt;/math&amp;gt;  mit k=Anzahl der Datenpunkte und p=Erfolgswahrscheinlichkeit, &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;=Outlier-Anteil&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Erfolgswahrscheinlichkeit: p=99%'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{|c||c|c|c|c|c|}&lt;br /&gt;
         Beispiel &amp;amp; k &amp;amp; \epsilon=10% &amp;amp; 20% &amp;amp; 50% &amp;amp; 70%\\&lt;br /&gt;
         \hline&lt;br /&gt;
         Linie\;in\;2D &amp;amp; 2 &amp;amp; 3 &amp;amp;5 &amp;amp; 17 &amp;amp; 49\\&lt;br /&gt;
         Kreis\;in\;2D &amp;amp; 3 &amp;amp; 4 &amp;amp; 7 &amp;amp; 35 &amp;amp; 169\\&lt;br /&gt;
         Ebene\;in\;3D &amp;amp; 8 &amp;amp; 9 &amp;amp; 26 &amp;amp; 1172 &amp;amp; 70188\\&lt;br /&gt;
       \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Ein Spiel: Wie viel Schritte braucht man im Mittel zum Ziel?'''&lt;br /&gt;
&lt;br /&gt;
   geg.: 5 Plätze, 2 Personen: eine Person rückt vom einem Platz zu dem enderen Platz;&lt;br /&gt;
         die zweite Person wirft die Münze.&lt;br /&gt;
         Wenn die Münze auf Kopf landet, rücke nach rechts und wenn die Münze auf Zahl landet, rücke nach links.&lt;br /&gt;
         &amp;lt;--- Zahl                                                         Kopf--&amp;gt;&lt;br /&gt;
         Kopf: /////&lt;br /&gt;
         Zahl: /// &lt;br /&gt;
&lt;br /&gt;
:: =&amp;gt; mit 8 Schritten bis zum Ziel&lt;br /&gt;
:im Mittel: bei N Plätzen braucht man N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritte&lt;br /&gt;
&lt;br /&gt;
: all: mit N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritten um N Plätze rücken&lt;br /&gt;
: Wie viel Schritte braucht man im Mittel zum Ziel?&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(N\right)=0&amp;lt;/math&amp;gt;    #wenn wir uns im Stuhl Nr.1 befinden&lt;br /&gt;
           &lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)=\frac 1 2 S\left(1 + S\left(i+1\right)\right) + \frac 1 2 S\left(1 + S\left(i-1\right)\right) = \frac 1 2 S\left(i+1\right) + \frac 1 2 S\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(0\right)=1 + S\left(1\right)&amp;lt;/math&amp;gt;    #bei 0.Platz&lt;br /&gt;
&lt;br /&gt;
:::*Lösung: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:::*speziell: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2&amp;lt;/math&amp;gt;           #wenn man am ungünstigsten Platz startet&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
'''Beziehung zu randomisiertem 2-SAT'''&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Platz &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; &amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert,  &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)=N^2 - \left(\frac N 2\right)^2 = N^2 - \frac N 4 ^2 = \frac 3 4 N^2 &amp;lt;/math&amp;gt;&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)&amp;lt;/math&amp;gt;     # Anfangszustand&lt;br /&gt;
----&lt;br /&gt;
== '''Las Vegas vs. Monte Carlo'''==&lt;br /&gt;
&lt;br /&gt;
   * ''Las Vegas - Algorithmen''&lt;br /&gt;
     - Ergebnis ist immer korrekt.&lt;br /&gt;
     - Berechnung ist mit hoher Wahrscheinlichkeit effizient (d.h. Randomisierung macht den ungünstigsten Fall unwahrscheinlich).&lt;br /&gt;
&lt;br /&gt;
   * ''Monte Carlo - Algorithmen''&lt;br /&gt;
     - Berechnung immer effizient.&lt;br /&gt;
     - Ergebnis mit hoher Wahrscheinlichkeit korrekt (falls kein effizienter Algorithmus bekannt, der immer die richtige Lösung liefert).&lt;br /&gt;
&lt;br /&gt;
{| border = &amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;center&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! Las Vegas&lt;br /&gt;
! Monte Carlo&lt;br /&gt;
|- &lt;br /&gt;
| - Erzeugen einer perfekten Hashfuktion &lt;br /&gt;
| - Algorithmus von Freiwald(Matrizenmultiplikation)&lt;br /&gt;
|-&lt;br /&gt;
| - universelles Hashing&lt;br /&gt;
| - RANSAC&lt;br /&gt;
|-&lt;br /&gt;
| - Quick Sort mit zufälliger Wahl des Pivot-Elements&lt;br /&gt;
| - randomisierte K-SAT(k&amp;gt;=3)(Alg. von Schöning)&lt;br /&gt;
|-&lt;br /&gt;
| - Treep mit zufälligen Prioritäten&lt;br /&gt;
| -&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== ''' Zufallszahlen ''' ==&lt;br /&gt;
&lt;br /&gt;
:- kann man nicht mit deterministischen Computern erzeugen&lt;br /&gt;
:- aber man kann Pseudo-Zufallszahlen erzeugen, die viele Eigenschaften von echten Zufallszahlen haben&lt;br /&gt;
::: * sehr ähnlich  zum Hash&lt;br /&gt;
&lt;br /&gt;
     ''&amp;quot;linear Conguential Random number generator&amp;quot;''&lt;br /&gt;
        &amp;lt;math&amp;gt;I_{i+1}= \left(a*I_i + c\right)\textrm{mod\ } m&amp;lt;/math&amp;gt;&lt;br /&gt;
        &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{=&amp;gt; } &amp;amp; I_i \in [0, m-1]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:-sorgfältige Wahl von  a, c, m notwendig&lt;br /&gt;
::'''Bsp.'''  m = 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;&lt;br /&gt;
::: a = 1664525, c = 1013904223&lt;br /&gt;
::: ''&amp;quot;quick and dirty generator&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
==='''Nachteile'''===&lt;br /&gt;
&lt;br /&gt;
* nicht zufällig genug für viele Anwendungen&lt;br /&gt;
::'''Bsp.''' wähle Punkt in R&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
      \mathrm{ } &amp;amp; p = (rand(), rand(), rand())\\&lt;br /&gt;
&lt;br /&gt;
      \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::gibt Zahl u, v, w so, dass &lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; u * p[0] + v * p[1] + w * p[3]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::stark geclustert ist.&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge ist zu kurz:&lt;br /&gt;
:: spätestens nach m Schritten wiederholt sich die Folge&lt;br /&gt;
&lt;br /&gt;
::'''allgemein''': falls der interne Zustand des Zufallsgenerators ''k'' bits hat, ist Periodenlänge:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; Periode &amp;lt; 2^k\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* ''lowbits'' sind weniger zufällig als die ''highbits''&lt;br /&gt;
----&lt;br /&gt;
=== ''Mersenne Twister''===&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
'''bester zur Zeit bekannter Zufallszahlengenerator (ZZG)'''&lt;br /&gt;
* innere Zustand: &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; 624*32 bit\ Integers  =&amp;gt; 19968 bits\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge: &amp;lt;math&amp;gt;2^ {19937} \approx 4 * 10^{6000}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Punkte aus aufeinanderfolgende Zufallszahlen in &amp;lt;math&amp;gt;\mathbb{R}^n&amp;lt;/math&amp;gt; sind gleich verteilt bis &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; n = 623\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* alle Bits sind unabhängig voneinander zufällig (&amp;quot;Twister&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
* schnell&lt;br /&gt;
&lt;br /&gt;
    class MersenneTwister:&lt;br /&gt;
        &lt;br /&gt;
        def __init__(self, seed):&lt;br /&gt;
            self.N = 624  # Größe des inneren Zustands festlegen&lt;br /&gt;
            self.i = 0    # zählt mit in welchem Zustand wir uns gerade aufhalten&lt;br /&gt;
            &lt;br /&gt;
            self.state = [0]*self.N  # Speicher für den inneren Zustand reservieren&lt;br /&gt;
            &lt;br /&gt;
            self.state[0] = seed     # initiale Zufallszahl vom Benutzer&lt;br /&gt;
            # den Rest des inneren Zustands mit einfachem Zufallszahlengenerator initialisieren&lt;br /&gt;
            for i in xrange(1, self.N):&lt;br /&gt;
                self.state[i] = (1812433253 * (self.state[i-1] ^ (self.state[i-1] &amp;gt;&amp;gt; 30)) + i) % 4294967296&lt;br /&gt;
     &lt;br /&gt;
        def __call__(self):&lt;br /&gt;
            &amp;quot;&amp;quot;&amp;quot;gibt die nächste Zufallszahl im Bereich [0, 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;-1] aus&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
            N, M = self.N, 397&lt;br /&gt;
            &lt;br /&gt;
            # Zustand aktualisieren (neue Zufallszahl ausrechnen)&lt;br /&gt;
            i = self.i&lt;br /&gt;
            r = ((self.state[i] &amp;amp; 0x80000000) | (self.state[(i+1)%N] &amp;amp; 0x7FFFFFFF)) &amp;gt;&amp;gt; 1&lt;br /&gt;
            if self.state[(i+1)%N] &amp;amp; 1:&lt;br /&gt;
                r ^= 0x9908B0DF&lt;br /&gt;
            self.state[i] = self.state[(i+M)%N] ^ r&lt;br /&gt;
     &lt;br /&gt;
            # aktuelle Zufallszahl auslesen und ihre Zufälligkeit durch verwürfeln der Bits verbessern&lt;br /&gt;
            y = self.state[i]&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 11)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt;  7) &amp;amp; 0x9D2C5680)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt; 15) &amp;amp; 0xEFC60000)&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 18)&lt;br /&gt;
            &lt;br /&gt;
            # Zustand weitersetzen und endgültige Zufallszahl ausgeben&lt;br /&gt;
            self.i = (self.i + 1) % N&lt;br /&gt;
            return y&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''geg.:''' Zufallszahl &lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, \overbrace{2^{32}-1}^{m-1}]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ges.:''' Zufallszahl&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, k - 1]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''naive Lösung:'''  &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; rand()%k\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;  ist schlecht.&lt;br /&gt;
&lt;br /&gt;
'''Bsp.'''&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; \qquad m = 16\qquad k = 11\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
! rand() || 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15&lt;br /&gt;
|-&lt;br /&gt;
! rand()%k&lt;br /&gt;
! 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 0 || 1 || 2 || 3 || 4 &lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; 0,...,4 kommt doppelt so häufig wie 5,...,10 &amp;quot;nicht zufällig&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lösung:'''  Zurückweisen des Rests der Zahlen (''rejektion sampling'')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; remainder = (m - 1 - (k - 1))% k = (m - k)%k\\&lt;br /&gt;
        \mathrm{ } &amp;amp; last\ Good\ Value = m-1-remainder\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  r = rand()&lt;br /&gt;
  while r &amp;gt; last.GoodValue:&lt;br /&gt;
        r = rand()&lt;br /&gt;
        return r%k&lt;br /&gt;
&lt;br /&gt;
[[Greedy-Algorithmen und Dynamische Programmierung|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5432</id>
		<title>Randomisierte Algorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5432"/>
				<updated>2012-07-27T18:50:29Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Randomisierte Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
;Definition: Randomisierte Algorithmen sind Algorithmen, die bei Entscheidungen über ihr weiteres Vorgehen oder bei der Wahl ihrer Parameter Zufallszahlen benutzen.&lt;br /&gt;
&lt;br /&gt;
Anschaulich gesprochen, wersucht man bei randomisierten Algorithmen, einen Teil der Lösung zu &amp;lt;i&amp;gt;raten&amp;lt;/i&amp;gt;. Auf den ersten Blick würde man vermuten, dass dabei nicht viel Sinnvolles herauskommen kann. Diese Kapitel wird jedoch zeigen, dass man durch geschicktes Raten tatsächlich zu sehr eleganten Algorithmen gelangen kann.&lt;br /&gt;
&lt;br /&gt;
Grundsätzlich unterscheidet man zwei Arten von randomisierten Algorithmen:&lt;br /&gt;
;Las Vegas - Algorithmen: Das Ergebnis des Algorithmus ist immer korrekt, und die Berechnung erfolgt mit hoher Wahrscheinlichkeit effizient.&lt;br /&gt;
;Monte Carlo - Algorithmen: Die Berechnung ist immer effizient, und das Ergebnis ist mit hoher Wahrscheinlichkeit korrekt.&lt;br /&gt;
Las Vegas-Algorithmen verwendet man, wenn der Algorithmus im ungünstigen Fall eine schlechte Laufzeit hat, und der ungünstige Fall kann durch die Randomisierung sehr unwahrscheinlich gemacht werden. Wir haben in der Vorlesung schon mehrere Las Vegas-Algorithmen kennen gelernt:&lt;br /&gt;
* Quick Sort mit zufälliger Wahl des Pivot-Elements: Die Randomisierung verhindert, dass das Array immer wieder in Subarrays von sehr unterschiedlicher Größe aufgeteilt wird.&lt;br /&gt;
* Treap mit zufälligen Prioritäten: Die Randomisierung verhindert, dass der Baum schlecht balanciert ist.&lt;br /&gt;
* Universelles Hashing: Die zufällige Wahl der Hashfunktion verhindert, dass ein Angreifer eine Schlüsselmenge mit sehr vielen Kollisionen konstruieren kann.&lt;br /&gt;
* Erzeugung einer perfekten Hashfunktion: Durch die Randomisierung entsteht mit nach wenigen Versuchen ein zyklenfreier Graph, der zur Definition der Hashfunktion geeignet ist.&lt;br /&gt;
Monte Carlo-Algorithmen verwendet man dagegen, wenn kein effizienter deterministischer Algorithmus für ein Problem bekannt ist. Man gibt sich dann damit zufrieden, dass der randomisierte Algorithmus die korrekte Lösung nur mit hoher Wahrscheinlichkeit findet, wenn dies dafür sehr effizient geschieht. Bei manchen Problemen ist auch dies unerreichbar - man muss dann bereits zufrieden sein, wenn der Algorithmus mit hoher Wahrscheinlichkeit eine sehr gute Näherungslösung findet. Beliebte Anwendungsgebiete für Monte Carlo-Algorithmen sind beispielsweise&lt;br /&gt;
* Randomisierte Primzahl-Tests: Moderne Verschlüsselungsverfahren benötigen zahlreiche Primzahlen, aber exakte Primzahltests sind teuer. Der [http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin-Test] findet effizient Zahlen, die mit sehr hoher Wahrscheinlichkeit tatsächlich Primzahlen sind.&lt;br /&gt;
* Randomisiertes Testen: Wie jeder Test kann auch eine randomisierter Test nicht die Abwesenheit von Programmierfehlern garantieren, aber man kann durch die Randomisierung viel mehr Testfälle generieren und erhöht so die Erfolgswarscheinlichkeit. Wir haben als Beispiel dafür den [[Korrektheit#Beispiel_f.C3.BCr_das_Testen:_Freivalds_Algorithmus|Algorithmus von Freivald]] behandelt.&lt;br /&gt;
* Lösung schwieriger Optimierungsprobleme: Wir zeigen unten, dass ein randomisierter Algorithmus effizient eine Lösung für das 2-SAT-Problem aus dem vorherigen Kapitel findet (für k-SAT mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; liefert der Algorithmus immer noch mit einer gewissen Wahrscheinlichkeit das richtige Ergebnis, ist aber nicht mehr effizient). Einen effizienten Approximationsalgorithmus für des Problem des Handelsreisenden behandlen wir im Kapitel [[NP-Vollständigkeit]]. Weitere wichtige Beispiele für diesen Bereich sind [http://en.wikipedia.org/wiki/Simulated_annealing simulated annealing] und das [http://de.wikipedia.org/wiki/MCMC-Verfahren Markov-Chain-Monte-Carlo-Verfahren].&lt;br /&gt;
* Robuste Statistik: Eine Grundaufgabe der Statistik ist das Anpassen (Fitten) von Modellen an gemessene Werte. Wenn die Messungen jedoch &amp;quot;Ausreißer&amp;quot; (einige völlig falsche Werte) enthalten, geht die Anpassung schief. Wir beschreiben unten den RANSAC-Algorithmus, der die Ausreißer identifizieren und beim Modellfitten ignorieren kann.&lt;br /&gt;
&lt;br /&gt;
Obwohl randomisierte Algorithmen oft einfach und elegant sind, ist ihre theoretische Analyse (also das Führen von Korrektheits- und Komplexitätsbeweisen) häufig sehr schwierig. Man muss fortgeschrittene Methoden der Wahrscheinlichkeitsrechnung und Statistik beherrschen, um die Wahrscheinlichkeit für das Versagen des Algorithmus zu berechnen und um zu zeigen, wie man den Algorithmus benutzt, damit diese Wahrscheinlichkeit unter einer akzeptablen Schranke bleibt. Die Algorithmen, die wir für diese Vorlesung ausgewählt haben, zeichnen sich dadurch aus, dass die Beweise hier einfach zu erbringen sind.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Lösen des K-SAT-Problems ===&lt;br /&gt;
&lt;br /&gt;
Der &amp;lt;b&amp;gt;Algorithmus von Schöning&amp;lt;/b&amp;gt; löst das [[Graphen_und_Graphenalgorithmen#Normalformen für logische Ausdrücke|k-SAT-Problem]] durch Raten: Wenn ein Ausdruck in k-CNF den Wert False hat, gibt es mindestens eine Klausel, die den Wert False hat. Alle Literale in dieser Klausel haben ebenfalls den Wert False, denn jede Klausel ist eine ODER-Verknüpfung, die nur dann False werden kann. Um den Ausdruck zu erfüllen, muss jede Klausel den Wert True annehmen, also müssen wir den Wert von mindestens einem Literal umdrehen. Wenn der Ausruck tatsächlich erfüllbar ist, gibt es immer ein geeignetes Literal, wir wissen nur nicht, welches. Deshalb drehen wir ein unter den k Literalen der betreffenden Klausel zufällig gewähltes. Liegen wir mit unserer Wahl richtig, sind wir der Lösung näher gekommen - im besten Fall sind jetzt alle Klauseln erfüllt. Wählen wir jedoch die falsche Variable, ist die aktuelle Klausel zwar jetzt True, aber dafür werden andere Klauseln zu False, die bisher True waren, und wir entfernen uns somit von der Lösung.&lt;br /&gt;
&lt;br /&gt;
    geg.: logischer Ausdruck in K-CNF (n Variablen, m Klauseln, k Variablen pro Klausel)&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;math&amp;gt;\underbrace {\underbrace {\left(x_1 \vee x_3 \vee...\right)}_{k\; Literale} \wedge \left( x_2 \vee x_4 \vee...\right)}_{m\;Klauseln}&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Algorithmus von Schöning lautet in Pseudocode:&lt;br /&gt;
    for i in range (trials):    #Anzahl der Versuche&lt;br /&gt;
         Bestimme eine Zufallsbelegung der Variablen &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
         for j in range (steps):&lt;br /&gt;
               if &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt; erfüllt alle Klauseln: &lt;br /&gt;
                   return &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
               wähle zufällig eine Klausel, die nicht erfüllt ist und negiere zufällig eine der Variablen in dieser Klausel &lt;br /&gt;
               # (die Klausel ist jetzt erfüllt)&lt;br /&gt;
    return None  # keine Lösung gefunden&lt;br /&gt;
&lt;br /&gt;
Findet der Algorithmus eine Lösung, wissen wir, dass der Ausdruck erfüllbar ist. Andernfalls könnte der Ausdruck unerfüllbar sein, oder wir haben nur Pech gehabt. Je mehr erfolglose Versuche wir machen, desto höher ist die Wahrscheinlichkeit, dass das erste zutrifft.&lt;br /&gt;
&lt;br /&gt;
Es ist sinnvoll, &amp;lt;tt&amp;gt;steps = k*n&amp;lt;/tt&amp;gt; zu wählen. Dann gilt der &lt;br /&gt;
;Satz: Wenn ein Ausdruck in k-CNF mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; erfüllbar ist, muss man im Mittel &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in O\left(\left(\frac{2(k-1)}{k}\right)^n \right)&amp;lt;/math&amp;gt; Versuche machen, um eine Lösung zu finden.&lt;br /&gt;
&lt;br /&gt;
Für &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; gilt stets &amp;lt;math&amp;gt;\frac{2(k-1)}{k} &amp;gt; 1&amp;lt;/math&amp;gt;, man benötigt also eine in n exponentielle Anzahl von Versuchen. Bei &amp;lt;math&amp;gt;k=3&amp;lt;/math&amp;gt; gilt z.B. &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt; \in O\left(\left(\frac{4}{3}\right)^n\right)&amp;lt;/math&amp;gt;. Dies ist zwar im Mittel effizienter also die erschöpfende Suche, die &amp;lt;math&amp;gt;O(2^n)&amp;lt;/math&amp;gt; Schritte benötigt, aber immer noch sehr langsam.&lt;br /&gt;
&lt;br /&gt;
Der Fall &amp;lt;b&amp;gt;&amp;lt;math&amp;gt;k=2&amp;lt;/math&amp;gt; ist jedoch ein Sonderfall&amp;lt;/b&amp;gt;: Hier kann man leicht beweisen, dass eine Lösung im Mittel bereits nach &amp;lt;math&amp;gt;O\left(n^2\right)&amp;lt;/math&amp;gt; Schritten gefunden wird. Wenn man schon weiss, dass der Ausdruck erfüllbar ist (was mit [[Graphen_und_Graphenalgorithmen#Lösung des 2-SAT-Problems mit Implikationgraphen|Implikationgraphen]] leicht geprüft werden kann), lässt man den randomisierten Algorithmus einfach so lange laufen, bis er eine Lösung findet. Man setzt also &amp;lt;tt&amp;gt;step = infinity&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;trials = 1&amp;lt;/tt&amp;gt; und verlässt sich darauf, dass das &amp;lt;tt&amp;gt;return&amp;lt;/tt&amp;gt; mit einer gültigen Lösung früher oder später ausgeführt wird. Dass man darauf im Mittel nur &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte warten muss, zeigen wir jetzt mit Hilfe eines &amp;lt;i&amp;gt;random walk&amp;lt;/i&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk====&lt;br /&gt;
&lt;br /&gt;
Um die Random Walk Analyse zu verstehen, betrachten wir folgendes Spiel:&lt;br /&gt;
&lt;br /&gt;
   geg.: eine Stuhlreihe mit N Stühlen. Wir nummerieren die Stühle so, dass links der Stuhl 0 und rechts der Stuhl N steht.&lt;br /&gt;
   &lt;br /&gt;
   * Eine Person setzt sich zufällig auf einen der Stühle.&lt;br /&gt;
   * Eine zweite Person wirft eine Münze.&lt;br /&gt;
   &lt;br /&gt;
         Wenn die Münze auf Zahl fällt, rückt die erste Person einen Stuhl nach links, andernfalls nach rechts.&lt;br /&gt;
         &amp;lt;--- Zahl                                                                                    Kopf ---&amp;gt;&lt;br /&gt;
&lt;br /&gt;
   * Frage: Wie oft muss man die Münze im Durchschnitt werfen, bis Person 1 zum ersten Mal auf Stuhl N sitzt?&lt;br /&gt;
&lt;br /&gt;
Da die erste Person sich anfangs zufällig hinsetzt, haben wir eine Chance von 1/N, dass sie gleich auf dem richtigen Stuhl landet und wir 0 Schritte benötigen. Mit der gleichen Wahrscheinlichkeit von 1/N setzt sie sich anfangs auf Stuhl Nummer (N-1), und wir haben eine fifty-fifty-Chance, mit nur einem Wurf durchzukommen. Wir können aber auch Pech haben und landen auf Stuhl Nummer (N-2). Das ist das Gleiche, als wenn Person 1 von Anfang an auf diesem Stuhl gesessen hätte, nur dass wir jetzt bereits einen Wurf verbraucht haben. Man sieht, dass man die Zahl der Restwürfe immer in dieser Art ausdrücken kann: Sitzt Person 1 auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, kann sie entweder nach rechts rücken und benötigt dann noch soviele Würfe, wie man typischerweise für Stuhl &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; benötigt, plus den Wurf von &amp;lt;tt&amp;gt;i =&amp;gt; i+1&amp;lt;/tt&amp;gt;. Oder sie kann nach links rücken und benötigt dann die typische Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; plus den Wurf &amp;lt;tt&amp;gt;i =&amp;gt; i-1&amp;lt;/tt&amp;gt;. Beide Möglichkeiten haben die Wahrscheinlichkeit 1/2. Mathematisch kann man dies elegant als Rekursionsformel schreiben, die die erwartete Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; als Funktion der entsprechenden Wurfzahlen für die Stühle &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; ausdrückt:&lt;br /&gt;
&lt;br /&gt;
* Wenn wir uns auf Stuhl Nr.1 befinden, werfen wir gar nicht: &amp;lt;math&amp;gt;W\left(N\right)=0&amp;lt;/math&amp;gt;&lt;br /&gt;
* Von Stuhl 0 gehen wir immer zu Stuhl 1: &amp;lt;math&amp;gt;W\left(0\right)=1 + W\left(1\right)&amp;lt;/math&amp;gt;&lt;br /&gt;
* Allgemeiner Fall: &amp;lt;math&amp;gt;W\left(i\right)=\frac 1 2 \left(1 + W\left(i+1\right)\right) + \frac 1 2 \left(1 + W\left(i-1\right)\right) = \frac 1 2 W\left(i+1\right) + \frac 1 2 W\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
Diese Rekursion wird durch die explizite Formel&lt;br /&gt;
::&amp;lt;math&amp;gt;W\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
gelöst, wie man durch Einsetzen leicht nachprüft:&lt;br /&gt;
::&amp;lt;math&amp;gt;&lt;br /&gt;
       \begin{align} &lt;br /&gt;
             W\left(N\right) &amp;amp; = N^2-N^2=0 \\&lt;br /&gt;
                  &lt;br /&gt;
             W\left(0\right) &amp;amp;= N^2-0^2 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= W\left(1\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-1^2+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2 \\&lt;br /&gt;
                 &lt;br /&gt;
              W\left(i\right) &amp;amp;= \frac 1 2 \left(N^2-\left(i-1\right)^2\right) + \frac 1 2 \left(N^2-\left(i+1\right)^2\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= \frac 1 2 N^2-\frac 1 2 \left( i^2-2i+1\right) + \frac 1 2 N^2-\frac 1 2 \left(i^2+2i+1\right) + 1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-i^2&lt;br /&gt;
       \end{align}&amp;lt;/math&amp;gt;&lt;br /&gt;
Insbesondere braucht man im ungünstigen Fall (Start auf Stuhl 0) im Durchschnitt &amp;lt;math&amp;gt;N^2&amp;lt;/math&amp;gt; Würfe, im typischen Fall (start in der Mitte, also bei &amp;lt;math&amp;gt;i = N/2&amp;lt;/math&amp;gt; im Durchschnitt :&amp;lt;math&amp;gt;N^2 - (N/2)^2=\frac 3 4 N^2\in O(N^2)&amp;lt;/math&amp;gt; Würfe.&lt;br /&gt;
&lt;br /&gt;
Die '''Beziehung zum randomisiertem 2-SAT-Algorithmus''' ist jetzt leicht zu erkennen. Sitzt die Person auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, interpretieren wir das als:&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Stuhl &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt;&amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert, &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
Wählt der Algorithmus eine Klausel, die nicht erfüllt ist, gibt es zwei Möglichkeiten:&lt;br /&gt;
# Beide Literale in der Klausel haben den falschen Wert: Die Lösung wird auf jeden Fall besser, egal welche der beiden wir umdrehen. Wir gehen also von Zustand &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
# Nur eins der Literale hat den falschen Wert: Beim Umdrehen haben wir eine fifty-fifty-Chance, das richtige Literal zu wählen und in den Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; zu gelangen. Mit der selben Wahrscheinlichkeit wählen wir das falsche Literal und landen im Zustand &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
Falls 2 ist der ungünstigere und entspricht unserem Spiel. Deshalb gilt genau wie beim Spiel der &lt;br /&gt;
;Satz: Der randomisierte 2-SAT-Algorithmus findet im Durchschnitt nach O(N^2) Versuchen eine Lösung, wenn das Problem erfüllbar ist.&lt;br /&gt;
Damit ist der randomisierte Algorithmus für dieses Problem effizient, was Sie in Übung 12 experimentell nachprüfen sollen.&lt;br /&gt;
&lt;br /&gt;
== 2. RANSAC-ALGORITHMUS (Random Sample Consensus)==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''Aufgabe:''&amp;lt;/u&amp;gt; gegeben: Datenpunkte&lt;br /&gt;
::gesucht: Modell, das die Datenpunkte erklärt&lt;br /&gt;
&lt;br /&gt;
[[Image:Rubto.png|thumb|250px|none]]&lt;br /&gt;
&lt;br /&gt;
'''Messpunkte:'''&lt;br /&gt;
   &lt;br /&gt;
      übliche Lösung: Methode der kleinsten Quadrate&lt;br /&gt;
      &lt;br /&gt;
      &amp;lt;math&amp;gt;\min_{a,b} 	\sum_{i} \left(a x_i + b + y_i\right)^2&amp;lt;/math&amp;gt;&lt;br /&gt;
      &lt;br /&gt;
      Schulmathematik:      &amp;lt;math&amp;gt;Minimum\stackrel{\wedge}{=}Ableitung=0&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lineares Gleichungssystem'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{da}\sum{i} \left(ax_i+b-y_i\right)^2=\sum{i} \frac{d}{da} \left[ax_i+b-y_i\right)^2&amp;lt;/math&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(g\left(x\right)\right)&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(x\right)=x^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;y\left(a\right)=ax_i+b-y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;=\sum_{i}2\left(ax_i+b-y_i\right)\frac{d}{da} \underbrace {ax_i+b-y_i}_{x_i}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\underline {=2\sum_{i}\left(ax_i+b-y_i\right)x_i\stackrel{!}{=}0}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}{x_i}^2+b\sum_{i}x_i=\sum_{i}x_iy_i&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}x_i+b\sum_{i}1=\sum_{i}y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{db}\sum_{i}\left(ax_i+b-y_i\right)^2=2\sum_{i}\left(ax_i+b-y_i\right)*1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:Problem: &amp;lt;math&amp;gt;\epsilon  %&amp;lt;/math&amp;gt; der Datenpunkte sind Outlier&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\Longrightarrow&amp;lt;/math&amp;gt; Einfaches Anpassen des Modells an die Datenpunkte funktioniert nicht&lt;br /&gt;
&lt;br /&gt;
:Seien mindestens k Datenpunkte notwendig, um das Programm anpassen zu können&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
RANSAC-Algorithmus&lt;br /&gt;
&lt;br /&gt;
      for  l in range (trials):&lt;br /&gt;
           wähle zufällig k Punkte aus&lt;br /&gt;
           passe das Modell an die k Punkte an&lt;br /&gt;
           zähle, wieviele Punkte in der Nähe des Modells liegen (d.h. &amp;lt;math&amp;gt;d_i &amp;lt; d_max&amp;lt;/math&amp;gt; muss geschickt gewählt werden) &lt;br /&gt;
                                           #Bsp. Geradenfinden:-wähle a,b aus zwei Punkten&lt;br /&gt;
                                                               -berechne: &amp;lt;math&amp;gt;|ax_i+b-y_i|=d_i&amp;lt;/math&amp;gt;&lt;br /&gt;
                                                               -zähle Punkt i als Inlier, falls &amp;lt;math&amp;gt;d_i&amp;lt;d_ma&amp;lt;/math&amp;gt;&lt;br /&gt;
      return: Modell mit höchster Zahl der Inlier&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;trials= \frac{log\left(1-p\right)}{log\left(1-\left(1-\epsilon\right)^k\right)}&amp;lt;/math&amp;gt;  mit k=Anzahl der Datenpunkte und p=Erfolgswahrscheinlichkeit, &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;=Outlier-Anteil&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Erfolgswahrscheinlichkeit: p=99%'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{|c||c|c|c|c|c|}&lt;br /&gt;
         Beispiel &amp;amp; k &amp;amp; \epsilon=10% &amp;amp; 20% &amp;amp; 50% &amp;amp; 70%\\&lt;br /&gt;
         \hline&lt;br /&gt;
         Linie\;in\;2D &amp;amp; 2 &amp;amp; 3 &amp;amp;5 &amp;amp; 17 &amp;amp; 49\\&lt;br /&gt;
         Kreis\;in\;2D &amp;amp; 3 &amp;amp; 4 &amp;amp; 7 &amp;amp; 35 &amp;amp; 169\\&lt;br /&gt;
         Ebene\;in\;3D &amp;amp; 8 &amp;amp; 9 &amp;amp; 26 &amp;amp; 1172 &amp;amp; 70188\\&lt;br /&gt;
       \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Ein Spiel: Wie viel Schritte braucht man im Mittel zum Ziel?'''&lt;br /&gt;
&lt;br /&gt;
   geg.: 5 Plätze, 2 Personen: eine Person rückt vom einem Platz zu dem enderen Platz;&lt;br /&gt;
         die zweite Person wirft die Münze.&lt;br /&gt;
         Wenn die Münze auf Kopf landet, rücke nach rechts und wenn die Münze auf Zahl landet, rücke nach links.&lt;br /&gt;
         &amp;lt;--- Zahl                                                         Kopf--&amp;gt;&lt;br /&gt;
         Kopf: /////&lt;br /&gt;
         Zahl: /// &lt;br /&gt;
&lt;br /&gt;
:: =&amp;gt; mit 8 Schritten bis zum Ziel&lt;br /&gt;
:im Mittel: bei N Plätzen braucht man N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritte&lt;br /&gt;
&lt;br /&gt;
: all: mit N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritten um N Plätze rücken&lt;br /&gt;
: Wie viel Schritte braucht man im Mittel zum Ziel?&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(N\right)=0&amp;lt;/math&amp;gt;    #wenn wir uns im Stuhl Nr.1 befinden&lt;br /&gt;
           &lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)=\frac 1 2 S\left(1 + S\left(i+1\right)\right) + \frac 1 2 S\left(1 + S\left(i-1\right)\right) = \frac 1 2 S\left(i+1\right) + \frac 1 2 S\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(0\right)=1 + S\left(1\right)&amp;lt;/math&amp;gt;    #bei 0.Platz&lt;br /&gt;
&lt;br /&gt;
:::*Lösung: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:::*speziell: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2&amp;lt;/math&amp;gt;           #wenn man am ungünstigsten Platz startet&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
'''Beziehung zu randomisiertem 2-SAT'''&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Platz &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; &amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert,  &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)=N^2 - \left(\frac N 2\right)^2 = N^2 - \frac N 4 ^2 = \frac 3 4 N^2 &amp;lt;/math&amp;gt;&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)&amp;lt;/math&amp;gt;     # Anfangszustand&lt;br /&gt;
----&lt;br /&gt;
== '''Las Vegas vs. Monte Carlo'''==&lt;br /&gt;
&lt;br /&gt;
   * ''Las Vegas - Algorithmen''&lt;br /&gt;
     - Ergebnis ist immer korrekt.&lt;br /&gt;
     - Berechnung ist mit hoher Wahrscheinlichkeit effizient (d.h. Randomisierung macht den ungünstigsten Fall unwahrscheinlich).&lt;br /&gt;
&lt;br /&gt;
   * ''Monte Carlo - Algorithmen''&lt;br /&gt;
     - Berechnung immer effizient.&lt;br /&gt;
     - Ergebnis mit hoher Wahrscheinlichkeit korrekt (falls kein effizienter Algorithmus bekannt, der immer die richtige Lösung liefert).&lt;br /&gt;
&lt;br /&gt;
{| border = &amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;center&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! Las Vegas&lt;br /&gt;
! Monte Carlo&lt;br /&gt;
|- &lt;br /&gt;
| - Erzeugen einer perfekten Hashfuktion &lt;br /&gt;
| - Algorithmus von Freiwald(Matrizenmultiplikation)&lt;br /&gt;
|-&lt;br /&gt;
| - universelles Hashing&lt;br /&gt;
| - RANSAC&lt;br /&gt;
|-&lt;br /&gt;
| - Quick Sort mit zufälliger Wahl des Pivot-Elements&lt;br /&gt;
| - randomisierte K-SAT(k&amp;gt;=3)(Alg. von Schöning)&lt;br /&gt;
|-&lt;br /&gt;
| - Treep mit zufälligen Prioritäten&lt;br /&gt;
| -&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== ''' Zufallszahlen ''' ==&lt;br /&gt;
&lt;br /&gt;
:- kann man nicht mit deterministischen Computern erzeugen&lt;br /&gt;
:- aber man kann Pseudo-Zufallszahlen erzeugen, die viele Eigenschaften von echten Zufallszahlen haben&lt;br /&gt;
::: * sehr ähnlich  zum Hash&lt;br /&gt;
&lt;br /&gt;
     ''&amp;quot;linear Conguential Random number generator&amp;quot;''&lt;br /&gt;
        &amp;lt;math&amp;gt;I_{i+1}= \left(a*I_i + c\right)\textrm{mod\ } m&amp;lt;/math&amp;gt;&lt;br /&gt;
        &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{=&amp;gt; } &amp;amp; I_i \in [0, m-1]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:-sorgfältige Wahl von  a, c, m notwendig&lt;br /&gt;
::'''Bsp.'''  m = 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;&lt;br /&gt;
::: a = 1664525, c = 1013904223&lt;br /&gt;
::: ''&amp;quot;quick and dirty generator&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
==='''Nachteile'''===&lt;br /&gt;
&lt;br /&gt;
* nicht zufällig genug für viele Anwendungen&lt;br /&gt;
::'''Bsp.''' wähle Punkt in R&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
      \mathrm{ } &amp;amp; p = (rand(), rand(), rand())\\&lt;br /&gt;
&lt;br /&gt;
      \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::gibt Zahl u, v, w so, dass &lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; u * p[0] + v * p[1] + w * p[3]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::stark geclustert ist.&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge ist zu kurz:&lt;br /&gt;
:: spätestens nach m Schritten wiederholt sich die Folge&lt;br /&gt;
&lt;br /&gt;
::'''allgemein''': falls der interne Zustand des Zufallsgenerators ''k'' bits hat, ist Periodenlänge:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; Periode &amp;lt; 2^k\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* ''lowbits'' sind weniger zufällig als die ''highbits''&lt;br /&gt;
----&lt;br /&gt;
=== ''Mersenne Twister''===&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
'''bester zur Zeit bekannter Zufallszahlengenerator (ZZG)'''&lt;br /&gt;
* innere Zustand: &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; 624*32 bit\ Integers  =&amp;gt; 19968 bits\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge: &amp;lt;math&amp;gt;2^ {19937} \approx 4 * 10^{6000}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Punkte aus aufeinanderfolgende Zufallszahlen in &amp;lt;math&amp;gt;\mathbb{R}^n&amp;lt;/math&amp;gt; sind gleich verteilt bis &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; n = 623\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* alle Bits sind unabhängig voneinander zufällig (&amp;quot;Twister&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
* schnell&lt;br /&gt;
&lt;br /&gt;
    class MersenneTwister:&lt;br /&gt;
        &lt;br /&gt;
        def __init__(self, seed):&lt;br /&gt;
            self.N = 624  # Größe des inneren Zustands festlegen&lt;br /&gt;
            self.i = 0    # zählt mit in welchem Zustand wir uns gerade aufhalten&lt;br /&gt;
            &lt;br /&gt;
            self.state = [0]*self.N  # Speicher für den inneren Zustand reservieren&lt;br /&gt;
            &lt;br /&gt;
            self.state[0] = seed     # initiale Zufallszahl vom Benutzer&lt;br /&gt;
            # den Rest des inneren Zustands mit einfachem Zufallszahlengenerator initialisieren&lt;br /&gt;
            for i in xrange(1, self.N):&lt;br /&gt;
                self.state[i] = (1812433253 * (self.state[i-1] ^ (self.state[i-1] &amp;gt;&amp;gt; 30)) + i) % 4294967296&lt;br /&gt;
     &lt;br /&gt;
        def __call__(self):&lt;br /&gt;
            &amp;quot;&amp;quot;&amp;quot;gibt die nächste Zufallszahl im Bereich [0, 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;-1] aus&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
            N, M = self.N, 397&lt;br /&gt;
            &lt;br /&gt;
            # Zustand aktualisieren (neue Zufallszahl ausrechnen)&lt;br /&gt;
            i = self.i&lt;br /&gt;
            r = ((self.state[i] &amp;amp; 0x80000000) | (self.state[(i+1)%N] &amp;amp; 0x7FFFFFFF)) &amp;gt;&amp;gt; 1&lt;br /&gt;
            if self.state[(i+1)%N] &amp;amp; 1:&lt;br /&gt;
                r ^= 0x9908B0DF&lt;br /&gt;
            self.state[i] = self.state[(i+M)%N] ^ r&lt;br /&gt;
     &lt;br /&gt;
            # aktuelle Zufallszahl auslesen und ihre Zufälligkeit durch verwürfeln der Bits verbessern&lt;br /&gt;
            y = self.state[i]&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 11)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt;  7) &amp;amp; 0x9D2C5680)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt; 15) &amp;amp; 0xEFC60000)&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 18)&lt;br /&gt;
            &lt;br /&gt;
            # Zustand weitersetzen und endgültige Zufallszahl ausgeben&lt;br /&gt;
            self.i = (self.i + 1) % N&lt;br /&gt;
            return y&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''geg.:''' Zufallszahl &lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, \overbrace{2^{32}-1}^{m-1}]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ges.:''' Zufallszahl&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, k - 1]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''naive Lösung:'''  &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; rand()%k\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;  ist schlecht.&lt;br /&gt;
&lt;br /&gt;
'''Bsp.'''&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; \qquad m = 16\qquad k = 11\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
! rand() || 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15&lt;br /&gt;
|-&lt;br /&gt;
! rand()%k&lt;br /&gt;
! 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 0 || 1 || 2 || 3 || 4 &lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; 0,...,4 kommt doppelt so häufig wie 5,...,10 &amp;quot;nicht zufällig&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lösung:'''  Zurückweisen des Rests der Zahlen (''rejektion sampling'')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; remainder = (m - 1 - (k - 1))% k = (m - k)%k\\&lt;br /&gt;
        \mathrm{ } &amp;amp; last\ Good\ Value = m-1-remainder\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  r = rand()&lt;br /&gt;
  while r &amp;gt; last.GoodValue:&lt;br /&gt;
        r = rand()&lt;br /&gt;
        return r%k&lt;br /&gt;
&lt;br /&gt;
[[Greedy-Algorithmen und Dynamische Programmierung|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5431</id>
		<title>Randomisierte Algorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5431"/>
				<updated>2012-07-27T18:49:57Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Randomisierte Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
;Definition: Randomisierte Algorithmen sind Algorithmen, die bei Entscheidungen über ihr weiteres Vorgehen oder bei der Wahl ihrer Parameter Zufallszahlen benutzen.&lt;br /&gt;
&lt;br /&gt;
Anschaulich gesprochen, wersucht man bei randomisierten Algorithmen, einen Teil der Lösung zu &amp;lt;i&amp;gt;raten&amp;lt;/i&amp;gt;. Auf den ersten Blick würde man vermuten, dass dabei nicht viel Sinnvolles herauskommen kann. Diese Kapitel wird jedoch zeigen, dass man durch geschicktes Raten tatsächlich zu sehr eleganten Algorithmen gelangen kann.&lt;br /&gt;
&lt;br /&gt;
Grundsätzlich unterscheidet man zwei Arten von randomisierten Algorithmen:&lt;br /&gt;
;Las Vegas - Algorithmen: Das Ergebnis des Algorithmus ist immer korrekt, und die Berechnung erfolgt mit hoher Wahrscheinlichkeit effizient.&lt;br /&gt;
;Monte Carlo - Algorithmen: Die Berechnung ist immer effizient, und das Ergebnis ist mit hoher Wahrscheinlichkeit korrekt.&lt;br /&gt;
Las Vegas-Algorithmen verwendet man, wenn der Algorithmus im ungünstigen Fall eine schlechte Laufzeit hat, und der ungünstige Fall kann durch die Randomisierung sehr unwahrscheinlich gemacht werden. Wir haben in der Vorlesung schon mehrere Las Vegas-Algorithmen kennen gelernt:&lt;br /&gt;
* Quick Sort mit zufälliger Wahl des Pivot-Elements: Die Randomisierung verhindert, dass das Array immer wieder in Subarrays von sehr unterschiedlicher Größe aufgeteilt wird.&lt;br /&gt;
* Treap mit zufälligen Prioritäten: Die Randomisierung verhindert, dass der Baum schlecht balanciert ist.&lt;br /&gt;
* Universelles Hashing: Die zufällige Wahl der Hashfunktion verhindert, dass ein Angreifer eine Schlüsselmenge mit sehr vielen Kollisionen konstruieren kann.&lt;br /&gt;
* Erzeugung einer perfekten Hashfunktion: Durch die Randomisierung entsteht mit nach wenigen Versuchen ein zyklenfreier Graph, der zur Definition der Hashfunktion geeignet ist.&lt;br /&gt;
Monte Carlo-Algorithmen verwendet man dagegen, wenn kein effizienter deterministischer Algorithmus für ein Problem bekannt ist. Man gibt sich dann damit zufrieden, dass der randomisierte Algorithmus die korrekte Lösung nur mit hoher Wahrscheinlichkeit findet, wenn dies dafür sehr effizient geschieht. Bei manchen Problemen ist auch dies unerreichbar - man muss dann bereits zufrieden sein, wenn der Algorithmus mit hoher Wahrscheinlichkeit eine sehr gute Näherungslösung findet. Beliebte Anwendungsgebiete für Monte Carlo-Algorithmen sind beispielsweise&lt;br /&gt;
* Randomisierte Primzahl-Tests: Moderne Verschlüsselungsverfahren benötigen zahlreiche Primzahlen, aber exakte Primzahltests sind teuer. Der [http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin-Test] findet effizient Zahlen, die mit sehr hoher Wahrscheinlichkeit tatsächlich Primzahlen sind.&lt;br /&gt;
* Randomisiertes Testen: Wie jeder Test kann auch eine randomisierter Test nicht die Abwesenheit von Programmierfehlern garantieren, aber man kann durch die Randomisierung viel mehr Testfälle generieren und erhöht so die Erfolgswarscheinlichkeit. Wir haben als Beispiel dafür den [[Korrektheit#Beispiel_f.C3.BCr_das_Testen:_Freivalds_Algorithmus|Algorithmus von Freivald]] behandelt.&lt;br /&gt;
* Lösung schwieriger Optimierungsprobleme: Wir zeigen unten, dass ein randomisierter Algorithmus effizient eine Lösung für das 2-SAT-Problem aus dem vorherigen Kapitel findet (für k-SAT mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; liefert der Algorithmus immer noch mit einer gewissen Wahrscheinlichkeit das richtige Ergebnis, ist aber nicht mehr effizient). Einen effizienten Approximationsalgorithmus für des Problem des Handelsreisenden behandlen wir im Kapitel [[NP-Vollständigkeit]]. Weitere wichtige Beispiele für diesen Bereich sind [http://en.wikipedia.org/wiki/Simulated_annealing simulated annealing] und das [http://de.wikipedia.org/wiki/MCMC-Verfahren Markov-Chain-Monte-Carlo-Verfahren].&lt;br /&gt;
* Robuste Statistik: Eine Grundaufgabe der Statistik ist das Anpassen (Fitten) von Modellen an gemessene Werte. Wenn die Messungen jedoch &amp;quot;Ausreißer&amp;quot; (einige völlig falsche Werte) enthalten, geht die Anpassung schief. Wir beschreiben unten den RANSAC-Algorithmus, der die Ausreißer identifizieren und beim Modellfitten ignorieren kann.&lt;br /&gt;
&lt;br /&gt;
Obwohl randomisierte Algorithmen oft einfach und elegant sind, ist ihre theoretische Analyse (also das Führen von Korrektheits- und Komplexitätsbeweisen) häufig sehr schwierig. Man muss fortgeschrittene Methoden der Wahrscheinlichkeitsrechnung und Statistik beherrschen, um die Wahrscheinlichkeit für das Versagen des Algorithmus zu berechnen und um zu zeigen, wie man den Algorithmus benutzt, damit diese Wahrscheinlichkeit unter einer akzeptablen Schranke bleibt. Die Algorithmen, die wir für diese Vorlesung ausgewählt haben, zeichnen sich dadurch aus, dass die Beweise hier einfach zu erbringen sind.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Lösen des K-SAT-Problems ===&lt;br /&gt;
&lt;br /&gt;
Der &amp;lt;b&amp;gt;Algorithmus von Schöning&amp;lt;/b&amp;gt; löst das [[Graphen_und_Graphenalgorithmen#Normalformen für logische Ausdrücke|k-SAT-Problem]] durch Raten: Wenn ein Ausdruck in k-CNF den Wert False hat, gibt es mindestens eine Klausel, die den Wert False hat. Alle Literale in dieser Klausel haben ebenfalls den Wert False, denn jede Klausel ist eine ODER-Verknüpfung, die nur dann False werden kann. Um den Ausdruck zu erfüllen, muss jede Klausel den Wert True annehmen, also müssen wir den Wert von mindestens einem Literal umdrehen. Wenn der Ausruck tatsächlich erfüllbar ist, gibt es immer ein geeignetes Literal, wir wissen nur nicht, welches. Deshalb drehen wir ein unter den k Literalen der betreffenden Klausel zufällig gewähltes. Liegen wir mit unserer Wahl richtig, sind wir der Lösung näher gekommen - im besten Fall sind jetzt alle Klauseln erfüllt. Wählen wir jedoch die falsche Variable, ist die aktuelle Klausel zwar jetzt True, aber dafür werden andere Klauseln zu False, die bisher True waren, und wir entfernen uns somit von der Lösung.&lt;br /&gt;
&lt;br /&gt;
    geg.: logischer Ausdruck in K-CNF (n Variablen, m Klauseln, k Variablen pro Klausel)&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;math&amp;gt;\underbrace {\underbrace {\left(x_1 \vee x_3 \vee...\right)}_{k\; Literale} \wedge \left( x_2 \vee x_4 \vee...\right)}_{m\;Klauseln}&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Algorithmus von Schöning lautet in Pseudocode:&lt;br /&gt;
    for i in range (trials):    #Anzahl der Versuche&lt;br /&gt;
         Bestimme eine Zufallsbelegung der Variablen &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
         for j in range (steps):&lt;br /&gt;
               if &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt; erfüllt alle Klauseln: &lt;br /&gt;
                   return &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
               wähle zufällig eine Klausel, die nicht erfüllt ist und negiere zufällig eine der Variablen in dieser Klausel &lt;br /&gt;
               # (die Klausel ist jetzt erfüllt)&lt;br /&gt;
    return None  # keine Lösung gefunden&lt;br /&gt;
&lt;br /&gt;
Findet der Algorithmus eine Lösung, wissen wir, dass der Ausdruck erfüllbar ist. Andernfalls könnte der Ausdruck unerfüllbar sein, oder wir haben nur Pech gehabt. Je mehr erfolglose Versuche wir machen, desto höher ist die Wahrscheinlichkeit, dass das erste zutrifft.&lt;br /&gt;
&lt;br /&gt;
Es ist sinnvoll, &amp;lt;tt&amp;gt;steps = k*n&amp;lt;/tt&amp;gt; zu wählen. Dann gilt der &lt;br /&gt;
;Satz: Wenn ein Ausdruck in k-CNF mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; erfüllbar ist, muss man im Mittel &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in O\left(\left(\frac{2(k-1)}{k}\right)^n \right)&amp;lt;/math&amp;gt; Versuche machen, um eine Lösung zu finden.&lt;br /&gt;
&lt;br /&gt;
Für &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; gilt stets &amp;lt;math&amp;gt;\frac{2(k-1)}{k} &amp;gt; 1&amp;lt;/math&amp;gt;, man benötigt also eine in n exponentielle Anzahl von Versuchen. Bei &amp;lt;math&amp;gt;k=3&amp;lt;/math&amp;gt; gilt z.B. &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt; \in O\left(\left(\frac{4}{3}\right)^n\right)&amp;lt;/math&amp;gt;. Dies ist zwar im Mittel effizienter also die erschöpfende Suche, die &amp;lt;math&amp;gt;O(2^n)&amp;lt;/math&amp;gt; Schritte benötigt, aber immer noch sehr langsam.&lt;br /&gt;
&lt;br /&gt;
Der Fall &amp;lt;b&amp;gt;&amp;lt;math&amp;gt;k=2&amp;lt;/math&amp;gt; ist jedoch ein Sonderfall&amp;lt;/b&amp;gt;: Hier kann man leicht beweisen, dass eine Lösung im Mittel bereits nach &amp;lt;math&amp;gt;O\left(n^2\right)&amp;lt;/math&amp;gt; Schritten gefunden wird. Wenn man schon weiss, dass der Ausdruck erfüllbar ist (was mit [[Graphen_und_Graphenalgorithmen#Lösung des 2-SAT-Problems mit Implikationgraphen|Implikationgraphen]] leicht geprüft werden kann), lässt man den randomisierten Algorithmus einfach so lange laufen, bis er eine Lösung findet. Man setzt also &amp;lt;tt&amp;gt;step = infinity&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;trials = 1&amp;lt;/tt&amp;gt; und verlässt sich darauf, dass das &amp;lt;tt&amp;gt;return&amp;lt;/tt&amp;gt; mit einer gültigen Lösung früher oder später ausgeführt wird. Dass man darauf im Mittel nur &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte warten muss, zeigen wir jetzt mit Hilfe eines &amp;lt;i&amp;gt;random walk&amp;lt;/i&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk====&lt;br /&gt;
&lt;br /&gt;
Um die Random Walk Analyse zu verstehen, betrachten wir folgendes Spiel:&lt;br /&gt;
&lt;br /&gt;
   geg.: eine Stuhlreihe mit N Stühlen. Wir nummerieren die Stühle so, dass links der Stuhl 0 und rechts der Stuhl N steht.&lt;br /&gt;
   &lt;br /&gt;
   * Eine Person setzt sich zufällig auf einen der Stühle.&lt;br /&gt;
   * Eine zweite Person wirft eine Münze.&lt;br /&gt;
   &lt;br /&gt;
         Wenn die Münze auf Zahl fällt, rückt die erste Person einen Stuhl nach links, andernfalls nach rechts.&lt;br /&gt;
         &amp;lt;--- Zahl                                                                                    Kopf ---&amp;gt;&lt;br /&gt;
&lt;br /&gt;
   * Frage: Wie oft muss man die Münze im Durchschnitt werfen, bis Person 1 zum ersten Mal auf Stuhl N sitzt?&lt;br /&gt;
&lt;br /&gt;
Da die erste Person sich anfangs zufällig hinsetzt, haben wir eine Chance von 1/N, dass sie gleich auf dem richtigen Stuhl landet und wir 0 Schritte benötigen. Mit der gleichen Wahrscheinlichkeit von 1/N setzt sie sich anfangs auf Stuhl Nummer (N-1), und wir haben eine fifty-fifty-Chance, mit nur einem Wurf durchzukommen. Wir können aber auch Pech haben und landen auf Stuhl Nummer (N-2). Das ist das Gleiche, als wenn Person 1 von Anfang an auf diesem Stuhl gesessen hätte, nur dass wir jetzt bereits einen Wurf verbraucht haben. Man sieht, dass man die Zahl der Restwürfe immer in dieser Art ausdrücken kann: Sitzt Person 1 auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;, kann sie entweder nach rechts rücken und benötigt dann noch soviele Würfe, wie man typischerweise für Stuhl &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; benötigt, plus den Wurf von &amp;lt;tt&amp;gt;i =&amp;gt; i+1&amp;lt;/tt&amp;gt;. Oder sie kann nach links rücken und benötigt dann die typische Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; plus den Wurf &amp;lt;tt&amp;gt;i =&amp;gt; i-1&amp;lt;/tt&amp;gt;. Beide Möglichkeiten haben die Wahrscheinlichkeit 1/2. Mathematisch kann man dies elegant als Rekursionsformel schreiben, die die erwartete Wurfzahl für Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; als Funktion der entsprechenden Wurfzahlen für die Stühle &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; ausdrückt:&lt;br /&gt;
&lt;br /&gt;
* Wenn wir uns auf Stuhl Nr.1 befinden, werfen wir gar nicht: &amp;lt;math&amp;gt;W\left(N\right)=0&amp;lt;/math&amp;gt;&lt;br /&gt;
* Von Stuhl 0 gehen wir immer zu Stuhl 1: &amp;lt;math&amp;gt;W\left(0\right)=1 + W\left(1\right)&amp;lt;/math&amp;gt;&lt;br /&gt;
* Allgemeiner Fall: &amp;lt;math&amp;gt;W\left(i\right)=\frac 1 2 \left(1 + W\left(i+1\right)\right) + \frac 1 2 \left(1 + W\left(i-1\right)\right) = \frac 1 2 W\left(i+1\right) + \frac 1 2 W\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
Diese Rekursion wird durch die explizite Formel&lt;br /&gt;
::&amp;lt;math&amp;gt;W\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
gelöst, wie man durch Einsetzen leicht nachprüft:&lt;br /&gt;
::&amp;lt;math&amp;gt;&lt;br /&gt;
       \begin{align} &lt;br /&gt;
             W\left(N\right) &amp;amp; = N^2-N^2=0 \\&lt;br /&gt;
                  &lt;br /&gt;
             W\left(0\right) &amp;amp;= N^2-0^2 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= W\left(1\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-1^2+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2 \\&lt;br /&gt;
                 &lt;br /&gt;
              W\left(i\right) &amp;amp;= \frac 1 2 \left(N^2-\left(i-1\right)^2\right) + \frac 1 2 \left(N^2-\left(i+1\right)^2\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= \frac 1 2 N^2-\frac 1 2 \left( i^2-2i+1\right) + \frac 1 2 N^2-\frac 1 2 \left(i^2+2i+1\right) + 1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= N^2-i^2&lt;br /&gt;
       \end{align}&amp;lt;/math&amp;gt;&lt;br /&gt;
Insbesondere braucht man im ungünstigen Fall (Start auf Stuhl 0) im Durchschnitt &amp;lt;math&amp;gt;N^2&amp;lt;/math&amp;gt; Würfe, im typischen Fall (start in der Mitte, also bei &amp;lt;math&amp;gt;i = N/2&amp;lt;/math&amp;gt; im Durchschnitt :&amp;lt;math&amp;gt;N^2 - (N/2)^2=\frac 3 4 N^2\in O(N^2)&amp;lt;/math&amp;gt; Würfe.&lt;br /&gt;
&lt;br /&gt;
Die '''Beziehung zum randomisiertem 2-SAT-Algorithmus''' ist jetzt leicht zu erkennen. Sitz die Person auf Stuhl &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; sitzt, interpretieren wir das als:&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Stuhl &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt;&amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert, &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
Wählt der Algorithmus eine Klausel, die nicht erfüllt ist, gibt es zwei Möglichkeiten:&lt;br /&gt;
# Beide Literale in der Klausel haben den falschen Wert: Die Lösung wird auf jeden Fall besser, egal welche der beiden wir umdrehen. Wir gehen also von Zustand &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
# Nur eins der Literale hat den falschen Wert: Beim Umdrehen haben wir eine fifty-fifty-Chance, das richtige Literal zu wählen und in den Zustand &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; zu gelangen. Mit der selben Wahrscheinlichkeit wählen wir das falsche Literal und landen im Zustand &amp;lt;tt&amp;gt;i-1&amp;lt;/tt&amp;gt;.&lt;br /&gt;
Falls 2 ist der ungünstigere und entspricht unserem Spiel. Deshalb gilt genau wie beim Spiel der &lt;br /&gt;
;Satz: Der randomisierte 2-SAT-Algorithmus findet im Durchschnitt nach O(N^2) Versuchen eine Lösung, wenn das Problem erfüllbar ist.&lt;br /&gt;
Damit ist der randomisierte Algorithmus für dieses Problem effizient, was Sie in Übung 12 experimentell nachprüfen sollen.&lt;br /&gt;
&lt;br /&gt;
== 2. RANSAC-ALGORITHMUS (Random Sample Consensus)==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''Aufgabe:''&amp;lt;/u&amp;gt; gegeben: Datenpunkte&lt;br /&gt;
::gesucht: Modell, das die Datenpunkte erklärt&lt;br /&gt;
&lt;br /&gt;
[[Image:Rubto.png|thumb|250px|none]]&lt;br /&gt;
&lt;br /&gt;
'''Messpunkte:'''&lt;br /&gt;
   &lt;br /&gt;
      übliche Lösung: Methode der kleinsten Quadrate&lt;br /&gt;
      &lt;br /&gt;
      &amp;lt;math&amp;gt;\min_{a,b} 	\sum_{i} \left(a x_i + b + y_i\right)^2&amp;lt;/math&amp;gt;&lt;br /&gt;
      &lt;br /&gt;
      Schulmathematik:      &amp;lt;math&amp;gt;Minimum\stackrel{\wedge}{=}Ableitung=0&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lineares Gleichungssystem'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{da}\sum{i} \left(ax_i+b-y_i\right)^2=\sum{i} \frac{d}{da} \left[ax_i+b-y_i\right)^2&amp;lt;/math&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(g\left(x\right)\right)&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(x\right)=x^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;y\left(a\right)=ax_i+b-y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;=\sum_{i}2\left(ax_i+b-y_i\right)\frac{d}{da} \underbrace {ax_i+b-y_i}_{x_i}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\underline {=2\sum_{i}\left(ax_i+b-y_i\right)x_i\stackrel{!}{=}0}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}{x_i}^2+b\sum_{i}x_i=\sum_{i}x_iy_i&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}x_i+b\sum_{i}1=\sum_{i}y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{db}\sum_{i}\left(ax_i+b-y_i\right)^2=2\sum_{i}\left(ax_i+b-y_i\right)*1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:Problem: &amp;lt;math&amp;gt;\epsilon  %&amp;lt;/math&amp;gt; der Datenpunkte sind Outlier&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\Longrightarrow&amp;lt;/math&amp;gt; Einfaches Anpassen des Modells an die Datenpunkte funktioniert nicht&lt;br /&gt;
&lt;br /&gt;
:Seien mindestens k Datenpunkte notwendig, um das Programm anpassen zu können&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
RANSAC-Algorithmus&lt;br /&gt;
&lt;br /&gt;
      for  l in range (trials):&lt;br /&gt;
           wähle zufällig k Punkte aus&lt;br /&gt;
           passe das Modell an die k Punkte an&lt;br /&gt;
           zähle, wieviele Punkte in der Nähe des Modells liegen (d.h. &amp;lt;math&amp;gt;d_i &amp;lt; d_max&amp;lt;/math&amp;gt; muss geschickt gewählt werden) &lt;br /&gt;
                                           #Bsp. Geradenfinden:-wähle a,b aus zwei Punkten&lt;br /&gt;
                                                               -berechne: &amp;lt;math&amp;gt;|ax_i+b-y_i|=d_i&amp;lt;/math&amp;gt;&lt;br /&gt;
                                                               -zähle Punkt i als Inlier, falls &amp;lt;math&amp;gt;d_i&amp;lt;d_ma&amp;lt;/math&amp;gt;&lt;br /&gt;
      return: Modell mit höchster Zahl der Inlier&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;trials= \frac{log\left(1-p\right)}{log\left(1-\left(1-\epsilon\right)^k\right)}&amp;lt;/math&amp;gt;  mit k=Anzahl der Datenpunkte und p=Erfolgswahrscheinlichkeit, &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;=Outlier-Anteil&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Erfolgswahrscheinlichkeit: p=99%'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{|c||c|c|c|c|c|}&lt;br /&gt;
         Beispiel &amp;amp; k &amp;amp; \epsilon=10% &amp;amp; 20% &amp;amp; 50% &amp;amp; 70%\\&lt;br /&gt;
         \hline&lt;br /&gt;
         Linie\;in\;2D &amp;amp; 2 &amp;amp; 3 &amp;amp;5 &amp;amp; 17 &amp;amp; 49\\&lt;br /&gt;
         Kreis\;in\;2D &amp;amp; 3 &amp;amp; 4 &amp;amp; 7 &amp;amp; 35 &amp;amp; 169\\&lt;br /&gt;
         Ebene\;in\;3D &amp;amp; 8 &amp;amp; 9 &amp;amp; 26 &amp;amp; 1172 &amp;amp; 70188\\&lt;br /&gt;
       \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Ein Spiel: Wie viel Schritte braucht man im Mittel zum Ziel?'''&lt;br /&gt;
&lt;br /&gt;
   geg.: 5 Plätze, 2 Personen: eine Person rückt vom einem Platz zu dem enderen Platz;&lt;br /&gt;
         die zweite Person wirft die Münze.&lt;br /&gt;
         Wenn die Münze auf Kopf landet, rücke nach rechts und wenn die Münze auf Zahl landet, rücke nach links.&lt;br /&gt;
         &amp;lt;--- Zahl                                                         Kopf--&amp;gt;&lt;br /&gt;
         Kopf: /////&lt;br /&gt;
         Zahl: /// &lt;br /&gt;
&lt;br /&gt;
:: =&amp;gt; mit 8 Schritten bis zum Ziel&lt;br /&gt;
:im Mittel: bei N Plätzen braucht man N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritte&lt;br /&gt;
&lt;br /&gt;
: all: mit N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritten um N Plätze rücken&lt;br /&gt;
: Wie viel Schritte braucht man im Mittel zum Ziel?&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(N\right)=0&amp;lt;/math&amp;gt;    #wenn wir uns im Stuhl Nr.1 befinden&lt;br /&gt;
           &lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)=\frac 1 2 S\left(1 + S\left(i+1\right)\right) + \frac 1 2 S\left(1 + S\left(i-1\right)\right) = \frac 1 2 S\left(i+1\right) + \frac 1 2 S\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(0\right)=1 + S\left(1\right)&amp;lt;/math&amp;gt;    #bei 0.Platz&lt;br /&gt;
&lt;br /&gt;
:::*Lösung: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:::*speziell: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2&amp;lt;/math&amp;gt;           #wenn man am ungünstigsten Platz startet&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
'''Beziehung zu randomisiertem 2-SAT'''&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Platz &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; &amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert,  &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)=N^2 - \left(\frac N 2\right)^2 = N^2 - \frac N 4 ^2 = \frac 3 4 N^2 &amp;lt;/math&amp;gt;&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)&amp;lt;/math&amp;gt;     # Anfangszustand&lt;br /&gt;
----&lt;br /&gt;
== '''Las Vegas vs. Monte Carlo'''==&lt;br /&gt;
&lt;br /&gt;
   * ''Las Vegas - Algorithmen''&lt;br /&gt;
     - Ergebnis ist immer korrekt.&lt;br /&gt;
     - Berechnung ist mit hoher Wahrscheinlichkeit effizient (d.h. Randomisierung macht den ungünstigsten Fall unwahrscheinlich).&lt;br /&gt;
&lt;br /&gt;
   * ''Monte Carlo - Algorithmen''&lt;br /&gt;
     - Berechnung immer effizient.&lt;br /&gt;
     - Ergebnis mit hoher Wahrscheinlichkeit korrekt (falls kein effizienter Algorithmus bekannt, der immer die richtige Lösung liefert).&lt;br /&gt;
&lt;br /&gt;
{| border = &amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;center&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! Las Vegas&lt;br /&gt;
! Monte Carlo&lt;br /&gt;
|- &lt;br /&gt;
| - Erzeugen einer perfekten Hashfuktion &lt;br /&gt;
| - Algorithmus von Freiwald(Matrizenmultiplikation)&lt;br /&gt;
|-&lt;br /&gt;
| - universelles Hashing&lt;br /&gt;
| - RANSAC&lt;br /&gt;
|-&lt;br /&gt;
| - Quick Sort mit zufälliger Wahl des Pivot-Elements&lt;br /&gt;
| - randomisierte K-SAT(k&amp;gt;=3)(Alg. von Schöning)&lt;br /&gt;
|-&lt;br /&gt;
| - Treep mit zufälligen Prioritäten&lt;br /&gt;
| -&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== ''' Zufallszahlen ''' ==&lt;br /&gt;
&lt;br /&gt;
:- kann man nicht mit deterministischen Computern erzeugen&lt;br /&gt;
:- aber man kann Pseudo-Zufallszahlen erzeugen, die viele Eigenschaften von echten Zufallszahlen haben&lt;br /&gt;
::: * sehr ähnlich  zum Hash&lt;br /&gt;
&lt;br /&gt;
     ''&amp;quot;linear Conguential Random number generator&amp;quot;''&lt;br /&gt;
        &amp;lt;math&amp;gt;I_{i+1}= \left(a*I_i + c\right)\textrm{mod\ } m&amp;lt;/math&amp;gt;&lt;br /&gt;
        &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{=&amp;gt; } &amp;amp; I_i \in [0, m-1]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:-sorgfältige Wahl von  a, c, m notwendig&lt;br /&gt;
::'''Bsp.'''  m = 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;&lt;br /&gt;
::: a = 1664525, c = 1013904223&lt;br /&gt;
::: ''&amp;quot;quick and dirty generator&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
==='''Nachteile'''===&lt;br /&gt;
&lt;br /&gt;
* nicht zufällig genug für viele Anwendungen&lt;br /&gt;
::'''Bsp.''' wähle Punkt in R&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
      \mathrm{ } &amp;amp; p = (rand(), rand(), rand())\\&lt;br /&gt;
&lt;br /&gt;
      \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::gibt Zahl u, v, w so, dass &lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; u * p[0] + v * p[1] + w * p[3]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::stark geclustert ist.&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge ist zu kurz:&lt;br /&gt;
:: spätestens nach m Schritten wiederholt sich die Folge&lt;br /&gt;
&lt;br /&gt;
::'''allgemein''': falls der interne Zustand des Zufallsgenerators ''k'' bits hat, ist Periodenlänge:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; Periode &amp;lt; 2^k\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* ''lowbits'' sind weniger zufällig als die ''highbits''&lt;br /&gt;
----&lt;br /&gt;
=== ''Mersenne Twister''===&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
'''bester zur Zeit bekannter Zufallszahlengenerator (ZZG)'''&lt;br /&gt;
* innere Zustand: &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; 624*32 bit\ Integers  =&amp;gt; 19968 bits\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge: &amp;lt;math&amp;gt;2^ {19937} \approx 4 * 10^{6000}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Punkte aus aufeinanderfolgende Zufallszahlen in &amp;lt;math&amp;gt;\mathbb{R}^n&amp;lt;/math&amp;gt; sind gleich verteilt bis &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; n = 623\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* alle Bits sind unabhängig voneinander zufällig (&amp;quot;Twister&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
* schnell&lt;br /&gt;
&lt;br /&gt;
    class MersenneTwister:&lt;br /&gt;
        &lt;br /&gt;
        def __init__(self, seed):&lt;br /&gt;
            self.N = 624  # Größe des inneren Zustands festlegen&lt;br /&gt;
            self.i = 0    # zählt mit in welchem Zustand wir uns gerade aufhalten&lt;br /&gt;
            &lt;br /&gt;
            self.state = [0]*self.N  # Speicher für den inneren Zustand reservieren&lt;br /&gt;
            &lt;br /&gt;
            self.state[0] = seed     # initiale Zufallszahl vom Benutzer&lt;br /&gt;
            # den Rest des inneren Zustands mit einfachem Zufallszahlengenerator initialisieren&lt;br /&gt;
            for i in xrange(1, self.N):&lt;br /&gt;
                self.state[i] = (1812433253 * (self.state[i-1] ^ (self.state[i-1] &amp;gt;&amp;gt; 30)) + i) % 4294967296&lt;br /&gt;
     &lt;br /&gt;
        def __call__(self):&lt;br /&gt;
            &amp;quot;&amp;quot;&amp;quot;gibt die nächste Zufallszahl im Bereich [0, 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;-1] aus&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
            N, M = self.N, 397&lt;br /&gt;
            &lt;br /&gt;
            # Zustand aktualisieren (neue Zufallszahl ausrechnen)&lt;br /&gt;
            i = self.i&lt;br /&gt;
            r = ((self.state[i] &amp;amp; 0x80000000) | (self.state[(i+1)%N] &amp;amp; 0x7FFFFFFF)) &amp;gt;&amp;gt; 1&lt;br /&gt;
            if self.state[(i+1)%N] &amp;amp; 1:&lt;br /&gt;
                r ^= 0x9908B0DF&lt;br /&gt;
            self.state[i] = self.state[(i+M)%N] ^ r&lt;br /&gt;
     &lt;br /&gt;
            # aktuelle Zufallszahl auslesen und ihre Zufälligkeit durch verwürfeln der Bits verbessern&lt;br /&gt;
            y = self.state[i]&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 11)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt;  7) &amp;amp; 0x9D2C5680)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt; 15) &amp;amp; 0xEFC60000)&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 18)&lt;br /&gt;
            &lt;br /&gt;
            # Zustand weitersetzen und endgültige Zufallszahl ausgeben&lt;br /&gt;
            self.i = (self.i + 1) % N&lt;br /&gt;
            return y&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''geg.:''' Zufallszahl &lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, \overbrace{2^{32}-1}^{m-1}]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ges.:''' Zufallszahl&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, k - 1]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''naive Lösung:'''  &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; rand()%k\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;  ist schlecht.&lt;br /&gt;
&lt;br /&gt;
'''Bsp.'''&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; \qquad m = 16\qquad k = 11\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
! rand() || 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15&lt;br /&gt;
|-&lt;br /&gt;
! rand()%k&lt;br /&gt;
! 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 0 || 1 || 2 || 3 || 4 &lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; 0,...,4 kommt doppelt so häufig wie 5,...,10 &amp;quot;nicht zufällig&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lösung:'''  Zurückweisen des Rests der Zahlen (''rejektion sampling'')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; remainder = (m - 1 - (k - 1))% k = (m - k)%k\\&lt;br /&gt;
        \mathrm{ } &amp;amp; last\ Good\ Value = m-1-remainder\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  r = rand()&lt;br /&gt;
  while r &amp;gt; last.GoodValue:&lt;br /&gt;
        r = rand()&lt;br /&gt;
        return r%k&lt;br /&gt;
&lt;br /&gt;
[[Greedy-Algorithmen und Dynamische Programmierung|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5430</id>
		<title>Randomisierte Algorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5430"/>
				<updated>2012-07-27T17:13:11Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Anwendung: Lösen des K-SAT-Problems */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Randomisierte Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
;Definition: Randomisierte Algorithmen sind Algorithmen, die bei Entscheidungen über ihr weiteres Vorgehen oder bei der Wahl ihrer Parameter Zufallszahlen benutzen.&lt;br /&gt;
&lt;br /&gt;
Anschaulich gesprochen, wersucht man bei randomisierten Algorithmen, einen Teil der Lösung zu &amp;lt;i&amp;gt;raten&amp;lt;/i&amp;gt;. Auf den ersten Blick würde man vermuten, dass dabei nicht viel Sinnvolles herauskommen kann. Diese Kapitel wird jedoch zeigen, dass man durch geschicktes Raten tatsächlich zu sehr eleganten Algorithmen gelangen kann.&lt;br /&gt;
&lt;br /&gt;
Grundsätzlich unterscheidet man zwei Arten von randomisierten Algorithmen:&lt;br /&gt;
;Las Vegas - Algorithmen: Das Ergebnis des Algorithmus ist immer korrekt, und die Berechnung erfolgt mit hoher Wahrscheinlichkeit effizient.&lt;br /&gt;
;Monte Carlo - Algorithmen: Die Berechnung ist immer effizient, und das Ergebnis ist mit hoher Wahrscheinlichkeit korrekt.&lt;br /&gt;
Las Vegas-Algorithmen verwendet man, wenn der Algorithmus im ungünstigen Fall eine schlechte Laufzeit hat, und der ungünstige Fall kann durch die Randomisierung sehr unwahrscheinlich gemacht werden. Wir haben in der Vorlesung schon mehrere Las Vegas-Algorithmen kennen gelernt:&lt;br /&gt;
* Quick Sort mit zufälliger Wahl des Pivot-Elements: Die Randomisierung verhindert, dass das Array immer wieder in Subarrays von sehr unterschiedlicher Größe aufgeteilt wird.&lt;br /&gt;
* Treap mit zufälligen Prioritäten: Die Randomisierung verhindert, dass der Baum schlecht balanciert ist.&lt;br /&gt;
* Universelles Hashing: Die zufällige Wahl der Hashfunktion verhindert, dass ein Angreifer eine Schlüsselmenge mit sehr vielen Kollisionen konstruieren kann.&lt;br /&gt;
* Erzeugung einer perfekten Hashfunktion: Durch die Randomisierung entsteht mit nach wenigen Versuchen ein zyklenfreier Graph, der zur Definition der Hashfunktion geeignet ist.&lt;br /&gt;
Monte Carlo-Algorithmen verwendet man dagegen, wenn kein effizienter deterministischer Algorithmus für ein Problem bekannt ist. Man gibt sich dann damit zufrieden, dass der randomisierte Algorithmus die korrekte Lösung nur mit hoher Wahrscheinlichkeit findet, wenn dies dafür sehr effizient geschieht. Bei manchen Problemen ist auch dies unerreichbar - man muss dann bereits zufrieden sein, wenn der Algorithmus mit hoher Wahrscheinlichkeit eine sehr gute Näherungslösung findet. Beliebte Anwendungsgebiete für Monte Carlo-Algorithmen sind beispielsweise&lt;br /&gt;
* Randomisierte Primzahl-Tests: Moderne Verschlüsselungsverfahren benötigen zahlreiche Primzahlen, aber exakte Primzahltests sind teuer. Der [http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin-Test] findet effizient Zahlen, die mit sehr hoher Wahrscheinlichkeit tatsächlich Primzahlen sind.&lt;br /&gt;
* Randomisiertes Testen: Wie jeder Test kann auch eine randomisierter Test nicht die Abwesenheit von Programmierfehlern garantieren, aber man kann durch die Randomisierung viel mehr Testfälle generieren und erhöht so die Erfolgswarscheinlichkeit. Wir haben als Beispiel dafür den [[Korrektheit#Beispiel_f.C3.BCr_das_Testen:_Freivalds_Algorithmus|Algorithmus von Freivald]] behandelt.&lt;br /&gt;
* Lösung schwieriger Optimierungsprobleme: Wir zeigen unten, dass ein randomisierter Algorithmus effizient eine Lösung für das 2-SAT-Problem aus dem vorherigen Kapitel findet (für k-SAT mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; liefert der Algorithmus immer noch mit einer gewissen Wahrscheinlichkeit das richtige Ergebnis, ist aber nicht mehr effizient). Einen effizienten Approximationsalgorithmus für des Problem des Handelsreisenden behandlen wir im Kapitel [[NP-Vollständigkeit]]. Weitere wichtige Beispiele für diesen Bereich sind [http://en.wikipedia.org/wiki/Simulated_annealing simulated annealing] und das [http://de.wikipedia.org/wiki/MCMC-Verfahren Markov-Chain-Monte-Carlo-Verfahren].&lt;br /&gt;
* Robuste Statistik: Eine Grundaufgabe der Statistik ist das Anpassen (Fitten) von Modellen an gemessene Werte. Wenn die Messungen jedoch &amp;quot;Ausreißer&amp;quot; (einige völlig falsche Werte) enthalten, geht die Anpassung schief. Wir beschreiben unten den RANSAC-Algorithmus, der die Ausreißer identifizieren und beim Modellfitten ignorieren kann.&lt;br /&gt;
&lt;br /&gt;
Obwohl randomisierte Algorithmen oft einfach und elegant sind, ist ihre theoretische Analyse (also das Führen von Korrektheits- und Komplexitätsbeweisen) häufig sehr schwierig. Man muss fortgeschrittene Methoden der Wahrscheinlichkeitsrechnung und Statistik beherrschen, um die Wahrscheinlichkeit für das Versagen des Algorithmus zu berechnen und um zu zeigen, wie man den Algorithmus benutzt, damit diese Wahrscheinlichkeit unter einer akzeptablen Schranke bleibt. Die Algorithmen, die wir für diese Vorlesung ausgewählt haben, zeichnen sich dadurch aus, dass die Beweise hier einfach zu erbringen sind.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Lösen des K-SAT-Problems ===&lt;br /&gt;
&lt;br /&gt;
Der &amp;lt;b&amp;gt;Algorithmus von Schöning&amp;lt;/b&amp;gt; löst das [[Graphen_und_Graphenalgorithmen#Normalformen für logische Ausdrücke|k-SAT-Problem]] durch Raten: Wenn ein Ausdruck in k-CNF den Wert False hat, gibt es mindestens eine Klausel, die den Wert False hat. Alle Literale in dieser Klausel haben ebenfalls den Wert False, denn jede Klausel ist eine ODER-Verknüpfung, die nur dann False werden kann. Um den Ausdruck zu erfüllen, muss jede Klausel den Wert True annehmen, also müssen wir den Wert von mindestens einem Literal umdrehen. Wenn der Ausruck tatsächlich erfüllbar ist, gibt es immer ein geeignetes Literal, wir wissen nur nicht, welches. Deshalb drehen wir ein unter den k Literalen der betreffenden Klausel zufällig gewähltes. Liegen wir mit unserer Wahl richtig, sind wir der Lösung näher gekommen - im besten Fall sind jetzt alle Klauseln erfüllt. Wählen wir jedoch die falsche Variable, ist die aktuelle Klausel zwar jetzt True, aber dafür werden andere Klauseln zu False, die bisher True waren, und wir entfernen uns somit von der Lösung.&lt;br /&gt;
&lt;br /&gt;
    geg.: logischer Ausdruck in K-CNF (n Variablen, m Klauseln, k Variablen pro Klausel)&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;math&amp;gt;\underbrace {\underbrace {\left(x_1 \vee x_3 \vee...\right)}_{k\; Literale} \wedge \left( x_2 \vee x_4 \vee...\right)}_{m\;Klauseln}&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Algorithmus von Schöning lautet in Pseudocode:&lt;br /&gt;
    for i in range (trials):    #Anzahl der Versuche&lt;br /&gt;
         Bestimme eine Zufallsbelegung der Variablen &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
         for j in range (steps):&lt;br /&gt;
               if &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt; erfüllt alle Klauseln: &lt;br /&gt;
                   return &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
               wähle zufällig eine Klausel, die nicht erfüllt ist und negiere zufällig eine der Variablen in dieser Klausel &lt;br /&gt;
               # (die Klausel ist jetzt erfüllt)&lt;br /&gt;
    return None  # keine Lösung gefunden&lt;br /&gt;
&lt;br /&gt;
Findet der Algorithmus eine Lösung, wissen wir, dass der Ausdruck erfüllbar ist. Andernfalls könnte der Ausdruck unerfüllbar sein, oder wir haben nur Pech gehabt. Je mehr erfolglose Versuche wir machen, desto höher ist die Wahrscheinlichkeit, dass das erste zutrifft.&lt;br /&gt;
&lt;br /&gt;
Es ist sinnvoll, &amp;lt;tt&amp;gt;steps = k*n&amp;lt;/tt&amp;gt; zu wählen. Dann gilt der &lt;br /&gt;
;Satz: Wenn ein Ausdruck in k-CNF mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; erfüllbar ist, muss man im Mittel &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in O\left(\left(\frac{2(k-1)}{k}\right)^n \right)&amp;lt;/math&amp;gt; Versuche machen, um eine Lösung zu finden.&lt;br /&gt;
&lt;br /&gt;
Für &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; gilt stets &amp;lt;math&amp;gt;\frac{2(k-1)}{k} &amp;gt; 1&amp;lt;/math&amp;gt;, man benötigt also eine in n exponentielle Anzahl von Versuchen. Bei &amp;lt;math&amp;gt;k=3&amp;lt;/math&amp;gt; gilt z.B. &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt; \in O\left(\left(\frac{4}{3}\right)^n\right)&amp;lt;/math&amp;gt;. Dies ist zwar im Mittel effizienter also die erschöpfende Suche, die &amp;lt;math&amp;gt;O(2^n)&amp;lt;/math&amp;gt; Schritte benötigt, aber immer noch sehr langsam.&lt;br /&gt;
&lt;br /&gt;
Der Fall &amp;lt;b&amp;gt;&amp;lt;math&amp;gt;k=2&amp;lt;/math&amp;gt; ist jedoch ein Sonderfall&amp;lt;/b&amp;gt;: Hier kann man leicht beweisen, dass eine Lösung im Mittel bereits nach &amp;lt;math&amp;gt;O\left(n^2\right)&amp;lt;/math&amp;gt; Schritten gefunden wird. Wenn man schon weiss, dass der Ausdruck erfüllbar ist (was mit [[Graphen_und_Graphenalgorithmen#Lösung des 2-SAT-Problems mit Implikationgraphen|Implikationgraphen]] leicht geprüft werden kann), lässt man den randomisierten Algorithmus einfach so lange laufen, bis er eine Lösung findet. Man setzt also &amp;lt;tt&amp;gt;step = infinity&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;trials = 1&amp;lt;/tt&amp;gt; und verlässt sich darauf, dass das &amp;lt;tt&amp;gt;return&amp;lt;/tt&amp;gt; mit einer gültigen Lösung früher oder später ausgeführt wird. Dass man darauf im Mittel nur &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte warten muss, zeigen wir jetzt mit Hilfe eines &amp;lt;i&amp;gt;random walk&amp;lt;/i&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Laufzeitanalyse der randomisierten 2-SAT-Algorithmus mittels Random Walk====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''-Zufallsbelegung hat  &amp;lt;math&amp;gt;t\leq n&amp;lt;/math&amp;gt; richtige Variablen (im Mittel &amp;lt;math&amp;gt;t\approx \frac {n} 2&amp;lt;/math&amp;gt;)'''&lt;br /&gt;
&lt;br /&gt;
Negieren einer Variable ändert t um 1,&lt;br /&gt;
u.Z. &amp;lt;math&amp;gt;t\rightarrow t+1&amp;lt;/math&amp;gt; mit Wahrscheinlichkeit &amp;lt;math&amp;gt;\frac 1 2&amp;lt;/math&amp;gt; ::(für beliebiges k: &amp;lt;math&amp;gt;\frac 1 k&amp;lt;/math&amp;gt;)&lt;br /&gt;
::::::::::&amp;lt;math&amp;gt;t\rightarrow t-1&amp;lt;/math&amp;gt; mit Wahrscheinlichkeit &amp;lt;math&amp;gt;\frac 1 2&amp;lt;/math&amp;gt; ::(für beliebiges k: &amp;lt;math&amp;gt;\frac {k-1} k&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''-Wieviele Schritte braucht man im Mittel, um zu einer Lösung mit t Richtigen zu kommen?'''&lt;br /&gt;
&lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(t\right)=\frac 1 2 S\left(t-1\right) + \frac 1 2 S\left(t+1\right) +1&amp;lt;/math&amp;gt;&lt;br /&gt;
       &lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(n\right)=0&amp;lt;/math&amp;gt;    #Abbruchbedingung der Schleife&lt;br /&gt;
       &lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(0\right) = S\left( 1\right) + 1 \Rightarrow S\left(t\right) = n^2-t^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
       '''Probe:''' &lt;br /&gt;
 &lt;br /&gt;
       &amp;lt;math&amp;gt;&lt;br /&gt;
       \begin{align} &lt;br /&gt;
             S\left(n\right) &amp;amp; = n^2-n^2=0 \\&lt;br /&gt;
                  &lt;br /&gt;
             S\left(0\right) &amp;amp;= n^2-0^2 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= S\left(1\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2-1^2+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2 \\&lt;br /&gt;
                 &lt;br /&gt;
              S\left(t\right) &amp;amp;= \frac 1 2 \left(n^2-\left(t-1\right)^2\right) + \frac 1 2 \left(n^2-\left(t+1\right)^2\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= \frac 1 2 n^2-\frac 1 2 \left( t^2-2t+1\right) + \frac 1 2 n^2-\frac 1 2 \left(t^2+2t+1\right) + 1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2-t^2&lt;br /&gt;
       \end{align}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Das ist das Random Walk Problem'''&lt;br /&gt;
&lt;br /&gt;
Im ungünstigsten Fall (t=0) werden im Mittel &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte benötigt, um durch random walk nach t=n zu gelangen.&lt;br /&gt;
&lt;br /&gt;
== 2. RANSAC-ALGORITHMUS (Random Sample Consensus)==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''Aufgabe:''&amp;lt;/u&amp;gt; gegeben: Datenpunkte&lt;br /&gt;
::gesucht: Modell, das die Datenpunkte erklärt&lt;br /&gt;
&lt;br /&gt;
[[Image:Rubto.png|thumb|250px|none]]&lt;br /&gt;
&lt;br /&gt;
'''Messpunkte:'''&lt;br /&gt;
   &lt;br /&gt;
      übliche Lösung: Methode der kleinsten Quadrate&lt;br /&gt;
      &lt;br /&gt;
      &amp;lt;math&amp;gt;\min_{a,b} 	\sum_{i} \left(a x_i + b + y_i\right)^2&amp;lt;/math&amp;gt;&lt;br /&gt;
      &lt;br /&gt;
      Schulmathematik:      &amp;lt;math&amp;gt;Minimum\stackrel{\wedge}{=}Ableitung=0&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lineares Gleichungssystem'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{da}\sum{i} \left(ax_i+b-y_i\right)^2=\sum{i} \frac{d}{da} \left[ax_i+b-y_i\right)^2&amp;lt;/math&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(g\left(x\right)\right)&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(x\right)=x^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;y\left(a\right)=ax_i+b-y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;=\sum_{i}2\left(ax_i+b-y_i\right)\frac{d}{da} \underbrace {ax_i+b-y_i}_{x_i}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\underline {=2\sum_{i}\left(ax_i+b-y_i\right)x_i\stackrel{!}{=}0}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}{x_i}^2+b\sum_{i}x_i=\sum_{i}x_iy_i&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}x_i+b\sum_{i}1=\sum_{i}y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{db}\sum_{i}\left(ax_i+b-y_i\right)^2=2\sum_{i}\left(ax_i+b-y_i\right)*1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:Problem: &amp;lt;math&amp;gt;\epsilon  %&amp;lt;/math&amp;gt; der Datenpunkte sind Outlier&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\Longrightarrow&amp;lt;/math&amp;gt; Einfaches Anpassen des Modells an die Datenpunkte funktioniert nicht&lt;br /&gt;
&lt;br /&gt;
:Seien mindestens k Datenpunkte notwendig, um das Programm anpassen zu können&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
RANSAC-Algorithmus&lt;br /&gt;
&lt;br /&gt;
      for  l in range (trials):&lt;br /&gt;
           wähle zufällig k Punkte aus&lt;br /&gt;
           passe das Modell an die k Punkte an&lt;br /&gt;
           zähle, wieviele Punkte in der Nähe des Modells liegen (d.h. &amp;lt;math&amp;gt;d_i &amp;lt; d_max&amp;lt;/math&amp;gt; muss geschickt gewählt werden) &lt;br /&gt;
                                           #Bsp. Geradenfinden:-wähle a,b aus zwei Punkten&lt;br /&gt;
                                                               -berechne: &amp;lt;math&amp;gt;|ax_i+b-y_i|=d_i&amp;lt;/math&amp;gt;&lt;br /&gt;
                                                               -zähle Punkt i als Inlier, falls &amp;lt;math&amp;gt;d_i&amp;lt;d_ma&amp;lt;/math&amp;gt;&lt;br /&gt;
      return: Modell mit höchster Zahl der Inlier&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;trials= \frac{log\left(1-p\right)}{log\left(1-\left(1-\epsilon\right)^k\right)}&amp;lt;/math&amp;gt;  mit k=Anzahl der Datenpunkte und p=Erfolgswahrscheinlichkeit, &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;=Outlier-Anteil&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Erfolgswahrscheinlichkeit: p=99%'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{|c||c|c|c|c|c|}&lt;br /&gt;
         Beispiel &amp;amp; k &amp;amp; \epsilon=10% &amp;amp; 20% &amp;amp; 50% &amp;amp; 70%\\&lt;br /&gt;
         \hline&lt;br /&gt;
         Linie\;in\;2D &amp;amp; 2 &amp;amp; 3 &amp;amp;5 &amp;amp; 17 &amp;amp; 49\\&lt;br /&gt;
         Kreis\;in\;2D &amp;amp; 3 &amp;amp; 4 &amp;amp; 7 &amp;amp; 35 &amp;amp; 169\\&lt;br /&gt;
         Ebene\;in\;3D &amp;amp; 8 &amp;amp; 9 &amp;amp; 26 &amp;amp; 1172 &amp;amp; 70188\\&lt;br /&gt;
       \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Ein Spiel: Wie viel Schritte braucht man im Mittel zum Ziel?'''&lt;br /&gt;
&lt;br /&gt;
   geg.: 5 Plätze, 2 Personen: eine Person rückt vom einem Platz zu dem enderen Platz;&lt;br /&gt;
         die zweite Person wirft die Münze.&lt;br /&gt;
         Wenn die Münze auf Kopf landet, rücke nach rechts und wenn die Münze auf Zahl landet, rücke nach links.&lt;br /&gt;
         &amp;lt;--- Zahl                                                         Kopf--&amp;gt;&lt;br /&gt;
         Kopf: /////&lt;br /&gt;
         Zahl: /// &lt;br /&gt;
&lt;br /&gt;
:: =&amp;gt; mit 8 Schritten bis zum Ziel&lt;br /&gt;
:im Mittel: bei N Plätzen braucht man N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritte&lt;br /&gt;
&lt;br /&gt;
: all: mit N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritten um N Plätze rücken&lt;br /&gt;
: Wie viel Schritte braucht man im Mittel zum Ziel?&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(N\right)=0&amp;lt;/math&amp;gt;    #wenn wir uns im Stuhl Nr.1 befinden&lt;br /&gt;
           &lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)=\frac 1 2 S\left(1 + S\left(i+1\right)\right) + \frac 1 2 S\left(1 + S\left(i-1\right)\right) = \frac 1 2 S\left(i+1\right) + \frac 1 2 S\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(0\right)=1 + S\left(1\right)&amp;lt;/math&amp;gt;    #bei 0.Platz&lt;br /&gt;
&lt;br /&gt;
:::*Lösung: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:::*speziell: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2&amp;lt;/math&amp;gt;           #wenn man am ungünstigsten Platz startet&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
'''Beziehung zu randomisiertem 2-SAT'''&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Platz &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; &amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert,  &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)=N^2 - \left(\frac N 2\right)^2 = N^2 - \frac N 4 ^2 = \frac 3 4 N^2 &amp;lt;/math&amp;gt;&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)&amp;lt;/math&amp;gt;     # Anfangszustand&lt;br /&gt;
----&lt;br /&gt;
== '''Las Vegas vs. Monte Carlo'''==&lt;br /&gt;
&lt;br /&gt;
   * ''Las Vegas - Algorithmen''&lt;br /&gt;
     - Ergebnis ist immer korrekt.&lt;br /&gt;
     - Berechnung ist mit hoher Wahrscheinlichkeit effizient (d.h. Randomisierung macht den ungünstigsten Fall unwahrscheinlich).&lt;br /&gt;
&lt;br /&gt;
   * ''Monte Carlo - Algorithmen''&lt;br /&gt;
     - Berechnung immer effizient.&lt;br /&gt;
     - Ergebnis mit hoher Wahrscheinlichkeit korrekt (falls kein effizienter Algorithmus bekannt, der immer die richtige Lösung liefert).&lt;br /&gt;
&lt;br /&gt;
{| border = &amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;center&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! Las Vegas&lt;br /&gt;
! Monte Carlo&lt;br /&gt;
|- &lt;br /&gt;
| - Erzeugen einer perfekten Hashfuktion &lt;br /&gt;
| - Algorithmus von Freiwald(Matrizenmultiplikation)&lt;br /&gt;
|-&lt;br /&gt;
| - universelles Hashing&lt;br /&gt;
| - RANSAC&lt;br /&gt;
|-&lt;br /&gt;
| - Quick Sort mit zufälliger Wahl des Pivot-Elements&lt;br /&gt;
| - randomisierte K-SAT(k&amp;gt;=3)(Alg. von Schöning)&lt;br /&gt;
|-&lt;br /&gt;
| - Treep mit zufälligen Prioritäten&lt;br /&gt;
| -&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== ''' Zufallszahlen ''' ==&lt;br /&gt;
&lt;br /&gt;
:- kann man nicht mit deterministischen Computern erzeugen&lt;br /&gt;
:- aber man kann Pseudo-Zufallszahlen erzeugen, die viele Eigenschaften von echten Zufallszahlen haben&lt;br /&gt;
::: * sehr ähnlich  zum Hash&lt;br /&gt;
&lt;br /&gt;
     ''&amp;quot;linear Conguential Random number generator&amp;quot;''&lt;br /&gt;
        &amp;lt;math&amp;gt;I_{i+1}= \left(a*I_i + c\right)\textrm{mod\ } m&amp;lt;/math&amp;gt;&lt;br /&gt;
        &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{=&amp;gt; } &amp;amp; I_i \in [0, m-1]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:-sorgfältige Wahl von  a, c, m notwendig&lt;br /&gt;
::'''Bsp.'''  m = 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;&lt;br /&gt;
::: a = 1664525, c = 1013904223&lt;br /&gt;
::: ''&amp;quot;quick and dirty generator&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
==='''Nachteile'''===&lt;br /&gt;
&lt;br /&gt;
* nicht zufällig genug für viele Anwendungen&lt;br /&gt;
::'''Bsp.''' wähle Punkt in R&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
      \mathrm{ } &amp;amp; p = (rand(), rand(), rand())\\&lt;br /&gt;
&lt;br /&gt;
      \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::gibt Zahl u, v, w so, dass &lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; u * p[0] + v * p[1] + w * p[3]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::stark geclustert ist.&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge ist zu kurz:&lt;br /&gt;
:: spätestens nach m Schritten wiederholt sich die Folge&lt;br /&gt;
&lt;br /&gt;
::'''allgemein''': falls der interne Zustand des Zufallsgenerators ''k'' bits hat, ist Periodenlänge:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; Periode &amp;lt; 2^k\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* ''lowbits'' sind weniger zufällig als die ''highbits''&lt;br /&gt;
----&lt;br /&gt;
=== ''Mersenne Twister''===&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
'''bester zur Zeit bekannter Zufallszahlengenerator (ZZG)'''&lt;br /&gt;
* innere Zustand: &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; 624*32 bit\ Integers  =&amp;gt; 19968 bits\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge: &amp;lt;math&amp;gt;2^ {19937} \approx 4 * 10^{6000}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Punkte aus aufeinanderfolgende Zufallszahlen in &amp;lt;math&amp;gt;\mathbb{R}^n&amp;lt;/math&amp;gt; sind gleich verteilt bis &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; n = 623\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* alle Bits sind unabhängig voneinander zufällig (&amp;quot;Twister&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
* schnell&lt;br /&gt;
&lt;br /&gt;
    class MersenneTwister:&lt;br /&gt;
        &lt;br /&gt;
        def __init__(self, seed):&lt;br /&gt;
            self.N = 624  # Größe des inneren Zustands festlegen&lt;br /&gt;
            self.i = 0    # zählt mit in welchem Zustand wir uns gerade aufhalten&lt;br /&gt;
            &lt;br /&gt;
            self.state = [0]*self.N  # Speicher für den inneren Zustand reservieren&lt;br /&gt;
            &lt;br /&gt;
            self.state[0] = seed     # initiale Zufallszahl vom Benutzer&lt;br /&gt;
            # den Rest des inneren Zustands mit einfachem Zufallszahlengenerator initialisieren&lt;br /&gt;
            for i in xrange(1, self.N):&lt;br /&gt;
                self.state[i] = (1812433253 * (self.state[i-1] ^ (self.state[i-1] &amp;gt;&amp;gt; 30)) + i) % 4294967296&lt;br /&gt;
     &lt;br /&gt;
        def __call__(self):&lt;br /&gt;
            &amp;quot;&amp;quot;&amp;quot;gibt die nächste Zufallszahl im Bereich [0, 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;-1] aus&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
            N, M = self.N, 397&lt;br /&gt;
            &lt;br /&gt;
            # Zustand aktualisieren (neue Zufallszahl ausrechnen)&lt;br /&gt;
            i = self.i&lt;br /&gt;
            r = ((self.state[i] &amp;amp; 0x80000000) | (self.state[(i+1)%N] &amp;amp; 0x7FFFFFFF)) &amp;gt;&amp;gt; 1&lt;br /&gt;
            if self.state[(i+1)%N] &amp;amp; 1:&lt;br /&gt;
                r ^= 0x9908B0DF&lt;br /&gt;
            self.state[i] = self.state[(i+M)%N] ^ r&lt;br /&gt;
     &lt;br /&gt;
            # aktuelle Zufallszahl auslesen und ihre Zufälligkeit durch verwürfeln der Bits verbessern&lt;br /&gt;
            y = self.state[i]&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 11)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt;  7) &amp;amp; 0x9D2C5680)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt; 15) &amp;amp; 0xEFC60000)&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 18)&lt;br /&gt;
            &lt;br /&gt;
            # Zustand weitersetzen und endgültige Zufallszahl ausgeben&lt;br /&gt;
            self.i = (self.i + 1) % N&lt;br /&gt;
            return y&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''geg.:''' Zufallszahl &lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, \overbrace{2^{32}-1}^{m-1}]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ges.:''' Zufallszahl&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, k - 1]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''naive Lösung:'''  &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; rand()%k\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;  ist schlecht.&lt;br /&gt;
&lt;br /&gt;
'''Bsp.'''&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; \qquad m = 16\qquad k = 11\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
! rand() || 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15&lt;br /&gt;
|-&lt;br /&gt;
! rand()%k&lt;br /&gt;
! 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 0 || 1 || 2 || 3 || 4 &lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; 0,...,4 kommt doppelt so häufig wie 5,...,10 &amp;quot;nicht zufällig&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lösung:'''  Zurückweisen des Rests der Zahlen (''rejektion sampling'')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; remainder = (m - 1 - (k - 1))% k = (m - k)%k\\&lt;br /&gt;
        \mathrm{ } &amp;amp; last\ Good\ Value = m-1-remainder\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  r = rand()&lt;br /&gt;
  while r &amp;gt; last.GoodValue:&lt;br /&gt;
        r = rand()&lt;br /&gt;
        return r%k&lt;br /&gt;
&lt;br /&gt;
[[Greedy-Algorithmen und Dynamische Programmierung|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5429</id>
		<title>Randomisierte Algorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5429"/>
				<updated>2012-07-27T17:08:10Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Anwendung: Lösen des K-SAT-Problems */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Randomisierte Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
;Definition: Randomisierte Algorithmen sind Algorithmen, die bei Entscheidungen über ihr weiteres Vorgehen oder bei der Wahl ihrer Parameter Zufallszahlen benutzen.&lt;br /&gt;
&lt;br /&gt;
Anschaulich gesprochen, wersucht man bei randomisierten Algorithmen, einen Teil der Lösung zu &amp;lt;i&amp;gt;raten&amp;lt;/i&amp;gt;. Auf den ersten Blick würde man vermuten, dass dabei nicht viel Sinnvolles herauskommen kann. Diese Kapitel wird jedoch zeigen, dass man durch geschicktes Raten tatsächlich zu sehr eleganten Algorithmen gelangen kann.&lt;br /&gt;
&lt;br /&gt;
Grundsätzlich unterscheidet man zwei Arten von randomisierten Algorithmen:&lt;br /&gt;
;Las Vegas - Algorithmen: Das Ergebnis des Algorithmus ist immer korrekt, und die Berechnung erfolgt mit hoher Wahrscheinlichkeit effizient.&lt;br /&gt;
;Monte Carlo - Algorithmen: Die Berechnung ist immer effizient, und das Ergebnis ist mit hoher Wahrscheinlichkeit korrekt.&lt;br /&gt;
Las Vegas-Algorithmen verwendet man, wenn der Algorithmus im ungünstigen Fall eine schlechte Laufzeit hat, und der ungünstige Fall kann durch die Randomisierung sehr unwahrscheinlich gemacht werden. Wir haben in der Vorlesung schon mehrere Las Vegas-Algorithmen kennen gelernt:&lt;br /&gt;
* Quick Sort mit zufälliger Wahl des Pivot-Elements: Die Randomisierung verhindert, dass das Array immer wieder in Subarrays von sehr unterschiedlicher Größe aufgeteilt wird.&lt;br /&gt;
* Treap mit zufälligen Prioritäten: Die Randomisierung verhindert, dass der Baum schlecht balanciert ist.&lt;br /&gt;
* Universelles Hashing: Die zufällige Wahl der Hashfunktion verhindert, dass ein Angreifer eine Schlüsselmenge mit sehr vielen Kollisionen konstruieren kann.&lt;br /&gt;
* Erzeugung einer perfekten Hashfunktion: Durch die Randomisierung entsteht mit nach wenigen Versuchen ein zyklenfreier Graph, der zur Definition der Hashfunktion geeignet ist.&lt;br /&gt;
Monte Carlo-Algorithmen verwendet man dagegen, wenn kein effizienter deterministischer Algorithmus für ein Problem bekannt ist. Man gibt sich dann damit zufrieden, dass der randomisierte Algorithmus die korrekte Lösung nur mit hoher Wahrscheinlichkeit findet, wenn dies dafür sehr effizient geschieht. Bei manchen Problemen ist auch dies unerreichbar - man muss dann bereits zufrieden sein, wenn der Algorithmus mit hoher Wahrscheinlichkeit eine sehr gute Näherungslösung findet. Beliebte Anwendungsgebiete für Monte Carlo-Algorithmen sind beispielsweise&lt;br /&gt;
* Randomisierte Primzahl-Tests: Moderne Verschlüsselungsverfahren benötigen zahlreiche Primzahlen, aber exakte Primzahltests sind teuer. Der [http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin-Test] findet effizient Zahlen, die mit sehr hoher Wahrscheinlichkeit tatsächlich Primzahlen sind.&lt;br /&gt;
* Randomisiertes Testen: Wie jeder Test kann auch eine randomisierter Test nicht die Abwesenheit von Programmierfehlern garantieren, aber man kann durch die Randomisierung viel mehr Testfälle generieren und erhöht so die Erfolgswarscheinlichkeit. Wir haben als Beispiel dafür den [[Korrektheit#Beispiel_f.C3.BCr_das_Testen:_Freivalds_Algorithmus|Algorithmus von Freivald]] behandelt.&lt;br /&gt;
* Lösung schwieriger Optimierungsprobleme: Wir zeigen unten, dass ein randomisierter Algorithmus effizient eine Lösung für das 2-SAT-Problem aus dem vorherigen Kapitel findet (für k-SAT mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; liefert der Algorithmus immer noch mit einer gewissen Wahrscheinlichkeit das richtige Ergebnis, ist aber nicht mehr effizient). Einen effizienten Approximationsalgorithmus für des Problem des Handelsreisenden behandlen wir im Kapitel [[NP-Vollständigkeit]]. Weitere wichtige Beispiele für diesen Bereich sind [http://en.wikipedia.org/wiki/Simulated_annealing simulated annealing] und das [http://de.wikipedia.org/wiki/MCMC-Verfahren Markov-Chain-Monte-Carlo-Verfahren].&lt;br /&gt;
* Robuste Statistik: Eine Grundaufgabe der Statistik ist das Anpassen (Fitten) von Modellen an gemessene Werte. Wenn die Messungen jedoch &amp;quot;Ausreißer&amp;quot; (einige völlig falsche Werte) enthalten, geht die Anpassung schief. Wir beschreiben unten den RANSAC-Algorithmus, der die Ausreißer identifizieren und beim Modellfitten ignorieren kann.&lt;br /&gt;
&lt;br /&gt;
Obwohl randomisierte Algorithmen oft einfach und elegant sind, ist ihre theoretische Analyse (also das Führen von Korrektheits- und Komplexitätsbeweisen) häufig sehr schwierig. Man muss fortgeschrittene Methoden der Wahrscheinlichkeitsrechnung und Statistik beherrschen, um die Wahrscheinlichkeit für das Versagen des Algorithmus zu berechnen und um zu zeigen, wie man den Algorithmus benutzt, damit diese Wahrscheinlichkeit unter einer akzeptablen Schranke bleibt. Die Algorithmen, die wir für diese Vorlesung ausgewählt haben, zeichnen sich dadurch aus, dass die Beweise hier einfach zu erbringen sind.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Lösen des K-SAT-Problems ===&lt;br /&gt;
&lt;br /&gt;
Der &amp;lt;b&amp;gt;Algorithmus von Schöning&amp;lt;/b&amp;gt; löst das [[Graphen_und_Graphenalgorithmen#Normalformen für logische Ausdrücke|k-SAT-Problem]] durch Raten: Wenn ein Ausdruck in k-CNF den Wert False hat, gibt es mindestens eine Klausel, die den Wert False hat. Alle Literale in dieser Klausel haben ebenfalls den Wert False, denn jede Klausel ist eine ODER-Verknüpfung, die nur dann False werden kann. Um den Ausdruck zu erfüllen, muss jede Klausel den Wert True annehmen, also müssen wir den Wert von mindestens einem Literal umdrehen. Wenn der Ausruck tatsächlich erfüllbar ist, gibt es immer ein geeignetes Literal, wir wissen nur nicht, welches. Deshalb drehen wir ein unter den k Literalen der betreffenden Klausel zufällig gewähltes. Liegen wir mit unserer Wahl richtig, sind wir der Lösung näher gekommen - im besten Fall sind jetzt alle Klauseln erfüllt. Wählen wir jedoch die falsche Variable, ist die aktuelle Klausel zwar jetzt True, aber dafür werden andere Klauseln zu False, die bisher True waren, und wir entfernen uns somit von der Lösung.&lt;br /&gt;
&lt;br /&gt;
    geg.: logischer Ausdruck in K-CNF (n Variablen, m Klauseln, k Variablen pro Klausel)&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;math&amp;gt;\underbrace {\underbrace {\left(x_1 \vee x_3 \vee...\right)}_{k\; Literale} \wedge \left( x_2 \vee x_4 \vee...\right)}_{m\;Klauseln}&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Algorithmus von Schöning lautet in Pseudocode:&lt;br /&gt;
    for i in range (trials):    #Anzahl der Versuche&lt;br /&gt;
         Bestimme eine Zufallsbelegung der Variablen &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
         for j in range (steps):&lt;br /&gt;
               if &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt; erfüllt alle Klauseln: &lt;br /&gt;
                   return &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
               wähle zufällig eine Klausel, die nicht erfüllt ist und negiere zufällig eine der Variablen in dieser Klausel &lt;br /&gt;
               # (die Klausel ist jetzt erfüllt)&lt;br /&gt;
    return None  # keine Lösung gefunden&lt;br /&gt;
&lt;br /&gt;
Findet der Algorithmus eine Lösung, wissen wir, dass der Ausdruck erfüllbar ist. Andernfalls könnte der Ausdruck unerfüllbar sein, oder wir haben nur Pech gehabt. Je mehr erfolglose Versuche wir machen, desto höher ist die Wahrscheinlichkeit, dass das erste zutrifft.&lt;br /&gt;
&lt;br /&gt;
Es ist sinnvoll, &amp;lt;tt&amp;gt;steps = k*n&amp;lt;/tt&amp;gt; zu wählen. Dann gilt der &lt;br /&gt;
;Satz: Wenn ein Ausdruck in k-CNF mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; erfüllbar ist, muss man im Mittel &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in O\left(\left(\frac{2(k-1)}{k}\right)^n \right)&amp;lt;/math&amp;gt; Versuche machen, um eine Lösung zu finden.&lt;br /&gt;
&lt;br /&gt;
Für &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; gilt stets &amp;lt;math&amp;gt;\frac{2(k-1)}{k} &amp;gt; 1&amp;lt;/math&amp;gt;, man benötigt also eine in n exponentielle Anzahl von Versuchen. Bei &amp;lt;math&amp;gt;k=3&amp;lt;/math&amp;gt; gilt z.B. &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt; \in O\left(\left(\frac{4}{3}\right)^n\right)&amp;lt;/math&amp;gt;. Dies ist zwar im Mittel effizienter also die erschöpfende Suche, die &amp;lt;math&amp;gt;O(2^n)&amp;lt;/math&amp;gt; Schritte benötigt, aber immer noch sehr langsam.&lt;br /&gt;
&lt;br /&gt;
Der Fall &amp;lt;b&amp;gt;&amp;lt;math&amp;gt;k=2&amp;lt;/math&amp;gt; ist jedoch ein Sonderfall&amp;lt;/b&amp;gt;: Hier kann man leicht beweisen, dass eine Lösung im Mittel bereits nach &amp;lt;math&amp;gt;O\left(n^2\right)&amp;lt;/math&amp;gt; Schritten gefunden wird. Wenn man schon weiss, dass der Ausdruck erfüllbar ist (was mit [[Graphen_und_Graphenalgorithmen#Lösung des 2-SAT-Problems mit Implikationgraphen|Implikationgraphen]] leicht geprüft werden kann), lässt man den randomisierten Algorithmus einfach so lange laufen, bis er eine Lösung findet. Man setzt also &amp;lt;tt&amp;gt;step = infinity&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;trials = 1&amp;lt;/tt&amp;gt; und verlässt sich darauf, dass das &amp;lt;tt&amp;gt;return&amp;lt;/tt&amp;gt; mit einer gültigen Lösung früher oder später ausgeführt wird. Dass man darauf im Mittel nur &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte warten muss, zeigen wir jetzt mit Hilfe eines &amp;lt;i&amp;gt;random walk&amp;lt;/i&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''-Zufallsbelegung hat  &amp;lt;math&amp;gt;t\leq n&amp;lt;/math&amp;gt; richtige Variablen (im Mittel &amp;lt;math&amp;gt;t\approx \frac {n} 2&amp;lt;/math&amp;gt;)'''&lt;br /&gt;
&lt;br /&gt;
Negieren einer Variable ändert t um 1,&lt;br /&gt;
u.Z. &amp;lt;math&amp;gt;t\rightarrow t+1&amp;lt;/math&amp;gt; mit Wahrscheinlichkeit &amp;lt;math&amp;gt;\frac 1 2&amp;lt;/math&amp;gt; ::(für beliebiges k: &amp;lt;math&amp;gt;\frac 1 k&amp;lt;/math&amp;gt;)&lt;br /&gt;
::::::::::&amp;lt;math&amp;gt;t\rightarrow t-1&amp;lt;/math&amp;gt; mit Wahrscheinlichkeit &amp;lt;math&amp;gt;\frac 1 2&amp;lt;/math&amp;gt; ::(für beliebiges k: &amp;lt;math&amp;gt;\frac {k-1} k&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''-Wieviele Schritte braucht man im Mittel, um zu einer Lösung mit t Richtigen zu kommen?'''&lt;br /&gt;
&lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(t\right)=\frac 1 2 S\left(t-1\right) + \frac 1 2 S\left(t+1\right) +1&amp;lt;/math&amp;gt;&lt;br /&gt;
       &lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(n\right)=0&amp;lt;/math&amp;gt;    #Abbruchbedingung der Schleife&lt;br /&gt;
       &lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(0\right) = S\left( 1\right) + 1 \Rightarrow S\left(t\right) = n^2-t^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
       '''Probe:''' &lt;br /&gt;
 &lt;br /&gt;
       &amp;lt;math&amp;gt;&lt;br /&gt;
       \begin{align} &lt;br /&gt;
             S\left(n\right) &amp;amp; = n^2-n^2=0 \\&lt;br /&gt;
                  &lt;br /&gt;
             S\left(0\right) &amp;amp;= n^2-0^2 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= S\left(1\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2-1^2+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2 \\&lt;br /&gt;
                 &lt;br /&gt;
              S\left(t\right) &amp;amp;= \frac 1 2 \left(n^2-\left(t-1\right)^2\right) + \frac 1 2 \left(n^2-\left(t+1\right)^2\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= \frac 1 2 n^2-\frac 1 2 \left( t^2-2t+1\right) + \frac 1 2 n^2-\frac 1 2 \left(t^2+2t+1\right) + 1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2-t^2&lt;br /&gt;
       \end{align}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Das ist das Random Walk Problem'''&lt;br /&gt;
&lt;br /&gt;
Im ungünstigsten Fall (t=0) werden im Mittel &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte benötigt, um durch random walk nach t=n zu gelangen.&lt;br /&gt;
&lt;br /&gt;
== 2. RANSAC-ALGORITHMUS (Random Sample Consensus)==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''Aufgabe:''&amp;lt;/u&amp;gt; gegeben: Datenpunkte&lt;br /&gt;
::gesucht: Modell, das die Datenpunkte erklärt&lt;br /&gt;
&lt;br /&gt;
[[Image:Rubto.png|thumb|250px|none]]&lt;br /&gt;
&lt;br /&gt;
'''Messpunkte:'''&lt;br /&gt;
   &lt;br /&gt;
      übliche Lösung: Methode der kleinsten Quadrate&lt;br /&gt;
      &lt;br /&gt;
      &amp;lt;math&amp;gt;\min_{a,b} 	\sum_{i} \left(a x_i + b + y_i\right)^2&amp;lt;/math&amp;gt;&lt;br /&gt;
      &lt;br /&gt;
      Schulmathematik:      &amp;lt;math&amp;gt;Minimum\stackrel{\wedge}{=}Ableitung=0&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lineares Gleichungssystem'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{da}\sum{i} \left(ax_i+b-y_i\right)^2=\sum{i} \frac{d}{da} \left[ax_i+b-y_i\right)^2&amp;lt;/math&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(g\left(x\right)\right)&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(x\right)=x^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;y\left(a\right)=ax_i+b-y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;=\sum_{i}2\left(ax_i+b-y_i\right)\frac{d}{da} \underbrace {ax_i+b-y_i}_{x_i}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\underline {=2\sum_{i}\left(ax_i+b-y_i\right)x_i\stackrel{!}{=}0}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}{x_i}^2+b\sum_{i}x_i=\sum_{i}x_iy_i&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}x_i+b\sum_{i}1=\sum_{i}y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{db}\sum_{i}\left(ax_i+b-y_i\right)^2=2\sum_{i}\left(ax_i+b-y_i\right)*1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:Problem: &amp;lt;math&amp;gt;\epsilon  %&amp;lt;/math&amp;gt; der Datenpunkte sind Outlier&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\Longrightarrow&amp;lt;/math&amp;gt; Einfaches Anpassen des Modells an die Datenpunkte funktioniert nicht&lt;br /&gt;
&lt;br /&gt;
:Seien mindestens k Datenpunkte notwendig, um das Programm anpassen zu können&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
RANSAC-Algorithmus&lt;br /&gt;
&lt;br /&gt;
      for  l in range (trials):&lt;br /&gt;
           wähle zufällig k Punkte aus&lt;br /&gt;
           passe das Modell an die k Punkte an&lt;br /&gt;
           zähle, wieviele Punkte in der Nähe des Modells liegen (d.h. &amp;lt;math&amp;gt;d_i &amp;lt; d_max&amp;lt;/math&amp;gt; muss geschickt gewählt werden) &lt;br /&gt;
                                           #Bsp. Geradenfinden:-wähle a,b aus zwei Punkten&lt;br /&gt;
                                                               -berechne: &amp;lt;math&amp;gt;|ax_i+b-y_i|=d_i&amp;lt;/math&amp;gt;&lt;br /&gt;
                                                               -zähle Punkt i als Inlier, falls &amp;lt;math&amp;gt;d_i&amp;lt;d_ma&amp;lt;/math&amp;gt;&lt;br /&gt;
      return: Modell mit höchster Zahl der Inlier&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;trials= \frac{log\left(1-p\right)}{log\left(1-\left(1-\epsilon\right)^k\right)}&amp;lt;/math&amp;gt;  mit k=Anzahl der Datenpunkte und p=Erfolgswahrscheinlichkeit, &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;=Outlier-Anteil&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Erfolgswahrscheinlichkeit: p=99%'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{|c||c|c|c|c|c|}&lt;br /&gt;
         Beispiel &amp;amp; k &amp;amp; \epsilon=10% &amp;amp; 20% &amp;amp; 50% &amp;amp; 70%\\&lt;br /&gt;
         \hline&lt;br /&gt;
         Linie\;in\;2D &amp;amp; 2 &amp;amp; 3 &amp;amp;5 &amp;amp; 17 &amp;amp; 49\\&lt;br /&gt;
         Kreis\;in\;2D &amp;amp; 3 &amp;amp; 4 &amp;amp; 7 &amp;amp; 35 &amp;amp; 169\\&lt;br /&gt;
         Ebene\;in\;3D &amp;amp; 8 &amp;amp; 9 &amp;amp; 26 &amp;amp; 1172 &amp;amp; 70188\\&lt;br /&gt;
       \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Ein Spiel: Wie viel Schritte braucht man im Mittel zum Ziel?'''&lt;br /&gt;
&lt;br /&gt;
   geg.: 5 Plätze, 2 Personen: eine Person rückt vom einem Platz zu dem enderen Platz;&lt;br /&gt;
         die zweite Person wirft die Münze.&lt;br /&gt;
         Wenn die Münze auf Kopf landet, rücke nach rechts und wenn die Münze auf Zahl landet, rücke nach links.&lt;br /&gt;
         &amp;lt;--- Zahl                                                         Kopf--&amp;gt;&lt;br /&gt;
         Kopf: /////&lt;br /&gt;
         Zahl: /// &lt;br /&gt;
&lt;br /&gt;
:: =&amp;gt; mit 8 Schritten bis zum Ziel&lt;br /&gt;
:im Mittel: bei N Plätzen braucht man N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritte&lt;br /&gt;
&lt;br /&gt;
: all: mit N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritten um N Plätze rücken&lt;br /&gt;
: Wie viel Schritte braucht man im Mittel zum Ziel?&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(N\right)=0&amp;lt;/math&amp;gt;    #wenn wir uns im Stuhl Nr.1 befinden&lt;br /&gt;
           &lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)=\frac 1 2 S\left(1 + S\left(i+1\right)\right) + \frac 1 2 S\left(1 + S\left(i-1\right)\right) = \frac 1 2 S\left(i+1\right) + \frac 1 2 S\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(0\right)=1 + S\left(1\right)&amp;lt;/math&amp;gt;    #bei 0.Platz&lt;br /&gt;
&lt;br /&gt;
:::*Lösung: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:::*speziell: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2&amp;lt;/math&amp;gt;           #wenn man am ungünstigsten Platz startet&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
'''Beziehung zu randomisiertem 2-SAT'''&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Platz &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; &amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert,  &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)=N^2 - \left(\frac N 2\right)^2 = N^2 - \frac N 4 ^2 = \frac 3 4 N^2 &amp;lt;/math&amp;gt;&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)&amp;lt;/math&amp;gt;     # Anfangszustand&lt;br /&gt;
----&lt;br /&gt;
== '''Las Vegas vs. Monte Carlo'''==&lt;br /&gt;
&lt;br /&gt;
   * ''Las Vegas - Algorithmen''&lt;br /&gt;
     - Ergebnis ist immer korrekt.&lt;br /&gt;
     - Berechnung ist mit hoher Wahrscheinlichkeit effizient (d.h. Randomisierung macht den ungünstigsten Fall unwahrscheinlich).&lt;br /&gt;
&lt;br /&gt;
   * ''Monte Carlo - Algorithmen''&lt;br /&gt;
     - Berechnung immer effizient.&lt;br /&gt;
     - Ergebnis mit hoher Wahrscheinlichkeit korrekt (falls kein effizienter Algorithmus bekannt, der immer die richtige Lösung liefert).&lt;br /&gt;
&lt;br /&gt;
{| border = &amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;center&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! Las Vegas&lt;br /&gt;
! Monte Carlo&lt;br /&gt;
|- &lt;br /&gt;
| - Erzeugen einer perfekten Hashfuktion &lt;br /&gt;
| - Algorithmus von Freiwald(Matrizenmultiplikation)&lt;br /&gt;
|-&lt;br /&gt;
| - universelles Hashing&lt;br /&gt;
| - RANSAC&lt;br /&gt;
|-&lt;br /&gt;
| - Quick Sort mit zufälliger Wahl des Pivot-Elements&lt;br /&gt;
| - randomisierte K-SAT(k&amp;gt;=3)(Alg. von Schöning)&lt;br /&gt;
|-&lt;br /&gt;
| - Treep mit zufälligen Prioritäten&lt;br /&gt;
| -&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== ''' Zufallszahlen ''' ==&lt;br /&gt;
&lt;br /&gt;
:- kann man nicht mit deterministischen Computern erzeugen&lt;br /&gt;
:- aber man kann Pseudo-Zufallszahlen erzeugen, die viele Eigenschaften von echten Zufallszahlen haben&lt;br /&gt;
::: * sehr ähnlich  zum Hash&lt;br /&gt;
&lt;br /&gt;
     ''&amp;quot;linear Conguential Random number generator&amp;quot;''&lt;br /&gt;
        &amp;lt;math&amp;gt;I_{i+1}= \left(a*I_i + c\right)\textrm{mod\ } m&amp;lt;/math&amp;gt;&lt;br /&gt;
        &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{=&amp;gt; } &amp;amp; I_i \in [0, m-1]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:-sorgfältige Wahl von  a, c, m notwendig&lt;br /&gt;
::'''Bsp.'''  m = 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;&lt;br /&gt;
::: a = 1664525, c = 1013904223&lt;br /&gt;
::: ''&amp;quot;quick and dirty generator&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
==='''Nachteile'''===&lt;br /&gt;
&lt;br /&gt;
* nicht zufällig genug für viele Anwendungen&lt;br /&gt;
::'''Bsp.''' wähle Punkt in R&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
      \mathrm{ } &amp;amp; p = (rand(), rand(), rand())\\&lt;br /&gt;
&lt;br /&gt;
      \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::gibt Zahl u, v, w so, dass &lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; u * p[0] + v * p[1] + w * p[3]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::stark geclustert ist.&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge ist zu kurz:&lt;br /&gt;
:: spätestens nach m Schritten wiederholt sich die Folge&lt;br /&gt;
&lt;br /&gt;
::'''allgemein''': falls der interne Zustand des Zufallsgenerators ''k'' bits hat, ist Periodenlänge:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; Periode &amp;lt; 2^k\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* ''lowbits'' sind weniger zufällig als die ''highbits''&lt;br /&gt;
----&lt;br /&gt;
=== ''Mersenne Twister''===&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
'''bester zur Zeit bekannter Zufallszahlengenerator (ZZG)'''&lt;br /&gt;
* innere Zustand: &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; 624*32 bit\ Integers  =&amp;gt; 19968 bits\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge: &amp;lt;math&amp;gt;2^ {19937} \approx 4 * 10^{6000}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Punkte aus aufeinanderfolgende Zufallszahlen in &amp;lt;math&amp;gt;\mathbb{R}^n&amp;lt;/math&amp;gt; sind gleich verteilt bis &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; n = 623\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* alle Bits sind unabhängig voneinander zufällig (&amp;quot;Twister&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
* schnell&lt;br /&gt;
&lt;br /&gt;
    class MersenneTwister:&lt;br /&gt;
        &lt;br /&gt;
        def __init__(self, seed):&lt;br /&gt;
            self.N = 624  # Größe des inneren Zustands festlegen&lt;br /&gt;
            self.i = 0    # zählt mit in welchem Zustand wir uns gerade aufhalten&lt;br /&gt;
            &lt;br /&gt;
            self.state = [0]*self.N  # Speicher für den inneren Zustand reservieren&lt;br /&gt;
            &lt;br /&gt;
            self.state[0] = seed     # initiale Zufallszahl vom Benutzer&lt;br /&gt;
            # den Rest des inneren Zustands mit einfachem Zufallszahlengenerator initialisieren&lt;br /&gt;
            for i in xrange(1, self.N):&lt;br /&gt;
                self.state[i] = (1812433253 * (self.state[i-1] ^ (self.state[i-1] &amp;gt;&amp;gt; 30)) + i) % 4294967296&lt;br /&gt;
     &lt;br /&gt;
        def __call__(self):&lt;br /&gt;
            &amp;quot;&amp;quot;&amp;quot;gibt die nächste Zufallszahl im Bereich [0, 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;-1] aus&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
            N, M = self.N, 397&lt;br /&gt;
            &lt;br /&gt;
            # Zustand aktualisieren (neue Zufallszahl ausrechnen)&lt;br /&gt;
            i = self.i&lt;br /&gt;
            r = ((self.state[i] &amp;amp; 0x80000000) | (self.state[(i+1)%N] &amp;amp; 0x7FFFFFFF)) &amp;gt;&amp;gt; 1&lt;br /&gt;
            if self.state[(i+1)%N] &amp;amp; 1:&lt;br /&gt;
                r ^= 0x9908B0DF&lt;br /&gt;
            self.state[i] = self.state[(i+M)%N] ^ r&lt;br /&gt;
     &lt;br /&gt;
            # aktuelle Zufallszahl auslesen und ihre Zufälligkeit durch verwürfeln der Bits verbessern&lt;br /&gt;
            y = self.state[i]&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 11)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt;  7) &amp;amp; 0x9D2C5680)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt; 15) &amp;amp; 0xEFC60000)&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 18)&lt;br /&gt;
            &lt;br /&gt;
            # Zustand weitersetzen und endgültige Zufallszahl ausgeben&lt;br /&gt;
            self.i = (self.i + 1) % N&lt;br /&gt;
            return y&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''geg.:''' Zufallszahl &lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, \overbrace{2^{32}-1}^{m-1}]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ges.:''' Zufallszahl&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, k - 1]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''naive Lösung:'''  &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; rand()%k\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;  ist schlecht.&lt;br /&gt;
&lt;br /&gt;
'''Bsp.'''&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; \qquad m = 16\qquad k = 11\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
! rand() || 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15&lt;br /&gt;
|-&lt;br /&gt;
! rand()%k&lt;br /&gt;
! 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 0 || 1 || 2 || 3 || 4 &lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; 0,...,4 kommt doppelt so häufig wie 5,...,10 &amp;quot;nicht zufällig&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lösung:'''  Zurückweisen des Rests der Zahlen (''rejektion sampling'')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; remainder = (m - 1 - (k - 1))% k = (m - k)%k\\&lt;br /&gt;
        \mathrm{ } &amp;amp; last\ Good\ Value = m-1-remainder\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  r = rand()&lt;br /&gt;
  while r &amp;gt; last.GoodValue:&lt;br /&gt;
        r = rand()&lt;br /&gt;
        return r%k&lt;br /&gt;
&lt;br /&gt;
[[Greedy-Algorithmen und Dynamische Programmierung|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5428</id>
		<title>Randomisierte Algorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5428"/>
				<updated>2012-07-27T17:07:25Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Anwendung: Lösen des K-SAT-Problems */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Randomisierte Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
;Definition: Randomisierte Algorithmen sind Algorithmen, die bei Entscheidungen über ihr weiteres Vorgehen oder bei der Wahl ihrer Parameter Zufallszahlen benutzen.&lt;br /&gt;
&lt;br /&gt;
Anschaulich gesprochen, wersucht man bei randomisierten Algorithmen, einen Teil der Lösung zu &amp;lt;i&amp;gt;raten&amp;lt;/i&amp;gt;. Auf den ersten Blick würde man vermuten, dass dabei nicht viel Sinnvolles herauskommen kann. Diese Kapitel wird jedoch zeigen, dass man durch geschicktes Raten tatsächlich zu sehr eleganten Algorithmen gelangen kann.&lt;br /&gt;
&lt;br /&gt;
Grundsätzlich unterscheidet man zwei Arten von randomisierten Algorithmen:&lt;br /&gt;
;Las Vegas - Algorithmen: Das Ergebnis des Algorithmus ist immer korrekt, und die Berechnung erfolgt mit hoher Wahrscheinlichkeit effizient.&lt;br /&gt;
;Monte Carlo - Algorithmen: Die Berechnung ist immer effizient, und das Ergebnis ist mit hoher Wahrscheinlichkeit korrekt.&lt;br /&gt;
Las Vegas-Algorithmen verwendet man, wenn der Algorithmus im ungünstigen Fall eine schlechte Laufzeit hat, und der ungünstige Fall kann durch die Randomisierung sehr unwahrscheinlich gemacht werden. Wir haben in der Vorlesung schon mehrere Las Vegas-Algorithmen kennen gelernt:&lt;br /&gt;
* Quick Sort mit zufälliger Wahl des Pivot-Elements: Die Randomisierung verhindert, dass das Array immer wieder in Subarrays von sehr unterschiedlicher Größe aufgeteilt wird.&lt;br /&gt;
* Treap mit zufälligen Prioritäten: Die Randomisierung verhindert, dass der Baum schlecht balanciert ist.&lt;br /&gt;
* Universelles Hashing: Die zufällige Wahl der Hashfunktion verhindert, dass ein Angreifer eine Schlüsselmenge mit sehr vielen Kollisionen konstruieren kann.&lt;br /&gt;
* Erzeugung einer perfekten Hashfunktion: Durch die Randomisierung entsteht mit nach wenigen Versuchen ein zyklenfreier Graph, der zur Definition der Hashfunktion geeignet ist.&lt;br /&gt;
Monte Carlo-Algorithmen verwendet man dagegen, wenn kein effizienter deterministischer Algorithmus für ein Problem bekannt ist. Man gibt sich dann damit zufrieden, dass der randomisierte Algorithmus die korrekte Lösung nur mit hoher Wahrscheinlichkeit findet, wenn dies dafür sehr effizient geschieht. Bei manchen Problemen ist auch dies unerreichbar - man muss dann bereits zufrieden sein, wenn der Algorithmus mit hoher Wahrscheinlichkeit eine sehr gute Näherungslösung findet. Beliebte Anwendungsgebiete für Monte Carlo-Algorithmen sind beispielsweise&lt;br /&gt;
* Randomisierte Primzahl-Tests: Moderne Verschlüsselungsverfahren benötigen zahlreiche Primzahlen, aber exakte Primzahltests sind teuer. Der [http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin-Test] findet effizient Zahlen, die mit sehr hoher Wahrscheinlichkeit tatsächlich Primzahlen sind.&lt;br /&gt;
* Randomisiertes Testen: Wie jeder Test kann auch eine randomisierter Test nicht die Abwesenheit von Programmierfehlern garantieren, aber man kann durch die Randomisierung viel mehr Testfälle generieren und erhöht so die Erfolgswarscheinlichkeit. Wir haben als Beispiel dafür den [[Korrektheit#Beispiel_f.C3.BCr_das_Testen:_Freivalds_Algorithmus|Algorithmus von Freivald]] behandelt.&lt;br /&gt;
* Lösung schwieriger Optimierungsprobleme: Wir zeigen unten, dass ein randomisierter Algorithmus effizient eine Lösung für das 2-SAT-Problem aus dem vorherigen Kapitel findet (für k-SAT mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; liefert der Algorithmus immer noch mit einer gewissen Wahrscheinlichkeit das richtige Ergebnis, ist aber nicht mehr effizient). Einen effizienten Approximationsalgorithmus für des Problem des Handelsreisenden behandlen wir im Kapitel [[NP-Vollständigkeit]]. Weitere wichtige Beispiele für diesen Bereich sind [http://en.wikipedia.org/wiki/Simulated_annealing simulated annealing] und das [http://de.wikipedia.org/wiki/MCMC-Verfahren Markov-Chain-Monte-Carlo-Verfahren].&lt;br /&gt;
* Robuste Statistik: Eine Grundaufgabe der Statistik ist das Anpassen (Fitten) von Modellen an gemessene Werte. Wenn die Messungen jedoch &amp;quot;Ausreißer&amp;quot; (einige völlig falsche Werte) enthalten, geht die Anpassung schief. Wir beschreiben unten den RANSAC-Algorithmus, der die Ausreißer identifizieren und beim Modellfitten ignorieren kann.&lt;br /&gt;
&lt;br /&gt;
Obwohl randomisierte Algorithmen oft einfach und elegant sind, ist ihre theoretische Analyse (also das Führen von Korrektheits- und Komplexitätsbeweisen) häufig sehr schwierig. Man muss fortgeschrittene Methoden der Wahrscheinlichkeitsrechnung und Statistik beherrschen, um die Wahrscheinlichkeit für das Versagen des Algorithmus zu berechnen und um zu zeigen, wie man den Algorithmus benutzt, damit diese Wahrscheinlichkeit unter einer akzeptablen Schranke bleibt. Die Algorithmen, die wir für diese Vorlesung ausgewählt haben, zeichnen sich dadurch aus, dass die Beweise hier einfach zu erbringen sind.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Lösen des K-SAT-Problems ===&lt;br /&gt;
&lt;br /&gt;
Der &amp;lt;b&amp;gt;Algorithmus von Schöning&amp;lt;/b&amp;gt; löst das [[Graphen_und_Graphenalgorithmen#Normalformen für logische Ausdrücke|k-SAT-Problem]] durch Raten: Wenn ein Ausdruck in k-CNF den Wert False hat, gibt es mindestens eine Klausel, die den Wert False hat. Alle Literale in dieser Klausel haben ebenfalls den Wert False, denn jede Klausel ist eine ODER-Verknüpfung, die nur dann False werden kann. Um den Ausdruck zu erfüllen, muss jede Klausel den Wert True annehmen, also müssen wir den Wert von mindestens einem Literal umdrehen. Wenn der Ausruck tatsächlich erfüllbar ist, gibt es immer ein geeignetes Literal, wir wissen nur nicht, welches. Deshalb drehen wir ein unter den k Literalen der betreffenden Klausel zufällig gewähltes. Liegen wir mit unserer Wahl richtig, sind wir der Lösung näher gekommen - im besten Fall sind jetzt alle Klauseln erfüllt. Wählen wir jedoch die falsche Variable, ist die aktuelle Klausel zwar jetzt True, aber dafür werden andere Klauseln zu False, die bisher True waren, und wir entfernen uns somit von der Lösung.&lt;br /&gt;
&lt;br /&gt;
    geg.: logischer Ausdruck in K-CNF (n Variablen, m Klauseln, k Variablen pro Klausel)&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;math&amp;gt;\underbrace {\underbrace {\left(x_1 \vee x_3 \vee...\right)}_{k\; Literale} \wedge \left( x_2 \vee x_4 \vee...\right)}_{m\;Klauseln}&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Algorithmus von Schöning lautet in Pseudocode:&lt;br /&gt;
    for i in range (trials):    #Anzahl der Versuche&lt;br /&gt;
         Bestimme eine Zufallsbelegung der Variablen &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
         for j in range (steps):&lt;br /&gt;
               if &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt; erfüllt alle Klauseln: &lt;br /&gt;
                   return &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
               wähle zufällig eine Klausel, die nicht erfüllt ist und negiere zufällig eine der Variablen in dieser Klausel &lt;br /&gt;
               # (die Klausel ist jetzt erfüllt)&lt;br /&gt;
    return None  # keine Lösung gefunden&lt;br /&gt;
&lt;br /&gt;
Findet der Algorithmus eine Lösung, wissen wir, dass der Ausdruck erfüllbar ist. Andernfalls könnte der Ausdruck unerfüllbar sein, oder wir haben nur Pech gehabt. Je mehr erfolglose Versuche wir machen, desto höher ist die Wahrscheinlichkeit, dass das erste zutrifft.&lt;br /&gt;
&lt;br /&gt;
Es ist sinnvoll, &amp;lt;tt&amp;gt;steps = k*n&amp;lt;/tt&amp;gt; zu wählen. Dann gilt der &lt;br /&gt;
;Satz: Wenn ein Ausdruck in k-CNF mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; erfüllbar ist, muss man im Mittel &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in O\left(\left(\frac{2(k-1)}{k}\right)^n \right)&amp;lt;/math&amp;gt; Versuche machen, um eine Lösung zu finden.&lt;br /&gt;
&lt;br /&gt;
Es gilt stets &amp;lt;math&amp;gt;\frac{2(k-1)}{k} &amp;gt; 1&amp;lt;/math&amp;gt;, man benötigt also eine in n exponentielle Anzahl von Versuchen. Bei &amp;lt;math&amp;gt;k=3&amp;lt;/math&amp;gt; gilt z.B. &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt; \in O\left(\left(\frac{4}{3}\right)^n\right)&amp;lt;/math&amp;gt;. Dies ist zwar im Mittel effizienter also die erschöpfende Suche, die &amp;lt;math&amp;gt;O(2^n)&amp;lt;/math&amp;gt; Schritte benötigt, aber immer noch sehr langsam.&lt;br /&gt;
&lt;br /&gt;
Der Fall &amp;lt;b&amp;gt;&amp;lt;math&amp;gt;k=2&amp;lt;/math&amp;gt; ist jedoch ein Sonderfall&amp;lt;/b&amp;gt;: Hier kann man leicht beweisen, dass eine Lösung im Mittel bereits nach &amp;lt;math&amp;gt;O\left(n^2\right)&amp;lt;/math&amp;gt; Schritten gefunden wird. Wenn man schon weiss, dass der Ausdruck erfüllbar ist (was mit [[Graphen_und_Graphenalgorithmen#Lösung des 2-SAT-Problems mit Implikationgraphen|Implikationgraphen]] leicht geprüft werden kann), lässt man den randomisierten Algorithmus einfach so lange laufen, bis er eine Lösung findet. Man setzt also &amp;lt;tt&amp;gt;step = infinity&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;trials = 1&amp;lt;/tt&amp;gt; und verlässt sich darauf, dass das &amp;lt;tt&amp;gt;return&amp;lt;/tt&amp;gt; mit einer gültigen Lösung früher oder später ausgeführt wird. Dass man darauf im Mittel nur &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte warten muss, zeigen wir jetzt mit Hilfe eines &amp;lt;i&amp;gt;random walk&amp;lt;/i&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''-Zufallsbelegung hat  &amp;lt;math&amp;gt;t\leq n&amp;lt;/math&amp;gt; richtige Variablen (im Mittel &amp;lt;math&amp;gt;t\approx \frac {n} 2&amp;lt;/math&amp;gt;)'''&lt;br /&gt;
&lt;br /&gt;
Negieren einer Variable ändert t um 1,&lt;br /&gt;
u.Z. &amp;lt;math&amp;gt;t\rightarrow t+1&amp;lt;/math&amp;gt; mit Wahrscheinlichkeit &amp;lt;math&amp;gt;\frac 1 2&amp;lt;/math&amp;gt; ::(für beliebiges k: &amp;lt;math&amp;gt;\frac 1 k&amp;lt;/math&amp;gt;)&lt;br /&gt;
::::::::::&amp;lt;math&amp;gt;t\rightarrow t-1&amp;lt;/math&amp;gt; mit Wahrscheinlichkeit &amp;lt;math&amp;gt;\frac 1 2&amp;lt;/math&amp;gt; ::(für beliebiges k: &amp;lt;math&amp;gt;\frac {k-1} k&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''-Wieviele Schritte braucht man im Mittel, um zu einer Lösung mit t Richtigen zu kommen?'''&lt;br /&gt;
&lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(t\right)=\frac 1 2 S\left(t-1\right) + \frac 1 2 S\left(t+1\right) +1&amp;lt;/math&amp;gt;&lt;br /&gt;
       &lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(n\right)=0&amp;lt;/math&amp;gt;    #Abbruchbedingung der Schleife&lt;br /&gt;
       &lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(0\right) = S\left( 1\right) + 1 \Rightarrow S\left(t\right) = n^2-t^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
       '''Probe:''' &lt;br /&gt;
 &lt;br /&gt;
       &amp;lt;math&amp;gt;&lt;br /&gt;
       \begin{align} &lt;br /&gt;
             S\left(n\right) &amp;amp; = n^2-n^2=0 \\&lt;br /&gt;
                  &lt;br /&gt;
             S\left(0\right) &amp;amp;= n^2-0^2 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= S\left(1\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2-1^2+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2 \\&lt;br /&gt;
                 &lt;br /&gt;
              S\left(t\right) &amp;amp;= \frac 1 2 \left(n^2-\left(t-1\right)^2\right) + \frac 1 2 \left(n^2-\left(t+1\right)^2\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= \frac 1 2 n^2-\frac 1 2 \left( t^2-2t+1\right) + \frac 1 2 n^2-\frac 1 2 \left(t^2+2t+1\right) + 1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2-t^2&lt;br /&gt;
       \end{align}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Das ist das Random Walk Problem'''&lt;br /&gt;
&lt;br /&gt;
Im ungünstigsten Fall (t=0) werden im Mittel &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte benötigt, um durch random walk nach t=n zu gelangen.&lt;br /&gt;
&lt;br /&gt;
== 2. RANSAC-ALGORITHMUS (Random Sample Consensus)==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''Aufgabe:''&amp;lt;/u&amp;gt; gegeben: Datenpunkte&lt;br /&gt;
::gesucht: Modell, das die Datenpunkte erklärt&lt;br /&gt;
&lt;br /&gt;
[[Image:Rubto.png|thumb|250px|none]]&lt;br /&gt;
&lt;br /&gt;
'''Messpunkte:'''&lt;br /&gt;
   &lt;br /&gt;
      übliche Lösung: Methode der kleinsten Quadrate&lt;br /&gt;
      &lt;br /&gt;
      &amp;lt;math&amp;gt;\min_{a,b} 	\sum_{i} \left(a x_i + b + y_i\right)^2&amp;lt;/math&amp;gt;&lt;br /&gt;
      &lt;br /&gt;
      Schulmathematik:      &amp;lt;math&amp;gt;Minimum\stackrel{\wedge}{=}Ableitung=0&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lineares Gleichungssystem'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{da}\sum{i} \left(ax_i+b-y_i\right)^2=\sum{i} \frac{d}{da} \left[ax_i+b-y_i\right)^2&amp;lt;/math&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(g\left(x\right)\right)&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(x\right)=x^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;y\left(a\right)=ax_i+b-y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;=\sum_{i}2\left(ax_i+b-y_i\right)\frac{d}{da} \underbrace {ax_i+b-y_i}_{x_i}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\underline {=2\sum_{i}\left(ax_i+b-y_i\right)x_i\stackrel{!}{=}0}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}{x_i}^2+b\sum_{i}x_i=\sum_{i}x_iy_i&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}x_i+b\sum_{i}1=\sum_{i}y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{db}\sum_{i}\left(ax_i+b-y_i\right)^2=2\sum_{i}\left(ax_i+b-y_i\right)*1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:Problem: &amp;lt;math&amp;gt;\epsilon  %&amp;lt;/math&amp;gt; der Datenpunkte sind Outlier&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\Longrightarrow&amp;lt;/math&amp;gt; Einfaches Anpassen des Modells an die Datenpunkte funktioniert nicht&lt;br /&gt;
&lt;br /&gt;
:Seien mindestens k Datenpunkte notwendig, um das Programm anpassen zu können&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
RANSAC-Algorithmus&lt;br /&gt;
&lt;br /&gt;
      for  l in range (trials):&lt;br /&gt;
           wähle zufällig k Punkte aus&lt;br /&gt;
           passe das Modell an die k Punkte an&lt;br /&gt;
           zähle, wieviele Punkte in der Nähe des Modells liegen (d.h. &amp;lt;math&amp;gt;d_i &amp;lt; d_max&amp;lt;/math&amp;gt; muss geschickt gewählt werden) &lt;br /&gt;
                                           #Bsp. Geradenfinden:-wähle a,b aus zwei Punkten&lt;br /&gt;
                                                               -berechne: &amp;lt;math&amp;gt;|ax_i+b-y_i|=d_i&amp;lt;/math&amp;gt;&lt;br /&gt;
                                                               -zähle Punkt i als Inlier, falls &amp;lt;math&amp;gt;d_i&amp;lt;d_ma&amp;lt;/math&amp;gt;&lt;br /&gt;
      return: Modell mit höchster Zahl der Inlier&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;trials= \frac{log\left(1-p\right)}{log\left(1-\left(1-\epsilon\right)^k\right)}&amp;lt;/math&amp;gt;  mit k=Anzahl der Datenpunkte und p=Erfolgswahrscheinlichkeit, &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;=Outlier-Anteil&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Erfolgswahrscheinlichkeit: p=99%'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{|c||c|c|c|c|c|}&lt;br /&gt;
         Beispiel &amp;amp; k &amp;amp; \epsilon=10% &amp;amp; 20% &amp;amp; 50% &amp;amp; 70%\\&lt;br /&gt;
         \hline&lt;br /&gt;
         Linie\;in\;2D &amp;amp; 2 &amp;amp; 3 &amp;amp;5 &amp;amp; 17 &amp;amp; 49\\&lt;br /&gt;
         Kreis\;in\;2D &amp;amp; 3 &amp;amp; 4 &amp;amp; 7 &amp;amp; 35 &amp;amp; 169\\&lt;br /&gt;
         Ebene\;in\;3D &amp;amp; 8 &amp;amp; 9 &amp;amp; 26 &amp;amp; 1172 &amp;amp; 70188\\&lt;br /&gt;
       \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Ein Spiel: Wie viel Schritte braucht man im Mittel zum Ziel?'''&lt;br /&gt;
&lt;br /&gt;
   geg.: 5 Plätze, 2 Personen: eine Person rückt vom einem Platz zu dem enderen Platz;&lt;br /&gt;
         die zweite Person wirft die Münze.&lt;br /&gt;
         Wenn die Münze auf Kopf landet, rücke nach rechts und wenn die Münze auf Zahl landet, rücke nach links.&lt;br /&gt;
         &amp;lt;--- Zahl                                                         Kopf--&amp;gt;&lt;br /&gt;
         Kopf: /////&lt;br /&gt;
         Zahl: /// &lt;br /&gt;
&lt;br /&gt;
:: =&amp;gt; mit 8 Schritten bis zum Ziel&lt;br /&gt;
:im Mittel: bei N Plätzen braucht man N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritte&lt;br /&gt;
&lt;br /&gt;
: all: mit N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritten um N Plätze rücken&lt;br /&gt;
: Wie viel Schritte braucht man im Mittel zum Ziel?&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(N\right)=0&amp;lt;/math&amp;gt;    #wenn wir uns im Stuhl Nr.1 befinden&lt;br /&gt;
           &lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)=\frac 1 2 S\left(1 + S\left(i+1\right)\right) + \frac 1 2 S\left(1 + S\left(i-1\right)\right) = \frac 1 2 S\left(i+1\right) + \frac 1 2 S\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(0\right)=1 + S\left(1\right)&amp;lt;/math&amp;gt;    #bei 0.Platz&lt;br /&gt;
&lt;br /&gt;
:::*Lösung: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:::*speziell: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2&amp;lt;/math&amp;gt;           #wenn man am ungünstigsten Platz startet&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
'''Beziehung zu randomisiertem 2-SAT'''&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Platz &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; &amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert,  &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)=N^2 - \left(\frac N 2\right)^2 = N^2 - \frac N 4 ^2 = \frac 3 4 N^2 &amp;lt;/math&amp;gt;&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)&amp;lt;/math&amp;gt;     # Anfangszustand&lt;br /&gt;
----&lt;br /&gt;
== '''Las Vegas vs. Monte Carlo'''==&lt;br /&gt;
&lt;br /&gt;
   * ''Las Vegas - Algorithmen''&lt;br /&gt;
     - Ergebnis ist immer korrekt.&lt;br /&gt;
     - Berechnung ist mit hoher Wahrscheinlichkeit effizient (d.h. Randomisierung macht den ungünstigsten Fall unwahrscheinlich).&lt;br /&gt;
&lt;br /&gt;
   * ''Monte Carlo - Algorithmen''&lt;br /&gt;
     - Berechnung immer effizient.&lt;br /&gt;
     - Ergebnis mit hoher Wahrscheinlichkeit korrekt (falls kein effizienter Algorithmus bekannt, der immer die richtige Lösung liefert).&lt;br /&gt;
&lt;br /&gt;
{| border = &amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;center&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! Las Vegas&lt;br /&gt;
! Monte Carlo&lt;br /&gt;
|- &lt;br /&gt;
| - Erzeugen einer perfekten Hashfuktion &lt;br /&gt;
| - Algorithmus von Freiwald(Matrizenmultiplikation)&lt;br /&gt;
|-&lt;br /&gt;
| - universelles Hashing&lt;br /&gt;
| - RANSAC&lt;br /&gt;
|-&lt;br /&gt;
| - Quick Sort mit zufälliger Wahl des Pivot-Elements&lt;br /&gt;
| - randomisierte K-SAT(k&amp;gt;=3)(Alg. von Schöning)&lt;br /&gt;
|-&lt;br /&gt;
| - Treep mit zufälligen Prioritäten&lt;br /&gt;
| -&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== ''' Zufallszahlen ''' ==&lt;br /&gt;
&lt;br /&gt;
:- kann man nicht mit deterministischen Computern erzeugen&lt;br /&gt;
:- aber man kann Pseudo-Zufallszahlen erzeugen, die viele Eigenschaften von echten Zufallszahlen haben&lt;br /&gt;
::: * sehr ähnlich  zum Hash&lt;br /&gt;
&lt;br /&gt;
     ''&amp;quot;linear Conguential Random number generator&amp;quot;''&lt;br /&gt;
        &amp;lt;math&amp;gt;I_{i+1}= \left(a*I_i + c\right)\textrm{mod\ } m&amp;lt;/math&amp;gt;&lt;br /&gt;
        &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{=&amp;gt; } &amp;amp; I_i \in [0, m-1]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:-sorgfältige Wahl von  a, c, m notwendig&lt;br /&gt;
::'''Bsp.'''  m = 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;&lt;br /&gt;
::: a = 1664525, c = 1013904223&lt;br /&gt;
::: ''&amp;quot;quick and dirty generator&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
==='''Nachteile'''===&lt;br /&gt;
&lt;br /&gt;
* nicht zufällig genug für viele Anwendungen&lt;br /&gt;
::'''Bsp.''' wähle Punkt in R&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
      \mathrm{ } &amp;amp; p = (rand(), rand(), rand())\\&lt;br /&gt;
&lt;br /&gt;
      \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::gibt Zahl u, v, w so, dass &lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; u * p[0] + v * p[1] + w * p[3]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::stark geclustert ist.&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge ist zu kurz:&lt;br /&gt;
:: spätestens nach m Schritten wiederholt sich die Folge&lt;br /&gt;
&lt;br /&gt;
::'''allgemein''': falls der interne Zustand des Zufallsgenerators ''k'' bits hat, ist Periodenlänge:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; Periode &amp;lt; 2^k\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* ''lowbits'' sind weniger zufällig als die ''highbits''&lt;br /&gt;
----&lt;br /&gt;
=== ''Mersenne Twister''===&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
'''bester zur Zeit bekannter Zufallszahlengenerator (ZZG)'''&lt;br /&gt;
* innere Zustand: &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; 624*32 bit\ Integers  =&amp;gt; 19968 bits\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge: &amp;lt;math&amp;gt;2^ {19937} \approx 4 * 10^{6000}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Punkte aus aufeinanderfolgende Zufallszahlen in &amp;lt;math&amp;gt;\mathbb{R}^n&amp;lt;/math&amp;gt; sind gleich verteilt bis &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; n = 623\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* alle Bits sind unabhängig voneinander zufällig (&amp;quot;Twister&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
* schnell&lt;br /&gt;
&lt;br /&gt;
    class MersenneTwister:&lt;br /&gt;
        &lt;br /&gt;
        def __init__(self, seed):&lt;br /&gt;
            self.N = 624  # Größe des inneren Zustands festlegen&lt;br /&gt;
            self.i = 0    # zählt mit in welchem Zustand wir uns gerade aufhalten&lt;br /&gt;
            &lt;br /&gt;
            self.state = [0]*self.N  # Speicher für den inneren Zustand reservieren&lt;br /&gt;
            &lt;br /&gt;
            self.state[0] = seed     # initiale Zufallszahl vom Benutzer&lt;br /&gt;
            # den Rest des inneren Zustands mit einfachem Zufallszahlengenerator initialisieren&lt;br /&gt;
            for i in xrange(1, self.N):&lt;br /&gt;
                self.state[i] = (1812433253 * (self.state[i-1] ^ (self.state[i-1] &amp;gt;&amp;gt; 30)) + i) % 4294967296&lt;br /&gt;
     &lt;br /&gt;
        def __call__(self):&lt;br /&gt;
            &amp;quot;&amp;quot;&amp;quot;gibt die nächste Zufallszahl im Bereich [0, 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;-1] aus&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
            N, M = self.N, 397&lt;br /&gt;
            &lt;br /&gt;
            # Zustand aktualisieren (neue Zufallszahl ausrechnen)&lt;br /&gt;
            i = self.i&lt;br /&gt;
            r = ((self.state[i] &amp;amp; 0x80000000) | (self.state[(i+1)%N] &amp;amp; 0x7FFFFFFF)) &amp;gt;&amp;gt; 1&lt;br /&gt;
            if self.state[(i+1)%N] &amp;amp; 1:&lt;br /&gt;
                r ^= 0x9908B0DF&lt;br /&gt;
            self.state[i] = self.state[(i+M)%N] ^ r&lt;br /&gt;
     &lt;br /&gt;
            # aktuelle Zufallszahl auslesen und ihre Zufälligkeit durch verwürfeln der Bits verbessern&lt;br /&gt;
            y = self.state[i]&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 11)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt;  7) &amp;amp; 0x9D2C5680)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt; 15) &amp;amp; 0xEFC60000)&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 18)&lt;br /&gt;
            &lt;br /&gt;
            # Zustand weitersetzen und endgültige Zufallszahl ausgeben&lt;br /&gt;
            self.i = (self.i + 1) % N&lt;br /&gt;
            return y&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''geg.:''' Zufallszahl &lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, \overbrace{2^{32}-1}^{m-1}]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ges.:''' Zufallszahl&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, k - 1]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''naive Lösung:'''  &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; rand()%k\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;  ist schlecht.&lt;br /&gt;
&lt;br /&gt;
'''Bsp.'''&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; \qquad m = 16\qquad k = 11\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
! rand() || 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15&lt;br /&gt;
|-&lt;br /&gt;
! rand()%k&lt;br /&gt;
! 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 0 || 1 || 2 || 3 || 4 &lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; 0,...,4 kommt doppelt so häufig wie 5,...,10 &amp;quot;nicht zufällig&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lösung:'''  Zurückweisen des Rests der Zahlen (''rejektion sampling'')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; remainder = (m - 1 - (k - 1))% k = (m - k)%k\\&lt;br /&gt;
        \mathrm{ } &amp;amp; last\ Good\ Value = m-1-remainder\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  r = rand()&lt;br /&gt;
  while r &amp;gt; last.GoodValue:&lt;br /&gt;
        r = rand()&lt;br /&gt;
        return r%k&lt;br /&gt;
&lt;br /&gt;
[[Greedy-Algorithmen und Dynamische Programmierung|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5427</id>
		<title>Randomisierte Algorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5427"/>
				<updated>2012-07-27T17:06:16Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Anwendung: Lösen des K-SAT-Problems */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Randomisierte Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
;Definition: Randomisierte Algorithmen sind Algorithmen, die bei Entscheidungen über ihr weiteres Vorgehen oder bei der Wahl ihrer Parameter Zufallszahlen benutzen.&lt;br /&gt;
&lt;br /&gt;
Anschaulich gesprochen, wersucht man bei randomisierten Algorithmen, einen Teil der Lösung zu &amp;lt;i&amp;gt;raten&amp;lt;/i&amp;gt;. Auf den ersten Blick würde man vermuten, dass dabei nicht viel Sinnvolles herauskommen kann. Diese Kapitel wird jedoch zeigen, dass man durch geschicktes Raten tatsächlich zu sehr eleganten Algorithmen gelangen kann.&lt;br /&gt;
&lt;br /&gt;
Grundsätzlich unterscheidet man zwei Arten von randomisierten Algorithmen:&lt;br /&gt;
;Las Vegas - Algorithmen: Das Ergebnis des Algorithmus ist immer korrekt, und die Berechnung erfolgt mit hoher Wahrscheinlichkeit effizient.&lt;br /&gt;
;Monte Carlo - Algorithmen: Die Berechnung ist immer effizient, und das Ergebnis ist mit hoher Wahrscheinlichkeit korrekt.&lt;br /&gt;
Las Vegas-Algorithmen verwendet man, wenn der Algorithmus im ungünstigen Fall eine schlechte Laufzeit hat, und der ungünstige Fall kann durch die Randomisierung sehr unwahrscheinlich gemacht werden. Wir haben in der Vorlesung schon mehrere Las Vegas-Algorithmen kennen gelernt:&lt;br /&gt;
* Quick Sort mit zufälliger Wahl des Pivot-Elements: Die Randomisierung verhindert, dass das Array immer wieder in Subarrays von sehr unterschiedlicher Größe aufgeteilt wird.&lt;br /&gt;
* Treap mit zufälligen Prioritäten: Die Randomisierung verhindert, dass der Baum schlecht balanciert ist.&lt;br /&gt;
* Universelles Hashing: Die zufällige Wahl der Hashfunktion verhindert, dass ein Angreifer eine Schlüsselmenge mit sehr vielen Kollisionen konstruieren kann.&lt;br /&gt;
* Erzeugung einer perfekten Hashfunktion: Durch die Randomisierung entsteht mit nach wenigen Versuchen ein zyklenfreier Graph, der zur Definition der Hashfunktion geeignet ist.&lt;br /&gt;
Monte Carlo-Algorithmen verwendet man dagegen, wenn kein effizienter deterministischer Algorithmus für ein Problem bekannt ist. Man gibt sich dann damit zufrieden, dass der randomisierte Algorithmus die korrekte Lösung nur mit hoher Wahrscheinlichkeit findet, wenn dies dafür sehr effizient geschieht. Bei manchen Problemen ist auch dies unerreichbar - man muss dann bereits zufrieden sein, wenn der Algorithmus mit hoher Wahrscheinlichkeit eine sehr gute Näherungslösung findet. Beliebte Anwendungsgebiete für Monte Carlo-Algorithmen sind beispielsweise&lt;br /&gt;
* Randomisierte Primzahl-Tests: Moderne Verschlüsselungsverfahren benötigen zahlreiche Primzahlen, aber exakte Primzahltests sind teuer. Der [http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin-Test] findet effizient Zahlen, die mit sehr hoher Wahrscheinlichkeit tatsächlich Primzahlen sind.&lt;br /&gt;
* Randomisiertes Testen: Wie jeder Test kann auch eine randomisierter Test nicht die Abwesenheit von Programmierfehlern garantieren, aber man kann durch die Randomisierung viel mehr Testfälle generieren und erhöht so die Erfolgswarscheinlichkeit. Wir haben als Beispiel dafür den [[Korrektheit#Beispiel_f.C3.BCr_das_Testen:_Freivalds_Algorithmus|Algorithmus von Freivald]] behandelt.&lt;br /&gt;
* Lösung schwieriger Optimierungsprobleme: Wir zeigen unten, dass ein randomisierter Algorithmus effizient eine Lösung für das 2-SAT-Problem aus dem vorherigen Kapitel findet (für k-SAT mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; liefert der Algorithmus immer noch mit einer gewissen Wahrscheinlichkeit das richtige Ergebnis, ist aber nicht mehr effizient). Einen effizienten Approximationsalgorithmus für des Problem des Handelsreisenden behandlen wir im Kapitel [[NP-Vollständigkeit]]. Weitere wichtige Beispiele für diesen Bereich sind [http://en.wikipedia.org/wiki/Simulated_annealing simulated annealing] und das [http://de.wikipedia.org/wiki/MCMC-Verfahren Markov-Chain-Monte-Carlo-Verfahren].&lt;br /&gt;
* Robuste Statistik: Eine Grundaufgabe der Statistik ist das Anpassen (Fitten) von Modellen an gemessene Werte. Wenn die Messungen jedoch &amp;quot;Ausreißer&amp;quot; (einige völlig falsche Werte) enthalten, geht die Anpassung schief. Wir beschreiben unten den RANSAC-Algorithmus, der die Ausreißer identifizieren und beim Modellfitten ignorieren kann.&lt;br /&gt;
&lt;br /&gt;
Obwohl randomisierte Algorithmen oft einfach und elegant sind, ist ihre theoretische Analyse (also das Führen von Korrektheits- und Komplexitätsbeweisen) häufig sehr schwierig. Man muss fortgeschrittene Methoden der Wahrscheinlichkeitsrechnung und Statistik beherrschen, um die Wahrscheinlichkeit für das Versagen des Algorithmus zu berechnen und um zu zeigen, wie man den Algorithmus benutzt, damit diese Wahrscheinlichkeit unter einer akzeptablen Schranke bleibt. Die Algorithmen, die wir für diese Vorlesung ausgewählt haben, zeichnen sich dadurch aus, dass die Beweise hier einfach zu erbringen sind.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Lösen des K-SAT-Problems ===&lt;br /&gt;
&lt;br /&gt;
Der &amp;lt;b&amp;gt;Algorithmus von Schöning&amp;lt;/b&amp;gt; löst das [[Graphen_und_Graphenalgorithmen#Normalformen für logische Ausdrücke|k-SAT-Problem]] durch Raten: Wenn ein Ausdruck in k-CNF den Wert False hat, gibt es mindestens eine Klausel, die den Wert False hat. Alle Literale in dieser Klausel haben ebenfalls den Wert False, denn jede Klausel ist eine ODER-Verknüpfung, die nur dann False werden kann. Um den Ausdruck zu erfüllen, muss jede Klausel den Wert True annehmen, also müssen wir den Wert von mindestens einem Literal umdrehen. Wenn der Ausruck tatsächlich erfüllbar ist, gibt es immer ein geeignetes Literal, wir wissen nur nicht, welches. Deshalb drehen wir ein unter den k Literalen der betreffenden Klausel zufällig gewähltes. Liegen wir mit unserer Wahl richtig, sind wir der Lösung näher gekommen - im besten Fall sind jetzt alle Klauseln erfüllt. Wählen wir jedoch die falsche Variable, ist die aktuelle Klausel zwar jetzt True, aber dafür werden andere Klauseln zu False, die bisher True waren, und wir entfernen uns somit von der Lösung.&lt;br /&gt;
&lt;br /&gt;
    geg.: logischer Ausdruck in K-CNF (n Variablen, m Klauseln, k Variablen pro Klausel)&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;math&amp;gt;\underbrace {\underbrace {\left(x_1 \vee x_3 \vee...\right)}_{k\; Literale} \wedge \left( x_2 \vee x_4 \vee...\right)}_{m\;Klauseln}&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Algorithmus von Schöning lautet in Pseudocode:&lt;br /&gt;
    for i in range (trials):    #Anzahl der Versuche&lt;br /&gt;
         # Bestimme eine Zufallsbelegung der Variablen &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;:&lt;br /&gt;
         for j in range (steps):&lt;br /&gt;
               if &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt; erfüllt alle Klauseln: &lt;br /&gt;
                   return &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
               # wähle zufällig eine Klausel, die nicht erfüllt ist und negiere zufällig eine der Variablen in dieser Klausel &lt;br /&gt;
               # (die Klausel ist jetzt erfüllt)&lt;br /&gt;
    return None  # keine Lösung gefunden&lt;br /&gt;
&lt;br /&gt;
Findet der Algorithmus eine Lösung, wissen wir, dass der Ausdruck erfüllbar ist. Andernfalls könnte der Ausdruck unerfüllbar sein, oder wir haben nur Pech gehabt. Je mehr erfolglose Versuche wir machen, desto höher ist die Wahrscheinlichkeit, dass das erste zutrifft.&lt;br /&gt;
&lt;br /&gt;
Es ist sinnvoll, &amp;lt;tt&amp;gt;steps = k*n&amp;lt;/tt&amp;gt; zu wählen. Dann gilt der &lt;br /&gt;
;Satz: Wenn ein Ausdruck in k-CNF mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; erfüllbar ist, muss man im Mittel &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in O\left(\left(\frac{2(k-1)}{k}\right)^n \right)&amp;lt;/math&amp;gt; Versuche machen, um eine Lösung zu finden.&lt;br /&gt;
&lt;br /&gt;
Es gilt stets &amp;lt;math&amp;gt;\frac{2(k-1)}{k} &amp;gt; 1&amp;lt;/math&amp;gt;, man benötigt also eine in n exponentielle Anzahl von Versuchen. Bei &amp;lt;math&amp;gt;k=3&amp;lt;/math&amp;gt; gilt z.B. &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt; \in O\left(\left(\frac{4}{3}\right)^n\right)&amp;lt;/math&amp;gt;. Dies ist zwar im Mittel effizienter also die erschöpfende Suche, die &amp;lt;math&amp;gt;O(2^n)&amp;lt;/math&amp;gt; Schritte benötigt, aber immer noch sehr langsam.&lt;br /&gt;
&lt;br /&gt;
Der Fall &amp;lt;b&amp;gt;&amp;lt;math&amp;gt;k=2&amp;lt;/math&amp;gt; ist jedoch ein Sonderfall&amp;lt;/b&amp;gt;: Hier kann man leicht beweisen, dass eine Lösung im Mittel bereits nach &amp;lt;math&amp;gt;O\left(n^2\right)&amp;lt;/math&amp;gt; Schritten gefunden wird. Wenn man schon weiss, dass der Ausdruck erfüllbar ist (was mit [[Graphen_und_Graphenalgorithmen#Lösung des 2-SAT-Problems mit Implikationgraphen|Implikationgraphen]] leicht geprüft werden kann), lässt man den randomisierten Algorithmus einfach so lange laufen, bis er eine Lösung findet. Man setzt also &amp;lt;tt&amp;gt;step = infinity&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;trials = 1&amp;lt;/tt&amp;gt; und verlässt sich darauf, dass das &amp;lt;tt&amp;gt;return&amp;lt;/tt&amp;gt; mit einer gültigen Lösung früher oder später ausgeführt wird. Dass man darauf im Mittel nur &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte warten muss, zeigen wir jetzt mit Hilfe eines &amp;lt;i&amp;gt;random walk&amp;lt;/i&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''-Zufallsbelegung hat  &amp;lt;math&amp;gt;t\leq n&amp;lt;/math&amp;gt; richtige Variablen (im Mittel &amp;lt;math&amp;gt;t\approx \frac {n} 2&amp;lt;/math&amp;gt;)'''&lt;br /&gt;
&lt;br /&gt;
Negieren einer Variable ändert t um 1,&lt;br /&gt;
u.Z. &amp;lt;math&amp;gt;t\rightarrow t+1&amp;lt;/math&amp;gt; mit Wahrscheinlichkeit &amp;lt;math&amp;gt;\frac 1 2&amp;lt;/math&amp;gt; ::(für beliebiges k: &amp;lt;math&amp;gt;\frac 1 k&amp;lt;/math&amp;gt;)&lt;br /&gt;
::::::::::&amp;lt;math&amp;gt;t\rightarrow t-1&amp;lt;/math&amp;gt; mit Wahrscheinlichkeit &amp;lt;math&amp;gt;\frac 1 2&amp;lt;/math&amp;gt; ::(für beliebiges k: &amp;lt;math&amp;gt;\frac {k-1} k&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''-Wieviele Schritte braucht man im Mittel, um zu einer Lösung mit t Richtigen zu kommen?'''&lt;br /&gt;
&lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(t\right)=\frac 1 2 S\left(t-1\right) + \frac 1 2 S\left(t+1\right) +1&amp;lt;/math&amp;gt;&lt;br /&gt;
       &lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(n\right)=0&amp;lt;/math&amp;gt;    #Abbruchbedingung der Schleife&lt;br /&gt;
       &lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(0\right) = S\left( 1\right) + 1 \Rightarrow S\left(t\right) = n^2-t^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
       '''Probe:''' &lt;br /&gt;
 &lt;br /&gt;
       &amp;lt;math&amp;gt;&lt;br /&gt;
       \begin{align} &lt;br /&gt;
             S\left(n\right) &amp;amp; = n^2-n^2=0 \\&lt;br /&gt;
                  &lt;br /&gt;
             S\left(0\right) &amp;amp;= n^2-0^2 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= S\left(1\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2-1^2+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2 \\&lt;br /&gt;
                 &lt;br /&gt;
              S\left(t\right) &amp;amp;= \frac 1 2 \left(n^2-\left(t-1\right)^2\right) + \frac 1 2 \left(n^2-\left(t+1\right)^2\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= \frac 1 2 n^2-\frac 1 2 \left( t^2-2t+1\right) + \frac 1 2 n^2-\frac 1 2 \left(t^2+2t+1\right) + 1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2-t^2&lt;br /&gt;
       \end{align}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Das ist das Random Walk Problem'''&lt;br /&gt;
&lt;br /&gt;
Im ungünstigsten Fall (t=0) werden im Mittel &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte benötigt, um durch random walk nach t=n zu gelangen.&lt;br /&gt;
&lt;br /&gt;
== 2. RANSAC-ALGORITHMUS (Random Sample Consensus)==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''Aufgabe:''&amp;lt;/u&amp;gt; gegeben: Datenpunkte&lt;br /&gt;
::gesucht: Modell, das die Datenpunkte erklärt&lt;br /&gt;
&lt;br /&gt;
[[Image:Rubto.png|thumb|250px|none]]&lt;br /&gt;
&lt;br /&gt;
'''Messpunkte:'''&lt;br /&gt;
   &lt;br /&gt;
      übliche Lösung: Methode der kleinsten Quadrate&lt;br /&gt;
      &lt;br /&gt;
      &amp;lt;math&amp;gt;\min_{a,b} 	\sum_{i} \left(a x_i + b + y_i\right)^2&amp;lt;/math&amp;gt;&lt;br /&gt;
      &lt;br /&gt;
      Schulmathematik:      &amp;lt;math&amp;gt;Minimum\stackrel{\wedge}{=}Ableitung=0&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lineares Gleichungssystem'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{da}\sum{i} \left(ax_i+b-y_i\right)^2=\sum{i} \frac{d}{da} \left[ax_i+b-y_i\right)^2&amp;lt;/math&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(g\left(x\right)\right)&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(x\right)=x^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;y\left(a\right)=ax_i+b-y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;=\sum_{i}2\left(ax_i+b-y_i\right)\frac{d}{da} \underbrace {ax_i+b-y_i}_{x_i}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\underline {=2\sum_{i}\left(ax_i+b-y_i\right)x_i\stackrel{!}{=}0}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}{x_i}^2+b\sum_{i}x_i=\sum_{i}x_iy_i&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}x_i+b\sum_{i}1=\sum_{i}y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{db}\sum_{i}\left(ax_i+b-y_i\right)^2=2\sum_{i}\left(ax_i+b-y_i\right)*1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:Problem: &amp;lt;math&amp;gt;\epsilon  %&amp;lt;/math&amp;gt; der Datenpunkte sind Outlier&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\Longrightarrow&amp;lt;/math&amp;gt; Einfaches Anpassen des Modells an die Datenpunkte funktioniert nicht&lt;br /&gt;
&lt;br /&gt;
:Seien mindestens k Datenpunkte notwendig, um das Programm anpassen zu können&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
RANSAC-Algorithmus&lt;br /&gt;
&lt;br /&gt;
      for  l in range (trials):&lt;br /&gt;
           wähle zufällig k Punkte aus&lt;br /&gt;
           passe das Modell an die k Punkte an&lt;br /&gt;
           zähle, wieviele Punkte in der Nähe des Modells liegen (d.h. &amp;lt;math&amp;gt;d_i &amp;lt; d_max&amp;lt;/math&amp;gt; muss geschickt gewählt werden) &lt;br /&gt;
                                           #Bsp. Geradenfinden:-wähle a,b aus zwei Punkten&lt;br /&gt;
                                                               -berechne: &amp;lt;math&amp;gt;|ax_i+b-y_i|=d_i&amp;lt;/math&amp;gt;&lt;br /&gt;
                                                               -zähle Punkt i als Inlier, falls &amp;lt;math&amp;gt;d_i&amp;lt;d_ma&amp;lt;/math&amp;gt;&lt;br /&gt;
      return: Modell mit höchster Zahl der Inlier&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;trials= \frac{log\left(1-p\right)}{log\left(1-\left(1-\epsilon\right)^k\right)}&amp;lt;/math&amp;gt;  mit k=Anzahl der Datenpunkte und p=Erfolgswahrscheinlichkeit, &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;=Outlier-Anteil&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Erfolgswahrscheinlichkeit: p=99%'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{|c||c|c|c|c|c|}&lt;br /&gt;
         Beispiel &amp;amp; k &amp;amp; \epsilon=10% &amp;amp; 20% &amp;amp; 50% &amp;amp; 70%\\&lt;br /&gt;
         \hline&lt;br /&gt;
         Linie\;in\;2D &amp;amp; 2 &amp;amp; 3 &amp;amp;5 &amp;amp; 17 &amp;amp; 49\\&lt;br /&gt;
         Kreis\;in\;2D &amp;amp; 3 &amp;amp; 4 &amp;amp; 7 &amp;amp; 35 &amp;amp; 169\\&lt;br /&gt;
         Ebene\;in\;3D &amp;amp; 8 &amp;amp; 9 &amp;amp; 26 &amp;amp; 1172 &amp;amp; 70188\\&lt;br /&gt;
       \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Ein Spiel: Wie viel Schritte braucht man im Mittel zum Ziel?'''&lt;br /&gt;
&lt;br /&gt;
   geg.: 5 Plätze, 2 Personen: eine Person rückt vom einem Platz zu dem enderen Platz;&lt;br /&gt;
         die zweite Person wirft die Münze.&lt;br /&gt;
         Wenn die Münze auf Kopf landet, rücke nach rechts und wenn die Münze auf Zahl landet, rücke nach links.&lt;br /&gt;
         &amp;lt;--- Zahl                                                         Kopf--&amp;gt;&lt;br /&gt;
         Kopf: /////&lt;br /&gt;
         Zahl: /// &lt;br /&gt;
&lt;br /&gt;
:: =&amp;gt; mit 8 Schritten bis zum Ziel&lt;br /&gt;
:im Mittel: bei N Plätzen braucht man N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritte&lt;br /&gt;
&lt;br /&gt;
: all: mit N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritten um N Plätze rücken&lt;br /&gt;
: Wie viel Schritte braucht man im Mittel zum Ziel?&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(N\right)=0&amp;lt;/math&amp;gt;    #wenn wir uns im Stuhl Nr.1 befinden&lt;br /&gt;
           &lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)=\frac 1 2 S\left(1 + S\left(i+1\right)\right) + \frac 1 2 S\left(1 + S\left(i-1\right)\right) = \frac 1 2 S\left(i+1\right) + \frac 1 2 S\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(0\right)=1 + S\left(1\right)&amp;lt;/math&amp;gt;    #bei 0.Platz&lt;br /&gt;
&lt;br /&gt;
:::*Lösung: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:::*speziell: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2&amp;lt;/math&amp;gt;           #wenn man am ungünstigsten Platz startet&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
'''Beziehung zu randomisiertem 2-SAT'''&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Platz &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; &amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert,  &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)=N^2 - \left(\frac N 2\right)^2 = N^2 - \frac N 4 ^2 = \frac 3 4 N^2 &amp;lt;/math&amp;gt;&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)&amp;lt;/math&amp;gt;     # Anfangszustand&lt;br /&gt;
----&lt;br /&gt;
== '''Las Vegas vs. Monte Carlo'''==&lt;br /&gt;
&lt;br /&gt;
   * ''Las Vegas - Algorithmen''&lt;br /&gt;
     - Ergebnis ist immer korrekt.&lt;br /&gt;
     - Berechnung ist mit hoher Wahrscheinlichkeit effizient (d.h. Randomisierung macht den ungünstigsten Fall unwahrscheinlich).&lt;br /&gt;
&lt;br /&gt;
   * ''Monte Carlo - Algorithmen''&lt;br /&gt;
     - Berechnung immer effizient.&lt;br /&gt;
     - Ergebnis mit hoher Wahrscheinlichkeit korrekt (falls kein effizienter Algorithmus bekannt, der immer die richtige Lösung liefert).&lt;br /&gt;
&lt;br /&gt;
{| border = &amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;center&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! Las Vegas&lt;br /&gt;
! Monte Carlo&lt;br /&gt;
|- &lt;br /&gt;
| - Erzeugen einer perfekten Hashfuktion &lt;br /&gt;
| - Algorithmus von Freiwald(Matrizenmultiplikation)&lt;br /&gt;
|-&lt;br /&gt;
| - universelles Hashing&lt;br /&gt;
| - RANSAC&lt;br /&gt;
|-&lt;br /&gt;
| - Quick Sort mit zufälliger Wahl des Pivot-Elements&lt;br /&gt;
| - randomisierte K-SAT(k&amp;gt;=3)(Alg. von Schöning)&lt;br /&gt;
|-&lt;br /&gt;
| - Treep mit zufälligen Prioritäten&lt;br /&gt;
| -&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== ''' Zufallszahlen ''' ==&lt;br /&gt;
&lt;br /&gt;
:- kann man nicht mit deterministischen Computern erzeugen&lt;br /&gt;
:- aber man kann Pseudo-Zufallszahlen erzeugen, die viele Eigenschaften von echten Zufallszahlen haben&lt;br /&gt;
::: * sehr ähnlich  zum Hash&lt;br /&gt;
&lt;br /&gt;
     ''&amp;quot;linear Conguential Random number generator&amp;quot;''&lt;br /&gt;
        &amp;lt;math&amp;gt;I_{i+1}= \left(a*I_i + c\right)\textrm{mod\ } m&amp;lt;/math&amp;gt;&lt;br /&gt;
        &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{=&amp;gt; } &amp;amp; I_i \in [0, m-1]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:-sorgfältige Wahl von  a, c, m notwendig&lt;br /&gt;
::'''Bsp.'''  m = 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;&lt;br /&gt;
::: a = 1664525, c = 1013904223&lt;br /&gt;
::: ''&amp;quot;quick and dirty generator&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
==='''Nachteile'''===&lt;br /&gt;
&lt;br /&gt;
* nicht zufällig genug für viele Anwendungen&lt;br /&gt;
::'''Bsp.''' wähle Punkt in R&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
      \mathrm{ } &amp;amp; p = (rand(), rand(), rand())\\&lt;br /&gt;
&lt;br /&gt;
      \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::gibt Zahl u, v, w so, dass &lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; u * p[0] + v * p[1] + w * p[3]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::stark geclustert ist.&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge ist zu kurz:&lt;br /&gt;
:: spätestens nach m Schritten wiederholt sich die Folge&lt;br /&gt;
&lt;br /&gt;
::'''allgemein''': falls der interne Zustand des Zufallsgenerators ''k'' bits hat, ist Periodenlänge:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; Periode &amp;lt; 2^k\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* ''lowbits'' sind weniger zufällig als die ''highbits''&lt;br /&gt;
----&lt;br /&gt;
=== ''Mersenne Twister''===&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
'''bester zur Zeit bekannter Zufallszahlengenerator (ZZG)'''&lt;br /&gt;
* innere Zustand: &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; 624*32 bit\ Integers  =&amp;gt; 19968 bits\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge: &amp;lt;math&amp;gt;2^ {19937} \approx 4 * 10^{6000}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Punkte aus aufeinanderfolgende Zufallszahlen in &amp;lt;math&amp;gt;\mathbb{R}^n&amp;lt;/math&amp;gt; sind gleich verteilt bis &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; n = 623\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* alle Bits sind unabhängig voneinander zufällig (&amp;quot;Twister&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
* schnell&lt;br /&gt;
&lt;br /&gt;
    class MersenneTwister:&lt;br /&gt;
        &lt;br /&gt;
        def __init__(self, seed):&lt;br /&gt;
            self.N = 624  # Größe des inneren Zustands festlegen&lt;br /&gt;
            self.i = 0    # zählt mit in welchem Zustand wir uns gerade aufhalten&lt;br /&gt;
            &lt;br /&gt;
            self.state = [0]*self.N  # Speicher für den inneren Zustand reservieren&lt;br /&gt;
            &lt;br /&gt;
            self.state[0] = seed     # initiale Zufallszahl vom Benutzer&lt;br /&gt;
            # den Rest des inneren Zustands mit einfachem Zufallszahlengenerator initialisieren&lt;br /&gt;
            for i in xrange(1, self.N):&lt;br /&gt;
                self.state[i] = (1812433253 * (self.state[i-1] ^ (self.state[i-1] &amp;gt;&amp;gt; 30)) + i) % 4294967296&lt;br /&gt;
     &lt;br /&gt;
        def __call__(self):&lt;br /&gt;
            &amp;quot;&amp;quot;&amp;quot;gibt die nächste Zufallszahl im Bereich [0, 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;-1] aus&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
            N, M = self.N, 397&lt;br /&gt;
            &lt;br /&gt;
            # Zustand aktualisieren (neue Zufallszahl ausrechnen)&lt;br /&gt;
            i = self.i&lt;br /&gt;
            r = ((self.state[i] &amp;amp; 0x80000000) | (self.state[(i+1)%N] &amp;amp; 0x7FFFFFFF)) &amp;gt;&amp;gt; 1&lt;br /&gt;
            if self.state[(i+1)%N] &amp;amp; 1:&lt;br /&gt;
                r ^= 0x9908B0DF&lt;br /&gt;
            self.state[i] = self.state[(i+M)%N] ^ r&lt;br /&gt;
     &lt;br /&gt;
            # aktuelle Zufallszahl auslesen und ihre Zufälligkeit durch verwürfeln der Bits verbessern&lt;br /&gt;
            y = self.state[i]&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 11)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt;  7) &amp;amp; 0x9D2C5680)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt; 15) &amp;amp; 0xEFC60000)&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 18)&lt;br /&gt;
            &lt;br /&gt;
            # Zustand weitersetzen und endgültige Zufallszahl ausgeben&lt;br /&gt;
            self.i = (self.i + 1) % N&lt;br /&gt;
            return y&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''geg.:''' Zufallszahl &lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, \overbrace{2^{32}-1}^{m-1}]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ges.:''' Zufallszahl&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, k - 1]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''naive Lösung:'''  &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; rand()%k\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;  ist schlecht.&lt;br /&gt;
&lt;br /&gt;
'''Bsp.'''&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; \qquad m = 16\qquad k = 11\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
! rand() || 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15&lt;br /&gt;
|-&lt;br /&gt;
! rand()%k&lt;br /&gt;
! 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 0 || 1 || 2 || 3 || 4 &lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; 0,...,4 kommt doppelt so häufig wie 5,...,10 &amp;quot;nicht zufällig&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lösung:'''  Zurückweisen des Rests der Zahlen (''rejektion sampling'')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; remainder = (m - 1 - (k - 1))% k = (m - k)%k\\&lt;br /&gt;
        \mathrm{ } &amp;amp; last\ Good\ Value = m-1-remainder\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  r = rand()&lt;br /&gt;
  while r &amp;gt; last.GoodValue:&lt;br /&gt;
        r = rand()&lt;br /&gt;
        return r%k&lt;br /&gt;
&lt;br /&gt;
[[Greedy-Algorithmen und Dynamische Programmierung|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5426</id>
		<title>Randomisierte Algorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5426"/>
				<updated>2012-07-27T17:05:37Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Anwendung: Lösen des K-SAT-Problems */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Randomisierte Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
;Definition: Randomisierte Algorithmen sind Algorithmen, die bei Entscheidungen über ihr weiteres Vorgehen oder bei der Wahl ihrer Parameter Zufallszahlen benutzen.&lt;br /&gt;
&lt;br /&gt;
Anschaulich gesprochen, wersucht man bei randomisierten Algorithmen, einen Teil der Lösung zu &amp;lt;i&amp;gt;raten&amp;lt;/i&amp;gt;. Auf den ersten Blick würde man vermuten, dass dabei nicht viel Sinnvolles herauskommen kann. Diese Kapitel wird jedoch zeigen, dass man durch geschicktes Raten tatsächlich zu sehr eleganten Algorithmen gelangen kann.&lt;br /&gt;
&lt;br /&gt;
Grundsätzlich unterscheidet man zwei Arten von randomisierten Algorithmen:&lt;br /&gt;
;Las Vegas - Algorithmen: Das Ergebnis des Algorithmus ist immer korrekt, und die Berechnung erfolgt mit hoher Wahrscheinlichkeit effizient.&lt;br /&gt;
;Monte Carlo - Algorithmen: Die Berechnung ist immer effizient, und das Ergebnis ist mit hoher Wahrscheinlichkeit korrekt.&lt;br /&gt;
Las Vegas-Algorithmen verwendet man, wenn der Algorithmus im ungünstigen Fall eine schlechte Laufzeit hat, und der ungünstige Fall kann durch die Randomisierung sehr unwahrscheinlich gemacht werden. Wir haben in der Vorlesung schon mehrere Las Vegas-Algorithmen kennen gelernt:&lt;br /&gt;
* Quick Sort mit zufälliger Wahl des Pivot-Elements: Die Randomisierung verhindert, dass das Array immer wieder in Subarrays von sehr unterschiedlicher Größe aufgeteilt wird.&lt;br /&gt;
* Treap mit zufälligen Prioritäten: Die Randomisierung verhindert, dass der Baum schlecht balanciert ist.&lt;br /&gt;
* Universelles Hashing: Die zufällige Wahl der Hashfunktion verhindert, dass ein Angreifer eine Schlüsselmenge mit sehr vielen Kollisionen konstruieren kann.&lt;br /&gt;
* Erzeugung einer perfekten Hashfunktion: Durch die Randomisierung entsteht mit nach wenigen Versuchen ein zyklenfreier Graph, der zur Definition der Hashfunktion geeignet ist.&lt;br /&gt;
Monte Carlo-Algorithmen verwendet man dagegen, wenn kein effizienter deterministischer Algorithmus für ein Problem bekannt ist. Man gibt sich dann damit zufrieden, dass der randomisierte Algorithmus die korrekte Lösung nur mit hoher Wahrscheinlichkeit findet, wenn dies dafür sehr effizient geschieht. Bei manchen Problemen ist auch dies unerreichbar - man muss dann bereits zufrieden sein, wenn der Algorithmus mit hoher Wahrscheinlichkeit eine sehr gute Näherungslösung findet. Beliebte Anwendungsgebiete für Monte Carlo-Algorithmen sind beispielsweise&lt;br /&gt;
* Randomisierte Primzahl-Tests: Moderne Verschlüsselungsverfahren benötigen zahlreiche Primzahlen, aber exakte Primzahltests sind teuer. Der [http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin-Test] findet effizient Zahlen, die mit sehr hoher Wahrscheinlichkeit tatsächlich Primzahlen sind.&lt;br /&gt;
* Randomisiertes Testen: Wie jeder Test kann auch eine randomisierter Test nicht die Abwesenheit von Programmierfehlern garantieren, aber man kann durch die Randomisierung viel mehr Testfälle generieren und erhöht so die Erfolgswarscheinlichkeit. Wir haben als Beispiel dafür den [[Korrektheit#Beispiel_f.C3.BCr_das_Testen:_Freivalds_Algorithmus|Algorithmus von Freivald]] behandelt.&lt;br /&gt;
* Lösung schwieriger Optimierungsprobleme: Wir zeigen unten, dass ein randomisierter Algorithmus effizient eine Lösung für das 2-SAT-Problem aus dem vorherigen Kapitel findet (für k-SAT mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; liefert der Algorithmus immer noch mit einer gewissen Wahrscheinlichkeit das richtige Ergebnis, ist aber nicht mehr effizient). Einen effizienten Approximationsalgorithmus für des Problem des Handelsreisenden behandlen wir im Kapitel [[NP-Vollständigkeit]]. Weitere wichtige Beispiele für diesen Bereich sind [http://en.wikipedia.org/wiki/Simulated_annealing simulated annealing] und das [http://de.wikipedia.org/wiki/MCMC-Verfahren Markov-Chain-Monte-Carlo-Verfahren].&lt;br /&gt;
* Robuste Statistik: Eine Grundaufgabe der Statistik ist das Anpassen (Fitten) von Modellen an gemessene Werte. Wenn die Messungen jedoch &amp;quot;Ausreißer&amp;quot; (einige völlig falsche Werte) enthalten, geht die Anpassung schief. Wir beschreiben unten den RANSAC-Algorithmus, der die Ausreißer identifizieren und beim Modellfitten ignorieren kann.&lt;br /&gt;
&lt;br /&gt;
Obwohl randomisierte Algorithmen oft einfach und elegant sind, ist ihre theoretische Analyse (also das Führen von Korrektheits- und Komplexitätsbeweisen) häufig sehr schwierig. Man muss fortgeschrittene Methoden der Wahrscheinlichkeitsrechnung und Statistik beherrschen, um die Wahrscheinlichkeit für das Versagen des Algorithmus zu berechnen und um zu zeigen, wie man den Algorithmus benutzt, damit diese Wahrscheinlichkeit unter einer akzeptablen Schranke bleibt. Die Algorithmen, die wir für diese Vorlesung ausgewählt haben, zeichnen sich dadurch aus, dass die Beweise hier einfach zu erbringen sind.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Lösen des K-SAT-Problems ===&lt;br /&gt;
&lt;br /&gt;
Der &amp;lt;b&amp;gt;Algorithmus von Schöning&amp;lt;/b&amp;gt; löst das [[Graphen_und_Graphenalgorithmen#Normalformen für logische Ausdrücke|k-SAT-Problem]] durch Raten: Wenn ein Ausdruck in k-CNF den Wert False hat, gibt es mindestens eine Klausel, die den Wert False hat. Alle Literale in dieser Klausel haben ebenfalls den Wert False, denn jede Klausel ist eine ODER-Verknüpfung, die nur dann False werden kann. Um den Ausdruck zu erfüllen, muss jede Klausel True sein, also müssen wir den Wert von mindestens einem Literal umdrehen. Wenn der Ausruck tatsächlich erfüllbar ist, gibt es immer ein geeignetes Literal, wir wissen nur nicht, welches. Deshalb drehen wir ein unter den k Literalen der betreffenden Klausel zufällig gewähltes. Liegen wir mit unserer Wahl richtig, sind wir der Lösung näher gekommen - im besten Fall sind jetzt alle Klauseln erfüllt. Wählen wir jedoch die falsche Variable, ist die aktuelle Klausel zwar jetzt True, aber dafür werden andere Klauseln zu False, die bisher True waren, und wir entfernen uns somit von der Lösung.&lt;br /&gt;
&lt;br /&gt;
    geg.: logischer Ausdruck in K-CNF (n Variablen, m Klauseln, k Variablen pro Klausel)&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;math&amp;gt;\underbrace {\underbrace {\left(x_1 \vee x_3 \vee...\right)}_{k\; Literale} \wedge \left( x_2 \vee x_4 \vee...\right)}_{m\;Klauseln}&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Algorithmus von Schöning lautet in Pseudocode:&lt;br /&gt;
    for i in range (trials):    #Anzahl der Versuche&lt;br /&gt;
         # Bestimme eine Zufallsbelegung der Variablen &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;:&lt;br /&gt;
         for j in range (steps):&lt;br /&gt;
               if &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt; erfüllt alle Klauseln: &lt;br /&gt;
                   return &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
               # wähle zufällig eine Klausel, die nicht erfüllt ist und negiere zufällig eine der Variablen in dieser Klausel &lt;br /&gt;
               # (die Klausel ist jetzt erfüllt)&lt;br /&gt;
    return None  # keine Lösung gefunden&lt;br /&gt;
&lt;br /&gt;
Findet der Algorithmus eine Lösung, wissen wir, dass der Ausdruck erfüllbar ist. Andernfalls könnte der Ausdruck unerfüllbar sein, oder wir haben nur Pech gehabt. Je mehr erfolglose Versuche wir machen, desto höher ist die Wahrscheinlichkeit, dass das erste zutrifft.&lt;br /&gt;
&lt;br /&gt;
Es ist sinnvoll, &amp;lt;tt&amp;gt;steps = k*n&amp;lt;/tt&amp;gt; zu wählen. Dann gilt der &lt;br /&gt;
;Satz: Wenn ein Ausdruck in k-CNF mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; erfüllbar ist, muss man im Mittel &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in O\left(\left(\frac{2(k-1)}{k}\right)^n \right)&amp;lt;/math&amp;gt; Versuche machen, um eine Lösung zu finden.&lt;br /&gt;
&lt;br /&gt;
Es gilt stets &amp;lt;math&amp;gt;\frac{2(k-1)}{k} &amp;gt; 1&amp;lt;/math&amp;gt;, man benötigt also eine in n exponentielle Anzahl von Versuchen. Bei &amp;lt;math&amp;gt;k=3&amp;lt;/math&amp;gt; gilt z.B. &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt; \in O\left(\left(\frac{4}{3}\right)^n\right)&amp;lt;/math&amp;gt;. Dies ist zwar im Mittel effizienter also die erschöpfende Suche, die &amp;lt;math&amp;gt;O(2^n)&amp;lt;/math&amp;gt; Schritte benötigt, aber immer noch sehr langsam.&lt;br /&gt;
&lt;br /&gt;
Der Fall &amp;lt;b&amp;gt;&amp;lt;math&amp;gt;k=2&amp;lt;/math&amp;gt; ist jedoch ein Sonderfall&amp;lt;/b&amp;gt;: Hier kann man leicht beweisen, dass eine Lösung im Mittel bereits nach &amp;lt;math&amp;gt;O\left(n^2\right)&amp;lt;/math&amp;gt; Schritten gefunden wird. Wenn man schon weiss, dass der Ausdruck erfüllbar ist (was mit [[Graphen_und_Graphenalgorithmen#Lösung des 2-SAT-Problems mit Implikationgraphen|Implikationgraphen]] leicht geprüft werden kann), lässt man den randomisierten Algorithmus einfach so lange laufen, bis er eine Lösung findet. Man setzt also &amp;lt;tt&amp;gt;step = infinity&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;trials = 1&amp;lt;/tt&amp;gt; und verlässt sich darauf, dass das &amp;lt;tt&amp;gt;return&amp;lt;/tt&amp;gt; mit einer gültigen Lösung früher oder später ausgeführt wird. Dass man darauf im Mittel nur &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte warten muss, zeigen wir jetzt mit Hilfe eines &amp;lt;i&amp;gt;random walk&amp;lt;/i&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''-Zufallsbelegung hat  &amp;lt;math&amp;gt;t\leq n&amp;lt;/math&amp;gt; richtige Variablen (im Mittel &amp;lt;math&amp;gt;t\approx \frac {n} 2&amp;lt;/math&amp;gt;)'''&lt;br /&gt;
&lt;br /&gt;
Negieren einer Variable ändert t um 1,&lt;br /&gt;
u.Z. &amp;lt;math&amp;gt;t\rightarrow t+1&amp;lt;/math&amp;gt; mit Wahrscheinlichkeit &amp;lt;math&amp;gt;\frac 1 2&amp;lt;/math&amp;gt; ::(für beliebiges k: &amp;lt;math&amp;gt;\frac 1 k&amp;lt;/math&amp;gt;)&lt;br /&gt;
::::::::::&amp;lt;math&amp;gt;t\rightarrow t-1&amp;lt;/math&amp;gt; mit Wahrscheinlichkeit &amp;lt;math&amp;gt;\frac 1 2&amp;lt;/math&amp;gt; ::(für beliebiges k: &amp;lt;math&amp;gt;\frac {k-1} k&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''-Wieviele Schritte braucht man im Mittel, um zu einer Lösung mit t Richtigen zu kommen?'''&lt;br /&gt;
&lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(t\right)=\frac 1 2 S\left(t-1\right) + \frac 1 2 S\left(t+1\right) +1&amp;lt;/math&amp;gt;&lt;br /&gt;
       &lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(n\right)=0&amp;lt;/math&amp;gt;    #Abbruchbedingung der Schleife&lt;br /&gt;
       &lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(0\right) = S\left( 1\right) + 1 \Rightarrow S\left(t\right) = n^2-t^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
       '''Probe:''' &lt;br /&gt;
 &lt;br /&gt;
       &amp;lt;math&amp;gt;&lt;br /&gt;
       \begin{align} &lt;br /&gt;
             S\left(n\right) &amp;amp; = n^2-n^2=0 \\&lt;br /&gt;
                  &lt;br /&gt;
             S\left(0\right) &amp;amp;= n^2-0^2 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= S\left(1\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2-1^2+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2 \\&lt;br /&gt;
                 &lt;br /&gt;
              S\left(t\right) &amp;amp;= \frac 1 2 \left(n^2-\left(t-1\right)^2\right) + \frac 1 2 \left(n^2-\left(t+1\right)^2\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= \frac 1 2 n^2-\frac 1 2 \left( t^2-2t+1\right) + \frac 1 2 n^2-\frac 1 2 \left(t^2+2t+1\right) + 1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2-t^2&lt;br /&gt;
       \end{align}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Das ist das Random Walk Problem'''&lt;br /&gt;
&lt;br /&gt;
Im ungünstigsten Fall (t=0) werden im Mittel &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte benötigt, um durch random walk nach t=n zu gelangen.&lt;br /&gt;
&lt;br /&gt;
== 2. RANSAC-ALGORITHMUS (Random Sample Consensus)==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''Aufgabe:''&amp;lt;/u&amp;gt; gegeben: Datenpunkte&lt;br /&gt;
::gesucht: Modell, das die Datenpunkte erklärt&lt;br /&gt;
&lt;br /&gt;
[[Image:Rubto.png|thumb|250px|none]]&lt;br /&gt;
&lt;br /&gt;
'''Messpunkte:'''&lt;br /&gt;
   &lt;br /&gt;
      übliche Lösung: Methode der kleinsten Quadrate&lt;br /&gt;
      &lt;br /&gt;
      &amp;lt;math&amp;gt;\min_{a,b} 	\sum_{i} \left(a x_i + b + y_i\right)^2&amp;lt;/math&amp;gt;&lt;br /&gt;
      &lt;br /&gt;
      Schulmathematik:      &amp;lt;math&amp;gt;Minimum\stackrel{\wedge}{=}Ableitung=0&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lineares Gleichungssystem'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{da}\sum{i} \left(ax_i+b-y_i\right)^2=\sum{i} \frac{d}{da} \left[ax_i+b-y_i\right)^2&amp;lt;/math&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(g\left(x\right)\right)&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(x\right)=x^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;y\left(a\right)=ax_i+b-y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;=\sum_{i}2\left(ax_i+b-y_i\right)\frac{d}{da} \underbrace {ax_i+b-y_i}_{x_i}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\underline {=2\sum_{i}\left(ax_i+b-y_i\right)x_i\stackrel{!}{=}0}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}{x_i}^2+b\sum_{i}x_i=\sum_{i}x_iy_i&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}x_i+b\sum_{i}1=\sum_{i}y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{db}\sum_{i}\left(ax_i+b-y_i\right)^2=2\sum_{i}\left(ax_i+b-y_i\right)*1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:Problem: &amp;lt;math&amp;gt;\epsilon  %&amp;lt;/math&amp;gt; der Datenpunkte sind Outlier&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\Longrightarrow&amp;lt;/math&amp;gt; Einfaches Anpassen des Modells an die Datenpunkte funktioniert nicht&lt;br /&gt;
&lt;br /&gt;
:Seien mindestens k Datenpunkte notwendig, um das Programm anpassen zu können&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
RANSAC-Algorithmus&lt;br /&gt;
&lt;br /&gt;
      for  l in range (trials):&lt;br /&gt;
           wähle zufällig k Punkte aus&lt;br /&gt;
           passe das Modell an die k Punkte an&lt;br /&gt;
           zähle, wieviele Punkte in der Nähe des Modells liegen (d.h. &amp;lt;math&amp;gt;d_i &amp;lt; d_max&amp;lt;/math&amp;gt; muss geschickt gewählt werden) &lt;br /&gt;
                                           #Bsp. Geradenfinden:-wähle a,b aus zwei Punkten&lt;br /&gt;
                                                               -berechne: &amp;lt;math&amp;gt;|ax_i+b-y_i|=d_i&amp;lt;/math&amp;gt;&lt;br /&gt;
                                                               -zähle Punkt i als Inlier, falls &amp;lt;math&amp;gt;d_i&amp;lt;d_ma&amp;lt;/math&amp;gt;&lt;br /&gt;
      return: Modell mit höchster Zahl der Inlier&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;trials= \frac{log\left(1-p\right)}{log\left(1-\left(1-\epsilon\right)^k\right)}&amp;lt;/math&amp;gt;  mit k=Anzahl der Datenpunkte und p=Erfolgswahrscheinlichkeit, &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;=Outlier-Anteil&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Erfolgswahrscheinlichkeit: p=99%'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{|c||c|c|c|c|c|}&lt;br /&gt;
         Beispiel &amp;amp; k &amp;amp; \epsilon=10% &amp;amp; 20% &amp;amp; 50% &amp;amp; 70%\\&lt;br /&gt;
         \hline&lt;br /&gt;
         Linie\;in\;2D &amp;amp; 2 &amp;amp; 3 &amp;amp;5 &amp;amp; 17 &amp;amp; 49\\&lt;br /&gt;
         Kreis\;in\;2D &amp;amp; 3 &amp;amp; 4 &amp;amp; 7 &amp;amp; 35 &amp;amp; 169\\&lt;br /&gt;
         Ebene\;in\;3D &amp;amp; 8 &amp;amp; 9 &amp;amp; 26 &amp;amp; 1172 &amp;amp; 70188\\&lt;br /&gt;
       \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Ein Spiel: Wie viel Schritte braucht man im Mittel zum Ziel?'''&lt;br /&gt;
&lt;br /&gt;
   geg.: 5 Plätze, 2 Personen: eine Person rückt vom einem Platz zu dem enderen Platz;&lt;br /&gt;
         die zweite Person wirft die Münze.&lt;br /&gt;
         Wenn die Münze auf Kopf landet, rücke nach rechts und wenn die Münze auf Zahl landet, rücke nach links.&lt;br /&gt;
         &amp;lt;--- Zahl                                                         Kopf--&amp;gt;&lt;br /&gt;
         Kopf: /////&lt;br /&gt;
         Zahl: /// &lt;br /&gt;
&lt;br /&gt;
:: =&amp;gt; mit 8 Schritten bis zum Ziel&lt;br /&gt;
:im Mittel: bei N Plätzen braucht man N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritte&lt;br /&gt;
&lt;br /&gt;
: all: mit N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritten um N Plätze rücken&lt;br /&gt;
: Wie viel Schritte braucht man im Mittel zum Ziel?&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(N\right)=0&amp;lt;/math&amp;gt;    #wenn wir uns im Stuhl Nr.1 befinden&lt;br /&gt;
           &lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)=\frac 1 2 S\left(1 + S\left(i+1\right)\right) + \frac 1 2 S\left(1 + S\left(i-1\right)\right) = \frac 1 2 S\left(i+1\right) + \frac 1 2 S\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(0\right)=1 + S\left(1\right)&amp;lt;/math&amp;gt;    #bei 0.Platz&lt;br /&gt;
&lt;br /&gt;
:::*Lösung: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:::*speziell: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2&amp;lt;/math&amp;gt;           #wenn man am ungünstigsten Platz startet&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
'''Beziehung zu randomisiertem 2-SAT'''&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Platz &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; &amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert,  &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)=N^2 - \left(\frac N 2\right)^2 = N^2 - \frac N 4 ^2 = \frac 3 4 N^2 &amp;lt;/math&amp;gt;&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)&amp;lt;/math&amp;gt;     # Anfangszustand&lt;br /&gt;
----&lt;br /&gt;
== '''Las Vegas vs. Monte Carlo'''==&lt;br /&gt;
&lt;br /&gt;
   * ''Las Vegas - Algorithmen''&lt;br /&gt;
     - Ergebnis ist immer korrekt.&lt;br /&gt;
     - Berechnung ist mit hoher Wahrscheinlichkeit effizient (d.h. Randomisierung macht den ungünstigsten Fall unwahrscheinlich).&lt;br /&gt;
&lt;br /&gt;
   * ''Monte Carlo - Algorithmen''&lt;br /&gt;
     - Berechnung immer effizient.&lt;br /&gt;
     - Ergebnis mit hoher Wahrscheinlichkeit korrekt (falls kein effizienter Algorithmus bekannt, der immer die richtige Lösung liefert).&lt;br /&gt;
&lt;br /&gt;
{| border = &amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;center&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! Las Vegas&lt;br /&gt;
! Monte Carlo&lt;br /&gt;
|- &lt;br /&gt;
| - Erzeugen einer perfekten Hashfuktion &lt;br /&gt;
| - Algorithmus von Freiwald(Matrizenmultiplikation)&lt;br /&gt;
|-&lt;br /&gt;
| - universelles Hashing&lt;br /&gt;
| - RANSAC&lt;br /&gt;
|-&lt;br /&gt;
| - Quick Sort mit zufälliger Wahl des Pivot-Elements&lt;br /&gt;
| - randomisierte K-SAT(k&amp;gt;=3)(Alg. von Schöning)&lt;br /&gt;
|-&lt;br /&gt;
| - Treep mit zufälligen Prioritäten&lt;br /&gt;
| -&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== ''' Zufallszahlen ''' ==&lt;br /&gt;
&lt;br /&gt;
:- kann man nicht mit deterministischen Computern erzeugen&lt;br /&gt;
:- aber man kann Pseudo-Zufallszahlen erzeugen, die viele Eigenschaften von echten Zufallszahlen haben&lt;br /&gt;
::: * sehr ähnlich  zum Hash&lt;br /&gt;
&lt;br /&gt;
     ''&amp;quot;linear Conguential Random number generator&amp;quot;''&lt;br /&gt;
        &amp;lt;math&amp;gt;I_{i+1}= \left(a*I_i + c\right)\textrm{mod\ } m&amp;lt;/math&amp;gt;&lt;br /&gt;
        &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{=&amp;gt; } &amp;amp; I_i \in [0, m-1]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:-sorgfältige Wahl von  a, c, m notwendig&lt;br /&gt;
::'''Bsp.'''  m = 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;&lt;br /&gt;
::: a = 1664525, c = 1013904223&lt;br /&gt;
::: ''&amp;quot;quick and dirty generator&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
==='''Nachteile'''===&lt;br /&gt;
&lt;br /&gt;
* nicht zufällig genug für viele Anwendungen&lt;br /&gt;
::'''Bsp.''' wähle Punkt in R&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
      \mathrm{ } &amp;amp; p = (rand(), rand(), rand())\\&lt;br /&gt;
&lt;br /&gt;
      \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::gibt Zahl u, v, w so, dass &lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; u * p[0] + v * p[1] + w * p[3]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::stark geclustert ist.&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge ist zu kurz:&lt;br /&gt;
:: spätestens nach m Schritten wiederholt sich die Folge&lt;br /&gt;
&lt;br /&gt;
::'''allgemein''': falls der interne Zustand des Zufallsgenerators ''k'' bits hat, ist Periodenlänge:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; Periode &amp;lt; 2^k\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* ''lowbits'' sind weniger zufällig als die ''highbits''&lt;br /&gt;
----&lt;br /&gt;
=== ''Mersenne Twister''===&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
'''bester zur Zeit bekannter Zufallszahlengenerator (ZZG)'''&lt;br /&gt;
* innere Zustand: &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; 624*32 bit\ Integers  =&amp;gt; 19968 bits\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge: &amp;lt;math&amp;gt;2^ {19937} \approx 4 * 10^{6000}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Punkte aus aufeinanderfolgende Zufallszahlen in &amp;lt;math&amp;gt;\mathbb{R}^n&amp;lt;/math&amp;gt; sind gleich verteilt bis &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; n = 623\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* alle Bits sind unabhängig voneinander zufällig (&amp;quot;Twister&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
* schnell&lt;br /&gt;
&lt;br /&gt;
    class MersenneTwister:&lt;br /&gt;
        &lt;br /&gt;
        def __init__(self, seed):&lt;br /&gt;
            self.N = 624  # Größe des inneren Zustands festlegen&lt;br /&gt;
            self.i = 0    # zählt mit in welchem Zustand wir uns gerade aufhalten&lt;br /&gt;
            &lt;br /&gt;
            self.state = [0]*self.N  # Speicher für den inneren Zustand reservieren&lt;br /&gt;
            &lt;br /&gt;
            self.state[0] = seed     # initiale Zufallszahl vom Benutzer&lt;br /&gt;
            # den Rest des inneren Zustands mit einfachem Zufallszahlengenerator initialisieren&lt;br /&gt;
            for i in xrange(1, self.N):&lt;br /&gt;
                self.state[i] = (1812433253 * (self.state[i-1] ^ (self.state[i-1] &amp;gt;&amp;gt; 30)) + i) % 4294967296&lt;br /&gt;
     &lt;br /&gt;
        def __call__(self):&lt;br /&gt;
            &amp;quot;&amp;quot;&amp;quot;gibt die nächste Zufallszahl im Bereich [0, 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;-1] aus&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
            N, M = self.N, 397&lt;br /&gt;
            &lt;br /&gt;
            # Zustand aktualisieren (neue Zufallszahl ausrechnen)&lt;br /&gt;
            i = self.i&lt;br /&gt;
            r = ((self.state[i] &amp;amp; 0x80000000) | (self.state[(i+1)%N] &amp;amp; 0x7FFFFFFF)) &amp;gt;&amp;gt; 1&lt;br /&gt;
            if self.state[(i+1)%N] &amp;amp; 1:&lt;br /&gt;
                r ^= 0x9908B0DF&lt;br /&gt;
            self.state[i] = self.state[(i+M)%N] ^ r&lt;br /&gt;
     &lt;br /&gt;
            # aktuelle Zufallszahl auslesen und ihre Zufälligkeit durch verwürfeln der Bits verbessern&lt;br /&gt;
            y = self.state[i]&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 11)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt;  7) &amp;amp; 0x9D2C5680)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt; 15) &amp;amp; 0xEFC60000)&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 18)&lt;br /&gt;
            &lt;br /&gt;
            # Zustand weitersetzen und endgültige Zufallszahl ausgeben&lt;br /&gt;
            self.i = (self.i + 1) % N&lt;br /&gt;
            return y&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''geg.:''' Zufallszahl &lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, \overbrace{2^{32}-1}^{m-1}]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ges.:''' Zufallszahl&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, k - 1]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''naive Lösung:'''  &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; rand()%k\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;  ist schlecht.&lt;br /&gt;
&lt;br /&gt;
'''Bsp.'''&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; \qquad m = 16\qquad k = 11\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
! rand() || 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15&lt;br /&gt;
|-&lt;br /&gt;
! rand()%k&lt;br /&gt;
! 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 0 || 1 || 2 || 3 || 4 &lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; 0,...,4 kommt doppelt so häufig wie 5,...,10 &amp;quot;nicht zufällig&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lösung:'''  Zurückweisen des Rests der Zahlen (''rejektion sampling'')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; remainder = (m - 1 - (k - 1))% k = (m - k)%k\\&lt;br /&gt;
        \mathrm{ } &amp;amp; last\ Good\ Value = m-1-remainder\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  r = rand()&lt;br /&gt;
  while r &amp;gt; last.GoodValue:&lt;br /&gt;
        r = rand()&lt;br /&gt;
        return r%k&lt;br /&gt;
&lt;br /&gt;
[[Greedy-Algorithmen und Dynamische Programmierung|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5425</id>
		<title>Randomisierte Algorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5425"/>
				<updated>2012-07-27T17:05:04Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Anwendung: Lösen des K-SAT-Problems */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Randomisierte Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
;Definition: Randomisierte Algorithmen sind Algorithmen, die bei Entscheidungen über ihr weiteres Vorgehen oder bei der Wahl ihrer Parameter Zufallszahlen benutzen.&lt;br /&gt;
&lt;br /&gt;
Anschaulich gesprochen, wersucht man bei randomisierten Algorithmen, einen Teil der Lösung zu &amp;lt;i&amp;gt;raten&amp;lt;/i&amp;gt;. Auf den ersten Blick würde man vermuten, dass dabei nicht viel Sinnvolles herauskommen kann. Diese Kapitel wird jedoch zeigen, dass man durch geschicktes Raten tatsächlich zu sehr eleganten Algorithmen gelangen kann.&lt;br /&gt;
&lt;br /&gt;
Grundsätzlich unterscheidet man zwei Arten von randomisierten Algorithmen:&lt;br /&gt;
;Las Vegas - Algorithmen: Das Ergebnis des Algorithmus ist immer korrekt, und die Berechnung erfolgt mit hoher Wahrscheinlichkeit effizient.&lt;br /&gt;
;Monte Carlo - Algorithmen: Die Berechnung ist immer effizient, und das Ergebnis ist mit hoher Wahrscheinlichkeit korrekt.&lt;br /&gt;
Las Vegas-Algorithmen verwendet man, wenn der Algorithmus im ungünstigen Fall eine schlechte Laufzeit hat, und der ungünstige Fall kann durch die Randomisierung sehr unwahrscheinlich gemacht werden. Wir haben in der Vorlesung schon mehrere Las Vegas-Algorithmen kennen gelernt:&lt;br /&gt;
* Quick Sort mit zufälliger Wahl des Pivot-Elements: Die Randomisierung verhindert, dass das Array immer wieder in Subarrays von sehr unterschiedlicher Größe aufgeteilt wird.&lt;br /&gt;
* Treap mit zufälligen Prioritäten: Die Randomisierung verhindert, dass der Baum schlecht balanciert ist.&lt;br /&gt;
* Universelles Hashing: Die zufällige Wahl der Hashfunktion verhindert, dass ein Angreifer eine Schlüsselmenge mit sehr vielen Kollisionen konstruieren kann.&lt;br /&gt;
* Erzeugung einer perfekten Hashfunktion: Durch die Randomisierung entsteht mit nach wenigen Versuchen ein zyklenfreier Graph, der zur Definition der Hashfunktion geeignet ist.&lt;br /&gt;
Monte Carlo-Algorithmen verwendet man dagegen, wenn kein effizienter deterministischer Algorithmus für ein Problem bekannt ist. Man gibt sich dann damit zufrieden, dass der randomisierte Algorithmus die korrekte Lösung nur mit hoher Wahrscheinlichkeit findet, wenn dies dafür sehr effizient geschieht. Bei manchen Problemen ist auch dies unerreichbar - man muss dann bereits zufrieden sein, wenn der Algorithmus mit hoher Wahrscheinlichkeit eine sehr gute Näherungslösung findet. Beliebte Anwendungsgebiete für Monte Carlo-Algorithmen sind beispielsweise&lt;br /&gt;
* Randomisierte Primzahl-Tests: Moderne Verschlüsselungsverfahren benötigen zahlreiche Primzahlen, aber exakte Primzahltests sind teuer. Der [http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin-Test] findet effizient Zahlen, die mit sehr hoher Wahrscheinlichkeit tatsächlich Primzahlen sind.&lt;br /&gt;
* Randomisiertes Testen: Wie jeder Test kann auch eine randomisierter Test nicht die Abwesenheit von Programmierfehlern garantieren, aber man kann durch die Randomisierung viel mehr Testfälle generieren und erhöht so die Erfolgswarscheinlichkeit. Wir haben als Beispiel dafür den [[Korrektheit#Beispiel_f.C3.BCr_das_Testen:_Freivalds_Algorithmus|Algorithmus von Freivald]] behandelt.&lt;br /&gt;
* Lösung schwieriger Optimierungsprobleme: Wir zeigen unten, dass ein randomisierter Algorithmus effizient eine Lösung für das 2-SAT-Problem aus dem vorherigen Kapitel findet (für k-SAT mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; liefert der Algorithmus immer noch mit einer gewissen Wahrscheinlichkeit das richtige Ergebnis, ist aber nicht mehr effizient). Einen effizienten Approximationsalgorithmus für des Problem des Handelsreisenden behandlen wir im Kapitel [[NP-Vollständigkeit]]. Weitere wichtige Beispiele für diesen Bereich sind [http://en.wikipedia.org/wiki/Simulated_annealing simulated annealing] und das [http://de.wikipedia.org/wiki/MCMC-Verfahren Markov-Chain-Monte-Carlo-Verfahren].&lt;br /&gt;
* Robuste Statistik: Eine Grundaufgabe der Statistik ist das Anpassen (Fitten) von Modellen an gemessene Werte. Wenn die Messungen jedoch &amp;quot;Ausreißer&amp;quot; (einige völlig falsche Werte) enthalten, geht die Anpassung schief. Wir beschreiben unten den RANSAC-Algorithmus, der die Ausreißer identifizieren und beim Modellfitten ignorieren kann.&lt;br /&gt;
&lt;br /&gt;
Obwohl randomisierte Algorithmen oft einfach und elegant sind, ist ihre theoretische Analyse (also das Führen von Korrektheits- und Komplexitätsbeweisen) häufig sehr schwierig. Man muss fortgeschrittene Methoden der Wahrscheinlichkeitsrechnung und Statistik beherrschen, um die Wahrscheinlichkeit für das Versagen des Algorithmus zu berechnen und um zu zeigen, wie man den Algorithmus benutzt, damit diese Wahrscheinlichkeit unter einer akzeptablen Schranke bleibt. Die Algorithmen, die wir für diese Vorlesung ausgewählt haben, zeichnen sich dadurch aus, dass die Beweise hier einfach zu erbringen sind.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Lösen des K-SAT-Problems ===&lt;br /&gt;
&lt;br /&gt;
Der &amp;lt;b&amp;gt;Algorithmus von Schöning&amp;lt;/b&amp;gt; löst das [[Graphen_und_Graphenalgorithmen#Normalformen für logische Ausdrücke|k-SAT-Problem]] durch Raten: Wenn ein Ausdruck in k-CNF den Wert False hat, gibt es mindestens eine Klausel, die den Wert False hat. Alle Literale in dieser Klausel haben ebenfalls den Wert False, denn jede Klausel ist eine ODER-Verknüpfung, die nur so False werden kann. Um den Ausdruck zu erfüllen, muss jede Klausel True sein, also müssen wir den Wert von mindestens einem Literal umdrehen. Wenn der Ausruck tatsächlich erfüllbar ist, gibt es immer ein geeignetes Literal, wir wissen nur nicht, welches. Deshalb drehen wir ein unter den k Literalen der betreffenden Klausel zufällig gewähltes. Liegen wir mit unserer Wahl richtig, sind wir der Lösung näher gekommen - im besten Fall sind jetzt alle Klauseln erfüllt. Wählen wir jedoch die falsche Variable, ist die aktuelle Klausel zwar jetzt True, aber dafür werden andere Klauseln zu False, die bisher True waren, und wir entfernen uns somit von der Lösung.&lt;br /&gt;
&lt;br /&gt;
    geg.: logischer Ausdruck in K-CNF (n Variablen, m Klauseln, k Variablen pro Klausel)&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;math&amp;gt;\underbrace {\underbrace {\left(x_1 \vee x_3 \vee...\right)}_{k\; Literale} \wedge \left( x_2 \vee x_4 \vee...\right)}_{m\;Klauseln}&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Algorithmus von Schöning lautet in Pseudocode:&lt;br /&gt;
    for i in range (trials):    #Anzahl der Versuche&lt;br /&gt;
         # Bestimme eine Zufallsbelegung der Variablen &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;:&lt;br /&gt;
         for j in range (steps):&lt;br /&gt;
               if &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt; erfüllt alle Klauseln: &lt;br /&gt;
                   return &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
               # wähle zufällig eine Klausel, die nicht erfüllt ist und negiere zufällig eine der Variablen in dieser Klausel &lt;br /&gt;
               # (die Klausel ist jetzt erfüllt)&lt;br /&gt;
    return None  # keine Lösung gefunden&lt;br /&gt;
&lt;br /&gt;
Findet der Algorithmus eine Lösung, wissen wir, dass der Ausdruck erfüllbar ist. Andernfalls könnte der Ausdruck unerfüllbar sein, oder wir haben nur Pech gehabt. Je mehr erfolglose Versuche wir machen, desto höher ist die Wahrscheinlichkeit, dass das erste zutrifft.&lt;br /&gt;
&lt;br /&gt;
Es ist sinnvoll, &amp;lt;tt&amp;gt;steps = k*n&amp;lt;/tt&amp;gt; zu wählen. Dann gilt der &lt;br /&gt;
;Satz: Wenn ein Ausdruck in k-CNF mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; erfüllbar ist, muss man im Mittel &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in O\left(\left(\frac{2(k-1)}{k}\right)^n \right)&amp;lt;/math&amp;gt; Versuche machen, um eine Lösung zu finden.&lt;br /&gt;
&lt;br /&gt;
Es gilt stets &amp;lt;math&amp;gt;\frac{2(k-1)}{k} &amp;gt; 1&amp;lt;/math&amp;gt;, man benötigt also eine in n exponentielle Anzahl von Versuchen. Bei &amp;lt;math&amp;gt;k=3&amp;lt;/math&amp;gt; gilt z.B. &amp;lt;tt&amp;gt;trials&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt; \in O\left(\left(\frac{4}{3}\right)^n\right)&amp;lt;/math&amp;gt;. Dies ist zwar im Mittel effizienter also die erschöpfende Suche, die &amp;lt;math&amp;gt;O(2^n)&amp;lt;/math&amp;gt; Schritte benötigt, aber immer noch sehr langsam.&lt;br /&gt;
&lt;br /&gt;
Der Fall &amp;lt;b&amp;gt;&amp;lt;math&amp;gt;k=2&amp;lt;/math&amp;gt; ist jedoch ein Sonderfall&amp;lt;/b&amp;gt;: Hier kann man leicht beweisen, dass eine Lösung im Mittel bereits nach &amp;lt;math&amp;gt;O\left(n^2\right)&amp;lt;/math&amp;gt; Schritten gefunden wird. Wenn man schon weiss, dass der Ausdruck erfüllbar ist (was mit [[Graphen_und_Graphenalgorithmen#Lösung des 2-SAT-Problems mit Implikationgraphen|Implikationgraphen]] leicht geprüft werden kann), lässt man den randomisierten Algorithmus einfach so lange laufen, bis er eine Lösung findet. Man setzt also &amp;lt;tt&amp;gt;step = infinity&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;trials = 1&amp;lt;/tt&amp;gt; und verlässt sich darauf, dass das &amp;lt;tt&amp;gt;return&amp;lt;/tt&amp;gt; mit einer gültigen Lösung früher oder später ausgeführt wird. Dass man darauf im Mittel nur &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte warten muss, zeigen wir jetzt mit Hilfe eines &amp;lt;i&amp;gt;random walk&amp;lt;/i&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''-Zufallsbelegung hat  &amp;lt;math&amp;gt;t\leq n&amp;lt;/math&amp;gt; richtige Variablen (im Mittel &amp;lt;math&amp;gt;t\approx \frac {n} 2&amp;lt;/math&amp;gt;)'''&lt;br /&gt;
&lt;br /&gt;
Negieren einer Variable ändert t um 1,&lt;br /&gt;
u.Z. &amp;lt;math&amp;gt;t\rightarrow t+1&amp;lt;/math&amp;gt; mit Wahrscheinlichkeit &amp;lt;math&amp;gt;\frac 1 2&amp;lt;/math&amp;gt; ::(für beliebiges k: &amp;lt;math&amp;gt;\frac 1 k&amp;lt;/math&amp;gt;)&lt;br /&gt;
::::::::::&amp;lt;math&amp;gt;t\rightarrow t-1&amp;lt;/math&amp;gt; mit Wahrscheinlichkeit &amp;lt;math&amp;gt;\frac 1 2&amp;lt;/math&amp;gt; ::(für beliebiges k: &amp;lt;math&amp;gt;\frac {k-1} k&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''-Wieviele Schritte braucht man im Mittel, um zu einer Lösung mit t Richtigen zu kommen?'''&lt;br /&gt;
&lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(t\right)=\frac 1 2 S\left(t-1\right) + \frac 1 2 S\left(t+1\right) +1&amp;lt;/math&amp;gt;&lt;br /&gt;
       &lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(n\right)=0&amp;lt;/math&amp;gt;    #Abbruchbedingung der Schleife&lt;br /&gt;
       &lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(0\right) = S\left( 1\right) + 1 \Rightarrow S\left(t\right) = n^2-t^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
       '''Probe:''' &lt;br /&gt;
 &lt;br /&gt;
       &amp;lt;math&amp;gt;&lt;br /&gt;
       \begin{align} &lt;br /&gt;
             S\left(n\right) &amp;amp; = n^2-n^2=0 \\&lt;br /&gt;
                  &lt;br /&gt;
             S\left(0\right) &amp;amp;= n^2-0^2 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= S\left(1\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2-1^2+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2 \\&lt;br /&gt;
                 &lt;br /&gt;
              S\left(t\right) &amp;amp;= \frac 1 2 \left(n^2-\left(t-1\right)^2\right) + \frac 1 2 \left(n^2-\left(t+1\right)^2\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= \frac 1 2 n^2-\frac 1 2 \left( t^2-2t+1\right) + \frac 1 2 n^2-\frac 1 2 \left(t^2+2t+1\right) + 1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2-t^2&lt;br /&gt;
       \end{align}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Das ist das Random Walk Problem'''&lt;br /&gt;
&lt;br /&gt;
Im ungünstigsten Fall (t=0) werden im Mittel &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte benötigt, um durch random walk nach t=n zu gelangen.&lt;br /&gt;
&lt;br /&gt;
== 2. RANSAC-ALGORITHMUS (Random Sample Consensus)==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''Aufgabe:''&amp;lt;/u&amp;gt; gegeben: Datenpunkte&lt;br /&gt;
::gesucht: Modell, das die Datenpunkte erklärt&lt;br /&gt;
&lt;br /&gt;
[[Image:Rubto.png|thumb|250px|none]]&lt;br /&gt;
&lt;br /&gt;
'''Messpunkte:'''&lt;br /&gt;
   &lt;br /&gt;
      übliche Lösung: Methode der kleinsten Quadrate&lt;br /&gt;
      &lt;br /&gt;
      &amp;lt;math&amp;gt;\min_{a,b} 	\sum_{i} \left(a x_i + b + y_i\right)^2&amp;lt;/math&amp;gt;&lt;br /&gt;
      &lt;br /&gt;
      Schulmathematik:      &amp;lt;math&amp;gt;Minimum\stackrel{\wedge}{=}Ableitung=0&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lineares Gleichungssystem'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{da}\sum{i} \left(ax_i+b-y_i\right)^2=\sum{i} \frac{d}{da} \left[ax_i+b-y_i\right)^2&amp;lt;/math&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(g\left(x\right)\right)&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(x\right)=x^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;y\left(a\right)=ax_i+b-y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;=\sum_{i}2\left(ax_i+b-y_i\right)\frac{d}{da} \underbrace {ax_i+b-y_i}_{x_i}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\underline {=2\sum_{i}\left(ax_i+b-y_i\right)x_i\stackrel{!}{=}0}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}{x_i}^2+b\sum_{i}x_i=\sum_{i}x_iy_i&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}x_i+b\sum_{i}1=\sum_{i}y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{db}\sum_{i}\left(ax_i+b-y_i\right)^2=2\sum_{i}\left(ax_i+b-y_i\right)*1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:Problem: &amp;lt;math&amp;gt;\epsilon  %&amp;lt;/math&amp;gt; der Datenpunkte sind Outlier&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\Longrightarrow&amp;lt;/math&amp;gt; Einfaches Anpassen des Modells an die Datenpunkte funktioniert nicht&lt;br /&gt;
&lt;br /&gt;
:Seien mindestens k Datenpunkte notwendig, um das Programm anpassen zu können&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
RANSAC-Algorithmus&lt;br /&gt;
&lt;br /&gt;
      for  l in range (trials):&lt;br /&gt;
           wähle zufällig k Punkte aus&lt;br /&gt;
           passe das Modell an die k Punkte an&lt;br /&gt;
           zähle, wieviele Punkte in der Nähe des Modells liegen (d.h. &amp;lt;math&amp;gt;d_i &amp;lt; d_max&amp;lt;/math&amp;gt; muss geschickt gewählt werden) &lt;br /&gt;
                                           #Bsp. Geradenfinden:-wähle a,b aus zwei Punkten&lt;br /&gt;
                                                               -berechne: &amp;lt;math&amp;gt;|ax_i+b-y_i|=d_i&amp;lt;/math&amp;gt;&lt;br /&gt;
                                                               -zähle Punkt i als Inlier, falls &amp;lt;math&amp;gt;d_i&amp;lt;d_ma&amp;lt;/math&amp;gt;&lt;br /&gt;
      return: Modell mit höchster Zahl der Inlier&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;trials= \frac{log\left(1-p\right)}{log\left(1-\left(1-\epsilon\right)^k\right)}&amp;lt;/math&amp;gt;  mit k=Anzahl der Datenpunkte und p=Erfolgswahrscheinlichkeit, &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;=Outlier-Anteil&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Erfolgswahrscheinlichkeit: p=99%'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{|c||c|c|c|c|c|}&lt;br /&gt;
         Beispiel &amp;amp; k &amp;amp; \epsilon=10% &amp;amp; 20% &amp;amp; 50% &amp;amp; 70%\\&lt;br /&gt;
         \hline&lt;br /&gt;
         Linie\;in\;2D &amp;amp; 2 &amp;amp; 3 &amp;amp;5 &amp;amp; 17 &amp;amp; 49\\&lt;br /&gt;
         Kreis\;in\;2D &amp;amp; 3 &amp;amp; 4 &amp;amp; 7 &amp;amp; 35 &amp;amp; 169\\&lt;br /&gt;
         Ebene\;in\;3D &amp;amp; 8 &amp;amp; 9 &amp;amp; 26 &amp;amp; 1172 &amp;amp; 70188\\&lt;br /&gt;
       \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Ein Spiel: Wie viel Schritte braucht man im Mittel zum Ziel?'''&lt;br /&gt;
&lt;br /&gt;
   geg.: 5 Plätze, 2 Personen: eine Person rückt vom einem Platz zu dem enderen Platz;&lt;br /&gt;
         die zweite Person wirft die Münze.&lt;br /&gt;
         Wenn die Münze auf Kopf landet, rücke nach rechts und wenn die Münze auf Zahl landet, rücke nach links.&lt;br /&gt;
         &amp;lt;--- Zahl                                                         Kopf--&amp;gt;&lt;br /&gt;
         Kopf: /////&lt;br /&gt;
         Zahl: /// &lt;br /&gt;
&lt;br /&gt;
:: =&amp;gt; mit 8 Schritten bis zum Ziel&lt;br /&gt;
:im Mittel: bei N Plätzen braucht man N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritte&lt;br /&gt;
&lt;br /&gt;
: all: mit N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritten um N Plätze rücken&lt;br /&gt;
: Wie viel Schritte braucht man im Mittel zum Ziel?&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(N\right)=0&amp;lt;/math&amp;gt;    #wenn wir uns im Stuhl Nr.1 befinden&lt;br /&gt;
           &lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)=\frac 1 2 S\left(1 + S\left(i+1\right)\right) + \frac 1 2 S\left(1 + S\left(i-1\right)\right) = \frac 1 2 S\left(i+1\right) + \frac 1 2 S\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(0\right)=1 + S\left(1\right)&amp;lt;/math&amp;gt;    #bei 0.Platz&lt;br /&gt;
&lt;br /&gt;
:::*Lösung: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:::*speziell: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2&amp;lt;/math&amp;gt;           #wenn man am ungünstigsten Platz startet&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
'''Beziehung zu randomisiertem 2-SAT'''&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Platz &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; &amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert,  &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)=N^2 - \left(\frac N 2\right)^2 = N^2 - \frac N 4 ^2 = \frac 3 4 N^2 &amp;lt;/math&amp;gt;&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)&amp;lt;/math&amp;gt;     # Anfangszustand&lt;br /&gt;
----&lt;br /&gt;
== '''Las Vegas vs. Monte Carlo'''==&lt;br /&gt;
&lt;br /&gt;
   * ''Las Vegas - Algorithmen''&lt;br /&gt;
     - Ergebnis ist immer korrekt.&lt;br /&gt;
     - Berechnung ist mit hoher Wahrscheinlichkeit effizient (d.h. Randomisierung macht den ungünstigsten Fall unwahrscheinlich).&lt;br /&gt;
&lt;br /&gt;
   * ''Monte Carlo - Algorithmen''&lt;br /&gt;
     - Berechnung immer effizient.&lt;br /&gt;
     - Ergebnis mit hoher Wahrscheinlichkeit korrekt (falls kein effizienter Algorithmus bekannt, der immer die richtige Lösung liefert).&lt;br /&gt;
&lt;br /&gt;
{| border = &amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;center&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! Las Vegas&lt;br /&gt;
! Monte Carlo&lt;br /&gt;
|- &lt;br /&gt;
| - Erzeugen einer perfekten Hashfuktion &lt;br /&gt;
| - Algorithmus von Freiwald(Matrizenmultiplikation)&lt;br /&gt;
|-&lt;br /&gt;
| - universelles Hashing&lt;br /&gt;
| - RANSAC&lt;br /&gt;
|-&lt;br /&gt;
| - Quick Sort mit zufälliger Wahl des Pivot-Elements&lt;br /&gt;
| - randomisierte K-SAT(k&amp;gt;=3)(Alg. von Schöning)&lt;br /&gt;
|-&lt;br /&gt;
| - Treep mit zufälligen Prioritäten&lt;br /&gt;
| -&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== ''' Zufallszahlen ''' ==&lt;br /&gt;
&lt;br /&gt;
:- kann man nicht mit deterministischen Computern erzeugen&lt;br /&gt;
:- aber man kann Pseudo-Zufallszahlen erzeugen, die viele Eigenschaften von echten Zufallszahlen haben&lt;br /&gt;
::: * sehr ähnlich  zum Hash&lt;br /&gt;
&lt;br /&gt;
     ''&amp;quot;linear Conguential Random number generator&amp;quot;''&lt;br /&gt;
        &amp;lt;math&amp;gt;I_{i+1}= \left(a*I_i + c\right)\textrm{mod\ } m&amp;lt;/math&amp;gt;&lt;br /&gt;
        &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{=&amp;gt; } &amp;amp; I_i \in [0, m-1]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:-sorgfältige Wahl von  a, c, m notwendig&lt;br /&gt;
::'''Bsp.'''  m = 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;&lt;br /&gt;
::: a = 1664525, c = 1013904223&lt;br /&gt;
::: ''&amp;quot;quick and dirty generator&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
==='''Nachteile'''===&lt;br /&gt;
&lt;br /&gt;
* nicht zufällig genug für viele Anwendungen&lt;br /&gt;
::'''Bsp.''' wähle Punkt in R&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
      \mathrm{ } &amp;amp; p = (rand(), rand(), rand())\\&lt;br /&gt;
&lt;br /&gt;
      \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::gibt Zahl u, v, w so, dass &lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; u * p[0] + v * p[1] + w * p[3]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::stark geclustert ist.&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge ist zu kurz:&lt;br /&gt;
:: spätestens nach m Schritten wiederholt sich die Folge&lt;br /&gt;
&lt;br /&gt;
::'''allgemein''': falls der interne Zustand des Zufallsgenerators ''k'' bits hat, ist Periodenlänge:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; Periode &amp;lt; 2^k\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* ''lowbits'' sind weniger zufällig als die ''highbits''&lt;br /&gt;
----&lt;br /&gt;
=== ''Mersenne Twister''===&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
'''bester zur Zeit bekannter Zufallszahlengenerator (ZZG)'''&lt;br /&gt;
* innere Zustand: &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; 624*32 bit\ Integers  =&amp;gt; 19968 bits\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge: &amp;lt;math&amp;gt;2^ {19937} \approx 4 * 10^{6000}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Punkte aus aufeinanderfolgende Zufallszahlen in &amp;lt;math&amp;gt;\mathbb{R}^n&amp;lt;/math&amp;gt; sind gleich verteilt bis &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; n = 623\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* alle Bits sind unabhängig voneinander zufällig (&amp;quot;Twister&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
* schnell&lt;br /&gt;
&lt;br /&gt;
    class MersenneTwister:&lt;br /&gt;
        &lt;br /&gt;
        def __init__(self, seed):&lt;br /&gt;
            self.N = 624  # Größe des inneren Zustands festlegen&lt;br /&gt;
            self.i = 0    # zählt mit in welchem Zustand wir uns gerade aufhalten&lt;br /&gt;
            &lt;br /&gt;
            self.state = [0]*self.N  # Speicher für den inneren Zustand reservieren&lt;br /&gt;
            &lt;br /&gt;
            self.state[0] = seed     # initiale Zufallszahl vom Benutzer&lt;br /&gt;
            # den Rest des inneren Zustands mit einfachem Zufallszahlengenerator initialisieren&lt;br /&gt;
            for i in xrange(1, self.N):&lt;br /&gt;
                self.state[i] = (1812433253 * (self.state[i-1] ^ (self.state[i-1] &amp;gt;&amp;gt; 30)) + i) % 4294967296&lt;br /&gt;
     &lt;br /&gt;
        def __call__(self):&lt;br /&gt;
            &amp;quot;&amp;quot;&amp;quot;gibt die nächste Zufallszahl im Bereich [0, 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;-1] aus&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
            N, M = self.N, 397&lt;br /&gt;
            &lt;br /&gt;
            # Zustand aktualisieren (neue Zufallszahl ausrechnen)&lt;br /&gt;
            i = self.i&lt;br /&gt;
            r = ((self.state[i] &amp;amp; 0x80000000) | (self.state[(i+1)%N] &amp;amp; 0x7FFFFFFF)) &amp;gt;&amp;gt; 1&lt;br /&gt;
            if self.state[(i+1)%N] &amp;amp; 1:&lt;br /&gt;
                r ^= 0x9908B0DF&lt;br /&gt;
            self.state[i] = self.state[(i+M)%N] ^ r&lt;br /&gt;
     &lt;br /&gt;
            # aktuelle Zufallszahl auslesen und ihre Zufälligkeit durch verwürfeln der Bits verbessern&lt;br /&gt;
            y = self.state[i]&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 11)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt;  7) &amp;amp; 0x9D2C5680)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt; 15) &amp;amp; 0xEFC60000)&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 18)&lt;br /&gt;
            &lt;br /&gt;
            # Zustand weitersetzen und endgültige Zufallszahl ausgeben&lt;br /&gt;
            self.i = (self.i + 1) % N&lt;br /&gt;
            return y&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''geg.:''' Zufallszahl &lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, \overbrace{2^{32}-1}^{m-1}]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ges.:''' Zufallszahl&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, k - 1]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''naive Lösung:'''  &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; rand()%k\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;  ist schlecht.&lt;br /&gt;
&lt;br /&gt;
'''Bsp.'''&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; \qquad m = 16\qquad k = 11\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
! rand() || 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15&lt;br /&gt;
|-&lt;br /&gt;
! rand()%k&lt;br /&gt;
! 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 0 || 1 || 2 || 3 || 4 &lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; 0,...,4 kommt doppelt so häufig wie 5,...,10 &amp;quot;nicht zufällig&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lösung:'''  Zurückweisen des Rests der Zahlen (''rejektion sampling'')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; remainder = (m - 1 - (k - 1))% k = (m - k)%k\\&lt;br /&gt;
        \mathrm{ } &amp;amp; last\ Good\ Value = m-1-remainder\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  r = rand()&lt;br /&gt;
  while r &amp;gt; last.GoodValue:&lt;br /&gt;
        r = rand()&lt;br /&gt;
        return r%k&lt;br /&gt;
&lt;br /&gt;
[[Greedy-Algorithmen und Dynamische Programmierung|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5424</id>
		<title>Randomisierte Algorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Randomisierte_Algorithmen&amp;diff=5424"/>
				<updated>2012-07-27T16:16:54Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Randomisierte Algorithmen */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Randomisierte Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
;Definition: Randomisierte Algorithmen sind Algorithmen, die bei Entscheidungen über ihr weiteres Vorgehen oder bei der Wahl ihrer Parameter Zufallszahlen benutzen.&lt;br /&gt;
&lt;br /&gt;
Anschaulich gesprochen, wersucht man bei randomisierten Algorithmen, einen Teil der Lösung zu &amp;lt;i&amp;gt;raten&amp;lt;/i&amp;gt;. Auf den ersten Blick würde man vermuten, dass dabei nicht viel Sinnvolles herauskommen kann. Diese Kapitel wird jedoch zeigen, dass man durch geschicktes Raten tatsächlich zu sehr eleganten Algorithmen gelangen kann.&lt;br /&gt;
&lt;br /&gt;
Grundsätzlich unterscheidet man zwei Arten von randomisierten Algorithmen:&lt;br /&gt;
;Las Vegas - Algorithmen: Das Ergebnis des Algorithmus ist immer korrekt, und die Berechnung erfolgt mit hoher Wahrscheinlichkeit effizient.&lt;br /&gt;
;Monte Carlo - Algorithmen: Die Berechnung ist immer effizient, und das Ergebnis ist mit hoher Wahrscheinlichkeit korrekt.&lt;br /&gt;
Las Vegas-Algorithmen verwendet man, wenn der Algorithmus im ungünstigen Fall eine schlechte Laufzeit hat, und der ungünstige Fall kann durch die Randomisierung sehr unwahrscheinlich gemacht werden. Wir haben in der Vorlesung schon mehrere Las Vegas-Algorithmen kennen gelernt:&lt;br /&gt;
* Quick Sort mit zufälliger Wahl des Pivot-Elements: Die Randomisierung verhindert, dass das Array immer wieder in Subarrays von sehr unterschiedlicher Größe aufgeteilt wird.&lt;br /&gt;
* Treap mit zufälligen Prioritäten: Die Randomisierung verhindert, dass der Baum schlecht balanciert ist.&lt;br /&gt;
* Universelles Hashing: Die zufällige Wahl der Hashfunktion verhindert, dass ein Angreifer eine Schlüsselmenge mit sehr vielen Kollisionen konstruieren kann.&lt;br /&gt;
* Erzeugung einer perfekten Hashfunktion: Durch die Randomisierung entsteht mit nach wenigen Versuchen ein zyklenfreier Graph, der zur Definition der Hashfunktion geeignet ist.&lt;br /&gt;
Monte Carlo-Algorithmen verwendet man dagegen, wenn kein effizienter deterministischer Algorithmus für ein Problem bekannt ist. Man gibt sich dann damit zufrieden, dass der randomisierte Algorithmus die korrekte Lösung nur mit hoher Wahrscheinlichkeit findet, wenn dies dafür sehr effizient geschieht. Bei manchen Problemen ist auch dies unerreichbar - man muss dann bereits zufrieden sein, wenn der Algorithmus mit hoher Wahrscheinlichkeit eine sehr gute Näherungslösung findet. Beliebte Anwendungsgebiete für Monte Carlo-Algorithmen sind beispielsweise&lt;br /&gt;
* Randomisierte Primzahl-Tests: Moderne Verschlüsselungsverfahren benötigen zahlreiche Primzahlen, aber exakte Primzahltests sind teuer. Der [http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin-Test] findet effizient Zahlen, die mit sehr hoher Wahrscheinlichkeit tatsächlich Primzahlen sind.&lt;br /&gt;
* Randomisiertes Testen: Wie jeder Test kann auch eine randomisierter Test nicht die Abwesenheit von Programmierfehlern garantieren, aber man kann durch die Randomisierung viel mehr Testfälle generieren und erhöht so die Erfolgswarscheinlichkeit. Wir haben als Beispiel dafür den [[Korrektheit#Beispiel_f.C3.BCr_das_Testen:_Freivalds_Algorithmus|Algorithmus von Freivald]] behandelt.&lt;br /&gt;
* Lösung schwieriger Optimierungsprobleme: Wir zeigen unten, dass ein randomisierter Algorithmus effizient eine Lösung für das 2-SAT-Problem aus dem vorherigen Kapitel findet (für k-SAT mit &amp;lt;math&amp;gt;k \ge 3&amp;lt;/math&amp;gt; liefert der Algorithmus immer noch mit einer gewissen Wahrscheinlichkeit das richtige Ergebnis, ist aber nicht mehr effizient). Einen effizienten Approximationsalgorithmus für des Problem des Handelsreisenden behandlen wir im Kapitel [[NP-Vollständigkeit]]. Weitere wichtige Beispiele für diesen Bereich sind [http://en.wikipedia.org/wiki/Simulated_annealing simulated annealing] und das [http://de.wikipedia.org/wiki/MCMC-Verfahren Markov-Chain-Monte-Carlo-Verfahren].&lt;br /&gt;
* Robuste Statistik: Eine Grundaufgabe der Statistik ist das Anpassen (Fitten) von Modellen an gemessene Werte. Wenn die Messungen jedoch &amp;quot;Ausreißer&amp;quot; (einige völlig falsche Werte) enthalten, geht die Anpassung schief. Wir beschreiben unten den RANSAC-Algorithmus, der die Ausreißer identifizieren und beim Modellfitten ignorieren kann.&lt;br /&gt;
&lt;br /&gt;
Obwohl randomisierte Algorithmen oft einfach und elegant sind, ist ihre theoretische Analyse (also das Führen von Korrektheits- und Komplexitätsbeweisen) häufig sehr schwierig. Man muss fortgeschrittene Methoden der Wahrscheinlichkeitsrechnung und Statistik beherrschen, um die Wahrscheinlichkeit für das Versagen des Algorithmus zu berechnen und um zu zeigen, wie man den Algorithmus benutzt, damit diese Wahrscheinlichkeit unter einer akzeptablen Schranke bleibt. Die Algorithmen, die wir für diese Vorlesung ausgewählt haben, zeichnen sich dadurch aus, dass die Beweise hier einfach zu erbringen sind.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Lösen des K-SAT-Problems ===&lt;br /&gt;
    geg.: logischer Ausdruck in K-CNF (n Variablen, m Klauseln, k Variablen pro Klausel)&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;math&amp;gt;\underbrace {\underbrace {\left(x_1 \vee x_3 \vee...\right)}_{k\; Variablen} \wedge \left( x_2 \vee x_4 \vee...\right)}_{m\;Klauseln}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    for i in range (trials):    #Anzahl der Versuche&lt;br /&gt;
         #Bestimme eine Zufallsbelegung des &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;:&lt;br /&gt;
         for j in range (steps):&lt;br /&gt;
               if &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt; erfüllt alle Klauseln: return &amp;lt;math&amp;gt;\{ x_i \}&amp;lt;/math&amp;gt;&lt;br /&gt;
               #wähle zufällig eine Klausel, die nicht erfüllt ist und negiere zufällig eine der Variablen in dieser Klausel &lt;br /&gt;
               (die Klausel ist jetzt erfüllt)&lt;br /&gt;
    return None&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Eigenschaft: falls &amp;lt;math&amp;gt;k&amp;gt;2&amp;lt;/math&amp;gt; : steps *trials &amp;lt;math&amp;gt;\in O\left(\Alpha^n \right) \Alpha &amp;gt;1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
z.B. &amp;lt;math&amp;gt;k=3&amp;lt;/math&amp;gt; steps=3*n, trials=&amp;lt;math&amp;gt;\left(\frac{4}3\right)^n&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
aber: bei &amp;lt;math&amp;gt;k=2&amp;lt;/math&amp;gt; sind im Mittel nur steps=&amp;lt;math&amp;gt;O\left(n^2\right)&amp;lt;/math&amp;gt; nötig, trials=&amp;lt;math&amp;gt;O\left(1\right)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''-Zufallsbelegung hat  &amp;lt;math&amp;gt;t\leq n&amp;lt;/math&amp;gt; richtige Variablen (im Mittel &amp;lt;math&amp;gt;t\approx \frac {n} 2&amp;lt;/math&amp;gt;)'''&lt;br /&gt;
&lt;br /&gt;
Negieren einer Variable ändert t um 1,&lt;br /&gt;
u.Z. &amp;lt;math&amp;gt;t\rightarrow t+1&amp;lt;/math&amp;gt; mit Wahrscheinlichkeit &amp;lt;math&amp;gt;\frac 1 2&amp;lt;/math&amp;gt; ::(für beliebiges k: &amp;lt;math&amp;gt;\frac 1 k&amp;lt;/math&amp;gt;)&lt;br /&gt;
::::::::::&amp;lt;math&amp;gt;t\rightarrow t-1&amp;lt;/math&amp;gt; mit Wahrscheinlichkeit &amp;lt;math&amp;gt;\frac 1 2&amp;lt;/math&amp;gt; ::(für beliebiges k: &amp;lt;math&amp;gt;\frac {k-1} k&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''-Wieviele Schritte braucht man im Mittel, um zu einer Lösung mit t Richtigen zu kommen?'''&lt;br /&gt;
&lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(t\right)=\frac 1 2 S\left(t-1\right) + \frac 1 2 S\left(t+1\right) +1&amp;lt;/math&amp;gt;&lt;br /&gt;
       &lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(n\right)=0&amp;lt;/math&amp;gt;    #Abbruchbedingung der Schleife&lt;br /&gt;
       &lt;br /&gt;
       &amp;lt;math&amp;gt;S\left(0\right) = S\left( 1\right) + 1 \Rightarrow S\left(t\right) = n^2-t^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
       '''Probe:''' &lt;br /&gt;
 &lt;br /&gt;
       &amp;lt;math&amp;gt;&lt;br /&gt;
       \begin{align} &lt;br /&gt;
             S\left(n\right) &amp;amp; = n^2-n^2=0 \\&lt;br /&gt;
                  &lt;br /&gt;
             S\left(0\right) &amp;amp;= n^2-0^2 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= S\left(1\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2-1^2+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2 \\&lt;br /&gt;
                 &lt;br /&gt;
              S\left(t\right) &amp;amp;= \frac 1 2 \left(n^2-\left(t-1\right)^2\right) + \frac 1 2 \left(n^2-\left(t+1\right)^2\right)+1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= \frac 1 2 n^2-\frac 1 2 \left( t^2-2t+1\right) + \frac 1 2 n^2-\frac 1 2 \left(t^2+2t+1\right) + 1 \\&lt;br /&gt;
              &lt;br /&gt;
                   &amp;amp;= n^2-t^2&lt;br /&gt;
       \end{align}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Das ist das Random Walk Problem'''&lt;br /&gt;
&lt;br /&gt;
Im ungünstigsten Fall (t=0) werden im Mittel &amp;lt;math&amp;gt;n^2&amp;lt;/math&amp;gt; Schritte benötigt, um durch random walk nach t=n zu gelangen.&lt;br /&gt;
&lt;br /&gt;
== 2. RANSAC-ALGORITHMUS (Random Sample Consensus)==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''Aufgabe:''&amp;lt;/u&amp;gt; gegeben: Datenpunkte&lt;br /&gt;
::gesucht: Modell, das die Datenpunkte erklärt&lt;br /&gt;
&lt;br /&gt;
[[Image:Rubto.png|thumb|250px|none]]&lt;br /&gt;
&lt;br /&gt;
'''Messpunkte:'''&lt;br /&gt;
   &lt;br /&gt;
      übliche Lösung: Methode der kleinsten Quadrate&lt;br /&gt;
      &lt;br /&gt;
      &amp;lt;math&amp;gt;\min_{a,b} 	\sum_{i} \left(a x_i + b + y_i\right)^2&amp;lt;/math&amp;gt;&lt;br /&gt;
      &lt;br /&gt;
      Schulmathematik:      &amp;lt;math&amp;gt;Minimum\stackrel{\wedge}{=}Ableitung=0&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lineares Gleichungssystem'''&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{da}\sum{i} \left(ax_i+b-y_i\right)^2=\sum{i} \frac{d}{da} \left[ax_i+b-y_i\right)^2&amp;lt;/math&amp;gt;  &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(g\left(x\right)\right)&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;f\left(x\right)=x^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::&amp;lt;math&amp;gt;y\left(a\right)=ax_i+b-y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;=\sum_{i}2\left(ax_i+b-y_i\right)\frac{d}{da} \underbrace {ax_i+b-y_i}_{x_i}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\underline {=2\sum_{i}\left(ax_i+b-y_i\right)x_i\stackrel{!}{=}0}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}{x_i}^2+b\sum_{i}x_i=\sum_{i}x_iy_i&amp;lt;/math&amp;gt;   &lt;br /&gt;
&lt;br /&gt;
::::::&amp;lt;math&amp;gt;a\sum_{i}x_i+b\sum_{i}1=\sum_{i}y_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\frac{d}{db}\sum_{i}\left(ax_i+b-y_i\right)^2=2\sum_{i}\left(ax_i+b-y_i\right)*1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:Problem: &amp;lt;math&amp;gt;\epsilon  %&amp;lt;/math&amp;gt; der Datenpunkte sind Outlier&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;\Longrightarrow&amp;lt;/math&amp;gt; Einfaches Anpassen des Modells an die Datenpunkte funktioniert nicht&lt;br /&gt;
&lt;br /&gt;
:Seien mindestens k Datenpunkte notwendig, um das Programm anpassen zu können&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
RANSAC-Algorithmus&lt;br /&gt;
&lt;br /&gt;
      for  l in range (trials):&lt;br /&gt;
           wähle zufällig k Punkte aus&lt;br /&gt;
           passe das Modell an die k Punkte an&lt;br /&gt;
           zähle, wieviele Punkte in der Nähe des Modells liegen (d.h. &amp;lt;math&amp;gt;d_i &amp;lt; d_max&amp;lt;/math&amp;gt; muss geschickt gewählt werden) &lt;br /&gt;
                                           #Bsp. Geradenfinden:-wähle a,b aus zwei Punkten&lt;br /&gt;
                                                               -berechne: &amp;lt;math&amp;gt;|ax_i+b-y_i|=d_i&amp;lt;/math&amp;gt;&lt;br /&gt;
                                                               -zähle Punkt i als Inlier, falls &amp;lt;math&amp;gt;d_i&amp;lt;d_ma&amp;lt;/math&amp;gt;&lt;br /&gt;
      return: Modell mit höchster Zahl der Inlier&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;trials= \frac{log\left(1-p\right)}{log\left(1-\left(1-\epsilon\right)^k\right)}&amp;lt;/math&amp;gt;  mit k=Anzahl der Datenpunkte und p=Erfolgswahrscheinlichkeit, &amp;lt;math&amp;gt;\epsilon&amp;lt;/math&amp;gt;=Outlier-Anteil&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Erfolgswahrscheinlichkeit: p=99%'''&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{|c||c|c|c|c|c|}&lt;br /&gt;
         Beispiel &amp;amp; k &amp;amp; \epsilon=10% &amp;amp; 20% &amp;amp; 50% &amp;amp; 70%\\&lt;br /&gt;
         \hline&lt;br /&gt;
         Linie\;in\;2D &amp;amp; 2 &amp;amp; 3 &amp;amp;5 &amp;amp; 17 &amp;amp; 49\\&lt;br /&gt;
         Kreis\;in\;2D &amp;amp; 3 &amp;amp; 4 &amp;amp; 7 &amp;amp; 35 &amp;amp; 169\\&lt;br /&gt;
         Ebene\;in\;3D &amp;amp; 8 &amp;amp; 9 &amp;amp; 26 &amp;amp; 1172 &amp;amp; 70188\\&lt;br /&gt;
       \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Ein Spiel: Wie viel Schritte braucht man im Mittel zum Ziel?'''&lt;br /&gt;
&lt;br /&gt;
   geg.: 5 Plätze, 2 Personen: eine Person rückt vom einem Platz zu dem enderen Platz;&lt;br /&gt;
         die zweite Person wirft die Münze.&lt;br /&gt;
         Wenn die Münze auf Kopf landet, rücke nach rechts und wenn die Münze auf Zahl landet, rücke nach links.&lt;br /&gt;
         &amp;lt;--- Zahl                                                         Kopf--&amp;gt;&lt;br /&gt;
         Kopf: /////&lt;br /&gt;
         Zahl: /// &lt;br /&gt;
&lt;br /&gt;
:: =&amp;gt; mit 8 Schritten bis zum Ziel&lt;br /&gt;
:im Mittel: bei N Plätzen braucht man N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritte&lt;br /&gt;
&lt;br /&gt;
: all: mit N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; Schritten um N Plätze rücken&lt;br /&gt;
: Wie viel Schritte braucht man im Mittel zum Ziel?&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(N\right)=0&amp;lt;/math&amp;gt;    #wenn wir uns im Stuhl Nr.1 befinden&lt;br /&gt;
           &lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)=\frac 1 2 S\left(1 + S\left(i+1\right)\right) + \frac 1 2 S\left(1 + S\left(i-1\right)\right) = \frac 1 2 S\left(i+1\right) + \frac 1 2 S\left(i-1\right) +1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(0\right)=1 + S\left(1\right)&amp;lt;/math&amp;gt;    #bei 0.Platz&lt;br /&gt;
&lt;br /&gt;
:::*Lösung: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2 - i^2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:::*speziell: &lt;br /&gt;
&lt;br /&gt;
         &amp;lt;math&amp;gt;S\left(i\right)= N^2&amp;lt;/math&amp;gt;           #wenn man am ungünstigsten Platz startet&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
'''Beziehung zu randomisiertem 2-SAT'''&lt;br /&gt;
&lt;br /&gt;
      &amp;quot;Platz &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; &amp;quot;: &amp;lt;math&amp;gt;i&amp;lt;/math&amp;gt; Variablen haben den richtigen Wert,  &amp;lt;math&amp;gt;\left(N-i\right)&amp;lt;/math&amp;gt;  sind falsch gesetzt&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)=N^2 - \left(\frac N 2\right)^2 = N^2 - \frac N 4 ^2 = \frac 3 4 N^2 &amp;lt;/math&amp;gt;&lt;br /&gt;
      &amp;lt;math&amp;gt;S\left(\frac N 2\right)&amp;lt;/math&amp;gt;     # Anfangszustand&lt;br /&gt;
----&lt;br /&gt;
== '''Las Vegas vs. Monte Carlo'''==&lt;br /&gt;
&lt;br /&gt;
   * ''Las Vegas - Algorithmen''&lt;br /&gt;
     - Ergebnis ist immer korrekt.&lt;br /&gt;
     - Berechnung ist mit hoher Wahrscheinlichkeit effizient (d.h. Randomisierung macht den ungünstigsten Fall unwahrscheinlich).&lt;br /&gt;
&lt;br /&gt;
   * ''Monte Carlo - Algorithmen''&lt;br /&gt;
     - Berechnung immer effizient.&lt;br /&gt;
     - Ergebnis mit hoher Wahrscheinlichkeit korrekt (falls kein effizienter Algorithmus bekannt, der immer die richtige Lösung liefert).&lt;br /&gt;
&lt;br /&gt;
{| border = &amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;center&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
! Las Vegas&lt;br /&gt;
! Monte Carlo&lt;br /&gt;
|- &lt;br /&gt;
| - Erzeugen einer perfekten Hashfuktion &lt;br /&gt;
| - Algorithmus von Freiwald(Matrizenmultiplikation)&lt;br /&gt;
|-&lt;br /&gt;
| - universelles Hashing&lt;br /&gt;
| - RANSAC&lt;br /&gt;
|-&lt;br /&gt;
| - Quick Sort mit zufälliger Wahl des Pivot-Elements&lt;br /&gt;
| - randomisierte K-SAT(k&amp;gt;=3)(Alg. von Schöning)&lt;br /&gt;
|-&lt;br /&gt;
| - Treep mit zufälligen Prioritäten&lt;br /&gt;
| -&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== ''' Zufallszahlen ''' ==&lt;br /&gt;
&lt;br /&gt;
:- kann man nicht mit deterministischen Computern erzeugen&lt;br /&gt;
:- aber man kann Pseudo-Zufallszahlen erzeugen, die viele Eigenschaften von echten Zufallszahlen haben&lt;br /&gt;
::: * sehr ähnlich  zum Hash&lt;br /&gt;
&lt;br /&gt;
     ''&amp;quot;linear Conguential Random number generator&amp;quot;''&lt;br /&gt;
        &amp;lt;math&amp;gt;I_{i+1}= \left(a*I_i + c\right)\textrm{mod\ } m&amp;lt;/math&amp;gt;&lt;br /&gt;
        &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{=&amp;gt; } &amp;amp; I_i \in [0, m-1]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:-sorgfältige Wahl von  a, c, m notwendig&lt;br /&gt;
::'''Bsp.'''  m = 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;&lt;br /&gt;
::: a = 1664525, c = 1013904223&lt;br /&gt;
::: ''&amp;quot;quick and dirty generator&amp;quot;''&lt;br /&gt;
&lt;br /&gt;
==='''Nachteile'''===&lt;br /&gt;
&lt;br /&gt;
* nicht zufällig genug für viele Anwendungen&lt;br /&gt;
::'''Bsp.''' wähle Punkt in R&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
      \mathrm{ } &amp;amp; p = (rand(), rand(), rand())\\&lt;br /&gt;
&lt;br /&gt;
      \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::gibt Zahl u, v, w so, dass &lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; u * p[0] + v * p[1] + w * p[3]\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
::stark geclustert ist.&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge ist zu kurz:&lt;br /&gt;
:: spätestens nach m Schritten wiederholt sich die Folge&lt;br /&gt;
&lt;br /&gt;
::'''allgemein''': falls der interne Zustand des Zufallsgenerators ''k'' bits hat, ist Periodenlänge:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; Periode &amp;lt; 2^k\\&lt;br /&gt;
&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* ''lowbits'' sind weniger zufällig als die ''highbits''&lt;br /&gt;
----&lt;br /&gt;
=== ''Mersenne Twister''===&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
'''bester zur Zeit bekannter Zufallszahlengenerator (ZZG)'''&lt;br /&gt;
* innere Zustand: &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; 624*32 bit\ Integers  =&amp;gt; 19968 bits\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Periodenlänge: &amp;lt;math&amp;gt;2^ {19937} \approx 4 * 10^{6000}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Punkte aus aufeinanderfolgende Zufallszahlen in &amp;lt;math&amp;gt;\mathbb{R}^n&amp;lt;/math&amp;gt; sind gleich verteilt bis &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; n = 623\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* alle Bits sind unabhängig voneinander zufällig (&amp;quot;Twister&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
* schnell&lt;br /&gt;
&lt;br /&gt;
    class MersenneTwister:&lt;br /&gt;
        &lt;br /&gt;
        def __init__(self, seed):&lt;br /&gt;
            self.N = 624  # Größe des inneren Zustands festlegen&lt;br /&gt;
            self.i = 0    # zählt mit in welchem Zustand wir uns gerade aufhalten&lt;br /&gt;
            &lt;br /&gt;
            self.state = [0]*self.N  # Speicher für den inneren Zustand reservieren&lt;br /&gt;
            &lt;br /&gt;
            self.state[0] = seed     # initiale Zufallszahl vom Benutzer&lt;br /&gt;
            # den Rest des inneren Zustands mit einfachem Zufallszahlengenerator initialisieren&lt;br /&gt;
            for i in xrange(1, self.N):&lt;br /&gt;
                self.state[i] = (1812433253 * (self.state[i-1] ^ (self.state[i-1] &amp;gt;&amp;gt; 30)) + i) % 4294967296&lt;br /&gt;
     &lt;br /&gt;
        def __call__(self):&lt;br /&gt;
            &amp;quot;&amp;quot;&amp;quot;gibt die nächste Zufallszahl im Bereich [0, 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;-1] aus&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
            N, M = self.N, 397&lt;br /&gt;
            &lt;br /&gt;
            # Zustand aktualisieren (neue Zufallszahl ausrechnen)&lt;br /&gt;
            i = self.i&lt;br /&gt;
            r = ((self.state[i] &amp;amp; 0x80000000) | (self.state[(i+1)%N] &amp;amp; 0x7FFFFFFF)) &amp;gt;&amp;gt; 1&lt;br /&gt;
            if self.state[(i+1)%N] &amp;amp; 1:&lt;br /&gt;
                r ^= 0x9908B0DF&lt;br /&gt;
            self.state[i] = self.state[(i+M)%N] ^ r&lt;br /&gt;
     &lt;br /&gt;
            # aktuelle Zufallszahl auslesen und ihre Zufälligkeit durch verwürfeln der Bits verbessern&lt;br /&gt;
            y = self.state[i]&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 11)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt;  7) &amp;amp; 0x9D2C5680)&lt;br /&gt;
            y ^= ((y &amp;lt;&amp;lt; 15) &amp;amp; 0xEFC60000)&lt;br /&gt;
            y ^=  (y &amp;gt;&amp;gt; 18)&lt;br /&gt;
            &lt;br /&gt;
            # Zustand weitersetzen und endgültige Zufallszahl ausgeben&lt;br /&gt;
            self.i = (self.i + 1) % N&lt;br /&gt;
            return y&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''geg.:''' Zufallszahl &lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, \overbrace{2^{32}-1}^{m-1}]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''ges.:''' Zufallszahl&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; [0, k - 1]\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''naive Lösung:'''  &amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; rand()%k\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;  ist schlecht.&lt;br /&gt;
&lt;br /&gt;
'''Bsp.'''&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; \qquad m = 16\qquad k = 11\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
! rand() || 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15&lt;br /&gt;
|-&lt;br /&gt;
! rand()%k&lt;br /&gt;
! 0 || 1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 0 || 1 || 2 || 3 || 4 &lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; 0,...,4 kommt doppelt so häufig wie 5,...,10 &amp;quot;nicht zufällig&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Lösung:'''  Zurückweisen des Rests der Zahlen (''rejektion sampling'')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
        \mathrm{ } &amp;amp; remainder = (m - 1 - (k - 1))% k = (m - k)%k\\&lt;br /&gt;
        \mathrm{ } &amp;amp; last\ Good\ Value = m-1-remainder\\&lt;br /&gt;
        \end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  r = rand()&lt;br /&gt;
  while r &amp;gt; last.GoodValue:&lt;br /&gt;
        r = rand()&lt;br /&gt;
        return r%k&lt;br /&gt;
&lt;br /&gt;
[[Greedy-Algorithmen und Dynamische Programmierung|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Effizienz&amp;diff=5423</id>
		<title>Effizienz</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Effizienz&amp;diff=5423"/>
				<updated>2012-07-27T16:04:44Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Effiziente Lösung durch Verdoppeln der Kapazität */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Bei der Diskussion von Effizienz müssen wir zwischen der Laufzeit eines Algorithmus auf einem bestimmten System und seiner prinzipiellen Leistungsfähigkeit (Algorithmenkomplexität) unterscheiden. Der Benutzer ist natürlich vor allem an der Laufzeit interessiert, denn diese bestimmt letztendlich seine Arbeitsproduktivität. Ein Softwaredesigner hingegen muss eine Implementation wählen, die auf verschiedenen Systemen und in verschiedenen Anwendungen schnell ist. Für ihn sind daher auch Aussagen zur Algorithmenkomplexität sehr wichtig, um den am besten geeigneten Algorithmus auszuwählen.&lt;br /&gt;
&lt;br /&gt;
== Laufzeit ==&lt;br /&gt;
&lt;br /&gt;
Aus Anwendersicht ist ein Algorithmus effizient, wenn er die in der Spezifikation verlangten Laufzeitgrenzen einhält. Ein Algorithmus muss also nicht immer so schnell wie möglich sein, sondern so schnell wie nötig. Dies führt in verschiedenen Anwendungen zu ganz unterschiedliche Laufzeitanforderungen:&lt;br /&gt;
&lt;br /&gt;
* Berechnen des nächsten Steuerkommandos für eine Maschine: ca. 1/1000s&lt;br /&gt;
* Berechnen des nächsten Bildes für eine Videopräsentation (z.B. Dekompression von MPEG-kodierten Bildern): ca. 1/25s&lt;br /&gt;
: Geringere Bildraten führen zu ruckeligen Filmen.&lt;br /&gt;
* Sichtbare Antwort auf ein interaktives Kommando (z.B. Mausklick): ca. 1/2s&lt;br /&gt;
: Wird diese Antwortzeit überschritten, vermuten viele Benutzer, dass der Mausklick nicht funktioniert hat, und klicken nochmals, mit eventuell fatalen Folgen. Wenn ein Algorithmus notwendigerweise länger dauert als 1/2s, sollte ein Fortschrittsbalken angezeigt werden.&lt;br /&gt;
* Wettervorhersage: muss spätestens am Vorabend des vorhergesagten Tages beendet sein&lt;br /&gt;
&lt;br /&gt;
===Laufzeitvergleich===&lt;br /&gt;
&lt;br /&gt;
Da die Laufzeit für den Benutzer ein so wichtiges Kriterium ist, werden häufig Laufzeitvergleiche durchgeführt. Deren Ergebnisse hängen allerdings von vielen Faktoren ab, die möglicherweise nicht kontrollierbar sind:&lt;br /&gt;
* Geschwindigkeit und Anzahl der Prozessoren&lt;br /&gt;
* Auslastung des Systems&lt;br /&gt;
* Größe des Hauptspeichers  und Cache, Geschwindigkeit des  Datenbus&lt;br /&gt;
* Qualität des Compilers/Optimierers (ist der Compiler für die spezielle Prozessor-Architektur optimiert?)&lt;br /&gt;
* Geschick des Programmierers&lt;br /&gt;
* Daten (Beispiel Quicksort: Best case und worst case [vorsortierter Input] stark unterschiedlich)&lt;br /&gt;
All diese Faktoren sind untereinander abhängig. Laufzeitvergleiche sind daher mit Vorsicht zu interpretieren.&lt;br /&gt;
Generell sollten bei Vergleichen möglichst wenige Parameter verändert werden, z.B.&lt;br /&gt;
* gleiches Programm (gleiche Kompilierung), gleiche Daten, andere Prozessoren&lt;br /&gt;
oder&lt;br /&gt;
* gleiche CPU, Daten, andere Programme (Vergleich von Algorithmen)&lt;br /&gt;
Zur Verbesserung der Vergleichbarkeit gibt es standardisierte [http://en.wikipedia.org/wiki/Benchmark_(computing) Benchmarks], die bestimmte Aspekte eines Systems unter möglichst realitätsnahen Bedingungen testen. Generell gilt aber: Durch Laufzeitmessung ist schwer festzustellen, ob ein Algorithmus ''prinzipiell'' besser ist als ein anderer. Dafür ist die Analyse der [[Effizienz#Algorithmen-Komplexität|Algorithmenkomplexität]] notwendig.&lt;br /&gt;
&lt;br /&gt;
===Optimierung der Laufzeit===&lt;br /&gt;
&lt;br /&gt;
Wenn sich herausstellt, dass ein bereits implementierter Algorithmus zu langsam läuft, geht man wie folgt vor:&lt;br /&gt;
&lt;br /&gt;
# Man verwendet einen [http://en.wikipedia.org/wiki/Performance_analysis Profiler], um zunächst den Flaschenhals zu bestimmen. Ein Profiler ist ein Hilfsprogramm, das während der Ausführung eines Programms misst, wieviel Zeit in jeder Funktion und Unterfunktion verbraucht wird. Dadurch kann man herausfinden, welcher Teil des Algorithmus überhaupt Probleme bereitet. Donald Knuth gibt z.B. als Erfahrungswert an, dass Programme während des größten Teils ihrer Laufzeit nur 3% des Quellcodes (natürlich mehrmals wiederholt) ausführen [http://pplab.snu.ac.kr/courses/adv_pl05/papers/p261-knuth.pdf]. Es ist sehr wichtig, diese 3% experimentell zu bestimmen, weil die Erfahrung zeigt, dass man beim Erraten der kritischen Programmteile oft falsch liegt. Man spricht dann von &amp;quot;[http://en.wikipedia.org/wiki/Optimization_%28computer_science%29#When_to_optimize premature optimization]&amp;quot;, also von voreiliger Optimierung ohne experimentelle Untersuchung der wirklichen Laufzeiten, was laut Knuth &amp;quot;the root of all evil&amp;quot; ist. Der Python-Profiler wird in [http://docs.python.org/lib/profile.html Kapitel 25] der Python-Dokumentation beschrieben.&lt;br /&gt;
# Man kann dann versuchen, die kritischen Programmteile zu optimieren.&lt;br /&gt;
# Falls der Laufzeitgewinn durch Optimierung zu gering ist, muss man einen prinzipiell schnelleren Algorithmus verwenden, falls es einen gibt.&lt;br /&gt;
&lt;br /&gt;
Einige wichtige Techniken der Programmoptimierung sollen hier erwähnt werden. Wenn man einen optimierenden Compiler verwendet, werden einige Optimierungen automatisch ausgeführt [http://en.wikipedia.org/wiki/Compiler_optimization]. In Python trifft dies jedoch nicht zu. Um den Sinn einiger Optimierungen zu verstehen, benötigt man Grundkenntnisse der Computerarchitektur.&lt;br /&gt;
&lt;br /&gt;
;Elimination von redundantem Code: Es ist offensichtlich überflüssig, dasselbe Ergebnis mehrmals zu berechnen, wenn es auch zwischengespeichert werden könnte. Diese Optimierung wird von vielen automatischen Optimierern unterstützt und kommt im wesentlichen in zwei Ausprägungen vor:&lt;br /&gt;
:; common subexpression elimination: In mathematischen Ausdrücken wird ein Teilergebnis häufig mehrmals benötigt. Man betrachte z.B. die Lösung der quadratischen Gleichung &amp;lt;math&amp;gt;x^2+p\,x+q = 0&amp;lt;/math&amp;gt;:&lt;br /&gt;
        x1 = - p / 2.0 + sqrt(p*p/4.0 - q)&lt;br /&gt;
        x2 = - p / 2.0 - sqrt(p*p/4.0 - q)&lt;br /&gt;
::Die mehrmalige Berechnung von Teilausdrücken wird vermieden, wenn man stattdessen schreibt:&lt;br /&gt;
        p2 = - p / 2.0&lt;br /&gt;
        r  = sqrt(p2*p2 - q)&lt;br /&gt;
        x1 = p2 + r&lt;br /&gt;
        x2 = p2 - r&lt;br /&gt;
:; loop invariant elimination: Wenn ein Teilausdruck sich in einer Schleife nicht ändert, muss man ihn nicht bei jedem Schleifendurchlauf neu berechnen, sondern kann dies einmal vor Beginn der Schleife tun. Ein typisches Beispiel hierfür ist die Adressierung von Matrizen, die als 1-dimensionales Array gespeichert sind. Angenommen, wir speichern eine NxN Matrix &amp;lt;tt&amp;gt;m&amp;lt;/tt&amp;gt; in einem Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; der Größe N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt;, so dass das Matrixelement &amp;lt;tt&amp;gt;m&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt;&amp;lt;/tt&amp;gt; durch &amp;lt;tt&amp;gt;a[i + j*N]&amp;lt;/tt&amp;gt; indexiert wird. Wir betrachten die Aufgabe, eine Einheitsmatrix zu initialisieren. Ein nicht optimierter Algorithmus dafür lautet:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + j*N] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + j*N] = 0.0&lt;br /&gt;
::Der Ausdruck &amp;lt;tt&amp;gt;j*N&amp;lt;/tt&amp;gt; wird hier in jedem Schleifendurchlauf erneut berechnet, obwohl sich &amp;lt;tt&amp;gt;j&amp;lt;/tt&amp;gt; in der inneren Schleife gar nicht verändert. Man kann deshalb optimieren zu:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + jN] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + jN] = 0.0&lt;br /&gt;
;Vereinfachung der inneren Schleife: Generell sollte man sich bei der Optimierung auf die innere Schleife eines Algorithmus konzentrieren, weil dieser Code am häufigsten ausgeführt wird. Insbesondere sollte man die Anzahl der Befehle in der inneren Schleife so gering wie möglich halten und teure Befehle vermeiden. Früher waren vor allem Floating-Point Befehle teuer, die man oft durch die schnellere Integer-Arithmetik ersetzt hat, falls dies algorithmisch möglich war (diesen Rat findet man noch oft in der Literatur). Heute hat sich die Hardware so verbessert, dass im Allgemeinen nur noch die Floating-Point Division deutlich langsamer ist als die anderen Operatoren. Im obigen Beispiel der quadratischen Gleichung ist es daher sinnvoll, den Ausdruck &lt;br /&gt;
        p2 = -p / 2.0&lt;br /&gt;
:durch&lt;br /&gt;
        p2 = -0.5 * p&lt;br /&gt;
:zu ersetzen. Dadurch ersetzt man eine Division durch eine Multiplikation und spart außerdem das Negieren von &amp;lt;tt&amp;gt;p&amp;lt;/tt&amp;gt;, da der Compiler direkt mit &amp;lt;tt&amp;gt;-0.5&amp;lt;/tt&amp;gt; multipliziert.&lt;br /&gt;
;Ausnutzung der Prozessor-Pipeline: Moderne Prozessoren führen mehrere Befehle parallel aus. Dies ist möglich, weil jeder Befehl in mehrere Teilschritte zerlegt werden kann. Eine generische Unterteilung in vier Teilschritte ist z.B.:&lt;br /&gt;
:# Dekodieren des nächsten Befehls&lt;br /&gt;
:# Beschaffen der Daten, die der Befehl verwendet (aus Prozessorregistern, dem Cache, oder dem Hauptspeicher)&lt;br /&gt;
:# Ausführen des Befehls&lt;br /&gt;
:# Schreiben der Ergebnisse&lt;br /&gt;
:Man bezeichnet dies als die &amp;quot;[http://en.wikipedia.org/wiki/Instruction_pipeline instruction pipeline]&amp;quot; des Prozessors (heutige Prozessoren verwenden wesentlich feinere Unterteilungen). Prozessoren werden nun so gebaut, dass mehrere Befehle parallel, auf verschiedenen Ausführungsstufen ausgeführt werden. Wenn Befehl 1 also beim Schreiben der Ergebnisse angelangt ist, kann Befehl 2 die Hardware zum Ausführen des Befehls benutzen, während Befehl 3 seine Daten holt, und Befehl 4 soeben dekodiert wird. Unter bestimmten Bedingungen funktioniert diese Parallelverarbeitung jedoch nicht. Dies gibt Anlass zu Optimierungen:&lt;br /&gt;
:;Vermeiden unnötiger Typkonvertierungen: Der Prozessor verarbeitet Interger- und Floating-Point-Befehle in verschiedenen Pipelines, weil die Hardwareanforderungen sehr verschieden sind. Wird jetzt ein Ergebnis von Integer nach Floating-Point umgewandelt oder umgekehrt, muss die jeweils andere Pipeline warten, bis die erste Pipeline ihre Berechnung beendet. Es kann dann besser sein, Berechnungen in Floating-Point zu Ende zu führen, auch wenn sie semantisch eigentlich Integer-Berechnungen sind.&lt;br /&gt;
:;Reduzierung der Anzahl von Verzweigungen: Wenn der Code verzweigt (z.B. durch eine &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;- oder  &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Anweisung), ist nicht klar, welcher Befehl nach der Verzweigung ausgeführt werden soll, bevor Stufe 3 der Pipeline die Verzweigungsbedingung ausgewertet hat. Bis dahin wären die ersten beiden Stufen der Pipeline unbenutzt. Moderne Prozessoren benutzen zwar ausgefeilte Heuristiken, um das Ergebnis der Bedingung vorherzusagen, und führen den hoffentlich richtigen Zweig des Codes spekulativ aus, aber dies funktioniert nicht immer. Man sollte deshalb generell die Anzahl der Verzweigungen minimieren. Als Nebeneffekt führt dies meist auch zu besser lesbarem, verständlicherem Code. Im Matrixbeispiel kann man&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + jN] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + jN] = 0.0&lt;br /&gt;
::durch&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               a[i + jN] = 0.0&lt;br /&gt;
           a[j + jN] = 1.0&lt;br /&gt;
ersetzen. Die Diagonalelemente &amp;lt;tt&amp;gt;a[j + jN]&amp;lt;/tt&amp;gt; werden jetzt zwar zweimal initialisiert (in der Schleife auf Null, dann auf Eins), aber durch Elimination der &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage wird dies wahrscheinlich mehr als ausgeglichen, zumal dadurch die innere Schleife wesentlich vereinfacht wurde.&lt;br /&gt;
;Ausnutzen des Prozessor-Cache: Zugriffe auf den Hauptspeicher sind sehr langsam. Deshalb werden stets ganze Speicherseiten auf einmal in den [http://en.wikipedia.org/wiki/Cache Cache] des Prozessors geladen. Wenn unmittelbar nacheinander benutzte Daten auch im Speicher nahe beieinander liegen (sogenannte &amp;quot;[http://en.wikipedia.org/wiki/Locality_of_reference locality of reference]&amp;quot;), ist die Wahrscheinlichkeit groß, dass die als nächstes benötigten Daten bereits im Cache sind und damit schnell gelesen werden können. Bei vielen Algorithmen kann man die Implementation so umordnen, dass die locality of reference verbessert wird, was zu einer drastischen Beschleunigung führt. Im Matrix-Beispiel ist z.B. die Reihenfolge der Schleifen wichtig. Für konstanten Index &amp;lt;tt&amp;gt;j&amp;lt;/tt&amp;gt; liegen die Indizes &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; im Speicher hintereinander. Deshalb ist es günstig, in der inneren Schleife über &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu iterieren:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               a[i + jN] = 0.0&lt;br /&gt;
           a[j + jN] = 1.0&lt;br /&gt;
:Die umgekehrte Reihenfolge der Schleifen ist hingegen ungünstig&lt;br /&gt;
       for i in range(N):&lt;br /&gt;
           for j in range(N):&lt;br /&gt;
               a[i + j*N] = 0.0&lt;br /&gt;
           a[i + i*N] = 1.0&lt;br /&gt;
:Jetzt werden in der inneren Schleife stets N Datenelemente übersprungen. Besonders bei großem N muss man daher häufig den Cache neu füllen, was bei der ersten Implementation nicht notwendig war.  (Außerdem verliert man hier die Optimierung &amp;lt;tt&amp;gt;jN = j*N&amp;lt;/tt&amp;gt;, die jetzt nicht mehr möglich ist.)&lt;br /&gt;
&lt;br /&gt;
Als Faustregel kann man durch Optimierung eine Verdoppelung der Geschwindigkeit erreichen (in Ausnahmefällen auch mehr). Benötigt man stärkere Verbesserungen, muss man wohl oder übel einen besseren Algorithmus oder einen schnelleren Computer verwenden.&lt;br /&gt;
&lt;br /&gt;
== Algorithmen-Komplexität ==&lt;br /&gt;
&lt;br /&gt;
Komplexitätsbetrachtungen ermöglichen den Vergleich der prinzipiellen Eigenschaften von Algorithmen unabhängig von einer Implementation, Umgebung etc.&lt;br /&gt;
      &lt;br /&gt;
Eine einfache Möglichkeit ist das Zählen der Aufrufe einer Schlüsseloperation. Beispiel Sortieren:&lt;br /&gt;
* Anzahl der Vergleiche&lt;br /&gt;
* Anzahl der Vertauschungen&lt;br /&gt;
&lt;br /&gt;
=== Beispiel: Selection Sort ===&lt;br /&gt;
&lt;br /&gt;
  for i in range(len(a)-1):&lt;br /&gt;
    max = i&lt;br /&gt;
    for j in range(i+1, len(a)):&lt;br /&gt;
      if a[j] &amp;lt; a[max]:&lt;br /&gt;
        max = j&lt;br /&gt;
    a[max], a[i] = a[i], a[max]      # swap&lt;br /&gt;
&lt;br /&gt;
*Anzahl der Vergleiche: Ein Vergleich in jedem Durchlauf der inneren Schleife. Es ergibt sich folgende Komplexität:&lt;br /&gt;
*:Ingesamt &amp;lt;math&amp;gt;\sum_{i=0}^{N-2} \sum_{j=i+1}^{N-1}1 = \frac{N}{2} (N-1) \!&amp;lt;/math&amp;gt; Vergleiche.&lt;br /&gt;
&lt;br /&gt;
*Anzahl der Vertauschungen (swaps): Eine Vertauschung pro Durchlauf der äußeren Schleife:&lt;br /&gt;
*:Insgesamt &amp;lt;math&amp;gt;N-1 \!&amp;lt;/math&amp;gt; Vertauschungen&lt;br /&gt;
&lt;br /&gt;
Die Komplexität wird durch die Operationen bestimmt, die am häufigsten ausgeführt werden, hier also die Anzahl der Vergleiche. Die Anzahl der Vertauschungen ist hingegen kein geeignetes Kriterium für die Komplexität von selection sort, weil der Aufwand in der inneren Schleife ignoriert würde.&lt;br /&gt;
&lt;br /&gt;
=== Fallunterscheidung: Worst und Average Case ===&lt;br /&gt;
&lt;br /&gt;
Die Komplexität ist in der Regel eine Funktion der Eingabegröße (Anzahl der Eingabebits, Anzahl der Eingabeelemente). Sie kann aber auch von der Art der Daten abhängen, nicht nur von der Menge, z.B. vorsortierte Daten bei Quicksort. Um von der Art der Daten unabhängig zu werden, kann man zwei Fälle der Komplexität unterscheiden:&lt;br /&gt;
      &lt;br /&gt;
* Komplexität im ungünstigsten Fall &lt;br /&gt;
*: Der ungünstigste Fall ist die Eingabe gegebener Länge, für die der Algorithmus am langsamsten ist. Der Nachteil dieser Methode besteht darin, dass dieser ungünstige Fall in der Praxis vielleicht gar nicht oder nur selten vorkommt, so dass sich der Algorithmus in Wirklichkeit besser verhält als man nach dieser Analyse erwarten würde. Beim Quicksort-Algorithmus mit zufälliger Wahl des Pivot-Elements müsste z.B. stets das kleinste oder größte Element des aktuellen Intervalls als Pivot-Element gewählt werden, was äußerst unwahrscheinlich ist.&lt;br /&gt;
* Komplexität im durchschnittlichen/typischen Fall&lt;br /&gt;
*: Der typische Fall ist die mittlere Komplexität des Algorithmus über alle möglichen Eingaben. Dazu muss man die Wahrscheinlichkeit jeder möglichen Eingabe kennen, und berechnet dann die mittlere Laufzeit über dieser Wahrscheinlichkeitsverteilung. Leider ist die Wahrscheinlichkeit der Eingaben oft nicht bekannt, so dass man geeignete Annahmen treffen muss. Bei Sortieralgorithmen können z.B. alle möglichen Permutationen des Eingabearrays als gleich wahrscheinlich angenommen werden, und der typische Fall ist dann die mittlere Komplexität über alle diese Eingaben. Oft hat man jedoch in der Praxis andere Wahrscheinlichkeitsverteilungen, z.B. sind die Daten oft &amp;quot;fast sortiert&amp;quot; (nur wenige Elemente sind an der falschen Stelle). Dann verhält sich der Algorithmus ebenfalls anders als vorhergesagt.&lt;br /&gt;
&lt;br /&gt;
Wir beschränken uns in dieser Vorlesung auf die Komplexität im ungünstigseten Fall. '''Exakte''' Formeln für Komplexität sind aber auch dann schwer zu gewinnen, wie das folgende Beispiel zeigt:&lt;br /&gt;
&lt;br /&gt;
=== Beispiele aus den Übungen (Gemessene Laufzeiten für Mergesort/Selectionsort) ===&lt;br /&gt;
&lt;br /&gt;
* Mergesort: &amp;lt;math&amp;gt;\frac{0,977N\log N}{\log 2} + 0,267N-4.39 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: andere Lösung: &amp;lt;math&amp;gt;1140 N\log(N) - 1819N + 6413 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* Selectionsort: &amp;lt;math&amp;gt;\frac{1}{2}N^2 - \frac{1}{2N} - 10^{-12} \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: andere Lösung: &amp;lt;math&amp;gt;1275N^2 - 116003^N + 11111144 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Aus diesen Formeln wird nicht offensichtlich, welcher Algorithmus besser ist.&lt;br /&gt;
Näherung: Betrachte nur '''sehr große Eingaben''' (meist sind alle Algorithmen schnell genug für kleine Eingaben). Dieses Vorgehen wird als '''Asymptotische Komplexität''' bezeichnet (N gegen unendlich).&lt;br /&gt;
&lt;br /&gt;
=== Asymptotische Komplexität am Beispiel Polynom ===&lt;br /&gt;
&lt;br /&gt;
Polynom: &amp;lt;math&amp;gt;a\,x^2+b\,x+c=p\!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;x \!&amp;lt;/math&amp;gt; sei die Eingabegröße, und wir betrachten die Entwicklung von &amp;lt;math&amp;gt;p \!&amp;lt;/math&amp;gt; in Abhängigkeit von &amp;lt;math&amp;gt;x \!&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;math&amp;gt;x=0 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=c \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x=1 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=a+b+c \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x=1000 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=1000000a+1000b+c \approx 1000000a\!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x \to \infty \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p \approx x^2a\!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für sehr große Eingaben verlieren also ''b'' und ''c'' immer mehr an Bedeutung, so dass am Ende nur noch ''a'' für die Komplexitätsbetrachtung wichtig ist.&lt;br /&gt;
&lt;br /&gt;
== Landau-Symbole ==&lt;br /&gt;
&lt;br /&gt;
Um die asymptotische Komplexität verschiedener Algorithmen miteinander vergleichen zu können, verwendet man die sogenannten [http://de.wikipedia.org/wiki/Landau-Symbole Landau-Symbole]. Das wichtigste Landau-Symbol ist &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;, mit dem man eine ''obere Schranke'' &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; für die Komplexität angeben kann. &lt;br /&gt;
&lt;br /&gt;
Schreibt man &amp;lt;math&amp;gt;f \in \Omega(g)&amp;lt;/math&amp;gt;, so stellt dies eine asymptotische ''untere Schranke'' für die Funktion f dar.&lt;br /&gt;
&lt;br /&gt;
Schließlich bedeutet &amp;lt;math&amp;gt;f \in \Theta(g)&amp;lt;/math&amp;gt;, dass die Funktion f genauso schnell wie die Funktion g wächst, das heißt man hat eine asymptotisch ''scharfe Schranke'' für f. Hierzu muss sowohl &amp;lt;math&amp;gt;f\in\mathcal{O}(g)&amp;lt;/math&amp;gt; als auch &amp;lt;math&amp;gt;f \in \Omega(g)&amp;lt;/math&amp;gt; erfüllt sein. &lt;br /&gt;
&lt;br /&gt;
Im nun folgenden soll auf die verschiedenen Landau-Symbole noch näher eingegeangen werden.&lt;br /&gt;
&lt;br /&gt;
===O-Notation===&lt;br /&gt;
&lt;br /&gt;
Intuitiv gilt: Für große N dominieren die am schnellsten wachsenden Terme einer Funktion. Die Notation &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; (sprich &amp;quot;f ist in O von g&amp;quot; oder &amp;quot;f ist von derselben Größenordnung wie g&amp;quot;) formalisiert eine solche Abschätzung der asymptotischen Komplexität der Funktion f von oben. &lt;br /&gt;
; Asymptotische Komplexität: Für zwei Funktionen f(x) und g(x) gilt&lt;br /&gt;
::&amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x))&amp;lt;/math&amp;gt;&lt;br /&gt;
: genau dann wenn es eine Konstante &amp;lt;math&amp;gt;c&amp;gt;0&amp;lt;/math&amp;gt; und ein Argument &amp;lt;math&amp;gt;x_0&amp;lt;/math&amp;gt; gibt, so dass &lt;br /&gt;
::&amp;lt;math&amp;gt;\forall x \ge x_0:\quad f(x) \le c\,g(x)&amp;lt;/math&amp;gt;.&lt;br /&gt;
:Die Menge &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; aller durch g(x) abschätzbaren Funktionen ist also formal definiert durch&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(g(x)) = \{ f(x)\ |\ \exists c&amp;gt;0: \forall x \ge x_0: 0 \le f(x) \le c\,g(x)\}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Idee hinter dieser Definition ist, dass g(x) eine wesentlich einfachere Funktion ist als f(x), die sich aber nach geeigneter Skalierung (Multiplikation mit c) und für große Argumente x im wesentlichen genauso wie f(x) verhält. Man kann deshalb in der Algorithmenanalyse f(x) durch g(x) ersetzen. &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x))&amp;lt;/math&amp;gt; spielt für Funktionen eine ähnliche Rolle wie der Operator &amp;amp;le; für Zahlen: Falls a &amp;amp;le; b gilt, kann bei einer Abschätzung von oben ebenfalls a durch b ersetzt werden.&lt;br /&gt;
&lt;br /&gt;
==== Ein einfaches Beispiel ====&lt;br /&gt;
&lt;br /&gt;
[[Image:Sqsqrt.png]]&lt;br /&gt;
&lt;br /&gt;
Rot = &amp;lt;math&amp;gt;x^2 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
Blau = &amp;lt;math&amp;gt;\sqrt{x} \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\sqrt{x} \in \mathcal{O}(x^2)\!&amp;lt;/math&amp;gt; weil &amp;lt;math&amp;gt;\sqrt{x} \le c\,x^2\!&amp;lt;/math&amp;gt; für alle &amp;lt;math&amp;gt;x \ge x_0 = 1 \!&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c = 1\!&amp;lt;/math&amp;gt;, oder auch für &amp;lt;math&amp;gt;x \ge x_0 = 4 \!&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c = 1/16&amp;lt;/math&amp;gt; (die Wahl von c und x&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; in der Definition von O(.) ist beliebig, solange die Bedingungen erfüllt sind).&lt;br /&gt;
&lt;br /&gt;
==== Komplexität bei kleinen Eingaben ==== &lt;br /&gt;
&lt;br /&gt;
Algorithmus 1: &amp;lt;math&amp;gt;\mathcal{O}(N^2) \!&amp;lt;/math&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Algorithmus 2: &amp;lt;math&amp;gt;\mathcal{O}(N\log{N}) \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus 2 ist schneller (von geringerer Komplexität) für große Eingaben, aber bei kleinen Eingaben (insbesondere, wenn der Algorithmus in einer Schleife immer wieder mit kleinen Eingaben aufgerufen wird) könnte Algorithmus 1 schneller sein, falls der in der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation verborgene konstante Faktor ''c'' bei Algorithmus 2 einen wesentlich größeren Wert hat als bei Algorithmus 1.&lt;br /&gt;
&lt;br /&gt;
==== Eigenschaften der O-Notation (Rechenregeln) ==== &lt;br /&gt;
&lt;br /&gt;
# Transitiv:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x)) \land g(x) \in \mathcal{O}(h(x)) \to f(x) \in \mathcal{O}(h(x)) \!&amp;lt;/math&amp;gt;           &lt;br /&gt;
# Additiv:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(h(x)) \land g(x) \in \mathcal{O}(h(x)) \to f(x) + g(x) \in \mathcal{O}(h(x)) \!&amp;lt;/math&amp;gt;           &lt;br /&gt;
# Für Monome gilt:&lt;br /&gt;
#: &amp;lt;math&amp;gt;x^k \in \mathcal{O}(x^k)&amp;lt;/math&amp;gt; und&lt;br /&gt;
#: &amp;lt;math&amp;gt;x^k \in \mathcal{O}(x^{k+j}), \forall j \ge 0 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Multiplikation mit einer Konstanten:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x)) \to c\,f(x) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: andere Schreibweise:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) = c\,g(x) \to f(x) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Folgerung aus 3. und 4. für Polynome: &lt;br /&gt;
#: &amp;lt;math&amp;gt;a_0+a_1\,x + ... + a_n\,x^n \in \mathcal{O}(x^n)\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: Beispiel: &amp;lt;math&amp;gt;a\,x^2+b\,x+c \in \mathcal{O}(x^2)\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Logarithmus:&lt;br /&gt;
#: &amp;lt;math&amp;gt;a, b &amp;gt; 1\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{b}{x})\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: Die Basis des Logarithmus spielt also keine Rolle.&lt;br /&gt;
#: Beweis hierfür:&lt;br /&gt;
#:: &amp;lt;math&amp;gt;\log_{a}{x} = \frac{\log_{b}{x}}{\log_{b}{a}}\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#:: Mit &amp;lt;math&amp;gt;c = 1 / \log_{b}{a}\,&amp;lt;/math&amp;gt; gilt: &amp;lt;math&amp;gt;\log_{a}{x} = c\,\log_{b}{x}\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#:: Wird hier die (zweite) Regel für Multiplikation mit einer Konstanten angewendet, fällt der konstante Faktor weg, also &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{b}{x})\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Insbesondere gilt auch &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{2}{x})\!&amp;lt;/math&amp;gt;, es kann also immer der 2er Logarithmus verwendet werden.&lt;br /&gt;
&lt;br /&gt;
==== O-Kalkül ==== &lt;br /&gt;
&lt;br /&gt;
Das O-Kalkül definiert wichtige Vereinfachungsregeln for Ausdrücke in O-Notation (Beweise: siehe Übungsaufgabe):&lt;br /&gt;
&lt;br /&gt;
# &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(f(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# &amp;lt;math&amp;gt;\mathcal{O}(\mathcal{O}(f(x))) \in \mathcal{O}(f(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# &amp;lt;math&amp;gt;c\,\mathcal{O}(f(x)) \in \mathcal{O}(f(x))\,&amp;lt;/math&amp;gt; für jede Konstante ''c''&lt;br /&gt;
# &amp;lt;math&amp;gt;\mathcal{O}(f(x))+c \in \mathcal{O}(f(x))\,&amp;lt;/math&amp;gt; für jede Konstante ''c''&lt;br /&gt;
# Sequenzregel:&lt;br /&gt;
#: Wenn zwei nacheinander ausgeführte Programmteile die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt; bzw. &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; haben, gilt für beide gemeinsam:&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(f(x))&amp;lt;/math&amp;gt; falls &amp;lt;math&amp;gt;g(x) &amp;lt; \mathcal{O}(f(x))&amp;lt;/math&amp;gt; bzw.&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt; falls &amp;lt;math&amp;gt;f(x) &amp;lt; \mathcal{O}(g(x))&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Informell schreibt man auch: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(max(f(x), g(x)))\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
# Schachtelungsregel bzw. Aufrufregel:&lt;br /&gt;
#: Wenn in einer geschachtelten Schleife die äußere Schleife die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt; hat, und die innere &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt;, gilt für beide gemeinsam:&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) * \mathcal{O}(g(x)) \in \mathcal{O}(f(x) * g(x))\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Gleiches gilt wenn eine Funktion &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt;-mal aufgerufen wird, und die Komplexität der Funktion selbst &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; ist.&lt;br /&gt;
&lt;br /&gt;
;Beispiel für 5.: Beide Schleifen haben die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Dies gilt auch für ihre Hintereinanderausführung:&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          a[i] = i&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          print a[i]&lt;br /&gt;
;Beispiele für 6.: Beide Schleifen haben die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Ihre Verschachtelung hat daher die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N^2)&amp;lt;/math&amp;gt;. &lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          for j in range(N):&lt;br /&gt;
              a[i*N + j] = i+j&lt;br /&gt;
: Dies gilt ebenso, wenn statt der inneren Schleife eine Funktion mit Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt; ausgeführt wird:&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          a[i] = foo(i, N)  # &amp;lt;math&amp;gt;\mathrm{foo}(i, N) \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== O-Kalkül auf das Beispiel des Selectionsort angewandt ==== &lt;br /&gt;
&lt;br /&gt;
Selectionsort: Wir hatten gezeigt dass &amp;lt;math&amp;gt;f(N) = \frac{N^2}{2} - \frac{N}{2}&amp;lt;/math&amp;gt;. Nach der Regel für Polynome vereinfacht sich dies zu &amp;lt;math&amp;gt;f(N) \in \mathcal{O}\left(\frac{N^2}{2}\right) = \mathcal{O}(N^2)\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Alternativ via Schachtelungsregel:&lt;br /&gt;
: Die äußere Schleife wird (''N''-1)-mal durchlaufen: &amp;lt;math&amp;gt;N-1 \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
: Die innere Schleife wird (''N-i''-1)-mal durchlaufen. Das sind im Mittel ''N''/2 Durchläufe: &amp;lt;math&amp;gt;N/2 \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
: Zusammen: &amp;lt;math&amp;gt;\mathcal{O}(N)*\mathcal{O}(N) \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nach beiden Vorgehensweisen kommen wir zur Schlussfolgerung, dass der Selectionsort die asymptotische Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N^2)\!&amp;lt;/math&amp;gt; besitzt.&lt;br /&gt;
&lt;br /&gt;
==== Zusammenhang zwischen Komplexität und Laufzeit ==== &lt;br /&gt;
&lt;br /&gt;
Wenn eine Operation 1ms dauert, erreichen Algorithmen verschiedener Komplexität folgende Leistungen (wobei angenommen wird, dass der in der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation verborgene konstante Faktor immer etwa gleich 1 ist):&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:left&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
|-&lt;br /&gt;
! Komplexität !! Operationen in 1s !! Operationen in 1min !! Operationen in 1h&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 1000 || 60.000 || 3.600.000&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N\log_2{N})&amp;lt;/math&amp;gt;&lt;br /&gt;
| 140 || 4895 || 204094&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 32 || 245 || 1898&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 10 || 39 || 153&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(2^N)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 10 || 16 || 21&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Exponentielle Komplexität ==== &lt;br /&gt;
Der letzte Fall &amp;lt;math&amp;gt;\mathcal{O}(2^N)&amp;lt;/math&amp;gt; ist von exponentieller Komplexität. Das bedeutet, dass eine Verdopplung des Aufwands nur bewirkt, dass die maximale Problemgröße um eine Konstante wächst. Algorithmen mit exponentieller (oder noch höherer) Komplexität werden deshalb als '''ineffizient''' bezeichnet. Algorithmen mit höchstens polynomieller Komplexität gelten hingegen als effizient.&lt;br /&gt;
&lt;br /&gt;
In der Praxis sind allerdings auch polynomielle Algorithmen mit hohem Exponenten meist zu langsam. Als Faustregel kann man eine praktische Grenze von &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt; ansehen. Bei einer Komplexität von &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt; bewirkt ein verdoppelter Aufwand immer noch eine Steigerung der maximalen Problemgröße um den Faktor &amp;lt;math&amp;gt;\sqrt[3]{2}&amp;lt;/math&amp;gt; (also eine ''multiplikative'' Vergrößerung um ca. 25%, statt nur einer additiven Vergrößerung wie bei exponentieller Komplexität).&lt;br /&gt;
&lt;br /&gt;
===&amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;- Notation===&lt;br /&gt;
&lt;br /&gt;
Genauso wie &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; eine Art &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;-Operator für Funktionen ist, definiert &amp;lt;math&amp;gt;f \in \Omega(g) &amp;lt;/math&amp;gt; eine Abschätzung von unten, analog zum &amp;lt;math&amp;gt;\ge&amp;lt;/math&amp;gt;-Operator für Zahlen. Formal kann man &amp;lt;math&amp;gt;f(N) \in \Omega(g(N)) &amp;lt;/math&amp;gt; genau dann schreiben, falls es eine Konstante &amp;lt;math&amp;gt; c &amp;gt; 0 &amp;lt;/math&amp;gt; gibt, so dass &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; f(N) \ge c \cdot g(N) &amp;lt;/math&amp;gt; für &amp;lt;math&amp;gt; N  \ge N_0 &amp;lt;/math&amp;gt; &lt;br /&gt;
&lt;br /&gt;
gilt.&lt;br /&gt;
Man verwendet diese Notation also um abzuschätzen, wie groß der Aufwand (die Komplexität) für einen bestimmten Algorithmus ''mindestens'' ist und nicht ''höchstens'', was man mit der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt; - Notation ausdrücken würde.&lt;br /&gt;
&lt;br /&gt;
Ein praktisches Beispiel für eine Anwendung der &amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;- Notation wäre die Fragestellung, ob es ''prinzipiell'' einen besseren Algorithmus für ein bestimmtes Problem gibt. Wie später im Abschnitt [[Suchen#Sortieren_als_Suchproblem|Sortieren als Suchproblem]] gezeigt wird, ist das Sortieren eines Arrays durch paarweise Vergleiche von Elementen immer mindestens von der Komplexität &amp;lt;math&amp;gt; \Omega(N\cdot \ln N) &amp;lt;/math&amp;gt;, was konkret bedeutet, dass kein Sortieralgorithmus, der nach diesem Prinzip arbeitet, jemals eine geringere Komplexität als beispielsweise Merge-Sort haben wird. Natürlich kann man den entsprechenden Sortieralgorithmus, also Merge-Sort zum Beispiel, unter Umständen noch optimieren, aber die Komplexität wird erhalten bleiben. Mit diesem Wissen kann man sich viel (vergebliche) Arbeit sparen.&lt;br /&gt;
&lt;br /&gt;
===&amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;- Notation===&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;math&amp;gt;f(N) \in \Theta(g(N))&amp;lt;/math&amp;gt; ist eine scharfe Abschätzung der asymptotischen Komplexität einer Funktion f. &lt;br /&gt;
&lt;br /&gt;
Damit dies gilt, muss &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; und ''gleichzeitig'' &amp;lt;math&amp;gt;f(N) \in \Omega(g(N))&amp;lt;/math&amp;gt; erfüllt sein.&lt;br /&gt;
&lt;br /&gt;
Dies ist natürlich auch die beste Abschätzung der asymptotischen Komplexität einer Funktion f. Formal bedeutet &amp;lt;math&amp;gt;f(N) \in \Theta(g(N))&amp;lt;/math&amp;gt; dass es zwei Konstanten &amp;lt;math&amp;gt; c_1 &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; c_2 &amp;lt;/math&amp;gt;, beide größer als Null, gibt, so dass für alle &amp;lt;math&amp;gt; N \geq N_0 &amp;lt;/math&amp;gt; gilt: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; c_1 \cdot g(N) \leq f(N) \leq c_2 \cdot g(N) &amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In der Praxis wird manchmal statt der &amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;-Notation auch dann die &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation benutzt, wenn eine scharfe Schranke ausgedrückt werden soll. Dies ist zwar formal nicht korrekt, aber man kann die intendierte Bedeutung meist aus dem Kontext erschließen.&lt;br /&gt;
&lt;br /&gt;
== Komplexitätsvergleich zweier Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
In diesem Abschnitt wollen wir der Frage nachgehen, wie ein formaler Beweis für die Behauptung &amp;lt;math&amp;gt; f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; geschehen kann. Hierbei werden zwei Beweismethoden vorgestellt werden, und zwar der '''Beweis über die Definition der Komplexität''' sowie der '''Beweis durch Dividieren'''.&lt;br /&gt;
&lt;br /&gt;
===Beweis über die Definition der asymptotischen Komplexität===&lt;br /&gt;
&lt;br /&gt;
Die Definition der asymptotischen Komplexität &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; war: &lt;br /&gt;
&lt;br /&gt;
Es gibt eine Konstante &amp;lt;math&amp;gt; c &amp;gt; 0 &amp;lt;/math&amp;gt;, so dass &amp;lt;math&amp;gt; f(N) \le c \cdot g(N) &amp;lt;/math&amp;gt; für &amp;lt;math&amp;gt; N  \ge N_0 &amp;lt;/math&amp;gt; erfüllt ist. &lt;br /&gt;
&lt;br /&gt;
Um also die die asymptotische Komplexität &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; zu beweisen, muss man die oben erwähnten Konstanten c und &amp;lt;math&amp;gt; N_0 &amp;lt;/math&amp;gt; finden, so dass &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; f(N) \leq c \cdot g(N) &amp;lt;/math&amp;gt; für alle &amp;lt;math&amp;gt; N \ge N_0 &amp;lt;/math&amp;gt; erfüllt ist. &lt;br /&gt;
&lt;br /&gt;
Dies geschieht zweckmäßigerweise mit dem Beweisprinzip der ''vollständigen Induktion''. Hierbei ist zu zeigen, dass&lt;br /&gt;
# &amp;lt;math&amp;gt; f(N_0) \leq g(N_0) &amp;lt;/math&amp;gt; für die eine zu bestimmende Konstante &amp;lt;math&amp;gt; N_0 &amp;lt;/math&amp;gt; gilt (''Induktionsanfang'') und &lt;br /&gt;
# falls &amp;lt;math&amp;gt; f(N) \leq g(N) &amp;lt;/math&amp;gt;, dann auch &amp;lt;math&amp;gt; f(N+1) \leq g(N+1) &amp;lt;/math&amp;gt; (''Induktionsschritt'') gilt.&lt;br /&gt;
&lt;br /&gt;
===Beweis durch Dividieren===&lt;br /&gt;
&lt;br /&gt;
Hierbei wählt man eine Konstante c und zeigt, dass &amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f(N)}{c \cdot g(N)} \leq 1 &amp;lt;/math&amp;gt; gilt (für die O-Notation, bei &amp;amp;Omega;-Notation gilt entsprechend &amp;lt;math&amp;gt;\geq 1 &amp;lt;/math&amp;gt;). Man kann dies auch als alternative Definition der Komplexität verwenden.&lt;br /&gt;
&lt;br /&gt;
Als Beispiel betrachten wir die beiden Funktionen &amp;lt;math&amp;gt; f(N) = N \,\lg N &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; g(N) = N^2 &amp;lt;/math&amp;gt; und wollen zeigen, dass &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; gilt. &lt;br /&gt;
&lt;br /&gt;
Als Konstante c wählen wir &amp;lt;math&amp;gt; c = 1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f(N)}{g(N)} = \lim_{N \rightarrow \infty} \frac{\lg N}{N} = \frac{\infty}{\infty} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Unbestimmte Ausdrücke der Form &lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{x \rightarrow x_0} \frac{f(x)}{g(x)} &amp;lt;/math&amp;gt;,&lt;br /&gt;
in denen sowohl &amp;lt;math&amp;gt; f(x) &amp;lt;/math&amp;gt; als auch &amp;lt;math&amp;gt; g(x) &amp;lt;/math&amp;gt; mit &amp;lt;math&amp;gt; x \rightarrow x_0 &amp;lt;/math&amp;gt; gegen Null oder gegen Unendlich streben, kann man manchmal mit den Regeln von [http://de.wikipedia.org/wiki/L%27Hospital%27sche_Regel ''l'Hospital''] berechnen. Danach darf man die Funktionen f und g zur Berechnung des unbestimmten Ausdrucks durch ihre k-ten Ableitungen ersetzen:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{x \rightarrow x_0} \frac{f(x)}{g(x)} = \lim_{x \rightarrow x_0} \frac{f^{(k)}(x)}{g^{(k)}(x)} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In unserem Fall verwenden wir die erste Ableitung und erhalten:&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f'(x)}{g'(x)} = \lim_{N \rightarrow \infty} \frac{1/N}{1} \rightarrow 0 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Damit wurde &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt;, also &amp;lt;math&amp;gt;N \lg N \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt; gezeigt.&lt;br /&gt;
&lt;br /&gt;
Man beachte hierbei, dass &amp;lt;math&amp;gt;N \lg N \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt; keine enge Grenze für die Komplexität von &amp;lt;math&amp;gt;N \,\lg N&amp;lt;/math&amp;gt; darstellt, da der Grenzwert &amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f'(x)}{g'(x)}\, &amp;lt;/math&amp;gt; gegen 0 und nicht gegen eine von Null verschiedene Konstante strebt. In diesem Fall haben wir die Komplexität von &amp;lt;math&amp;gt;N \cdot \lg N &amp;lt;/math&amp;gt; also nur nach oben abschätzen können.&lt;br /&gt;
&lt;br /&gt;
===Beispiel für den Komplexitätsvergleich: Gleitender Mittelwert (Running Average)===&lt;br /&gt;
&lt;br /&gt;
Wir berechnen für ein gegebenes Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; einen gleitenden Mittelwert über &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; Elemente:&amp;lt;br/&amp;gt;&lt;br /&gt;
::&amp;lt;math&amp;gt;r_i = \frac{1}{k} \sum_{j=i-k+1}^i a_j&amp;lt;/math&amp;gt; &amp;lt;br/&amp;gt;&lt;br /&gt;
Das heisst, für jedes &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; mitteln wir die letzten &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; Elemente von &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; und schreiben das Ergebnis in &amp;lt;tt&amp;gt;r[i]&amp;lt;/tt&amp;gt;. Diese Operation ist z.B. bei Börsenkursen wichtig: Neben dem aktuellen Kurs für jeden Tag wird dort meist auch der gleitende Mittelwert der letzten 30 Tage sowie der letzten 200 Tage angegeben. In diesen Mittelwerten erkennt man besser die langfristige Tendenz, weil die täglichen Schwankungen herausgemittelt werden. Wir nehmen außerdem an, dass&lt;br /&gt;
* Array-Zugriff hat eine Komplexit&amp;amp;auml;t von O(1)&lt;br /&gt;
* &amp;lt;math&amp;gt;k \ll N&amp;lt;/math&amp;gt;, d.h. &amp;lt;math&amp;gt;N-k\approx N&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die beiden folgenden Algorithmen berechnen die Mittelwerte auf unterschiedliche Art. Der linke folgt der obigen Definition durch eine Summe, während der rechte inkrementell arbeitet: Man kann den Bereich der &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; letzten Werte als Fenster betrachten, das über das Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; geschoben wird. Schiebt man das Fenster ein Element weiter, fällt links ein Element heraus, und rechts kommt eins hinzu. Man muss also nicht jedes Mal die Summe neu berechnen, sondern kann den vorigen Wert aktualisieren. Wir werden sehen, dass dies Folgen für die Komplexität des Algorithmus hat.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;2&amp;quot; &lt;br /&gt;
|-&lt;br /&gt;
! Programmzeile&lt;br /&gt;
! Version 1: O(N * k)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
! Version 2: O(N)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
1.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
2.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
3.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
4.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k-1, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for i in range(k):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
5.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;for i in range(j-k+1, j+1):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[k-1] += a[i]&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
6.&lt;br /&gt;
|&lt;br /&gt;
:::: &amp;lt;tt&amp;gt;r[j] += a[i]&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
7.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] = (a[j] - a[j-k] + r[j-1])&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
8.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
9.&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
10.&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Wir zeigen unten dass Version 2 eine geringere Komplexit&amp;amp;auml;t besitzt, obwohl sie mehr Zeilen ben&amp;amp;ouml;tigt.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Wir haben in der Tabelle die Komplexität jeder Zeile für sich angegeben. Einfache Anweisungen (Berechnungen, Lese- und Schreibzugriffe auf das Array, Zuweiseungen) haben konstante Komplexität, die Komplexität des Schleifenkopfes allein (also der &amp;lt;tt&amp;gt;for&amp;lt;/tt&amp;gt;-Anweisung ohne den eingerückten Schleifenkörper) entspricht der Anzahl der Durchläufe. Wir müssen jetzt noch die Verschachtelung der Schleifen und die Nacheinanderausführung von Anweisungen berücksichtigen. &lt;br /&gt;
&lt;br /&gt;
====Berechnung der Komplexität von Version 1====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;small&amp;gt;(Wiederholung der Rechenregeln: siehe Abschnitt [[Effizienz#O-Notation|O-Notation]])&amp;lt;/small&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wir betrachten zunächst die innere Schleife (Zeilen 5 und 6 von Version 1):&lt;br /&gt;
&lt;br /&gt;
Der Schleifenkopf (Zeile 5) hat die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt;, weil die Schleife k-mal durchlaufen wird. Der Schleifenkörper (Zeile 6) hat die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;. Nach der Verschachtelungsregel müssen wir die beiden Komplexitäten multiplizieren, und es ergibt sich:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(k)\cdot\mathcal{O}(1) = \mathcal{O}(k\cdot 1)=\mathcal{O}(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wir betrachten nun die äußere Schleife. Der Schleifenkopf (Zeile 4) wird (N-k)-mal durchlaufen und hat somit eine Komplexität von &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Der Schleifenkörper (Zeilen 5 bis 7) besteht aus der inneren Schleife (Zeilen 5 und 6) mit der gerade berechneten Komplexität &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt; sowie einer einfachen Anweisung (Zeile 7) mit Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;. Nach der Sequenzregel wird die Komplexität des Schleifenkörpers durch Addition berechnet:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(k)+\mathcal{O}(1) = \mathcal{O}(\max(k,1)) = \mathcal{O}(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexität der gesamten äußeren Schleife erhalten wir nach der Verschachtelungsregel wieder durch multiplizieren:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)\cdot\mathcal{O}(k) = \mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die übrigen Schritte des Algorithmus werden einfach nacheinander ausgeführt, so dass sie ebenfalls nach der Sequenzregel behandelt werden. Wir erhalten&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)+\mathcal{O}(1)+\mathcal{O}(N\cdot k)+\mathcal{O}(1) = \mathcal{O}(\max(N,1,N\cdot k,1)) = \mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Der gesamte Algorithmus hat also die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Berechnung der Komplexität von Version 2====&lt;br /&gt;
&lt;br /&gt;
Hier gibt es nur einfache Schleifen ohne Verschachtelung. Da der Schleifenkörper jeder Schleife nur einfache Anweisungen der Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt; enthält, ergibt sich die Komplexität der Schleifen nach der Verschachtelungsregel als&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(X)\cdot\mathcal{O}(1) = \mathcal{O}(X\cdot 1)=\mathcal{O}(X)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
wobei &amp;lt;math&amp;gt;\mathcal{O}(X)&amp;lt;/math&amp;gt; die Komplexität des jeweiligen Schleifenkopfes ist. Wir erhalten also für Zeilen 4 und 5: &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt;, Zeilen 6 und 7: &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;, Zeilen 8 und 9: &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Die Hintereinanderausführung wird nach der Sequenzregel behandelt:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)+\mathcal{O}(1)+\mathcal{O}(k)+\mathcal{O}(N)+\mathcal{O}(N)+\mathcal{O}(1) = \mathcal{O}(\max(N,1,k,N,N,1)) = \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Dieser Algorithmus hat also nur die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Fazit====&lt;br /&gt;
&lt;br /&gt;
Obwohl Version 2 mehr Schritte benötigt hat sie eine geringere Komplexität, da die for-Schleifen nicht wie bei Version 1 verschachtelt/untergeordnet sind. Bei verschachtelten for-Schleifen muss die Multiplikationsregel angewendet werden &amp;amp;rarr; höhere Komplexität.&lt;br /&gt;
&lt;br /&gt;
Die gerade berechnete Komplexität gilt aber &amp;lt;u&amp;gt;nur&amp;lt;/u&amp;gt; unter der Annahme, dass Array-Zugriffe konstante Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt; besitzen. Wenn dies nicht der Fall ist, kann sich die Komplexität des Algorithmus drastisch verschlechtern.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|Allgemein gilt:&amp;lt;br/&amp;gt;&lt;br /&gt;
Algorithmen-Analysen beruhen auf der Annahme, dass Zugriffe auf die Daten optimal schnell sind, dass heißt, dass die für den jeweiligen Algorithmus am besten geeignete Datenstruktur verwendetet wird.&amp;lt;br /&amp;gt; &amp;amp;rarr; Ansonsten: Komplexitätsverschlechterung!&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Beispiel für eine Verschlechterung der Komplexität durch Verwendung einer nicht optimalen Datenstruktur====&lt;br /&gt;
&lt;br /&gt;
Wir verwende im Mittelwert-Algorithmus eine verkettete Liste anstelle des Eingabe-Arrays &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt;. Wir benötigen dazu eine Funktion, die das j-te Element der Liste zurückgibt. Wie üblich ist die Liste mit Hilfe einer Knotenklasse implementiert:&lt;br /&gt;
      class Node:&lt;br /&gt;
          def __init__(self, data):&lt;br /&gt;
              self.data = data&lt;br /&gt;
              self.next = None&lt;br /&gt;
&lt;br /&gt;
Die Listenklasse selbst hat ein Feld &amp;lt;tt&amp;gt;head&amp;lt;/tt&amp;gt;, das eine Referenz auf den ersten Knoten speichert, und jeder Knoten speichert im Feld &amp;lt;tt&amp;gt;next&amp;lt;/tt&amp;gt; eine Referenz auf seinen Nachfolger. Um zum j-ten Element zu gelangen, muss man die Liste sequenziell durchlaufen&lt;br /&gt;
      def get_jth(list, j):&lt;br /&gt;
           r = list.head&lt;br /&gt;
           while j &amp;gt; 0:&lt;br /&gt;
               r = r.head&lt;br /&gt;
               j -= 1&lt;br /&gt;
           return r.data&lt;br /&gt;
Die Komplexität dieser Funktion ist offensichtlich &amp;lt;math&amp;gt;\mathcal{O}(j)&amp;lt;/math&amp;gt; (Komplexitätsberechnung wie oben). Wir setzen jetzt bei Version 1 des Mittelwert-Algorithmus diese Funktion in Zeile 6 anstelle des Indexzugriffs &amp;lt;tt&amp;gt;a[i]&amp;lt;/tt&amp;gt; ein (nur in dieser Zeile wird auf die Elemente des Arrays zugegriffen). Wir erhalten folgende Implementation (die Änderungen sind rot markiert):&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;2&amp;quot; &lt;br /&gt;
|-&lt;br /&gt;
! Programmzeile&lt;br /&gt;
! Version 1 mit Liste: O(N * k)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
1.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
2.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
3.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
4.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k-1, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
5.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;for i in range(j-k+1, j+1):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
6.&lt;br /&gt;
|&lt;br /&gt;
:::: &amp;lt;tt&amp;gt;r[j] += &amp;lt;font color=red&amp;gt;get_jth(a, i)&amp;lt;/font&amp;gt;&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;&amp;lt;font color=red&amp;gt;O(i)&amp;lt;/font&amp;gt;&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
7.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
8.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Der Aufruf der Funktion &amp;lt;tt&amp;gt;get_jth&amp;lt;/tt&amp;gt; ist jetzt gleichbedeutend mit einer dreifach verschachtelten Schleife (weil &amp;lt;tt&amp;gt;get_jth&amp;lt;/tt&amp;gt; ja eine zusatzliche Schleife enthält). Die Anzahl der Operationen in Zeile 4 bis 6 ist jetzt&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;f(N,k)=\sum_{j=k-1}^{N-1}\,\sum_{i=j-k+1}^j\,\mathcal{O}(i)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
wobei das &amp;lt;math&amp;gt;\mathcal{O}(i)&amp;lt;/math&amp;gt; die neue Schleife durch Verwendung der Liste repräsentiert. Mit Mathematica-Hilfe [http://www.wolfram.com/] lässt sich diese Summe exakt ausrechnen&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;f(N,k)=\frac{1}{2}(k N^2-k^2 N+k^2-k)\in \mathcal{O}(k N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexitätsberechnung erfolgte dabei nach der Regel für Polynome unter Beachtung von &amp;lt;math&amp;gt;k \ll N&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Fazit:====&lt;br /&gt;
&lt;br /&gt;
Die Komplexität von Version 1 mit einer verketteten Liste wäre O(N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; * k)&lt;br /&gt;
'''&amp;amp;rarr; Die richtige Datenstruktur ist wichtig, da es sonst zu einer Komplexitätsverschlechterung kommen kann!'''&lt;br /&gt;
&lt;br /&gt;
Auf Version 2 unseres Running Average-Beispiels hätte eine verkettete Liste allerdings keine Auswirkungen, da die inkrementelle Berechnung der Summen in Zeile 7 weiterhin möglich ist (bei geschickter Implementation!) und somit Version 2 immer noch eine Komplexität von O(N) hätte.&lt;br /&gt;
&lt;br /&gt;
==Amortisierte Komplexität==&lt;br /&gt;
&lt;br /&gt;
Bis jetzt wurde die Komplexität nur im schlechtesten Fall (Worst Case) betrachtet. Bei einigen Operationen schwankt die Komplexität jedoch sehr stark, wenn man sie mehrmals hintereinander ausführt, und der schlechteste Fall kommt nur selten vor. Dann ist es sinnvoll, die &amp;lt;i&amp;gt;amortisierte Komplexität&amp;lt;/i&amp;gt; zu betrachten, die sich mit der &amp;lt;i&amp;gt;durchschnittlichen&amp;lt;/i&amp;gt; Komplexität über viele Aufrufe der selben Operation beschäftigt.&lt;br /&gt;
&lt;br /&gt;
Zum weiter Lesen: [[http://de.wikipedia.org/wiki/Amortisierte_Laufzeitanalyse Wikipedia: Amortisierte Laufzeitanalyse]]&lt;br /&gt;
&lt;br /&gt;
===Beispiel: Inkrementieren von Binärzahlen===&lt;br /&gt;
&lt;br /&gt;
Frage: Angenommen, das Umdrehen eines Bits einer Binärzahl verursacht Kosten von 1 Einheit. Wir erzeugen die Folge der natürlichen Zahlen in Binärdarstellung durch sukzessives Inkrementieren, von Null beginnend. Bei jeder Inkrementierung werden einige Bits verändert, aber diese Zahl (und damit die Kosten der Inkrementierungen) ''schwanken'' sehr stark. Wir fragen jetzt, was eine Inkrementierung im Durchschnitt kostet?&lt;br /&gt;
&lt;br /&gt;
Um diese Durchschnittskosten zu berechnen, bezahlen wir bei jeder Inkrementierung 2 Einheiten. Wenn davon nach Abzug der Kosten der jeweiligen Operation noch etwas übrig bleibt, wird der Rest dem  Guthaben zugeschrieben. Umgekehrt wird ein eventueller Fehlbetrag (wenn eine Inkrementierung mehr als 2 Bits umdreht) aus dem Guthaben gedeckt. Dadurch werden die ansonsten großen Schwankungen der Kosten ausgeglichen:&lt;br /&gt;
:: Kosten &amp;lt; Einzahlung &amp;amp;rarr; es wird gespart&lt;br /&gt;
:: Kosten = Einzahlung &amp;amp;rarr; Guthaben bleibt unverändert&lt;br /&gt;
:: Kosten &amp;gt; Einzahlung &amp;amp;rarr; Guthaben wird für die Kosten verbraucht&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
!Schritte&lt;br /&gt;
!Zahlen&lt;br /&gt;
!Kosten &amp;lt;br/&amp;gt;&lt;br /&gt;
(Anzahl der geänderten Bits)&lt;br /&gt;
! Einzahlung&lt;br /&gt;
!Guthaben =&amp;lt;br/&amp;gt;&lt;br /&gt;
altes Guthaben + Einzahlung - Kosten&lt;br /&gt;
|-&lt;br /&gt;
|1.&lt;br /&gt;
|0000&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|2.&lt;br /&gt;
|000&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|2&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|3.&lt;br /&gt;
|0001&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|4.&lt;br /&gt;
|00&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|3&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|5.&lt;br /&gt;
|0010&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|6.&lt;br /&gt;
|001&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|2&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|7.&lt;br /&gt;
|0011&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''3'''&lt;br /&gt;
|-&lt;br /&gt;
|8.&lt;br /&gt;
|0&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1000&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|4&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Die Kosten ergeben sich aus der Anzahl der Ziffern die von 1 nach 0, bzw. von 0 nach 1 verändert werden&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Rechnung:&amp;lt;/u&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Schritt: Kosten: 1 &amp;lt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Schritt: Kosten: 2 = Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird nicht gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; Guthaben bleibt so wie es ist &amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Schritt: Kosten: 1 &amp;lt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. Schritt: Kosten: 3 &amp;gt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird eine 1 vom Guthaben genommen um die Kosten zu zahlen&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
usw.&lt;br /&gt;
&lt;br /&gt;
Man erkennt, dass vor teuren Operation (Wechsel von 3 auf 4 bzw. von 7 auf 8) genügend Guthaben angespart wurde, um die Kosten zu decken. Das Guthaben geht bei diesen Operationen immer wieder auf 1 zurück, aber es wird nie vollständig verbraucht (Dies kann natürlich auch mathematisch exakt bewiesen werden, wie wir es unten am Beispiel des dynamische Arrays zeigen). Wir schließen daraus, dass die durchschnittlichen oder '''amortisierten Kosten''' einer Inkrementierungsoperation gleich 2 sind.&lt;br /&gt;
&lt;br /&gt;
Zum Weiterlesen: [[http://de.wikipedia.org/wiki/Account-Methode Wikipedia Account-Methode]]&lt;br /&gt;
&lt;br /&gt;
====Fazit====&lt;br /&gt;
Die amortisierte Komplexität beschäftigt sich mit dem Durchschnitt aller Operation im ungünstigsten Fall. Operationen mit hohen Kosten, die aber nur selten ausgeführt werden, fallen bei der amortisierten Komplexität nicht so ins Gewicht. Bei Algorithmen, die gelegentlich eine &amp;quot;teure&amp;quot; Operation benutzen, ansonsten jedoch &amp;quot;billige&amp;quot; Operationen aufrufen, kann die amortisierte Komplexität niedriger sein als die Komplexität im schlechtesten (Einzel-)Fall.&lt;br /&gt;
&lt;br /&gt;
In unserem Beispiel fallen die teuren Einzelschritte (z.B. 4. und 8. Schritt) bei den amortisierten Kosten nicht so ins Gewicht, da wir die Kosten aus unserem Guthaben mitbezahlen können. Das Guthaben ist immer groß genug, weil jeder zweite Aufruf eine billige Operation ist, die nur ein Bit umdreht und somit das Ansparen ermöglicht. Diese Betrachtung zeigt, dass die amortisierte (d.h. durchschnittliche) Komplexität des Algoithmus niedriger (nämlich konstant) ist als die Komplexität im schlechtesten Fall.&lt;br /&gt;
&lt;br /&gt;
===Anwendung: Dynamisches Array===&lt;br /&gt;
&lt;br /&gt;
Ein dynamisches Array hat die Eigenschaft, dass man effizient am Ende des Arrays neue Elemente anfügen kann, indem man die Länge des Arrays entsprechend vergrößert (siehe Übung 1). Die Analyse der amortisierten Komplexität der Anfüge-Operation zeigt uns, wie man das Vergrößern des Arrays richtig implementiert, damit die Operation wirklich effizient abläuft.&lt;br /&gt;
&lt;br /&gt;
==== Ineffiziente naive Lösung ====&lt;br /&gt;
&lt;br /&gt;
Wenn wir an ein Array ein Element anhängen wollen, müssen wir neuen Speicher allokieren, der die gewünschte Länge hat. Die Werte aus dem alten Array müssen dann in den neuen Speicher umkopiert werden. Danach kann das neue Element hinten angefügt werden, weil wir im neuen Array bereits Speicher für dieses Element reserviert haben. Bei der naiven Implementation des dynamischen Arrays wiederholt man dies bei jeder Anfügeoperation. Für die Analyse nehmen wir an, dass das Kopieren eines Elements konstante Zeit O(1) erfordert, ebenso das Einfügen eines neuen Elements auf in eine noch unbenutzte Speicherposition.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Naives Anhängen eines weiteren Elements an ein Array:&amp;lt;/u&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;right&amp;quot;&lt;br /&gt;
!Schritte&lt;br /&gt;
|'''Array'''&lt;br /&gt;
&amp;lt;small&amp;gt;(wie es nach jedem Schritt aussieht)&amp;lt;/small&amp;gt;&lt;br /&gt;
!Komplexität&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;center&amp;gt;altes Array (N=4)&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|1. neuer Speicher für&amp;lt;br&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;(N+1) Elemente&lt;br /&gt;
|&amp;lt;center&amp;gt;[None,None,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;O(N+1) = '''O(N)'''&amp;lt;/center&amp;gt;(wenn der Speicher initialisiert wird&amp;lt;br&amp;gt;(hier auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;), sonst O(1))&lt;br /&gt;
|-&lt;br /&gt;
|2. Kopieren &lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;'''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|3. append von &amp;quot;x&amp;quot;&lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3,'x']&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;'''O(1)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
altesArray = [0,1,2,3]&amp;lt;br /&amp;gt;&lt;br /&gt;
altesArray.append('x')&lt;br /&gt;
&lt;br /&gt;
1. Es wird ein neues Array der Größe N+1 erzeugt&amp;lt;br /&amp;gt;&lt;br /&gt;
2. Die N Datenelemente aus dem alten Array werden in das neue Array kopiert&amp;lt;small&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Das sind N Operationen der Komplexität O(1), also ein Gesamtaufwand von O(N).&amp;lt;/small&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
3. 'x' wird mit Aufwand O(1) an die letzte Stelle des neuen Arrays geschrieben&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Additionsregel:&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
O(N) + O(1) &amp;amp;isin; O(N)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Folgerung:&amp;lt;/b&amp;gt; &lt;br /&gt;
&lt;br /&gt;
Bei der naiven Methode erfordert jede Anfügung einen Aufwand O(N) (wobei N die derzeitige Arraygröße ist). &amp;lt;b&amp;gt;Das ist nicht effizient.&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Effiziente Lösung durch Verdoppeln der Kapazität====&lt;br /&gt;
&lt;br /&gt;
Offensichtlich kommt man nicht darum herum, den Inhalt des alten Arrays zu kopieren, wenn der allokierte Speicher voll ist. Der Trick für die effiziente Implementation der Anfügeoperation besteht darin, das Kopieren so selten wie möglich durchzuführen, also nicht wie in der naiven Lösung bei jeder Anfügeoperation. Hier kommt die amortisierte Komplexität ins Spiel: Ab und zu gibt es eine teure Anfügeoperation (wenn nämlich kopiert werden muss), aber wenn man den durchschnittlichen Aufwand über viele Anfügungen betrachtet, ist die Operation effizient. Der teure Fall wird sozusagen &amp;quot;herausgemittelt&amp;quot;. &lt;br /&gt;
&lt;br /&gt;
Um nur selten kopieren zu müssen, werden beim dynamischen Array mehr Speicherelemente reserviert als zur Zeit benötigt werden (in der naiven Lösung wurde dagegen immer nur Speicher für ein einziges neues Element reserviert). Wir unterscheiden deshalb &lt;br /&gt;
&lt;br /&gt;
:&amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; = Anzahl der allokierten Speicherzellen, d.h. der möglichen Elemente, die in das Array passen&amp;lt;br /&amp;gt;&lt;br /&gt;
:&amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt; = Anzahl der Elemente, die im Array zur Zeit gespeichert sind&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Daten selbst werden in einem statischen Array gespeichert:&lt;br /&gt;
:&amp;lt;tt&amp;gt;data&amp;lt;/tt&amp;gt; = statisches Array der Größe &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die folgende intuitive Abschätzung zeigt, dass es sinnvoll ist, die Größe des allokierten Speichers jeweils zu verdoppeln. Wir starten bei einem Array der Größe &amp;lt;tt&amp;gt;size = capacity&amp;lt;/tt&amp;gt; = N. Da der verfügbare Speicher voll ist, müssen wir bei der nächsten Anfügung die N vorhandenen Elemente in ein neues Array der Länge &amp;lt;tt&amp;gt;new_capacity&amp;lt;/tt&amp;gt; kopieren (Aufwand &amp;lt;math&amp;gt;N\cdot O(1)&amp;lt;/math&amp;gt;). Danach können wir K Elemente billig einfügen (Aufwand &amp;lt;math&amp;gt;K\cdot O(1)&amp;lt;/math&amp;gt;), wobei &lt;br /&gt;
:K = &amp;lt;tt&amp;gt;new_capacity - capacity&amp;lt;/tt&amp;gt; &lt;br /&gt;
die Anzahl der nach dem Kopieren noch unbenutzen Speicherzellen ist. Der durchschnittliche Aufwand für diese K Einfügungen ist somit&lt;br /&gt;
:&amp;lt;math&amp;gt;\bar T = \frac{N \cdot O(1) + K \cdot O(1)}{K}=\frac{N+K}{K}\cdot O(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Damit die mittlere Zeit in O(1) sein kann, muss der Quotient &amp;lt;math&amp;gt;(N+K)/K&amp;lt;/math&amp;gt; eine Konstante sein. Wir setzen &amp;lt;math&amp;gt;K = a N&amp;lt;/math&amp;gt; und erhalten:&lt;br /&gt;
:&amp;lt;math&amp;gt;\bar T = \frac{(a+1)N}{a N}\cdot O(1)=\frac{a+1}{a}\cdot O(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Der amortisierte Aufwand über K Einfügungen ist also konstant, wenn &amp;lt;math&amp;gt;a&amp;lt;/math&amp;gt; eine (kleine) von N unabhängige Zahl ist. Typischerweise wählt man &lt;br /&gt;
:&amp;lt;math&amp;gt;a = 1&amp;lt;/math&amp;gt;&lt;br /&gt;
und mit &amp;lt;math&amp;gt;K = 1\cdot N&amp;lt;/math&amp;gt; ergibt sich&lt;br /&gt;
:&amp;lt;tt&amp;gt;new_capacity = capacity&amp;lt;/tt&amp;gt; + N = &amp;lt;tt&amp;gt;2 * capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Vorgehensweise beim Zufügen eines neuen Elements im Fall &amp;lt;tt&amp;gt;size == capacity&amp;lt;/tt&amp;gt; ist also&lt;br /&gt;
* capacity wird verdoppelt&amp;lt;br /&amp;gt;&lt;br /&gt;
: &amp;lt;tt&amp;gt;neue capacity = 2 * alte capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
: (allgemein genügt es auch, wenn capacity um einen bestimmten Prozentsatz vergrößert wird, &lt;br /&gt;
:: &amp;lt;tt&amp;gt;neue capacity = alte capacity * c&amp;lt;/tt&amp;gt; &lt;br /&gt;
: mit c &amp;gt; 1, z.B. c = 1.2, das entspricht oben der Wahl &amp;lt;math&amp;gt;a = 0.2&amp;lt;/math&amp;gt;)&lt;br /&gt;
* ein neues statisches Array der Größe 'neue capacity' wird erzeugt&lt;br /&gt;
* das alte Array wird ins neue kopiert und danach freigegeben&lt;br /&gt;
* das anzufügende Element wird ins neue Array eingefügt&lt;br /&gt;
Umgekehrt geht man beim Entfernen des ''letzten'' Array-Elements vor. Normalerweise überschreibt man einfach das letzte Element mit &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; und dekrementiert &amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt;. Wird dadurch das Array zu klein (üblicherweise &amp;lt;tt&amp;gt;size &amp;amp;lt; capacity / 4&amp;lt;/tt&amp;gt;), wird die Kapazität halbiert, genauer:&lt;br /&gt;
* ein neues Array mit &amp;lt;br/&amp;gt;&lt;br /&gt;
: &amp;lt;tt&amp;gt;neue capacity = alte capacity / 2 &amp;lt;/tt&amp;gt;&lt;br /&gt;
: wird angelegt (bzw. mit&lt;br /&gt;
:: &amp;lt;tt&amp;gt;neue capacity = alte capacity / c &amp;lt;/tt&amp;gt;&lt;br /&gt;
: wenn ein anderer Vergrößerungsfaktor verwendet wird)&lt;br /&gt;
* das alte Array wird ins neue kopiert und danach freigegeben&lt;br /&gt;
&lt;br /&gt;
'''Folge:''' Die Kosten für das Vergrößern/Verkleinern der Kapazität werden amortisiert über viele Einfügungen, die kein Vergrößern erfordern. Die Operation &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; besitzt amortisierte Komplexität O(1). Im folgenden Abschnitt zeigen wir dies mathematisch exakt mit der Potentialmethode.&lt;br /&gt;
&lt;br /&gt;
====Komplexitätsanalyse des dynamischen Arrays mit Potentialmethode====&lt;br /&gt;
&lt;br /&gt;
Durchschnitt der Gesamtkosten für N-maliges append = &amp;lt;math&amp;gt;\frac{1}{N} \sum_{i = 1}^N Kosten(i)&amp;lt;/math&amp;gt;. Zur Analyse der amortisierten Komplexität wird ein Potential&amp;lt;br/&amp;gt;&lt;br /&gt;
::&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2*size&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; - capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
eingeführt, wobei das Array nach dem i-ten Einfüge-Schritt die Größe size&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; und die Kapizität capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; hat. Wir nehmen vereinfachend an, dass es keine Löschoperationen gibt. Dann gilt nach dem i-ten Schritt jeweils&lt;br /&gt;
::&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2*i - capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Fall 1: Array ist nicht voll&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Es wird kein Umkopieren benötigt, da das Array noch nicht voll ist&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; size&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; &amp;lt; capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; == capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Kosten: 1 (für Einfügen des neuen Elements)&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial vor append: &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = 2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial nach append: &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
amortisierte Kosten = Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Phi;&amp;lt;sub&amp;gt;(i)&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;(i-1)&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::                 = 1            + (2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;)        - [2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;]&lt;br /&gt;
:::::                 = 1            + 2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;         - 2i + 2 + capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::                 = 1            + &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; - &amp;lt;del&amp;gt;capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;/del&amp;gt; - &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; + 2 + &amp;lt;del&amp;gt;capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;/del&amp;gt;&lt;br /&gt;
:::::                 = 1 + 2&lt;br /&gt;
:::::                 = 3 = O(1) &amp;amp;rarr; konstant&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Fall 2: Array ist voll&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Vor dem i-ten append muss umkopiert werden&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; size&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; == capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; == i-1&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; Allokieren eines neuen statischen Arrays mit verdoppelter Kapazität notwendig, also capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; == 2*capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Kosten: (i-1) + 1 (für Umkopieren und Einfügen des neuen Elements)&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial vor append =  &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = 2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial nach append =  &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; =  2i - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
amortisierte Kosten = Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Phi;&amp;lt;sub&amp;gt;(i)&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;(i-1)&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = ((i - 1) + 1) + 2i - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; - [2(i-1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;]&lt;br /&gt;
:::::               = i + &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; - &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; + 2 - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = i + 2 - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = i + 2 - (i - 1) &amp;lt;small&amp;gt;(da capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = i-1)&amp;lt;/small&amp;gt;&lt;br /&gt;
:::::               = 3 = O(1) &amp;amp;rarr; konstant            &lt;br /&gt;
&lt;br /&gt;
'''Damit wurde bewiesen, dass die Operation &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; beim dynamischen Array eine amortisierte Komplexität von 3 Einheiten hat, also &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; &amp;amp;isin; O(1)'''. Diese Operation kann deshalb gefahrlos in der inneren Schleife eines Algorithmus benutzt werden.&lt;br /&gt;
&lt;br /&gt;
==== Beispiel für 9 Einfügeoperationen ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot;&lt;br /&gt;
!Array&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;lt;small&amp;gt;(wie es aussehen könnte)&amp;lt;/small&amp;gt;&lt;br /&gt;
!size&lt;br /&gt;
!capacity&lt;br /&gt;
!Kosten für append&amp;lt;br /&amp;gt;(einschließlich Umkopieren)&lt;br /&gt;
!Summe Kosten&lt;br /&gt;
!Durchschnittskosten&lt;br /&gt;
!&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2 * size - capacity&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;lt;small&amp;gt;(i = size)&amp;lt;/small&amp;gt;&lt;br /&gt;
!Potenzialdifferenz&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;amp;Delta; &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
!amortisierte Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
= Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Delta; &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;0&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3/2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6/3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;0&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7/4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;5&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;12&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;12/5&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;13&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;13/6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;14&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;14/7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,h]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;15&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;15/8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,h,j,None,None,None,&amp;lt;br /&amp;gt;&lt;br /&gt;
None,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;9&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;16&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;24&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;24/9&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die durchschnittlichen Kosten betragen stets etwa 2 Einheiten, schwanken allerdings so, dass nicht unmittelbar ersichtlich ist, ob dies für sämtliche Einfügeoperationen gilt. Die amortisierte Komplexität, die mit Hilfe des Potentials berechnet wird, ist hingegen konstant 3, wie auch im obigen Beweis für alle Einfügeoperationen allgemein gezeigt wurde.&lt;br /&gt;
&lt;br /&gt;
[[Suchen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Effizienz&amp;diff=5422</id>
		<title>Effizienz</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Effizienz&amp;diff=5422"/>
				<updated>2012-07-27T16:04:27Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Ineffiziente naive Lösung */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Bei der Diskussion von Effizienz müssen wir zwischen der Laufzeit eines Algorithmus auf einem bestimmten System und seiner prinzipiellen Leistungsfähigkeit (Algorithmenkomplexität) unterscheiden. Der Benutzer ist natürlich vor allem an der Laufzeit interessiert, denn diese bestimmt letztendlich seine Arbeitsproduktivität. Ein Softwaredesigner hingegen muss eine Implementation wählen, die auf verschiedenen Systemen und in verschiedenen Anwendungen schnell ist. Für ihn sind daher auch Aussagen zur Algorithmenkomplexität sehr wichtig, um den am besten geeigneten Algorithmus auszuwählen.&lt;br /&gt;
&lt;br /&gt;
== Laufzeit ==&lt;br /&gt;
&lt;br /&gt;
Aus Anwendersicht ist ein Algorithmus effizient, wenn er die in der Spezifikation verlangten Laufzeitgrenzen einhält. Ein Algorithmus muss also nicht immer so schnell wie möglich sein, sondern so schnell wie nötig. Dies führt in verschiedenen Anwendungen zu ganz unterschiedliche Laufzeitanforderungen:&lt;br /&gt;
&lt;br /&gt;
* Berechnen des nächsten Steuerkommandos für eine Maschine: ca. 1/1000s&lt;br /&gt;
* Berechnen des nächsten Bildes für eine Videopräsentation (z.B. Dekompression von MPEG-kodierten Bildern): ca. 1/25s&lt;br /&gt;
: Geringere Bildraten führen zu ruckeligen Filmen.&lt;br /&gt;
* Sichtbare Antwort auf ein interaktives Kommando (z.B. Mausklick): ca. 1/2s&lt;br /&gt;
: Wird diese Antwortzeit überschritten, vermuten viele Benutzer, dass der Mausklick nicht funktioniert hat, und klicken nochmals, mit eventuell fatalen Folgen. Wenn ein Algorithmus notwendigerweise länger dauert als 1/2s, sollte ein Fortschrittsbalken angezeigt werden.&lt;br /&gt;
* Wettervorhersage: muss spätestens am Vorabend des vorhergesagten Tages beendet sein&lt;br /&gt;
&lt;br /&gt;
===Laufzeitvergleich===&lt;br /&gt;
&lt;br /&gt;
Da die Laufzeit für den Benutzer ein so wichtiges Kriterium ist, werden häufig Laufzeitvergleiche durchgeführt. Deren Ergebnisse hängen allerdings von vielen Faktoren ab, die möglicherweise nicht kontrollierbar sind:&lt;br /&gt;
* Geschwindigkeit und Anzahl der Prozessoren&lt;br /&gt;
* Auslastung des Systems&lt;br /&gt;
* Größe des Hauptspeichers  und Cache, Geschwindigkeit des  Datenbus&lt;br /&gt;
* Qualität des Compilers/Optimierers (ist der Compiler für die spezielle Prozessor-Architektur optimiert?)&lt;br /&gt;
* Geschick des Programmierers&lt;br /&gt;
* Daten (Beispiel Quicksort: Best case und worst case [vorsortierter Input] stark unterschiedlich)&lt;br /&gt;
All diese Faktoren sind untereinander abhängig. Laufzeitvergleiche sind daher mit Vorsicht zu interpretieren.&lt;br /&gt;
Generell sollten bei Vergleichen möglichst wenige Parameter verändert werden, z.B.&lt;br /&gt;
* gleiches Programm (gleiche Kompilierung), gleiche Daten, andere Prozessoren&lt;br /&gt;
oder&lt;br /&gt;
* gleiche CPU, Daten, andere Programme (Vergleich von Algorithmen)&lt;br /&gt;
Zur Verbesserung der Vergleichbarkeit gibt es standardisierte [http://en.wikipedia.org/wiki/Benchmark_(computing) Benchmarks], die bestimmte Aspekte eines Systems unter möglichst realitätsnahen Bedingungen testen. Generell gilt aber: Durch Laufzeitmessung ist schwer festzustellen, ob ein Algorithmus ''prinzipiell'' besser ist als ein anderer. Dafür ist die Analyse der [[Effizienz#Algorithmen-Komplexität|Algorithmenkomplexität]] notwendig.&lt;br /&gt;
&lt;br /&gt;
===Optimierung der Laufzeit===&lt;br /&gt;
&lt;br /&gt;
Wenn sich herausstellt, dass ein bereits implementierter Algorithmus zu langsam läuft, geht man wie folgt vor:&lt;br /&gt;
&lt;br /&gt;
# Man verwendet einen [http://en.wikipedia.org/wiki/Performance_analysis Profiler], um zunächst den Flaschenhals zu bestimmen. Ein Profiler ist ein Hilfsprogramm, das während der Ausführung eines Programms misst, wieviel Zeit in jeder Funktion und Unterfunktion verbraucht wird. Dadurch kann man herausfinden, welcher Teil des Algorithmus überhaupt Probleme bereitet. Donald Knuth gibt z.B. als Erfahrungswert an, dass Programme während des größten Teils ihrer Laufzeit nur 3% des Quellcodes (natürlich mehrmals wiederholt) ausführen [http://pplab.snu.ac.kr/courses/adv_pl05/papers/p261-knuth.pdf]. Es ist sehr wichtig, diese 3% experimentell zu bestimmen, weil die Erfahrung zeigt, dass man beim Erraten der kritischen Programmteile oft falsch liegt. Man spricht dann von &amp;quot;[http://en.wikipedia.org/wiki/Optimization_%28computer_science%29#When_to_optimize premature optimization]&amp;quot;, also von voreiliger Optimierung ohne experimentelle Untersuchung der wirklichen Laufzeiten, was laut Knuth &amp;quot;the root of all evil&amp;quot; ist. Der Python-Profiler wird in [http://docs.python.org/lib/profile.html Kapitel 25] der Python-Dokumentation beschrieben.&lt;br /&gt;
# Man kann dann versuchen, die kritischen Programmteile zu optimieren.&lt;br /&gt;
# Falls der Laufzeitgewinn durch Optimierung zu gering ist, muss man einen prinzipiell schnelleren Algorithmus verwenden, falls es einen gibt.&lt;br /&gt;
&lt;br /&gt;
Einige wichtige Techniken der Programmoptimierung sollen hier erwähnt werden. Wenn man einen optimierenden Compiler verwendet, werden einige Optimierungen automatisch ausgeführt [http://en.wikipedia.org/wiki/Compiler_optimization]. In Python trifft dies jedoch nicht zu. Um den Sinn einiger Optimierungen zu verstehen, benötigt man Grundkenntnisse der Computerarchitektur.&lt;br /&gt;
&lt;br /&gt;
;Elimination von redundantem Code: Es ist offensichtlich überflüssig, dasselbe Ergebnis mehrmals zu berechnen, wenn es auch zwischengespeichert werden könnte. Diese Optimierung wird von vielen automatischen Optimierern unterstützt und kommt im wesentlichen in zwei Ausprägungen vor:&lt;br /&gt;
:; common subexpression elimination: In mathematischen Ausdrücken wird ein Teilergebnis häufig mehrmals benötigt. Man betrachte z.B. die Lösung der quadratischen Gleichung &amp;lt;math&amp;gt;x^2+p\,x+q = 0&amp;lt;/math&amp;gt;:&lt;br /&gt;
        x1 = - p / 2.0 + sqrt(p*p/4.0 - q)&lt;br /&gt;
        x2 = - p / 2.0 - sqrt(p*p/4.0 - q)&lt;br /&gt;
::Die mehrmalige Berechnung von Teilausdrücken wird vermieden, wenn man stattdessen schreibt:&lt;br /&gt;
        p2 = - p / 2.0&lt;br /&gt;
        r  = sqrt(p2*p2 - q)&lt;br /&gt;
        x1 = p2 + r&lt;br /&gt;
        x2 = p2 - r&lt;br /&gt;
:; loop invariant elimination: Wenn ein Teilausdruck sich in einer Schleife nicht ändert, muss man ihn nicht bei jedem Schleifendurchlauf neu berechnen, sondern kann dies einmal vor Beginn der Schleife tun. Ein typisches Beispiel hierfür ist die Adressierung von Matrizen, die als 1-dimensionales Array gespeichert sind. Angenommen, wir speichern eine NxN Matrix &amp;lt;tt&amp;gt;m&amp;lt;/tt&amp;gt; in einem Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; der Größe N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt;, so dass das Matrixelement &amp;lt;tt&amp;gt;m&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt;&amp;lt;/tt&amp;gt; durch &amp;lt;tt&amp;gt;a[i + j*N]&amp;lt;/tt&amp;gt; indexiert wird. Wir betrachten die Aufgabe, eine Einheitsmatrix zu initialisieren. Ein nicht optimierter Algorithmus dafür lautet:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + j*N] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + j*N] = 0.0&lt;br /&gt;
::Der Ausdruck &amp;lt;tt&amp;gt;j*N&amp;lt;/tt&amp;gt; wird hier in jedem Schleifendurchlauf erneut berechnet, obwohl sich &amp;lt;tt&amp;gt;j&amp;lt;/tt&amp;gt; in der inneren Schleife gar nicht verändert. Man kann deshalb optimieren zu:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + jN] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + jN] = 0.0&lt;br /&gt;
;Vereinfachung der inneren Schleife: Generell sollte man sich bei der Optimierung auf die innere Schleife eines Algorithmus konzentrieren, weil dieser Code am häufigsten ausgeführt wird. Insbesondere sollte man die Anzahl der Befehle in der inneren Schleife so gering wie möglich halten und teure Befehle vermeiden. Früher waren vor allem Floating-Point Befehle teuer, die man oft durch die schnellere Integer-Arithmetik ersetzt hat, falls dies algorithmisch möglich war (diesen Rat findet man noch oft in der Literatur). Heute hat sich die Hardware so verbessert, dass im Allgemeinen nur noch die Floating-Point Division deutlich langsamer ist als die anderen Operatoren. Im obigen Beispiel der quadratischen Gleichung ist es daher sinnvoll, den Ausdruck &lt;br /&gt;
        p2 = -p / 2.0&lt;br /&gt;
:durch&lt;br /&gt;
        p2 = -0.5 * p&lt;br /&gt;
:zu ersetzen. Dadurch ersetzt man eine Division durch eine Multiplikation und spart außerdem das Negieren von &amp;lt;tt&amp;gt;p&amp;lt;/tt&amp;gt;, da der Compiler direkt mit &amp;lt;tt&amp;gt;-0.5&amp;lt;/tt&amp;gt; multipliziert.&lt;br /&gt;
;Ausnutzung der Prozessor-Pipeline: Moderne Prozessoren führen mehrere Befehle parallel aus. Dies ist möglich, weil jeder Befehl in mehrere Teilschritte zerlegt werden kann. Eine generische Unterteilung in vier Teilschritte ist z.B.:&lt;br /&gt;
:# Dekodieren des nächsten Befehls&lt;br /&gt;
:# Beschaffen der Daten, die der Befehl verwendet (aus Prozessorregistern, dem Cache, oder dem Hauptspeicher)&lt;br /&gt;
:# Ausführen des Befehls&lt;br /&gt;
:# Schreiben der Ergebnisse&lt;br /&gt;
:Man bezeichnet dies als die &amp;quot;[http://en.wikipedia.org/wiki/Instruction_pipeline instruction pipeline]&amp;quot; des Prozessors (heutige Prozessoren verwenden wesentlich feinere Unterteilungen). Prozessoren werden nun so gebaut, dass mehrere Befehle parallel, auf verschiedenen Ausführungsstufen ausgeführt werden. Wenn Befehl 1 also beim Schreiben der Ergebnisse angelangt ist, kann Befehl 2 die Hardware zum Ausführen des Befehls benutzen, während Befehl 3 seine Daten holt, und Befehl 4 soeben dekodiert wird. Unter bestimmten Bedingungen funktioniert diese Parallelverarbeitung jedoch nicht. Dies gibt Anlass zu Optimierungen:&lt;br /&gt;
:;Vermeiden unnötiger Typkonvertierungen: Der Prozessor verarbeitet Interger- und Floating-Point-Befehle in verschiedenen Pipelines, weil die Hardwareanforderungen sehr verschieden sind. Wird jetzt ein Ergebnis von Integer nach Floating-Point umgewandelt oder umgekehrt, muss die jeweils andere Pipeline warten, bis die erste Pipeline ihre Berechnung beendet. Es kann dann besser sein, Berechnungen in Floating-Point zu Ende zu führen, auch wenn sie semantisch eigentlich Integer-Berechnungen sind.&lt;br /&gt;
:;Reduzierung der Anzahl von Verzweigungen: Wenn der Code verzweigt (z.B. durch eine &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;- oder  &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Anweisung), ist nicht klar, welcher Befehl nach der Verzweigung ausgeführt werden soll, bevor Stufe 3 der Pipeline die Verzweigungsbedingung ausgewertet hat. Bis dahin wären die ersten beiden Stufen der Pipeline unbenutzt. Moderne Prozessoren benutzen zwar ausgefeilte Heuristiken, um das Ergebnis der Bedingung vorherzusagen, und führen den hoffentlich richtigen Zweig des Codes spekulativ aus, aber dies funktioniert nicht immer. Man sollte deshalb generell die Anzahl der Verzweigungen minimieren. Als Nebeneffekt führt dies meist auch zu besser lesbarem, verständlicherem Code. Im Matrixbeispiel kann man&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + jN] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + jN] = 0.0&lt;br /&gt;
::durch&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               a[i + jN] = 0.0&lt;br /&gt;
           a[j + jN] = 1.0&lt;br /&gt;
ersetzen. Die Diagonalelemente &amp;lt;tt&amp;gt;a[j + jN]&amp;lt;/tt&amp;gt; werden jetzt zwar zweimal initialisiert (in der Schleife auf Null, dann auf Eins), aber durch Elimination der &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage wird dies wahrscheinlich mehr als ausgeglichen, zumal dadurch die innere Schleife wesentlich vereinfacht wurde.&lt;br /&gt;
;Ausnutzen des Prozessor-Cache: Zugriffe auf den Hauptspeicher sind sehr langsam. Deshalb werden stets ganze Speicherseiten auf einmal in den [http://en.wikipedia.org/wiki/Cache Cache] des Prozessors geladen. Wenn unmittelbar nacheinander benutzte Daten auch im Speicher nahe beieinander liegen (sogenannte &amp;quot;[http://en.wikipedia.org/wiki/Locality_of_reference locality of reference]&amp;quot;), ist die Wahrscheinlichkeit groß, dass die als nächstes benötigten Daten bereits im Cache sind und damit schnell gelesen werden können. Bei vielen Algorithmen kann man die Implementation so umordnen, dass die locality of reference verbessert wird, was zu einer drastischen Beschleunigung führt. Im Matrix-Beispiel ist z.B. die Reihenfolge der Schleifen wichtig. Für konstanten Index &amp;lt;tt&amp;gt;j&amp;lt;/tt&amp;gt; liegen die Indizes &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; im Speicher hintereinander. Deshalb ist es günstig, in der inneren Schleife über &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu iterieren:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               a[i + jN] = 0.0&lt;br /&gt;
           a[j + jN] = 1.0&lt;br /&gt;
:Die umgekehrte Reihenfolge der Schleifen ist hingegen ungünstig&lt;br /&gt;
       for i in range(N):&lt;br /&gt;
           for j in range(N):&lt;br /&gt;
               a[i + j*N] = 0.0&lt;br /&gt;
           a[i + i*N] = 1.0&lt;br /&gt;
:Jetzt werden in der inneren Schleife stets N Datenelemente übersprungen. Besonders bei großem N muss man daher häufig den Cache neu füllen, was bei der ersten Implementation nicht notwendig war.  (Außerdem verliert man hier die Optimierung &amp;lt;tt&amp;gt;jN = j*N&amp;lt;/tt&amp;gt;, die jetzt nicht mehr möglich ist.)&lt;br /&gt;
&lt;br /&gt;
Als Faustregel kann man durch Optimierung eine Verdoppelung der Geschwindigkeit erreichen (in Ausnahmefällen auch mehr). Benötigt man stärkere Verbesserungen, muss man wohl oder übel einen besseren Algorithmus oder einen schnelleren Computer verwenden.&lt;br /&gt;
&lt;br /&gt;
== Algorithmen-Komplexität ==&lt;br /&gt;
&lt;br /&gt;
Komplexitätsbetrachtungen ermöglichen den Vergleich der prinzipiellen Eigenschaften von Algorithmen unabhängig von einer Implementation, Umgebung etc.&lt;br /&gt;
      &lt;br /&gt;
Eine einfache Möglichkeit ist das Zählen der Aufrufe einer Schlüsseloperation. Beispiel Sortieren:&lt;br /&gt;
* Anzahl der Vergleiche&lt;br /&gt;
* Anzahl der Vertauschungen&lt;br /&gt;
&lt;br /&gt;
=== Beispiel: Selection Sort ===&lt;br /&gt;
&lt;br /&gt;
  for i in range(len(a)-1):&lt;br /&gt;
    max = i&lt;br /&gt;
    for j in range(i+1, len(a)):&lt;br /&gt;
      if a[j] &amp;lt; a[max]:&lt;br /&gt;
        max = j&lt;br /&gt;
    a[max], a[i] = a[i], a[max]      # swap&lt;br /&gt;
&lt;br /&gt;
*Anzahl der Vergleiche: Ein Vergleich in jedem Durchlauf der inneren Schleife. Es ergibt sich folgende Komplexität:&lt;br /&gt;
*:Ingesamt &amp;lt;math&amp;gt;\sum_{i=0}^{N-2} \sum_{j=i+1}^{N-1}1 = \frac{N}{2} (N-1) \!&amp;lt;/math&amp;gt; Vergleiche.&lt;br /&gt;
&lt;br /&gt;
*Anzahl der Vertauschungen (swaps): Eine Vertauschung pro Durchlauf der äußeren Schleife:&lt;br /&gt;
*:Insgesamt &amp;lt;math&amp;gt;N-1 \!&amp;lt;/math&amp;gt; Vertauschungen&lt;br /&gt;
&lt;br /&gt;
Die Komplexität wird durch die Operationen bestimmt, die am häufigsten ausgeführt werden, hier also die Anzahl der Vergleiche. Die Anzahl der Vertauschungen ist hingegen kein geeignetes Kriterium für die Komplexität von selection sort, weil der Aufwand in der inneren Schleife ignoriert würde.&lt;br /&gt;
&lt;br /&gt;
=== Fallunterscheidung: Worst und Average Case ===&lt;br /&gt;
&lt;br /&gt;
Die Komplexität ist in der Regel eine Funktion der Eingabegröße (Anzahl der Eingabebits, Anzahl der Eingabeelemente). Sie kann aber auch von der Art der Daten abhängen, nicht nur von der Menge, z.B. vorsortierte Daten bei Quicksort. Um von der Art der Daten unabhängig zu werden, kann man zwei Fälle der Komplexität unterscheiden:&lt;br /&gt;
      &lt;br /&gt;
* Komplexität im ungünstigsten Fall &lt;br /&gt;
*: Der ungünstigste Fall ist die Eingabe gegebener Länge, für die der Algorithmus am langsamsten ist. Der Nachteil dieser Methode besteht darin, dass dieser ungünstige Fall in der Praxis vielleicht gar nicht oder nur selten vorkommt, so dass sich der Algorithmus in Wirklichkeit besser verhält als man nach dieser Analyse erwarten würde. Beim Quicksort-Algorithmus mit zufälliger Wahl des Pivot-Elements müsste z.B. stets das kleinste oder größte Element des aktuellen Intervalls als Pivot-Element gewählt werden, was äußerst unwahrscheinlich ist.&lt;br /&gt;
* Komplexität im durchschnittlichen/typischen Fall&lt;br /&gt;
*: Der typische Fall ist die mittlere Komplexität des Algorithmus über alle möglichen Eingaben. Dazu muss man die Wahrscheinlichkeit jeder möglichen Eingabe kennen, und berechnet dann die mittlere Laufzeit über dieser Wahrscheinlichkeitsverteilung. Leider ist die Wahrscheinlichkeit der Eingaben oft nicht bekannt, so dass man geeignete Annahmen treffen muss. Bei Sortieralgorithmen können z.B. alle möglichen Permutationen des Eingabearrays als gleich wahrscheinlich angenommen werden, und der typische Fall ist dann die mittlere Komplexität über alle diese Eingaben. Oft hat man jedoch in der Praxis andere Wahrscheinlichkeitsverteilungen, z.B. sind die Daten oft &amp;quot;fast sortiert&amp;quot; (nur wenige Elemente sind an der falschen Stelle). Dann verhält sich der Algorithmus ebenfalls anders als vorhergesagt.&lt;br /&gt;
&lt;br /&gt;
Wir beschränken uns in dieser Vorlesung auf die Komplexität im ungünstigseten Fall. '''Exakte''' Formeln für Komplexität sind aber auch dann schwer zu gewinnen, wie das folgende Beispiel zeigt:&lt;br /&gt;
&lt;br /&gt;
=== Beispiele aus den Übungen (Gemessene Laufzeiten für Mergesort/Selectionsort) ===&lt;br /&gt;
&lt;br /&gt;
* Mergesort: &amp;lt;math&amp;gt;\frac{0,977N\log N}{\log 2} + 0,267N-4.39 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: andere Lösung: &amp;lt;math&amp;gt;1140 N\log(N) - 1819N + 6413 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* Selectionsort: &amp;lt;math&amp;gt;\frac{1}{2}N^2 - \frac{1}{2N} - 10^{-12} \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: andere Lösung: &amp;lt;math&amp;gt;1275N^2 - 116003^N + 11111144 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Aus diesen Formeln wird nicht offensichtlich, welcher Algorithmus besser ist.&lt;br /&gt;
Näherung: Betrachte nur '''sehr große Eingaben''' (meist sind alle Algorithmen schnell genug für kleine Eingaben). Dieses Vorgehen wird als '''Asymptotische Komplexität''' bezeichnet (N gegen unendlich).&lt;br /&gt;
&lt;br /&gt;
=== Asymptotische Komplexität am Beispiel Polynom ===&lt;br /&gt;
&lt;br /&gt;
Polynom: &amp;lt;math&amp;gt;a\,x^2+b\,x+c=p\!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;x \!&amp;lt;/math&amp;gt; sei die Eingabegröße, und wir betrachten die Entwicklung von &amp;lt;math&amp;gt;p \!&amp;lt;/math&amp;gt; in Abhängigkeit von &amp;lt;math&amp;gt;x \!&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;math&amp;gt;x=0 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=c \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x=1 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=a+b+c \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x=1000 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=1000000a+1000b+c \approx 1000000a\!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x \to \infty \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p \approx x^2a\!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für sehr große Eingaben verlieren also ''b'' und ''c'' immer mehr an Bedeutung, so dass am Ende nur noch ''a'' für die Komplexitätsbetrachtung wichtig ist.&lt;br /&gt;
&lt;br /&gt;
== Landau-Symbole ==&lt;br /&gt;
&lt;br /&gt;
Um die asymptotische Komplexität verschiedener Algorithmen miteinander vergleichen zu können, verwendet man die sogenannten [http://de.wikipedia.org/wiki/Landau-Symbole Landau-Symbole]. Das wichtigste Landau-Symbol ist &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;, mit dem man eine ''obere Schranke'' &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; für die Komplexität angeben kann. &lt;br /&gt;
&lt;br /&gt;
Schreibt man &amp;lt;math&amp;gt;f \in \Omega(g)&amp;lt;/math&amp;gt;, so stellt dies eine asymptotische ''untere Schranke'' für die Funktion f dar.&lt;br /&gt;
&lt;br /&gt;
Schließlich bedeutet &amp;lt;math&amp;gt;f \in \Theta(g)&amp;lt;/math&amp;gt;, dass die Funktion f genauso schnell wie die Funktion g wächst, das heißt man hat eine asymptotisch ''scharfe Schranke'' für f. Hierzu muss sowohl &amp;lt;math&amp;gt;f\in\mathcal{O}(g)&amp;lt;/math&amp;gt; als auch &amp;lt;math&amp;gt;f \in \Omega(g)&amp;lt;/math&amp;gt; erfüllt sein. &lt;br /&gt;
&lt;br /&gt;
Im nun folgenden soll auf die verschiedenen Landau-Symbole noch näher eingegeangen werden.&lt;br /&gt;
&lt;br /&gt;
===O-Notation===&lt;br /&gt;
&lt;br /&gt;
Intuitiv gilt: Für große N dominieren die am schnellsten wachsenden Terme einer Funktion. Die Notation &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; (sprich &amp;quot;f ist in O von g&amp;quot; oder &amp;quot;f ist von derselben Größenordnung wie g&amp;quot;) formalisiert eine solche Abschätzung der asymptotischen Komplexität der Funktion f von oben. &lt;br /&gt;
; Asymptotische Komplexität: Für zwei Funktionen f(x) und g(x) gilt&lt;br /&gt;
::&amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x))&amp;lt;/math&amp;gt;&lt;br /&gt;
: genau dann wenn es eine Konstante &amp;lt;math&amp;gt;c&amp;gt;0&amp;lt;/math&amp;gt; und ein Argument &amp;lt;math&amp;gt;x_0&amp;lt;/math&amp;gt; gibt, so dass &lt;br /&gt;
::&amp;lt;math&amp;gt;\forall x \ge x_0:\quad f(x) \le c\,g(x)&amp;lt;/math&amp;gt;.&lt;br /&gt;
:Die Menge &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; aller durch g(x) abschätzbaren Funktionen ist also formal definiert durch&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(g(x)) = \{ f(x)\ |\ \exists c&amp;gt;0: \forall x \ge x_0: 0 \le f(x) \le c\,g(x)\}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Idee hinter dieser Definition ist, dass g(x) eine wesentlich einfachere Funktion ist als f(x), die sich aber nach geeigneter Skalierung (Multiplikation mit c) und für große Argumente x im wesentlichen genauso wie f(x) verhält. Man kann deshalb in der Algorithmenanalyse f(x) durch g(x) ersetzen. &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x))&amp;lt;/math&amp;gt; spielt für Funktionen eine ähnliche Rolle wie der Operator &amp;amp;le; für Zahlen: Falls a &amp;amp;le; b gilt, kann bei einer Abschätzung von oben ebenfalls a durch b ersetzt werden.&lt;br /&gt;
&lt;br /&gt;
==== Ein einfaches Beispiel ====&lt;br /&gt;
&lt;br /&gt;
[[Image:Sqsqrt.png]]&lt;br /&gt;
&lt;br /&gt;
Rot = &amp;lt;math&amp;gt;x^2 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
Blau = &amp;lt;math&amp;gt;\sqrt{x} \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\sqrt{x} \in \mathcal{O}(x^2)\!&amp;lt;/math&amp;gt; weil &amp;lt;math&amp;gt;\sqrt{x} \le c\,x^2\!&amp;lt;/math&amp;gt; für alle &amp;lt;math&amp;gt;x \ge x_0 = 1 \!&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c = 1\!&amp;lt;/math&amp;gt;, oder auch für &amp;lt;math&amp;gt;x \ge x_0 = 4 \!&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c = 1/16&amp;lt;/math&amp;gt; (die Wahl von c und x&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; in der Definition von O(.) ist beliebig, solange die Bedingungen erfüllt sind).&lt;br /&gt;
&lt;br /&gt;
==== Komplexität bei kleinen Eingaben ==== &lt;br /&gt;
&lt;br /&gt;
Algorithmus 1: &amp;lt;math&amp;gt;\mathcal{O}(N^2) \!&amp;lt;/math&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Algorithmus 2: &amp;lt;math&amp;gt;\mathcal{O}(N\log{N}) \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus 2 ist schneller (von geringerer Komplexität) für große Eingaben, aber bei kleinen Eingaben (insbesondere, wenn der Algorithmus in einer Schleife immer wieder mit kleinen Eingaben aufgerufen wird) könnte Algorithmus 1 schneller sein, falls der in der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation verborgene konstante Faktor ''c'' bei Algorithmus 2 einen wesentlich größeren Wert hat als bei Algorithmus 1.&lt;br /&gt;
&lt;br /&gt;
==== Eigenschaften der O-Notation (Rechenregeln) ==== &lt;br /&gt;
&lt;br /&gt;
# Transitiv:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x)) \land g(x) \in \mathcal{O}(h(x)) \to f(x) \in \mathcal{O}(h(x)) \!&amp;lt;/math&amp;gt;           &lt;br /&gt;
# Additiv:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(h(x)) \land g(x) \in \mathcal{O}(h(x)) \to f(x) + g(x) \in \mathcal{O}(h(x)) \!&amp;lt;/math&amp;gt;           &lt;br /&gt;
# Für Monome gilt:&lt;br /&gt;
#: &amp;lt;math&amp;gt;x^k \in \mathcal{O}(x^k)&amp;lt;/math&amp;gt; und&lt;br /&gt;
#: &amp;lt;math&amp;gt;x^k \in \mathcal{O}(x^{k+j}), \forall j \ge 0 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Multiplikation mit einer Konstanten:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x)) \to c\,f(x) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: andere Schreibweise:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) = c\,g(x) \to f(x) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Folgerung aus 3. und 4. für Polynome: &lt;br /&gt;
#: &amp;lt;math&amp;gt;a_0+a_1\,x + ... + a_n\,x^n \in \mathcal{O}(x^n)\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: Beispiel: &amp;lt;math&amp;gt;a\,x^2+b\,x+c \in \mathcal{O}(x^2)\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Logarithmus:&lt;br /&gt;
#: &amp;lt;math&amp;gt;a, b &amp;gt; 1\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{b}{x})\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: Die Basis des Logarithmus spielt also keine Rolle.&lt;br /&gt;
#: Beweis hierfür:&lt;br /&gt;
#:: &amp;lt;math&amp;gt;\log_{a}{x} = \frac{\log_{b}{x}}{\log_{b}{a}}\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#:: Mit &amp;lt;math&amp;gt;c = 1 / \log_{b}{a}\,&amp;lt;/math&amp;gt; gilt: &amp;lt;math&amp;gt;\log_{a}{x} = c\,\log_{b}{x}\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#:: Wird hier die (zweite) Regel für Multiplikation mit einer Konstanten angewendet, fällt der konstante Faktor weg, also &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{b}{x})\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Insbesondere gilt auch &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{2}{x})\!&amp;lt;/math&amp;gt;, es kann also immer der 2er Logarithmus verwendet werden.&lt;br /&gt;
&lt;br /&gt;
==== O-Kalkül ==== &lt;br /&gt;
&lt;br /&gt;
Das O-Kalkül definiert wichtige Vereinfachungsregeln for Ausdrücke in O-Notation (Beweise: siehe Übungsaufgabe):&lt;br /&gt;
&lt;br /&gt;
# &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(f(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# &amp;lt;math&amp;gt;\mathcal{O}(\mathcal{O}(f(x))) \in \mathcal{O}(f(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# &amp;lt;math&amp;gt;c\,\mathcal{O}(f(x)) \in \mathcal{O}(f(x))\,&amp;lt;/math&amp;gt; für jede Konstante ''c''&lt;br /&gt;
# &amp;lt;math&amp;gt;\mathcal{O}(f(x))+c \in \mathcal{O}(f(x))\,&amp;lt;/math&amp;gt; für jede Konstante ''c''&lt;br /&gt;
# Sequenzregel:&lt;br /&gt;
#: Wenn zwei nacheinander ausgeführte Programmteile die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt; bzw. &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; haben, gilt für beide gemeinsam:&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(f(x))&amp;lt;/math&amp;gt; falls &amp;lt;math&amp;gt;g(x) &amp;lt; \mathcal{O}(f(x))&amp;lt;/math&amp;gt; bzw.&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt; falls &amp;lt;math&amp;gt;f(x) &amp;lt; \mathcal{O}(g(x))&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Informell schreibt man auch: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(max(f(x), g(x)))\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
# Schachtelungsregel bzw. Aufrufregel:&lt;br /&gt;
#: Wenn in einer geschachtelten Schleife die äußere Schleife die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt; hat, und die innere &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt;, gilt für beide gemeinsam:&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) * \mathcal{O}(g(x)) \in \mathcal{O}(f(x) * g(x))\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Gleiches gilt wenn eine Funktion &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt;-mal aufgerufen wird, und die Komplexität der Funktion selbst &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; ist.&lt;br /&gt;
&lt;br /&gt;
;Beispiel für 5.: Beide Schleifen haben die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Dies gilt auch für ihre Hintereinanderausführung:&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          a[i] = i&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          print a[i]&lt;br /&gt;
;Beispiele für 6.: Beide Schleifen haben die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Ihre Verschachtelung hat daher die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N^2)&amp;lt;/math&amp;gt;. &lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          for j in range(N):&lt;br /&gt;
              a[i*N + j] = i+j&lt;br /&gt;
: Dies gilt ebenso, wenn statt der inneren Schleife eine Funktion mit Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt; ausgeführt wird:&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          a[i] = foo(i, N)  # &amp;lt;math&amp;gt;\mathrm{foo}(i, N) \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== O-Kalkül auf das Beispiel des Selectionsort angewandt ==== &lt;br /&gt;
&lt;br /&gt;
Selectionsort: Wir hatten gezeigt dass &amp;lt;math&amp;gt;f(N) = \frac{N^2}{2} - \frac{N}{2}&amp;lt;/math&amp;gt;. Nach der Regel für Polynome vereinfacht sich dies zu &amp;lt;math&amp;gt;f(N) \in \mathcal{O}\left(\frac{N^2}{2}\right) = \mathcal{O}(N^2)\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Alternativ via Schachtelungsregel:&lt;br /&gt;
: Die äußere Schleife wird (''N''-1)-mal durchlaufen: &amp;lt;math&amp;gt;N-1 \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
: Die innere Schleife wird (''N-i''-1)-mal durchlaufen. Das sind im Mittel ''N''/2 Durchläufe: &amp;lt;math&amp;gt;N/2 \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
: Zusammen: &amp;lt;math&amp;gt;\mathcal{O}(N)*\mathcal{O}(N) \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nach beiden Vorgehensweisen kommen wir zur Schlussfolgerung, dass der Selectionsort die asymptotische Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N^2)\!&amp;lt;/math&amp;gt; besitzt.&lt;br /&gt;
&lt;br /&gt;
==== Zusammenhang zwischen Komplexität und Laufzeit ==== &lt;br /&gt;
&lt;br /&gt;
Wenn eine Operation 1ms dauert, erreichen Algorithmen verschiedener Komplexität folgende Leistungen (wobei angenommen wird, dass der in der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation verborgene konstante Faktor immer etwa gleich 1 ist):&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:left&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
|-&lt;br /&gt;
! Komplexität !! Operationen in 1s !! Operationen in 1min !! Operationen in 1h&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 1000 || 60.000 || 3.600.000&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N\log_2{N})&amp;lt;/math&amp;gt;&lt;br /&gt;
| 140 || 4895 || 204094&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 32 || 245 || 1898&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 10 || 39 || 153&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(2^N)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 10 || 16 || 21&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Exponentielle Komplexität ==== &lt;br /&gt;
Der letzte Fall &amp;lt;math&amp;gt;\mathcal{O}(2^N)&amp;lt;/math&amp;gt; ist von exponentieller Komplexität. Das bedeutet, dass eine Verdopplung des Aufwands nur bewirkt, dass die maximale Problemgröße um eine Konstante wächst. Algorithmen mit exponentieller (oder noch höherer) Komplexität werden deshalb als '''ineffizient''' bezeichnet. Algorithmen mit höchstens polynomieller Komplexität gelten hingegen als effizient.&lt;br /&gt;
&lt;br /&gt;
In der Praxis sind allerdings auch polynomielle Algorithmen mit hohem Exponenten meist zu langsam. Als Faustregel kann man eine praktische Grenze von &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt; ansehen. Bei einer Komplexität von &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt; bewirkt ein verdoppelter Aufwand immer noch eine Steigerung der maximalen Problemgröße um den Faktor &amp;lt;math&amp;gt;\sqrt[3]{2}&amp;lt;/math&amp;gt; (also eine ''multiplikative'' Vergrößerung um ca. 25%, statt nur einer additiven Vergrößerung wie bei exponentieller Komplexität).&lt;br /&gt;
&lt;br /&gt;
===&amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;- Notation===&lt;br /&gt;
&lt;br /&gt;
Genauso wie &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; eine Art &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;-Operator für Funktionen ist, definiert &amp;lt;math&amp;gt;f \in \Omega(g) &amp;lt;/math&amp;gt; eine Abschätzung von unten, analog zum &amp;lt;math&amp;gt;\ge&amp;lt;/math&amp;gt;-Operator für Zahlen. Formal kann man &amp;lt;math&amp;gt;f(N) \in \Omega(g(N)) &amp;lt;/math&amp;gt; genau dann schreiben, falls es eine Konstante &amp;lt;math&amp;gt; c &amp;gt; 0 &amp;lt;/math&amp;gt; gibt, so dass &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; f(N) \ge c \cdot g(N) &amp;lt;/math&amp;gt; für &amp;lt;math&amp;gt; N  \ge N_0 &amp;lt;/math&amp;gt; &lt;br /&gt;
&lt;br /&gt;
gilt.&lt;br /&gt;
Man verwendet diese Notation also um abzuschätzen, wie groß der Aufwand (die Komplexität) für einen bestimmten Algorithmus ''mindestens'' ist und nicht ''höchstens'', was man mit der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt; - Notation ausdrücken würde.&lt;br /&gt;
&lt;br /&gt;
Ein praktisches Beispiel für eine Anwendung der &amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;- Notation wäre die Fragestellung, ob es ''prinzipiell'' einen besseren Algorithmus für ein bestimmtes Problem gibt. Wie später im Abschnitt [[Suchen#Sortieren_als_Suchproblem|Sortieren als Suchproblem]] gezeigt wird, ist das Sortieren eines Arrays durch paarweise Vergleiche von Elementen immer mindestens von der Komplexität &amp;lt;math&amp;gt; \Omega(N\cdot \ln N) &amp;lt;/math&amp;gt;, was konkret bedeutet, dass kein Sortieralgorithmus, der nach diesem Prinzip arbeitet, jemals eine geringere Komplexität als beispielsweise Merge-Sort haben wird. Natürlich kann man den entsprechenden Sortieralgorithmus, also Merge-Sort zum Beispiel, unter Umständen noch optimieren, aber die Komplexität wird erhalten bleiben. Mit diesem Wissen kann man sich viel (vergebliche) Arbeit sparen.&lt;br /&gt;
&lt;br /&gt;
===&amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;- Notation===&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;math&amp;gt;f(N) \in \Theta(g(N))&amp;lt;/math&amp;gt; ist eine scharfe Abschätzung der asymptotischen Komplexität einer Funktion f. &lt;br /&gt;
&lt;br /&gt;
Damit dies gilt, muss &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; und ''gleichzeitig'' &amp;lt;math&amp;gt;f(N) \in \Omega(g(N))&amp;lt;/math&amp;gt; erfüllt sein.&lt;br /&gt;
&lt;br /&gt;
Dies ist natürlich auch die beste Abschätzung der asymptotischen Komplexität einer Funktion f. Formal bedeutet &amp;lt;math&amp;gt;f(N) \in \Theta(g(N))&amp;lt;/math&amp;gt; dass es zwei Konstanten &amp;lt;math&amp;gt; c_1 &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; c_2 &amp;lt;/math&amp;gt;, beide größer als Null, gibt, so dass für alle &amp;lt;math&amp;gt; N \geq N_0 &amp;lt;/math&amp;gt; gilt: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; c_1 \cdot g(N) \leq f(N) \leq c_2 \cdot g(N) &amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In der Praxis wird manchmal statt der &amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;-Notation auch dann die &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation benutzt, wenn eine scharfe Schranke ausgedrückt werden soll. Dies ist zwar formal nicht korrekt, aber man kann die intendierte Bedeutung meist aus dem Kontext erschließen.&lt;br /&gt;
&lt;br /&gt;
== Komplexitätsvergleich zweier Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
In diesem Abschnitt wollen wir der Frage nachgehen, wie ein formaler Beweis für die Behauptung &amp;lt;math&amp;gt; f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; geschehen kann. Hierbei werden zwei Beweismethoden vorgestellt werden, und zwar der '''Beweis über die Definition der Komplexität''' sowie der '''Beweis durch Dividieren'''.&lt;br /&gt;
&lt;br /&gt;
===Beweis über die Definition der asymptotischen Komplexität===&lt;br /&gt;
&lt;br /&gt;
Die Definition der asymptotischen Komplexität &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; war: &lt;br /&gt;
&lt;br /&gt;
Es gibt eine Konstante &amp;lt;math&amp;gt; c &amp;gt; 0 &amp;lt;/math&amp;gt;, so dass &amp;lt;math&amp;gt; f(N) \le c \cdot g(N) &amp;lt;/math&amp;gt; für &amp;lt;math&amp;gt; N  \ge N_0 &amp;lt;/math&amp;gt; erfüllt ist. &lt;br /&gt;
&lt;br /&gt;
Um also die die asymptotische Komplexität &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; zu beweisen, muss man die oben erwähnten Konstanten c und &amp;lt;math&amp;gt; N_0 &amp;lt;/math&amp;gt; finden, so dass &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; f(N) \leq c \cdot g(N) &amp;lt;/math&amp;gt; für alle &amp;lt;math&amp;gt; N \ge N_0 &amp;lt;/math&amp;gt; erfüllt ist. &lt;br /&gt;
&lt;br /&gt;
Dies geschieht zweckmäßigerweise mit dem Beweisprinzip der ''vollständigen Induktion''. Hierbei ist zu zeigen, dass&lt;br /&gt;
# &amp;lt;math&amp;gt; f(N_0) \leq g(N_0) &amp;lt;/math&amp;gt; für die eine zu bestimmende Konstante &amp;lt;math&amp;gt; N_0 &amp;lt;/math&amp;gt; gilt (''Induktionsanfang'') und &lt;br /&gt;
# falls &amp;lt;math&amp;gt; f(N) \leq g(N) &amp;lt;/math&amp;gt;, dann auch &amp;lt;math&amp;gt; f(N+1) \leq g(N+1) &amp;lt;/math&amp;gt; (''Induktionsschritt'') gilt.&lt;br /&gt;
&lt;br /&gt;
===Beweis durch Dividieren===&lt;br /&gt;
&lt;br /&gt;
Hierbei wählt man eine Konstante c und zeigt, dass &amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f(N)}{c \cdot g(N)} \leq 1 &amp;lt;/math&amp;gt; gilt (für die O-Notation, bei &amp;amp;Omega;-Notation gilt entsprechend &amp;lt;math&amp;gt;\geq 1 &amp;lt;/math&amp;gt;). Man kann dies auch als alternative Definition der Komplexität verwenden.&lt;br /&gt;
&lt;br /&gt;
Als Beispiel betrachten wir die beiden Funktionen &amp;lt;math&amp;gt; f(N) = N \,\lg N &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; g(N) = N^2 &amp;lt;/math&amp;gt; und wollen zeigen, dass &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; gilt. &lt;br /&gt;
&lt;br /&gt;
Als Konstante c wählen wir &amp;lt;math&amp;gt; c = 1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f(N)}{g(N)} = \lim_{N \rightarrow \infty} \frac{\lg N}{N} = \frac{\infty}{\infty} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Unbestimmte Ausdrücke der Form &lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{x \rightarrow x_0} \frac{f(x)}{g(x)} &amp;lt;/math&amp;gt;,&lt;br /&gt;
in denen sowohl &amp;lt;math&amp;gt; f(x) &amp;lt;/math&amp;gt; als auch &amp;lt;math&amp;gt; g(x) &amp;lt;/math&amp;gt; mit &amp;lt;math&amp;gt; x \rightarrow x_0 &amp;lt;/math&amp;gt; gegen Null oder gegen Unendlich streben, kann man manchmal mit den Regeln von [http://de.wikipedia.org/wiki/L%27Hospital%27sche_Regel ''l'Hospital''] berechnen. Danach darf man die Funktionen f und g zur Berechnung des unbestimmten Ausdrucks durch ihre k-ten Ableitungen ersetzen:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{x \rightarrow x_0} \frac{f(x)}{g(x)} = \lim_{x \rightarrow x_0} \frac{f^{(k)}(x)}{g^{(k)}(x)} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In unserem Fall verwenden wir die erste Ableitung und erhalten:&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f'(x)}{g'(x)} = \lim_{N \rightarrow \infty} \frac{1/N}{1} \rightarrow 0 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Damit wurde &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt;, also &amp;lt;math&amp;gt;N \lg N \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt; gezeigt.&lt;br /&gt;
&lt;br /&gt;
Man beachte hierbei, dass &amp;lt;math&amp;gt;N \lg N \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt; keine enge Grenze für die Komplexität von &amp;lt;math&amp;gt;N \,\lg N&amp;lt;/math&amp;gt; darstellt, da der Grenzwert &amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f'(x)}{g'(x)}\, &amp;lt;/math&amp;gt; gegen 0 und nicht gegen eine von Null verschiedene Konstante strebt. In diesem Fall haben wir die Komplexität von &amp;lt;math&amp;gt;N \cdot \lg N &amp;lt;/math&amp;gt; also nur nach oben abschätzen können.&lt;br /&gt;
&lt;br /&gt;
===Beispiel für den Komplexitätsvergleich: Gleitender Mittelwert (Running Average)===&lt;br /&gt;
&lt;br /&gt;
Wir berechnen für ein gegebenes Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; einen gleitenden Mittelwert über &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; Elemente:&amp;lt;br/&amp;gt;&lt;br /&gt;
::&amp;lt;math&amp;gt;r_i = \frac{1}{k} \sum_{j=i-k+1}^i a_j&amp;lt;/math&amp;gt; &amp;lt;br/&amp;gt;&lt;br /&gt;
Das heisst, für jedes &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; mitteln wir die letzten &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; Elemente von &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; und schreiben das Ergebnis in &amp;lt;tt&amp;gt;r[i]&amp;lt;/tt&amp;gt;. Diese Operation ist z.B. bei Börsenkursen wichtig: Neben dem aktuellen Kurs für jeden Tag wird dort meist auch der gleitende Mittelwert der letzten 30 Tage sowie der letzten 200 Tage angegeben. In diesen Mittelwerten erkennt man besser die langfristige Tendenz, weil die täglichen Schwankungen herausgemittelt werden. Wir nehmen außerdem an, dass&lt;br /&gt;
* Array-Zugriff hat eine Komplexit&amp;amp;auml;t von O(1)&lt;br /&gt;
* &amp;lt;math&amp;gt;k \ll N&amp;lt;/math&amp;gt;, d.h. &amp;lt;math&amp;gt;N-k\approx N&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die beiden folgenden Algorithmen berechnen die Mittelwerte auf unterschiedliche Art. Der linke folgt der obigen Definition durch eine Summe, während der rechte inkrementell arbeitet: Man kann den Bereich der &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; letzten Werte als Fenster betrachten, das über das Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; geschoben wird. Schiebt man das Fenster ein Element weiter, fällt links ein Element heraus, und rechts kommt eins hinzu. Man muss also nicht jedes Mal die Summe neu berechnen, sondern kann den vorigen Wert aktualisieren. Wir werden sehen, dass dies Folgen für die Komplexität des Algorithmus hat.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;2&amp;quot; &lt;br /&gt;
|-&lt;br /&gt;
! Programmzeile&lt;br /&gt;
! Version 1: O(N * k)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
! Version 2: O(N)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
1.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
2.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
3.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
4.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k-1, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for i in range(k):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
5.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;for i in range(j-k+1, j+1):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[k-1] += a[i]&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
6.&lt;br /&gt;
|&lt;br /&gt;
:::: &amp;lt;tt&amp;gt;r[j] += a[i]&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
7.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] = (a[j] - a[j-k] + r[j-1])&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
8.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
9.&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
10.&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Wir zeigen unten dass Version 2 eine geringere Komplexit&amp;amp;auml;t besitzt, obwohl sie mehr Zeilen ben&amp;amp;ouml;tigt.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Wir haben in der Tabelle die Komplexität jeder Zeile für sich angegeben. Einfache Anweisungen (Berechnungen, Lese- und Schreibzugriffe auf das Array, Zuweiseungen) haben konstante Komplexität, die Komplexität des Schleifenkopfes allein (also der &amp;lt;tt&amp;gt;for&amp;lt;/tt&amp;gt;-Anweisung ohne den eingerückten Schleifenkörper) entspricht der Anzahl der Durchläufe. Wir müssen jetzt noch die Verschachtelung der Schleifen und die Nacheinanderausführung von Anweisungen berücksichtigen. &lt;br /&gt;
&lt;br /&gt;
====Berechnung der Komplexität von Version 1====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;small&amp;gt;(Wiederholung der Rechenregeln: siehe Abschnitt [[Effizienz#O-Notation|O-Notation]])&amp;lt;/small&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wir betrachten zunächst die innere Schleife (Zeilen 5 und 6 von Version 1):&lt;br /&gt;
&lt;br /&gt;
Der Schleifenkopf (Zeile 5) hat die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt;, weil die Schleife k-mal durchlaufen wird. Der Schleifenkörper (Zeile 6) hat die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;. Nach der Verschachtelungsregel müssen wir die beiden Komplexitäten multiplizieren, und es ergibt sich:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(k)\cdot\mathcal{O}(1) = \mathcal{O}(k\cdot 1)=\mathcal{O}(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wir betrachten nun die äußere Schleife. Der Schleifenkopf (Zeile 4) wird (N-k)-mal durchlaufen und hat somit eine Komplexität von &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Der Schleifenkörper (Zeilen 5 bis 7) besteht aus der inneren Schleife (Zeilen 5 und 6) mit der gerade berechneten Komplexität &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt; sowie einer einfachen Anweisung (Zeile 7) mit Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;. Nach der Sequenzregel wird die Komplexität des Schleifenkörpers durch Addition berechnet:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(k)+\mathcal{O}(1) = \mathcal{O}(\max(k,1)) = \mathcal{O}(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexität der gesamten äußeren Schleife erhalten wir nach der Verschachtelungsregel wieder durch multiplizieren:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)\cdot\mathcal{O}(k) = \mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die übrigen Schritte des Algorithmus werden einfach nacheinander ausgeführt, so dass sie ebenfalls nach der Sequenzregel behandelt werden. Wir erhalten&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)+\mathcal{O}(1)+\mathcal{O}(N\cdot k)+\mathcal{O}(1) = \mathcal{O}(\max(N,1,N\cdot k,1)) = \mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Der gesamte Algorithmus hat also die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Berechnung der Komplexität von Version 2====&lt;br /&gt;
&lt;br /&gt;
Hier gibt es nur einfache Schleifen ohne Verschachtelung. Da der Schleifenkörper jeder Schleife nur einfache Anweisungen der Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt; enthält, ergibt sich die Komplexität der Schleifen nach der Verschachtelungsregel als&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(X)\cdot\mathcal{O}(1) = \mathcal{O}(X\cdot 1)=\mathcal{O}(X)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
wobei &amp;lt;math&amp;gt;\mathcal{O}(X)&amp;lt;/math&amp;gt; die Komplexität des jeweiligen Schleifenkopfes ist. Wir erhalten also für Zeilen 4 und 5: &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt;, Zeilen 6 und 7: &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;, Zeilen 8 und 9: &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Die Hintereinanderausführung wird nach der Sequenzregel behandelt:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)+\mathcal{O}(1)+\mathcal{O}(k)+\mathcal{O}(N)+\mathcal{O}(N)+\mathcal{O}(1) = \mathcal{O}(\max(N,1,k,N,N,1)) = \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Dieser Algorithmus hat also nur die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Fazit====&lt;br /&gt;
&lt;br /&gt;
Obwohl Version 2 mehr Schritte benötigt hat sie eine geringere Komplexität, da die for-Schleifen nicht wie bei Version 1 verschachtelt/untergeordnet sind. Bei verschachtelten for-Schleifen muss die Multiplikationsregel angewendet werden &amp;amp;rarr; höhere Komplexität.&lt;br /&gt;
&lt;br /&gt;
Die gerade berechnete Komplexität gilt aber &amp;lt;u&amp;gt;nur&amp;lt;/u&amp;gt; unter der Annahme, dass Array-Zugriffe konstante Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt; besitzen. Wenn dies nicht der Fall ist, kann sich die Komplexität des Algorithmus drastisch verschlechtern.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|Allgemein gilt:&amp;lt;br/&amp;gt;&lt;br /&gt;
Algorithmen-Analysen beruhen auf der Annahme, dass Zugriffe auf die Daten optimal schnell sind, dass heißt, dass die für den jeweiligen Algorithmus am besten geeignete Datenstruktur verwendetet wird.&amp;lt;br /&amp;gt; &amp;amp;rarr; Ansonsten: Komplexitätsverschlechterung!&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Beispiel für eine Verschlechterung der Komplexität durch Verwendung einer nicht optimalen Datenstruktur====&lt;br /&gt;
&lt;br /&gt;
Wir verwende im Mittelwert-Algorithmus eine verkettete Liste anstelle des Eingabe-Arrays &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt;. Wir benötigen dazu eine Funktion, die das j-te Element der Liste zurückgibt. Wie üblich ist die Liste mit Hilfe einer Knotenklasse implementiert:&lt;br /&gt;
      class Node:&lt;br /&gt;
          def __init__(self, data):&lt;br /&gt;
              self.data = data&lt;br /&gt;
              self.next = None&lt;br /&gt;
&lt;br /&gt;
Die Listenklasse selbst hat ein Feld &amp;lt;tt&amp;gt;head&amp;lt;/tt&amp;gt;, das eine Referenz auf den ersten Knoten speichert, und jeder Knoten speichert im Feld &amp;lt;tt&amp;gt;next&amp;lt;/tt&amp;gt; eine Referenz auf seinen Nachfolger. Um zum j-ten Element zu gelangen, muss man die Liste sequenziell durchlaufen&lt;br /&gt;
      def get_jth(list, j):&lt;br /&gt;
           r = list.head&lt;br /&gt;
           while j &amp;gt; 0:&lt;br /&gt;
               r = r.head&lt;br /&gt;
               j -= 1&lt;br /&gt;
           return r.data&lt;br /&gt;
Die Komplexität dieser Funktion ist offensichtlich &amp;lt;math&amp;gt;\mathcal{O}(j)&amp;lt;/math&amp;gt; (Komplexitätsberechnung wie oben). Wir setzen jetzt bei Version 1 des Mittelwert-Algorithmus diese Funktion in Zeile 6 anstelle des Indexzugriffs &amp;lt;tt&amp;gt;a[i]&amp;lt;/tt&amp;gt; ein (nur in dieser Zeile wird auf die Elemente des Arrays zugegriffen). Wir erhalten folgende Implementation (die Änderungen sind rot markiert):&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;2&amp;quot; &lt;br /&gt;
|-&lt;br /&gt;
! Programmzeile&lt;br /&gt;
! Version 1 mit Liste: O(N * k)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
1.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
2.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
3.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
4.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k-1, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
5.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;for i in range(j-k+1, j+1):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
6.&lt;br /&gt;
|&lt;br /&gt;
:::: &amp;lt;tt&amp;gt;r[j] += &amp;lt;font color=red&amp;gt;get_jth(a, i)&amp;lt;/font&amp;gt;&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;&amp;lt;font color=red&amp;gt;O(i)&amp;lt;/font&amp;gt;&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
7.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
8.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Der Aufruf der Funktion &amp;lt;tt&amp;gt;get_jth&amp;lt;/tt&amp;gt; ist jetzt gleichbedeutend mit einer dreifach verschachtelten Schleife (weil &amp;lt;tt&amp;gt;get_jth&amp;lt;/tt&amp;gt; ja eine zusatzliche Schleife enthält). Die Anzahl der Operationen in Zeile 4 bis 6 ist jetzt&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;f(N,k)=\sum_{j=k-1}^{N-1}\,\sum_{i=j-k+1}^j\,\mathcal{O}(i)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
wobei das &amp;lt;math&amp;gt;\mathcal{O}(i)&amp;lt;/math&amp;gt; die neue Schleife durch Verwendung der Liste repräsentiert. Mit Mathematica-Hilfe [http://www.wolfram.com/] lässt sich diese Summe exakt ausrechnen&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;f(N,k)=\frac{1}{2}(k N^2-k^2 N+k^2-k)\in \mathcal{O}(k N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexitätsberechnung erfolgte dabei nach der Regel für Polynome unter Beachtung von &amp;lt;math&amp;gt;k \ll N&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Fazit:====&lt;br /&gt;
&lt;br /&gt;
Die Komplexität von Version 1 mit einer verketteten Liste wäre O(N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; * k)&lt;br /&gt;
'''&amp;amp;rarr; Die richtige Datenstruktur ist wichtig, da es sonst zu einer Komplexitätsverschlechterung kommen kann!'''&lt;br /&gt;
&lt;br /&gt;
Auf Version 2 unseres Running Average-Beispiels hätte eine verkettete Liste allerdings keine Auswirkungen, da die inkrementelle Berechnung der Summen in Zeile 7 weiterhin möglich ist (bei geschickter Implementation!) und somit Version 2 immer noch eine Komplexität von O(N) hätte.&lt;br /&gt;
&lt;br /&gt;
==Amortisierte Komplexität==&lt;br /&gt;
&lt;br /&gt;
Bis jetzt wurde die Komplexität nur im schlechtesten Fall (Worst Case) betrachtet. Bei einigen Operationen schwankt die Komplexität jedoch sehr stark, wenn man sie mehrmals hintereinander ausführt, und der schlechteste Fall kommt nur selten vor. Dann ist es sinnvoll, die &amp;lt;i&amp;gt;amortisierte Komplexität&amp;lt;/i&amp;gt; zu betrachten, die sich mit der &amp;lt;i&amp;gt;durchschnittlichen&amp;lt;/i&amp;gt; Komplexität über viele Aufrufe der selben Operation beschäftigt.&lt;br /&gt;
&lt;br /&gt;
Zum weiter Lesen: [[http://de.wikipedia.org/wiki/Amortisierte_Laufzeitanalyse Wikipedia: Amortisierte Laufzeitanalyse]]&lt;br /&gt;
&lt;br /&gt;
===Beispiel: Inkrementieren von Binärzahlen===&lt;br /&gt;
&lt;br /&gt;
Frage: Angenommen, das Umdrehen eines Bits einer Binärzahl verursacht Kosten von 1 Einheit. Wir erzeugen die Folge der natürlichen Zahlen in Binärdarstellung durch sukzessives Inkrementieren, von Null beginnend. Bei jeder Inkrementierung werden einige Bits verändert, aber diese Zahl (und damit die Kosten der Inkrementierungen) ''schwanken'' sehr stark. Wir fragen jetzt, was eine Inkrementierung im Durchschnitt kostet?&lt;br /&gt;
&lt;br /&gt;
Um diese Durchschnittskosten zu berechnen, bezahlen wir bei jeder Inkrementierung 2 Einheiten. Wenn davon nach Abzug der Kosten der jeweiligen Operation noch etwas übrig bleibt, wird der Rest dem  Guthaben zugeschrieben. Umgekehrt wird ein eventueller Fehlbetrag (wenn eine Inkrementierung mehr als 2 Bits umdreht) aus dem Guthaben gedeckt. Dadurch werden die ansonsten großen Schwankungen der Kosten ausgeglichen:&lt;br /&gt;
:: Kosten &amp;lt; Einzahlung &amp;amp;rarr; es wird gespart&lt;br /&gt;
:: Kosten = Einzahlung &amp;amp;rarr; Guthaben bleibt unverändert&lt;br /&gt;
:: Kosten &amp;gt; Einzahlung &amp;amp;rarr; Guthaben wird für die Kosten verbraucht&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
!Schritte&lt;br /&gt;
!Zahlen&lt;br /&gt;
!Kosten &amp;lt;br/&amp;gt;&lt;br /&gt;
(Anzahl der geänderten Bits)&lt;br /&gt;
! Einzahlung&lt;br /&gt;
!Guthaben =&amp;lt;br/&amp;gt;&lt;br /&gt;
altes Guthaben + Einzahlung - Kosten&lt;br /&gt;
|-&lt;br /&gt;
|1.&lt;br /&gt;
|0000&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|2.&lt;br /&gt;
|000&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|2&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|3.&lt;br /&gt;
|0001&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|4.&lt;br /&gt;
|00&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|3&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|5.&lt;br /&gt;
|0010&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|6.&lt;br /&gt;
|001&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|2&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|7.&lt;br /&gt;
|0011&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''3'''&lt;br /&gt;
|-&lt;br /&gt;
|8.&lt;br /&gt;
|0&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1000&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|4&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Die Kosten ergeben sich aus der Anzahl der Ziffern die von 1 nach 0, bzw. von 0 nach 1 verändert werden&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Rechnung:&amp;lt;/u&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Schritt: Kosten: 1 &amp;lt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Schritt: Kosten: 2 = Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird nicht gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; Guthaben bleibt so wie es ist &amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Schritt: Kosten: 1 &amp;lt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. Schritt: Kosten: 3 &amp;gt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird eine 1 vom Guthaben genommen um die Kosten zu zahlen&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
usw.&lt;br /&gt;
&lt;br /&gt;
Man erkennt, dass vor teuren Operation (Wechsel von 3 auf 4 bzw. von 7 auf 8) genügend Guthaben angespart wurde, um die Kosten zu decken. Das Guthaben geht bei diesen Operationen immer wieder auf 1 zurück, aber es wird nie vollständig verbraucht (Dies kann natürlich auch mathematisch exakt bewiesen werden, wie wir es unten am Beispiel des dynamische Arrays zeigen). Wir schließen daraus, dass die durchschnittlichen oder '''amortisierten Kosten''' einer Inkrementierungsoperation gleich 2 sind.&lt;br /&gt;
&lt;br /&gt;
Zum Weiterlesen: [[http://de.wikipedia.org/wiki/Account-Methode Wikipedia Account-Methode]]&lt;br /&gt;
&lt;br /&gt;
====Fazit====&lt;br /&gt;
Die amortisierte Komplexität beschäftigt sich mit dem Durchschnitt aller Operation im ungünstigsten Fall. Operationen mit hohen Kosten, die aber nur selten ausgeführt werden, fallen bei der amortisierten Komplexität nicht so ins Gewicht. Bei Algorithmen, die gelegentlich eine &amp;quot;teure&amp;quot; Operation benutzen, ansonsten jedoch &amp;quot;billige&amp;quot; Operationen aufrufen, kann die amortisierte Komplexität niedriger sein als die Komplexität im schlechtesten (Einzel-)Fall.&lt;br /&gt;
&lt;br /&gt;
In unserem Beispiel fallen die teuren Einzelschritte (z.B. 4. und 8. Schritt) bei den amortisierten Kosten nicht so ins Gewicht, da wir die Kosten aus unserem Guthaben mitbezahlen können. Das Guthaben ist immer groß genug, weil jeder zweite Aufruf eine billige Operation ist, die nur ein Bit umdreht und somit das Ansparen ermöglicht. Diese Betrachtung zeigt, dass die amortisierte (d.h. durchschnittliche) Komplexität des Algoithmus niedriger (nämlich konstant) ist als die Komplexität im schlechtesten Fall.&lt;br /&gt;
&lt;br /&gt;
===Anwendung: Dynamisches Array===&lt;br /&gt;
&lt;br /&gt;
Ein dynamisches Array hat die Eigenschaft, dass man effizient am Ende des Arrays neue Elemente anfügen kann, indem man die Länge des Arrays entsprechend vergrößert (siehe Übung 1). Die Analyse der amortisierten Komplexität der Anfüge-Operation zeigt uns, wie man das Vergrößern des Arrays richtig implementiert, damit die Operation wirklich effizient abläuft.&lt;br /&gt;
&lt;br /&gt;
==== Ineffiziente naive Lösung ====&lt;br /&gt;
&lt;br /&gt;
Wenn wir an ein Array ein Element anhängen wollen, müssen wir neuen Speicher allokieren, der die gewünschte Länge hat. Die Werte aus dem alten Array müssen dann in den neuen Speicher umkopiert werden. Danach kann das neue Element hinten angefügt werden, weil wir im neuen Array bereits Speicher für dieses Element reserviert haben. Bei der naiven Implementation des dynamischen Arrays wiederholt man dies bei jeder Anfügeoperation. Für die Analyse nehmen wir an, dass das Kopieren eines Elements konstante Zeit O(1) erfordert, ebenso das Einfügen eines neuen Elements auf in eine noch unbenutzte Speicherposition.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Naives Anhängen eines weiteren Elements an ein Array:&amp;lt;/u&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;right&amp;quot;&lt;br /&gt;
!Schritte&lt;br /&gt;
|'''Array'''&lt;br /&gt;
&amp;lt;small&amp;gt;(wie es nach jedem Schritt aussieht)&amp;lt;/small&amp;gt;&lt;br /&gt;
!Komplexität&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;center&amp;gt;altes Array (N=4)&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|1. neuer Speicher für&amp;lt;br&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;(N+1) Elemente&lt;br /&gt;
|&amp;lt;center&amp;gt;[None,None,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;O(N+1) = '''O(N)'''&amp;lt;/center&amp;gt;(wenn der Speicher initialisiert wird&amp;lt;br&amp;gt;(hier auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;), sonst O(1))&lt;br /&gt;
|-&lt;br /&gt;
|2. Kopieren &lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;'''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|3. append von &amp;quot;x&amp;quot;&lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3,'x']&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;'''O(1)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
altesArray = [0,1,2,3]&amp;lt;br /&amp;gt;&lt;br /&gt;
altesArray.append('x')&lt;br /&gt;
&lt;br /&gt;
1. Es wird ein neues Array der Größe N+1 erzeugt&amp;lt;br /&amp;gt;&lt;br /&gt;
2. Die N Datenelemente aus dem alten Array werden in das neue Array kopiert&amp;lt;small&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Das sind N Operationen der Komplexität O(1), also ein Gesamtaufwand von O(N).&amp;lt;/small&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
3. 'x' wird mit Aufwand O(1) an die letzte Stelle des neuen Arrays geschrieben&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Additionsregel:&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
O(N) + O(1) &amp;amp;isin; O(N)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Folgerung:&amp;lt;/b&amp;gt; &lt;br /&gt;
&lt;br /&gt;
Bei der naiven Methode erfordert jede Anfügung einen Aufwand O(N) (wobei N die derzeitige Arraygröße ist). &amp;lt;b&amp;gt;Das ist nicht effizient.&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Effiziente Lösung durch Verdoppeln der Kapazität====&lt;br /&gt;
&lt;br /&gt;
Offensichtlich kommt man nicht darum heraum, den Inhalt des alten Arrays zu kopieren, wenn der allokierte Speicher voll ist. Der Trick für die effiziente Implementation der Anfügeoperation besteht darin, das Kopieren so selten wie möglich durchzuführen, also nicht wie in der naiven Lösung bei jeder Anfügeoperation. Hier kommt die amortisierte Komplexität ins Spiel: Ab und zu gibt es eine teure Anfügeoperation (wenn nämlich kopiert werden muss), aber wenn man den durchschnittlichen Aufwand über viele Anfügungen betrachtet, ist die Operation effizient. Der teure Fall wird sozusagen &amp;quot;herausgemittelt&amp;quot;. &lt;br /&gt;
&lt;br /&gt;
Um nur selten kopieren zu müssen, werden beim dynamischen Array mehr Speicherelemente reserviert als zur Zeit benötigt werden (in der naiven Lösung wurde dagegen immer nur Speicher für ein einziges neues Element reserviert). Wir unterscheiden deshalb &lt;br /&gt;
&lt;br /&gt;
:&amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; = Anzahl der allokierten Speicherzellen, d.h. der möglichen Elemente, die in das Array passen&amp;lt;br /&amp;gt;&lt;br /&gt;
:&amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt; = Anzahl der Elemente, die im Array zur Zeit gespeichert sind&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Daten selbst werden in einem statischen Array gespeichert:&lt;br /&gt;
:&amp;lt;tt&amp;gt;data&amp;lt;/tt&amp;gt; = statisches Array der Größe &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die folgende intuitive Abschätzung zeigt, dass es sinnvoll ist, die Größe des allokierten Speichers jeweils zu verdoppeln. Wir starten bei einem Array der Größe &amp;lt;tt&amp;gt;size = capacity&amp;lt;/tt&amp;gt; = N. Da der verfügbare Speicher voll ist, müssen wir bei der nächsten Anfügung die N vorhandenen Elemente in ein neues Array der Länge &amp;lt;tt&amp;gt;new_capacity&amp;lt;/tt&amp;gt; kopieren (Aufwand &amp;lt;math&amp;gt;N\cdot O(1)&amp;lt;/math&amp;gt;). Danach können wir K Elemente billig einfügen (Aufwand &amp;lt;math&amp;gt;K\cdot O(1)&amp;lt;/math&amp;gt;), wobei &lt;br /&gt;
:K = &amp;lt;tt&amp;gt;new_capacity - capacity&amp;lt;/tt&amp;gt; &lt;br /&gt;
die Anzahl der nach dem Kopieren noch unbenutzen Speicherzellen ist. Der durchschnittliche Aufwand für diese K Einfügungen ist somit&lt;br /&gt;
:&amp;lt;math&amp;gt;\bar T = \frac{N \cdot O(1) + K \cdot O(1)}{K}=\frac{N+K}{K}\cdot O(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Damit die mittlere Zeit in O(1) sein kann, muss der Quotient &amp;lt;math&amp;gt;(N+K)/K&amp;lt;/math&amp;gt; eine Konstante sein. Wir setzen &amp;lt;math&amp;gt;K = a N&amp;lt;/math&amp;gt; und erhalten:&lt;br /&gt;
:&amp;lt;math&amp;gt;\bar T = \frac{(a+1)N}{a N}\cdot O(1)=\frac{a+1}{a}\cdot O(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Der amortisierte Aufwand über K Einfügungen ist also konstant, wenn &amp;lt;math&amp;gt;a&amp;lt;/math&amp;gt; eine (kleine) von N unabhängige Zahl ist. Typischerweise wählt man &lt;br /&gt;
:&amp;lt;math&amp;gt;a = 1&amp;lt;/math&amp;gt;&lt;br /&gt;
und mit &amp;lt;math&amp;gt;K = 1\cdot N&amp;lt;/math&amp;gt; ergibt sich&lt;br /&gt;
:&amp;lt;tt&amp;gt;new_capacity = capacity&amp;lt;/tt&amp;gt; + N = &amp;lt;tt&amp;gt;2 * capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Vorgehensweise beim Zufügen eines neuen Elements im Fall &amp;lt;tt&amp;gt;size == capacity&amp;lt;/tt&amp;gt; ist also&lt;br /&gt;
* capacity wird verdoppelt&amp;lt;br /&amp;gt;&lt;br /&gt;
: &amp;lt;tt&amp;gt;neue capacity = 2 * alte capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
: (allgemein genügt es auch, wenn capacity um einen bestimmten Prozentsatz vergrößert wird, &lt;br /&gt;
:: &amp;lt;tt&amp;gt;neue capacity = alte capacity * c&amp;lt;/tt&amp;gt; &lt;br /&gt;
: mit c &amp;gt; 1, z.B. c = 1.2, das entspricht oben der Wahl &amp;lt;math&amp;gt;a = 0.2&amp;lt;/math&amp;gt;)&lt;br /&gt;
* ein neues statisches Array der Größe 'neue capacity' wird erzeugt&lt;br /&gt;
* das alte Array wird ins neue kopiert und danach freigegeben&lt;br /&gt;
* das anzufügende Element wird ins neue Array eingefügt&lt;br /&gt;
Umgekehrt geht man beim Entfernen des ''letzten'' Array-Elements vor. Normalerweise überschreibt man einfach das letzte Element mit &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; und dekrementiert &amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt;. Wird dadurch das Array zu klein (üblicherweise &amp;lt;tt&amp;gt;size &amp;amp;lt; capacity / 4&amp;lt;/tt&amp;gt;), wird die Kapazität halbiert, genauer:&lt;br /&gt;
* ein neues Array mit &amp;lt;br/&amp;gt;&lt;br /&gt;
: &amp;lt;tt&amp;gt;neue capacity = alte capacity / 2 &amp;lt;/tt&amp;gt;&lt;br /&gt;
: wird angelegt (bzw. mit&lt;br /&gt;
:: &amp;lt;tt&amp;gt;neue capacity = alte capacity / c &amp;lt;/tt&amp;gt;&lt;br /&gt;
: wenn ein anderer Vergrößerungsfaktor verwendet wird)&lt;br /&gt;
* das alte Array wird ins neue kopiert und danach freigegeben&lt;br /&gt;
&lt;br /&gt;
'''Folge:''' Die Kosten für das Vergrößern/Verkleinern der Kapazität werden amortisiert über viele Einfügungen, die kein Vergrößern erfordern. Die Operation &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; besitzt amortisierte Komplexität O(1). Im folgenden Abschnitt zeigen wir dies mathematisch exakt mit der Potentialmethode.&lt;br /&gt;
&lt;br /&gt;
====Komplexitätsanalyse des dynamischen Arrays mit Potentialmethode====&lt;br /&gt;
&lt;br /&gt;
Durchschnitt der Gesamtkosten für N-maliges append = &amp;lt;math&amp;gt;\frac{1}{N} \sum_{i = 1}^N Kosten(i)&amp;lt;/math&amp;gt;. Zur Analyse der amortisierten Komplexität wird ein Potential&amp;lt;br/&amp;gt;&lt;br /&gt;
::&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2*size&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; - capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
eingeführt, wobei das Array nach dem i-ten Einfüge-Schritt die Größe size&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; und die Kapizität capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; hat. Wir nehmen vereinfachend an, dass es keine Löschoperationen gibt. Dann gilt nach dem i-ten Schritt jeweils&lt;br /&gt;
::&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2*i - capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Fall 1: Array ist nicht voll&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Es wird kein Umkopieren benötigt, da das Array noch nicht voll ist&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; size&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; &amp;lt; capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; == capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Kosten: 1 (für Einfügen des neuen Elements)&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial vor append: &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = 2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial nach append: &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
amortisierte Kosten = Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Phi;&amp;lt;sub&amp;gt;(i)&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;(i-1)&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::                 = 1            + (2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;)        - [2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;]&lt;br /&gt;
:::::                 = 1            + 2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;         - 2i + 2 + capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::                 = 1            + &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; - &amp;lt;del&amp;gt;capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;/del&amp;gt; - &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; + 2 + &amp;lt;del&amp;gt;capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;/del&amp;gt;&lt;br /&gt;
:::::                 = 1 + 2&lt;br /&gt;
:::::                 = 3 = O(1) &amp;amp;rarr; konstant&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Fall 2: Array ist voll&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Vor dem i-ten append muss umkopiert werden&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; size&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; == capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; == i-1&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; Allokieren eines neuen statischen Arrays mit verdoppelter Kapazität notwendig, also capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; == 2*capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Kosten: (i-1) + 1 (für Umkopieren und Einfügen des neuen Elements)&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial vor append =  &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = 2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial nach append =  &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; =  2i - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
amortisierte Kosten = Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Phi;&amp;lt;sub&amp;gt;(i)&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;(i-1)&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = ((i - 1) + 1) + 2i - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; - [2(i-1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;]&lt;br /&gt;
:::::               = i + &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; - &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; + 2 - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = i + 2 - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = i + 2 - (i - 1) &amp;lt;small&amp;gt;(da capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = i-1)&amp;lt;/small&amp;gt;&lt;br /&gt;
:::::               = 3 = O(1) &amp;amp;rarr; konstant            &lt;br /&gt;
&lt;br /&gt;
'''Damit wurde bewiesen, dass die Operation &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; beim dynamischen Array eine amortisierte Komplexität von 3 Einheiten hat, also &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; &amp;amp;isin; O(1)'''. Diese Operation kann deshalb gefahrlos in der inneren Schleife eines Algorithmus benutzt werden.&lt;br /&gt;
&lt;br /&gt;
==== Beispiel für 9 Einfügeoperationen ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot;&lt;br /&gt;
!Array&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;lt;small&amp;gt;(wie es aussehen könnte)&amp;lt;/small&amp;gt;&lt;br /&gt;
!size&lt;br /&gt;
!capacity&lt;br /&gt;
!Kosten für append&amp;lt;br /&amp;gt;(einschließlich Umkopieren)&lt;br /&gt;
!Summe Kosten&lt;br /&gt;
!Durchschnittskosten&lt;br /&gt;
!&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2 * size - capacity&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;lt;small&amp;gt;(i = size)&amp;lt;/small&amp;gt;&lt;br /&gt;
!Potenzialdifferenz&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;amp;Delta; &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
!amortisierte Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
= Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Delta; &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;0&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3/2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6/3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;0&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7/4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;5&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;12&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;12/5&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;13&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;13/6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;14&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;14/7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,h]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;15&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;15/8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,h,j,None,None,None,&amp;lt;br /&amp;gt;&lt;br /&gt;
None,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;9&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;16&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;24&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;24/9&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die durchschnittlichen Kosten betragen stets etwa 2 Einheiten, schwanken allerdings so, dass nicht unmittelbar ersichtlich ist, ob dies für sämtliche Einfügeoperationen gilt. Die amortisierte Komplexität, die mit Hilfe des Potentials berechnet wird, ist hingegen konstant 3, wie auch im obigen Beweis für alle Einfügeoperationen allgemein gezeigt wurde.&lt;br /&gt;
&lt;br /&gt;
[[Suchen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Effizienz&amp;diff=5421</id>
		<title>Effizienz</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Effizienz&amp;diff=5421"/>
				<updated>2012-07-27T16:03:52Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Beispiel: Inkrementieren von Binärzahlen */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Bei der Diskussion von Effizienz müssen wir zwischen der Laufzeit eines Algorithmus auf einem bestimmten System und seiner prinzipiellen Leistungsfähigkeit (Algorithmenkomplexität) unterscheiden. Der Benutzer ist natürlich vor allem an der Laufzeit interessiert, denn diese bestimmt letztendlich seine Arbeitsproduktivität. Ein Softwaredesigner hingegen muss eine Implementation wählen, die auf verschiedenen Systemen und in verschiedenen Anwendungen schnell ist. Für ihn sind daher auch Aussagen zur Algorithmenkomplexität sehr wichtig, um den am besten geeigneten Algorithmus auszuwählen.&lt;br /&gt;
&lt;br /&gt;
== Laufzeit ==&lt;br /&gt;
&lt;br /&gt;
Aus Anwendersicht ist ein Algorithmus effizient, wenn er die in der Spezifikation verlangten Laufzeitgrenzen einhält. Ein Algorithmus muss also nicht immer so schnell wie möglich sein, sondern so schnell wie nötig. Dies führt in verschiedenen Anwendungen zu ganz unterschiedliche Laufzeitanforderungen:&lt;br /&gt;
&lt;br /&gt;
* Berechnen des nächsten Steuerkommandos für eine Maschine: ca. 1/1000s&lt;br /&gt;
* Berechnen des nächsten Bildes für eine Videopräsentation (z.B. Dekompression von MPEG-kodierten Bildern): ca. 1/25s&lt;br /&gt;
: Geringere Bildraten führen zu ruckeligen Filmen.&lt;br /&gt;
* Sichtbare Antwort auf ein interaktives Kommando (z.B. Mausklick): ca. 1/2s&lt;br /&gt;
: Wird diese Antwortzeit überschritten, vermuten viele Benutzer, dass der Mausklick nicht funktioniert hat, und klicken nochmals, mit eventuell fatalen Folgen. Wenn ein Algorithmus notwendigerweise länger dauert als 1/2s, sollte ein Fortschrittsbalken angezeigt werden.&lt;br /&gt;
* Wettervorhersage: muss spätestens am Vorabend des vorhergesagten Tages beendet sein&lt;br /&gt;
&lt;br /&gt;
===Laufzeitvergleich===&lt;br /&gt;
&lt;br /&gt;
Da die Laufzeit für den Benutzer ein so wichtiges Kriterium ist, werden häufig Laufzeitvergleiche durchgeführt. Deren Ergebnisse hängen allerdings von vielen Faktoren ab, die möglicherweise nicht kontrollierbar sind:&lt;br /&gt;
* Geschwindigkeit und Anzahl der Prozessoren&lt;br /&gt;
* Auslastung des Systems&lt;br /&gt;
* Größe des Hauptspeichers  und Cache, Geschwindigkeit des  Datenbus&lt;br /&gt;
* Qualität des Compilers/Optimierers (ist der Compiler für die spezielle Prozessor-Architektur optimiert?)&lt;br /&gt;
* Geschick des Programmierers&lt;br /&gt;
* Daten (Beispiel Quicksort: Best case und worst case [vorsortierter Input] stark unterschiedlich)&lt;br /&gt;
All diese Faktoren sind untereinander abhängig. Laufzeitvergleiche sind daher mit Vorsicht zu interpretieren.&lt;br /&gt;
Generell sollten bei Vergleichen möglichst wenige Parameter verändert werden, z.B.&lt;br /&gt;
* gleiches Programm (gleiche Kompilierung), gleiche Daten, andere Prozessoren&lt;br /&gt;
oder&lt;br /&gt;
* gleiche CPU, Daten, andere Programme (Vergleich von Algorithmen)&lt;br /&gt;
Zur Verbesserung der Vergleichbarkeit gibt es standardisierte [http://en.wikipedia.org/wiki/Benchmark_(computing) Benchmarks], die bestimmte Aspekte eines Systems unter möglichst realitätsnahen Bedingungen testen. Generell gilt aber: Durch Laufzeitmessung ist schwer festzustellen, ob ein Algorithmus ''prinzipiell'' besser ist als ein anderer. Dafür ist die Analyse der [[Effizienz#Algorithmen-Komplexität|Algorithmenkomplexität]] notwendig.&lt;br /&gt;
&lt;br /&gt;
===Optimierung der Laufzeit===&lt;br /&gt;
&lt;br /&gt;
Wenn sich herausstellt, dass ein bereits implementierter Algorithmus zu langsam läuft, geht man wie folgt vor:&lt;br /&gt;
&lt;br /&gt;
# Man verwendet einen [http://en.wikipedia.org/wiki/Performance_analysis Profiler], um zunächst den Flaschenhals zu bestimmen. Ein Profiler ist ein Hilfsprogramm, das während der Ausführung eines Programms misst, wieviel Zeit in jeder Funktion und Unterfunktion verbraucht wird. Dadurch kann man herausfinden, welcher Teil des Algorithmus überhaupt Probleme bereitet. Donald Knuth gibt z.B. als Erfahrungswert an, dass Programme während des größten Teils ihrer Laufzeit nur 3% des Quellcodes (natürlich mehrmals wiederholt) ausführen [http://pplab.snu.ac.kr/courses/adv_pl05/papers/p261-knuth.pdf]. Es ist sehr wichtig, diese 3% experimentell zu bestimmen, weil die Erfahrung zeigt, dass man beim Erraten der kritischen Programmteile oft falsch liegt. Man spricht dann von &amp;quot;[http://en.wikipedia.org/wiki/Optimization_%28computer_science%29#When_to_optimize premature optimization]&amp;quot;, also von voreiliger Optimierung ohne experimentelle Untersuchung der wirklichen Laufzeiten, was laut Knuth &amp;quot;the root of all evil&amp;quot; ist. Der Python-Profiler wird in [http://docs.python.org/lib/profile.html Kapitel 25] der Python-Dokumentation beschrieben.&lt;br /&gt;
# Man kann dann versuchen, die kritischen Programmteile zu optimieren.&lt;br /&gt;
# Falls der Laufzeitgewinn durch Optimierung zu gering ist, muss man einen prinzipiell schnelleren Algorithmus verwenden, falls es einen gibt.&lt;br /&gt;
&lt;br /&gt;
Einige wichtige Techniken der Programmoptimierung sollen hier erwähnt werden. Wenn man einen optimierenden Compiler verwendet, werden einige Optimierungen automatisch ausgeführt [http://en.wikipedia.org/wiki/Compiler_optimization]. In Python trifft dies jedoch nicht zu. Um den Sinn einiger Optimierungen zu verstehen, benötigt man Grundkenntnisse der Computerarchitektur.&lt;br /&gt;
&lt;br /&gt;
;Elimination von redundantem Code: Es ist offensichtlich überflüssig, dasselbe Ergebnis mehrmals zu berechnen, wenn es auch zwischengespeichert werden könnte. Diese Optimierung wird von vielen automatischen Optimierern unterstützt und kommt im wesentlichen in zwei Ausprägungen vor:&lt;br /&gt;
:; common subexpression elimination: In mathematischen Ausdrücken wird ein Teilergebnis häufig mehrmals benötigt. Man betrachte z.B. die Lösung der quadratischen Gleichung &amp;lt;math&amp;gt;x^2+p\,x+q = 0&amp;lt;/math&amp;gt;:&lt;br /&gt;
        x1 = - p / 2.0 + sqrt(p*p/4.0 - q)&lt;br /&gt;
        x2 = - p / 2.0 - sqrt(p*p/4.0 - q)&lt;br /&gt;
::Die mehrmalige Berechnung von Teilausdrücken wird vermieden, wenn man stattdessen schreibt:&lt;br /&gt;
        p2 = - p / 2.0&lt;br /&gt;
        r  = sqrt(p2*p2 - q)&lt;br /&gt;
        x1 = p2 + r&lt;br /&gt;
        x2 = p2 - r&lt;br /&gt;
:; loop invariant elimination: Wenn ein Teilausdruck sich in einer Schleife nicht ändert, muss man ihn nicht bei jedem Schleifendurchlauf neu berechnen, sondern kann dies einmal vor Beginn der Schleife tun. Ein typisches Beispiel hierfür ist die Adressierung von Matrizen, die als 1-dimensionales Array gespeichert sind. Angenommen, wir speichern eine NxN Matrix &amp;lt;tt&amp;gt;m&amp;lt;/tt&amp;gt; in einem Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; der Größe N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt;, so dass das Matrixelement &amp;lt;tt&amp;gt;m&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt;&amp;lt;/tt&amp;gt; durch &amp;lt;tt&amp;gt;a[i + j*N]&amp;lt;/tt&amp;gt; indexiert wird. Wir betrachten die Aufgabe, eine Einheitsmatrix zu initialisieren. Ein nicht optimierter Algorithmus dafür lautet:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + j*N] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + j*N] = 0.0&lt;br /&gt;
::Der Ausdruck &amp;lt;tt&amp;gt;j*N&amp;lt;/tt&amp;gt; wird hier in jedem Schleifendurchlauf erneut berechnet, obwohl sich &amp;lt;tt&amp;gt;j&amp;lt;/tt&amp;gt; in der inneren Schleife gar nicht verändert. Man kann deshalb optimieren zu:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + jN] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + jN] = 0.0&lt;br /&gt;
;Vereinfachung der inneren Schleife: Generell sollte man sich bei der Optimierung auf die innere Schleife eines Algorithmus konzentrieren, weil dieser Code am häufigsten ausgeführt wird. Insbesondere sollte man die Anzahl der Befehle in der inneren Schleife so gering wie möglich halten und teure Befehle vermeiden. Früher waren vor allem Floating-Point Befehle teuer, die man oft durch die schnellere Integer-Arithmetik ersetzt hat, falls dies algorithmisch möglich war (diesen Rat findet man noch oft in der Literatur). Heute hat sich die Hardware so verbessert, dass im Allgemeinen nur noch die Floating-Point Division deutlich langsamer ist als die anderen Operatoren. Im obigen Beispiel der quadratischen Gleichung ist es daher sinnvoll, den Ausdruck &lt;br /&gt;
        p2 = -p / 2.0&lt;br /&gt;
:durch&lt;br /&gt;
        p2 = -0.5 * p&lt;br /&gt;
:zu ersetzen. Dadurch ersetzt man eine Division durch eine Multiplikation und spart außerdem das Negieren von &amp;lt;tt&amp;gt;p&amp;lt;/tt&amp;gt;, da der Compiler direkt mit &amp;lt;tt&amp;gt;-0.5&amp;lt;/tt&amp;gt; multipliziert.&lt;br /&gt;
;Ausnutzung der Prozessor-Pipeline: Moderne Prozessoren führen mehrere Befehle parallel aus. Dies ist möglich, weil jeder Befehl in mehrere Teilschritte zerlegt werden kann. Eine generische Unterteilung in vier Teilschritte ist z.B.:&lt;br /&gt;
:# Dekodieren des nächsten Befehls&lt;br /&gt;
:# Beschaffen der Daten, die der Befehl verwendet (aus Prozessorregistern, dem Cache, oder dem Hauptspeicher)&lt;br /&gt;
:# Ausführen des Befehls&lt;br /&gt;
:# Schreiben der Ergebnisse&lt;br /&gt;
:Man bezeichnet dies als die &amp;quot;[http://en.wikipedia.org/wiki/Instruction_pipeline instruction pipeline]&amp;quot; des Prozessors (heutige Prozessoren verwenden wesentlich feinere Unterteilungen). Prozessoren werden nun so gebaut, dass mehrere Befehle parallel, auf verschiedenen Ausführungsstufen ausgeführt werden. Wenn Befehl 1 also beim Schreiben der Ergebnisse angelangt ist, kann Befehl 2 die Hardware zum Ausführen des Befehls benutzen, während Befehl 3 seine Daten holt, und Befehl 4 soeben dekodiert wird. Unter bestimmten Bedingungen funktioniert diese Parallelverarbeitung jedoch nicht. Dies gibt Anlass zu Optimierungen:&lt;br /&gt;
:;Vermeiden unnötiger Typkonvertierungen: Der Prozessor verarbeitet Interger- und Floating-Point-Befehle in verschiedenen Pipelines, weil die Hardwareanforderungen sehr verschieden sind. Wird jetzt ein Ergebnis von Integer nach Floating-Point umgewandelt oder umgekehrt, muss die jeweils andere Pipeline warten, bis die erste Pipeline ihre Berechnung beendet. Es kann dann besser sein, Berechnungen in Floating-Point zu Ende zu führen, auch wenn sie semantisch eigentlich Integer-Berechnungen sind.&lt;br /&gt;
:;Reduzierung der Anzahl von Verzweigungen: Wenn der Code verzweigt (z.B. durch eine &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;- oder  &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Anweisung), ist nicht klar, welcher Befehl nach der Verzweigung ausgeführt werden soll, bevor Stufe 3 der Pipeline die Verzweigungsbedingung ausgewertet hat. Bis dahin wären die ersten beiden Stufen der Pipeline unbenutzt. Moderne Prozessoren benutzen zwar ausgefeilte Heuristiken, um das Ergebnis der Bedingung vorherzusagen, und führen den hoffentlich richtigen Zweig des Codes spekulativ aus, aber dies funktioniert nicht immer. Man sollte deshalb generell die Anzahl der Verzweigungen minimieren. Als Nebeneffekt führt dies meist auch zu besser lesbarem, verständlicherem Code. Im Matrixbeispiel kann man&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + jN] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + jN] = 0.0&lt;br /&gt;
::durch&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               a[i + jN] = 0.0&lt;br /&gt;
           a[j + jN] = 1.0&lt;br /&gt;
ersetzen. Die Diagonalelemente &amp;lt;tt&amp;gt;a[j + jN]&amp;lt;/tt&amp;gt; werden jetzt zwar zweimal initialisiert (in der Schleife auf Null, dann auf Eins), aber durch Elimination der &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage wird dies wahrscheinlich mehr als ausgeglichen, zumal dadurch die innere Schleife wesentlich vereinfacht wurde.&lt;br /&gt;
;Ausnutzen des Prozessor-Cache: Zugriffe auf den Hauptspeicher sind sehr langsam. Deshalb werden stets ganze Speicherseiten auf einmal in den [http://en.wikipedia.org/wiki/Cache Cache] des Prozessors geladen. Wenn unmittelbar nacheinander benutzte Daten auch im Speicher nahe beieinander liegen (sogenannte &amp;quot;[http://en.wikipedia.org/wiki/Locality_of_reference locality of reference]&amp;quot;), ist die Wahrscheinlichkeit groß, dass die als nächstes benötigten Daten bereits im Cache sind und damit schnell gelesen werden können. Bei vielen Algorithmen kann man die Implementation so umordnen, dass die locality of reference verbessert wird, was zu einer drastischen Beschleunigung führt. Im Matrix-Beispiel ist z.B. die Reihenfolge der Schleifen wichtig. Für konstanten Index &amp;lt;tt&amp;gt;j&amp;lt;/tt&amp;gt; liegen die Indizes &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; im Speicher hintereinander. Deshalb ist es günstig, in der inneren Schleife über &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu iterieren:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               a[i + jN] = 0.0&lt;br /&gt;
           a[j + jN] = 1.0&lt;br /&gt;
:Die umgekehrte Reihenfolge der Schleifen ist hingegen ungünstig&lt;br /&gt;
       for i in range(N):&lt;br /&gt;
           for j in range(N):&lt;br /&gt;
               a[i + j*N] = 0.0&lt;br /&gt;
           a[i + i*N] = 1.0&lt;br /&gt;
:Jetzt werden in der inneren Schleife stets N Datenelemente übersprungen. Besonders bei großem N muss man daher häufig den Cache neu füllen, was bei der ersten Implementation nicht notwendig war.  (Außerdem verliert man hier die Optimierung &amp;lt;tt&amp;gt;jN = j*N&amp;lt;/tt&amp;gt;, die jetzt nicht mehr möglich ist.)&lt;br /&gt;
&lt;br /&gt;
Als Faustregel kann man durch Optimierung eine Verdoppelung der Geschwindigkeit erreichen (in Ausnahmefällen auch mehr). Benötigt man stärkere Verbesserungen, muss man wohl oder übel einen besseren Algorithmus oder einen schnelleren Computer verwenden.&lt;br /&gt;
&lt;br /&gt;
== Algorithmen-Komplexität ==&lt;br /&gt;
&lt;br /&gt;
Komplexitätsbetrachtungen ermöglichen den Vergleich der prinzipiellen Eigenschaften von Algorithmen unabhängig von einer Implementation, Umgebung etc.&lt;br /&gt;
      &lt;br /&gt;
Eine einfache Möglichkeit ist das Zählen der Aufrufe einer Schlüsseloperation. Beispiel Sortieren:&lt;br /&gt;
* Anzahl der Vergleiche&lt;br /&gt;
* Anzahl der Vertauschungen&lt;br /&gt;
&lt;br /&gt;
=== Beispiel: Selection Sort ===&lt;br /&gt;
&lt;br /&gt;
  for i in range(len(a)-1):&lt;br /&gt;
    max = i&lt;br /&gt;
    for j in range(i+1, len(a)):&lt;br /&gt;
      if a[j] &amp;lt; a[max]:&lt;br /&gt;
        max = j&lt;br /&gt;
    a[max], a[i] = a[i], a[max]      # swap&lt;br /&gt;
&lt;br /&gt;
*Anzahl der Vergleiche: Ein Vergleich in jedem Durchlauf der inneren Schleife. Es ergibt sich folgende Komplexität:&lt;br /&gt;
*:Ingesamt &amp;lt;math&amp;gt;\sum_{i=0}^{N-2} \sum_{j=i+1}^{N-1}1 = \frac{N}{2} (N-1) \!&amp;lt;/math&amp;gt; Vergleiche.&lt;br /&gt;
&lt;br /&gt;
*Anzahl der Vertauschungen (swaps): Eine Vertauschung pro Durchlauf der äußeren Schleife:&lt;br /&gt;
*:Insgesamt &amp;lt;math&amp;gt;N-1 \!&amp;lt;/math&amp;gt; Vertauschungen&lt;br /&gt;
&lt;br /&gt;
Die Komplexität wird durch die Operationen bestimmt, die am häufigsten ausgeführt werden, hier also die Anzahl der Vergleiche. Die Anzahl der Vertauschungen ist hingegen kein geeignetes Kriterium für die Komplexität von selection sort, weil der Aufwand in der inneren Schleife ignoriert würde.&lt;br /&gt;
&lt;br /&gt;
=== Fallunterscheidung: Worst und Average Case ===&lt;br /&gt;
&lt;br /&gt;
Die Komplexität ist in der Regel eine Funktion der Eingabegröße (Anzahl der Eingabebits, Anzahl der Eingabeelemente). Sie kann aber auch von der Art der Daten abhängen, nicht nur von der Menge, z.B. vorsortierte Daten bei Quicksort. Um von der Art der Daten unabhängig zu werden, kann man zwei Fälle der Komplexität unterscheiden:&lt;br /&gt;
      &lt;br /&gt;
* Komplexität im ungünstigsten Fall &lt;br /&gt;
*: Der ungünstigste Fall ist die Eingabe gegebener Länge, für die der Algorithmus am langsamsten ist. Der Nachteil dieser Methode besteht darin, dass dieser ungünstige Fall in der Praxis vielleicht gar nicht oder nur selten vorkommt, so dass sich der Algorithmus in Wirklichkeit besser verhält als man nach dieser Analyse erwarten würde. Beim Quicksort-Algorithmus mit zufälliger Wahl des Pivot-Elements müsste z.B. stets das kleinste oder größte Element des aktuellen Intervalls als Pivot-Element gewählt werden, was äußerst unwahrscheinlich ist.&lt;br /&gt;
* Komplexität im durchschnittlichen/typischen Fall&lt;br /&gt;
*: Der typische Fall ist die mittlere Komplexität des Algorithmus über alle möglichen Eingaben. Dazu muss man die Wahrscheinlichkeit jeder möglichen Eingabe kennen, und berechnet dann die mittlere Laufzeit über dieser Wahrscheinlichkeitsverteilung. Leider ist die Wahrscheinlichkeit der Eingaben oft nicht bekannt, so dass man geeignete Annahmen treffen muss. Bei Sortieralgorithmen können z.B. alle möglichen Permutationen des Eingabearrays als gleich wahrscheinlich angenommen werden, und der typische Fall ist dann die mittlere Komplexität über alle diese Eingaben. Oft hat man jedoch in der Praxis andere Wahrscheinlichkeitsverteilungen, z.B. sind die Daten oft &amp;quot;fast sortiert&amp;quot; (nur wenige Elemente sind an der falschen Stelle). Dann verhält sich der Algorithmus ebenfalls anders als vorhergesagt.&lt;br /&gt;
&lt;br /&gt;
Wir beschränken uns in dieser Vorlesung auf die Komplexität im ungünstigseten Fall. '''Exakte''' Formeln für Komplexität sind aber auch dann schwer zu gewinnen, wie das folgende Beispiel zeigt:&lt;br /&gt;
&lt;br /&gt;
=== Beispiele aus den Übungen (Gemessene Laufzeiten für Mergesort/Selectionsort) ===&lt;br /&gt;
&lt;br /&gt;
* Mergesort: &amp;lt;math&amp;gt;\frac{0,977N\log N}{\log 2} + 0,267N-4.39 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: andere Lösung: &amp;lt;math&amp;gt;1140 N\log(N) - 1819N + 6413 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* Selectionsort: &amp;lt;math&amp;gt;\frac{1}{2}N^2 - \frac{1}{2N} - 10^{-12} \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: andere Lösung: &amp;lt;math&amp;gt;1275N^2 - 116003^N + 11111144 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Aus diesen Formeln wird nicht offensichtlich, welcher Algorithmus besser ist.&lt;br /&gt;
Näherung: Betrachte nur '''sehr große Eingaben''' (meist sind alle Algorithmen schnell genug für kleine Eingaben). Dieses Vorgehen wird als '''Asymptotische Komplexität''' bezeichnet (N gegen unendlich).&lt;br /&gt;
&lt;br /&gt;
=== Asymptotische Komplexität am Beispiel Polynom ===&lt;br /&gt;
&lt;br /&gt;
Polynom: &amp;lt;math&amp;gt;a\,x^2+b\,x+c=p\!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;x \!&amp;lt;/math&amp;gt; sei die Eingabegröße, und wir betrachten die Entwicklung von &amp;lt;math&amp;gt;p \!&amp;lt;/math&amp;gt; in Abhängigkeit von &amp;lt;math&amp;gt;x \!&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;math&amp;gt;x=0 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=c \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x=1 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=a+b+c \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x=1000 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=1000000a+1000b+c \approx 1000000a\!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x \to \infty \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p \approx x^2a\!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für sehr große Eingaben verlieren also ''b'' und ''c'' immer mehr an Bedeutung, so dass am Ende nur noch ''a'' für die Komplexitätsbetrachtung wichtig ist.&lt;br /&gt;
&lt;br /&gt;
== Landau-Symbole ==&lt;br /&gt;
&lt;br /&gt;
Um die asymptotische Komplexität verschiedener Algorithmen miteinander vergleichen zu können, verwendet man die sogenannten [http://de.wikipedia.org/wiki/Landau-Symbole Landau-Symbole]. Das wichtigste Landau-Symbol ist &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;, mit dem man eine ''obere Schranke'' &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; für die Komplexität angeben kann. &lt;br /&gt;
&lt;br /&gt;
Schreibt man &amp;lt;math&amp;gt;f \in \Omega(g)&amp;lt;/math&amp;gt;, so stellt dies eine asymptotische ''untere Schranke'' für die Funktion f dar.&lt;br /&gt;
&lt;br /&gt;
Schließlich bedeutet &amp;lt;math&amp;gt;f \in \Theta(g)&amp;lt;/math&amp;gt;, dass die Funktion f genauso schnell wie die Funktion g wächst, das heißt man hat eine asymptotisch ''scharfe Schranke'' für f. Hierzu muss sowohl &amp;lt;math&amp;gt;f\in\mathcal{O}(g)&amp;lt;/math&amp;gt; als auch &amp;lt;math&amp;gt;f \in \Omega(g)&amp;lt;/math&amp;gt; erfüllt sein. &lt;br /&gt;
&lt;br /&gt;
Im nun folgenden soll auf die verschiedenen Landau-Symbole noch näher eingegeangen werden.&lt;br /&gt;
&lt;br /&gt;
===O-Notation===&lt;br /&gt;
&lt;br /&gt;
Intuitiv gilt: Für große N dominieren die am schnellsten wachsenden Terme einer Funktion. Die Notation &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; (sprich &amp;quot;f ist in O von g&amp;quot; oder &amp;quot;f ist von derselben Größenordnung wie g&amp;quot;) formalisiert eine solche Abschätzung der asymptotischen Komplexität der Funktion f von oben. &lt;br /&gt;
; Asymptotische Komplexität: Für zwei Funktionen f(x) und g(x) gilt&lt;br /&gt;
::&amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x))&amp;lt;/math&amp;gt;&lt;br /&gt;
: genau dann wenn es eine Konstante &amp;lt;math&amp;gt;c&amp;gt;0&amp;lt;/math&amp;gt; und ein Argument &amp;lt;math&amp;gt;x_0&amp;lt;/math&amp;gt; gibt, so dass &lt;br /&gt;
::&amp;lt;math&amp;gt;\forall x \ge x_0:\quad f(x) \le c\,g(x)&amp;lt;/math&amp;gt;.&lt;br /&gt;
:Die Menge &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; aller durch g(x) abschätzbaren Funktionen ist also formal definiert durch&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(g(x)) = \{ f(x)\ |\ \exists c&amp;gt;0: \forall x \ge x_0: 0 \le f(x) \le c\,g(x)\}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Idee hinter dieser Definition ist, dass g(x) eine wesentlich einfachere Funktion ist als f(x), die sich aber nach geeigneter Skalierung (Multiplikation mit c) und für große Argumente x im wesentlichen genauso wie f(x) verhält. Man kann deshalb in der Algorithmenanalyse f(x) durch g(x) ersetzen. &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x))&amp;lt;/math&amp;gt; spielt für Funktionen eine ähnliche Rolle wie der Operator &amp;amp;le; für Zahlen: Falls a &amp;amp;le; b gilt, kann bei einer Abschätzung von oben ebenfalls a durch b ersetzt werden.&lt;br /&gt;
&lt;br /&gt;
==== Ein einfaches Beispiel ====&lt;br /&gt;
&lt;br /&gt;
[[Image:Sqsqrt.png]]&lt;br /&gt;
&lt;br /&gt;
Rot = &amp;lt;math&amp;gt;x^2 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
Blau = &amp;lt;math&amp;gt;\sqrt{x} \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\sqrt{x} \in \mathcal{O}(x^2)\!&amp;lt;/math&amp;gt; weil &amp;lt;math&amp;gt;\sqrt{x} \le c\,x^2\!&amp;lt;/math&amp;gt; für alle &amp;lt;math&amp;gt;x \ge x_0 = 1 \!&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c = 1\!&amp;lt;/math&amp;gt;, oder auch für &amp;lt;math&amp;gt;x \ge x_0 = 4 \!&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c = 1/16&amp;lt;/math&amp;gt; (die Wahl von c und x&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; in der Definition von O(.) ist beliebig, solange die Bedingungen erfüllt sind).&lt;br /&gt;
&lt;br /&gt;
==== Komplexität bei kleinen Eingaben ==== &lt;br /&gt;
&lt;br /&gt;
Algorithmus 1: &amp;lt;math&amp;gt;\mathcal{O}(N^2) \!&amp;lt;/math&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Algorithmus 2: &amp;lt;math&amp;gt;\mathcal{O}(N\log{N}) \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus 2 ist schneller (von geringerer Komplexität) für große Eingaben, aber bei kleinen Eingaben (insbesondere, wenn der Algorithmus in einer Schleife immer wieder mit kleinen Eingaben aufgerufen wird) könnte Algorithmus 1 schneller sein, falls der in der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation verborgene konstante Faktor ''c'' bei Algorithmus 2 einen wesentlich größeren Wert hat als bei Algorithmus 1.&lt;br /&gt;
&lt;br /&gt;
==== Eigenschaften der O-Notation (Rechenregeln) ==== &lt;br /&gt;
&lt;br /&gt;
# Transitiv:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x)) \land g(x) \in \mathcal{O}(h(x)) \to f(x) \in \mathcal{O}(h(x)) \!&amp;lt;/math&amp;gt;           &lt;br /&gt;
# Additiv:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(h(x)) \land g(x) \in \mathcal{O}(h(x)) \to f(x) + g(x) \in \mathcal{O}(h(x)) \!&amp;lt;/math&amp;gt;           &lt;br /&gt;
# Für Monome gilt:&lt;br /&gt;
#: &amp;lt;math&amp;gt;x^k \in \mathcal{O}(x^k)&amp;lt;/math&amp;gt; und&lt;br /&gt;
#: &amp;lt;math&amp;gt;x^k \in \mathcal{O}(x^{k+j}), \forall j \ge 0 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Multiplikation mit einer Konstanten:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x)) \to c\,f(x) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: andere Schreibweise:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) = c\,g(x) \to f(x) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Folgerung aus 3. und 4. für Polynome: &lt;br /&gt;
#: &amp;lt;math&amp;gt;a_0+a_1\,x + ... + a_n\,x^n \in \mathcal{O}(x^n)\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: Beispiel: &amp;lt;math&amp;gt;a\,x^2+b\,x+c \in \mathcal{O}(x^2)\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Logarithmus:&lt;br /&gt;
#: &amp;lt;math&amp;gt;a, b &amp;gt; 1\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{b}{x})\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: Die Basis des Logarithmus spielt also keine Rolle.&lt;br /&gt;
#: Beweis hierfür:&lt;br /&gt;
#:: &amp;lt;math&amp;gt;\log_{a}{x} = \frac{\log_{b}{x}}{\log_{b}{a}}\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#:: Mit &amp;lt;math&amp;gt;c = 1 / \log_{b}{a}\,&amp;lt;/math&amp;gt; gilt: &amp;lt;math&amp;gt;\log_{a}{x} = c\,\log_{b}{x}\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#:: Wird hier die (zweite) Regel für Multiplikation mit einer Konstanten angewendet, fällt der konstante Faktor weg, also &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{b}{x})\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Insbesondere gilt auch &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{2}{x})\!&amp;lt;/math&amp;gt;, es kann also immer der 2er Logarithmus verwendet werden.&lt;br /&gt;
&lt;br /&gt;
==== O-Kalkül ==== &lt;br /&gt;
&lt;br /&gt;
Das O-Kalkül definiert wichtige Vereinfachungsregeln for Ausdrücke in O-Notation (Beweise: siehe Übungsaufgabe):&lt;br /&gt;
&lt;br /&gt;
# &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(f(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# &amp;lt;math&amp;gt;\mathcal{O}(\mathcal{O}(f(x))) \in \mathcal{O}(f(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# &amp;lt;math&amp;gt;c\,\mathcal{O}(f(x)) \in \mathcal{O}(f(x))\,&amp;lt;/math&amp;gt; für jede Konstante ''c''&lt;br /&gt;
# &amp;lt;math&amp;gt;\mathcal{O}(f(x))+c \in \mathcal{O}(f(x))\,&amp;lt;/math&amp;gt; für jede Konstante ''c''&lt;br /&gt;
# Sequenzregel:&lt;br /&gt;
#: Wenn zwei nacheinander ausgeführte Programmteile die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt; bzw. &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; haben, gilt für beide gemeinsam:&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(f(x))&amp;lt;/math&amp;gt; falls &amp;lt;math&amp;gt;g(x) &amp;lt; \mathcal{O}(f(x))&amp;lt;/math&amp;gt; bzw.&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt; falls &amp;lt;math&amp;gt;f(x) &amp;lt; \mathcal{O}(g(x))&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Informell schreibt man auch: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(max(f(x), g(x)))\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
# Schachtelungsregel bzw. Aufrufregel:&lt;br /&gt;
#: Wenn in einer geschachtelten Schleife die äußere Schleife die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt; hat, und die innere &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt;, gilt für beide gemeinsam:&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) * \mathcal{O}(g(x)) \in \mathcal{O}(f(x) * g(x))\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Gleiches gilt wenn eine Funktion &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt;-mal aufgerufen wird, und die Komplexität der Funktion selbst &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; ist.&lt;br /&gt;
&lt;br /&gt;
;Beispiel für 5.: Beide Schleifen haben die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Dies gilt auch für ihre Hintereinanderausführung:&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          a[i] = i&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          print a[i]&lt;br /&gt;
;Beispiele für 6.: Beide Schleifen haben die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Ihre Verschachtelung hat daher die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N^2)&amp;lt;/math&amp;gt;. &lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          for j in range(N):&lt;br /&gt;
              a[i*N + j] = i+j&lt;br /&gt;
: Dies gilt ebenso, wenn statt der inneren Schleife eine Funktion mit Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt; ausgeführt wird:&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          a[i] = foo(i, N)  # &amp;lt;math&amp;gt;\mathrm{foo}(i, N) \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== O-Kalkül auf das Beispiel des Selectionsort angewandt ==== &lt;br /&gt;
&lt;br /&gt;
Selectionsort: Wir hatten gezeigt dass &amp;lt;math&amp;gt;f(N) = \frac{N^2}{2} - \frac{N}{2}&amp;lt;/math&amp;gt;. Nach der Regel für Polynome vereinfacht sich dies zu &amp;lt;math&amp;gt;f(N) \in \mathcal{O}\left(\frac{N^2}{2}\right) = \mathcal{O}(N^2)\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Alternativ via Schachtelungsregel:&lt;br /&gt;
: Die äußere Schleife wird (''N''-1)-mal durchlaufen: &amp;lt;math&amp;gt;N-1 \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
: Die innere Schleife wird (''N-i''-1)-mal durchlaufen. Das sind im Mittel ''N''/2 Durchläufe: &amp;lt;math&amp;gt;N/2 \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
: Zusammen: &amp;lt;math&amp;gt;\mathcal{O}(N)*\mathcal{O}(N) \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nach beiden Vorgehensweisen kommen wir zur Schlussfolgerung, dass der Selectionsort die asymptotische Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N^2)\!&amp;lt;/math&amp;gt; besitzt.&lt;br /&gt;
&lt;br /&gt;
==== Zusammenhang zwischen Komplexität und Laufzeit ==== &lt;br /&gt;
&lt;br /&gt;
Wenn eine Operation 1ms dauert, erreichen Algorithmen verschiedener Komplexität folgende Leistungen (wobei angenommen wird, dass der in der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation verborgene konstante Faktor immer etwa gleich 1 ist):&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:left&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
|-&lt;br /&gt;
! Komplexität !! Operationen in 1s !! Operationen in 1min !! Operationen in 1h&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 1000 || 60.000 || 3.600.000&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N\log_2{N})&amp;lt;/math&amp;gt;&lt;br /&gt;
| 140 || 4895 || 204094&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 32 || 245 || 1898&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 10 || 39 || 153&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(2^N)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 10 || 16 || 21&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Exponentielle Komplexität ==== &lt;br /&gt;
Der letzte Fall &amp;lt;math&amp;gt;\mathcal{O}(2^N)&amp;lt;/math&amp;gt; ist von exponentieller Komplexität. Das bedeutet, dass eine Verdopplung des Aufwands nur bewirkt, dass die maximale Problemgröße um eine Konstante wächst. Algorithmen mit exponentieller (oder noch höherer) Komplexität werden deshalb als '''ineffizient''' bezeichnet. Algorithmen mit höchstens polynomieller Komplexität gelten hingegen als effizient.&lt;br /&gt;
&lt;br /&gt;
In der Praxis sind allerdings auch polynomielle Algorithmen mit hohem Exponenten meist zu langsam. Als Faustregel kann man eine praktische Grenze von &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt; ansehen. Bei einer Komplexität von &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt; bewirkt ein verdoppelter Aufwand immer noch eine Steigerung der maximalen Problemgröße um den Faktor &amp;lt;math&amp;gt;\sqrt[3]{2}&amp;lt;/math&amp;gt; (also eine ''multiplikative'' Vergrößerung um ca. 25%, statt nur einer additiven Vergrößerung wie bei exponentieller Komplexität).&lt;br /&gt;
&lt;br /&gt;
===&amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;- Notation===&lt;br /&gt;
&lt;br /&gt;
Genauso wie &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; eine Art &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;-Operator für Funktionen ist, definiert &amp;lt;math&amp;gt;f \in \Omega(g) &amp;lt;/math&amp;gt; eine Abschätzung von unten, analog zum &amp;lt;math&amp;gt;\ge&amp;lt;/math&amp;gt;-Operator für Zahlen. Formal kann man &amp;lt;math&amp;gt;f(N) \in \Omega(g(N)) &amp;lt;/math&amp;gt; genau dann schreiben, falls es eine Konstante &amp;lt;math&amp;gt; c &amp;gt; 0 &amp;lt;/math&amp;gt; gibt, so dass &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; f(N) \ge c \cdot g(N) &amp;lt;/math&amp;gt; für &amp;lt;math&amp;gt; N  \ge N_0 &amp;lt;/math&amp;gt; &lt;br /&gt;
&lt;br /&gt;
gilt.&lt;br /&gt;
Man verwendet diese Notation also um abzuschätzen, wie groß der Aufwand (die Komplexität) für einen bestimmten Algorithmus ''mindestens'' ist und nicht ''höchstens'', was man mit der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt; - Notation ausdrücken würde.&lt;br /&gt;
&lt;br /&gt;
Ein praktisches Beispiel für eine Anwendung der &amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;- Notation wäre die Fragestellung, ob es ''prinzipiell'' einen besseren Algorithmus für ein bestimmtes Problem gibt. Wie später im Abschnitt [[Suchen#Sortieren_als_Suchproblem|Sortieren als Suchproblem]] gezeigt wird, ist das Sortieren eines Arrays durch paarweise Vergleiche von Elementen immer mindestens von der Komplexität &amp;lt;math&amp;gt; \Omega(N\cdot \ln N) &amp;lt;/math&amp;gt;, was konkret bedeutet, dass kein Sortieralgorithmus, der nach diesem Prinzip arbeitet, jemals eine geringere Komplexität als beispielsweise Merge-Sort haben wird. Natürlich kann man den entsprechenden Sortieralgorithmus, also Merge-Sort zum Beispiel, unter Umständen noch optimieren, aber die Komplexität wird erhalten bleiben. Mit diesem Wissen kann man sich viel (vergebliche) Arbeit sparen.&lt;br /&gt;
&lt;br /&gt;
===&amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;- Notation===&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;math&amp;gt;f(N) \in \Theta(g(N))&amp;lt;/math&amp;gt; ist eine scharfe Abschätzung der asymptotischen Komplexität einer Funktion f. &lt;br /&gt;
&lt;br /&gt;
Damit dies gilt, muss &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; und ''gleichzeitig'' &amp;lt;math&amp;gt;f(N) \in \Omega(g(N))&amp;lt;/math&amp;gt; erfüllt sein.&lt;br /&gt;
&lt;br /&gt;
Dies ist natürlich auch die beste Abschätzung der asymptotischen Komplexität einer Funktion f. Formal bedeutet &amp;lt;math&amp;gt;f(N) \in \Theta(g(N))&amp;lt;/math&amp;gt; dass es zwei Konstanten &amp;lt;math&amp;gt; c_1 &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; c_2 &amp;lt;/math&amp;gt;, beide größer als Null, gibt, so dass für alle &amp;lt;math&amp;gt; N \geq N_0 &amp;lt;/math&amp;gt; gilt: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; c_1 \cdot g(N) \leq f(N) \leq c_2 \cdot g(N) &amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In der Praxis wird manchmal statt der &amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;-Notation auch dann die &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation benutzt, wenn eine scharfe Schranke ausgedrückt werden soll. Dies ist zwar formal nicht korrekt, aber man kann die intendierte Bedeutung meist aus dem Kontext erschließen.&lt;br /&gt;
&lt;br /&gt;
== Komplexitätsvergleich zweier Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
In diesem Abschnitt wollen wir der Frage nachgehen, wie ein formaler Beweis für die Behauptung &amp;lt;math&amp;gt; f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; geschehen kann. Hierbei werden zwei Beweismethoden vorgestellt werden, und zwar der '''Beweis über die Definition der Komplexität''' sowie der '''Beweis durch Dividieren'''.&lt;br /&gt;
&lt;br /&gt;
===Beweis über die Definition der asymptotischen Komplexität===&lt;br /&gt;
&lt;br /&gt;
Die Definition der asymptotischen Komplexität &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; war: &lt;br /&gt;
&lt;br /&gt;
Es gibt eine Konstante &amp;lt;math&amp;gt; c &amp;gt; 0 &amp;lt;/math&amp;gt;, so dass &amp;lt;math&amp;gt; f(N) \le c \cdot g(N) &amp;lt;/math&amp;gt; für &amp;lt;math&amp;gt; N  \ge N_0 &amp;lt;/math&amp;gt; erfüllt ist. &lt;br /&gt;
&lt;br /&gt;
Um also die die asymptotische Komplexität &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; zu beweisen, muss man die oben erwähnten Konstanten c und &amp;lt;math&amp;gt; N_0 &amp;lt;/math&amp;gt; finden, so dass &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; f(N) \leq c \cdot g(N) &amp;lt;/math&amp;gt; für alle &amp;lt;math&amp;gt; N \ge N_0 &amp;lt;/math&amp;gt; erfüllt ist. &lt;br /&gt;
&lt;br /&gt;
Dies geschieht zweckmäßigerweise mit dem Beweisprinzip der ''vollständigen Induktion''. Hierbei ist zu zeigen, dass&lt;br /&gt;
# &amp;lt;math&amp;gt; f(N_0) \leq g(N_0) &amp;lt;/math&amp;gt; für die eine zu bestimmende Konstante &amp;lt;math&amp;gt; N_0 &amp;lt;/math&amp;gt; gilt (''Induktionsanfang'') und &lt;br /&gt;
# falls &amp;lt;math&amp;gt; f(N) \leq g(N) &amp;lt;/math&amp;gt;, dann auch &amp;lt;math&amp;gt; f(N+1) \leq g(N+1) &amp;lt;/math&amp;gt; (''Induktionsschritt'') gilt.&lt;br /&gt;
&lt;br /&gt;
===Beweis durch Dividieren===&lt;br /&gt;
&lt;br /&gt;
Hierbei wählt man eine Konstante c und zeigt, dass &amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f(N)}{c \cdot g(N)} \leq 1 &amp;lt;/math&amp;gt; gilt (für die O-Notation, bei &amp;amp;Omega;-Notation gilt entsprechend &amp;lt;math&amp;gt;\geq 1 &amp;lt;/math&amp;gt;). Man kann dies auch als alternative Definition der Komplexität verwenden.&lt;br /&gt;
&lt;br /&gt;
Als Beispiel betrachten wir die beiden Funktionen &amp;lt;math&amp;gt; f(N) = N \,\lg N &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; g(N) = N^2 &amp;lt;/math&amp;gt; und wollen zeigen, dass &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; gilt. &lt;br /&gt;
&lt;br /&gt;
Als Konstante c wählen wir &amp;lt;math&amp;gt; c = 1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f(N)}{g(N)} = \lim_{N \rightarrow \infty} \frac{\lg N}{N} = \frac{\infty}{\infty} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Unbestimmte Ausdrücke der Form &lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{x \rightarrow x_0} \frac{f(x)}{g(x)} &amp;lt;/math&amp;gt;,&lt;br /&gt;
in denen sowohl &amp;lt;math&amp;gt; f(x) &amp;lt;/math&amp;gt; als auch &amp;lt;math&amp;gt; g(x) &amp;lt;/math&amp;gt; mit &amp;lt;math&amp;gt; x \rightarrow x_0 &amp;lt;/math&amp;gt; gegen Null oder gegen Unendlich streben, kann man manchmal mit den Regeln von [http://de.wikipedia.org/wiki/L%27Hospital%27sche_Regel ''l'Hospital''] berechnen. Danach darf man die Funktionen f und g zur Berechnung des unbestimmten Ausdrucks durch ihre k-ten Ableitungen ersetzen:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{x \rightarrow x_0} \frac{f(x)}{g(x)} = \lim_{x \rightarrow x_0} \frac{f^{(k)}(x)}{g^{(k)}(x)} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In unserem Fall verwenden wir die erste Ableitung und erhalten:&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f'(x)}{g'(x)} = \lim_{N \rightarrow \infty} \frac{1/N}{1} \rightarrow 0 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Damit wurde &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt;, also &amp;lt;math&amp;gt;N \lg N \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt; gezeigt.&lt;br /&gt;
&lt;br /&gt;
Man beachte hierbei, dass &amp;lt;math&amp;gt;N \lg N \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt; keine enge Grenze für die Komplexität von &amp;lt;math&amp;gt;N \,\lg N&amp;lt;/math&amp;gt; darstellt, da der Grenzwert &amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f'(x)}{g'(x)}\, &amp;lt;/math&amp;gt; gegen 0 und nicht gegen eine von Null verschiedene Konstante strebt. In diesem Fall haben wir die Komplexität von &amp;lt;math&amp;gt;N \cdot \lg N &amp;lt;/math&amp;gt; also nur nach oben abschätzen können.&lt;br /&gt;
&lt;br /&gt;
===Beispiel für den Komplexitätsvergleich: Gleitender Mittelwert (Running Average)===&lt;br /&gt;
&lt;br /&gt;
Wir berechnen für ein gegebenes Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; einen gleitenden Mittelwert über &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; Elemente:&amp;lt;br/&amp;gt;&lt;br /&gt;
::&amp;lt;math&amp;gt;r_i = \frac{1}{k} \sum_{j=i-k+1}^i a_j&amp;lt;/math&amp;gt; &amp;lt;br/&amp;gt;&lt;br /&gt;
Das heisst, für jedes &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; mitteln wir die letzten &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; Elemente von &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; und schreiben das Ergebnis in &amp;lt;tt&amp;gt;r[i]&amp;lt;/tt&amp;gt;. Diese Operation ist z.B. bei Börsenkursen wichtig: Neben dem aktuellen Kurs für jeden Tag wird dort meist auch der gleitende Mittelwert der letzten 30 Tage sowie der letzten 200 Tage angegeben. In diesen Mittelwerten erkennt man besser die langfristige Tendenz, weil die täglichen Schwankungen herausgemittelt werden. Wir nehmen außerdem an, dass&lt;br /&gt;
* Array-Zugriff hat eine Komplexit&amp;amp;auml;t von O(1)&lt;br /&gt;
* &amp;lt;math&amp;gt;k \ll N&amp;lt;/math&amp;gt;, d.h. &amp;lt;math&amp;gt;N-k\approx N&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die beiden folgenden Algorithmen berechnen die Mittelwerte auf unterschiedliche Art. Der linke folgt der obigen Definition durch eine Summe, während der rechte inkrementell arbeitet: Man kann den Bereich der &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; letzten Werte als Fenster betrachten, das über das Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; geschoben wird. Schiebt man das Fenster ein Element weiter, fällt links ein Element heraus, und rechts kommt eins hinzu. Man muss also nicht jedes Mal die Summe neu berechnen, sondern kann den vorigen Wert aktualisieren. Wir werden sehen, dass dies Folgen für die Komplexität des Algorithmus hat.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;2&amp;quot; &lt;br /&gt;
|-&lt;br /&gt;
! Programmzeile&lt;br /&gt;
! Version 1: O(N * k)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
! Version 2: O(N)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
1.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
2.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
3.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
4.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k-1, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for i in range(k):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
5.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;for i in range(j-k+1, j+1):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[k-1] += a[i]&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
6.&lt;br /&gt;
|&lt;br /&gt;
:::: &amp;lt;tt&amp;gt;r[j] += a[i]&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
7.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] = (a[j] - a[j-k] + r[j-1])&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
8.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
9.&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
10.&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Wir zeigen unten dass Version 2 eine geringere Komplexit&amp;amp;auml;t besitzt, obwohl sie mehr Zeilen ben&amp;amp;ouml;tigt.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Wir haben in der Tabelle die Komplexität jeder Zeile für sich angegeben. Einfache Anweisungen (Berechnungen, Lese- und Schreibzugriffe auf das Array, Zuweiseungen) haben konstante Komplexität, die Komplexität des Schleifenkopfes allein (also der &amp;lt;tt&amp;gt;for&amp;lt;/tt&amp;gt;-Anweisung ohne den eingerückten Schleifenkörper) entspricht der Anzahl der Durchläufe. Wir müssen jetzt noch die Verschachtelung der Schleifen und die Nacheinanderausführung von Anweisungen berücksichtigen. &lt;br /&gt;
&lt;br /&gt;
====Berechnung der Komplexität von Version 1====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;small&amp;gt;(Wiederholung der Rechenregeln: siehe Abschnitt [[Effizienz#O-Notation|O-Notation]])&amp;lt;/small&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wir betrachten zunächst die innere Schleife (Zeilen 5 und 6 von Version 1):&lt;br /&gt;
&lt;br /&gt;
Der Schleifenkopf (Zeile 5) hat die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt;, weil die Schleife k-mal durchlaufen wird. Der Schleifenkörper (Zeile 6) hat die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;. Nach der Verschachtelungsregel müssen wir die beiden Komplexitäten multiplizieren, und es ergibt sich:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(k)\cdot\mathcal{O}(1) = \mathcal{O}(k\cdot 1)=\mathcal{O}(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wir betrachten nun die äußere Schleife. Der Schleifenkopf (Zeile 4) wird (N-k)-mal durchlaufen und hat somit eine Komplexität von &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Der Schleifenkörper (Zeilen 5 bis 7) besteht aus der inneren Schleife (Zeilen 5 und 6) mit der gerade berechneten Komplexität &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt; sowie einer einfachen Anweisung (Zeile 7) mit Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;. Nach der Sequenzregel wird die Komplexität des Schleifenkörpers durch Addition berechnet:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(k)+\mathcal{O}(1) = \mathcal{O}(\max(k,1)) = \mathcal{O}(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexität der gesamten äußeren Schleife erhalten wir nach der Verschachtelungsregel wieder durch multiplizieren:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)\cdot\mathcal{O}(k) = \mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die übrigen Schritte des Algorithmus werden einfach nacheinander ausgeführt, so dass sie ebenfalls nach der Sequenzregel behandelt werden. Wir erhalten&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)+\mathcal{O}(1)+\mathcal{O}(N\cdot k)+\mathcal{O}(1) = \mathcal{O}(\max(N,1,N\cdot k,1)) = \mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Der gesamte Algorithmus hat also die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Berechnung der Komplexität von Version 2====&lt;br /&gt;
&lt;br /&gt;
Hier gibt es nur einfache Schleifen ohne Verschachtelung. Da der Schleifenkörper jeder Schleife nur einfache Anweisungen der Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt; enthält, ergibt sich die Komplexität der Schleifen nach der Verschachtelungsregel als&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(X)\cdot\mathcal{O}(1) = \mathcal{O}(X\cdot 1)=\mathcal{O}(X)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
wobei &amp;lt;math&amp;gt;\mathcal{O}(X)&amp;lt;/math&amp;gt; die Komplexität des jeweiligen Schleifenkopfes ist. Wir erhalten also für Zeilen 4 und 5: &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt;, Zeilen 6 und 7: &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;, Zeilen 8 und 9: &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Die Hintereinanderausführung wird nach der Sequenzregel behandelt:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)+\mathcal{O}(1)+\mathcal{O}(k)+\mathcal{O}(N)+\mathcal{O}(N)+\mathcal{O}(1) = \mathcal{O}(\max(N,1,k,N,N,1)) = \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Dieser Algorithmus hat also nur die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Fazit====&lt;br /&gt;
&lt;br /&gt;
Obwohl Version 2 mehr Schritte benötigt hat sie eine geringere Komplexität, da die for-Schleifen nicht wie bei Version 1 verschachtelt/untergeordnet sind. Bei verschachtelten for-Schleifen muss die Multiplikationsregel angewendet werden &amp;amp;rarr; höhere Komplexität.&lt;br /&gt;
&lt;br /&gt;
Die gerade berechnete Komplexität gilt aber &amp;lt;u&amp;gt;nur&amp;lt;/u&amp;gt; unter der Annahme, dass Array-Zugriffe konstante Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt; besitzen. Wenn dies nicht der Fall ist, kann sich die Komplexität des Algorithmus drastisch verschlechtern.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|Allgemein gilt:&amp;lt;br/&amp;gt;&lt;br /&gt;
Algorithmen-Analysen beruhen auf der Annahme, dass Zugriffe auf die Daten optimal schnell sind, dass heißt, dass die für den jeweiligen Algorithmus am besten geeignete Datenstruktur verwendetet wird.&amp;lt;br /&amp;gt; &amp;amp;rarr; Ansonsten: Komplexitätsverschlechterung!&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Beispiel für eine Verschlechterung der Komplexität durch Verwendung einer nicht optimalen Datenstruktur====&lt;br /&gt;
&lt;br /&gt;
Wir verwende im Mittelwert-Algorithmus eine verkettete Liste anstelle des Eingabe-Arrays &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt;. Wir benötigen dazu eine Funktion, die das j-te Element der Liste zurückgibt. Wie üblich ist die Liste mit Hilfe einer Knotenklasse implementiert:&lt;br /&gt;
      class Node:&lt;br /&gt;
          def __init__(self, data):&lt;br /&gt;
              self.data = data&lt;br /&gt;
              self.next = None&lt;br /&gt;
&lt;br /&gt;
Die Listenklasse selbst hat ein Feld &amp;lt;tt&amp;gt;head&amp;lt;/tt&amp;gt;, das eine Referenz auf den ersten Knoten speichert, und jeder Knoten speichert im Feld &amp;lt;tt&amp;gt;next&amp;lt;/tt&amp;gt; eine Referenz auf seinen Nachfolger. Um zum j-ten Element zu gelangen, muss man die Liste sequenziell durchlaufen&lt;br /&gt;
      def get_jth(list, j):&lt;br /&gt;
           r = list.head&lt;br /&gt;
           while j &amp;gt; 0:&lt;br /&gt;
               r = r.head&lt;br /&gt;
               j -= 1&lt;br /&gt;
           return r.data&lt;br /&gt;
Die Komplexität dieser Funktion ist offensichtlich &amp;lt;math&amp;gt;\mathcal{O}(j)&amp;lt;/math&amp;gt; (Komplexitätsberechnung wie oben). Wir setzen jetzt bei Version 1 des Mittelwert-Algorithmus diese Funktion in Zeile 6 anstelle des Indexzugriffs &amp;lt;tt&amp;gt;a[i]&amp;lt;/tt&amp;gt; ein (nur in dieser Zeile wird auf die Elemente des Arrays zugegriffen). Wir erhalten folgende Implementation (die Änderungen sind rot markiert):&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;2&amp;quot; &lt;br /&gt;
|-&lt;br /&gt;
! Programmzeile&lt;br /&gt;
! Version 1 mit Liste: O(N * k)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
1.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
2.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
3.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
4.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k-1, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
5.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;for i in range(j-k+1, j+1):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
6.&lt;br /&gt;
|&lt;br /&gt;
:::: &amp;lt;tt&amp;gt;r[j] += &amp;lt;font color=red&amp;gt;get_jth(a, i)&amp;lt;/font&amp;gt;&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;&amp;lt;font color=red&amp;gt;O(i)&amp;lt;/font&amp;gt;&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
7.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
8.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Der Aufruf der Funktion &amp;lt;tt&amp;gt;get_jth&amp;lt;/tt&amp;gt; ist jetzt gleichbedeutend mit einer dreifach verschachtelten Schleife (weil &amp;lt;tt&amp;gt;get_jth&amp;lt;/tt&amp;gt; ja eine zusatzliche Schleife enthält). Die Anzahl der Operationen in Zeile 4 bis 6 ist jetzt&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;f(N,k)=\sum_{j=k-1}^{N-1}\,\sum_{i=j-k+1}^j\,\mathcal{O}(i)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
wobei das &amp;lt;math&amp;gt;\mathcal{O}(i)&amp;lt;/math&amp;gt; die neue Schleife durch Verwendung der Liste repräsentiert. Mit Mathematica-Hilfe [http://www.wolfram.com/] lässt sich diese Summe exakt ausrechnen&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;f(N,k)=\frac{1}{2}(k N^2-k^2 N+k^2-k)\in \mathcal{O}(k N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexitätsberechnung erfolgte dabei nach der Regel für Polynome unter Beachtung von &amp;lt;math&amp;gt;k \ll N&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Fazit:====&lt;br /&gt;
&lt;br /&gt;
Die Komplexität von Version 1 mit einer verketteten Liste wäre O(N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; * k)&lt;br /&gt;
'''&amp;amp;rarr; Die richtige Datenstruktur ist wichtig, da es sonst zu einer Komplexitätsverschlechterung kommen kann!'''&lt;br /&gt;
&lt;br /&gt;
Auf Version 2 unseres Running Average-Beispiels hätte eine verkettete Liste allerdings keine Auswirkungen, da die inkrementelle Berechnung der Summen in Zeile 7 weiterhin möglich ist (bei geschickter Implementation!) und somit Version 2 immer noch eine Komplexität von O(N) hätte.&lt;br /&gt;
&lt;br /&gt;
==Amortisierte Komplexität==&lt;br /&gt;
&lt;br /&gt;
Bis jetzt wurde die Komplexität nur im schlechtesten Fall (Worst Case) betrachtet. Bei einigen Operationen schwankt die Komplexität jedoch sehr stark, wenn man sie mehrmals hintereinander ausführt, und der schlechteste Fall kommt nur selten vor. Dann ist es sinnvoll, die &amp;lt;i&amp;gt;amortisierte Komplexität&amp;lt;/i&amp;gt; zu betrachten, die sich mit der &amp;lt;i&amp;gt;durchschnittlichen&amp;lt;/i&amp;gt; Komplexität über viele Aufrufe der selben Operation beschäftigt.&lt;br /&gt;
&lt;br /&gt;
Zum weiter Lesen: [[http://de.wikipedia.org/wiki/Amortisierte_Laufzeitanalyse Wikipedia: Amortisierte Laufzeitanalyse]]&lt;br /&gt;
&lt;br /&gt;
===Beispiel: Inkrementieren von Binärzahlen===&lt;br /&gt;
&lt;br /&gt;
Frage: Angenommen, das Umdrehen eines Bits einer Binärzahl verursacht Kosten von 1 Einheit. Wir erzeugen die Folge der natürlichen Zahlen in Binärdarstellung durch sukzessives Inkrementieren, von Null beginnend. Bei jeder Inkrementierung werden einige Bits verändert, aber diese Zahl (und damit die Kosten der Inkrementierungen) ''schwanken'' sehr stark. Wir fragen jetzt, was eine Inkrementierung im Durchschnitt kostet?&lt;br /&gt;
&lt;br /&gt;
Um diese Durchschnittskosten zu berechnen, bezahlen wir bei jeder Inkrementierung 2 Einheiten. Wenn davon nach Abzug der Kosten der jeweiligen Operation noch etwas übrig bleibt, wird der Rest dem  Guthaben zugeschrieben. Umgekehrt wird ein eventueller Fehlbetrag (wenn eine Inkrementierung mehr als 2 Bits umdreht) aus dem Guthaben gedeckt. Dadurch werden die ansonsten großen Schwankungen der Kosten ausgeglichen:&lt;br /&gt;
:: Kosten &amp;lt; Einzahlung &amp;amp;rarr; es wird gespart&lt;br /&gt;
:: Kosten = Einzahlung &amp;amp;rarr; Guthaben bleibt unverändert&lt;br /&gt;
:: Kosten &amp;gt; Einzahlung &amp;amp;rarr; Guthaben wird für die Kosten verbraucht&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
!Schritte&lt;br /&gt;
!Zahlen&lt;br /&gt;
!Kosten &amp;lt;br/&amp;gt;&lt;br /&gt;
(Anzahl der geänderten Bits)&lt;br /&gt;
! Einzahlung&lt;br /&gt;
!Guthaben =&amp;lt;br/&amp;gt;&lt;br /&gt;
altes Guthaben + Einzahlung - Kosten&lt;br /&gt;
|-&lt;br /&gt;
|1.&lt;br /&gt;
|0000&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|2.&lt;br /&gt;
|000&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|2&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|3.&lt;br /&gt;
|0001&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|4.&lt;br /&gt;
|00&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|3&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|5.&lt;br /&gt;
|0010&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|6.&lt;br /&gt;
|001&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|2&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|7.&lt;br /&gt;
|0011&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''3'''&lt;br /&gt;
|-&lt;br /&gt;
|8.&lt;br /&gt;
|0&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1000&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|4&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Die Kosten ergeben sich aus der Anzahl der Ziffern die von 1 nach 0, bzw. von 0 nach 1 verändert werden&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Rechnung:&amp;lt;/u&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Schritt: Kosten: 1 &amp;lt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Schritt: Kosten: 2 = Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird nicht gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; Guthaben bleibt so wie es ist &amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Schritt: Kosten: 1 &amp;lt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. Schritt: Kosten: 3 &amp;gt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird eine 1 vom Guthaben genommen um die Kosten zu zahlen&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
usw.&lt;br /&gt;
&lt;br /&gt;
Man erkennt, dass vor teuren Operation (Wechsel von 3 auf 4 bzw. von 7 auf 8) genügend Guthaben angespart wurde, um die Kosten zu decken. Das Guthaben geht bei diesen Operationen immer wieder auf 1 zurück, aber es wird nie vollständig verbraucht (Dies kann natürlich auch mathematisch exakt bewiesen werden, wie wir es unten am Beispiel des dynamische Arrays zeigen). Wir schließen daraus, dass die durchschnittlichen oder '''amortisierten Kosten''' einer Inkrementierungsoperation gleich 2 sind.&lt;br /&gt;
&lt;br /&gt;
Zum Weiterlesen: [[http://de.wikipedia.org/wiki/Account-Methode Wikipedia Account-Methode]]&lt;br /&gt;
&lt;br /&gt;
====Fazit====&lt;br /&gt;
Die amortisierte Komplexität beschäftigt sich mit dem Durchschnitt aller Operation im ungünstigsten Fall. Operationen mit hohen Kosten, die aber nur selten ausgeführt werden, fallen bei der amortisierten Komplexität nicht so ins Gewicht. Bei Algorithmen, die gelegentlich eine &amp;quot;teure&amp;quot; Operation benutzen, ansonsten jedoch &amp;quot;billige&amp;quot; Operationen aufrufen, kann die amortisierte Komplexität niedriger sein als die Komplexität im schlechtesten (Einzel-)Fall.&lt;br /&gt;
&lt;br /&gt;
In unserem Beispiel fallen die teuren Einzelschritte (z.B. 4. und 8. Schritt) bei den amortisierten Kosten nicht so ins Gewicht, da wir die Kosten aus unserem Guthaben mitbezahlen können. Das Guthaben ist immer groß genug, weil jeder zweite Aufruf eine billige Operation ist, die nur ein Bit umdreht und somit das Ansparen ermöglicht. Diese Betrachtung zeigt, dass die amortisierte (d.h. durchschnittliche) Komplexität des Algoithmus niedriger (nämlich konstant) ist als die Komplexität im schlechtesten Fall.&lt;br /&gt;
&lt;br /&gt;
===Anwendung: Dynamisches Array===&lt;br /&gt;
&lt;br /&gt;
Ein dynamisches Array hat die Eigenschaft, dass man effizient am Ende des Arrays neue Elemente anfügen kann, indem man die Länge des Arrays entsprechend vergrößert (siehe Übung 1). Die Analyse der amortisierten Komplexität der Anfüge-Operation zeigt uns, wie man das Vergrößern des Arrays richtig implementiert, damit die Operation wirklich effizient abläuft.&lt;br /&gt;
&lt;br /&gt;
==== Ineffiziente naive Lösung ====&lt;br /&gt;
&lt;br /&gt;
Wenn wir an ein Array ein Element anhängen wollen, müssen wir neuen Speicher allokieren, der die gewünschte Länge hat. Die Werte aus dem alten Array müssen dann in den neuen Speicher umkopiert werden. Danach kann das neue Element hinten angefügt werden, weil wir im neuen Array bereits Speicher für dieses Element reserviert haben. Bei der naiven Implementation des dynamischen Arrays wiederholt man dies bei jeder Anfügeoperation. Für die Analyse nehmen wir an, dass das Kopieren eines Elements konstante Zeit O(1) erfordert, ebenso das Einfügen eines neuen Elements auf in eine noch unbenutzte Speicherposition.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Naives Anhängen eines weiteren Elements an ein Array:&amp;lt;/u&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;right&amp;quot;&lt;br /&gt;
!Schritte&lt;br /&gt;
|'''Array'''&lt;br /&gt;
&amp;lt;small&amp;gt;(wie es nach jedem Schritt aussieht)&amp;lt;/small&amp;gt;&lt;br /&gt;
!Komplexität&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;center&amp;gt;altes Array (N=4)&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|1. neuer Speicher für&amp;lt;br&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;(N+1) Elemente&lt;br /&gt;
|&amp;lt;center&amp;gt;[None,None,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;O(N+1) = '''O(N)'''&amp;lt;/center&amp;gt;(wenn der Speicher initialisiert wird&amp;lt;br&amp;gt;(hier auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;), sonst O(1))&lt;br /&gt;
|-&lt;br /&gt;
|2. Kopieren &lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;'''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|3. append von &amp;quot;x&amp;quot;&lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3,'x']&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;'''O(1)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
altesArray = [0,1,2,3]&amp;lt;br /&amp;gt;&lt;br /&gt;
altesArray.append('x')&lt;br /&gt;
&lt;br /&gt;
1. Es wird ein neues Array der Größe N+1 erzeugt&amp;lt;br /&amp;gt;&lt;br /&gt;
2. Die N Datenelemente aus dem alten Array werden in das neue Array kopiert&amp;lt;small&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Das sind N Operationen der Komplexität O(1), also ein Gesamtaufwand von O(N).&amp;lt;/small&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
3. 'x' wird mit Aufwand O(1) an die letzte Stelle des neuen Arrays geschrieben&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Additionsregel:&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
O(N) + O(1) &amp;amp;isin; O(N)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Folgerung:&amp;lt;/b&amp;gt; &lt;br /&gt;
&lt;br /&gt;
Bei der naiven Methode erfordert jede Anfügung einen Aufwand O(N) (wobei N die derzeitige Arraygröße ist). Dies ist nicht effizient.&lt;br /&gt;
&lt;br /&gt;
====Effiziente Lösung durch Verdoppeln der Kapazität====&lt;br /&gt;
&lt;br /&gt;
Offensichtlich kommt man nicht darum heraum, den Inhalt des alten Arrays zu kopieren, wenn der allokierte Speicher voll ist. Der Trick für die effiziente Implementation der Anfügeoperation besteht darin, das Kopieren so selten wie möglich durchzuführen, also nicht wie in der naiven Lösung bei jeder Anfügeoperation. Hier kommt die amortisierte Komplexität ins Spiel: Ab und zu gibt es eine teure Anfügeoperation (wenn nämlich kopiert werden muss), aber wenn man den durchschnittlichen Aufwand über viele Anfügungen betrachtet, ist die Operation effizient. Der teure Fall wird sozusagen &amp;quot;herausgemittelt&amp;quot;. &lt;br /&gt;
&lt;br /&gt;
Um nur selten kopieren zu müssen, werden beim dynamischen Array mehr Speicherelemente reserviert als zur Zeit benötigt werden (in der naiven Lösung wurde dagegen immer nur Speicher für ein einziges neues Element reserviert). Wir unterscheiden deshalb &lt;br /&gt;
&lt;br /&gt;
:&amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; = Anzahl der allokierten Speicherzellen, d.h. der möglichen Elemente, die in das Array passen&amp;lt;br /&amp;gt;&lt;br /&gt;
:&amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt; = Anzahl der Elemente, die im Array zur Zeit gespeichert sind&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Daten selbst werden in einem statischen Array gespeichert:&lt;br /&gt;
:&amp;lt;tt&amp;gt;data&amp;lt;/tt&amp;gt; = statisches Array der Größe &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die folgende intuitive Abschätzung zeigt, dass es sinnvoll ist, die Größe des allokierten Speichers jeweils zu verdoppeln. Wir starten bei einem Array der Größe &amp;lt;tt&amp;gt;size = capacity&amp;lt;/tt&amp;gt; = N. Da der verfügbare Speicher voll ist, müssen wir bei der nächsten Anfügung die N vorhandenen Elemente in ein neues Array der Länge &amp;lt;tt&amp;gt;new_capacity&amp;lt;/tt&amp;gt; kopieren (Aufwand &amp;lt;math&amp;gt;N\cdot O(1)&amp;lt;/math&amp;gt;). Danach können wir K Elemente billig einfügen (Aufwand &amp;lt;math&amp;gt;K\cdot O(1)&amp;lt;/math&amp;gt;), wobei &lt;br /&gt;
:K = &amp;lt;tt&amp;gt;new_capacity - capacity&amp;lt;/tt&amp;gt; &lt;br /&gt;
die Anzahl der nach dem Kopieren noch unbenutzen Speicherzellen ist. Der durchschnittliche Aufwand für diese K Einfügungen ist somit&lt;br /&gt;
:&amp;lt;math&amp;gt;\bar T = \frac{N \cdot O(1) + K \cdot O(1)}{K}=\frac{N+K}{K}\cdot O(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Damit die mittlere Zeit in O(1) sein kann, muss der Quotient &amp;lt;math&amp;gt;(N+K)/K&amp;lt;/math&amp;gt; eine Konstante sein. Wir setzen &amp;lt;math&amp;gt;K = a N&amp;lt;/math&amp;gt; und erhalten:&lt;br /&gt;
:&amp;lt;math&amp;gt;\bar T = \frac{(a+1)N}{a N}\cdot O(1)=\frac{a+1}{a}\cdot O(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Der amortisierte Aufwand über K Einfügungen ist also konstant, wenn &amp;lt;math&amp;gt;a&amp;lt;/math&amp;gt; eine (kleine) von N unabhängige Zahl ist. Typischerweise wählt man &lt;br /&gt;
:&amp;lt;math&amp;gt;a = 1&amp;lt;/math&amp;gt;&lt;br /&gt;
und mit &amp;lt;math&amp;gt;K = 1\cdot N&amp;lt;/math&amp;gt; ergibt sich&lt;br /&gt;
:&amp;lt;tt&amp;gt;new_capacity = capacity&amp;lt;/tt&amp;gt; + N = &amp;lt;tt&amp;gt;2 * capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Vorgehensweise beim Zufügen eines neuen Elements im Fall &amp;lt;tt&amp;gt;size == capacity&amp;lt;/tt&amp;gt; ist also&lt;br /&gt;
* capacity wird verdoppelt&amp;lt;br /&amp;gt;&lt;br /&gt;
: &amp;lt;tt&amp;gt;neue capacity = 2 * alte capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
: (allgemein genügt es auch, wenn capacity um einen bestimmten Prozentsatz vergrößert wird, &lt;br /&gt;
:: &amp;lt;tt&amp;gt;neue capacity = alte capacity * c&amp;lt;/tt&amp;gt; &lt;br /&gt;
: mit c &amp;gt; 1, z.B. c = 1.2, das entspricht oben der Wahl &amp;lt;math&amp;gt;a = 0.2&amp;lt;/math&amp;gt;)&lt;br /&gt;
* ein neues statisches Array der Größe 'neue capacity' wird erzeugt&lt;br /&gt;
* das alte Array wird ins neue kopiert und danach freigegeben&lt;br /&gt;
* das anzufügende Element wird ins neue Array eingefügt&lt;br /&gt;
Umgekehrt geht man beim Entfernen des ''letzten'' Array-Elements vor. Normalerweise überschreibt man einfach das letzte Element mit &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; und dekrementiert &amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt;. Wird dadurch das Array zu klein (üblicherweise &amp;lt;tt&amp;gt;size &amp;amp;lt; capacity / 4&amp;lt;/tt&amp;gt;), wird die Kapazität halbiert, genauer:&lt;br /&gt;
* ein neues Array mit &amp;lt;br/&amp;gt;&lt;br /&gt;
: &amp;lt;tt&amp;gt;neue capacity = alte capacity / 2 &amp;lt;/tt&amp;gt;&lt;br /&gt;
: wird angelegt (bzw. mit&lt;br /&gt;
:: &amp;lt;tt&amp;gt;neue capacity = alte capacity / c &amp;lt;/tt&amp;gt;&lt;br /&gt;
: wenn ein anderer Vergrößerungsfaktor verwendet wird)&lt;br /&gt;
* das alte Array wird ins neue kopiert und danach freigegeben&lt;br /&gt;
&lt;br /&gt;
'''Folge:''' Die Kosten für das Vergrößern/Verkleinern der Kapazität werden amortisiert über viele Einfügungen, die kein Vergrößern erfordern. Die Operation &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; besitzt amortisierte Komplexität O(1). Im folgenden Abschnitt zeigen wir dies mathematisch exakt mit der Potentialmethode.&lt;br /&gt;
&lt;br /&gt;
====Komplexitätsanalyse des dynamischen Arrays mit Potentialmethode====&lt;br /&gt;
&lt;br /&gt;
Durchschnitt der Gesamtkosten für N-maliges append = &amp;lt;math&amp;gt;\frac{1}{N} \sum_{i = 1}^N Kosten(i)&amp;lt;/math&amp;gt;. Zur Analyse der amortisierten Komplexität wird ein Potential&amp;lt;br/&amp;gt;&lt;br /&gt;
::&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2*size&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; - capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
eingeführt, wobei das Array nach dem i-ten Einfüge-Schritt die Größe size&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; und die Kapizität capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; hat. Wir nehmen vereinfachend an, dass es keine Löschoperationen gibt. Dann gilt nach dem i-ten Schritt jeweils&lt;br /&gt;
::&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2*i - capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Fall 1: Array ist nicht voll&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Es wird kein Umkopieren benötigt, da das Array noch nicht voll ist&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; size&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; &amp;lt; capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; == capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Kosten: 1 (für Einfügen des neuen Elements)&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial vor append: &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = 2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial nach append: &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
amortisierte Kosten = Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Phi;&amp;lt;sub&amp;gt;(i)&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;(i-1)&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::                 = 1            + (2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;)        - [2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;]&lt;br /&gt;
:::::                 = 1            + 2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;         - 2i + 2 + capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::                 = 1            + &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; - &amp;lt;del&amp;gt;capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;/del&amp;gt; - &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; + 2 + &amp;lt;del&amp;gt;capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;/del&amp;gt;&lt;br /&gt;
:::::                 = 1 + 2&lt;br /&gt;
:::::                 = 3 = O(1) &amp;amp;rarr; konstant&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Fall 2: Array ist voll&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Vor dem i-ten append muss umkopiert werden&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; size&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; == capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; == i-1&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; Allokieren eines neuen statischen Arrays mit verdoppelter Kapazität notwendig, also capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; == 2*capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Kosten: (i-1) + 1 (für Umkopieren und Einfügen des neuen Elements)&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial vor append =  &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = 2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial nach append =  &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; =  2i - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
amortisierte Kosten = Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Phi;&amp;lt;sub&amp;gt;(i)&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;(i-1)&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = ((i - 1) + 1) + 2i - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; - [2(i-1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;]&lt;br /&gt;
:::::               = i + &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; - &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; + 2 - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = i + 2 - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = i + 2 - (i - 1) &amp;lt;small&amp;gt;(da capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = i-1)&amp;lt;/small&amp;gt;&lt;br /&gt;
:::::               = 3 = O(1) &amp;amp;rarr; konstant            &lt;br /&gt;
&lt;br /&gt;
'''Damit wurde bewiesen, dass die Operation &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; beim dynamischen Array eine amortisierte Komplexität von 3 Einheiten hat, also &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; &amp;amp;isin; O(1)'''. Diese Operation kann deshalb gefahrlos in der inneren Schleife eines Algorithmus benutzt werden.&lt;br /&gt;
&lt;br /&gt;
==== Beispiel für 9 Einfügeoperationen ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot;&lt;br /&gt;
!Array&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;lt;small&amp;gt;(wie es aussehen könnte)&amp;lt;/small&amp;gt;&lt;br /&gt;
!size&lt;br /&gt;
!capacity&lt;br /&gt;
!Kosten für append&amp;lt;br /&amp;gt;(einschließlich Umkopieren)&lt;br /&gt;
!Summe Kosten&lt;br /&gt;
!Durchschnittskosten&lt;br /&gt;
!&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2 * size - capacity&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;lt;small&amp;gt;(i = size)&amp;lt;/small&amp;gt;&lt;br /&gt;
!Potenzialdifferenz&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;amp;Delta; &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
!amortisierte Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
= Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Delta; &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;0&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3/2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6/3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;0&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7/4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;5&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;12&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;12/5&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;13&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;13/6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;14&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;14/7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,h]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;15&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;15/8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,h,j,None,None,None,&amp;lt;br /&amp;gt;&lt;br /&gt;
None,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;9&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;16&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;24&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;24/9&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die durchschnittlichen Kosten betragen stets etwa 2 Einheiten, schwanken allerdings so, dass nicht unmittelbar ersichtlich ist, ob dies für sämtliche Einfügeoperationen gilt. Die amortisierte Komplexität, die mit Hilfe des Potentials berechnet wird, ist hingegen konstant 3, wie auch im obigen Beweis für alle Einfügeoperationen allgemein gezeigt wurde.&lt;br /&gt;
&lt;br /&gt;
[[Suchen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Effizienz&amp;diff=5420</id>
		<title>Effizienz</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Effizienz&amp;diff=5420"/>
				<updated>2012-07-27T16:02:47Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Beispiel für eine Verschlechterung der Komplexität durch Verwendung einer nicht optimalen Datenstruktur */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Bei der Diskussion von Effizienz müssen wir zwischen der Laufzeit eines Algorithmus auf einem bestimmten System und seiner prinzipiellen Leistungsfähigkeit (Algorithmenkomplexität) unterscheiden. Der Benutzer ist natürlich vor allem an der Laufzeit interessiert, denn diese bestimmt letztendlich seine Arbeitsproduktivität. Ein Softwaredesigner hingegen muss eine Implementation wählen, die auf verschiedenen Systemen und in verschiedenen Anwendungen schnell ist. Für ihn sind daher auch Aussagen zur Algorithmenkomplexität sehr wichtig, um den am besten geeigneten Algorithmus auszuwählen.&lt;br /&gt;
&lt;br /&gt;
== Laufzeit ==&lt;br /&gt;
&lt;br /&gt;
Aus Anwendersicht ist ein Algorithmus effizient, wenn er die in der Spezifikation verlangten Laufzeitgrenzen einhält. Ein Algorithmus muss also nicht immer so schnell wie möglich sein, sondern so schnell wie nötig. Dies führt in verschiedenen Anwendungen zu ganz unterschiedliche Laufzeitanforderungen:&lt;br /&gt;
&lt;br /&gt;
* Berechnen des nächsten Steuerkommandos für eine Maschine: ca. 1/1000s&lt;br /&gt;
* Berechnen des nächsten Bildes für eine Videopräsentation (z.B. Dekompression von MPEG-kodierten Bildern): ca. 1/25s&lt;br /&gt;
: Geringere Bildraten führen zu ruckeligen Filmen.&lt;br /&gt;
* Sichtbare Antwort auf ein interaktives Kommando (z.B. Mausklick): ca. 1/2s&lt;br /&gt;
: Wird diese Antwortzeit überschritten, vermuten viele Benutzer, dass der Mausklick nicht funktioniert hat, und klicken nochmals, mit eventuell fatalen Folgen. Wenn ein Algorithmus notwendigerweise länger dauert als 1/2s, sollte ein Fortschrittsbalken angezeigt werden.&lt;br /&gt;
* Wettervorhersage: muss spätestens am Vorabend des vorhergesagten Tages beendet sein&lt;br /&gt;
&lt;br /&gt;
===Laufzeitvergleich===&lt;br /&gt;
&lt;br /&gt;
Da die Laufzeit für den Benutzer ein so wichtiges Kriterium ist, werden häufig Laufzeitvergleiche durchgeführt. Deren Ergebnisse hängen allerdings von vielen Faktoren ab, die möglicherweise nicht kontrollierbar sind:&lt;br /&gt;
* Geschwindigkeit und Anzahl der Prozessoren&lt;br /&gt;
* Auslastung des Systems&lt;br /&gt;
* Größe des Hauptspeichers  und Cache, Geschwindigkeit des  Datenbus&lt;br /&gt;
* Qualität des Compilers/Optimierers (ist der Compiler für die spezielle Prozessor-Architektur optimiert?)&lt;br /&gt;
* Geschick des Programmierers&lt;br /&gt;
* Daten (Beispiel Quicksort: Best case und worst case [vorsortierter Input] stark unterschiedlich)&lt;br /&gt;
All diese Faktoren sind untereinander abhängig. Laufzeitvergleiche sind daher mit Vorsicht zu interpretieren.&lt;br /&gt;
Generell sollten bei Vergleichen möglichst wenige Parameter verändert werden, z.B.&lt;br /&gt;
* gleiches Programm (gleiche Kompilierung), gleiche Daten, andere Prozessoren&lt;br /&gt;
oder&lt;br /&gt;
* gleiche CPU, Daten, andere Programme (Vergleich von Algorithmen)&lt;br /&gt;
Zur Verbesserung der Vergleichbarkeit gibt es standardisierte [http://en.wikipedia.org/wiki/Benchmark_(computing) Benchmarks], die bestimmte Aspekte eines Systems unter möglichst realitätsnahen Bedingungen testen. Generell gilt aber: Durch Laufzeitmessung ist schwer festzustellen, ob ein Algorithmus ''prinzipiell'' besser ist als ein anderer. Dafür ist die Analyse der [[Effizienz#Algorithmen-Komplexität|Algorithmenkomplexität]] notwendig.&lt;br /&gt;
&lt;br /&gt;
===Optimierung der Laufzeit===&lt;br /&gt;
&lt;br /&gt;
Wenn sich herausstellt, dass ein bereits implementierter Algorithmus zu langsam läuft, geht man wie folgt vor:&lt;br /&gt;
&lt;br /&gt;
# Man verwendet einen [http://en.wikipedia.org/wiki/Performance_analysis Profiler], um zunächst den Flaschenhals zu bestimmen. Ein Profiler ist ein Hilfsprogramm, das während der Ausführung eines Programms misst, wieviel Zeit in jeder Funktion und Unterfunktion verbraucht wird. Dadurch kann man herausfinden, welcher Teil des Algorithmus überhaupt Probleme bereitet. Donald Knuth gibt z.B. als Erfahrungswert an, dass Programme während des größten Teils ihrer Laufzeit nur 3% des Quellcodes (natürlich mehrmals wiederholt) ausführen [http://pplab.snu.ac.kr/courses/adv_pl05/papers/p261-knuth.pdf]. Es ist sehr wichtig, diese 3% experimentell zu bestimmen, weil die Erfahrung zeigt, dass man beim Erraten der kritischen Programmteile oft falsch liegt. Man spricht dann von &amp;quot;[http://en.wikipedia.org/wiki/Optimization_%28computer_science%29#When_to_optimize premature optimization]&amp;quot;, also von voreiliger Optimierung ohne experimentelle Untersuchung der wirklichen Laufzeiten, was laut Knuth &amp;quot;the root of all evil&amp;quot; ist. Der Python-Profiler wird in [http://docs.python.org/lib/profile.html Kapitel 25] der Python-Dokumentation beschrieben.&lt;br /&gt;
# Man kann dann versuchen, die kritischen Programmteile zu optimieren.&lt;br /&gt;
# Falls der Laufzeitgewinn durch Optimierung zu gering ist, muss man einen prinzipiell schnelleren Algorithmus verwenden, falls es einen gibt.&lt;br /&gt;
&lt;br /&gt;
Einige wichtige Techniken der Programmoptimierung sollen hier erwähnt werden. Wenn man einen optimierenden Compiler verwendet, werden einige Optimierungen automatisch ausgeführt [http://en.wikipedia.org/wiki/Compiler_optimization]. In Python trifft dies jedoch nicht zu. Um den Sinn einiger Optimierungen zu verstehen, benötigt man Grundkenntnisse der Computerarchitektur.&lt;br /&gt;
&lt;br /&gt;
;Elimination von redundantem Code: Es ist offensichtlich überflüssig, dasselbe Ergebnis mehrmals zu berechnen, wenn es auch zwischengespeichert werden könnte. Diese Optimierung wird von vielen automatischen Optimierern unterstützt und kommt im wesentlichen in zwei Ausprägungen vor:&lt;br /&gt;
:; common subexpression elimination: In mathematischen Ausdrücken wird ein Teilergebnis häufig mehrmals benötigt. Man betrachte z.B. die Lösung der quadratischen Gleichung &amp;lt;math&amp;gt;x^2+p\,x+q = 0&amp;lt;/math&amp;gt;:&lt;br /&gt;
        x1 = - p / 2.0 + sqrt(p*p/4.0 - q)&lt;br /&gt;
        x2 = - p / 2.0 - sqrt(p*p/4.0 - q)&lt;br /&gt;
::Die mehrmalige Berechnung von Teilausdrücken wird vermieden, wenn man stattdessen schreibt:&lt;br /&gt;
        p2 = - p / 2.0&lt;br /&gt;
        r  = sqrt(p2*p2 - q)&lt;br /&gt;
        x1 = p2 + r&lt;br /&gt;
        x2 = p2 - r&lt;br /&gt;
:; loop invariant elimination: Wenn ein Teilausdruck sich in einer Schleife nicht ändert, muss man ihn nicht bei jedem Schleifendurchlauf neu berechnen, sondern kann dies einmal vor Beginn der Schleife tun. Ein typisches Beispiel hierfür ist die Adressierung von Matrizen, die als 1-dimensionales Array gespeichert sind. Angenommen, wir speichern eine NxN Matrix &amp;lt;tt&amp;gt;m&amp;lt;/tt&amp;gt; in einem Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; der Größe N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt;, so dass das Matrixelement &amp;lt;tt&amp;gt;m&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt;&amp;lt;/tt&amp;gt; durch &amp;lt;tt&amp;gt;a[i + j*N]&amp;lt;/tt&amp;gt; indexiert wird. Wir betrachten die Aufgabe, eine Einheitsmatrix zu initialisieren. Ein nicht optimierter Algorithmus dafür lautet:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + j*N] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + j*N] = 0.0&lt;br /&gt;
::Der Ausdruck &amp;lt;tt&amp;gt;j*N&amp;lt;/tt&amp;gt; wird hier in jedem Schleifendurchlauf erneut berechnet, obwohl sich &amp;lt;tt&amp;gt;j&amp;lt;/tt&amp;gt; in der inneren Schleife gar nicht verändert. Man kann deshalb optimieren zu:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + jN] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + jN] = 0.0&lt;br /&gt;
;Vereinfachung der inneren Schleife: Generell sollte man sich bei der Optimierung auf die innere Schleife eines Algorithmus konzentrieren, weil dieser Code am häufigsten ausgeführt wird. Insbesondere sollte man die Anzahl der Befehle in der inneren Schleife so gering wie möglich halten und teure Befehle vermeiden. Früher waren vor allem Floating-Point Befehle teuer, die man oft durch die schnellere Integer-Arithmetik ersetzt hat, falls dies algorithmisch möglich war (diesen Rat findet man noch oft in der Literatur). Heute hat sich die Hardware so verbessert, dass im Allgemeinen nur noch die Floating-Point Division deutlich langsamer ist als die anderen Operatoren. Im obigen Beispiel der quadratischen Gleichung ist es daher sinnvoll, den Ausdruck &lt;br /&gt;
        p2 = -p / 2.0&lt;br /&gt;
:durch&lt;br /&gt;
        p2 = -0.5 * p&lt;br /&gt;
:zu ersetzen. Dadurch ersetzt man eine Division durch eine Multiplikation und spart außerdem das Negieren von &amp;lt;tt&amp;gt;p&amp;lt;/tt&amp;gt;, da der Compiler direkt mit &amp;lt;tt&amp;gt;-0.5&amp;lt;/tt&amp;gt; multipliziert.&lt;br /&gt;
;Ausnutzung der Prozessor-Pipeline: Moderne Prozessoren führen mehrere Befehle parallel aus. Dies ist möglich, weil jeder Befehl in mehrere Teilschritte zerlegt werden kann. Eine generische Unterteilung in vier Teilschritte ist z.B.:&lt;br /&gt;
:# Dekodieren des nächsten Befehls&lt;br /&gt;
:# Beschaffen der Daten, die der Befehl verwendet (aus Prozessorregistern, dem Cache, oder dem Hauptspeicher)&lt;br /&gt;
:# Ausführen des Befehls&lt;br /&gt;
:# Schreiben der Ergebnisse&lt;br /&gt;
:Man bezeichnet dies als die &amp;quot;[http://en.wikipedia.org/wiki/Instruction_pipeline instruction pipeline]&amp;quot; des Prozessors (heutige Prozessoren verwenden wesentlich feinere Unterteilungen). Prozessoren werden nun so gebaut, dass mehrere Befehle parallel, auf verschiedenen Ausführungsstufen ausgeführt werden. Wenn Befehl 1 also beim Schreiben der Ergebnisse angelangt ist, kann Befehl 2 die Hardware zum Ausführen des Befehls benutzen, während Befehl 3 seine Daten holt, und Befehl 4 soeben dekodiert wird. Unter bestimmten Bedingungen funktioniert diese Parallelverarbeitung jedoch nicht. Dies gibt Anlass zu Optimierungen:&lt;br /&gt;
:;Vermeiden unnötiger Typkonvertierungen: Der Prozessor verarbeitet Interger- und Floating-Point-Befehle in verschiedenen Pipelines, weil die Hardwareanforderungen sehr verschieden sind. Wird jetzt ein Ergebnis von Integer nach Floating-Point umgewandelt oder umgekehrt, muss die jeweils andere Pipeline warten, bis die erste Pipeline ihre Berechnung beendet. Es kann dann besser sein, Berechnungen in Floating-Point zu Ende zu führen, auch wenn sie semantisch eigentlich Integer-Berechnungen sind.&lt;br /&gt;
:;Reduzierung der Anzahl von Verzweigungen: Wenn der Code verzweigt (z.B. durch eine &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;- oder  &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Anweisung), ist nicht klar, welcher Befehl nach der Verzweigung ausgeführt werden soll, bevor Stufe 3 der Pipeline die Verzweigungsbedingung ausgewertet hat. Bis dahin wären die ersten beiden Stufen der Pipeline unbenutzt. Moderne Prozessoren benutzen zwar ausgefeilte Heuristiken, um das Ergebnis der Bedingung vorherzusagen, und führen den hoffentlich richtigen Zweig des Codes spekulativ aus, aber dies funktioniert nicht immer. Man sollte deshalb generell die Anzahl der Verzweigungen minimieren. Als Nebeneffekt führt dies meist auch zu besser lesbarem, verständlicherem Code. Im Matrixbeispiel kann man&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + jN] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + jN] = 0.0&lt;br /&gt;
::durch&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               a[i + jN] = 0.0&lt;br /&gt;
           a[j + jN] = 1.0&lt;br /&gt;
ersetzen. Die Diagonalelemente &amp;lt;tt&amp;gt;a[j + jN]&amp;lt;/tt&amp;gt; werden jetzt zwar zweimal initialisiert (in der Schleife auf Null, dann auf Eins), aber durch Elimination der &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage wird dies wahrscheinlich mehr als ausgeglichen, zumal dadurch die innere Schleife wesentlich vereinfacht wurde.&lt;br /&gt;
;Ausnutzen des Prozessor-Cache: Zugriffe auf den Hauptspeicher sind sehr langsam. Deshalb werden stets ganze Speicherseiten auf einmal in den [http://en.wikipedia.org/wiki/Cache Cache] des Prozessors geladen. Wenn unmittelbar nacheinander benutzte Daten auch im Speicher nahe beieinander liegen (sogenannte &amp;quot;[http://en.wikipedia.org/wiki/Locality_of_reference locality of reference]&amp;quot;), ist die Wahrscheinlichkeit groß, dass die als nächstes benötigten Daten bereits im Cache sind und damit schnell gelesen werden können. Bei vielen Algorithmen kann man die Implementation so umordnen, dass die locality of reference verbessert wird, was zu einer drastischen Beschleunigung führt. Im Matrix-Beispiel ist z.B. die Reihenfolge der Schleifen wichtig. Für konstanten Index &amp;lt;tt&amp;gt;j&amp;lt;/tt&amp;gt; liegen die Indizes &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; im Speicher hintereinander. Deshalb ist es günstig, in der inneren Schleife über &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu iterieren:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               a[i + jN] = 0.0&lt;br /&gt;
           a[j + jN] = 1.0&lt;br /&gt;
:Die umgekehrte Reihenfolge der Schleifen ist hingegen ungünstig&lt;br /&gt;
       for i in range(N):&lt;br /&gt;
           for j in range(N):&lt;br /&gt;
               a[i + j*N] = 0.0&lt;br /&gt;
           a[i + i*N] = 1.0&lt;br /&gt;
:Jetzt werden in der inneren Schleife stets N Datenelemente übersprungen. Besonders bei großem N muss man daher häufig den Cache neu füllen, was bei der ersten Implementation nicht notwendig war.  (Außerdem verliert man hier die Optimierung &amp;lt;tt&amp;gt;jN = j*N&amp;lt;/tt&amp;gt;, die jetzt nicht mehr möglich ist.)&lt;br /&gt;
&lt;br /&gt;
Als Faustregel kann man durch Optimierung eine Verdoppelung der Geschwindigkeit erreichen (in Ausnahmefällen auch mehr). Benötigt man stärkere Verbesserungen, muss man wohl oder übel einen besseren Algorithmus oder einen schnelleren Computer verwenden.&lt;br /&gt;
&lt;br /&gt;
== Algorithmen-Komplexität ==&lt;br /&gt;
&lt;br /&gt;
Komplexitätsbetrachtungen ermöglichen den Vergleich der prinzipiellen Eigenschaften von Algorithmen unabhängig von einer Implementation, Umgebung etc.&lt;br /&gt;
      &lt;br /&gt;
Eine einfache Möglichkeit ist das Zählen der Aufrufe einer Schlüsseloperation. Beispiel Sortieren:&lt;br /&gt;
* Anzahl der Vergleiche&lt;br /&gt;
* Anzahl der Vertauschungen&lt;br /&gt;
&lt;br /&gt;
=== Beispiel: Selection Sort ===&lt;br /&gt;
&lt;br /&gt;
  for i in range(len(a)-1):&lt;br /&gt;
    max = i&lt;br /&gt;
    for j in range(i+1, len(a)):&lt;br /&gt;
      if a[j] &amp;lt; a[max]:&lt;br /&gt;
        max = j&lt;br /&gt;
    a[max], a[i] = a[i], a[max]      # swap&lt;br /&gt;
&lt;br /&gt;
*Anzahl der Vergleiche: Ein Vergleich in jedem Durchlauf der inneren Schleife. Es ergibt sich folgende Komplexität:&lt;br /&gt;
*:Ingesamt &amp;lt;math&amp;gt;\sum_{i=0}^{N-2} \sum_{j=i+1}^{N-1}1 = \frac{N}{2} (N-1) \!&amp;lt;/math&amp;gt; Vergleiche.&lt;br /&gt;
&lt;br /&gt;
*Anzahl der Vertauschungen (swaps): Eine Vertauschung pro Durchlauf der äußeren Schleife:&lt;br /&gt;
*:Insgesamt &amp;lt;math&amp;gt;N-1 \!&amp;lt;/math&amp;gt; Vertauschungen&lt;br /&gt;
&lt;br /&gt;
Die Komplexität wird durch die Operationen bestimmt, die am häufigsten ausgeführt werden, hier also die Anzahl der Vergleiche. Die Anzahl der Vertauschungen ist hingegen kein geeignetes Kriterium für die Komplexität von selection sort, weil der Aufwand in der inneren Schleife ignoriert würde.&lt;br /&gt;
&lt;br /&gt;
=== Fallunterscheidung: Worst und Average Case ===&lt;br /&gt;
&lt;br /&gt;
Die Komplexität ist in der Regel eine Funktion der Eingabegröße (Anzahl der Eingabebits, Anzahl der Eingabeelemente). Sie kann aber auch von der Art der Daten abhängen, nicht nur von der Menge, z.B. vorsortierte Daten bei Quicksort. Um von der Art der Daten unabhängig zu werden, kann man zwei Fälle der Komplexität unterscheiden:&lt;br /&gt;
      &lt;br /&gt;
* Komplexität im ungünstigsten Fall &lt;br /&gt;
*: Der ungünstigste Fall ist die Eingabe gegebener Länge, für die der Algorithmus am langsamsten ist. Der Nachteil dieser Methode besteht darin, dass dieser ungünstige Fall in der Praxis vielleicht gar nicht oder nur selten vorkommt, so dass sich der Algorithmus in Wirklichkeit besser verhält als man nach dieser Analyse erwarten würde. Beim Quicksort-Algorithmus mit zufälliger Wahl des Pivot-Elements müsste z.B. stets das kleinste oder größte Element des aktuellen Intervalls als Pivot-Element gewählt werden, was äußerst unwahrscheinlich ist.&lt;br /&gt;
* Komplexität im durchschnittlichen/typischen Fall&lt;br /&gt;
*: Der typische Fall ist die mittlere Komplexität des Algorithmus über alle möglichen Eingaben. Dazu muss man die Wahrscheinlichkeit jeder möglichen Eingabe kennen, und berechnet dann die mittlere Laufzeit über dieser Wahrscheinlichkeitsverteilung. Leider ist die Wahrscheinlichkeit der Eingaben oft nicht bekannt, so dass man geeignete Annahmen treffen muss. Bei Sortieralgorithmen können z.B. alle möglichen Permutationen des Eingabearrays als gleich wahrscheinlich angenommen werden, und der typische Fall ist dann die mittlere Komplexität über alle diese Eingaben. Oft hat man jedoch in der Praxis andere Wahrscheinlichkeitsverteilungen, z.B. sind die Daten oft &amp;quot;fast sortiert&amp;quot; (nur wenige Elemente sind an der falschen Stelle). Dann verhält sich der Algorithmus ebenfalls anders als vorhergesagt.&lt;br /&gt;
&lt;br /&gt;
Wir beschränken uns in dieser Vorlesung auf die Komplexität im ungünstigseten Fall. '''Exakte''' Formeln für Komplexität sind aber auch dann schwer zu gewinnen, wie das folgende Beispiel zeigt:&lt;br /&gt;
&lt;br /&gt;
=== Beispiele aus den Übungen (Gemessene Laufzeiten für Mergesort/Selectionsort) ===&lt;br /&gt;
&lt;br /&gt;
* Mergesort: &amp;lt;math&amp;gt;\frac{0,977N\log N}{\log 2} + 0,267N-4.39 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: andere Lösung: &amp;lt;math&amp;gt;1140 N\log(N) - 1819N + 6413 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* Selectionsort: &amp;lt;math&amp;gt;\frac{1}{2}N^2 - \frac{1}{2N} - 10^{-12} \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: andere Lösung: &amp;lt;math&amp;gt;1275N^2 - 116003^N + 11111144 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Aus diesen Formeln wird nicht offensichtlich, welcher Algorithmus besser ist.&lt;br /&gt;
Näherung: Betrachte nur '''sehr große Eingaben''' (meist sind alle Algorithmen schnell genug für kleine Eingaben). Dieses Vorgehen wird als '''Asymptotische Komplexität''' bezeichnet (N gegen unendlich).&lt;br /&gt;
&lt;br /&gt;
=== Asymptotische Komplexität am Beispiel Polynom ===&lt;br /&gt;
&lt;br /&gt;
Polynom: &amp;lt;math&amp;gt;a\,x^2+b\,x+c=p\!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;x \!&amp;lt;/math&amp;gt; sei die Eingabegröße, und wir betrachten die Entwicklung von &amp;lt;math&amp;gt;p \!&amp;lt;/math&amp;gt; in Abhängigkeit von &amp;lt;math&amp;gt;x \!&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;math&amp;gt;x=0 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=c \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x=1 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=a+b+c \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x=1000 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=1000000a+1000b+c \approx 1000000a\!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x \to \infty \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p \approx x^2a\!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für sehr große Eingaben verlieren also ''b'' und ''c'' immer mehr an Bedeutung, so dass am Ende nur noch ''a'' für die Komplexitätsbetrachtung wichtig ist.&lt;br /&gt;
&lt;br /&gt;
== Landau-Symbole ==&lt;br /&gt;
&lt;br /&gt;
Um die asymptotische Komplexität verschiedener Algorithmen miteinander vergleichen zu können, verwendet man die sogenannten [http://de.wikipedia.org/wiki/Landau-Symbole Landau-Symbole]. Das wichtigste Landau-Symbol ist &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;, mit dem man eine ''obere Schranke'' &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; für die Komplexität angeben kann. &lt;br /&gt;
&lt;br /&gt;
Schreibt man &amp;lt;math&amp;gt;f \in \Omega(g)&amp;lt;/math&amp;gt;, so stellt dies eine asymptotische ''untere Schranke'' für die Funktion f dar.&lt;br /&gt;
&lt;br /&gt;
Schließlich bedeutet &amp;lt;math&amp;gt;f \in \Theta(g)&amp;lt;/math&amp;gt;, dass die Funktion f genauso schnell wie die Funktion g wächst, das heißt man hat eine asymptotisch ''scharfe Schranke'' für f. Hierzu muss sowohl &amp;lt;math&amp;gt;f\in\mathcal{O}(g)&amp;lt;/math&amp;gt; als auch &amp;lt;math&amp;gt;f \in \Omega(g)&amp;lt;/math&amp;gt; erfüllt sein. &lt;br /&gt;
&lt;br /&gt;
Im nun folgenden soll auf die verschiedenen Landau-Symbole noch näher eingegeangen werden.&lt;br /&gt;
&lt;br /&gt;
===O-Notation===&lt;br /&gt;
&lt;br /&gt;
Intuitiv gilt: Für große N dominieren die am schnellsten wachsenden Terme einer Funktion. Die Notation &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; (sprich &amp;quot;f ist in O von g&amp;quot; oder &amp;quot;f ist von derselben Größenordnung wie g&amp;quot;) formalisiert eine solche Abschätzung der asymptotischen Komplexität der Funktion f von oben. &lt;br /&gt;
; Asymptotische Komplexität: Für zwei Funktionen f(x) und g(x) gilt&lt;br /&gt;
::&amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x))&amp;lt;/math&amp;gt;&lt;br /&gt;
: genau dann wenn es eine Konstante &amp;lt;math&amp;gt;c&amp;gt;0&amp;lt;/math&amp;gt; und ein Argument &amp;lt;math&amp;gt;x_0&amp;lt;/math&amp;gt; gibt, so dass &lt;br /&gt;
::&amp;lt;math&amp;gt;\forall x \ge x_0:\quad f(x) \le c\,g(x)&amp;lt;/math&amp;gt;.&lt;br /&gt;
:Die Menge &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; aller durch g(x) abschätzbaren Funktionen ist also formal definiert durch&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(g(x)) = \{ f(x)\ |\ \exists c&amp;gt;0: \forall x \ge x_0: 0 \le f(x) \le c\,g(x)\}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Idee hinter dieser Definition ist, dass g(x) eine wesentlich einfachere Funktion ist als f(x), die sich aber nach geeigneter Skalierung (Multiplikation mit c) und für große Argumente x im wesentlichen genauso wie f(x) verhält. Man kann deshalb in der Algorithmenanalyse f(x) durch g(x) ersetzen. &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x))&amp;lt;/math&amp;gt; spielt für Funktionen eine ähnliche Rolle wie der Operator &amp;amp;le; für Zahlen: Falls a &amp;amp;le; b gilt, kann bei einer Abschätzung von oben ebenfalls a durch b ersetzt werden.&lt;br /&gt;
&lt;br /&gt;
==== Ein einfaches Beispiel ====&lt;br /&gt;
&lt;br /&gt;
[[Image:Sqsqrt.png]]&lt;br /&gt;
&lt;br /&gt;
Rot = &amp;lt;math&amp;gt;x^2 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
Blau = &amp;lt;math&amp;gt;\sqrt{x} \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\sqrt{x} \in \mathcal{O}(x^2)\!&amp;lt;/math&amp;gt; weil &amp;lt;math&amp;gt;\sqrt{x} \le c\,x^2\!&amp;lt;/math&amp;gt; für alle &amp;lt;math&amp;gt;x \ge x_0 = 1 \!&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c = 1\!&amp;lt;/math&amp;gt;, oder auch für &amp;lt;math&amp;gt;x \ge x_0 = 4 \!&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c = 1/16&amp;lt;/math&amp;gt; (die Wahl von c und x&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; in der Definition von O(.) ist beliebig, solange die Bedingungen erfüllt sind).&lt;br /&gt;
&lt;br /&gt;
==== Komplexität bei kleinen Eingaben ==== &lt;br /&gt;
&lt;br /&gt;
Algorithmus 1: &amp;lt;math&amp;gt;\mathcal{O}(N^2) \!&amp;lt;/math&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Algorithmus 2: &amp;lt;math&amp;gt;\mathcal{O}(N\log{N}) \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus 2 ist schneller (von geringerer Komplexität) für große Eingaben, aber bei kleinen Eingaben (insbesondere, wenn der Algorithmus in einer Schleife immer wieder mit kleinen Eingaben aufgerufen wird) könnte Algorithmus 1 schneller sein, falls der in der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation verborgene konstante Faktor ''c'' bei Algorithmus 2 einen wesentlich größeren Wert hat als bei Algorithmus 1.&lt;br /&gt;
&lt;br /&gt;
==== Eigenschaften der O-Notation (Rechenregeln) ==== &lt;br /&gt;
&lt;br /&gt;
# Transitiv:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x)) \land g(x) \in \mathcal{O}(h(x)) \to f(x) \in \mathcal{O}(h(x)) \!&amp;lt;/math&amp;gt;           &lt;br /&gt;
# Additiv:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(h(x)) \land g(x) \in \mathcal{O}(h(x)) \to f(x) + g(x) \in \mathcal{O}(h(x)) \!&amp;lt;/math&amp;gt;           &lt;br /&gt;
# Für Monome gilt:&lt;br /&gt;
#: &amp;lt;math&amp;gt;x^k \in \mathcal{O}(x^k)&amp;lt;/math&amp;gt; und&lt;br /&gt;
#: &amp;lt;math&amp;gt;x^k \in \mathcal{O}(x^{k+j}), \forall j \ge 0 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Multiplikation mit einer Konstanten:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x)) \to c\,f(x) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: andere Schreibweise:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) = c\,g(x) \to f(x) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Folgerung aus 3. und 4. für Polynome: &lt;br /&gt;
#: &amp;lt;math&amp;gt;a_0+a_1\,x + ... + a_n\,x^n \in \mathcal{O}(x^n)\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: Beispiel: &amp;lt;math&amp;gt;a\,x^2+b\,x+c \in \mathcal{O}(x^2)\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Logarithmus:&lt;br /&gt;
#: &amp;lt;math&amp;gt;a, b &amp;gt; 1\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{b}{x})\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: Die Basis des Logarithmus spielt also keine Rolle.&lt;br /&gt;
#: Beweis hierfür:&lt;br /&gt;
#:: &amp;lt;math&amp;gt;\log_{a}{x} = \frac{\log_{b}{x}}{\log_{b}{a}}\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#:: Mit &amp;lt;math&amp;gt;c = 1 / \log_{b}{a}\,&amp;lt;/math&amp;gt; gilt: &amp;lt;math&amp;gt;\log_{a}{x} = c\,\log_{b}{x}\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#:: Wird hier die (zweite) Regel für Multiplikation mit einer Konstanten angewendet, fällt der konstante Faktor weg, also &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{b}{x})\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Insbesondere gilt auch &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{2}{x})\!&amp;lt;/math&amp;gt;, es kann also immer der 2er Logarithmus verwendet werden.&lt;br /&gt;
&lt;br /&gt;
==== O-Kalkül ==== &lt;br /&gt;
&lt;br /&gt;
Das O-Kalkül definiert wichtige Vereinfachungsregeln for Ausdrücke in O-Notation (Beweise: siehe Übungsaufgabe):&lt;br /&gt;
&lt;br /&gt;
# &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(f(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# &amp;lt;math&amp;gt;\mathcal{O}(\mathcal{O}(f(x))) \in \mathcal{O}(f(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# &amp;lt;math&amp;gt;c\,\mathcal{O}(f(x)) \in \mathcal{O}(f(x))\,&amp;lt;/math&amp;gt; für jede Konstante ''c''&lt;br /&gt;
# &amp;lt;math&amp;gt;\mathcal{O}(f(x))+c \in \mathcal{O}(f(x))\,&amp;lt;/math&amp;gt; für jede Konstante ''c''&lt;br /&gt;
# Sequenzregel:&lt;br /&gt;
#: Wenn zwei nacheinander ausgeführte Programmteile die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt; bzw. &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; haben, gilt für beide gemeinsam:&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(f(x))&amp;lt;/math&amp;gt; falls &amp;lt;math&amp;gt;g(x) &amp;lt; \mathcal{O}(f(x))&amp;lt;/math&amp;gt; bzw.&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt; falls &amp;lt;math&amp;gt;f(x) &amp;lt; \mathcal{O}(g(x))&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Informell schreibt man auch: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(max(f(x), g(x)))\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
# Schachtelungsregel bzw. Aufrufregel:&lt;br /&gt;
#: Wenn in einer geschachtelten Schleife die äußere Schleife die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt; hat, und die innere &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt;, gilt für beide gemeinsam:&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) * \mathcal{O}(g(x)) \in \mathcal{O}(f(x) * g(x))\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Gleiches gilt wenn eine Funktion &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt;-mal aufgerufen wird, und die Komplexität der Funktion selbst &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; ist.&lt;br /&gt;
&lt;br /&gt;
;Beispiel für 5.: Beide Schleifen haben die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Dies gilt auch für ihre Hintereinanderausführung:&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          a[i] = i&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          print a[i]&lt;br /&gt;
;Beispiele für 6.: Beide Schleifen haben die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Ihre Verschachtelung hat daher die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N^2)&amp;lt;/math&amp;gt;. &lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          for j in range(N):&lt;br /&gt;
              a[i*N + j] = i+j&lt;br /&gt;
: Dies gilt ebenso, wenn statt der inneren Schleife eine Funktion mit Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt; ausgeführt wird:&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          a[i] = foo(i, N)  # &amp;lt;math&amp;gt;\mathrm{foo}(i, N) \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== O-Kalkül auf das Beispiel des Selectionsort angewandt ==== &lt;br /&gt;
&lt;br /&gt;
Selectionsort: Wir hatten gezeigt dass &amp;lt;math&amp;gt;f(N) = \frac{N^2}{2} - \frac{N}{2}&amp;lt;/math&amp;gt;. Nach der Regel für Polynome vereinfacht sich dies zu &amp;lt;math&amp;gt;f(N) \in \mathcal{O}\left(\frac{N^2}{2}\right) = \mathcal{O}(N^2)\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Alternativ via Schachtelungsregel:&lt;br /&gt;
: Die äußere Schleife wird (''N''-1)-mal durchlaufen: &amp;lt;math&amp;gt;N-1 \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
: Die innere Schleife wird (''N-i''-1)-mal durchlaufen. Das sind im Mittel ''N''/2 Durchläufe: &amp;lt;math&amp;gt;N/2 \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
: Zusammen: &amp;lt;math&amp;gt;\mathcal{O}(N)*\mathcal{O}(N) \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nach beiden Vorgehensweisen kommen wir zur Schlussfolgerung, dass der Selectionsort die asymptotische Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N^2)\!&amp;lt;/math&amp;gt; besitzt.&lt;br /&gt;
&lt;br /&gt;
==== Zusammenhang zwischen Komplexität und Laufzeit ==== &lt;br /&gt;
&lt;br /&gt;
Wenn eine Operation 1ms dauert, erreichen Algorithmen verschiedener Komplexität folgende Leistungen (wobei angenommen wird, dass der in der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation verborgene konstante Faktor immer etwa gleich 1 ist):&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:left&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
|-&lt;br /&gt;
! Komplexität !! Operationen in 1s !! Operationen in 1min !! Operationen in 1h&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 1000 || 60.000 || 3.600.000&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N\log_2{N})&amp;lt;/math&amp;gt;&lt;br /&gt;
| 140 || 4895 || 204094&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 32 || 245 || 1898&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 10 || 39 || 153&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(2^N)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 10 || 16 || 21&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Exponentielle Komplexität ==== &lt;br /&gt;
Der letzte Fall &amp;lt;math&amp;gt;\mathcal{O}(2^N)&amp;lt;/math&amp;gt; ist von exponentieller Komplexität. Das bedeutet, dass eine Verdopplung des Aufwands nur bewirkt, dass die maximale Problemgröße um eine Konstante wächst. Algorithmen mit exponentieller (oder noch höherer) Komplexität werden deshalb als '''ineffizient''' bezeichnet. Algorithmen mit höchstens polynomieller Komplexität gelten hingegen als effizient.&lt;br /&gt;
&lt;br /&gt;
In der Praxis sind allerdings auch polynomielle Algorithmen mit hohem Exponenten meist zu langsam. Als Faustregel kann man eine praktische Grenze von &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt; ansehen. Bei einer Komplexität von &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt; bewirkt ein verdoppelter Aufwand immer noch eine Steigerung der maximalen Problemgröße um den Faktor &amp;lt;math&amp;gt;\sqrt[3]{2}&amp;lt;/math&amp;gt; (also eine ''multiplikative'' Vergrößerung um ca. 25%, statt nur einer additiven Vergrößerung wie bei exponentieller Komplexität).&lt;br /&gt;
&lt;br /&gt;
===&amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;- Notation===&lt;br /&gt;
&lt;br /&gt;
Genauso wie &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; eine Art &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;-Operator für Funktionen ist, definiert &amp;lt;math&amp;gt;f \in \Omega(g) &amp;lt;/math&amp;gt; eine Abschätzung von unten, analog zum &amp;lt;math&amp;gt;\ge&amp;lt;/math&amp;gt;-Operator für Zahlen. Formal kann man &amp;lt;math&amp;gt;f(N) \in \Omega(g(N)) &amp;lt;/math&amp;gt; genau dann schreiben, falls es eine Konstante &amp;lt;math&amp;gt; c &amp;gt; 0 &amp;lt;/math&amp;gt; gibt, so dass &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; f(N) \ge c \cdot g(N) &amp;lt;/math&amp;gt; für &amp;lt;math&amp;gt; N  \ge N_0 &amp;lt;/math&amp;gt; &lt;br /&gt;
&lt;br /&gt;
gilt.&lt;br /&gt;
Man verwendet diese Notation also um abzuschätzen, wie groß der Aufwand (die Komplexität) für einen bestimmten Algorithmus ''mindestens'' ist und nicht ''höchstens'', was man mit der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt; - Notation ausdrücken würde.&lt;br /&gt;
&lt;br /&gt;
Ein praktisches Beispiel für eine Anwendung der &amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;- Notation wäre die Fragestellung, ob es ''prinzipiell'' einen besseren Algorithmus für ein bestimmtes Problem gibt. Wie später im Abschnitt [[Suchen#Sortieren_als_Suchproblem|Sortieren als Suchproblem]] gezeigt wird, ist das Sortieren eines Arrays durch paarweise Vergleiche von Elementen immer mindestens von der Komplexität &amp;lt;math&amp;gt; \Omega(N\cdot \ln N) &amp;lt;/math&amp;gt;, was konkret bedeutet, dass kein Sortieralgorithmus, der nach diesem Prinzip arbeitet, jemals eine geringere Komplexität als beispielsweise Merge-Sort haben wird. Natürlich kann man den entsprechenden Sortieralgorithmus, also Merge-Sort zum Beispiel, unter Umständen noch optimieren, aber die Komplexität wird erhalten bleiben. Mit diesem Wissen kann man sich viel (vergebliche) Arbeit sparen.&lt;br /&gt;
&lt;br /&gt;
===&amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;- Notation===&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;math&amp;gt;f(N) \in \Theta(g(N))&amp;lt;/math&amp;gt; ist eine scharfe Abschätzung der asymptotischen Komplexität einer Funktion f. &lt;br /&gt;
&lt;br /&gt;
Damit dies gilt, muss &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; und ''gleichzeitig'' &amp;lt;math&amp;gt;f(N) \in \Omega(g(N))&amp;lt;/math&amp;gt; erfüllt sein.&lt;br /&gt;
&lt;br /&gt;
Dies ist natürlich auch die beste Abschätzung der asymptotischen Komplexität einer Funktion f. Formal bedeutet &amp;lt;math&amp;gt;f(N) \in \Theta(g(N))&amp;lt;/math&amp;gt; dass es zwei Konstanten &amp;lt;math&amp;gt; c_1 &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; c_2 &amp;lt;/math&amp;gt;, beide größer als Null, gibt, so dass für alle &amp;lt;math&amp;gt; N \geq N_0 &amp;lt;/math&amp;gt; gilt: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; c_1 \cdot g(N) \leq f(N) \leq c_2 \cdot g(N) &amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In der Praxis wird manchmal statt der &amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;-Notation auch dann die &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation benutzt, wenn eine scharfe Schranke ausgedrückt werden soll. Dies ist zwar formal nicht korrekt, aber man kann die intendierte Bedeutung meist aus dem Kontext erschließen.&lt;br /&gt;
&lt;br /&gt;
== Komplexitätsvergleich zweier Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
In diesem Abschnitt wollen wir der Frage nachgehen, wie ein formaler Beweis für die Behauptung &amp;lt;math&amp;gt; f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; geschehen kann. Hierbei werden zwei Beweismethoden vorgestellt werden, und zwar der '''Beweis über die Definition der Komplexität''' sowie der '''Beweis durch Dividieren'''.&lt;br /&gt;
&lt;br /&gt;
===Beweis über die Definition der asymptotischen Komplexität===&lt;br /&gt;
&lt;br /&gt;
Die Definition der asymptotischen Komplexität &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; war: &lt;br /&gt;
&lt;br /&gt;
Es gibt eine Konstante &amp;lt;math&amp;gt; c &amp;gt; 0 &amp;lt;/math&amp;gt;, so dass &amp;lt;math&amp;gt; f(N) \le c \cdot g(N) &amp;lt;/math&amp;gt; für &amp;lt;math&amp;gt; N  \ge N_0 &amp;lt;/math&amp;gt; erfüllt ist. &lt;br /&gt;
&lt;br /&gt;
Um also die die asymptotische Komplexität &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; zu beweisen, muss man die oben erwähnten Konstanten c und &amp;lt;math&amp;gt; N_0 &amp;lt;/math&amp;gt; finden, so dass &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; f(N) \leq c \cdot g(N) &amp;lt;/math&amp;gt; für alle &amp;lt;math&amp;gt; N \ge N_0 &amp;lt;/math&amp;gt; erfüllt ist. &lt;br /&gt;
&lt;br /&gt;
Dies geschieht zweckmäßigerweise mit dem Beweisprinzip der ''vollständigen Induktion''. Hierbei ist zu zeigen, dass&lt;br /&gt;
# &amp;lt;math&amp;gt; f(N_0) \leq g(N_0) &amp;lt;/math&amp;gt; für die eine zu bestimmende Konstante &amp;lt;math&amp;gt; N_0 &amp;lt;/math&amp;gt; gilt (''Induktionsanfang'') und &lt;br /&gt;
# falls &amp;lt;math&amp;gt; f(N) \leq g(N) &amp;lt;/math&amp;gt;, dann auch &amp;lt;math&amp;gt; f(N+1) \leq g(N+1) &amp;lt;/math&amp;gt; (''Induktionsschritt'') gilt.&lt;br /&gt;
&lt;br /&gt;
===Beweis durch Dividieren===&lt;br /&gt;
&lt;br /&gt;
Hierbei wählt man eine Konstante c und zeigt, dass &amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f(N)}{c \cdot g(N)} \leq 1 &amp;lt;/math&amp;gt; gilt (für die O-Notation, bei &amp;amp;Omega;-Notation gilt entsprechend &amp;lt;math&amp;gt;\geq 1 &amp;lt;/math&amp;gt;). Man kann dies auch als alternative Definition der Komplexität verwenden.&lt;br /&gt;
&lt;br /&gt;
Als Beispiel betrachten wir die beiden Funktionen &amp;lt;math&amp;gt; f(N) = N \,\lg N &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; g(N) = N^2 &amp;lt;/math&amp;gt; und wollen zeigen, dass &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; gilt. &lt;br /&gt;
&lt;br /&gt;
Als Konstante c wählen wir &amp;lt;math&amp;gt; c = 1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f(N)}{g(N)} = \lim_{N \rightarrow \infty} \frac{\lg N}{N} = \frac{\infty}{\infty} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Unbestimmte Ausdrücke der Form &lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{x \rightarrow x_0} \frac{f(x)}{g(x)} &amp;lt;/math&amp;gt;,&lt;br /&gt;
in denen sowohl &amp;lt;math&amp;gt; f(x) &amp;lt;/math&amp;gt; als auch &amp;lt;math&amp;gt; g(x) &amp;lt;/math&amp;gt; mit &amp;lt;math&amp;gt; x \rightarrow x_0 &amp;lt;/math&amp;gt; gegen Null oder gegen Unendlich streben, kann man manchmal mit den Regeln von [http://de.wikipedia.org/wiki/L%27Hospital%27sche_Regel ''l'Hospital''] berechnen. Danach darf man die Funktionen f und g zur Berechnung des unbestimmten Ausdrucks durch ihre k-ten Ableitungen ersetzen:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{x \rightarrow x_0} \frac{f(x)}{g(x)} = \lim_{x \rightarrow x_0} \frac{f^{(k)}(x)}{g^{(k)}(x)} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In unserem Fall verwenden wir die erste Ableitung und erhalten:&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f'(x)}{g'(x)} = \lim_{N \rightarrow \infty} \frac{1/N}{1} \rightarrow 0 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Damit wurde &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt;, also &amp;lt;math&amp;gt;N \lg N \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt; gezeigt.&lt;br /&gt;
&lt;br /&gt;
Man beachte hierbei, dass &amp;lt;math&amp;gt;N \lg N \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt; keine enge Grenze für die Komplexität von &amp;lt;math&amp;gt;N \,\lg N&amp;lt;/math&amp;gt; darstellt, da der Grenzwert &amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f'(x)}{g'(x)}\, &amp;lt;/math&amp;gt; gegen 0 und nicht gegen eine von Null verschiedene Konstante strebt. In diesem Fall haben wir die Komplexität von &amp;lt;math&amp;gt;N \cdot \lg N &amp;lt;/math&amp;gt; also nur nach oben abschätzen können.&lt;br /&gt;
&lt;br /&gt;
===Beispiel für den Komplexitätsvergleich: Gleitender Mittelwert (Running Average)===&lt;br /&gt;
&lt;br /&gt;
Wir berechnen für ein gegebenes Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; einen gleitenden Mittelwert über &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; Elemente:&amp;lt;br/&amp;gt;&lt;br /&gt;
::&amp;lt;math&amp;gt;r_i = \frac{1}{k} \sum_{j=i-k+1}^i a_j&amp;lt;/math&amp;gt; &amp;lt;br/&amp;gt;&lt;br /&gt;
Das heisst, für jedes &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; mitteln wir die letzten &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; Elemente von &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; und schreiben das Ergebnis in &amp;lt;tt&amp;gt;r[i]&amp;lt;/tt&amp;gt;. Diese Operation ist z.B. bei Börsenkursen wichtig: Neben dem aktuellen Kurs für jeden Tag wird dort meist auch der gleitende Mittelwert der letzten 30 Tage sowie der letzten 200 Tage angegeben. In diesen Mittelwerten erkennt man besser die langfristige Tendenz, weil die täglichen Schwankungen herausgemittelt werden. Wir nehmen außerdem an, dass&lt;br /&gt;
* Array-Zugriff hat eine Komplexit&amp;amp;auml;t von O(1)&lt;br /&gt;
* &amp;lt;math&amp;gt;k \ll N&amp;lt;/math&amp;gt;, d.h. &amp;lt;math&amp;gt;N-k\approx N&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die beiden folgenden Algorithmen berechnen die Mittelwerte auf unterschiedliche Art. Der linke folgt der obigen Definition durch eine Summe, während der rechte inkrementell arbeitet: Man kann den Bereich der &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; letzten Werte als Fenster betrachten, das über das Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; geschoben wird. Schiebt man das Fenster ein Element weiter, fällt links ein Element heraus, und rechts kommt eins hinzu. Man muss also nicht jedes Mal die Summe neu berechnen, sondern kann den vorigen Wert aktualisieren. Wir werden sehen, dass dies Folgen für die Komplexität des Algorithmus hat.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;2&amp;quot; &lt;br /&gt;
|-&lt;br /&gt;
! Programmzeile&lt;br /&gt;
! Version 1: O(N * k)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
! Version 2: O(N)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
1.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
2.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
3.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
4.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k-1, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for i in range(k):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
5.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;for i in range(j-k+1, j+1):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[k-1] += a[i]&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
6.&lt;br /&gt;
|&lt;br /&gt;
:::: &amp;lt;tt&amp;gt;r[j] += a[i]&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
7.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] = (a[j] - a[j-k] + r[j-1])&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
8.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
9.&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
10.&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Wir zeigen unten dass Version 2 eine geringere Komplexit&amp;amp;auml;t besitzt, obwohl sie mehr Zeilen ben&amp;amp;ouml;tigt.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Wir haben in der Tabelle die Komplexität jeder Zeile für sich angegeben. Einfache Anweisungen (Berechnungen, Lese- und Schreibzugriffe auf das Array, Zuweiseungen) haben konstante Komplexität, die Komplexität des Schleifenkopfes allein (also der &amp;lt;tt&amp;gt;for&amp;lt;/tt&amp;gt;-Anweisung ohne den eingerückten Schleifenkörper) entspricht der Anzahl der Durchläufe. Wir müssen jetzt noch die Verschachtelung der Schleifen und die Nacheinanderausführung von Anweisungen berücksichtigen. &lt;br /&gt;
&lt;br /&gt;
====Berechnung der Komplexität von Version 1====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;small&amp;gt;(Wiederholung der Rechenregeln: siehe Abschnitt [[Effizienz#O-Notation|O-Notation]])&amp;lt;/small&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wir betrachten zunächst die innere Schleife (Zeilen 5 und 6 von Version 1):&lt;br /&gt;
&lt;br /&gt;
Der Schleifenkopf (Zeile 5) hat die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt;, weil die Schleife k-mal durchlaufen wird. Der Schleifenkörper (Zeile 6) hat die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;. Nach der Verschachtelungsregel müssen wir die beiden Komplexitäten multiplizieren, und es ergibt sich:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(k)\cdot\mathcal{O}(1) = \mathcal{O}(k\cdot 1)=\mathcal{O}(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wir betrachten nun die äußere Schleife. Der Schleifenkopf (Zeile 4) wird (N-k)-mal durchlaufen und hat somit eine Komplexität von &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Der Schleifenkörper (Zeilen 5 bis 7) besteht aus der inneren Schleife (Zeilen 5 und 6) mit der gerade berechneten Komplexität &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt; sowie einer einfachen Anweisung (Zeile 7) mit Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;. Nach der Sequenzregel wird die Komplexität des Schleifenkörpers durch Addition berechnet:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(k)+\mathcal{O}(1) = \mathcal{O}(\max(k,1)) = \mathcal{O}(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexität der gesamten äußeren Schleife erhalten wir nach der Verschachtelungsregel wieder durch multiplizieren:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)\cdot\mathcal{O}(k) = \mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die übrigen Schritte des Algorithmus werden einfach nacheinander ausgeführt, so dass sie ebenfalls nach der Sequenzregel behandelt werden. Wir erhalten&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)+\mathcal{O}(1)+\mathcal{O}(N\cdot k)+\mathcal{O}(1) = \mathcal{O}(\max(N,1,N\cdot k,1)) = \mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Der gesamte Algorithmus hat also die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Berechnung der Komplexität von Version 2====&lt;br /&gt;
&lt;br /&gt;
Hier gibt es nur einfache Schleifen ohne Verschachtelung. Da der Schleifenkörper jeder Schleife nur einfache Anweisungen der Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt; enthält, ergibt sich die Komplexität der Schleifen nach der Verschachtelungsregel als&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(X)\cdot\mathcal{O}(1) = \mathcal{O}(X\cdot 1)=\mathcal{O}(X)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
wobei &amp;lt;math&amp;gt;\mathcal{O}(X)&amp;lt;/math&amp;gt; die Komplexität des jeweiligen Schleifenkopfes ist. Wir erhalten also für Zeilen 4 und 5: &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt;, Zeilen 6 und 7: &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;, Zeilen 8 und 9: &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Die Hintereinanderausführung wird nach der Sequenzregel behandelt:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)+\mathcal{O}(1)+\mathcal{O}(k)+\mathcal{O}(N)+\mathcal{O}(N)+\mathcal{O}(1) = \mathcal{O}(\max(N,1,k,N,N,1)) = \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Dieser Algorithmus hat also nur die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Fazit====&lt;br /&gt;
&lt;br /&gt;
Obwohl Version 2 mehr Schritte benötigt hat sie eine geringere Komplexität, da die for-Schleifen nicht wie bei Version 1 verschachtelt/untergeordnet sind. Bei verschachtelten for-Schleifen muss die Multiplikationsregel angewendet werden &amp;amp;rarr; höhere Komplexität.&lt;br /&gt;
&lt;br /&gt;
Die gerade berechnete Komplexität gilt aber &amp;lt;u&amp;gt;nur&amp;lt;/u&amp;gt; unter der Annahme, dass Array-Zugriffe konstante Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt; besitzen. Wenn dies nicht der Fall ist, kann sich die Komplexität des Algorithmus drastisch verschlechtern.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|Allgemein gilt:&amp;lt;br/&amp;gt;&lt;br /&gt;
Algorithmen-Analysen beruhen auf der Annahme, dass Zugriffe auf die Daten optimal schnell sind, dass heißt, dass die für den jeweiligen Algorithmus am besten geeignete Datenstruktur verwendetet wird.&amp;lt;br /&amp;gt; &amp;amp;rarr; Ansonsten: Komplexitätsverschlechterung!&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Beispiel für eine Verschlechterung der Komplexität durch Verwendung einer nicht optimalen Datenstruktur====&lt;br /&gt;
&lt;br /&gt;
Wir verwende im Mittelwert-Algorithmus eine verkettete Liste anstelle des Eingabe-Arrays &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt;. Wir benötigen dazu eine Funktion, die das j-te Element der Liste zurückgibt. Wie üblich ist die Liste mit Hilfe einer Knotenklasse implementiert:&lt;br /&gt;
      class Node:&lt;br /&gt;
          def __init__(self, data):&lt;br /&gt;
              self.data = data&lt;br /&gt;
              self.next = None&lt;br /&gt;
&lt;br /&gt;
Die Listenklasse selbst hat ein Feld &amp;lt;tt&amp;gt;head&amp;lt;/tt&amp;gt;, das eine Referenz auf den ersten Knoten speichert, und jeder Knoten speichert im Feld &amp;lt;tt&amp;gt;next&amp;lt;/tt&amp;gt; eine Referenz auf seinen Nachfolger. Um zum j-ten Element zu gelangen, muss man die Liste sequenziell durchlaufen&lt;br /&gt;
      def get_jth(list, j):&lt;br /&gt;
           r = list.head&lt;br /&gt;
           while j &amp;gt; 0:&lt;br /&gt;
               r = r.head&lt;br /&gt;
               j -= 1&lt;br /&gt;
           return r.data&lt;br /&gt;
Die Komplexität dieser Funktion ist offensichtlich &amp;lt;math&amp;gt;\mathcal{O}(j)&amp;lt;/math&amp;gt; (Komplexitätsberechnung wie oben). Wir setzen jetzt bei Version 1 des Mittelwert-Algorithmus diese Funktion in Zeile 6 anstelle des Indexzugriffs &amp;lt;tt&amp;gt;a[i]&amp;lt;/tt&amp;gt; ein (nur in dieser Zeile wird auf die Elemente des Arrays zugegriffen). Wir erhalten folgende Implementation (die Änderungen sind rot markiert):&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;2&amp;quot; &lt;br /&gt;
|-&lt;br /&gt;
! Programmzeile&lt;br /&gt;
! Version 1 mit Liste: O(N * k)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
1.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
2.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
3.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
4.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k-1, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
5.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;for i in range(j-k+1, j+1):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
6.&lt;br /&gt;
|&lt;br /&gt;
:::: &amp;lt;tt&amp;gt;r[j] += &amp;lt;font color=red&amp;gt;get_jth(a, i)&amp;lt;/font&amp;gt;&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;&amp;lt;font color=red&amp;gt;O(i)&amp;lt;/font&amp;gt;&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
7.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
8.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Der Aufruf der Funktion &amp;lt;tt&amp;gt;get_jth&amp;lt;/tt&amp;gt; ist jetzt gleichbedeutend mit einer dreifach verschachtelten Schleife (weil &amp;lt;tt&amp;gt;get_jth&amp;lt;/tt&amp;gt; ja eine zusatzliche Schleife enthält). Die Anzahl der Operationen in Zeile 4 bis 6 ist jetzt&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;f(N,k)=\sum_{j=k-1}^{N-1}\,\sum_{i=j-k+1}^j\,\mathcal{O}(i)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
wobei das &amp;lt;math&amp;gt;\mathcal{O}(i)&amp;lt;/math&amp;gt; die neue Schleife durch Verwendung der Liste repräsentiert. Mit Mathematica-Hilfe [http://www.wolfram.com/] lässt sich diese Summe exakt ausrechnen&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;f(N,k)=\frac{1}{2}(k N^2-k^2 N+k^2-k)\in \mathcal{O}(k N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexitätsberechnung erfolgte dabei nach der Regel für Polynome unter Beachtung von &amp;lt;math&amp;gt;k \ll N&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Fazit:====&lt;br /&gt;
&lt;br /&gt;
Die Komplexität von Version 1 mit einer verketteten Liste wäre O(N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; * k)&lt;br /&gt;
'''&amp;amp;rarr; Die richtige Datenstruktur ist wichtig, da es sonst zu einer Komplexitätsverschlechterung kommen kann!'''&lt;br /&gt;
&lt;br /&gt;
Auf Version 2 unseres Running Average-Beispiels hätte eine verkettete Liste allerdings keine Auswirkungen, da die inkrementelle Berechnung der Summen in Zeile 7 weiterhin möglich ist (bei geschickter Implementation!) und somit Version 2 immer noch eine Komplexität von O(N) hätte.&lt;br /&gt;
&lt;br /&gt;
==Amortisierte Komplexität==&lt;br /&gt;
&lt;br /&gt;
Bis jetzt wurde die Komplexität nur im schlechtesten Fall (Worst Case) betrachtet. Bei einigen Operationen schwankt die Komplexität jedoch sehr stark, wenn man sie mehrmals hintereinander ausführt, und der schlechteste Fall kommt nur selten vor. Dann ist es sinnvoll, die &amp;lt;i&amp;gt;amortisierte Komplexität&amp;lt;/i&amp;gt; zu betrachten, die sich mit der &amp;lt;i&amp;gt;durchschnittlichen&amp;lt;/i&amp;gt; Komplexität über viele Aufrufe der selben Operation beschäftigt.&lt;br /&gt;
&lt;br /&gt;
Zum weiter Lesen: [[http://de.wikipedia.org/wiki/Amortisierte_Laufzeitanalyse Wikipedia: Amortisierte Laufzeitanalyse]]&lt;br /&gt;
&lt;br /&gt;
===Beispiel: Inkrementieren von Binärzahlen===&lt;br /&gt;
&lt;br /&gt;
Frage: Angenommen, das Umdrehen eines Bits einer Binärzahl verursacht Kosten von 1 Einheit. Wir erzeugen die Folge der natürlichen Zahlen in Binärdarstellung durch sukzessives Inkrementieren, von Null beginnend. Bei jeder Inkrementierung werden einige Bits verändert, aber diese Zahl (und damit die Kosten der Inkrementierungen) ''schwanken'' sehr stark. Wir fragen jetzt, was eine Inkrementierung im Durchschnitt kostet?&lt;br /&gt;
&lt;br /&gt;
Um diese Durchschnittskosten zu berechnen, bezahlen wir bei jeder Inkrementierung 2 Einheiten. Wenn davon nach Abzug der Kosten der jeweiligen Operation noch etwas übrig bleibt, wird der Rest dem  Guthaben zugeschrieben. Umgekehrt wird ein eventueller Fehlbetrag (wenn eine Inkrementierung mehr als 2 Bits umdreht) aus dem Guthaben gedeckt. Dadurch werden die ansonsten großen Schwankungen der Kosten ausgeglichen:&lt;br /&gt;
:: Kosten &amp;lt; Einzahlung &amp;amp;rarr; es wird gespart&lt;br /&gt;
:: Kosten = Einzahlung &amp;amp;rarr; Guthaben bleibt unverändert&lt;br /&gt;
:: Kosten &amp;gt; Einzahlung &amp;amp;rarr; Guthaben wird für die Kosten verbraucht&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
!Schritte&lt;br /&gt;
!Zahlen&lt;br /&gt;
!Kosten &amp;lt;br/&amp;gt;&lt;br /&gt;
(Anzahl der geänderten Bits)&lt;br /&gt;
! Einzahlung&lt;br /&gt;
!Guthaben =&amp;lt;br/&amp;gt;&lt;br /&gt;
altes Guthaben + Einzahlung - Kosten&lt;br /&gt;
|-&lt;br /&gt;
|1.&lt;br /&gt;
|0000&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|2.&lt;br /&gt;
|000&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|2&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|3.&lt;br /&gt;
|0001&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|4.&lt;br /&gt;
|00&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|3&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|5.&lt;br /&gt;
|0010&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|6.&lt;br /&gt;
|001&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|2&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|7.&lt;br /&gt;
|0011&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''3'''&lt;br /&gt;
|-&lt;br /&gt;
|8.&lt;br /&gt;
|0&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1000&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|4&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Die Kosten ergeben sich aus der Anzahl der Ziffern die von 1 nach 0, bzw. von 0 nach 1 verändert werden&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Rechnung:&amp;lt;/u&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Schritt: Kosten: 1 &amp;lt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Schritt: Kosten: 2 = Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird nicht gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; Guthaben bleibt so wie es ist &amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Schritt: Kosten: 1 &amp;lt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. Schritt: Kosten: 3 &amp;gt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird eine 1 vom Guthaben genommen um die Kosten zu zahlen&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
usw.&lt;br /&gt;
&lt;br /&gt;
Man erkennt, dass vor teuren Operation (Wechsel von 3 auf 4 bzw. von 7 auf 8) genügend Guthaben angespart wurde, um die Kosten zu decken. Das Guthaben geht bei diesen Operationen immer wieder auf 1 zurück, aber es wird nie vollständig verbraucht (Dies kann natürlich auch mathematisch exakt bewiesen werden, wie wir es unten am Beispiel des dynamische Arrays zeigen). Wir schließen daraus, dass die durchschnittlichen oder '''amortisierten Kosten''' einer Inkrementierungsoperation gleich 2 sind.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Zum weiter Lesen: [[http://de.wikipedia.org/wiki/Account-Methode Wikipedia Account-Methode]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
====Fazit====&lt;br /&gt;
Die amortisierte Komplexität beschäftigt sich mit dem Durchschnitt aller Operation im ungünstigsten Fall. Operationen mit hohen Kosten, die aber nur selten ausgeführt werden, fallen bei der amortisierten Komplexität nicht so ins Gewicht. Bei Algorithmen, die gelegentlich eine &amp;quot;teure&amp;quot; Operation benutzen, ansonsten jedoch &amp;quot;billige&amp;quot; Operationen aufrufen, kann die amortisierte Komplexität niedriger sein als die Komplexität im schlechtesten (Einzel-)Fall.&lt;br /&gt;
&lt;br /&gt;
In unserem Beispiel fallen die teuren Einzelschritte (z.B. 4. und 8. Schritt) bei den amortisierten Kosten nicht so ins Gewicht, da wir die Kosten aus unserem Guthaben mitbezahlen können. Das Guthaben ist immer groß genug, weil jeder zweite Aufruf eine billige Operation ist, die nur ein Bit umdreht und somit das Ansparen ermöglicht. Diese Betrachtung zeigt, dass die amortisierte (d.h. durchschnittliche) Komplexität des Algoithmus niedriger (nämlich konstant) ist als die Komplexität im schlechtesten Fall.&lt;br /&gt;
&lt;br /&gt;
===Anwendung: Dynamisches Array===&lt;br /&gt;
&lt;br /&gt;
Ein dynamisches Array hat die Eigenschaft, dass man effizient am Ende des Arrays neue Elemente anfügen kann, indem man die Länge des Arrays entsprechend vergrößert (siehe Übung 1). Die Analyse der amortisierten Komplexität der Anfüge-Operation zeigt uns, wie man das Vergrößern des Arrays richtig implementiert, damit die Operation wirklich effizient abläuft.&lt;br /&gt;
&lt;br /&gt;
==== Ineffiziente naive Lösung ====&lt;br /&gt;
&lt;br /&gt;
Wenn wir an ein Array ein Element anhängen wollen, müssen wir neuen Speicher allokieren, der die gewünschte Länge hat. Die Werte aus dem alten Array müssen dann in den neuen Speicher umkopiert werden. Danach kann das neue Element hinten angefügt werden, weil wir im neuen Array bereits Speicher für dieses Element reserviert haben. Bei der naiven Implementation des dynamischen Arrays wiederholt man dies bei jeder Anfügeoperation. Für die Analyse nehmen wir an, dass das Kopieren eines Elements konstante Zeit O(1) erfordert, ebenso das Einfügen eines neuen Elements auf in eine noch unbenutzte Speicherposition.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Naives Anhängen eines weiteren Elements an ein Array:&amp;lt;/u&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;right&amp;quot;&lt;br /&gt;
!Schritte&lt;br /&gt;
|'''Array'''&lt;br /&gt;
&amp;lt;small&amp;gt;(wie es nach jedem Schritt aussieht)&amp;lt;/small&amp;gt;&lt;br /&gt;
!Komplexität&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;center&amp;gt;altes Array (N=4)&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|1. neuer Speicher für&amp;lt;br&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;(N+1) Elemente&lt;br /&gt;
|&amp;lt;center&amp;gt;[None,None,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;O(N+1) = '''O(N)'''&amp;lt;/center&amp;gt;(wenn der Speicher initialisiert wird&amp;lt;br&amp;gt;(hier auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;), sonst O(1))&lt;br /&gt;
|-&lt;br /&gt;
|2. Kopieren &lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;'''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|3. append von &amp;quot;x&amp;quot;&lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3,'x']&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;'''O(1)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
altesArray = [0,1,2,3]&amp;lt;br /&amp;gt;&lt;br /&gt;
altesArray.append('x')&lt;br /&gt;
&lt;br /&gt;
1. Es wird ein neues Array der Größe N+1 erzeugt&amp;lt;br /&amp;gt;&lt;br /&gt;
2. Die N Datenelemente aus dem alten Array werden in das neue Array kopiert&amp;lt;small&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Das sind N Operationen der Komplexität O(1), also ein Gesamtaufwand von O(N).&amp;lt;/small&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
3. 'x' wird mit Aufwand O(1) an die letzte Stelle des neuen Arrays geschrieben&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Additionsregel:&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
O(N) + O(1) &amp;amp;isin; O(N)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Folgerung:&amp;lt;/b&amp;gt; &lt;br /&gt;
&lt;br /&gt;
Bei der naiven Methode erfordert jede Anfügung einen Aufwand O(N) (wobei N die derzeitige Arraygröße ist). Dies ist nicht effizient.&lt;br /&gt;
&lt;br /&gt;
====Effiziente Lösung durch Verdoppeln der Kapazität====&lt;br /&gt;
&lt;br /&gt;
Offensichtlich kommt man nicht darum heraum, den Inhalt des alten Arrays zu kopieren, wenn der allokierte Speicher voll ist. Der Trick für die effiziente Implementation der Anfügeoperation besteht darin, das Kopieren so selten wie möglich durchzuführen, also nicht wie in der naiven Lösung bei jeder Anfügeoperation. Hier kommt die amortisierte Komplexität ins Spiel: Ab und zu gibt es eine teure Anfügeoperation (wenn nämlich kopiert werden muss), aber wenn man den durchschnittlichen Aufwand über viele Anfügungen betrachtet, ist die Operation effizient. Der teure Fall wird sozusagen &amp;quot;herausgemittelt&amp;quot;. &lt;br /&gt;
&lt;br /&gt;
Um nur selten kopieren zu müssen, werden beim dynamischen Array mehr Speicherelemente reserviert als zur Zeit benötigt werden (in der naiven Lösung wurde dagegen immer nur Speicher für ein einziges neues Element reserviert). Wir unterscheiden deshalb &lt;br /&gt;
&lt;br /&gt;
:&amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; = Anzahl der allokierten Speicherzellen, d.h. der möglichen Elemente, die in das Array passen&amp;lt;br /&amp;gt;&lt;br /&gt;
:&amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt; = Anzahl der Elemente, die im Array zur Zeit gespeichert sind&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Daten selbst werden in einem statischen Array gespeichert:&lt;br /&gt;
:&amp;lt;tt&amp;gt;data&amp;lt;/tt&amp;gt; = statisches Array der Größe &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die folgende intuitive Abschätzung zeigt, dass es sinnvoll ist, die Größe des allokierten Speichers jeweils zu verdoppeln. Wir starten bei einem Array der Größe &amp;lt;tt&amp;gt;size = capacity&amp;lt;/tt&amp;gt; = N. Da der verfügbare Speicher voll ist, müssen wir bei der nächsten Anfügung die N vorhandenen Elemente in ein neues Array der Länge &amp;lt;tt&amp;gt;new_capacity&amp;lt;/tt&amp;gt; kopieren (Aufwand &amp;lt;math&amp;gt;N\cdot O(1)&amp;lt;/math&amp;gt;). Danach können wir K Elemente billig einfügen (Aufwand &amp;lt;math&amp;gt;K\cdot O(1)&amp;lt;/math&amp;gt;), wobei &lt;br /&gt;
:K = &amp;lt;tt&amp;gt;new_capacity - capacity&amp;lt;/tt&amp;gt; &lt;br /&gt;
die Anzahl der nach dem Kopieren noch unbenutzen Speicherzellen ist. Der durchschnittliche Aufwand für diese K Einfügungen ist somit&lt;br /&gt;
:&amp;lt;math&amp;gt;\bar T = \frac{N \cdot O(1) + K \cdot O(1)}{K}=\frac{N+K}{K}\cdot O(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Damit die mittlere Zeit in O(1) sein kann, muss der Quotient &amp;lt;math&amp;gt;(N+K)/K&amp;lt;/math&amp;gt; eine Konstante sein. Wir setzen &amp;lt;math&amp;gt;K = a N&amp;lt;/math&amp;gt; und erhalten:&lt;br /&gt;
:&amp;lt;math&amp;gt;\bar T = \frac{(a+1)N}{a N}\cdot O(1)=\frac{a+1}{a}\cdot O(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Der amortisierte Aufwand über K Einfügungen ist also konstant, wenn &amp;lt;math&amp;gt;a&amp;lt;/math&amp;gt; eine (kleine) von N unabhängige Zahl ist. Typischerweise wählt man &lt;br /&gt;
:&amp;lt;math&amp;gt;a = 1&amp;lt;/math&amp;gt;&lt;br /&gt;
und mit &amp;lt;math&amp;gt;K = 1\cdot N&amp;lt;/math&amp;gt; ergibt sich&lt;br /&gt;
:&amp;lt;tt&amp;gt;new_capacity = capacity&amp;lt;/tt&amp;gt; + N = &amp;lt;tt&amp;gt;2 * capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Vorgehensweise beim Zufügen eines neuen Elements im Fall &amp;lt;tt&amp;gt;size == capacity&amp;lt;/tt&amp;gt; ist also&lt;br /&gt;
* capacity wird verdoppelt&amp;lt;br /&amp;gt;&lt;br /&gt;
: &amp;lt;tt&amp;gt;neue capacity = 2 * alte capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
: (allgemein genügt es auch, wenn capacity um einen bestimmten Prozentsatz vergrößert wird, &lt;br /&gt;
:: &amp;lt;tt&amp;gt;neue capacity = alte capacity * c&amp;lt;/tt&amp;gt; &lt;br /&gt;
: mit c &amp;gt; 1, z.B. c = 1.2, das entspricht oben der Wahl &amp;lt;math&amp;gt;a = 0.2&amp;lt;/math&amp;gt;)&lt;br /&gt;
* ein neues statisches Array der Größe 'neue capacity' wird erzeugt&lt;br /&gt;
* das alte Array wird ins neue kopiert und danach freigegeben&lt;br /&gt;
* das anzufügende Element wird ins neue Array eingefügt&lt;br /&gt;
Umgekehrt geht man beim Entfernen des ''letzten'' Array-Elements vor. Normalerweise überschreibt man einfach das letzte Element mit &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; und dekrementiert &amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt;. Wird dadurch das Array zu klein (üblicherweise &amp;lt;tt&amp;gt;size &amp;amp;lt; capacity / 4&amp;lt;/tt&amp;gt;), wird die Kapazität halbiert, genauer:&lt;br /&gt;
* ein neues Array mit &amp;lt;br/&amp;gt;&lt;br /&gt;
: &amp;lt;tt&amp;gt;neue capacity = alte capacity / 2 &amp;lt;/tt&amp;gt;&lt;br /&gt;
: wird angelegt (bzw. mit&lt;br /&gt;
:: &amp;lt;tt&amp;gt;neue capacity = alte capacity / c &amp;lt;/tt&amp;gt;&lt;br /&gt;
: wenn ein anderer Vergrößerungsfaktor verwendet wird)&lt;br /&gt;
* das alte Array wird ins neue kopiert und danach freigegeben&lt;br /&gt;
&lt;br /&gt;
'''Folge:''' Die Kosten für das Vergrößern/Verkleinern der Kapazität werden amortisiert über viele Einfügungen, die kein Vergrößern erfordern. Die Operation &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; besitzt amortisierte Komplexität O(1). Im folgenden Abschnitt zeigen wir dies mathematisch exakt mit der Potentialmethode.&lt;br /&gt;
&lt;br /&gt;
====Komplexitätsanalyse des dynamischen Arrays mit Potentialmethode====&lt;br /&gt;
&lt;br /&gt;
Durchschnitt der Gesamtkosten für N-maliges append = &amp;lt;math&amp;gt;\frac{1}{N} \sum_{i = 1}^N Kosten(i)&amp;lt;/math&amp;gt;. Zur Analyse der amortisierten Komplexität wird ein Potential&amp;lt;br/&amp;gt;&lt;br /&gt;
::&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2*size&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; - capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
eingeführt, wobei das Array nach dem i-ten Einfüge-Schritt die Größe size&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; und die Kapizität capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; hat. Wir nehmen vereinfachend an, dass es keine Löschoperationen gibt. Dann gilt nach dem i-ten Schritt jeweils&lt;br /&gt;
::&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2*i - capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Fall 1: Array ist nicht voll&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Es wird kein Umkopieren benötigt, da das Array noch nicht voll ist&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; size&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; &amp;lt; capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; == capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Kosten: 1 (für Einfügen des neuen Elements)&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial vor append: &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = 2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial nach append: &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
amortisierte Kosten = Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Phi;&amp;lt;sub&amp;gt;(i)&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;(i-1)&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::                 = 1            + (2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;)        - [2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;]&lt;br /&gt;
:::::                 = 1            + 2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;         - 2i + 2 + capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::                 = 1            + &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; - &amp;lt;del&amp;gt;capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;/del&amp;gt; - &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; + 2 + &amp;lt;del&amp;gt;capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;/del&amp;gt;&lt;br /&gt;
:::::                 = 1 + 2&lt;br /&gt;
:::::                 = 3 = O(1) &amp;amp;rarr; konstant&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Fall 2: Array ist voll&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Vor dem i-ten append muss umkopiert werden&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; size&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; == capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; == i-1&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; Allokieren eines neuen statischen Arrays mit verdoppelter Kapazität notwendig, also capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; == 2*capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Kosten: (i-1) + 1 (für Umkopieren und Einfügen des neuen Elements)&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial vor append =  &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = 2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial nach append =  &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; =  2i - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
amortisierte Kosten = Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Phi;&amp;lt;sub&amp;gt;(i)&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;(i-1)&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = ((i - 1) + 1) + 2i - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; - [2(i-1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;]&lt;br /&gt;
:::::               = i + &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; - &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; + 2 - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = i + 2 - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = i + 2 - (i - 1) &amp;lt;small&amp;gt;(da capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = i-1)&amp;lt;/small&amp;gt;&lt;br /&gt;
:::::               = 3 = O(1) &amp;amp;rarr; konstant            &lt;br /&gt;
&lt;br /&gt;
'''Damit wurde bewiesen, dass die Operation &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; beim dynamischen Array eine amortisierte Komplexität von 3 Einheiten hat, also &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; &amp;amp;isin; O(1)'''. Diese Operation kann deshalb gefahrlos in der inneren Schleife eines Algorithmus benutzt werden.&lt;br /&gt;
&lt;br /&gt;
==== Beispiel für 9 Einfügeoperationen ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot;&lt;br /&gt;
!Array&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;lt;small&amp;gt;(wie es aussehen könnte)&amp;lt;/small&amp;gt;&lt;br /&gt;
!size&lt;br /&gt;
!capacity&lt;br /&gt;
!Kosten für append&amp;lt;br /&amp;gt;(einschließlich Umkopieren)&lt;br /&gt;
!Summe Kosten&lt;br /&gt;
!Durchschnittskosten&lt;br /&gt;
!&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2 * size - capacity&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;lt;small&amp;gt;(i = size)&amp;lt;/small&amp;gt;&lt;br /&gt;
!Potenzialdifferenz&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;amp;Delta; &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
!amortisierte Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
= Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Delta; &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;0&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3/2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6/3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;0&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7/4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;5&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;12&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;12/5&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;13&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;13/6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;14&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;14/7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,h]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;15&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;15/8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,h,j,None,None,None,&amp;lt;br /&amp;gt;&lt;br /&gt;
None,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;9&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;16&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;24&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;24/9&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die durchschnittlichen Kosten betragen stets etwa 2 Einheiten, schwanken allerdings so, dass nicht unmittelbar ersichtlich ist, ob dies für sämtliche Einfügeoperationen gilt. Die amortisierte Komplexität, die mit Hilfe des Potentials berechnet wird, ist hingegen konstant 3, wie auch im obigen Beweis für alle Einfügeoperationen allgemein gezeigt wurde.&lt;br /&gt;
&lt;br /&gt;
[[Suchen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Effizienz&amp;diff=5419</id>
		<title>Effizienz</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Effizienz&amp;diff=5419"/>
				<updated>2012-07-27T16:02:09Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Fazit */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Bei der Diskussion von Effizienz müssen wir zwischen der Laufzeit eines Algorithmus auf einem bestimmten System und seiner prinzipiellen Leistungsfähigkeit (Algorithmenkomplexität) unterscheiden. Der Benutzer ist natürlich vor allem an der Laufzeit interessiert, denn diese bestimmt letztendlich seine Arbeitsproduktivität. Ein Softwaredesigner hingegen muss eine Implementation wählen, die auf verschiedenen Systemen und in verschiedenen Anwendungen schnell ist. Für ihn sind daher auch Aussagen zur Algorithmenkomplexität sehr wichtig, um den am besten geeigneten Algorithmus auszuwählen.&lt;br /&gt;
&lt;br /&gt;
== Laufzeit ==&lt;br /&gt;
&lt;br /&gt;
Aus Anwendersicht ist ein Algorithmus effizient, wenn er die in der Spezifikation verlangten Laufzeitgrenzen einhält. Ein Algorithmus muss also nicht immer so schnell wie möglich sein, sondern so schnell wie nötig. Dies führt in verschiedenen Anwendungen zu ganz unterschiedliche Laufzeitanforderungen:&lt;br /&gt;
&lt;br /&gt;
* Berechnen des nächsten Steuerkommandos für eine Maschine: ca. 1/1000s&lt;br /&gt;
* Berechnen des nächsten Bildes für eine Videopräsentation (z.B. Dekompression von MPEG-kodierten Bildern): ca. 1/25s&lt;br /&gt;
: Geringere Bildraten führen zu ruckeligen Filmen.&lt;br /&gt;
* Sichtbare Antwort auf ein interaktives Kommando (z.B. Mausklick): ca. 1/2s&lt;br /&gt;
: Wird diese Antwortzeit überschritten, vermuten viele Benutzer, dass der Mausklick nicht funktioniert hat, und klicken nochmals, mit eventuell fatalen Folgen. Wenn ein Algorithmus notwendigerweise länger dauert als 1/2s, sollte ein Fortschrittsbalken angezeigt werden.&lt;br /&gt;
* Wettervorhersage: muss spätestens am Vorabend des vorhergesagten Tages beendet sein&lt;br /&gt;
&lt;br /&gt;
===Laufzeitvergleich===&lt;br /&gt;
&lt;br /&gt;
Da die Laufzeit für den Benutzer ein so wichtiges Kriterium ist, werden häufig Laufzeitvergleiche durchgeführt. Deren Ergebnisse hängen allerdings von vielen Faktoren ab, die möglicherweise nicht kontrollierbar sind:&lt;br /&gt;
* Geschwindigkeit und Anzahl der Prozessoren&lt;br /&gt;
* Auslastung des Systems&lt;br /&gt;
* Größe des Hauptspeichers  und Cache, Geschwindigkeit des  Datenbus&lt;br /&gt;
* Qualität des Compilers/Optimierers (ist der Compiler für die spezielle Prozessor-Architektur optimiert?)&lt;br /&gt;
* Geschick des Programmierers&lt;br /&gt;
* Daten (Beispiel Quicksort: Best case und worst case [vorsortierter Input] stark unterschiedlich)&lt;br /&gt;
All diese Faktoren sind untereinander abhängig. Laufzeitvergleiche sind daher mit Vorsicht zu interpretieren.&lt;br /&gt;
Generell sollten bei Vergleichen möglichst wenige Parameter verändert werden, z.B.&lt;br /&gt;
* gleiches Programm (gleiche Kompilierung), gleiche Daten, andere Prozessoren&lt;br /&gt;
oder&lt;br /&gt;
* gleiche CPU, Daten, andere Programme (Vergleich von Algorithmen)&lt;br /&gt;
Zur Verbesserung der Vergleichbarkeit gibt es standardisierte [http://en.wikipedia.org/wiki/Benchmark_(computing) Benchmarks], die bestimmte Aspekte eines Systems unter möglichst realitätsnahen Bedingungen testen. Generell gilt aber: Durch Laufzeitmessung ist schwer festzustellen, ob ein Algorithmus ''prinzipiell'' besser ist als ein anderer. Dafür ist die Analyse der [[Effizienz#Algorithmen-Komplexität|Algorithmenkomplexität]] notwendig.&lt;br /&gt;
&lt;br /&gt;
===Optimierung der Laufzeit===&lt;br /&gt;
&lt;br /&gt;
Wenn sich herausstellt, dass ein bereits implementierter Algorithmus zu langsam läuft, geht man wie folgt vor:&lt;br /&gt;
&lt;br /&gt;
# Man verwendet einen [http://en.wikipedia.org/wiki/Performance_analysis Profiler], um zunächst den Flaschenhals zu bestimmen. Ein Profiler ist ein Hilfsprogramm, das während der Ausführung eines Programms misst, wieviel Zeit in jeder Funktion und Unterfunktion verbraucht wird. Dadurch kann man herausfinden, welcher Teil des Algorithmus überhaupt Probleme bereitet. Donald Knuth gibt z.B. als Erfahrungswert an, dass Programme während des größten Teils ihrer Laufzeit nur 3% des Quellcodes (natürlich mehrmals wiederholt) ausführen [http://pplab.snu.ac.kr/courses/adv_pl05/papers/p261-knuth.pdf]. Es ist sehr wichtig, diese 3% experimentell zu bestimmen, weil die Erfahrung zeigt, dass man beim Erraten der kritischen Programmteile oft falsch liegt. Man spricht dann von &amp;quot;[http://en.wikipedia.org/wiki/Optimization_%28computer_science%29#When_to_optimize premature optimization]&amp;quot;, also von voreiliger Optimierung ohne experimentelle Untersuchung der wirklichen Laufzeiten, was laut Knuth &amp;quot;the root of all evil&amp;quot; ist. Der Python-Profiler wird in [http://docs.python.org/lib/profile.html Kapitel 25] der Python-Dokumentation beschrieben.&lt;br /&gt;
# Man kann dann versuchen, die kritischen Programmteile zu optimieren.&lt;br /&gt;
# Falls der Laufzeitgewinn durch Optimierung zu gering ist, muss man einen prinzipiell schnelleren Algorithmus verwenden, falls es einen gibt.&lt;br /&gt;
&lt;br /&gt;
Einige wichtige Techniken der Programmoptimierung sollen hier erwähnt werden. Wenn man einen optimierenden Compiler verwendet, werden einige Optimierungen automatisch ausgeführt [http://en.wikipedia.org/wiki/Compiler_optimization]. In Python trifft dies jedoch nicht zu. Um den Sinn einiger Optimierungen zu verstehen, benötigt man Grundkenntnisse der Computerarchitektur.&lt;br /&gt;
&lt;br /&gt;
;Elimination von redundantem Code: Es ist offensichtlich überflüssig, dasselbe Ergebnis mehrmals zu berechnen, wenn es auch zwischengespeichert werden könnte. Diese Optimierung wird von vielen automatischen Optimierern unterstützt und kommt im wesentlichen in zwei Ausprägungen vor:&lt;br /&gt;
:; common subexpression elimination: In mathematischen Ausdrücken wird ein Teilergebnis häufig mehrmals benötigt. Man betrachte z.B. die Lösung der quadratischen Gleichung &amp;lt;math&amp;gt;x^2+p\,x+q = 0&amp;lt;/math&amp;gt;:&lt;br /&gt;
        x1 = - p / 2.0 + sqrt(p*p/4.0 - q)&lt;br /&gt;
        x2 = - p / 2.0 - sqrt(p*p/4.0 - q)&lt;br /&gt;
::Die mehrmalige Berechnung von Teilausdrücken wird vermieden, wenn man stattdessen schreibt:&lt;br /&gt;
        p2 = - p / 2.0&lt;br /&gt;
        r  = sqrt(p2*p2 - q)&lt;br /&gt;
        x1 = p2 + r&lt;br /&gt;
        x2 = p2 - r&lt;br /&gt;
:; loop invariant elimination: Wenn ein Teilausdruck sich in einer Schleife nicht ändert, muss man ihn nicht bei jedem Schleifendurchlauf neu berechnen, sondern kann dies einmal vor Beginn der Schleife tun. Ein typisches Beispiel hierfür ist die Adressierung von Matrizen, die als 1-dimensionales Array gespeichert sind. Angenommen, wir speichern eine NxN Matrix &amp;lt;tt&amp;gt;m&amp;lt;/tt&amp;gt; in einem Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; der Größe N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt;, so dass das Matrixelement &amp;lt;tt&amp;gt;m&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt;&amp;lt;/tt&amp;gt; durch &amp;lt;tt&amp;gt;a[i + j*N]&amp;lt;/tt&amp;gt; indexiert wird. Wir betrachten die Aufgabe, eine Einheitsmatrix zu initialisieren. Ein nicht optimierter Algorithmus dafür lautet:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + j*N] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + j*N] = 0.0&lt;br /&gt;
::Der Ausdruck &amp;lt;tt&amp;gt;j*N&amp;lt;/tt&amp;gt; wird hier in jedem Schleifendurchlauf erneut berechnet, obwohl sich &amp;lt;tt&amp;gt;j&amp;lt;/tt&amp;gt; in der inneren Schleife gar nicht verändert. Man kann deshalb optimieren zu:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + jN] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + jN] = 0.0&lt;br /&gt;
;Vereinfachung der inneren Schleife: Generell sollte man sich bei der Optimierung auf die innere Schleife eines Algorithmus konzentrieren, weil dieser Code am häufigsten ausgeführt wird. Insbesondere sollte man die Anzahl der Befehle in der inneren Schleife so gering wie möglich halten und teure Befehle vermeiden. Früher waren vor allem Floating-Point Befehle teuer, die man oft durch die schnellere Integer-Arithmetik ersetzt hat, falls dies algorithmisch möglich war (diesen Rat findet man noch oft in der Literatur). Heute hat sich die Hardware so verbessert, dass im Allgemeinen nur noch die Floating-Point Division deutlich langsamer ist als die anderen Operatoren. Im obigen Beispiel der quadratischen Gleichung ist es daher sinnvoll, den Ausdruck &lt;br /&gt;
        p2 = -p / 2.0&lt;br /&gt;
:durch&lt;br /&gt;
        p2 = -0.5 * p&lt;br /&gt;
:zu ersetzen. Dadurch ersetzt man eine Division durch eine Multiplikation und spart außerdem das Negieren von &amp;lt;tt&amp;gt;p&amp;lt;/tt&amp;gt;, da der Compiler direkt mit &amp;lt;tt&amp;gt;-0.5&amp;lt;/tt&amp;gt; multipliziert.&lt;br /&gt;
;Ausnutzung der Prozessor-Pipeline: Moderne Prozessoren führen mehrere Befehle parallel aus. Dies ist möglich, weil jeder Befehl in mehrere Teilschritte zerlegt werden kann. Eine generische Unterteilung in vier Teilschritte ist z.B.:&lt;br /&gt;
:# Dekodieren des nächsten Befehls&lt;br /&gt;
:# Beschaffen der Daten, die der Befehl verwendet (aus Prozessorregistern, dem Cache, oder dem Hauptspeicher)&lt;br /&gt;
:# Ausführen des Befehls&lt;br /&gt;
:# Schreiben der Ergebnisse&lt;br /&gt;
:Man bezeichnet dies als die &amp;quot;[http://en.wikipedia.org/wiki/Instruction_pipeline instruction pipeline]&amp;quot; des Prozessors (heutige Prozessoren verwenden wesentlich feinere Unterteilungen). Prozessoren werden nun so gebaut, dass mehrere Befehle parallel, auf verschiedenen Ausführungsstufen ausgeführt werden. Wenn Befehl 1 also beim Schreiben der Ergebnisse angelangt ist, kann Befehl 2 die Hardware zum Ausführen des Befehls benutzen, während Befehl 3 seine Daten holt, und Befehl 4 soeben dekodiert wird. Unter bestimmten Bedingungen funktioniert diese Parallelverarbeitung jedoch nicht. Dies gibt Anlass zu Optimierungen:&lt;br /&gt;
:;Vermeiden unnötiger Typkonvertierungen: Der Prozessor verarbeitet Interger- und Floating-Point-Befehle in verschiedenen Pipelines, weil die Hardwareanforderungen sehr verschieden sind. Wird jetzt ein Ergebnis von Integer nach Floating-Point umgewandelt oder umgekehrt, muss die jeweils andere Pipeline warten, bis die erste Pipeline ihre Berechnung beendet. Es kann dann besser sein, Berechnungen in Floating-Point zu Ende zu führen, auch wenn sie semantisch eigentlich Integer-Berechnungen sind.&lt;br /&gt;
:;Reduzierung der Anzahl von Verzweigungen: Wenn der Code verzweigt (z.B. durch eine &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;- oder  &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Anweisung), ist nicht klar, welcher Befehl nach der Verzweigung ausgeführt werden soll, bevor Stufe 3 der Pipeline die Verzweigungsbedingung ausgewertet hat. Bis dahin wären die ersten beiden Stufen der Pipeline unbenutzt. Moderne Prozessoren benutzen zwar ausgefeilte Heuristiken, um das Ergebnis der Bedingung vorherzusagen, und führen den hoffentlich richtigen Zweig des Codes spekulativ aus, aber dies funktioniert nicht immer. Man sollte deshalb generell die Anzahl der Verzweigungen minimieren. Als Nebeneffekt führt dies meist auch zu besser lesbarem, verständlicherem Code. Im Matrixbeispiel kann man&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + jN] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + jN] = 0.0&lt;br /&gt;
::durch&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               a[i + jN] = 0.0&lt;br /&gt;
           a[j + jN] = 1.0&lt;br /&gt;
ersetzen. Die Diagonalelemente &amp;lt;tt&amp;gt;a[j + jN]&amp;lt;/tt&amp;gt; werden jetzt zwar zweimal initialisiert (in der Schleife auf Null, dann auf Eins), aber durch Elimination der &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage wird dies wahrscheinlich mehr als ausgeglichen, zumal dadurch die innere Schleife wesentlich vereinfacht wurde.&lt;br /&gt;
;Ausnutzen des Prozessor-Cache: Zugriffe auf den Hauptspeicher sind sehr langsam. Deshalb werden stets ganze Speicherseiten auf einmal in den [http://en.wikipedia.org/wiki/Cache Cache] des Prozessors geladen. Wenn unmittelbar nacheinander benutzte Daten auch im Speicher nahe beieinander liegen (sogenannte &amp;quot;[http://en.wikipedia.org/wiki/Locality_of_reference locality of reference]&amp;quot;), ist die Wahrscheinlichkeit groß, dass die als nächstes benötigten Daten bereits im Cache sind und damit schnell gelesen werden können. Bei vielen Algorithmen kann man die Implementation so umordnen, dass die locality of reference verbessert wird, was zu einer drastischen Beschleunigung führt. Im Matrix-Beispiel ist z.B. die Reihenfolge der Schleifen wichtig. Für konstanten Index &amp;lt;tt&amp;gt;j&amp;lt;/tt&amp;gt; liegen die Indizes &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; im Speicher hintereinander. Deshalb ist es günstig, in der inneren Schleife über &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu iterieren:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               a[i + jN] = 0.0&lt;br /&gt;
           a[j + jN] = 1.0&lt;br /&gt;
:Die umgekehrte Reihenfolge der Schleifen ist hingegen ungünstig&lt;br /&gt;
       for i in range(N):&lt;br /&gt;
           for j in range(N):&lt;br /&gt;
               a[i + j*N] = 0.0&lt;br /&gt;
           a[i + i*N] = 1.0&lt;br /&gt;
:Jetzt werden in der inneren Schleife stets N Datenelemente übersprungen. Besonders bei großem N muss man daher häufig den Cache neu füllen, was bei der ersten Implementation nicht notwendig war.  (Außerdem verliert man hier die Optimierung &amp;lt;tt&amp;gt;jN = j*N&amp;lt;/tt&amp;gt;, die jetzt nicht mehr möglich ist.)&lt;br /&gt;
&lt;br /&gt;
Als Faustregel kann man durch Optimierung eine Verdoppelung der Geschwindigkeit erreichen (in Ausnahmefällen auch mehr). Benötigt man stärkere Verbesserungen, muss man wohl oder übel einen besseren Algorithmus oder einen schnelleren Computer verwenden.&lt;br /&gt;
&lt;br /&gt;
== Algorithmen-Komplexität ==&lt;br /&gt;
&lt;br /&gt;
Komplexitätsbetrachtungen ermöglichen den Vergleich der prinzipiellen Eigenschaften von Algorithmen unabhängig von einer Implementation, Umgebung etc.&lt;br /&gt;
      &lt;br /&gt;
Eine einfache Möglichkeit ist das Zählen der Aufrufe einer Schlüsseloperation. Beispiel Sortieren:&lt;br /&gt;
* Anzahl der Vergleiche&lt;br /&gt;
* Anzahl der Vertauschungen&lt;br /&gt;
&lt;br /&gt;
=== Beispiel: Selection Sort ===&lt;br /&gt;
&lt;br /&gt;
  for i in range(len(a)-1):&lt;br /&gt;
    max = i&lt;br /&gt;
    for j in range(i+1, len(a)):&lt;br /&gt;
      if a[j] &amp;lt; a[max]:&lt;br /&gt;
        max = j&lt;br /&gt;
    a[max], a[i] = a[i], a[max]      # swap&lt;br /&gt;
&lt;br /&gt;
*Anzahl der Vergleiche: Ein Vergleich in jedem Durchlauf der inneren Schleife. Es ergibt sich folgende Komplexität:&lt;br /&gt;
*:Ingesamt &amp;lt;math&amp;gt;\sum_{i=0}^{N-2} \sum_{j=i+1}^{N-1}1 = \frac{N}{2} (N-1) \!&amp;lt;/math&amp;gt; Vergleiche.&lt;br /&gt;
&lt;br /&gt;
*Anzahl der Vertauschungen (swaps): Eine Vertauschung pro Durchlauf der äußeren Schleife:&lt;br /&gt;
*:Insgesamt &amp;lt;math&amp;gt;N-1 \!&amp;lt;/math&amp;gt; Vertauschungen&lt;br /&gt;
&lt;br /&gt;
Die Komplexität wird durch die Operationen bestimmt, die am häufigsten ausgeführt werden, hier also die Anzahl der Vergleiche. Die Anzahl der Vertauschungen ist hingegen kein geeignetes Kriterium für die Komplexität von selection sort, weil der Aufwand in der inneren Schleife ignoriert würde.&lt;br /&gt;
&lt;br /&gt;
=== Fallunterscheidung: Worst und Average Case ===&lt;br /&gt;
&lt;br /&gt;
Die Komplexität ist in der Regel eine Funktion der Eingabegröße (Anzahl der Eingabebits, Anzahl der Eingabeelemente). Sie kann aber auch von der Art der Daten abhängen, nicht nur von der Menge, z.B. vorsortierte Daten bei Quicksort. Um von der Art der Daten unabhängig zu werden, kann man zwei Fälle der Komplexität unterscheiden:&lt;br /&gt;
      &lt;br /&gt;
* Komplexität im ungünstigsten Fall &lt;br /&gt;
*: Der ungünstigste Fall ist die Eingabe gegebener Länge, für die der Algorithmus am langsamsten ist. Der Nachteil dieser Methode besteht darin, dass dieser ungünstige Fall in der Praxis vielleicht gar nicht oder nur selten vorkommt, so dass sich der Algorithmus in Wirklichkeit besser verhält als man nach dieser Analyse erwarten würde. Beim Quicksort-Algorithmus mit zufälliger Wahl des Pivot-Elements müsste z.B. stets das kleinste oder größte Element des aktuellen Intervalls als Pivot-Element gewählt werden, was äußerst unwahrscheinlich ist.&lt;br /&gt;
* Komplexität im durchschnittlichen/typischen Fall&lt;br /&gt;
*: Der typische Fall ist die mittlere Komplexität des Algorithmus über alle möglichen Eingaben. Dazu muss man die Wahrscheinlichkeit jeder möglichen Eingabe kennen, und berechnet dann die mittlere Laufzeit über dieser Wahrscheinlichkeitsverteilung. Leider ist die Wahrscheinlichkeit der Eingaben oft nicht bekannt, so dass man geeignete Annahmen treffen muss. Bei Sortieralgorithmen können z.B. alle möglichen Permutationen des Eingabearrays als gleich wahrscheinlich angenommen werden, und der typische Fall ist dann die mittlere Komplexität über alle diese Eingaben. Oft hat man jedoch in der Praxis andere Wahrscheinlichkeitsverteilungen, z.B. sind die Daten oft &amp;quot;fast sortiert&amp;quot; (nur wenige Elemente sind an der falschen Stelle). Dann verhält sich der Algorithmus ebenfalls anders als vorhergesagt.&lt;br /&gt;
&lt;br /&gt;
Wir beschränken uns in dieser Vorlesung auf die Komplexität im ungünstigseten Fall. '''Exakte''' Formeln für Komplexität sind aber auch dann schwer zu gewinnen, wie das folgende Beispiel zeigt:&lt;br /&gt;
&lt;br /&gt;
=== Beispiele aus den Übungen (Gemessene Laufzeiten für Mergesort/Selectionsort) ===&lt;br /&gt;
&lt;br /&gt;
* Mergesort: &amp;lt;math&amp;gt;\frac{0,977N\log N}{\log 2} + 0,267N-4.39 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: andere Lösung: &amp;lt;math&amp;gt;1140 N\log(N) - 1819N + 6413 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* Selectionsort: &amp;lt;math&amp;gt;\frac{1}{2}N^2 - \frac{1}{2N} - 10^{-12} \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: andere Lösung: &amp;lt;math&amp;gt;1275N^2 - 116003^N + 11111144 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Aus diesen Formeln wird nicht offensichtlich, welcher Algorithmus besser ist.&lt;br /&gt;
Näherung: Betrachte nur '''sehr große Eingaben''' (meist sind alle Algorithmen schnell genug für kleine Eingaben). Dieses Vorgehen wird als '''Asymptotische Komplexität''' bezeichnet (N gegen unendlich).&lt;br /&gt;
&lt;br /&gt;
=== Asymptotische Komplexität am Beispiel Polynom ===&lt;br /&gt;
&lt;br /&gt;
Polynom: &amp;lt;math&amp;gt;a\,x^2+b\,x+c=p\!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;x \!&amp;lt;/math&amp;gt; sei die Eingabegröße, und wir betrachten die Entwicklung von &amp;lt;math&amp;gt;p \!&amp;lt;/math&amp;gt; in Abhängigkeit von &amp;lt;math&amp;gt;x \!&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;math&amp;gt;x=0 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=c \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x=1 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=a+b+c \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x=1000 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=1000000a+1000b+c \approx 1000000a\!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x \to \infty \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p \approx x^2a\!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für sehr große Eingaben verlieren also ''b'' und ''c'' immer mehr an Bedeutung, so dass am Ende nur noch ''a'' für die Komplexitätsbetrachtung wichtig ist.&lt;br /&gt;
&lt;br /&gt;
== Landau-Symbole ==&lt;br /&gt;
&lt;br /&gt;
Um die asymptotische Komplexität verschiedener Algorithmen miteinander vergleichen zu können, verwendet man die sogenannten [http://de.wikipedia.org/wiki/Landau-Symbole Landau-Symbole]. Das wichtigste Landau-Symbol ist &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;, mit dem man eine ''obere Schranke'' &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; für die Komplexität angeben kann. &lt;br /&gt;
&lt;br /&gt;
Schreibt man &amp;lt;math&amp;gt;f \in \Omega(g)&amp;lt;/math&amp;gt;, so stellt dies eine asymptotische ''untere Schranke'' für die Funktion f dar.&lt;br /&gt;
&lt;br /&gt;
Schließlich bedeutet &amp;lt;math&amp;gt;f \in \Theta(g)&amp;lt;/math&amp;gt;, dass die Funktion f genauso schnell wie die Funktion g wächst, das heißt man hat eine asymptotisch ''scharfe Schranke'' für f. Hierzu muss sowohl &amp;lt;math&amp;gt;f\in\mathcal{O}(g)&amp;lt;/math&amp;gt; als auch &amp;lt;math&amp;gt;f \in \Omega(g)&amp;lt;/math&amp;gt; erfüllt sein. &lt;br /&gt;
&lt;br /&gt;
Im nun folgenden soll auf die verschiedenen Landau-Symbole noch näher eingegeangen werden.&lt;br /&gt;
&lt;br /&gt;
===O-Notation===&lt;br /&gt;
&lt;br /&gt;
Intuitiv gilt: Für große N dominieren die am schnellsten wachsenden Terme einer Funktion. Die Notation &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; (sprich &amp;quot;f ist in O von g&amp;quot; oder &amp;quot;f ist von derselben Größenordnung wie g&amp;quot;) formalisiert eine solche Abschätzung der asymptotischen Komplexität der Funktion f von oben. &lt;br /&gt;
; Asymptotische Komplexität: Für zwei Funktionen f(x) und g(x) gilt&lt;br /&gt;
::&amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x))&amp;lt;/math&amp;gt;&lt;br /&gt;
: genau dann wenn es eine Konstante &amp;lt;math&amp;gt;c&amp;gt;0&amp;lt;/math&amp;gt; und ein Argument &amp;lt;math&amp;gt;x_0&amp;lt;/math&amp;gt; gibt, so dass &lt;br /&gt;
::&amp;lt;math&amp;gt;\forall x \ge x_0:\quad f(x) \le c\,g(x)&amp;lt;/math&amp;gt;.&lt;br /&gt;
:Die Menge &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; aller durch g(x) abschätzbaren Funktionen ist also formal definiert durch&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(g(x)) = \{ f(x)\ |\ \exists c&amp;gt;0: \forall x \ge x_0: 0 \le f(x) \le c\,g(x)\}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Idee hinter dieser Definition ist, dass g(x) eine wesentlich einfachere Funktion ist als f(x), die sich aber nach geeigneter Skalierung (Multiplikation mit c) und für große Argumente x im wesentlichen genauso wie f(x) verhält. Man kann deshalb in der Algorithmenanalyse f(x) durch g(x) ersetzen. &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x))&amp;lt;/math&amp;gt; spielt für Funktionen eine ähnliche Rolle wie der Operator &amp;amp;le; für Zahlen: Falls a &amp;amp;le; b gilt, kann bei einer Abschätzung von oben ebenfalls a durch b ersetzt werden.&lt;br /&gt;
&lt;br /&gt;
==== Ein einfaches Beispiel ====&lt;br /&gt;
&lt;br /&gt;
[[Image:Sqsqrt.png]]&lt;br /&gt;
&lt;br /&gt;
Rot = &amp;lt;math&amp;gt;x^2 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
Blau = &amp;lt;math&amp;gt;\sqrt{x} \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\sqrt{x} \in \mathcal{O}(x^2)\!&amp;lt;/math&amp;gt; weil &amp;lt;math&amp;gt;\sqrt{x} \le c\,x^2\!&amp;lt;/math&amp;gt; für alle &amp;lt;math&amp;gt;x \ge x_0 = 1 \!&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c = 1\!&amp;lt;/math&amp;gt;, oder auch für &amp;lt;math&amp;gt;x \ge x_0 = 4 \!&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c = 1/16&amp;lt;/math&amp;gt; (die Wahl von c und x&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; in der Definition von O(.) ist beliebig, solange die Bedingungen erfüllt sind).&lt;br /&gt;
&lt;br /&gt;
==== Komplexität bei kleinen Eingaben ==== &lt;br /&gt;
&lt;br /&gt;
Algorithmus 1: &amp;lt;math&amp;gt;\mathcal{O}(N^2) \!&amp;lt;/math&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Algorithmus 2: &amp;lt;math&amp;gt;\mathcal{O}(N\log{N}) \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus 2 ist schneller (von geringerer Komplexität) für große Eingaben, aber bei kleinen Eingaben (insbesondere, wenn der Algorithmus in einer Schleife immer wieder mit kleinen Eingaben aufgerufen wird) könnte Algorithmus 1 schneller sein, falls der in der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation verborgene konstante Faktor ''c'' bei Algorithmus 2 einen wesentlich größeren Wert hat als bei Algorithmus 1.&lt;br /&gt;
&lt;br /&gt;
==== Eigenschaften der O-Notation (Rechenregeln) ==== &lt;br /&gt;
&lt;br /&gt;
# Transitiv:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x)) \land g(x) \in \mathcal{O}(h(x)) \to f(x) \in \mathcal{O}(h(x)) \!&amp;lt;/math&amp;gt;           &lt;br /&gt;
# Additiv:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(h(x)) \land g(x) \in \mathcal{O}(h(x)) \to f(x) + g(x) \in \mathcal{O}(h(x)) \!&amp;lt;/math&amp;gt;           &lt;br /&gt;
# Für Monome gilt:&lt;br /&gt;
#: &amp;lt;math&amp;gt;x^k \in \mathcal{O}(x^k)&amp;lt;/math&amp;gt; und&lt;br /&gt;
#: &amp;lt;math&amp;gt;x^k \in \mathcal{O}(x^{k+j}), \forall j \ge 0 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Multiplikation mit einer Konstanten:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x)) \to c\,f(x) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: andere Schreibweise:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) = c\,g(x) \to f(x) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Folgerung aus 3. und 4. für Polynome: &lt;br /&gt;
#: &amp;lt;math&amp;gt;a_0+a_1\,x + ... + a_n\,x^n \in \mathcal{O}(x^n)\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: Beispiel: &amp;lt;math&amp;gt;a\,x^2+b\,x+c \in \mathcal{O}(x^2)\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Logarithmus:&lt;br /&gt;
#: &amp;lt;math&amp;gt;a, b &amp;gt; 1\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{b}{x})\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: Die Basis des Logarithmus spielt also keine Rolle.&lt;br /&gt;
#: Beweis hierfür:&lt;br /&gt;
#:: &amp;lt;math&amp;gt;\log_{a}{x} = \frac{\log_{b}{x}}{\log_{b}{a}}\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#:: Mit &amp;lt;math&amp;gt;c = 1 / \log_{b}{a}\,&amp;lt;/math&amp;gt; gilt: &amp;lt;math&amp;gt;\log_{a}{x} = c\,\log_{b}{x}\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#:: Wird hier die (zweite) Regel für Multiplikation mit einer Konstanten angewendet, fällt der konstante Faktor weg, also &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{b}{x})\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Insbesondere gilt auch &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{2}{x})\!&amp;lt;/math&amp;gt;, es kann also immer der 2er Logarithmus verwendet werden.&lt;br /&gt;
&lt;br /&gt;
==== O-Kalkül ==== &lt;br /&gt;
&lt;br /&gt;
Das O-Kalkül definiert wichtige Vereinfachungsregeln for Ausdrücke in O-Notation (Beweise: siehe Übungsaufgabe):&lt;br /&gt;
&lt;br /&gt;
# &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(f(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# &amp;lt;math&amp;gt;\mathcal{O}(\mathcal{O}(f(x))) \in \mathcal{O}(f(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# &amp;lt;math&amp;gt;c\,\mathcal{O}(f(x)) \in \mathcal{O}(f(x))\,&amp;lt;/math&amp;gt; für jede Konstante ''c''&lt;br /&gt;
# &amp;lt;math&amp;gt;\mathcal{O}(f(x))+c \in \mathcal{O}(f(x))\,&amp;lt;/math&amp;gt; für jede Konstante ''c''&lt;br /&gt;
# Sequenzregel:&lt;br /&gt;
#: Wenn zwei nacheinander ausgeführte Programmteile die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt; bzw. &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; haben, gilt für beide gemeinsam:&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(f(x))&amp;lt;/math&amp;gt; falls &amp;lt;math&amp;gt;g(x) &amp;lt; \mathcal{O}(f(x))&amp;lt;/math&amp;gt; bzw.&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt; falls &amp;lt;math&amp;gt;f(x) &amp;lt; \mathcal{O}(g(x))&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Informell schreibt man auch: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(max(f(x), g(x)))\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
# Schachtelungsregel bzw. Aufrufregel:&lt;br /&gt;
#: Wenn in einer geschachtelten Schleife die äußere Schleife die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt; hat, und die innere &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt;, gilt für beide gemeinsam:&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) * \mathcal{O}(g(x)) \in \mathcal{O}(f(x) * g(x))\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Gleiches gilt wenn eine Funktion &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt;-mal aufgerufen wird, und die Komplexität der Funktion selbst &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; ist.&lt;br /&gt;
&lt;br /&gt;
;Beispiel für 5.: Beide Schleifen haben die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Dies gilt auch für ihre Hintereinanderausführung:&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          a[i] = i&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          print a[i]&lt;br /&gt;
;Beispiele für 6.: Beide Schleifen haben die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Ihre Verschachtelung hat daher die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N^2)&amp;lt;/math&amp;gt;. &lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          for j in range(N):&lt;br /&gt;
              a[i*N + j] = i+j&lt;br /&gt;
: Dies gilt ebenso, wenn statt der inneren Schleife eine Funktion mit Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt; ausgeführt wird:&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          a[i] = foo(i, N)  # &amp;lt;math&amp;gt;\mathrm{foo}(i, N) \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== O-Kalkül auf das Beispiel des Selectionsort angewandt ==== &lt;br /&gt;
&lt;br /&gt;
Selectionsort: Wir hatten gezeigt dass &amp;lt;math&amp;gt;f(N) = \frac{N^2}{2} - \frac{N}{2}&amp;lt;/math&amp;gt;. Nach der Regel für Polynome vereinfacht sich dies zu &amp;lt;math&amp;gt;f(N) \in \mathcal{O}\left(\frac{N^2}{2}\right) = \mathcal{O}(N^2)\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Alternativ via Schachtelungsregel:&lt;br /&gt;
: Die äußere Schleife wird (''N''-1)-mal durchlaufen: &amp;lt;math&amp;gt;N-1 \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
: Die innere Schleife wird (''N-i''-1)-mal durchlaufen. Das sind im Mittel ''N''/2 Durchläufe: &amp;lt;math&amp;gt;N/2 \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
: Zusammen: &amp;lt;math&amp;gt;\mathcal{O}(N)*\mathcal{O}(N) \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nach beiden Vorgehensweisen kommen wir zur Schlussfolgerung, dass der Selectionsort die asymptotische Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N^2)\!&amp;lt;/math&amp;gt; besitzt.&lt;br /&gt;
&lt;br /&gt;
==== Zusammenhang zwischen Komplexität und Laufzeit ==== &lt;br /&gt;
&lt;br /&gt;
Wenn eine Operation 1ms dauert, erreichen Algorithmen verschiedener Komplexität folgende Leistungen (wobei angenommen wird, dass der in der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation verborgene konstante Faktor immer etwa gleich 1 ist):&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:left&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
|-&lt;br /&gt;
! Komplexität !! Operationen in 1s !! Operationen in 1min !! Operationen in 1h&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 1000 || 60.000 || 3.600.000&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N\log_2{N})&amp;lt;/math&amp;gt;&lt;br /&gt;
| 140 || 4895 || 204094&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 32 || 245 || 1898&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 10 || 39 || 153&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(2^N)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 10 || 16 || 21&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Exponentielle Komplexität ==== &lt;br /&gt;
Der letzte Fall &amp;lt;math&amp;gt;\mathcal{O}(2^N)&amp;lt;/math&amp;gt; ist von exponentieller Komplexität. Das bedeutet, dass eine Verdopplung des Aufwands nur bewirkt, dass die maximale Problemgröße um eine Konstante wächst. Algorithmen mit exponentieller (oder noch höherer) Komplexität werden deshalb als '''ineffizient''' bezeichnet. Algorithmen mit höchstens polynomieller Komplexität gelten hingegen als effizient.&lt;br /&gt;
&lt;br /&gt;
In der Praxis sind allerdings auch polynomielle Algorithmen mit hohem Exponenten meist zu langsam. Als Faustregel kann man eine praktische Grenze von &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt; ansehen. Bei einer Komplexität von &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt; bewirkt ein verdoppelter Aufwand immer noch eine Steigerung der maximalen Problemgröße um den Faktor &amp;lt;math&amp;gt;\sqrt[3]{2}&amp;lt;/math&amp;gt; (also eine ''multiplikative'' Vergrößerung um ca. 25%, statt nur einer additiven Vergrößerung wie bei exponentieller Komplexität).&lt;br /&gt;
&lt;br /&gt;
===&amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;- Notation===&lt;br /&gt;
&lt;br /&gt;
Genauso wie &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; eine Art &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;-Operator für Funktionen ist, definiert &amp;lt;math&amp;gt;f \in \Omega(g) &amp;lt;/math&amp;gt; eine Abschätzung von unten, analog zum &amp;lt;math&amp;gt;\ge&amp;lt;/math&amp;gt;-Operator für Zahlen. Formal kann man &amp;lt;math&amp;gt;f(N) \in \Omega(g(N)) &amp;lt;/math&amp;gt; genau dann schreiben, falls es eine Konstante &amp;lt;math&amp;gt; c &amp;gt; 0 &amp;lt;/math&amp;gt; gibt, so dass &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; f(N) \ge c \cdot g(N) &amp;lt;/math&amp;gt; für &amp;lt;math&amp;gt; N  \ge N_0 &amp;lt;/math&amp;gt; &lt;br /&gt;
&lt;br /&gt;
gilt.&lt;br /&gt;
Man verwendet diese Notation also um abzuschätzen, wie groß der Aufwand (die Komplexität) für einen bestimmten Algorithmus ''mindestens'' ist und nicht ''höchstens'', was man mit der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt; - Notation ausdrücken würde.&lt;br /&gt;
&lt;br /&gt;
Ein praktisches Beispiel für eine Anwendung der &amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;- Notation wäre die Fragestellung, ob es ''prinzipiell'' einen besseren Algorithmus für ein bestimmtes Problem gibt. Wie später im Abschnitt [[Suchen#Sortieren_als_Suchproblem|Sortieren als Suchproblem]] gezeigt wird, ist das Sortieren eines Arrays durch paarweise Vergleiche von Elementen immer mindestens von der Komplexität &amp;lt;math&amp;gt; \Omega(N\cdot \ln N) &amp;lt;/math&amp;gt;, was konkret bedeutet, dass kein Sortieralgorithmus, der nach diesem Prinzip arbeitet, jemals eine geringere Komplexität als beispielsweise Merge-Sort haben wird. Natürlich kann man den entsprechenden Sortieralgorithmus, also Merge-Sort zum Beispiel, unter Umständen noch optimieren, aber die Komplexität wird erhalten bleiben. Mit diesem Wissen kann man sich viel (vergebliche) Arbeit sparen.&lt;br /&gt;
&lt;br /&gt;
===&amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;- Notation===&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;math&amp;gt;f(N) \in \Theta(g(N))&amp;lt;/math&amp;gt; ist eine scharfe Abschätzung der asymptotischen Komplexität einer Funktion f. &lt;br /&gt;
&lt;br /&gt;
Damit dies gilt, muss &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; und ''gleichzeitig'' &amp;lt;math&amp;gt;f(N) \in \Omega(g(N))&amp;lt;/math&amp;gt; erfüllt sein.&lt;br /&gt;
&lt;br /&gt;
Dies ist natürlich auch die beste Abschätzung der asymptotischen Komplexität einer Funktion f. Formal bedeutet &amp;lt;math&amp;gt;f(N) \in \Theta(g(N))&amp;lt;/math&amp;gt; dass es zwei Konstanten &amp;lt;math&amp;gt; c_1 &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; c_2 &amp;lt;/math&amp;gt;, beide größer als Null, gibt, so dass für alle &amp;lt;math&amp;gt; N \geq N_0 &amp;lt;/math&amp;gt; gilt: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; c_1 \cdot g(N) \leq f(N) \leq c_2 \cdot g(N) &amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In der Praxis wird manchmal statt der &amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;-Notation auch dann die &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation benutzt, wenn eine scharfe Schranke ausgedrückt werden soll. Dies ist zwar formal nicht korrekt, aber man kann die intendierte Bedeutung meist aus dem Kontext erschließen.&lt;br /&gt;
&lt;br /&gt;
== Komplexitätsvergleich zweier Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
In diesem Abschnitt wollen wir der Frage nachgehen, wie ein formaler Beweis für die Behauptung &amp;lt;math&amp;gt; f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; geschehen kann. Hierbei werden zwei Beweismethoden vorgestellt werden, und zwar der '''Beweis über die Definition der Komplexität''' sowie der '''Beweis durch Dividieren'''.&lt;br /&gt;
&lt;br /&gt;
===Beweis über die Definition der asymptotischen Komplexität===&lt;br /&gt;
&lt;br /&gt;
Die Definition der asymptotischen Komplexität &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; war: &lt;br /&gt;
&lt;br /&gt;
Es gibt eine Konstante &amp;lt;math&amp;gt; c &amp;gt; 0 &amp;lt;/math&amp;gt;, so dass &amp;lt;math&amp;gt; f(N) \le c \cdot g(N) &amp;lt;/math&amp;gt; für &amp;lt;math&amp;gt; N  \ge N_0 &amp;lt;/math&amp;gt; erfüllt ist. &lt;br /&gt;
&lt;br /&gt;
Um also die die asymptotische Komplexität &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; zu beweisen, muss man die oben erwähnten Konstanten c und &amp;lt;math&amp;gt; N_0 &amp;lt;/math&amp;gt; finden, so dass &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; f(N) \leq c \cdot g(N) &amp;lt;/math&amp;gt; für alle &amp;lt;math&amp;gt; N \ge N_0 &amp;lt;/math&amp;gt; erfüllt ist. &lt;br /&gt;
&lt;br /&gt;
Dies geschieht zweckmäßigerweise mit dem Beweisprinzip der ''vollständigen Induktion''. Hierbei ist zu zeigen, dass&lt;br /&gt;
# &amp;lt;math&amp;gt; f(N_0) \leq g(N_0) &amp;lt;/math&amp;gt; für die eine zu bestimmende Konstante &amp;lt;math&amp;gt; N_0 &amp;lt;/math&amp;gt; gilt (''Induktionsanfang'') und &lt;br /&gt;
# falls &amp;lt;math&amp;gt; f(N) \leq g(N) &amp;lt;/math&amp;gt;, dann auch &amp;lt;math&amp;gt; f(N+1) \leq g(N+1) &amp;lt;/math&amp;gt; (''Induktionsschritt'') gilt.&lt;br /&gt;
&lt;br /&gt;
===Beweis durch Dividieren===&lt;br /&gt;
&lt;br /&gt;
Hierbei wählt man eine Konstante c und zeigt, dass &amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f(N)}{c \cdot g(N)} \leq 1 &amp;lt;/math&amp;gt; gilt (für die O-Notation, bei &amp;amp;Omega;-Notation gilt entsprechend &amp;lt;math&amp;gt;\geq 1 &amp;lt;/math&amp;gt;). Man kann dies auch als alternative Definition der Komplexität verwenden.&lt;br /&gt;
&lt;br /&gt;
Als Beispiel betrachten wir die beiden Funktionen &amp;lt;math&amp;gt; f(N) = N \,\lg N &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; g(N) = N^2 &amp;lt;/math&amp;gt; und wollen zeigen, dass &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; gilt. &lt;br /&gt;
&lt;br /&gt;
Als Konstante c wählen wir &amp;lt;math&amp;gt; c = 1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f(N)}{g(N)} = \lim_{N \rightarrow \infty} \frac{\lg N}{N} = \frac{\infty}{\infty} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Unbestimmte Ausdrücke der Form &lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{x \rightarrow x_0} \frac{f(x)}{g(x)} &amp;lt;/math&amp;gt;,&lt;br /&gt;
in denen sowohl &amp;lt;math&amp;gt; f(x) &amp;lt;/math&amp;gt; als auch &amp;lt;math&amp;gt; g(x) &amp;lt;/math&amp;gt; mit &amp;lt;math&amp;gt; x \rightarrow x_0 &amp;lt;/math&amp;gt; gegen Null oder gegen Unendlich streben, kann man manchmal mit den Regeln von [http://de.wikipedia.org/wiki/L%27Hospital%27sche_Regel ''l'Hospital''] berechnen. Danach darf man die Funktionen f und g zur Berechnung des unbestimmten Ausdrucks durch ihre k-ten Ableitungen ersetzen:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{x \rightarrow x_0} \frac{f(x)}{g(x)} = \lim_{x \rightarrow x_0} \frac{f^{(k)}(x)}{g^{(k)}(x)} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In unserem Fall verwenden wir die erste Ableitung und erhalten:&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f'(x)}{g'(x)} = \lim_{N \rightarrow \infty} \frac{1/N}{1} \rightarrow 0 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Damit wurde &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt;, also &amp;lt;math&amp;gt;N \lg N \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt; gezeigt.&lt;br /&gt;
&lt;br /&gt;
Man beachte hierbei, dass &amp;lt;math&amp;gt;N \lg N \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt; keine enge Grenze für die Komplexität von &amp;lt;math&amp;gt;N \,\lg N&amp;lt;/math&amp;gt; darstellt, da der Grenzwert &amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f'(x)}{g'(x)}\, &amp;lt;/math&amp;gt; gegen 0 und nicht gegen eine von Null verschiedene Konstante strebt. In diesem Fall haben wir die Komplexität von &amp;lt;math&amp;gt;N \cdot \lg N &amp;lt;/math&amp;gt; also nur nach oben abschätzen können.&lt;br /&gt;
&lt;br /&gt;
===Beispiel für den Komplexitätsvergleich: Gleitender Mittelwert (Running Average)===&lt;br /&gt;
&lt;br /&gt;
Wir berechnen für ein gegebenes Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; einen gleitenden Mittelwert über &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; Elemente:&amp;lt;br/&amp;gt;&lt;br /&gt;
::&amp;lt;math&amp;gt;r_i = \frac{1}{k} \sum_{j=i-k+1}^i a_j&amp;lt;/math&amp;gt; &amp;lt;br/&amp;gt;&lt;br /&gt;
Das heisst, für jedes &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; mitteln wir die letzten &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; Elemente von &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; und schreiben das Ergebnis in &amp;lt;tt&amp;gt;r[i]&amp;lt;/tt&amp;gt;. Diese Operation ist z.B. bei Börsenkursen wichtig: Neben dem aktuellen Kurs für jeden Tag wird dort meist auch der gleitende Mittelwert der letzten 30 Tage sowie der letzten 200 Tage angegeben. In diesen Mittelwerten erkennt man besser die langfristige Tendenz, weil die täglichen Schwankungen herausgemittelt werden. Wir nehmen außerdem an, dass&lt;br /&gt;
* Array-Zugriff hat eine Komplexit&amp;amp;auml;t von O(1)&lt;br /&gt;
* &amp;lt;math&amp;gt;k \ll N&amp;lt;/math&amp;gt;, d.h. &amp;lt;math&amp;gt;N-k\approx N&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die beiden folgenden Algorithmen berechnen die Mittelwerte auf unterschiedliche Art. Der linke folgt der obigen Definition durch eine Summe, während der rechte inkrementell arbeitet: Man kann den Bereich der &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; letzten Werte als Fenster betrachten, das über das Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; geschoben wird. Schiebt man das Fenster ein Element weiter, fällt links ein Element heraus, und rechts kommt eins hinzu. Man muss also nicht jedes Mal die Summe neu berechnen, sondern kann den vorigen Wert aktualisieren. Wir werden sehen, dass dies Folgen für die Komplexität des Algorithmus hat.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;2&amp;quot; &lt;br /&gt;
|-&lt;br /&gt;
! Programmzeile&lt;br /&gt;
! Version 1: O(N * k)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
! Version 2: O(N)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
1.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
2.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
3.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
4.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k-1, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for i in range(k):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
5.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;for i in range(j-k+1, j+1):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[k-1] += a[i]&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
6.&lt;br /&gt;
|&lt;br /&gt;
:::: &amp;lt;tt&amp;gt;r[j] += a[i]&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
7.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] = (a[j] - a[j-k] + r[j-1])&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
8.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
9.&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
10.&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Wir zeigen unten dass Version 2 eine geringere Komplexit&amp;amp;auml;t besitzt, obwohl sie mehr Zeilen ben&amp;amp;ouml;tigt.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Wir haben in der Tabelle die Komplexität jeder Zeile für sich angegeben. Einfache Anweisungen (Berechnungen, Lese- und Schreibzugriffe auf das Array, Zuweiseungen) haben konstante Komplexität, die Komplexität des Schleifenkopfes allein (also der &amp;lt;tt&amp;gt;for&amp;lt;/tt&amp;gt;-Anweisung ohne den eingerückten Schleifenkörper) entspricht der Anzahl der Durchläufe. Wir müssen jetzt noch die Verschachtelung der Schleifen und die Nacheinanderausführung von Anweisungen berücksichtigen. &lt;br /&gt;
&lt;br /&gt;
====Berechnung der Komplexität von Version 1====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;small&amp;gt;(Wiederholung der Rechenregeln: siehe Abschnitt [[Effizienz#O-Notation|O-Notation]])&amp;lt;/small&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wir betrachten zunächst die innere Schleife (Zeilen 5 und 6 von Version 1):&lt;br /&gt;
&lt;br /&gt;
Der Schleifenkopf (Zeile 5) hat die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt;, weil die Schleife k-mal durchlaufen wird. Der Schleifenkörper (Zeile 6) hat die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;. Nach der Verschachtelungsregel müssen wir die beiden Komplexitäten multiplizieren, und es ergibt sich:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(k)\cdot\mathcal{O}(1) = \mathcal{O}(k\cdot 1)=\mathcal{O}(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wir betrachten nun die äußere Schleife. Der Schleifenkopf (Zeile 4) wird (N-k)-mal durchlaufen und hat somit eine Komplexität von &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Der Schleifenkörper (Zeilen 5 bis 7) besteht aus der inneren Schleife (Zeilen 5 und 6) mit der gerade berechneten Komplexität &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt; sowie einer einfachen Anweisung (Zeile 7) mit Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;. Nach der Sequenzregel wird die Komplexität des Schleifenkörpers durch Addition berechnet:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(k)+\mathcal{O}(1) = \mathcal{O}(\max(k,1)) = \mathcal{O}(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexität der gesamten äußeren Schleife erhalten wir nach der Verschachtelungsregel wieder durch multiplizieren:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)\cdot\mathcal{O}(k) = \mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die übrigen Schritte des Algorithmus werden einfach nacheinander ausgeführt, so dass sie ebenfalls nach der Sequenzregel behandelt werden. Wir erhalten&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)+\mathcal{O}(1)+\mathcal{O}(N\cdot k)+\mathcal{O}(1) = \mathcal{O}(\max(N,1,N\cdot k,1)) = \mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Der gesamte Algorithmus hat also die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Berechnung der Komplexität von Version 2====&lt;br /&gt;
&lt;br /&gt;
Hier gibt es nur einfache Schleifen ohne Verschachtelung. Da der Schleifenkörper jeder Schleife nur einfache Anweisungen der Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt; enthält, ergibt sich die Komplexität der Schleifen nach der Verschachtelungsregel als&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(X)\cdot\mathcal{O}(1) = \mathcal{O}(X\cdot 1)=\mathcal{O}(X)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
wobei &amp;lt;math&amp;gt;\mathcal{O}(X)&amp;lt;/math&amp;gt; die Komplexität des jeweiligen Schleifenkopfes ist. Wir erhalten also für Zeilen 4 und 5: &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt;, Zeilen 6 und 7: &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;, Zeilen 8 und 9: &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Die Hintereinanderausführung wird nach der Sequenzregel behandelt:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)+\mathcal{O}(1)+\mathcal{O}(k)+\mathcal{O}(N)+\mathcal{O}(N)+\mathcal{O}(1) = \mathcal{O}(\max(N,1,k,N,N,1)) = \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Dieser Algorithmus hat also nur die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Fazit====&lt;br /&gt;
&lt;br /&gt;
Obwohl Version 2 mehr Schritte benötigt hat sie eine geringere Komplexität, da die for-Schleifen nicht wie bei Version 1 verschachtelt/untergeordnet sind. Bei verschachtelten for-Schleifen muss die Multiplikationsregel angewendet werden &amp;amp;rarr; höhere Komplexität.&lt;br /&gt;
&lt;br /&gt;
Die gerade berechnete Komplexität gilt aber &amp;lt;u&amp;gt;nur&amp;lt;/u&amp;gt; unter der Annahme, dass Array-Zugriffe konstante Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt; besitzen. Wenn dies nicht der Fall ist, kann sich die Komplexität des Algorithmus drastisch verschlechtern.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|Allgemein gilt:&amp;lt;br/&amp;gt;&lt;br /&gt;
Algorithmen-Analysen beruhen auf der Annahme, dass Zugriffe auf die Daten optimal schnell sind, dass heißt, dass die für den jeweiligen Algorithmus am besten geeignete Datenstruktur verwendetet wird.&amp;lt;br /&amp;gt; &amp;amp;rarr; Ansonsten: Komplexitätsverschlechterung!&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Beispiel für eine Verschlechterung der Komplexität durch Verwendung einer nicht optimalen Datenstruktur====&lt;br /&gt;
&lt;br /&gt;
Wir verwende im Mittelwert-Algorithmus eine verkettete Liste anstelle des Eingabe-Arrays a&amp;lt;/u&amp;gt;. Wir benötigen dazu eine Funktion, die das j-te Element der Liste zurückgibt. Wie üblich ist die Liste mit Hilfe einer Knotenklasse implementiert:&lt;br /&gt;
      class Node:&lt;br /&gt;
          def __init__(self, data):&lt;br /&gt;
              self.data = data&lt;br /&gt;
              self.next = None&lt;br /&gt;
&lt;br /&gt;
Die Listenklasse selbst hat ein Feld &amp;lt;tt&amp;gt;head&amp;lt;/tt&amp;gt;, das eine Referenz auf den ersten Knoten speichert, und jeder Knoten speichert im Feld &amp;lt;tt&amp;gt;next&amp;lt;/tt&amp;gt; eine Referenz auf seinen Nachfolger. Um zum j-ten Element zu gelangen, muss man die Liste sequenziell durchlaufen&lt;br /&gt;
      def get_jth(list, j):&lt;br /&gt;
           r = list.head&lt;br /&gt;
           while j &amp;gt; 0:&lt;br /&gt;
               r = r.head&lt;br /&gt;
               j -= 1&lt;br /&gt;
           return r.data&lt;br /&gt;
Die Komplexität dieser Funktion ist offensichtlich &amp;lt;math&amp;gt;\mathcal{O}(j)&amp;lt;/math&amp;gt; (Komplexitätsberechnung wie oben). Wir setzen jetzt bei Version 1 des Mittelwert-Algorithmus diese Funktion in Zeile 6 anstelle des Indexzugriffs &amp;lt;tt&amp;gt;a[i]&amp;lt;/tt&amp;gt; ein (nur in dieser Zeile wird auf die Elemente des Arrays zugegriffen). Wir erhalten folgende Implementation (die Änderungen sind rot markiert):&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;2&amp;quot; &lt;br /&gt;
|-&lt;br /&gt;
! Programmzeile&lt;br /&gt;
! Version 1 mit Liste: O(N * k)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
1.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
2.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
3.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
4.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k-1, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
5.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;for i in range(j-k+1, j+1):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
6.&lt;br /&gt;
|&lt;br /&gt;
:::: &amp;lt;tt&amp;gt;r[j] += &amp;lt;font color=red&amp;gt;get_jth(a, i)&amp;lt;/font&amp;gt;&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;&amp;lt;font color=red&amp;gt;O(i)&amp;lt;/font&amp;gt;&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
7.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
8.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Der Aufruf der Funktion &amp;lt;tt&amp;gt;get_jth&amp;lt;/tt&amp;gt; ist jetzt gleichbedeutend mit einer dreifach verschachtelten Schleife (weil &amp;lt;tt&amp;gt;get_jth&amp;lt;/tt&amp;gt; ja eine zusatzliche Schleife enthält). Die Anzahl der Operationen in Zeile 4 bis 6 ist jetzt&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;f(N,k)=\sum_{j=k-1}^{N-1}\,\sum_{i=j-k+1}^j\,\mathcal{O}(i)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
wobei das &amp;lt;math&amp;gt;\mathcal{O}(i)&amp;lt;/math&amp;gt; die neue Schleife durch Verwendung der Liste repräsentiert. Mit Mathematica-Hilfe [http://www.wolfram.com/] lässt sich diese Summe exakt ausrechnen&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;f(N,k)=\frac{1}{2}(k N^2-k^2 N+k^2-k)\in \mathcal{O}(k N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexitätsberechnung erfolgte dabei nach der Regel für Polynome unter Beachtung von &amp;lt;math&amp;gt;k \ll N&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Fazit:====&lt;br /&gt;
&lt;br /&gt;
Die Komplexität von Version 1 mit einer verketteten Liste wäre O(N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; * k)&lt;br /&gt;
'''&amp;amp;rarr; Die richtige Datenstruktur ist wichtig, da es sonst zu einer Komplexitätsverschlechterung kommen kann!'''&lt;br /&gt;
&lt;br /&gt;
Auf Version 2 unseres Running Average-Beispiels hätte eine verkettete Liste allerdings keine Auswirkungen, da die inkrementelle Berechnung der Summen in Zeile 7 weiterhin möglich ist (bei geschickter Implementation!) und somit Version 2 immer noch eine Komplexität von O(N) hätte.&lt;br /&gt;
&lt;br /&gt;
==Amortisierte Komplexität==&lt;br /&gt;
&lt;br /&gt;
Bis jetzt wurde die Komplexität nur im schlechtesten Fall (Worst Case) betrachtet. Bei einigen Operationen schwankt die Komplexität jedoch sehr stark, wenn man sie mehrmals hintereinander ausführt, und der schlechteste Fall kommt nur selten vor. Dann ist es sinnvoll, die &amp;lt;i&amp;gt;amortisierte Komplexität&amp;lt;/i&amp;gt; zu betrachten, die sich mit der &amp;lt;i&amp;gt;durchschnittlichen&amp;lt;/i&amp;gt; Komplexität über viele Aufrufe der selben Operation beschäftigt.&lt;br /&gt;
&lt;br /&gt;
Zum weiter Lesen: [[http://de.wikipedia.org/wiki/Amortisierte_Laufzeitanalyse Wikipedia: Amortisierte Laufzeitanalyse]]&lt;br /&gt;
&lt;br /&gt;
===Beispiel: Inkrementieren von Binärzahlen===&lt;br /&gt;
&lt;br /&gt;
Frage: Angenommen, das Umdrehen eines Bits einer Binärzahl verursacht Kosten von 1 Einheit. Wir erzeugen die Folge der natürlichen Zahlen in Binärdarstellung durch sukzessives Inkrementieren, von Null beginnend. Bei jeder Inkrementierung werden einige Bits verändert, aber diese Zahl (und damit die Kosten der Inkrementierungen) ''schwanken'' sehr stark. Wir fragen jetzt, was eine Inkrementierung im Durchschnitt kostet?&lt;br /&gt;
&lt;br /&gt;
Um diese Durchschnittskosten zu berechnen, bezahlen wir bei jeder Inkrementierung 2 Einheiten. Wenn davon nach Abzug der Kosten der jeweiligen Operation noch etwas übrig bleibt, wird der Rest dem  Guthaben zugeschrieben. Umgekehrt wird ein eventueller Fehlbetrag (wenn eine Inkrementierung mehr als 2 Bits umdreht) aus dem Guthaben gedeckt. Dadurch werden die ansonsten großen Schwankungen der Kosten ausgeglichen:&lt;br /&gt;
:: Kosten &amp;lt; Einzahlung &amp;amp;rarr; es wird gespart&lt;br /&gt;
:: Kosten = Einzahlung &amp;amp;rarr; Guthaben bleibt unverändert&lt;br /&gt;
:: Kosten &amp;gt; Einzahlung &amp;amp;rarr; Guthaben wird für die Kosten verbraucht&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
!Schritte&lt;br /&gt;
!Zahlen&lt;br /&gt;
!Kosten &amp;lt;br/&amp;gt;&lt;br /&gt;
(Anzahl der geänderten Bits)&lt;br /&gt;
! Einzahlung&lt;br /&gt;
!Guthaben =&amp;lt;br/&amp;gt;&lt;br /&gt;
altes Guthaben + Einzahlung - Kosten&lt;br /&gt;
|-&lt;br /&gt;
|1.&lt;br /&gt;
|0000&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|2.&lt;br /&gt;
|000&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|2&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|3.&lt;br /&gt;
|0001&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|4.&lt;br /&gt;
|00&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|3&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|5.&lt;br /&gt;
|0010&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|6.&lt;br /&gt;
|001&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|2&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|7.&lt;br /&gt;
|0011&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''3'''&lt;br /&gt;
|-&lt;br /&gt;
|8.&lt;br /&gt;
|0&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1000&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|4&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Die Kosten ergeben sich aus der Anzahl der Ziffern die von 1 nach 0, bzw. von 0 nach 1 verändert werden&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Rechnung:&amp;lt;/u&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Schritt: Kosten: 1 &amp;lt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Schritt: Kosten: 2 = Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird nicht gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; Guthaben bleibt so wie es ist &amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Schritt: Kosten: 1 &amp;lt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. Schritt: Kosten: 3 &amp;gt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird eine 1 vom Guthaben genommen um die Kosten zu zahlen&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
usw.&lt;br /&gt;
&lt;br /&gt;
Man erkennt, dass vor teuren Operation (Wechsel von 3 auf 4 bzw. von 7 auf 8) genügend Guthaben angespart wurde, um die Kosten zu decken. Das Guthaben geht bei diesen Operationen immer wieder auf 1 zurück, aber es wird nie vollständig verbraucht (Dies kann natürlich auch mathematisch exakt bewiesen werden, wie wir es unten am Beispiel des dynamische Arrays zeigen). Wir schließen daraus, dass die durchschnittlichen oder '''amortisierten Kosten''' einer Inkrementierungsoperation gleich 2 sind.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Zum weiter Lesen: [[http://de.wikipedia.org/wiki/Account-Methode Wikipedia Account-Methode]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
====Fazit====&lt;br /&gt;
Die amortisierte Komplexität beschäftigt sich mit dem Durchschnitt aller Operation im ungünstigsten Fall. Operationen mit hohen Kosten, die aber nur selten ausgeführt werden, fallen bei der amortisierten Komplexität nicht so ins Gewicht. Bei Algorithmen, die gelegentlich eine &amp;quot;teure&amp;quot; Operation benutzen, ansonsten jedoch &amp;quot;billige&amp;quot; Operationen aufrufen, kann die amortisierte Komplexität niedriger sein als die Komplexität im schlechtesten (Einzel-)Fall.&lt;br /&gt;
&lt;br /&gt;
In unserem Beispiel fallen die teuren Einzelschritte (z.B. 4. und 8. Schritt) bei den amortisierten Kosten nicht so ins Gewicht, da wir die Kosten aus unserem Guthaben mitbezahlen können. Das Guthaben ist immer groß genug, weil jeder zweite Aufruf eine billige Operation ist, die nur ein Bit umdreht und somit das Ansparen ermöglicht. Diese Betrachtung zeigt, dass die amortisierte (d.h. durchschnittliche) Komplexität des Algoithmus niedriger (nämlich konstant) ist als die Komplexität im schlechtesten Fall.&lt;br /&gt;
&lt;br /&gt;
===Anwendung: Dynamisches Array===&lt;br /&gt;
&lt;br /&gt;
Ein dynamisches Array hat die Eigenschaft, dass man effizient am Ende des Arrays neue Elemente anfügen kann, indem man die Länge des Arrays entsprechend vergrößert (siehe Übung 1). Die Analyse der amortisierten Komplexität der Anfüge-Operation zeigt uns, wie man das Vergrößern des Arrays richtig implementiert, damit die Operation wirklich effizient abläuft.&lt;br /&gt;
&lt;br /&gt;
==== Ineffiziente naive Lösung ====&lt;br /&gt;
&lt;br /&gt;
Wenn wir an ein Array ein Element anhängen wollen, müssen wir neuen Speicher allokieren, der die gewünschte Länge hat. Die Werte aus dem alten Array müssen dann in den neuen Speicher umkopiert werden. Danach kann das neue Element hinten angefügt werden, weil wir im neuen Array bereits Speicher für dieses Element reserviert haben. Bei der naiven Implementation des dynamischen Arrays wiederholt man dies bei jeder Anfügeoperation. Für die Analyse nehmen wir an, dass das Kopieren eines Elements konstante Zeit O(1) erfordert, ebenso das Einfügen eines neuen Elements auf in eine noch unbenutzte Speicherposition.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Naives Anhängen eines weiteren Elements an ein Array:&amp;lt;/u&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;right&amp;quot;&lt;br /&gt;
!Schritte&lt;br /&gt;
|'''Array'''&lt;br /&gt;
&amp;lt;small&amp;gt;(wie es nach jedem Schritt aussieht)&amp;lt;/small&amp;gt;&lt;br /&gt;
!Komplexität&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;center&amp;gt;altes Array (N=4)&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|1. neuer Speicher für&amp;lt;br&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;(N+1) Elemente&lt;br /&gt;
|&amp;lt;center&amp;gt;[None,None,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;O(N+1) = '''O(N)'''&amp;lt;/center&amp;gt;(wenn der Speicher initialisiert wird&amp;lt;br&amp;gt;(hier auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;), sonst O(1))&lt;br /&gt;
|-&lt;br /&gt;
|2. Kopieren &lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;'''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|3. append von &amp;quot;x&amp;quot;&lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3,'x']&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;'''O(1)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
altesArray = [0,1,2,3]&amp;lt;br /&amp;gt;&lt;br /&gt;
altesArray.append('x')&lt;br /&gt;
&lt;br /&gt;
1. Es wird ein neues Array der Größe N+1 erzeugt&amp;lt;br /&amp;gt;&lt;br /&gt;
2. Die N Datenelemente aus dem alten Array werden in das neue Array kopiert&amp;lt;small&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Das sind N Operationen der Komplexität O(1), also ein Gesamtaufwand von O(N).&amp;lt;/small&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
3. 'x' wird mit Aufwand O(1) an die letzte Stelle des neuen Arrays geschrieben&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Additionsregel:&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
O(N) + O(1) &amp;amp;isin; O(N)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Folgerung:&amp;lt;/b&amp;gt; &lt;br /&gt;
&lt;br /&gt;
Bei der naiven Methode erfordert jede Anfügung einen Aufwand O(N) (wobei N die derzeitige Arraygröße ist). Dies ist nicht effizient.&lt;br /&gt;
&lt;br /&gt;
====Effiziente Lösung durch Verdoppeln der Kapazität====&lt;br /&gt;
&lt;br /&gt;
Offensichtlich kommt man nicht darum heraum, den Inhalt des alten Arrays zu kopieren, wenn der allokierte Speicher voll ist. Der Trick für die effiziente Implementation der Anfügeoperation besteht darin, das Kopieren so selten wie möglich durchzuführen, also nicht wie in der naiven Lösung bei jeder Anfügeoperation. Hier kommt die amortisierte Komplexität ins Spiel: Ab und zu gibt es eine teure Anfügeoperation (wenn nämlich kopiert werden muss), aber wenn man den durchschnittlichen Aufwand über viele Anfügungen betrachtet, ist die Operation effizient. Der teure Fall wird sozusagen &amp;quot;herausgemittelt&amp;quot;. &lt;br /&gt;
&lt;br /&gt;
Um nur selten kopieren zu müssen, werden beim dynamischen Array mehr Speicherelemente reserviert als zur Zeit benötigt werden (in der naiven Lösung wurde dagegen immer nur Speicher für ein einziges neues Element reserviert). Wir unterscheiden deshalb &lt;br /&gt;
&lt;br /&gt;
:&amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; = Anzahl der allokierten Speicherzellen, d.h. der möglichen Elemente, die in das Array passen&amp;lt;br /&amp;gt;&lt;br /&gt;
:&amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt; = Anzahl der Elemente, die im Array zur Zeit gespeichert sind&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Daten selbst werden in einem statischen Array gespeichert:&lt;br /&gt;
:&amp;lt;tt&amp;gt;data&amp;lt;/tt&amp;gt; = statisches Array der Größe &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die folgende intuitive Abschätzung zeigt, dass es sinnvoll ist, die Größe des allokierten Speichers jeweils zu verdoppeln. Wir starten bei einem Array der Größe &amp;lt;tt&amp;gt;size = capacity&amp;lt;/tt&amp;gt; = N. Da der verfügbare Speicher voll ist, müssen wir bei der nächsten Anfügung die N vorhandenen Elemente in ein neues Array der Länge &amp;lt;tt&amp;gt;new_capacity&amp;lt;/tt&amp;gt; kopieren (Aufwand &amp;lt;math&amp;gt;N\cdot O(1)&amp;lt;/math&amp;gt;). Danach können wir K Elemente billig einfügen (Aufwand &amp;lt;math&amp;gt;K\cdot O(1)&amp;lt;/math&amp;gt;), wobei &lt;br /&gt;
:K = &amp;lt;tt&amp;gt;new_capacity - capacity&amp;lt;/tt&amp;gt; &lt;br /&gt;
die Anzahl der nach dem Kopieren noch unbenutzen Speicherzellen ist. Der durchschnittliche Aufwand für diese K Einfügungen ist somit&lt;br /&gt;
:&amp;lt;math&amp;gt;\bar T = \frac{N \cdot O(1) + K \cdot O(1)}{K}=\frac{N+K}{K}\cdot O(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Damit die mittlere Zeit in O(1) sein kann, muss der Quotient &amp;lt;math&amp;gt;(N+K)/K&amp;lt;/math&amp;gt; eine Konstante sein. Wir setzen &amp;lt;math&amp;gt;K = a N&amp;lt;/math&amp;gt; und erhalten:&lt;br /&gt;
:&amp;lt;math&amp;gt;\bar T = \frac{(a+1)N}{a N}\cdot O(1)=\frac{a+1}{a}\cdot O(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Der amortisierte Aufwand über K Einfügungen ist also konstant, wenn &amp;lt;math&amp;gt;a&amp;lt;/math&amp;gt; eine (kleine) von N unabhängige Zahl ist. Typischerweise wählt man &lt;br /&gt;
:&amp;lt;math&amp;gt;a = 1&amp;lt;/math&amp;gt;&lt;br /&gt;
und mit &amp;lt;math&amp;gt;K = 1\cdot N&amp;lt;/math&amp;gt; ergibt sich&lt;br /&gt;
:&amp;lt;tt&amp;gt;new_capacity = capacity&amp;lt;/tt&amp;gt; + N = &amp;lt;tt&amp;gt;2 * capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Vorgehensweise beim Zufügen eines neuen Elements im Fall &amp;lt;tt&amp;gt;size == capacity&amp;lt;/tt&amp;gt; ist also&lt;br /&gt;
* capacity wird verdoppelt&amp;lt;br /&amp;gt;&lt;br /&gt;
: &amp;lt;tt&amp;gt;neue capacity = 2 * alte capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
: (allgemein genügt es auch, wenn capacity um einen bestimmten Prozentsatz vergrößert wird, &lt;br /&gt;
:: &amp;lt;tt&amp;gt;neue capacity = alte capacity * c&amp;lt;/tt&amp;gt; &lt;br /&gt;
: mit c &amp;gt; 1, z.B. c = 1.2, das entspricht oben der Wahl &amp;lt;math&amp;gt;a = 0.2&amp;lt;/math&amp;gt;)&lt;br /&gt;
* ein neues statisches Array der Größe 'neue capacity' wird erzeugt&lt;br /&gt;
* das alte Array wird ins neue kopiert und danach freigegeben&lt;br /&gt;
* das anzufügende Element wird ins neue Array eingefügt&lt;br /&gt;
Umgekehrt geht man beim Entfernen des ''letzten'' Array-Elements vor. Normalerweise überschreibt man einfach das letzte Element mit &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; und dekrementiert &amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt;. Wird dadurch das Array zu klein (üblicherweise &amp;lt;tt&amp;gt;size &amp;amp;lt; capacity / 4&amp;lt;/tt&amp;gt;), wird die Kapazität halbiert, genauer:&lt;br /&gt;
* ein neues Array mit &amp;lt;br/&amp;gt;&lt;br /&gt;
: &amp;lt;tt&amp;gt;neue capacity = alte capacity / 2 &amp;lt;/tt&amp;gt;&lt;br /&gt;
: wird angelegt (bzw. mit&lt;br /&gt;
:: &amp;lt;tt&amp;gt;neue capacity = alte capacity / c &amp;lt;/tt&amp;gt;&lt;br /&gt;
: wenn ein anderer Vergrößerungsfaktor verwendet wird)&lt;br /&gt;
* das alte Array wird ins neue kopiert und danach freigegeben&lt;br /&gt;
&lt;br /&gt;
'''Folge:''' Die Kosten für das Vergrößern/Verkleinern der Kapazität werden amortisiert über viele Einfügungen, die kein Vergrößern erfordern. Die Operation &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; besitzt amortisierte Komplexität O(1). Im folgenden Abschnitt zeigen wir dies mathematisch exakt mit der Potentialmethode.&lt;br /&gt;
&lt;br /&gt;
====Komplexitätsanalyse des dynamischen Arrays mit Potentialmethode====&lt;br /&gt;
&lt;br /&gt;
Durchschnitt der Gesamtkosten für N-maliges append = &amp;lt;math&amp;gt;\frac{1}{N} \sum_{i = 1}^N Kosten(i)&amp;lt;/math&amp;gt;. Zur Analyse der amortisierten Komplexität wird ein Potential&amp;lt;br/&amp;gt;&lt;br /&gt;
::&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2*size&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; - capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
eingeführt, wobei das Array nach dem i-ten Einfüge-Schritt die Größe size&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; und die Kapizität capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; hat. Wir nehmen vereinfachend an, dass es keine Löschoperationen gibt. Dann gilt nach dem i-ten Schritt jeweils&lt;br /&gt;
::&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2*i - capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Fall 1: Array ist nicht voll&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Es wird kein Umkopieren benötigt, da das Array noch nicht voll ist&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; size&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; &amp;lt; capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; == capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Kosten: 1 (für Einfügen des neuen Elements)&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial vor append: &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = 2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial nach append: &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
amortisierte Kosten = Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Phi;&amp;lt;sub&amp;gt;(i)&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;(i-1)&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::                 = 1            + (2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;)        - [2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;]&lt;br /&gt;
:::::                 = 1            + 2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;         - 2i + 2 + capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::                 = 1            + &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; - &amp;lt;del&amp;gt;capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;/del&amp;gt; - &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; + 2 + &amp;lt;del&amp;gt;capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;/del&amp;gt;&lt;br /&gt;
:::::                 = 1 + 2&lt;br /&gt;
:::::                 = 3 = O(1) &amp;amp;rarr; konstant&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Fall 2: Array ist voll&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Vor dem i-ten append muss umkopiert werden&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; size&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; == capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; == i-1&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; Allokieren eines neuen statischen Arrays mit verdoppelter Kapazität notwendig, also capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; == 2*capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Kosten: (i-1) + 1 (für Umkopieren und Einfügen des neuen Elements)&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial vor append =  &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = 2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial nach append =  &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; =  2i - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
amortisierte Kosten = Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Phi;&amp;lt;sub&amp;gt;(i)&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;(i-1)&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = ((i - 1) + 1) + 2i - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; - [2(i-1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;]&lt;br /&gt;
:::::               = i + &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; - &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; + 2 - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = i + 2 - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = i + 2 - (i - 1) &amp;lt;small&amp;gt;(da capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = i-1)&amp;lt;/small&amp;gt;&lt;br /&gt;
:::::               = 3 = O(1) &amp;amp;rarr; konstant            &lt;br /&gt;
&lt;br /&gt;
'''Damit wurde bewiesen, dass die Operation &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; beim dynamischen Array eine amortisierte Komplexität von 3 Einheiten hat, also &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; &amp;amp;isin; O(1)'''. Diese Operation kann deshalb gefahrlos in der inneren Schleife eines Algorithmus benutzt werden.&lt;br /&gt;
&lt;br /&gt;
==== Beispiel für 9 Einfügeoperationen ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot;&lt;br /&gt;
!Array&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;lt;small&amp;gt;(wie es aussehen könnte)&amp;lt;/small&amp;gt;&lt;br /&gt;
!size&lt;br /&gt;
!capacity&lt;br /&gt;
!Kosten für append&amp;lt;br /&amp;gt;(einschließlich Umkopieren)&lt;br /&gt;
!Summe Kosten&lt;br /&gt;
!Durchschnittskosten&lt;br /&gt;
!&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2 * size - capacity&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;lt;small&amp;gt;(i = size)&amp;lt;/small&amp;gt;&lt;br /&gt;
!Potenzialdifferenz&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;amp;Delta; &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
!amortisierte Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
= Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Delta; &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;0&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3/2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6/3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;0&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7/4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;5&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;12&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;12/5&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;13&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;13/6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;14&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;14/7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,h]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;15&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;15/8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,h,j,None,None,None,&amp;lt;br /&amp;gt;&lt;br /&gt;
None,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;9&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;16&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;24&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;24/9&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die durchschnittlichen Kosten betragen stets etwa 2 Einheiten, schwanken allerdings so, dass nicht unmittelbar ersichtlich ist, ob dies für sämtliche Einfügeoperationen gilt. Die amortisierte Komplexität, die mit Hilfe des Potentials berechnet wird, ist hingegen konstant 3, wie auch im obigen Beweis für alle Einfügeoperationen allgemein gezeigt wurde.&lt;br /&gt;
&lt;br /&gt;
[[Suchen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Effizienz&amp;diff=5418</id>
		<title>Effizienz</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Effizienz&amp;diff=5418"/>
				<updated>2012-07-27T16:01:27Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Fazit */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Bei der Diskussion von Effizienz müssen wir zwischen der Laufzeit eines Algorithmus auf einem bestimmten System und seiner prinzipiellen Leistungsfähigkeit (Algorithmenkomplexität) unterscheiden. Der Benutzer ist natürlich vor allem an der Laufzeit interessiert, denn diese bestimmt letztendlich seine Arbeitsproduktivität. Ein Softwaredesigner hingegen muss eine Implementation wählen, die auf verschiedenen Systemen und in verschiedenen Anwendungen schnell ist. Für ihn sind daher auch Aussagen zur Algorithmenkomplexität sehr wichtig, um den am besten geeigneten Algorithmus auszuwählen.&lt;br /&gt;
&lt;br /&gt;
== Laufzeit ==&lt;br /&gt;
&lt;br /&gt;
Aus Anwendersicht ist ein Algorithmus effizient, wenn er die in der Spezifikation verlangten Laufzeitgrenzen einhält. Ein Algorithmus muss also nicht immer so schnell wie möglich sein, sondern so schnell wie nötig. Dies führt in verschiedenen Anwendungen zu ganz unterschiedliche Laufzeitanforderungen:&lt;br /&gt;
&lt;br /&gt;
* Berechnen des nächsten Steuerkommandos für eine Maschine: ca. 1/1000s&lt;br /&gt;
* Berechnen des nächsten Bildes für eine Videopräsentation (z.B. Dekompression von MPEG-kodierten Bildern): ca. 1/25s&lt;br /&gt;
: Geringere Bildraten führen zu ruckeligen Filmen.&lt;br /&gt;
* Sichtbare Antwort auf ein interaktives Kommando (z.B. Mausklick): ca. 1/2s&lt;br /&gt;
: Wird diese Antwortzeit überschritten, vermuten viele Benutzer, dass der Mausklick nicht funktioniert hat, und klicken nochmals, mit eventuell fatalen Folgen. Wenn ein Algorithmus notwendigerweise länger dauert als 1/2s, sollte ein Fortschrittsbalken angezeigt werden.&lt;br /&gt;
* Wettervorhersage: muss spätestens am Vorabend des vorhergesagten Tages beendet sein&lt;br /&gt;
&lt;br /&gt;
===Laufzeitvergleich===&lt;br /&gt;
&lt;br /&gt;
Da die Laufzeit für den Benutzer ein so wichtiges Kriterium ist, werden häufig Laufzeitvergleiche durchgeführt. Deren Ergebnisse hängen allerdings von vielen Faktoren ab, die möglicherweise nicht kontrollierbar sind:&lt;br /&gt;
* Geschwindigkeit und Anzahl der Prozessoren&lt;br /&gt;
* Auslastung des Systems&lt;br /&gt;
* Größe des Hauptspeichers  und Cache, Geschwindigkeit des  Datenbus&lt;br /&gt;
* Qualität des Compilers/Optimierers (ist der Compiler für die spezielle Prozessor-Architektur optimiert?)&lt;br /&gt;
* Geschick des Programmierers&lt;br /&gt;
* Daten (Beispiel Quicksort: Best case und worst case [vorsortierter Input] stark unterschiedlich)&lt;br /&gt;
All diese Faktoren sind untereinander abhängig. Laufzeitvergleiche sind daher mit Vorsicht zu interpretieren.&lt;br /&gt;
Generell sollten bei Vergleichen möglichst wenige Parameter verändert werden, z.B.&lt;br /&gt;
* gleiches Programm (gleiche Kompilierung), gleiche Daten, andere Prozessoren&lt;br /&gt;
oder&lt;br /&gt;
* gleiche CPU, Daten, andere Programme (Vergleich von Algorithmen)&lt;br /&gt;
Zur Verbesserung der Vergleichbarkeit gibt es standardisierte [http://en.wikipedia.org/wiki/Benchmark_(computing) Benchmarks], die bestimmte Aspekte eines Systems unter möglichst realitätsnahen Bedingungen testen. Generell gilt aber: Durch Laufzeitmessung ist schwer festzustellen, ob ein Algorithmus ''prinzipiell'' besser ist als ein anderer. Dafür ist die Analyse der [[Effizienz#Algorithmen-Komplexität|Algorithmenkomplexität]] notwendig.&lt;br /&gt;
&lt;br /&gt;
===Optimierung der Laufzeit===&lt;br /&gt;
&lt;br /&gt;
Wenn sich herausstellt, dass ein bereits implementierter Algorithmus zu langsam läuft, geht man wie folgt vor:&lt;br /&gt;
&lt;br /&gt;
# Man verwendet einen [http://en.wikipedia.org/wiki/Performance_analysis Profiler], um zunächst den Flaschenhals zu bestimmen. Ein Profiler ist ein Hilfsprogramm, das während der Ausführung eines Programms misst, wieviel Zeit in jeder Funktion und Unterfunktion verbraucht wird. Dadurch kann man herausfinden, welcher Teil des Algorithmus überhaupt Probleme bereitet. Donald Knuth gibt z.B. als Erfahrungswert an, dass Programme während des größten Teils ihrer Laufzeit nur 3% des Quellcodes (natürlich mehrmals wiederholt) ausführen [http://pplab.snu.ac.kr/courses/adv_pl05/papers/p261-knuth.pdf]. Es ist sehr wichtig, diese 3% experimentell zu bestimmen, weil die Erfahrung zeigt, dass man beim Erraten der kritischen Programmteile oft falsch liegt. Man spricht dann von &amp;quot;[http://en.wikipedia.org/wiki/Optimization_%28computer_science%29#When_to_optimize premature optimization]&amp;quot;, also von voreiliger Optimierung ohne experimentelle Untersuchung der wirklichen Laufzeiten, was laut Knuth &amp;quot;the root of all evil&amp;quot; ist. Der Python-Profiler wird in [http://docs.python.org/lib/profile.html Kapitel 25] der Python-Dokumentation beschrieben.&lt;br /&gt;
# Man kann dann versuchen, die kritischen Programmteile zu optimieren.&lt;br /&gt;
# Falls der Laufzeitgewinn durch Optimierung zu gering ist, muss man einen prinzipiell schnelleren Algorithmus verwenden, falls es einen gibt.&lt;br /&gt;
&lt;br /&gt;
Einige wichtige Techniken der Programmoptimierung sollen hier erwähnt werden. Wenn man einen optimierenden Compiler verwendet, werden einige Optimierungen automatisch ausgeführt [http://en.wikipedia.org/wiki/Compiler_optimization]. In Python trifft dies jedoch nicht zu. Um den Sinn einiger Optimierungen zu verstehen, benötigt man Grundkenntnisse der Computerarchitektur.&lt;br /&gt;
&lt;br /&gt;
;Elimination von redundantem Code: Es ist offensichtlich überflüssig, dasselbe Ergebnis mehrmals zu berechnen, wenn es auch zwischengespeichert werden könnte. Diese Optimierung wird von vielen automatischen Optimierern unterstützt und kommt im wesentlichen in zwei Ausprägungen vor:&lt;br /&gt;
:; common subexpression elimination: In mathematischen Ausdrücken wird ein Teilergebnis häufig mehrmals benötigt. Man betrachte z.B. die Lösung der quadratischen Gleichung &amp;lt;math&amp;gt;x^2+p\,x+q = 0&amp;lt;/math&amp;gt;:&lt;br /&gt;
        x1 = - p / 2.0 + sqrt(p*p/4.0 - q)&lt;br /&gt;
        x2 = - p / 2.0 - sqrt(p*p/4.0 - q)&lt;br /&gt;
::Die mehrmalige Berechnung von Teilausdrücken wird vermieden, wenn man stattdessen schreibt:&lt;br /&gt;
        p2 = - p / 2.0&lt;br /&gt;
        r  = sqrt(p2*p2 - q)&lt;br /&gt;
        x1 = p2 + r&lt;br /&gt;
        x2 = p2 - r&lt;br /&gt;
:; loop invariant elimination: Wenn ein Teilausdruck sich in einer Schleife nicht ändert, muss man ihn nicht bei jedem Schleifendurchlauf neu berechnen, sondern kann dies einmal vor Beginn der Schleife tun. Ein typisches Beispiel hierfür ist die Adressierung von Matrizen, die als 1-dimensionales Array gespeichert sind. Angenommen, wir speichern eine NxN Matrix &amp;lt;tt&amp;gt;m&amp;lt;/tt&amp;gt; in einem Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; der Größe N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt;, so dass das Matrixelement &amp;lt;tt&amp;gt;m&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt;&amp;lt;/tt&amp;gt; durch &amp;lt;tt&amp;gt;a[i + j*N]&amp;lt;/tt&amp;gt; indexiert wird. Wir betrachten die Aufgabe, eine Einheitsmatrix zu initialisieren. Ein nicht optimierter Algorithmus dafür lautet:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + j*N] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + j*N] = 0.0&lt;br /&gt;
::Der Ausdruck &amp;lt;tt&amp;gt;j*N&amp;lt;/tt&amp;gt; wird hier in jedem Schleifendurchlauf erneut berechnet, obwohl sich &amp;lt;tt&amp;gt;j&amp;lt;/tt&amp;gt; in der inneren Schleife gar nicht verändert. Man kann deshalb optimieren zu:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + jN] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + jN] = 0.0&lt;br /&gt;
;Vereinfachung der inneren Schleife: Generell sollte man sich bei der Optimierung auf die innere Schleife eines Algorithmus konzentrieren, weil dieser Code am häufigsten ausgeführt wird. Insbesondere sollte man die Anzahl der Befehle in der inneren Schleife so gering wie möglich halten und teure Befehle vermeiden. Früher waren vor allem Floating-Point Befehle teuer, die man oft durch die schnellere Integer-Arithmetik ersetzt hat, falls dies algorithmisch möglich war (diesen Rat findet man noch oft in der Literatur). Heute hat sich die Hardware so verbessert, dass im Allgemeinen nur noch die Floating-Point Division deutlich langsamer ist als die anderen Operatoren. Im obigen Beispiel der quadratischen Gleichung ist es daher sinnvoll, den Ausdruck &lt;br /&gt;
        p2 = -p / 2.0&lt;br /&gt;
:durch&lt;br /&gt;
        p2 = -0.5 * p&lt;br /&gt;
:zu ersetzen. Dadurch ersetzt man eine Division durch eine Multiplikation und spart außerdem das Negieren von &amp;lt;tt&amp;gt;p&amp;lt;/tt&amp;gt;, da der Compiler direkt mit &amp;lt;tt&amp;gt;-0.5&amp;lt;/tt&amp;gt; multipliziert.&lt;br /&gt;
;Ausnutzung der Prozessor-Pipeline: Moderne Prozessoren führen mehrere Befehle parallel aus. Dies ist möglich, weil jeder Befehl in mehrere Teilschritte zerlegt werden kann. Eine generische Unterteilung in vier Teilschritte ist z.B.:&lt;br /&gt;
:# Dekodieren des nächsten Befehls&lt;br /&gt;
:# Beschaffen der Daten, die der Befehl verwendet (aus Prozessorregistern, dem Cache, oder dem Hauptspeicher)&lt;br /&gt;
:# Ausführen des Befehls&lt;br /&gt;
:# Schreiben der Ergebnisse&lt;br /&gt;
:Man bezeichnet dies als die &amp;quot;[http://en.wikipedia.org/wiki/Instruction_pipeline instruction pipeline]&amp;quot; des Prozessors (heutige Prozessoren verwenden wesentlich feinere Unterteilungen). Prozessoren werden nun so gebaut, dass mehrere Befehle parallel, auf verschiedenen Ausführungsstufen ausgeführt werden. Wenn Befehl 1 also beim Schreiben der Ergebnisse angelangt ist, kann Befehl 2 die Hardware zum Ausführen des Befehls benutzen, während Befehl 3 seine Daten holt, und Befehl 4 soeben dekodiert wird. Unter bestimmten Bedingungen funktioniert diese Parallelverarbeitung jedoch nicht. Dies gibt Anlass zu Optimierungen:&lt;br /&gt;
:;Vermeiden unnötiger Typkonvertierungen: Der Prozessor verarbeitet Interger- und Floating-Point-Befehle in verschiedenen Pipelines, weil die Hardwareanforderungen sehr verschieden sind. Wird jetzt ein Ergebnis von Integer nach Floating-Point umgewandelt oder umgekehrt, muss die jeweils andere Pipeline warten, bis die erste Pipeline ihre Berechnung beendet. Es kann dann besser sein, Berechnungen in Floating-Point zu Ende zu führen, auch wenn sie semantisch eigentlich Integer-Berechnungen sind.&lt;br /&gt;
:;Reduzierung der Anzahl von Verzweigungen: Wenn der Code verzweigt (z.B. durch eine &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;- oder  &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Anweisung), ist nicht klar, welcher Befehl nach der Verzweigung ausgeführt werden soll, bevor Stufe 3 der Pipeline die Verzweigungsbedingung ausgewertet hat. Bis dahin wären die ersten beiden Stufen der Pipeline unbenutzt. Moderne Prozessoren benutzen zwar ausgefeilte Heuristiken, um das Ergebnis der Bedingung vorherzusagen, und führen den hoffentlich richtigen Zweig des Codes spekulativ aus, aber dies funktioniert nicht immer. Man sollte deshalb generell die Anzahl der Verzweigungen minimieren. Als Nebeneffekt führt dies meist auch zu besser lesbarem, verständlicherem Code. Im Matrixbeispiel kann man&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + jN] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + jN] = 0.0&lt;br /&gt;
::durch&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               a[i + jN] = 0.0&lt;br /&gt;
           a[j + jN] = 1.0&lt;br /&gt;
ersetzen. Die Diagonalelemente &amp;lt;tt&amp;gt;a[j + jN]&amp;lt;/tt&amp;gt; werden jetzt zwar zweimal initialisiert (in der Schleife auf Null, dann auf Eins), aber durch Elimination der &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage wird dies wahrscheinlich mehr als ausgeglichen, zumal dadurch die innere Schleife wesentlich vereinfacht wurde.&lt;br /&gt;
;Ausnutzen des Prozessor-Cache: Zugriffe auf den Hauptspeicher sind sehr langsam. Deshalb werden stets ganze Speicherseiten auf einmal in den [http://en.wikipedia.org/wiki/Cache Cache] des Prozessors geladen. Wenn unmittelbar nacheinander benutzte Daten auch im Speicher nahe beieinander liegen (sogenannte &amp;quot;[http://en.wikipedia.org/wiki/Locality_of_reference locality of reference]&amp;quot;), ist die Wahrscheinlichkeit groß, dass die als nächstes benötigten Daten bereits im Cache sind und damit schnell gelesen werden können. Bei vielen Algorithmen kann man die Implementation so umordnen, dass die locality of reference verbessert wird, was zu einer drastischen Beschleunigung führt. Im Matrix-Beispiel ist z.B. die Reihenfolge der Schleifen wichtig. Für konstanten Index &amp;lt;tt&amp;gt;j&amp;lt;/tt&amp;gt; liegen die Indizes &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; im Speicher hintereinander. Deshalb ist es günstig, in der inneren Schleife über &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu iterieren:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               a[i + jN] = 0.0&lt;br /&gt;
           a[j + jN] = 1.0&lt;br /&gt;
:Die umgekehrte Reihenfolge der Schleifen ist hingegen ungünstig&lt;br /&gt;
       for i in range(N):&lt;br /&gt;
           for j in range(N):&lt;br /&gt;
               a[i + j*N] = 0.0&lt;br /&gt;
           a[i + i*N] = 1.0&lt;br /&gt;
:Jetzt werden in der inneren Schleife stets N Datenelemente übersprungen. Besonders bei großem N muss man daher häufig den Cache neu füllen, was bei der ersten Implementation nicht notwendig war.  (Außerdem verliert man hier die Optimierung &amp;lt;tt&amp;gt;jN = j*N&amp;lt;/tt&amp;gt;, die jetzt nicht mehr möglich ist.)&lt;br /&gt;
&lt;br /&gt;
Als Faustregel kann man durch Optimierung eine Verdoppelung der Geschwindigkeit erreichen (in Ausnahmefällen auch mehr). Benötigt man stärkere Verbesserungen, muss man wohl oder übel einen besseren Algorithmus oder einen schnelleren Computer verwenden.&lt;br /&gt;
&lt;br /&gt;
== Algorithmen-Komplexität ==&lt;br /&gt;
&lt;br /&gt;
Komplexitätsbetrachtungen ermöglichen den Vergleich der prinzipiellen Eigenschaften von Algorithmen unabhängig von einer Implementation, Umgebung etc.&lt;br /&gt;
      &lt;br /&gt;
Eine einfache Möglichkeit ist das Zählen der Aufrufe einer Schlüsseloperation. Beispiel Sortieren:&lt;br /&gt;
* Anzahl der Vergleiche&lt;br /&gt;
* Anzahl der Vertauschungen&lt;br /&gt;
&lt;br /&gt;
=== Beispiel: Selection Sort ===&lt;br /&gt;
&lt;br /&gt;
  for i in range(len(a)-1):&lt;br /&gt;
    max = i&lt;br /&gt;
    for j in range(i+1, len(a)):&lt;br /&gt;
      if a[j] &amp;lt; a[max]:&lt;br /&gt;
        max = j&lt;br /&gt;
    a[max], a[i] = a[i], a[max]      # swap&lt;br /&gt;
&lt;br /&gt;
*Anzahl der Vergleiche: Ein Vergleich in jedem Durchlauf der inneren Schleife. Es ergibt sich folgende Komplexität:&lt;br /&gt;
*:Ingesamt &amp;lt;math&amp;gt;\sum_{i=0}^{N-2} \sum_{j=i+1}^{N-1}1 = \frac{N}{2} (N-1) \!&amp;lt;/math&amp;gt; Vergleiche.&lt;br /&gt;
&lt;br /&gt;
*Anzahl der Vertauschungen (swaps): Eine Vertauschung pro Durchlauf der äußeren Schleife:&lt;br /&gt;
*:Insgesamt &amp;lt;math&amp;gt;N-1 \!&amp;lt;/math&amp;gt; Vertauschungen&lt;br /&gt;
&lt;br /&gt;
Die Komplexität wird durch die Operationen bestimmt, die am häufigsten ausgeführt werden, hier also die Anzahl der Vergleiche. Die Anzahl der Vertauschungen ist hingegen kein geeignetes Kriterium für die Komplexität von selection sort, weil der Aufwand in der inneren Schleife ignoriert würde.&lt;br /&gt;
&lt;br /&gt;
=== Fallunterscheidung: Worst und Average Case ===&lt;br /&gt;
&lt;br /&gt;
Die Komplexität ist in der Regel eine Funktion der Eingabegröße (Anzahl der Eingabebits, Anzahl der Eingabeelemente). Sie kann aber auch von der Art der Daten abhängen, nicht nur von der Menge, z.B. vorsortierte Daten bei Quicksort. Um von der Art der Daten unabhängig zu werden, kann man zwei Fälle der Komplexität unterscheiden:&lt;br /&gt;
      &lt;br /&gt;
* Komplexität im ungünstigsten Fall &lt;br /&gt;
*: Der ungünstigste Fall ist die Eingabe gegebener Länge, für die der Algorithmus am langsamsten ist. Der Nachteil dieser Methode besteht darin, dass dieser ungünstige Fall in der Praxis vielleicht gar nicht oder nur selten vorkommt, so dass sich der Algorithmus in Wirklichkeit besser verhält als man nach dieser Analyse erwarten würde. Beim Quicksort-Algorithmus mit zufälliger Wahl des Pivot-Elements müsste z.B. stets das kleinste oder größte Element des aktuellen Intervalls als Pivot-Element gewählt werden, was äußerst unwahrscheinlich ist.&lt;br /&gt;
* Komplexität im durchschnittlichen/typischen Fall&lt;br /&gt;
*: Der typische Fall ist die mittlere Komplexität des Algorithmus über alle möglichen Eingaben. Dazu muss man die Wahrscheinlichkeit jeder möglichen Eingabe kennen, und berechnet dann die mittlere Laufzeit über dieser Wahrscheinlichkeitsverteilung. Leider ist die Wahrscheinlichkeit der Eingaben oft nicht bekannt, so dass man geeignete Annahmen treffen muss. Bei Sortieralgorithmen können z.B. alle möglichen Permutationen des Eingabearrays als gleich wahrscheinlich angenommen werden, und der typische Fall ist dann die mittlere Komplexität über alle diese Eingaben. Oft hat man jedoch in der Praxis andere Wahrscheinlichkeitsverteilungen, z.B. sind die Daten oft &amp;quot;fast sortiert&amp;quot; (nur wenige Elemente sind an der falschen Stelle). Dann verhält sich der Algorithmus ebenfalls anders als vorhergesagt.&lt;br /&gt;
&lt;br /&gt;
Wir beschränken uns in dieser Vorlesung auf die Komplexität im ungünstigseten Fall. '''Exakte''' Formeln für Komplexität sind aber auch dann schwer zu gewinnen, wie das folgende Beispiel zeigt:&lt;br /&gt;
&lt;br /&gt;
=== Beispiele aus den Übungen (Gemessene Laufzeiten für Mergesort/Selectionsort) ===&lt;br /&gt;
&lt;br /&gt;
* Mergesort: &amp;lt;math&amp;gt;\frac{0,977N\log N}{\log 2} + 0,267N-4.39 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: andere Lösung: &amp;lt;math&amp;gt;1140 N\log(N) - 1819N + 6413 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* Selectionsort: &amp;lt;math&amp;gt;\frac{1}{2}N^2 - \frac{1}{2N} - 10^{-12} \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: andere Lösung: &amp;lt;math&amp;gt;1275N^2 - 116003^N + 11111144 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Aus diesen Formeln wird nicht offensichtlich, welcher Algorithmus besser ist.&lt;br /&gt;
Näherung: Betrachte nur '''sehr große Eingaben''' (meist sind alle Algorithmen schnell genug für kleine Eingaben). Dieses Vorgehen wird als '''Asymptotische Komplexität''' bezeichnet (N gegen unendlich).&lt;br /&gt;
&lt;br /&gt;
=== Asymptotische Komplexität am Beispiel Polynom ===&lt;br /&gt;
&lt;br /&gt;
Polynom: &amp;lt;math&amp;gt;a\,x^2+b\,x+c=p\!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;x \!&amp;lt;/math&amp;gt; sei die Eingabegröße, und wir betrachten die Entwicklung von &amp;lt;math&amp;gt;p \!&amp;lt;/math&amp;gt; in Abhängigkeit von &amp;lt;math&amp;gt;x \!&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;math&amp;gt;x=0 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=c \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x=1 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=a+b+c \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x=1000 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=1000000a+1000b+c \approx 1000000a\!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x \to \infty \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p \approx x^2a\!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für sehr große Eingaben verlieren also ''b'' und ''c'' immer mehr an Bedeutung, so dass am Ende nur noch ''a'' für die Komplexitätsbetrachtung wichtig ist.&lt;br /&gt;
&lt;br /&gt;
== Landau-Symbole ==&lt;br /&gt;
&lt;br /&gt;
Um die asymptotische Komplexität verschiedener Algorithmen miteinander vergleichen zu können, verwendet man die sogenannten [http://de.wikipedia.org/wiki/Landau-Symbole Landau-Symbole]. Das wichtigste Landau-Symbol ist &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;, mit dem man eine ''obere Schranke'' &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; für die Komplexität angeben kann. &lt;br /&gt;
&lt;br /&gt;
Schreibt man &amp;lt;math&amp;gt;f \in \Omega(g)&amp;lt;/math&amp;gt;, so stellt dies eine asymptotische ''untere Schranke'' für die Funktion f dar.&lt;br /&gt;
&lt;br /&gt;
Schließlich bedeutet &amp;lt;math&amp;gt;f \in \Theta(g)&amp;lt;/math&amp;gt;, dass die Funktion f genauso schnell wie die Funktion g wächst, das heißt man hat eine asymptotisch ''scharfe Schranke'' für f. Hierzu muss sowohl &amp;lt;math&amp;gt;f\in\mathcal{O}(g)&amp;lt;/math&amp;gt; als auch &amp;lt;math&amp;gt;f \in \Omega(g)&amp;lt;/math&amp;gt; erfüllt sein. &lt;br /&gt;
&lt;br /&gt;
Im nun folgenden soll auf die verschiedenen Landau-Symbole noch näher eingegeangen werden.&lt;br /&gt;
&lt;br /&gt;
===O-Notation===&lt;br /&gt;
&lt;br /&gt;
Intuitiv gilt: Für große N dominieren die am schnellsten wachsenden Terme einer Funktion. Die Notation &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; (sprich &amp;quot;f ist in O von g&amp;quot; oder &amp;quot;f ist von derselben Größenordnung wie g&amp;quot;) formalisiert eine solche Abschätzung der asymptotischen Komplexität der Funktion f von oben. &lt;br /&gt;
; Asymptotische Komplexität: Für zwei Funktionen f(x) und g(x) gilt&lt;br /&gt;
::&amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x))&amp;lt;/math&amp;gt;&lt;br /&gt;
: genau dann wenn es eine Konstante &amp;lt;math&amp;gt;c&amp;gt;0&amp;lt;/math&amp;gt; und ein Argument &amp;lt;math&amp;gt;x_0&amp;lt;/math&amp;gt; gibt, so dass &lt;br /&gt;
::&amp;lt;math&amp;gt;\forall x \ge x_0:\quad f(x) \le c\,g(x)&amp;lt;/math&amp;gt;.&lt;br /&gt;
:Die Menge &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; aller durch g(x) abschätzbaren Funktionen ist also formal definiert durch&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(g(x)) = \{ f(x)\ |\ \exists c&amp;gt;0: \forall x \ge x_0: 0 \le f(x) \le c\,g(x)\}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Idee hinter dieser Definition ist, dass g(x) eine wesentlich einfachere Funktion ist als f(x), die sich aber nach geeigneter Skalierung (Multiplikation mit c) und für große Argumente x im wesentlichen genauso wie f(x) verhält. Man kann deshalb in der Algorithmenanalyse f(x) durch g(x) ersetzen. &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x))&amp;lt;/math&amp;gt; spielt für Funktionen eine ähnliche Rolle wie der Operator &amp;amp;le; für Zahlen: Falls a &amp;amp;le; b gilt, kann bei einer Abschätzung von oben ebenfalls a durch b ersetzt werden.&lt;br /&gt;
&lt;br /&gt;
==== Ein einfaches Beispiel ====&lt;br /&gt;
&lt;br /&gt;
[[Image:Sqsqrt.png]]&lt;br /&gt;
&lt;br /&gt;
Rot = &amp;lt;math&amp;gt;x^2 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
Blau = &amp;lt;math&amp;gt;\sqrt{x} \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\sqrt{x} \in \mathcal{O}(x^2)\!&amp;lt;/math&amp;gt; weil &amp;lt;math&amp;gt;\sqrt{x} \le c\,x^2\!&amp;lt;/math&amp;gt; für alle &amp;lt;math&amp;gt;x \ge x_0 = 1 \!&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c = 1\!&amp;lt;/math&amp;gt;, oder auch für &amp;lt;math&amp;gt;x \ge x_0 = 4 \!&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c = 1/16&amp;lt;/math&amp;gt; (die Wahl von c und x&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; in der Definition von O(.) ist beliebig, solange die Bedingungen erfüllt sind).&lt;br /&gt;
&lt;br /&gt;
==== Komplexität bei kleinen Eingaben ==== &lt;br /&gt;
&lt;br /&gt;
Algorithmus 1: &amp;lt;math&amp;gt;\mathcal{O}(N^2) \!&amp;lt;/math&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Algorithmus 2: &amp;lt;math&amp;gt;\mathcal{O}(N\log{N}) \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus 2 ist schneller (von geringerer Komplexität) für große Eingaben, aber bei kleinen Eingaben (insbesondere, wenn der Algorithmus in einer Schleife immer wieder mit kleinen Eingaben aufgerufen wird) könnte Algorithmus 1 schneller sein, falls der in der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation verborgene konstante Faktor ''c'' bei Algorithmus 2 einen wesentlich größeren Wert hat als bei Algorithmus 1.&lt;br /&gt;
&lt;br /&gt;
==== Eigenschaften der O-Notation (Rechenregeln) ==== &lt;br /&gt;
&lt;br /&gt;
# Transitiv:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x)) \land g(x) \in \mathcal{O}(h(x)) \to f(x) \in \mathcal{O}(h(x)) \!&amp;lt;/math&amp;gt;           &lt;br /&gt;
# Additiv:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(h(x)) \land g(x) \in \mathcal{O}(h(x)) \to f(x) + g(x) \in \mathcal{O}(h(x)) \!&amp;lt;/math&amp;gt;           &lt;br /&gt;
# Für Monome gilt:&lt;br /&gt;
#: &amp;lt;math&amp;gt;x^k \in \mathcal{O}(x^k)&amp;lt;/math&amp;gt; und&lt;br /&gt;
#: &amp;lt;math&amp;gt;x^k \in \mathcal{O}(x^{k+j}), \forall j \ge 0 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Multiplikation mit einer Konstanten:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x)) \to c\,f(x) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: andere Schreibweise:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) = c\,g(x) \to f(x) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Folgerung aus 3. und 4. für Polynome: &lt;br /&gt;
#: &amp;lt;math&amp;gt;a_0+a_1\,x + ... + a_n\,x^n \in \mathcal{O}(x^n)\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: Beispiel: &amp;lt;math&amp;gt;a\,x^2+b\,x+c \in \mathcal{O}(x^2)\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Logarithmus:&lt;br /&gt;
#: &amp;lt;math&amp;gt;a, b &amp;gt; 1\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{b}{x})\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: Die Basis des Logarithmus spielt also keine Rolle.&lt;br /&gt;
#: Beweis hierfür:&lt;br /&gt;
#:: &amp;lt;math&amp;gt;\log_{a}{x} = \frac{\log_{b}{x}}{\log_{b}{a}}\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#:: Mit &amp;lt;math&amp;gt;c = 1 / \log_{b}{a}\,&amp;lt;/math&amp;gt; gilt: &amp;lt;math&amp;gt;\log_{a}{x} = c\,\log_{b}{x}\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#:: Wird hier die (zweite) Regel für Multiplikation mit einer Konstanten angewendet, fällt der konstante Faktor weg, also &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{b}{x})\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Insbesondere gilt auch &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{2}{x})\!&amp;lt;/math&amp;gt;, es kann also immer der 2er Logarithmus verwendet werden.&lt;br /&gt;
&lt;br /&gt;
==== O-Kalkül ==== &lt;br /&gt;
&lt;br /&gt;
Das O-Kalkül definiert wichtige Vereinfachungsregeln for Ausdrücke in O-Notation (Beweise: siehe Übungsaufgabe):&lt;br /&gt;
&lt;br /&gt;
# &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(f(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# &amp;lt;math&amp;gt;\mathcal{O}(\mathcal{O}(f(x))) \in \mathcal{O}(f(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# &amp;lt;math&amp;gt;c\,\mathcal{O}(f(x)) \in \mathcal{O}(f(x))\,&amp;lt;/math&amp;gt; für jede Konstante ''c''&lt;br /&gt;
# &amp;lt;math&amp;gt;\mathcal{O}(f(x))+c \in \mathcal{O}(f(x))\,&amp;lt;/math&amp;gt; für jede Konstante ''c''&lt;br /&gt;
# Sequenzregel:&lt;br /&gt;
#: Wenn zwei nacheinander ausgeführte Programmteile die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt; bzw. &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; haben, gilt für beide gemeinsam:&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(f(x))&amp;lt;/math&amp;gt; falls &amp;lt;math&amp;gt;g(x) &amp;lt; \mathcal{O}(f(x))&amp;lt;/math&amp;gt; bzw.&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt; falls &amp;lt;math&amp;gt;f(x) &amp;lt; \mathcal{O}(g(x))&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Informell schreibt man auch: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(max(f(x), g(x)))\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
# Schachtelungsregel bzw. Aufrufregel:&lt;br /&gt;
#: Wenn in einer geschachtelten Schleife die äußere Schleife die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt; hat, und die innere &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt;, gilt für beide gemeinsam:&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) * \mathcal{O}(g(x)) \in \mathcal{O}(f(x) * g(x))\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Gleiches gilt wenn eine Funktion &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt;-mal aufgerufen wird, und die Komplexität der Funktion selbst &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; ist.&lt;br /&gt;
&lt;br /&gt;
;Beispiel für 5.: Beide Schleifen haben die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Dies gilt auch für ihre Hintereinanderausführung:&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          a[i] = i&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          print a[i]&lt;br /&gt;
;Beispiele für 6.: Beide Schleifen haben die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Ihre Verschachtelung hat daher die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N^2)&amp;lt;/math&amp;gt;. &lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          for j in range(N):&lt;br /&gt;
              a[i*N + j] = i+j&lt;br /&gt;
: Dies gilt ebenso, wenn statt der inneren Schleife eine Funktion mit Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt; ausgeführt wird:&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          a[i] = foo(i, N)  # &amp;lt;math&amp;gt;\mathrm{foo}(i, N) \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== O-Kalkül auf das Beispiel des Selectionsort angewandt ==== &lt;br /&gt;
&lt;br /&gt;
Selectionsort: Wir hatten gezeigt dass &amp;lt;math&amp;gt;f(N) = \frac{N^2}{2} - \frac{N}{2}&amp;lt;/math&amp;gt;. Nach der Regel für Polynome vereinfacht sich dies zu &amp;lt;math&amp;gt;f(N) \in \mathcal{O}\left(\frac{N^2}{2}\right) = \mathcal{O}(N^2)\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Alternativ via Schachtelungsregel:&lt;br /&gt;
: Die äußere Schleife wird (''N''-1)-mal durchlaufen: &amp;lt;math&amp;gt;N-1 \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
: Die innere Schleife wird (''N-i''-1)-mal durchlaufen. Das sind im Mittel ''N''/2 Durchläufe: &amp;lt;math&amp;gt;N/2 \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
: Zusammen: &amp;lt;math&amp;gt;\mathcal{O}(N)*\mathcal{O}(N) \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nach beiden Vorgehensweisen kommen wir zur Schlussfolgerung, dass der Selectionsort die asymptotische Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N^2)\!&amp;lt;/math&amp;gt; besitzt.&lt;br /&gt;
&lt;br /&gt;
==== Zusammenhang zwischen Komplexität und Laufzeit ==== &lt;br /&gt;
&lt;br /&gt;
Wenn eine Operation 1ms dauert, erreichen Algorithmen verschiedener Komplexität folgende Leistungen (wobei angenommen wird, dass der in der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation verborgene konstante Faktor immer etwa gleich 1 ist):&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:left&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
|-&lt;br /&gt;
! Komplexität !! Operationen in 1s !! Operationen in 1min !! Operationen in 1h&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 1000 || 60.000 || 3.600.000&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N\log_2{N})&amp;lt;/math&amp;gt;&lt;br /&gt;
| 140 || 4895 || 204094&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 32 || 245 || 1898&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 10 || 39 || 153&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(2^N)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 10 || 16 || 21&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Exponentielle Komplexität ==== &lt;br /&gt;
Der letzte Fall &amp;lt;math&amp;gt;\mathcal{O}(2^N)&amp;lt;/math&amp;gt; ist von exponentieller Komplexität. Das bedeutet, dass eine Verdopplung des Aufwands nur bewirkt, dass die maximale Problemgröße um eine Konstante wächst. Algorithmen mit exponentieller (oder noch höherer) Komplexität werden deshalb als '''ineffizient''' bezeichnet. Algorithmen mit höchstens polynomieller Komplexität gelten hingegen als effizient.&lt;br /&gt;
&lt;br /&gt;
In der Praxis sind allerdings auch polynomielle Algorithmen mit hohem Exponenten meist zu langsam. Als Faustregel kann man eine praktische Grenze von &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt; ansehen. Bei einer Komplexität von &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt; bewirkt ein verdoppelter Aufwand immer noch eine Steigerung der maximalen Problemgröße um den Faktor &amp;lt;math&amp;gt;\sqrt[3]{2}&amp;lt;/math&amp;gt; (also eine ''multiplikative'' Vergrößerung um ca. 25%, statt nur einer additiven Vergrößerung wie bei exponentieller Komplexität).&lt;br /&gt;
&lt;br /&gt;
===&amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;- Notation===&lt;br /&gt;
&lt;br /&gt;
Genauso wie &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; eine Art &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;-Operator für Funktionen ist, definiert &amp;lt;math&amp;gt;f \in \Omega(g) &amp;lt;/math&amp;gt; eine Abschätzung von unten, analog zum &amp;lt;math&amp;gt;\ge&amp;lt;/math&amp;gt;-Operator für Zahlen. Formal kann man &amp;lt;math&amp;gt;f(N) \in \Omega(g(N)) &amp;lt;/math&amp;gt; genau dann schreiben, falls es eine Konstante &amp;lt;math&amp;gt; c &amp;gt; 0 &amp;lt;/math&amp;gt; gibt, so dass &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; f(N) \ge c \cdot g(N) &amp;lt;/math&amp;gt; für &amp;lt;math&amp;gt; N  \ge N_0 &amp;lt;/math&amp;gt; &lt;br /&gt;
&lt;br /&gt;
gilt.&lt;br /&gt;
Man verwendet diese Notation also um abzuschätzen, wie groß der Aufwand (die Komplexität) für einen bestimmten Algorithmus ''mindestens'' ist und nicht ''höchstens'', was man mit der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt; - Notation ausdrücken würde.&lt;br /&gt;
&lt;br /&gt;
Ein praktisches Beispiel für eine Anwendung der &amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;- Notation wäre die Fragestellung, ob es ''prinzipiell'' einen besseren Algorithmus für ein bestimmtes Problem gibt. Wie später im Abschnitt [[Suchen#Sortieren_als_Suchproblem|Sortieren als Suchproblem]] gezeigt wird, ist das Sortieren eines Arrays durch paarweise Vergleiche von Elementen immer mindestens von der Komplexität &amp;lt;math&amp;gt; \Omega(N\cdot \ln N) &amp;lt;/math&amp;gt;, was konkret bedeutet, dass kein Sortieralgorithmus, der nach diesem Prinzip arbeitet, jemals eine geringere Komplexität als beispielsweise Merge-Sort haben wird. Natürlich kann man den entsprechenden Sortieralgorithmus, also Merge-Sort zum Beispiel, unter Umständen noch optimieren, aber die Komplexität wird erhalten bleiben. Mit diesem Wissen kann man sich viel (vergebliche) Arbeit sparen.&lt;br /&gt;
&lt;br /&gt;
===&amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;- Notation===&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;math&amp;gt;f(N) \in \Theta(g(N))&amp;lt;/math&amp;gt; ist eine scharfe Abschätzung der asymptotischen Komplexität einer Funktion f. &lt;br /&gt;
&lt;br /&gt;
Damit dies gilt, muss &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; und ''gleichzeitig'' &amp;lt;math&amp;gt;f(N) \in \Omega(g(N))&amp;lt;/math&amp;gt; erfüllt sein.&lt;br /&gt;
&lt;br /&gt;
Dies ist natürlich auch die beste Abschätzung der asymptotischen Komplexität einer Funktion f. Formal bedeutet &amp;lt;math&amp;gt;f(N) \in \Theta(g(N))&amp;lt;/math&amp;gt; dass es zwei Konstanten &amp;lt;math&amp;gt; c_1 &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; c_2 &amp;lt;/math&amp;gt;, beide größer als Null, gibt, so dass für alle &amp;lt;math&amp;gt; N \geq N_0 &amp;lt;/math&amp;gt; gilt: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; c_1 \cdot g(N) \leq f(N) \leq c_2 \cdot g(N) &amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In der Praxis wird manchmal statt der &amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;-Notation auch dann die &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation benutzt, wenn eine scharfe Schranke ausgedrückt werden soll. Dies ist zwar formal nicht korrekt, aber man kann die intendierte Bedeutung meist aus dem Kontext erschließen.&lt;br /&gt;
&lt;br /&gt;
== Komplexitätsvergleich zweier Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
In diesem Abschnitt wollen wir der Frage nachgehen, wie ein formaler Beweis für die Behauptung &amp;lt;math&amp;gt; f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; geschehen kann. Hierbei werden zwei Beweismethoden vorgestellt werden, und zwar der '''Beweis über die Definition der Komplexität''' sowie der '''Beweis durch Dividieren'''.&lt;br /&gt;
&lt;br /&gt;
===Beweis über die Definition der asymptotischen Komplexität===&lt;br /&gt;
&lt;br /&gt;
Die Definition der asymptotischen Komplexität &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; war: &lt;br /&gt;
&lt;br /&gt;
Es gibt eine Konstante &amp;lt;math&amp;gt; c &amp;gt; 0 &amp;lt;/math&amp;gt;, so dass &amp;lt;math&amp;gt; f(N) \le c \cdot g(N) &amp;lt;/math&amp;gt; für &amp;lt;math&amp;gt; N  \ge N_0 &amp;lt;/math&amp;gt; erfüllt ist. &lt;br /&gt;
&lt;br /&gt;
Um also die die asymptotische Komplexität &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; zu beweisen, muss man die oben erwähnten Konstanten c und &amp;lt;math&amp;gt; N_0 &amp;lt;/math&amp;gt; finden, so dass &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; f(N) \leq c \cdot g(N) &amp;lt;/math&amp;gt; für alle &amp;lt;math&amp;gt; N \ge N_0 &amp;lt;/math&amp;gt; erfüllt ist. &lt;br /&gt;
&lt;br /&gt;
Dies geschieht zweckmäßigerweise mit dem Beweisprinzip der ''vollständigen Induktion''. Hierbei ist zu zeigen, dass&lt;br /&gt;
# &amp;lt;math&amp;gt; f(N_0) \leq g(N_0) &amp;lt;/math&amp;gt; für die eine zu bestimmende Konstante &amp;lt;math&amp;gt; N_0 &amp;lt;/math&amp;gt; gilt (''Induktionsanfang'') und &lt;br /&gt;
# falls &amp;lt;math&amp;gt; f(N) \leq g(N) &amp;lt;/math&amp;gt;, dann auch &amp;lt;math&amp;gt; f(N+1) \leq g(N+1) &amp;lt;/math&amp;gt; (''Induktionsschritt'') gilt.&lt;br /&gt;
&lt;br /&gt;
===Beweis durch Dividieren===&lt;br /&gt;
&lt;br /&gt;
Hierbei wählt man eine Konstante c und zeigt, dass &amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f(N)}{c \cdot g(N)} \leq 1 &amp;lt;/math&amp;gt; gilt (für die O-Notation, bei &amp;amp;Omega;-Notation gilt entsprechend &amp;lt;math&amp;gt;\geq 1 &amp;lt;/math&amp;gt;). Man kann dies auch als alternative Definition der Komplexität verwenden.&lt;br /&gt;
&lt;br /&gt;
Als Beispiel betrachten wir die beiden Funktionen &amp;lt;math&amp;gt; f(N) = N \,\lg N &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; g(N) = N^2 &amp;lt;/math&amp;gt; und wollen zeigen, dass &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; gilt. &lt;br /&gt;
&lt;br /&gt;
Als Konstante c wählen wir &amp;lt;math&amp;gt; c = 1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f(N)}{g(N)} = \lim_{N \rightarrow \infty} \frac{\lg N}{N} = \frac{\infty}{\infty} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Unbestimmte Ausdrücke der Form &lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{x \rightarrow x_0} \frac{f(x)}{g(x)} &amp;lt;/math&amp;gt;,&lt;br /&gt;
in denen sowohl &amp;lt;math&amp;gt; f(x) &amp;lt;/math&amp;gt; als auch &amp;lt;math&amp;gt; g(x) &amp;lt;/math&amp;gt; mit &amp;lt;math&amp;gt; x \rightarrow x_0 &amp;lt;/math&amp;gt; gegen Null oder gegen Unendlich streben, kann man manchmal mit den Regeln von [http://de.wikipedia.org/wiki/L%27Hospital%27sche_Regel ''l'Hospital''] berechnen. Danach darf man die Funktionen f und g zur Berechnung des unbestimmten Ausdrucks durch ihre k-ten Ableitungen ersetzen:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{x \rightarrow x_0} \frac{f(x)}{g(x)} = \lim_{x \rightarrow x_0} \frac{f^{(k)}(x)}{g^{(k)}(x)} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In unserem Fall verwenden wir die erste Ableitung und erhalten:&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f'(x)}{g'(x)} = \lim_{N \rightarrow \infty} \frac{1/N}{1} \rightarrow 0 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Damit wurde &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt;, also &amp;lt;math&amp;gt;N \lg N \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt; gezeigt.&lt;br /&gt;
&lt;br /&gt;
Man beachte hierbei, dass &amp;lt;math&amp;gt;N \lg N \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt; keine enge Grenze für die Komplexität von &amp;lt;math&amp;gt;N \,\lg N&amp;lt;/math&amp;gt; darstellt, da der Grenzwert &amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f'(x)}{g'(x)}\, &amp;lt;/math&amp;gt; gegen 0 und nicht gegen eine von Null verschiedene Konstante strebt. In diesem Fall haben wir die Komplexität von &amp;lt;math&amp;gt;N \cdot \lg N &amp;lt;/math&amp;gt; also nur nach oben abschätzen können.&lt;br /&gt;
&lt;br /&gt;
===Beispiel für den Komplexitätsvergleich: Gleitender Mittelwert (Running Average)===&lt;br /&gt;
&lt;br /&gt;
Wir berechnen für ein gegebenes Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; einen gleitenden Mittelwert über &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; Elemente:&amp;lt;br/&amp;gt;&lt;br /&gt;
::&amp;lt;math&amp;gt;r_i = \frac{1}{k} \sum_{j=i-k+1}^i a_j&amp;lt;/math&amp;gt; &amp;lt;br/&amp;gt;&lt;br /&gt;
Das heisst, für jedes &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; mitteln wir die letzten &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; Elemente von &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; und schreiben das Ergebnis in &amp;lt;tt&amp;gt;r[i]&amp;lt;/tt&amp;gt;. Diese Operation ist z.B. bei Börsenkursen wichtig: Neben dem aktuellen Kurs für jeden Tag wird dort meist auch der gleitende Mittelwert der letzten 30 Tage sowie der letzten 200 Tage angegeben. In diesen Mittelwerten erkennt man besser die langfristige Tendenz, weil die täglichen Schwankungen herausgemittelt werden. Wir nehmen außerdem an, dass&lt;br /&gt;
* Array-Zugriff hat eine Komplexit&amp;amp;auml;t von O(1)&lt;br /&gt;
* &amp;lt;math&amp;gt;k \ll N&amp;lt;/math&amp;gt;, d.h. &amp;lt;math&amp;gt;N-k\approx N&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die beiden folgenden Algorithmen berechnen die Mittelwerte auf unterschiedliche Art. Der linke folgt der obigen Definition durch eine Summe, während der rechte inkrementell arbeitet: Man kann den Bereich der &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; letzten Werte als Fenster betrachten, das über das Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; geschoben wird. Schiebt man das Fenster ein Element weiter, fällt links ein Element heraus, und rechts kommt eins hinzu. Man muss also nicht jedes Mal die Summe neu berechnen, sondern kann den vorigen Wert aktualisieren. Wir werden sehen, dass dies Folgen für die Komplexität des Algorithmus hat.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;2&amp;quot; &lt;br /&gt;
|-&lt;br /&gt;
! Programmzeile&lt;br /&gt;
! Version 1: O(N * k)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
! Version 2: O(N)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
1.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
2.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
3.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
4.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k-1, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for i in range(k):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
5.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;for i in range(j-k+1, j+1):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[k-1] += a[i]&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
6.&lt;br /&gt;
|&lt;br /&gt;
:::: &amp;lt;tt&amp;gt;r[j] += a[i]&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
7.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] = (a[j] - a[j-k] + r[j-1])&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
8.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
9.&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
10.&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Wir zeigen unten dass Version 2 eine geringere Komplexit&amp;amp;auml;t besitzt, obwohl sie mehr Zeilen ben&amp;amp;ouml;tigt.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Wir haben in der Tabelle die Komplexität jeder Zeile für sich angegeben. Einfache Anweisungen (Berechnungen, Lese- und Schreibzugriffe auf das Array, Zuweiseungen) haben konstante Komplexität, die Komplexität des Schleifenkopfes allein (also der &amp;lt;tt&amp;gt;for&amp;lt;/tt&amp;gt;-Anweisung ohne den eingerückten Schleifenkörper) entspricht der Anzahl der Durchläufe. Wir müssen jetzt noch die Verschachtelung der Schleifen und die Nacheinanderausführung von Anweisungen berücksichtigen. &lt;br /&gt;
&lt;br /&gt;
====Berechnung der Komplexität von Version 1====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;small&amp;gt;(Wiederholung der Rechenregeln: siehe Abschnitt [[Effizienz#O-Notation|O-Notation]])&amp;lt;/small&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wir betrachten zunächst die innere Schleife (Zeilen 5 und 6 von Version 1):&lt;br /&gt;
&lt;br /&gt;
Der Schleifenkopf (Zeile 5) hat die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt;, weil die Schleife k-mal durchlaufen wird. Der Schleifenkörper (Zeile 6) hat die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;. Nach der Verschachtelungsregel müssen wir die beiden Komplexitäten multiplizieren, und es ergibt sich:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(k)\cdot\mathcal{O}(1) = \mathcal{O}(k\cdot 1)=\mathcal{O}(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wir betrachten nun die äußere Schleife. Der Schleifenkopf (Zeile 4) wird (N-k)-mal durchlaufen und hat somit eine Komplexität von &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Der Schleifenkörper (Zeilen 5 bis 7) besteht aus der inneren Schleife (Zeilen 5 und 6) mit der gerade berechneten Komplexität &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt; sowie einer einfachen Anweisung (Zeile 7) mit Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;. Nach der Sequenzregel wird die Komplexität des Schleifenkörpers durch Addition berechnet:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(k)+\mathcal{O}(1) = \mathcal{O}(\max(k,1)) = \mathcal{O}(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexität der gesamten äußeren Schleife erhalten wir nach der Verschachtelungsregel wieder durch multiplizieren:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)\cdot\mathcal{O}(k) = \mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die übrigen Schritte des Algorithmus werden einfach nacheinander ausgeführt, so dass sie ebenfalls nach der Sequenzregel behandelt werden. Wir erhalten&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)+\mathcal{O}(1)+\mathcal{O}(N\cdot k)+\mathcal{O}(1) = \mathcal{O}(\max(N,1,N\cdot k,1)) = \mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Der gesamte Algorithmus hat also die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Berechnung der Komplexität von Version 2====&lt;br /&gt;
&lt;br /&gt;
Hier gibt es nur einfache Schleifen ohne Verschachtelung. Da der Schleifenkörper jeder Schleife nur einfache Anweisungen der Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt; enthält, ergibt sich die Komplexität der Schleifen nach der Verschachtelungsregel als&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(X)\cdot\mathcal{O}(1) = \mathcal{O}(X\cdot 1)=\mathcal{O}(X)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
wobei &amp;lt;math&amp;gt;\mathcal{O}(X)&amp;lt;/math&amp;gt; die Komplexität des jeweiligen Schleifenkopfes ist. Wir erhalten also für Zeilen 4 und 5: &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt;, Zeilen 6 und 7: &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;, Zeilen 8 und 9: &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Die Hintereinanderausführung wird nach der Sequenzregel behandelt:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)+\mathcal{O}(1)+\mathcal{O}(k)+\mathcal{O}(N)+\mathcal{O}(N)+\mathcal{O}(1) = \mathcal{O}(\max(N,1,k,N,N,1)) = \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Dieser Algorithmus hat also nur die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Fazit====&lt;br /&gt;
&lt;br /&gt;
Obwohl Version 2 mehr Schritte benötigt hat sie eine geringere Komplexität, da die for-Schleifen nicht wie bei Version 1 verschachtelt/untergeordnet sind. Bei verschachtelten for-Schleifen muss die Multiplikationsregel angewendet werden &amp;amp;rarr; höhere Komplexität.&lt;br /&gt;
&lt;br /&gt;
Die gerade berechnete Komplexität gilt aber &amp;lt;u&amp;gt;nur&amp;lt;/u&amp;gt; unter der Annahme, dass Array-Zugriffe konstante Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt; besitzen. Wenn dies nicht der Fall ist, kann sich die Komplexität des Algorithmus drastisch verschlechtern.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|Allgemein gilt:&amp;lt;br/&amp;gt;&lt;br /&gt;
Algorithmen-Analysen beruhen auf der Annahme, dass Zugriffe auf die Daten optimal schnell sind, dass heißt, dass die für den jeweiligen Algorithmus am besten geeignete Datenstruktur verwendetet wird.&amp;lt;br /&amp;gt; &amp;amp;rarr; Ansonsten: Komplexitätsverschlechterung!&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Beispiel für eine Verschlechterung der Komplexität durch Verwendung einer nicht optimalen Datenstruktur====&lt;br /&gt;
&lt;br /&gt;
Wir verwende im Mittelwert-Algorithmus eine verkettete Liste anstelle des Eingabe-Arrays a&amp;lt;/u&amp;gt;. Wir benötigen dazu eine Funktion, die das j-te Element der Liste zurückgibt. Wie üblich ist die Liste mit Hilfe einer Knotenklasse implementiert:&lt;br /&gt;
      class Node:&lt;br /&gt;
          def __init__(self, data):&lt;br /&gt;
              self.data = data&lt;br /&gt;
              self.next = None&lt;br /&gt;
&lt;br /&gt;
Die Listenklasse selbst hat ein Feld &amp;lt;tt&amp;gt;head&amp;lt;/tt&amp;gt;, das eine Referenz auf den ersten Knoten speichert, und jeder Knoten speichert im Feld &amp;lt;tt&amp;gt;next&amp;lt;/tt&amp;gt; eine Referenz auf seinen Nachfolger. Um zum j-ten Element zu gelangen, muss man die Liste sequenziell durchlaufen&lt;br /&gt;
      def get_jth(list, j):&lt;br /&gt;
           r = list.head&lt;br /&gt;
           while j &amp;gt; 0:&lt;br /&gt;
               r = r.head&lt;br /&gt;
               j -= 1&lt;br /&gt;
           return r.data&lt;br /&gt;
Die Komplexität dieser Funktion ist offensichtlich &amp;lt;math&amp;gt;\mathcal{O}(j)&amp;lt;/math&amp;gt; (Komplexitätsberechnung wie oben). Wir setzen jetzt bei Version 1 des Mittelwert-Algorithmus diese Funktion in Zeile 6 anstelle des Indexzugriffs &amp;lt;tt&amp;gt;a[i]&amp;lt;/tt&amp;gt; ein (nur in dieser Zeile wird auf die Elemente des Arrays zugegriffen). Wir erhalten folgende Implementation (die Änderungen sind rot markiert):&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;2&amp;quot; &lt;br /&gt;
|-&lt;br /&gt;
! Programmzeile&lt;br /&gt;
! Version 1 mit Liste: O(N * k)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
1.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
2.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
3.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
4.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k-1, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
5.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;for i in range(j-k+1, j+1):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
6.&lt;br /&gt;
|&lt;br /&gt;
:::: &amp;lt;tt&amp;gt;r[j] += &amp;lt;font color=red&amp;gt;get_jth(a, i)&amp;lt;/font&amp;gt;&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;&amp;lt;font color=red&amp;gt;O(i)&amp;lt;/font&amp;gt;&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
7.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
8.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Der Aufruf der Funktion &amp;lt;tt&amp;gt;get_jth&amp;lt;/tt&amp;gt; ist jetzt gleichbedeutend mit einer dreifach verschachtelten Schleife (weil &amp;lt;tt&amp;gt;get_jth&amp;lt;/tt&amp;gt; ja eine zusatzliche Schleife enthält). Die Anzahl der Operationen in Zeile 4 bis 6 ist jetzt&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;f(N,k)=\sum_{j=k-1}^{N-1}\,\sum_{i=j-k+1}^j\,\mathcal{O}(i)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
wobei das &amp;lt;math&amp;gt;\mathcal{O}(i)&amp;lt;/math&amp;gt; die neue Schleife durch Verwendung der Liste repräsentiert. Mit Mathematica-Hilfe [http://www.wolfram.com/] lässt sich diese Summe exakt ausrechnen&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;f(N,k)=\frac{1}{2}(k N^2-k^2 N+k^2-k)\in \mathcal{O}(k N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexitätsberechnung erfolgte dabei nach der Regel für Polynome unter Beachtung von &amp;lt;math&amp;gt;k \ll N&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Fazit:====&lt;br /&gt;
&lt;br /&gt;
Die Komplexität von Version 1 mit einer verketteten Liste wäre O(N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; * k)&lt;br /&gt;
'''&amp;amp;rarr; Die richtige Datenstruktur ist wichtig, da es sonst zu einer Komplexitätsverschlechterung kommen kann!'''&lt;br /&gt;
&lt;br /&gt;
Auf Version 2 unseres Running Average-Beispiels hätte eine verkettete Liste allerdings keine Auswirkungen, da die inkrementelle Berechnung der Summen in Zeile 7 weiterhin möglich ist (bei geschickter Implementation!) und somit Version 2 immer noch eine Komplexität von O(N) hätte.&lt;br /&gt;
&lt;br /&gt;
==Amortisierte Komplexität==&lt;br /&gt;
&lt;br /&gt;
Bis jetzt wurde die Komplexität nur im schlechtesten Fall (Worst Case) betrachtet. Bei einigen Operationen schwankt die Komplexität jedoch sehr stark, wenn man sie mehrmals hintereinander ausführt, und der schlechteste Fall kommt nur selten vor. Dann ist es sinnvoll, die &amp;lt;i&amp;gt;amortisierte Komplexität&amp;lt;/i&amp;gt; zu betrachten, die sich mit der &amp;lt;i&amp;gt;durchschnittlichen&amp;lt;/i&amp;gt; Komplexität über viele Aufrufe der selben Operation beschäftigt.&lt;br /&gt;
&lt;br /&gt;
Zum weiter Lesen: [[http://de.wikipedia.org/wiki/Amortisierte_Laufzeitanalyse Wikipedia: Amortisierte Laufzeitanalyse]]&lt;br /&gt;
&lt;br /&gt;
===Beispiel: Inkrementieren von Binärzahlen===&lt;br /&gt;
&lt;br /&gt;
Frage: Angenommen, das Umdrehen eines Bits einer Binärzahl verursacht Kosten von 1 Einheit. Wir erzeugen die Folge der natürlichen Zahlen in Binärdarstellung durch sukzessives Inkrementieren, von Null beginnend. Bei jeder Inkrementierung werden einige Bits verändert, aber diese Zahl (und damit die Kosten der Inkrementierungen) ''schwanken'' sehr stark. Wir fragen jetzt, was eine Inkrementierung im Durchschnitt kostet?&lt;br /&gt;
&lt;br /&gt;
Um diese Durchschnittskosten zu berechnen, bezahlen wir bei jeder Inkrementierung 2 Einheiten. Wenn davon nach Abzug der Kosten der jeweiligen Operation noch etwas übrig bleibt, wird der Rest dem  Guthaben zugeschrieben. Umgekehrt wird ein eventueller Fehlbetrag (wenn eine Inkrementierung mehr als 2 Bits umdreht) aus dem Guthaben gedeckt. Dadurch werden die ansonsten großen Schwankungen der Kosten ausgeglichen:&lt;br /&gt;
:: Kosten &amp;lt; Einzahlung &amp;amp;rarr; es wird gespart&lt;br /&gt;
:: Kosten = Einzahlung &amp;amp;rarr; Guthaben bleibt unverändert&lt;br /&gt;
:: Kosten &amp;gt; Einzahlung &amp;amp;rarr; Guthaben wird für die Kosten verbraucht&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
!Schritte&lt;br /&gt;
!Zahlen&lt;br /&gt;
!Kosten &amp;lt;br/&amp;gt;&lt;br /&gt;
(Anzahl der geänderten Bits)&lt;br /&gt;
! Einzahlung&lt;br /&gt;
!Guthaben =&amp;lt;br/&amp;gt;&lt;br /&gt;
altes Guthaben + Einzahlung - Kosten&lt;br /&gt;
|-&lt;br /&gt;
|1.&lt;br /&gt;
|0000&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|2.&lt;br /&gt;
|000&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|2&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|3.&lt;br /&gt;
|0001&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|4.&lt;br /&gt;
|00&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|3&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|5.&lt;br /&gt;
|0010&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|6.&lt;br /&gt;
|001&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|2&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|7.&lt;br /&gt;
|0011&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''3'''&lt;br /&gt;
|-&lt;br /&gt;
|8.&lt;br /&gt;
|0&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1000&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|4&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Die Kosten ergeben sich aus der Anzahl der Ziffern die von 1 nach 0, bzw. von 0 nach 1 verändert werden&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Rechnung:&amp;lt;/u&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Schritt: Kosten: 1 &amp;lt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Schritt: Kosten: 2 = Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird nicht gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; Guthaben bleibt so wie es ist &amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Schritt: Kosten: 1 &amp;lt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. Schritt: Kosten: 3 &amp;gt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird eine 1 vom Guthaben genommen um die Kosten zu zahlen&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
usw.&lt;br /&gt;
&lt;br /&gt;
Man erkennt, dass vor teuren Operation (Wechsel von 3 auf 4 bzw. von 7 auf 8) genügend Guthaben angespart wurde, um die Kosten zu decken. Das Guthaben geht bei diesen Operationen immer wieder auf 1 zurück, aber es wird nie vollständig verbraucht (Dies kann natürlich auch mathematisch exakt bewiesen werden, wie wir es unten am Beispiel des dynamische Arrays zeigen). Wir schließen daraus, dass die durchschnittlichen oder '''amortisierten Kosten''' einer Inkrementierungsoperation gleich 2 sind.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Zum weiter Lesen: [[http://de.wikipedia.org/wiki/Account-Methode Wikipedia Account-Methode]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
====Fazit====&lt;br /&gt;
Die amortisierte Komplexität beschäftigt sich mit dem Durchschnitt aller Operation im ungünstigsten Fall. Operationen mit hohen Kosten, die aber nur selten ausgeführt werden, fallen bei der amortisierten Komplexität nicht so ins Gewicht. Bei Algorithmen, die gelegentlich eine &amp;quot;teure&amp;quot; Operation benutzen, ansonsten jedoch &amp;quot;billige&amp;quot; Operationen aufrufen, kann die amortisierte Komplexität niedriger sein als die Komplexität im schlechtesten (Einzel-)Fall.&lt;br /&gt;
&lt;br /&gt;
In unserem Beispiel fallen die teuren Einzelschritte (z.B. 4. und 8. Schritt) bei den amortisierten Kosten nicht so ins Gewicht, da wir die Kosten aus unserem Guthaben mitbezahlen können. Das Guthaben ist immer groß genug, weil jeder zweite Aufruf eine billige Operation ist, die nur ein Bit umdreht und somit das Ansparen ermöglichen. Diese Betrachtung zeigt, dass die amortisierte (d.h. durchschnittliche) Komplexität des Algoithmus niedriger (nämlich konstant) ist als die Komplexität im schlechtesten Fall.&lt;br /&gt;
&lt;br /&gt;
===Anwendung: Dynamisches Array===&lt;br /&gt;
&lt;br /&gt;
Ein dynamisches Array hat die Eigenschaft, dass man effizient am Ende des Arrays neue Elemente anfügen kann, indem man die Länge des Arrays entsprechend vergrößert (siehe Übung 1). Die Analyse der amortisierten Komplexität der Anfüge-Operation zeigt uns, wie man das Vergrößern des Arrays richtig implementiert, damit die Operation wirklich effizient abläuft.&lt;br /&gt;
&lt;br /&gt;
==== Ineffiziente naive Lösung ====&lt;br /&gt;
&lt;br /&gt;
Wenn wir an ein Array ein Element anhängen wollen, müssen wir neuen Speicher allokieren, der die gewünschte Länge hat. Die Werte aus dem alten Array müssen dann in den neuen Speicher umkopiert werden. Danach kann das neue Element hinten angefügt werden, weil wir im neuen Array bereits Speicher für dieses Element reserviert haben. Bei der naiven Implementation des dynamischen Arrays wiederholt man dies bei jeder Anfügeoperation. Für die Analyse nehmen wir an, dass das Kopieren eines Elements konstante Zeit O(1) erfordert, ebenso das Einfügen eines neuen Elements auf in eine noch unbenutzte Speicherposition.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Naives Anhängen eines weiteren Elements an ein Array:&amp;lt;/u&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;right&amp;quot;&lt;br /&gt;
!Schritte&lt;br /&gt;
|'''Array'''&lt;br /&gt;
&amp;lt;small&amp;gt;(wie es nach jedem Schritt aussieht)&amp;lt;/small&amp;gt;&lt;br /&gt;
!Komplexität&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;center&amp;gt;altes Array (N=4)&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|1. neuer Speicher für&amp;lt;br&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;(N+1) Elemente&lt;br /&gt;
|&amp;lt;center&amp;gt;[None,None,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;O(N+1) = '''O(N)'''&amp;lt;/center&amp;gt;(wenn der Speicher initialisiert wird&amp;lt;br&amp;gt;(hier auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;), sonst O(1))&lt;br /&gt;
|-&lt;br /&gt;
|2. Kopieren &lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;'''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|3. append von &amp;quot;x&amp;quot;&lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3,'x']&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;'''O(1)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
altesArray = [0,1,2,3]&amp;lt;br /&amp;gt;&lt;br /&gt;
altesArray.append('x')&lt;br /&gt;
&lt;br /&gt;
1. Es wird ein neues Array der Größe N+1 erzeugt&amp;lt;br /&amp;gt;&lt;br /&gt;
2. Die N Datenelemente aus dem alten Array werden in das neue Array kopiert&amp;lt;small&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Das sind N Operationen der Komplexität O(1), also ein Gesamtaufwand von O(N).&amp;lt;/small&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
3. 'x' wird mit Aufwand O(1) an die letzte Stelle des neuen Arrays geschrieben&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Additionsregel:&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
O(N) + O(1) &amp;amp;isin; O(N)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Folgerung:&amp;lt;/b&amp;gt; &lt;br /&gt;
&lt;br /&gt;
Bei der naiven Methode erfordert jede Anfügung einen Aufwand O(N) (wobei N die derzeitige Arraygröße ist). Dies ist nicht effizient.&lt;br /&gt;
&lt;br /&gt;
====Effiziente Lösung durch Verdoppeln der Kapazität====&lt;br /&gt;
&lt;br /&gt;
Offensichtlich kommt man nicht darum heraum, den Inhalt des alten Arrays zu kopieren, wenn der allokierte Speicher voll ist. Der Trick für die effiziente Implementation der Anfügeoperation besteht darin, das Kopieren so selten wie möglich durchzuführen, also nicht wie in der naiven Lösung bei jeder Anfügeoperation. Hier kommt die amortisierte Komplexität ins Spiel: Ab und zu gibt es eine teure Anfügeoperation (wenn nämlich kopiert werden muss), aber wenn man den durchschnittlichen Aufwand über viele Anfügungen betrachtet, ist die Operation effizient. Der teure Fall wird sozusagen &amp;quot;herausgemittelt&amp;quot;. &lt;br /&gt;
&lt;br /&gt;
Um nur selten kopieren zu müssen, werden beim dynamischen Array mehr Speicherelemente reserviert als zur Zeit benötigt werden (in der naiven Lösung wurde dagegen immer nur Speicher für ein einziges neues Element reserviert). Wir unterscheiden deshalb &lt;br /&gt;
&lt;br /&gt;
:&amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; = Anzahl der allokierten Speicherzellen, d.h. der möglichen Elemente, die in das Array passen&amp;lt;br /&amp;gt;&lt;br /&gt;
:&amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt; = Anzahl der Elemente, die im Array zur Zeit gespeichert sind&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Daten selbst werden in einem statischen Array gespeichert:&lt;br /&gt;
:&amp;lt;tt&amp;gt;data&amp;lt;/tt&amp;gt; = statisches Array der Größe &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die folgende intuitive Abschätzung zeigt, dass es sinnvoll ist, die Größe des allokierten Speichers jeweils zu verdoppeln. Wir starten bei einem Array der Größe &amp;lt;tt&amp;gt;size = capacity&amp;lt;/tt&amp;gt; = N. Da der verfügbare Speicher voll ist, müssen wir bei der nächsten Anfügung die N vorhandenen Elemente in ein neues Array der Länge &amp;lt;tt&amp;gt;new_capacity&amp;lt;/tt&amp;gt; kopieren (Aufwand &amp;lt;math&amp;gt;N\cdot O(1)&amp;lt;/math&amp;gt;). Danach können wir K Elemente billig einfügen (Aufwand &amp;lt;math&amp;gt;K\cdot O(1)&amp;lt;/math&amp;gt;), wobei &lt;br /&gt;
:K = &amp;lt;tt&amp;gt;new_capacity - capacity&amp;lt;/tt&amp;gt; &lt;br /&gt;
die Anzahl der nach dem Kopieren noch unbenutzen Speicherzellen ist. Der durchschnittliche Aufwand für diese K Einfügungen ist somit&lt;br /&gt;
:&amp;lt;math&amp;gt;\bar T = \frac{N \cdot O(1) + K \cdot O(1)}{K}=\frac{N+K}{K}\cdot O(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Damit die mittlere Zeit in O(1) sein kann, muss der Quotient &amp;lt;math&amp;gt;(N+K)/K&amp;lt;/math&amp;gt; eine Konstante sein. Wir setzen &amp;lt;math&amp;gt;K = a N&amp;lt;/math&amp;gt; und erhalten:&lt;br /&gt;
:&amp;lt;math&amp;gt;\bar T = \frac{(a+1)N}{a N}\cdot O(1)=\frac{a+1}{a}\cdot O(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Der amortisierte Aufwand über K Einfügungen ist also konstant, wenn &amp;lt;math&amp;gt;a&amp;lt;/math&amp;gt; eine (kleine) von N unabhängige Zahl ist. Typischerweise wählt man &lt;br /&gt;
:&amp;lt;math&amp;gt;a = 1&amp;lt;/math&amp;gt;&lt;br /&gt;
und mit &amp;lt;math&amp;gt;K = 1\cdot N&amp;lt;/math&amp;gt; ergibt sich&lt;br /&gt;
:&amp;lt;tt&amp;gt;new_capacity = capacity&amp;lt;/tt&amp;gt; + N = &amp;lt;tt&amp;gt;2 * capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Vorgehensweise beim Zufügen eines neuen Elements im Fall &amp;lt;tt&amp;gt;size == capacity&amp;lt;/tt&amp;gt; ist also&lt;br /&gt;
* capacity wird verdoppelt&amp;lt;br /&amp;gt;&lt;br /&gt;
: &amp;lt;tt&amp;gt;neue capacity = 2 * alte capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
: (allgemein genügt es auch, wenn capacity um einen bestimmten Prozentsatz vergrößert wird, &lt;br /&gt;
:: &amp;lt;tt&amp;gt;neue capacity = alte capacity * c&amp;lt;/tt&amp;gt; &lt;br /&gt;
: mit c &amp;gt; 1, z.B. c = 1.2, das entspricht oben der Wahl &amp;lt;math&amp;gt;a = 0.2&amp;lt;/math&amp;gt;)&lt;br /&gt;
* ein neues statisches Array der Größe 'neue capacity' wird erzeugt&lt;br /&gt;
* das alte Array wird ins neue kopiert und danach freigegeben&lt;br /&gt;
* das anzufügende Element wird ins neue Array eingefügt&lt;br /&gt;
Umgekehrt geht man beim Entfernen des ''letzten'' Array-Elements vor. Normalerweise überschreibt man einfach das letzte Element mit &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; und dekrementiert &amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt;. Wird dadurch das Array zu klein (üblicherweise &amp;lt;tt&amp;gt;size &amp;amp;lt; capacity / 4&amp;lt;/tt&amp;gt;), wird die Kapazität halbiert, genauer:&lt;br /&gt;
* ein neues Array mit &amp;lt;br/&amp;gt;&lt;br /&gt;
: &amp;lt;tt&amp;gt;neue capacity = alte capacity / 2 &amp;lt;/tt&amp;gt;&lt;br /&gt;
: wird angelegt (bzw. mit&lt;br /&gt;
:: &amp;lt;tt&amp;gt;neue capacity = alte capacity / c &amp;lt;/tt&amp;gt;&lt;br /&gt;
: wenn ein anderer Vergrößerungsfaktor verwendet wird)&lt;br /&gt;
* das alte Array wird ins neue kopiert und danach freigegeben&lt;br /&gt;
&lt;br /&gt;
'''Folge:''' Die Kosten für das Vergrößern/Verkleinern der Kapazität werden amortisiert über viele Einfügungen, die kein Vergrößern erfordern. Die Operation &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; besitzt amortisierte Komplexität O(1). Im folgenden Abschnitt zeigen wir dies mathematisch exakt mit der Potentialmethode.&lt;br /&gt;
&lt;br /&gt;
====Komplexitätsanalyse des dynamischen Arrays mit Potentialmethode====&lt;br /&gt;
&lt;br /&gt;
Durchschnitt der Gesamtkosten für N-maliges append = &amp;lt;math&amp;gt;\frac{1}{N} \sum_{i = 1}^N Kosten(i)&amp;lt;/math&amp;gt;. Zur Analyse der amortisierten Komplexität wird ein Potential&amp;lt;br/&amp;gt;&lt;br /&gt;
::&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2*size&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; - capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
eingeführt, wobei das Array nach dem i-ten Einfüge-Schritt die Größe size&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; und die Kapizität capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; hat. Wir nehmen vereinfachend an, dass es keine Löschoperationen gibt. Dann gilt nach dem i-ten Schritt jeweils&lt;br /&gt;
::&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2*i - capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Fall 1: Array ist nicht voll&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Es wird kein Umkopieren benötigt, da das Array noch nicht voll ist&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; size&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; &amp;lt; capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; == capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Kosten: 1 (für Einfügen des neuen Elements)&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial vor append: &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = 2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial nach append: &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
amortisierte Kosten = Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Phi;&amp;lt;sub&amp;gt;(i)&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;(i-1)&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::                 = 1            + (2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;)        - [2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;]&lt;br /&gt;
:::::                 = 1            + 2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;         - 2i + 2 + capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::                 = 1            + &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; - &amp;lt;del&amp;gt;capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;/del&amp;gt; - &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; + 2 + &amp;lt;del&amp;gt;capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;/del&amp;gt;&lt;br /&gt;
:::::                 = 1 + 2&lt;br /&gt;
:::::                 = 3 = O(1) &amp;amp;rarr; konstant&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Fall 2: Array ist voll&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Vor dem i-ten append muss umkopiert werden&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; size&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; == capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; == i-1&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; Allokieren eines neuen statischen Arrays mit verdoppelter Kapazität notwendig, also capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; == 2*capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Kosten: (i-1) + 1 (für Umkopieren und Einfügen des neuen Elements)&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial vor append =  &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = 2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial nach append =  &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; =  2i - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
amortisierte Kosten = Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Phi;&amp;lt;sub&amp;gt;(i)&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;(i-1)&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = ((i - 1) + 1) + 2i - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; - [2(i-1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;]&lt;br /&gt;
:::::               = i + &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; - &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; + 2 - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = i + 2 - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = i + 2 - (i - 1) &amp;lt;small&amp;gt;(da capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = i-1)&amp;lt;/small&amp;gt;&lt;br /&gt;
:::::               = 3 = O(1) &amp;amp;rarr; konstant            &lt;br /&gt;
&lt;br /&gt;
'''Damit wurde bewiesen, dass die Operation &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; beim dynamischen Array eine amortisierte Komplexität von 3 Einheiten hat, also &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; &amp;amp;isin; O(1)'''. Diese Operation kann deshalb gefahrlos in der inneren Schleife eines Algorithmus benutzt werden.&lt;br /&gt;
&lt;br /&gt;
==== Beispiel für 9 Einfügeoperationen ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot;&lt;br /&gt;
!Array&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;lt;small&amp;gt;(wie es aussehen könnte)&amp;lt;/small&amp;gt;&lt;br /&gt;
!size&lt;br /&gt;
!capacity&lt;br /&gt;
!Kosten für append&amp;lt;br /&amp;gt;(einschließlich Umkopieren)&lt;br /&gt;
!Summe Kosten&lt;br /&gt;
!Durchschnittskosten&lt;br /&gt;
!&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2 * size - capacity&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;lt;small&amp;gt;(i = size)&amp;lt;/small&amp;gt;&lt;br /&gt;
!Potenzialdifferenz&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;amp;Delta; &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
!amortisierte Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
= Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Delta; &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;0&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3/2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6/3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;0&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7/4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;5&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;12&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;12/5&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;13&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;13/6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;14&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;14/7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,h]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;15&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;15/8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,h,j,None,None,None,&amp;lt;br /&amp;gt;&lt;br /&gt;
None,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;9&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;16&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;24&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;24/9&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die durchschnittlichen Kosten betragen stets etwa 2 Einheiten, schwanken allerdings so, dass nicht unmittelbar ersichtlich ist, ob dies für sämtliche Einfügeoperationen gilt. Die amortisierte Komplexität, die mit Hilfe des Potentials berechnet wird, ist hingegen konstant 3, wie auch im obigen Beweis für alle Einfügeoperationen allgemein gezeigt wurde.&lt;br /&gt;
&lt;br /&gt;
[[Suchen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Effizienz&amp;diff=5417</id>
		<title>Effizienz</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Effizienz&amp;diff=5417"/>
				<updated>2012-07-27T15:58:31Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Amortisierte Komplexität */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Bei der Diskussion von Effizienz müssen wir zwischen der Laufzeit eines Algorithmus auf einem bestimmten System und seiner prinzipiellen Leistungsfähigkeit (Algorithmenkomplexität) unterscheiden. Der Benutzer ist natürlich vor allem an der Laufzeit interessiert, denn diese bestimmt letztendlich seine Arbeitsproduktivität. Ein Softwaredesigner hingegen muss eine Implementation wählen, die auf verschiedenen Systemen und in verschiedenen Anwendungen schnell ist. Für ihn sind daher auch Aussagen zur Algorithmenkomplexität sehr wichtig, um den am besten geeigneten Algorithmus auszuwählen.&lt;br /&gt;
&lt;br /&gt;
== Laufzeit ==&lt;br /&gt;
&lt;br /&gt;
Aus Anwendersicht ist ein Algorithmus effizient, wenn er die in der Spezifikation verlangten Laufzeitgrenzen einhält. Ein Algorithmus muss also nicht immer so schnell wie möglich sein, sondern so schnell wie nötig. Dies führt in verschiedenen Anwendungen zu ganz unterschiedliche Laufzeitanforderungen:&lt;br /&gt;
&lt;br /&gt;
* Berechnen des nächsten Steuerkommandos für eine Maschine: ca. 1/1000s&lt;br /&gt;
* Berechnen des nächsten Bildes für eine Videopräsentation (z.B. Dekompression von MPEG-kodierten Bildern): ca. 1/25s&lt;br /&gt;
: Geringere Bildraten führen zu ruckeligen Filmen.&lt;br /&gt;
* Sichtbare Antwort auf ein interaktives Kommando (z.B. Mausklick): ca. 1/2s&lt;br /&gt;
: Wird diese Antwortzeit überschritten, vermuten viele Benutzer, dass der Mausklick nicht funktioniert hat, und klicken nochmals, mit eventuell fatalen Folgen. Wenn ein Algorithmus notwendigerweise länger dauert als 1/2s, sollte ein Fortschrittsbalken angezeigt werden.&lt;br /&gt;
* Wettervorhersage: muss spätestens am Vorabend des vorhergesagten Tages beendet sein&lt;br /&gt;
&lt;br /&gt;
===Laufzeitvergleich===&lt;br /&gt;
&lt;br /&gt;
Da die Laufzeit für den Benutzer ein so wichtiges Kriterium ist, werden häufig Laufzeitvergleiche durchgeführt. Deren Ergebnisse hängen allerdings von vielen Faktoren ab, die möglicherweise nicht kontrollierbar sind:&lt;br /&gt;
* Geschwindigkeit und Anzahl der Prozessoren&lt;br /&gt;
* Auslastung des Systems&lt;br /&gt;
* Größe des Hauptspeichers  und Cache, Geschwindigkeit des  Datenbus&lt;br /&gt;
* Qualität des Compilers/Optimierers (ist der Compiler für die spezielle Prozessor-Architektur optimiert?)&lt;br /&gt;
* Geschick des Programmierers&lt;br /&gt;
* Daten (Beispiel Quicksort: Best case und worst case [vorsortierter Input] stark unterschiedlich)&lt;br /&gt;
All diese Faktoren sind untereinander abhängig. Laufzeitvergleiche sind daher mit Vorsicht zu interpretieren.&lt;br /&gt;
Generell sollten bei Vergleichen möglichst wenige Parameter verändert werden, z.B.&lt;br /&gt;
* gleiches Programm (gleiche Kompilierung), gleiche Daten, andere Prozessoren&lt;br /&gt;
oder&lt;br /&gt;
* gleiche CPU, Daten, andere Programme (Vergleich von Algorithmen)&lt;br /&gt;
Zur Verbesserung der Vergleichbarkeit gibt es standardisierte [http://en.wikipedia.org/wiki/Benchmark_(computing) Benchmarks], die bestimmte Aspekte eines Systems unter möglichst realitätsnahen Bedingungen testen. Generell gilt aber: Durch Laufzeitmessung ist schwer festzustellen, ob ein Algorithmus ''prinzipiell'' besser ist als ein anderer. Dafür ist die Analyse der [[Effizienz#Algorithmen-Komplexität|Algorithmenkomplexität]] notwendig.&lt;br /&gt;
&lt;br /&gt;
===Optimierung der Laufzeit===&lt;br /&gt;
&lt;br /&gt;
Wenn sich herausstellt, dass ein bereits implementierter Algorithmus zu langsam läuft, geht man wie folgt vor:&lt;br /&gt;
&lt;br /&gt;
# Man verwendet einen [http://en.wikipedia.org/wiki/Performance_analysis Profiler], um zunächst den Flaschenhals zu bestimmen. Ein Profiler ist ein Hilfsprogramm, das während der Ausführung eines Programms misst, wieviel Zeit in jeder Funktion und Unterfunktion verbraucht wird. Dadurch kann man herausfinden, welcher Teil des Algorithmus überhaupt Probleme bereitet. Donald Knuth gibt z.B. als Erfahrungswert an, dass Programme während des größten Teils ihrer Laufzeit nur 3% des Quellcodes (natürlich mehrmals wiederholt) ausführen [http://pplab.snu.ac.kr/courses/adv_pl05/papers/p261-knuth.pdf]. Es ist sehr wichtig, diese 3% experimentell zu bestimmen, weil die Erfahrung zeigt, dass man beim Erraten der kritischen Programmteile oft falsch liegt. Man spricht dann von &amp;quot;[http://en.wikipedia.org/wiki/Optimization_%28computer_science%29#When_to_optimize premature optimization]&amp;quot;, also von voreiliger Optimierung ohne experimentelle Untersuchung der wirklichen Laufzeiten, was laut Knuth &amp;quot;the root of all evil&amp;quot; ist. Der Python-Profiler wird in [http://docs.python.org/lib/profile.html Kapitel 25] der Python-Dokumentation beschrieben.&lt;br /&gt;
# Man kann dann versuchen, die kritischen Programmteile zu optimieren.&lt;br /&gt;
# Falls der Laufzeitgewinn durch Optimierung zu gering ist, muss man einen prinzipiell schnelleren Algorithmus verwenden, falls es einen gibt.&lt;br /&gt;
&lt;br /&gt;
Einige wichtige Techniken der Programmoptimierung sollen hier erwähnt werden. Wenn man einen optimierenden Compiler verwendet, werden einige Optimierungen automatisch ausgeführt [http://en.wikipedia.org/wiki/Compiler_optimization]. In Python trifft dies jedoch nicht zu. Um den Sinn einiger Optimierungen zu verstehen, benötigt man Grundkenntnisse der Computerarchitektur.&lt;br /&gt;
&lt;br /&gt;
;Elimination von redundantem Code: Es ist offensichtlich überflüssig, dasselbe Ergebnis mehrmals zu berechnen, wenn es auch zwischengespeichert werden könnte. Diese Optimierung wird von vielen automatischen Optimierern unterstützt und kommt im wesentlichen in zwei Ausprägungen vor:&lt;br /&gt;
:; common subexpression elimination: In mathematischen Ausdrücken wird ein Teilergebnis häufig mehrmals benötigt. Man betrachte z.B. die Lösung der quadratischen Gleichung &amp;lt;math&amp;gt;x^2+p\,x+q = 0&amp;lt;/math&amp;gt;:&lt;br /&gt;
        x1 = - p / 2.0 + sqrt(p*p/4.0 - q)&lt;br /&gt;
        x2 = - p / 2.0 - sqrt(p*p/4.0 - q)&lt;br /&gt;
::Die mehrmalige Berechnung von Teilausdrücken wird vermieden, wenn man stattdessen schreibt:&lt;br /&gt;
        p2 = - p / 2.0&lt;br /&gt;
        r  = sqrt(p2*p2 - q)&lt;br /&gt;
        x1 = p2 + r&lt;br /&gt;
        x2 = p2 - r&lt;br /&gt;
:; loop invariant elimination: Wenn ein Teilausdruck sich in einer Schleife nicht ändert, muss man ihn nicht bei jedem Schleifendurchlauf neu berechnen, sondern kann dies einmal vor Beginn der Schleife tun. Ein typisches Beispiel hierfür ist die Adressierung von Matrizen, die als 1-dimensionales Array gespeichert sind. Angenommen, wir speichern eine NxN Matrix &amp;lt;tt&amp;gt;m&amp;lt;/tt&amp;gt; in einem Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; der Größe N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt;, so dass das Matrixelement &amp;lt;tt&amp;gt;m&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt;&amp;lt;/tt&amp;gt; durch &amp;lt;tt&amp;gt;a[i + j*N]&amp;lt;/tt&amp;gt; indexiert wird. Wir betrachten die Aufgabe, eine Einheitsmatrix zu initialisieren. Ein nicht optimierter Algorithmus dafür lautet:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + j*N] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + j*N] = 0.0&lt;br /&gt;
::Der Ausdruck &amp;lt;tt&amp;gt;j*N&amp;lt;/tt&amp;gt; wird hier in jedem Schleifendurchlauf erneut berechnet, obwohl sich &amp;lt;tt&amp;gt;j&amp;lt;/tt&amp;gt; in der inneren Schleife gar nicht verändert. Man kann deshalb optimieren zu:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + jN] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + jN] = 0.0&lt;br /&gt;
;Vereinfachung der inneren Schleife: Generell sollte man sich bei der Optimierung auf die innere Schleife eines Algorithmus konzentrieren, weil dieser Code am häufigsten ausgeführt wird. Insbesondere sollte man die Anzahl der Befehle in der inneren Schleife so gering wie möglich halten und teure Befehle vermeiden. Früher waren vor allem Floating-Point Befehle teuer, die man oft durch die schnellere Integer-Arithmetik ersetzt hat, falls dies algorithmisch möglich war (diesen Rat findet man noch oft in der Literatur). Heute hat sich die Hardware so verbessert, dass im Allgemeinen nur noch die Floating-Point Division deutlich langsamer ist als die anderen Operatoren. Im obigen Beispiel der quadratischen Gleichung ist es daher sinnvoll, den Ausdruck &lt;br /&gt;
        p2 = -p / 2.0&lt;br /&gt;
:durch&lt;br /&gt;
        p2 = -0.5 * p&lt;br /&gt;
:zu ersetzen. Dadurch ersetzt man eine Division durch eine Multiplikation und spart außerdem das Negieren von &amp;lt;tt&amp;gt;p&amp;lt;/tt&amp;gt;, da der Compiler direkt mit &amp;lt;tt&amp;gt;-0.5&amp;lt;/tt&amp;gt; multipliziert.&lt;br /&gt;
;Ausnutzung der Prozessor-Pipeline: Moderne Prozessoren führen mehrere Befehle parallel aus. Dies ist möglich, weil jeder Befehl in mehrere Teilschritte zerlegt werden kann. Eine generische Unterteilung in vier Teilschritte ist z.B.:&lt;br /&gt;
:# Dekodieren des nächsten Befehls&lt;br /&gt;
:# Beschaffen der Daten, die der Befehl verwendet (aus Prozessorregistern, dem Cache, oder dem Hauptspeicher)&lt;br /&gt;
:# Ausführen des Befehls&lt;br /&gt;
:# Schreiben der Ergebnisse&lt;br /&gt;
:Man bezeichnet dies als die &amp;quot;[http://en.wikipedia.org/wiki/Instruction_pipeline instruction pipeline]&amp;quot; des Prozessors (heutige Prozessoren verwenden wesentlich feinere Unterteilungen). Prozessoren werden nun so gebaut, dass mehrere Befehle parallel, auf verschiedenen Ausführungsstufen ausgeführt werden. Wenn Befehl 1 also beim Schreiben der Ergebnisse angelangt ist, kann Befehl 2 die Hardware zum Ausführen des Befehls benutzen, während Befehl 3 seine Daten holt, und Befehl 4 soeben dekodiert wird. Unter bestimmten Bedingungen funktioniert diese Parallelverarbeitung jedoch nicht. Dies gibt Anlass zu Optimierungen:&lt;br /&gt;
:;Vermeiden unnötiger Typkonvertierungen: Der Prozessor verarbeitet Interger- und Floating-Point-Befehle in verschiedenen Pipelines, weil die Hardwareanforderungen sehr verschieden sind. Wird jetzt ein Ergebnis von Integer nach Floating-Point umgewandelt oder umgekehrt, muss die jeweils andere Pipeline warten, bis die erste Pipeline ihre Berechnung beendet. Es kann dann besser sein, Berechnungen in Floating-Point zu Ende zu führen, auch wenn sie semantisch eigentlich Integer-Berechnungen sind.&lt;br /&gt;
:;Reduzierung der Anzahl von Verzweigungen: Wenn der Code verzweigt (z.B. durch eine &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;- oder  &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Anweisung), ist nicht klar, welcher Befehl nach der Verzweigung ausgeführt werden soll, bevor Stufe 3 der Pipeline die Verzweigungsbedingung ausgewertet hat. Bis dahin wären die ersten beiden Stufen der Pipeline unbenutzt. Moderne Prozessoren benutzen zwar ausgefeilte Heuristiken, um das Ergebnis der Bedingung vorherzusagen, und führen den hoffentlich richtigen Zweig des Codes spekulativ aus, aber dies funktioniert nicht immer. Man sollte deshalb generell die Anzahl der Verzweigungen minimieren. Als Nebeneffekt führt dies meist auch zu besser lesbarem, verständlicherem Code. Im Matrixbeispiel kann man&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + jN] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + jN] = 0.0&lt;br /&gt;
::durch&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               a[i + jN] = 0.0&lt;br /&gt;
           a[j + jN] = 1.0&lt;br /&gt;
ersetzen. Die Diagonalelemente &amp;lt;tt&amp;gt;a[j + jN]&amp;lt;/tt&amp;gt; werden jetzt zwar zweimal initialisiert (in der Schleife auf Null, dann auf Eins), aber durch Elimination der &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage wird dies wahrscheinlich mehr als ausgeglichen, zumal dadurch die innere Schleife wesentlich vereinfacht wurde.&lt;br /&gt;
;Ausnutzen des Prozessor-Cache: Zugriffe auf den Hauptspeicher sind sehr langsam. Deshalb werden stets ganze Speicherseiten auf einmal in den [http://en.wikipedia.org/wiki/Cache Cache] des Prozessors geladen. Wenn unmittelbar nacheinander benutzte Daten auch im Speicher nahe beieinander liegen (sogenannte &amp;quot;[http://en.wikipedia.org/wiki/Locality_of_reference locality of reference]&amp;quot;), ist die Wahrscheinlichkeit groß, dass die als nächstes benötigten Daten bereits im Cache sind und damit schnell gelesen werden können. Bei vielen Algorithmen kann man die Implementation so umordnen, dass die locality of reference verbessert wird, was zu einer drastischen Beschleunigung führt. Im Matrix-Beispiel ist z.B. die Reihenfolge der Schleifen wichtig. Für konstanten Index &amp;lt;tt&amp;gt;j&amp;lt;/tt&amp;gt; liegen die Indizes &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; im Speicher hintereinander. Deshalb ist es günstig, in der inneren Schleife über &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu iterieren:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               a[i + jN] = 0.0&lt;br /&gt;
           a[j + jN] = 1.0&lt;br /&gt;
:Die umgekehrte Reihenfolge der Schleifen ist hingegen ungünstig&lt;br /&gt;
       for i in range(N):&lt;br /&gt;
           for j in range(N):&lt;br /&gt;
               a[i + j*N] = 0.0&lt;br /&gt;
           a[i + i*N] = 1.0&lt;br /&gt;
:Jetzt werden in der inneren Schleife stets N Datenelemente übersprungen. Besonders bei großem N muss man daher häufig den Cache neu füllen, was bei der ersten Implementation nicht notwendig war.  (Außerdem verliert man hier die Optimierung &amp;lt;tt&amp;gt;jN = j*N&amp;lt;/tt&amp;gt;, die jetzt nicht mehr möglich ist.)&lt;br /&gt;
&lt;br /&gt;
Als Faustregel kann man durch Optimierung eine Verdoppelung der Geschwindigkeit erreichen (in Ausnahmefällen auch mehr). Benötigt man stärkere Verbesserungen, muss man wohl oder übel einen besseren Algorithmus oder einen schnelleren Computer verwenden.&lt;br /&gt;
&lt;br /&gt;
== Algorithmen-Komplexität ==&lt;br /&gt;
&lt;br /&gt;
Komplexitätsbetrachtungen ermöglichen den Vergleich der prinzipiellen Eigenschaften von Algorithmen unabhängig von einer Implementation, Umgebung etc.&lt;br /&gt;
      &lt;br /&gt;
Eine einfache Möglichkeit ist das Zählen der Aufrufe einer Schlüsseloperation. Beispiel Sortieren:&lt;br /&gt;
* Anzahl der Vergleiche&lt;br /&gt;
* Anzahl der Vertauschungen&lt;br /&gt;
&lt;br /&gt;
=== Beispiel: Selection Sort ===&lt;br /&gt;
&lt;br /&gt;
  for i in range(len(a)-1):&lt;br /&gt;
    max = i&lt;br /&gt;
    for j in range(i+1, len(a)):&lt;br /&gt;
      if a[j] &amp;lt; a[max]:&lt;br /&gt;
        max = j&lt;br /&gt;
    a[max], a[i] = a[i], a[max]      # swap&lt;br /&gt;
&lt;br /&gt;
*Anzahl der Vergleiche: Ein Vergleich in jedem Durchlauf der inneren Schleife. Es ergibt sich folgende Komplexität:&lt;br /&gt;
*:Ingesamt &amp;lt;math&amp;gt;\sum_{i=0}^{N-2} \sum_{j=i+1}^{N-1}1 = \frac{N}{2} (N-1) \!&amp;lt;/math&amp;gt; Vergleiche.&lt;br /&gt;
&lt;br /&gt;
*Anzahl der Vertauschungen (swaps): Eine Vertauschung pro Durchlauf der äußeren Schleife:&lt;br /&gt;
*:Insgesamt &amp;lt;math&amp;gt;N-1 \!&amp;lt;/math&amp;gt; Vertauschungen&lt;br /&gt;
&lt;br /&gt;
Die Komplexität wird durch die Operationen bestimmt, die am häufigsten ausgeführt werden, hier also die Anzahl der Vergleiche. Die Anzahl der Vertauschungen ist hingegen kein geeignetes Kriterium für die Komplexität von selection sort, weil der Aufwand in der inneren Schleife ignoriert würde.&lt;br /&gt;
&lt;br /&gt;
=== Fallunterscheidung: Worst und Average Case ===&lt;br /&gt;
&lt;br /&gt;
Die Komplexität ist in der Regel eine Funktion der Eingabegröße (Anzahl der Eingabebits, Anzahl der Eingabeelemente). Sie kann aber auch von der Art der Daten abhängen, nicht nur von der Menge, z.B. vorsortierte Daten bei Quicksort. Um von der Art der Daten unabhängig zu werden, kann man zwei Fälle der Komplexität unterscheiden:&lt;br /&gt;
      &lt;br /&gt;
* Komplexität im ungünstigsten Fall &lt;br /&gt;
*: Der ungünstigste Fall ist die Eingabe gegebener Länge, für die der Algorithmus am langsamsten ist. Der Nachteil dieser Methode besteht darin, dass dieser ungünstige Fall in der Praxis vielleicht gar nicht oder nur selten vorkommt, so dass sich der Algorithmus in Wirklichkeit besser verhält als man nach dieser Analyse erwarten würde. Beim Quicksort-Algorithmus mit zufälliger Wahl des Pivot-Elements müsste z.B. stets das kleinste oder größte Element des aktuellen Intervalls als Pivot-Element gewählt werden, was äußerst unwahrscheinlich ist.&lt;br /&gt;
* Komplexität im durchschnittlichen/typischen Fall&lt;br /&gt;
*: Der typische Fall ist die mittlere Komplexität des Algorithmus über alle möglichen Eingaben. Dazu muss man die Wahrscheinlichkeit jeder möglichen Eingabe kennen, und berechnet dann die mittlere Laufzeit über dieser Wahrscheinlichkeitsverteilung. Leider ist die Wahrscheinlichkeit der Eingaben oft nicht bekannt, so dass man geeignete Annahmen treffen muss. Bei Sortieralgorithmen können z.B. alle möglichen Permutationen des Eingabearrays als gleich wahrscheinlich angenommen werden, und der typische Fall ist dann die mittlere Komplexität über alle diese Eingaben. Oft hat man jedoch in der Praxis andere Wahrscheinlichkeitsverteilungen, z.B. sind die Daten oft &amp;quot;fast sortiert&amp;quot; (nur wenige Elemente sind an der falschen Stelle). Dann verhält sich der Algorithmus ebenfalls anders als vorhergesagt.&lt;br /&gt;
&lt;br /&gt;
Wir beschränken uns in dieser Vorlesung auf die Komplexität im ungünstigseten Fall. '''Exakte''' Formeln für Komplexität sind aber auch dann schwer zu gewinnen, wie das folgende Beispiel zeigt:&lt;br /&gt;
&lt;br /&gt;
=== Beispiele aus den Übungen (Gemessene Laufzeiten für Mergesort/Selectionsort) ===&lt;br /&gt;
&lt;br /&gt;
* Mergesort: &amp;lt;math&amp;gt;\frac{0,977N\log N}{\log 2} + 0,267N-4.39 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: andere Lösung: &amp;lt;math&amp;gt;1140 N\log(N) - 1819N + 6413 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* Selectionsort: &amp;lt;math&amp;gt;\frac{1}{2}N^2 - \frac{1}{2N} - 10^{-12} \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: andere Lösung: &amp;lt;math&amp;gt;1275N^2 - 116003^N + 11111144 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Aus diesen Formeln wird nicht offensichtlich, welcher Algorithmus besser ist.&lt;br /&gt;
Näherung: Betrachte nur '''sehr große Eingaben''' (meist sind alle Algorithmen schnell genug für kleine Eingaben). Dieses Vorgehen wird als '''Asymptotische Komplexität''' bezeichnet (N gegen unendlich).&lt;br /&gt;
&lt;br /&gt;
=== Asymptotische Komplexität am Beispiel Polynom ===&lt;br /&gt;
&lt;br /&gt;
Polynom: &amp;lt;math&amp;gt;a\,x^2+b\,x+c=p\!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;x \!&amp;lt;/math&amp;gt; sei die Eingabegröße, und wir betrachten die Entwicklung von &amp;lt;math&amp;gt;p \!&amp;lt;/math&amp;gt; in Abhängigkeit von &amp;lt;math&amp;gt;x \!&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;math&amp;gt;x=0 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=c \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x=1 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=a+b+c \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x=1000 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=1000000a+1000b+c \approx 1000000a\!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x \to \infty \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p \approx x^2a\!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für sehr große Eingaben verlieren also ''b'' und ''c'' immer mehr an Bedeutung, so dass am Ende nur noch ''a'' für die Komplexitätsbetrachtung wichtig ist.&lt;br /&gt;
&lt;br /&gt;
== Landau-Symbole ==&lt;br /&gt;
&lt;br /&gt;
Um die asymptotische Komplexität verschiedener Algorithmen miteinander vergleichen zu können, verwendet man die sogenannten [http://de.wikipedia.org/wiki/Landau-Symbole Landau-Symbole]. Das wichtigste Landau-Symbol ist &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;, mit dem man eine ''obere Schranke'' &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; für die Komplexität angeben kann. &lt;br /&gt;
&lt;br /&gt;
Schreibt man &amp;lt;math&amp;gt;f \in \Omega(g)&amp;lt;/math&amp;gt;, so stellt dies eine asymptotische ''untere Schranke'' für die Funktion f dar.&lt;br /&gt;
&lt;br /&gt;
Schließlich bedeutet &amp;lt;math&amp;gt;f \in \Theta(g)&amp;lt;/math&amp;gt;, dass die Funktion f genauso schnell wie die Funktion g wächst, das heißt man hat eine asymptotisch ''scharfe Schranke'' für f. Hierzu muss sowohl &amp;lt;math&amp;gt;f\in\mathcal{O}(g)&amp;lt;/math&amp;gt; als auch &amp;lt;math&amp;gt;f \in \Omega(g)&amp;lt;/math&amp;gt; erfüllt sein. &lt;br /&gt;
&lt;br /&gt;
Im nun folgenden soll auf die verschiedenen Landau-Symbole noch näher eingegeangen werden.&lt;br /&gt;
&lt;br /&gt;
===O-Notation===&lt;br /&gt;
&lt;br /&gt;
Intuitiv gilt: Für große N dominieren die am schnellsten wachsenden Terme einer Funktion. Die Notation &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; (sprich &amp;quot;f ist in O von g&amp;quot; oder &amp;quot;f ist von derselben Größenordnung wie g&amp;quot;) formalisiert eine solche Abschätzung der asymptotischen Komplexität der Funktion f von oben. &lt;br /&gt;
; Asymptotische Komplexität: Für zwei Funktionen f(x) und g(x) gilt&lt;br /&gt;
::&amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x))&amp;lt;/math&amp;gt;&lt;br /&gt;
: genau dann wenn es eine Konstante &amp;lt;math&amp;gt;c&amp;gt;0&amp;lt;/math&amp;gt; und ein Argument &amp;lt;math&amp;gt;x_0&amp;lt;/math&amp;gt; gibt, so dass &lt;br /&gt;
::&amp;lt;math&amp;gt;\forall x \ge x_0:\quad f(x) \le c\,g(x)&amp;lt;/math&amp;gt;.&lt;br /&gt;
:Die Menge &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; aller durch g(x) abschätzbaren Funktionen ist also formal definiert durch&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(g(x)) = \{ f(x)\ |\ \exists c&amp;gt;0: \forall x \ge x_0: 0 \le f(x) \le c\,g(x)\}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Idee hinter dieser Definition ist, dass g(x) eine wesentlich einfachere Funktion ist als f(x), die sich aber nach geeigneter Skalierung (Multiplikation mit c) und für große Argumente x im wesentlichen genauso wie f(x) verhält. Man kann deshalb in der Algorithmenanalyse f(x) durch g(x) ersetzen. &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x))&amp;lt;/math&amp;gt; spielt für Funktionen eine ähnliche Rolle wie der Operator &amp;amp;le; für Zahlen: Falls a &amp;amp;le; b gilt, kann bei einer Abschätzung von oben ebenfalls a durch b ersetzt werden.&lt;br /&gt;
&lt;br /&gt;
==== Ein einfaches Beispiel ====&lt;br /&gt;
&lt;br /&gt;
[[Image:Sqsqrt.png]]&lt;br /&gt;
&lt;br /&gt;
Rot = &amp;lt;math&amp;gt;x^2 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
Blau = &amp;lt;math&amp;gt;\sqrt{x} \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\sqrt{x} \in \mathcal{O}(x^2)\!&amp;lt;/math&amp;gt; weil &amp;lt;math&amp;gt;\sqrt{x} \le c\,x^2\!&amp;lt;/math&amp;gt; für alle &amp;lt;math&amp;gt;x \ge x_0 = 1 \!&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c = 1\!&amp;lt;/math&amp;gt;, oder auch für &amp;lt;math&amp;gt;x \ge x_0 = 4 \!&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c = 1/16&amp;lt;/math&amp;gt; (die Wahl von c und x&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; in der Definition von O(.) ist beliebig, solange die Bedingungen erfüllt sind).&lt;br /&gt;
&lt;br /&gt;
==== Komplexität bei kleinen Eingaben ==== &lt;br /&gt;
&lt;br /&gt;
Algorithmus 1: &amp;lt;math&amp;gt;\mathcal{O}(N^2) \!&amp;lt;/math&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Algorithmus 2: &amp;lt;math&amp;gt;\mathcal{O}(N\log{N}) \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus 2 ist schneller (von geringerer Komplexität) für große Eingaben, aber bei kleinen Eingaben (insbesondere, wenn der Algorithmus in einer Schleife immer wieder mit kleinen Eingaben aufgerufen wird) könnte Algorithmus 1 schneller sein, falls der in der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation verborgene konstante Faktor ''c'' bei Algorithmus 2 einen wesentlich größeren Wert hat als bei Algorithmus 1.&lt;br /&gt;
&lt;br /&gt;
==== Eigenschaften der O-Notation (Rechenregeln) ==== &lt;br /&gt;
&lt;br /&gt;
# Transitiv:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x)) \land g(x) \in \mathcal{O}(h(x)) \to f(x) \in \mathcal{O}(h(x)) \!&amp;lt;/math&amp;gt;           &lt;br /&gt;
# Additiv:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(h(x)) \land g(x) \in \mathcal{O}(h(x)) \to f(x) + g(x) \in \mathcal{O}(h(x)) \!&amp;lt;/math&amp;gt;           &lt;br /&gt;
# Für Monome gilt:&lt;br /&gt;
#: &amp;lt;math&amp;gt;x^k \in \mathcal{O}(x^k)&amp;lt;/math&amp;gt; und&lt;br /&gt;
#: &amp;lt;math&amp;gt;x^k \in \mathcal{O}(x^{k+j}), \forall j \ge 0 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Multiplikation mit einer Konstanten:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x)) \to c\,f(x) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: andere Schreibweise:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) = c\,g(x) \to f(x) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Folgerung aus 3. und 4. für Polynome: &lt;br /&gt;
#: &amp;lt;math&amp;gt;a_0+a_1\,x + ... + a_n\,x^n \in \mathcal{O}(x^n)\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: Beispiel: &amp;lt;math&amp;gt;a\,x^2+b\,x+c \in \mathcal{O}(x^2)\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Logarithmus:&lt;br /&gt;
#: &amp;lt;math&amp;gt;a, b &amp;gt; 1\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{b}{x})\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: Die Basis des Logarithmus spielt also keine Rolle.&lt;br /&gt;
#: Beweis hierfür:&lt;br /&gt;
#:: &amp;lt;math&amp;gt;\log_{a}{x} = \frac{\log_{b}{x}}{\log_{b}{a}}\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#:: Mit &amp;lt;math&amp;gt;c = 1 / \log_{b}{a}\,&amp;lt;/math&amp;gt; gilt: &amp;lt;math&amp;gt;\log_{a}{x} = c\,\log_{b}{x}\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#:: Wird hier die (zweite) Regel für Multiplikation mit einer Konstanten angewendet, fällt der konstante Faktor weg, also &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{b}{x})\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Insbesondere gilt auch &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{2}{x})\!&amp;lt;/math&amp;gt;, es kann also immer der 2er Logarithmus verwendet werden.&lt;br /&gt;
&lt;br /&gt;
==== O-Kalkül ==== &lt;br /&gt;
&lt;br /&gt;
Das O-Kalkül definiert wichtige Vereinfachungsregeln for Ausdrücke in O-Notation (Beweise: siehe Übungsaufgabe):&lt;br /&gt;
&lt;br /&gt;
# &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(f(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# &amp;lt;math&amp;gt;\mathcal{O}(\mathcal{O}(f(x))) \in \mathcal{O}(f(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# &amp;lt;math&amp;gt;c\,\mathcal{O}(f(x)) \in \mathcal{O}(f(x))\,&amp;lt;/math&amp;gt; für jede Konstante ''c''&lt;br /&gt;
# &amp;lt;math&amp;gt;\mathcal{O}(f(x))+c \in \mathcal{O}(f(x))\,&amp;lt;/math&amp;gt; für jede Konstante ''c''&lt;br /&gt;
# Sequenzregel:&lt;br /&gt;
#: Wenn zwei nacheinander ausgeführte Programmteile die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt; bzw. &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; haben, gilt für beide gemeinsam:&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(f(x))&amp;lt;/math&amp;gt; falls &amp;lt;math&amp;gt;g(x) &amp;lt; \mathcal{O}(f(x))&amp;lt;/math&amp;gt; bzw.&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt; falls &amp;lt;math&amp;gt;f(x) &amp;lt; \mathcal{O}(g(x))&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Informell schreibt man auch: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(max(f(x), g(x)))\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
# Schachtelungsregel bzw. Aufrufregel:&lt;br /&gt;
#: Wenn in einer geschachtelten Schleife die äußere Schleife die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt; hat, und die innere &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt;, gilt für beide gemeinsam:&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) * \mathcal{O}(g(x)) \in \mathcal{O}(f(x) * g(x))\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Gleiches gilt wenn eine Funktion &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt;-mal aufgerufen wird, und die Komplexität der Funktion selbst &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; ist.&lt;br /&gt;
&lt;br /&gt;
;Beispiel für 5.: Beide Schleifen haben die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Dies gilt auch für ihre Hintereinanderausführung:&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          a[i] = i&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          print a[i]&lt;br /&gt;
;Beispiele für 6.: Beide Schleifen haben die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Ihre Verschachtelung hat daher die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N^2)&amp;lt;/math&amp;gt;. &lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          for j in range(N):&lt;br /&gt;
              a[i*N + j] = i+j&lt;br /&gt;
: Dies gilt ebenso, wenn statt der inneren Schleife eine Funktion mit Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt; ausgeführt wird:&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          a[i] = foo(i, N)  # &amp;lt;math&amp;gt;\mathrm{foo}(i, N) \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== O-Kalkül auf das Beispiel des Selectionsort angewandt ==== &lt;br /&gt;
&lt;br /&gt;
Selectionsort: Wir hatten gezeigt dass &amp;lt;math&amp;gt;f(N) = \frac{N^2}{2} - \frac{N}{2}&amp;lt;/math&amp;gt;. Nach der Regel für Polynome vereinfacht sich dies zu &amp;lt;math&amp;gt;f(N) \in \mathcal{O}\left(\frac{N^2}{2}\right) = \mathcal{O}(N^2)\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Alternativ via Schachtelungsregel:&lt;br /&gt;
: Die äußere Schleife wird (''N''-1)-mal durchlaufen: &amp;lt;math&amp;gt;N-1 \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
: Die innere Schleife wird (''N-i''-1)-mal durchlaufen. Das sind im Mittel ''N''/2 Durchläufe: &amp;lt;math&amp;gt;N/2 \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
: Zusammen: &amp;lt;math&amp;gt;\mathcal{O}(N)*\mathcal{O}(N) \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nach beiden Vorgehensweisen kommen wir zur Schlussfolgerung, dass der Selectionsort die asymptotische Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N^2)\!&amp;lt;/math&amp;gt; besitzt.&lt;br /&gt;
&lt;br /&gt;
==== Zusammenhang zwischen Komplexität und Laufzeit ==== &lt;br /&gt;
&lt;br /&gt;
Wenn eine Operation 1ms dauert, erreichen Algorithmen verschiedener Komplexität folgende Leistungen (wobei angenommen wird, dass der in der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation verborgene konstante Faktor immer etwa gleich 1 ist):&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:left&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
|-&lt;br /&gt;
! Komplexität !! Operationen in 1s !! Operationen in 1min !! Operationen in 1h&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 1000 || 60.000 || 3.600.000&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N\log_2{N})&amp;lt;/math&amp;gt;&lt;br /&gt;
| 140 || 4895 || 204094&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 32 || 245 || 1898&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 10 || 39 || 153&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(2^N)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 10 || 16 || 21&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Exponentielle Komplexität ==== &lt;br /&gt;
Der letzte Fall &amp;lt;math&amp;gt;\mathcal{O}(2^N)&amp;lt;/math&amp;gt; ist von exponentieller Komplexität. Das bedeutet, dass eine Verdopplung des Aufwands nur bewirkt, dass die maximale Problemgröße um eine Konstante wächst. Algorithmen mit exponentieller (oder noch höherer) Komplexität werden deshalb als '''ineffizient''' bezeichnet. Algorithmen mit höchstens polynomieller Komplexität gelten hingegen als effizient.&lt;br /&gt;
&lt;br /&gt;
In der Praxis sind allerdings auch polynomielle Algorithmen mit hohem Exponenten meist zu langsam. Als Faustregel kann man eine praktische Grenze von &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt; ansehen. Bei einer Komplexität von &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt; bewirkt ein verdoppelter Aufwand immer noch eine Steigerung der maximalen Problemgröße um den Faktor &amp;lt;math&amp;gt;\sqrt[3]{2}&amp;lt;/math&amp;gt; (also eine ''multiplikative'' Vergrößerung um ca. 25%, statt nur einer additiven Vergrößerung wie bei exponentieller Komplexität).&lt;br /&gt;
&lt;br /&gt;
===&amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;- Notation===&lt;br /&gt;
&lt;br /&gt;
Genauso wie &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; eine Art &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;-Operator für Funktionen ist, definiert &amp;lt;math&amp;gt;f \in \Omega(g) &amp;lt;/math&amp;gt; eine Abschätzung von unten, analog zum &amp;lt;math&amp;gt;\ge&amp;lt;/math&amp;gt;-Operator für Zahlen. Formal kann man &amp;lt;math&amp;gt;f(N) \in \Omega(g(N)) &amp;lt;/math&amp;gt; genau dann schreiben, falls es eine Konstante &amp;lt;math&amp;gt; c &amp;gt; 0 &amp;lt;/math&amp;gt; gibt, so dass &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; f(N) \ge c \cdot g(N) &amp;lt;/math&amp;gt; für &amp;lt;math&amp;gt; N  \ge N_0 &amp;lt;/math&amp;gt; &lt;br /&gt;
&lt;br /&gt;
gilt.&lt;br /&gt;
Man verwendet diese Notation also um abzuschätzen, wie groß der Aufwand (die Komplexität) für einen bestimmten Algorithmus ''mindestens'' ist und nicht ''höchstens'', was man mit der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt; - Notation ausdrücken würde.&lt;br /&gt;
&lt;br /&gt;
Ein praktisches Beispiel für eine Anwendung der &amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;- Notation wäre die Fragestellung, ob es ''prinzipiell'' einen besseren Algorithmus für ein bestimmtes Problem gibt. Wie später im Abschnitt [[Suchen#Sortieren_als_Suchproblem|Sortieren als Suchproblem]] gezeigt wird, ist das Sortieren eines Arrays durch paarweise Vergleiche von Elementen immer mindestens von der Komplexität &amp;lt;math&amp;gt; \Omega(N\cdot \ln N) &amp;lt;/math&amp;gt;, was konkret bedeutet, dass kein Sortieralgorithmus, der nach diesem Prinzip arbeitet, jemals eine geringere Komplexität als beispielsweise Merge-Sort haben wird. Natürlich kann man den entsprechenden Sortieralgorithmus, also Merge-Sort zum Beispiel, unter Umständen noch optimieren, aber die Komplexität wird erhalten bleiben. Mit diesem Wissen kann man sich viel (vergebliche) Arbeit sparen.&lt;br /&gt;
&lt;br /&gt;
===&amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;- Notation===&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;math&amp;gt;f(N) \in \Theta(g(N))&amp;lt;/math&amp;gt; ist eine scharfe Abschätzung der asymptotischen Komplexität einer Funktion f. &lt;br /&gt;
&lt;br /&gt;
Damit dies gilt, muss &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; und ''gleichzeitig'' &amp;lt;math&amp;gt;f(N) \in \Omega(g(N))&amp;lt;/math&amp;gt; erfüllt sein.&lt;br /&gt;
&lt;br /&gt;
Dies ist natürlich auch die beste Abschätzung der asymptotischen Komplexität einer Funktion f. Formal bedeutet &amp;lt;math&amp;gt;f(N) \in \Theta(g(N))&amp;lt;/math&amp;gt; dass es zwei Konstanten &amp;lt;math&amp;gt; c_1 &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; c_2 &amp;lt;/math&amp;gt;, beide größer als Null, gibt, so dass für alle &amp;lt;math&amp;gt; N \geq N_0 &amp;lt;/math&amp;gt; gilt: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; c_1 \cdot g(N) \leq f(N) \leq c_2 \cdot g(N) &amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In der Praxis wird manchmal statt der &amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;-Notation auch dann die &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation benutzt, wenn eine scharfe Schranke ausgedrückt werden soll. Dies ist zwar formal nicht korrekt, aber man kann die intendierte Bedeutung meist aus dem Kontext erschließen.&lt;br /&gt;
&lt;br /&gt;
== Komplexitätsvergleich zweier Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
In diesem Abschnitt wollen wir der Frage nachgehen, wie ein formaler Beweis für die Behauptung &amp;lt;math&amp;gt; f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; geschehen kann. Hierbei werden zwei Beweismethoden vorgestellt werden, und zwar der '''Beweis über die Definition der Komplexität''' sowie der '''Beweis durch Dividieren'''.&lt;br /&gt;
&lt;br /&gt;
===Beweis über die Definition der asymptotischen Komplexität===&lt;br /&gt;
&lt;br /&gt;
Die Definition der asymptotischen Komplexität &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; war: &lt;br /&gt;
&lt;br /&gt;
Es gibt eine Konstante &amp;lt;math&amp;gt; c &amp;gt; 0 &amp;lt;/math&amp;gt;, so dass &amp;lt;math&amp;gt; f(N) \le c \cdot g(N) &amp;lt;/math&amp;gt; für &amp;lt;math&amp;gt; N  \ge N_0 &amp;lt;/math&amp;gt; erfüllt ist. &lt;br /&gt;
&lt;br /&gt;
Um also die die asymptotische Komplexität &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; zu beweisen, muss man die oben erwähnten Konstanten c und &amp;lt;math&amp;gt; N_0 &amp;lt;/math&amp;gt; finden, so dass &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; f(N) \leq c \cdot g(N) &amp;lt;/math&amp;gt; für alle &amp;lt;math&amp;gt; N \ge N_0 &amp;lt;/math&amp;gt; erfüllt ist. &lt;br /&gt;
&lt;br /&gt;
Dies geschieht zweckmäßigerweise mit dem Beweisprinzip der ''vollständigen Induktion''. Hierbei ist zu zeigen, dass&lt;br /&gt;
# &amp;lt;math&amp;gt; f(N_0) \leq g(N_0) &amp;lt;/math&amp;gt; für die eine zu bestimmende Konstante &amp;lt;math&amp;gt; N_0 &amp;lt;/math&amp;gt; gilt (''Induktionsanfang'') und &lt;br /&gt;
# falls &amp;lt;math&amp;gt; f(N) \leq g(N) &amp;lt;/math&amp;gt;, dann auch &amp;lt;math&amp;gt; f(N+1) \leq g(N+1) &amp;lt;/math&amp;gt; (''Induktionsschritt'') gilt.&lt;br /&gt;
&lt;br /&gt;
===Beweis durch Dividieren===&lt;br /&gt;
&lt;br /&gt;
Hierbei wählt man eine Konstante c und zeigt, dass &amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f(N)}{c \cdot g(N)} \leq 1 &amp;lt;/math&amp;gt; gilt (für die O-Notation, bei &amp;amp;Omega;-Notation gilt entsprechend &amp;lt;math&amp;gt;\geq 1 &amp;lt;/math&amp;gt;). Man kann dies auch als alternative Definition der Komplexität verwenden.&lt;br /&gt;
&lt;br /&gt;
Als Beispiel betrachten wir die beiden Funktionen &amp;lt;math&amp;gt; f(N) = N \,\lg N &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; g(N) = N^2 &amp;lt;/math&amp;gt; und wollen zeigen, dass &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; gilt. &lt;br /&gt;
&lt;br /&gt;
Als Konstante c wählen wir &amp;lt;math&amp;gt; c = 1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f(N)}{g(N)} = \lim_{N \rightarrow \infty} \frac{\lg N}{N} = \frac{\infty}{\infty} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Unbestimmte Ausdrücke der Form &lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{x \rightarrow x_0} \frac{f(x)}{g(x)} &amp;lt;/math&amp;gt;,&lt;br /&gt;
in denen sowohl &amp;lt;math&amp;gt; f(x) &amp;lt;/math&amp;gt; als auch &amp;lt;math&amp;gt; g(x) &amp;lt;/math&amp;gt; mit &amp;lt;math&amp;gt; x \rightarrow x_0 &amp;lt;/math&amp;gt; gegen Null oder gegen Unendlich streben, kann man manchmal mit den Regeln von [http://de.wikipedia.org/wiki/L%27Hospital%27sche_Regel ''l'Hospital''] berechnen. Danach darf man die Funktionen f und g zur Berechnung des unbestimmten Ausdrucks durch ihre k-ten Ableitungen ersetzen:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{x \rightarrow x_0} \frac{f(x)}{g(x)} = \lim_{x \rightarrow x_0} \frac{f^{(k)}(x)}{g^{(k)}(x)} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In unserem Fall verwenden wir die erste Ableitung und erhalten:&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f'(x)}{g'(x)} = \lim_{N \rightarrow \infty} \frac{1/N}{1} \rightarrow 0 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Damit wurde &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt;, also &amp;lt;math&amp;gt;N \lg N \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt; gezeigt.&lt;br /&gt;
&lt;br /&gt;
Man beachte hierbei, dass &amp;lt;math&amp;gt;N \lg N \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt; keine enge Grenze für die Komplexität von &amp;lt;math&amp;gt;N \,\lg N&amp;lt;/math&amp;gt; darstellt, da der Grenzwert &amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f'(x)}{g'(x)}\, &amp;lt;/math&amp;gt; gegen 0 und nicht gegen eine von Null verschiedene Konstante strebt. In diesem Fall haben wir die Komplexität von &amp;lt;math&amp;gt;N \cdot \lg N &amp;lt;/math&amp;gt; also nur nach oben abschätzen können.&lt;br /&gt;
&lt;br /&gt;
===Beispiel für den Komplexitätsvergleich: Gleitender Mittelwert (Running Average)===&lt;br /&gt;
&lt;br /&gt;
Wir berechnen für ein gegebenes Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; einen gleitenden Mittelwert über &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; Elemente:&amp;lt;br/&amp;gt;&lt;br /&gt;
::&amp;lt;math&amp;gt;r_i = \frac{1}{k} \sum_{j=i-k+1}^i a_j&amp;lt;/math&amp;gt; &amp;lt;br/&amp;gt;&lt;br /&gt;
Das heisst, für jedes &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; mitteln wir die letzten &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; Elemente von &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; und schreiben das Ergebnis in &amp;lt;tt&amp;gt;r[i]&amp;lt;/tt&amp;gt;. Diese Operation ist z.B. bei Börsenkursen wichtig: Neben dem aktuellen Kurs für jeden Tag wird dort meist auch der gleitende Mittelwert der letzten 30 Tage sowie der letzten 200 Tage angegeben. In diesen Mittelwerten erkennt man besser die langfristige Tendenz, weil die täglichen Schwankungen herausgemittelt werden. Wir nehmen außerdem an, dass&lt;br /&gt;
* Array-Zugriff hat eine Komplexit&amp;amp;auml;t von O(1)&lt;br /&gt;
* &amp;lt;math&amp;gt;k \ll N&amp;lt;/math&amp;gt;, d.h. &amp;lt;math&amp;gt;N-k\approx N&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die beiden folgenden Algorithmen berechnen die Mittelwerte auf unterschiedliche Art. Der linke folgt der obigen Definition durch eine Summe, während der rechte inkrementell arbeitet: Man kann den Bereich der &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; letzten Werte als Fenster betrachten, das über das Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; geschoben wird. Schiebt man das Fenster ein Element weiter, fällt links ein Element heraus, und rechts kommt eins hinzu. Man muss also nicht jedes Mal die Summe neu berechnen, sondern kann den vorigen Wert aktualisieren. Wir werden sehen, dass dies Folgen für die Komplexität des Algorithmus hat.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;2&amp;quot; &lt;br /&gt;
|-&lt;br /&gt;
! Programmzeile&lt;br /&gt;
! Version 1: O(N * k)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
! Version 2: O(N)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
1.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
2.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
3.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
4.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k-1, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for i in range(k):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
5.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;for i in range(j-k+1, j+1):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[k-1] += a[i]&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
6.&lt;br /&gt;
|&lt;br /&gt;
:::: &amp;lt;tt&amp;gt;r[j] += a[i]&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
7.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] = (a[j] - a[j-k] + r[j-1])&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
8.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
9.&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
10.&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Wir zeigen unten dass Version 2 eine geringere Komplexit&amp;amp;auml;t besitzt, obwohl sie mehr Zeilen ben&amp;amp;ouml;tigt.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Wir haben in der Tabelle die Komplexität jeder Zeile für sich angegeben. Einfache Anweisungen (Berechnungen, Lese- und Schreibzugriffe auf das Array, Zuweiseungen) haben konstante Komplexität, die Komplexität des Schleifenkopfes allein (also der &amp;lt;tt&amp;gt;for&amp;lt;/tt&amp;gt;-Anweisung ohne den eingerückten Schleifenkörper) entspricht der Anzahl der Durchläufe. Wir müssen jetzt noch die Verschachtelung der Schleifen und die Nacheinanderausführung von Anweisungen berücksichtigen. &lt;br /&gt;
&lt;br /&gt;
====Berechnung der Komplexität von Version 1====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;small&amp;gt;(Wiederholung der Rechenregeln: siehe Abschnitt [[Effizienz#O-Notation|O-Notation]])&amp;lt;/small&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wir betrachten zunächst die innere Schleife (Zeilen 5 und 6 von Version 1):&lt;br /&gt;
&lt;br /&gt;
Der Schleifenkopf (Zeile 5) hat die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt;, weil die Schleife k-mal durchlaufen wird. Der Schleifenkörper (Zeile 6) hat die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;. Nach der Verschachtelungsregel müssen wir die beiden Komplexitäten multiplizieren, und es ergibt sich:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(k)\cdot\mathcal{O}(1) = \mathcal{O}(k\cdot 1)=\mathcal{O}(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wir betrachten nun die äußere Schleife. Der Schleifenkopf (Zeile 4) wird (N-k)-mal durchlaufen und hat somit eine Komplexität von &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Der Schleifenkörper (Zeilen 5 bis 7) besteht aus der inneren Schleife (Zeilen 5 und 6) mit der gerade berechneten Komplexität &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt; sowie einer einfachen Anweisung (Zeile 7) mit Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;. Nach der Sequenzregel wird die Komplexität des Schleifenkörpers durch Addition berechnet:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(k)+\mathcal{O}(1) = \mathcal{O}(\max(k,1)) = \mathcal{O}(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexität der gesamten äußeren Schleife erhalten wir nach der Verschachtelungsregel wieder durch multiplizieren:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)\cdot\mathcal{O}(k) = \mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die übrigen Schritte des Algorithmus werden einfach nacheinander ausgeführt, so dass sie ebenfalls nach der Sequenzregel behandelt werden. Wir erhalten&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)+\mathcal{O}(1)+\mathcal{O}(N\cdot k)+\mathcal{O}(1) = \mathcal{O}(\max(N,1,N\cdot k,1)) = \mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Der gesamte Algorithmus hat also die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Berechnung der Komplexität von Version 2====&lt;br /&gt;
&lt;br /&gt;
Hier gibt es nur einfache Schleifen ohne Verschachtelung. Da der Schleifenkörper jeder Schleife nur einfache Anweisungen der Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt; enthält, ergibt sich die Komplexität der Schleifen nach der Verschachtelungsregel als&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(X)\cdot\mathcal{O}(1) = \mathcal{O}(X\cdot 1)=\mathcal{O}(X)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
wobei &amp;lt;math&amp;gt;\mathcal{O}(X)&amp;lt;/math&amp;gt; die Komplexität des jeweiligen Schleifenkopfes ist. Wir erhalten also für Zeilen 4 und 5: &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt;, Zeilen 6 und 7: &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;, Zeilen 8 und 9: &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Die Hintereinanderausführung wird nach der Sequenzregel behandelt:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)+\mathcal{O}(1)+\mathcal{O}(k)+\mathcal{O}(N)+\mathcal{O}(N)+\mathcal{O}(1) = \mathcal{O}(\max(N,1,k,N,N,1)) = \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Dieser Algorithmus hat also nur die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Fazit====&lt;br /&gt;
&lt;br /&gt;
Obwohl Version 2 mehr Schritte benötigt hat sie eine geringere Komplexität, da die for-Schleifen nicht wie bei Version 1 verschachtelt/untergeordnet sind. Bei verschachtelten for-Schleifen muss die Multiplikationsregel angewendet werden &amp;amp;rarr; höhere Komplexität.&lt;br /&gt;
&lt;br /&gt;
Die gerade berechnete Komplexität gilt aber &amp;lt;u&amp;gt;nur&amp;lt;/u&amp;gt; unter der Annahme, dass Array-Zugriffe konstante Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt; besitzen. Wenn dies nicht der Fall ist, kann sich die Komplexität des Algorithmus drastisch verschlechtern.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|Allgemein gilt:&amp;lt;br/&amp;gt;&lt;br /&gt;
Algorithmen-Analysen beruhen auf der Annahme, dass Zugriffe auf die Daten optimal schnell sind, dass heißt, dass die für den jeweiligen Algorithmus am besten geeignete Datenstruktur verwendetet wird.&amp;lt;br /&amp;gt; &amp;amp;rarr; Ansonsten: Komplexitätsverschlechterung!&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Beispiel für eine Verschlechterung der Komplexität durch Verwendung einer nicht optimalen Datenstruktur====&lt;br /&gt;
&lt;br /&gt;
Wir verwende im Mittelwert-Algorithmus eine verkettete Liste anstelle des Eingabe-Arrays a&amp;lt;/u&amp;gt;. Wir benötigen dazu eine Funktion, die das j-te Element der Liste zurückgibt. Wie üblich ist die Liste mit Hilfe einer Knotenklasse implementiert:&lt;br /&gt;
      class Node:&lt;br /&gt;
          def __init__(self, data):&lt;br /&gt;
              self.data = data&lt;br /&gt;
              self.next = None&lt;br /&gt;
&lt;br /&gt;
Die Listenklasse selbst hat ein Feld &amp;lt;tt&amp;gt;head&amp;lt;/tt&amp;gt;, das eine Referenz auf den ersten Knoten speichert, und jeder Knoten speichert im Feld &amp;lt;tt&amp;gt;next&amp;lt;/tt&amp;gt; eine Referenz auf seinen Nachfolger. Um zum j-ten Element zu gelangen, muss man die Liste sequenziell durchlaufen&lt;br /&gt;
      def get_jth(list, j):&lt;br /&gt;
           r = list.head&lt;br /&gt;
           while j &amp;gt; 0:&lt;br /&gt;
               r = r.head&lt;br /&gt;
               j -= 1&lt;br /&gt;
           return r.data&lt;br /&gt;
Die Komplexität dieser Funktion ist offensichtlich &amp;lt;math&amp;gt;\mathcal{O}(j)&amp;lt;/math&amp;gt; (Komplexitätsberechnung wie oben). Wir setzen jetzt bei Version 1 des Mittelwert-Algorithmus diese Funktion in Zeile 6 anstelle des Indexzugriffs &amp;lt;tt&amp;gt;a[i]&amp;lt;/tt&amp;gt; ein (nur in dieser Zeile wird auf die Elemente des Arrays zugegriffen). Wir erhalten folgende Implementation (die Änderungen sind rot markiert):&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;2&amp;quot; &lt;br /&gt;
|-&lt;br /&gt;
! Programmzeile&lt;br /&gt;
! Version 1 mit Liste: O(N * k)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
1.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
2.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
3.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
4.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k-1, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
5.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;for i in range(j-k+1, j+1):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
6.&lt;br /&gt;
|&lt;br /&gt;
:::: &amp;lt;tt&amp;gt;r[j] += &amp;lt;font color=red&amp;gt;get_jth(a, i)&amp;lt;/font&amp;gt;&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;&amp;lt;font color=red&amp;gt;O(i)&amp;lt;/font&amp;gt;&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
7.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
8.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Der Aufruf der Funktion &amp;lt;tt&amp;gt;get_jth&amp;lt;/tt&amp;gt; ist jetzt gleichbedeutend mit einer dreifach verschachtelten Schleife (weil &amp;lt;tt&amp;gt;get_jth&amp;lt;/tt&amp;gt; ja eine zusatzliche Schleife enthält). Die Anzahl der Operationen in Zeile 4 bis 6 ist jetzt&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;f(N,k)=\sum_{j=k-1}^{N-1}\,\sum_{i=j-k+1}^j\,\mathcal{O}(i)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
wobei das &amp;lt;math&amp;gt;\mathcal{O}(i)&amp;lt;/math&amp;gt; die neue Schleife durch Verwendung der Liste repräsentiert. Mit Mathematica-Hilfe [http://www.wolfram.com/] lässt sich diese Summe exakt ausrechnen&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;f(N,k)=\frac{1}{2}(k N^2-k^2 N+k^2-k)\in \mathcal{O}(k N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexitätsberechnung erfolgte dabei nach der Regel für Polynome unter Beachtung von &amp;lt;math&amp;gt;k \ll N&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Fazit:====&lt;br /&gt;
&lt;br /&gt;
Die Komplexität von Version 1 mit einer verketteten Liste wäre O(N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; * k)&lt;br /&gt;
'''&amp;amp;rarr; Die richtige Datenstruktur ist wichtig, da es sonst zu einer Komplexitätsverschlechterung kommen kann!'''&lt;br /&gt;
&lt;br /&gt;
Auf Version 2 unseres Running Average-Beispiels hätte eine verkettete Liste allerdings keine Auswirkungen, da die inkrementelle Berechnung der Summen in Zeile 7 weiterhin möglich ist (bei geschickter Implementation!) und somit Version 2 immer noch eine Komplexität von O(N) hätte.&lt;br /&gt;
&lt;br /&gt;
==Amortisierte Komplexität==&lt;br /&gt;
&lt;br /&gt;
Bis jetzt wurde die Komplexität nur im schlechtesten Fall (Worst Case) betrachtet. Bei einigen Operationen schwankt die Komplexität jedoch sehr stark, wenn man sie mehrmals hintereinander ausführt, und der schlechteste Fall kommt nur selten vor. Dann ist es sinnvoll, die &amp;lt;i&amp;gt;amortisierte Komplexität&amp;lt;/i&amp;gt; zu betrachten, die sich mit der &amp;lt;i&amp;gt;durchschnittlichen&amp;lt;/i&amp;gt; Komplexität über viele Aufrufe der selben Operation beschäftigt.&lt;br /&gt;
&lt;br /&gt;
Zum weiter Lesen: [[http://de.wikipedia.org/wiki/Amortisierte_Laufzeitanalyse Wikipedia: Amortisierte Laufzeitanalyse]]&lt;br /&gt;
&lt;br /&gt;
===Beispiel: Inkrementieren von Binärzahlen===&lt;br /&gt;
&lt;br /&gt;
Frage: Angenommen, das Umdrehen eines Bits einer Binärzahl verursacht Kosten von 1 Einheit. Wir erzeugen die Folge der natürlichen Zahlen in Binärdarstellung durch sukzessives Inkrementieren, von Null beginnend. Bei jeder Inkrementierung werden einige Bits verändert, aber diese Zahl (und damit die Kosten der Inkrementierungen) ''schwanken'' sehr stark. Wir fragen jetzt, was eine Inkrementierung im Durchschnitt kostet?&lt;br /&gt;
&lt;br /&gt;
Um diese Durchschnittskosten zu berechnen, bezahlen wir bei jeder Inkrementierung 2 Einheiten. Wenn davon nach Abzug der Kosten der jeweiligen Operation noch etwas übrig bleibt, wird der Rest dem  Guthaben zugeschrieben. Umgekehrt wird ein eventueller Fehlbetrag (wenn eine Inkrementierung mehr als 2 Bits umdreht) aus dem Guthaben gedeckt. Dadurch werden die ansonsten großen Schwankungen der Kosten ausgeglichen:&lt;br /&gt;
:: Kosten &amp;lt; Einzahlung &amp;amp;rarr; es wird gespart&lt;br /&gt;
:: Kosten = Einzahlung &amp;amp;rarr; Guthaben bleibt unverändert&lt;br /&gt;
:: Kosten &amp;gt; Einzahlung &amp;amp;rarr; Guthaben wird für die Kosten verbraucht&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
!Schritte&lt;br /&gt;
!Zahlen&lt;br /&gt;
!Kosten &amp;lt;br/&amp;gt;&lt;br /&gt;
(Anzahl der geänderten Bits)&lt;br /&gt;
! Einzahlung&lt;br /&gt;
!Guthaben =&amp;lt;br/&amp;gt;&lt;br /&gt;
altes Guthaben + Einzahlung - Kosten&lt;br /&gt;
|-&lt;br /&gt;
|1.&lt;br /&gt;
|0000&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|2.&lt;br /&gt;
|000&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|2&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|3.&lt;br /&gt;
|0001&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|4.&lt;br /&gt;
|00&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|3&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|5.&lt;br /&gt;
|0010&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|6.&lt;br /&gt;
|001&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|2&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|7.&lt;br /&gt;
|0011&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''3'''&lt;br /&gt;
|-&lt;br /&gt;
|8.&lt;br /&gt;
|0&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1000&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|4&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Die Kosten ergeben sich aus der Anzahl der Ziffern die von 1 nach 0, bzw. von 0 nach 1 verändert werden&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Rechnung:&amp;lt;/u&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Schritt: Kosten: 1 &amp;lt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Schritt: Kosten: 2 = Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird nicht gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; Guthaben bleibt so wie es ist &amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Schritt: Kosten: 1 &amp;lt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. Schritt: Kosten: 3 &amp;gt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird eine 1 vom Guthaben genommen um die Kosten zu zahlen&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
usw.&lt;br /&gt;
&lt;br /&gt;
Man erkennt, dass vor teuren Operation (Wechsel von 3 auf 4 bzw. von 7 auf 8) genügend Guthaben angespart wurde, um die Kosten zu decken. Das Guthaben geht bei diesen Operationen immer wieder auf 1 zurück, aber es wird nie vollständig verbraucht (Dies kann natürlich auch mathematisch exakt bewiesen werden, wie wir es unten am Beispiel des dynamische Arrays zeigen). Wir schließen daraus, dass die durchschnittlichen oder '''amortisierten Kosten''' einer Inkrementierungsoperation gleich 2 sind.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Zum weiter Lesen: [[http://de.wikipedia.org/wiki/Account-Methode Wikipedia Account-Methode]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
====Fazit====&lt;br /&gt;
Die amortisierte Komplexität beschäftigt sich mit dem Durchschnitt aller Operation im ungünstigsten Fall. Operationen mit hohen Kosten, die aber nur selten ausgeführt werden, fallen bei der amortisierten Komplexität nicht so ins Gewicht. Bei Algorithmen, die gelegentlich eine &amp;quot;teure&amp;quot; Operation benutzen, ansonsten jedoch &amp;quot;billigen&amp;quot; Operationen aufrufen, kann die amortisierte Komplexität niedriger sein als die Komplexität im schlechtesten (Einzel-)Fall.&lt;br /&gt;
&lt;br /&gt;
In unserem Beispiel fallen die teuren Einzelschritte (z.B. 4. und 8. Schritt) bei den amortisierten Kosten nicht so ins Gewicht, da wir die Kosten aus unserem Guthaben mitbezahlen können. Das Guthaben ist immer groß genug, weil jeder zweite Aufruf eine billige Operation ist, die nur ein Bit umdreht und somit das Ansparen ermöglichen. Diese Betrachtung zeigt, dass die amortisierte (d.h. durchschnittliche) Komplexität des Algoithmus niedriger (nämlich konstant) ist als die Komplexität im schlechtesten Fall.&lt;br /&gt;
&lt;br /&gt;
===Anwendung: Dynamisches Array===&lt;br /&gt;
&lt;br /&gt;
Ein dynamisches Array hat die Eigenschaft, dass man effizient am Ende des Arrays neue Elemente anfügen kann, indem man die Länge des Arrays entsprechend vergrößert (siehe Übung 1). Die Analyse der amortisierten Komplexität der Anfüge-Operation zeigt uns, wie man das Vergrößern des Arrays richtig implementiert, damit die Operation wirklich effizient abläuft.&lt;br /&gt;
&lt;br /&gt;
==== Ineffiziente naive Lösung ====&lt;br /&gt;
&lt;br /&gt;
Wenn wir an ein Array ein Element anhängen wollen, müssen wir neuen Speicher allokieren, der die gewünschte Länge hat. Die Werte aus dem alten Array müssen dann in den neuen Speicher umkopiert werden. Danach kann das neue Element hinten angefügt werden, weil wir im neuen Array bereits Speicher für dieses Element reserviert haben. Bei der naiven Implementation des dynamischen Arrays wiederholt man dies bei jeder Anfügeoperation. Für die Analyse nehmen wir an, dass das Kopieren eines Elements konstante Zeit O(1) erfordert, ebenso das Einfügen eines neuen Elements auf in eine noch unbenutzte Speicherposition.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Naives Anhängen eines weiteren Elements an ein Array:&amp;lt;/u&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;right&amp;quot;&lt;br /&gt;
!Schritte&lt;br /&gt;
|'''Array'''&lt;br /&gt;
&amp;lt;small&amp;gt;(wie es nach jedem Schritt aussieht)&amp;lt;/small&amp;gt;&lt;br /&gt;
!Komplexität&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;center&amp;gt;altes Array (N=4)&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|1. neuer Speicher für&amp;lt;br&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;(N+1) Elemente&lt;br /&gt;
|&amp;lt;center&amp;gt;[None,None,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;O(N+1) = '''O(N)'''&amp;lt;/center&amp;gt;(wenn der Speicher initialisiert wird&amp;lt;br&amp;gt;(hier auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;), sonst O(1))&lt;br /&gt;
|-&lt;br /&gt;
|2. Kopieren &lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;'''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|3. append von &amp;quot;x&amp;quot;&lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3,'x']&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;'''O(1)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
altesArray = [0,1,2,3]&amp;lt;br /&amp;gt;&lt;br /&gt;
altesArray.append('x')&lt;br /&gt;
&lt;br /&gt;
1. Es wird ein neues Array der Größe N+1 erzeugt&amp;lt;br /&amp;gt;&lt;br /&gt;
2. Die N Datenelemente aus dem alten Array werden in das neue Array kopiert&amp;lt;small&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Das sind N Operationen der Komplexität O(1), also ein Gesamtaufwand von O(N).&amp;lt;/small&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
3. 'x' wird mit Aufwand O(1) an die letzte Stelle des neuen Arrays geschrieben&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Additionsregel:&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
O(N) + O(1) &amp;amp;isin; O(N)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Folgerung:&amp;lt;/b&amp;gt; &lt;br /&gt;
&lt;br /&gt;
Bei der naiven Methode erfordert jede Anfügung einen Aufwand O(N) (wobei N die derzeitige Arraygröße ist). Dies ist nicht effizient.&lt;br /&gt;
&lt;br /&gt;
====Effiziente Lösung durch Verdoppeln der Kapazität====&lt;br /&gt;
&lt;br /&gt;
Offensichtlich kommt man nicht darum heraum, den Inhalt des alten Arrays zu kopieren, wenn der allokierte Speicher voll ist. Der Trick für die effiziente Implementation der Anfügeoperation besteht darin, das Kopieren so selten wie möglich durchzuführen, also nicht wie in der naiven Lösung bei jeder Anfügeoperation. Hier kommt die amortisierte Komplexität ins Spiel: Ab und zu gibt es eine teure Anfügeoperation (wenn nämlich kopiert werden muss), aber wenn man den durchschnittlichen Aufwand über viele Anfügungen betrachtet, ist die Operation effizient. Der teure Fall wird sozusagen &amp;quot;herausgemittelt&amp;quot;. &lt;br /&gt;
&lt;br /&gt;
Um nur selten kopieren zu müssen, werden beim dynamischen Array mehr Speicherelemente reserviert als zur Zeit benötigt werden (in der naiven Lösung wurde dagegen immer nur Speicher für ein einziges neues Element reserviert). Wir unterscheiden deshalb &lt;br /&gt;
&lt;br /&gt;
:&amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; = Anzahl der allokierten Speicherzellen, d.h. der möglichen Elemente, die in das Array passen&amp;lt;br /&amp;gt;&lt;br /&gt;
:&amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt; = Anzahl der Elemente, die im Array zur Zeit gespeichert sind&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Daten selbst werden in einem statischen Array gespeichert:&lt;br /&gt;
:&amp;lt;tt&amp;gt;data&amp;lt;/tt&amp;gt; = statisches Array der Größe &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die folgende intuitive Abschätzung zeigt, dass es sinnvoll ist, die Größe des allokierten Speichers jeweils zu verdoppeln. Wir starten bei einem Array der Größe &amp;lt;tt&amp;gt;size = capacity&amp;lt;/tt&amp;gt; = N. Da der verfügbare Speicher voll ist, müssen wir bei der nächsten Anfügung die N vorhandenen Elemente in ein neues Array der Länge &amp;lt;tt&amp;gt;new_capacity&amp;lt;/tt&amp;gt; kopieren (Aufwand &amp;lt;math&amp;gt;N\cdot O(1)&amp;lt;/math&amp;gt;). Danach können wir K Elemente billig einfügen (Aufwand &amp;lt;math&amp;gt;K\cdot O(1)&amp;lt;/math&amp;gt;), wobei &lt;br /&gt;
:K = &amp;lt;tt&amp;gt;new_capacity - capacity&amp;lt;/tt&amp;gt; &lt;br /&gt;
die Anzahl der nach dem Kopieren noch unbenutzen Speicherzellen ist. Der durchschnittliche Aufwand für diese K Einfügungen ist somit&lt;br /&gt;
:&amp;lt;math&amp;gt;\bar T = \frac{N \cdot O(1) + K \cdot O(1)}{K}=\frac{N+K}{K}\cdot O(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Damit die mittlere Zeit in O(1) sein kann, muss der Quotient &amp;lt;math&amp;gt;(N+K)/K&amp;lt;/math&amp;gt; eine Konstante sein. Wir setzen &amp;lt;math&amp;gt;K = a N&amp;lt;/math&amp;gt; und erhalten:&lt;br /&gt;
:&amp;lt;math&amp;gt;\bar T = \frac{(a+1)N}{a N}\cdot O(1)=\frac{a+1}{a}\cdot O(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Der amortisierte Aufwand über K Einfügungen ist also konstant, wenn &amp;lt;math&amp;gt;a&amp;lt;/math&amp;gt; eine (kleine) von N unabhängige Zahl ist. Typischerweise wählt man &lt;br /&gt;
:&amp;lt;math&amp;gt;a = 1&amp;lt;/math&amp;gt;&lt;br /&gt;
und mit &amp;lt;math&amp;gt;K = 1\cdot N&amp;lt;/math&amp;gt; ergibt sich&lt;br /&gt;
:&amp;lt;tt&amp;gt;new_capacity = capacity&amp;lt;/tt&amp;gt; + N = &amp;lt;tt&amp;gt;2 * capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Vorgehensweise beim Zufügen eines neuen Elements im Fall &amp;lt;tt&amp;gt;size == capacity&amp;lt;/tt&amp;gt; ist also&lt;br /&gt;
* capacity wird verdoppelt&amp;lt;br /&amp;gt;&lt;br /&gt;
: &amp;lt;tt&amp;gt;neue capacity = 2 * alte capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
: (allgemein genügt es auch, wenn capacity um einen bestimmten Prozentsatz vergrößert wird, &lt;br /&gt;
:: &amp;lt;tt&amp;gt;neue capacity = alte capacity * c&amp;lt;/tt&amp;gt; &lt;br /&gt;
: mit c &amp;gt; 1, z.B. c = 1.2, das entspricht oben der Wahl &amp;lt;math&amp;gt;a = 0.2&amp;lt;/math&amp;gt;)&lt;br /&gt;
* ein neues statisches Array der Größe 'neue capacity' wird erzeugt&lt;br /&gt;
* das alte Array wird ins neue kopiert und danach freigegeben&lt;br /&gt;
* das anzufügende Element wird ins neue Array eingefügt&lt;br /&gt;
Umgekehrt geht man beim Entfernen des ''letzten'' Array-Elements vor. Normalerweise überschreibt man einfach das letzte Element mit &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; und dekrementiert &amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt;. Wird dadurch das Array zu klein (üblicherweise &amp;lt;tt&amp;gt;size &amp;amp;lt; capacity / 4&amp;lt;/tt&amp;gt;), wird die Kapazität halbiert, genauer:&lt;br /&gt;
* ein neues Array mit &amp;lt;br/&amp;gt;&lt;br /&gt;
: &amp;lt;tt&amp;gt;neue capacity = alte capacity / 2 &amp;lt;/tt&amp;gt;&lt;br /&gt;
: wird angelegt (bzw. mit&lt;br /&gt;
:: &amp;lt;tt&amp;gt;neue capacity = alte capacity / c &amp;lt;/tt&amp;gt;&lt;br /&gt;
: wenn ein anderer Vergrößerungsfaktor verwendet wird)&lt;br /&gt;
* das alte Array wird ins neue kopiert und danach freigegeben&lt;br /&gt;
&lt;br /&gt;
'''Folge:''' Die Kosten für das Vergrößern/Verkleinern der Kapazität werden amortisiert über viele Einfügungen, die kein Vergrößern erfordern. Die Operation &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; besitzt amortisierte Komplexität O(1). Im folgenden Abschnitt zeigen wir dies mathematisch exakt mit der Potentialmethode.&lt;br /&gt;
&lt;br /&gt;
====Komplexitätsanalyse des dynamischen Arrays mit Potentialmethode====&lt;br /&gt;
&lt;br /&gt;
Durchschnitt der Gesamtkosten für N-maliges append = &amp;lt;math&amp;gt;\frac{1}{N} \sum_{i = 1}^N Kosten(i)&amp;lt;/math&amp;gt;. Zur Analyse der amortisierten Komplexität wird ein Potential&amp;lt;br/&amp;gt;&lt;br /&gt;
::&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2*size&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; - capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
eingeführt, wobei das Array nach dem i-ten Einfüge-Schritt die Größe size&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; und die Kapizität capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; hat. Wir nehmen vereinfachend an, dass es keine Löschoperationen gibt. Dann gilt nach dem i-ten Schritt jeweils&lt;br /&gt;
::&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2*i - capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Fall 1: Array ist nicht voll&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Es wird kein Umkopieren benötigt, da das Array noch nicht voll ist&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; size&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; &amp;lt; capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; == capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Kosten: 1 (für Einfügen des neuen Elements)&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial vor append: &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = 2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial nach append: &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
amortisierte Kosten = Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Phi;&amp;lt;sub&amp;gt;(i)&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;(i-1)&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::                 = 1            + (2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;)        - [2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;]&lt;br /&gt;
:::::                 = 1            + 2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;         - 2i + 2 + capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::                 = 1            + &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; - &amp;lt;del&amp;gt;capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;/del&amp;gt; - &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; + 2 + &amp;lt;del&amp;gt;capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;/del&amp;gt;&lt;br /&gt;
:::::                 = 1 + 2&lt;br /&gt;
:::::                 = 3 = O(1) &amp;amp;rarr; konstant&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Fall 2: Array ist voll&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Vor dem i-ten append muss umkopiert werden&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; size&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; == capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; == i-1&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; Allokieren eines neuen statischen Arrays mit verdoppelter Kapazität notwendig, also capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; == 2*capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Kosten: (i-1) + 1 (für Umkopieren und Einfügen des neuen Elements)&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial vor append =  &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = 2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial nach append =  &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; =  2i - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
amortisierte Kosten = Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Phi;&amp;lt;sub&amp;gt;(i)&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;(i-1)&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = ((i - 1) + 1) + 2i - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; - [2(i-1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;]&lt;br /&gt;
:::::               = i + &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; - &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; + 2 - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = i + 2 - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = i + 2 - (i - 1) &amp;lt;small&amp;gt;(da capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = i-1)&amp;lt;/small&amp;gt;&lt;br /&gt;
:::::               = 3 = O(1) &amp;amp;rarr; konstant            &lt;br /&gt;
&lt;br /&gt;
'''Damit wurde bewiesen, dass die Operation &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; beim dynamischen Array eine amortisierte Komplexität von 3 Einheiten hat, also &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; &amp;amp;isin; O(1)'''. Diese Operation kann deshalb gefahrlos in der inneren Schleife eines Algorithmus benutzt werden.&lt;br /&gt;
&lt;br /&gt;
==== Beispiel für 9 Einfügeoperationen ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot;&lt;br /&gt;
!Array&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;lt;small&amp;gt;(wie es aussehen könnte)&amp;lt;/small&amp;gt;&lt;br /&gt;
!size&lt;br /&gt;
!capacity&lt;br /&gt;
!Kosten für append&amp;lt;br /&amp;gt;(einschließlich Umkopieren)&lt;br /&gt;
!Summe Kosten&lt;br /&gt;
!Durchschnittskosten&lt;br /&gt;
!&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2 * size - capacity&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;lt;small&amp;gt;(i = size)&amp;lt;/small&amp;gt;&lt;br /&gt;
!Potenzialdifferenz&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;amp;Delta; &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
!amortisierte Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
= Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Delta; &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;0&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3/2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6/3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;0&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7/4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;5&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;12&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;12/5&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;13&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;13/6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;14&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;14/7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,h]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;15&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;15/8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,h,j,None,None,None,&amp;lt;br /&amp;gt;&lt;br /&gt;
None,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;9&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;16&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;24&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;24/9&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die durchschnittlichen Kosten betragen stets etwa 2 Einheiten, schwanken allerdings so, dass nicht unmittelbar ersichtlich ist, ob dies für sämtliche Einfügeoperationen gilt. Die amortisierte Komplexität, die mit Hilfe des Potentials berechnet wird, ist hingegen konstant 3, wie auch im obigen Beweis für alle Einfügeoperationen allgemein gezeigt wurde.&lt;br /&gt;
&lt;br /&gt;
[[Suchen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Effizienz&amp;diff=5416</id>
		<title>Effizienz</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Effizienz&amp;diff=5416"/>
				<updated>2012-07-27T15:37:50Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Amortisierte Komplexität */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Bei der Diskussion von Effizienz müssen wir zwischen der Laufzeit eines Algorithmus auf einem bestimmten System und seiner prinzipiellen Leistungsfähigkeit (Algorithmenkomplexität) unterscheiden. Der Benutzer ist natürlich vor allem an der Laufzeit interessiert, denn diese bestimmt letztendlich seine Arbeitsproduktivität. Ein Softwaredesigner hingegen muss eine Implementation wählen, die auf verschiedenen Systemen und in verschiedenen Anwendungen schnell ist. Für ihn sind daher auch Aussagen zur Algorithmenkomplexität sehr wichtig, um den am besten geeigneten Algorithmus auszuwählen.&lt;br /&gt;
&lt;br /&gt;
== Laufzeit ==&lt;br /&gt;
&lt;br /&gt;
Aus Anwendersicht ist ein Algorithmus effizient, wenn er die in der Spezifikation verlangten Laufzeitgrenzen einhält. Ein Algorithmus muss also nicht immer so schnell wie möglich sein, sondern so schnell wie nötig. Dies führt in verschiedenen Anwendungen zu ganz unterschiedliche Laufzeitanforderungen:&lt;br /&gt;
&lt;br /&gt;
* Berechnen des nächsten Steuerkommandos für eine Maschine: ca. 1/1000s&lt;br /&gt;
* Berechnen des nächsten Bildes für eine Videopräsentation (z.B. Dekompression von MPEG-kodierten Bildern): ca. 1/25s&lt;br /&gt;
: Geringere Bildraten führen zu ruckeligen Filmen.&lt;br /&gt;
* Sichtbare Antwort auf ein interaktives Kommando (z.B. Mausklick): ca. 1/2s&lt;br /&gt;
: Wird diese Antwortzeit überschritten, vermuten viele Benutzer, dass der Mausklick nicht funktioniert hat, und klicken nochmals, mit eventuell fatalen Folgen. Wenn ein Algorithmus notwendigerweise länger dauert als 1/2s, sollte ein Fortschrittsbalken angezeigt werden.&lt;br /&gt;
* Wettervorhersage: muss spätestens am Vorabend des vorhergesagten Tages beendet sein&lt;br /&gt;
&lt;br /&gt;
===Laufzeitvergleich===&lt;br /&gt;
&lt;br /&gt;
Da die Laufzeit für den Benutzer ein so wichtiges Kriterium ist, werden häufig Laufzeitvergleiche durchgeführt. Deren Ergebnisse hängen allerdings von vielen Faktoren ab, die möglicherweise nicht kontrollierbar sind:&lt;br /&gt;
* Geschwindigkeit und Anzahl der Prozessoren&lt;br /&gt;
* Auslastung des Systems&lt;br /&gt;
* Größe des Hauptspeichers  und Cache, Geschwindigkeit des  Datenbus&lt;br /&gt;
* Qualität des Compilers/Optimierers (ist der Compiler für die spezielle Prozessor-Architektur optimiert?)&lt;br /&gt;
* Geschick des Programmierers&lt;br /&gt;
* Daten (Beispiel Quicksort: Best case und worst case [vorsortierter Input] stark unterschiedlich)&lt;br /&gt;
All diese Faktoren sind untereinander abhängig. Laufzeitvergleiche sind daher mit Vorsicht zu interpretieren.&lt;br /&gt;
Generell sollten bei Vergleichen möglichst wenige Parameter verändert werden, z.B.&lt;br /&gt;
* gleiches Programm (gleiche Kompilierung), gleiche Daten, andere Prozessoren&lt;br /&gt;
oder&lt;br /&gt;
* gleiche CPU, Daten, andere Programme (Vergleich von Algorithmen)&lt;br /&gt;
Zur Verbesserung der Vergleichbarkeit gibt es standardisierte [http://en.wikipedia.org/wiki/Benchmark_(computing) Benchmarks], die bestimmte Aspekte eines Systems unter möglichst realitätsnahen Bedingungen testen. Generell gilt aber: Durch Laufzeitmessung ist schwer festzustellen, ob ein Algorithmus ''prinzipiell'' besser ist als ein anderer. Dafür ist die Analyse der [[Effizienz#Algorithmen-Komplexität|Algorithmenkomplexität]] notwendig.&lt;br /&gt;
&lt;br /&gt;
===Optimierung der Laufzeit===&lt;br /&gt;
&lt;br /&gt;
Wenn sich herausstellt, dass ein bereits implementierter Algorithmus zu langsam läuft, geht man wie folgt vor:&lt;br /&gt;
&lt;br /&gt;
# Man verwendet einen [http://en.wikipedia.org/wiki/Performance_analysis Profiler], um zunächst den Flaschenhals zu bestimmen. Ein Profiler ist ein Hilfsprogramm, das während der Ausführung eines Programms misst, wieviel Zeit in jeder Funktion und Unterfunktion verbraucht wird. Dadurch kann man herausfinden, welcher Teil des Algorithmus überhaupt Probleme bereitet. Donald Knuth gibt z.B. als Erfahrungswert an, dass Programme während des größten Teils ihrer Laufzeit nur 3% des Quellcodes (natürlich mehrmals wiederholt) ausführen [http://pplab.snu.ac.kr/courses/adv_pl05/papers/p261-knuth.pdf]. Es ist sehr wichtig, diese 3% experimentell zu bestimmen, weil die Erfahrung zeigt, dass man beim Erraten der kritischen Programmteile oft falsch liegt. Man spricht dann von &amp;quot;[http://en.wikipedia.org/wiki/Optimization_%28computer_science%29#When_to_optimize premature optimization]&amp;quot;, also von voreiliger Optimierung ohne experimentelle Untersuchung der wirklichen Laufzeiten, was laut Knuth &amp;quot;the root of all evil&amp;quot; ist. Der Python-Profiler wird in [http://docs.python.org/lib/profile.html Kapitel 25] der Python-Dokumentation beschrieben.&lt;br /&gt;
# Man kann dann versuchen, die kritischen Programmteile zu optimieren.&lt;br /&gt;
# Falls der Laufzeitgewinn durch Optimierung zu gering ist, muss man einen prinzipiell schnelleren Algorithmus verwenden, falls es einen gibt.&lt;br /&gt;
&lt;br /&gt;
Einige wichtige Techniken der Programmoptimierung sollen hier erwähnt werden. Wenn man einen optimierenden Compiler verwendet, werden einige Optimierungen automatisch ausgeführt [http://en.wikipedia.org/wiki/Compiler_optimization]. In Python trifft dies jedoch nicht zu. Um den Sinn einiger Optimierungen zu verstehen, benötigt man Grundkenntnisse der Computerarchitektur.&lt;br /&gt;
&lt;br /&gt;
;Elimination von redundantem Code: Es ist offensichtlich überflüssig, dasselbe Ergebnis mehrmals zu berechnen, wenn es auch zwischengespeichert werden könnte. Diese Optimierung wird von vielen automatischen Optimierern unterstützt und kommt im wesentlichen in zwei Ausprägungen vor:&lt;br /&gt;
:; common subexpression elimination: In mathematischen Ausdrücken wird ein Teilergebnis häufig mehrmals benötigt. Man betrachte z.B. die Lösung der quadratischen Gleichung &amp;lt;math&amp;gt;x^2+p\,x+q = 0&amp;lt;/math&amp;gt;:&lt;br /&gt;
        x1 = - p / 2.0 + sqrt(p*p/4.0 - q)&lt;br /&gt;
        x2 = - p / 2.0 - sqrt(p*p/4.0 - q)&lt;br /&gt;
::Die mehrmalige Berechnung von Teilausdrücken wird vermieden, wenn man stattdessen schreibt:&lt;br /&gt;
        p2 = - p / 2.0&lt;br /&gt;
        r  = sqrt(p2*p2 - q)&lt;br /&gt;
        x1 = p2 + r&lt;br /&gt;
        x2 = p2 - r&lt;br /&gt;
:; loop invariant elimination: Wenn ein Teilausdruck sich in einer Schleife nicht ändert, muss man ihn nicht bei jedem Schleifendurchlauf neu berechnen, sondern kann dies einmal vor Beginn der Schleife tun. Ein typisches Beispiel hierfür ist die Adressierung von Matrizen, die als 1-dimensionales Array gespeichert sind. Angenommen, wir speichern eine NxN Matrix &amp;lt;tt&amp;gt;m&amp;lt;/tt&amp;gt; in einem Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; der Größe N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt;, so dass das Matrixelement &amp;lt;tt&amp;gt;m&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt;&amp;lt;/tt&amp;gt; durch &amp;lt;tt&amp;gt;a[i + j*N]&amp;lt;/tt&amp;gt; indexiert wird. Wir betrachten die Aufgabe, eine Einheitsmatrix zu initialisieren. Ein nicht optimierter Algorithmus dafür lautet:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + j*N] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + j*N] = 0.0&lt;br /&gt;
::Der Ausdruck &amp;lt;tt&amp;gt;j*N&amp;lt;/tt&amp;gt; wird hier in jedem Schleifendurchlauf erneut berechnet, obwohl sich &amp;lt;tt&amp;gt;j&amp;lt;/tt&amp;gt; in der inneren Schleife gar nicht verändert. Man kann deshalb optimieren zu:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + jN] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + jN] = 0.0&lt;br /&gt;
;Vereinfachung der inneren Schleife: Generell sollte man sich bei der Optimierung auf die innere Schleife eines Algorithmus konzentrieren, weil dieser Code am häufigsten ausgeführt wird. Insbesondere sollte man die Anzahl der Befehle in der inneren Schleife so gering wie möglich halten und teure Befehle vermeiden. Früher waren vor allem Floating-Point Befehle teuer, die man oft durch die schnellere Integer-Arithmetik ersetzt hat, falls dies algorithmisch möglich war (diesen Rat findet man noch oft in der Literatur). Heute hat sich die Hardware so verbessert, dass im Allgemeinen nur noch die Floating-Point Division deutlich langsamer ist als die anderen Operatoren. Im obigen Beispiel der quadratischen Gleichung ist es daher sinnvoll, den Ausdruck &lt;br /&gt;
        p2 = -p / 2.0&lt;br /&gt;
:durch&lt;br /&gt;
        p2 = -0.5 * p&lt;br /&gt;
:zu ersetzen. Dadurch ersetzt man eine Division durch eine Multiplikation und spart außerdem das Negieren von &amp;lt;tt&amp;gt;p&amp;lt;/tt&amp;gt;, da der Compiler direkt mit &amp;lt;tt&amp;gt;-0.5&amp;lt;/tt&amp;gt; multipliziert.&lt;br /&gt;
;Ausnutzung der Prozessor-Pipeline: Moderne Prozessoren führen mehrere Befehle parallel aus. Dies ist möglich, weil jeder Befehl in mehrere Teilschritte zerlegt werden kann. Eine generische Unterteilung in vier Teilschritte ist z.B.:&lt;br /&gt;
:# Dekodieren des nächsten Befehls&lt;br /&gt;
:# Beschaffen der Daten, die der Befehl verwendet (aus Prozessorregistern, dem Cache, oder dem Hauptspeicher)&lt;br /&gt;
:# Ausführen des Befehls&lt;br /&gt;
:# Schreiben der Ergebnisse&lt;br /&gt;
:Man bezeichnet dies als die &amp;quot;[http://en.wikipedia.org/wiki/Instruction_pipeline instruction pipeline]&amp;quot; des Prozessors (heutige Prozessoren verwenden wesentlich feinere Unterteilungen). Prozessoren werden nun so gebaut, dass mehrere Befehle parallel, auf verschiedenen Ausführungsstufen ausgeführt werden. Wenn Befehl 1 also beim Schreiben der Ergebnisse angelangt ist, kann Befehl 2 die Hardware zum Ausführen des Befehls benutzen, während Befehl 3 seine Daten holt, und Befehl 4 soeben dekodiert wird. Unter bestimmten Bedingungen funktioniert diese Parallelverarbeitung jedoch nicht. Dies gibt Anlass zu Optimierungen:&lt;br /&gt;
:;Vermeiden unnötiger Typkonvertierungen: Der Prozessor verarbeitet Interger- und Floating-Point-Befehle in verschiedenen Pipelines, weil die Hardwareanforderungen sehr verschieden sind. Wird jetzt ein Ergebnis von Integer nach Floating-Point umgewandelt oder umgekehrt, muss die jeweils andere Pipeline warten, bis die erste Pipeline ihre Berechnung beendet. Es kann dann besser sein, Berechnungen in Floating-Point zu Ende zu führen, auch wenn sie semantisch eigentlich Integer-Berechnungen sind.&lt;br /&gt;
:;Reduzierung der Anzahl von Verzweigungen: Wenn der Code verzweigt (z.B. durch eine &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;- oder  &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Anweisung), ist nicht klar, welcher Befehl nach der Verzweigung ausgeführt werden soll, bevor Stufe 3 der Pipeline die Verzweigungsbedingung ausgewertet hat. Bis dahin wären die ersten beiden Stufen der Pipeline unbenutzt. Moderne Prozessoren benutzen zwar ausgefeilte Heuristiken, um das Ergebnis der Bedingung vorherzusagen, und führen den hoffentlich richtigen Zweig des Codes spekulativ aus, aber dies funktioniert nicht immer. Man sollte deshalb generell die Anzahl der Verzweigungen minimieren. Als Nebeneffekt führt dies meist auch zu besser lesbarem, verständlicherem Code. Im Matrixbeispiel kann man&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               if i == j:&lt;br /&gt;
                    a[i + jN] = 1.0&lt;br /&gt;
               else:&lt;br /&gt;
                    a[i + jN] = 0.0&lt;br /&gt;
::durch&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               a[i + jN] = 0.0&lt;br /&gt;
           a[j + jN] = 1.0&lt;br /&gt;
ersetzen. Die Diagonalelemente &amp;lt;tt&amp;gt;a[j + jN]&amp;lt;/tt&amp;gt; werden jetzt zwar zweimal initialisiert (in der Schleife auf Null, dann auf Eins), aber durch Elimination der &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage wird dies wahrscheinlich mehr als ausgeglichen, zumal dadurch die innere Schleife wesentlich vereinfacht wurde.&lt;br /&gt;
;Ausnutzen des Prozessor-Cache: Zugriffe auf den Hauptspeicher sind sehr langsam. Deshalb werden stets ganze Speicherseiten auf einmal in den [http://en.wikipedia.org/wiki/Cache Cache] des Prozessors geladen. Wenn unmittelbar nacheinander benutzte Daten auch im Speicher nahe beieinander liegen (sogenannte &amp;quot;[http://en.wikipedia.org/wiki/Locality_of_reference locality of reference]&amp;quot;), ist die Wahrscheinlichkeit groß, dass die als nächstes benötigten Daten bereits im Cache sind und damit schnell gelesen werden können. Bei vielen Algorithmen kann man die Implementation so umordnen, dass die locality of reference verbessert wird, was zu einer drastischen Beschleunigung führt. Im Matrix-Beispiel ist z.B. die Reihenfolge der Schleifen wichtig. Für konstanten Index &amp;lt;tt&amp;gt;j&amp;lt;/tt&amp;gt; liegen die Indizes &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; im Speicher hintereinander. Deshalb ist es günstig, in der inneren Schleife über &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; zu iterieren:&lt;br /&gt;
       for j in range(N):&lt;br /&gt;
           jN = j*N&lt;br /&gt;
           for i in range(N):&lt;br /&gt;
               a[i + jN] = 0.0&lt;br /&gt;
           a[j + jN] = 1.0&lt;br /&gt;
:Die umgekehrte Reihenfolge der Schleifen ist hingegen ungünstig&lt;br /&gt;
       for i in range(N):&lt;br /&gt;
           for j in range(N):&lt;br /&gt;
               a[i + j*N] = 0.0&lt;br /&gt;
           a[i + i*N] = 1.0&lt;br /&gt;
:Jetzt werden in der inneren Schleife stets N Datenelemente übersprungen. Besonders bei großem N muss man daher häufig den Cache neu füllen, was bei der ersten Implementation nicht notwendig war.  (Außerdem verliert man hier die Optimierung &amp;lt;tt&amp;gt;jN = j*N&amp;lt;/tt&amp;gt;, die jetzt nicht mehr möglich ist.)&lt;br /&gt;
&lt;br /&gt;
Als Faustregel kann man durch Optimierung eine Verdoppelung der Geschwindigkeit erreichen (in Ausnahmefällen auch mehr). Benötigt man stärkere Verbesserungen, muss man wohl oder übel einen besseren Algorithmus oder einen schnelleren Computer verwenden.&lt;br /&gt;
&lt;br /&gt;
== Algorithmen-Komplexität ==&lt;br /&gt;
&lt;br /&gt;
Komplexitätsbetrachtungen ermöglichen den Vergleich der prinzipiellen Eigenschaften von Algorithmen unabhängig von einer Implementation, Umgebung etc.&lt;br /&gt;
      &lt;br /&gt;
Eine einfache Möglichkeit ist das Zählen der Aufrufe einer Schlüsseloperation. Beispiel Sortieren:&lt;br /&gt;
* Anzahl der Vergleiche&lt;br /&gt;
* Anzahl der Vertauschungen&lt;br /&gt;
&lt;br /&gt;
=== Beispiel: Selection Sort ===&lt;br /&gt;
&lt;br /&gt;
  for i in range(len(a)-1):&lt;br /&gt;
    max = i&lt;br /&gt;
    for j in range(i+1, len(a)):&lt;br /&gt;
      if a[j] &amp;lt; a[max]:&lt;br /&gt;
        max = j&lt;br /&gt;
    a[max], a[i] = a[i], a[max]      # swap&lt;br /&gt;
&lt;br /&gt;
*Anzahl der Vergleiche: Ein Vergleich in jedem Durchlauf der inneren Schleife. Es ergibt sich folgende Komplexität:&lt;br /&gt;
*:Ingesamt &amp;lt;math&amp;gt;\sum_{i=0}^{N-2} \sum_{j=i+1}^{N-1}1 = \frac{N}{2} (N-1) \!&amp;lt;/math&amp;gt; Vergleiche.&lt;br /&gt;
&lt;br /&gt;
*Anzahl der Vertauschungen (swaps): Eine Vertauschung pro Durchlauf der äußeren Schleife:&lt;br /&gt;
*:Insgesamt &amp;lt;math&amp;gt;N-1 \!&amp;lt;/math&amp;gt; Vertauschungen&lt;br /&gt;
&lt;br /&gt;
Die Komplexität wird durch die Operationen bestimmt, die am häufigsten ausgeführt werden, hier also die Anzahl der Vergleiche. Die Anzahl der Vertauschungen ist hingegen kein geeignetes Kriterium für die Komplexität von selection sort, weil der Aufwand in der inneren Schleife ignoriert würde.&lt;br /&gt;
&lt;br /&gt;
=== Fallunterscheidung: Worst und Average Case ===&lt;br /&gt;
&lt;br /&gt;
Die Komplexität ist in der Regel eine Funktion der Eingabegröße (Anzahl der Eingabebits, Anzahl der Eingabeelemente). Sie kann aber auch von der Art der Daten abhängen, nicht nur von der Menge, z.B. vorsortierte Daten bei Quicksort. Um von der Art der Daten unabhängig zu werden, kann man zwei Fälle der Komplexität unterscheiden:&lt;br /&gt;
      &lt;br /&gt;
* Komplexität im ungünstigsten Fall &lt;br /&gt;
*: Der ungünstigste Fall ist die Eingabe gegebener Länge, für die der Algorithmus am langsamsten ist. Der Nachteil dieser Methode besteht darin, dass dieser ungünstige Fall in der Praxis vielleicht gar nicht oder nur selten vorkommt, so dass sich der Algorithmus in Wirklichkeit besser verhält als man nach dieser Analyse erwarten würde. Beim Quicksort-Algorithmus mit zufälliger Wahl des Pivot-Elements müsste z.B. stets das kleinste oder größte Element des aktuellen Intervalls als Pivot-Element gewählt werden, was äußerst unwahrscheinlich ist.&lt;br /&gt;
* Komplexität im durchschnittlichen/typischen Fall&lt;br /&gt;
*: Der typische Fall ist die mittlere Komplexität des Algorithmus über alle möglichen Eingaben. Dazu muss man die Wahrscheinlichkeit jeder möglichen Eingabe kennen, und berechnet dann die mittlere Laufzeit über dieser Wahrscheinlichkeitsverteilung. Leider ist die Wahrscheinlichkeit der Eingaben oft nicht bekannt, so dass man geeignete Annahmen treffen muss. Bei Sortieralgorithmen können z.B. alle möglichen Permutationen des Eingabearrays als gleich wahrscheinlich angenommen werden, und der typische Fall ist dann die mittlere Komplexität über alle diese Eingaben. Oft hat man jedoch in der Praxis andere Wahrscheinlichkeitsverteilungen, z.B. sind die Daten oft &amp;quot;fast sortiert&amp;quot; (nur wenige Elemente sind an der falschen Stelle). Dann verhält sich der Algorithmus ebenfalls anders als vorhergesagt.&lt;br /&gt;
&lt;br /&gt;
Wir beschränken uns in dieser Vorlesung auf die Komplexität im ungünstigseten Fall. '''Exakte''' Formeln für Komplexität sind aber auch dann schwer zu gewinnen, wie das folgende Beispiel zeigt:&lt;br /&gt;
&lt;br /&gt;
=== Beispiele aus den Übungen (Gemessene Laufzeiten für Mergesort/Selectionsort) ===&lt;br /&gt;
&lt;br /&gt;
* Mergesort: &amp;lt;math&amp;gt;\frac{0,977N\log N}{\log 2} + 0,267N-4.39 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: andere Lösung: &amp;lt;math&amp;gt;1140 N\log(N) - 1819N + 6413 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* Selectionsort: &amp;lt;math&amp;gt;\frac{1}{2}N^2 - \frac{1}{2N} - 10^{-12} \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: andere Lösung: &amp;lt;math&amp;gt;1275N^2 - 116003^N + 11111144 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Aus diesen Formeln wird nicht offensichtlich, welcher Algorithmus besser ist.&lt;br /&gt;
Näherung: Betrachte nur '''sehr große Eingaben''' (meist sind alle Algorithmen schnell genug für kleine Eingaben). Dieses Vorgehen wird als '''Asymptotische Komplexität''' bezeichnet (N gegen unendlich).&lt;br /&gt;
&lt;br /&gt;
=== Asymptotische Komplexität am Beispiel Polynom ===&lt;br /&gt;
&lt;br /&gt;
Polynom: &amp;lt;math&amp;gt;a\,x^2+b\,x+c=p\!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;x \!&amp;lt;/math&amp;gt; sei die Eingabegröße, und wir betrachten die Entwicklung von &amp;lt;math&amp;gt;p \!&amp;lt;/math&amp;gt; in Abhängigkeit von &amp;lt;math&amp;gt;x \!&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;math&amp;gt;x=0 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=c \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x=1 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=a+b+c \!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x=1000 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p=1000000a+1000b+c \approx 1000000a\!&amp;lt;/math&amp;gt;&lt;br /&gt;
* &amp;lt;math&amp;gt;x \to \infty \!&amp;lt;/math&amp;gt;&lt;br /&gt;
*: &amp;lt;math&amp;gt;p \approx x^2a\!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für sehr große Eingaben verlieren also ''b'' und ''c'' immer mehr an Bedeutung, so dass am Ende nur noch ''a'' für die Komplexitätsbetrachtung wichtig ist.&lt;br /&gt;
&lt;br /&gt;
== Landau-Symbole ==&lt;br /&gt;
&lt;br /&gt;
Um die asymptotische Komplexität verschiedener Algorithmen miteinander vergleichen zu können, verwendet man die sogenannten [http://de.wikipedia.org/wiki/Landau-Symbole Landau-Symbole]. Das wichtigste Landau-Symbol ist &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;, mit dem man eine ''obere Schranke'' &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; für die Komplexität angeben kann. &lt;br /&gt;
&lt;br /&gt;
Schreibt man &amp;lt;math&amp;gt;f \in \Omega(g)&amp;lt;/math&amp;gt;, so stellt dies eine asymptotische ''untere Schranke'' für die Funktion f dar.&lt;br /&gt;
&lt;br /&gt;
Schließlich bedeutet &amp;lt;math&amp;gt;f \in \Theta(g)&amp;lt;/math&amp;gt;, dass die Funktion f genauso schnell wie die Funktion g wächst, das heißt man hat eine asymptotisch ''scharfe Schranke'' für f. Hierzu muss sowohl &amp;lt;math&amp;gt;f\in\mathcal{O}(g)&amp;lt;/math&amp;gt; als auch &amp;lt;math&amp;gt;f \in \Omega(g)&amp;lt;/math&amp;gt; erfüllt sein. &lt;br /&gt;
&lt;br /&gt;
Im nun folgenden soll auf die verschiedenen Landau-Symbole noch näher eingegeangen werden.&lt;br /&gt;
&lt;br /&gt;
===O-Notation===&lt;br /&gt;
&lt;br /&gt;
Intuitiv gilt: Für große N dominieren die am schnellsten wachsenden Terme einer Funktion. Die Notation &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; (sprich &amp;quot;f ist in O von g&amp;quot; oder &amp;quot;f ist von derselben Größenordnung wie g&amp;quot;) formalisiert eine solche Abschätzung der asymptotischen Komplexität der Funktion f von oben. &lt;br /&gt;
; Asymptotische Komplexität: Für zwei Funktionen f(x) und g(x) gilt&lt;br /&gt;
::&amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x))&amp;lt;/math&amp;gt;&lt;br /&gt;
: genau dann wenn es eine Konstante &amp;lt;math&amp;gt;c&amp;gt;0&amp;lt;/math&amp;gt; und ein Argument &amp;lt;math&amp;gt;x_0&amp;lt;/math&amp;gt; gibt, so dass &lt;br /&gt;
::&amp;lt;math&amp;gt;\forall x \ge x_0:\quad f(x) \le c\,g(x)&amp;lt;/math&amp;gt;.&lt;br /&gt;
:Die Menge &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; aller durch g(x) abschätzbaren Funktionen ist also formal definiert durch&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(g(x)) = \{ f(x)\ |\ \exists c&amp;gt;0: \forall x \ge x_0: 0 \le f(x) \le c\,g(x)\}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Idee hinter dieser Definition ist, dass g(x) eine wesentlich einfachere Funktion ist als f(x), die sich aber nach geeigneter Skalierung (Multiplikation mit c) und für große Argumente x im wesentlichen genauso wie f(x) verhält. Man kann deshalb in der Algorithmenanalyse f(x) durch g(x) ersetzen. &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x))&amp;lt;/math&amp;gt; spielt für Funktionen eine ähnliche Rolle wie der Operator &amp;amp;le; für Zahlen: Falls a &amp;amp;le; b gilt, kann bei einer Abschätzung von oben ebenfalls a durch b ersetzt werden.&lt;br /&gt;
&lt;br /&gt;
==== Ein einfaches Beispiel ====&lt;br /&gt;
&lt;br /&gt;
[[Image:Sqsqrt.png]]&lt;br /&gt;
&lt;br /&gt;
Rot = &amp;lt;math&amp;gt;x^2 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
Blau = &amp;lt;math&amp;gt;\sqrt{x} \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\sqrt{x} \in \mathcal{O}(x^2)\!&amp;lt;/math&amp;gt; weil &amp;lt;math&amp;gt;\sqrt{x} \le c\,x^2\!&amp;lt;/math&amp;gt; für alle &amp;lt;math&amp;gt;x \ge x_0 = 1 \!&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c = 1\!&amp;lt;/math&amp;gt;, oder auch für &amp;lt;math&amp;gt;x \ge x_0 = 4 \!&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c = 1/16&amp;lt;/math&amp;gt; (die Wahl von c und x&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; in der Definition von O(.) ist beliebig, solange die Bedingungen erfüllt sind).&lt;br /&gt;
&lt;br /&gt;
==== Komplexität bei kleinen Eingaben ==== &lt;br /&gt;
&lt;br /&gt;
Algorithmus 1: &amp;lt;math&amp;gt;\mathcal{O}(N^2) \!&amp;lt;/math&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Algorithmus 2: &amp;lt;math&amp;gt;\mathcal{O}(N\log{N}) \!&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus 2 ist schneller (von geringerer Komplexität) für große Eingaben, aber bei kleinen Eingaben (insbesondere, wenn der Algorithmus in einer Schleife immer wieder mit kleinen Eingaben aufgerufen wird) könnte Algorithmus 1 schneller sein, falls der in der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation verborgene konstante Faktor ''c'' bei Algorithmus 2 einen wesentlich größeren Wert hat als bei Algorithmus 1.&lt;br /&gt;
&lt;br /&gt;
==== Eigenschaften der O-Notation (Rechenregeln) ==== &lt;br /&gt;
&lt;br /&gt;
# Transitiv:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x)) \land g(x) \in \mathcal{O}(h(x)) \to f(x) \in \mathcal{O}(h(x)) \!&amp;lt;/math&amp;gt;           &lt;br /&gt;
# Additiv:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(h(x)) \land g(x) \in \mathcal{O}(h(x)) \to f(x) + g(x) \in \mathcal{O}(h(x)) \!&amp;lt;/math&amp;gt;           &lt;br /&gt;
# Für Monome gilt:&lt;br /&gt;
#: &amp;lt;math&amp;gt;x^k \in \mathcal{O}(x^k)&amp;lt;/math&amp;gt; und&lt;br /&gt;
#: &amp;lt;math&amp;gt;x^k \in \mathcal{O}(x^{k+j}), \forall j \ge 0 \!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Multiplikation mit einer Konstanten:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(g(x)) \to c\,f(x) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: andere Schreibweise:&lt;br /&gt;
#: &amp;lt;math&amp;gt;f(x) = c\,g(x) \to f(x) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Folgerung aus 3. und 4. für Polynome: &lt;br /&gt;
#: &amp;lt;math&amp;gt;a_0+a_1\,x + ... + a_n\,x^n \in \mathcal{O}(x^n)\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: Beispiel: &amp;lt;math&amp;gt;a\,x^2+b\,x+c \in \mathcal{O}(x^2)\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# Logarithmus:&lt;br /&gt;
#: &amp;lt;math&amp;gt;a, b &amp;gt; 1\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{b}{x})\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#: Die Basis des Logarithmus spielt also keine Rolle.&lt;br /&gt;
#: Beweis hierfür:&lt;br /&gt;
#:: &amp;lt;math&amp;gt;\log_{a}{x} = \frac{\log_{b}{x}}{\log_{b}{a}}\!&amp;lt;/math&amp;gt;&lt;br /&gt;
#:: Mit &amp;lt;math&amp;gt;c = 1 / \log_{b}{a}\,&amp;lt;/math&amp;gt; gilt: &amp;lt;math&amp;gt;\log_{a}{x} = c\,\log_{b}{x}\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#:: Wird hier die (zweite) Regel für Multiplikation mit einer Konstanten angewendet, fällt der konstante Faktor weg, also &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{b}{x})\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Insbesondere gilt auch &amp;lt;math&amp;gt;\log_{a}{x} \in \mathcal{O}(\log_{2}{x})\!&amp;lt;/math&amp;gt;, es kann also immer der 2er Logarithmus verwendet werden.&lt;br /&gt;
&lt;br /&gt;
==== O-Kalkül ==== &lt;br /&gt;
&lt;br /&gt;
Das O-Kalkül definiert wichtige Vereinfachungsregeln for Ausdrücke in O-Notation (Beweise: siehe Übungsaufgabe):&lt;br /&gt;
&lt;br /&gt;
# &amp;lt;math&amp;gt;f(x) \in \mathcal{O}(f(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# &amp;lt;math&amp;gt;\mathcal{O}(\mathcal{O}(f(x))) \in \mathcal{O}(f(x))\!&amp;lt;/math&amp;gt;&lt;br /&gt;
# &amp;lt;math&amp;gt;c\,\mathcal{O}(f(x)) \in \mathcal{O}(f(x))\,&amp;lt;/math&amp;gt; für jede Konstante ''c''&lt;br /&gt;
# &amp;lt;math&amp;gt;\mathcal{O}(f(x))+c \in \mathcal{O}(f(x))\,&amp;lt;/math&amp;gt; für jede Konstante ''c''&lt;br /&gt;
# Sequenzregel:&lt;br /&gt;
#: Wenn zwei nacheinander ausgeführte Programmteile die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt; bzw. &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; haben, gilt für beide gemeinsam:&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(f(x))&amp;lt;/math&amp;gt; falls &amp;lt;math&amp;gt;g(x) &amp;lt; \mathcal{O}(f(x))&amp;lt;/math&amp;gt; bzw.&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(g(x))\!&amp;lt;/math&amp;gt; falls &amp;lt;math&amp;gt;f(x) &amp;lt; \mathcal{O}(g(x))&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Informell schreibt man auch: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) + \mathcal{O}(g(x)) \in \mathcal{O}(max(f(x), g(x)))\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
# Schachtelungsregel bzw. Aufrufregel:&lt;br /&gt;
#: Wenn in einer geschachtelten Schleife die äußere Schleife die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt; hat, und die innere &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt;, gilt für beide gemeinsam:&lt;br /&gt;
#: &amp;lt;math&amp;gt;\mathcal{O}(f(x)) * \mathcal{O}(g(x)) \in \mathcal{O}(f(x) * g(x))\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
#: Gleiches gilt wenn eine Funktion &amp;lt;math&amp;gt;\mathcal{O}(f(x))&amp;lt;/math&amp;gt;-mal aufgerufen wird, und die Komplexität der Funktion selbst &amp;lt;math&amp;gt;\mathcal{O}(g(x))&amp;lt;/math&amp;gt; ist.&lt;br /&gt;
&lt;br /&gt;
;Beispiel für 5.: Beide Schleifen haben die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Dies gilt auch für ihre Hintereinanderausführung:&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          a[i] = i&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          print a[i]&lt;br /&gt;
;Beispiele für 6.: Beide Schleifen haben die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Ihre Verschachtelung hat daher die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N^2)&amp;lt;/math&amp;gt;. &lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          for j in range(N):&lt;br /&gt;
              a[i*N + j] = i+j&lt;br /&gt;
: Dies gilt ebenso, wenn statt der inneren Schleife eine Funktion mit Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt; ausgeführt wird:&lt;br /&gt;
      for i in range(N):&lt;br /&gt;
          a[i] = foo(i, N)  # &amp;lt;math&amp;gt;\mathrm{foo}(i, N) \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== O-Kalkül auf das Beispiel des Selectionsort angewandt ==== &lt;br /&gt;
&lt;br /&gt;
Selectionsort: Wir hatten gezeigt dass &amp;lt;math&amp;gt;f(N) = \frac{N^2}{2} - \frac{N}{2}&amp;lt;/math&amp;gt;. Nach der Regel für Polynome vereinfacht sich dies zu &amp;lt;math&amp;gt;f(N) \in \mathcal{O}\left(\frac{N^2}{2}\right) = \mathcal{O}(N^2)\!&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Alternativ via Schachtelungsregel:&lt;br /&gt;
: Die äußere Schleife wird (''N''-1)-mal durchlaufen: &amp;lt;math&amp;gt;N-1 \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
: Die innere Schleife wird (''N-i''-1)-mal durchlaufen. Das sind im Mittel ''N''/2 Durchläufe: &amp;lt;math&amp;gt;N/2 \in \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
: Zusammen: &amp;lt;math&amp;gt;\mathcal{O}(N)*\mathcal{O}(N) \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Nach beiden Vorgehensweisen kommen wir zur Schlussfolgerung, dass der Selectionsort die asymptotische Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N^2)\!&amp;lt;/math&amp;gt; besitzt.&lt;br /&gt;
&lt;br /&gt;
==== Zusammenhang zwischen Komplexität und Laufzeit ==== &lt;br /&gt;
&lt;br /&gt;
Wenn eine Operation 1ms dauert, erreichen Algorithmen verschiedener Komplexität folgende Leistungen (wobei angenommen wird, dass der in der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation verborgene konstante Faktor immer etwa gleich 1 ist):&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:left&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
|-&lt;br /&gt;
! Komplexität !! Operationen in 1s !! Operationen in 1min !! Operationen in 1h&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 1000 || 60.000 || 3.600.000&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N\log_2{N})&amp;lt;/math&amp;gt;&lt;br /&gt;
| 140 || 4895 || 204094&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 32 || 245 || 1898&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 10 || 39 || 153&lt;br /&gt;
|-&lt;br /&gt;
! &amp;lt;math&amp;gt;\mathcal{O}(2^N)&amp;lt;/math&amp;gt;&lt;br /&gt;
| 10 || 16 || 21&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Exponentielle Komplexität ==== &lt;br /&gt;
Der letzte Fall &amp;lt;math&amp;gt;\mathcal{O}(2^N)&amp;lt;/math&amp;gt; ist von exponentieller Komplexität. Das bedeutet, dass eine Verdopplung des Aufwands nur bewirkt, dass die maximale Problemgröße um eine Konstante wächst. Algorithmen mit exponentieller (oder noch höherer) Komplexität werden deshalb als '''ineffizient''' bezeichnet. Algorithmen mit höchstens polynomieller Komplexität gelten hingegen als effizient.&lt;br /&gt;
&lt;br /&gt;
In der Praxis sind allerdings auch polynomielle Algorithmen mit hohem Exponenten meist zu langsam. Als Faustregel kann man eine praktische Grenze von &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt; ansehen. Bei einer Komplexität von &amp;lt;math&amp;gt;\mathcal{O}(N^3)&amp;lt;/math&amp;gt; bewirkt ein verdoppelter Aufwand immer noch eine Steigerung der maximalen Problemgröße um den Faktor &amp;lt;math&amp;gt;\sqrt[3]{2}&amp;lt;/math&amp;gt; (also eine ''multiplikative'' Vergrößerung um ca. 25%, statt nur einer additiven Vergrößerung wie bei exponentieller Komplexität).&lt;br /&gt;
&lt;br /&gt;
===&amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;- Notation===&lt;br /&gt;
&lt;br /&gt;
Genauso wie &amp;lt;math&amp;gt;f \in \mathcal{O}(g)&amp;lt;/math&amp;gt; eine Art &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;-Operator für Funktionen ist, definiert &amp;lt;math&amp;gt;f \in \Omega(g) &amp;lt;/math&amp;gt; eine Abschätzung von unten, analog zum &amp;lt;math&amp;gt;\ge&amp;lt;/math&amp;gt;-Operator für Zahlen. Formal kann man &amp;lt;math&amp;gt;f(N) \in \Omega(g(N)) &amp;lt;/math&amp;gt; genau dann schreiben, falls es eine Konstante &amp;lt;math&amp;gt; c &amp;gt; 0 &amp;lt;/math&amp;gt; gibt, so dass &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; f(N) \ge c \cdot g(N) &amp;lt;/math&amp;gt; für &amp;lt;math&amp;gt; N  \ge N_0 &amp;lt;/math&amp;gt; &lt;br /&gt;
&lt;br /&gt;
gilt.&lt;br /&gt;
Man verwendet diese Notation also um abzuschätzen, wie groß der Aufwand (die Komplexität) für einen bestimmten Algorithmus ''mindestens'' ist und nicht ''höchstens'', was man mit der &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt; - Notation ausdrücken würde.&lt;br /&gt;
&lt;br /&gt;
Ein praktisches Beispiel für eine Anwendung der &amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;- Notation wäre die Fragestellung, ob es ''prinzipiell'' einen besseren Algorithmus für ein bestimmtes Problem gibt. Wie später im Abschnitt [[Suchen#Sortieren_als_Suchproblem|Sortieren als Suchproblem]] gezeigt wird, ist das Sortieren eines Arrays durch paarweise Vergleiche von Elementen immer mindestens von der Komplexität &amp;lt;math&amp;gt; \Omega(N\cdot \ln N) &amp;lt;/math&amp;gt;, was konkret bedeutet, dass kein Sortieralgorithmus, der nach diesem Prinzip arbeitet, jemals eine geringere Komplexität als beispielsweise Merge-Sort haben wird. Natürlich kann man den entsprechenden Sortieralgorithmus, also Merge-Sort zum Beispiel, unter Umständen noch optimieren, aber die Komplexität wird erhalten bleiben. Mit diesem Wissen kann man sich viel (vergebliche) Arbeit sparen.&lt;br /&gt;
&lt;br /&gt;
===&amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;- Notation===&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;math&amp;gt;f(N) \in \Theta(g(N))&amp;lt;/math&amp;gt; ist eine scharfe Abschätzung der asymptotischen Komplexität einer Funktion f. &lt;br /&gt;
&lt;br /&gt;
Damit dies gilt, muss &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; und ''gleichzeitig'' &amp;lt;math&amp;gt;f(N) \in \Omega(g(N))&amp;lt;/math&amp;gt; erfüllt sein.&lt;br /&gt;
&lt;br /&gt;
Dies ist natürlich auch die beste Abschätzung der asymptotischen Komplexität einer Funktion f. Formal bedeutet &amp;lt;math&amp;gt;f(N) \in \Theta(g(N))&amp;lt;/math&amp;gt; dass es zwei Konstanten &amp;lt;math&amp;gt; c_1 &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; c_2 &amp;lt;/math&amp;gt;, beide größer als Null, gibt, so dass für alle &amp;lt;math&amp;gt; N \geq N_0 &amp;lt;/math&amp;gt; gilt: &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; c_1 \cdot g(N) \leq f(N) \leq c_2 \cdot g(N) &amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
In der Praxis wird manchmal statt der &amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;-Notation auch dann die &amp;lt;math&amp;gt;\mathcal{O}&amp;lt;/math&amp;gt;-Notation benutzt, wenn eine scharfe Schranke ausgedrückt werden soll. Dies ist zwar formal nicht korrekt, aber man kann die intendierte Bedeutung meist aus dem Kontext erschließen.&lt;br /&gt;
&lt;br /&gt;
== Komplexitätsvergleich zweier Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
In diesem Abschnitt wollen wir der Frage nachgehen, wie ein formaler Beweis für die Behauptung &amp;lt;math&amp;gt; f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; geschehen kann. Hierbei werden zwei Beweismethoden vorgestellt werden, und zwar der '''Beweis über die Definition der Komplexität''' sowie der '''Beweis durch Dividieren'''.&lt;br /&gt;
&lt;br /&gt;
===Beweis über die Definition der asymptotischen Komplexität===&lt;br /&gt;
&lt;br /&gt;
Die Definition der asymptotischen Komplexität &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; war: &lt;br /&gt;
&lt;br /&gt;
Es gibt eine Konstante &amp;lt;math&amp;gt; c &amp;gt; 0 &amp;lt;/math&amp;gt;, so dass &amp;lt;math&amp;gt; f(N) \le c \cdot g(N) &amp;lt;/math&amp;gt; für &amp;lt;math&amp;gt; N  \ge N_0 &amp;lt;/math&amp;gt; erfüllt ist. &lt;br /&gt;
&lt;br /&gt;
Um also die die asymptotische Komplexität &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; zu beweisen, muss man die oben erwähnten Konstanten c und &amp;lt;math&amp;gt; N_0 &amp;lt;/math&amp;gt; finden, so dass &lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; f(N) \leq c \cdot g(N) &amp;lt;/math&amp;gt; für alle &amp;lt;math&amp;gt; N \ge N_0 &amp;lt;/math&amp;gt; erfüllt ist. &lt;br /&gt;
&lt;br /&gt;
Dies geschieht zweckmäßigerweise mit dem Beweisprinzip der ''vollständigen Induktion''. Hierbei ist zu zeigen, dass&lt;br /&gt;
# &amp;lt;math&amp;gt; f(N_0) \leq g(N_0) &amp;lt;/math&amp;gt; für die eine zu bestimmende Konstante &amp;lt;math&amp;gt; N_0 &amp;lt;/math&amp;gt; gilt (''Induktionsanfang'') und &lt;br /&gt;
# falls &amp;lt;math&amp;gt; f(N) \leq g(N) &amp;lt;/math&amp;gt;, dann auch &amp;lt;math&amp;gt; f(N+1) \leq g(N+1) &amp;lt;/math&amp;gt; (''Induktionsschritt'') gilt.&lt;br /&gt;
&lt;br /&gt;
===Beweis durch Dividieren===&lt;br /&gt;
&lt;br /&gt;
Hierbei wählt man eine Konstante c und zeigt, dass &amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f(N)}{c \cdot g(N)} \leq 1 &amp;lt;/math&amp;gt; gilt (für die O-Notation, bei &amp;amp;Omega;-Notation gilt entsprechend &amp;lt;math&amp;gt;\geq 1 &amp;lt;/math&amp;gt;). Man kann dies auch als alternative Definition der Komplexität verwenden.&lt;br /&gt;
&lt;br /&gt;
Als Beispiel betrachten wir die beiden Funktionen &amp;lt;math&amp;gt; f(N) = N \,\lg N &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; g(N) = N^2 &amp;lt;/math&amp;gt; und wollen zeigen, dass &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt; gilt. &lt;br /&gt;
&lt;br /&gt;
Als Konstante c wählen wir &amp;lt;math&amp;gt; c = 1 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f(N)}{g(N)} = \lim_{N \rightarrow \infty} \frac{\lg N}{N} = \frac{\infty}{\infty} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Unbestimmte Ausdrücke der Form &lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{x \rightarrow x_0} \frac{f(x)}{g(x)} &amp;lt;/math&amp;gt;,&lt;br /&gt;
in denen sowohl &amp;lt;math&amp;gt; f(x) &amp;lt;/math&amp;gt; als auch &amp;lt;math&amp;gt; g(x) &amp;lt;/math&amp;gt; mit &amp;lt;math&amp;gt; x \rightarrow x_0 &amp;lt;/math&amp;gt; gegen Null oder gegen Unendlich streben, kann man manchmal mit den Regeln von [http://de.wikipedia.org/wiki/L%27Hospital%27sche_Regel ''l'Hospital''] berechnen. Danach darf man die Funktionen f und g zur Berechnung des unbestimmten Ausdrucks durch ihre k-ten Ableitungen ersetzen:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{x \rightarrow x_0} \frac{f(x)}{g(x)} = \lim_{x \rightarrow x_0} \frac{f^{(k)}(x)}{g^{(k)}(x)} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In unserem Fall verwenden wir die erste Ableitung und erhalten:&lt;br /&gt;
&amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f'(x)}{g'(x)} = \lim_{N \rightarrow \infty} \frac{1/N}{1} \rightarrow 0 &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Damit wurde &amp;lt;math&amp;gt;f(N) \in \mathcal{O}(g(N))&amp;lt;/math&amp;gt;, also &amp;lt;math&amp;gt;N \lg N \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt; gezeigt.&lt;br /&gt;
&lt;br /&gt;
Man beachte hierbei, dass &amp;lt;math&amp;gt;N \lg N \in \mathcal{O}(N^2)&amp;lt;/math&amp;gt; keine enge Grenze für die Komplexität von &amp;lt;math&amp;gt;N \,\lg N&amp;lt;/math&amp;gt; darstellt, da der Grenzwert &amp;lt;math&amp;gt; \lim_{N \rightarrow \infty} \frac{f'(x)}{g'(x)}\, &amp;lt;/math&amp;gt; gegen 0 und nicht gegen eine von Null verschiedene Konstante strebt. In diesem Fall haben wir die Komplexität von &amp;lt;math&amp;gt;N \cdot \lg N &amp;lt;/math&amp;gt; also nur nach oben abschätzen können.&lt;br /&gt;
&lt;br /&gt;
===Beispiel für den Komplexitätsvergleich: Gleitender Mittelwert (Running Average)===&lt;br /&gt;
&lt;br /&gt;
Wir berechnen für ein gegebenes Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; einen gleitenden Mittelwert über &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; Elemente:&amp;lt;br/&amp;gt;&lt;br /&gt;
::&amp;lt;math&amp;gt;r_i = \frac{1}{k} \sum_{j=i-k+1}^i a_j&amp;lt;/math&amp;gt; &amp;lt;br/&amp;gt;&lt;br /&gt;
Das heisst, für jedes &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; mitteln wir die letzten &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; Elemente von &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; und schreiben das Ergebnis in &amp;lt;tt&amp;gt;r[i]&amp;lt;/tt&amp;gt;. Diese Operation ist z.B. bei Börsenkursen wichtig: Neben dem aktuellen Kurs für jeden Tag wird dort meist auch der gleitende Mittelwert der letzten 30 Tage sowie der letzten 200 Tage angegeben. In diesen Mittelwerten erkennt man besser die langfristige Tendenz, weil die täglichen Schwankungen herausgemittelt werden. Wir nehmen außerdem an, dass&lt;br /&gt;
* Array-Zugriff hat eine Komplexit&amp;amp;auml;t von O(1)&lt;br /&gt;
* &amp;lt;math&amp;gt;k \ll N&amp;lt;/math&amp;gt;, d.h. &amp;lt;math&amp;gt;N-k\approx N&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die beiden folgenden Algorithmen berechnen die Mittelwerte auf unterschiedliche Art. Der linke folgt der obigen Definition durch eine Summe, während der rechte inkrementell arbeitet: Man kann den Bereich der &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; letzten Werte als Fenster betrachten, das über das Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; geschoben wird. Schiebt man das Fenster ein Element weiter, fällt links ein Element heraus, und rechts kommt eins hinzu. Man muss also nicht jedes Mal die Summe neu berechnen, sondern kann den vorigen Wert aktualisieren. Wir werden sehen, dass dies Folgen für die Komplexität des Algorithmus hat.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;2&amp;quot; &lt;br /&gt;
|-&lt;br /&gt;
! Programmzeile&lt;br /&gt;
! Version 1: O(N * k)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
! Version 2: O(N)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
1.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
2.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
3.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
4.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k-1, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for i in range(k):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
5.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;for i in range(j-k+1, j+1):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[k-1] += a[i]&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
6.&lt;br /&gt;
|&lt;br /&gt;
:::: &amp;lt;tt&amp;gt;r[j] += a[i]&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
7.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] = (a[j] - a[j-k] + r[j-1])&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
8.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
9.&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
10.&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Wir zeigen unten dass Version 2 eine geringere Komplexit&amp;amp;auml;t besitzt, obwohl sie mehr Zeilen ben&amp;amp;ouml;tigt.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Wir haben in der Tabelle die Komplexität jeder Zeile für sich angegeben. Einfache Anweisungen (Berechnungen, Lese- und Schreibzugriffe auf das Array, Zuweiseungen) haben konstante Komplexität, die Komplexität des Schleifenkopfes allein (also der &amp;lt;tt&amp;gt;for&amp;lt;/tt&amp;gt;-Anweisung ohne den eingerückten Schleifenkörper) entspricht der Anzahl der Durchläufe. Wir müssen jetzt noch die Verschachtelung der Schleifen und die Nacheinanderausführung von Anweisungen berücksichtigen. &lt;br /&gt;
&lt;br /&gt;
====Berechnung der Komplexität von Version 1====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;small&amp;gt;(Wiederholung der Rechenregeln: siehe Abschnitt [[Effizienz#O-Notation|O-Notation]])&amp;lt;/small&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wir betrachten zunächst die innere Schleife (Zeilen 5 und 6 von Version 1):&lt;br /&gt;
&lt;br /&gt;
Der Schleifenkopf (Zeile 5) hat die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt;, weil die Schleife k-mal durchlaufen wird. Der Schleifenkörper (Zeile 6) hat die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;. Nach der Verschachtelungsregel müssen wir die beiden Komplexitäten multiplizieren, und es ergibt sich:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(k)\cdot\mathcal{O}(1) = \mathcal{O}(k\cdot 1)=\mathcal{O}(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wir betrachten nun die äußere Schleife. Der Schleifenkopf (Zeile 4) wird (N-k)-mal durchlaufen und hat somit eine Komplexität von &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Der Schleifenkörper (Zeilen 5 bis 7) besteht aus der inneren Schleife (Zeilen 5 und 6) mit der gerade berechneten Komplexität &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt; sowie einer einfachen Anweisung (Zeile 7) mit Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt;. Nach der Sequenzregel wird die Komplexität des Schleifenkörpers durch Addition berechnet:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(k)+\mathcal{O}(1) = \mathcal{O}(\max(k,1)) = \mathcal{O}(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexität der gesamten äußeren Schleife erhalten wir nach der Verschachtelungsregel wieder durch multiplizieren:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)\cdot\mathcal{O}(k) = \mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die übrigen Schritte des Algorithmus werden einfach nacheinander ausgeführt, so dass sie ebenfalls nach der Sequenzregel behandelt werden. Wir erhalten&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)+\mathcal{O}(1)+\mathcal{O}(N\cdot k)+\mathcal{O}(1) = \mathcal{O}(\max(N,1,N\cdot k,1)) = \mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Der gesamte Algorithmus hat also die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N\cdot k)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Berechnung der Komplexität von Version 2====&lt;br /&gt;
&lt;br /&gt;
Hier gibt es nur einfache Schleifen ohne Verschachtelung. Da der Schleifenkörper jeder Schleife nur einfache Anweisungen der Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt; enthält, ergibt sich die Komplexität der Schleifen nach der Verschachtelungsregel als&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(X)\cdot\mathcal{O}(1) = \mathcal{O}(X\cdot 1)=\mathcal{O}(X)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
wobei &amp;lt;math&amp;gt;\mathcal{O}(X)&amp;lt;/math&amp;gt; die Komplexität des jeweiligen Schleifenkopfes ist. Wir erhalten also für Zeilen 4 und 5: &amp;lt;math&amp;gt;\mathcal{O}(k)&amp;lt;/math&amp;gt;, Zeilen 6 und 7: &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;, Zeilen 8 und 9: &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;. Die Hintereinanderausführung wird nach der Sequenzregel behandelt:&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;\mathcal{O}(N)+\mathcal{O}(1)+\mathcal{O}(k)+\mathcal{O}(N)+\mathcal{O}(N)+\mathcal{O}(1) = \mathcal{O}(\max(N,1,k,N,N,1)) = \mathcal{O}(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Dieser Algorithmus hat also nur die Komplexität &amp;lt;math&amp;gt;\mathcal{O}(N)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Fazit====&lt;br /&gt;
&lt;br /&gt;
Obwohl Version 2 mehr Schritte benötigt hat sie eine geringere Komplexität, da die for-Schleifen nicht wie bei Version 1 verschachtelt/untergeordnet sind. Bei verschachtelten for-Schleifen muss die Multiplikationsregel angewendet werden &amp;amp;rarr; höhere Komplexität.&lt;br /&gt;
&lt;br /&gt;
Die gerade berechnete Komplexität gilt aber &amp;lt;u&amp;gt;nur&amp;lt;/u&amp;gt; unter der Annahme, dass Array-Zugriffe konstante Komplexität &amp;lt;math&amp;gt;\mathcal{O}(1)&amp;lt;/math&amp;gt; besitzen. Wenn dies nicht der Fall ist, kann sich die Komplexität des Algorithmus drastisch verschlechtern.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|Allgemein gilt:&amp;lt;br/&amp;gt;&lt;br /&gt;
Algorithmen-Analysen beruhen auf der Annahme, dass Zugriffe auf die Daten optimal schnell sind, dass heißt, dass die für den jeweiligen Algorithmus am besten geeignete Datenstruktur verwendetet wird.&amp;lt;br /&amp;gt; &amp;amp;rarr; Ansonsten: Komplexitätsverschlechterung!&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Beispiel für eine Verschlechterung der Komplexität durch Verwendung einer nicht optimalen Datenstruktur====&lt;br /&gt;
&lt;br /&gt;
Wir verwende im Mittelwert-Algorithmus eine verkettete Liste anstelle des Eingabe-Arrays a&amp;lt;/u&amp;gt;. Wir benötigen dazu eine Funktion, die das j-te Element der Liste zurückgibt. Wie üblich ist die Liste mit Hilfe einer Knotenklasse implementiert:&lt;br /&gt;
      class Node:&lt;br /&gt;
          def __init__(self, data):&lt;br /&gt;
              self.data = data&lt;br /&gt;
              self.next = None&lt;br /&gt;
&lt;br /&gt;
Die Listenklasse selbst hat ein Feld &amp;lt;tt&amp;gt;head&amp;lt;/tt&amp;gt;, das eine Referenz auf den ersten Knoten speichert, und jeder Knoten speichert im Feld &amp;lt;tt&amp;gt;next&amp;lt;/tt&amp;gt; eine Referenz auf seinen Nachfolger. Um zum j-ten Element zu gelangen, muss man die Liste sequenziell durchlaufen&lt;br /&gt;
      def get_jth(list, j):&lt;br /&gt;
           r = list.head&lt;br /&gt;
           while j &amp;gt; 0:&lt;br /&gt;
               r = r.head&lt;br /&gt;
               j -= 1&lt;br /&gt;
           return r.data&lt;br /&gt;
Die Komplexität dieser Funktion ist offensichtlich &amp;lt;math&amp;gt;\mathcal{O}(j)&amp;lt;/math&amp;gt; (Komplexitätsberechnung wie oben). Wir setzen jetzt bei Version 1 des Mittelwert-Algorithmus diese Funktion in Zeile 6 anstelle des Indexzugriffs &amp;lt;tt&amp;gt;a[i]&amp;lt;/tt&amp;gt; ein (nur in dieser Zeile wird auf die Elemente des Arrays zugegriffen). Wir erhalten folgende Implementation (die Änderungen sind rot markiert):&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;2&amp;quot; &lt;br /&gt;
|-&lt;br /&gt;
! Programmzeile&lt;br /&gt;
! Version 1 mit Liste: O(N * k)&lt;br /&gt;
! Komplexit&amp;amp;auml;t&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
1.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;r = [0] * len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(N)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
2.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;if k &amp;gt; len(a):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
3.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;raise RuntimeError (&amp;quot;k zu gro&amp;amp;szlig;&amp;quot;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
4.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;for j in range(k-1, len(a)):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;center&amp;gt;O(N-k+1) = '''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
5.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;for i in range(j-k+1, j+1):&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(k)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
6.&lt;br /&gt;
|&lt;br /&gt;
:::: &amp;lt;tt&amp;gt;r[j] += &amp;lt;font color=red&amp;gt;get_jth(a, i)&amp;lt;/font&amp;gt;&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;&amp;lt;font color=red&amp;gt;O(i)&amp;lt;/font&amp;gt;&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
7.&lt;br /&gt;
|&lt;br /&gt;
:: &amp;lt;tt&amp;gt;r[j] /= float(k)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
8.&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;tt&amp;gt;return r&amp;lt;/tt&amp;gt;&lt;br /&gt;
|&lt;br /&gt;
'''&amp;lt;center&amp;gt;O(1)&amp;lt;/center&amp;gt;'''&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Der Aufruf der Funktion &amp;lt;tt&amp;gt;get_jth&amp;lt;/tt&amp;gt; ist jetzt gleichbedeutend mit einer dreifach verschachtelten Schleife (weil &amp;lt;tt&amp;gt;get_jth&amp;lt;/tt&amp;gt; ja eine zusatzliche Schleife enthält). Die Anzahl der Operationen in Zeile 4 bis 6 ist jetzt&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;f(N,k)=\sum_{j=k-1}^{N-1}\,\sum_{i=j-k+1}^j\,\mathcal{O}(i)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
wobei das &amp;lt;math&amp;gt;\mathcal{O}(i)&amp;lt;/math&amp;gt; die neue Schleife durch Verwendung der Liste repräsentiert. Mit Mathematica-Hilfe [http://www.wolfram.com/] lässt sich diese Summe exakt ausrechnen&lt;br /&gt;
&lt;br /&gt;
::&amp;lt;math&amp;gt;f(N,k)=\frac{1}{2}(k N^2-k^2 N+k^2-k)\in \mathcal{O}(k N^2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexitätsberechnung erfolgte dabei nach der Regel für Polynome unter Beachtung von &amp;lt;math&amp;gt;k \ll N&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
====Fazit:====&lt;br /&gt;
&lt;br /&gt;
Die Komplexität von Version 1 mit einer verketteten Liste wäre O(N&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; * k)&lt;br /&gt;
'''&amp;amp;rarr; Die richtige Datenstruktur ist wichtig, da es sonst zu einer Komplexitätsverschlechterung kommen kann!'''&lt;br /&gt;
&lt;br /&gt;
Auf Version 2 unseres Running Average-Beispiels hätte eine verkettete Liste allerdings keine Auswirkungen, da die inkrementelle Berechnung der Summen in Zeile 7 weiterhin möglich ist (bei geschickter Implementation!) und somit Version 2 immer noch eine Komplexität von O(N) hätte.&lt;br /&gt;
&lt;br /&gt;
==Amortisierte Komplexität==&lt;br /&gt;
&lt;br /&gt;
Bis jetzt wurde die Komplexität nur im schlechtesten Fall (Worst Case) betrachtet. Bei einigen Algorithmen schwankt die Komplexität im schlechtesten Fall jedoch, wenn man die ungünstige Operation mehrmals hintereinander ausführt. Die amortisierte Komplexität beschäftigt sich mit der durchschnittlichen Komplexität über viele Aufrufe der ungünstigsten Operation.&lt;br /&gt;
&lt;br /&gt;
Zum weiter Lesen: [[http://de.wikipedia.org/wiki/Amortisierte_Laufzeitanalyse Wikipedia: Amortisierte Laufzeitanalyse]]&lt;br /&gt;
&lt;br /&gt;
===Beispiel: Inkrementieren von Binärzahlen===&lt;br /&gt;
&lt;br /&gt;
Frage: Angenommen, das Umdrehen eines Bits einer Binärzahl verursacht Kosten von 1 Einheit. Wir erzeugen die Folge der natürlichen Zahlen durch sukzessives Inkrementieren, von Null beginnend. Bei jeder Inkrementierung werden einige Bits verändert, aber diese Zahl (und damit die Kosten der Inkrementierungen) ''schwanken'' sehr stark. Wir fragen jetzt, was eine Inkrementierung im Durchschnitt kostet?&lt;br /&gt;
&lt;br /&gt;
Um diese Durchschnittskosten zu berechnen, bezahlen wir bei jeder Inkrementierung 2 Einheiten. Wenn davon nach Abzug der Kosten der jeweiligen Operation noch etwas übrig bleibt, wird der Rest dem  Guthaben zugeschrieben. Umgekehrt wird ein eventueller Fehlbetrag (wenn eine Inkrementierung mehr als 2 Bits umdreht) aus dem Guthaben gedeckt. Dadurch werden die ansonsten großen Schwankungen der Kosten ausgeglichen:&lt;br /&gt;
:: Kosten &amp;lt; Einzahlung &amp;amp;rarr; es wird gespart&lt;br /&gt;
:: Kosten = Einzahlung &amp;amp;rarr; Guthaben bleibt unverändert&lt;br /&gt;
:: Kosten &amp;gt; Einzahlung &amp;amp;rarr; Guthaben wird für die Kosten verbraucht&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
!Schritte&lt;br /&gt;
!Zahlen&lt;br /&gt;
!Kosten &amp;lt;br/&amp;gt;&lt;br /&gt;
(Anzahl der geänderten Bits)&lt;br /&gt;
! Einzahlung&lt;br /&gt;
!Guthaben =&amp;lt;br/&amp;gt;&lt;br /&gt;
altes Guthaben + Einzahlung - Kosten&lt;br /&gt;
|-&lt;br /&gt;
|1.&lt;br /&gt;
|0000&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|2.&lt;br /&gt;
|000&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|2&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|3.&lt;br /&gt;
|0001&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|4.&lt;br /&gt;
|00&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|3&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|5.&lt;br /&gt;
|0010&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|6.&lt;br /&gt;
|001&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|2&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''2'''&lt;br /&gt;
|-&lt;br /&gt;
|7.&lt;br /&gt;
|0011&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|1&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''3'''&lt;br /&gt;
|-&lt;br /&gt;
|8.&lt;br /&gt;
|0&amp;lt;u&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;1000&amp;lt;/span&amp;gt;&amp;lt;/u&amp;gt;&lt;br /&gt;
|4&lt;br /&gt;
|'''2'''&lt;br /&gt;
|'''1'''&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Die Kosten ergeben sich aus der Anzahl der Ziffern die von 1 nach 0, bzw. von 0 nach 1 verändert werden&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Rechnung:&amp;lt;/u&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Schritt: Kosten: 1 &amp;lt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Schritt: Kosten: 2 = Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird nicht gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; Guthaben bleibt so wie es ist &amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Schritt: Kosten: 1 &amp;lt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird gespart&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. Schritt: Kosten: 3 &amp;gt; Einzahlung: 2&amp;lt;br /&amp;gt;&lt;br /&gt;
:: &amp;amp;rarr; es wird eine 1 vom Guthaben genommen um die Kosten zu zahlen&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
usw.&lt;br /&gt;
&lt;br /&gt;
Man erkennt, dass vor teuren Operation (Wechsel von 3 auf 4 bzw. von 7 auf 8) genügend Guthaben angespart wurde, um die Kosten zu decken. Das Guthaben geht bei diesen Operationen immer wieder auf 1 zurück, aber es wird nie vollständig verbraucht (Dies kann natürlich auch mathematisch exakt bewiesen werden, wie wir es unten am Beispiel des dynamische Arrays zeigen). Wir schließen daraus, dass die durchschnittlichen oder '''amortisierten Kosten''' einer Inkrementierungsoperation gleich 2 sind.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Zum weiter Lesen: [[http://de.wikipedia.org/wiki/Account-Methode Wikipedia Account-Methode]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
====Fazit====&lt;br /&gt;
Die amortisierte Komplexität beschäftigt sich mit dem Durchschnitt aller Operation im ungünstigsten Fall. Operationen mit hohen Kosten, die aber nur selten ausgeführt werden, fallen bei der amortisierten Komplexität nicht so ins Gewicht. Bei Algorithmen, die gelegentlich eine &amp;quot;teure&amp;quot; Operation benutzen, ansonsten jedoch &amp;quot;billigen&amp;quot; Operationen aufrufen, kann die amortisierte Komplexität niedriger sein als die Komplexität im schlechtesten (Einzel-)Fall.&lt;br /&gt;
&lt;br /&gt;
In unserem Beispiel fallen die teuren Einzelschritte (z.B. 4. und 8. Schritt) bei den amortisierten Kosten nicht so ins Gewicht, da wir die Kosten aus unserem Guthaben mitbezahlen können. Das Guthaben ist immer groß genug, weil jeder zweite Aufruf eine billige Operation ist, die nur ein Bit umdreht und somit das Ansparen ermöglichen. Diese Betrachtung zeigt, dass die amortisierte (d.h. durchschnittliche) Komplexität des Algoithmus niedriger (nämlich konstant) ist als die Komplexität im schlechtesten Fall.&lt;br /&gt;
&lt;br /&gt;
===Anwendung: Dynamisches Array===&lt;br /&gt;
&lt;br /&gt;
Ein dynamisches Array hat die Eigenschaft, dass man effizient am Ende des Arrays neue Elemente anfügen kann, indem man die Länge des Arrays entsprechend vergrößert (siehe Übung 1). Die Analyse der amortisierten Komplexität der Anfüge-Operation zeigt uns, wie man das Vergrößern des Arrays richtig implementiert, damit die Operation wirklich effizient abläuft.&lt;br /&gt;
&lt;br /&gt;
==== Ineffiziente naive Lösung ====&lt;br /&gt;
&lt;br /&gt;
Wenn wir an ein Array ein Element anhängen wollen, müssen wir neuen Speicher allokieren, der die gewünschte Länge hat. Die Werte aus dem alten Array müssen dann in den neuen Speicher umkopiert werden. Danach kann das neue Element hinten angefügt werden, weil wir im neuen Array bereits Speicher für dieses Element reserviert haben. Bei der naiven Implementation des dynamischen Arrays wiederholt man dies bei jeder Anfügeoperation. Für die Analyse nehmen wir an, dass das Kopieren eines Elements konstante Zeit O(1) erfordert, ebenso das Einfügen eines neuen Elements auf in eine noch unbenutzte Speicherposition.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Naives Anhängen eines weiteren Elements an ein Array:&amp;lt;/u&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; align=&amp;quot;right&amp;quot;&lt;br /&gt;
!Schritte&lt;br /&gt;
|'''Array'''&lt;br /&gt;
&amp;lt;small&amp;gt;(wie es nach jedem Schritt aussieht)&amp;lt;/small&amp;gt;&lt;br /&gt;
!Komplexität&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;center&amp;gt;altes Array (N=4)&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|1. neuer Speicher für&amp;lt;br&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;(N+1) Elemente&lt;br /&gt;
|&amp;lt;center&amp;gt;[None,None,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;O(N+1) = '''O(N)'''&amp;lt;/center&amp;gt;(wenn der Speicher initialisiert wird&amp;lt;br&amp;gt;(hier auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;), sonst O(1))&lt;br /&gt;
|-&lt;br /&gt;
|2. Kopieren &lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;'''O(N)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|3. append von &amp;quot;x&amp;quot;&lt;br /&gt;
|&amp;lt;center&amp;gt;[0,1,2,3,'x']&amp;lt;/center&amp;gt;&lt;br /&gt;
|&amp;lt;center&amp;gt;'''O(1)'''&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
altesArray = [0,1,2,3]&amp;lt;br /&amp;gt;&lt;br /&gt;
altesArray.append('x')&lt;br /&gt;
&lt;br /&gt;
1. Es wird ein neues Array der Größe N+1 erzeugt&amp;lt;br /&amp;gt;&lt;br /&gt;
2. Die N Datenelemente aus dem alten Array werden in das neue Array kopiert&amp;lt;small&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Das sind N Operationen der Komplexität O(1), also ein Gesamtaufwand von O(N).&amp;lt;/small&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
3. 'x' wird mit Aufwand O(1) an die letzte Stelle des neuen Arrays geschrieben&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Additionsregel:&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
O(N) + O(1) &amp;amp;isin; O(N)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Folgerung:&amp;lt;/b&amp;gt; &lt;br /&gt;
&lt;br /&gt;
Bei der naiven Methode erfordert jede Anfügung einen Aufwand O(N) (wobei N die derzeitige Arraygröße ist). Dies ist nicht effizient.&lt;br /&gt;
&lt;br /&gt;
====Effiziente Lösung durch Verdoppeln der Kapazität====&lt;br /&gt;
&lt;br /&gt;
Offensichtlich kommt man nicht darum heraum, den Inhalt des alten Arrays zu kopieren, wenn der allokierte Speicher voll ist. Der Trick für die effiziente Implementation der Anfügeoperation besteht darin, das Kopieren so selten wie möglich durchzuführen, also nicht wie in der naiven Lösung bei jeder Anfügeoperation. Hier kommt die amortisierte Komplexität ins Spiel: Ab und zu gibt es eine teure Anfügeoperation (wenn nämlich kopiert werden muss), aber wenn man den durchschnittlichen Aufwand über viele Anfügungen betrachtet, ist die Operation effizient. Der teure Fall wird sozusagen &amp;quot;herausgemittelt&amp;quot;. &lt;br /&gt;
&lt;br /&gt;
Um nur selten kopieren zu müssen, werden beim dynamischen Array mehr Speicherelemente reserviert als zur Zeit benötigt werden (in der naiven Lösung wurde dagegen immer nur Speicher für ein einziges neues Element reserviert). Wir unterscheiden deshalb &lt;br /&gt;
&lt;br /&gt;
:&amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; = Anzahl der allokierten Speicherzellen, d.h. der möglichen Elemente, die in das Array passen&amp;lt;br /&amp;gt;&lt;br /&gt;
:&amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt; = Anzahl der Elemente, die im Array zur Zeit gespeichert sind&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Daten selbst werden in einem statischen Array gespeichert:&lt;br /&gt;
:&amp;lt;tt&amp;gt;data&amp;lt;/tt&amp;gt; = statisches Array der Größe &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die folgende intuitive Abschätzung zeigt, dass es sinnvoll ist, die Größe des allokierten Speichers jeweils zu verdoppeln. Wir starten bei einem Array der Größe &amp;lt;tt&amp;gt;size = capacity&amp;lt;/tt&amp;gt; = N. Da der verfügbare Speicher voll ist, müssen wir bei der nächsten Anfügung die N vorhandenen Elemente in ein neues Array der Länge &amp;lt;tt&amp;gt;new_capacity&amp;lt;/tt&amp;gt; kopieren (Aufwand &amp;lt;math&amp;gt;N\cdot O(1)&amp;lt;/math&amp;gt;). Danach können wir K Elemente billig einfügen (Aufwand &amp;lt;math&amp;gt;K\cdot O(1)&amp;lt;/math&amp;gt;), wobei &lt;br /&gt;
:K = &amp;lt;tt&amp;gt;new_capacity - capacity&amp;lt;/tt&amp;gt; &lt;br /&gt;
die Anzahl der nach dem Kopieren noch unbenutzen Speicherzellen ist. Der durchschnittliche Aufwand für diese K Einfügungen ist somit&lt;br /&gt;
:&amp;lt;math&amp;gt;\bar T = \frac{N \cdot O(1) + K \cdot O(1)}{K}=\frac{N+K}{K}\cdot O(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Damit die mittlere Zeit in O(1) sein kann, muss der Quotient &amp;lt;math&amp;gt;(N+K)/K&amp;lt;/math&amp;gt; eine Konstante sein. Wir setzen &amp;lt;math&amp;gt;K = a N&amp;lt;/math&amp;gt; und erhalten:&lt;br /&gt;
:&amp;lt;math&amp;gt;\bar T = \frac{(a+1)N}{a N}\cdot O(1)=\frac{a+1}{a}\cdot O(1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Der amortisierte Aufwand über K Einfügungen ist also konstant, wenn &amp;lt;math&amp;gt;a&amp;lt;/math&amp;gt; eine (kleine) von N unabhängige Zahl ist. Typischerweise wählt man &lt;br /&gt;
:&amp;lt;math&amp;gt;a = 1&amp;lt;/math&amp;gt;&lt;br /&gt;
und mit &amp;lt;math&amp;gt;K = 1\cdot N&amp;lt;/math&amp;gt; ergibt sich&lt;br /&gt;
:&amp;lt;tt&amp;gt;new_capacity = capacity&amp;lt;/tt&amp;gt; + N = &amp;lt;tt&amp;gt;2 * capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Vorgehensweise beim Zufügen eines neuen Elements im Fall &amp;lt;tt&amp;gt;size == capacity&amp;lt;/tt&amp;gt; ist also&lt;br /&gt;
* capacity wird verdoppelt&amp;lt;br /&amp;gt;&lt;br /&gt;
: &amp;lt;tt&amp;gt;neue capacity = 2 * alte capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
: (allgemein genügt es auch, wenn capacity um einen bestimmten Prozentsatz vergrößert wird, &lt;br /&gt;
:: &amp;lt;tt&amp;gt;neue capacity = alte capacity * c&amp;lt;/tt&amp;gt; &lt;br /&gt;
: mit c &amp;gt; 1, z.B. c = 1.2, das entspricht oben der Wahl &amp;lt;math&amp;gt;a = 0.2&amp;lt;/math&amp;gt;)&lt;br /&gt;
* ein neues statisches Array der Größe 'neue capacity' wird erzeugt&lt;br /&gt;
* das alte Array wird ins neue kopiert und danach freigegeben&lt;br /&gt;
* das anzufügende Element wird ins neue Array eingefügt&lt;br /&gt;
Umgekehrt geht man beim Entfernen des ''letzten'' Array-Elements vor. Normalerweise überschreibt man einfach das letzte Element mit &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; und dekrementiert &amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt;. Wird dadurch das Array zu klein (üblicherweise &amp;lt;tt&amp;gt;size &amp;amp;lt; capacity / 4&amp;lt;/tt&amp;gt;), wird die Kapazität halbiert, genauer:&lt;br /&gt;
* ein neues Array mit &amp;lt;br/&amp;gt;&lt;br /&gt;
: &amp;lt;tt&amp;gt;neue capacity = alte capacity / 2 &amp;lt;/tt&amp;gt;&lt;br /&gt;
: wird angelegt (bzw. mit&lt;br /&gt;
:: &amp;lt;tt&amp;gt;neue capacity = alte capacity / c &amp;lt;/tt&amp;gt;&lt;br /&gt;
: wenn ein anderer Vergrößerungsfaktor verwendet wird)&lt;br /&gt;
* das alte Array wird ins neue kopiert und danach freigegeben&lt;br /&gt;
&lt;br /&gt;
'''Folge:''' Die Kosten für das Vergrößern/Verkleinern der Kapazität werden amortisiert über viele Einfügungen, die kein Vergrößern erfordern. Die Operation &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; besitzt amortisierte Komplexität O(1). Im folgenden Abschnitt zeigen wir dies mathematisch exakt mit der Potentialmethode.&lt;br /&gt;
&lt;br /&gt;
====Komplexitätsanalyse des dynamischen Arrays mit Potentialmethode====&lt;br /&gt;
&lt;br /&gt;
Durchschnitt der Gesamtkosten für N-maliges append = &amp;lt;math&amp;gt;\frac{1}{N} \sum_{i = 1}^N Kosten(i)&amp;lt;/math&amp;gt;. Zur Analyse der amortisierten Komplexität wird ein Potential&amp;lt;br/&amp;gt;&lt;br /&gt;
::&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2*size&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; - capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
eingeführt, wobei das Array nach dem i-ten Einfüge-Schritt die Größe size&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; und die Kapizität capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; hat. Wir nehmen vereinfachend an, dass es keine Löschoperationen gibt. Dann gilt nach dem i-ten Schritt jeweils&lt;br /&gt;
::&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2*i - capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Fall 1: Array ist nicht voll&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Es wird kein Umkopieren benötigt, da das Array noch nicht voll ist&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; size&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; &amp;lt; capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; == capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Kosten: 1 (für Einfügen des neuen Elements)&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial vor append: &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = 2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial nach append: &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
amortisierte Kosten = Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Phi;&amp;lt;sub&amp;gt;(i)&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;(i-1)&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::                 = 1            + (2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;)        - [2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;]&lt;br /&gt;
:::::                 = 1            + 2i - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;         - 2i + 2 + capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::                 = 1            + &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; - &amp;lt;del&amp;gt;capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;/del&amp;gt; - &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; + 2 + &amp;lt;del&amp;gt;capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;/del&amp;gt;&lt;br /&gt;
:::::                 = 1 + 2&lt;br /&gt;
:::::                 = 3 = O(1) &amp;amp;rarr; konstant&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;Fall 2: Array ist voll&amp;lt;/u&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Vor dem i-ten append muss umkopiert werden&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; size&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; == capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; == i-1&amp;lt;br/&amp;gt; &lt;br /&gt;
&amp;amp;rarr; Allokieren eines neuen statischen Arrays mit verdoppelter Kapazität notwendig, also capacity&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; == 2*capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Kosten: (i-1) + 1 (für Umkopieren und Einfügen des neuen Elements)&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial vor append =  &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = 2(i - 1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
Potenzial nach append =  &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; =  2i - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
amortisierte Kosten = Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Phi;&amp;lt;sub&amp;gt;(i)&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;(i-1)&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = ((i - 1) + 1) + 2i - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; - [2(i-1) - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;]&lt;br /&gt;
:::::               = i + &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; - 2 capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; - &amp;lt;del&amp;gt;2i&amp;lt;/del&amp;gt; + 2 - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = i + 2 - capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
:::::               = i + 2 - (i - 1) &amp;lt;small&amp;gt;(da capacity&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt; = i-1)&amp;lt;/small&amp;gt;&lt;br /&gt;
:::::               = 3 = O(1) &amp;amp;rarr; konstant            &lt;br /&gt;
&lt;br /&gt;
'''Damit wurde bewiesen, dass die Operation &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; beim dynamischen Array eine amortisierte Komplexität von 3 Einheiten hat, also &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt; &amp;amp;isin; O(1)'''. Diese Operation kann deshalb gefahrlos in der inneren Schleife eines Algorithmus benutzt werden.&lt;br /&gt;
&lt;br /&gt;
==== Beispiel für 9 Einfügeoperationen ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot;&lt;br /&gt;
!Array&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;lt;small&amp;gt;(wie es aussehen könnte)&amp;lt;/small&amp;gt;&lt;br /&gt;
!size&lt;br /&gt;
!capacity&lt;br /&gt;
!Kosten für append&amp;lt;br /&amp;gt;(einschließlich Umkopieren)&lt;br /&gt;
!Summe Kosten&lt;br /&gt;
!Durchschnittskosten&lt;br /&gt;
!&amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = 2 * size - capacity&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;lt;small&amp;gt;(i = size)&amp;lt;/small&amp;gt;&lt;br /&gt;
!Potenzialdifferenz&amp;lt;br /&amp;gt;&lt;br /&gt;
&amp;amp;Delta; &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; = &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; - &amp;amp;Phi;&amp;lt;sub&amp;gt;i-1&amp;lt;/sub&amp;gt;&lt;br /&gt;
!amortisierte Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&amp;lt;br /&amp;gt;&lt;br /&gt;
= Kosten&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; + &amp;amp;Delta; &amp;amp;Phi;&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;0&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3/2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6/3&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;0&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7/4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;5&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;12&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;12/5&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;13&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;13/6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;4&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;14&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;14/7&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,h]&amp;lt;/center&amp;gt;&amp;lt;center&amp;gt;&amp;lt;span style=&amp;quot;color:#00BFFF;&amp;quot;&amp;gt;Array ist voll!&amp;lt;/span&amp;gt;&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;15&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;15/8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;center&amp;gt;[a,b,c,d,e,f,g,h,j,None,None,None,&amp;lt;br /&amp;gt;&lt;br /&gt;
None,None,None,None]&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;9&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;16&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;8 + 1&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;24&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;24/9&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;2&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;-6&amp;lt;/center&amp;gt;&lt;br /&gt;
| &amp;lt;center&amp;gt;3&amp;lt;/center&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Die durchschnittlichen Kosten betragen stets etwa 2 Einheiten, schwanken allerdings so, dass nicht unmittelbar ersichtlich ist, ob dies für sämtliche Einfügeoperationen gilt. Die amortisierte Komplexität, die mit Hilfe des Potentials berechnet wird, ist hingegen konstant 3, wie auch im obigen Beweis für alle Einfügeoperationen allgemein gezeigt wurde.&lt;br /&gt;
&lt;br /&gt;
[[Suchen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Main_Page&amp;diff=5414</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Main_Page&amp;diff=5414"/>
				<updated>2012-07-27T10:32:28Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Übungsaufgaben */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Vorlesung Algorithmen und Datenstrukturen ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Dr. Ullrich Köthe, Universität Heidelberg, Sommersemester 2012&lt;br /&gt;
&lt;br /&gt;
Die Vorlesung findet '''dienstags''' und '''donnerstags''' jeweils um 14:15 Uhr in INF 227 (KIP), HS 2 statt. &lt;br /&gt;
&lt;br /&gt;
=== Klausur und Nachprüfung ===&lt;br /&gt;
&lt;br /&gt;
Die '''Abschlussklausur''' findet am Dienstag, dem 31.7.2012 von 10:00 bis 12:00 Uhr im HS 1 in INF 306 statt. Zur Klausur wird zugelassen, wer mindestens 50% der Übungspunkte erreicht. (Hinweis: Sie benötigen einen Lichtbildausweis, um sich bei der Klausur zu indentifizieren!) Falls notwendig, wird eine Nachklausur kurz vor Beginn des neuen Semesters stattfinden, näheres wird noch bekanntgegeben.&lt;br /&gt;
&amp;lt;!---&lt;br /&gt;
* '''[[Media:Prüfungsteilnehmer.pdf|Liste der Studenten]], die sich verbindlich zur Klausur angemeldet und die notwendige Übungspunktzahl erreicht haben.'''&lt;br /&gt;
* '''[[Media:Ergebnis-Klausur-23-07-2008.pdf|Ergebnis der Klausur vom 23.7.2008]]''' (anonymisiert)&lt;br /&gt;
* '''Scheine''' können ab 1.9.2008 im Sekretariat Informatik bei Frau Tenschert abgeholt werden.&lt;br /&gt;
* Die '''Wiederholungsklausur''' findet am 1.10.2008 um 9:00 Uhr im Seminarraum des [http://hci.iwr.uni-heidelberg.de/contact.php HCI, Speyerer Str. 4], statt.&lt;br /&gt;
* '''[[Media:Ergebnis-Klausur-01-10-2008.pdf|Ergebnis der Wiederholungsklausur vom 1.10.2008]]''' (anonymisiert)&lt;br /&gt;
---&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Leistungsnachweise ===&lt;br /&gt;
Für alle Leistungsnachweise ist die erfolgreiche Teilnahme an den Übungen erforderlich. Für Leistungspunkte bzw. den Klausurschein muss außerdem die schriftliche Prüfung bestanden werden. Einzelheiten werden noch bekanntgegeben.&lt;br /&gt;
&amp;lt;!-------&lt;br /&gt;
Im einzelnen können erworben werden:&lt;br /&gt;
* ein unbenoteter Übungsschein, falls jemand nicht an der Klausur teilnimmt bzw. die Klausur nicht bestanden wurde.&lt;br /&gt;
* ein benoteter Übungsschein (Magister mit Computerlinguistik im ''Nebenfach'', Physik Diplom)&lt;br /&gt;
* ein Klausurschein (Magister mit Computerlinguistik im ''Hauptfach'')&lt;br /&gt;
* ein Leistungsnachweis über 9 Leistungspunkte (B.A. Computerlinguistik - alte Studienordnung) &lt;br /&gt;
* ein Leistungsnachweis über 8 Leistungspunkte (B.Sc. Informatik, B.A. Computerlinguistik - neue Studienordnung) &lt;br /&gt;
* ein Leistungsnachweis über 7 Leistungspunkte (B.Sc. Physik).&lt;br /&gt;
---------&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Übungsbetrieb ===&lt;br /&gt;
* Termine und Räume: &lt;br /&gt;
** Mo 14:00 - 16:00 Uhr, INF 227 (KIP), Seminarraum 2.402 (Tutor: Sven Ebser [mailto:sven@ebsers.de sven AT ebsers.de])&lt;br /&gt;
** Di  9:00 - 11:00 Uhr, INF 227 (KIP), Seminarraum 2.403 (Tutor: Christoph Koke [mailto:koke@kip.uni-heidelberg.de koke AT kip.uni-heidelberg.de])&lt;br /&gt;
** Di 11:00 - 13:00 Uhr, INF 227 (KIP), Seminarraum 2.403 (Tutor: Kai Karius [mailto:kai.karius@googlemail.com kai.karius AT googlemail.com])&lt;br /&gt;
** Mi 14:00 - 16:00 Uhr, INF 227 (KIP), Seminarraum 2.401 (Tutor: Stephan Meister [mailto:stephan.meister@iwr.uni-heidelberg.de stephan.meister AT iwr.uni-heidelberg.de])  &lt;br /&gt;
* Die Übungsgruppen werden über &amp;lt;b&amp;gt;[https://www.mathi.uni-heidelberg.de/muesli/lecture/view/169 MÜSLI]&amp;lt;/b&amp;gt; verwaltet. Dort erfolgt auch die &amp;lt;b&amp;gt;Anmeldung&amp;lt;/b&amp;gt;.&lt;br /&gt;
&amp;lt;!------&lt;br /&gt;
* [[Media:Punktestand.pdf|aktueller Punktestand]] (PDF, anonymisiert, so aktuell, wie von den Tutoren an mich übermittelt -- UK)&lt;br /&gt;
-------&amp;gt;&lt;br /&gt;
* &amp;lt;b&amp;gt;[[Main Page#Übungsaufgaben|Übungsaufgaben]]&amp;lt;/b&amp;gt; (Übungszettel mit Abgabetermin, Musterlösungen). Lösungen bitte per Email an den jeweiligen Übungsgruppenleiter.&lt;br /&gt;
* Zur Klausur wird zugelassen, wer mindestens 50% der Übungspunkte erreicht. Außerdem muss jeder Teilnehmer eine Lösung (bzw. einen Teil davon) in der Übungsgruppe vorrechnen. &lt;br /&gt;
* Durch das Lösen von Bonusaufgaben und gute Mitarbeit in den Übungen können Sie Zusatzpunkte erlangen. Zusatzpunkte werden auch vergeben, wenn Sie größere Verbesserungen an diesem Wiki vornehmen. Damit solche Verbesserungen der richtigen Person zugeordnet werden, sollten Sie dafür ein eigenes Wiki-Login verwenden, das Ihnen Stephan Meister oder Ullrich Köthe auf Anfrage gerne einrichten.&lt;br /&gt;
&lt;br /&gt;
=== Prüfungsvorbereitung ===&lt;br /&gt;
&lt;br /&gt;
Zur Hilfe bei der Prüfungsvorbereitung hat Andreas Fay [http://de.neemoy.com/quizcategories/31/ Quizfragen] erstellt.&lt;br /&gt;
&lt;br /&gt;
=== Literatur ===&lt;br /&gt;
&lt;br /&gt;
* R. Sedgewick: Algorithmen (empfohlen für den ersten Teil, bis einschließlich Graphenalgorithmen)&lt;br /&gt;
* J. Kleinberg, E.Tardos: Algorithm Design (empfohlen für den zweiten Teil, einschließlich Graphenalgorithmen)&lt;br /&gt;
* T. Cormen, C. Leiserson, R.Rivest: Algorithmen - eine Einführung (empfohlen zum Thema Komplexität)&lt;br /&gt;
* Wikipedia und andere Internetseiten (sehr gute Seiten über viele Algorithmen und Datenstrukturen)&lt;br /&gt;
&lt;br /&gt;
=== Gliederung der Vorlesung ===&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Einführung]] (17.4.2012) &lt;br /&gt;
#* Definition von Algorithmen und Datenstrukturen, Geschichte&lt;br /&gt;
#* Fundamentale Algorithmen: create, assign, copy, swap, compare etc.&lt;br /&gt;
#* Fundamentale Datenstrukturen: Zahlen, Container, Handles&lt;br /&gt;
#* Python-Grundlagen&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Container]] (19.4.2012)&lt;br /&gt;
#* Anforderungen von Algorithmen an Container&lt;br /&gt;
#* Einteilung der Container&lt;br /&gt;
#* Grundlegende Container: Array, verkettete Liste, Stack und Queue&lt;br /&gt;
#* Sequenzen und Intervalle (Ranges)&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Sortieren]] (24. und 26.4.2012)&lt;br /&gt;
#* Spezifikation des Sortierproblems&lt;br /&gt;
#* Selection Sort und Insertion Sort&lt;br /&gt;
#* Merge Sort&lt;br /&gt;
#* Quick Sort und seine Varianten&lt;br /&gt;
#* Vergleich der Anzahl der benötigten Schritte&lt;br /&gt;
#* Laufzeitmessung in Python&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Korrektheit]] (3. und 8.5.2012)&lt;br /&gt;
#* Definition von Korrektheit, Algorithmen-Spezifikation&lt;br /&gt;
#* Korrektheitsbeweise versus Testen&lt;br /&gt;
#* Vor- und Nachbedingungen, Invarianten, Programming by contract&lt;br /&gt;
#* Testen, Execution paths, Unit Tests in Python&lt;br /&gt;
#* Ausnahmen (exceptions) und Ausnahmebehandlung in Python&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Effizienz]] (10. und 15.5.2012)&lt;br /&gt;
#* Laufzeit und Optimierung: Innere Schleife, Caches, locality of reference&lt;br /&gt;
#* Laufzeit versus Komplexität&lt;br /&gt;
#* Landausymbole (O-Notation, &amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;-Notation, &amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;-Notation), Komplexitätsklassen&lt;br /&gt;
#* Bester, schlechtester, durchschnittlicher Fall&lt;br /&gt;
#* Amortisierte Komplexität&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Suchen]] (22. und 24.5.2012)&lt;br /&gt;
#* Sequentielle Suche&lt;br /&gt;
#* Binäre Suche in sortierten Arrays, Medianproblem&lt;br /&gt;
#* Suchbäume, balancierte Bäume&lt;br /&gt;
#* selbst-balancierende Bäume, Rotationen&lt;br /&gt;
#* Komplexität der Suche&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Sortieren in linearer Zeit]] (29.5.2012)&lt;br /&gt;
#* Permutationen&lt;br /&gt;
#* Sortieren als Suchproblem&lt;br /&gt;
#* Bucket Prinzip, Bucket Sort&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Prioritätswarteschlangen]] (31.5.2012)&lt;br /&gt;
#* Heap-Datenstruktur&lt;br /&gt;
#* Einfüge- und Löschoperationen&lt;br /&gt;
#* Heapsort&lt;br /&gt;
#* Komplexität des Heaps&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Assoziative Arrays]] (5.6.2012)&lt;br /&gt;
#* Datenstruktur-Dreieck für assoziative Arrays&lt;br /&gt;
#* Definition des abstrakten Datentyps&lt;br /&gt;
#* JSON-Datenformat&lt;br /&gt;
#* Realisierung durch sequentielle Suche und durch Suchbäume&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Hashing und Hashtabellen]] (5.6.und 12.6.2012)&lt;br /&gt;
#* Implementation assoziativer Arrays mit Bäumen&lt;br /&gt;
#* Hashing und Hashfunktionen&lt;br /&gt;
#* Implementation assoziativer Arrays als Hashtabelle mit linearer Verkettung bzw. mit offener Adressierung&lt;br /&gt;
#* Anwendung des Hashing zur String-Suche: Rabin-Karp-Algorithmus&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Iteration versus Rekursion]] (14.6.2012)&lt;br /&gt;
#* Typen der Rekursion und ihre Umwandlung in Iteration&lt;br /&gt;
#* Auflösung rekursiver Formeln mittels Master-Methode und Substitutionsmethode&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Generizität]] (19.6.2012)&lt;br /&gt;
#* Abstrakte Datentypen, Typspezifikation&lt;br /&gt;
#* Required Interface versus Offered Interface&lt;br /&gt;
#* Adapter und Typattribute, Funktoren&lt;br /&gt;
#* Beispiel: Algebraische Konzepte und Zahlendatentypen&lt;br /&gt;
#* Operator overloading in Python&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Graphen und Graphenalgorithmen]] (21.6. bis 5.7.2012)&lt;br /&gt;
#* Einführung&lt;br /&gt;
#* Graphendatenstrukturen, Adjazenzlisten und Adjazenzmatrizen&lt;br /&gt;
#* Gerichtete und ungerichtete Graphen&lt;br /&gt;
#* Vollständige Graphen&lt;br /&gt;
#* Planare Graphen, duale Graphen&lt;br /&gt;
#* Pfade, Zyklen&lt;br /&gt;
#* Tiefensuche und Breitensuche&lt;br /&gt;
#* Zusammenhang, Komponenten&lt;br /&gt;
#* Gewichtete Graphen&lt;br /&gt;
#* Minimaler Spannbaum&lt;br /&gt;
#* Kürzeste Wege, Best-first search (Dijkstra)&lt;br /&gt;
#* Most-Promising-first search (A*)&lt;br /&gt;
#* Problem des Handlungsreisenden, exakte Algorithmen (erschöpfende Suche, Branch-and-Bound-Methode) und Approximationen&lt;br /&gt;
#* Erfüllbarkeitsproblem, Darstellung des 2-SAT-Problems durch gerichtete Graphen, stark zusammenhängende Komponenten&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Repetition---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Orthogonale Zerlegung des Problems---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Hierarchische Zerlegung der Daten (Divide and Conquer)---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Randomisierung---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Optimierung, Zielfunktionen---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Systematisierung von Algorithmen aus der bisherigen Vorlesung---&amp;gt;&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
&amp;lt;!---# [[Analytische Optimierung]] (25.6.2008)---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Methode der kleinsten Quadrate---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Approximation von Geraden---&amp;gt;&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Randomisierte Algorithmen]] (10. und 12.7.2012)&lt;br /&gt;
#* Zufallszahlen, Zyklenlänge, Pitfalls&lt;br /&gt;
#* Zufallszahlengeneratoren: linear congruential generator, Mersenne Twister&lt;br /&gt;
#* Randomisierte vs. deterministische Algorithmen&lt;br /&gt;
#* Las Vegas vs. Monte Carlo Algorithmen&lt;br /&gt;
#* Beispiel für Las Vegas: Randomisiertes Quicksort&lt;br /&gt;
#* Beispiele für Monte Carlo: Randomisierte Lösung des k-SAT Problems &lt;br /&gt;
#* RANSAC-Algorithmus, Erfolgswahrscheinlichkeit, Vergleich mit analytischer Optimierung (Methode der kleinsten Quadrate)&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Greedy-Algorithmen und Dynamische Programmierung]] (17.7.2012)&lt;br /&gt;
#* Prinzipien, Aufwandsreduktion in Entscheidungsbäumen&lt;br /&gt;
#* bereits bekannte Algorithmen: minimale Spannbäume nach Kruskal, kürzeste Wege nach Dijkstra&lt;br /&gt;
#* Beispiel: Interval Scheduling Problem und Weighted Interval Scheduling Problem&lt;br /&gt;
#* Beweis der Optimalität beim Scheduling Problem: &amp;quot;greedy stays ahead&amp;quot;-Prinzip, Directed Acyclic Graph bei dynamischer Programmierung&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[NP-Vollständigkeit]] (19.7.2012)&lt;br /&gt;
#* die Klassen P und NP&lt;br /&gt;
#* NP-Vollständigkeit und Problemreduktion&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# Reserve und/oder Wiederholung (24. und 26.7.2012)&lt;br /&gt;
&lt;br /&gt;
== Übungsaufgaben ==&lt;br /&gt;
&lt;br /&gt;
(im PDF Format). Die Abgabe erfolgt am angegebenen Tag bis 14:00 Uhr per Email an den jeweiligen Übungsgruppenleiter. Bei Abgabe bis zum folgenden Montag 11:00 Uhr werden noch 50% der erreichten Punkte angerechnet. Danach wird die Musterlösung freigeschaltet. Erreichbare Punkte (ohne Bonusaufgaben): 466.&lt;br /&gt;
&lt;br /&gt;
# [[Media:Übung-1.pdf|Übung]] (Abgabe 24.4.2012) und [[Media:Uebung-1-Musterloesung.pdf|Musterlösung]]&lt;br /&gt;
#* Python-Tutorial&lt;br /&gt;
#* Sieb des Eratosthenes&lt;br /&gt;
#* Wert- und Referenzsemantik&lt;br /&gt;
#* Dynamisches Array&lt;br /&gt;
# [[Media:Uebung-2.pdf|Übung]] (Abgabe 3.5.2012) und [[Media:Uebung-2-Musterloesung.pdf|Musterlösung]]&lt;br /&gt;
#* Sortieren: Implementation und Geschwindigkeitsvergleich (Diagramme in Abhängigkeit von der Problemgröße)&lt;br /&gt;
#* Entwicklung eines Gewinnalgorithmus für ein Spiel&lt;br /&gt;
#* Bonus: Dynamisches Array mit verringertem Speicherverbrauch&lt;br /&gt;
# [[Media:Uebung-3.pdf|Übung]] (Abgabe 10.5.2012) und [[Media:Uebung-3-Musterlösung.pdf|Musterlösung]]&lt;br /&gt;
#* Experimente zur Effektivität von Unit Tests&lt;br /&gt;
#* Bestimmung von Pi mit dem Algorithmus von Archimedes&lt;br /&gt;
#* Deque-Datenstruktur: Vor- und Nachbedingungen der Operationen, Implementation und Unit Tests&lt;br /&gt;
# [[Media:Uebung-4.pdf|Übung]] (Abgabe '''Montag''' 21.5.2012) und [[Media:muster_blatt4.pdf|Musterlösung]]&lt;br /&gt;
#* Theoretische Aufgaben zur Komplexität&lt;br /&gt;
#* Amortisierte Komplexität von array.append()&lt;br /&gt;
#* Optimierung der Matrizenmultiplikation&lt;br /&gt;
# [[Media:Uebung-5.pdf|Übung]] (31.5.2012) und [[Media:muster_blatt5.pdf|Musterlösung]]&lt;br /&gt;
#* Implementation und Analyse eines Binärbaumes&lt;br /&gt;
#* Anwendung: einfacher Taschenrechner&lt;br /&gt;
# [[Media:Uebung-6.pdf|Übung]] (Abgabe '''Freitag''' 8.6.2012) und [[Media:muster_blatt6.pdf|Musterlösung]]&lt;br /&gt;
#* Treap-Datenstruktur: Verbindung von Suchbaum und Heap&lt;br /&gt;
#* Anwendung: Worthäufigkeiten (Dazu benötigen Sie das File  [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/die-drei-musketiere.txt die-drei-musketiere.txt]. Die Zeichenkodierung in diesem File ist Latin-1.)&lt;br /&gt;
#* BucketSort&lt;br /&gt;
# [[Media:Uebung-7.pdf|Übung]] (Abgabe 14.6.2012) und [[Media:muster_blatt07.pdf|Musterlösung]]&lt;br /&gt;
#* Absichtliche Konstruktion von Kollisionen für eine Hashfunktion&lt;br /&gt;
#* Übungen zum Assoziativen Array und zum JSON-Format: Cocktail-Datenbank (Dazu benötigen Sie das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/cocktails.json cocktails.json]. Die Zeichenkodierung in diesem File ist UTF-8.)&lt;br /&gt;
# [[Media:Uebung-8.pdf|Übung]] (Abgabe 21.6.2012) und [[Media:muster_blatt8.pdf|Musterlösung]]&lt;br /&gt;
#* Übungen zu Rekursion und Iteration: Fibonaccizahlen, Koch-Schneeflocke, Komplexität rekursiver Algorithmen, Umwandlung von Rekursion in Iteration&lt;br /&gt;
# [[Media:Uebung-9.pdf|Übung]] (Abgabe 28.6.2012) und [[Media:muster_blatt9.pdf|Musterlösung]]&lt;br /&gt;
#* Planare Graphen: Aufstellen von Adjazenzmatrizen und Adjazenzlisten, obere Schranke für die Zahl der Kanten&lt;br /&gt;
#* Übungen zur Generizität: Sortieren mit veränderter Ordnung, Iterator für Tiefensuche&lt;br /&gt;
# [[Media:Uebung-10.pdf|Übung]] (Abgabe 5.7.2012) und [[Media:muster_blatt10.pdf|Musterlösung]]&lt;br /&gt;
#* Fortgeschrittene Graphenaufgaben: Erzeugen einer perfekten Hashfunktion, Routenplaner (Dazu benötigen Sie das File  [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/entfernungen.json entfernungen.json]. Die Zeichenkodierung in diesem File ist UTF-8.)&lt;br /&gt;
# [[Media:Uebung-11.pdf|Übung]] (Abgabe 12.7.2012) und [[Media:muster_blatt11.pdf|Musterlösung]] sowie schöne [[Media:ballungsgebiete.pdf|Visualisierung der Ballungsgebiete]] von Thorben Kröger&lt;br /&gt;
#* Fortgeschrittene Graphenaufgaben 2: Clusterung mittels minimaler Spannbäume, Bildverarbeitung mit Graphen (Dazu benötigen Sie wieder das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/entfernungen.json entfernungen.json] sowie die Files [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/cells.pgm cells.pgm] und [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/pgm.py pgm.py].)&lt;br /&gt;
# [[Media:Uebung-12.pdf|Übung]] (Abgabe 19.7.2012) und [[Media:muster_blatt12.pdf|Musterlösung]]&lt;br /&gt;
#* Erfüllbarkeitsproblem, Anwendung: Heim- und Auswärtsspiele im Fussball (Dazu benötigen sie das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/bundesliga-paarungen-12-13.json bundesliga-paarungen-12-13.json].)&lt;br /&gt;
#* Randomisierte Algorithmen: RANSAC für Kreise (Dazu benötigen sie das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/noisy-circles.txt noisy-circles.txt].)&lt;br /&gt;
# [[Media:Bonusuebung.pdf|Übung (Bonus)]] (&amp;lt;font color=red&amp;gt;Achtung: Abgabe bereits am Dienstag, 24.7.2012&amp;lt;/font&amp;gt;)&lt;br /&gt;
#* Greedy-Algorithmus&lt;br /&gt;
#* Weg durch einen Graphen&lt;br /&gt;
#* Wiederholungsaufgaben für die Klausur&lt;br /&gt;
&lt;br /&gt;
== Sonstiges ==&lt;br /&gt;
* [[Gnuplot| Gnuplot Kurztutorial]]&lt;br /&gt;
* [[Git Kurztutorial]]&lt;br /&gt;
* [[neue Startseite|mögliche neue Startseite]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=File:Muster_blatt12.pdf&amp;diff=5413</id>
		<title>File:Muster blatt12.pdf</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=File:Muster_blatt12.pdf&amp;diff=5413"/>
				<updated>2012-07-27T10:32:22Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Korrektheit&amp;diff=5411</id>
		<title>Korrektheit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Korrektheit&amp;diff=5411"/>
				<updated>2012-07-25T18:43:09Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Häufige Fehler */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Man unterscheidet zwischen Prüfung der Korrektheit (Verifikation) und Prüfung der Spezifikation (Validierung). Ein Algorithmus heißt korrekt, wenn er sich gemäß seiner Spezifikation verhält, auch wenn seine Spezifikation nicht immer die gewünschten Ergebnisse liefert. Die Spezifikation beschreibt die Vorbedingungen (was vor der Anwendung des Algorithmus gilt, so dass der Algorithmus überhaupt angewendet werden darf) und die Nachbedingungen (was nach der Anwendung des Algorithmus gilt, welchen Zustand des Systems der Algorithmus also erzeugt). Hier geht es ausschliesslich um die Prüfung der Korrektheit eines Algorithmus, also darum, ob die spezifizierten Nachbedingungen wirklich gelten.&lt;br /&gt;
 &lt;br /&gt;
Nebenbemerkungen&lt;br /&gt;
# Approximationsalgorithmen liefern nie ein exaktes Ergebnis. Sie gelten als korrekt, wenn der in der Spezifikation angegebene Approximationsfehler nicht überschritten wird.&lt;br /&gt;
# Es gibt Algorithmen, die ''nie'' mit einer 100-prozentigen Wahrscheinlichkeit richtige Ergebnisse liefern können (z.B. [http://en.wikipedia.org/wiki/Primality_test#Probabilistic_tests nichtdeterministische Primzahltests]). In diesem Fall muss die in der Spezifikation angegebene Erfolgswahrscheinlichleit erreicht werden.&lt;br /&gt;
# '''Korrektheit''' wird in Algorithmenbüchern meist nur im Zusammenhang mit konkreten Algorithmen behandelt, aber nicht als übergreifendes Problem. Dies erscheint der Bedeutung von Korrektheit nicht angemessen.&lt;br /&gt;
&lt;br /&gt;
Will man die Korrektheit eines Algorithmus/Programms feststellen, hat man 3 Vorgehensweisen zur Verfügung: Korrektheitsprüfungen durch die Programmiersprache, formaler Korrektheitsbeweis und Softwaretest.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Korrektheitsprüfungen durch die Programmiersprache ==&lt;br /&gt;
&lt;br /&gt;
Alle Programmiersprachen beinhalten gewisse Hilfen, um Programmierfehler zu vermeiden, insbesondere die syntaktische Prüfung und die Typprüfung. Zwar kann man dadurch nur relativ einfache Fehler finden (siehe Beispiele unten), aber da diese Prüfungen ohne zusätzlichen Aufwand automatisch passieren, sind sie trotzdem sehr nützlich. Die hier kurz beschriebenen Konzepte werden in den Veranstaltungen zur theoretischen Informatik (Grammatiken) und zum Compilerbau ausführlich behandelt.&lt;br /&gt;
&lt;br /&gt;
=== Syntaktische Prüfung ===&lt;br /&gt;
Es wird eine Grammatik definiert, deren Regeln die Implementation des Algorithmus befolgen muss. Für ein Programm heißt das beispielsweise, dass die Syntax der Programmiersprache eingehalten werden muss.&lt;br /&gt;
&lt;br /&gt;
Vorteile des Verfahrens: die Richtigkeit der Syntax lässt sich leicht vom Compiler/Interpreter überprüfen (mehr dazu in der Theoretischen Informatik und Compilerbau). Somit ist es die einfachste Möglichkeit, viele inkorrekte Programme schnell zu erkennen und zurückzuweisen.&lt;br /&gt;
  &amp;gt;&amp;gt;&amp;gt; if a = 0:  # sollte heissen: if a == 0:&lt;br /&gt;
    File &amp;quot;&amp;lt;stdin&amp;gt;&amp;quot;, line 1&lt;br /&gt;
      if a = 0:&lt;br /&gt;
           ^&lt;br /&gt;
  SyntaxError: invalid syntax&lt;br /&gt;
&lt;br /&gt;
=== Typprüfung ===&lt;br /&gt;
Ein Typ definiert Gruppierung der Daten und die Operationen, die für diese Datengruppierung erlaubt sind (konkreter Typ) bzw. die Bedeutung der Daten und die erlaubten Operationen (abstrakter Datentyp, vgl. Dreieck aus der [[Einführung#Definition von Datenstrukturen|ersten Vorlesung]]). Typen sind Zusicherungen an den Algorithmus und den Compiler/Interpreter, dass Daten und deren Operationen bestimmte semantische Bedingungen einhalten. Wenn man innerhalb des Algorithmus mit Typen arbeitet, darf man von der semantischen Korrektheit der erlaubten Operationen ausgehen. Umgekehrt können Operationen, die zu Typkonflikten führen würden, leicht als inkorrekt zurückgewiesen werden.&lt;br /&gt;
&lt;br /&gt;
Vorteile des Verfahrens: Typprüfung ist teuerer als syntaktische Prüfung, aber billiger als andere Prüfungen der Korrektheit (mehr dazu im Kapitel [[Generizität]]).&lt;br /&gt;
  &amp;gt;&amp;gt;&amp;gt; a=3&lt;br /&gt;
  &amp;gt;&amp;gt;&amp;gt; b=None&lt;br /&gt;
  &amp;gt;&amp;gt;&amp;gt; a+b&lt;br /&gt;
  Traceback (most recent call last):&lt;br /&gt;
    File &amp;quot;&amp;lt;stdin&amp;gt;&amp;quot;, line 1, in &amp;lt;module&amp;gt;&lt;br /&gt;
  TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'&lt;br /&gt;
&lt;br /&gt;
In python ist (ebenso wie in vielen anderen Programmiersprachen) explizite Typprüfung möglich:&lt;br /&gt;
  &amp;gt;&amp;gt;&amp;gt; import types&lt;br /&gt;
  &amp;gt;&amp;gt;&amp;gt; a=3&lt;br /&gt;
  &amp;gt;&amp;gt;&amp;gt; b=None&lt;br /&gt;
  &amp;gt;&amp;gt;&amp;gt; if isinstance(b, types.IntType): # prüft, ob b ein Integer ist&lt;br /&gt;
  ...     print a+b&lt;br /&gt;
  ... else:&lt;br /&gt;
  ...     raise TypeError, &amp;quot;b ist kein Integer&amp;quot; # falls b kein Integer ist, wird ein TypeError ausgelöst&lt;br /&gt;
  ... &lt;br /&gt;
 &lt;br /&gt;
  Traceback (most recent call last):&lt;br /&gt;
    File &amp;quot;&amp;lt;stdin&amp;gt;&amp;quot;, line 4, in &amp;lt;module&amp;gt;&lt;br /&gt;
  TypeError: b ist kein Integer&lt;br /&gt;
&lt;br /&gt;
=== Prüfen der Vorbedingungen eines Algorithmus ===&lt;br /&gt;
&lt;br /&gt;
Manche Programmiersprachen (z.B. [http://en.wikipedia.org/wiki/Eiffel_%28programming_language%29 Eiffel]) testen am Anfang jeder Funktion automatisch alle spezifizierten Vorbedingungen. Dies wird als ''[http://en.wikipedia.org/wiki/Design_by_contract Programming by Contract]'' bezeichnet. In Python hingegen muss man solche Prüfungen, mit Ausnahme der Typprüfungen (die man als Spezialfall der Vorbedingungen betrachten kann), selbst implementieren. Es steht aber mit den ''Exceptions'' ein leistungsfähiger Mechanismus zur Verfügung, um eventuelle Fehler in geordneter Weise zu signalisieren, siehe dazu [http://docs.python.org/tutorial/errors.html Kapitel 8 (Errors and Exceptions) der Pythondokumentation]. Beispielsweise darf die Quadratwurzel nicht für negative Zahlen aufgerufen werden. Man schreibt deshalb:&lt;br /&gt;
 def sqrt(x):&lt;br /&gt;
     if x &amp;lt; 0.0:&lt;br /&gt;
         raise ValueError(&amp;quot;sqrt() of negative number.&amp;quot;)&lt;br /&gt;
Qualitativ hochwertige Software zeichnet sich unter anderem dadurch aus, dass das Programming by Contract konsequent umgesetzt ist, auch wenn die Programmiersprache dafür keine dedizierten Sprachkonstrukte bereitstellt.&lt;br /&gt;
&lt;br /&gt;
== Formaler Korrektheitsbeweis ==&lt;br /&gt;
&lt;br /&gt;
Korrektheitsbeweise können auf drei Arten geführt werden:&lt;br /&gt;
* In Algorithmenbüchern findet man typischerweise Beweise für die Korrektheit der grundlegenden Idee eines Algorithmus. Diese Beweise werden auf der Pseudocodeebene geführt, so dass bei der Implementation wieder Fehler unterlaufen können.&lt;br /&gt;
* Ein formaler Beweis der Korrektheit einer konkreten Implementation erfordert weit größeren Aufwand, sichert aber, dass der Code keine Fehler mehr enthalten kann.&lt;br /&gt;
* Werden im Algorithmus reelle Zahlen mit Hilfe von Gleitkommazahlen implementiert, ist der Algorithmus automatisch ein Approximationsalgorithmus, weil die Gleitkommazahlen nur eine Approximation der reellen Zahlen sind. In diesem Falle beweist man, dass der Approximationsfehler bestimmte Schranken nicht überschreitet. Dies ist eine wichtige Aufgabe der [http://de.wikipedia.org/wiki/Numerische_Mathematik Numerischen Mathematik] und wird hier nicht weiter vertieft.&lt;br /&gt;
&lt;br /&gt;
=== Korrektheitsbeweis der Algorithmenidee ===&lt;br /&gt;
&lt;br /&gt;
Hier ist die entscheidende Technik die Identifikation von ''Invarianten'', die (dank der Vorbedingungen) am Anfang und während der gesamten Ausführung des Algorithmus gelten. Kann man die Erhaltung der Invarianten nachweisen, folgen daraus die Nachbedingungen des Algorithmus und somit dessen Korrektheit. Die Identifikation geeigneter Invarianten ist häufig eine schwierige Aufgabe. Hat man einen Kandidaten gefunden, geht man zum Beweis ähnlich vor wie beim mathematischen Verfahren der vollständigen Induktion: Man beweist zunächst, dass die Invariante am Anfang gilt (''initialization''). Dann nimmt man an, dass die Invariante vor einem bestimmten Statement (z.B. vor der i-ten Iteration einer Schleife) gilt, und beweist, dass daraus die Gültigkeit am Ende des Statement (also nach der i-ten Iteration) folgt (''maintainance''). Kann man außerdem zeigen, dass der Algorithmus terminiert, folgt aus initialization und maintainance die Gültigkeit der Invariante am Ende des Algorithmus.&lt;br /&gt;
&lt;br /&gt;
Wir wollen das Verfahren am Beispiel des '''Selection Sort'''-Algorithmus vorführen. Um den Beweis zu vereinfachen, definieren wir die folgenden Konventionen:&lt;br /&gt;
* Ein leeres Array &amp;lt;tt&amp;gt;[]&amp;lt;/tt&amp;gt; ist sortiert.&lt;br /&gt;
* Das Minimum eines leeren Arrays ist &amp;lt;math&amp;gt;+\infty&amp;lt;/math&amp;gt;, und das Maximum ist &amp;lt;math&amp;gt;-\infty&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Der selection sort-Algorithmus hat zwei Invarianten:&lt;br /&gt;
&lt;br /&gt;
* '''I1:''' Vor der i-ten Iteration der äußeren Schleife ist das linke Teilarray &amp;lt;tt&amp;gt;a[:i]&amp;lt;/tt&amp;gt; sortiert.&lt;br /&gt;
&lt;br /&gt;
* '''I2:''' Vor der i-ten Iteration der äußeren Schleife ist das Maximum des linken Teilarrays &amp;lt;tt&amp;gt;max(a[:i])&amp;lt;/tt&amp;gt; kleiner oder gleich dem Minimum des rechten Teilarrays &amp;lt;tt&amp;gt;min(a[i:])&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Der Beweis der Initialisierung (Fall &amp;lt;tt&amp;gt;i==0&amp;lt;/tt&amp;gt;) ist sehr einfach, weil das linke Teilarray zunächst leer und somit sortiert ist ('''I1'''). Außerdem ist sein Maximum &amp;lt;math&amp;gt;-\infty&amp;lt;/math&amp;gt; and damit sicherlich kleiner als jedes Element im Array ('''I2''').&lt;br /&gt;
&lt;br /&gt;
Wir nehmen nun an, dass die Invarianten für ein gewisses &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; gelten und beweisen, dass sie dann auch für &amp;lt;tt&amp;gt;i+1&amp;lt;/tt&amp;gt; gelten. Das heißt, wir nehmen an, dass &amp;lt;tt&amp;gt;a[:i]&amp;lt;/tt&amp;gt; sortiert ist ('''I1'''), und dass &amp;lt;tt&amp;gt;max(a[:i]) &amp;amp;le; min(a[i:])&amp;lt;/tt&amp;gt; ('''I2'''). Da das Element &amp;lt;tt&amp;gt;a[i]&amp;lt;/tt&amp;gt; zum rechten Teilarray gehört, gilt insbesondere auch &amp;lt;tt&amp;gt;max(a[:i]) &amp;amp;le; a[i]&amp;lt;/tt&amp;gt;, und daraus folgt sofort, dass das um ein Element vergrößerte linke Teilarray &amp;lt;tt&amp;gt;a[:i+1]&amp;lt;/tt&amp;gt; ebenfalls sortiert ist ('''I1'''), unabhängig davon, welches Element sich an Position &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; befindet. Um aber auch die zweite Invariante zu erfüllen, müssen wir zusätzlich sicherstellen, dass &amp;lt;tt&amp;gt;a[i] &amp;amp;le; min(a[i:])&amp;lt;/tt&amp;gt; gilt, dass sich also ein minimales Element des rechten Teilarrays an Position &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; befindet. Entfernt man nämlich das minimale Element aus einer Menge, wird das neue Minimum der verkleinerten Menge sicherlich nicht kleiner sein als das alte. Die innere Schleife sucht nun gerade das Minimum und verschiebt es an Position &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt;. Nach dem Swap gilt somit: &amp;lt;tt&amp;gt; max(a[:i]) &amp;amp;le; a[i] = min(a[i:]) &amp;amp;le; min(a[i+1:])&amp;lt;/tt&amp;gt; und damit auch &amp;lt;tt&amp;gt;max(a[:i+1]) = a[i] &amp;amp;le; min(a[i+1:])&amp;lt;/tt&amp;gt; ('''I2'''). Außerdem ist klar, dass der Algorithmus terminiert, weil jede Schleife nur endlich viele Schritte ausführt (Iteration bis &amp;lt;tt&amp;gt;len(a)&amp;lt;/tt&amp;gt;). Durch Induktion auf den Fall &amp;lt;tt&amp;gt;i == len(a)&amp;lt;/tt&amp;gt; folgt aus Invariante '''I1''', dass das Teilarray &amp;lt;tt&amp;gt;a[:len(a)]&amp;lt;/tt&amp;gt; sortiert ist. Dies ist aber gerade das gesamte Array, was zu beweisen war.&lt;br /&gt;
&lt;br /&gt;
Zehlreiche Beweise nach diesem Muster findet man z.B. bei Cormen et al.&lt;br /&gt;
&lt;br /&gt;
=== Formales Beweisen der Implementation ===&lt;br /&gt;
Man versucht, die Hypothese H: ''die Implementation ist korrekt'' entweder mathematisch zu beweisen oder zu widerlegen. Dieses Beweisverfahren heißt automatisch, wenn es allein von einem Computer durchgeführt wird, und halbautomatisch, wenn der Mensch in den Entscheidungsprozess miteinbezogen ist. Allerdings sind solche Beweise sehr aufwändig und werden daher nur für sicherheitskritische Software verwendet, z.B. für&lt;br /&gt;
* die automatische Steuerung der fahrerlosen U-Bahnlinie 14 in Paris (vgl. Lecomte et al.: ''[http://rodin.cs.ncl.ac.uk/Publications/fm_sc_rs_v2.pdf Formal Methods in Safety-Critical Railway Systems]'' and Su et al.: ''[http://deploy-eprints.ecs.soton.ac.uk/316/1/Modes_version_55.pdf From Requirements to Development: Methodology and Example]'' - die Autoren der Steuersoftware versichern, dass in 10 Jahren Betrieb der U-Bahn kein Softwarefehler aufgetreten ist),&lt;br /&gt;
* die Sicherheitsmerkmale von [http://en.wikipedia.org/wiki/Smart_card Chipkarten] und&lt;br /&gt;
* das Flugzeugbetriebssystem [http://en.wikipedia.org/wiki/INTEGRITY-178B INTEGRITY 178B], das z.B. im Airbus A380 und in der Boeing 787 eingesetzt wird.&lt;br /&gt;
&lt;br /&gt;
Um den Beweis durchführen zu können, ist folgendes nötig:&lt;br /&gt;
;eine [http://en.wikipedia.org/wiki/Formal_specification formale Spezifikation] des Algorithmus: eine formale Spezifikation wird in einer [http://en.wikipedia.org/wiki/Specification_language Spezifikationssprache] geschrieben (z.B. der [http://en.wikipedia.org/wiki/B-Method B-Methode] oder der [http://en.wikipedia.org/wiki/Z_notation Z-Notation]). Sie ist &lt;br /&gt;
:* deklarativ (d.h. beschreibt, was das Programm tun soll, ist selbst aber nicht ausführbar)&lt;br /&gt;
:* formal präzise (kann nur auf eine einzige Weise interpretiert werden)&lt;br /&gt;
:* hierarchisch aufgebaut (eine Spezifikation für einen komplizierten Algorithmus greift auf Spezifikationen für einfache Bestandteile dieses Algorithmus zurück)&lt;br /&gt;
:* so einfach, dass ihre Korrektheit für einen Menschen mit entsprechender Erfahrung unmittelbar einsichtig ist (denn eine Spezifikation kann nicht formal bewiesen werden - dafür wäre eine weitere Spezifikation nötig, die auch bewiesen werden müsste usw.)&lt;br /&gt;
;ein axiomatisiertes Programmiermodell: zum Beispiel&lt;br /&gt;
:* eine axiomatisierbare Programmiersprache, wie z.B. WHILE-Programm (s. [[Einführung#Zur Frage der elementaren Schritte|erste Vorlesung]]), Pascal (siehe dazu Hoare's [http://delivery.acm.org/10.1145/70000/63445/cb-p153-hoare.pdf?key1=63445&amp;amp;key2=5041959021&amp;amp;coll=ACM&amp;amp;dl=ACM&amp;amp;CFID=15151515&amp;amp;CFTOKEN=6184618 grundlegenden Artikel]) und rein funktionale Programmiersprachen&lt;br /&gt;
:* ein axiomatisierbares Subset einer Programmiersprache (die meisten Programmiersprachen sind zu komplex, um als Ganzes axiomatisierbar zu sein)&lt;br /&gt;
:* endliche Automaten&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Der Korrektheitsbeweis kann beispielsweise mit dem Hoare-Kalkül (Hoare-Logik) durchgeführt werden (Hoare erfand u.a. den Quicksort-Algorithmus). Diese Methode wurde in &lt;br /&gt;
:  C.A.R. Hoare: ''&amp;quot;An Axiomatic Basis for Computer Programming&amp;quot;'', Communications of the ACM, 1969 [http://www.cs.ucsb.edu/~kemm/courses/cs266/hoare69.pdf] &lt;br /&gt;
erstmalig beschrieben. Im folgenden wird das Verfahren an einem Beispiel erläutert.&lt;br /&gt;
&lt;br /&gt;
==== Beispiel-Algorithmus ====&lt;br /&gt;
Zuerst brauchen wir einen Algorithmus, den wir auf Korrektheit prüfen wollen. Wir nehmen als Beispiel die Division x/y durch sukzessives Subtrahieren.&lt;br /&gt;
&lt;br /&gt;
 Vorbedingungen:&lt;br /&gt;
    int x,y&lt;br /&gt;
   0 &amp;lt; y &amp;lt;= x&lt;br /&gt;
 Gesucht:&lt;br /&gt;
    Quotient q, Rest r&lt;br /&gt;
 Algorithmus:&lt;br /&gt;
    r = x&lt;br /&gt;
    q = 0&lt;br /&gt;
    while y &amp;lt;= r:&lt;br /&gt;
        r = r - y&lt;br /&gt;
        q = q + 1&lt;br /&gt;
 Nachbedingungen:&lt;br /&gt;
    x == r + y*q and r &amp;lt; y&lt;br /&gt;
&lt;br /&gt;
==== Aufbau der Hoare-Logik ====&lt;br /&gt;
&lt;br /&gt;
Grundlegende syntaktische Struktur:&lt;br /&gt;
: p {Q} r&lt;br /&gt;
mit '''p''':Vorbedingung, '''Q''': Operation, '''r''': Nachbedingung.&lt;br /&gt;
Es bedeutet also schlicht: wenn man im Zustand '''p''' ist und eine Operation '''Q''' ausführt, kommt man in den Zustand '''r'''. Hat eine Operation keine Vorbedingung, schreibt man &lt;br /&gt;
: true {Q} r&lt;br /&gt;
&lt;br /&gt;
Die Hoare-Logik besteht aus 5 Axiomen:&lt;br /&gt;
;D0 - Axiom der Zuweisung: (Rule of Assignment)&lt;br /&gt;
:: R[t] {x=t} R[x]&lt;br /&gt;
  &lt;br /&gt;
: '''Beispiel:''' t==5 {x=t} x==5&lt;br /&gt;
&lt;br /&gt;
:Vorbedingung und Nachbedingung sind gleich, mit Ausnahme der Variablen x und t, die in der Zuweisung verknüpft werden: Man erhält die Vorbedingung, wenn man in der Nachbedingung alle Vorkommen von x (bzw. allgemein: alle Vorkommen der linken Variable der Zuweisung) durch t (bzw. allgemein: durch die rechte Variable der Zuweisung) ersetzt.&lt;br /&gt;
&lt;br /&gt;
;D1 - Konsequenzregeln: (Rules of Consequence, besteht aus zwei Axiomen)&lt;br /&gt;
:'''D1(a):''' wenn gilt&lt;br /&gt;
:: P {Q} R und R &amp;amp;rArr; S&lt;br /&gt;
:dann gilt auch&lt;br /&gt;
:: P {Q} S&lt;br /&gt;
:'''D1(b):''' wenn gilt &lt;br /&gt;
:: P {Q} R und S &amp;amp;rArr; P&lt;br /&gt;
:dann gilt auch&lt;br /&gt;
:: S {Q} R&lt;br /&gt;
:'''Beispiel:''' Für jede ganze Zahl gilt (x&amp;gt;5) &amp;amp;rArr; (x&amp;gt;0). Gilt außerdem (x&amp;gt;5) dann gilt erst recht (x&amp;gt;0).&lt;br /&gt;
&lt;br /&gt;
;D2 - Sequenzregel: (Rule of Composition)&lt;br /&gt;
:wenn gilt&lt;br /&gt;
:: P {Q&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;} R&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; und R&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; {Q&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;} R &lt;br /&gt;
:dann gilt auch&lt;br /&gt;
:: P {Q&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;, Q&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;} R&lt;br /&gt;
:Das heißt: wenn man P hat und Q&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; darauf anwendet, kommt man zu R&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;. Wenn man R&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; hat und Q&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; darauf anwendet, kommt man zu R. Deshalb kann man das so verkürzen: wenn man P hat und nacheinander Q&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; und Q&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; darauf anwendet, kommt man zu R.&lt;br /&gt;
&lt;br /&gt;
;D3 - Iterationsregel: (Rule of Iteration)&lt;br /&gt;
:wenn gilt&lt;br /&gt;
:: (P &amp;amp;and; B) {S} P&lt;br /&gt;
:dann gilt auch&lt;br /&gt;
:: P { while B do S } (&amp;amp;not;B &amp;amp;and; P)&lt;br /&gt;
:P wird dabei als '''Schleifeninvariante''' bezeichnet, weil es sowohl in der Vor- als auch in der Nachbedingung gilt. B ist die '''Schleifenbedingung''' - solange B erfüllt ist, wird die Schleife weiter ausgeführt.&lt;br /&gt;
&lt;br /&gt;
Da wir in dem Divisions-Algorithmus mit dem Typ '''int''' arbeiten, brauchen wir außerdem die für diesen Typ erlaubten Operationen, also die Axiome der ganzen Zahlen.&lt;br /&gt;
: '''A1:''' Kommutativität  x+y=y+x, x*y=y*x&lt;br /&gt;
: '''A2:''' Assoziativität  (x+y)+z=x+(y+z), (x*y)*z=x*(y*z)&lt;br /&gt;
: '''A3:''' Distributivität  x*(y+z)=x*y+x*z&lt;br /&gt;
: '''A4:''' Subtraktion (Inverses Element)  y&amp;amp;le;x &amp;amp;rArr; (x-y)+y=x&lt;br /&gt;
: '''A5:''' Neutrale Elemente  x+0=x, x*0=0, x*1=x&lt;br /&gt;
&lt;br /&gt;
==== Beweisen des Algorithmus ====&lt;br /&gt;
Vorbedingung: 0 &amp;lt; y,x&lt;br /&gt;
&lt;br /&gt;
Schleifeninvariante P (gleichzeitig Nachbedingung): x == y*q + r&lt;br /&gt;
  (1)  true &amp;amp;rArr; x==x+y*0                                          y*0==0 und x==x+0 folgen aus A5&lt;br /&gt;
  (2)  x==x+y*0              {r=x}                  x==r+y*0     D0: ersetze x durch r&lt;br /&gt;
  (3)  x==r+y*0              {q=0}                  x==r+y*q     D0: ersetze 0 durch q&lt;br /&gt;
  (4)  true                  {r=x}                  x==r+y*0     D1(b): kombiniere (1) und (2)&lt;br /&gt;
  (5)  true                  {r=x, q=0}             x==r+y*q     D2: kombiniere (4) und (3)&lt;br /&gt;
  (6)  x==r+y*q &amp;amp;and; y=r &amp;amp;rArr; x==(r-y)+y*(1+q)                       folgt aus A1...A5&lt;br /&gt;
  (7)  x==(r-y)+y*(1+q)      {r=r-y}                x==r+y*(1+q) D0: ersetze (r-y) durch r&lt;br /&gt;
  (8)  x==r+y*(1+q)          {q=q+1}                x==r+y*q     D0: ersetze (q+1) durch q&lt;br /&gt;
  (9)  x==(r-y)+y*(1+q)      {r=r-y, q=q+1}         x==r+y*q     D2: kombiniere (7) und (8)&lt;br /&gt;
  (10) x==r+y*q &amp;amp;and; y&amp;amp;le;r        {r=r-y, q=q+1}         x==r+y*q     D1(b): kombiniere (6) und (9)&lt;br /&gt;
  (11) x==r+y*q    {while y&amp;amp;le;r do (r=r-y, q=q+1)} x==r+y*q &amp;amp;and; &amp;amp;not;(y&amp;amp;le;r) D3: transformiere (10)&lt;br /&gt;
  (12) true        {r=x, q=0, &lt;br /&gt;
                    while y&amp;amp;le;r do (r=r-y, q=q+1)} x==r+y*q &amp;amp;and; &amp;amp;not;(y&amp;amp;le;r) D2: kombiniere (5) und (11)&lt;br /&gt;
&lt;br /&gt;
Im obigen Beweis ergibt sich sogar ''true'' als Vorbedingung (i.e. es gibt keine Vorbedingung). Dies liegt daran, dass Hoare in seinem Artikel durchweg von nicht-negativen Zahlen ausgeht. Diese Annahme wird beim Beweis von Zeile (6) benutzt.&lt;br /&gt;
&lt;br /&gt;
In der Praxis führt man solche Beweise natürlich nicht von Hand, sondern benutzt geeignete Programme, sogenannte [http://en.wikipedia.org/wiki/Automated_theorem_proving automatische Beweiser], die man allerding oft interaktiv steuern muss, weil der Beweis ohne diese Hilfe zu lange dauern würde.&lt;br /&gt;
&lt;br /&gt;
=== (Halb-)Automatisches Verfeinern ===&lt;br /&gt;
Dieses Verfahren ist beliebter, als das (halb-)automatische Beweisen. Die formale Spezifikation wird nach bestimmten, semantik-erhaltenden Transformationsregeln in ein ausführbares Programm umgewandelt. Mehr dazu z.B. in der [http://en.wikipedia.org/wiki/Program_refinement Wikipedia (Program refinement)]. Der Vorteil dieser Methode besteht darin, dass man die Transformationsregeln so definieren kann, dass nur das axiomatisierte Subset der Zielsprache benutzt wird. Dadurch wird der Korrektheitsbeweis stark vereinfacht.&lt;br /&gt;
&lt;br /&gt;
==Software-Tests==&lt;br /&gt;
&lt;br /&gt;
Dijkstra [http://de.wikipedia.org/wiki/Edsger_Wybe_Dijkstra] ließ einmal den Satz verlauten: &amp;quot;Tests können nie die Abwesenheit von Fehlern beweisen [Anwesenheit schon]&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Nach solch einer Aussage stellt sich die Frage, ob es sich überhaupt lohnt, mit dem Testverfahren die Korrektheit eines Algorithmus zu zeigen. Es erscheint einem doch plausibler sich auf die &amp;quot;formalen Methoden&amp;quot; zu berufen, mit dem Wissen, dass diese uns tatsächlich einen Beweis liefern können, ob nun H oder nicht H gilt. Zudem kommt noch erschwerend hinzu, dass es bei Tests bisher keine Theorie gibt, die sicherstellt, dass das Testprogramm einen vorhandenen Fehler zumindest mit hoher Wahrscheinlichkeit findet.&lt;br /&gt;
&lt;br /&gt;
Ein [http://de.wikipedia.org/wiki/Softwaretest Software-Test] versucht, ein Gegenbeispiel zur Hypothese H &amp;quot;der Algorithmus ist korrekt&amp;quot; zu finden. Dabei gibt es 4 Möglichkeiten:&lt;br /&gt;
 &lt;br /&gt;
   Algorithmus	   Testantwort	&lt;br /&gt;
      +	               +	        Algorithmus ist richtig, kein Gegenbeispiel gefunden&lt;br /&gt;
      -	               -	        Alg. ist falsch, und der Test erkennt den Fehler&lt;br /&gt;
      +	               -	        Bug im Test (Gegenbeispiel, obwohl Alg. richtig ist)&lt;br /&gt;
      -	               +	        Test hat versagt, da er den Fehler im Alg. nicht erkannt hat&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Wenn ein Gegenbeispiel zu H gefunden wird, kann man den Algorithmus (oder den Test) debuggen. Wird hingegen keines gefunden, nimmt man an, dass der Algorithmus korrekt ist. Man sieht, dass diese Annahme im Fall 4 nicht stimmt. Da Softwaretests jedoch in der Praxis sehr erfolgreich verwendet werden, ist dieser Fall offenbar nicht so häufig, dass man das Testen als Methode generell ablehnen müßte.&lt;br /&gt;
&lt;br /&gt;
=== Beispiel für das Testen: Freivalds Algorithmus ===&lt;br /&gt;
&lt;br /&gt;
Wir wollen die Wahrscheinlichkeit, dass ein Test einen vorhandenen Fehler übersieht, am Beispiel des [http://en.wikipedia.org/wiki/Freivald's_algorithm Algorithmus von Freivald] studieren. Es handelt sich dabei um einen randomisierten Algorithmus zum Testen der Matrixmultiplikation (siehe J. Hromkovič: ''&amp;quot;Randomisierte Algorithmen&amp;quot;'', Teubner 2004). Ziel dieses Algorithmuses ist es, die Hypothese H: &amp;quot;C ist das Produkt der Matrizen A und B&amp;quot; durch ein Gegenbeispiel zu widerlegen, wobei der Test einen anderen Algorithmus verwendet, um Vergleichsdaten zu gewinnen.&lt;br /&gt;
&lt;br /&gt;
  gegeben:&lt;br /&gt;
       Matrizen A, B, C  der Größe NxN &lt;br /&gt;
       Testhypothese H:  &amp;lt;tt&amp;gt;A*B == C&amp;lt;/tt&amp;gt;  Matrixmultiplikation (d.h. C wurde vorher durch C = mmul(A, B) berechnet, &lt;br /&gt;
                                            wobei mmul() der zu testende Multiplikationsalgorithmus ist).&lt;br /&gt;
 &lt;br /&gt;
  (1) Initialisierung      &lt;br /&gt;
       wähle Zufallsvektor der Länge N aus Nullen und Einsen: &amp;lt;math&amp;gt;\alpha \in \{0, 1\}^N &amp;lt;/math&amp;gt;  &lt;br /&gt;
  (2) Matrix-Vektor-Multiplikation (keine Matrix-Matrix-Multiplikation, denn die soll ja gerade verifiziert werden)&lt;br /&gt;
 &lt;br /&gt;
       &amp;lt;math&amp;gt;\left.\begin{array}{l}&lt;br /&gt;
                \beta = B*\alpha \\&lt;br /&gt;
                \gamma=A*\beta&lt;br /&gt;
                \end{array}\right\}A*(B*\alpha) == (A*B)*\alpha&lt;br /&gt;
       &amp;lt;/math&amp;gt; &lt;br /&gt;
 &lt;br /&gt;
       &amp;lt;math&amp;gt;\delta=C*\alpha&amp;lt;/math&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
  (3) Test der Korrektheit: falls &amp;lt;tt&amp;gt;A*B == C&amp;lt;/tt&amp;gt;, liefert der folgende Test stets &amp;lt;tt&amp;gt;true&amp;lt;/tt&amp;gt;:&lt;br /&gt;
 &lt;br /&gt;
       return   γ==δ&lt;br /&gt;
&lt;br /&gt;
Wir analysieren nun, mit welcher Wahrscheinlichkeit der Algorithmus den Fehler findet, wenn es denn einen gibt, d.h.&lt;br /&gt;
   &lt;br /&gt;
*Wahrscheinlichkeit '''p''', dass Freivalds Algorithmus den Fehler findet&amp;lt;br/&amp;gt;&lt;br /&gt;
oder&amp;lt;br/&amp;gt;&lt;br /&gt;
*Wahrscheinlichkeit '''q = 1 - p''', dass Freivalds Algorithmus den Fehler '''nicht''' findet.&lt;br /&gt;
&lt;br /&gt;
Wir schätzen diese Wahrscheinlichkeit ab für den einfachen Fall N=2. Wir definieren:&lt;br /&gt;
    &lt;br /&gt;
   &amp;lt;math&amp;gt;C=&lt;br /&gt;
  \begin{pmatrix} &lt;br /&gt;
    c_{11} &amp;amp; c_{12}  \\ &lt;br /&gt;
    c_{21} &amp;amp; c_{22}  &lt;br /&gt;
  \end{pmatrix},\qquad&lt;br /&gt;
\alpha=\begin{pmatrix}&lt;br /&gt;
    \alpha_1 \\&lt;br /&gt;
    \alpha_2 &lt;br /&gt;
     \end{pmatrix},\qquad&lt;br /&gt;
 \delta=\begin{pmatrix}&lt;br /&gt;
    \delta_1 \\&lt;br /&gt;
    \delta_2&lt;br /&gt;
 \end{pmatrix}&lt;br /&gt;
  = \begin{pmatrix}&lt;br /&gt;
    c_{11}\alpha_1 + c_{12}\alpha_2 \\&lt;br /&gt;
    c_{21}\alpha_1 + c_{22}\alpha_2&lt;br /&gt;
   \end{pmatrix}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''Fallunterscheidung:'''&lt;br /&gt;
      &lt;br /&gt;
'''Fall 1:'''  C enthält genau 1 Fehler, z.B. &amp;lt;math&amp;gt;c_{11}&amp;lt;/math&amp;gt; hat falschen Wert&lt;br /&gt;
&lt;br /&gt;
:Der Fehler wird gefunden, wenn &amp;lt;math&amp;gt;\delta_1 \ne \gamma_1 \Leftrightarrow\alpha_1\ne 0&amp;lt;/math&amp;gt;. Da &amp;lt;math&amp;gt;\alpha_1&amp;lt;/math&amp;gt; eine Zufallszahl aus &amp;lt;math&amp;gt;\{0,1\}&amp;lt;/math&amp;gt; ist, folgt daraus, dass '''p''' = '''q''' = &amp;lt;math&amp;gt;\frac{1}{2}&amp;lt;/math&amp;gt;&lt;br /&gt;
       &lt;br /&gt;
'''Fall 2:'''  C enthält 2 Fehler&lt;br /&gt;
:(a)   in verschiedenen Zeilen und Spalten, z.B. &amp;lt;math&amp;gt;c_{11}&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c_{22}&amp;lt;/math&amp;gt;. Es gilt: Der Fehler in &amp;lt;math&amp;gt;c_{11}&amp;lt;/math&amp;gt; wird gefunden, wenn &amp;lt;math&amp;gt;\delta_1 \ne \gamma_1 \Leftrightarrow \alpha_1\ne 0&amp;lt;/math&amp;gt;. Unabhängig davon wird der Fehler in &amp;lt;math&amp;gt;c_{22}&amp;lt;/math&amp;gt; gefunden, wenn &amp;lt;math&amp;gt;\delta_2 \ne \gamma_2 \Leftrightarrow \alpha_2\ne 0&amp;lt;/math&amp;gt;. Da &amp;lt;math&amp;gt;\alpha_1&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\alpha_2&amp;lt;/math&amp;gt; statistisch unabhängig sind, ist die Wahrscheinlichkeit für jedes dieser Ereignisse &amp;lt;math&amp;gt;q_1&amp;lt;/math&amp;gt; bzw. &amp;lt;math&amp;gt;q_2&amp;lt;/math&amp;gt; jeweils &amp;lt;math&amp;gt;\frac{1}{2}&amp;lt;/math&amp;gt;, und die Gesamtwahrscheinlichkeit '''q''', dass ''keiner'' der beiden Fehler gefunden wird, ist deren Produkt: '''q''' = &amp;lt;math&amp;gt;q_1*q_2 = \frac{1}{2}* \frac{1}{2} = \frac{1}{4}&amp;lt;/math&amp;gt;.        &lt;br /&gt;
&lt;br /&gt;
:(b) in verschiedenen Zeilen, gleichen Spalten, z.B. &amp;lt;math&amp;gt;c_{11}&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c_{21}&amp;lt;/math&amp;gt;. Es gilt: Der Fehler in &amp;lt;math&amp;gt;c_{11}&amp;lt;/math&amp;gt; wird gefunden, wenn &amp;lt;math&amp;gt;\delta_1 \ne \gamma_1 \Leftrightarrow \alpha_1\ne 0&amp;lt;/math&amp;gt;. Das gleiche gilt für den Fehler in &amp;lt;math&amp;gt;c_{21}&amp;lt;/math&amp;gt;. Die Wahrscheinlichkeit '''q''', dass ''keiner'' der beiden Fehler gefunden wird, ist demzufolge: '''q''' = &amp;lt;math&amp;gt;\frac{1}{2}&amp;lt;/math&amp;gt;.&lt;br /&gt;
               &lt;br /&gt;
:(c) in der gleichen Zeile, z.B. &amp;lt;math&amp;gt;c_{11}&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c_{12}&amp;lt;/math&amp;gt;. Es gilt: Der Fehler wird gefunden, wenn &amp;lt;math&amp;gt;\delta_1 \ne \gamma_1 \Leftrightarrow \alpha_1*c_{11}+\alpha_2*c_{12}\ne 0&amp;lt;/math&amp;gt;. Hier treten nun zwei ungünstige Fälle auf: &lt;br /&gt;
::1) Der Fehler wird u.a. dann nicht gefunden, wenn &amp;lt;math&amp;gt;\alpha_1 = \alpha_2=0&amp;lt;/math&amp;gt;. Die Wahrscheinlichkeit dafür ist  wieder '''q'''=&amp;lt;math&amp;gt;\frac{1}{4}&amp;lt;/math&amp;gt;&lt;br /&gt;
::2) &amp;lt;math&amp;gt;\alpha_1=\alpha_2=1&amp;lt;/math&amp;gt; (dies geschieht ebenfalls mit Wahrscheinlichkeit &amp;lt;math&amp;gt;\frac{1}{4}&amp;lt;/math&amp;gt;), aber die Werte &amp;lt;math&amp;gt;c_{11}&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;c_{12}&amp;lt;/math&amp;gt; sind &amp;quot;zufälligerweise&amp;quot; so falsch, dass sich die Fehler gegenseitig aufheben. Die Wahrscheinlichkeit, dass beide Bedingungen gelten, ist auf jeden Fall '''q''' =  &amp;lt;math&amp;gt;\epsilon&amp;lt;\frac{1}{4}&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Analog behandelt man die Fälle, dass C drei oder vier Fehler enthält. Fasst man die Fälle zusammen, ergibt sich, dass die Wahrscheinlichkeit, einen vorhandenen Fehler '''nicht''' zu entdecken, sicher kleiner als &amp;lt;math&amp;gt;\frac{1}{2}&amp;lt;/math&amp;gt; ist. Dies gilt auch allgemein:&lt;br /&gt;
&lt;br /&gt;
;Satz:&lt;br /&gt;
*Die Wahrscheinlichkeit, dass Freivalds Algorithmus einen vorhandenen Fehler '''nicht''' findet, ist '''q''' &amp;lt; &amp;lt;math&amp;gt;\frac{1}{2}&amp;lt;/math&amp;gt;. Wir haben diesen Satz oben für N=2 bewiesen, ein vollständiger Beweis findet sich in der [http://en.wikipedia.org/wiki/Freivald's_algorithm#Error_Analysis Wikipedia].&lt;br /&gt;
 &lt;br /&gt;
;Folgerung: &lt;br /&gt;
*Lässt man Freivalds Algorithmus mit verschiedenen &amp;lt;math&amp;gt;\alpha&amp;lt;/math&amp;gt; k-mal laufen, gilt &amp;lt;math&amp;gt;q_k &amp;lt; 2^{-k}&amp;lt;/math&amp;gt; für die Wahrscheinlichkeit, dass '''keiner''' der k Durchläufe einen vorhandenen Fehler findet. Diese Wahrscheinlichkeit konvergiert sehr schnell gegen 0. Das heißt, der Algorithmus findet mit beliebig hoher Wahrscheinlichkeit ein Gegenbeispiel zu H (falls es eins gibt), wenn man ihn nur genügend oft mit jeweils anderen Zufallszahlen wiederholt. Daraus folgt, dass Testen ein effektives Fehlersuchverfahren sein kann -- die oben erwähnte Einschränkung von Dijktra trifft zwar zu, aber Tests, die mit so hoher Wahrscheinlichkeit funktionieren, sind für die Praxis meistens vollkommen ausreichend.&lt;br /&gt;
&lt;br /&gt;
=== Vergleich formaler Korrektheitsbeweis und Testen ===&lt;br /&gt;
&lt;br /&gt;
Nachdem nun die formalen Methoden sowie der Software-Test vorgestellt worden sind, ist nun die Frage aufzugreifen, welcher der beiden Vorgänge der bessere ist. Allgemein gilt:&lt;br /&gt;
&lt;br /&gt;
;randomisierte Algorithmen&lt;br /&gt;
               &lt;br /&gt;
*sind schnell und einfach:&lt;br /&gt;
#da die Operationen einfach sind und wenig Zeit kosten&lt;br /&gt;
#des öfteren eine Auswahl vorgenommen wird ohne die Gesamtmenge näher zu betrachten&lt;br /&gt;
#die Auswahl selbst aufgrund einfacher Kriterien (bspw. zufällige Auswahl) erfolgt&lt;br /&gt;
*können Lösungen approximieren und liefern gute approximative Lösungen&lt;br /&gt;
&lt;br /&gt;
;formaler Korrektheitsbeweis mit deterministischen Algorithmen (siehe auch [http://de.wikipedia.org/wiki/Determinismus_(Algorithmus)])&lt;br /&gt;
  &lt;br /&gt;
*bei jedem Aufruf des Beweisers werden immer die selben Schritte durchlaufen&lt;br /&gt;
*keine Zufallswerte&lt;br /&gt;
*komplexer Aufbau&lt;br /&gt;
*oft sehr lange Laufzeit, z.B. mehrere Tage oder gar Monate&lt;br /&gt;
&lt;br /&gt;
Für die formalen Methoden spricht, dass man mit ihnen im Prinzip beweisen kann, dass H nun entweder tatsächlich falsch oder richtig ist. Die formalen Beweise bei realen Problemen sind allerdings so kompliziert, dass sie ebenfalls mit Computerhilfe erbracht werden müssen. Dadurch liegt auch hier keine 100%-ige Korrektheitsgarantie vor: Auch formale Methoden können zum falschen Ergebnis kommen, z.B. durch Hardwarefehler, Compilerbugs, oder unvorhergesehenes Umkippen von Bits (z.B. durch kosmische Strahlung -- diese Gefahr ist im Weltall sehr ernst zu nehmen). Die Möglichkeit von Hardwarefehlern wirkt sich auf die formalen Methoden wesentlich stärker aus, weil diese typischerweise wesentlich längere Laufzeiten haben als entsprechende Testalgorithmen. Es kann deshalb durchaus vorkommen, dass Tests eine höhere Erfolgswahrscheinlichkeit haben als ein formaler Beweis, wie die folgende Beispielrechnung zeigt. Wir nehmen an, dass die Hardware eine &amp;quot;Halbwertszeit&amp;quot; von 50 Millionen Sekunden hat, d.h. ein Hardwarefehler tritt im Durchschnitt etwa alle 20 Monate auf. Dann ist die Wahrscheinlichkeit, dass ein deterministischer Algorithmus '''nicht''' zum Ergebnis (oder zum falschen Ergebnis) kommt:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;math&amp;gt;q_{\mathrm{Beweis}} \approx 0.001&amp;lt;/math&amp;gt;, falls der Beweisalgorithmus 1 Tag benötigt,&lt;br /&gt;
* &amp;lt;math&amp;gt;q_{\mathrm{Beweis}} \approx 0.01&amp;lt;/math&amp;gt;, falls der Beweisalgorithmus 1 Woche benötigt,&lt;br /&gt;
* &amp;lt;math&amp;gt;q_{\mathrm{Beweis}} \approx 0.035&amp;lt;/math&amp;gt;, falls der Beweisalgorithmus 1 Monat benötigt.&lt;br /&gt;
&lt;br /&gt;
Zum Vergleich nehmen wir an, dass der entsprechende Softwaretest einmal pro Sekunde ausgeführt werden kann, und dass jeder Durchlauf den Fehler mit einer Wahrscheinlichkeit von &amp;lt;math&amp;gt;\frac{1}{2}&amp;lt;/math&amp;gt; '''nicht''' findet. Unter gleichzeitiger Berücksichtigung der Wahrscheinlichkeit von Hardwarefehlern gilt dann&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;math&amp;gt;q_{\mathrm{Test}} \approx 0.5&amp;lt;/math&amp;gt;, falls der Test 1-mal wiederholt wird,&lt;br /&gt;
* &amp;lt;math&amp;gt;q_{\mathrm{Test}} \approx 0.001&amp;lt;/math&amp;gt;, falls der Test 10-mal wiederholt wird,&lt;br /&gt;
* &amp;lt;math&amp;gt;q_{\mathrm{Test}} \approx 10^{-6}&amp;lt;/math&amp;gt;, falls der Test 100-mal wiederholt wird.&lt;br /&gt;
&lt;br /&gt;
Mit anderen Worten: hier ist das Testen vorzuziehen, weil es unter realistischen Bedingungen eine höhere Erfolgswahrscheinlichkeit hat als der formale Beweis. Leider gibt es bisher keine Theorie, mit deren Hilfe man für ein gegebenes Problem systematisch Tests konstruieren kann, deren Misserfolgswahrscheinlichkeit bei wiederholter Anwendung garantiert so schnell gegen Null konvergiert wie die des Freivalds Algorithmus. Dies ist ein offenes Problem der Informatik.&lt;br /&gt;
&lt;br /&gt;
==Anwendung des Softwaretestverfahren==&lt;br /&gt;
===Beispiel an Python-Code===&lt;br /&gt;
&lt;br /&gt;
Man betrachte die Aufgabe, aus einer Zahl x die Wurzel zu ziehen. Dies kann man erreichen, indem man mit Hilfe des Newtonschen Iterationsverfahrens eine Nullstelle des Polynoms &lt;br /&gt;
:&amp;lt;math&amp;gt;f(y) = x - y^2 = 0&amp;lt;/math&amp;gt; &lt;br /&gt;
sucht. Ist eine Näherungslösung &amp;lt;math&amp;gt;y^{(t)}&amp;lt;/math&amp;gt; bekannt, erhält man eine bessere Näherung durch&lt;br /&gt;
:&amp;lt;math&amp;gt;y^{(t+1)} = y^{(t)} - \frac{f(y^{(t)})}{f'(y^{(t)})}&amp;lt;/math&amp;gt;.&lt;br /&gt;
Mit &amp;lt;math&amp;gt;f\,'(y) = -2y&amp;lt;/math&amp;gt; wird das zu&lt;br /&gt;
:&amp;lt;math&amp;gt;y^{(t+1)} = y^{(t)} + \frac{x-(y^{(t)})^2}{2y^{(t)}}=\frac{y^{(t)}+x/y^{(t)}}{2}&amp;lt;/math&amp;gt;. &lt;br /&gt;
Im Spezialfall des Wurzelziehens war diese Newton-Iteration übrigens bereits im Altertum als [http://en.wikipedia.org/wiki/Babylonian_method#Babylonian_method Babylonische Methode] bekannt. Man kann dieselbe durch das folgende (allerdings noch nicht korrekte) Pythonprogramm realisieren:&lt;br /&gt;
&lt;br /&gt;
           1   def sqrt(x):&lt;br /&gt;
           2       if (x&amp;lt;0):&lt;br /&gt;
           3           raise ValueError(&amp;quot;sqrt of negative number&amp;quot;)&lt;br /&gt;
           4       y = x / 2&lt;br /&gt;
           5       while y*y != x:&lt;br /&gt;
           6           y =(y + x/y) / 2&lt;br /&gt;
           7       return y:&lt;br /&gt;
&lt;br /&gt;
Für den oben aufgeführten Pythoncode können Tests mit Hilfe des Python-Moduls &amp;quot;[http://docs.python.org/library/unittest.html unittest]&amp;quot; geschrieben werden (siehe auch Übungsaufgaben). Wir erklären hier die wichtigsten Befehle aus diesem Modul. Wir implementieren eine Testfunktionen (diese muss, wie im Python-Handbuch beschrieben, Methode einer Testklasse sein).&lt;br /&gt;
&lt;br /&gt;
   class SqrtTest(unittest.TestCase):&lt;br /&gt;
     def testsqrt(self): &lt;br /&gt;
         ...&lt;br /&gt;
&lt;br /&gt;
Zunächst muss man prüfen, ob die Vorbedingung korrekt getestet wird, d.h. ob bei einer negativen Zahl x eine Exception ausgelöst wird; dafür benötigt man &lt;br /&gt;
&lt;br /&gt;
         self.assertRaises(ValueError, sqrt, -1) &lt;br /&gt;
Sollte keine Exception vom Type &amp;lt;tt&amp;gt;ValueError&amp;lt;/tt&amp;gt; ausgelöst werden, dann würde der Test hier einen Fehler signalisieren. Dieser Test funktioniert aber.&lt;br /&gt;
&lt;br /&gt;
Weiter testen wir einige Beispiele, deren Wurzel wir kennen:&lt;br /&gt;
&lt;br /&gt;
         self.assertEqual(sqrt(9),3) &lt;br /&gt;
Wäre hier das Ergebnis ungleich 3, würde ebenfalls ein Fehler signalisiert, aber es funktioniert in unserem Falle. Der Test&lt;br /&gt;
&lt;br /&gt;
         self.assertEqual(sqrt(1),1)&lt;br /&gt;
schlägt jedoch mit &amp;lt;tt&amp;gt;ZeroDivisionError&amp;lt;/tt&amp;gt; fehl! Wir sehen, dass in Zeile 4 eine Ganzzahldivision durchgeführt wird, deren Ergebnis stets abgerundet wird, was hier zu &amp;lt;tt&amp;gt;y = 0&amp;lt;/tt&amp;gt; und damit zum Fehler in Zeile 6 führt. Wieso hat dann aber der erste Test &amp;lt;tt&amp;gt;sqrt(9) == 3&amp;lt;/tt&amp;gt; funktioniert? Hier gilt &amp;lt;tt&amp;gt;x / 2 == 4&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;x / y == 2&amp;lt;/tt&amp;gt; (jeweils nach Abrunden), und der Mittelwert der beiden Schätzungen ist gerade &amp;lt;tt&amp;gt;y == 3&amp;lt;/tt&amp;gt;, also zufällig das richtige Ergebnis. Allgemein sehen wir jedoch, dass es nicht korrekt ist, mit ganzen Zahlen zu rechnen. Wir müssen also den Input zunächst in einen Gleitkommawert umwandeln:&lt;br /&gt;
&lt;br /&gt;
           1   def sqrt(x):&lt;br /&gt;
           1a      x = float(x)&lt;br /&gt;
           2       if (x&amp;lt;0):&lt;br /&gt;
           3           raise ValueError(&amp;quot;sqrt of negative number&amp;quot;)&lt;br /&gt;
           4       y = x / 2&lt;br /&gt;
           5       while y*y != x:&lt;br /&gt;
           6           y =(y + x/y) / 2&lt;br /&gt;
           7       return y:&lt;br /&gt;
&lt;br /&gt;
Jetzt funktionieren die vorhandenen Tests, aber bei anderen Zahlen (z.B. &amp;lt;tt&amp;gt;x = 1.21&amp;lt;/tt&amp;gt;) läuft das Programm in eine Endlosschleife. Dies liegt daran, dass durch die beschränkte Genauigkeit der Gleitkomma-Darstellung selten exakte Gleichheit in der &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Bedingung erreicht wird. Man darf nicht auf Gleichheit prüfen, sondern muss den relativen Fehler beschränken:&lt;br /&gt;
&lt;br /&gt;
           1   def sqrt(x):&lt;br /&gt;
           1a      x = float(x)&lt;br /&gt;
           2       if (x&amp;lt;0):&lt;br /&gt;
           3           raise ValueError(&amp;quot;sqrt of negative number&amp;quot;)&lt;br /&gt;
           4       y = x / 2&lt;br /&gt;
           5       while abs(1.0 - x / y**2) &amp;gt; 1e-15:  # check for relative difference&lt;br /&gt;
           6           y =(y + x/y) / 2&lt;br /&gt;
           7       return y:&lt;br /&gt;
&lt;br /&gt;
Jetzt terminiert das Programm, aber der Test&lt;br /&gt;
&lt;br /&gt;
        self.assertEqual(sqrt(1.21)**2, 1.21)  # schlägt fehl&lt;br /&gt;
&lt;br /&gt;
schlägt wegen der beschränkten Genauigkeit der Gleitkommadarstellung fehl. Man umgeht dieses Problem, indem man im Test selbst nur näherungsweise Gleichheit fordert, z.B. auf 15 Dezimalstellen genau (bei 16 Dezimalen würde es nicht mehr funktionieren):&lt;br /&gt;
&lt;br /&gt;
        self.assertAlmostEqual(sqrt(1.21)**2, 1.21, 15)&lt;br /&gt;
&lt;br /&gt;
Wenden wir jetzt das ''Prinzip der Condition Coverage'' an (siehe unten), sehen wir, dass die &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Bedingung bei allen bisherigen Tests zunächst mindestens einmal &amp;lt;tt&amp;gt;true&amp;lt;/tt&amp;gt; gewesen ist. Ein weiterer sinnvoller Tests ist deshalb einer, der diese Bedingung sofort &amp;lt;tt&amp;gt;false&amp;lt;/tt&amp;gt; macht. Dies trifft z.B. bei &amp;lt;tt&amp;gt;x == 4&amp;lt;/tt&amp;gt; zu, weil &amp;lt;tt&amp;gt;y = x / 2&amp;lt;/tt&amp;gt; hier gerade die korrekte Wurzel liefert. Wir fügen deshalb den Test&lt;br /&gt;
&lt;br /&gt;
         self.assertEqual(sqrt(4), 2) &lt;br /&gt;
&lt;br /&gt;
hinzu, der erfolgreich verläuft. Das ''Prinzip der Domänen-Zerlegung'' (siehe unten) führt uns weiter dazu, die Wurzel aus Null als sinnvollen Test zu betrachten, weil die Null am Rand des erlaubten Wertebereichs liegt. Der Test&lt;br /&gt;
&lt;br /&gt;
        self.assertEqual(sqrt(0), 0)  # schlägt fehl&lt;br /&gt;
&lt;br /&gt;
schlägt in der Tat mit einem &amp;lt;tt&amp;gt;ZeroDivisionError&amp;lt;/tt&amp;gt; fehl: In der Abfrage der &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Bedingung wird jetzt durch &amp;lt;tt&amp;gt;y == 0&amp;lt;/tt&amp;gt; geteilt. Wir können diesen Fehler beheben, indem wir die Division aus der Bedingung eliminieren:&lt;br /&gt;
&lt;br /&gt;
           1   def sqrt(x):&lt;br /&gt;
           1a      x = float(x)&lt;br /&gt;
           2       if (x&amp;lt;0):&lt;br /&gt;
           3           raise ValueError(&amp;quot;sqrt of negative number&amp;quot;)&lt;br /&gt;
           4       y = x / 2&lt;br /&gt;
           5       while abs(y**2 - x) &amp;gt; 1e-15*x:  # check for relative difference without division&lt;br /&gt;
           6           y =(y + x/y) / 2&lt;br /&gt;
           7       return y:&lt;br /&gt;
&lt;br /&gt;
Damit ist auch dieses Problem behoben. Wir sehen also, wie das systematische Testen uns dabei hilft, Fehler im Programm zu finden und zu eliminieren. Eine ausführbare Version dieses Beispiels finden Sie im File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/lehre/Algorithmen2012/SquareRootDebugging.py SquareRootDebugging.py].&lt;br /&gt;
&lt;br /&gt;
===Definition guter Tests===&lt;br /&gt;
&lt;br /&gt;
Wir haben gezeigt, dass Testen eine effektive Methode ist, um Fehler in Algorithmen zu finden. Allerdings gilt das nur, wenn Tests und Testdaten geschickt gewählt werden. Wir zeigen bewährte Methoden dafür. &lt;br /&gt;
&lt;br /&gt;
====Häufige Fehler====&lt;br /&gt;
&lt;br /&gt;
Einige Fehlerklassen treten sehr häufig auf und sollten deshalb beim Testen besondere Aufmerksamkeit genießen:&lt;br /&gt;
; [http://en.wikipedia.org/wiki/Off-by-one_error Off-by-One] : Dieser Fehler bezeichnet den Fall, dass eine Berechnung oder Bedingung um Eins neben dem korrekten Wert liegt. Dies passiert besonders bei Schleifenindizes. Man schreibt beispielsweise &amp;lt;tt&amp;gt;if i &amp;amp;lt; j:&amp;lt;/tt&amp;gt; wenn &amp;lt;tt&amp;gt;if i &amp;amp;lt;= j:&amp;lt;/tt&amp;gt; richtig gewesen wäre, oder &amp;lt;tt&amp;gt;a[i] = a[i+1]&amp;lt;/tt&amp;gt; wenn &amp;lt;tt&amp;gt;a[i-1] = a[i]&amp;lt;/tt&amp;gt; gemeint war. Die beste Methode um solche Fehler zu finden ist das manuelle Nachvollziehen des Algorithmus auf Papier für kleine Eingaben. Wenn die Schleife, die den Fehler enthält, beispielsweise nur bis zum Index 3 geht, erkennt man den off-by-one-Error meistens sofort, weil offensichtlich auf das falsche Element zugegriffen oder die Schleife zu früh abgebrochen wird.&lt;br /&gt;
; Integer-Überlauf : In vielen Sprachen (z.B. C und C++) sind die Integer-Datentypen so definiert, dass die Berechnung auf die kleinstmöglichen Zahl zurückspringt, wenn man zur größtmöglichen Zahl eins addiert (zyklisches Verhalten). Im Falle eines 8-bit Intergertyps gilt z.B.&lt;br /&gt;
 uint8 i = 255; // größtmögliche 8-bit Zahl&lt;br /&gt;
 i += 1;&lt;br /&gt;
 assert(i == 0); // zyklisches Verhalten&lt;br /&gt;
:und entsprechend:&lt;br /&gt;
 uint8 i = 0;&lt;br /&gt;
 i -= 1;&lt;br /&gt;
 assert(i == 255);&lt;br /&gt;
:Solche Fehler äußern sich typischerweise, wenn man versucht, viele kleine Zahlen zu addieren. Dieses Problem kann allerdings in Python nicht auftreten, weil Python automatisch zum Type &amp;lt;tt&amp;gt;long&amp;lt;/tt&amp;gt; (für beliebig große Zahlen) wechselt, wenn die Werte zu groß werden.&lt;br /&gt;
; Float-Überlauf : Ein ähnlicher Fehler kann auch bei Gleitkommazahlen auftreten, wenn man zur größten exakt darstellbaren ganzen Zahl eins addiert. Die Grenze hängt hier von der Länge der Mantisse ab. Für 32-bit Gleitkommazahlen (23 bit Mantisse) gilt beispielsweise:&lt;br /&gt;
 float32 f = pow(2.0, 24); // dies ist die größte ganze Zahl, die float32 exakt darstellen kann&lt;br /&gt;
 f += 1.0;&lt;br /&gt;
 assert(f == pow(2.0, 24));&lt;br /&gt;
:Im Unterschied zum Integerverhalten hat die Addition hier gar keinen Effekt. Bei 64-bit Gleitkommazahlen tritt der Fehler entsprechend bei &amp;lt;tt&amp;gt;pow(2.0, 53)&amp;lt;/tt&amp;gt; auf. &lt;br /&gt;
; [http://en.wikipedia.org/wiki/Loss_of_significance Loss-of-Precision] : Dieser Fehler besagt, dass Gleitkommazahlen unter bestimmten Bedingungen ihre Genauigkeit verlieren und dann ungenaue oder sogar unsinnige Ergenisse herauskommen. Dies passiert beispielsweise, wenn man fast gleich große Zahlen voneinander subtrahiert. Dann sind die höherwertigen Bits der Eingaben gleich und löschen sich bei der Subtraktion aus, so dass das Ergebnis nur noch sehr wenige gültige Bits hat und somit sehr ungenau ist. Bei 6-stelliger Dezimaldarstellung wäre z.B. &amp;lt;tt&amp;gt;100.003 - 100.002 = 0.001&amp;lt;/tt&amp;gt;, und das Ergebnis hat nur noch eine gültige Dezimalstelle. Dies ist ungünstig, weil die Eingaben ja nur gerundete Darstellungen der wahren Werte sind. Mit 12-stelliger Arithmetik hätte man vielleicht die Zahlen &amp;lt;tt&amp;gt;100.002634611 - 100.002456354 = 0.000178257&amp;lt;/tt&amp;gt; erhalten, und das ursprüngliche Resultat &amp;lt;tt&amp;gt;0.001&amp;lt;/tt&amp;gt; ist mehr als 5-mal zu groß. In der Praxis beobachtet man dieses Problem z.B. beim Lösen von quadratischen Gleichungen. &amp;lt;br&amp;gt;&lt;br /&gt;
:Ein verwandtes Problem tritt auf, wenn das exakte Ergebniss gleich Null sein sollte. Durch die begrenzte Genauigkeit der Gleitkommaoperationen kommen dann häufig von Null verschiedene kleine Zahlen heraus. Beispielsweise erhält man unter Python &amp;lt;tt&amp;gt;sin(pi) = 1.2246467991473532e-16&amp;lt;/tt&amp;gt;, obwohl das Ergebnis Null sein sollte. Daraus folgt, dass man Gleitkommazahlen nicht zuverlässig auf Gleichheit testen kann, weil der Test &amp;lt;tt&amp;gt;f1 == f2&amp;lt;/tt&amp;gt; equivalent zum Test &amp;lt;tt&amp;gt;(f1 - f2) == 0.0&amp;lt;/tt&amp;gt; ist und meistens fehlschlägt, auch wenn die Zahlen theoretisch gleich sein müssten. &amp;lt;br&amp;gt;&lt;br /&gt;
:Man vermeidet derartige Probleme durch geschicktes algebraisches Umformen der Formeln und durch das Einbauen geeigneter Fehlertoleranzen (z.B. testet man statt auf Gleichheit auf den Ausdruck &amp;lt;tt&amp;gt;abs(f1 -f2) &amp;lt;= 3e-16&amp;lt;/tt&amp;gt;, siehe das Beispiel zum &amp;lt;tt&amp;gt;sqrt()&amp;lt;/tt&amp;gt;-Algorithmus oben).&lt;br /&gt;
; Randwertfehler : Wenn ein Algorithmus verschiedene Eingabedomänen hat, für die er sich prinzipiell anders verhält (der Algorithmus für die Quadratwurzel berechnet z.B. das Ergebnis für nicht-negative Eingaben, aber signalisiert einen Fehler für negative Eingaben), dann treten Bugs besonders gern an der Domänengrenze auf. Bei der Wurzel wäre das der Randwert 0, das heisst &amp;lt;tt&amp;gt;sqrt(0)&amp;lt;/tt&amp;gt; verhält sich anders als erwartet (z.B. könnte es einen &amp;lt;tt&amp;gt;ValueError&amp;lt;/tt&amp;gt; auslösen, weil der Test &amp;lt;tt&amp;gt;if x &amp;amp;lt; 0.0:&amp;lt;/tt&amp;gt; fälschlicherweise als &amp;lt;tt&amp;gt;if x &amp;amp;lt;= 0.0:&amp;lt;/tt&amp;gt; geschrieben wurde, oder es passiert eine Division durch Null, weil der Spezialfall nicht richtig abgefangen wurde - siehe das tt&amp;gt;sqrt()&amp;lt;/tt&amp;gt;-Beispiel oben). Gute Testprogramme enthalten immer auch Tests für die Randwerte.&lt;br /&gt;
&lt;br /&gt;
====Generieren von Referenzdaten====&lt;br /&gt;
&lt;br /&gt;
Wie immer man die Tests definiert hat, muss man am Ende die Ausgabe des Algorithmus mit dem korrekten Ergebnis vergleichen. Man bezeichnet ein bekanntes korrektes Ergebnis als ''Referenz-Ergebnis''. Dieses muss man aber erst einmal kennen, was sich mitunter als schwierig erweist. Folgende Verfahren haben sich als zweckmäßig erwiesen:&lt;br /&gt;
* Bei bestimmten Eingaben ist das Ergebnis für den Menschen einfach zu bestimmen, für den Algorithmus ist diese Eingabe aber ebenso schwierig wie jede andere. Dies gilt zum Beispiel für die Quadratzahlen im obigen Beispiel: der Algorithmus kennt keine Quadratzahlen und behandelt sie wie jede andere reelle Zahl. Deshalb eignen sich die Quadratzahlen zum Testen. Auch beim Sortieren kleiner Listen kann die korrekte Sortierung leicht bestimmt und als Referenz-Ergebnis abgespeichert werden. Der Test vergleicht dann einfach die Ausgabe des Sortieralgorithmus mit dem Referenz-Ergebnis.&lt;br /&gt;
* Oft kann man das korrekte Ergenis mit einem alternativen Verfahren berechnen. Dies gilt insbesondere, wenn man einen effizienten, aber komplizierten Algorithmus testen will. Dann berechnet man die Referenz-Ergebnisse mit einem langsamen, aber einfachen Verfahren. Dies ist möglich, weil man die Referenz-Ergebnisse ja abspeichern kann und der langsame Algorithmus daher nur wenige Male benutzt werden muss. Beispielsweise kann man einen komplizierten Sortieralgorithmus (Quicksort) mit Hilfe von selection sort testen.&lt;br /&gt;
* In vielen Fällen steht ein alternatives Programm zur Verfügung, z.B. eine ältere Version des zu testenden Programms, oder ein kommerzielles Programm (bzw. eine Demoversion), das dasselbe Problem löst, aber im aktuellen Kontext nicht verwendet werden kann (weil es z.B. zu teuer ist, oder nur auf einem Mac läuft). Diese Methode bietet sich auch an, wenn man einen Algorithmus aus einer Programmiersprache in eine andere portieren muss. &lt;br /&gt;
* Manchmal kann das korrekte Ergebnis nicht direkt angegeben werden, aber man kennt bestimmte Eigenschaften. Beim Sortieren kann man z.B. testen, dass kein Element des sortierten Arrays größer ist als das darauffolgende. Man testet also die Nachbedingungen. Eine abgeschwächte Versionen dieser Methode wird für randomisierte Algorithmen verwendet: Ist die Wahrscheinlichkeitsverteilung der Testeingaben bekannt, kann man die Wahrscheinlichkeitsverteilung der Ergebnisse, oder zumindest wichtige Eigenschaften wie z.B. den Mittelwert, mathematisch vorhersagen. Der Test ermittelt dann, ob die Ausgaben über viele Durchläufe des Algorithmus diese statistischen Eigenschaften aufweisen.&lt;br /&gt;
&lt;br /&gt;
====Arten von Tests====&lt;br /&gt;
&lt;br /&gt;
Man unterscheidet 3 grundlegende Arten von Tests:&lt;br /&gt;
&lt;br /&gt;
;Black-box Tests [http://en.wikipedia.org/wiki/Black_box_testing]: Hier ist dem Tester nur die Spezifikation, aber nicht die Implementation des Algorithmus bekannt. Alle Tests sowie die Eingaben und Referenz-Ergebnisse müssen aus der Spezifikation abgeleitet werden. Die automatisierte Generierung guter Tests aus der Spezifikation ist ein aktives Forschungsgebiet.&lt;br /&gt;
;Gray-box Tests (auch Glass-box Tests) [http://www.cse.fau.edu/~maria/COURSES/CEN4010-SE/C13/glass.htm]: Hier kennt der Tester auch die Implementation und kann dadurch Tests entwerfen, die für diese spezielle Implementation besonders aussagekräftig sind. Es besteht allerdings die Gefahr, dass der Tester nicht mehr unvoreingenommen an das Testproblem herangeht, und Zustände, die seiner Meinung nach gar nicht vorkommen können, auch nicht testet (erst später stellt sich heraus, dass diese Zustände doch vorkommen).&lt;br /&gt;
;White-box Tests [http://en.wikipedia.org/wiki/White_box_testing]: Hier kann der Tester die Implementation sogar in geeigneter Weise verändern, z.B. &lt;br /&gt;
:* explizite Tests für Vor- und Nachbedingungen (&amp;quot;Assertions&amp;quot;) einbauen. Dies bietet sich insbesondere in der alpha- und beta-Testphase eines Programms an, um Fehler schnell zu lokalisieren. Auch die unter Windows bekannte Dialogbox &amp;quot;Diesen Fehler bitte auch an Microsoft melden&amp;quot; wird durch solche eingebauten Assertions ausgelöst, wenn das Programm in einen illegalen Zustand geraten ist und abgebrochen werden muss.&lt;br /&gt;
:* zusätzlichen Code einbauen, der feststellt, ob alle Teile des Programms auch tatsächlich getestet wurden (&amp;quot;[http://blogs.msdn.com/phuene/archive/2007/05/03/code-coverage-instrumentation.aspx code coverage instrumentation]&amp;quot;). Dieser Code gibt nach dem Testen z.B. aus, welche Programmzeilen von keinem existierenden Test aufgerufen worden sind. Wenn der ausgeführte Code sehr stark von den Daten abhängt (z.B. bei interaktiven Programmen), kann es sehr schwierig sein, die ''coverage'' auf andere Weise festzustellen.&lt;br /&gt;
:* absichtlich Bugs einbauen (die automatisch wieder abgeschaltet werden, wenn das Testen vorbei ist). Durch diese &amp;quot;[http://en.wikipedia.org/wiki/Fault_injection fault injection]&amp;quot; kann man herausfinden, ob die Tests mächtig genug sind, vorhandene Bugs zu finden.&lt;br /&gt;
&lt;br /&gt;
====Prinzipien für die Generierung von Testdaten====&lt;br /&gt;
&lt;br /&gt;
;Prinzip der Regressionstests (&amp;quot;[http://en.wikipedia.org/wiki/Regression_testing Regression testing]&amp;quot;): Häufig werden Tests während der Programmentwicklung verwendet, um einen Algorithmus zu debuggen. Sobald der Algorithmus aber funktioniert werden die Tests gelöscht, denn sie werden ja jetzt nicht mehr gebraucht. Dies ist ein schwerwiegender ''Fehler'': Jedes erfolgreiche Programm muss früher oder später weiterentwickelt werden (zumindest die Anpassung an eine neue Betriebssystemversion ist ab und zu notwendig). Jede Änderung birgt aber die Gefahr, dass sich neue Bugs in bisher funktionierenden Code einschleichen. Man sollte deshalb alle Tests aufheben und in einer ''test suite'' sammeln. Durch diese &amp;quot;regression tests&amp;quot; kann man nach jeder Änderung feststellen, ob die alte Funktionalität noch intakt ist, und gegebenenfalls die letzte Änderung einfach rückgängig machen. Tut man dies nicht, kann die Gefahr von unbeabsichtigten destruktiven Änderungen so groß werden, dass das Programm gar nicht mehr weiterentwickelt werden kann. Dies wird drastisch durch den bekannten Spruch &amp;quot;never change a running program&amp;quot; ausgedrückt.&lt;br /&gt;
&lt;br /&gt;
;Prinzip der äquivalenten Eingaben (Domain Partitioning oder Equivalence Partitioning) [http://en.wikipedia.org/wiki/Equivalence_partitioning]: Für ähnliche Eingaben verhält sich ein Algorithmus normalerweise ähnlich, und es hat keinen Sinn, alle diese Eingaben zu testen. Statt dessen teilt (partitioniert) man die Eingabedomäne in Äquivalenzklassen, die vom Algorithmus im wesentlichen gleich behandelt werden. Im obigen Beispiel der Wurzelberechnung ergeben sich zwei Klassen aus der Spezifikation: die negativen Zahlen (für die die Wurzel undefiniert ist und deshalb ein Fehler signalisiert werden muss) und die nicht-negativen Zahlen. Wenn man auch den Quellcode kennt (gray-box testing), kann man die Eingaben oft feiner unterteilen. Z.B. werden häufig unterschiedliche Algorithmen für kleine und für große Eingaben benutzt. Viele Quicksort-Implementationen verwenden beispielsweise für Arrays mit höchstens vier Elementen ein explizites Sortierverfahren, für Arrays der Länge 5 bis 25 selection sort, und erst für größere Arrays das eigentliche Quicksort. Aus der Einteilung der Eingabedomäne ergeben sich zwei wichtige Regeln für die Wahl der Testdaten:&lt;br /&gt;
:* Aus jeder Äquivelenzklasse wählt man mindestens einen typischen Vertreter, um das normale Verhalten des Algorithmus in jedem Fall zu testen.&lt;br /&gt;
:* Aus jeder Äquivelenzklasse wählt man Randwerte, weil gerade bei diesen Werten am häufigsten Fehler gemacht werden. Im obigen Wurzelbeispiel ist der Randwert die Null, die in der Tat in einer Version des Algorithmus zu einem &amp;lt;TT&amp;gt;ZeroDivisionError&amp;lt;/tt&amp;gt; geführt hat. Andere typische Randfehler sind, dass Randelemente dem falschen Algorithmenzweig zugeordnet werden (z.B. wenn bei unserem Wurzelbeispiel die Abfrage am Anfang &amp;lt;tt&amp;gt;if x &amp;lt;= 0:&amp;lt;/tt&amp;gt; statt &amp;lt;tt&amp;gt;if x &amp;lt; 0:&amp;lt;/tt&amp;gt; gewesen wäre), dass Schleifen um einen Index zu spät beginnen oder zu früh abbrechen (&amp;quot;[http://en.wikipedia.org/wiki/Off-by-one_error Off-by-one errors]&amp;quot;), oder dass ein seltener Randfall gar nicht implementiert ist und einfach zum Absturz führt.&lt;br /&gt;
&lt;br /&gt;
;Prinzip, den Fehler zu reproduzieren (Failure Reproduction): Wenn ein Bug gemeldet wird, welches die Tests bisher übersehen haben, fügt man einen Test hinzu, der dieses Bug findet. Im Zusammenhang mit regression tests ist damit sichergestellt, dass dasselbe Bug nicht noch einmal auftreten kann.&lt;br /&gt;
&lt;br /&gt;
;Prinzip der Code Coverage [http://en.wikipedia.org/wiki/Code_coverage]: Hier stellt man sicher, dass tatsächlich der gesamte Code (oder ein vorher festgelegter hoher Prozentsatz) getestet wurde. Gerade bei komplizierten interaktiven Programmen ist diese &amp;quot;code coverage&amp;quot; mitunter nicht leicht zu erreichen, weil manche Programmteile nur bei sehr seltenen oder obskuren Eingaben ausgeführt werden. Eine minimale code coverage erreicht man allerdings bereits, wenn man in einem black-box-Test die Testdaten nach dem Prinzip der äquivalenten Eingaben auswählt, weil dann aus jeder Äquivalenzklasse mindestens ein Vertreter getestet wird. Im Allgemeinen muss man aber den Quellcode zumindest kennen (gray-box-Test), um geeignete Testdaten für code coverage zu identifizieren. Code coverage kann in verschiednen Graden angestrebt werden&lt;br /&gt;
:* Function coverage: Jede Funktion eines Programms sollte mindestens einmal aufgerufen werden.&lt;br /&gt;
:* Statement coverage: Jedes Statement (d.h. im wesentlichen jede Programmzeile) sollte mindestens einmal ausgeführt werden. Im obigen Wurzelbeispiel erfordert dies, dass z.B. mindestens einmal eine negative Zahl getestet wird, um die Exception zu prüfen.&lt;br /&gt;
:* Condition coverage: Jede Bedingung (explizit in &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Bedingungen, implizit in den Abbruchbedingungen von &amp;lt;tt&amp;gt;for&amp;lt;/tt&amp;gt;- und &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Schleifen) sollte mindestens einmal mit dem Ergebnis &amp;lt;tt&amp;gt;True&amp;lt;/tt&amp;gt; und einmal mit dem Ergebnis &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt; durchlaufen werden. Im Wurzelbeispiel haben wir die Eingabe &amp;lt;tt&amp;gt;x = 4&amp;lt;/tt&amp;gt; gewählt, damit die &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Schleife auch einmal beim ersten Aufruf sofort &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt; liefert.&lt;br /&gt;
:* Path coverage: Jeder Programmpfad (d.h. jede Kombination von Wahrheitswerten bei allen Bedingungen) sollte einmal ausgeführt werden. Dies ist im Allgemeinen unerreichbar, weil es unendlich viele, oder zumindest zu viele verschiedene Pfade gibt.&lt;br /&gt;
:Die Qualität der Tests steigt, wenn eine hohe Coverage (am besten 100%) erreicht wird, und/oder man eine mächtigere Art von Coverage fordert.&lt;br /&gt;
&lt;br /&gt;
;Prinzip der erschöpfenden Tests: Wenn ein Algorithmus nur wenige mögliche Eingaben hat, kann man sämtliche Eingaben testen. Bei sehr wichtigen Algorithmen kann das auch dann noch sinnvoll sein, wenn es relativ viele mögliche Eingaben gibt. In den meisten Fällen ist es jedoch zu aufwändig.&lt;br /&gt;
&lt;br /&gt;
;Prinzip der vollständigen Paarung (Pair-wise coverage) [http://citeseer.ist.psu.edu/78354.html]: Wenn ein Algorithmus N Eingabeparameter hat, und jeder Parameter hat K&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; mögliche Werte, müssen bei der erschöpfenden Suche K&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;*...*K&amp;lt;sub&amp;gt;N&amp;lt;/sub&amp;gt; Kombinationen getestet werden. Beschränkt man sich in jedem Parameter auf typische Werte und Randwerte jeder Äquivalenzklasse, kann man K&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; zwar drastisch reduzieren, aber das Produkt K&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;*...*K&amp;lt;sub&amp;gt;N&amp;lt;/sub&amp;gt; wird immer noch sehr groß (bei 4 Parametern und nur 3 möglichen Werten pro Parameter hat man bereits 3&amp;lt;sup&amp;gt;4&amp;lt;/sup&amp;gt;=81 mögliche Kombinationen). Sei v&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; der j-te Wert des Parameters i. Anstatt zu versuchen, alle Kombinationen zu testen, kann man fordern, dass zumindest alle möglichen Paare v&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; und v&amp;lt;sub&amp;gt;mj&amp;lt;/sub&amp;gt; (i&amp;amp;ne;m) in mindestens einem Test vorkommen. Gibt es nur zwei Parameter, gewinnt man durch diese Einschränkung natürlich nichts, denn man muss mindestens K&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;*K&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; Tests durchführen. Hat man jedoch 3 Parameter, kann man mit weniger Tests auskommen als zuvor, da jeder Test bis zu drei verschiedene Paarungen abdecken kann (eine für den ersten und zweiten Parameter, eine für den ersten und dritten, eine für den zweiten und dritten). Bei vier Parametern werden sogar sechs Paarungen pro Test abgearbeitet usw. Die Theorie des &amp;quot;experimental design&amp;quot; beschreibt nun, wie man systematisch alle möglichen Paarungen mit möglichst wenigen Tests erzeugt. Es stellt sich heraus, dass man alle Paarungen von 3, 4 oder mehr Parametern oft mit genauso vielen Tests erzeugen kann wie bei 2 Parametern nötig wären. Dazu verwendet man die Methode der [http://en.wikipedia.org/wiki/Latin_square Latin Squares].  Wir beschreiben diese Methode für den einfachen Fall von 3 möglichen Werten pro Parameter.&lt;br /&gt;
&lt;br /&gt;
:Ein Latin Square der Größe 3 ist eine 3x3 Matrix, deren Einträge die Zahlen 1...3 sind, und zwar so, dass jede Zahl genau einmal in jeder Zeile und Spalte vorkommt (ähnlich wie beim Sudoku). Eine mögliche Matrix ist z.B.&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;math&amp;gt;P=\begin{pmatrix}1 &amp;amp; 2 &amp;amp; 3 \\&lt;br /&gt;
                      2 &amp;amp; 3 &amp;amp; 1 \\&lt;br /&gt;
                      3 &amp;amp; 1 &amp;amp; 2\end{pmatrix}&amp;lt;/math&amp;gt;&lt;br /&gt;
:Man bildet jetzt 9 Kombinationen der Zahlen 1...3, indem man zeilenweise durch die Matrix P geht, und den Zeilenindex (die Nummer der aktuellen Zeile) als erste Zahl, den Spaltenindex als zweite Zahl, und den Eintrag an der aktuallen Position als dritte Zahl verwendet. Man erhält&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;7&amp;quot; align=&amp;quot;center&amp;quot;&lt;br /&gt;
|-align=&amp;quot;center&amp;quot; &lt;br /&gt;
|&lt;br /&gt;
! Komb. 1&lt;br /&gt;
! Komb. 2&lt;br /&gt;
! Komb. 3&lt;br /&gt;
! Komb. 4&lt;br /&gt;
! Komb. 5&lt;br /&gt;
! Komb. 6&lt;br /&gt;
! Komb. 7&lt;br /&gt;
! Komb. 8&lt;br /&gt;
! Komb. 9&lt;br /&gt;
|-&lt;br /&gt;
!Zahl 1 (Zeilenindex)&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|-&lt;br /&gt;
! Zahl 2 (Spaltenindex)&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|-&lt;br /&gt;
! Zahl 3 (aktueller Matrixeintrag von P)&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
:Diese Tabelle bestimmt, welcher Wert in jedem Test für jeden Parameter verwendet wird. Z.B. wird der erste Test mit v&amp;lt;sub&amp;gt;11&amp;lt;/sub&amp;gt; (erster Wert des ersten Parameters), v&amp;lt;sub&amp;gt;21&amp;lt;/sub&amp;gt; (erster Wert des zweiten Parameters), v&amp;lt;sub&amp;gt;31&amp;lt;/sub&amp;gt; (erster Wert des dritten Parameters) aufgerufen&lt;br /&gt;
       assertEqual( foo(v11, v21, v31), foo_reference1)&lt;br /&gt;
:(reference1 ist das korrekte Referenz-Ergebnis für diese Parameterbelegung). Der letzte Test hat die Parameter v&amp;lt;sub&amp;gt;13&amp;lt;/sub&amp;gt;, v&amp;lt;sub&amp;gt;23&amp;lt;/sub&amp;gt;, v&amp;lt;sub&amp;gt;32&amp;lt;/sub&amp;gt;&lt;br /&gt;
       assertEqual( foo(v13, v23, v32), foo_reference9)&lt;br /&gt;
:Man überzeugt sich leicht, dass diese 9 Tests jede mögliche Paarung genau einmal enthalten. Hat der Algorithmus 4 Parameter, benötigt man einen zweiten Latin Square, der zum ersten orthogonal ist. Zwei Latin Squares P und Q heißen orthogonal, wenn alle Paare c&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt;=(P&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt;, Q&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt;) eindeutig sind, d.h. es gilt c&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt;&amp;amp;ne;c&amp;lt;sub&amp;gt;kl&amp;lt;/sub&amp;gt; falls i&amp;amp;ne;k und j&amp;amp;ne;l. Ein zu dem obigen P orthogonales Q ist z.B.&lt;br /&gt;
:&amp;lt;math&amp;gt;Q=\begin{pmatrix}1 &amp;amp; 2 &amp;amp; 3 \\&lt;br /&gt;
                        3 &amp;amp; 1 &amp;amp; 2 \\&lt;br /&gt;
                        2 &amp;amp; 3 &amp;amp; 1\end{pmatrix}&amp;lt;/math&amp;gt;&lt;br /&gt;
: Jetzt bildet man Kombinationen aus 4 Zahlen, indem man zur obigen Tabelle noch eine vierte Zeile hinzufügt, die die aktuellen Einträge von Q für den jeweiligen Zeilen- und Spaltenindex enthält:&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;7&amp;quot; align=&amp;quot;center&amp;quot;&lt;br /&gt;
|-align=&amp;quot;center&amp;quot; &lt;br /&gt;
|&lt;br /&gt;
! Komb. 1&lt;br /&gt;
! Komb. 2&lt;br /&gt;
! Komb. 3&lt;br /&gt;
! Komb. 4&lt;br /&gt;
! Komb. 5&lt;br /&gt;
! Komb. 6&lt;br /&gt;
! Komb. 7&lt;br /&gt;
! Komb. 8&lt;br /&gt;
! Komb. 9&lt;br /&gt;
|-&lt;br /&gt;
!Zahl 1 (Zeilenindex)&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|-&lt;br /&gt;
! Zahl 2 (Spaltenindex)&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|-&lt;br /&gt;
! Zahl 3 (aktueller Matrixeintrag von P)&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|-&lt;br /&gt;
! Zahl 4 (aktueller Matrixeintrag von Q)&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 2&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 3&lt;br /&gt;
|align=&amp;quot;center&amp;quot; | 1&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
:Es sind immer noch nur 9 Tests nötig, um alle Paarungen zu erzeugen. Der erste und letzte Test sind nun:&lt;br /&gt;
       assertEqual( bar(v11, v21, v31, v41), bar_reference1)&lt;br /&gt;
       ...&lt;br /&gt;
       assertEqual( bar(v13, v23, v32, v41), bar_reference9)&lt;br /&gt;
:Die Methode der Latin Squares  funktioniert auch, wenn mehr als 3 Belegungen für jeden Parameter möglich sind, und wenn es mehr als 4 Parameter gibt. Für die Einzelheiten verweisen wir auf die Literatur, z.B. [http://citeseer.ist.psu.edu/78354.html], [http://en.wikipedia.org/wiki/Latin_square]. Empirische Untersuchungen haben ergeben, dass die Methode der vollständigen Paarung oft über 90% der Fehler in einem Programm finden kann.&lt;br /&gt;
&lt;br /&gt;
[[Effizienz|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5410</id>
		<title>Graphen und Graphenalgorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5410"/>
				<updated>2012-07-25T17:38:46Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Transitive Hülle und stark zusammenhängende Komponenten */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Einführung zu Graphen ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation -- Königsberger Brückenproblem ===&lt;br /&gt;
Leonhard Euler [http://de.wikipedia.org/wiki/Leonhard_Euler] erfand den Graphen-Formalismus 1736, um eine scheinbar banale Frage zu beantworten: Ist es möglich, in Königsberg (siehe Stadtplan von 1809 und die schematische Darstellung) einen Spaziergang zu unternehmen, bei dem jede der 7 Brücken genau einmal überquert wird?&lt;br /&gt;
&lt;br /&gt;
[[Image:Koenigsberg1809.png]]&amp;lt;br&amp;gt;&lt;br /&gt;
[[Image:Koenigsberg.jpg]]&lt;br /&gt;
&lt;br /&gt;
Ein Graph abstrahiert von der Geometrie des Problems und repräsentiert nur die Topologie. Jeder Stadtteil von Königsberg ist ein Knoten des Graphen, jede Brücke eine Kante. Der zum Brückenproblem gehörende Graph sieht also so aus:&lt;br /&gt;
&lt;br /&gt;
     O&lt;br /&gt;
    /| \&lt;br /&gt;
    \|  \&lt;br /&gt;
     O---O&lt;br /&gt;
    /|  /&lt;br /&gt;
    \| /&lt;br /&gt;
     O&lt;br /&gt;
&lt;br /&gt;
Der gesuchte Spaziergang würde existieren, wenn es maximal 2 Knoten gäbe, an denen sich eine ungerade Zahl von Kanten trifft. Die Frage muss für Königsberg also verneint werden, denn hier gibt es vier solche Knoten. Ein leicht modifiziertes Problem ist allerdings lösbar: Im obigen Stadtplan erkennt man eine Fähre, die die Stadtteile Kneiphof und Altstadt verbindet. Bezieht man dieselbe in den Spaziergang ein, ergibt sich folgender Graph, bei dem nur noch zwei Knoten mit ungerader Kantenzahl existieren:&lt;br /&gt;
&lt;br /&gt;
   --O&lt;br /&gt;
  / /| \&lt;br /&gt;
  \ \|  \&lt;br /&gt;
   --O---O&lt;br /&gt;
    /|  /&lt;br /&gt;
    \| /&lt;br /&gt;
     O&lt;br /&gt;
&lt;br /&gt;
Inzwischen haben Graphen eine riesige Zahl weiterer Anwendungen gefunden. Einige Beispiele:&lt;br /&gt;
&lt;br /&gt;
* Landkarten:&lt;br /&gt;
** Knoten: Länder&lt;br /&gt;
** Kanten: gemeinsame Grenzen&lt;br /&gt;
&lt;br /&gt;
* Logische Schaltkreise:&lt;br /&gt;
** Knoten: Gatter&lt;br /&gt;
** Kanten: Verbindungen&lt;br /&gt;
&lt;br /&gt;
* Chemie (Summenformeln):&lt;br /&gt;
** Knoten: chemische Elemente&lt;br /&gt;
** Kanten: Bindungen &lt;br /&gt;
&lt;br /&gt;
* Soziologie (StudiVZ)&lt;br /&gt;
** Soziogramm&lt;br /&gt;
*** Knoten: Personen&lt;br /&gt;
*** Kanten: Freund von ...&lt;br /&gt;
&lt;br /&gt;
=== Definitionen ===&lt;br /&gt;
&lt;br /&gt;
;Ungerichteter Graph: Ein ungerichteter Graph G = ( V, E ) besteht aus&lt;br /&gt;
:* einer endliche Menge V von Knoten (vertices)&lt;br /&gt;
:* einer endlichen Menge &amp;lt;math&amp;gt;E \subset V \times V&amp;lt;/math&amp;gt; von Kanten (edges)&lt;br /&gt;
:Die Paare (u,v) und (v,u) gelten dabei als nur ''eine'' Kante (somit gilt die Symmetriebeziehung: (u,v) ∈ E =&amp;gt; (v,u) ∈ E ). Die Anzahl der Kanten, die sich an einem Knoten treffen, wird als ''Grad'' (engl. ''degree'') dieses Knotens bezeichnet:&lt;br /&gt;
:::degree(v) = |{v' ∈ V | (v,v') ∈ E}|&lt;br /&gt;
:(Die Syntax |{...}| bezeichnet dabei die Mächtigkeit der angegebenen Menge, also die Anzahl der Elemente in der Menge.)&lt;br /&gt;
&lt;br /&gt;
Der Graph des Königsberger Brückenproblems ist ungerichtet. Bezeichnet man die Knoten entsprechend des folgenden Bildes&lt;br /&gt;
    c&lt;br /&gt;
   /| \&lt;br /&gt;
   \|  \&lt;br /&gt;
    b---d &lt;br /&gt;
   /|  /&lt;br /&gt;
   \| /&lt;br /&gt;
    a&lt;br /&gt;
&lt;br /&gt;
gilt für die Knotengrade: &amp;lt;tt&amp;gt;degree(a) == degree(c) == degree(d) == 3&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;degree(b) == 5&amp;lt;/tt&amp;gt;. Genauer muss man bei diesem Graphen von einem ''Multigraphen'' sprechen, weil es zwischen einigen Knotenpaaren (nämlich (a, b) sowie (b, c)) mehrere Kanten (&amp;quot;Mehrfachkanten&amp;quot;) gibt. Wir werden in dieser Vorlesung nicht näher auf Multigraphen eingehen.&lt;br /&gt;
&lt;br /&gt;
;Gerichteter Graph: Ein Graph heißt ''gerichtet'', wenn die Kanten (u,v) und (v,u) unterschieden werden. Die Kante (u,v) ∈ E wird nun als Kante von u nach v (aber nicht umgekehrt) interpretiert. Entsprechend unterscheidet man jetzt den ''eingehenden'' und den ''ausgehenden Grad'' jedes Knotens:&lt;br /&gt;
:*out_degree(v) = |{v' ∈ V | (v,v') ∈ E}|&amp;lt;br/&amp;gt;&lt;br /&gt;
:*in_degree(v)  = |{v' ∈ V| (v',v) ∈ E}|&lt;br /&gt;
&lt;br /&gt;
Das folgende Bild zeigt einen gerichteten Graphen. Hier gilt &amp;lt;tt&amp;gt;out_degree(1) == out_degree(3) == in_degree(2) == in_degree(4) == 2&amp;lt;/tt&amp;gt; und &lt;br /&gt;
&amp;lt;tt&amp;gt;in_degree(1) == in_degree(3) == out_degree(2) == out_degree(4) == 0&amp;lt;/tt&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
[[Image:digraph.png|gerichteter Graph]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Vollständiger Graph: Ein vollständiger Graph ist ein ungerichteter Graph, bei dem jeder Knoten mit allen anderen Knoten verbunden ist.&lt;br /&gt;
:::&amp;lt;math&amp;gt;E = \{ (v,w) |  v \in V, w \in V, v \ne w \}&amp;lt;/math&amp;gt;&lt;br /&gt;
:Ein vollständiger Graph mit |V| Knoten hat &amp;lt;math&amp;gt;|E| = \frac{|V|(|V|-1)}{2}&amp;lt;/math&amp;gt; Kanten.&lt;br /&gt;
&lt;br /&gt;
Die folgenden Abbildungen zeigen die vollständigen Graphen mit einem bis fünf Knoten (auch als K&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; bis K&amp;lt;sub&amp;gt;5&amp;lt;/sub&amp;gt; bezeichnet).&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;0&amp;quot; style=&amp;quot;margin: 1em auto 1em auto&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
| [[Image:k1.png|frame|k1]]&lt;br /&gt;
| [[Image:k2.png|frame|k2]]&lt;br /&gt;
| [[Image:k3.png|frame|k3]]&lt;br /&gt;
|-&lt;br /&gt;
| [[Image:k4.png|frame|k4]]&lt;br /&gt;
| [[Image:k5.png|frame|k5]]&lt;br /&gt;
|&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
''Rätsel''&amp;lt;br/&amp;gt;&lt;br /&gt;
Auf einer Party sind Leute. Alle stoßen miteinander an. Es hat 78 mal &amp;quot;Pling&amp;quot; gemacht.&lt;br /&gt;
Wieviele Leute waren da? Antwort: Jede Person ist ein Knoten des Graphen, jedes Antoßen eine Kante. &lt;br /&gt;
Da alle miteinander angestoßen haben, handelt es sich um einen vollständigen Graphen. Mit&lt;br /&gt;
|V|(|V|-1)/2 = 78 folgt, dass es 13 Personen waren.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Gewichteter Graph: Ein Graph heißt ''gewichtet'', wenn jeder Kante eine reelle Zahl zugeordnet ist. Bei vielen Anwendungen beschränkt man sich auch auf nichtnegative reelle Gewichte. In einem gerichteten Graphen können die Gewichte der Kanten (u,v) und (v,u) unterschiedlich sein.&lt;br /&gt;
&lt;br /&gt;
Die Gewichte kodieren Eigenschaften der Kanten, die für die jeweilige Anwendung interessant sind. Bei der Berechnung des maximalen Flusses in einem Netzwerk sind die Gewichte z.B. die Durchflusskapazitäten jeder Kante, bei der Suche nach kürzesten Weges kodieren Sie den Abstand zwischen den Endknoten der Kante, bei Währungsnetzwerken (jeder Knoten ist eine Währung) geben sie die Wechselkurse an, usw..&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Teilgraphen: Ein Graph G' = (V',E') ist ein Teilgraph eines Graphen G, wenn gilt:&lt;br /&gt;
:* V' &amp;amp;sube; V &lt;br /&gt;
:* E' &amp;amp;sub; E &lt;br /&gt;
:Er heißt ''(auf)spannender Teilgraph'', wenn gilt:&lt;br /&gt;
:* V' = V&lt;br /&gt;
:Er heißt ''induzierter Teilgraph'', wenn gilt:&lt;br /&gt;
:* e = (u,v) ∈ E' &amp;amp;sub; E &amp;amp;hArr; u ∈ V' und v ∈ V'&lt;br /&gt;
:Den von V' induzierten Teilgraphen erhält man also, indem man aus G alle Knoten löscht, die nicht in V' sind, sowie alle Kanten (und nur diese Kanten), die einen der gelöschten Knoten als Endknoten haben.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Wege, Pfade, Zyklen, Kreise, Erreichbarkeit: Sei G = (V,E) ein Graph (ungerichtet oder gerichteter) Graph. Dann gilt folgende rekursive Definition:&lt;br /&gt;
:* Für v ∈ V ist (v) ein Weg der Länge 0 in G&lt;br /&gt;
:* Falls &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1})&amp;lt;/math&amp;gt; ein Weg ist, und eine Kante &amp;lt;math&amp;gt;(v_{n-1}, v_n)\in E&amp;lt;/math&amp;gt; existiert, dann ist auch &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ein Weg, und er hat die Länge n. &lt;br /&gt;
: Ein Weg ist also eine nichtleere Folge von Knoten, so dass aufeinander folgende Knoten stets durch eine Kante verbunden sind. Die Länge des Weges entspricht der Anzahl der Kanten im Weg (= Anzahl der Knoten - 1).&lt;br /&gt;
:* Ein ''Pfad'' &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ist ein Weg, bei dem alle Knoten v&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; verschieden sind.&lt;br /&gt;
:* ''Ein Zyklus'' &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ist ein Weg, der zum Ausgangspunkt zurückkehrt, wenn also v&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; = v&amp;lt;sub&amp;gt;n&amp;lt;/sub&amp;gt; gilt.&lt;br /&gt;
:* Ein ''Kreis'' ist ein Zyklus ohne Überkreuzungen. Das heisst, es gilt v&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; = v&amp;lt;sub&amp;gt;n&amp;lt;/sub&amp;gt; und &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1})&amp;lt;/math&amp;gt; ist ein Pfad.&lt;br /&gt;
:* Ein Knoten w ∈ V ist von einem anderen Knoten v ∈ V aus ''erreichbar'' genau dann, wenn ein Weg (v, ..., w) existiert. Wir schreiben dann &amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt;.&lt;br /&gt;
In einem ungerichteten Graph ist die Erreichbarkeits-Relation stets symmetrisch, das heisst aus &amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; folgt &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. In einem gerichteten Graphen ist dies im allgemeinen nicht der Fall.&lt;br /&gt;
&lt;br /&gt;
Bestimmte Wege haben spezielle Namen&lt;br /&gt;
&lt;br /&gt;
;Eulerweg: Ein Eulerweg ist ein Weg, der alle '''Kanten''' genau einmal enthält.&lt;br /&gt;
&lt;br /&gt;
Die eingangs erwähnte Frage des Königsberger Brückenproblems ist equivalent zu der Frage, ob der dazugehörige Graph einen Eulerweg besitzt (daher der Name). Ein anderes bekanntes Beispiel ist das &amp;quot;Haus vom Nikolaus&amp;quot;: Wenn man diesen Graphen in üblicher Weise in einem Zug zeichnet, erhält man gerade den Eulerweg. &lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O----O&lt;br /&gt;
  | \/ |&lt;br /&gt;
  | /\ |   &amp;quot;Das Haus vom Nikolaus&amp;quot;: Alle ''Kanten'' werden nur ''einmal'' passiert&lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Hamiltonweg: Ein Hamiltonweg ist ein Weg, der alle '''Knoten''' genau einmal enthält. Das &amp;quot;Haus vom Nikolaus&amp;quot; besitzt auch einen Hamiltonweg:&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /   &lt;br /&gt;
  O----O&lt;br /&gt;
     /  &lt;br /&gt;
    /      Alle ''Knoten'' werden nur ''einmal'' passiert&lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Hamiltonkreis: Ein Hamiltonkreis ist ein Kreis, der alle '''Knoten''' genau einmal enthält. Auch ein solches Gebilde ist im Haus von Nilolaus enthalten:&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O    O&lt;br /&gt;
  |    |   v0 = vn&lt;br /&gt;
  |    |   vi != vj   Für Alle i,j   i !=j; i,j &amp;gt;0; i,j &amp;lt; n&lt;br /&gt;
  O----O     &lt;br /&gt;
&lt;br /&gt;
Die folgende Skizze zeigt hingegen einen Zyklus: Der Knoten rechts unten sowie die untere Kante sind zweimal enthalten (die Kante einmal von links nach rechts und einmal von rechts nach links):&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O    O&lt;br /&gt;
    \  |&lt;br /&gt;
     \ |   Zyklus&lt;br /&gt;
  O====O&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Zusammenhang, Zusammenhangskomponenten: Ein ungerichteter Graph G heißt ''zusammenhängend'', wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt;&lt;br /&gt;
:Ein gerichteter Graph G ist zusammenhängend, wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; '''oder''' &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. &lt;br /&gt;
:Er ist ''stark zusammenhängend'', wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; '''und''' &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. &lt;br /&gt;
:Entsprechende Definitionen gelten für Teilgraphen G'. Ein Teilgraph G' heisst ''Zusammenhangskomponente'' von G, wenn er ein ''maximaler'' zusammenhängender Teilgraph ist, d.h. wenn G' zusammenhängend ist, und man keine Knoten und Kanten aus G mehr zu G' hinzufügen kann, so dass G' immer noch zusammenhängend bleibt. Entsprechend definiert man ''starke Zusammenhangskomponenten'' in einem gerichteten Graphen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Planarer Graph, ebener Graph: Ein Graph heißt ''planar'', wenn er so in einer Ebene gezeichnet werden ''kann'', dass sich die Kanten nicht schneiden (außer an den Knoten). Ein Graph heißt ''eben'', wenn er tatsächlich so gezeichnet ''ist'', dass sich die Kanten nicht schneiden. Die Einbettung in die Ebene ist im allgemeinen nicht eindeutig.&lt;br /&gt;
&lt;br /&gt;
'''Beispiele:'''&lt;br /&gt;
&lt;br /&gt;
Der folgende Graph ist planar und eben:&lt;br /&gt;
 &lt;br /&gt;
      O&lt;br /&gt;
     /|\&lt;br /&gt;
    / O \&lt;br /&gt;
   / / \ \&lt;br /&gt;
   O     O&lt;br /&gt;
&lt;br /&gt;
Das &amp;quot;Haus vom Nikolaus&amp;quot; ist ebenfalls planar, wird aber üblicherweise nicht als ebener Graph gezeichnet, weil sich die Diagonalen auf der Wand überkreuzen:&lt;br /&gt;
 &lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O----O&lt;br /&gt;
  | \/ |&lt;br /&gt;
  | /\ |   &lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
Eine ebene Einbettung dieses Graphen wird erreicht, wenn man eine der Diagonalen ausserhalb des Hauses zeichnet. Der Graph (also die Menge der Knoten und Kanten) ändert sich dadurch nicht.&lt;br /&gt;
 &lt;br /&gt;
      O  &lt;br /&gt;
     /  \&lt;br /&gt;
  --O----O&lt;br /&gt;
 /  |  / |&lt;br /&gt;
 |  | /  |   &lt;br /&gt;
 |  O----O      Das &amp;quot;Haus vom Nikolaus&amp;quot; als ebener Graph gezeichnet.&lt;br /&gt;
  \     /&lt;br /&gt;
   -----&lt;br /&gt;
&lt;br /&gt;
Eine alternative Einbettung erhalten wir, wenn wir die andere Diagonale außerhalb des Hauses zeichnen:&lt;br /&gt;
 &lt;br /&gt;
      O  &lt;br /&gt;
     /  \&lt;br /&gt;
    O----O--|&lt;br /&gt;
    | \  |  |&lt;br /&gt;
    |  \ |  | &lt;br /&gt;
    O----O  |     Alternative Einbettung des &amp;quot;Haus vom Nikolaus&amp;quot;.&lt;br /&gt;
    |       |&lt;br /&gt;
    |-------|&lt;br /&gt;
&lt;br /&gt;
Jede Einbettung eines planaren Graphen (also jeder ebene Graph) definiert eine eindeutige Menge von ''Regionen'':&lt;br /&gt;
&lt;br /&gt;
 |----O   @&lt;br /&gt;
 |   /@ \&lt;br /&gt;
 |  O----O&lt;br /&gt;
 |  |@ / |&lt;br /&gt;
 |  | / @|   &lt;br /&gt;
 |  O----O        @ entspricht jeweils einer ''Region''. Auch ausserhalb der Figur ist eine Region (die sogenannte ''unendliche'' Region).&lt;br /&gt;
 |@      |&lt;br /&gt;
 |-------|&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Der vollständige Graph K5 ist kein planarer Graph, da sich zwangsweise Kanten schneiden, wenn man diesen Graphen in der Ebene zeichnet.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
;Dualer Graph: Jeder ebene Graph G = (V, E) hat einen ''dualen Graphen'' D = (V&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;, E&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;), dessen Knoten und Kanten wie folgt definiert sind:&lt;br /&gt;
:* V&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt; enthält einen Knoten für jede Region des Graphen G&lt;br /&gt;
:* Für jede Kante e ∈ E gibt es eine duale Kante e&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt; ∈ E&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;, die die an e angrenzenden Regionen (genauer: die entsprechenden Knoten in D) verbindet.&lt;br /&gt;
&lt;br /&gt;
Die folgende Abbildung zeigt einen Graphen (grau) und seinen dualen Graphen (schwarz). Die Knoten des dualen Graphen sind mit Zahlen gekennzeichnet und entsprechen den Regionen des Originalgraphen. Jeder (grauen) Kante des Originalgraphen entspricht eine (schwarze) Kante des dualen Graphen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
[[Image:dual-graphs.png]]&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für duale Graphen gilt: Wenn der Originalgraph zusammenhängend ist, enthält jede Region des dualen Graphen genau einen Knoten des Originalgraphen. Deshalb ist der duale Graph des dualen Graphen wieder der Originalgraph. Bei nicht-zusammenhängenden Graphen gilt dies nicht (vgl. das Fenster bei obigem Bild). In diesem Fall hat der duale Graph mehrere mögliche Einbettungen in die Ebene (man kann z.B. die rechte Kante zwischen Knoten 2 und 4 auch links vom Fenster einzeichnen), und man erhält nicht notwendigerweise den Originalgraphen, wenn man den dualen Graphen des dualen berechnet.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Baum: Ein ''Baum'' ist ein zusammenhängender, kreisfreier Graph.&lt;br /&gt;
&lt;br /&gt;
Beispiel: Binärer Suchbaum&lt;br /&gt;
&lt;br /&gt;
;Spannbaum: Ein ''Spannbaum'' eines zusammenhängenden Graphen G ist ein zusammenhängender, kreisfreier Teilgraph von G, der alle Knoten von G enthält&lt;br /&gt;
&lt;br /&gt;
Beispiel: Spannbaum für das &amp;quot;Haus des Nikolaus&amp;quot; &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    O   &lt;br /&gt;
   /       &lt;br /&gt;
  O    O&lt;br /&gt;
  |  /  &lt;br /&gt;
  | /   &lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
Der Spannbaum eines Graphen mit |V| Knoten hat stets |V| - 1 Kanten.&lt;br /&gt;
&lt;br /&gt;
;Wald: Ein ''Wald'' ist ein unzusammenhängender, kreisfreier Graph.&lt;br /&gt;
: Jede Zusammenhangskomponente eines Waldes ist ein Baum.&lt;br /&gt;
&lt;br /&gt;
=== Repräsentation von Graphen ===&lt;br /&gt;
&lt;br /&gt;
Sei G = ( V, E ) gegeben und liege V in einer linearen Sortierung vor.&amp;lt;br/&amp;gt; &lt;br /&gt;
:::&amp;lt;math&amp;gt;V = \{ v_1, ...., v_n \}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Adjazenzmatrix: Ein Graph kann durch eine Adjazenzmatrix repräsentiert werden, die soviele Zeilen und Spalten enthält, wie der Graph Knoten hat. Die Elemente der Adjazenzmatrix sind &amp;quot;1&amp;quot;, falls eine Kante zwischen den zugehörigen Knoten existiert:&lt;br /&gt;
:::&amp;lt;math&amp;gt;\mathrm{\bold A} = a_{ij} = &lt;br /&gt;
\begin{cases}&lt;br /&gt;
1 &amp;amp; \mathrm{falls}\quad (v_i, v_j) \in E \\&lt;br /&gt;
0 &amp;amp; \mathrm{sonst}&lt;br /&gt;
\end{cases} &lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
:Die Indizes der Matrix entsprechen also den Indizes der Knoten gemäß der gegebenen Sortierung. Im Falle eines ungerichteten Graphen ist die Adjazenzmatrix stets symmetrisch (d.h. es gilt &amp;lt;math&amp;gt;a_{ij}=a_{ji}&amp;lt;/math&amp;gt;), bei einem gerichteten Graphen ist sie im allgemeinen unsymmetrisch.&lt;br /&gt;
&lt;br /&gt;
Beispiel für einen ungerichteten Graphen:&lt;br /&gt;
&lt;br /&gt;
 v = { a,b,c,d }     b      d&lt;br /&gt;
                     | \  / |&lt;br /&gt;
                     |  \/  |&lt;br /&gt;
                     |  /\  |&lt;br /&gt;
                     | /  \ |&lt;br /&gt;
                     a      c&lt;br /&gt;
 &lt;br /&gt;
       a b c d&lt;br /&gt;
      -----------&lt;br /&gt;
      (0 1 0 1) |a &lt;br /&gt;
  A = (1 0 1 0) |b&lt;br /&gt;
      (0 1 0 1) |c&lt;br /&gt;
      (1 0 1 0) |d&lt;br /&gt;
&lt;br /&gt;
Die Adjazenzmatrixdarstellung eignet sich besonders für dichte Graphen (d.h. wenn die Zahl der Kanten in O(|V|&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt;) ist.&lt;br /&gt;
&lt;br /&gt;
;Adjazenzlisten: In der Adjazenzlistendarstellung wird der Graph als Liste von Knoten repräsentiert, die für jeden Knoten einen Eintrag enthält. Der Eintrag für jeden Knoten ist wiederum eine Liste, die die Nachbarknoten dieses Knotens enthält:&lt;br /&gt;
:* graph = {adjazencyList(v) | v ∈ V}&lt;br /&gt;
:* adjazencyList(v) = {v' ∈ V | (v, v') ∈ E}&lt;br /&gt;
&lt;br /&gt;
In Python implementieren wir Adjazenzlisten zweckmäßig als Array von Arrays:&lt;br /&gt;
&lt;br /&gt;
                   graph = [[...],[...],...,[...]]&lt;br /&gt;
 Adjazenzliste für Knoten =&amp;gt;  0     1         n&lt;br /&gt;
&lt;br /&gt;
Wenn wir bei dem Graphen oben die Knoten wie bei der Adjazenzmatrix indizieren (also &amp;lt;tt&amp;gt;a =&amp;gt; 0&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;b =&amp;gt; 1&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;c =&amp;gt; 2&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;d =&amp;gt; 3&amp;lt;/tt&amp;gt;), erhalten wir die Adjazenzlistendarstellung:&lt;br /&gt;
&lt;br /&gt;
 graph = [[b, d], [a, c],[b, d], [a, c]]&lt;br /&gt;
&lt;br /&gt;
Auf die Nachbarknoten eines durch seinen Index &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; gegebenen Knotens können wir also wie folgt zugreifen:&lt;br /&gt;
&lt;br /&gt;
      for neighbors in graph[node]:&lt;br /&gt;
          ... # do something with neighbor&lt;br /&gt;
&lt;br /&gt;
Die Adjazenzlistendarstellung ist effizienter, wenn der Graph nicht dicht ist, so dass viele Einträge der Adjazenzmatrix Null wären. In der Vorlesung werden wir nur diese Darstellung verwenden.&lt;br /&gt;
&lt;br /&gt;
;&amp;lt;div id=&amp;quot;transposed_graph&amp;quot;&amp;gt;Transponierter Graph&amp;lt;/div&amp;gt;: Den ''transponierten Graphen'' G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; eines gerichteten Graphen G erhält man, wenn man alle Kantenrichtungen umkehrt.&lt;br /&gt;
&lt;br /&gt;
Bei ungerichteten Graphen hat die Transposition offensichtlich keinen Effekt, weil alle Kanten bereits in beiden Richtungen vorhanden sind, so dass G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; = G gilt. Bei gerichteten Graphen ist die Transposition einfach, wenn der Graph als Adjazenzmatrix implementiert ist, weil man einfach die transponierte Adjazenzmatrix verwenden muss (beachte, dass sich die Reihenfolge der Indizes umkehrt):&lt;br /&gt;
:::A&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; = a&amp;lt;sub&amp;gt;ji&amp;lt;/sub&amp;gt;&lt;br /&gt;
Ist der Graph hingegen durch eine Adjazenzliste repräsentiert, muss etwas mehr Aufwand getrieben werden:&lt;br /&gt;
&lt;br /&gt;
 def transposeGraph(graph):&lt;br /&gt;
      gt = [[] for k in graph]   # zunächst leere Adjazenzlisten von G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt;&lt;br /&gt;
      for node in range(len(graph)):&lt;br /&gt;
           for neighbor in graph[node]:&lt;br /&gt;
               gt[neighbor].append(node)  # füge die umgekehrte Kante in G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; ein&lt;br /&gt;
      return gt&lt;br /&gt;
&lt;br /&gt;
== Durchlaufen von Graphen (Graph Traversal) ==&lt;br /&gt;
&lt;br /&gt;
Wir betrachten zunächst ungerichtete Graphen mit V Knoten und E Kanten. Eine grundlegende Aufgabe in diesen Graphen besteht darin, alle Knoten in einer bestimmten Reihenfolge genau einmal zu besuchen. Hierbei darf man sich von einem gegebenen Startknoten aus nur entlang der Kanten des Graphen bewegen. Die beim Traversieren benutzen Kanten bilden einen Baum, dessen Wurzel der Startknoten ist und der den gesamten Graphen aufspannt, falls der Graph zusammenhängend ist. (Beweis: Da jeder Knoten nur einmal besucht wird, gibt es für jeden besuchten Knoten [mit Ausnahme des Startknotens] genau eine eingehende Kante. Ist der Graph zusammenhängend, wird jeder Knoten tatsächlich erreicht und es gibt genau (V-1) Kanten, exakt soviele wie für einen Baum mit V Knoten notwendig sind.) Ist der Graph nicht zusammenhängend, wird jeder zusammenhängende Teilgraph (jede &amp;lt;i&amp;gt;Zusammenhangskomponente&amp;lt;/i&amp;gt;) getrennt traversiert, und man erhält einen sogenannten &amp;lt;i&amp;gt;Wald&amp;lt;/i&amp;gt; mit einem Baum pro Zusammenhangskomponente. Die beiden grundlegenden Traversierungsmethoden &amp;lt;i&amp;gt;Tiefensuche&amp;lt;/i&amp;gt; und &amp;lt;i&amp;gt;Breitensuche&amp;lt;/i&amp;gt; werden im folgenden vorgestellt.&lt;br /&gt;
&lt;br /&gt;
=== Tiefensuche in Graphen (Depth First Search, DFS) ===&lt;br /&gt;
&lt;br /&gt;
Die Idee der Tiefensuche besteht darin, jeden besuchten Knoten sofort über die erste Kante wieder zu verlassen, die zu einem noch nicht besuchten Knoten führt. Man findet dadurch schnell einen möglichst langen Pfad durch den Graphen, und der Traversierungs-Baum wird zunächst in die Tiefe verfolgt, daher der Name des Verfahrens. Hat ein Knoten keine unbesuchten Nachbarknoten mehr, geht man im Baum zurück (sogenanntes &amp;lt;i&amp;gt;back tracking&amp;lt;/i&amp;gt;), bis man einen Knoten findet, der noch eine unbesuchte Nachbarn besitzt, und traversiert diese nach dem gleichen Muster. Gibt es gar keine unbesuchten Knoten mehr, kehrt die Suche zum Startknoten zurück und endet dort.&lt;br /&gt;
&lt;br /&gt;
WDie folgende rekursive Implementation der Tiefensuche erwartet den Graphen in Adjazenzlistendarstellung und beginnt die Suche beim Knoten &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt;. Die Information, ob ein Knoten bereits besucht wurde, wird im Array &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; gespeichert. Ein solches Array, das zusätzliche Informationen über die Knoten des Graphen bereitstellt, wir häufig &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; genannt.&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)  # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):              # rekursive Hilfsfunktion, die den gegebenen Knoten und dessen Nachbarn besucht&lt;br /&gt;
         if not visited[node]:     # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True  # Markiere node als besucht&lt;br /&gt;
             print node            # Ausgabe der Knotennummer - pre-order&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode)&lt;br /&gt;
&lt;br /&gt;
[[Image:Tiefens.jpg]]&lt;br /&gt;
&lt;br /&gt;
Ausgabe für den Graphen in diesem Bild (es handelt sich um einen ungerichteten Graphen, die Pfeile symbolisieren nur die Suchrichtung beim Traversal):&lt;br /&gt;
&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; dfs(graph, 1)&lt;br /&gt;
 1&lt;br /&gt;
 2&lt;br /&gt;
 4&lt;br /&gt;
 3&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
 5&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div id=&amp;quot;pre_and_post_order&amp;quot;&amp;gt;In dieser Version des Algorithmus werden die Knotennummern ausgegeben, bevor die Nachbarknoten besucht werden. Man bezeichnet die resultierende Sortierung der Knoten als &amp;lt;b&amp;gt;pre-order&amp;lt;/b&amp;gt; oder als &amp;lt;b&amp;gt;discovery order&amp;lt;/b&amp;gt;. Alternativ kann man die Knotennummern erst ausgeben, nachdem alle Nachbarn besucht wurden, also auf dem Rückweg der Rekursion. In diesem Fall spricht man von &amp;lt;b&amp;gt;post-order&amp;lt;/b&amp;gt; oder &amp;lt;b&amp;gt;finishing order&amp;lt;/b&amp;gt;:&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)  # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):              # rekursive Hilfsfunktion, die den gegebenen Knoten und dessen Nachbarn besucht&lt;br /&gt;
         if not visited[node]:     # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True  # Markiere node als besucht&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
             &amp;lt;font color=red&amp;gt;print node            # Ausgabe der Knotennummer - post-order&amp;lt;/font&amp;gt;&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode)&lt;br /&gt;
&lt;br /&gt;
Es ergibt sich jetzt die Ausgabe:&lt;br /&gt;
&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; dfs(graph, 1)&amp;lt;font color=red&amp;gt;&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
 3&lt;br /&gt;
 4&lt;br /&gt;
 5&lt;br /&gt;
 2&lt;br /&gt;
 1&amp;lt;/font&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In realem Code ersetzt man die print-Ausgaben natürlich durch anwendungsspezifische Aktionen und Berechnungen. Einige Anwendungen sind uns im Kapitel [[Suchen]] bereits begegnet. &lt;br /&gt;
; Anwendungen der Pre-Order Traversierung&lt;br /&gt;
* Kopieren eines Graphen: kopiere zuerst den besuchten Knoten, dann seine Nachbarn und die dazugehörigen Kanten (sowie die Kanten zu bereits besuchten Knoten, die in der Grundversion der Tiefensuche ignoriert werden).&lt;br /&gt;
* Bestimmen der Zusammenhangskomponenten eines Graphen (siehe unten)&lt;br /&gt;
* In einem Zeichenprogramm: fülle eine Region mit einer Farbe (&amp;quot;flood fill&amp;quot;). Dabei ist jedes Pixel ein Knoten des Graphen und wird mit seinen 4 Nachbarpixceln verbunden. Die Tiefensuche startet bei der Mausposition und endet am Rand des betreffendcen Gebiets.&lt;br /&gt;
* Falls der Graph ein Baum ist: bestimme den Abstand jedes Knotens von der Wurzel&lt;br /&gt;
* Falls der Graph ein Parse-Baum ist, wobei innere Knoten Funktionsaufrufe, Kindknoten Funktionsargumente, und Blattknoten Werte repräsentieren: drucke den zugehörigen Ausdruck aus (also immer zuerst den Funktionsnamen, dann die Argumente, die wiederum geschachtelte Funktionsaufrufe sein können).&lt;br /&gt;
; Anwendungen der Post-Order Traversierung&lt;br /&gt;
* Löschen eines Graphen: lösche zuerst die Nachbarn, dann den Knoten selbst&lt;br /&gt;
* Bestimmen einer topologischen Sortierung eines azyklischen gerichteten Graphens (siehe unten)&lt;br /&gt;
* Falls der Graph ein Baum ist: bestimme den Abstand jedes Knotens von den Blättern (also die Tiefe des Baumes, siehe Übung 5)&lt;br /&gt;
* Falls der Graph ein Parse-Baum ist: führe die zugehörige Berechnung aus (d.h. berechne zuerst die geschachtelten inneren Funktionen, dann mit diesen Ergebnissen die nächst äußeren usw., siehe Übung 5).&lt;br /&gt;
; Anwendungen, die Pre- und Post-Order benötigen&lt;br /&gt;
* Weg aus einem Labyrinth: die Pre-Order dokumentiert die Suche nach dem Weg, die Post-Order zeigt den Rückweg aus Sackgassen (siehe Übung 9).&lt;br /&gt;
Im Spezialfall, wenn der Graph ein Binärbaum ist, unterscheidet man noch eine dritte Variante der Traversierung, nämlich die &amp;lt;i&amp;gt;in-order&amp;lt;/i&amp;gt; Traversierung. In diesem Fall behandelt man den Vaterknoten nach den linken, aber vor den rechten Kindern. Diese Reihenfolge wird beim [[Suchen#Beziehungen zwischen dem Suchproblem und dem Sortierproblem|Tree Sort Algorithmus]] verwendet. Diese Sortierung verwendet man auch, wenn man einen Parse-Baum mit binären Operatoren (statt Funktionsaufrufen) ausgeben will, siehe Übung 5.&lt;br /&gt;
&lt;br /&gt;
Eine nützliche Erweiterung der Tiefensuche besteht darin, in der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; nicht nur zu dokumentieren, dass ein Knoten bereits besucht wurde, sondern auch, von welchem Knoten aus man den jeweiligen Knoten zuerst erreicht hat. Im entstehenden Tiefensuchbaum ist dies gerade der Vaterknoten, weshalb wir die verbesserte property map zweckmäßigerweise in &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; umbenennen. Für den Startknoten, also die Wurzel des Baumes, wählen wir die Konvention, dass er sein eigener Vaterknoten ist (die Konvention, dafür den Wert &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zu verwenden, scheidet aus, weil dies bereits die Tatsache signalisiert, dass ein Knoten noch nicht besucht wurde):&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     parents = [None]*len(graph)     # Registriere für jeden Knoten den Vaterknoten im Tiefensuchbaum&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, parent):        # rekursive Hilfsfunktion&lt;br /&gt;
         if parents[node] is None:   # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             parents[node] = parent  # Markiere node als besucht und speichere seinen Vaterknoten&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn ...&lt;br /&gt;
                 visit(neighbor, node)      #  ... wobei node zu deren Vaterknoten wird&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode, startnode)     # Konvention für Wurzel: startnode ist sein eigener Vater&lt;br /&gt;
     &lt;br /&gt;
     return parents                  # Rückgabe des berechneten Tiefensuch-Baums&lt;br /&gt;
&lt;br /&gt;
Die Ausgabe für den obigen Beispielgraphen lautet: &lt;br /&gt;
  Knotennummer  |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7&lt;br /&gt;
  --------------+-----+-----+-----+-----+-----+-----+-----+-----&lt;br /&gt;
  Vaterknoten   | None|  1  |  1  |  4  |  2  |  2  |  3  |  3&lt;br /&gt;
&lt;br /&gt;
Dabei ist die Knotennummer der Index im Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt;, und der Vaterknoten ist der dazugehörige Arrayeintrag. Beachte, dass Knoten 0 in diesem Graphen nicht existiert, daher ist sein Eintrag &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;. Per Konvention hat der Wurzelknoten 1 sich selbst als Vater.&lt;br /&gt;
&lt;br /&gt;
=== Breitensuche in Graphen (Breadth First Search, BFS) ===&lt;br /&gt;
&lt;br /&gt;
Im Gegensatz zur Tiefensuche werden bei der Breitensuche alle Nachbarnknoten abgearbeitet, &amp;lt;i&amp;gt;bevor&amp;lt;/i&amp;gt; man rekursiv deren Nachbarn besucht. Man betrachtet somit zuerst alle Knoten, die den Abstand 1 von Startknoten haben, dann diejenigen mit dem Abstand 2 usw. Diese Reihenfolge bezeichnet man als &amp;lt;i&amp;gt;level-order&amp;lt;/i&amp;gt;. Wir sind ihr beispielsweise in Übung 6 begegnet, als die ersten 7 Ebenen eines Treap ausgegeben werden sollten. Man implementiert Breitensuche zweckmäßig mit Hilfe einer Queue, die die Knoten in First In - First Out - Reihenfolge bearbeitet. Eine geeignete Datenstruktur hierfür ist die Klasse &amp;lt;tt&amp;gt;[http://docs.python.org/library/collections.html#collections.deque deque]&amp;lt;/tt&amp;gt; aus dem Python-Modul &amp;lt;tt&amp;gt;[http://docs.python.org/library/collections.html collections]&amp;lt;/tt&amp;gt; (eine Deque implementiert sowohl die Funktionalität einer Queue wie auch die eines Stacks, siehe Übung 3):&lt;br /&gt;
&lt;br /&gt;
 from collections import deque&lt;br /&gt;
 &lt;br /&gt;
 def bfs(graph, startnode)&lt;br /&gt;
     visited = [False]*len(graph)   # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
   &lt;br /&gt;
     q = deque()                    # Queue für die zu besuchenden Knoten&lt;br /&gt;
     q.append(startnode)            # Startknoten in die Queue einfügen&lt;br /&gt;
     &lt;br /&gt;
     while len(q) &amp;gt; 0:              # Solange es noch unbesuchte Knoten gibt&lt;br /&gt;
         node = q.popleft()         # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
         if not visited[node]:      # Falls node noch nicht (auf einem anderen Weg) besucht wurde&lt;br /&gt;
              visited[node] = True  # Markiere node als besucht&lt;br /&gt;
              print node            # Drucke Knotennummer&lt;br /&gt;
              for neighbor in graph[node]:    # Füge Nachbarn in die Queue ein&lt;br /&gt;
                  q.append(neighbor)&lt;br /&gt;
&lt;br /&gt;
[[Image:Breitens.jpg]]&lt;br /&gt;
&lt;br /&gt;
Der Aufruf dieser Funktion liefert die Knoten des obigen Graphens ebenenweise, also zufällig genau in der Reihenfolge der Knotennummern:&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; bfs(graph, 1)&lt;br /&gt;
 1&lt;br /&gt;
 2&lt;br /&gt;
 3&lt;br /&gt;
 4&lt;br /&gt;
 5&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
&lt;br /&gt;
Neben der ebenenweisen Ausgabe hat die Breitensuche viele weitere wichtige Anwendungen, z.B. beim Testen, ob ein gegebener Graph bi-partit ist (siehe [http://en.wikipedia.org/wiki/Breadth-first_search#Testing_bipartiteness WikiPedia]), sowie bei der Suche nach kürzesten Wegen (siehe unten) und kürzesten Zyklen.&lt;br /&gt;
&lt;br /&gt;
== Weitere Anwendungen der Tiefensuche ==&lt;br /&gt;
&lt;br /&gt;
Die Tiefensuche hat zahlreiche Anwendungen, wobei der grundlegende Algorithmus immer wieder leicht modifiziert und an die jeweilige Aufgabe angepasst wird. Wir beschreiben im folgenden einige Beispiele.&lt;br /&gt;
&lt;br /&gt;
=== Damenproblem ===&lt;br /&gt;
&lt;br /&gt;
Tiefensuche wird häufig verwendet, um systematisch nach der Lösung eines logischen Rätsels (oder allgemeiner nach der Lösung eines diskreten Optimierungsproblems) zu suchen. Besonders anschaulich hierfür ist das Damenproblem. Die Aufgabe besteht darin, &amp;lt;math&amp;gt;k&amp;lt;/math&amp;gt; Damen auf einem Schachbrett der Größe &amp;lt;math&amp;gt;k \times k&amp;lt;/math&amp;gt; so zu platzieren, dass sie sich (nach den üblichen Schach-Regeln) nicht gegenseitig schlagen können. Das folgende Diagramm zeigt eine Lösung für den Fall &amp;lt;math&amp;gt;k=4&amp;lt;/math&amp;gt;. Die Positionen der Damen werden dabei wie üblich durch die Angabe der Spalte (Linie) mit Buchstaben und der Zeile (Reihe) mit Zahlen kodiert, hier also A2, B4, C1, D3:&lt;br /&gt;
&lt;br /&gt;
  ---------------&lt;br /&gt;
 |   | X |   |   | 4&lt;br /&gt;
 |---|---|---|---| &lt;br /&gt;
 |   |   |   | X | 3&lt;br /&gt;
 |---|---|---|---|&lt;br /&gt;
 | X |   |   |   | 2&lt;br /&gt;
 |---|---|---|---|&lt;br /&gt;
 |   |   | X |   | 1&lt;br /&gt;
  ---------------&lt;br /&gt;
   A   B   C   D&lt;br /&gt;
&lt;br /&gt;
Um das Problem systematisch zu lösen, konstruieren wir einen gerichteten Graphen, dessen Knoten die möglichen Positionen der Damen kodieren. Wir verbinden Knoten, die zu benachbarten Linien gehören, genau dann mit einer Kante, wenn die zugehörigen Positionen kompatibel sind, also wenn sich die dort positionierten Damen nicht schlagen können. Der resultierende Graph für &amp;lt;math&amp;gt;k=4&amp;lt;/math&amp;gt; hat folgende Gestalt:&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
Knoten, die zur selben Reihe oder Linie gehören, sind beispielsweise nicht direkt verbunden, weil zwei Damen niemals in derselben Linie oder Reihe stehen dürfen. Um eine erlaubte Konfiguration zu finden, verwenden wir nun eine angepasste Version der Tiefensuche: Wir beginnen die Suche beim Knoten &amp;lt;tt&amp;gt;START&amp;lt;/tt&amp;gt;. Sobald wir den Knoten &amp;lt;tt&amp;gt;STOP&amp;lt;/tt&amp;gt; erreichen, beenden wir die Suche und lesen die Lösung am gerade gefundenen Weg von Start nach Stop ab. Zwei kleine Modifikationen des Grundalgorithmus stellen sicher, dass die Bedingungen der Aufgabe eingehalten werden: Wir dürfen bei der Tiefensuche nur dann zu einem Nachbarn weitergehen, wenn die betreffende Position mit allen im Pfad bereits gesetzten Positionen kompatibel ist, andernfalls ist diese Kante tabu. Landen wir aufgrund dieser Regel in einer Sackgasse (also in einem Knoten, wo keine der ausgehenden Kanten erlaubt ist), müssen wir zur nächsten erlaubten Abzweigung zurückgehen (Backtracking). Beim Zurückgehen müssen wir das &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Flag wieder auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zurücksetzen, weil der betreffende Knoten ja möglicherweise auf einem anderen erlaubten Weg erreichbar ist.&lt;br /&gt;
&lt;br /&gt;
Der folgende Graph zeigt einen solchen Fall: Wir haben zwei Damen auf die Felder A1 und B3 positioniert (grüne Pfeile). Die einzig ausgehende Kante von B3 führt zum Knoten C1, welcher aber mit der Position A1 inkompatibel ist, so dass diese Kante nicht verwendet werden darf (roter Pfeil). Das Backtracking muss jetzt zu Knoten A1 zurückgehen (dabei wird das &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Flag von B3 wieder auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; gesetzt), weil A1 mit der Kante nach B4 eine weitere Option hat, die geprüft werden muss (die allerdings hier auch nicht zum Ziel führt).&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph-failure.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
Nach einigen weiteren Sackgassen findet man schließlich den Pfad A2, B4, C1, D3, der im folgenden Graphen grün markiert ist und der obigen Lösung entspricht:&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph-success.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
=== Test, ob ein ungerichteter Graph azyklisch ist ===&lt;br /&gt;
&lt;br /&gt;
Ein zusammenhängender ungerichteter Graph ist azyklisch (also ein Baum) genau dann, wenn es nur einen möglichen Weg von jedem Knoten zu jedem anderen gibt. (Bei gerichteten Graphen sind die Verhältnisse komplizierter. Wir behandeln dies weiter unten.) Das kann man mittels Tiefensuche leicht feststellen: Die Kante, über die wir einen Knoten erstmals erreichen, ist eine &amp;lt;i&amp;gt;Baumkante&amp;lt;/i&amp;gt; des Tiefensuchbaums. Erreichen wir einen bereits besuchten Knoten nochmals über eine andere Kante, haben wir einen Zyklus gefunden. Dabei müssen wir allerdings beachten, dass in einem ungerichteten Graphen jede Baumkante zweimal gefunden wird, einmal in Richtung vom Vater zum Kind und einmal in umgekehrter Richtung. Im zweiten Fall endet die Kante zwar in einem bereits besuchten Knoten (dem Vater), aber es entsteht dadurch kein Zyklus. Den Vaterknoten müssen wir deshalb überspringen, wenn wir über die Nachbarn iterieren:&lt;br /&gt;
&lt;br /&gt;
 def undirected_cycle_test(graph):         # Annahme: der Graph ist zusammenhängend&lt;br /&gt;
                                           # (andernfalls führe den Algorithmus für jede Zusammenhangskomponente aus)&lt;br /&gt;
     visited = [False]*len(graph)          # Flags für bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, from_node):           # rekursive Hilfsfunktion: gibt True zurück, wenn Zyklus gefunden wurde&lt;br /&gt;
         if not visited[node]:             # wenn node noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True          # markiere node als besucht&lt;br /&gt;
             for neighbor in graph[node]:  # besuche die Nachbarn ...&lt;br /&gt;
                 if neighbor == from_node: # ... aber überspringe den Vaterknoten&lt;br /&gt;
                     continue&lt;br /&gt;
                 if visit(neighbor, node): # ... signalisiere, wenn rekursiv ein Zyklus gefunden wurde&lt;br /&gt;
                     return True&lt;br /&gt;
             return False                  # kein Zyklus gefunden&lt;br /&gt;
         else:&lt;br /&gt;
             return True                   # Knoten schon besucht =&amp;gt; Zyklus&lt;br /&gt;
     &lt;br /&gt;
     startnode = 0                         # starte bei beliebigem Knoten (hier: Knoten 0)&lt;br /&gt;
     return visit(startnode, startnode)    # gebe True zurück, wenn ein Zyklus gefunden wurde&lt;br /&gt;
&lt;br /&gt;
Wenn wir einen Zyklus finden, wird das weitere Traversieren das Graphen abgebrochen, denn ein Graph, der einmal zyklisch war, kann später nicht wieder azyklisch werden. Die notwendige Modifikation für unzusammenhängende Graphen erfolgt analog zum Algorithmus für die Detektion von Zusammenhangskomponenten, der im nächsten Abschnitt beschrieben wird.&lt;br /&gt;
&lt;br /&gt;
=== Finden von Zusammenhangskomponenten ===&lt;br /&gt;
&lt;br /&gt;
Das Auffinden und Markieren von Zusammenhangskomponenten (also maximalen zusammenhängenden Teilgraphen) ist eine grundlegende Aufgabe in ungerichteten, unzusammenhängenden Graphen (bei gerichteten Graphen sind die Verhältnisse wiederum komplizierter, siehe unten). Zwei Knoten u und v gehören zur selben Zusammenhangskomponente genau dann, wenn es einen Pfad von u nach v gibt (da der Graph ungerichtet ist, gibt es dann auch einen Pfad von v nach u). Man sagt auch, dass &amp;quot;v von u aus erreichbar&amp;quot; ist. Unzusammenhängende Graphen entstehen in der Praxis häufig, wenn die Kanten gewisse Relationen zwischen den Knoten kodieren: &lt;br /&gt;
* Wenn die Knoten Städte sind und die Kanten Straßen, sind diejenigen Städte in einer Zusammenhangskomponente, die per Auto von einander erreichbar sind. Unzusammenhängende Graphen entstehen hier beispielsweise, wenn eine Insel nicht durch eine Brücke erschlossen ist, wenn Grenzen gesperrt sind oder wenn ein Gebirge zu unwegsam ist, um Straßen zu bauen.&lt;br /&gt;
* Wenn Knoten Personen sind, und Kanten die Eltern-Kind-Relation beschreiben, so umfasst jede Zusammenhangskomponenten die Verwandten (auch wenn sie nur über viele &amp;quot;Ecken&amp;quot; verwandt sind).&lt;br /&gt;
* In der Bildverarbeitung entsprechen Knoten den Pixeln, und dieselben werden durch eine Kante verbunden, wenn sie zum selben Objekt gehören. Die Zusammenhangskomponenten entsprechen somit den Objekten im Bild (siehe Übungsaufgabe).&lt;br /&gt;
Die Zusammenhangskomponenten bilden eine Äquivalenzrelation. Folglich kann für jede Komponente ein Reprässentant bestimmt werden, der sogenannte &amp;quot;Anker&amp;quot;. Kennt jeder Knoten seinen Anker, ist das Problem der Zusammenhangskomponenten gelöst. &lt;br /&gt;
&lt;br /&gt;
==== Lösung mittels Tiefensuche ====&lt;br /&gt;
&lt;br /&gt;
Unser erster Ansatz ist, den Anker mit Hilfe der Tiefensuche zu finden. Anstelle der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; verwenden wir diesmal eine property map &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt;, die für jeden Knoten die Knotennummer des zugehörigen Ankers angibt, oder &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, wenn der Knoten noch nicht besucht wurde. Dabei verwenden wir wieder die Konvention, dass Anker auf sich selbst zeigen. Für viele Anwendungen ist es außerdem (oder stattdessen) zweckmäßig, die Zusammenhangskomponenten mit einer laufenden Nummer, einem sogenannten &amp;lt;i&amp;gt;Label&amp;lt;/i&amp;gt;, durchzuzählen. Dann kann man zusätzliche Informationen zu jeder Komponente (beispielsweise deren Größe) einfach in einem Array speichern, das über die Labels indexiert wird. Die folgende Version der Tiefensuche bestimmt sowohl die Anker als auch die Labels für jeden Knoten:&lt;br /&gt;
&lt;br /&gt;
 def connectedComponents(graph):&lt;br /&gt;
        anchors = [None] * len(graph)             # property map für Anker jedes Knotens&lt;br /&gt;
        labels  = [None] * len(graph)             # property map für Label jedes Knotens&lt;br /&gt;
        &lt;br /&gt;
        def visit(node, anchor):&lt;br /&gt;
                &amp;quot;&amp;quot;&amp;quot;anchor ist der Anker der aktuellen ZK&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
                if anchors[node] is None:         # wenn node noch nicht besucht wurde:&lt;br /&gt;
                    anchors[node] = anchor        # setze seinen Anker&lt;br /&gt;
                    labels[node] = labels[anchor] # und sein Label&lt;br /&gt;
                    for neighbor in graph[node]:  # und besuche die Nachbarn&lt;br /&gt;
                        visit(neighbor, anchor)&lt;br /&gt;
        &lt;br /&gt;
        current_label = 0                         # Zählung der ZK beginnt bei 0&lt;br /&gt;
        for node in xrange(len(graph)):&lt;br /&gt;
            if anchors[node] is None:             # Anker noch nicht bekannt =&amp;gt; neue ZK gefunden&lt;br /&gt;
                labels[node] = current_label      # Label des Ankers setzen&lt;br /&gt;
                visit(node, node)                 # Knoten der neuen ZK rekursiv suchen&lt;br /&gt;
                current_label += 1                # Label für die nächste ZK hochzählen&lt;br /&gt;
        return anchors, labels&lt;br /&gt;
Interessant ist hier die Schleife über alle Knoten des Graphen am Ende des Algorithmus, die bei den bisherigen Versionen der Tiefensuche nicht vorhanden war. Um ihre Funktionsweise zu verstehen, nehmen wir für den Moment an, dass der Graph zusammenhängend ist. Dann findet diese Schleife den ersten Knoten des Graphen und führt die Tiefensuche mit diesem Knoten als Startknoten aus. Sobald die Rekursion zurückkehrt, sind alle Knoten des Graphen besucht (weil der Graph ja zusammenhängend war), so dass die Schleife alle weiteren Knoten überspringt (die if-Anweisung liefert für keinen weiteren Knoten True). Bei unzusammenhängenden Graphen dagegen erreicht die Tiefensuche nur die Knoten derselben Komponente, die im weiteren Verlauf der Schleife übersprungen werden. Findet die if-Anweisung jetzt einen noch nicht besuchten Knoten, muss dieser folglich in einer neuen Komponente liegen. Wir verwenden diesen Knoten als Anker und bestimmen die übrigen Knoten dieser Komponente wiederum mit Tiefensuche.&lt;br /&gt;
&lt;br /&gt;
* Beispiel: ... &amp;lt;b&amp;gt; under construction &amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Man erkennt, dass die Tiefensuche nach dem &amp;lt;i&amp;gt;Anlagerungsprinzip&amp;lt;/i&amp;gt; vorgeht: Beginnend vom einem Startknoten (dem Anker) werden die Knoten der aktuellen Komponente nach und nach an den Tiefensuchbaum angehangen. Erst, wenn nichts mehr angelagert werden kann, geht der Algorithmus zur nächsten Komponente über.&lt;br /&gt;
&lt;br /&gt;
==== Lösung mittels Union-Find-Algorithmus ====&lt;br /&gt;
&lt;br /&gt;
Im Gegensatz zum Anlagerungsprinzip sucht der Union-Find-Algorithmus die Zusammenhangskomponenten mit dem &amp;lt;i&amp;gt;Verschmelzungsprinzip&amp;lt;/i&amp;gt;: Eingangs wird jeder Knoten als ein Teilgraph für sich betrachtet. Dann iteriert man über alle Kanten und verbindet deren Endknoten jeweils zu einem gemeinsamen Teilgraphen (falls die beiden Enden einer Kante bereits im selben Teilgraphen liegen, wird diese Kante ignoriert). Solange noch Kanten vorhanden sind, werden dadurch immer wieder Teilgraphen in größere Teilgraphen verschmolzen. Am Ende bleiben die maximalen zusammenhängenden Teilgraphen (also gerade die Zusammenhangskomponenten) übrig. Dieser Algorithmus kommt ohne Tiefensuche aus und ist daher in der Praxis oft schneller, allerdings auch etwas komplizierter zu implementieren.&lt;br /&gt;
&lt;br /&gt;
Der Schlüssel des Algorithmus ist eine Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt;, die zu jedem Knoten den aktuellen Anker sucht. Der Anker existiert immer, da jeder Knoten von Anfang an zu einem Teilgraphen gehört (anfangs ist jeder Teilgraph trivial und besteht nur aus dem Knoten selbst). Die Verschmelzung wird realisiert, indem der Anker des einen Teilgraphen seine Rolle verliert und stattdessen der Anker des anderen Teilgraphen eingesetzt wird. &lt;br /&gt;
&lt;br /&gt;
Zur Verwaltung der Anker verwenden wir wieder eine property map &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt; mit der Konvention, dass die Anker auf sich selbst verweisen. Es wäre jedoch zu teuer, wenn man bei jeder Verschmelzung alle Anker-Einträge der beteiligten Knoten aktualisieren müsste, da jeder Knoten im Laufe des Algorithmus mehrmals seinen Anker wechseln kann. Statt dessen definiert man Anker rekursiv: Verweist ein Knoten auf einen Anker, der mittlerweile diese Rolle verloren hat, folgt man dem Verweis von diesem Knoten (dem ehemaligen Anker) weiter, bis man einen tatsächlichen Anker gefunden hat - erkennbar daran, dass er auf sich selbst verweist. Diese Suchfunktion kann folgendermassen implementiert werden:&lt;br /&gt;
&lt;br /&gt;
  def findAnchor(anchors, node):&lt;br /&gt;
      while node != anchors[node]:   # wenn node kein Anker ist&lt;br /&gt;
          node = anchors[node]       # ... verfolge die Ankerkette weiter&lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Allerdings kann diese Kette im Laufe vieler Verschmelzungen sehr lang werden, so dass das Verfolgen der Kette teuer wird. Man vermeidet dies durch die sogenannte &amp;lt;i&amp;gt;Pfadkompression&amp;lt;/i&amp;gt;: Immer, wenn man den Anker gefunden hat, aktualisiert man den Eintrag am Anfang der Kette. Die Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt; wird dadurch nur wenig komplizierter:&lt;br /&gt;
&lt;br /&gt;
  def findAnchor(anchors, node):&lt;br /&gt;
      start = node                   # wir merken uns den Anfang der Kette&lt;br /&gt;
      while node != anchors[node]:   # wenn node kein Anker ist&lt;br /&gt;
          node = anchors[node]       # ... verfolge die Ankerkette weiter&lt;br /&gt;
      anchors[start] = node          # Pfadkompression: aktualisiere den Eintrag am Anfang der Kette&lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Man kann zeigen, dass die Ankersuche mit Pfadkompression zu einer fast konstanten amortisierten Laufzeit pro Aufruf führt.&lt;br /&gt;
&lt;br /&gt;
Um mit jeder Kante des (ungerichteten) Graphen nur maximal einmal eine Verschmelzung durchzuführen, betrachten wir jede Kante nur in der Richtung von der kleineren zur größeren Knotennummer, die umgekehrte Richtung wird ignoriert. Außerdem ist es zweckmäßig, bei jeder Verschmelzung denjenigen Anker mit der kleineren Knotennummer als neuen Anker zu übernehmen. Dann gilt für jede Zusammenhangskomponente, dass gerade der Knoten mit der kleinsten Knotennummer der Anker ist (genau wie bei der Lösung mittels Tiefensuche), was die weitere Analyse vereinfacht, z.B. die Zuordnung der Labels zu den Komponenten am Ende des Algorithmus. &lt;br /&gt;
&lt;br /&gt;
 def unionFindConnectedComponents(graph):&lt;br /&gt;
     anchors = range(len(graph))        # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):    # iteriere über alle Knoten&lt;br /&gt;
         for neighbor in graph[node]:   # ... und über deren ausgehende Kanten&lt;br /&gt;
             if neighbor &amp;lt; node:        # ignoriere Kanten, die in falscher Richtung verlaufen&lt;br /&gt;
                 continue&lt;br /&gt;
             # hier landen wir für jede Kante des Graphen genau einmal&lt;br /&gt;
             a1 = findAnchor(anchors, node)       # finde Anker ...&lt;br /&gt;
             a2 = findAnchor(anchors, neighbor)   # ... der beiden Endknoten&lt;br /&gt;
             if a1 &amp;lt; a2:                          # Verschmelze die beiden Teilgraphen&lt;br /&gt;
                 anchors[a2] = a1                 # (verwende den kleineren der beiden Anker als Anker des&lt;br /&gt;
             elif a2 &amp;lt; a1:                        #  entstehenden Teilgraphen. Falls node und neighbor &lt;br /&gt;
                 anchors[a1] = a2                 #  den gleichen Anker haben, waren sie bereits im gleichen&lt;br /&gt;
                                                  #  Teilgraphen, und es passiert hier nichts.)&lt;br /&gt;
     # Bestimme jetzt noch die Labels der Komponenten&lt;br /&gt;
     labels = [None]*len(graph)         # Initialisierung der property map für Labels&lt;br /&gt;
     current_label = 0                  # die Zählung beginnt bei 0&lt;br /&gt;
     for node in xrange(len(graph)):&lt;br /&gt;
         a = findAnchor(anchors, node)  # wegen der Pfadkompression zeigt jeder Knoten jetzt direkt auf seinen Anker&lt;br /&gt;
         if a == node:                  # node ist ein Anker&lt;br /&gt;
             labels[a] = current_label  # =&amp;gt; beginne eine neue Komponente&lt;br /&gt;
             current_label += 1         # und zähle Label für die nächste ZK hoch&lt;br /&gt;
         else:&lt;br /&gt;
             labels[node] = labels[a]   # node ist kein Anker =&amp;gt; setzte das Label des Ankers&lt;br /&gt;
                                        # (wir wissen, dass labels[a] bereits gesetzt ist, weil &lt;br /&gt;
                                        #  der Anker immer der Knoten mit der kleinsten Nummer ist)&lt;br /&gt;
     return anchors, labels&lt;br /&gt;
 &lt;br /&gt;
* Beispiel: ... &amp;lt;b&amp;gt;under construction&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Kürzeste Wege (Pfade) ==&lt;br /&gt;
&lt;br /&gt;
Eine weitere grundlegende Aufgabe in Graphen ist die Bestimmung eines kürzesten Weges zwischen zwei gegebenen Knoten. Dies hat offensichtliche Anwendungen bei Routenplanern und Navigationssystemen und ist darüber hinaus wichtiger Bestandteil anderer Algorithmen, z.B. bei der Berechnung eines maximalen Flusses mit der [http://en.wikipedia.org/wiki/Edmonds%E2%80%93Karp_algorithm Methode von Edmonds und Karp].&lt;br /&gt;
&lt;br /&gt;
=== Kürzeste Wege in ungewichteten Graphen mittels Breitensuche ===&lt;br /&gt;
&lt;br /&gt;
Im Fall eines ungewichteten Graphen ist die Länge eines Weges einfach durch die Anzahl der durchlaufenen Kanten definiert. Daraus folgt, dass kürzeste Pfade mit einer leicht angepassten Version der Breitensuche gefunden werden können: Aufgrund des first in-first out-Verhaltens der Queue betrachtet die Breitensuche alle (erreichbaren) Knoten in der Reihenfolge ihres Abstandes vom Startknoten. Wenn wir den Zielknoten zum ersten Mal erreichen, und der gerade gefundene Weg vom Start zum Ziel hat die Länge L, muss dies der kürzeste Weg sein: Alle möglichen Wege der Länge L' &amp;amp;lt; L hat die Breitensuche ja bereits betrachtet, ohne dass dabei der Zielknoten erreicht wurde. Daraus folgt übrigens eine allgemeine Eigenschaft aller Algorithmen für kürzeste Wege: Wenn der kürzeste Weg vom Start zum Ziel die Länge L hat, finden diese Algorithmen als Nebenprodukt auch die kürzesten Wege zu allen Knoten, für die L' &amp;amp;lt; L gilt. &lt;br /&gt;
&lt;br /&gt;
Um den Algorithmus zu implementieren, passen wir die Breitensuche so an, dass anstelle der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; eine property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; verwendet wird, die für jeden besuchten Knoten den Vaterknoten im Breitensuchbaum speichert. Durch Rückverfolgen der &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette können wir den Pfad vom Ziel zum Start rekonstruieren, und durch Umdrehen der Reihenfolge erhalten wir den gesuchten Pfad vom Start zum Ziel. Sobald der Zielknoten erreicht wurde, können wir die Breitensuche abbrechen (&amp;lt;tt&amp;gt;break&amp;lt;/tt&amp;gt;-Befehl in der ersten &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Schleife). Falls der gegebene Graph unzusammenhängend ist, kann es passieren, dass gar kein Weg gefunden wird, weil Start und Ziel in verschiedenen Zusammenhangskomponenten liegen. Dies erkennen wir daran, dass die Breitensuche beendet wurde, ohne den Zielknoten zu besuchen. Dann gibt die Funktion statt eines Pfades dern Wert &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zurück:&lt;br /&gt;
&lt;br /&gt;
  from collections import deque&lt;br /&gt;
  &lt;br /&gt;
  def shortestPath(graph, startnode, destination):&lt;br /&gt;
      parents = [None]*len(graph)      # Registriere für jeden Knoten den Vaterknoten im Breitensuchbaum&lt;br /&gt;
      parents[startnode] = startnode   # startnode ist die Wurzel des Baums =&amp;gt; verweist auf sich selbst&lt;br /&gt;
      &lt;br /&gt;
      q = deque()                      # Queue für die zu besuchenden Knoten&lt;br /&gt;
      q.append(startnode)              # Startknoten in die Queue einfügen&lt;br /&gt;
      &lt;br /&gt;
      while len(q) &amp;gt; 0:                # Solange es noch unbesuchte Knoten gibt&lt;br /&gt;
          node = q.popleft()           # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
          if node == destination:      # Zielknoten erreicht&lt;br /&gt;
              break                    #   =&amp;gt; Suche beenden&lt;br /&gt;
          for neighbor in graph[node]: # Besuche die Nachbarn von node&lt;br /&gt;
              if parents[neighbor] is None:  # aber nur, wenn sie noch nicht besucht wurden&lt;br /&gt;
                  parents[neighbor] = node   # setze node als Vaterknoten&lt;br /&gt;
                  q.append(neighbor)         # und füge neighbor in die Queue ein&lt;br /&gt;
      &lt;br /&gt;
      if parents[destination] is None: # Breitensuche wurde beendet ohne den Zielknoten zu besuchen&lt;br /&gt;
          return None                  # =&amp;gt; kein Pfad gefunden (unzusammenhängender Graph)&lt;br /&gt;
      &lt;br /&gt;
      # Pfad durch die parents-Kette zurückverfolgen und speichern&lt;br /&gt;
      path = [destination]&lt;br /&gt;
      while path[-1] != startnode:&lt;br /&gt;
          path.append(parents[path[-1]])&lt;br /&gt;
      path.reverse()     # Reihenfolge umdrehen (Ziel =&amp;gt; Start wird zu Start =&amp;gt; Ziel)&lt;br /&gt;
      return path        # gefundenen Pfad zurückgeben&lt;br /&gt;
&lt;br /&gt;
=== Gewichtete Graphen ===&lt;br /&gt;
&lt;br /&gt;
Das Problem der Suche nach kürzesten Wegen wird wesentlich interessanter und realistischer, wenn wir zu gewichteten Graphen übergehen:&lt;br /&gt;
&lt;br /&gt;
; Definition - kantengewichteter Graph&lt;br /&gt;
: Jeder Kante (s,t) des Graphen ist eine reelle oder natürliche Zahl w&amp;lt;sub&amp;gt;st&amp;lt;/sub&amp;gt; zugeordnet, die üblicherweise als ''Kantengewicht'' bezeichnet wird.&lt;br /&gt;
&lt;br /&gt;
; Definition - knotengewichteter Graph&lt;br /&gt;
: Jedem Knoten v des Graphen ist eine reelle oder natürliche Zahl w&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; zugeordnet, die üblicherweise als ''Knotengewicht'' bezeichnet wird.&lt;br /&gt;
&lt;br /&gt;
Je nach Anwendung benötigt man Knoten- oder Kantengewichte oder auch beides zugleich. Wir beschränken uns in der Vorlesung auf kantengewichtete Graphen. Beispiele für die Informationen, die man durch Kantengewichte ausdrücken kann, sind&lt;br /&gt;
* wenn die Knoten Orte sind: Abstand von Anfangs- und Endknoten jeder Kante (z.B. Luftline oder Straßenentfernung), Fahrzeit zwischen den Orten&lt;br /&gt;
* wenn der Knoten ein Rohrnetzwerk beschreibt: Durchflusskapazität der einzelnen Rohre (für max-Flussprobleme), analog bei elektrischen Netzwerken: elektrischer Widerstand&lt;br /&gt;
* wenn die Knoten Währungen repräsentieren, können deren Wechselkurse durch Kantengewichte angegeben werden.&lt;br /&gt;
Bei einigen Beispielen ergeben sich unterschiedliche Kantengewichte, wenn eine Kante von s nach t anstatt von t nach s durchlaufen wird. Beispielsweise können sich die Fahrzeiten erheblich unterscheiden, wenn es in einer Richtung bergauf, in der anderen bergab geht, obwohl die Entfernung in beiden Fällen gleich ist. Hier ergibt sich natürlicherweise ein gerichteter Graph. In anderen Beispielen (z.B. bei Luftlinienentfernungen, in guter Näherung auch bei Straßenentfernungen) sind die Gewichte von der Richtung unabhängig, so dass wir ungerichtete Graphen verwenden können.&lt;br /&gt;
&lt;br /&gt;
Die Repräsentation der Kantengewichte im Programm richtet sich nach der Repräsentation des Graphen selbst. Am einfachsten ist wiederum die Adjazenzmatrix, die aber nur für dichte Graphen (&amp;lt;math&amp;gt;E = O(V^2)&amp;lt;/math&amp;gt;, mit E als Anzahl der Kanten und V als Anzahl der Knoten) effizient ist. Bei gewichteten Graphen gibt das Matrixelement a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; das Gewicht der Kante i &amp;amp;rArr; j (wobei a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; = 0 gesetzt wird, wenn diese Kante nicht existiert). Wie zuvor gilt für ungerichtete Graphen a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; = a&amp;lt;sub&amp;gt;ji&amp;lt;/sub&amp;gt; (symmetrische Matrix), während dies für gerichtete Graphen nicht gelten muss.&lt;br /&gt;
&lt;br /&gt;
Bei Graphen in Adjazenzlistendarstellung hat es sich bewährt, die Gewichte in einer &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; zu speichern. Weiter oben haben wir bereits property maps für Knoteneigenschaften (z.B. &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt;) gesehen. Property maps für Kanten funktionieren ganz analog, allerdings muss man jetzt Paare von Knoten (nämlich Anfangs- und Endknoten der Kante) als Schlüssel verwenden und die Daten entsprechend in einem assoziativen Array ablegen:&lt;br /&gt;
  w = weights[(i,j)]   # Zugriff auf das Gewicht der Kante i &amp;amp;rArr; j&lt;br /&gt;
Alternativ könnte man auch die Graph-Datenstruktur selbst erweitern, aber dies ist weniger zu empfehlen, weil jeder Algorithmus andere Erwiterungen benötigt und damit die Datenstruktur sehr unübersichtlich würde.&lt;br /&gt;
&lt;br /&gt;
Der kürzeste Weg ist nun definiert als der Weg, bei dem die Summe der Kantengewichte minimal ist:&lt;br /&gt;
;Definition - Problem des kürzesten Weges&lt;br /&gt;
: Sei P die Menge aller Wege von u nach v, und &amp;lt;math&amp;gt;p \in P&amp;lt;/math&amp;gt; einer dieser Wege. Wenn der Grpah einfach ist (es also keine Mehrfachkanten zwischen denselben Knoten und keine Schleifen gibt), ist der Weg p durch die Folge der besuchten Knoten eindeutig bestimmt:&lt;br /&gt;
: &amp;lt;math&amp;gt;p : \ \ u = x_0 \rightarrow x_1 \rightarrow x_2 \rightarrow ... \rightarrow v = x_{n_p}&amp;lt;/math&amp;gt;&lt;br /&gt;
:wo &amp;lt;math&amp;gt;n_p&amp;lt;/math&amp;gt; die Anzahl der Kanten im Weg p ist. Seine Kosten W&amp;lt;sub&amp;gt;p&amp;lt;/sub&amp;gt; ergeben sich als Summer der Gewichte der einzelnen Kanten&lt;br /&gt;
: &amp;lt;math&amp;gt;W_p = \sum_{k=1}^{n_p} w_{x_{k-1}x_k}&amp;lt;/math&amp;gt;&lt;br /&gt;
: und ein kürzester Weg &amp;lt;math&amp;gt;p^* \in P&amp;lt;/math&amp;gt; ist ein Weg mit minimalen Kosten&lt;br /&gt;
: &amp;lt;math&amp;gt;p^* = \textrm{argmin}_{p\in P}\ \ W_p&amp;lt;/math&amp;gt;&lt;br /&gt;
: Das Problem des kürzesten Weges besteht darin, einen optimalen Weg &amp;lt;i&amp;gt;p*&amp;lt;/i&amp;gt; zwischen gegebenen Knoten u und v zu finden.&lt;br /&gt;
Die Lösung dieses Problems hängt davon ab, ob alle Kantengewichte positiv sind, oder ob es auch negative Kantengewichte gibt. In letzeren Fall ist es möglich, durch eine Verlängerung des Weges die Kosten zu redizieren, während sich im ersteren Fall die Kosten immer erhöhen, wenn man den Weg verlängert. &lt;br /&gt;
&lt;br /&gt;
Negative Gewichte treten z.B. bei den Währungsgraphen auf. Auf den ersten Blick entsprechen diese Graphen nicht den Anforderungen an das Problem des kürzesten Weges, weil Wechselkurse miteinander (und mit Geldbeträgen) multipliziert anstatt addiert werden. Man beseitigt diese Schwierigkeit aber leicht, indem man die &amp;lt;i&amp;gt;Logarithmen&amp;lt;/i&amp;gt; der Wechselkurse als Kantengewichte verwendet, wodurch sich die Multiplikation in eine Addition der Logarithmen verwandelt. Wechselkurse &amp;amp;lt; 1 führen nun zu negativen Gewichten. &lt;br /&gt;
&lt;br /&gt;
Interessant werden negative Gewichte vor allem in Graphen mit Zyklen. Dann kann es nämlich passieren, dass die Gesamtkosten eines Zyklus ebenfalls negativ sind. Jeder Weg, der den Zyklus enthält, hat dann Kosten von &amp;lt;math&amp;gt;-\infty&amp;lt;/math&amp;gt;, weil man den Zyklus beliebig oft durchlaufen und dadurch die Gesamtkosten immer weiter verkleinern kann:&lt;br /&gt;
&lt;br /&gt;
     /\		1. Durchlauf: Kosten -1&lt;br /&gt;
  1 /  \ -4	2. Durchlauf: Kosten -2&lt;br /&gt;
   /____\	etc.&lt;br /&gt;
      2&lt;br /&gt;
&lt;br /&gt;
Um hier nicht in einer Endlosschleife zu landen, benötigt man spezielle Algorithmen, die mit dieser Situation umgehen können. Der [http://de.wikipedia.org/wiki/Bellman-Ford-Algorithmus Algorithmus von Bellmann und Ford] beispielsweise bricht die Suche nach dem kürzesten Weg ab, sobald er einen negativen Zyklus entdeckt, aber andernfalls kann er negative Gewichte problemlos verarbeiten. &lt;br /&gt;
&lt;br /&gt;
Die Detektion negativer Zyklen hat wiederum eine interessante Anwendung bei Währungsgraphen: Ein Zyklus bedeutet hier, dass man Geld über mehrere Stufen von einer Währung in die nächste und am Schluß wieder in die Originalwährung umtauscht, und ein negativer Zyklus führt dazu, dass man am Ende &amp;lt;i&amp;gt;mehr&amp;lt;/i&amp;gt; Geld besitzt als am Anfang (damit negative Zyklen wirklich einen Gewinn bedeuten und keinen Verlust, müssen die Wechselkurse vor der Logarithmierung in [http://de.wikipedia.org/wiki/Wechselkurs#Nominaler_Wechselkurs Preisnotierung] angegeben sein). Bei Privatpersonen ist dies ausgeschlossen, weil die Umtauschgebühren den möglichen Gewinn mehr als aufzehren. Banken mit direktem weltweitem Börsenzugang hingegen unternehmen große Anstrengungen, um solche negativen Zyklen möglichst schnell (nämlich vor der Konkurrenz) zu entdecken und auszunutzen. Diese Geschäftsmethode bezeichnet man als [http://de.wikipedia.org/wiki/Arbitrage Arbitrage] und die Existenz eines negativen Zyklus als Arbitragegelegenheit. Durch die Kursschwankungen (und durch die ausgleichende Wirkung der Arbitragegeschäfte selbst) existieren die Arbitragegelegenheiten nur für kurze Zeit, und ihre Detektion erfordert leistungsfähige Echtzeitalgorithmen.&lt;br /&gt;
&lt;br /&gt;
In dieser Vorlesung beschränken wir uns hingegen auf Graphen mit ausschließlich positiven Gewichten. In diesem Fall ist der Algorithmus von Dijkstra die Methode der Wahl, weil er wesentlich schneller arbeitet als der Bellmann-Ford-Algorithmus.&lt;br /&gt;
&lt;br /&gt;
=== Algorithmus von Dijkstra ===&lt;br /&gt;
&lt;br /&gt;
==== Edsger Wybe Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
geb. 11. Mai 1930 in Rotterdam&lt;br /&gt;
&lt;br /&gt;
ges. 06. August 2002&lt;br /&gt;
&lt;br /&gt;
Dijkstra war ein niederländischer Informatiker und Wegbereiter der strukturierten Programmierung. 1972 erhielt er für seine Leistung in der Technik und Kunst der Programmiersprachen den Turing Award, der jährlich von der Association for Computing Machinery (ACM) an Personen verliehen wird, die sich besonders um die Entwicklung der Informatik verdient gemacht haben. Zu seinen Beiträgen zur Informatik gehören unter anderem der Dijkstra-Algorithmus zur Berechnung des kürzesten Weges in einem Graphen sowie eine Abhandlung über den go-to-Befehl und warum er nicht benutzt werden sollte. Der go-to-Befehl war in den 60er und 70er Jahren weit verbreitet, führte aber zu Spaghetti-Code. In seinem berühmten Paper &amp;quot;A Case against the GO TO Statement&amp;quot;[http://www.cs.utexas.edu/users/EWD/ewd02xx/EWD215.PDF], das als Brief mit dem Titel &amp;quot;Go-to statement considered harmful&amp;quot; veröffentlicht wurde, argumentiert Dijkstra, dass es umso schwieriger ist, dem Quellcode eines Programmes zu folgen, je mehr go-to-Befehle darin enthalten sind und zeigt, dass man auch ohne diesen Befehl gute Programme schreiben kann.&lt;br /&gt;
&lt;br /&gt;
==== Algorithmus ====&lt;br /&gt;
&lt;br /&gt;
Der Dijkstra-Algorithmus für kürzeste Wege ist dem oben vorgestellten Algorithmus &amp;lt;tt&amp;gt;shortestPath()&amp;lt;/tt&amp;gt; auf der Basis von Breitensuche sehr ähnlich. Insbesondere gilt auch hier, dass neben dem kürzesten Weg vom Start zum Ziel auch alle kürzesten Wege gefunden werden, deren Endknoten dem Start näher sind als der Zielknoten. Aufgrund der Kantengewichte gibt es aber einen wichtigen Unterschied: Der erste gefundene Weg zu einem Knoten ist nicht mehr notwendigerweise der kürzeste. Wir bestimmen deshalb für jeden Knoten mehrere Kandidatenwege und verwenden eine Prioritätswarteschlange (statt einer einfachen First in - First out - Queue), um diese Wege nach ihrer Länge zu sortieren. Die Kandidatenwege für einen gegebenen Knoten werden unterschieden, indem wir auch den Vorgängerknoten im jeweiligen Weg speichern. Wenn ein Knoten &amp;lt;i&amp;gt;erstmals&amp;lt;/i&amp;gt; an die Spitze der Prioritätswarteschlange gelangt, haben wir den kürzesten Weg zu diesem Knoten gefunden (das wird weiter unten formal bewiesen), und der Vorgänger des Knotens in diesem Weg wird zu seinem Vaterknoten. Erscheint derselbe Knoten später nochmals an der Spitze der Prioritätswarteschlange, handelt es sich um einen Kandidatenweg, der sich nicht als kürzester erwiesen hat und deshalb ignoriert werden kann. Wir erkennen dies leicht daran, dass der Vaterknoten in der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; bereits gesetzt ist. &lt;br /&gt;
&lt;br /&gt;
Eine geeignete Datenstruktur für die Prioritätswarteschlange wird durch das Python-Modul [http://docs.python.org/library/heapq.html heapq] realisiert. Es verwendet ein normales Pythonarray als unterliegende Repräsentation für einen Heap und stellt effiziente &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt;-Funktionen zur Verfügung. Dies entspricht genau unserer Vorgehensweise im Kapitel [[Prioritätswarteschlangen]]. Als Datenelement erwartet die Funktion &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; ein Tupel, dessen erstes Element die Priorität sein muss. Die übrigen Elemente des Tupels (und damit auch deren Anzahl) können je nach Anwendung frei festgelegt werden. Wir legen fest, dass das zweite Element den Endknoten des betrachteten Weges und das dritte den Vorgängerknoten speichert. &lt;br /&gt;
&lt;br /&gt;
Die Kantengewichte werden dem Algorithmus in der property map &amp;lt;tt&amp;gt;weights&amp;lt;/tt&amp;gt; übergeben:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;code python&amp;gt;&lt;br /&gt;
    import heapq	                  # heapq implementiert die Funktionen für Heaps&lt;br /&gt;
    &lt;br /&gt;
    def dijkstra(graph, weights, startnode, destination):&lt;br /&gt;
        parents = [None]*len(graph)       # registriere für jeden Knoten den Vaterknoten im Pfadbaum&lt;br /&gt;
      &lt;br /&gt;
        q = []                            # Array q wird als Heap verwendet&lt;br /&gt;
        &amp;lt;font color=red&amp;gt;heapq.heappush(q, (0.0, startnode, startnode))&amp;lt;/font&amp;gt;  # Startknoten in Heap einfügen&lt;br /&gt;
      &lt;br /&gt;
        while len(q) &amp;gt; 0:                 # solange es noch Knoten im Heap gibt:&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;length, node, predecessor = heapq.heappop(q)&amp;lt;/font&amp;gt;   # Knoten aus dem Heap nehmen&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;if parents[node] is not None:&amp;lt;/font&amp;gt; # parent ist schon gesetzt =&amp;gt; es gab einen anderen, kürzeren Weg&lt;br /&gt;
                &amp;lt;font color=red&amp;gt;continue&amp;lt;/font&amp;gt;                  #   =&amp;gt; wir können diesen Weg ignorieren&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;parents[node] = predecessor&amp;lt;/font&amp;gt;   # parent setzen&lt;br /&gt;
            if node == destination:       # Zielknoten erreicht&lt;br /&gt;
                break                     #   =&amp;gt; Suche beenden&lt;br /&gt;
            for neighbor in graph[node]:  # die Nachbarn von node besuchen,&lt;br /&gt;
                if parents[neighbor] is None:   # aber nur, wenn ihr kürzester Weg noch nicht bekannt ist&lt;br /&gt;
                    &amp;lt;font color=red&amp;gt;newLength = length + weights[(node,neighbor)]&amp;lt;/font&amp;gt;   # berechne Pfadlänge zu neighbor              &lt;br /&gt;
                    &amp;lt;font color=red&amp;gt;heapq.heappush(q, (newLength, neighbor, node))&amp;lt;/font&amp;gt;  # und füge neighbor in den Heap ein&lt;br /&gt;
      &lt;br /&gt;
        if parents[destination] is None:  # Suche wurde beendet ohne den Zielknoten zu besuchen&lt;br /&gt;
            return None, None             # =&amp;gt; kein Pfad gefunden (unzusammenhängender Graph)&lt;br /&gt;
      &lt;br /&gt;
        # Pfad durch die parents-Kette zurückverfolgen und speichern&lt;br /&gt;
        path = [destination]&lt;br /&gt;
        while path[-1] != startnode:&lt;br /&gt;
            path.append(parents[path[-1]])&lt;br /&gt;
        path.reverse()                    # Reihenfolge umdrehen (Ziel =&amp;gt; Start wird zu Start =&amp;gt; Ziel)&lt;br /&gt;
        return path, length               # gefundenen Pfad und dessen Länge zurückgeben&lt;br /&gt;
  &amp;lt;/code&amp;gt;&lt;br /&gt;
Die wesentlichen Unterschiede zur Breitensuche sind im Code rot markiert: Anstelle der Queue verwenden wir jetzt einen Heap, und der Startknoten wird mit Pfadlänge 0 als erstes eingefügt. In der Schleife &amp;lt;tt&amp;gt;while len(q) &amp;gt; 0:&amp;lt;/tt&amp;gt; wird jeweils der Knoten &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; mit der aktuell kürzesten Pfadlänge aus dem Heap entfernt. Die Pfadlänge vom Start zu diesem Knoten wird in der Variable &amp;lt;tt&amp;gt;length&amp;lt;/tt&amp;gt; gespeichert, sein Vorgänger in der Variable &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;. Wenn der aktuelle Weg nicht der kürzeste ist (&amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; war bereits gesetzt), wird dieser Weg ignoriert. Andernfalls werden die property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; aktualisiert und die Nachbarn von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; besucht. Beim Scannen der Nachbarn berechnen wir zunächst die Länge &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; das Weges &amp;lt;tt&amp;gt;startnode =&amp;amp;gt; node =&amp;amp;gt; neighbor&amp;lt;/tt&amp;gt; als Summe von &amp;lt;tt&amp;gt;length&amp;lt;/tt&amp;gt; und dem Gewicht der Kante &amp;lt;tt&amp;gt;(node, neighbode)&amp;lt;/tt&amp;gt;. Diese Länge wird beim Einfügen des Nachbarknotens in den Heap zur Priorität des aktuellen Weges.&lt;br /&gt;
&lt;br /&gt;
Die wichtigsten Prinzipien des Dijkstra-Algorithmus noch einmal im Überblick:&lt;br /&gt;
* Der Dijkstra-Algorithmus ist Breitensuche mit Prioritätswarteschlange (Heap) statt einer einfache Warteschlange (Queue).&lt;br /&gt;
* Die Prioritätswarteschlange speichert alle Wege, die bereits gefunden worden sind und ordnet sie aufsteigend nach ihrer Länge. &lt;br /&gt;
* Das Sortieren (und damit der ganze Algorithmus) funktioniert nur mit positiven Kantengewichten korrekt.&lt;br /&gt;
* Da ein Knoten auf mehreren Wegen erreichbar sein kann, kann er auch mehrmals im Heap sein. &lt;br /&gt;
* Wenn ein Knoten &amp;lt;i&amp;gt;erstmals&amp;lt;/i&amp;gt; aus der Prioritätswarteschlange entnommen wird, ist der gefundene Weg der kürzeste zu diesem Knoten. Andernfalls wird der Weg ignoriert.&lt;br /&gt;
* Wenn der Knoten &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; aus dem Heap entnommen wird, ist der kürzeste Weg von Start nach Ziel gefunden, und die Suche kann beendet werden.&lt;br /&gt;
In unserer Implementation können, wie gesagt, mehrere Wege zum selben Knoten gleichzeitig in der Prioritätswarteschlange sein. Im Prinzip wäre es auch möglich, immer nur den besten zur Zeit bekannten Weg zu jedem Enknoten in der Prioritätswarteschlange zu halten - sobald ein besserer Kandidat gefunden wird, ersetzt er den bisherigen Kandidaten, anstatt zusätzlich eingefügt zu werden. Dies erfordert aber eine wesentlich kompliziertere Prioritätswarteschlange, die eine effiziente &amp;lt;tt&amp;gt;updatePriority&amp;lt;/tt&amp;gt;-Funktion anbietet, ohne dass dadurch eine signifikante Beschleunigung erreicht wird. Deshalb verfolgen wir diesen Ansatz nicht.&lt;br /&gt;
&lt;br /&gt;
==== Beispiel ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;under construction&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Image:Bsp.jpg]]&lt;br /&gt;
&lt;br /&gt;
==== Komplexität von Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
Zur Analyse der Komplexität nehmen wir an, dass der Graph V Knoten und E Kanten hat. Die Initialisierung der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; am Anfang der Funktion hat offensichtlich Komplexität O(V), weil Speicher für V Knoten allokiert wird. Der Code am Ende der Funktion, der aus der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; den Pfad extrahiert, hat ebenfalls die Komplexität O(V), weil der Pfad im ungünstigen Fall sämtliche Knoten des Graphen umfasst. Beides wird durch die Komplexität der Hauptschleife dominiert, zu deren Analyse wir den folgenden Codeausschnitt genauer anschauen wollen:&lt;br /&gt;
&lt;br /&gt;
      while len(q) &amp;gt; 0:&lt;br /&gt;
           ... # 1&lt;br /&gt;
           if parents[node] is not None: &lt;br /&gt;
               continue                  &lt;br /&gt;
           parents[node] = predecessor&lt;br /&gt;
           ... # 2&lt;br /&gt;
Wir erkennen, dass der Codeabschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; für jeden Knoten höchstens einmal erreicht werden kann: Da &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; beim ersten Durchlauf gesetzt wird, kann die &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage beim gleichen Knoten nie wieder &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt; liefern, und das nachfolgende &amp;lt;tt&amp;gt;continue&amp;lt;/tt&amp;gt; bewirkt, dass der Abschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; dann übersprungen wird. Man sagt auch, dass jeder Knoten &amp;lt;i&amp;gt;höchstens einmal expandiert&amp;lt;/i&amp;gt; wird, auch wenn er mehrmals im Heap war. &lt;br /&gt;
&lt;br /&gt;
Der Codeabschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; selbst enthält eine Schleife über alle ausgehenden Kanten des Knotens &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt;. Im ungünstigsten Fall iterieren wir bei &amp;lt;i&amp;gt;allen&amp;lt;/i&amp;gt; Knoten über &amp;lt;i&amp;gt;alle&amp;lt;/i&amp;gt; ausgehenden Kanten, aber das sind gerade alle Kanten des Graphen je einmal in den beiden möglichen Richtungen. Die Funktion &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; wird sogar höchstens E Mal aufgerufen, weil eine Kante nur in den Heap eingefügt wird, wenn der kürzeste Weg der jeweiligen Endknotens noch nicht bekannt ist (siehe die &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage in der &amp;lt;tt&amp;gt;for&amp;lt;/tt&amp;gt;-Schleife), und das ist nur ein einer Richtung möglich. Dies hat zwei Konsequenzen:&lt;br /&gt;
* Die Schleife &amp;lt;tt&amp;gt;while len(q) &amp;gt; 0:&amp;lt;/tt&amp;gt; wird nur so oft ausgeführt, wie Elemente im Heap sind, also höchstens E Mal. Das gleiche gilt für den Codeabschnitt &amp;lt;tt&amp;gt;# 1&amp;lt;/tt&amp;gt;, der das &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; enthält.&lt;br /&gt;
* Die Operationen &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; haben logarithmische Komplexität in der Größe des Heaps, sind also in &amp;lt;math&amp;gt;O(\log\,E)&amp;lt;/math&amp;gt;. In einfachen Graphen gilt aber &amp;lt;math&amp;gt;E = O(V^2)&amp;lt;/math&amp;gt;, so dass sich die Komplexität der Heapoperationen vereinfacht zu &amp;lt;math&amp;gt;O(\log\,E)=O(\log\,V^2)=O(2\log\,V)=O(\log\,V)&amp;lt;/math&amp;gt;.&lt;br /&gt;
Zusammenfassend gilt: &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; werden maximal E Mal aufgerufen und haben eine Komplexität in &amp;lt;math&amp;gt;O(\log\,V)&amp;lt;/math&amp;gt;. Folglich hat der Algorithmus von Dijkstra die Komplexität:&lt;br /&gt;
:&amp;lt;math&amp;gt;O(E\,\log\,V)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Vergleich mit Breitensuche und Tiefensuche ====&lt;br /&gt;
&lt;br /&gt;
Der Dijkstra-Algorithmus ist eng mit der Breiten- und Tiefensuche verwandt - man kann diese Algorithmen aus dem Dijkstra-Algorithmus gewinnen, indem man einfach die Regel zur Festlegung der Prioritäten ändert. Anstelle der Länge des Pfades verwenden wir als Priorität den Wert eine Zählvariable &amp;lt;tt&amp;gt;count&amp;lt;/tt&amp;gt;, die nach jeder Einfügung in den Heap (also nach jedem Aufruf von &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt;) aktualisiert wird. Zählen wir die Variable hoch, haben die zuerst eingefügten Kanten die höchste Priorität, der Heap verhält sich also wie eine Queue (First in-First out), und wir erhalten eine Breitensuche. Zählen wir die Variable hingegen (von E beginnend) herunter, haben die zuletzt eingefügten Kanten höchste Priorität. Der Heap verhält sich dann wie ein Stack (Last in-First out), und wir bekommen Tiefensuche. Statt eines Heaps plus Zählvariable kann man jetzt natürlich direkt eine Queue bzw. einen Stack verwenden. Dadurch fällt der Aufwand &amp;lt;math&amp;gt;O(\log\,V)&amp;lt;/math&amp;gt; für die Heapoperationen weg und wird durch die effizienten O(1)-Operationen von Queue bzw. Stack ersetzt. Damit erhalten wir für Breiten- und Tiefensuche die schon bekannte Komplexität O(E).&lt;br /&gt;
&lt;br /&gt;
==== Korrektheit von Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
Wir beweisen mittels vollständiger Induktion die Schleifen-Invariante: Falls &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; gesetzt (also ungleich &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;) ist, dann liefert das Zurückverfolgen des Weges von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; nach &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; den kürzesten Weg. &lt;br /&gt;
;Induktionsanfang: &amp;lt;tt&amp;gt;parents[startnode]&amp;lt;/tt&amp;gt; ist als einziges gesetzt. Zurückverfolgen liefert den trivialen Weg &amp;lt;tt&amp;gt;[startnode]&amp;lt;/tt&amp;gt;, der mit Länge 0 offensichtlich der kürzeste Pfad ist &amp;amp;rarr; die Bedingung ist erfüllt.&lt;br /&gt;
;Induktionsschritt: Wir zeigen mit einem indirektem Beweis, dass wir immer einen kürzesten Weg bekommen, wenn &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; gesetzt wird.&lt;br /&gt;
:Sei &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt; = &amp;lt;tt&amp;gt;{v | parents[v] is not None}&amp;lt;/tt&amp;gt; die Menge aller Knoten, von denen wir den kürzesten Weg schon kennen (Induktionsvoraussetzung), und &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; der Knoten, der sich gerade an der Spitze des Heaps befindet. Dann ist &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; der Vorgänger von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; im aktuellen Weg, und es muss &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt; gelten, weil die Nachbarn von &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; (und damit auch der aktuelle &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt;) erst in den Heap eingefügt werden, wenn der kürzeste Weg für &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; gefunden wurde. Man beachte auch, dass alle Knoten, die noch nicht in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt; enthalten sind, weiter vom Start entfernt sind als alle Knoten in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt;, weil alle neu in den Heap eingefügten Wege länger sind als der kürzeste Weg des jeweiligen Vorgängers. &lt;br /&gt;
:Der indirekte Beweis nimmt jetzt an, dass der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; nicht der kürzeste Weg ist. Dann muss es einen anderen, kürzeren Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; geben. Für den Vorgänger &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; in diesem Weg unterscheiden wir zwei Fälle:&lt;br /&gt;
:* &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt;: In diesem Fall ist die Länge des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; bereits bekannt, und dieser Weg ist in der Prioritätswarteschlange enthalten. Dann kann er aber nicht der kürzeste sein, denn an der Spitze der Warteschlange war nach Voraussetzung der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt;.&lt;br /&gt;
:* &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\notin S&amp;lt;/math&amp;gt;: Die Kosten des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; berechnen sich als &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) + weight[(x, node)]&amp;lt;/tt&amp;gt;, und die Kosten des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; sind analog &amp;lt;tt&amp;gt;Kosten(predecessor &amp;amp;rarr; startnode) + weight[(predecessor, node)]&amp;lt;/tt&amp;gt;. Aufgrund der Induktionsvoraussetzung gilt aber &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt;, und somit &amp;lt;tt&amp;gt;Kosten(predecessor &amp;amp;rarr; startnode) &amp;amp;lt; Kosten(x &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt;, weil &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; andernfalls vor &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; an der Spitze des Heaps gewesen wäre, was mit der Annahme &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\notin S&amp;lt;/math&amp;gt; unverträglich ist. Damit der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; trotzdem der kürzeste Weg sein kann, müsste &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) &amp;amp;lt; Kosten(node &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt; gelten, denn durch die Kante &amp;lt;tt&amp;gt;(x, node)&amp;lt;/tt&amp;gt; kommen ja noch Kosten hinzu. Das wäre aber nur möglich, wenn der Knoten &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; vor dem Knoten &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; an die Spitze des Heaps gelangt, im Widerspruch zur Annahme, dass &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; sich gerade an der Spitze des Heaps befindet. Somit kann die Behauptung, dass der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; der kürzeste Weg ist, nicht stimmen.&lt;br /&gt;
In beiden Fällen erhalten wir einen Widerspruch, und die Behauptung ist somit bewiesen. Da die Invariante insbesondere für den Weg zum Zielknoten &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; erfüllt ist, folgt daraus auch die Korrektheit des Algorithmus von Dijkstra.&lt;br /&gt;
&lt;br /&gt;
===  A*-Algorithmus - Wie kann man Dijkstra noch verbessern? ===&lt;br /&gt;
&lt;br /&gt;
Eine wichtige Eigenschaft des Dijkstra-Algorithmus ist, dass neben dem kürzesten Weg vom Start zum Ziel auch die kürzesten Wege zu allen Knoten berechnet werden, die näher am Startknoten liegen als das Ziel, obwohl uns diese Wege gar nicht interessieren. Sucht man beispielsweise in einem Graphen mit den Straßenverbindungen in Deutschland den kürzesten Weg von Frankfurt (Main) nach Dresden (ca. 460 km), werden auch die kürzesten Wege von Frankfurt nach Köln (190 km), Dortmund (220 km) und Stuttgart (210 km) und vielen anderen Städten gefunden. Aufgrund der geographischen Lage dieser Städte ist eigentlich von vornherein klar, dass sie mit dem kürzesten Weg nach Dresden nicht das geringste zu tun haben. Anders sieht es mit Erfurt (260 km) oder Suhl (210 km) aus - diese Städte liegen zwischen Frankfurt und Dresden und kommen deshalb als Zwischenstationen des gesuchten Weges in Frage.&lt;br /&gt;
&lt;br /&gt;
Damit Dijkstra korrekt funktioniert, würde es im Prinzip ausreichen, wenn man die kürzesten Wege nur für diejenigen Knoten ausrechnet, die auf dem kürzesten Weg vom Start zum Ziel liegen, denn nur diese Knoten braucht man, um den gesuchten Weg über die &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette zurückzuverfolgen. Das Problem ist nur, dass man diese Knoten erst kennt, wenn der Algorithmus fertig durchgelaufen ist. Schließt man Knoten zu früh von der Betrachtung aus, kommt am Ende möglicherweise nicht der korrekte kürzeste Weg heraus. &lt;br /&gt;
&lt;br /&gt;
Der A*-Algorithmus löst dieses Dilemma mit folgender Idee: Ändere die Prioritäten für den Heap so ab, dass unwichtige Knoten nur mit geringerer Wahscheinlichkeit expandiert werden, aber stelle gleichzeitig sicher, dass alle wichtigen Knoten (also diejenigen auf dem korrekten kürzesten Weg) auf jeden Fall expandiert werden. Es zeigt sich, dass man diese Idee umsetzen kann, wenn eine &amp;lt;i&amp;gt;Schätzung für den Restweg&amp;lt;/i&amp;gt; (also für die noch verbleibende Entfernung von jedem Knoten zum Ziel) verfügbar ist:&lt;br /&gt;
 rest = guess(neighbor, destination)&lt;br /&gt;
Diese Schätzung addiert man einfach zur wahren Länge des Weges &amp;lt;tt&amp;gt;startnode &amp;amp;rarr; node&amp;lt;/tt&amp;gt; dazu, um die verbesserte Priorität zu erhalten:&lt;br /&gt;
 priority = newLength + guess(neighbor, destination)&lt;br /&gt;
(Im originalen Dijkstra-Algorithmus wird als Priorität nur &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; allein verwendet. Man beachte, dass man &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; jetzt zusätzlich im Heap speichern muss, weil man es für die Expansion des Knotens später noch benötigt.)&lt;br /&gt;
&lt;br /&gt;
Damit sicher gestellt ist, dass der A*-Algorithmus immer noch die korrekten kürzesten Wege findet, darf die Schätzung den wahren Restweg &amp;lt;i&amp;gt;niemals überschätzen&amp;lt;/i&amp;gt;. Es muss immer gelten:&lt;br /&gt;
 0 &amp;lt;= guess(node, destination) &amp;lt;= trueDistance(node, destination)&lt;br /&gt;
Damit gilt insbesondere &amp;lt;tt&amp;gt;guess(destination, destination) = trueDistance(destination, destination) = 0&amp;lt;/tt&amp;gt;, an der Priorität des Knotens &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; ändert sich also nichts. Die Prioritäten aller anderen Knoten veschlechtern sich hingegen, weil zur bisherigen Priorität noch atwas addiert wird. Für die wichtigen Knoten auf dem kürzesten Weg vom Start nach Ziel gilt jedoch, dass deren neue Priorität immer noch besser ist als die Priorität des Zielknotens selbst. Für diese Knoten gilt nämlich&lt;br /&gt;
 falls node auf dem kürzesten Weg von startnode nach destination liegt:&lt;br /&gt;
 trueDistance(startnode, node) + guess(node, destination) &amp;lt;= trueDistance(startnode, destination)&lt;br /&gt;
weil der Weg von Start nach &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; ein Teil des kürzesten Wegs von Start nach Ziel ist und die Restschätzung die wahre Entfernung immer unterschätzt. Diese Knoten werden deshalb stets vor dem Zielknoten expandiert, so dass wir die &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette immer noch korrekt zurückverfolgen können. Für alle anderen Knoten gilt idealerweise, dass die neue Priorität schlechter ist als die Priorität von &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt;, so dass man sich diese irrelevanten Knotenexpansionen sparen kann.&lt;br /&gt;
&lt;br /&gt;
Für das Beispiel eines Straßennetzwerks bietet sich als Schätzung die Luftlinienentfernung an, weil Straßen nie kürzer sein können als die Luftlinie. Damit erreicht man in der Praxis deutliche Einsparungen. Generell gilt, dass der A*-Algorithmus im typischen Fall schneller ist als der Algorithmus von Dijkstra, aber man kann immer pathologische Fälle konstruieren, wo die Änderung der Prioritäten nichts bringt. Die Komplexität des A*-Algorithmus im ungünstigen Fall ist deshalb nach wie vor &amp;lt;math&amp;gt;O(E\,\log\,V)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=='''Minimaler Spannbaum'''==&lt;br /&gt;
'''(engl.: minimum spanning tree; abgekürzt: MST)'''&lt;br /&gt;
&lt;br /&gt;
[[Image:Minimum_spanning_tree.png‎ |thumb|200px|right|Ein minimal aufspannender Baum verbindet alle Punkte eines Graphen bei minimaler Kantenlänge ([http://de.wikipedia.org/wiki/Spannbaum Quelle])]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: gewichteter Graph G, zusammenhängend&amp;lt;br/&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: Untermenge &amp;lt;math&amp;gt;E'\subseteq E&amp;lt;/math&amp;gt; der Kanten, so dass die Summe der Kantengewichte &amp;lt;math&amp;gt;\sum_{e\in E'} w_e&amp;lt;/math&amp;gt; minimal und der entstehende Graph G' zusammenhängend ist.&amp;lt;br/&amp;gt;&lt;br /&gt;
* G' definiert immer einen Baum, denn andernfalls könnte man eine Kante weglassen und dadurch die Summe &amp;lt;math&amp;gt;\sum_{e\in E'} w_e&amp;lt;/math&amp;gt; verringern, ohne dass sich am Zusammenhang von G' etwas ändert. &amp;lt;br/&amp;gt;&lt;br /&gt;
* Wenn der Graph G nicht zusammenhängend ist, kann man den Spannbaum für jede Zusammenhangskomponente getrennt ausrechnen. Man erhält dann einen aufspannenden Wald. &lt;br /&gt;
* Der MST ist ähnlich wie der Dijkstra-Algorithmus: Dort ist ein Pfad gesucht, bei dem die Summe der Gewichte über den Pfad minimal ist. Beim MST suchen wir eine Lösung, bei der die Summe der Gewichte über den ganzen Graphen minimal ist. &lt;br /&gt;
* Das Problem des MST ist nahe verwandt mit der Bestimmung der Zusammenhangskomponente, z.B. über den Tiefensuchbaum. Für die Zusammenhangskomponenten genügt allerdings ein beliebiger Baum, während beim MST ein minimaler Baum gesucht ist.&lt;br /&gt;
&lt;br /&gt;
=== Anwendungen ===&lt;br /&gt;
==== Wie verbindet man n gegebene Punkte mit möglichst kurzen Straßen (Eisenbahnen, Drähten [bei Schaltungen] usw.)?====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:center&amp;quot; border=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; cellspacing=&amp;quot;0&amp;quot; &lt;br /&gt;
|MST minimale Verbindung (Abb.1)&lt;br /&gt;
|MST = 2 (Länge = Kantengewicht)(Abb.2)&lt;br /&gt;
|- valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| [[Image:mst.png]] &lt;br /&gt;
| [[Image:Gleichseitigesdreieck.png]]&lt;br /&gt;
|}&lt;br /&gt;
*In der Praxis: Die Festlegung, dass man nur die gegebenen Punkte verwenden darf, ist eine ziemliche starke Einschränkung. &lt;br /&gt;
&lt;br /&gt;
* Wenn man sich vorstellt, es sind drei Punkte gegeben, die als gleichseitiges Dreieck angeordnet sind, dann ist der MST (siehe Abb.2, schwarz gezeichnet) und hat die Länge 2. Man kann hier die Länge als Kantengewicht verwenden. &lt;br /&gt;
&lt;br /&gt;
* Wenn es erlaubt ist zusätzliche Punkte einzufügen, dann kann man in der Mitte einen neuen Punkt setzen &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; neuer MST (siehe Abb.2, orange gezeichnet).&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Höhe = &amp;lt;math&amp;gt;\frac{1}{2}\sqrt{3}&amp;lt;/math&amp;gt;, Schwerpunkt: teilt die Höhe des Dreiecks im Verhältnis 2:1; der Abstand von obersten Punkt bis zum neu eingeführten Punkt: &amp;lt;math&amp;gt;\frac{2}{3}h = \frac{\sqrt{3}}{3}&amp;lt;/math&amp;gt;, davon insgesamt 3 Stück, damit (gilt für den MST in orange eingezeichnet): MST = &amp;lt;math&amp;gt;3\left(\frac{1}{3}\right) \sqrt{3} = \sqrt{3} \approx 1,7&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Damit ist der MST in orange kürzer als der schwarz gezeichnete MST. &amp;lt;br\&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt;Folgerung: MST kann kürzer werden, wenn man einen Punkt dazu nimmt. &lt;br /&gt;
* Umgekehrt kann der MST auch kürzer werden, wenn man einen Punkt aus dem Graphen entfernt, aber wie das Beipiel des gleichseitigen Dreiecks zeigt, ist dies nicht immer der Fall.&lt;br /&gt;
&lt;br /&gt;
[[Image: bahn.png|Bahnstrecke Verbindung (Abb.3)]]&lt;br /&gt;
&lt;br /&gt;
* Methode der zusätzlichen Punkteinfügung hat man früher beim Bahnstreckenbau verwendet. Durch Einführung eines Knotenpunktes kann die Streckenlänge verkürzt werden (Dreiecksungleichung).&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Bestimmung von Datenclustern ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Image:cluster.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Daten (in der Abb.: Punkte) bilden Gruppen. &lt;br /&gt;
&lt;br /&gt;
* In der Abbildung hat man 2 verschiedene Messungen gemacht (als x- und y-Achse aufgetragen), bspw. Größe und Gewicht von Personen. Für jede Person i wird ein Punkt an der Koordinate (Größe&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;, Gewicht&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;) gezeichnet (siehe Bild a). Dies bezeichnet man als ''Scatter Plot''. Wenn bestimmte Wertkombinationen häufiger auftreten als andere, bilden sich mitunter Gruppen aus, bspw. eine Gruppe für &amp;quot;klein und schwer&amp;quot; etc.&lt;br /&gt;
&lt;br /&gt;
* Durch Verbinden der Punkte mittels eines MST (siehe Abbildung (b)) sieht man, dass es kurze (innerhalb der Gruppen) und lange Kanten (zwischen den Gruppen) gibt. &lt;br /&gt;
&lt;br /&gt;
* Wenn man geschickt eine Schwelle einführt und alle Kanten löscht, die länger sind als die Schwelle, dann bekommt man als Zusammenhangskomponente die einzelnen Gruppen. &lt;br /&gt;
&lt;br /&gt;
=== Algorithmen ===&lt;br /&gt;
&lt;br /&gt;
Genau wie bei der Bestimmung von Zusammenhangskomponenten kann man auch das MST-Problem entweder nach dem Anlagerungsprinzip oder nach dem Verschmelzungsprinzip lösen (dazu gibt es noch weitere Möglichkeiten, z.B. den [http://de.wikipedia.org/wiki/Algorithmus_von_Bor%C5%AFvka Algorithmus von Boruvka]). Der Anlagerungsalgorithmus für MST wurde zuerst von Prim beschrieben und trägt deshalb seinen Namen, der Verschmelzungsalgorithmus stammt von Kruskal. Im Vergleich zu den Algorithmen für Zusammenhangskomponenten ändert sich im wesentlichen nur die Reihenfolge, in der die Kanten betrachtet werden: Eine Prioritätswarteschlange stellt jetzt sicher, dass am Ende wirklich der Baum mit den geringstmöglichen Kosten herauskommt.&lt;br /&gt;
&lt;br /&gt;
====Algorithmus von Prim====&lt;br /&gt;
[http://de.wikipedia.org/wiki/Algorithmus_von_Prim Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus von Prim geht nach dem Anlagerungsprinzip vor (vgl. den Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Tiefensuche|Zusammenhangskomponenten mit Tiefensuche]]): Starte an der Wurzel (ein willkürlich gewählter Knoten) und füge jeweils die günstigste Kante an die aktuellen Teillösung an, die keinen Zyklus verursacht. Die Sortierung der Kanten nach Priorität erfolgt analog zum Dijsktra-Algorithmus, aber die Definitionen, welche Kante die günstigste ist, unterscheiden sich. Die Konvention für die Bedeutung der Elemente des Heaps ist ebenfalls identisch: ein Tupel mit &amp;lt;tt&amp;gt;(priority, node, predecessor)&amp;lt;/tt&amp;gt;. Die folgende Implementation verdeutlicht sehr schön die Ähnlichkeit der beiden Algorithmen. Das Ergebnis wird als property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; zurückgegeben, in der für jeden Knoten sein Vorgänger im MST steht, wobei die Wurzel wie üblich auf sich selbst verweist.&lt;br /&gt;
&lt;br /&gt;
 import heapq&lt;br /&gt;
 &lt;br /&gt;
 def prim(graph, weights):                # Kantengewichte wie bei Dijkstra als property map&lt;br /&gt;
 	sum = 0.0                         # wird später das Gewicht des Spannbaums sein&lt;br /&gt;
 	start = 0                         # Knoten 0 wird willkürlich als Wurzel gewählt&lt;br /&gt;
        &lt;br /&gt;
        parents = [None]*len(graph)       # property map, die den resultierenden Baum kodiert&lt;br /&gt;
        parents[start] = start            # Wurzel zeigt auf sich selbst&lt;br /&gt;
        &lt;br /&gt;
 	heap = []                         # Heap für die Kanten des Graphen&lt;br /&gt;
 	for neighbor in graph[start]:     # besuche die Nachbarn von start&lt;br /&gt;
 	    heapq.heappush(heap, (weights[(start, neighbor)], neighbor, start))  # und fülle Heap &lt;br /&gt;
 	&lt;br /&gt;
        while len(heap) &amp;gt; 0:&lt;br /&gt;
 	    w, node, predecessor = heapq.heappop(heap) # hole billigste Kante aus dem Heap&lt;br /&gt;
            if parents[node] is not None: # die Kante würde einen Zyklus verursachen&lt;br /&gt;
                continue                  #   =&amp;gt; ignoriere diese Kante&lt;br /&gt;
            parents[node] = predecessor   # füge Kante in den MST ein&lt;br /&gt;
            sum += w                      # und aktualisiere das Gesamtgewicht &lt;br /&gt;
            for neighbor in graph[node]:  # besuche die Nachbarn von node&lt;br /&gt;
                if parents[neighbor] is None:  # aber nur, wenn kein Zyklus entsteht&lt;br /&gt;
                    heapq.heappush(heap, (weights[(node,neighbor)], neighbor, node)) # füge Kandidaten in Heap ein&lt;br /&gt;
        &lt;br /&gt;
 	return parents, sum               # MST und Gesamtgewicht zurückgeben&lt;br /&gt;
&lt;br /&gt;
====Algorithmus von Kruskal====&lt;br /&gt;
[http://de.wikipedia.org/wiki/Algorithmus_von_Kruskal Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Kruskal%27s_algorithm (en)]&lt;br /&gt;
&lt;br /&gt;
Die alternative Vorgehensweise ist das Verschmelzungsprinzip (vgl. den Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Union-Find-Algorithmus|Zusammenhangskomponenten mit Union-Find-Algorithmus]]), das der Algorithmus von Kruskal verwendet. Jeder Knoten wird zunächst als trivialer Baum mit nur einem Knoten betrachtet, und alle Kanten werden aufsteigend nach Gewicht sortiert. Dann wird die billigste noch nicht betrachtete Kante in den MST eingefügt, falls sich dadurch kein Zyklus bildet (erkennbar daran, dass die Endknoten in verschiedenen Zusammenhangskomponenten liegen, das heisst verschiedene Anker haben). Da der fertige Baum (V-1) Kanten haben muss, wird dies (V-1) Mal zutreffen. Andernfalls wird diese Kante ignoriert. Anders ausgedrückt: Der Algorithmus beginnt mit ''V'' Bäumen; in (''V''-1) Verschmelzungsschritten kombiniert er jeweils zwei Bäume (unter Verwendung der kürzesten möglichen Kante), bis nur noch ein Baum übrig bleibt. Der einzige Unterschied zum einfachen Union-Find besteht darin, dass die Kanten in aufsteigender Reihenfolge betrachtet werden müssen, was wir hier durch eine Prioritätswarteschlange realisieren. Der Algorithmus von J.Kruskal ist seit 1956 bekannt. &lt;br /&gt;
&lt;br /&gt;
 def kruskal(graph, weights):&lt;br /&gt;
     anchors = range(len(graph))           # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     results = []                          # result wird später die Kanten des MST enthalten    &lt;br /&gt;
     &lt;br /&gt;
     heap = []                             # Heap zum Sortieren der Kanten nach Gewicht&lt;br /&gt;
     for edge, w in weights.iteritems():   # alle Kanten einfügen&lt;br /&gt;
         heapq.heappush(heap, (w, edge))&lt;br /&gt;
     &lt;br /&gt;
     while len(heap) &amp;gt; 0:                  # solange noch Kanten vorhanden sind&lt;br /&gt;
         w, edge = heapq.heappop(heap)     # billigste Kante aus dem Heap nehmen&lt;br /&gt;
         a1 = findAnchor(anchors, edge[0]) # Anker von Startknoten der Kante&lt;br /&gt;
         a2 = findAnchor(anchors, edge[1]) # ... und Endknoten bestimmen&lt;br /&gt;
         if a1 != a2:                      # wenn die Knoten in verschiedenen Komponenten sind&lt;br /&gt;
             anchors[a2] = a1              # Komponenten verschmelzen&lt;br /&gt;
             result.append(edge)           # ... und Kante in MST einfügen&lt;br /&gt;
     &lt;br /&gt;
     return result                         # Kanten des MST zurückgeben&lt;br /&gt;
&lt;br /&gt;
Die Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt; wurde im Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Union-Find-Algorithmus|Zusammenhangskomponenten mit Union-Find-Algorithmus]] implementiert. Im Unterschied zum Algorithmus von Prim geben wir hier nicht die property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; zurück, sondern einfach eine Liste der Kanten im MST.&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus eignet sich insbesondere für das Clusteringproblem, da der Schwellwert von vornerein als maximales Kantengewicht an den Algorithmus übergeben werden kann. Man hört mit dem Vereinigen auf, wenn das Gewicht der billigste Kante im Heap den Schwellwert überschreitet. Beim Algorithmus von Kruskal kann dann keine bessere Kante als der Schwellwert mehr kommen, da die Kanten vorher sortiert worden sind. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Komplexität:&amp;lt;/b&amp;gt; wie beim Dijkstra-Algorithmus, weil jede Kante genau einmal in den Heap kommt. Der Aufwand für das Sortieren ist somit &amp;lt;math&amp;gt;O\left(E\log E\right)&amp;lt;/math&amp;gt;, was sich zu &amp;lt;math&amp;gt;O \left(E\,\log\,V\right)&amp;lt;/math&amp;gt; reduziert, falls keine Mehrfachkanten vorhanden sind.&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; geeignet für Übungsaufgabe&lt;br /&gt;
&lt;br /&gt;
====Verwendung einer BucketPriorityQueue====&lt;br /&gt;
&lt;br /&gt;
Beide Algorithmen zur Bestimmung des minimalen Spannbaums benötigen eine Prioritätswarteschlange. Wenn die Kantengewichte ganze Zahlen im Bereich &amp;lt;tt&amp;gt;0...(m-1)&amp;lt;/tt&amp;gt; sind, kann man die MST-Algorithmen deutlich beschleunigen, wenn man anstelle des Heaps eine [[Prioritätswarteschlangen#Prioritätssuche mit dem Bucket-Prinzip|&amp;lt;tt&amp;gt;BucketPriorityQueue&amp;lt;/tt&amp;gt;]] verwendet. Die Operationen zum Einfügen einer Kante in die Queue und zum Entfernen der billibsten Kante aus der Queue beschleunigen sich dadurch auf O(1) statt O(log V) (außer wenn die Gewichte sehr ungünstig auf die Kanten verteilt sind). In der Praxis erreicht man durch diese Änderung typischerweise deutliche Verbesserungen. In der Bildverarbeitung können die Prioritäten beispielsweise die Wahrscheinlichkeit kodieren, dass zwei benachbarte Pixel zu verschiedenen Objekten gehören. Bildet man jetzt den MST, und bricht bei einer bestimmten Wahrscheinlichkeit ab, erhält man Cluster von Pixeln, die wahrscheinlich zum selben Objekt gehören (weil der MST ja die Kanten mit minimalem Gewicht bevorzugt, und kleine Gewichte bedeuten kleine Wahrscheinlichkeit, dass benachbarte Pixel von einander getrennt werden). Da man die Wahrscheinlichkeiten nur mit einer Genauigkeit von ca. 1% berechnen kann, reichen hiefür 100 bis 200 Quantisierungstufen aus. Durch Verwendung der schnellen &amp;lt;tt&amp;gt;BucketPriorityQueue&amp;lt;/tt&amp;gt; kann man jetzt wesentlich größere Bilder in akzeptabler Zeit bearbeiten als dies mit einem Heap möglich wäre.&lt;br /&gt;
&lt;br /&gt;
== Algorithmen für gerichtete Graphen ==&lt;br /&gt;
&lt;br /&gt;
Zur Erinnerung: in einem gerichteten Graphen sind die Kanten (i &amp;amp;rarr; j) und (j &amp;amp;rarr; i) voneinander verschieden, und eventuell existiert nur eine der beiden Richtungen. Im allgemeinen unterscheidet sich der [[Graphen_und_Graphenalgorithmen#transposed_graph|transponierte Graph]] G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; also vom Originalgraphen G. Beim Traversieren des Graphen und bei der Pfadsuche dürfen Kanten nur in passender Richtung verwendet werden. Bei gewichteten Graphen tritt häufig der Fall auf, dass zwar Kanten in beiden Richtungen existieren, diese aber unterschiedliche Gewichte haben.&lt;br /&gt;
&lt;br /&gt;
Gerichtete Graphen ergeben sich in natürlicher Weise aus vielen Anwendungsproblemen:&lt;br /&gt;
* Routenplanung&lt;br /&gt;
** Bei Straßennetzwerken enstehen gerichtete Graphen, sobald es Einbahnstraßen gibt.&lt;br /&gt;
** Verwendet man Gewichte, um die erwarteten Fahrzeiten entlang einer Straße zu kodieren, gibt es Asymmetrien z.B. dann, wenn Straßen in einer Richtung bergab, in der anderen bergauf befahren werden. Hier existieren zwar Kanten in beiden Richtungen, sie haben aber unterschiedliche Gewichte. Ähnliches gilt für Flüge: Durch den Gegenwind des Jetstreams braucht man von Frankfurt nach New York länger als umgekehrt von New York nach Frankfurt.&lt;br /&gt;
* zeitliche oder kausale Abhängigkeiten&lt;br /&gt;
** Wenn die Knoten Ereignisse repräsentieren, von denen einige die Ursache von anderen sind, diese wiederum die Ursache der nächsten usw., verbindet man die Knoten zweckmäßig durch gerichtete Kanten, die die Kausalitätsbeziehungen kodieren. Handelt es sich um logische &amp;quot;wenn-dann&amp;quot;-Regeln, erhält man einen [[Graphen_und_Graphenalgorithmen#Anwendung:_Das_Erf.C3.BCllbarkeitsproblem_in_Implikationengraphen|Implikationengraph]] (siehe unten). Handelt es sich hingegen um Wahrscheinlichkeitsaussagen (&amp;quot;Wenn das Wetter schön ist, haben Studenten tendenziell gute Laune, wenn eine Prüfung bevorsteht eher schlechte usw.&amp;quot;), erhält man ein [http://de.wikipedia.org/wiki/Bayessches_Netz Bayessches Netz].&lt;br /&gt;
** Wenn bestimmte Aufgaben erst begonnen werden können, nachdem andere Aufgaben erledigt sind, erhält man einen Abhängigkeitsgraphen. Beispielsweise dürfen Sie erst an der Klausur teilnehmen, nachdem Sie die Übungsaufgaben gelöst haben, und Sie dürfen erst die Abschlussarbeit beginnen, nachdem Sie bestimmte Prüfungen bestanden haben. Ein anderes schönes Beispiel liefern die Regeln für das [[Graphen_und_Graphenalgorithmen#Anwendung:_Abh.C3.A4ngigkeitsgraph|Ankleiden]] weiter unten.&lt;br /&gt;
** Gerichtete Graphen kodieren die Abhängigkeiten zwischen Programmbibliotheken. Beispielsweise benötigt das Pythonmodul &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; die internen Submodule &amp;lt;tt&amp;gt;json.encoder&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;json.decode&amp;lt;/tt&amp;gt; sowie das externe Modul &amp;lt;tt&amp;gt;decimal&amp;lt;/tt&amp;gt;. Die Submodule benötigen wiederum die externen Module &amp;lt;tt&amp;gt;re&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;sys&amp;lt;/tt&amp;gt;, das Modul &amp;lt;tt&amp;gt;decimal&amp;lt;/tt&amp;gt; braucht &amp;lt;tt&amp;gt;copy&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;collections&amp;lt;/tt&amp;gt; usw.&lt;br /&gt;
** Das Internet kann als gerichteter Graph dargestellt werden, wobei die Webseiten die Knoten, und die Hyperlinks die Kanten sind.&lt;br /&gt;
* Sequence Alignment&lt;br /&gt;
** Eine gute Rechtschreibprüfung markiert nicht nur fehlerhafte Wörter, sondern macht auch plausible Vorschläge, was eigentlich gemeint gewesen sein könnte. Dazu muss sie das gegebene Wort mit den Wörtern eines Wörterbuchs vergleichen und die Ähnlichkeit bewerten. Ein analoges Problem ergibt sich, wenn man DNA Fragmente mit der Information in einer Genomdatenbank abgleichen will. &lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Sequence Alignment / Edit Distance ===&lt;br /&gt;
&lt;br /&gt;
:gegeben: zwei Wörter (allgemein: beliebige Zeichenfolgen)&lt;br /&gt;
:gesucht: Wie kann man die Buchstaben am besten in Übereinstimmung bringen?&lt;br /&gt;
&lt;br /&gt;
:Beispiel: WORTE – NORDEN&lt;br /&gt;
&lt;br /&gt;
Zwei mögliche Alignments sind&lt;br /&gt;
&lt;br /&gt;
  W&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;OR&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;T&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;E&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;.          W.ORTE&lt;br /&gt;
  N&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;OR&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;D&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;E&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;N          NORDEN&lt;br /&gt;
&lt;br /&gt;
wobei der Punkt anzeigt, dass der untere Buchstabe keinen Partner hat, und rote Buchstaben oben und unten übereinstimmen. Jede Nicht-Übereinstimmung verursacht nun gewisse Kosten. Dabei unterscheiden wir zwei Fälle:&lt;br /&gt;
# Matche a[i] mit b[j]. Falls a[i] == b[j], ist das gut (rote Buchstaben), und es entstehen keine Kosten. Andernfalls entstehen Kosten U (schwarze Buchstaben).&lt;br /&gt;
# Wir überspringen a[i] oder b[j] (Buchstabe vs. Punkt). Dann entstehen Kosten V. (Manchmal unterscheidet man auch noch Kosten Va und Vb, wenn das Überspringen bei a und b unterschieldiche Signifikanz hat.)&lt;br /&gt;
&lt;br /&gt;
Gesucht ist nun das &amp;lt;b&amp;gt;Alignment mit minimalen Kosten&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Diese Aufgabe kann man sehr schön als gerichteten Graphen darstellen: Wir definieren ein rechteckiges Gitter und schreiben das erste Wort über das Gitter und das andere links davon. Die Gitterpunkte verbinden wir mit Pfeilen (gerichteten Kanten), wobei ein Pfeil nach rechts bedeutet, dass wir beim oberen Wort einen Buchstaben überspringen, ein Pfeil nach unten, dass wir beim linken Wort einen Buchstaben überspringen, und ein diagonaler Pfeil, dass wir zwei Buchstaben matchen (und zwar die am Pfeilende). Die Farben der Pfeile symbolisieren die Kosten: rot für das Überspringen eines Buchstabens (Kosten V), blau für das Matchen, wenn die Buchstaben nicht übereinstimmen (Kosten U), und grün, wenn die Buchstaben übereinstimmen (keine Kosten). &lt;br /&gt;
&lt;br /&gt;
[[Image:sequence-alignment.png|300px]]&lt;br /&gt;
&lt;br /&gt;
Lösung:&lt;br /&gt;
:Suche den kürzesten Pfad vom Knoten &amp;quot;START&amp;quot; (oben links) nach unten rechts. Dazu kann der [[Graphen und Graphenalgorithmen#Algorithmus von Dijkstra|Algorithmus von Dijkstra]] verwendet werden, der auf gerichteten Graphen genauso funktioniert wie auf ungerichteten.&lt;br /&gt;
&lt;br /&gt;
Für unser Beispiel von oben erhalten wir die folgenden Pfade:&lt;br /&gt;
&lt;br /&gt;
[[Image:sequence-alignment-weg1.png|400px]]&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;[[Image:sequence-alignment-weg2.png|400px]]&lt;br /&gt;
&lt;br /&gt;
Durch Addieren der Kosten entsprechend der Farben sieht man, dass der erste Weg die Kosten 2U+V und der zweite die Kosten 5U+V hat. Der erste Weg ist offensichtlich günstiger und entspricht dem besten Alignment.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Abhängigkeitsgraph ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Beispiel: &amp;lt;/b&amp;gt; Wie erklärt man einem zerstreuten Professor, wie er sich morgens anziehen soll? Der folgende Graph enthält einen Knoten für jede Aktion, und eine Kante (i &amp;amp;rarr; j) bedeutet, dass die Aktion i vor der Aktion j abgeschlossen werden muss.&lt;br /&gt;
&lt;br /&gt;
[[Image:anziehen-graph.png|600px]]&lt;br /&gt;
&lt;br /&gt;
In derartigen Abhängigkeitsgraphen ist die wichtigste Frage immer, ob der Graph azyklisch ist. Wäre dies nämlich nicht der Fall, kann es keine Reihenfolge der Aktionen geben, die alle Abhängigkeiten erfüllt. Dies sieht man leicht, wenn man den einfachsten möglichen Zyklus betrachtet: es gibt sowohl eine Kante (i &amp;amp;rarr; j) als auch eine (j &amp;amp;rarr; i). Dann müsste man i vor j erledigen, aber ebenso j vor i, was offensichtlich unmöglich ist - das im Graph kodierte Problem ist dann unlösbar. Wegen ihrer Wichtigkeit wird für gerichtete azyklische Graphen oft die Abkürzung &amp;lt;b&amp;gt;DAG&amp;lt;/b&amp;gt; (von &amp;lt;i&amp;gt;directed acyclic graph&amp;lt;/i&amp;gt;) verwendet. Ein Graph ist genau dann ein DAG, wenn es eine topologische Sortierung gibt:&lt;br /&gt;
;topologische Sortierung: Zeichne die Knoten so auf eine Gerade, dass alle Kanten (Pfeile) nach rechts zeigen. &lt;br /&gt;
Arbeitet man die Aktionen nach einer (beliebigen) topologischen Sortierung ab, werden automatisch alle Abhängigkeiten eingehalten: Da alle Pfeile nach rechts zeigen, werden abhängige Aktionen immer später ausgeführt. Die topologische Sortierung ist im allgemeinen nicht eindeutig. Die folgende Skizze zeigt eine mögliche topologische Sortierung für das Anziehen:&lt;br /&gt;
&lt;br /&gt;
[[Image:anziehen-topologische-sortierung.png|600px]]&lt;br /&gt;
&lt;br /&gt;
Eine solche fest vorgegebene Reihenfolge ist für den zerstreuten Professor sicherlich eine größere Hilfe als der ursprüngliche Graph. Man erkennt, dass die Sortierung nicht eindeutig ist, beispielsweise bei der Uhr: Da für die Uhr keine Abhängigkeiten definiert sind, kann man diese Aktion an beliebiger Stelle einsortieren. Hier wurde willkürlich die letzte Stelle gewählt.&lt;br /&gt;
&lt;br /&gt;
==== Zwei Algorithmen zum Finden der topologischen Sortierung ====&lt;br /&gt;
&lt;br /&gt;
Die folgenden Algorithmen finden entweder eine topologische Sortierung, oder signalisieren, dass der Graph zyklisch ist.&lt;br /&gt;
&lt;br /&gt;
===== Algorithmus 1 =====&lt;br /&gt;
# Suche einen Knoten mit Eingangsgrad 0 (ohne eingehende Pfeile) =&amp;gt; in einem gerichteten azyklischen Graphen gibt es immer einen solchen Knoten&lt;br /&gt;
# Platziere diesen Knoten auf der Geraden (beliebig)&lt;br /&gt;
# Entferne den Knoten aus dem Graphen zusammen mit den ausgehenden Kanten&lt;br /&gt;
# Gehe zu 1., aber platziere in 2. immer rechts der Knoten, die schon auf der Geraden vorhanden sind.&lt;br /&gt;
: =&amp;gt; Wenn noch Knoten übrig sind, aber keiner Eingangsgrad 0 hat, muss der Graph zyklisch sein.&lt;br /&gt;
&lt;br /&gt;
[[Image:bild6.JPG]]&lt;br /&gt;
&lt;br /&gt;
Beispiel für einen zyklischen Graphen: kein Knoten hat Eingangsgrad 0.&lt;br /&gt;
&lt;br /&gt;
Um den Algorithmus zu implementieren, verwenden wir eine property map &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt;, die wir in einem ersten Durchlauf durch den Graphen füllen und die dann für jeden Knoten die Anzahl der eingehenden Kanten speichert. Dann gehen wir sukzessive zu allen Knoten mit &amp;lt;tt&amp;gt;in_degree == 0&amp;lt;/tt&amp;gt;. Anstatt sie aber tatsächlich aus dem Graphen zu entfernen wie im obigen Pseudocode, dekrementieren wir nur den &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt; ihrer Nachbarn. Wird der &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt; eines Nachbarn dadurch 0, wird er ebenfalls in das Array der zu scannenden Knoten aufgenommen. Wenn der Graph azyklisch ist, enthält das Array am Ende alle Knoten des Graphen, und die Reihenfolge der Einfügungen definiert eine topologische Sortierung. Andernfalls ist das Array zu kurz, und wir signalisieren durch Zurückgeben von &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, dass der Graph zyklisch ist:&lt;br /&gt;
&lt;br /&gt;
 def topological_sort(graph):              # ein gerichteter Graph&lt;br /&gt;
     in_degree = [0]*len(graph)            # property map für den Eingangsgrad jeden Knotens&lt;br /&gt;
     for node in xrange(len(graph)):       # besuche alle Knoten&lt;br /&gt;
         for neighbor in graph[node]:      #  ... und deren Nachbarn&lt;br /&gt;
             in_degree[neighbor] += 1      #  ... und inkrementiere den Eingangsgrad&lt;br /&gt;
     &lt;br /&gt;
     result = []                           # wird später die topologische Sortierung enthalten&lt;br /&gt;
     for node in xrange(len(graph)):&lt;br /&gt;
         if in_degree[node] == 0:&lt;br /&gt;
             result.append(node)           # füge alle Knoten mit Eingangsgrad 0 in result ein&lt;br /&gt;
     &lt;br /&gt;
     k = 0&lt;br /&gt;
     while k &amp;lt; len(result):                # besuche alle Knoten mit Eingangsgrad 0&lt;br /&gt;
         node = result[k]&lt;br /&gt;
         k += 1&lt;br /&gt;
         for neighbor in graph[node]:      # besuche alle Nachbarn&lt;br /&gt;
             in_degree[neighbor] -= 1      # entferne 'virtuell' die eingehende Kante&lt;br /&gt;
             if in_degree[neighbor] == 0:  # wenn neighbor jetzt Eingangsgrad 0 hat&lt;br /&gt;
                 result.append(neighbor)   #  ... füge ihn in result ein&lt;br /&gt;
     &lt;br /&gt;
     if len(result) == len(graph):         # wenn alle Knoten jetzt Eingangsgrad 0 haben&lt;br /&gt;
         return result                     # ... ist result eine topologische Sortierung&lt;br /&gt;
     else:&lt;br /&gt;
         return None                       # andernfalls ist der Graph zyklisch&lt;br /&gt;
&lt;br /&gt;
===== Algorithmus 2 =====&lt;br /&gt;
Der obige Algorithmus hat den Nachteil, dass er jeden Knoten zweimal expandiert. Man kann eine topologische Sortierung stattdessen auch mit Tiefensuche bestimmen. Es gilt nämlich der folgende&lt;br /&gt;
;Satz: Wird ein DAG mittels Tiefensuche traversiert, definiert die &amp;lt;i&amp;gt;reverse post-order&amp;lt;/i&amp;gt; eine topologische Sortierung.&lt;br /&gt;
Zur Erinnerung: die post-order erhält man, indem man jeden Knoten ausgibt, &amp;lt;i&amp;gt;nachdem&amp;lt;/i&amp;gt; die Rekursion zu allen seinen Nachbarn beendet ist, siehe unsere [[Graphen_und_Graphenalgorithmen#pre_and_post_order|Diskussion weiter oben]]. Die reverse post-order ist gerade die Umkehrung dieser Reihenfolge. Die folgende Implementation verwendet die rekursive Version der Tiefensuche, in der Praxis wird man meist die iterative Version mit Stack bevorzugen, weil bei großen Graphen die Aufruftiefe sehr groß werden kann:&lt;br /&gt;
&lt;br /&gt;
 def reverse_post_order(graph):               # gerichteter Graph&lt;br /&gt;
     result = []                              # enthält später die reverse post-order&lt;br /&gt;
     visited = [False]*len(graph)             # Flags für bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):                         # besuche node&lt;br /&gt;
         if not visited[node]:                # aber nur, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True             # markiere ihn als besucht&lt;br /&gt;
             for neighbor in graph[node]:     # und besuche die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
             result.append(node)              # alle Nachbarn besucht =&amp;gt; Anhängen an result liefert post-order&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):          # besuche alle Knoten&lt;br /&gt;
         visit(node)&lt;br /&gt;
     &lt;br /&gt;
     result.reverse()                         # post-order =&amp;gt; reverse post-order&lt;br /&gt;
     return result&lt;br /&gt;
&lt;br /&gt;
Die Tatsache, dass die reverse post-order tatsächlich eine topologische Sortierung liefert, leuchtet wahrscheinlich nicht unmittelbar ein. Bevor wir diese Tatsache beweisen. wollen wir uns anhand des Ankleidegraphen klar machen, dass die pre-order (die man intuitiv vielleicht eher wählen würde) keine topologische Sortierung ist. Startet man die Tiefensuche beim Knoten &amp;quot;Unterhemd&amp;quot;, werden die Knoten in der Reihenfolge &amp;quot;Unterhemd&amp;quot;, &amp;quot;Oberhemd&amp;quot;, &amp;quot;Schlips&amp;quot;, &amp;quot;Jackett&amp;quot;, &amp;quot;Gürtel&amp;quot; gefunden. Da dann alle von &amp;quot;Unterhemd&amp;quot; erreichbaren Knoten erschöpft sind, startet man die Tiefensuche als nächstes bei &amp;quot;Unterhose&amp;quot; und erreicht von dort aus &amp;quot;Hose&amp;quot; und &amp;quot;Schuhe&amp;quot;. Man erkennt sofort, dass diese Reihenfolge nicht funktioniert: &amp;quot;Hose&amp;quot; kommt nach &amp;quot;Gürtel&amp;quot;, und &amp;quot;Jackett&amp;quot; kommt vor &amp;quot;Gürtel&amp;quot;. Bei dieser Anordnung gibt es Pfeile nach links, die Abhängigkeitsbedingungen sind somit verletzt.&lt;br /&gt;
&lt;br /&gt;
Damit die reverse post-order eine zulässige Sortierung sein kann, muss stets gelten, dass Knoten u vor Knoten v einsortiert wurde, wenn die Kante (u &amp;amp;rarr; v) existiert. Das ist aber äquivalent zur Forderung, dass in der ursprünglichen post-order (vor dem &amp;lt;tt&amp;gt;reverse&amp;lt;/tt&amp;gt;) u hinter v stehen muss. Wir betrachten den &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt;-Aufruf, bei dem u expandiert wird. Gelangt man jetzt zu u's Nachbarn v, gibt es zwei Möglichkeiten: Wenn v bereits expandiert wurde, befindet es sich bereits im Array &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt; kehrt sofort zurück. Andernfalls wird v ebenfalls expandiert und demzufolge in &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; eingetragen, &amp;lt;i&amp;gt;bevor&amp;lt;/i&amp;gt; der rekursive Aufruf &amp;lt;tt&amp;gt;visit(v)&amp;lt;/tt&amp;gt; zurückkehrt. Knoten u wird aber erst in &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; eingefügt, &amp;lt;i&amp;gt;nachdem&amp;lt;/i&amp;gt; alle rekursiven &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt;-Aufrufe seiner Nachbarn zurückgekehrt sind. In beiden Fällen steht u in der post-order wie gefordert hinter v, und daraus folgt die Behauptung.&lt;br /&gt;
&lt;br /&gt;
Der obige Algorithmus liefert natürlich nur dann eine topologische Sortierung, wenn der Graph wirklich azyklisch ist (man kann ihn aber auch anwenden, um die reverse post-order für einen zyklischen Graphen zu bestimmen, siehe Abschnitt &amp;quot;[[Graphen_und_Graphenalgorithmen#Transitive Hülle und stark zusammenhängende Komponenten|Stark zusammenhängende Komponenten]]&amp;quot;). Dieser Fall tritt in der Praxis häufig auf, weil zyklische Graphen bei vielen Anwendungen gar nicht erst entstehen können. Weiß man allerdings nicht, ob der Graph azyklisch ist oder nicht, muss man einen zusätzlichen Test auf Zyklen in den Algorithmus einbauen. &lt;br /&gt;
&lt;br /&gt;
Zyklische Graphen sind dadurch gekennzeichnet, dass es im obigen Beweis eine dritte Möglichkeit gibt: Während der Expansion von u wird rekursiv v expandiert, und es gibt eine Rückwärtskante (v &amp;amp;rarr; u). (Es spielt dabei keine Rolle, ob v von u aus direkt oder indirekt erreicht wurde.) Ein Zyklus wird also entdeckt, wenn die Tiefensuche zu u zurückkehrt, solange u noch &amp;lt;i&amp;gt;aktiv&amp;lt;/i&amp;gt; ist, d.h. wenn die Rekursion von u aus gestartet und noch nicht beendet wurde. Dies kann man leicht feststellen, wenn man in der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; drei Werte zulässt: 0 für &amp;quot;noch nicht besucht&amp;quot;, 1 für &amp;quot;aktiv&amp;quot; und 2 für &amp;quot;beendet&amp;quot;. Wir signalisieren einen Zyklus, sobald &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt; für einen Knoten aufgerufen wird, der gerade aktiv ist:&lt;br /&gt;
&lt;br /&gt;
 def topological_sort_DFS(graph):             # gerichteter Graph&lt;br /&gt;
     result = []                              # enthält später die topologische Sortierung&lt;br /&gt;
     &lt;br /&gt;
     not_visited, active, finished = 0, 1, 2  # drei Zustände für visited&lt;br /&gt;
     visited = [not_visited]*len(graph)       # Flags für aktive und bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):                         # besuche node (gibt &amp;quot;True&amp;quot; zurück, wenn Zyklus gefunden wurde)&lt;br /&gt;
         if visited[node] == not_visited:     # ... aber nur, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = active           # markiere ihn als aktiv&lt;br /&gt;
             for neighbor in graph[node]:     # und besuche die Nachbarn&lt;br /&gt;
                 if visit(neighbor):          # wenn rekursiv ein Zyklus gefunden wurde&lt;br /&gt;
                     return True              # ... brechen wir ab und signalisieren den Zyklus&lt;br /&gt;
             visited[node] = finished         # Rekursion beendet, node ist nicht mehr aktiv&lt;br /&gt;
             result.append(node)              # alle Nachbarn besucht =&amp;gt; Anhängen an result liefert post-order&lt;br /&gt;
             return False                     # kein Zyklus gefunden&lt;br /&gt;
         elif visited[node] == active:        # Rekursion erreicht einen noch aktiven Knoten&lt;br /&gt;
             return True                      #  =&amp;gt; Zyklus gefunden&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):          # besuche alle Knoten&lt;br /&gt;
         if visit(node):                      # wenn Zyklus gefunden wurde&lt;br /&gt;
             return None                      # ... gibt es keine topologische Sortierung&lt;br /&gt;
     &lt;br /&gt;
     result.reverse()                         # post-order =&amp;gt; reverse post-order (=topologische Sortierung)&lt;br /&gt;
     return result&lt;br /&gt;
&lt;br /&gt;
Man macht sich leicht klar, dass kein Zyklus vorliegt, wenn die Rekursion einen Knoten erreicht, der bereits auf &amp;lt;tt&amp;gt;finished&amp;lt;/tt&amp;gt; gesetzt ist. Nehmen wir an, dass u gerade expandiert wird, und sein Nachbar v ist bereits &amp;lt;tt&amp;gt;finished&amp;lt;/tt&amp;gt;. Wenn es einen Zyklus gäbe, müsste es einen Weg von v nach u geben. Dann wäre u aber bereits während der Expansion von v gefunden worden. Da v nicht mehr im Zustand &amp;lt;tt&amp;gt;active&amp;lt;/tt&amp;gt; ist, muss die Expansion von v schon abgeschlossen gewesen sein, ohne dass u gefunden wurde. Folglich kann es keinen solchen Zyklus geben.&lt;br /&gt;
&lt;br /&gt;
=== Transitive Hülle und stark zusammenhängende Komponenten ===&lt;br /&gt;
&lt;br /&gt;
Auch bei gerichteten Graphen ist die Frage, welche Knoten miteinander zusammenhängen, von großem Interesse. Wir betrachten dazu wieder die Relation &amp;quot;Knoten v ist von Knoten u aus erreichbar&amp;quot;, die anzeigt, ob es einen Weg von u nach v gibt oder nicht. In ungerichteten Graphen ist diese Relation immer symmetrisch, weil jeder Weg in beiden Richtungen benutzt werden kann. In gerichteten Graphen gilt dies nicht. Man muss hier zwei Arten von Zusammenhangskomponenten unterscheiden:&lt;br /&gt;
;Transitive Hülle: Die transitive Hülle eines Knotens u ist die Menge aller Knoten, die von u aus erreichbar sind:&lt;br /&gt;
:&amp;lt;math&amp;gt;T(u) = \{v\ |\ u \rightsquigarrow v\}&amp;lt;/math&amp;gt;&lt;br /&gt;
;Stark zusammenhängende Komponenten: Die stark zusammenhängende Komponenten &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; eines gerichteten Graphen sind maximale Teilgraphen, so dass alle Knoten innerhalb einer Komponente von jedem anderen Knoten der selben Komponente aus erreichbar sind&lt;br /&gt;
:&amp;lt;math&amp;gt;u,v \in C_i\ \ \Leftrightarrow\ \ u \rightsquigarrow v \wedge v \rightsquigarrow u&amp;lt;/math&amp;gt;&lt;br /&gt;
Die erste Definition betrachtet den Zusammenhang asymmetrisch, ohne Beachtung der Frage, ob es auch einen Rückweg von Knoten v nach u gibt, die zweite hingegen symmetrisch.&lt;br /&gt;
&lt;br /&gt;
Die &amp;lt;b&amp;gt;transitive Hülle&amp;lt;/b&amp;gt; benötigt man, wenn man Fragen der Erreichbarkeit besonders effizient beantworten will. Wir hatten bespielsweise oben erwähnt, dass das Python-Modul &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; direkt und indirekt von mehreren anderen Module abhängt, die vorher installiert werden müssen, damit &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; funktioniert. Bittet man den Systemadministrator, das &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt;-Paket zu installieren, will er diese Abhängigkeiten wahrscheinlich nicht erst mühsam rekursiv heraussuchen, sondern er verlangt eine Liste aller Pakete, die installiert werden müssen. Dies ist gerade die transitive Hülle von &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; im Abhängigkeitsgraphen. Damit man diese nicht manuell bestimmen muss, verwendet man Installationsprogramme wie z.B. [http://pypi.python.org/pypi/pip/ pip], die die Abhängigkeiten automatisch herausfinden und installieren. &lt;br /&gt;
&lt;br /&gt;
Bei der Bestimmung der transitiven Hülle modifiziert man den gegebenen Graphen, indem man jedesmal eine neue Kante (u &amp;amp;rarr; v) einfügt, wenn diese Kante noch nicht existiert, aber v von u aus erreichbar ist. Dies gelingt mit einer sehr einfachen Variation der Tiefensuche: Wir rufen &amp;lt;tt&amp;gt;visit(k)&amp;lt;/tt&amp;gt; für jeden Knoten k auf, aber setzen die property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; zuvor auf &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt; zurück. Alle Knoten, die während der Rekursion erreicht werden, sind im modifizierten Graphen Nachbarn von k. Ein etwas effizienterer Ansatz ist der [http://de.wikipedia.org/wiki/Algorithmus_von_Floyd_und_Warshall Algorithmus von Floyd und Warshall].&lt;br /&gt;
&lt;br /&gt;
Die Bestimmung der &amp;lt;b&amp;gt;stark zusammenhängenden Komponenten&amp;lt;/b&amp;gt; ist etwas schwieriger. Es existieren eine ganze Reihe von effizienten Algorithmen (siehe [http://en.wikipedia.org/wiki/Strongly_connected_component WikiPedia]), deren einfachster der Algorithmus von Kosaraju ist:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;gegeben:&amp;lt;/b&amp;gt; gerichteter Graph&lt;br /&gt;
&lt;br /&gt;
# Bestimme die reverse post-order (mit der Funktion &amp;lt;tt&amp;gt;reverse_post_order&amp;lt;/tt&amp;gt;)&lt;br /&gt;
# Bilde den transponierten Graphen &amp;lt;math&amp;gt;G^T&amp;lt;/math&amp;gt; (mit der Funktion &amp;lt;tt&amp;gt;transposeGraph&amp;lt;/tt&amp;gt;)&lt;br /&gt;
# Bestimme die Zusammenhangskomponenten von &amp;lt;math&amp;gt;G^T&amp;lt;/math&amp;gt; mittels Tiefensuche, aber betrachte die Knoten dabei in der reverse post-order aus Schritt 1 (dies kann mit einer minimalen Modifikation der Funktion &amp;lt;tt&amp;gt;connectedComponents&amp;lt;/tt&amp;gt; geschehen, indem man die Zeile &amp;lt;tt&amp;gt;for node in xrange(len(graph)):&amp;lt;/tt&amp;gt; einfach nach &amp;lt;tt&amp;gt;for node in ordered:&amp;lt;/tt&amp;gt; abändert, wobei &amp;lt;tt&amp;gt;ordered&amp;lt;/tt&amp;gt; das Ergebnis der Funktion &amp;lt;tt&amp;gt;reverse_post_order&amp;lt;/tt&amp;gt; ist, also ein Array, das die Knoten in der gewünschten Reihenfolge enthält).&lt;br /&gt;
Die Zusammenhangskomponenten, die man in Schritt 3 findet, sind gerade die stark zusammenhängenden Komponenten des Originalgraphen G. Die folgende Skizze zeigt diese in grün für den schwarz gezeichneten gerichteten Graphen. &lt;br /&gt;
&lt;br /&gt;
[[Image:strongly-connected-components.png|400px]]    &lt;br /&gt;
&lt;br /&gt;
Zum Beweis der Korrektheit des Algorithmus von Kosaraju zeigen wir zwei Implikationen: 1. Wenn die Knoten u und v in der selben stark zusammenhängenden Komponente liegen, werden sie in Schritt 3 des Algorithmus auch der selben Komponente zugewiesen. 2. Wenn die Knoten u und v in Schritt 3 der selben Komponente zugewiesen wurden, müssen sie auch in der selben stark zusammenhängenden Komponente liegen. &lt;br /&gt;
# Knoten u und v gehören zur selben stark zusammenhängenden Komponente von G. Per Definition gilt, dass u von v aus erreichbar ist und umgekehrt. Dies muss auch im transponierten Graphen G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; gelten (der Weg &amp;lt;math&amp;gt;u \rightsquigarrow v&amp;lt;/math&amp;gt; wird jetzt zum Weg &amp;lt;math&amp;gt;v \rightsquigarrow u&amp;lt;/math&amp;gt; und umgekehrt). Wird u bei der Tiefensuche in Schritt 3 vor v expandiert, ist v von u aus erreichbar und gehört somit zur selben Komponente. Das umgekehrte gilt, wenn v vor u expandiert wird. Daraus folgt die Behauptung 1.&lt;br /&gt;
# Knoten u und v werden in Schritt 3 der selben Komponente zugewiesen: Sei x der Anker dieser Komponente. Da u in der gleichen Komponente wie x liegt, muss es in G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; einen Weg &amp;lt;math&amp;gt;x \rightsquigarrow u&amp;lt;/math&amp;gt;, und demnach in G einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow x&amp;lt;/math&amp;gt; geben. Da x der Anker seiner Komponente ist, wissen wir aber auch, dass x in der reverse post-order &amp;lt;i&amp;gt;vor&amp;lt;/i&amp;gt; u liegt (denn der Anker ist der Knoten, mit dem eine neue Komponente gestartet wird; er muss deshalb im Array &amp;lt;tt&amp;gt;ordered&amp;lt;/tt&amp;gt; als erster Konten seiner Komponente gefunden worden sein). Wir unterscheiden jetzt im Schritt 1 des Algorithmus zwei Fälle:&lt;br /&gt;
## u wurde bei der Bestimmung der post-order vor x expandiert. Dann kann x nur dann in der reverse post-order &amp;lt;i&amp;gt;vor&amp;lt;/i&amp;gt; u liegen (oder, einfacher ausgedrückt, x kann nur dann in der post-order &amp;lt;i&amp;gt;hinter&amp;lt;/i&amp;gt; u liegen), wenn x im Graphen G nicht von u aus erreichbar war. Das ist aber unmöglich, weil wir ja schon wissen, dass es in G einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow x&amp;lt;/math&amp;gt; gibt.&lt;br /&gt;
## Folglich wurde u bei der Bestimmung der post-order nach x expandiert. Da x in der post-order hinter u liegt, muss u während der Expansion von x erreicht worden sein. Deshalb muss es in G auch einen Weg &amp;lt;math&amp;gt;x \rightsquigarrow u&amp;lt;/math&amp;gt; geben.&lt;br /&gt;
#:Somit sind x und u in der selben stark zusammenhängenden Komponente. Die gleiche Überlegung gilt für x und v. Wegen der Transitivität der Relation &amp;quot;ist erreichbar&amp;quot; folgt daraus, dass auch u und v in der selben Komponente liegen, also die Behauptung 2.&lt;br /&gt;
&lt;br /&gt;
Die folgende Skizze illustriert den Komponentengraphen, den man erhält, indem man für jede Komponente &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; einen Knoten erzeugt (grün), und die Knoten i und j durch eine gerichtete Kante verbindet (rot), wenn es im Originalgraphen eine Kante (u &amp;amp;rarr; v) mit &amp;lt;math&amp;gt;u \in C_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v \in C_j&amp;lt;/math&amp;gt; gibt. Man sieht leicht, dass der Komponentengraph stets azyklisch sein muss, denn wären &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; gleichzeitig von &amp;lt;math&amp;gt;C_j&amp;lt;/math&amp;gt; aus erreichbar, müssten sie eine gemeinsame stark zusammenhängende Komponente bilden. Daraus folgt auch, dass ein von vornherein azyklischer Graph nur triviale stark verbundene Komponenten haben kann, die aus einzelnen Knoten bestehen.&lt;br /&gt;
&lt;br /&gt;
[[Image:strongly-connected-components-graph.png|400px]]&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Das Erfüllbarkeitsproblem in Implikationengraphen ===&lt;br /&gt;
&lt;br /&gt;
Das Erfüllbarkeitsproblem hat auf den ersten Blick nichts mit Graphen zu tun, denn es geht um Wahrheitswerte logischer Ausdrücke. Man kann logische Ausdrücke jedoch unter bestimmten Bedingungen in eine Graphendarstellung überführen und somit das ursprüngliche Problem auf ein Problem der Graphentheorie reduzieren, für das bereits ein Lösungsverfahren bekannt ist. In diesem Abschnitt wollen wir dies für die sogenannten Implikationengraphen zeigen, ein weiteres Beispiel findet sich im Kapitel [[NP-Vollständigkeit]].&lt;br /&gt;
&lt;br /&gt;
==== Das Erfüllbarkeitsproblem ====&lt;br /&gt;
&lt;br /&gt;
(vgl. [http://de.wikipedia.org/wiki/Erfüllbarkeitsproblem_der_Aussagenlogik WikiPedia (de)])&lt;br /&gt;
&lt;br /&gt;
Das Erfüllbarkeitsproblem (SAT-Problem, von &amp;lt;i&amp;gt;satisfiability&amp;lt;/i&amp;gt;) befasst sich mit logischen (oder Booleschen) Funktionen: Gegeben sei eine Menge &amp;lt;math&amp;gt;\{x_1, ... ,x_n\}&amp;lt;/math&amp;gt; Boolscher Variablen (d.h., die &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; können nur die Werte True oder False annehmen), sowie eine logische Formel, in der die Variablen mit den üblichen logischen Operatoren &lt;br /&gt;
:&amp;lt;math&amp;gt;\neg\quad&amp;lt;/math&amp;gt;: Negation (&amp;quot;nicht&amp;quot;, in Python: &amp;lt;tt&amp;gt;not&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\vee\quad&amp;lt;/math&amp;gt;: Disjunktion (&amp;quot;oder&amp;quot;, in Python: &amp;lt;tt&amp;gt;or&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\wedge\quad&amp;lt;/math&amp;gt;: Konjuktion (&amp;quot;und&amp;quot;, in Python: &amp;lt;tt&amp;gt;and&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\rightarrow\quad&amp;lt;/math&amp;gt;: Implikation (&amp;quot;wenn, dann&amp;quot;, in Python nicht als Operator definiert)&lt;br /&gt;
:&amp;lt;math&amp;gt;\leftrightarrow\quad&amp;lt;/math&amp;gt;: Äquivalenz (&amp;quot;genau dann, wenn&amp;quot;, in Python: &amp;lt;tt&amp;gt;==&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\neq\quad&amp;lt;/math&amp;gt;: exklusive Disjunktion (&amp;quot;entweder oder&amp;quot;, in Python: &amp;lt;tt&amp;gt;!=&amp;lt;/tt&amp;gt;)&lt;br /&gt;
verknüpft sind. Klammern definieren die Reihenfolge der Auswertung der Operationen. Für jede Belegung der Variablen &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; mit True oder False liefert die Formel den Wert der Funktion, der natürlich auch nur True oder False sein kann. Wenn Formel und Belegung gegeben sind, ist die Auswertung der Funktion ein sehr einfaches Problem: Man transformiert die Formel in einen Parse-Baum (siehe Übungsaufgabe &amp;quot;Taschenrechner) und wertet jeden Knoten mit Hilfe der üblichen Wertetabellen für logische Operatoren aus, die wir hier zur Erinnerung noch einmal angeben:&lt;br /&gt;
{| cellspacing=&amp;quot;0&amp;quot; border=&amp;quot;1&amp;quot;&lt;br /&gt;
|- style=&amp;quot;text-align:center;background-color:#ffffcc;width:50px&amp;quot;&lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \vee b &amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \wedge b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \rightarrow b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;b \rightarrow a&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \leftrightarrow b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \neq b&amp;lt;/math&amp;gt; &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 0 || 1 || 1 || 1 || 0&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 0 || 1 || 0 || 0 || 1&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 0 || 1 || 0 || 1&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 || 0&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beim Erfüllbarkeitsproblem wird die Frage umgekehrt gestellt: &lt;br /&gt;
:Gegeben sei eine logische Funktion. Ist es möglich, dass die Funktion jemals den Wert True annimmt? &lt;br /&gt;
Das heisst, kann man die Variablen &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; so mit True oder False belegen, dass die Formel am Ende wahr ist? Im Prinzip kann man diese Frage durch erschöpfende Suche leicht beantworten, indem man die Funktion für alle &amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt; möglichen Belegungen einfach ausrechnet, aber das dauert für große n (ab ca. &amp;lt;math&amp;gt;n\ge 40&amp;lt;/math&amp;gt;) viel zu lange. Erstaunlicherweise ist es aber noch niemanden gelungen, einen Algorithmus zu finden, der für beliebige logische Funktionen schneller funktioniert. Im Gegenteil wurde gezeigt, dass das Erfüllbarkeitsproblem [[NP-Vollständigkeit|NP-vollständig]] ist, so dass wahrscheinlich kein solcher Algorithmus existiert. Trotz (oder gerade wegen) seiner Schwierigkeit hat das Erfüllbarkeitsproblem viele Anwendungen gefunden, vor allem beim Testen logischer Schaltkreise (&amp;quot;Gibt es eine Belegung der Eingänge, so dass am Ausgang der verbotene Wert X entsteht?&amp;quot;) und bei der Planerstellung in der künstlichen Intelligenz (&amp;quot;Kann man ausschließen, dass der generierte Plan Konflikte enthält?&amp;quot;). Es ist außerdem ein beliebtes Modellproblem für die Erforschung neuer Ideen und Algorithmen für schwierige Probleme.&lt;br /&gt;
&lt;br /&gt;
==== Normalformen für logische Ausdrücke ====&lt;br /&gt;
&lt;br /&gt;
Um die Beschreibung von Erfüllbarkeitsproblemen zu vereinfachen und zu vereinheitlichen, hat man verschiedene &amp;lt;i&amp;gt;Normalformen&amp;lt;/i&amp;gt; für logische Ausdrücke eingeführt. Die wichtigste ist die &amp;lt;i&amp;gt;Konjuktionen-Normalform&amp;lt;/i&amp;gt; (CNF - conjunctive normal form). Ein Ausdruck in &amp;lt;i&amp;gt;Konjuktionen-Normalform&amp;lt;/i&amp;gt; ist eine UND-Verknüpfung von M &amp;lt;i&amp;gt;Klauseln&amp;lt;/i&amp;gt;:&lt;br /&gt;
 (CLAUSE&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;) &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; (CLAUSE&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;) &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; ...  &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; (CLAUSE&amp;lt;sub&amp;gt;M&amp;lt;/sub&amp;gt;)&lt;br /&gt;
Jede Klausel ist wiederum ein logischer Ausdruck, der aber sehr einfach sein muss: Er darf nur noch k Variablen enthalten, die nur mit den Operatoren NICHT und ODER verknüpft werden dürfen, z.B.&lt;br /&gt;
  CLAUSE&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; := &amp;lt;math&amp;gt;x_1 \vee \neg x_3 \vee x_8&amp;lt;/math&amp;gt;&lt;br /&gt;
Je nachdem, wie viele Variablen pro Klausel erlaubt sind, spricht man von &amp;lt;b&amp;gt;k-CNF&amp;lt;/b&amp;gt; und entsprechend von einem &amp;lt;b&amp;gt;k-SAT&amp;lt;/b&amp;gt; Problem. Es ist außerdem üblich, die Menge der Variablen und die Menge der negierten Variablen zusammen als Menge der &amp;lt;i&amp;gt;Literale&amp;lt;/i&amp;gt; zu bezeichnen:&lt;br /&gt;
  LITERALS := &amp;lt;math&amp;gt;\{x_1,...,x_n\} \cup \{\neg x_1,...,\neg x_n\}&amp;lt;/math&amp;gt;&lt;br /&gt;
Formal definiert man die &amp;lt;b&amp;gt;k-Konjunktionen-Normalform (k-CNF)&amp;lt;/b&amp;gt; am besten durch eine Grammatik in [http://de.wikipedia.org/wiki/Backus-Naur-Form Backus-Naur-Form]:&lt;br /&gt;
    k_CNF    ::=  CLAUSE | k_CNF &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; CLAUSE&lt;br /&gt;
    CLAUSE   ::= (LITERAL &amp;lt;math&amp;gt;\vee&amp;lt;/math&amp;gt; ... &amp;lt;math&amp;gt;\vee&amp;lt;/math&amp;gt; LITERAL)  # genau k Literale pro Klausel&lt;br /&gt;
    LITERAL  ::=  VARIABLE | &amp;lt;math&amp;gt;\neg&amp;lt;/math&amp;gt;VARIABLE&lt;br /&gt;
    VARIABLE ::=  &amp;lt;math&amp;gt;x_1&amp;lt;/math&amp;gt; | ... | &amp;lt;math&amp;gt;x_n&amp;lt;/math&amp;gt;&lt;br /&gt;
Beispiele:&lt;br /&gt;
* 3-CNF: &amp;lt;math&amp;gt;(x_1 \vee \neg x_2 \vee x_4) \wedge (x_2 \vee x_3 \vee \neg x_4) \wedge (\neg x_1 \vee x_4 \vee \neg x_5)&amp;lt;/math&amp;gt;&lt;br /&gt;
* 2-CNF: &amp;lt;math&amp;gt;(x_1 \vee \neg x_2) \wedge (x_3 \vee x_4)&amp;lt;/math&amp;gt; ...&lt;br /&gt;
&amp;lt;b&amp;gt;Gesucht&amp;lt;/b&amp;gt; ist eine Belegung der Variablen mit True und False, so dass der Ausdruck den Wert True hat. Aus den Eigenschaften der UND- und ODER-Verknüpfungen folgt, dass ein Ausdruck in k-CNF genau dann True ist, wenn jede einzelne Klausel True ist. In jeder Klausel wiederum hat man k Chancen, die Klausel True zu machen, indem man eins der Literale zu True macht. Eventuell werden dadurch aber andere Klauseln wieder zu False, was die Aufgabe so schwierig macht. Die Bedeutung der k-CNF ergibt sich aus folgendem&lt;br /&gt;
;Satz: Jeder logische Ausdruck kann effizient nach 3-CNF transformiert werden, jedoch im allgemeinen nicht nach 2-CNF.&lt;br /&gt;
Man kann sich also auf Algorithmen für 3-SAT-Probleme konzentrieren, ohne dabei an Ausdrucksmächtigkeit zu verlieren. &lt;br /&gt;
&lt;br /&gt;
Leider gilt der entsprechende Satz nicht für k=2: Ausdrücke in 2-CNF sind weit weniger mächtig, weil man in jeder Klausel nur noch zwei Wahlmöglichkeiten hat. Bestimmte logische Ausdrücke sind aber auch nach 2-CNF transformierbar, beispielsweise die Bedingung, dass zwei Literale u und v immer den entgegegesetzten Wert haben müssen. Dies ergibt ein Paar von ODER-Verknüpfungen:&lt;br /&gt;
:&amp;lt;math&amp;gt;(u \leftrightarrow \neg v) \equiv (u \vee \neg v) \wedge (\neg u \vee v)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die 2-CNF hat den Vorteil, dass es effiziente Algorithmen für das 2-SAT-Problem gibt, die wir jetzt kennenlernen wollen. Es zeigt sich, dass man Ausdrücke in 2-CNF als Graphen repräsentieren kann, indem man sie zunächst in die &amp;lt;i&amp;gt;Implikationen-Normalform&amp;lt;/i&amp;gt; (INF für &amp;lt;i&amp;gt;implicative normal form&amp;lt;/i&amp;gt;) überführt. Die Implikationen-Normalform besteht ebenfalls aus einer Menge von Klauseln, die durch UND-Operationen verknüpft sind, aber jede Klausel ist jetzt eine Implikation. &lt;br /&gt;
Die Grammatik der &amp;lt;b&amp;gt;Implikationen-Normalform (INF)&amp;lt;/b&amp;gt; lautet:&lt;br /&gt;
    INF      ::=  CLAUSE | INF &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; CLAUSE&lt;br /&gt;
    CLAUSE   ::= (LITERAL &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; LITERAL)  # genau 2 Literale pro Implikation&lt;br /&gt;
    LITERAL  ::=  VARIABLE | &amp;lt;math&amp;gt;\neg&amp;lt;/math&amp;gt;VARIABLE&lt;br /&gt;
    VARIABLE ::=  &amp;lt;math&amp;gt;x_1&amp;lt;/math&amp;gt; | ... | &amp;lt;math&amp;gt;x_n&amp;lt;/math&amp;gt;&lt;br /&gt;
und ein gültiger Ausdruck wäre z.B.&lt;br /&gt;
:&amp;lt;math&amp;gt;(x_1 \to x_2) \wedge (x_2 \to \neg x_3) \wedge (x_4 \to x_3)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die Umwandlung von 2-CNF nach INF beruht auf folgender Äquivalenz, die man sich aus der obigen Wahrheitstabelle leicht herleitet:&lt;br /&gt;
:&amp;lt;math&amp;gt;(x \vee y) \equiv (\neg x \rightarrow y) \equiv (\neg y \rightarrow x)&amp;lt;/math&amp;gt;&lt;br /&gt;
Aus dieser Äquivalenz folgt der &lt;br /&gt;
;Satz: Ein Ausdruck in 2-CNF kann nach INF transformiert werden, indem man jede Klausel &amp;lt;math&amp;gt;(x \vee y)&amp;lt;/math&amp;gt; durch das Klauselpaar &amp;lt;math&amp;gt;(\neg x \rightarrow y) \wedge (\neg y \rightarrow x)&amp;lt;/math&amp;gt; ersetzt.&lt;br /&gt;
Man beachte, dass man für jede ODER-Klausel des ursprünglichen Ausdrucks &amp;lt;i&amp;gt;zwei&amp;lt;/i&amp;gt; Implikationen (eine für jede Richtung des &amp;quot;wenn, dann&amp;quot;) einfügen muss, um die Symmetrie des Problems zu erhalten.&lt;br /&gt;
&lt;br /&gt;
==== Lösung des 2-SAT-Problems mit Implikationgraphen ====&lt;br /&gt;
&lt;br /&gt;
Jeder Ausdruck in INF kann als gerichteter Graph dargestellt werden:&lt;br /&gt;
# Für jedes Literal wird ein Knoten in den Graphen eingefügt. Es gibt also für jede Variable und für ihre Negation jeweils einen Knoten, d.h. 2n Knoten insgesamt.&lt;br /&gt;
# Jede Implikation ist eine gerichtete Kante.&lt;br /&gt;
Implikationengraphen eignen sich, um Ursache-Folge-Beziehungen oder Konflikte zwischen Aktionen auszudrücken. Beispielsweise kann man die Klausel &amp;lt;math&amp;gt;(x \rightarrow \neg y)&amp;lt;/math&amp;gt; als &amp;quot;wenn man x tut, darf man y nicht tun&amp;quot; interpretieren. Ein anderes schönes Beispiel findet sich in Übung 12.&lt;br /&gt;
&lt;br /&gt;
Für die Implementation eines Implikationengraphen in Python empfiehlt es sich, die Knoten geschickt zu numerieren: Ist die Variable &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; dem Knoten i zugeordnet, so sollte die negierte Variable &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; dem Knoten (i+n) zugeordnet werden. Zu jedem gegebenen Knoten i findet man dann den negierten Partnerknoten j leicht durch die Formel &amp;lt;tt&amp;gt;j = (i + n ) % (2*n)&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die Aufgabe besteht jetzt darin, folgende Fragen zu beantworten:&lt;br /&gt;
# Ist der durch den Implikationengraphen gegebene Ausdruck erfüllbar?&lt;br /&gt;
# Finde eine geeignete Belegung der Variablen, wenn der Ausduck erfüllbar ist.&lt;br /&gt;
Die erste Frage beantwortet man leicht, indem man die stark zusammenhängenden Komponenten des Implikationengraphen bildet. Dann gilt folgender&lt;br /&gt;
;Satz: Seien u und v zwei Literale, die sich in der selben stark zusammenhängenden Komponente befinden. Dann müssen u und v stets den selben Wert haben, damit der Ausdruck erfüllt sein kann.&lt;br /&gt;
Die Korrektheit des Satzes folgt aus der Definition der stark zusammenhängenden Komponenten: Da u und v in der selben Komponente liegen, gibt es im Implikationengraphen einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow v&amp;lt;/math&amp;gt; sowie einen Weg &amp;lt;math&amp;gt;v \rightsquigarrow u&amp;lt;/math&amp;gt;. Wegen der Transitivität der &amp;quot;wenn, dann&amp;quot; Relation kann man die Wege zu zwei Implikationen verkürzen, die gleichzeitig gelten müssen: &amp;lt;math&amp;gt;(u \rightarrow v) \wedge (v \rightarrow u)&amp;lt;/math&amp;gt; (die Verkürzung von Wegen zu direkten Kanten entspricht gerade der Bildung der transitiven Hülle für die Knoten u und v). In der obigen Wertetabelle für logische Operatoren erkennt mann, dass dies äquivalent zur Bedingung &amp;lt;math&amp;gt;(u \leftrightarrow v)&amp;lt;/math&amp;gt; ist. Dies ist aber gerade die Behauptung des Satzes.&lt;br /&gt;
&lt;br /&gt;
Die Erfüllbarkeit des Ausdrucks ist nun ein einfacher Spezialfall dieses Satzes. &lt;br /&gt;
;Korrolar: Der gegebene Ausdruck ist genau dann erfüllbar, wenn die Literale &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; sich für kein i in derselben stark zusammenhängenden Komponente befinden.&lt;br /&gt;
Setzt man nämlich im Satz &amp;lt;math&amp;gt;u = x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v = \neg x_i&amp;lt;/math&amp;gt;, und beide Knoten befinden sich in der selben Komponente, dann müsste gelten &amp;lt;math&amp;gt;x_i \leftrightarrow\neg x_i&amp;lt;/math&amp;gt;, was offensichtlich ein Widerspruch ist. Damit kann der Ausdruck nicht erfüllbar sein. Umgekehrt gilt, dass der Ausdruck immer erfüllbar ist, wenn &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; stets in verschiedenen Komponenten liegen, weil der folgende Algorithmus von Aspvall, Plass und Tarjan in diesem Fall stets eine gültige Belegung aller Variablen liefert:&lt;br /&gt;
# Bestimme die stark zusammenhängenden Komponenten und bilde den Komponentengraphen. Ordne die Knoten des Komponentengraphen (also die stark zusammenhängenden Komponenten des Originalgraphen) in topologische Sortierung an.&lt;br /&gt;
# Betrachte die Komponenten in der topologischen Sortierung von hinten nach vorn und weise ihnen einen Wert nach folgenden Regeln zu (zur Erinnerung: alle Literale in der selben Komponente haben den selben Wert):&lt;br /&gt;
#* Wenn die Komponente noch nicht betrachtet wurde, setze ihren Wert auf True, und den Wert der komplementären Komponente (derjenigen, die die negierten Literale enthält) auf False.&lt;br /&gt;
#* Andernfalls, gehe zur nächsten Komponente weiter.&lt;br /&gt;
Der Algorithmus beruht auf der Symmetrie des Implikationengraphen: Weil Kanten immer paarweise &amp;lt;math&amp;gt;(\neg u \rightarrow v) \wedge (\neg v \rightarrow u)&amp;lt;/math&amp;gt; eingefügt werden, ist der Graph &amp;lt;i&amp;gt;schiefsymmetrisch&amp;lt;/i&amp;gt; (skew symmetric): die eine Hälfte das Graphen ist die transponierte Spiegelung der anderen Hälfte. Enthält eine stark zusammenhängende Komponente &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; die Knoten &amp;lt;tt&amp;gt;i1, i2, ...&amp;lt;/tt&amp;gt;, so gibt es stets eine komplementäre Komponente &amp;lt;math&amp;gt;C_j = \neg C_i&amp;lt;/math&amp;gt;, die die komplementären Knoten &amp;lt;tt&amp;gt;j1 = (i1 + n) % (2*n), j2 = (i2 + n) % (2*n), ...&amp;lt;/tt&amp;gt; enthält. Gilt &amp;lt;math&amp;gt;C_i = \neg C_i&amp;lt;/math&amp;gt; für irgendein i, so ist der Ausdruck nicht erfüllbar. Den Beweis für die Korrektheit des Algorithmus findet man im [http://www.math.ucsd.edu/~sbuss/CourseWeb/Math268_2007WS/2SAT.pdf Originalartikel]. Leider funktioniert dies nicht für k-SAT-Probleme mit &amp;lt;math&amp;gt;k &amp;gt; 2&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Will man nur die Erfüllbarkeit prüfen, vereinfacht sich der Algorithmus zu:&lt;br /&gt;
# Bestimme die stark zusammenhängenden Komponenten.&lt;br /&gt;
# Teste für alle &amp;lt;tt&amp;gt;i = 0,...,n-1&amp;lt;/tt&amp;gt;, dass Knoten &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; und Knoten &amp;lt;tt&amp;gt;(i+n)&amp;lt;/tt&amp;gt; in unterschiedlichen Komponenten liegen.&lt;br /&gt;
Ist der Ausdruck erfüllbar, kann man eine gültige Belegung der Variablen jetzt mit dem randomisierten Algorithmus bestimmen, den wir im Kapitel [[Randomisierte Algorithmen]] behandeln.&lt;br /&gt;
&lt;br /&gt;
== Weitere wichtige Graphenalgorithmen ==&lt;br /&gt;
&lt;br /&gt;
Eins der wichtigsten Einsatzgebiete für Graphen ist die Optimierung, also die Suche nach der &amp;lt;i&amp;gt;besten&amp;lt;/i&amp;gt; Lösung für ein gegebenes Problem:&lt;br /&gt;
* Das &amp;lt;i&amp;gt;interval scheduling&amp;lt;/i&amp;gt; befasst sich damit, aus einer gegebenen Menge von Aufträgen die richtigen auszuwählen und sie geschickt auf die zur Verfügung stehenden Ressourcen aufzuteilen. Damit beschäftigen wir uns im Kapitel [[Greedy-Algorithmen und Dynamische Programmierung]].&lt;br /&gt;
* Beim Problem des Handlungsreisenden sucht man nach der kürzesten Rundreise, die alle gegebenen Städte genau einmal besucht. Dieses Problem behandeln wir im Kapitel [[NP-Vollständigkeit]].&lt;br /&gt;
* Viele weitere Anwendungen können wir leider in der Vorlesung nicht mehr behandeln, z.B.&lt;br /&gt;
** Algorithmen für den [http://en.wikipedia.org/wiki/Maximum_flow_problem maximalen Fluss] beantworten die Frage, wie man die Durchflussmenge durch ein Netzwerk (z.B. von Ölpipelines) maximiert.&lt;br /&gt;
** Beim [http://en.wikipedia.org/wiki/Assignment_problem Problem der optimalen Paarung] (&amp;quot;matching problem&amp;quot; oder &amp;quot;assignment problem&amp;quot;) sucht man nach einer Teilmenge der Kanten (also nach einem Teilgraphen), so dass jeder Knoten in diesem Teilgraphen höchstens den Grad 1 hat. Im neuen Graphen gruppieren die Kanten also je zwei Knoten zu einem Paar, und die Paarung soll nach jeweils anwendungsspezifischen Kriterien optimal sein. Dies benötigt man z.B. bei der optimalen Zuordnung von Gruppen, etwas beim Arbeitsamt (Zuordnung Arbeitssuchender - Stellenangebot) und in der Universität (Zuordnung Studenten - Übungsgruppen).&lt;br /&gt;
** In Statistik und maschinellem Lernen haben in den letzten Jahren die [http://en.wikipedia.org/wiki/Graphical_model graphischen Modelle] große Bedeutung erlangt.&lt;br /&gt;
* usw. usf.&lt;br /&gt;
&lt;br /&gt;
[[Randomisierte Algorithmen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5409</id>
		<title>Graphen und Graphenalgorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5409"/>
				<updated>2012-07-25T17:16:32Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Transitive Hülle und stark zusammenhängende Komponenten */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Einführung zu Graphen ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation -- Königsberger Brückenproblem ===&lt;br /&gt;
Leonhard Euler [http://de.wikipedia.org/wiki/Leonhard_Euler] erfand den Graphen-Formalismus 1736, um eine scheinbar banale Frage zu beantworten: Ist es möglich, in Königsberg (siehe Stadtplan von 1809 und die schematische Darstellung) einen Spaziergang zu unternehmen, bei dem jede der 7 Brücken genau einmal überquert wird?&lt;br /&gt;
&lt;br /&gt;
[[Image:Koenigsberg1809.png]]&amp;lt;br&amp;gt;&lt;br /&gt;
[[Image:Koenigsberg.jpg]]&lt;br /&gt;
&lt;br /&gt;
Ein Graph abstrahiert von der Geometrie des Problems und repräsentiert nur die Topologie. Jeder Stadtteil von Königsberg ist ein Knoten des Graphen, jede Brücke eine Kante. Der zum Brückenproblem gehörende Graph sieht also so aus:&lt;br /&gt;
&lt;br /&gt;
     O&lt;br /&gt;
    /| \&lt;br /&gt;
    \|  \&lt;br /&gt;
     O---O&lt;br /&gt;
    /|  /&lt;br /&gt;
    \| /&lt;br /&gt;
     O&lt;br /&gt;
&lt;br /&gt;
Der gesuchte Spaziergang würde existieren, wenn es maximal 2 Knoten gäbe, an denen sich eine ungerade Zahl von Kanten trifft. Die Frage muss für Königsberg also verneint werden, denn hier gibt es vier solche Knoten. Ein leicht modifiziertes Problem ist allerdings lösbar: Im obigen Stadtplan erkennt man eine Fähre, die die Stadtteile Kneiphof und Altstadt verbindet. Bezieht man dieselbe in den Spaziergang ein, ergibt sich folgender Graph, bei dem nur noch zwei Knoten mit ungerader Kantenzahl existieren:&lt;br /&gt;
&lt;br /&gt;
   --O&lt;br /&gt;
  / /| \&lt;br /&gt;
  \ \|  \&lt;br /&gt;
   --O---O&lt;br /&gt;
    /|  /&lt;br /&gt;
    \| /&lt;br /&gt;
     O&lt;br /&gt;
&lt;br /&gt;
Inzwischen haben Graphen eine riesige Zahl weiterer Anwendungen gefunden. Einige Beispiele:&lt;br /&gt;
&lt;br /&gt;
* Landkarten:&lt;br /&gt;
** Knoten: Länder&lt;br /&gt;
** Kanten: gemeinsame Grenzen&lt;br /&gt;
&lt;br /&gt;
* Logische Schaltkreise:&lt;br /&gt;
** Knoten: Gatter&lt;br /&gt;
** Kanten: Verbindungen&lt;br /&gt;
&lt;br /&gt;
* Chemie (Summenformeln):&lt;br /&gt;
** Knoten: chemische Elemente&lt;br /&gt;
** Kanten: Bindungen &lt;br /&gt;
&lt;br /&gt;
* Soziologie (StudiVZ)&lt;br /&gt;
** Soziogramm&lt;br /&gt;
*** Knoten: Personen&lt;br /&gt;
*** Kanten: Freund von ...&lt;br /&gt;
&lt;br /&gt;
=== Definitionen ===&lt;br /&gt;
&lt;br /&gt;
;Ungerichteter Graph: Ein ungerichteter Graph G = ( V, E ) besteht aus&lt;br /&gt;
:* einer endliche Menge V von Knoten (vertices)&lt;br /&gt;
:* einer endlichen Menge &amp;lt;math&amp;gt;E \subset V \times V&amp;lt;/math&amp;gt; von Kanten (edges)&lt;br /&gt;
:Die Paare (u,v) und (v,u) gelten dabei als nur ''eine'' Kante (somit gilt die Symmetriebeziehung: (u,v) ∈ E =&amp;gt; (v,u) ∈ E ). Die Anzahl der Kanten, die sich an einem Knoten treffen, wird als ''Grad'' (engl. ''degree'') dieses Knotens bezeichnet:&lt;br /&gt;
:::degree(v) = |{v' ∈ V | (v,v') ∈ E}|&lt;br /&gt;
:(Die Syntax |{...}| bezeichnet dabei die Mächtigkeit der angegebenen Menge, also die Anzahl der Elemente in der Menge.)&lt;br /&gt;
&lt;br /&gt;
Der Graph des Königsberger Brückenproblems ist ungerichtet. Bezeichnet man die Knoten entsprechend des folgenden Bildes&lt;br /&gt;
    c&lt;br /&gt;
   /| \&lt;br /&gt;
   \|  \&lt;br /&gt;
    b---d &lt;br /&gt;
   /|  /&lt;br /&gt;
   \| /&lt;br /&gt;
    a&lt;br /&gt;
&lt;br /&gt;
gilt für die Knotengrade: &amp;lt;tt&amp;gt;degree(a) == degree(c) == degree(d) == 3&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;degree(b) == 5&amp;lt;/tt&amp;gt;. Genauer muss man bei diesem Graphen von einem ''Multigraphen'' sprechen, weil es zwischen einigen Knotenpaaren (nämlich (a, b) sowie (b, c)) mehrere Kanten (&amp;quot;Mehrfachkanten&amp;quot;) gibt. Wir werden in dieser Vorlesung nicht näher auf Multigraphen eingehen.&lt;br /&gt;
&lt;br /&gt;
;Gerichteter Graph: Ein Graph heißt ''gerichtet'', wenn die Kanten (u,v) und (v,u) unterschieden werden. Die Kante (u,v) ∈ E wird nun als Kante von u nach v (aber nicht umgekehrt) interpretiert. Entsprechend unterscheidet man jetzt den ''eingehenden'' und den ''ausgehenden Grad'' jedes Knotens:&lt;br /&gt;
:*out_degree(v) = |{v' ∈ V | (v,v') ∈ E}|&amp;lt;br/&amp;gt;&lt;br /&gt;
:*in_degree(v)  = |{v' ∈ V| (v',v) ∈ E}|&lt;br /&gt;
&lt;br /&gt;
Das folgende Bild zeigt einen gerichteten Graphen. Hier gilt &amp;lt;tt&amp;gt;out_degree(1) == out_degree(3) == in_degree(2) == in_degree(4) == 2&amp;lt;/tt&amp;gt; und &lt;br /&gt;
&amp;lt;tt&amp;gt;in_degree(1) == in_degree(3) == out_degree(2) == out_degree(4) == 0&amp;lt;/tt&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
[[Image:digraph.png|gerichteter Graph]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Vollständiger Graph: Ein vollständiger Graph ist ein ungerichteter Graph, bei dem jeder Knoten mit allen anderen Knoten verbunden ist.&lt;br /&gt;
:::&amp;lt;math&amp;gt;E = \{ (v,w) |  v \in V, w \in V, v \ne w \}&amp;lt;/math&amp;gt;&lt;br /&gt;
:Ein vollständiger Graph mit |V| Knoten hat &amp;lt;math&amp;gt;|E| = \frac{|V|(|V|-1)}{2}&amp;lt;/math&amp;gt; Kanten.&lt;br /&gt;
&lt;br /&gt;
Die folgenden Abbildungen zeigen die vollständigen Graphen mit einem bis fünf Knoten (auch als K&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; bis K&amp;lt;sub&amp;gt;5&amp;lt;/sub&amp;gt; bezeichnet).&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;0&amp;quot; style=&amp;quot;margin: 1em auto 1em auto&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
| [[Image:k1.png|frame|k1]]&lt;br /&gt;
| [[Image:k2.png|frame|k2]]&lt;br /&gt;
| [[Image:k3.png|frame|k3]]&lt;br /&gt;
|-&lt;br /&gt;
| [[Image:k4.png|frame|k4]]&lt;br /&gt;
| [[Image:k5.png|frame|k5]]&lt;br /&gt;
|&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
''Rätsel''&amp;lt;br/&amp;gt;&lt;br /&gt;
Auf einer Party sind Leute. Alle stoßen miteinander an. Es hat 78 mal &amp;quot;Pling&amp;quot; gemacht.&lt;br /&gt;
Wieviele Leute waren da? Antwort: Jede Person ist ein Knoten des Graphen, jedes Antoßen eine Kante. &lt;br /&gt;
Da alle miteinander angestoßen haben, handelt es sich um einen vollständigen Graphen. Mit&lt;br /&gt;
|V|(|V|-1)/2 = 78 folgt, dass es 13 Personen waren.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Gewichteter Graph: Ein Graph heißt ''gewichtet'', wenn jeder Kante eine reelle Zahl zugeordnet ist. Bei vielen Anwendungen beschränkt man sich auch auf nichtnegative reelle Gewichte. In einem gerichteten Graphen können die Gewichte der Kanten (u,v) und (v,u) unterschiedlich sein.&lt;br /&gt;
&lt;br /&gt;
Die Gewichte kodieren Eigenschaften der Kanten, die für die jeweilige Anwendung interessant sind. Bei der Berechnung des maximalen Flusses in einem Netzwerk sind die Gewichte z.B. die Durchflusskapazitäten jeder Kante, bei der Suche nach kürzesten Weges kodieren Sie den Abstand zwischen den Endknoten der Kante, bei Währungsnetzwerken (jeder Knoten ist eine Währung) geben sie die Wechselkurse an, usw..&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Teilgraphen: Ein Graph G' = (V',E') ist ein Teilgraph eines Graphen G, wenn gilt:&lt;br /&gt;
:* V' &amp;amp;sube; V &lt;br /&gt;
:* E' &amp;amp;sub; E &lt;br /&gt;
:Er heißt ''(auf)spannender Teilgraph'', wenn gilt:&lt;br /&gt;
:* V' = V&lt;br /&gt;
:Er heißt ''induzierter Teilgraph'', wenn gilt:&lt;br /&gt;
:* e = (u,v) ∈ E' &amp;amp;sub; E &amp;amp;hArr; u ∈ V' und v ∈ V'&lt;br /&gt;
:Den von V' induzierten Teilgraphen erhält man also, indem man aus G alle Knoten löscht, die nicht in V' sind, sowie alle Kanten (und nur diese Kanten), die einen der gelöschten Knoten als Endknoten haben.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Wege, Pfade, Zyklen, Kreise, Erreichbarkeit: Sei G = (V,E) ein Graph (ungerichtet oder gerichteter) Graph. Dann gilt folgende rekursive Definition:&lt;br /&gt;
:* Für v ∈ V ist (v) ein Weg der Länge 0 in G&lt;br /&gt;
:* Falls &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1})&amp;lt;/math&amp;gt; ein Weg ist, und eine Kante &amp;lt;math&amp;gt;(v_{n-1}, v_n)\in E&amp;lt;/math&amp;gt; existiert, dann ist auch &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ein Weg, und er hat die Länge n. &lt;br /&gt;
: Ein Weg ist also eine nichtleere Folge von Knoten, so dass aufeinander folgende Knoten stets durch eine Kante verbunden sind. Die Länge des Weges entspricht der Anzahl der Kanten im Weg (= Anzahl der Knoten - 1).&lt;br /&gt;
:* Ein ''Pfad'' &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ist ein Weg, bei dem alle Knoten v&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; verschieden sind.&lt;br /&gt;
:* ''Ein Zyklus'' &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ist ein Weg, der zum Ausgangspunkt zurückkehrt, wenn also v&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; = v&amp;lt;sub&amp;gt;n&amp;lt;/sub&amp;gt; gilt.&lt;br /&gt;
:* Ein ''Kreis'' ist ein Zyklus ohne Überkreuzungen. Das heisst, es gilt v&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; = v&amp;lt;sub&amp;gt;n&amp;lt;/sub&amp;gt; und &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1})&amp;lt;/math&amp;gt; ist ein Pfad.&lt;br /&gt;
:* Ein Knoten w ∈ V ist von einem anderen Knoten v ∈ V aus ''erreichbar'' genau dann, wenn ein Weg (v, ..., w) existiert. Wir schreiben dann &amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt;.&lt;br /&gt;
In einem ungerichteten Graph ist die Erreichbarkeits-Relation stets symmetrisch, das heisst aus &amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; folgt &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. In einem gerichteten Graphen ist dies im allgemeinen nicht der Fall.&lt;br /&gt;
&lt;br /&gt;
Bestimmte Wege haben spezielle Namen&lt;br /&gt;
&lt;br /&gt;
;Eulerweg: Ein Eulerweg ist ein Weg, der alle '''Kanten''' genau einmal enthält.&lt;br /&gt;
&lt;br /&gt;
Die eingangs erwähnte Frage des Königsberger Brückenproblems ist equivalent zu der Frage, ob der dazugehörige Graph einen Eulerweg besitzt (daher der Name). Ein anderes bekanntes Beispiel ist das &amp;quot;Haus vom Nikolaus&amp;quot;: Wenn man diesen Graphen in üblicher Weise in einem Zug zeichnet, erhält man gerade den Eulerweg. &lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O----O&lt;br /&gt;
  | \/ |&lt;br /&gt;
  | /\ |   &amp;quot;Das Haus vom Nikolaus&amp;quot;: Alle ''Kanten'' werden nur ''einmal'' passiert&lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Hamiltonweg: Ein Hamiltonweg ist ein Weg, der alle '''Knoten''' genau einmal enthält. Das &amp;quot;Haus vom Nikolaus&amp;quot; besitzt auch einen Hamiltonweg:&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /   &lt;br /&gt;
  O----O&lt;br /&gt;
     /  &lt;br /&gt;
    /      Alle ''Knoten'' werden nur ''einmal'' passiert&lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Hamiltonkreis: Ein Hamiltonkreis ist ein Kreis, der alle '''Knoten''' genau einmal enthält. Auch ein solches Gebilde ist im Haus von Nilolaus enthalten:&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O    O&lt;br /&gt;
  |    |   v0 = vn&lt;br /&gt;
  |    |   vi != vj   Für Alle i,j   i !=j; i,j &amp;gt;0; i,j &amp;lt; n&lt;br /&gt;
  O----O     &lt;br /&gt;
&lt;br /&gt;
Die folgende Skizze zeigt hingegen einen Zyklus: Der Knoten rechts unten sowie die untere Kante sind zweimal enthalten (die Kante einmal von links nach rechts und einmal von rechts nach links):&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O    O&lt;br /&gt;
    \  |&lt;br /&gt;
     \ |   Zyklus&lt;br /&gt;
  O====O&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Zusammenhang, Zusammenhangskomponenten: Ein ungerichteter Graph G heißt ''zusammenhängend'', wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt;&lt;br /&gt;
:Ein gerichteter Graph G ist zusammenhängend, wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; '''oder''' &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. &lt;br /&gt;
:Er ist ''stark zusammenhängend'', wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; '''und''' &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. &lt;br /&gt;
:Entsprechende Definitionen gelten für Teilgraphen G'. Ein Teilgraph G' heisst ''Zusammenhangskomponente'' von G, wenn er ein ''maximaler'' zusammenhängender Teilgraph ist, d.h. wenn G' zusammenhängend ist, und man keine Knoten und Kanten aus G mehr zu G' hinzufügen kann, so dass G' immer noch zusammenhängend bleibt. Entsprechend definiert man ''starke Zusammenhangskomponenten'' in einem gerichteten Graphen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Planarer Graph, ebener Graph: Ein Graph heißt ''planar'', wenn er so in einer Ebene gezeichnet werden ''kann'', dass sich die Kanten nicht schneiden (außer an den Knoten). Ein Graph heißt ''eben'', wenn er tatsächlich so gezeichnet ''ist'', dass sich die Kanten nicht schneiden. Die Einbettung in die Ebene ist im allgemeinen nicht eindeutig.&lt;br /&gt;
&lt;br /&gt;
'''Beispiele:'''&lt;br /&gt;
&lt;br /&gt;
Der folgende Graph ist planar und eben:&lt;br /&gt;
 &lt;br /&gt;
      O&lt;br /&gt;
     /|\&lt;br /&gt;
    / O \&lt;br /&gt;
   / / \ \&lt;br /&gt;
   O     O&lt;br /&gt;
&lt;br /&gt;
Das &amp;quot;Haus vom Nikolaus&amp;quot; ist ebenfalls planar, wird aber üblicherweise nicht als ebener Graph gezeichnet, weil sich die Diagonalen auf der Wand überkreuzen:&lt;br /&gt;
 &lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O----O&lt;br /&gt;
  | \/ |&lt;br /&gt;
  | /\ |   &lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
Eine ebene Einbettung dieses Graphen wird erreicht, wenn man eine der Diagonalen ausserhalb des Hauses zeichnet. Der Graph (also die Menge der Knoten und Kanten) ändert sich dadurch nicht.&lt;br /&gt;
 &lt;br /&gt;
      O  &lt;br /&gt;
     /  \&lt;br /&gt;
  --O----O&lt;br /&gt;
 /  |  / |&lt;br /&gt;
 |  | /  |   &lt;br /&gt;
 |  O----O      Das &amp;quot;Haus vom Nikolaus&amp;quot; als ebener Graph gezeichnet.&lt;br /&gt;
  \     /&lt;br /&gt;
   -----&lt;br /&gt;
&lt;br /&gt;
Eine alternative Einbettung erhalten wir, wenn wir die andere Diagonale außerhalb des Hauses zeichnen:&lt;br /&gt;
 &lt;br /&gt;
      O  &lt;br /&gt;
     /  \&lt;br /&gt;
    O----O--|&lt;br /&gt;
    | \  |  |&lt;br /&gt;
    |  \ |  | &lt;br /&gt;
    O----O  |     Alternative Einbettung des &amp;quot;Haus vom Nikolaus&amp;quot;.&lt;br /&gt;
    |       |&lt;br /&gt;
    |-------|&lt;br /&gt;
&lt;br /&gt;
Jede Einbettung eines planaren Graphen (also jeder ebene Graph) definiert eine eindeutige Menge von ''Regionen'':&lt;br /&gt;
&lt;br /&gt;
 |----O   @&lt;br /&gt;
 |   /@ \&lt;br /&gt;
 |  O----O&lt;br /&gt;
 |  |@ / |&lt;br /&gt;
 |  | / @|   &lt;br /&gt;
 |  O----O        @ entspricht jeweils einer ''Region''. Auch ausserhalb der Figur ist eine Region (die sogenannte ''unendliche'' Region).&lt;br /&gt;
 |@      |&lt;br /&gt;
 |-------|&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Der vollständige Graph K5 ist kein planarer Graph, da sich zwangsweise Kanten schneiden, wenn man diesen Graphen in der Ebene zeichnet.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
;Dualer Graph: Jeder ebene Graph G = (V, E) hat einen ''dualen Graphen'' D = (V&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;, E&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;), dessen Knoten und Kanten wie folgt definiert sind:&lt;br /&gt;
:* V&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt; enthält einen Knoten für jede Region des Graphen G&lt;br /&gt;
:* Für jede Kante e ∈ E gibt es eine duale Kante e&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt; ∈ E&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;, die die an e angrenzenden Regionen (genauer: die entsprechenden Knoten in D) verbindet.&lt;br /&gt;
&lt;br /&gt;
Die folgende Abbildung zeigt einen Graphen (grau) und seinen dualen Graphen (schwarz). Die Knoten des dualen Graphen sind mit Zahlen gekennzeichnet und entsprechen den Regionen des Originalgraphen. Jeder (grauen) Kante des Originalgraphen entspricht eine (schwarze) Kante des dualen Graphen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
[[Image:dual-graphs.png]]&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für duale Graphen gilt: Wenn der Originalgraph zusammenhängend ist, enthält jede Region des dualen Graphen genau einen Knoten des Originalgraphen. Deshalb ist der duale Graph des dualen Graphen wieder der Originalgraph. Bei nicht-zusammenhängenden Graphen gilt dies nicht (vgl. das Fenster bei obigem Bild). In diesem Fall hat der duale Graph mehrere mögliche Einbettungen in die Ebene (man kann z.B. die rechte Kante zwischen Knoten 2 und 4 auch links vom Fenster einzeichnen), und man erhält nicht notwendigerweise den Originalgraphen, wenn man den dualen Graphen des dualen berechnet.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Baum: Ein ''Baum'' ist ein zusammenhängender, kreisfreier Graph.&lt;br /&gt;
&lt;br /&gt;
Beispiel: Binärer Suchbaum&lt;br /&gt;
&lt;br /&gt;
;Spannbaum: Ein ''Spannbaum'' eines zusammenhängenden Graphen G ist ein zusammenhängender, kreisfreier Teilgraph von G, der alle Knoten von G enthält&lt;br /&gt;
&lt;br /&gt;
Beispiel: Spannbaum für das &amp;quot;Haus des Nikolaus&amp;quot; &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    O   &lt;br /&gt;
   /       &lt;br /&gt;
  O    O&lt;br /&gt;
  |  /  &lt;br /&gt;
  | /   &lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
Der Spannbaum eines Graphen mit |V| Knoten hat stets |V| - 1 Kanten.&lt;br /&gt;
&lt;br /&gt;
;Wald: Ein ''Wald'' ist ein unzusammenhängender, kreisfreier Graph.&lt;br /&gt;
: Jede Zusammenhangskomponente eines Waldes ist ein Baum.&lt;br /&gt;
&lt;br /&gt;
=== Repräsentation von Graphen ===&lt;br /&gt;
&lt;br /&gt;
Sei G = ( V, E ) gegeben und liege V in einer linearen Sortierung vor.&amp;lt;br/&amp;gt; &lt;br /&gt;
:::&amp;lt;math&amp;gt;V = \{ v_1, ...., v_n \}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Adjazenzmatrix: Ein Graph kann durch eine Adjazenzmatrix repräsentiert werden, die soviele Zeilen und Spalten enthält, wie der Graph Knoten hat. Die Elemente der Adjazenzmatrix sind &amp;quot;1&amp;quot;, falls eine Kante zwischen den zugehörigen Knoten existiert:&lt;br /&gt;
:::&amp;lt;math&amp;gt;\mathrm{\bold A} = a_{ij} = &lt;br /&gt;
\begin{cases}&lt;br /&gt;
1 &amp;amp; \mathrm{falls}\quad (v_i, v_j) \in E \\&lt;br /&gt;
0 &amp;amp; \mathrm{sonst}&lt;br /&gt;
\end{cases} &lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
:Die Indizes der Matrix entsprechen also den Indizes der Knoten gemäß der gegebenen Sortierung. Im Falle eines ungerichteten Graphen ist die Adjazenzmatrix stets symmetrisch (d.h. es gilt &amp;lt;math&amp;gt;a_{ij}=a_{ji}&amp;lt;/math&amp;gt;), bei einem gerichteten Graphen ist sie im allgemeinen unsymmetrisch.&lt;br /&gt;
&lt;br /&gt;
Beispiel für einen ungerichteten Graphen:&lt;br /&gt;
&lt;br /&gt;
 v = { a,b,c,d }     b      d&lt;br /&gt;
                     | \  / |&lt;br /&gt;
                     |  \/  |&lt;br /&gt;
                     |  /\  |&lt;br /&gt;
                     | /  \ |&lt;br /&gt;
                     a      c&lt;br /&gt;
 &lt;br /&gt;
       a b c d&lt;br /&gt;
      -----------&lt;br /&gt;
      (0 1 0 1) |a &lt;br /&gt;
  A = (1 0 1 0) |b&lt;br /&gt;
      (0 1 0 1) |c&lt;br /&gt;
      (1 0 1 0) |d&lt;br /&gt;
&lt;br /&gt;
Die Adjazenzmatrixdarstellung eignet sich besonders für dichte Graphen (d.h. wenn die Zahl der Kanten in O(|V|&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt;) ist.&lt;br /&gt;
&lt;br /&gt;
;Adjazenzlisten: In der Adjazenzlistendarstellung wird der Graph als Liste von Knoten repräsentiert, die für jeden Knoten einen Eintrag enthält. Der Eintrag für jeden Knoten ist wiederum eine Liste, die die Nachbarknoten dieses Knotens enthält:&lt;br /&gt;
:* graph = {adjazencyList(v) | v ∈ V}&lt;br /&gt;
:* adjazencyList(v) = {v' ∈ V | (v, v') ∈ E}&lt;br /&gt;
&lt;br /&gt;
In Python implementieren wir Adjazenzlisten zweckmäßig als Array von Arrays:&lt;br /&gt;
&lt;br /&gt;
                   graph = [[...],[...],...,[...]]&lt;br /&gt;
 Adjazenzliste für Knoten =&amp;gt;  0     1         n&lt;br /&gt;
&lt;br /&gt;
Wenn wir bei dem Graphen oben die Knoten wie bei der Adjazenzmatrix indizieren (also &amp;lt;tt&amp;gt;a =&amp;gt; 0&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;b =&amp;gt; 1&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;c =&amp;gt; 2&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;d =&amp;gt; 3&amp;lt;/tt&amp;gt;), erhalten wir die Adjazenzlistendarstellung:&lt;br /&gt;
&lt;br /&gt;
 graph = [[b, d], [a, c],[b, d], [a, c]]&lt;br /&gt;
&lt;br /&gt;
Auf die Nachbarknoten eines durch seinen Index &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; gegebenen Knotens können wir also wie folgt zugreifen:&lt;br /&gt;
&lt;br /&gt;
      for neighbors in graph[node]:&lt;br /&gt;
          ... # do something with neighbor&lt;br /&gt;
&lt;br /&gt;
Die Adjazenzlistendarstellung ist effizienter, wenn der Graph nicht dicht ist, so dass viele Einträge der Adjazenzmatrix Null wären. In der Vorlesung werden wir nur diese Darstellung verwenden.&lt;br /&gt;
&lt;br /&gt;
;&amp;lt;div id=&amp;quot;transposed_graph&amp;quot;&amp;gt;Transponierter Graph&amp;lt;/div&amp;gt;: Den ''transponierten Graphen'' G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; eines gerichteten Graphen G erhält man, wenn man alle Kantenrichtungen umkehrt.&lt;br /&gt;
&lt;br /&gt;
Bei ungerichteten Graphen hat die Transposition offensichtlich keinen Effekt, weil alle Kanten bereits in beiden Richtungen vorhanden sind, so dass G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; = G gilt. Bei gerichteten Graphen ist die Transposition einfach, wenn der Graph als Adjazenzmatrix implementiert ist, weil man einfach die transponierte Adjazenzmatrix verwenden muss (beachte, dass sich die Reihenfolge der Indizes umkehrt):&lt;br /&gt;
:::A&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; = a&amp;lt;sub&amp;gt;ji&amp;lt;/sub&amp;gt;&lt;br /&gt;
Ist der Graph hingegen durch eine Adjazenzliste repräsentiert, muss etwas mehr Aufwand getrieben werden:&lt;br /&gt;
&lt;br /&gt;
 def transposeGraph(graph):&lt;br /&gt;
      gt = [[] for k in graph]   # zunächst leere Adjazenzlisten von G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt;&lt;br /&gt;
      for node in range(len(graph)):&lt;br /&gt;
           for neighbor in graph[node]:&lt;br /&gt;
               gt[neighbor].append(node)  # füge die umgekehrte Kante in G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; ein&lt;br /&gt;
      return gt&lt;br /&gt;
&lt;br /&gt;
== Durchlaufen von Graphen (Graph Traversal) ==&lt;br /&gt;
&lt;br /&gt;
Wir betrachten zunächst ungerichtete Graphen mit V Knoten und E Kanten. Eine grundlegende Aufgabe in diesen Graphen besteht darin, alle Knoten in einer bestimmten Reihenfolge genau einmal zu besuchen. Hierbei darf man sich von einem gegebenen Startknoten aus nur entlang der Kanten des Graphen bewegen. Die beim Traversieren benutzen Kanten bilden einen Baum, dessen Wurzel der Startknoten ist und der den gesamten Graphen aufspannt, falls der Graph zusammenhängend ist. (Beweis: Da jeder Knoten nur einmal besucht wird, gibt es für jeden besuchten Knoten [mit Ausnahme des Startknotens] genau eine eingehende Kante. Ist der Graph zusammenhängend, wird jeder Knoten tatsächlich erreicht und es gibt genau (V-1) Kanten, exakt soviele wie für einen Baum mit V Knoten notwendig sind.) Ist der Graph nicht zusammenhängend, wird jeder zusammenhängende Teilgraph (jede &amp;lt;i&amp;gt;Zusammenhangskomponente&amp;lt;/i&amp;gt;) getrennt traversiert, und man erhält einen sogenannten &amp;lt;i&amp;gt;Wald&amp;lt;/i&amp;gt; mit einem Baum pro Zusammenhangskomponente. Die beiden grundlegenden Traversierungsmethoden &amp;lt;i&amp;gt;Tiefensuche&amp;lt;/i&amp;gt; und &amp;lt;i&amp;gt;Breitensuche&amp;lt;/i&amp;gt; werden im folgenden vorgestellt.&lt;br /&gt;
&lt;br /&gt;
=== Tiefensuche in Graphen (Depth First Search, DFS) ===&lt;br /&gt;
&lt;br /&gt;
Die Idee der Tiefensuche besteht darin, jeden besuchten Knoten sofort über die erste Kante wieder zu verlassen, die zu einem noch nicht besuchten Knoten führt. Man findet dadurch schnell einen möglichst langen Pfad durch den Graphen, und der Traversierungs-Baum wird zunächst in die Tiefe verfolgt, daher der Name des Verfahrens. Hat ein Knoten keine unbesuchten Nachbarknoten mehr, geht man im Baum zurück (sogenanntes &amp;lt;i&amp;gt;back tracking&amp;lt;/i&amp;gt;), bis man einen Knoten findet, der noch eine unbesuchte Nachbarn besitzt, und traversiert diese nach dem gleichen Muster. Gibt es gar keine unbesuchten Knoten mehr, kehrt die Suche zum Startknoten zurück und endet dort.&lt;br /&gt;
&lt;br /&gt;
WDie folgende rekursive Implementation der Tiefensuche erwartet den Graphen in Adjazenzlistendarstellung und beginnt die Suche beim Knoten &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt;. Die Information, ob ein Knoten bereits besucht wurde, wird im Array &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; gespeichert. Ein solches Array, das zusätzliche Informationen über die Knoten des Graphen bereitstellt, wir häufig &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; genannt.&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)  # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):              # rekursive Hilfsfunktion, die den gegebenen Knoten und dessen Nachbarn besucht&lt;br /&gt;
         if not visited[node]:     # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True  # Markiere node als besucht&lt;br /&gt;
             print node            # Ausgabe der Knotennummer - pre-order&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode)&lt;br /&gt;
&lt;br /&gt;
[[Image:Tiefens.jpg]]&lt;br /&gt;
&lt;br /&gt;
Ausgabe für den Graphen in diesem Bild (es handelt sich um einen ungerichteten Graphen, die Pfeile symbolisieren nur die Suchrichtung beim Traversal):&lt;br /&gt;
&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; dfs(graph, 1)&lt;br /&gt;
 1&lt;br /&gt;
 2&lt;br /&gt;
 4&lt;br /&gt;
 3&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
 5&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div id=&amp;quot;pre_and_post_order&amp;quot;&amp;gt;In dieser Version des Algorithmus werden die Knotennummern ausgegeben, bevor die Nachbarknoten besucht werden. Man bezeichnet die resultierende Sortierung der Knoten als &amp;lt;b&amp;gt;pre-order&amp;lt;/b&amp;gt; oder als &amp;lt;b&amp;gt;discovery order&amp;lt;/b&amp;gt;. Alternativ kann man die Knotennummern erst ausgeben, nachdem alle Nachbarn besucht wurden, also auf dem Rückweg der Rekursion. In diesem Fall spricht man von &amp;lt;b&amp;gt;post-order&amp;lt;/b&amp;gt; oder &amp;lt;b&amp;gt;finishing order&amp;lt;/b&amp;gt;:&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)  # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):              # rekursive Hilfsfunktion, die den gegebenen Knoten und dessen Nachbarn besucht&lt;br /&gt;
         if not visited[node]:     # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True  # Markiere node als besucht&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
             &amp;lt;font color=red&amp;gt;print node            # Ausgabe der Knotennummer - post-order&amp;lt;/font&amp;gt;&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode)&lt;br /&gt;
&lt;br /&gt;
Es ergibt sich jetzt die Ausgabe:&lt;br /&gt;
&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; dfs(graph, 1)&amp;lt;font color=red&amp;gt;&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
 3&lt;br /&gt;
 4&lt;br /&gt;
 5&lt;br /&gt;
 2&lt;br /&gt;
 1&amp;lt;/font&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In realem Code ersetzt man die print-Ausgaben natürlich durch anwendungsspezifische Aktionen und Berechnungen. Einige Anwendungen sind uns im Kapitel [[Suchen]] bereits begegnet. &lt;br /&gt;
; Anwendungen der Pre-Order Traversierung&lt;br /&gt;
* Kopieren eines Graphen: kopiere zuerst den besuchten Knoten, dann seine Nachbarn und die dazugehörigen Kanten (sowie die Kanten zu bereits besuchten Knoten, die in der Grundversion der Tiefensuche ignoriert werden).&lt;br /&gt;
* Bestimmen der Zusammenhangskomponenten eines Graphen (siehe unten)&lt;br /&gt;
* In einem Zeichenprogramm: fülle eine Region mit einer Farbe (&amp;quot;flood fill&amp;quot;). Dabei ist jedes Pixel ein Knoten des Graphen und wird mit seinen 4 Nachbarpixceln verbunden. Die Tiefensuche startet bei der Mausposition und endet am Rand des betreffendcen Gebiets.&lt;br /&gt;
* Falls der Graph ein Baum ist: bestimme den Abstand jedes Knotens von der Wurzel&lt;br /&gt;
* Falls der Graph ein Parse-Baum ist, wobei innere Knoten Funktionsaufrufe, Kindknoten Funktionsargumente, und Blattknoten Werte repräsentieren: drucke den zugehörigen Ausdruck aus (also immer zuerst den Funktionsnamen, dann die Argumente, die wiederum geschachtelte Funktionsaufrufe sein können).&lt;br /&gt;
; Anwendungen der Post-Order Traversierung&lt;br /&gt;
* Löschen eines Graphen: lösche zuerst die Nachbarn, dann den Knoten selbst&lt;br /&gt;
* Bestimmen einer topologischen Sortierung eines azyklischen gerichteten Graphens (siehe unten)&lt;br /&gt;
* Falls der Graph ein Baum ist: bestimme den Abstand jedes Knotens von den Blättern (also die Tiefe des Baumes, siehe Übung 5)&lt;br /&gt;
* Falls der Graph ein Parse-Baum ist: führe die zugehörige Berechnung aus (d.h. berechne zuerst die geschachtelten inneren Funktionen, dann mit diesen Ergebnissen die nächst äußeren usw., siehe Übung 5).&lt;br /&gt;
; Anwendungen, die Pre- und Post-Order benötigen&lt;br /&gt;
* Weg aus einem Labyrinth: die Pre-Order dokumentiert die Suche nach dem Weg, die Post-Order zeigt den Rückweg aus Sackgassen (siehe Übung 9).&lt;br /&gt;
Im Spezialfall, wenn der Graph ein Binärbaum ist, unterscheidet man noch eine dritte Variante der Traversierung, nämlich die &amp;lt;i&amp;gt;in-order&amp;lt;/i&amp;gt; Traversierung. In diesem Fall behandelt man den Vaterknoten nach den linken, aber vor den rechten Kindern. Diese Reihenfolge wird beim [[Suchen#Beziehungen zwischen dem Suchproblem und dem Sortierproblem|Tree Sort Algorithmus]] verwendet. Diese Sortierung verwendet man auch, wenn man einen Parse-Baum mit binären Operatoren (statt Funktionsaufrufen) ausgeben will, siehe Übung 5.&lt;br /&gt;
&lt;br /&gt;
Eine nützliche Erweiterung der Tiefensuche besteht darin, in der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; nicht nur zu dokumentieren, dass ein Knoten bereits besucht wurde, sondern auch, von welchem Knoten aus man den jeweiligen Knoten zuerst erreicht hat. Im entstehenden Tiefensuchbaum ist dies gerade der Vaterknoten, weshalb wir die verbesserte property map zweckmäßigerweise in &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; umbenennen. Für den Startknoten, also die Wurzel des Baumes, wählen wir die Konvention, dass er sein eigener Vaterknoten ist (die Konvention, dafür den Wert &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zu verwenden, scheidet aus, weil dies bereits die Tatsache signalisiert, dass ein Knoten noch nicht besucht wurde):&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     parents = [None]*len(graph)     # Registriere für jeden Knoten den Vaterknoten im Tiefensuchbaum&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, parent):        # rekursive Hilfsfunktion&lt;br /&gt;
         if parents[node] is None:   # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             parents[node] = parent  # Markiere node als besucht und speichere seinen Vaterknoten&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn ...&lt;br /&gt;
                 visit(neighbor, node)      #  ... wobei node zu deren Vaterknoten wird&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode, startnode)     # Konvention für Wurzel: startnode ist sein eigener Vater&lt;br /&gt;
     &lt;br /&gt;
     return parents                  # Rückgabe des berechneten Tiefensuch-Baums&lt;br /&gt;
&lt;br /&gt;
Die Ausgabe für den obigen Beispielgraphen lautet: &lt;br /&gt;
  Knotennummer  |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7&lt;br /&gt;
  --------------+-----+-----+-----+-----+-----+-----+-----+-----&lt;br /&gt;
  Vaterknoten   | None|  1  |  1  |  4  |  2  |  2  |  3  |  3&lt;br /&gt;
&lt;br /&gt;
Dabei ist die Knotennummer der Index im Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt;, und der Vaterknoten ist der dazugehörige Arrayeintrag. Beachte, dass Knoten 0 in diesem Graphen nicht existiert, daher ist sein Eintrag &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;. Per Konvention hat der Wurzelknoten 1 sich selbst als Vater.&lt;br /&gt;
&lt;br /&gt;
=== Breitensuche in Graphen (Breadth First Search, BFS) ===&lt;br /&gt;
&lt;br /&gt;
Im Gegensatz zur Tiefensuche werden bei der Breitensuche alle Nachbarnknoten abgearbeitet, &amp;lt;i&amp;gt;bevor&amp;lt;/i&amp;gt; man rekursiv deren Nachbarn besucht. Man betrachtet somit zuerst alle Knoten, die den Abstand 1 von Startknoten haben, dann diejenigen mit dem Abstand 2 usw. Diese Reihenfolge bezeichnet man als &amp;lt;i&amp;gt;level-order&amp;lt;/i&amp;gt;. Wir sind ihr beispielsweise in Übung 6 begegnet, als die ersten 7 Ebenen eines Treap ausgegeben werden sollten. Man implementiert Breitensuche zweckmäßig mit Hilfe einer Queue, die die Knoten in First In - First Out - Reihenfolge bearbeitet. Eine geeignete Datenstruktur hierfür ist die Klasse &amp;lt;tt&amp;gt;[http://docs.python.org/library/collections.html#collections.deque deque]&amp;lt;/tt&amp;gt; aus dem Python-Modul &amp;lt;tt&amp;gt;[http://docs.python.org/library/collections.html collections]&amp;lt;/tt&amp;gt; (eine Deque implementiert sowohl die Funktionalität einer Queue wie auch die eines Stacks, siehe Übung 3):&lt;br /&gt;
&lt;br /&gt;
 from collections import deque&lt;br /&gt;
 &lt;br /&gt;
 def bfs(graph, startnode)&lt;br /&gt;
     visited = [False]*len(graph)   # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
   &lt;br /&gt;
     q = deque()                    # Queue für die zu besuchenden Knoten&lt;br /&gt;
     q.append(startnode)            # Startknoten in die Queue einfügen&lt;br /&gt;
     &lt;br /&gt;
     while len(q) &amp;gt; 0:              # Solange es noch unbesuchte Knoten gibt&lt;br /&gt;
         node = q.popleft()         # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
         if not visited[node]:      # Falls node noch nicht (auf einem anderen Weg) besucht wurde&lt;br /&gt;
              visited[node] = True  # Markiere node als besucht&lt;br /&gt;
              print node            # Drucke Knotennummer&lt;br /&gt;
              for neighbor in graph[node]:    # Füge Nachbarn in die Queue ein&lt;br /&gt;
                  q.append(neighbor)&lt;br /&gt;
&lt;br /&gt;
[[Image:Breitens.jpg]]&lt;br /&gt;
&lt;br /&gt;
Der Aufruf dieser Funktion liefert die Knoten des obigen Graphens ebenenweise, also zufällig genau in der Reihenfolge der Knotennummern:&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; bfs(graph, 1)&lt;br /&gt;
 1&lt;br /&gt;
 2&lt;br /&gt;
 3&lt;br /&gt;
 4&lt;br /&gt;
 5&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
&lt;br /&gt;
Neben der ebenenweisen Ausgabe hat die Breitensuche viele weitere wichtige Anwendungen, z.B. beim Testen, ob ein gegebener Graph bi-partit ist (siehe [http://en.wikipedia.org/wiki/Breadth-first_search#Testing_bipartiteness WikiPedia]), sowie bei der Suche nach kürzesten Wegen (siehe unten) und kürzesten Zyklen.&lt;br /&gt;
&lt;br /&gt;
== Weitere Anwendungen der Tiefensuche ==&lt;br /&gt;
&lt;br /&gt;
Die Tiefensuche hat zahlreiche Anwendungen, wobei der grundlegende Algorithmus immer wieder leicht modifiziert und an die jeweilige Aufgabe angepasst wird. Wir beschreiben im folgenden einige Beispiele.&lt;br /&gt;
&lt;br /&gt;
=== Damenproblem ===&lt;br /&gt;
&lt;br /&gt;
Tiefensuche wird häufig verwendet, um systematisch nach der Lösung eines logischen Rätsels (oder allgemeiner nach der Lösung eines diskreten Optimierungsproblems) zu suchen. Besonders anschaulich hierfür ist das Damenproblem. Die Aufgabe besteht darin, &amp;lt;math&amp;gt;k&amp;lt;/math&amp;gt; Damen auf einem Schachbrett der Größe &amp;lt;math&amp;gt;k \times k&amp;lt;/math&amp;gt; so zu platzieren, dass sie sich (nach den üblichen Schach-Regeln) nicht gegenseitig schlagen können. Das folgende Diagramm zeigt eine Lösung für den Fall &amp;lt;math&amp;gt;k=4&amp;lt;/math&amp;gt;. Die Positionen der Damen werden dabei wie üblich durch die Angabe der Spalte (Linie) mit Buchstaben und der Zeile (Reihe) mit Zahlen kodiert, hier also A2, B4, C1, D3:&lt;br /&gt;
&lt;br /&gt;
  ---------------&lt;br /&gt;
 |   | X |   |   | 4&lt;br /&gt;
 |---|---|---|---| &lt;br /&gt;
 |   |   |   | X | 3&lt;br /&gt;
 |---|---|---|---|&lt;br /&gt;
 | X |   |   |   | 2&lt;br /&gt;
 |---|---|---|---|&lt;br /&gt;
 |   |   | X |   | 1&lt;br /&gt;
  ---------------&lt;br /&gt;
   A   B   C   D&lt;br /&gt;
&lt;br /&gt;
Um das Problem systematisch zu lösen, konstruieren wir einen gerichteten Graphen, dessen Knoten die möglichen Positionen der Damen kodieren. Wir verbinden Knoten, die zu benachbarten Linien gehören, genau dann mit einer Kante, wenn die zugehörigen Positionen kompatibel sind, also wenn sich die dort positionierten Damen nicht schlagen können. Der resultierende Graph für &amp;lt;math&amp;gt;k=4&amp;lt;/math&amp;gt; hat folgende Gestalt:&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
Knoten, die zur selben Reihe oder Linie gehören, sind beispielsweise nicht direkt verbunden, weil zwei Damen niemals in derselben Linie oder Reihe stehen dürfen. Um eine erlaubte Konfiguration zu finden, verwenden wir nun eine angepasste Version der Tiefensuche: Wir beginnen die Suche beim Knoten &amp;lt;tt&amp;gt;START&amp;lt;/tt&amp;gt;. Sobald wir den Knoten &amp;lt;tt&amp;gt;STOP&amp;lt;/tt&amp;gt; erreichen, beenden wir die Suche und lesen die Lösung am gerade gefundenen Weg von Start nach Stop ab. Zwei kleine Modifikationen des Grundalgorithmus stellen sicher, dass die Bedingungen der Aufgabe eingehalten werden: Wir dürfen bei der Tiefensuche nur dann zu einem Nachbarn weitergehen, wenn die betreffende Position mit allen im Pfad bereits gesetzten Positionen kompatibel ist, andernfalls ist diese Kante tabu. Landen wir aufgrund dieser Regel in einer Sackgasse (also in einem Knoten, wo keine der ausgehenden Kanten erlaubt ist), müssen wir zur nächsten erlaubten Abzweigung zurückgehen (Backtracking). Beim Zurückgehen müssen wir das &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Flag wieder auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zurücksetzen, weil der betreffende Knoten ja möglicherweise auf einem anderen erlaubten Weg erreichbar ist.&lt;br /&gt;
&lt;br /&gt;
Der folgende Graph zeigt einen solchen Fall: Wir haben zwei Damen auf die Felder A1 und B3 positioniert (grüne Pfeile). Die einzig ausgehende Kante von B3 führt zum Knoten C1, welcher aber mit der Position A1 inkompatibel ist, so dass diese Kante nicht verwendet werden darf (roter Pfeil). Das Backtracking muss jetzt zu Knoten A1 zurückgehen (dabei wird das &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Flag von B3 wieder auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; gesetzt), weil A1 mit der Kante nach B4 eine weitere Option hat, die geprüft werden muss (die allerdings hier auch nicht zum Ziel führt).&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph-failure.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
Nach einigen weiteren Sackgassen findet man schließlich den Pfad A2, B4, C1, D3, der im folgenden Graphen grün markiert ist und der obigen Lösung entspricht:&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph-success.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
=== Test, ob ein ungerichteter Graph azyklisch ist ===&lt;br /&gt;
&lt;br /&gt;
Ein zusammenhängender ungerichteter Graph ist azyklisch (also ein Baum) genau dann, wenn es nur einen möglichen Weg von jedem Knoten zu jedem anderen gibt. (Bei gerichteten Graphen sind die Verhältnisse komplizierter. Wir behandeln dies weiter unten.) Das kann man mittels Tiefensuche leicht feststellen: Die Kante, über die wir einen Knoten erstmals erreichen, ist eine &amp;lt;i&amp;gt;Baumkante&amp;lt;/i&amp;gt; des Tiefensuchbaums. Erreichen wir einen bereits besuchten Knoten nochmals über eine andere Kante, haben wir einen Zyklus gefunden. Dabei müssen wir allerdings beachten, dass in einem ungerichteten Graphen jede Baumkante zweimal gefunden wird, einmal in Richtung vom Vater zum Kind und einmal in umgekehrter Richtung. Im zweiten Fall endet die Kante zwar in einem bereits besuchten Knoten (dem Vater), aber es entsteht dadurch kein Zyklus. Den Vaterknoten müssen wir deshalb überspringen, wenn wir über die Nachbarn iterieren:&lt;br /&gt;
&lt;br /&gt;
 def undirected_cycle_test(graph):         # Annahme: der Graph ist zusammenhängend&lt;br /&gt;
                                           # (andernfalls führe den Algorithmus für jede Zusammenhangskomponente aus)&lt;br /&gt;
     visited = [False]*len(graph)          # Flags für bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, from_node):           # rekursive Hilfsfunktion: gibt True zurück, wenn Zyklus gefunden wurde&lt;br /&gt;
         if not visited[node]:             # wenn node noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True          # markiere node als besucht&lt;br /&gt;
             for neighbor in graph[node]:  # besuche die Nachbarn ...&lt;br /&gt;
                 if neighbor == from_node: # ... aber überspringe den Vaterknoten&lt;br /&gt;
                     continue&lt;br /&gt;
                 if visit(neighbor, node): # ... signalisiere, wenn rekursiv ein Zyklus gefunden wurde&lt;br /&gt;
                     return True&lt;br /&gt;
             return False                  # kein Zyklus gefunden&lt;br /&gt;
         else:&lt;br /&gt;
             return True                   # Knoten schon besucht =&amp;gt; Zyklus&lt;br /&gt;
     &lt;br /&gt;
     startnode = 0                         # starte bei beliebigem Knoten (hier: Knoten 0)&lt;br /&gt;
     return visit(startnode, startnode)    # gebe True zurück, wenn ein Zyklus gefunden wurde&lt;br /&gt;
&lt;br /&gt;
Wenn wir einen Zyklus finden, wird das weitere Traversieren das Graphen abgebrochen, denn ein Graph, der einmal zyklisch war, kann später nicht wieder azyklisch werden. Die notwendige Modifikation für unzusammenhängende Graphen erfolgt analog zum Algorithmus für die Detektion von Zusammenhangskomponenten, der im nächsten Abschnitt beschrieben wird.&lt;br /&gt;
&lt;br /&gt;
=== Finden von Zusammenhangskomponenten ===&lt;br /&gt;
&lt;br /&gt;
Das Auffinden und Markieren von Zusammenhangskomponenten (also maximalen zusammenhängenden Teilgraphen) ist eine grundlegende Aufgabe in ungerichteten, unzusammenhängenden Graphen (bei gerichteten Graphen sind die Verhältnisse wiederum komplizierter, siehe unten). Zwei Knoten u und v gehören zur selben Zusammenhangskomponente genau dann, wenn es einen Pfad von u nach v gibt (da der Graph ungerichtet ist, gibt es dann auch einen Pfad von v nach u). Man sagt auch, dass &amp;quot;v von u aus erreichbar&amp;quot; ist. Unzusammenhängende Graphen entstehen in der Praxis häufig, wenn die Kanten gewisse Relationen zwischen den Knoten kodieren: &lt;br /&gt;
* Wenn die Knoten Städte sind und die Kanten Straßen, sind diejenigen Städte in einer Zusammenhangskomponente, die per Auto von einander erreichbar sind. Unzusammenhängende Graphen entstehen hier beispielsweise, wenn eine Insel nicht durch eine Brücke erschlossen ist, wenn Grenzen gesperrt sind oder wenn ein Gebirge zu unwegsam ist, um Straßen zu bauen.&lt;br /&gt;
* Wenn Knoten Personen sind, und Kanten die Eltern-Kind-Relation beschreiben, so umfasst jede Zusammenhangskomponenten die Verwandten (auch wenn sie nur über viele &amp;quot;Ecken&amp;quot; verwandt sind).&lt;br /&gt;
* In der Bildverarbeitung entsprechen Knoten den Pixeln, und dieselben werden durch eine Kante verbunden, wenn sie zum selben Objekt gehören. Die Zusammenhangskomponenten entsprechen somit den Objekten im Bild (siehe Übungsaufgabe).&lt;br /&gt;
Die Zusammenhangskomponenten bilden eine Äquivalenzrelation. Folglich kann für jede Komponente ein Reprässentant bestimmt werden, der sogenannte &amp;quot;Anker&amp;quot;. Kennt jeder Knoten seinen Anker, ist das Problem der Zusammenhangskomponenten gelöst. &lt;br /&gt;
&lt;br /&gt;
==== Lösung mittels Tiefensuche ====&lt;br /&gt;
&lt;br /&gt;
Unser erster Ansatz ist, den Anker mit Hilfe der Tiefensuche zu finden. Anstelle der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; verwenden wir diesmal eine property map &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt;, die für jeden Knoten die Knotennummer des zugehörigen Ankers angibt, oder &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, wenn der Knoten noch nicht besucht wurde. Dabei verwenden wir wieder die Konvention, dass Anker auf sich selbst zeigen. Für viele Anwendungen ist es außerdem (oder stattdessen) zweckmäßig, die Zusammenhangskomponenten mit einer laufenden Nummer, einem sogenannten &amp;lt;i&amp;gt;Label&amp;lt;/i&amp;gt;, durchzuzählen. Dann kann man zusätzliche Informationen zu jeder Komponente (beispielsweise deren Größe) einfach in einem Array speichern, das über die Labels indexiert wird. Die folgende Version der Tiefensuche bestimmt sowohl die Anker als auch die Labels für jeden Knoten:&lt;br /&gt;
&lt;br /&gt;
 def connectedComponents(graph):&lt;br /&gt;
        anchors = [None] * len(graph)             # property map für Anker jedes Knotens&lt;br /&gt;
        labels  = [None] * len(graph)             # property map für Label jedes Knotens&lt;br /&gt;
        &lt;br /&gt;
        def visit(node, anchor):&lt;br /&gt;
                &amp;quot;&amp;quot;&amp;quot;anchor ist der Anker der aktuellen ZK&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
                if anchors[node] is None:         # wenn node noch nicht besucht wurde:&lt;br /&gt;
                    anchors[node] = anchor        # setze seinen Anker&lt;br /&gt;
                    labels[node] = labels[anchor] # und sein Label&lt;br /&gt;
                    for neighbor in graph[node]:  # und besuche die Nachbarn&lt;br /&gt;
                        visit(neighbor, anchor)&lt;br /&gt;
        &lt;br /&gt;
        current_label = 0                         # Zählung der ZK beginnt bei 0&lt;br /&gt;
        for node in xrange(len(graph)):&lt;br /&gt;
            if anchors[node] is None:             # Anker noch nicht bekannt =&amp;gt; neue ZK gefunden&lt;br /&gt;
                labels[node] = current_label      # Label des Ankers setzen&lt;br /&gt;
                visit(node, node)                 # Knoten der neuen ZK rekursiv suchen&lt;br /&gt;
                current_label += 1                # Label für die nächste ZK hochzählen&lt;br /&gt;
        return anchors, labels&lt;br /&gt;
Interessant ist hier die Schleife über alle Knoten des Graphen am Ende des Algorithmus, die bei den bisherigen Versionen der Tiefensuche nicht vorhanden war. Um ihre Funktionsweise zu verstehen, nehmen wir für den Moment an, dass der Graph zusammenhängend ist. Dann findet diese Schleife den ersten Knoten des Graphen und führt die Tiefensuche mit diesem Knoten als Startknoten aus. Sobald die Rekursion zurückkehrt, sind alle Knoten des Graphen besucht (weil der Graph ja zusammenhängend war), so dass die Schleife alle weiteren Knoten überspringt (die if-Anweisung liefert für keinen weiteren Knoten True). Bei unzusammenhängenden Graphen dagegen erreicht die Tiefensuche nur die Knoten derselben Komponente, die im weiteren Verlauf der Schleife übersprungen werden. Findet die if-Anweisung jetzt einen noch nicht besuchten Knoten, muss dieser folglich in einer neuen Komponente liegen. Wir verwenden diesen Knoten als Anker und bestimmen die übrigen Knoten dieser Komponente wiederum mit Tiefensuche.&lt;br /&gt;
&lt;br /&gt;
* Beispiel: ... &amp;lt;b&amp;gt; under construction &amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Man erkennt, dass die Tiefensuche nach dem &amp;lt;i&amp;gt;Anlagerungsprinzip&amp;lt;/i&amp;gt; vorgeht: Beginnend vom einem Startknoten (dem Anker) werden die Knoten der aktuellen Komponente nach und nach an den Tiefensuchbaum angehangen. Erst, wenn nichts mehr angelagert werden kann, geht der Algorithmus zur nächsten Komponente über.&lt;br /&gt;
&lt;br /&gt;
==== Lösung mittels Union-Find-Algorithmus ====&lt;br /&gt;
&lt;br /&gt;
Im Gegensatz zum Anlagerungsprinzip sucht der Union-Find-Algorithmus die Zusammenhangskomponenten mit dem &amp;lt;i&amp;gt;Verschmelzungsprinzip&amp;lt;/i&amp;gt;: Eingangs wird jeder Knoten als ein Teilgraph für sich betrachtet. Dann iteriert man über alle Kanten und verbindet deren Endknoten jeweils zu einem gemeinsamen Teilgraphen (falls die beiden Enden einer Kante bereits im selben Teilgraphen liegen, wird diese Kante ignoriert). Solange noch Kanten vorhanden sind, werden dadurch immer wieder Teilgraphen in größere Teilgraphen verschmolzen. Am Ende bleiben die maximalen zusammenhängenden Teilgraphen (also gerade die Zusammenhangskomponenten) übrig. Dieser Algorithmus kommt ohne Tiefensuche aus und ist daher in der Praxis oft schneller, allerdings auch etwas komplizierter zu implementieren.&lt;br /&gt;
&lt;br /&gt;
Der Schlüssel des Algorithmus ist eine Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt;, die zu jedem Knoten den aktuellen Anker sucht. Der Anker existiert immer, da jeder Knoten von Anfang an zu einem Teilgraphen gehört (anfangs ist jeder Teilgraph trivial und besteht nur aus dem Knoten selbst). Die Verschmelzung wird realisiert, indem der Anker des einen Teilgraphen seine Rolle verliert und stattdessen der Anker des anderen Teilgraphen eingesetzt wird. &lt;br /&gt;
&lt;br /&gt;
Zur Verwaltung der Anker verwenden wir wieder eine property map &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt; mit der Konvention, dass die Anker auf sich selbst verweisen. Es wäre jedoch zu teuer, wenn man bei jeder Verschmelzung alle Anker-Einträge der beteiligten Knoten aktualisieren müsste, da jeder Knoten im Laufe des Algorithmus mehrmals seinen Anker wechseln kann. Statt dessen definiert man Anker rekursiv: Verweist ein Knoten auf einen Anker, der mittlerweile diese Rolle verloren hat, folgt man dem Verweis von diesem Knoten (dem ehemaligen Anker) weiter, bis man einen tatsächlichen Anker gefunden hat - erkennbar daran, dass er auf sich selbst verweist. Diese Suchfunktion kann folgendermassen implementiert werden:&lt;br /&gt;
&lt;br /&gt;
  def findAnchor(anchors, node):&lt;br /&gt;
      while node != anchors[node]:   # wenn node kein Anker ist&lt;br /&gt;
          node = anchors[node]       # ... verfolge die Ankerkette weiter&lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Allerdings kann diese Kette im Laufe vieler Verschmelzungen sehr lang werden, so dass das Verfolgen der Kette teuer wird. Man vermeidet dies durch die sogenannte &amp;lt;i&amp;gt;Pfadkompression&amp;lt;/i&amp;gt;: Immer, wenn man den Anker gefunden hat, aktualisiert man den Eintrag am Anfang der Kette. Die Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt; wird dadurch nur wenig komplizierter:&lt;br /&gt;
&lt;br /&gt;
  def findAnchor(anchors, node):&lt;br /&gt;
      start = node                   # wir merken uns den Anfang der Kette&lt;br /&gt;
      while node != anchors[node]:   # wenn node kein Anker ist&lt;br /&gt;
          node = anchors[node]       # ... verfolge die Ankerkette weiter&lt;br /&gt;
      anchors[start] = node          # Pfadkompression: aktualisiere den Eintrag am Anfang der Kette&lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Man kann zeigen, dass die Ankersuche mit Pfadkompression zu einer fast konstanten amortisierten Laufzeit pro Aufruf führt.&lt;br /&gt;
&lt;br /&gt;
Um mit jeder Kante des (ungerichteten) Graphen nur maximal einmal eine Verschmelzung durchzuführen, betrachten wir jede Kante nur in der Richtung von der kleineren zur größeren Knotennummer, die umgekehrte Richtung wird ignoriert. Außerdem ist es zweckmäßig, bei jeder Verschmelzung denjenigen Anker mit der kleineren Knotennummer als neuen Anker zu übernehmen. Dann gilt für jede Zusammenhangskomponente, dass gerade der Knoten mit der kleinsten Knotennummer der Anker ist (genau wie bei der Lösung mittels Tiefensuche), was die weitere Analyse vereinfacht, z.B. die Zuordnung der Labels zu den Komponenten am Ende des Algorithmus. &lt;br /&gt;
&lt;br /&gt;
 def unionFindConnectedComponents(graph):&lt;br /&gt;
     anchors = range(len(graph))        # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):    # iteriere über alle Knoten&lt;br /&gt;
         for neighbor in graph[node]:   # ... und über deren ausgehende Kanten&lt;br /&gt;
             if neighbor &amp;lt; node:        # ignoriere Kanten, die in falscher Richtung verlaufen&lt;br /&gt;
                 continue&lt;br /&gt;
             # hier landen wir für jede Kante des Graphen genau einmal&lt;br /&gt;
             a1 = findAnchor(anchors, node)       # finde Anker ...&lt;br /&gt;
             a2 = findAnchor(anchors, neighbor)   # ... der beiden Endknoten&lt;br /&gt;
             if a1 &amp;lt; a2:                          # Verschmelze die beiden Teilgraphen&lt;br /&gt;
                 anchors[a2] = a1                 # (verwende den kleineren der beiden Anker als Anker des&lt;br /&gt;
             elif a2 &amp;lt; a1:                        #  entstehenden Teilgraphen. Falls node und neighbor &lt;br /&gt;
                 anchors[a1] = a2                 #  den gleichen Anker haben, waren sie bereits im gleichen&lt;br /&gt;
                                                  #  Teilgraphen, und es passiert hier nichts.)&lt;br /&gt;
     # Bestimme jetzt noch die Labels der Komponenten&lt;br /&gt;
     labels = [None]*len(graph)         # Initialisierung der property map für Labels&lt;br /&gt;
     current_label = 0                  # die Zählung beginnt bei 0&lt;br /&gt;
     for node in xrange(len(graph)):&lt;br /&gt;
         a = findAnchor(anchors, node)  # wegen der Pfadkompression zeigt jeder Knoten jetzt direkt auf seinen Anker&lt;br /&gt;
         if a == node:                  # node ist ein Anker&lt;br /&gt;
             labels[a] = current_label  # =&amp;gt; beginne eine neue Komponente&lt;br /&gt;
             current_label += 1         # und zähle Label für die nächste ZK hoch&lt;br /&gt;
         else:&lt;br /&gt;
             labels[node] = labels[a]   # node ist kein Anker =&amp;gt; setzte das Label des Ankers&lt;br /&gt;
                                        # (wir wissen, dass labels[a] bereits gesetzt ist, weil &lt;br /&gt;
                                        #  der Anker immer der Knoten mit der kleinsten Nummer ist)&lt;br /&gt;
     return anchors, labels&lt;br /&gt;
 &lt;br /&gt;
* Beispiel: ... &amp;lt;b&amp;gt;under construction&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Kürzeste Wege (Pfade) ==&lt;br /&gt;
&lt;br /&gt;
Eine weitere grundlegende Aufgabe in Graphen ist die Bestimmung eines kürzesten Weges zwischen zwei gegebenen Knoten. Dies hat offensichtliche Anwendungen bei Routenplanern und Navigationssystemen und ist darüber hinaus wichtiger Bestandteil anderer Algorithmen, z.B. bei der Berechnung eines maximalen Flusses mit der [http://en.wikipedia.org/wiki/Edmonds%E2%80%93Karp_algorithm Methode von Edmonds und Karp].&lt;br /&gt;
&lt;br /&gt;
=== Kürzeste Wege in ungewichteten Graphen mittels Breitensuche ===&lt;br /&gt;
&lt;br /&gt;
Im Fall eines ungewichteten Graphen ist die Länge eines Weges einfach durch die Anzahl der durchlaufenen Kanten definiert. Daraus folgt, dass kürzeste Pfade mit einer leicht angepassten Version der Breitensuche gefunden werden können: Aufgrund des first in-first out-Verhaltens der Queue betrachtet die Breitensuche alle (erreichbaren) Knoten in der Reihenfolge ihres Abstandes vom Startknoten. Wenn wir den Zielknoten zum ersten Mal erreichen, und der gerade gefundene Weg vom Start zum Ziel hat die Länge L, muss dies der kürzeste Weg sein: Alle möglichen Wege der Länge L' &amp;amp;lt; L hat die Breitensuche ja bereits betrachtet, ohne dass dabei der Zielknoten erreicht wurde. Daraus folgt übrigens eine allgemeine Eigenschaft aller Algorithmen für kürzeste Wege: Wenn der kürzeste Weg vom Start zum Ziel die Länge L hat, finden diese Algorithmen als Nebenprodukt auch die kürzesten Wege zu allen Knoten, für die L' &amp;amp;lt; L gilt. &lt;br /&gt;
&lt;br /&gt;
Um den Algorithmus zu implementieren, passen wir die Breitensuche so an, dass anstelle der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; eine property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; verwendet wird, die für jeden besuchten Knoten den Vaterknoten im Breitensuchbaum speichert. Durch Rückverfolgen der &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette können wir den Pfad vom Ziel zum Start rekonstruieren, und durch Umdrehen der Reihenfolge erhalten wir den gesuchten Pfad vom Start zum Ziel. Sobald der Zielknoten erreicht wurde, können wir die Breitensuche abbrechen (&amp;lt;tt&amp;gt;break&amp;lt;/tt&amp;gt;-Befehl in der ersten &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Schleife). Falls der gegebene Graph unzusammenhängend ist, kann es passieren, dass gar kein Weg gefunden wird, weil Start und Ziel in verschiedenen Zusammenhangskomponenten liegen. Dies erkennen wir daran, dass die Breitensuche beendet wurde, ohne den Zielknoten zu besuchen. Dann gibt die Funktion statt eines Pfades dern Wert &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zurück:&lt;br /&gt;
&lt;br /&gt;
  from collections import deque&lt;br /&gt;
  &lt;br /&gt;
  def shortestPath(graph, startnode, destination):&lt;br /&gt;
      parents = [None]*len(graph)      # Registriere für jeden Knoten den Vaterknoten im Breitensuchbaum&lt;br /&gt;
      parents[startnode] = startnode   # startnode ist die Wurzel des Baums =&amp;gt; verweist auf sich selbst&lt;br /&gt;
      &lt;br /&gt;
      q = deque()                      # Queue für die zu besuchenden Knoten&lt;br /&gt;
      q.append(startnode)              # Startknoten in die Queue einfügen&lt;br /&gt;
      &lt;br /&gt;
      while len(q) &amp;gt; 0:                # Solange es noch unbesuchte Knoten gibt&lt;br /&gt;
          node = q.popleft()           # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
          if node == destination:      # Zielknoten erreicht&lt;br /&gt;
              break                    #   =&amp;gt; Suche beenden&lt;br /&gt;
          for neighbor in graph[node]: # Besuche die Nachbarn von node&lt;br /&gt;
              if parents[neighbor] is None:  # aber nur, wenn sie noch nicht besucht wurden&lt;br /&gt;
                  parents[neighbor] = node   # setze node als Vaterknoten&lt;br /&gt;
                  q.append(neighbor)         # und füge neighbor in die Queue ein&lt;br /&gt;
      &lt;br /&gt;
      if parents[destination] is None: # Breitensuche wurde beendet ohne den Zielknoten zu besuchen&lt;br /&gt;
          return None                  # =&amp;gt; kein Pfad gefunden (unzusammenhängender Graph)&lt;br /&gt;
      &lt;br /&gt;
      # Pfad durch die parents-Kette zurückverfolgen und speichern&lt;br /&gt;
      path = [destination]&lt;br /&gt;
      while path[-1] != startnode:&lt;br /&gt;
          path.append(parents[path[-1]])&lt;br /&gt;
      path.reverse()     # Reihenfolge umdrehen (Ziel =&amp;gt; Start wird zu Start =&amp;gt; Ziel)&lt;br /&gt;
      return path        # gefundenen Pfad zurückgeben&lt;br /&gt;
&lt;br /&gt;
=== Gewichtete Graphen ===&lt;br /&gt;
&lt;br /&gt;
Das Problem der Suche nach kürzesten Wegen wird wesentlich interessanter und realistischer, wenn wir zu gewichteten Graphen übergehen:&lt;br /&gt;
&lt;br /&gt;
; Definition - kantengewichteter Graph&lt;br /&gt;
: Jeder Kante (s,t) des Graphen ist eine reelle oder natürliche Zahl w&amp;lt;sub&amp;gt;st&amp;lt;/sub&amp;gt; zugeordnet, die üblicherweise als ''Kantengewicht'' bezeichnet wird.&lt;br /&gt;
&lt;br /&gt;
; Definition - knotengewichteter Graph&lt;br /&gt;
: Jedem Knoten v des Graphen ist eine reelle oder natürliche Zahl w&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; zugeordnet, die üblicherweise als ''Knotengewicht'' bezeichnet wird.&lt;br /&gt;
&lt;br /&gt;
Je nach Anwendung benötigt man Knoten- oder Kantengewichte oder auch beides zugleich. Wir beschränken uns in der Vorlesung auf kantengewichtete Graphen. Beispiele für die Informationen, die man durch Kantengewichte ausdrücken kann, sind&lt;br /&gt;
* wenn die Knoten Orte sind: Abstand von Anfangs- und Endknoten jeder Kante (z.B. Luftline oder Straßenentfernung), Fahrzeit zwischen den Orten&lt;br /&gt;
* wenn der Knoten ein Rohrnetzwerk beschreibt: Durchflusskapazität der einzelnen Rohre (für max-Flussprobleme), analog bei elektrischen Netzwerken: elektrischer Widerstand&lt;br /&gt;
* wenn die Knoten Währungen repräsentieren, können deren Wechselkurse durch Kantengewichte angegeben werden.&lt;br /&gt;
Bei einigen Beispielen ergeben sich unterschiedliche Kantengewichte, wenn eine Kante von s nach t anstatt von t nach s durchlaufen wird. Beispielsweise können sich die Fahrzeiten erheblich unterscheiden, wenn es in einer Richtung bergauf, in der anderen bergab geht, obwohl die Entfernung in beiden Fällen gleich ist. Hier ergibt sich natürlicherweise ein gerichteter Graph. In anderen Beispielen (z.B. bei Luftlinienentfernungen, in guter Näherung auch bei Straßenentfernungen) sind die Gewichte von der Richtung unabhängig, so dass wir ungerichtete Graphen verwenden können.&lt;br /&gt;
&lt;br /&gt;
Die Repräsentation der Kantengewichte im Programm richtet sich nach der Repräsentation des Graphen selbst. Am einfachsten ist wiederum die Adjazenzmatrix, die aber nur für dichte Graphen (&amp;lt;math&amp;gt;E = O(V^2)&amp;lt;/math&amp;gt;, mit E als Anzahl der Kanten und V als Anzahl der Knoten) effizient ist. Bei gewichteten Graphen gibt das Matrixelement a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; das Gewicht der Kante i &amp;amp;rArr; j (wobei a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; = 0 gesetzt wird, wenn diese Kante nicht existiert). Wie zuvor gilt für ungerichtete Graphen a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; = a&amp;lt;sub&amp;gt;ji&amp;lt;/sub&amp;gt; (symmetrische Matrix), während dies für gerichtete Graphen nicht gelten muss.&lt;br /&gt;
&lt;br /&gt;
Bei Graphen in Adjazenzlistendarstellung hat es sich bewährt, die Gewichte in einer &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; zu speichern. Weiter oben haben wir bereits property maps für Knoteneigenschaften (z.B. &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt;) gesehen. Property maps für Kanten funktionieren ganz analog, allerdings muss man jetzt Paare von Knoten (nämlich Anfangs- und Endknoten der Kante) als Schlüssel verwenden und die Daten entsprechend in einem assoziativen Array ablegen:&lt;br /&gt;
  w = weights[(i,j)]   # Zugriff auf das Gewicht der Kante i &amp;amp;rArr; j&lt;br /&gt;
Alternativ könnte man auch die Graph-Datenstruktur selbst erweitern, aber dies ist weniger zu empfehlen, weil jeder Algorithmus andere Erwiterungen benötigt und damit die Datenstruktur sehr unübersichtlich würde.&lt;br /&gt;
&lt;br /&gt;
Der kürzeste Weg ist nun definiert als der Weg, bei dem die Summe der Kantengewichte minimal ist:&lt;br /&gt;
;Definition - Problem des kürzesten Weges&lt;br /&gt;
: Sei P die Menge aller Wege von u nach v, und &amp;lt;math&amp;gt;p \in P&amp;lt;/math&amp;gt; einer dieser Wege. Wenn der Grpah einfach ist (es also keine Mehrfachkanten zwischen denselben Knoten und keine Schleifen gibt), ist der Weg p durch die Folge der besuchten Knoten eindeutig bestimmt:&lt;br /&gt;
: &amp;lt;math&amp;gt;p : \ \ u = x_0 \rightarrow x_1 \rightarrow x_2 \rightarrow ... \rightarrow v = x_{n_p}&amp;lt;/math&amp;gt;&lt;br /&gt;
:wo &amp;lt;math&amp;gt;n_p&amp;lt;/math&amp;gt; die Anzahl der Kanten im Weg p ist. Seine Kosten W&amp;lt;sub&amp;gt;p&amp;lt;/sub&amp;gt; ergeben sich als Summer der Gewichte der einzelnen Kanten&lt;br /&gt;
: &amp;lt;math&amp;gt;W_p = \sum_{k=1}^{n_p} w_{x_{k-1}x_k}&amp;lt;/math&amp;gt;&lt;br /&gt;
: und ein kürzester Weg &amp;lt;math&amp;gt;p^* \in P&amp;lt;/math&amp;gt; ist ein Weg mit minimalen Kosten&lt;br /&gt;
: &amp;lt;math&amp;gt;p^* = \textrm{argmin}_{p\in P}\ \ W_p&amp;lt;/math&amp;gt;&lt;br /&gt;
: Das Problem des kürzesten Weges besteht darin, einen optimalen Weg &amp;lt;i&amp;gt;p*&amp;lt;/i&amp;gt; zwischen gegebenen Knoten u und v zu finden.&lt;br /&gt;
Die Lösung dieses Problems hängt davon ab, ob alle Kantengewichte positiv sind, oder ob es auch negative Kantengewichte gibt. In letzeren Fall ist es möglich, durch eine Verlängerung des Weges die Kosten zu redizieren, während sich im ersteren Fall die Kosten immer erhöhen, wenn man den Weg verlängert. &lt;br /&gt;
&lt;br /&gt;
Negative Gewichte treten z.B. bei den Währungsgraphen auf. Auf den ersten Blick entsprechen diese Graphen nicht den Anforderungen an das Problem des kürzesten Weges, weil Wechselkurse miteinander (und mit Geldbeträgen) multipliziert anstatt addiert werden. Man beseitigt diese Schwierigkeit aber leicht, indem man die &amp;lt;i&amp;gt;Logarithmen&amp;lt;/i&amp;gt; der Wechselkurse als Kantengewichte verwendet, wodurch sich die Multiplikation in eine Addition der Logarithmen verwandelt. Wechselkurse &amp;amp;lt; 1 führen nun zu negativen Gewichten. &lt;br /&gt;
&lt;br /&gt;
Interessant werden negative Gewichte vor allem in Graphen mit Zyklen. Dann kann es nämlich passieren, dass die Gesamtkosten eines Zyklus ebenfalls negativ sind. Jeder Weg, der den Zyklus enthält, hat dann Kosten von &amp;lt;math&amp;gt;-\infty&amp;lt;/math&amp;gt;, weil man den Zyklus beliebig oft durchlaufen und dadurch die Gesamtkosten immer weiter verkleinern kann:&lt;br /&gt;
&lt;br /&gt;
     /\		1. Durchlauf: Kosten -1&lt;br /&gt;
  1 /  \ -4	2. Durchlauf: Kosten -2&lt;br /&gt;
   /____\	etc.&lt;br /&gt;
      2&lt;br /&gt;
&lt;br /&gt;
Um hier nicht in einer Endlosschleife zu landen, benötigt man spezielle Algorithmen, die mit dieser Situation umgehen können. Der [http://de.wikipedia.org/wiki/Bellman-Ford-Algorithmus Algorithmus von Bellmann und Ford] beispielsweise bricht die Suche nach dem kürzesten Weg ab, sobald er einen negativen Zyklus entdeckt, aber andernfalls kann er negative Gewichte problemlos verarbeiten. &lt;br /&gt;
&lt;br /&gt;
Die Detektion negativer Zyklen hat wiederum eine interessante Anwendung bei Währungsgraphen: Ein Zyklus bedeutet hier, dass man Geld über mehrere Stufen von einer Währung in die nächste und am Schluß wieder in die Originalwährung umtauscht, und ein negativer Zyklus führt dazu, dass man am Ende &amp;lt;i&amp;gt;mehr&amp;lt;/i&amp;gt; Geld besitzt als am Anfang (damit negative Zyklen wirklich einen Gewinn bedeuten und keinen Verlust, müssen die Wechselkurse vor der Logarithmierung in [http://de.wikipedia.org/wiki/Wechselkurs#Nominaler_Wechselkurs Preisnotierung] angegeben sein). Bei Privatpersonen ist dies ausgeschlossen, weil die Umtauschgebühren den möglichen Gewinn mehr als aufzehren. Banken mit direktem weltweitem Börsenzugang hingegen unternehmen große Anstrengungen, um solche negativen Zyklen möglichst schnell (nämlich vor der Konkurrenz) zu entdecken und auszunutzen. Diese Geschäftsmethode bezeichnet man als [http://de.wikipedia.org/wiki/Arbitrage Arbitrage] und die Existenz eines negativen Zyklus als Arbitragegelegenheit. Durch die Kursschwankungen (und durch die ausgleichende Wirkung der Arbitragegeschäfte selbst) existieren die Arbitragegelegenheiten nur für kurze Zeit, und ihre Detektion erfordert leistungsfähige Echtzeitalgorithmen.&lt;br /&gt;
&lt;br /&gt;
In dieser Vorlesung beschränken wir uns hingegen auf Graphen mit ausschließlich positiven Gewichten. In diesem Fall ist der Algorithmus von Dijkstra die Methode der Wahl, weil er wesentlich schneller arbeitet als der Bellmann-Ford-Algorithmus.&lt;br /&gt;
&lt;br /&gt;
=== Algorithmus von Dijkstra ===&lt;br /&gt;
&lt;br /&gt;
==== Edsger Wybe Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
geb. 11. Mai 1930 in Rotterdam&lt;br /&gt;
&lt;br /&gt;
ges. 06. August 2002&lt;br /&gt;
&lt;br /&gt;
Dijkstra war ein niederländischer Informatiker und Wegbereiter der strukturierten Programmierung. 1972 erhielt er für seine Leistung in der Technik und Kunst der Programmiersprachen den Turing Award, der jährlich von der Association for Computing Machinery (ACM) an Personen verliehen wird, die sich besonders um die Entwicklung der Informatik verdient gemacht haben. Zu seinen Beiträgen zur Informatik gehören unter anderem der Dijkstra-Algorithmus zur Berechnung des kürzesten Weges in einem Graphen sowie eine Abhandlung über den go-to-Befehl und warum er nicht benutzt werden sollte. Der go-to-Befehl war in den 60er und 70er Jahren weit verbreitet, führte aber zu Spaghetti-Code. In seinem berühmten Paper &amp;quot;A Case against the GO TO Statement&amp;quot;[http://www.cs.utexas.edu/users/EWD/ewd02xx/EWD215.PDF], das als Brief mit dem Titel &amp;quot;Go-to statement considered harmful&amp;quot; veröffentlicht wurde, argumentiert Dijkstra, dass es umso schwieriger ist, dem Quellcode eines Programmes zu folgen, je mehr go-to-Befehle darin enthalten sind und zeigt, dass man auch ohne diesen Befehl gute Programme schreiben kann.&lt;br /&gt;
&lt;br /&gt;
==== Algorithmus ====&lt;br /&gt;
&lt;br /&gt;
Der Dijkstra-Algorithmus für kürzeste Wege ist dem oben vorgestellten Algorithmus &amp;lt;tt&amp;gt;shortestPath()&amp;lt;/tt&amp;gt; auf der Basis von Breitensuche sehr ähnlich. Insbesondere gilt auch hier, dass neben dem kürzesten Weg vom Start zum Ziel auch alle kürzesten Wege gefunden werden, deren Endknoten dem Start näher sind als der Zielknoten. Aufgrund der Kantengewichte gibt es aber einen wichtigen Unterschied: Der erste gefundene Weg zu einem Knoten ist nicht mehr notwendigerweise der kürzeste. Wir bestimmen deshalb für jeden Knoten mehrere Kandidatenwege und verwenden eine Prioritätswarteschlange (statt einer einfachen First in - First out - Queue), um diese Wege nach ihrer Länge zu sortieren. Die Kandidatenwege für einen gegebenen Knoten werden unterschieden, indem wir auch den Vorgängerknoten im jeweiligen Weg speichern. Wenn ein Knoten &amp;lt;i&amp;gt;erstmals&amp;lt;/i&amp;gt; an die Spitze der Prioritätswarteschlange gelangt, haben wir den kürzesten Weg zu diesem Knoten gefunden (das wird weiter unten formal bewiesen), und der Vorgänger des Knotens in diesem Weg wird zu seinem Vaterknoten. Erscheint derselbe Knoten später nochmals an der Spitze der Prioritätswarteschlange, handelt es sich um einen Kandidatenweg, der sich nicht als kürzester erwiesen hat und deshalb ignoriert werden kann. Wir erkennen dies leicht daran, dass der Vaterknoten in der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; bereits gesetzt ist. &lt;br /&gt;
&lt;br /&gt;
Eine geeignete Datenstruktur für die Prioritätswarteschlange wird durch das Python-Modul [http://docs.python.org/library/heapq.html heapq] realisiert. Es verwendet ein normales Pythonarray als unterliegende Repräsentation für einen Heap und stellt effiziente &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt;-Funktionen zur Verfügung. Dies entspricht genau unserer Vorgehensweise im Kapitel [[Prioritätswarteschlangen]]. Als Datenelement erwartet die Funktion &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; ein Tupel, dessen erstes Element die Priorität sein muss. Die übrigen Elemente des Tupels (und damit auch deren Anzahl) können je nach Anwendung frei festgelegt werden. Wir legen fest, dass das zweite Element den Endknoten des betrachteten Weges und das dritte den Vorgängerknoten speichert. &lt;br /&gt;
&lt;br /&gt;
Die Kantengewichte werden dem Algorithmus in der property map &amp;lt;tt&amp;gt;weights&amp;lt;/tt&amp;gt; übergeben:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;code python&amp;gt;&lt;br /&gt;
    import heapq	                  # heapq implementiert die Funktionen für Heaps&lt;br /&gt;
    &lt;br /&gt;
    def dijkstra(graph, weights, startnode, destination):&lt;br /&gt;
        parents = [None]*len(graph)       # registriere für jeden Knoten den Vaterknoten im Pfadbaum&lt;br /&gt;
      &lt;br /&gt;
        q = []                            # Array q wird als Heap verwendet&lt;br /&gt;
        &amp;lt;font color=red&amp;gt;heapq.heappush(q, (0.0, startnode, startnode))&amp;lt;/font&amp;gt;  # Startknoten in Heap einfügen&lt;br /&gt;
      &lt;br /&gt;
        while len(q) &amp;gt; 0:                 # solange es noch Knoten im Heap gibt:&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;length, node, predecessor = heapq.heappop(q)&amp;lt;/font&amp;gt;   # Knoten aus dem Heap nehmen&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;if parents[node] is not None:&amp;lt;/font&amp;gt; # parent ist schon gesetzt =&amp;gt; es gab einen anderen, kürzeren Weg&lt;br /&gt;
                &amp;lt;font color=red&amp;gt;continue&amp;lt;/font&amp;gt;                  #   =&amp;gt; wir können diesen Weg ignorieren&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;parents[node] = predecessor&amp;lt;/font&amp;gt;   # parent setzen&lt;br /&gt;
            if node == destination:       # Zielknoten erreicht&lt;br /&gt;
                break                     #   =&amp;gt; Suche beenden&lt;br /&gt;
            for neighbor in graph[node]:  # die Nachbarn von node besuchen,&lt;br /&gt;
                if parents[neighbor] is None:   # aber nur, wenn ihr kürzester Weg noch nicht bekannt ist&lt;br /&gt;
                    &amp;lt;font color=red&amp;gt;newLength = length + weights[(node,neighbor)]&amp;lt;/font&amp;gt;   # berechne Pfadlänge zu neighbor              &lt;br /&gt;
                    &amp;lt;font color=red&amp;gt;heapq.heappush(q, (newLength, neighbor, node))&amp;lt;/font&amp;gt;  # und füge neighbor in den Heap ein&lt;br /&gt;
      &lt;br /&gt;
        if parents[destination] is None:  # Suche wurde beendet ohne den Zielknoten zu besuchen&lt;br /&gt;
            return None, None             # =&amp;gt; kein Pfad gefunden (unzusammenhängender Graph)&lt;br /&gt;
      &lt;br /&gt;
        # Pfad durch die parents-Kette zurückverfolgen und speichern&lt;br /&gt;
        path = [destination]&lt;br /&gt;
        while path[-1] != startnode:&lt;br /&gt;
            path.append(parents[path[-1]])&lt;br /&gt;
        path.reverse()                    # Reihenfolge umdrehen (Ziel =&amp;gt; Start wird zu Start =&amp;gt; Ziel)&lt;br /&gt;
        return path, length               # gefundenen Pfad und dessen Länge zurückgeben&lt;br /&gt;
  &amp;lt;/code&amp;gt;&lt;br /&gt;
Die wesentlichen Unterschiede zur Breitensuche sind im Code rot markiert: Anstelle der Queue verwenden wir jetzt einen Heap, und der Startknoten wird mit Pfadlänge 0 als erstes eingefügt. In der Schleife &amp;lt;tt&amp;gt;while len(q) &amp;gt; 0:&amp;lt;/tt&amp;gt; wird jeweils der Knoten &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; mit der aktuell kürzesten Pfadlänge aus dem Heap entfernt. Die Pfadlänge vom Start zu diesem Knoten wird in der Variable &amp;lt;tt&amp;gt;length&amp;lt;/tt&amp;gt; gespeichert, sein Vorgänger in der Variable &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;. Wenn der aktuelle Weg nicht der kürzeste ist (&amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; war bereits gesetzt), wird dieser Weg ignoriert. Andernfalls werden die property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; aktualisiert und die Nachbarn von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; besucht. Beim Scannen der Nachbarn berechnen wir zunächst die Länge &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; das Weges &amp;lt;tt&amp;gt;startnode =&amp;amp;gt; node =&amp;amp;gt; neighbor&amp;lt;/tt&amp;gt; als Summe von &amp;lt;tt&amp;gt;length&amp;lt;/tt&amp;gt; und dem Gewicht der Kante &amp;lt;tt&amp;gt;(node, neighbode)&amp;lt;/tt&amp;gt;. Diese Länge wird beim Einfügen des Nachbarknotens in den Heap zur Priorität des aktuellen Weges.&lt;br /&gt;
&lt;br /&gt;
Die wichtigsten Prinzipien des Dijkstra-Algorithmus noch einmal im Überblick:&lt;br /&gt;
* Der Dijkstra-Algorithmus ist Breitensuche mit Prioritätswarteschlange (Heap) statt einer einfache Warteschlange (Queue).&lt;br /&gt;
* Die Prioritätswarteschlange speichert alle Wege, die bereits gefunden worden sind und ordnet sie aufsteigend nach ihrer Länge. &lt;br /&gt;
* Das Sortieren (und damit der ganze Algorithmus) funktioniert nur mit positiven Kantengewichten korrekt.&lt;br /&gt;
* Da ein Knoten auf mehreren Wegen erreichbar sein kann, kann er auch mehrmals im Heap sein. &lt;br /&gt;
* Wenn ein Knoten &amp;lt;i&amp;gt;erstmals&amp;lt;/i&amp;gt; aus der Prioritätswarteschlange entnommen wird, ist der gefundene Weg der kürzeste zu diesem Knoten. Andernfalls wird der Weg ignoriert.&lt;br /&gt;
* Wenn der Knoten &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; aus dem Heap entnommen wird, ist der kürzeste Weg von Start nach Ziel gefunden, und die Suche kann beendet werden.&lt;br /&gt;
In unserer Implementation können, wie gesagt, mehrere Wege zum selben Knoten gleichzeitig in der Prioritätswarteschlange sein. Im Prinzip wäre es auch möglich, immer nur den besten zur Zeit bekannten Weg zu jedem Enknoten in der Prioritätswarteschlange zu halten - sobald ein besserer Kandidat gefunden wird, ersetzt er den bisherigen Kandidaten, anstatt zusätzlich eingefügt zu werden. Dies erfordert aber eine wesentlich kompliziertere Prioritätswarteschlange, die eine effiziente &amp;lt;tt&amp;gt;updatePriority&amp;lt;/tt&amp;gt;-Funktion anbietet, ohne dass dadurch eine signifikante Beschleunigung erreicht wird. Deshalb verfolgen wir diesen Ansatz nicht.&lt;br /&gt;
&lt;br /&gt;
==== Beispiel ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;under construction&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Image:Bsp.jpg]]&lt;br /&gt;
&lt;br /&gt;
==== Komplexität von Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
Zur Analyse der Komplexität nehmen wir an, dass der Graph V Knoten und E Kanten hat. Die Initialisierung der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; am Anfang der Funktion hat offensichtlich Komplexität O(V), weil Speicher für V Knoten allokiert wird. Der Code am Ende der Funktion, der aus der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; den Pfad extrahiert, hat ebenfalls die Komplexität O(V), weil der Pfad im ungünstigen Fall sämtliche Knoten des Graphen umfasst. Beides wird durch die Komplexität der Hauptschleife dominiert, zu deren Analyse wir den folgenden Codeausschnitt genauer anschauen wollen:&lt;br /&gt;
&lt;br /&gt;
      while len(q) &amp;gt; 0:&lt;br /&gt;
           ... # 1&lt;br /&gt;
           if parents[node] is not None: &lt;br /&gt;
               continue                  &lt;br /&gt;
           parents[node] = predecessor&lt;br /&gt;
           ... # 2&lt;br /&gt;
Wir erkennen, dass der Codeabschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; für jeden Knoten höchstens einmal erreicht werden kann: Da &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; beim ersten Durchlauf gesetzt wird, kann die &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage beim gleichen Knoten nie wieder &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt; liefern, und das nachfolgende &amp;lt;tt&amp;gt;continue&amp;lt;/tt&amp;gt; bewirkt, dass der Abschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; dann übersprungen wird. Man sagt auch, dass jeder Knoten &amp;lt;i&amp;gt;höchstens einmal expandiert&amp;lt;/i&amp;gt; wird, auch wenn er mehrmals im Heap war. &lt;br /&gt;
&lt;br /&gt;
Der Codeabschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; selbst enthält eine Schleife über alle ausgehenden Kanten des Knotens &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt;. Im ungünstigsten Fall iterieren wir bei &amp;lt;i&amp;gt;allen&amp;lt;/i&amp;gt; Knoten über &amp;lt;i&amp;gt;alle&amp;lt;/i&amp;gt; ausgehenden Kanten, aber das sind gerade alle Kanten des Graphen je einmal in den beiden möglichen Richtungen. Die Funktion &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; wird sogar höchstens E Mal aufgerufen, weil eine Kante nur in den Heap eingefügt wird, wenn der kürzeste Weg der jeweiligen Endknotens noch nicht bekannt ist (siehe die &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage in der &amp;lt;tt&amp;gt;for&amp;lt;/tt&amp;gt;-Schleife), und das ist nur ein einer Richtung möglich. Dies hat zwei Konsequenzen:&lt;br /&gt;
* Die Schleife &amp;lt;tt&amp;gt;while len(q) &amp;gt; 0:&amp;lt;/tt&amp;gt; wird nur so oft ausgeführt, wie Elemente im Heap sind, also höchstens E Mal. Das gleiche gilt für den Codeabschnitt &amp;lt;tt&amp;gt;# 1&amp;lt;/tt&amp;gt;, der das &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; enthält.&lt;br /&gt;
* Die Operationen &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; haben logarithmische Komplexität in der Größe des Heaps, sind also in &amp;lt;math&amp;gt;O(\log\,E)&amp;lt;/math&amp;gt;. In einfachen Graphen gilt aber &amp;lt;math&amp;gt;E = O(V^2)&amp;lt;/math&amp;gt;, so dass sich die Komplexität der Heapoperationen vereinfacht zu &amp;lt;math&amp;gt;O(\log\,E)=O(\log\,V^2)=O(2\log\,V)=O(\log\,V)&amp;lt;/math&amp;gt;.&lt;br /&gt;
Zusammenfassend gilt: &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; werden maximal E Mal aufgerufen und haben eine Komplexität in &amp;lt;math&amp;gt;O(\log\,V)&amp;lt;/math&amp;gt;. Folglich hat der Algorithmus von Dijkstra die Komplexität:&lt;br /&gt;
:&amp;lt;math&amp;gt;O(E\,\log\,V)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Vergleich mit Breitensuche und Tiefensuche ====&lt;br /&gt;
&lt;br /&gt;
Der Dijkstra-Algorithmus ist eng mit der Breiten- und Tiefensuche verwandt - man kann diese Algorithmen aus dem Dijkstra-Algorithmus gewinnen, indem man einfach die Regel zur Festlegung der Prioritäten ändert. Anstelle der Länge des Pfades verwenden wir als Priorität den Wert eine Zählvariable &amp;lt;tt&amp;gt;count&amp;lt;/tt&amp;gt;, die nach jeder Einfügung in den Heap (also nach jedem Aufruf von &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt;) aktualisiert wird. Zählen wir die Variable hoch, haben die zuerst eingefügten Kanten die höchste Priorität, der Heap verhält sich also wie eine Queue (First in-First out), und wir erhalten eine Breitensuche. Zählen wir die Variable hingegen (von E beginnend) herunter, haben die zuletzt eingefügten Kanten höchste Priorität. Der Heap verhält sich dann wie ein Stack (Last in-First out), und wir bekommen Tiefensuche. Statt eines Heaps plus Zählvariable kann man jetzt natürlich direkt eine Queue bzw. einen Stack verwenden. Dadurch fällt der Aufwand &amp;lt;math&amp;gt;O(\log\,V)&amp;lt;/math&amp;gt; für die Heapoperationen weg und wird durch die effizienten O(1)-Operationen von Queue bzw. Stack ersetzt. Damit erhalten wir für Breiten- und Tiefensuche die schon bekannte Komplexität O(E).&lt;br /&gt;
&lt;br /&gt;
==== Korrektheit von Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
Wir beweisen mittels vollständiger Induktion die Schleifen-Invariante: Falls &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; gesetzt (also ungleich &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;) ist, dann liefert das Zurückverfolgen des Weges von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; nach &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; den kürzesten Weg. &lt;br /&gt;
;Induktionsanfang: &amp;lt;tt&amp;gt;parents[startnode]&amp;lt;/tt&amp;gt; ist als einziges gesetzt. Zurückverfolgen liefert den trivialen Weg &amp;lt;tt&amp;gt;[startnode]&amp;lt;/tt&amp;gt;, der mit Länge 0 offensichtlich der kürzeste Pfad ist &amp;amp;rarr; die Bedingung ist erfüllt.&lt;br /&gt;
;Induktionsschritt: Wir zeigen mit einem indirektem Beweis, dass wir immer einen kürzesten Weg bekommen, wenn &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; gesetzt wird.&lt;br /&gt;
:Sei &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt; = &amp;lt;tt&amp;gt;{v | parents[v] is not None}&amp;lt;/tt&amp;gt; die Menge aller Knoten, von denen wir den kürzesten Weg schon kennen (Induktionsvoraussetzung), und &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; der Knoten, der sich gerade an der Spitze des Heaps befindet. Dann ist &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; der Vorgänger von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; im aktuellen Weg, und es muss &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt; gelten, weil die Nachbarn von &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; (und damit auch der aktuelle &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt;) erst in den Heap eingefügt werden, wenn der kürzeste Weg für &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; gefunden wurde. Man beachte auch, dass alle Knoten, die noch nicht in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt; enthalten sind, weiter vom Start entfernt sind als alle Knoten in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt;, weil alle neu in den Heap eingefügten Wege länger sind als der kürzeste Weg des jeweiligen Vorgängers. &lt;br /&gt;
:Der indirekte Beweis nimmt jetzt an, dass der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; nicht der kürzeste Weg ist. Dann muss es einen anderen, kürzeren Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; geben. Für den Vorgänger &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; in diesem Weg unterscheiden wir zwei Fälle:&lt;br /&gt;
:* &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt;: In diesem Fall ist die Länge des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; bereits bekannt, und dieser Weg ist in der Prioritätswarteschlange enthalten. Dann kann er aber nicht der kürzeste sein, denn an der Spitze der Warteschlange war nach Voraussetzung der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt;.&lt;br /&gt;
:* &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\notin S&amp;lt;/math&amp;gt;: Die Kosten des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; berechnen sich als &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) + weight[(x, node)]&amp;lt;/tt&amp;gt;, und die Kosten des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; sind analog &amp;lt;tt&amp;gt;Kosten(predecessor &amp;amp;rarr; startnode) + weight[(predecessor, node)]&amp;lt;/tt&amp;gt;. Aufgrund der Induktionsvoraussetzung gilt aber &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt;, und somit &amp;lt;tt&amp;gt;Kosten(predecessor &amp;amp;rarr; startnode) &amp;amp;lt; Kosten(x &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt;, weil &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; andernfalls vor &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; an der Spitze des Heaps gewesen wäre, was mit der Annahme &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\notin S&amp;lt;/math&amp;gt; unverträglich ist. Damit der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; trotzdem der kürzeste Weg sein kann, müsste &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) &amp;amp;lt; Kosten(node &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt; gelten, denn durch die Kante &amp;lt;tt&amp;gt;(x, node)&amp;lt;/tt&amp;gt; kommen ja noch Kosten hinzu. Das wäre aber nur möglich, wenn der Knoten &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; vor dem Knoten &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; an die Spitze des Heaps gelangt, im Widerspruch zur Annahme, dass &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; sich gerade an der Spitze des Heaps befindet. Somit kann die Behauptung, dass der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; der kürzeste Weg ist, nicht stimmen.&lt;br /&gt;
In beiden Fällen erhalten wir einen Widerspruch, und die Behauptung ist somit bewiesen. Da die Invariante insbesondere für den Weg zum Zielknoten &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; erfüllt ist, folgt daraus auch die Korrektheit des Algorithmus von Dijkstra.&lt;br /&gt;
&lt;br /&gt;
===  A*-Algorithmus - Wie kann man Dijkstra noch verbessern? ===&lt;br /&gt;
&lt;br /&gt;
Eine wichtige Eigenschaft des Dijkstra-Algorithmus ist, dass neben dem kürzesten Weg vom Start zum Ziel auch die kürzesten Wege zu allen Knoten berechnet werden, die näher am Startknoten liegen als das Ziel, obwohl uns diese Wege gar nicht interessieren. Sucht man beispielsweise in einem Graphen mit den Straßenverbindungen in Deutschland den kürzesten Weg von Frankfurt (Main) nach Dresden (ca. 460 km), werden auch die kürzesten Wege von Frankfurt nach Köln (190 km), Dortmund (220 km) und Stuttgart (210 km) und vielen anderen Städten gefunden. Aufgrund der geographischen Lage dieser Städte ist eigentlich von vornherein klar, dass sie mit dem kürzesten Weg nach Dresden nicht das geringste zu tun haben. Anders sieht es mit Erfurt (260 km) oder Suhl (210 km) aus - diese Städte liegen zwischen Frankfurt und Dresden und kommen deshalb als Zwischenstationen des gesuchten Weges in Frage.&lt;br /&gt;
&lt;br /&gt;
Damit Dijkstra korrekt funktioniert, würde es im Prinzip ausreichen, wenn man die kürzesten Wege nur für diejenigen Knoten ausrechnet, die auf dem kürzesten Weg vom Start zum Ziel liegen, denn nur diese Knoten braucht man, um den gesuchten Weg über die &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette zurückzuverfolgen. Das Problem ist nur, dass man diese Knoten erst kennt, wenn der Algorithmus fertig durchgelaufen ist. Schließt man Knoten zu früh von der Betrachtung aus, kommt am Ende möglicherweise nicht der korrekte kürzeste Weg heraus. &lt;br /&gt;
&lt;br /&gt;
Der A*-Algorithmus löst dieses Dilemma mit folgender Idee: Ändere die Prioritäten für den Heap so ab, dass unwichtige Knoten nur mit geringerer Wahscheinlichkeit expandiert werden, aber stelle gleichzeitig sicher, dass alle wichtigen Knoten (also diejenigen auf dem korrekten kürzesten Weg) auf jeden Fall expandiert werden. Es zeigt sich, dass man diese Idee umsetzen kann, wenn eine &amp;lt;i&amp;gt;Schätzung für den Restweg&amp;lt;/i&amp;gt; (also für die noch verbleibende Entfernung von jedem Knoten zum Ziel) verfügbar ist:&lt;br /&gt;
 rest = guess(neighbor, destination)&lt;br /&gt;
Diese Schätzung addiert man einfach zur wahren Länge des Weges &amp;lt;tt&amp;gt;startnode &amp;amp;rarr; node&amp;lt;/tt&amp;gt; dazu, um die verbesserte Priorität zu erhalten:&lt;br /&gt;
 priority = newLength + guess(neighbor, destination)&lt;br /&gt;
(Im originalen Dijkstra-Algorithmus wird als Priorität nur &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; allein verwendet. Man beachte, dass man &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; jetzt zusätzlich im Heap speichern muss, weil man es für die Expansion des Knotens später noch benötigt.)&lt;br /&gt;
&lt;br /&gt;
Damit sicher gestellt ist, dass der A*-Algorithmus immer noch die korrekten kürzesten Wege findet, darf die Schätzung den wahren Restweg &amp;lt;i&amp;gt;niemals überschätzen&amp;lt;/i&amp;gt;. Es muss immer gelten:&lt;br /&gt;
 0 &amp;lt;= guess(node, destination) &amp;lt;= trueDistance(node, destination)&lt;br /&gt;
Damit gilt insbesondere &amp;lt;tt&amp;gt;guess(destination, destination) = trueDistance(destination, destination) = 0&amp;lt;/tt&amp;gt;, an der Priorität des Knotens &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; ändert sich also nichts. Die Prioritäten aller anderen Knoten veschlechtern sich hingegen, weil zur bisherigen Priorität noch atwas addiert wird. Für die wichtigen Knoten auf dem kürzesten Weg vom Start nach Ziel gilt jedoch, dass deren neue Priorität immer noch besser ist als die Priorität des Zielknotens selbst. Für diese Knoten gilt nämlich&lt;br /&gt;
 falls node auf dem kürzesten Weg von startnode nach destination liegt:&lt;br /&gt;
 trueDistance(startnode, node) + guess(node, destination) &amp;lt;= trueDistance(startnode, destination)&lt;br /&gt;
weil der Weg von Start nach &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; ein Teil des kürzesten Wegs von Start nach Ziel ist und die Restschätzung die wahre Entfernung immer unterschätzt. Diese Knoten werden deshalb stets vor dem Zielknoten expandiert, so dass wir die &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette immer noch korrekt zurückverfolgen können. Für alle anderen Knoten gilt idealerweise, dass die neue Priorität schlechter ist als die Priorität von &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt;, so dass man sich diese irrelevanten Knotenexpansionen sparen kann.&lt;br /&gt;
&lt;br /&gt;
Für das Beispiel eines Straßennetzwerks bietet sich als Schätzung die Luftlinienentfernung an, weil Straßen nie kürzer sein können als die Luftlinie. Damit erreicht man in der Praxis deutliche Einsparungen. Generell gilt, dass der A*-Algorithmus im typischen Fall schneller ist als der Algorithmus von Dijkstra, aber man kann immer pathologische Fälle konstruieren, wo die Änderung der Prioritäten nichts bringt. Die Komplexität des A*-Algorithmus im ungünstigen Fall ist deshalb nach wie vor &amp;lt;math&amp;gt;O(E\,\log\,V)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=='''Minimaler Spannbaum'''==&lt;br /&gt;
'''(engl.: minimum spanning tree; abgekürzt: MST)'''&lt;br /&gt;
&lt;br /&gt;
[[Image:Minimum_spanning_tree.png‎ |thumb|200px|right|Ein minimal aufspannender Baum verbindet alle Punkte eines Graphen bei minimaler Kantenlänge ([http://de.wikipedia.org/wiki/Spannbaum Quelle])]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: gewichteter Graph G, zusammenhängend&amp;lt;br/&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: Untermenge &amp;lt;math&amp;gt;E'\subseteq E&amp;lt;/math&amp;gt; der Kanten, so dass die Summe der Kantengewichte &amp;lt;math&amp;gt;\sum_{e\in E'} w_e&amp;lt;/math&amp;gt; minimal und der entstehende Graph G' zusammenhängend ist.&amp;lt;br/&amp;gt;&lt;br /&gt;
* G' definiert immer einen Baum, denn andernfalls könnte man eine Kante weglassen und dadurch die Summe &amp;lt;math&amp;gt;\sum_{e\in E'} w_e&amp;lt;/math&amp;gt; verringern, ohne dass sich am Zusammenhang von G' etwas ändert. &amp;lt;br/&amp;gt;&lt;br /&gt;
* Wenn der Graph G nicht zusammenhängend ist, kann man den Spannbaum für jede Zusammenhangskomponente getrennt ausrechnen. Man erhält dann einen aufspannenden Wald. &lt;br /&gt;
* Der MST ist ähnlich wie der Dijkstra-Algorithmus: Dort ist ein Pfad gesucht, bei dem die Summe der Gewichte über den Pfad minimal ist. Beim MST suchen wir eine Lösung, bei der die Summe der Gewichte über den ganzen Graphen minimal ist. &lt;br /&gt;
* Das Problem des MST ist nahe verwandt mit der Bestimmung der Zusammenhangskomponente, z.B. über den Tiefensuchbaum. Für die Zusammenhangskomponenten genügt allerdings ein beliebiger Baum, während beim MST ein minimaler Baum gesucht ist.&lt;br /&gt;
&lt;br /&gt;
=== Anwendungen ===&lt;br /&gt;
==== Wie verbindet man n gegebene Punkte mit möglichst kurzen Straßen (Eisenbahnen, Drähten [bei Schaltungen] usw.)?====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:center&amp;quot; border=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; cellspacing=&amp;quot;0&amp;quot; &lt;br /&gt;
|MST minimale Verbindung (Abb.1)&lt;br /&gt;
|MST = 2 (Länge = Kantengewicht)(Abb.2)&lt;br /&gt;
|- valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| [[Image:mst.png]] &lt;br /&gt;
| [[Image:Gleichseitigesdreieck.png]]&lt;br /&gt;
|}&lt;br /&gt;
*In der Praxis: Die Festlegung, dass man nur die gegebenen Punkte verwenden darf, ist eine ziemliche starke Einschränkung. &lt;br /&gt;
&lt;br /&gt;
* Wenn man sich vorstellt, es sind drei Punkte gegeben, die als gleichseitiges Dreieck angeordnet sind, dann ist der MST (siehe Abb.2, schwarz gezeichnet) und hat die Länge 2. Man kann hier die Länge als Kantengewicht verwenden. &lt;br /&gt;
&lt;br /&gt;
* Wenn es erlaubt ist zusätzliche Punkte einzufügen, dann kann man in der Mitte einen neuen Punkt setzen &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; neuer MST (siehe Abb.2, orange gezeichnet).&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Höhe = &amp;lt;math&amp;gt;\frac{1}{2}\sqrt{3}&amp;lt;/math&amp;gt;, Schwerpunkt: teilt die Höhe des Dreiecks im Verhältnis 2:1; der Abstand von obersten Punkt bis zum neu eingeführten Punkt: &amp;lt;math&amp;gt;\frac{2}{3}h = \frac{\sqrt{3}}{3}&amp;lt;/math&amp;gt;, davon insgesamt 3 Stück, damit (gilt für den MST in orange eingezeichnet): MST = &amp;lt;math&amp;gt;3\left(\frac{1}{3}\right) \sqrt{3} = \sqrt{3} \approx 1,7&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Damit ist der MST in orange kürzer als der schwarz gezeichnete MST. &amp;lt;br\&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt;Folgerung: MST kann kürzer werden, wenn man einen Punkt dazu nimmt. &lt;br /&gt;
* Umgekehrt kann der MST auch kürzer werden, wenn man einen Punkt aus dem Graphen entfernt, aber wie das Beipiel des gleichseitigen Dreiecks zeigt, ist dies nicht immer der Fall.&lt;br /&gt;
&lt;br /&gt;
[[Image: bahn.png|Bahnstrecke Verbindung (Abb.3)]]&lt;br /&gt;
&lt;br /&gt;
* Methode der zusätzlichen Punkteinfügung hat man früher beim Bahnstreckenbau verwendet. Durch Einführung eines Knotenpunktes kann die Streckenlänge verkürzt werden (Dreiecksungleichung).&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Bestimmung von Datenclustern ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Image:cluster.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Daten (in der Abb.: Punkte) bilden Gruppen. &lt;br /&gt;
&lt;br /&gt;
* In der Abbildung hat man 2 verschiedene Messungen gemacht (als x- und y-Achse aufgetragen), bspw. Größe und Gewicht von Personen. Für jede Person i wird ein Punkt an der Koordinate (Größe&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;, Gewicht&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;) gezeichnet (siehe Bild a). Dies bezeichnet man als ''Scatter Plot''. Wenn bestimmte Wertkombinationen häufiger auftreten als andere, bilden sich mitunter Gruppen aus, bspw. eine Gruppe für &amp;quot;klein und schwer&amp;quot; etc.&lt;br /&gt;
&lt;br /&gt;
* Durch Verbinden der Punkte mittels eines MST (siehe Abbildung (b)) sieht man, dass es kurze (innerhalb der Gruppen) und lange Kanten (zwischen den Gruppen) gibt. &lt;br /&gt;
&lt;br /&gt;
* Wenn man geschickt eine Schwelle einführt und alle Kanten löscht, die länger sind als die Schwelle, dann bekommt man als Zusammenhangskomponente die einzelnen Gruppen. &lt;br /&gt;
&lt;br /&gt;
=== Algorithmen ===&lt;br /&gt;
&lt;br /&gt;
Genau wie bei der Bestimmung von Zusammenhangskomponenten kann man auch das MST-Problem entweder nach dem Anlagerungsprinzip oder nach dem Verschmelzungsprinzip lösen (dazu gibt es noch weitere Möglichkeiten, z.B. den [http://de.wikipedia.org/wiki/Algorithmus_von_Bor%C5%AFvka Algorithmus von Boruvka]). Der Anlagerungsalgorithmus für MST wurde zuerst von Prim beschrieben und trägt deshalb seinen Namen, der Verschmelzungsalgorithmus stammt von Kruskal. Im Vergleich zu den Algorithmen für Zusammenhangskomponenten ändert sich im wesentlichen nur die Reihenfolge, in der die Kanten betrachtet werden: Eine Prioritätswarteschlange stellt jetzt sicher, dass am Ende wirklich der Baum mit den geringstmöglichen Kosten herauskommt.&lt;br /&gt;
&lt;br /&gt;
====Algorithmus von Prim====&lt;br /&gt;
[http://de.wikipedia.org/wiki/Algorithmus_von_Prim Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus von Prim geht nach dem Anlagerungsprinzip vor (vgl. den Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Tiefensuche|Zusammenhangskomponenten mit Tiefensuche]]): Starte an der Wurzel (ein willkürlich gewählter Knoten) und füge jeweils die günstigste Kante an die aktuellen Teillösung an, die keinen Zyklus verursacht. Die Sortierung der Kanten nach Priorität erfolgt analog zum Dijsktra-Algorithmus, aber die Definitionen, welche Kante die günstigste ist, unterscheiden sich. Die Konvention für die Bedeutung der Elemente des Heaps ist ebenfalls identisch: ein Tupel mit &amp;lt;tt&amp;gt;(priority, node, predecessor)&amp;lt;/tt&amp;gt;. Die folgende Implementation verdeutlicht sehr schön die Ähnlichkeit der beiden Algorithmen. Das Ergebnis wird als property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; zurückgegeben, in der für jeden Knoten sein Vorgänger im MST steht, wobei die Wurzel wie üblich auf sich selbst verweist.&lt;br /&gt;
&lt;br /&gt;
 import heapq&lt;br /&gt;
 &lt;br /&gt;
 def prim(graph, weights):                # Kantengewichte wie bei Dijkstra als property map&lt;br /&gt;
 	sum = 0.0                         # wird später das Gewicht des Spannbaums sein&lt;br /&gt;
 	start = 0                         # Knoten 0 wird willkürlich als Wurzel gewählt&lt;br /&gt;
        &lt;br /&gt;
        parents = [None]*len(graph)       # property map, die den resultierenden Baum kodiert&lt;br /&gt;
        parents[start] = start            # Wurzel zeigt auf sich selbst&lt;br /&gt;
        &lt;br /&gt;
 	heap = []                         # Heap für die Kanten des Graphen&lt;br /&gt;
 	for neighbor in graph[start]:     # besuche die Nachbarn von start&lt;br /&gt;
 	    heapq.heappush(heap, (weights[(start, neighbor)], neighbor, start))  # und fülle Heap &lt;br /&gt;
 	&lt;br /&gt;
        while len(heap) &amp;gt; 0:&lt;br /&gt;
 	    w, node, predecessor = heapq.heappop(heap) # hole billigste Kante aus dem Heap&lt;br /&gt;
            if parents[node] is not None: # die Kante würde einen Zyklus verursachen&lt;br /&gt;
                continue                  #   =&amp;gt; ignoriere diese Kante&lt;br /&gt;
            parents[node] = predecessor   # füge Kante in den MST ein&lt;br /&gt;
            sum += w                      # und aktualisiere das Gesamtgewicht &lt;br /&gt;
            for neighbor in graph[node]:  # besuche die Nachbarn von node&lt;br /&gt;
                if parents[neighbor] is None:  # aber nur, wenn kein Zyklus entsteht&lt;br /&gt;
                    heapq.heappush(heap, (weights[(node,neighbor)], neighbor, node)) # füge Kandidaten in Heap ein&lt;br /&gt;
        &lt;br /&gt;
 	return parents, sum               # MST und Gesamtgewicht zurückgeben&lt;br /&gt;
&lt;br /&gt;
====Algorithmus von Kruskal====&lt;br /&gt;
[http://de.wikipedia.org/wiki/Algorithmus_von_Kruskal Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Kruskal%27s_algorithm (en)]&lt;br /&gt;
&lt;br /&gt;
Die alternative Vorgehensweise ist das Verschmelzungsprinzip (vgl. den Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Union-Find-Algorithmus|Zusammenhangskomponenten mit Union-Find-Algorithmus]]), das der Algorithmus von Kruskal verwendet. Jeder Knoten wird zunächst als trivialer Baum mit nur einem Knoten betrachtet, und alle Kanten werden aufsteigend nach Gewicht sortiert. Dann wird die billigste noch nicht betrachtete Kante in den MST eingefügt, falls sich dadurch kein Zyklus bildet (erkennbar daran, dass die Endknoten in verschiedenen Zusammenhangskomponenten liegen, das heisst verschiedene Anker haben). Da der fertige Baum (V-1) Kanten haben muss, wird dies (V-1) Mal zutreffen. Andernfalls wird diese Kante ignoriert. Anders ausgedrückt: Der Algorithmus beginnt mit ''V'' Bäumen; in (''V''-1) Verschmelzungsschritten kombiniert er jeweils zwei Bäume (unter Verwendung der kürzesten möglichen Kante), bis nur noch ein Baum übrig bleibt. Der einzige Unterschied zum einfachen Union-Find besteht darin, dass die Kanten in aufsteigender Reihenfolge betrachtet werden müssen, was wir hier durch eine Prioritätswarteschlange realisieren. Der Algorithmus von J.Kruskal ist seit 1956 bekannt. &lt;br /&gt;
&lt;br /&gt;
 def kruskal(graph, weights):&lt;br /&gt;
     anchors = range(len(graph))           # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     results = []                          # result wird später die Kanten des MST enthalten    &lt;br /&gt;
     &lt;br /&gt;
     heap = []                             # Heap zum Sortieren der Kanten nach Gewicht&lt;br /&gt;
     for edge, w in weights.iteritems():   # alle Kanten einfügen&lt;br /&gt;
         heapq.heappush(heap, (w, edge))&lt;br /&gt;
     &lt;br /&gt;
     while len(heap) &amp;gt; 0:                  # solange noch Kanten vorhanden sind&lt;br /&gt;
         w, edge = heapq.heappop(heap)     # billigste Kante aus dem Heap nehmen&lt;br /&gt;
         a1 = findAnchor(anchors, edge[0]) # Anker von Startknoten der Kante&lt;br /&gt;
         a2 = findAnchor(anchors, edge[1]) # ... und Endknoten bestimmen&lt;br /&gt;
         if a1 != a2:                      # wenn die Knoten in verschiedenen Komponenten sind&lt;br /&gt;
             anchors[a2] = a1              # Komponenten verschmelzen&lt;br /&gt;
             result.append(edge)           # ... und Kante in MST einfügen&lt;br /&gt;
     &lt;br /&gt;
     return result                         # Kanten des MST zurückgeben&lt;br /&gt;
&lt;br /&gt;
Die Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt; wurde im Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Union-Find-Algorithmus|Zusammenhangskomponenten mit Union-Find-Algorithmus]] implementiert. Im Unterschied zum Algorithmus von Prim geben wir hier nicht die property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; zurück, sondern einfach eine Liste der Kanten im MST.&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus eignet sich insbesondere für das Clusteringproblem, da der Schwellwert von vornerein als maximales Kantengewicht an den Algorithmus übergeben werden kann. Man hört mit dem Vereinigen auf, wenn das Gewicht der billigste Kante im Heap den Schwellwert überschreitet. Beim Algorithmus von Kruskal kann dann keine bessere Kante als der Schwellwert mehr kommen, da die Kanten vorher sortiert worden sind. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Komplexität:&amp;lt;/b&amp;gt; wie beim Dijkstra-Algorithmus, weil jede Kante genau einmal in den Heap kommt. Der Aufwand für das Sortieren ist somit &amp;lt;math&amp;gt;O\left(E\log E\right)&amp;lt;/math&amp;gt;, was sich zu &amp;lt;math&amp;gt;O \left(E\,\log\,V\right)&amp;lt;/math&amp;gt; reduziert, falls keine Mehrfachkanten vorhanden sind.&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; geeignet für Übungsaufgabe&lt;br /&gt;
&lt;br /&gt;
====Verwendung einer BucketPriorityQueue====&lt;br /&gt;
&lt;br /&gt;
Beide Algorithmen zur Bestimmung des minimalen Spannbaums benötigen eine Prioritätswarteschlange. Wenn die Kantengewichte ganze Zahlen im Bereich &amp;lt;tt&amp;gt;0...(m-1)&amp;lt;/tt&amp;gt; sind, kann man die MST-Algorithmen deutlich beschleunigen, wenn man anstelle des Heaps eine [[Prioritätswarteschlangen#Prioritätssuche mit dem Bucket-Prinzip|&amp;lt;tt&amp;gt;BucketPriorityQueue&amp;lt;/tt&amp;gt;]] verwendet. Die Operationen zum Einfügen einer Kante in die Queue und zum Entfernen der billibsten Kante aus der Queue beschleunigen sich dadurch auf O(1) statt O(log V) (außer wenn die Gewichte sehr ungünstig auf die Kanten verteilt sind). In der Praxis erreicht man durch diese Änderung typischerweise deutliche Verbesserungen. In der Bildverarbeitung können die Prioritäten beispielsweise die Wahrscheinlichkeit kodieren, dass zwei benachbarte Pixel zu verschiedenen Objekten gehören. Bildet man jetzt den MST, und bricht bei einer bestimmten Wahrscheinlichkeit ab, erhält man Cluster von Pixeln, die wahrscheinlich zum selben Objekt gehören (weil der MST ja die Kanten mit minimalem Gewicht bevorzugt, und kleine Gewichte bedeuten kleine Wahrscheinlichkeit, dass benachbarte Pixel von einander getrennt werden). Da man die Wahrscheinlichkeiten nur mit einer Genauigkeit von ca. 1% berechnen kann, reichen hiefür 100 bis 200 Quantisierungstufen aus. Durch Verwendung der schnellen &amp;lt;tt&amp;gt;BucketPriorityQueue&amp;lt;/tt&amp;gt; kann man jetzt wesentlich größere Bilder in akzeptabler Zeit bearbeiten als dies mit einem Heap möglich wäre.&lt;br /&gt;
&lt;br /&gt;
== Algorithmen für gerichtete Graphen ==&lt;br /&gt;
&lt;br /&gt;
Zur Erinnerung: in einem gerichteten Graphen sind die Kanten (i &amp;amp;rarr; j) und (j &amp;amp;rarr; i) voneinander verschieden, und eventuell existiert nur eine der beiden Richtungen. Im allgemeinen unterscheidet sich der [[Graphen_und_Graphenalgorithmen#transposed_graph|transponierte Graph]] G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; also vom Originalgraphen G. Beim Traversieren des Graphen und bei der Pfadsuche dürfen Kanten nur in passender Richtung verwendet werden. Bei gewichteten Graphen tritt häufig der Fall auf, dass zwar Kanten in beiden Richtungen existieren, diese aber unterschiedliche Gewichte haben.&lt;br /&gt;
&lt;br /&gt;
Gerichtete Graphen ergeben sich in natürlicher Weise aus vielen Anwendungsproblemen:&lt;br /&gt;
* Routenplanung&lt;br /&gt;
** Bei Straßennetzwerken enstehen gerichtete Graphen, sobald es Einbahnstraßen gibt.&lt;br /&gt;
** Verwendet man Gewichte, um die erwarteten Fahrzeiten entlang einer Straße zu kodieren, gibt es Asymmetrien z.B. dann, wenn Straßen in einer Richtung bergab, in der anderen bergauf befahren werden. Hier existieren zwar Kanten in beiden Richtungen, sie haben aber unterschiedliche Gewichte. Ähnliches gilt für Flüge: Durch den Gegenwind des Jetstreams braucht man von Frankfurt nach New York länger als umgekehrt von New York nach Frankfurt.&lt;br /&gt;
* zeitliche oder kausale Abhängigkeiten&lt;br /&gt;
** Wenn die Knoten Ereignisse repräsentieren, von denen einige die Ursache von anderen sind, diese wiederum die Ursache der nächsten usw., verbindet man die Knoten zweckmäßig durch gerichtete Kanten, die die Kausalitätsbeziehungen kodieren. Handelt es sich um logische &amp;quot;wenn-dann&amp;quot;-Regeln, erhält man einen [[Graphen_und_Graphenalgorithmen#Anwendung:_Das_Erf.C3.BCllbarkeitsproblem_in_Implikationengraphen|Implikationengraph]] (siehe unten). Handelt es sich hingegen um Wahrscheinlichkeitsaussagen (&amp;quot;Wenn das Wetter schön ist, haben Studenten tendenziell gute Laune, wenn eine Prüfung bevorsteht eher schlechte usw.&amp;quot;), erhält man ein [http://de.wikipedia.org/wiki/Bayessches_Netz Bayessches Netz].&lt;br /&gt;
** Wenn bestimmte Aufgaben erst begonnen werden können, nachdem andere Aufgaben erledigt sind, erhält man einen Abhängigkeitsgraphen. Beispielsweise dürfen Sie erst an der Klausur teilnehmen, nachdem Sie die Übungsaufgaben gelöst haben, und Sie dürfen erst die Abschlussarbeit beginnen, nachdem Sie bestimmte Prüfungen bestanden haben. Ein anderes schönes Beispiel liefern die Regeln für das [[Graphen_und_Graphenalgorithmen#Anwendung:_Abh.C3.A4ngigkeitsgraph|Ankleiden]] weiter unten.&lt;br /&gt;
** Gerichtete Graphen kodieren die Abhängigkeiten zwischen Programmbibliotheken. Beispielsweise benötigt das Pythonmodul &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; die internen Submodule &amp;lt;tt&amp;gt;json.encoder&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;json.decode&amp;lt;/tt&amp;gt; sowie das externe Modul &amp;lt;tt&amp;gt;decimal&amp;lt;/tt&amp;gt;. Die Submodule benötigen wiederum die externen Module &amp;lt;tt&amp;gt;re&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;sys&amp;lt;/tt&amp;gt;, das Modul &amp;lt;tt&amp;gt;decimal&amp;lt;/tt&amp;gt; braucht &amp;lt;tt&amp;gt;copy&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;collections&amp;lt;/tt&amp;gt; usw.&lt;br /&gt;
** Das Internet kann als gerichteter Graph dargestellt werden, wobei die Webseiten die Knoten, und die Hyperlinks die Kanten sind.&lt;br /&gt;
* Sequence Alignment&lt;br /&gt;
** Eine gute Rechtschreibprüfung markiert nicht nur fehlerhafte Wörter, sondern macht auch plausible Vorschläge, was eigentlich gemeint gewesen sein könnte. Dazu muss sie das gegebene Wort mit den Wörtern eines Wörterbuchs vergleichen und die Ähnlichkeit bewerten. Ein analoges Problem ergibt sich, wenn man DNA Fragmente mit der Information in einer Genomdatenbank abgleichen will. &lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Sequence Alignment / Edit Distance ===&lt;br /&gt;
&lt;br /&gt;
:gegeben: zwei Wörter (allgemein: beliebige Zeichenfolgen)&lt;br /&gt;
:gesucht: Wie kann man die Buchstaben am besten in Übereinstimmung bringen?&lt;br /&gt;
&lt;br /&gt;
:Beispiel: WORTE – NORDEN&lt;br /&gt;
&lt;br /&gt;
Zwei mögliche Alignments sind&lt;br /&gt;
&lt;br /&gt;
  W&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;OR&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;T&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;E&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;.          W.ORTE&lt;br /&gt;
  N&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;OR&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;D&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;E&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;N          NORDEN&lt;br /&gt;
&lt;br /&gt;
wobei der Punkt anzeigt, dass der untere Buchstabe keinen Partner hat, und rote Buchstaben oben und unten übereinstimmen. Jede Nicht-Übereinstimmung verursacht nun gewisse Kosten. Dabei unterscheiden wir zwei Fälle:&lt;br /&gt;
# Matche a[i] mit b[j]. Falls a[i] == b[j], ist das gut (rote Buchstaben), und es entstehen keine Kosten. Andernfalls entstehen Kosten U (schwarze Buchstaben).&lt;br /&gt;
# Wir überspringen a[i] oder b[j] (Buchstabe vs. Punkt). Dann entstehen Kosten V. (Manchmal unterscheidet man auch noch Kosten Va und Vb, wenn das Überspringen bei a und b unterschieldiche Signifikanz hat.)&lt;br /&gt;
&lt;br /&gt;
Gesucht ist nun das &amp;lt;b&amp;gt;Alignment mit minimalen Kosten&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Diese Aufgabe kann man sehr schön als gerichteten Graphen darstellen: Wir definieren ein rechteckiges Gitter und schreiben das erste Wort über das Gitter und das andere links davon. Die Gitterpunkte verbinden wir mit Pfeilen (gerichteten Kanten), wobei ein Pfeil nach rechts bedeutet, dass wir beim oberen Wort einen Buchstaben überspringen, ein Pfeil nach unten, dass wir beim linken Wort einen Buchstaben überspringen, und ein diagonaler Pfeil, dass wir zwei Buchstaben matchen (und zwar die am Pfeilende). Die Farben der Pfeile symbolisieren die Kosten: rot für das Überspringen eines Buchstabens (Kosten V), blau für das Matchen, wenn die Buchstaben nicht übereinstimmen (Kosten U), und grün, wenn die Buchstaben übereinstimmen (keine Kosten). &lt;br /&gt;
&lt;br /&gt;
[[Image:sequence-alignment.png|300px]]&lt;br /&gt;
&lt;br /&gt;
Lösung:&lt;br /&gt;
:Suche den kürzesten Pfad vom Knoten &amp;quot;START&amp;quot; (oben links) nach unten rechts. Dazu kann der [[Graphen und Graphenalgorithmen#Algorithmus von Dijkstra|Algorithmus von Dijkstra]] verwendet werden, der auf gerichteten Graphen genauso funktioniert wie auf ungerichteten.&lt;br /&gt;
&lt;br /&gt;
Für unser Beispiel von oben erhalten wir die folgenden Pfade:&lt;br /&gt;
&lt;br /&gt;
[[Image:sequence-alignment-weg1.png|400px]]&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;[[Image:sequence-alignment-weg2.png|400px]]&lt;br /&gt;
&lt;br /&gt;
Durch Addieren der Kosten entsprechend der Farben sieht man, dass der erste Weg die Kosten 2U+V und der zweite die Kosten 5U+V hat. Der erste Weg ist offensichtlich günstiger und entspricht dem besten Alignment.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Abhängigkeitsgraph ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Beispiel: &amp;lt;/b&amp;gt; Wie erklärt man einem zerstreuten Professor, wie er sich morgens anziehen soll? Der folgende Graph enthält einen Knoten für jede Aktion, und eine Kante (i &amp;amp;rarr; j) bedeutet, dass die Aktion i vor der Aktion j abgeschlossen werden muss.&lt;br /&gt;
&lt;br /&gt;
[[Image:anziehen-graph.png|600px]]&lt;br /&gt;
&lt;br /&gt;
In derartigen Abhängigkeitsgraphen ist die wichtigste Frage immer, ob der Graph azyklisch ist. Wäre dies nämlich nicht der Fall, kann es keine Reihenfolge der Aktionen geben, die alle Abhängigkeiten erfüllt. Dies sieht man leicht, wenn man den einfachsten möglichen Zyklus betrachtet: es gibt sowohl eine Kante (i &amp;amp;rarr; j) als auch eine (j &amp;amp;rarr; i). Dann müsste man i vor j erledigen, aber ebenso j vor i, was offensichtlich unmöglich ist - das im Graph kodierte Problem ist dann unlösbar. Wegen ihrer Wichtigkeit wird für gerichtete azyklische Graphen oft die Abkürzung &amp;lt;b&amp;gt;DAG&amp;lt;/b&amp;gt; (von &amp;lt;i&amp;gt;directed acyclic graph&amp;lt;/i&amp;gt;) verwendet. Ein Graph ist genau dann ein DAG, wenn es eine topologische Sortierung gibt:&lt;br /&gt;
;topologische Sortierung: Zeichne die Knoten so auf eine Gerade, dass alle Kanten (Pfeile) nach rechts zeigen. &lt;br /&gt;
Arbeitet man die Aktionen nach einer (beliebigen) topologischen Sortierung ab, werden automatisch alle Abhängigkeiten eingehalten: Da alle Pfeile nach rechts zeigen, werden abhängige Aktionen immer später ausgeführt. Die topologische Sortierung ist im allgemeinen nicht eindeutig. Die folgende Skizze zeigt eine mögliche topologische Sortierung für das Anziehen:&lt;br /&gt;
&lt;br /&gt;
[[Image:anziehen-topologische-sortierung.png|600px]]&lt;br /&gt;
&lt;br /&gt;
Eine solche fest vorgegebene Reihenfolge ist für den zerstreuten Professor sicherlich eine größere Hilfe als der ursprüngliche Graph. Man erkennt, dass die Sortierung nicht eindeutig ist, beispielsweise bei der Uhr: Da für die Uhr keine Abhängigkeiten definiert sind, kann man diese Aktion an beliebiger Stelle einsortieren. Hier wurde willkürlich die letzte Stelle gewählt.&lt;br /&gt;
&lt;br /&gt;
==== Zwei Algorithmen zum Finden der topologischen Sortierung ====&lt;br /&gt;
&lt;br /&gt;
Die folgenden Algorithmen finden entweder eine topologische Sortierung, oder signalisieren, dass der Graph zyklisch ist.&lt;br /&gt;
&lt;br /&gt;
===== Algorithmus 1 =====&lt;br /&gt;
# Suche einen Knoten mit Eingangsgrad 0 (ohne eingehende Pfeile) =&amp;gt; in einem gerichteten azyklischen Graphen gibt es immer einen solchen Knoten&lt;br /&gt;
# Platziere diesen Knoten auf der Geraden (beliebig)&lt;br /&gt;
# Entferne den Knoten aus dem Graphen zusammen mit den ausgehenden Kanten&lt;br /&gt;
# Gehe zu 1., aber platziere in 2. immer rechts der Knoten, die schon auf der Geraden vorhanden sind.&lt;br /&gt;
: =&amp;gt; Wenn noch Knoten übrig sind, aber keiner Eingangsgrad 0 hat, muss der Graph zyklisch sein.&lt;br /&gt;
&lt;br /&gt;
[[Image:bild6.JPG]]&lt;br /&gt;
&lt;br /&gt;
Beispiel für einen zyklischen Graphen: kein Knoten hat Eingangsgrad 0.&lt;br /&gt;
&lt;br /&gt;
Um den Algorithmus zu implementieren, verwenden wir eine property map &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt;, die wir in einem ersten Durchlauf durch den Graphen füllen und die dann für jeden Knoten die Anzahl der eingehenden Kanten speichert. Dann gehen wir sukzessive zu allen Knoten mit &amp;lt;tt&amp;gt;in_degree == 0&amp;lt;/tt&amp;gt;. Anstatt sie aber tatsächlich aus dem Graphen zu entfernen wie im obigen Pseudocode, dekrementieren wir nur den &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt; ihrer Nachbarn. Wird der &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt; eines Nachbarn dadurch 0, wird er ebenfalls in das Array der zu scannenden Knoten aufgenommen. Wenn der Graph azyklisch ist, enthält das Array am Ende alle Knoten des Graphen, und die Reihenfolge der Einfügungen definiert eine topologische Sortierung. Andernfalls ist das Array zu kurz, und wir signalisieren durch Zurückgeben von &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, dass der Graph zyklisch ist:&lt;br /&gt;
&lt;br /&gt;
 def topological_sort(graph):              # ein gerichteter Graph&lt;br /&gt;
     in_degree = [0]*len(graph)            # property map für den Eingangsgrad jeden Knotens&lt;br /&gt;
     for node in xrange(len(graph)):       # besuche alle Knoten&lt;br /&gt;
         for neighbor in graph[node]:      #  ... und deren Nachbarn&lt;br /&gt;
             in_degree[neighbor] += 1      #  ... und inkrementiere den Eingangsgrad&lt;br /&gt;
     &lt;br /&gt;
     result = []                           # wird später die topologische Sortierung enthalten&lt;br /&gt;
     for node in xrange(len(graph)):&lt;br /&gt;
         if in_degree[node] == 0:&lt;br /&gt;
             result.append(node)           # füge alle Knoten mit Eingangsgrad 0 in result ein&lt;br /&gt;
     &lt;br /&gt;
     k = 0&lt;br /&gt;
     while k &amp;lt; len(result):                # besuche alle Knoten mit Eingangsgrad 0&lt;br /&gt;
         node = result[k]&lt;br /&gt;
         k += 1&lt;br /&gt;
         for neighbor in graph[node]:      # besuche alle Nachbarn&lt;br /&gt;
             in_degree[neighbor] -= 1      # entferne 'virtuell' die eingehende Kante&lt;br /&gt;
             if in_degree[neighbor] == 0:  # wenn neighbor jetzt Eingangsgrad 0 hat&lt;br /&gt;
                 result.append(neighbor)   #  ... füge ihn in result ein&lt;br /&gt;
     &lt;br /&gt;
     if len(result) == len(graph):         # wenn alle Knoten jetzt Eingangsgrad 0 haben&lt;br /&gt;
         return result                     # ... ist result eine topologische Sortierung&lt;br /&gt;
     else:&lt;br /&gt;
         return None                       # andernfalls ist der Graph zyklisch&lt;br /&gt;
&lt;br /&gt;
===== Algorithmus 2 =====&lt;br /&gt;
Der obige Algorithmus hat den Nachteil, dass er jeden Knoten zweimal expandiert. Man kann eine topologische Sortierung stattdessen auch mit Tiefensuche bestimmen. Es gilt nämlich der folgende&lt;br /&gt;
;Satz: Wird ein DAG mittels Tiefensuche traversiert, definiert die &amp;lt;i&amp;gt;reverse post-order&amp;lt;/i&amp;gt; eine topologische Sortierung.&lt;br /&gt;
Zur Erinnerung: die post-order erhält man, indem man jeden Knoten ausgibt, &amp;lt;i&amp;gt;nachdem&amp;lt;/i&amp;gt; die Rekursion zu allen seinen Nachbarn beendet ist, siehe unsere [[Graphen_und_Graphenalgorithmen#pre_and_post_order|Diskussion weiter oben]]. Die reverse post-order ist gerade die Umkehrung dieser Reihenfolge. Die folgende Implementation verwendet die rekursive Version der Tiefensuche, in der Praxis wird man meist die iterative Version mit Stack bevorzugen, weil bei großen Graphen die Aufruftiefe sehr groß werden kann:&lt;br /&gt;
&lt;br /&gt;
 def reverse_post_order(graph):               # gerichteter Graph&lt;br /&gt;
     result = []                              # enthält später die reverse post-order&lt;br /&gt;
     visited = [False]*len(graph)             # Flags für bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):                         # besuche node&lt;br /&gt;
         if not visited[node]:                # aber nur, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True             # markiere ihn als besucht&lt;br /&gt;
             for neighbor in graph[node]:     # und besuche die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
             result.append(node)              # alle Nachbarn besucht =&amp;gt; Anhängen an result liefert post-order&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):          # besuche alle Knoten&lt;br /&gt;
         visit(node)&lt;br /&gt;
     &lt;br /&gt;
     result.reverse()                         # post-order =&amp;gt; reverse post-order&lt;br /&gt;
     return result&lt;br /&gt;
&lt;br /&gt;
Die Tatsache, dass die reverse post-order tatsächlich eine topologische Sortierung liefert, leuchtet wahrscheinlich nicht unmittelbar ein. Bevor wir diese Tatsache beweisen. wollen wir uns anhand des Ankleidegraphen klar machen, dass die pre-order (die man intuitiv vielleicht eher wählen würde) keine topologische Sortierung ist. Startet man die Tiefensuche beim Knoten &amp;quot;Unterhemd&amp;quot;, werden die Knoten in der Reihenfolge &amp;quot;Unterhemd&amp;quot;, &amp;quot;Oberhemd&amp;quot;, &amp;quot;Schlips&amp;quot;, &amp;quot;Jackett&amp;quot;, &amp;quot;Gürtel&amp;quot; gefunden. Da dann alle von &amp;quot;Unterhemd&amp;quot; erreichbaren Knoten erschöpft sind, startet man die Tiefensuche als nächstes bei &amp;quot;Unterhose&amp;quot; und erreicht von dort aus &amp;quot;Hose&amp;quot; und &amp;quot;Schuhe&amp;quot;. Man erkennt sofort, dass diese Reihenfolge nicht funktioniert: &amp;quot;Hose&amp;quot; kommt nach &amp;quot;Gürtel&amp;quot;, und &amp;quot;Jackett&amp;quot; kommt vor &amp;quot;Gürtel&amp;quot;. Bei dieser Anordnung gibt es Pfeile nach links, die Abhängigkeitsbedingungen sind somit verletzt.&lt;br /&gt;
&lt;br /&gt;
Damit die reverse post-order eine zulässige Sortierung sein kann, muss stets gelten, dass Knoten u vor Knoten v einsortiert wurde, wenn die Kante (u &amp;amp;rarr; v) existiert. Das ist aber äquivalent zur Forderung, dass in der ursprünglichen post-order (vor dem &amp;lt;tt&amp;gt;reverse&amp;lt;/tt&amp;gt;) u hinter v stehen muss. Wir betrachten den &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt;-Aufruf, bei dem u expandiert wird. Gelangt man jetzt zu u's Nachbarn v, gibt es zwei Möglichkeiten: Wenn v bereits expandiert wurde, befindet es sich bereits im Array &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt; kehrt sofort zurück. Andernfalls wird v ebenfalls expandiert und demzufolge in &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; eingetragen, &amp;lt;i&amp;gt;bevor&amp;lt;/i&amp;gt; der rekursive Aufruf &amp;lt;tt&amp;gt;visit(v)&amp;lt;/tt&amp;gt; zurückkehrt. Knoten u wird aber erst in &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; eingefügt, &amp;lt;i&amp;gt;nachdem&amp;lt;/i&amp;gt; alle rekursiven &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt;-Aufrufe seiner Nachbarn zurückgekehrt sind. In beiden Fällen steht u in der post-order wie gefordert hinter v, und daraus folgt die Behauptung.&lt;br /&gt;
&lt;br /&gt;
Der obige Algorithmus liefert natürlich nur dann eine topologische Sortierung, wenn der Graph wirklich azyklisch ist (man kann ihn aber auch anwenden, um die reverse post-order für einen zyklischen Graphen zu bestimmen, siehe Abschnitt &amp;quot;[[Graphen_und_Graphenalgorithmen#Transitive Hülle und stark zusammenhängende Komponenten|Stark zusammenhängende Komponenten]]&amp;quot;). Dieser Fall tritt in der Praxis häufig auf, weil zyklische Graphen bei vielen Anwendungen gar nicht erst entstehen können. Weiß man allerdings nicht, ob der Graph azyklisch ist oder nicht, muss man einen zusätzlichen Test auf Zyklen in den Algorithmus einbauen. &lt;br /&gt;
&lt;br /&gt;
Zyklische Graphen sind dadurch gekennzeichnet, dass es im obigen Beweis eine dritte Möglichkeit gibt: Während der Expansion von u wird rekursiv v expandiert, und es gibt eine Rückwärtskante (v &amp;amp;rarr; u). (Es spielt dabei keine Rolle, ob v von u aus direkt oder indirekt erreicht wurde.) Ein Zyklus wird also entdeckt, wenn die Tiefensuche zu u zurückkehrt, solange u noch &amp;lt;i&amp;gt;aktiv&amp;lt;/i&amp;gt; ist, d.h. wenn die Rekursion von u aus gestartet und noch nicht beendet wurde. Dies kann man leicht feststellen, wenn man in der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; drei Werte zulässt: 0 für &amp;quot;noch nicht besucht&amp;quot;, 1 für &amp;quot;aktiv&amp;quot; und 2 für &amp;quot;beendet&amp;quot;. Wir signalisieren einen Zyklus, sobald &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt; für einen Knoten aufgerufen wird, der gerade aktiv ist:&lt;br /&gt;
&lt;br /&gt;
 def topological_sort_DFS(graph):             # gerichteter Graph&lt;br /&gt;
     result = []                              # enthält später die topologische Sortierung&lt;br /&gt;
     &lt;br /&gt;
     not_visited, active, finished = 0, 1, 2  # drei Zustände für visited&lt;br /&gt;
     visited = [not_visited]*len(graph)       # Flags für aktive und bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):                         # besuche node (gibt &amp;quot;True&amp;quot; zurück, wenn Zyklus gefunden wurde)&lt;br /&gt;
         if visited[node] == not_visited:     # ... aber nur, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = active           # markiere ihn als aktiv&lt;br /&gt;
             for neighbor in graph[node]:     # und besuche die Nachbarn&lt;br /&gt;
                 if visit(neighbor):          # wenn rekursiv ein Zyklus gefunden wurde&lt;br /&gt;
                     return True              # ... brechen wir ab und signalisieren den Zyklus&lt;br /&gt;
             visited[node] = finished         # Rekursion beendet, node ist nicht mehr aktiv&lt;br /&gt;
             result.append(node)              # alle Nachbarn besucht =&amp;gt; Anhängen an result liefert post-order&lt;br /&gt;
             return False                     # kein Zyklus gefunden&lt;br /&gt;
         elif visited[node] == active:        # Rekursion erreicht einen noch aktiven Knoten&lt;br /&gt;
             return True                      #  =&amp;gt; Zyklus gefunden&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):          # besuche alle Knoten&lt;br /&gt;
         if visit(node):                      # wenn Zyklus gefunden wurde&lt;br /&gt;
             return None                      # ... gibt es keine topologische Sortierung&lt;br /&gt;
     &lt;br /&gt;
     result.reverse()                         # post-order =&amp;gt; reverse post-order (=topologische Sortierung)&lt;br /&gt;
     return result&lt;br /&gt;
&lt;br /&gt;
Man macht sich leicht klar, dass kein Zyklus vorliegt, wenn die Rekursion einen Knoten erreicht, der bereits auf &amp;lt;tt&amp;gt;finished&amp;lt;/tt&amp;gt; gesetzt ist. Nehmen wir an, dass u gerade expandiert wird, und sein Nachbar v ist bereits &amp;lt;tt&amp;gt;finished&amp;lt;/tt&amp;gt;. Wenn es einen Zyklus gäbe, müsste es einen Weg von v nach u geben. Dann wäre u aber bereits während der Expansion von v gefunden worden. Da v nicht mehr im Zustand &amp;lt;tt&amp;gt;active&amp;lt;/tt&amp;gt; ist, muss die Expansion von v schon abgeschlossen gewesen sein, ohne dass u gefunden wurde. Folglich kann es keinen solchen Zyklus geben.&lt;br /&gt;
&lt;br /&gt;
=== Transitive Hülle und stark zusammenhängende Komponenten ===&lt;br /&gt;
&lt;br /&gt;
Auch bei gerichteten Graphen ist die Frage, welche Knoten miteinander zusammenhängen, von großem Interesse. Wir betrachten dazu wieder die Relation &amp;quot;Knoten v ist von Knoten u aus erreichbar&amp;quot;, die anzeigt, ob es einen Weg von u nach v gibt oder nicht. In ungerichteten Graphen ist diese Relation immer symmetrisch, weil jeder Weg in beiden Richtungen benutzt werden kann. In gerichteten Graphen gilt dies nicht. Man muss hier zwei Arten von Zusammenhangskomponenten unterscheiden:&lt;br /&gt;
;Transitive Hülle: Die transitive Hülle eines Knotens u ist die Menge aller Knoten, die von u aus erreichbar sind:&lt;br /&gt;
:&amp;lt;math&amp;gt;T(u) = \{v\ |\ u \rightsquigarrow v\}&amp;lt;/math&amp;gt;&lt;br /&gt;
;Stark zusammenhängende Komponenten: Die stark zusammenhängende Komponenten &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; eines gerichteten Graphen sind maximale Teilgraphen, so dass alle Knoten innerhalb einer Komponente von jedem anderen Knoten der selben Komponente aus erreichbar sind&lt;br /&gt;
:&amp;lt;math&amp;gt;u,v \in C_i\ \ \Leftrightarrow\ \ u \rightsquigarrow v \wedge v \rightsquigarrow u&amp;lt;/math&amp;gt;&lt;br /&gt;
Die erste Definition betrachtet den Zusammenhang asymmetrisch, ohne Beachtung der Frage, ob es auch einen Rückweg von Knoten v nach u gibt, die zweite hingegen symmetrisch.&lt;br /&gt;
&lt;br /&gt;
Die &amp;lt;b&amp;gt;transitive Hülle&amp;lt;/b&amp;gt; benötigt man, wenn man Fragen der Erreichbarkeit besonders effizient beantworten will. Wir hatten bespielsweise oben erwähnt, dass das Python-Modul &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; direkt und indirekt von mehreren anderen Module abhängt, die vorher installiert werden müssen, damit &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; funktioniert. Bittet man den Systemadministrator, das &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt;-Paket zu installieren, will er diese Abhängigkeiten wahrscheinlich nicht erst mühsam rekursiv heraussuchen, sondern er verlangt eine Liste aller Pakete, die installiert werden müssen. Dies ist gerade die transitive Hülle von &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; im Abhängigkeitsgraphen. Damit man diese nicht manuell bestimmen muss, verwendet man Installationsprogramme wie z.B. [http://pypi.python.org/pypi/pip/ pip], die die Abhängigkeiten automatisch herausfinden und installieren. &lt;br /&gt;
&lt;br /&gt;
Bei der Bestimmung der transitiven Hülle modifiziert man den gegebenen Graphen, indem man jedesmal eine neue Kante (u &amp;amp;rarr; v) einfügt, wenn diese Kante noch nicht existiert, aber v von u aus erreichbar ist. Dies gelingt mit einer sehr einfachen Variation der Tiefensuche: Wir rufen &amp;lt;tt&amp;gt;visit(k)&amp;lt;/tt&amp;gt; für jeden Knoten k auf, aber setzen die property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; zuvor auf &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt; zurück. Alle Knoten, die während der Rekursion erreicht werden, sind im modifizierten Graphen Nachbarn von k. Ein etwas effizienterer Ansatz ist der [http://de.wikipedia.org/wiki/Algorithmus_von_Floyd_und_Warshall Algorithmus von Floyd und Warshall].&lt;br /&gt;
&lt;br /&gt;
Die Bestimmung der &amp;lt;b&amp;gt;stark zusammenhängenden Komponenten&amp;lt;/b&amp;gt; ist etwas schwieriger. Es existieren eine ganze Reihe von effizienten Algorithmen (siehe [http://en.wikipedia.org/wiki/Strongly_connected_component WikiPedia]), deren einfachster der Algorithmus von Kosaraju ist:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;gegeben:&amp;lt;/b&amp;gt; gerichteter Graph&lt;br /&gt;
&lt;br /&gt;
# Bestimme die reverse post-order (mit der Funktion &amp;lt;tt&amp;gt;reverse_post_order&amp;lt;/tt&amp;gt;)&lt;br /&gt;
# Bilde den transponierten Graphen &amp;lt;math&amp;gt;G^T&amp;lt;/math&amp;gt; (mit der Funktion &amp;lt;tt&amp;gt;transposeGraph&amp;lt;/tt&amp;gt;)&lt;br /&gt;
# Bestimme die Zusammenhangskomponenten von &amp;lt;math&amp;gt;G^T&amp;lt;/math&amp;gt; mittels Tiefensuche, aber betrachte die Knoten dabei in der reverse post-order aus Schritt 1 (dies kann mit einer minimalen Modifikation der Funktion &amp;lt;tt&amp;gt;connectedComponents&amp;lt;/tt&amp;gt; geschehen, indem man die Zeile &amp;lt;tt&amp;gt;for node in xrange(len(graph)):&amp;lt;/tt&amp;gt; einfach nach &amp;lt;tt&amp;gt;for node in ordered:&amp;lt;/tt&amp;gt; abändert, wobei &amp;lt;tt&amp;gt;ordered&amp;lt;/tt&amp;gt; das Ergebnis der Funktion &amp;lt;tt&amp;gt;reverse_post_order&amp;lt;/tt&amp;gt; ist, also ein Array, das die Knoten in der gewünschten Reihenfolge enthält).&lt;br /&gt;
Die Zusammenhangskomponenten, die man in Schritt 3 findet, sind gerade die stark zusammenhängenden Komponenten des Originalgraphen G. Die folgende Skizze zeigt diese in grün für den schwarz gezeichneten gerichteten Graphen. &lt;br /&gt;
&lt;br /&gt;
[[Image:strongly-connected-components.png|400px]]    &lt;br /&gt;
&lt;br /&gt;
Zum Beweis der Korrektheit des Algorithmus von Kosaraju zeigen wir zwei Implikationen: 1. Wenn die Knoten u und v in der selben stark zusammenhängenden Komponente liegen, werden sie in Schritt 3 des Algorithmus auch der selben Komponente zugewiesen. 2. Wenn die Knoten u und v in Schritt 3 der selben Komponente zugewiesen wurden, müssen sie auch in der selben stark zusammenhängenden Komponente liegen. &lt;br /&gt;
# Knoten u und v gehören zur selben stark zusammenhängenden Komponente von G. Per Definition gilt, dass u von v aus erreichbar ist und umgekehrt. Dies muss auch im transponierten Graphen G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; gelten (der Weg &amp;lt;math&amp;gt;u \rightsquigarrow v&amp;lt;/math&amp;gt; wird jetzt zum Weg &amp;lt;math&amp;gt;v \rightsquigarrow u&amp;lt;/math&amp;gt; und umgekehrt). Wird u bei der Tiefensuche in Schritt 3 vor v expandiert, ist v von u aus erreichbar und gehört somit zur selben Komponente. Das umgekehrte gilt, wenn v vor u expandiert wird. Daraus folgt die Behauptung 1.&lt;br /&gt;
# Knoten u und v werden in Schritt 3 der selben Komponente zugewiesen: Sei x der Anker dieser Komponente. Da u in der gleichen Komponente wie x liegt, muss es in G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; einen Weg &amp;lt;math&amp;gt;x \rightsquigarrow u&amp;lt;/math&amp;gt;, und demnach in G einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow x&amp;lt;/math&amp;gt; geben. Da x der Anker seiner Komponente ist, wissen wir aber auch, dass x in der reverse post-order &amp;lt;i&amp;gt;vor&amp;lt;/i&amp;gt; u liegt (denn der Anker ist der Knoten, mit dem eine neue Komponente gestartet wird; er muss deshalb im Array &amp;lt;tt&amp;gt;ordered&amp;lt;/tt&amp;gt; als erster Konten seiner Komponente gefunden worden sein). Wir unterscheiden jetzt im Schritt 1 des Algorithmus zwei Fälle:&lt;br /&gt;
## u wurde bei der Bestimmung der post-order vor x expandiert. Dann kann x nur dann in der reverse post-order &amp;lt;i&amp;gt;vor&amp;lt;/i&amp;gt; u liegen (oder, einfacher ausgedrückt, x kann nur dann in der post-order &amp;lt;i&amp;gt;hinter&amp;lt;/i&amp;gt; u liegen), wenn x im Graphen G nicht von u aus erreichbar war. Das ist aber unmöglich, weil wir ja schon wissen, dass es in G einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow x&amp;lt;/math&amp;gt; gibt.&lt;br /&gt;
## Folglich wurde u bei der Bestimmung der post-order nach x expandiert. Da x in der post-order hinter u liegt, muss u während der Expansion von x erreicht worden sein. Deshalb muss es in G auch einen Weg &amp;lt;math&amp;gt;x \rightsquigarrow u&amp;lt;/math&amp;gt; geben.&lt;br /&gt;
#:Somit sind x und u in der selben stark zusammenhängenden Komponente. Die gleiche Überlegung gilt für x und v. Wegen der Transitivität der Relation &amp;quot;ist erreichbar&amp;quot; folgt daraus, dass auch u und v in der selben Komponente liegen, also die Behauptung 2.&lt;br /&gt;
&lt;br /&gt;
Die folgende Skizze illustriert, dass der Komponentengraph stets azyklisch ist. Den Komponentengraph erhält man, indem man für jede Komponente &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; einen Knoten erzeugt (grün), und die Knoten i und j durch eine gerichtete Kante verbindet (rot), wenn es im Originalgraphen eine Kante (u &amp;amp;rarr; v) mit &amp;lt;math&amp;gt;u \in C_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v \in C_j&amp;lt;/math&amp;gt; gibt. Es ist dann garantiert, dass es keine Kante in umgekehrter Richtung geben kann. Daraus folgt insbesondere, dass ein DAG nur triviale stark verbundene Komponenten haben kann, die aus einzelnen Knoten bestehen.&lt;br /&gt;
&lt;br /&gt;
[[Image:strongly-connected-components-graph.png|400px]]&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Das Erfüllbarkeitsproblem in Implikationengraphen ===&lt;br /&gt;
&lt;br /&gt;
Das Erfüllbarkeitsproblem hat auf den ersten Blick nichts mit Graphen zu tun, denn es geht um Wahrheitswerte logischer Ausdrücke. Man kann logische Ausdrücke jedoch unter bestimmten Bedingungen in eine Graphendarstellung überführen und somit das ursprüngliche Problem auf ein Problem der Graphentheorie reduzieren, für das bereits ein Lösungsverfahren bekannt ist. In diesem Abschnitt wollen wir dies für die sogenannten Implikationengraphen zeigen, ein weiteres Beispiel findet sich im Kapitel [[NP-Vollständigkeit]].&lt;br /&gt;
&lt;br /&gt;
==== Das Erfüllbarkeitsproblem ====&lt;br /&gt;
&lt;br /&gt;
(vgl. [http://de.wikipedia.org/wiki/Erfüllbarkeitsproblem_der_Aussagenlogik WikiPedia (de)])&lt;br /&gt;
&lt;br /&gt;
Das Erfüllbarkeitsproblem (SAT-Problem, von &amp;lt;i&amp;gt;satisfiability&amp;lt;/i&amp;gt;) befasst sich mit logischen (oder Booleschen) Funktionen: Gegeben sei eine Menge &amp;lt;math&amp;gt;\{x_1, ... ,x_n\}&amp;lt;/math&amp;gt; Boolscher Variablen (d.h., die &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; können nur die Werte True oder False annehmen), sowie eine logische Formel, in der die Variablen mit den üblichen logischen Operatoren &lt;br /&gt;
:&amp;lt;math&amp;gt;\neg\quad&amp;lt;/math&amp;gt;: Negation (&amp;quot;nicht&amp;quot;, in Python: &amp;lt;tt&amp;gt;not&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\vee\quad&amp;lt;/math&amp;gt;: Disjunktion (&amp;quot;oder&amp;quot;, in Python: &amp;lt;tt&amp;gt;or&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\wedge\quad&amp;lt;/math&amp;gt;: Konjuktion (&amp;quot;und&amp;quot;, in Python: &amp;lt;tt&amp;gt;and&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\rightarrow\quad&amp;lt;/math&amp;gt;: Implikation (&amp;quot;wenn, dann&amp;quot;, in Python nicht als Operator definiert)&lt;br /&gt;
:&amp;lt;math&amp;gt;\leftrightarrow\quad&amp;lt;/math&amp;gt;: Äquivalenz (&amp;quot;genau dann, wenn&amp;quot;, in Python: &amp;lt;tt&amp;gt;==&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\neq\quad&amp;lt;/math&amp;gt;: exklusive Disjunktion (&amp;quot;entweder oder&amp;quot;, in Python: &amp;lt;tt&amp;gt;!=&amp;lt;/tt&amp;gt;)&lt;br /&gt;
verknüpft sind. Klammern definieren die Reihenfolge der Auswertung der Operationen. Für jede Belegung der Variablen &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; mit True oder False liefert die Formel den Wert der Funktion, der natürlich auch nur True oder False sein kann. Wenn Formel und Belegung gegeben sind, ist die Auswertung der Funktion ein sehr einfaches Problem: Man transformiert die Formel in einen Parse-Baum (siehe Übungsaufgabe &amp;quot;Taschenrechner) und wertet jeden Knoten mit Hilfe der üblichen Wertetabellen für logische Operatoren aus, die wir hier zur Erinnerung noch einmal angeben:&lt;br /&gt;
{| cellspacing=&amp;quot;0&amp;quot; border=&amp;quot;1&amp;quot;&lt;br /&gt;
|- style=&amp;quot;text-align:center;background-color:#ffffcc;width:50px&amp;quot;&lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \vee b &amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \wedge b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \rightarrow b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;b \rightarrow a&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \leftrightarrow b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \neq b&amp;lt;/math&amp;gt; &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 0 || 1 || 1 || 1 || 0&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 0 || 1 || 0 || 0 || 1&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 0 || 1 || 0 || 1&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 || 0&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beim Erfüllbarkeitsproblem wird die Frage umgekehrt gestellt: &lt;br /&gt;
:Gegeben sei eine logische Funktion. Ist es möglich, dass die Funktion jemals den Wert True annimmt? &lt;br /&gt;
Das heisst, kann man die Variablen &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; so mit True oder False belegen, dass die Formel am Ende wahr ist? Im Prinzip kann man diese Frage durch erschöpfende Suche leicht beantworten, indem man die Funktion für alle &amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt; möglichen Belegungen einfach ausrechnet, aber das dauert für große n (ab ca. &amp;lt;math&amp;gt;n\ge 40&amp;lt;/math&amp;gt;) viel zu lange. Erstaunlicherweise ist es aber noch niemanden gelungen, einen Algorithmus zu finden, der für beliebige logische Funktionen schneller funktioniert. Im Gegenteil wurde gezeigt, dass das Erfüllbarkeitsproblem [[NP-Vollständigkeit|NP-vollständig]] ist, so dass wahrscheinlich kein solcher Algorithmus existiert. Trotz (oder gerade wegen) seiner Schwierigkeit hat das Erfüllbarkeitsproblem viele Anwendungen gefunden, vor allem beim Testen logischer Schaltkreise (&amp;quot;Gibt es eine Belegung der Eingänge, so dass am Ausgang der verbotene Wert X entsteht?&amp;quot;) und bei der Planerstellung in der künstlichen Intelligenz (&amp;quot;Kann man ausschließen, dass der generierte Plan Konflikte enthält?&amp;quot;). Es ist außerdem ein beliebtes Modellproblem für die Erforschung neuer Ideen und Algorithmen für schwierige Probleme.&lt;br /&gt;
&lt;br /&gt;
==== Normalformen für logische Ausdrücke ====&lt;br /&gt;
&lt;br /&gt;
Um die Beschreibung von Erfüllbarkeitsproblemen zu vereinfachen und zu vereinheitlichen, hat man verschiedene &amp;lt;i&amp;gt;Normalformen&amp;lt;/i&amp;gt; für logische Ausdrücke eingeführt. Die wichtigste ist die &amp;lt;i&amp;gt;Konjuktionen-Normalform&amp;lt;/i&amp;gt; (CNF - conjunctive normal form). Ein Ausdruck in &amp;lt;i&amp;gt;Konjuktionen-Normalform&amp;lt;/i&amp;gt; ist eine UND-Verknüpfung von M &amp;lt;i&amp;gt;Klauseln&amp;lt;/i&amp;gt;:&lt;br /&gt;
 (CLAUSE&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;) &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; (CLAUSE&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;) &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; ...  &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; (CLAUSE&amp;lt;sub&amp;gt;M&amp;lt;/sub&amp;gt;)&lt;br /&gt;
Jede Klausel ist wiederum ein logischer Ausdruck, der aber sehr einfach sein muss: Er darf nur noch k Variablen enthalten, die nur mit den Operatoren NICHT und ODER verknüpft werden dürfen, z.B.&lt;br /&gt;
  CLAUSE&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; := &amp;lt;math&amp;gt;x_1 \vee \neg x_3 \vee x_8&amp;lt;/math&amp;gt;&lt;br /&gt;
Je nachdem, wie viele Variablen pro Klausel erlaubt sind, spricht man von &amp;lt;b&amp;gt;k-CNF&amp;lt;/b&amp;gt; und entsprechend von einem &amp;lt;b&amp;gt;k-SAT&amp;lt;/b&amp;gt; Problem. Es ist außerdem üblich, die Menge der Variablen und die Menge der negierten Variablen zusammen als Menge der &amp;lt;i&amp;gt;Literale&amp;lt;/i&amp;gt; zu bezeichnen:&lt;br /&gt;
  LITERALS := &amp;lt;math&amp;gt;\{x_1,...,x_n\} \cup \{\neg x_1,...,\neg x_n\}&amp;lt;/math&amp;gt;&lt;br /&gt;
Formal definiert man die &amp;lt;b&amp;gt;k-Konjunktionen-Normalform (k-CNF)&amp;lt;/b&amp;gt; am besten durch eine Grammatik in [http://de.wikipedia.org/wiki/Backus-Naur-Form Backus-Naur-Form]:&lt;br /&gt;
    k_CNF    ::=  CLAUSE | k_CNF &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; CLAUSE&lt;br /&gt;
    CLAUSE   ::= (LITERAL &amp;lt;math&amp;gt;\vee&amp;lt;/math&amp;gt; ... &amp;lt;math&amp;gt;\vee&amp;lt;/math&amp;gt; LITERAL)  # genau k Literale pro Klausel&lt;br /&gt;
    LITERAL  ::=  VARIABLE | &amp;lt;math&amp;gt;\neg&amp;lt;/math&amp;gt;VARIABLE&lt;br /&gt;
    VARIABLE ::=  &amp;lt;math&amp;gt;x_1&amp;lt;/math&amp;gt; | ... | &amp;lt;math&amp;gt;x_n&amp;lt;/math&amp;gt;&lt;br /&gt;
Beispiele:&lt;br /&gt;
* 3-CNF: &amp;lt;math&amp;gt;(x_1 \vee \neg x_2 \vee x_4) \wedge (x_2 \vee x_3 \vee \neg x_4) \wedge (\neg x_1 \vee x_4 \vee \neg x_5)&amp;lt;/math&amp;gt;&lt;br /&gt;
* 2-CNF: &amp;lt;math&amp;gt;(x_1 \vee \neg x_2) \wedge (x_3 \vee x_4)&amp;lt;/math&amp;gt; ...&lt;br /&gt;
&amp;lt;b&amp;gt;Gesucht&amp;lt;/b&amp;gt; ist eine Belegung der Variablen mit True und False, so dass der Ausdruck den Wert True hat. Aus den Eigenschaften der UND- und ODER-Verknüpfungen folgt, dass ein Ausdruck in k-CNF genau dann True ist, wenn jede einzelne Klausel True ist. In jeder Klausel wiederum hat man k Chancen, die Klausel True zu machen, indem man eins der Literale zu True macht. Eventuell werden dadurch aber andere Klauseln wieder zu False, was die Aufgabe so schwierig macht. Die Bedeutung der k-CNF ergibt sich aus folgendem&lt;br /&gt;
;Satz: Jeder logische Ausdruck kann effizient nach 3-CNF transformiert werden, jedoch im allgemeinen nicht nach 2-CNF.&lt;br /&gt;
Man kann sich also auf Algorithmen für 3-SAT-Probleme konzentrieren, ohne dabei an Ausdrucksmächtigkeit zu verlieren. &lt;br /&gt;
&lt;br /&gt;
Leider gilt der entsprechende Satz nicht für k=2: Ausdrücke in 2-CNF sind weit weniger mächtig, weil man in jeder Klausel nur noch zwei Wahlmöglichkeiten hat. Bestimmte logische Ausdrücke sind aber auch nach 2-CNF transformierbar, beispielsweise die Bedingung, dass zwei Literale u und v immer den entgegegesetzten Wert haben müssen. Dies ergibt ein Paar von ODER-Verknüpfungen:&lt;br /&gt;
:&amp;lt;math&amp;gt;(u \leftrightarrow \neg v) \equiv (u \vee \neg v) \wedge (\neg u \vee v)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die 2-CNF hat den Vorteil, dass es effiziente Algorithmen für das 2-SAT-Problem gibt, die wir jetzt kennenlernen wollen. Es zeigt sich, dass man Ausdrücke in 2-CNF als Graphen repräsentieren kann, indem man sie zunächst in die &amp;lt;i&amp;gt;Implikationen-Normalform&amp;lt;/i&amp;gt; (INF für &amp;lt;i&amp;gt;implicative normal form&amp;lt;/i&amp;gt;) überführt. Die Implikationen-Normalform besteht ebenfalls aus einer Menge von Klauseln, die durch UND-Operationen verknüpft sind, aber jede Klausel ist jetzt eine Implikation. &lt;br /&gt;
Die Grammatik der &amp;lt;b&amp;gt;Implikationen-Normalform (INF)&amp;lt;/b&amp;gt; lautet:&lt;br /&gt;
    INF      ::=  CLAUSE | INF &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; CLAUSE&lt;br /&gt;
    CLAUSE   ::= (LITERAL &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; LITERAL)  # genau 2 Literale pro Implikation&lt;br /&gt;
    LITERAL  ::=  VARIABLE | &amp;lt;math&amp;gt;\neg&amp;lt;/math&amp;gt;VARIABLE&lt;br /&gt;
    VARIABLE ::=  &amp;lt;math&amp;gt;x_1&amp;lt;/math&amp;gt; | ... | &amp;lt;math&amp;gt;x_n&amp;lt;/math&amp;gt;&lt;br /&gt;
und ein gültiger Ausdruck wäre z.B.&lt;br /&gt;
:&amp;lt;math&amp;gt;(x_1 \to x_2) \wedge (x_2 \to \neg x_3) \wedge (x_4 \to x_3)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die Umwandlung von 2-CNF nach INF beruht auf folgender Äquivalenz, die man sich aus der obigen Wahrheitstabelle leicht herleitet:&lt;br /&gt;
:&amp;lt;math&amp;gt;(x \vee y) \equiv (\neg x \rightarrow y) \equiv (\neg y \rightarrow x)&amp;lt;/math&amp;gt;&lt;br /&gt;
Aus dieser Äquivalenz folgt der &lt;br /&gt;
;Satz: Ein Ausdruck in 2-CNF kann nach INF transformiert werden, indem man jede Klausel &amp;lt;math&amp;gt;(x \vee y)&amp;lt;/math&amp;gt; durch das Klauselpaar &amp;lt;math&amp;gt;(\neg x \rightarrow y) \wedge (\neg y \rightarrow x)&amp;lt;/math&amp;gt; ersetzt.&lt;br /&gt;
Man beachte, dass man für jede ODER-Klausel des ursprünglichen Ausdrucks &amp;lt;i&amp;gt;zwei&amp;lt;/i&amp;gt; Implikationen (eine für jede Richtung des &amp;quot;wenn, dann&amp;quot;) einfügen muss, um die Symmetrie des Problems zu erhalten.&lt;br /&gt;
&lt;br /&gt;
==== Lösung des 2-SAT-Problems mit Implikationgraphen ====&lt;br /&gt;
&lt;br /&gt;
Jeder Ausdruck in INF kann als gerichteter Graph dargestellt werden:&lt;br /&gt;
# Für jedes Literal wird ein Knoten in den Graphen eingefügt. Es gibt also für jede Variable und für ihre Negation jeweils einen Knoten, d.h. 2n Knoten insgesamt.&lt;br /&gt;
# Jede Implikation ist eine gerichtete Kante.&lt;br /&gt;
Implikationengraphen eignen sich, um Ursache-Folge-Beziehungen oder Konflikte zwischen Aktionen auszudrücken. Beispielsweise kann man die Klausel &amp;lt;math&amp;gt;(x \rightarrow \neg y)&amp;lt;/math&amp;gt; als &amp;quot;wenn man x tut, darf man y nicht tun&amp;quot; interpretieren. Ein anderes schönes Beispiel findet sich in Übung 12.&lt;br /&gt;
&lt;br /&gt;
Für die Implementation eines Implikationengraphen in Python empfiehlt es sich, die Knoten geschickt zu numerieren: Ist die Variable &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; dem Knoten i zugeordnet, so sollte die negierte Variable &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; dem Knoten (i+n) zugeordnet werden. Zu jedem gegebenen Knoten i findet man dann den negierten Partnerknoten j leicht durch die Formel &amp;lt;tt&amp;gt;j = (i + n ) % (2*n)&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die Aufgabe besteht jetzt darin, folgende Fragen zu beantworten:&lt;br /&gt;
# Ist der durch den Implikationengraphen gegebene Ausdruck erfüllbar?&lt;br /&gt;
# Finde eine geeignete Belegung der Variablen, wenn der Ausduck erfüllbar ist.&lt;br /&gt;
Die erste Frage beantwortet man leicht, indem man die stark zusammenhängenden Komponenten des Implikationengraphen bildet. Dann gilt folgender&lt;br /&gt;
;Satz: Seien u und v zwei Literale, die sich in der selben stark zusammenhängenden Komponente befinden. Dann müssen u und v stets den selben Wert haben, damit der Ausdruck erfüllt sein kann.&lt;br /&gt;
Die Korrektheit des Satzes folgt aus der Definition der stark zusammenhängenden Komponenten: Da u und v in der selben Komponente liegen, gibt es im Implikationengraphen einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow v&amp;lt;/math&amp;gt; sowie einen Weg &amp;lt;math&amp;gt;v \rightsquigarrow u&amp;lt;/math&amp;gt;. Wegen der Transitivität der &amp;quot;wenn, dann&amp;quot; Relation kann man die Wege zu zwei Implikationen verkürzen, die gleichzeitig gelten müssen: &amp;lt;math&amp;gt;(u \rightarrow v) \wedge (v \rightarrow u)&amp;lt;/math&amp;gt; (die Verkürzung von Wegen zu direkten Kanten entspricht gerade der Bildung der transitiven Hülle für die Knoten u und v). In der obigen Wertetabelle für logische Operatoren erkennt mann, dass dies äquivalent zur Bedingung &amp;lt;math&amp;gt;(u \leftrightarrow v)&amp;lt;/math&amp;gt; ist. Dies ist aber gerade die Behauptung des Satzes.&lt;br /&gt;
&lt;br /&gt;
Die Erfüllbarkeit des Ausdrucks ist nun ein einfacher Spezialfall dieses Satzes. &lt;br /&gt;
;Korrolar: Der gegebene Ausdruck ist genau dann erfüllbar, wenn die Literale &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; sich für kein i in derselben stark zusammenhängenden Komponente befinden.&lt;br /&gt;
Setzt man nämlich im Satz &amp;lt;math&amp;gt;u = x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v = \neg x_i&amp;lt;/math&amp;gt;, und beide Knoten befinden sich in der selben Komponente, dann müsste gelten &amp;lt;math&amp;gt;x_i \leftrightarrow\neg x_i&amp;lt;/math&amp;gt;, was offensichtlich ein Widerspruch ist. Damit kann der Ausdruck nicht erfüllbar sein. Umgekehrt gilt, dass der Ausdruck immer erfüllbar ist, wenn &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; stets in verschiedenen Komponenten liegen, weil der folgende Algorithmus von Aspvall, Plass und Tarjan in diesem Fall stets eine gültige Belegung aller Variablen liefert:&lt;br /&gt;
# Bestimme die stark zusammenhängenden Komponenten und bilde den Komponentengraphen. Ordne die Knoten des Komponentengraphen (also die stark zusammenhängenden Komponenten des Originalgraphen) in topologische Sortierung an.&lt;br /&gt;
# Betrachte die Komponenten in der topologischen Sortierung von hinten nach vorn und weise ihnen einen Wert nach folgenden Regeln zu (zur Erinnerung: alle Literale in der selben Komponente haben den selben Wert):&lt;br /&gt;
#* Wenn die Komponente noch nicht betrachtet wurde, setze ihren Wert auf True, und den Wert der komplementären Komponente (derjenigen, die die negierten Literale enthält) auf False.&lt;br /&gt;
#* Andernfalls, gehe zur nächsten Komponente weiter.&lt;br /&gt;
Der Algorithmus beruht auf der Symmetrie des Implikationengraphen: Weil Kanten immer paarweise &amp;lt;math&amp;gt;(\neg u \rightarrow v) \wedge (\neg v \rightarrow u)&amp;lt;/math&amp;gt; eingefügt werden, ist der Graph &amp;lt;i&amp;gt;schiefsymmetrisch&amp;lt;/i&amp;gt; (skew symmetric): die eine Hälfte das Graphen ist die transponierte Spiegelung der anderen Hälfte. Enthält eine stark zusammenhängende Komponente &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; die Knoten &amp;lt;tt&amp;gt;i1, i2, ...&amp;lt;/tt&amp;gt;, so gibt es stets eine komplementäre Komponente &amp;lt;math&amp;gt;C_j = \neg C_i&amp;lt;/math&amp;gt;, die die komplementären Knoten &amp;lt;tt&amp;gt;j1 = (i1 + n) % (2*n), j2 = (i2 + n) % (2*n), ...&amp;lt;/tt&amp;gt; enthält. Gilt &amp;lt;math&amp;gt;C_i = \neg C_i&amp;lt;/math&amp;gt; für irgendein i, so ist der Ausdruck nicht erfüllbar. Den Beweis für die Korrektheit des Algorithmus findet man im [http://www.math.ucsd.edu/~sbuss/CourseWeb/Math268_2007WS/2SAT.pdf Originalartikel]. Leider funktioniert dies nicht für k-SAT-Probleme mit &amp;lt;math&amp;gt;k &amp;gt; 2&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Will man nur die Erfüllbarkeit prüfen, vereinfacht sich der Algorithmus zu:&lt;br /&gt;
# Bestimme die stark zusammenhängenden Komponenten.&lt;br /&gt;
# Teste für alle &amp;lt;tt&amp;gt;i = 0,...,n-1&amp;lt;/tt&amp;gt;, dass Knoten &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; und Knoten &amp;lt;tt&amp;gt;(i+n)&amp;lt;/tt&amp;gt; in unterschiedlichen Komponenten liegen.&lt;br /&gt;
Ist der Ausdruck erfüllbar, kann man eine gültige Belegung der Variablen jetzt mit dem randomisierten Algorithmus bestimmen, den wir im Kapitel [[Randomisierte Algorithmen]] behandeln.&lt;br /&gt;
&lt;br /&gt;
== Weitere wichtige Graphenalgorithmen ==&lt;br /&gt;
&lt;br /&gt;
Eins der wichtigsten Einsatzgebiete für Graphen ist die Optimierung, also die Suche nach der &amp;lt;i&amp;gt;besten&amp;lt;/i&amp;gt; Lösung für ein gegebenes Problem:&lt;br /&gt;
* Das &amp;lt;i&amp;gt;interval scheduling&amp;lt;/i&amp;gt; befasst sich damit, aus einer gegebenen Menge von Aufträgen die richtigen auszuwählen und sie geschickt auf die zur Verfügung stehenden Ressourcen aufzuteilen. Damit beschäftigen wir uns im Kapitel [[Greedy-Algorithmen und Dynamische Programmierung]].&lt;br /&gt;
* Beim Problem des Handlungsreisenden sucht man nach der kürzesten Rundreise, die alle gegebenen Städte genau einmal besucht. Dieses Problem behandeln wir im Kapitel [[NP-Vollständigkeit]].&lt;br /&gt;
* Viele weitere Anwendungen können wir leider in der Vorlesung nicht mehr behandeln, z.B.&lt;br /&gt;
** Algorithmen für den [http://en.wikipedia.org/wiki/Maximum_flow_problem maximalen Fluss] beantworten die Frage, wie man die Durchflussmenge durch ein Netzwerk (z.B. von Ölpipelines) maximiert.&lt;br /&gt;
** Beim [http://en.wikipedia.org/wiki/Assignment_problem Problem der optimalen Paarung] (&amp;quot;matching problem&amp;quot; oder &amp;quot;assignment problem&amp;quot;) sucht man nach einer Teilmenge der Kanten (also nach einem Teilgraphen), so dass jeder Knoten in diesem Teilgraphen höchstens den Grad 1 hat. Im neuen Graphen gruppieren die Kanten also je zwei Knoten zu einem Paar, und die Paarung soll nach jeweils anwendungsspezifischen Kriterien optimal sein. Dies benötigt man z.B. bei der optimalen Zuordnung von Gruppen, etwas beim Arbeitsamt (Zuordnung Arbeitssuchender - Stellenangebot) und in der Universität (Zuordnung Studenten - Übungsgruppen).&lt;br /&gt;
** In Statistik und maschinellem Lernen haben in den letzten Jahren die [http://en.wikipedia.org/wiki/Graphical_model graphischen Modelle] große Bedeutung erlangt.&lt;br /&gt;
* usw. usf.&lt;br /&gt;
&lt;br /&gt;
[[Randomisierte Algorithmen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5408</id>
		<title>Graphen und Graphenalgorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5408"/>
				<updated>2012-07-25T17:12:11Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Transitive Hülle und stark zusammenhängende Komponenten */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Einführung zu Graphen ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation -- Königsberger Brückenproblem ===&lt;br /&gt;
Leonhard Euler [http://de.wikipedia.org/wiki/Leonhard_Euler] erfand den Graphen-Formalismus 1736, um eine scheinbar banale Frage zu beantworten: Ist es möglich, in Königsberg (siehe Stadtplan von 1809 und die schematische Darstellung) einen Spaziergang zu unternehmen, bei dem jede der 7 Brücken genau einmal überquert wird?&lt;br /&gt;
&lt;br /&gt;
[[Image:Koenigsberg1809.png]]&amp;lt;br&amp;gt;&lt;br /&gt;
[[Image:Koenigsberg.jpg]]&lt;br /&gt;
&lt;br /&gt;
Ein Graph abstrahiert von der Geometrie des Problems und repräsentiert nur die Topologie. Jeder Stadtteil von Königsberg ist ein Knoten des Graphen, jede Brücke eine Kante. Der zum Brückenproblem gehörende Graph sieht also so aus:&lt;br /&gt;
&lt;br /&gt;
     O&lt;br /&gt;
    /| \&lt;br /&gt;
    \|  \&lt;br /&gt;
     O---O&lt;br /&gt;
    /|  /&lt;br /&gt;
    \| /&lt;br /&gt;
     O&lt;br /&gt;
&lt;br /&gt;
Der gesuchte Spaziergang würde existieren, wenn es maximal 2 Knoten gäbe, an denen sich eine ungerade Zahl von Kanten trifft. Die Frage muss für Königsberg also verneint werden, denn hier gibt es vier solche Knoten. Ein leicht modifiziertes Problem ist allerdings lösbar: Im obigen Stadtplan erkennt man eine Fähre, die die Stadtteile Kneiphof und Altstadt verbindet. Bezieht man dieselbe in den Spaziergang ein, ergibt sich folgender Graph, bei dem nur noch zwei Knoten mit ungerader Kantenzahl existieren:&lt;br /&gt;
&lt;br /&gt;
   --O&lt;br /&gt;
  / /| \&lt;br /&gt;
  \ \|  \&lt;br /&gt;
   --O---O&lt;br /&gt;
    /|  /&lt;br /&gt;
    \| /&lt;br /&gt;
     O&lt;br /&gt;
&lt;br /&gt;
Inzwischen haben Graphen eine riesige Zahl weiterer Anwendungen gefunden. Einige Beispiele:&lt;br /&gt;
&lt;br /&gt;
* Landkarten:&lt;br /&gt;
** Knoten: Länder&lt;br /&gt;
** Kanten: gemeinsame Grenzen&lt;br /&gt;
&lt;br /&gt;
* Logische Schaltkreise:&lt;br /&gt;
** Knoten: Gatter&lt;br /&gt;
** Kanten: Verbindungen&lt;br /&gt;
&lt;br /&gt;
* Chemie (Summenformeln):&lt;br /&gt;
** Knoten: chemische Elemente&lt;br /&gt;
** Kanten: Bindungen &lt;br /&gt;
&lt;br /&gt;
* Soziologie (StudiVZ)&lt;br /&gt;
** Soziogramm&lt;br /&gt;
*** Knoten: Personen&lt;br /&gt;
*** Kanten: Freund von ...&lt;br /&gt;
&lt;br /&gt;
=== Definitionen ===&lt;br /&gt;
&lt;br /&gt;
;Ungerichteter Graph: Ein ungerichteter Graph G = ( V, E ) besteht aus&lt;br /&gt;
:* einer endliche Menge V von Knoten (vertices)&lt;br /&gt;
:* einer endlichen Menge &amp;lt;math&amp;gt;E \subset V \times V&amp;lt;/math&amp;gt; von Kanten (edges)&lt;br /&gt;
:Die Paare (u,v) und (v,u) gelten dabei als nur ''eine'' Kante (somit gilt die Symmetriebeziehung: (u,v) ∈ E =&amp;gt; (v,u) ∈ E ). Die Anzahl der Kanten, die sich an einem Knoten treffen, wird als ''Grad'' (engl. ''degree'') dieses Knotens bezeichnet:&lt;br /&gt;
:::degree(v) = |{v' ∈ V | (v,v') ∈ E}|&lt;br /&gt;
:(Die Syntax |{...}| bezeichnet dabei die Mächtigkeit der angegebenen Menge, also die Anzahl der Elemente in der Menge.)&lt;br /&gt;
&lt;br /&gt;
Der Graph des Königsberger Brückenproblems ist ungerichtet. Bezeichnet man die Knoten entsprechend des folgenden Bildes&lt;br /&gt;
    c&lt;br /&gt;
   /| \&lt;br /&gt;
   \|  \&lt;br /&gt;
    b---d &lt;br /&gt;
   /|  /&lt;br /&gt;
   \| /&lt;br /&gt;
    a&lt;br /&gt;
&lt;br /&gt;
gilt für die Knotengrade: &amp;lt;tt&amp;gt;degree(a) == degree(c) == degree(d) == 3&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;degree(b) == 5&amp;lt;/tt&amp;gt;. Genauer muss man bei diesem Graphen von einem ''Multigraphen'' sprechen, weil es zwischen einigen Knotenpaaren (nämlich (a, b) sowie (b, c)) mehrere Kanten (&amp;quot;Mehrfachkanten&amp;quot;) gibt. Wir werden in dieser Vorlesung nicht näher auf Multigraphen eingehen.&lt;br /&gt;
&lt;br /&gt;
;Gerichteter Graph: Ein Graph heißt ''gerichtet'', wenn die Kanten (u,v) und (v,u) unterschieden werden. Die Kante (u,v) ∈ E wird nun als Kante von u nach v (aber nicht umgekehrt) interpretiert. Entsprechend unterscheidet man jetzt den ''eingehenden'' und den ''ausgehenden Grad'' jedes Knotens:&lt;br /&gt;
:*out_degree(v) = |{v' ∈ V | (v,v') ∈ E}|&amp;lt;br/&amp;gt;&lt;br /&gt;
:*in_degree(v)  = |{v' ∈ V| (v',v) ∈ E}|&lt;br /&gt;
&lt;br /&gt;
Das folgende Bild zeigt einen gerichteten Graphen. Hier gilt &amp;lt;tt&amp;gt;out_degree(1) == out_degree(3) == in_degree(2) == in_degree(4) == 2&amp;lt;/tt&amp;gt; und &lt;br /&gt;
&amp;lt;tt&amp;gt;in_degree(1) == in_degree(3) == out_degree(2) == out_degree(4) == 0&amp;lt;/tt&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
[[Image:digraph.png|gerichteter Graph]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Vollständiger Graph: Ein vollständiger Graph ist ein ungerichteter Graph, bei dem jeder Knoten mit allen anderen Knoten verbunden ist.&lt;br /&gt;
:::&amp;lt;math&amp;gt;E = \{ (v,w) |  v \in V, w \in V, v \ne w \}&amp;lt;/math&amp;gt;&lt;br /&gt;
:Ein vollständiger Graph mit |V| Knoten hat &amp;lt;math&amp;gt;|E| = \frac{|V|(|V|-1)}{2}&amp;lt;/math&amp;gt; Kanten.&lt;br /&gt;
&lt;br /&gt;
Die folgenden Abbildungen zeigen die vollständigen Graphen mit einem bis fünf Knoten (auch als K&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; bis K&amp;lt;sub&amp;gt;5&amp;lt;/sub&amp;gt; bezeichnet).&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;0&amp;quot; style=&amp;quot;margin: 1em auto 1em auto&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
| [[Image:k1.png|frame|k1]]&lt;br /&gt;
| [[Image:k2.png|frame|k2]]&lt;br /&gt;
| [[Image:k3.png|frame|k3]]&lt;br /&gt;
|-&lt;br /&gt;
| [[Image:k4.png|frame|k4]]&lt;br /&gt;
| [[Image:k5.png|frame|k5]]&lt;br /&gt;
|&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
''Rätsel''&amp;lt;br/&amp;gt;&lt;br /&gt;
Auf einer Party sind Leute. Alle stoßen miteinander an. Es hat 78 mal &amp;quot;Pling&amp;quot; gemacht.&lt;br /&gt;
Wieviele Leute waren da? Antwort: Jede Person ist ein Knoten des Graphen, jedes Antoßen eine Kante. &lt;br /&gt;
Da alle miteinander angestoßen haben, handelt es sich um einen vollständigen Graphen. Mit&lt;br /&gt;
|V|(|V|-1)/2 = 78 folgt, dass es 13 Personen waren.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Gewichteter Graph: Ein Graph heißt ''gewichtet'', wenn jeder Kante eine reelle Zahl zugeordnet ist. Bei vielen Anwendungen beschränkt man sich auch auf nichtnegative reelle Gewichte. In einem gerichteten Graphen können die Gewichte der Kanten (u,v) und (v,u) unterschiedlich sein.&lt;br /&gt;
&lt;br /&gt;
Die Gewichte kodieren Eigenschaften der Kanten, die für die jeweilige Anwendung interessant sind. Bei der Berechnung des maximalen Flusses in einem Netzwerk sind die Gewichte z.B. die Durchflusskapazitäten jeder Kante, bei der Suche nach kürzesten Weges kodieren Sie den Abstand zwischen den Endknoten der Kante, bei Währungsnetzwerken (jeder Knoten ist eine Währung) geben sie die Wechselkurse an, usw..&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Teilgraphen: Ein Graph G' = (V',E') ist ein Teilgraph eines Graphen G, wenn gilt:&lt;br /&gt;
:* V' &amp;amp;sube; V &lt;br /&gt;
:* E' &amp;amp;sub; E &lt;br /&gt;
:Er heißt ''(auf)spannender Teilgraph'', wenn gilt:&lt;br /&gt;
:* V' = V&lt;br /&gt;
:Er heißt ''induzierter Teilgraph'', wenn gilt:&lt;br /&gt;
:* e = (u,v) ∈ E' &amp;amp;sub; E &amp;amp;hArr; u ∈ V' und v ∈ V'&lt;br /&gt;
:Den von V' induzierten Teilgraphen erhält man also, indem man aus G alle Knoten löscht, die nicht in V' sind, sowie alle Kanten (und nur diese Kanten), die einen der gelöschten Knoten als Endknoten haben.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Wege, Pfade, Zyklen, Kreise, Erreichbarkeit: Sei G = (V,E) ein Graph (ungerichtet oder gerichteter) Graph. Dann gilt folgende rekursive Definition:&lt;br /&gt;
:* Für v ∈ V ist (v) ein Weg der Länge 0 in G&lt;br /&gt;
:* Falls &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1})&amp;lt;/math&amp;gt; ein Weg ist, und eine Kante &amp;lt;math&amp;gt;(v_{n-1}, v_n)\in E&amp;lt;/math&amp;gt; existiert, dann ist auch &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ein Weg, und er hat die Länge n. &lt;br /&gt;
: Ein Weg ist also eine nichtleere Folge von Knoten, so dass aufeinander folgende Knoten stets durch eine Kante verbunden sind. Die Länge des Weges entspricht der Anzahl der Kanten im Weg (= Anzahl der Knoten - 1).&lt;br /&gt;
:* Ein ''Pfad'' &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ist ein Weg, bei dem alle Knoten v&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; verschieden sind.&lt;br /&gt;
:* ''Ein Zyklus'' &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ist ein Weg, der zum Ausgangspunkt zurückkehrt, wenn also v&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; = v&amp;lt;sub&amp;gt;n&amp;lt;/sub&amp;gt; gilt.&lt;br /&gt;
:* Ein ''Kreis'' ist ein Zyklus ohne Überkreuzungen. Das heisst, es gilt v&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; = v&amp;lt;sub&amp;gt;n&amp;lt;/sub&amp;gt; und &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1})&amp;lt;/math&amp;gt; ist ein Pfad.&lt;br /&gt;
:* Ein Knoten w ∈ V ist von einem anderen Knoten v ∈ V aus ''erreichbar'' genau dann, wenn ein Weg (v, ..., w) existiert. Wir schreiben dann &amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt;.&lt;br /&gt;
In einem ungerichteten Graph ist die Erreichbarkeits-Relation stets symmetrisch, das heisst aus &amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; folgt &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. In einem gerichteten Graphen ist dies im allgemeinen nicht der Fall.&lt;br /&gt;
&lt;br /&gt;
Bestimmte Wege haben spezielle Namen&lt;br /&gt;
&lt;br /&gt;
;Eulerweg: Ein Eulerweg ist ein Weg, der alle '''Kanten''' genau einmal enthält.&lt;br /&gt;
&lt;br /&gt;
Die eingangs erwähnte Frage des Königsberger Brückenproblems ist equivalent zu der Frage, ob der dazugehörige Graph einen Eulerweg besitzt (daher der Name). Ein anderes bekanntes Beispiel ist das &amp;quot;Haus vom Nikolaus&amp;quot;: Wenn man diesen Graphen in üblicher Weise in einem Zug zeichnet, erhält man gerade den Eulerweg. &lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O----O&lt;br /&gt;
  | \/ |&lt;br /&gt;
  | /\ |   &amp;quot;Das Haus vom Nikolaus&amp;quot;: Alle ''Kanten'' werden nur ''einmal'' passiert&lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Hamiltonweg: Ein Hamiltonweg ist ein Weg, der alle '''Knoten''' genau einmal enthält. Das &amp;quot;Haus vom Nikolaus&amp;quot; besitzt auch einen Hamiltonweg:&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /   &lt;br /&gt;
  O----O&lt;br /&gt;
     /  &lt;br /&gt;
    /      Alle ''Knoten'' werden nur ''einmal'' passiert&lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Hamiltonkreis: Ein Hamiltonkreis ist ein Kreis, der alle '''Knoten''' genau einmal enthält. Auch ein solches Gebilde ist im Haus von Nilolaus enthalten:&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O    O&lt;br /&gt;
  |    |   v0 = vn&lt;br /&gt;
  |    |   vi != vj   Für Alle i,j   i !=j; i,j &amp;gt;0; i,j &amp;lt; n&lt;br /&gt;
  O----O     &lt;br /&gt;
&lt;br /&gt;
Die folgende Skizze zeigt hingegen einen Zyklus: Der Knoten rechts unten sowie die untere Kante sind zweimal enthalten (die Kante einmal von links nach rechts und einmal von rechts nach links):&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O    O&lt;br /&gt;
    \  |&lt;br /&gt;
     \ |   Zyklus&lt;br /&gt;
  O====O&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Zusammenhang, Zusammenhangskomponenten: Ein ungerichteter Graph G heißt ''zusammenhängend'', wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt;&lt;br /&gt;
:Ein gerichteter Graph G ist zusammenhängend, wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; '''oder''' &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. &lt;br /&gt;
:Er ist ''stark zusammenhängend'', wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; '''und''' &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. &lt;br /&gt;
:Entsprechende Definitionen gelten für Teilgraphen G'. Ein Teilgraph G' heisst ''Zusammenhangskomponente'' von G, wenn er ein ''maximaler'' zusammenhängender Teilgraph ist, d.h. wenn G' zusammenhängend ist, und man keine Knoten und Kanten aus G mehr zu G' hinzufügen kann, so dass G' immer noch zusammenhängend bleibt. Entsprechend definiert man ''starke Zusammenhangskomponenten'' in einem gerichteten Graphen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Planarer Graph, ebener Graph: Ein Graph heißt ''planar'', wenn er so in einer Ebene gezeichnet werden ''kann'', dass sich die Kanten nicht schneiden (außer an den Knoten). Ein Graph heißt ''eben'', wenn er tatsächlich so gezeichnet ''ist'', dass sich die Kanten nicht schneiden. Die Einbettung in die Ebene ist im allgemeinen nicht eindeutig.&lt;br /&gt;
&lt;br /&gt;
'''Beispiele:'''&lt;br /&gt;
&lt;br /&gt;
Der folgende Graph ist planar und eben:&lt;br /&gt;
 &lt;br /&gt;
      O&lt;br /&gt;
     /|\&lt;br /&gt;
    / O \&lt;br /&gt;
   / / \ \&lt;br /&gt;
   O     O&lt;br /&gt;
&lt;br /&gt;
Das &amp;quot;Haus vom Nikolaus&amp;quot; ist ebenfalls planar, wird aber üblicherweise nicht als ebener Graph gezeichnet, weil sich die Diagonalen auf der Wand überkreuzen:&lt;br /&gt;
 &lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O----O&lt;br /&gt;
  | \/ |&lt;br /&gt;
  | /\ |   &lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
Eine ebene Einbettung dieses Graphen wird erreicht, wenn man eine der Diagonalen ausserhalb des Hauses zeichnet. Der Graph (also die Menge der Knoten und Kanten) ändert sich dadurch nicht.&lt;br /&gt;
 &lt;br /&gt;
      O  &lt;br /&gt;
     /  \&lt;br /&gt;
  --O----O&lt;br /&gt;
 /  |  / |&lt;br /&gt;
 |  | /  |   &lt;br /&gt;
 |  O----O      Das &amp;quot;Haus vom Nikolaus&amp;quot; als ebener Graph gezeichnet.&lt;br /&gt;
  \     /&lt;br /&gt;
   -----&lt;br /&gt;
&lt;br /&gt;
Eine alternative Einbettung erhalten wir, wenn wir die andere Diagonale außerhalb des Hauses zeichnen:&lt;br /&gt;
 &lt;br /&gt;
      O  &lt;br /&gt;
     /  \&lt;br /&gt;
    O----O--|&lt;br /&gt;
    | \  |  |&lt;br /&gt;
    |  \ |  | &lt;br /&gt;
    O----O  |     Alternative Einbettung des &amp;quot;Haus vom Nikolaus&amp;quot;.&lt;br /&gt;
    |       |&lt;br /&gt;
    |-------|&lt;br /&gt;
&lt;br /&gt;
Jede Einbettung eines planaren Graphen (also jeder ebene Graph) definiert eine eindeutige Menge von ''Regionen'':&lt;br /&gt;
&lt;br /&gt;
 |----O   @&lt;br /&gt;
 |   /@ \&lt;br /&gt;
 |  O----O&lt;br /&gt;
 |  |@ / |&lt;br /&gt;
 |  | / @|   &lt;br /&gt;
 |  O----O        @ entspricht jeweils einer ''Region''. Auch ausserhalb der Figur ist eine Region (die sogenannte ''unendliche'' Region).&lt;br /&gt;
 |@      |&lt;br /&gt;
 |-------|&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Der vollständige Graph K5 ist kein planarer Graph, da sich zwangsweise Kanten schneiden, wenn man diesen Graphen in der Ebene zeichnet.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
;Dualer Graph: Jeder ebene Graph G = (V, E) hat einen ''dualen Graphen'' D = (V&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;, E&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;), dessen Knoten und Kanten wie folgt definiert sind:&lt;br /&gt;
:* V&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt; enthält einen Knoten für jede Region des Graphen G&lt;br /&gt;
:* Für jede Kante e ∈ E gibt es eine duale Kante e&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt; ∈ E&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;, die die an e angrenzenden Regionen (genauer: die entsprechenden Knoten in D) verbindet.&lt;br /&gt;
&lt;br /&gt;
Die folgende Abbildung zeigt einen Graphen (grau) und seinen dualen Graphen (schwarz). Die Knoten des dualen Graphen sind mit Zahlen gekennzeichnet und entsprechen den Regionen des Originalgraphen. Jeder (grauen) Kante des Originalgraphen entspricht eine (schwarze) Kante des dualen Graphen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
[[Image:dual-graphs.png]]&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für duale Graphen gilt: Wenn der Originalgraph zusammenhängend ist, enthält jede Region des dualen Graphen genau einen Knoten des Originalgraphen. Deshalb ist der duale Graph des dualen Graphen wieder der Originalgraph. Bei nicht-zusammenhängenden Graphen gilt dies nicht (vgl. das Fenster bei obigem Bild). In diesem Fall hat der duale Graph mehrere mögliche Einbettungen in die Ebene (man kann z.B. die rechte Kante zwischen Knoten 2 und 4 auch links vom Fenster einzeichnen), und man erhält nicht notwendigerweise den Originalgraphen, wenn man den dualen Graphen des dualen berechnet.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Baum: Ein ''Baum'' ist ein zusammenhängender, kreisfreier Graph.&lt;br /&gt;
&lt;br /&gt;
Beispiel: Binärer Suchbaum&lt;br /&gt;
&lt;br /&gt;
;Spannbaum: Ein ''Spannbaum'' eines zusammenhängenden Graphen G ist ein zusammenhängender, kreisfreier Teilgraph von G, der alle Knoten von G enthält&lt;br /&gt;
&lt;br /&gt;
Beispiel: Spannbaum für das &amp;quot;Haus des Nikolaus&amp;quot; &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    O   &lt;br /&gt;
   /       &lt;br /&gt;
  O    O&lt;br /&gt;
  |  /  &lt;br /&gt;
  | /   &lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
Der Spannbaum eines Graphen mit |V| Knoten hat stets |V| - 1 Kanten.&lt;br /&gt;
&lt;br /&gt;
;Wald: Ein ''Wald'' ist ein unzusammenhängender, kreisfreier Graph.&lt;br /&gt;
: Jede Zusammenhangskomponente eines Waldes ist ein Baum.&lt;br /&gt;
&lt;br /&gt;
=== Repräsentation von Graphen ===&lt;br /&gt;
&lt;br /&gt;
Sei G = ( V, E ) gegeben und liege V in einer linearen Sortierung vor.&amp;lt;br/&amp;gt; &lt;br /&gt;
:::&amp;lt;math&amp;gt;V = \{ v_1, ...., v_n \}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Adjazenzmatrix: Ein Graph kann durch eine Adjazenzmatrix repräsentiert werden, die soviele Zeilen und Spalten enthält, wie der Graph Knoten hat. Die Elemente der Adjazenzmatrix sind &amp;quot;1&amp;quot;, falls eine Kante zwischen den zugehörigen Knoten existiert:&lt;br /&gt;
:::&amp;lt;math&amp;gt;\mathrm{\bold A} = a_{ij} = &lt;br /&gt;
\begin{cases}&lt;br /&gt;
1 &amp;amp; \mathrm{falls}\quad (v_i, v_j) \in E \\&lt;br /&gt;
0 &amp;amp; \mathrm{sonst}&lt;br /&gt;
\end{cases} &lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
:Die Indizes der Matrix entsprechen also den Indizes der Knoten gemäß der gegebenen Sortierung. Im Falle eines ungerichteten Graphen ist die Adjazenzmatrix stets symmetrisch (d.h. es gilt &amp;lt;math&amp;gt;a_{ij}=a_{ji}&amp;lt;/math&amp;gt;), bei einem gerichteten Graphen ist sie im allgemeinen unsymmetrisch.&lt;br /&gt;
&lt;br /&gt;
Beispiel für einen ungerichteten Graphen:&lt;br /&gt;
&lt;br /&gt;
 v = { a,b,c,d }     b      d&lt;br /&gt;
                     | \  / |&lt;br /&gt;
                     |  \/  |&lt;br /&gt;
                     |  /\  |&lt;br /&gt;
                     | /  \ |&lt;br /&gt;
                     a      c&lt;br /&gt;
 &lt;br /&gt;
       a b c d&lt;br /&gt;
      -----------&lt;br /&gt;
      (0 1 0 1) |a &lt;br /&gt;
  A = (1 0 1 0) |b&lt;br /&gt;
      (0 1 0 1) |c&lt;br /&gt;
      (1 0 1 0) |d&lt;br /&gt;
&lt;br /&gt;
Die Adjazenzmatrixdarstellung eignet sich besonders für dichte Graphen (d.h. wenn die Zahl der Kanten in O(|V|&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt;) ist.&lt;br /&gt;
&lt;br /&gt;
;Adjazenzlisten: In der Adjazenzlistendarstellung wird der Graph als Liste von Knoten repräsentiert, die für jeden Knoten einen Eintrag enthält. Der Eintrag für jeden Knoten ist wiederum eine Liste, die die Nachbarknoten dieses Knotens enthält:&lt;br /&gt;
:* graph = {adjazencyList(v) | v ∈ V}&lt;br /&gt;
:* adjazencyList(v) = {v' ∈ V | (v, v') ∈ E}&lt;br /&gt;
&lt;br /&gt;
In Python implementieren wir Adjazenzlisten zweckmäßig als Array von Arrays:&lt;br /&gt;
&lt;br /&gt;
                   graph = [[...],[...],...,[...]]&lt;br /&gt;
 Adjazenzliste für Knoten =&amp;gt;  0     1         n&lt;br /&gt;
&lt;br /&gt;
Wenn wir bei dem Graphen oben die Knoten wie bei der Adjazenzmatrix indizieren (also &amp;lt;tt&amp;gt;a =&amp;gt; 0&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;b =&amp;gt; 1&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;c =&amp;gt; 2&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;d =&amp;gt; 3&amp;lt;/tt&amp;gt;), erhalten wir die Adjazenzlistendarstellung:&lt;br /&gt;
&lt;br /&gt;
 graph = [[b, d], [a, c],[b, d], [a, c]]&lt;br /&gt;
&lt;br /&gt;
Auf die Nachbarknoten eines durch seinen Index &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; gegebenen Knotens können wir also wie folgt zugreifen:&lt;br /&gt;
&lt;br /&gt;
      for neighbors in graph[node]:&lt;br /&gt;
          ... # do something with neighbor&lt;br /&gt;
&lt;br /&gt;
Die Adjazenzlistendarstellung ist effizienter, wenn der Graph nicht dicht ist, so dass viele Einträge der Adjazenzmatrix Null wären. In der Vorlesung werden wir nur diese Darstellung verwenden.&lt;br /&gt;
&lt;br /&gt;
;&amp;lt;div id=&amp;quot;transposed_graph&amp;quot;&amp;gt;Transponierter Graph&amp;lt;/div&amp;gt;: Den ''transponierten Graphen'' G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; eines gerichteten Graphen G erhält man, wenn man alle Kantenrichtungen umkehrt.&lt;br /&gt;
&lt;br /&gt;
Bei ungerichteten Graphen hat die Transposition offensichtlich keinen Effekt, weil alle Kanten bereits in beiden Richtungen vorhanden sind, so dass G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; = G gilt. Bei gerichteten Graphen ist die Transposition einfach, wenn der Graph als Adjazenzmatrix implementiert ist, weil man einfach die transponierte Adjazenzmatrix verwenden muss (beachte, dass sich die Reihenfolge der Indizes umkehrt):&lt;br /&gt;
:::A&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; = a&amp;lt;sub&amp;gt;ji&amp;lt;/sub&amp;gt;&lt;br /&gt;
Ist der Graph hingegen durch eine Adjazenzliste repräsentiert, muss etwas mehr Aufwand getrieben werden:&lt;br /&gt;
&lt;br /&gt;
 def transposeGraph(graph):&lt;br /&gt;
      gt = [[] for k in graph]   # zunächst leere Adjazenzlisten von G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt;&lt;br /&gt;
      for node in range(len(graph)):&lt;br /&gt;
           for neighbor in graph[node]:&lt;br /&gt;
               gt[neighbor].append(node)  # füge die umgekehrte Kante in G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; ein&lt;br /&gt;
      return gt&lt;br /&gt;
&lt;br /&gt;
== Durchlaufen von Graphen (Graph Traversal) ==&lt;br /&gt;
&lt;br /&gt;
Wir betrachten zunächst ungerichtete Graphen mit V Knoten und E Kanten. Eine grundlegende Aufgabe in diesen Graphen besteht darin, alle Knoten in einer bestimmten Reihenfolge genau einmal zu besuchen. Hierbei darf man sich von einem gegebenen Startknoten aus nur entlang der Kanten des Graphen bewegen. Die beim Traversieren benutzen Kanten bilden einen Baum, dessen Wurzel der Startknoten ist und der den gesamten Graphen aufspannt, falls der Graph zusammenhängend ist. (Beweis: Da jeder Knoten nur einmal besucht wird, gibt es für jeden besuchten Knoten [mit Ausnahme des Startknotens] genau eine eingehende Kante. Ist der Graph zusammenhängend, wird jeder Knoten tatsächlich erreicht und es gibt genau (V-1) Kanten, exakt soviele wie für einen Baum mit V Knoten notwendig sind.) Ist der Graph nicht zusammenhängend, wird jeder zusammenhängende Teilgraph (jede &amp;lt;i&amp;gt;Zusammenhangskomponente&amp;lt;/i&amp;gt;) getrennt traversiert, und man erhält einen sogenannten &amp;lt;i&amp;gt;Wald&amp;lt;/i&amp;gt; mit einem Baum pro Zusammenhangskomponente. Die beiden grundlegenden Traversierungsmethoden &amp;lt;i&amp;gt;Tiefensuche&amp;lt;/i&amp;gt; und &amp;lt;i&amp;gt;Breitensuche&amp;lt;/i&amp;gt; werden im folgenden vorgestellt.&lt;br /&gt;
&lt;br /&gt;
=== Tiefensuche in Graphen (Depth First Search, DFS) ===&lt;br /&gt;
&lt;br /&gt;
Die Idee der Tiefensuche besteht darin, jeden besuchten Knoten sofort über die erste Kante wieder zu verlassen, die zu einem noch nicht besuchten Knoten führt. Man findet dadurch schnell einen möglichst langen Pfad durch den Graphen, und der Traversierungs-Baum wird zunächst in die Tiefe verfolgt, daher der Name des Verfahrens. Hat ein Knoten keine unbesuchten Nachbarknoten mehr, geht man im Baum zurück (sogenanntes &amp;lt;i&amp;gt;back tracking&amp;lt;/i&amp;gt;), bis man einen Knoten findet, der noch eine unbesuchte Nachbarn besitzt, und traversiert diese nach dem gleichen Muster. Gibt es gar keine unbesuchten Knoten mehr, kehrt die Suche zum Startknoten zurück und endet dort.&lt;br /&gt;
&lt;br /&gt;
WDie folgende rekursive Implementation der Tiefensuche erwartet den Graphen in Adjazenzlistendarstellung und beginnt die Suche beim Knoten &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt;. Die Information, ob ein Knoten bereits besucht wurde, wird im Array &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; gespeichert. Ein solches Array, das zusätzliche Informationen über die Knoten des Graphen bereitstellt, wir häufig &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; genannt.&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)  # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):              # rekursive Hilfsfunktion, die den gegebenen Knoten und dessen Nachbarn besucht&lt;br /&gt;
         if not visited[node]:     # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True  # Markiere node als besucht&lt;br /&gt;
             print node            # Ausgabe der Knotennummer - pre-order&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode)&lt;br /&gt;
&lt;br /&gt;
[[Image:Tiefens.jpg]]&lt;br /&gt;
&lt;br /&gt;
Ausgabe für den Graphen in diesem Bild (es handelt sich um einen ungerichteten Graphen, die Pfeile symbolisieren nur die Suchrichtung beim Traversal):&lt;br /&gt;
&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; dfs(graph, 1)&lt;br /&gt;
 1&lt;br /&gt;
 2&lt;br /&gt;
 4&lt;br /&gt;
 3&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
 5&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div id=&amp;quot;pre_and_post_order&amp;quot;&amp;gt;In dieser Version des Algorithmus werden die Knotennummern ausgegeben, bevor die Nachbarknoten besucht werden. Man bezeichnet die resultierende Sortierung der Knoten als &amp;lt;b&amp;gt;pre-order&amp;lt;/b&amp;gt; oder als &amp;lt;b&amp;gt;discovery order&amp;lt;/b&amp;gt;. Alternativ kann man die Knotennummern erst ausgeben, nachdem alle Nachbarn besucht wurden, also auf dem Rückweg der Rekursion. In diesem Fall spricht man von &amp;lt;b&amp;gt;post-order&amp;lt;/b&amp;gt; oder &amp;lt;b&amp;gt;finishing order&amp;lt;/b&amp;gt;:&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)  # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):              # rekursive Hilfsfunktion, die den gegebenen Knoten und dessen Nachbarn besucht&lt;br /&gt;
         if not visited[node]:     # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True  # Markiere node als besucht&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
             &amp;lt;font color=red&amp;gt;print node            # Ausgabe der Knotennummer - post-order&amp;lt;/font&amp;gt;&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode)&lt;br /&gt;
&lt;br /&gt;
Es ergibt sich jetzt die Ausgabe:&lt;br /&gt;
&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; dfs(graph, 1)&amp;lt;font color=red&amp;gt;&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
 3&lt;br /&gt;
 4&lt;br /&gt;
 5&lt;br /&gt;
 2&lt;br /&gt;
 1&amp;lt;/font&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In realem Code ersetzt man die print-Ausgaben natürlich durch anwendungsspezifische Aktionen und Berechnungen. Einige Anwendungen sind uns im Kapitel [[Suchen]] bereits begegnet. &lt;br /&gt;
; Anwendungen der Pre-Order Traversierung&lt;br /&gt;
* Kopieren eines Graphen: kopiere zuerst den besuchten Knoten, dann seine Nachbarn und die dazugehörigen Kanten (sowie die Kanten zu bereits besuchten Knoten, die in der Grundversion der Tiefensuche ignoriert werden).&lt;br /&gt;
* Bestimmen der Zusammenhangskomponenten eines Graphen (siehe unten)&lt;br /&gt;
* In einem Zeichenprogramm: fülle eine Region mit einer Farbe (&amp;quot;flood fill&amp;quot;). Dabei ist jedes Pixel ein Knoten des Graphen und wird mit seinen 4 Nachbarpixceln verbunden. Die Tiefensuche startet bei der Mausposition und endet am Rand des betreffendcen Gebiets.&lt;br /&gt;
* Falls der Graph ein Baum ist: bestimme den Abstand jedes Knotens von der Wurzel&lt;br /&gt;
* Falls der Graph ein Parse-Baum ist, wobei innere Knoten Funktionsaufrufe, Kindknoten Funktionsargumente, und Blattknoten Werte repräsentieren: drucke den zugehörigen Ausdruck aus (also immer zuerst den Funktionsnamen, dann die Argumente, die wiederum geschachtelte Funktionsaufrufe sein können).&lt;br /&gt;
; Anwendungen der Post-Order Traversierung&lt;br /&gt;
* Löschen eines Graphen: lösche zuerst die Nachbarn, dann den Knoten selbst&lt;br /&gt;
* Bestimmen einer topologischen Sortierung eines azyklischen gerichteten Graphens (siehe unten)&lt;br /&gt;
* Falls der Graph ein Baum ist: bestimme den Abstand jedes Knotens von den Blättern (also die Tiefe des Baumes, siehe Übung 5)&lt;br /&gt;
* Falls der Graph ein Parse-Baum ist: führe die zugehörige Berechnung aus (d.h. berechne zuerst die geschachtelten inneren Funktionen, dann mit diesen Ergebnissen die nächst äußeren usw., siehe Übung 5).&lt;br /&gt;
; Anwendungen, die Pre- und Post-Order benötigen&lt;br /&gt;
* Weg aus einem Labyrinth: die Pre-Order dokumentiert die Suche nach dem Weg, die Post-Order zeigt den Rückweg aus Sackgassen (siehe Übung 9).&lt;br /&gt;
Im Spezialfall, wenn der Graph ein Binärbaum ist, unterscheidet man noch eine dritte Variante der Traversierung, nämlich die &amp;lt;i&amp;gt;in-order&amp;lt;/i&amp;gt; Traversierung. In diesem Fall behandelt man den Vaterknoten nach den linken, aber vor den rechten Kindern. Diese Reihenfolge wird beim [[Suchen#Beziehungen zwischen dem Suchproblem und dem Sortierproblem|Tree Sort Algorithmus]] verwendet. Diese Sortierung verwendet man auch, wenn man einen Parse-Baum mit binären Operatoren (statt Funktionsaufrufen) ausgeben will, siehe Übung 5.&lt;br /&gt;
&lt;br /&gt;
Eine nützliche Erweiterung der Tiefensuche besteht darin, in der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; nicht nur zu dokumentieren, dass ein Knoten bereits besucht wurde, sondern auch, von welchem Knoten aus man den jeweiligen Knoten zuerst erreicht hat. Im entstehenden Tiefensuchbaum ist dies gerade der Vaterknoten, weshalb wir die verbesserte property map zweckmäßigerweise in &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; umbenennen. Für den Startknoten, also die Wurzel des Baumes, wählen wir die Konvention, dass er sein eigener Vaterknoten ist (die Konvention, dafür den Wert &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zu verwenden, scheidet aus, weil dies bereits die Tatsache signalisiert, dass ein Knoten noch nicht besucht wurde):&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     parents = [None]*len(graph)     # Registriere für jeden Knoten den Vaterknoten im Tiefensuchbaum&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, parent):        # rekursive Hilfsfunktion&lt;br /&gt;
         if parents[node] is None:   # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             parents[node] = parent  # Markiere node als besucht und speichere seinen Vaterknoten&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn ...&lt;br /&gt;
                 visit(neighbor, node)      #  ... wobei node zu deren Vaterknoten wird&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode, startnode)     # Konvention für Wurzel: startnode ist sein eigener Vater&lt;br /&gt;
     &lt;br /&gt;
     return parents                  # Rückgabe des berechneten Tiefensuch-Baums&lt;br /&gt;
&lt;br /&gt;
Die Ausgabe für den obigen Beispielgraphen lautet: &lt;br /&gt;
  Knotennummer  |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7&lt;br /&gt;
  --------------+-----+-----+-----+-----+-----+-----+-----+-----&lt;br /&gt;
  Vaterknoten   | None|  1  |  1  |  4  |  2  |  2  |  3  |  3&lt;br /&gt;
&lt;br /&gt;
Dabei ist die Knotennummer der Index im Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt;, und der Vaterknoten ist der dazugehörige Arrayeintrag. Beachte, dass Knoten 0 in diesem Graphen nicht existiert, daher ist sein Eintrag &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;. Per Konvention hat der Wurzelknoten 1 sich selbst als Vater.&lt;br /&gt;
&lt;br /&gt;
=== Breitensuche in Graphen (Breadth First Search, BFS) ===&lt;br /&gt;
&lt;br /&gt;
Im Gegensatz zur Tiefensuche werden bei der Breitensuche alle Nachbarnknoten abgearbeitet, &amp;lt;i&amp;gt;bevor&amp;lt;/i&amp;gt; man rekursiv deren Nachbarn besucht. Man betrachtet somit zuerst alle Knoten, die den Abstand 1 von Startknoten haben, dann diejenigen mit dem Abstand 2 usw. Diese Reihenfolge bezeichnet man als &amp;lt;i&amp;gt;level-order&amp;lt;/i&amp;gt;. Wir sind ihr beispielsweise in Übung 6 begegnet, als die ersten 7 Ebenen eines Treap ausgegeben werden sollten. Man implementiert Breitensuche zweckmäßig mit Hilfe einer Queue, die die Knoten in First In - First Out - Reihenfolge bearbeitet. Eine geeignete Datenstruktur hierfür ist die Klasse &amp;lt;tt&amp;gt;[http://docs.python.org/library/collections.html#collections.deque deque]&amp;lt;/tt&amp;gt; aus dem Python-Modul &amp;lt;tt&amp;gt;[http://docs.python.org/library/collections.html collections]&amp;lt;/tt&amp;gt; (eine Deque implementiert sowohl die Funktionalität einer Queue wie auch die eines Stacks, siehe Übung 3):&lt;br /&gt;
&lt;br /&gt;
 from collections import deque&lt;br /&gt;
 &lt;br /&gt;
 def bfs(graph, startnode)&lt;br /&gt;
     visited = [False]*len(graph)   # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
   &lt;br /&gt;
     q = deque()                    # Queue für die zu besuchenden Knoten&lt;br /&gt;
     q.append(startnode)            # Startknoten in die Queue einfügen&lt;br /&gt;
     &lt;br /&gt;
     while len(q) &amp;gt; 0:              # Solange es noch unbesuchte Knoten gibt&lt;br /&gt;
         node = q.popleft()         # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
         if not visited[node]:      # Falls node noch nicht (auf einem anderen Weg) besucht wurde&lt;br /&gt;
              visited[node] = True  # Markiere node als besucht&lt;br /&gt;
              print node            # Drucke Knotennummer&lt;br /&gt;
              for neighbor in graph[node]:    # Füge Nachbarn in die Queue ein&lt;br /&gt;
                  q.append(neighbor)&lt;br /&gt;
&lt;br /&gt;
[[Image:Breitens.jpg]]&lt;br /&gt;
&lt;br /&gt;
Der Aufruf dieser Funktion liefert die Knoten des obigen Graphens ebenenweise, also zufällig genau in der Reihenfolge der Knotennummern:&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; bfs(graph, 1)&lt;br /&gt;
 1&lt;br /&gt;
 2&lt;br /&gt;
 3&lt;br /&gt;
 4&lt;br /&gt;
 5&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
&lt;br /&gt;
Neben der ebenenweisen Ausgabe hat die Breitensuche viele weitere wichtige Anwendungen, z.B. beim Testen, ob ein gegebener Graph bi-partit ist (siehe [http://en.wikipedia.org/wiki/Breadth-first_search#Testing_bipartiteness WikiPedia]), sowie bei der Suche nach kürzesten Wegen (siehe unten) und kürzesten Zyklen.&lt;br /&gt;
&lt;br /&gt;
== Weitere Anwendungen der Tiefensuche ==&lt;br /&gt;
&lt;br /&gt;
Die Tiefensuche hat zahlreiche Anwendungen, wobei der grundlegende Algorithmus immer wieder leicht modifiziert und an die jeweilige Aufgabe angepasst wird. Wir beschreiben im folgenden einige Beispiele.&lt;br /&gt;
&lt;br /&gt;
=== Damenproblem ===&lt;br /&gt;
&lt;br /&gt;
Tiefensuche wird häufig verwendet, um systematisch nach der Lösung eines logischen Rätsels (oder allgemeiner nach der Lösung eines diskreten Optimierungsproblems) zu suchen. Besonders anschaulich hierfür ist das Damenproblem. Die Aufgabe besteht darin, &amp;lt;math&amp;gt;k&amp;lt;/math&amp;gt; Damen auf einem Schachbrett der Größe &amp;lt;math&amp;gt;k \times k&amp;lt;/math&amp;gt; so zu platzieren, dass sie sich (nach den üblichen Schach-Regeln) nicht gegenseitig schlagen können. Das folgende Diagramm zeigt eine Lösung für den Fall &amp;lt;math&amp;gt;k=4&amp;lt;/math&amp;gt;. Die Positionen der Damen werden dabei wie üblich durch die Angabe der Spalte (Linie) mit Buchstaben und der Zeile (Reihe) mit Zahlen kodiert, hier also A2, B4, C1, D3:&lt;br /&gt;
&lt;br /&gt;
  ---------------&lt;br /&gt;
 |   | X |   |   | 4&lt;br /&gt;
 |---|---|---|---| &lt;br /&gt;
 |   |   |   | X | 3&lt;br /&gt;
 |---|---|---|---|&lt;br /&gt;
 | X |   |   |   | 2&lt;br /&gt;
 |---|---|---|---|&lt;br /&gt;
 |   |   | X |   | 1&lt;br /&gt;
  ---------------&lt;br /&gt;
   A   B   C   D&lt;br /&gt;
&lt;br /&gt;
Um das Problem systematisch zu lösen, konstruieren wir einen gerichteten Graphen, dessen Knoten die möglichen Positionen der Damen kodieren. Wir verbinden Knoten, die zu benachbarten Linien gehören, genau dann mit einer Kante, wenn die zugehörigen Positionen kompatibel sind, also wenn sich die dort positionierten Damen nicht schlagen können. Der resultierende Graph für &amp;lt;math&amp;gt;k=4&amp;lt;/math&amp;gt; hat folgende Gestalt:&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
Knoten, die zur selben Reihe oder Linie gehören, sind beispielsweise nicht direkt verbunden, weil zwei Damen niemals in derselben Linie oder Reihe stehen dürfen. Um eine erlaubte Konfiguration zu finden, verwenden wir nun eine angepasste Version der Tiefensuche: Wir beginnen die Suche beim Knoten &amp;lt;tt&amp;gt;START&amp;lt;/tt&amp;gt;. Sobald wir den Knoten &amp;lt;tt&amp;gt;STOP&amp;lt;/tt&amp;gt; erreichen, beenden wir die Suche und lesen die Lösung am gerade gefundenen Weg von Start nach Stop ab. Zwei kleine Modifikationen des Grundalgorithmus stellen sicher, dass die Bedingungen der Aufgabe eingehalten werden: Wir dürfen bei der Tiefensuche nur dann zu einem Nachbarn weitergehen, wenn die betreffende Position mit allen im Pfad bereits gesetzten Positionen kompatibel ist, andernfalls ist diese Kante tabu. Landen wir aufgrund dieser Regel in einer Sackgasse (also in einem Knoten, wo keine der ausgehenden Kanten erlaubt ist), müssen wir zur nächsten erlaubten Abzweigung zurückgehen (Backtracking). Beim Zurückgehen müssen wir das &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Flag wieder auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zurücksetzen, weil der betreffende Knoten ja möglicherweise auf einem anderen erlaubten Weg erreichbar ist.&lt;br /&gt;
&lt;br /&gt;
Der folgende Graph zeigt einen solchen Fall: Wir haben zwei Damen auf die Felder A1 und B3 positioniert (grüne Pfeile). Die einzig ausgehende Kante von B3 führt zum Knoten C1, welcher aber mit der Position A1 inkompatibel ist, so dass diese Kante nicht verwendet werden darf (roter Pfeil). Das Backtracking muss jetzt zu Knoten A1 zurückgehen (dabei wird das &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Flag von B3 wieder auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; gesetzt), weil A1 mit der Kante nach B4 eine weitere Option hat, die geprüft werden muss (die allerdings hier auch nicht zum Ziel führt).&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph-failure.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
Nach einigen weiteren Sackgassen findet man schließlich den Pfad A2, B4, C1, D3, der im folgenden Graphen grün markiert ist und der obigen Lösung entspricht:&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph-success.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
=== Test, ob ein ungerichteter Graph azyklisch ist ===&lt;br /&gt;
&lt;br /&gt;
Ein zusammenhängender ungerichteter Graph ist azyklisch (also ein Baum) genau dann, wenn es nur einen möglichen Weg von jedem Knoten zu jedem anderen gibt. (Bei gerichteten Graphen sind die Verhältnisse komplizierter. Wir behandeln dies weiter unten.) Das kann man mittels Tiefensuche leicht feststellen: Die Kante, über die wir einen Knoten erstmals erreichen, ist eine &amp;lt;i&amp;gt;Baumkante&amp;lt;/i&amp;gt; des Tiefensuchbaums. Erreichen wir einen bereits besuchten Knoten nochmals über eine andere Kante, haben wir einen Zyklus gefunden. Dabei müssen wir allerdings beachten, dass in einem ungerichteten Graphen jede Baumkante zweimal gefunden wird, einmal in Richtung vom Vater zum Kind und einmal in umgekehrter Richtung. Im zweiten Fall endet die Kante zwar in einem bereits besuchten Knoten (dem Vater), aber es entsteht dadurch kein Zyklus. Den Vaterknoten müssen wir deshalb überspringen, wenn wir über die Nachbarn iterieren:&lt;br /&gt;
&lt;br /&gt;
 def undirected_cycle_test(graph):         # Annahme: der Graph ist zusammenhängend&lt;br /&gt;
                                           # (andernfalls führe den Algorithmus für jede Zusammenhangskomponente aus)&lt;br /&gt;
     visited = [False]*len(graph)          # Flags für bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, from_node):           # rekursive Hilfsfunktion: gibt True zurück, wenn Zyklus gefunden wurde&lt;br /&gt;
         if not visited[node]:             # wenn node noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True          # markiere node als besucht&lt;br /&gt;
             for neighbor in graph[node]:  # besuche die Nachbarn ...&lt;br /&gt;
                 if neighbor == from_node: # ... aber überspringe den Vaterknoten&lt;br /&gt;
                     continue&lt;br /&gt;
                 if visit(neighbor, node): # ... signalisiere, wenn rekursiv ein Zyklus gefunden wurde&lt;br /&gt;
                     return True&lt;br /&gt;
             return False                  # kein Zyklus gefunden&lt;br /&gt;
         else:&lt;br /&gt;
             return True                   # Knoten schon besucht =&amp;gt; Zyklus&lt;br /&gt;
     &lt;br /&gt;
     startnode = 0                         # starte bei beliebigem Knoten (hier: Knoten 0)&lt;br /&gt;
     return visit(startnode, startnode)    # gebe True zurück, wenn ein Zyklus gefunden wurde&lt;br /&gt;
&lt;br /&gt;
Wenn wir einen Zyklus finden, wird das weitere Traversieren das Graphen abgebrochen, denn ein Graph, der einmal zyklisch war, kann später nicht wieder azyklisch werden. Die notwendige Modifikation für unzusammenhängende Graphen erfolgt analog zum Algorithmus für die Detektion von Zusammenhangskomponenten, der im nächsten Abschnitt beschrieben wird.&lt;br /&gt;
&lt;br /&gt;
=== Finden von Zusammenhangskomponenten ===&lt;br /&gt;
&lt;br /&gt;
Das Auffinden und Markieren von Zusammenhangskomponenten (also maximalen zusammenhängenden Teilgraphen) ist eine grundlegende Aufgabe in ungerichteten, unzusammenhängenden Graphen (bei gerichteten Graphen sind die Verhältnisse wiederum komplizierter, siehe unten). Zwei Knoten u und v gehören zur selben Zusammenhangskomponente genau dann, wenn es einen Pfad von u nach v gibt (da der Graph ungerichtet ist, gibt es dann auch einen Pfad von v nach u). Man sagt auch, dass &amp;quot;v von u aus erreichbar&amp;quot; ist. Unzusammenhängende Graphen entstehen in der Praxis häufig, wenn die Kanten gewisse Relationen zwischen den Knoten kodieren: &lt;br /&gt;
* Wenn die Knoten Städte sind und die Kanten Straßen, sind diejenigen Städte in einer Zusammenhangskomponente, die per Auto von einander erreichbar sind. Unzusammenhängende Graphen entstehen hier beispielsweise, wenn eine Insel nicht durch eine Brücke erschlossen ist, wenn Grenzen gesperrt sind oder wenn ein Gebirge zu unwegsam ist, um Straßen zu bauen.&lt;br /&gt;
* Wenn Knoten Personen sind, und Kanten die Eltern-Kind-Relation beschreiben, so umfasst jede Zusammenhangskomponenten die Verwandten (auch wenn sie nur über viele &amp;quot;Ecken&amp;quot; verwandt sind).&lt;br /&gt;
* In der Bildverarbeitung entsprechen Knoten den Pixeln, und dieselben werden durch eine Kante verbunden, wenn sie zum selben Objekt gehören. Die Zusammenhangskomponenten entsprechen somit den Objekten im Bild (siehe Übungsaufgabe).&lt;br /&gt;
Die Zusammenhangskomponenten bilden eine Äquivalenzrelation. Folglich kann für jede Komponente ein Reprässentant bestimmt werden, der sogenannte &amp;quot;Anker&amp;quot;. Kennt jeder Knoten seinen Anker, ist das Problem der Zusammenhangskomponenten gelöst. &lt;br /&gt;
&lt;br /&gt;
==== Lösung mittels Tiefensuche ====&lt;br /&gt;
&lt;br /&gt;
Unser erster Ansatz ist, den Anker mit Hilfe der Tiefensuche zu finden. Anstelle der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; verwenden wir diesmal eine property map &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt;, die für jeden Knoten die Knotennummer des zugehörigen Ankers angibt, oder &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, wenn der Knoten noch nicht besucht wurde. Dabei verwenden wir wieder die Konvention, dass Anker auf sich selbst zeigen. Für viele Anwendungen ist es außerdem (oder stattdessen) zweckmäßig, die Zusammenhangskomponenten mit einer laufenden Nummer, einem sogenannten &amp;lt;i&amp;gt;Label&amp;lt;/i&amp;gt;, durchzuzählen. Dann kann man zusätzliche Informationen zu jeder Komponente (beispielsweise deren Größe) einfach in einem Array speichern, das über die Labels indexiert wird. Die folgende Version der Tiefensuche bestimmt sowohl die Anker als auch die Labels für jeden Knoten:&lt;br /&gt;
&lt;br /&gt;
 def connectedComponents(graph):&lt;br /&gt;
        anchors = [None] * len(graph)             # property map für Anker jedes Knotens&lt;br /&gt;
        labels  = [None] * len(graph)             # property map für Label jedes Knotens&lt;br /&gt;
        &lt;br /&gt;
        def visit(node, anchor):&lt;br /&gt;
                &amp;quot;&amp;quot;&amp;quot;anchor ist der Anker der aktuellen ZK&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
                if anchors[node] is None:         # wenn node noch nicht besucht wurde:&lt;br /&gt;
                    anchors[node] = anchor        # setze seinen Anker&lt;br /&gt;
                    labels[node] = labels[anchor] # und sein Label&lt;br /&gt;
                    for neighbor in graph[node]:  # und besuche die Nachbarn&lt;br /&gt;
                        visit(neighbor, anchor)&lt;br /&gt;
        &lt;br /&gt;
        current_label = 0                         # Zählung der ZK beginnt bei 0&lt;br /&gt;
        for node in xrange(len(graph)):&lt;br /&gt;
            if anchors[node] is None:             # Anker noch nicht bekannt =&amp;gt; neue ZK gefunden&lt;br /&gt;
                labels[node] = current_label      # Label des Ankers setzen&lt;br /&gt;
                visit(node, node)                 # Knoten der neuen ZK rekursiv suchen&lt;br /&gt;
                current_label += 1                # Label für die nächste ZK hochzählen&lt;br /&gt;
        return anchors, labels&lt;br /&gt;
Interessant ist hier die Schleife über alle Knoten des Graphen am Ende des Algorithmus, die bei den bisherigen Versionen der Tiefensuche nicht vorhanden war. Um ihre Funktionsweise zu verstehen, nehmen wir für den Moment an, dass der Graph zusammenhängend ist. Dann findet diese Schleife den ersten Knoten des Graphen und führt die Tiefensuche mit diesem Knoten als Startknoten aus. Sobald die Rekursion zurückkehrt, sind alle Knoten des Graphen besucht (weil der Graph ja zusammenhängend war), so dass die Schleife alle weiteren Knoten überspringt (die if-Anweisung liefert für keinen weiteren Knoten True). Bei unzusammenhängenden Graphen dagegen erreicht die Tiefensuche nur die Knoten derselben Komponente, die im weiteren Verlauf der Schleife übersprungen werden. Findet die if-Anweisung jetzt einen noch nicht besuchten Knoten, muss dieser folglich in einer neuen Komponente liegen. Wir verwenden diesen Knoten als Anker und bestimmen die übrigen Knoten dieser Komponente wiederum mit Tiefensuche.&lt;br /&gt;
&lt;br /&gt;
* Beispiel: ... &amp;lt;b&amp;gt; under construction &amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Man erkennt, dass die Tiefensuche nach dem &amp;lt;i&amp;gt;Anlagerungsprinzip&amp;lt;/i&amp;gt; vorgeht: Beginnend vom einem Startknoten (dem Anker) werden die Knoten der aktuellen Komponente nach und nach an den Tiefensuchbaum angehangen. Erst, wenn nichts mehr angelagert werden kann, geht der Algorithmus zur nächsten Komponente über.&lt;br /&gt;
&lt;br /&gt;
==== Lösung mittels Union-Find-Algorithmus ====&lt;br /&gt;
&lt;br /&gt;
Im Gegensatz zum Anlagerungsprinzip sucht der Union-Find-Algorithmus die Zusammenhangskomponenten mit dem &amp;lt;i&amp;gt;Verschmelzungsprinzip&amp;lt;/i&amp;gt;: Eingangs wird jeder Knoten als ein Teilgraph für sich betrachtet. Dann iteriert man über alle Kanten und verbindet deren Endknoten jeweils zu einem gemeinsamen Teilgraphen (falls die beiden Enden einer Kante bereits im selben Teilgraphen liegen, wird diese Kante ignoriert). Solange noch Kanten vorhanden sind, werden dadurch immer wieder Teilgraphen in größere Teilgraphen verschmolzen. Am Ende bleiben die maximalen zusammenhängenden Teilgraphen (also gerade die Zusammenhangskomponenten) übrig. Dieser Algorithmus kommt ohne Tiefensuche aus und ist daher in der Praxis oft schneller, allerdings auch etwas komplizierter zu implementieren.&lt;br /&gt;
&lt;br /&gt;
Der Schlüssel des Algorithmus ist eine Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt;, die zu jedem Knoten den aktuellen Anker sucht. Der Anker existiert immer, da jeder Knoten von Anfang an zu einem Teilgraphen gehört (anfangs ist jeder Teilgraph trivial und besteht nur aus dem Knoten selbst). Die Verschmelzung wird realisiert, indem der Anker des einen Teilgraphen seine Rolle verliert und stattdessen der Anker des anderen Teilgraphen eingesetzt wird. &lt;br /&gt;
&lt;br /&gt;
Zur Verwaltung der Anker verwenden wir wieder eine property map &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt; mit der Konvention, dass die Anker auf sich selbst verweisen. Es wäre jedoch zu teuer, wenn man bei jeder Verschmelzung alle Anker-Einträge der beteiligten Knoten aktualisieren müsste, da jeder Knoten im Laufe des Algorithmus mehrmals seinen Anker wechseln kann. Statt dessen definiert man Anker rekursiv: Verweist ein Knoten auf einen Anker, der mittlerweile diese Rolle verloren hat, folgt man dem Verweis von diesem Knoten (dem ehemaligen Anker) weiter, bis man einen tatsächlichen Anker gefunden hat - erkennbar daran, dass er auf sich selbst verweist. Diese Suchfunktion kann folgendermassen implementiert werden:&lt;br /&gt;
&lt;br /&gt;
  def findAnchor(anchors, node):&lt;br /&gt;
      while node != anchors[node]:   # wenn node kein Anker ist&lt;br /&gt;
          node = anchors[node]       # ... verfolge die Ankerkette weiter&lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Allerdings kann diese Kette im Laufe vieler Verschmelzungen sehr lang werden, so dass das Verfolgen der Kette teuer wird. Man vermeidet dies durch die sogenannte &amp;lt;i&amp;gt;Pfadkompression&amp;lt;/i&amp;gt;: Immer, wenn man den Anker gefunden hat, aktualisiert man den Eintrag am Anfang der Kette. Die Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt; wird dadurch nur wenig komplizierter:&lt;br /&gt;
&lt;br /&gt;
  def findAnchor(anchors, node):&lt;br /&gt;
      start = node                   # wir merken uns den Anfang der Kette&lt;br /&gt;
      while node != anchors[node]:   # wenn node kein Anker ist&lt;br /&gt;
          node = anchors[node]       # ... verfolge die Ankerkette weiter&lt;br /&gt;
      anchors[start] = node          # Pfadkompression: aktualisiere den Eintrag am Anfang der Kette&lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Man kann zeigen, dass die Ankersuche mit Pfadkompression zu einer fast konstanten amortisierten Laufzeit pro Aufruf führt.&lt;br /&gt;
&lt;br /&gt;
Um mit jeder Kante des (ungerichteten) Graphen nur maximal einmal eine Verschmelzung durchzuführen, betrachten wir jede Kante nur in der Richtung von der kleineren zur größeren Knotennummer, die umgekehrte Richtung wird ignoriert. Außerdem ist es zweckmäßig, bei jeder Verschmelzung denjenigen Anker mit der kleineren Knotennummer als neuen Anker zu übernehmen. Dann gilt für jede Zusammenhangskomponente, dass gerade der Knoten mit der kleinsten Knotennummer der Anker ist (genau wie bei der Lösung mittels Tiefensuche), was die weitere Analyse vereinfacht, z.B. die Zuordnung der Labels zu den Komponenten am Ende des Algorithmus. &lt;br /&gt;
&lt;br /&gt;
 def unionFindConnectedComponents(graph):&lt;br /&gt;
     anchors = range(len(graph))        # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):    # iteriere über alle Knoten&lt;br /&gt;
         for neighbor in graph[node]:   # ... und über deren ausgehende Kanten&lt;br /&gt;
             if neighbor &amp;lt; node:        # ignoriere Kanten, die in falscher Richtung verlaufen&lt;br /&gt;
                 continue&lt;br /&gt;
             # hier landen wir für jede Kante des Graphen genau einmal&lt;br /&gt;
             a1 = findAnchor(anchors, node)       # finde Anker ...&lt;br /&gt;
             a2 = findAnchor(anchors, neighbor)   # ... der beiden Endknoten&lt;br /&gt;
             if a1 &amp;lt; a2:                          # Verschmelze die beiden Teilgraphen&lt;br /&gt;
                 anchors[a2] = a1                 # (verwende den kleineren der beiden Anker als Anker des&lt;br /&gt;
             elif a2 &amp;lt; a1:                        #  entstehenden Teilgraphen. Falls node und neighbor &lt;br /&gt;
                 anchors[a1] = a2                 #  den gleichen Anker haben, waren sie bereits im gleichen&lt;br /&gt;
                                                  #  Teilgraphen, und es passiert hier nichts.)&lt;br /&gt;
     # Bestimme jetzt noch die Labels der Komponenten&lt;br /&gt;
     labels = [None]*len(graph)         # Initialisierung der property map für Labels&lt;br /&gt;
     current_label = 0                  # die Zählung beginnt bei 0&lt;br /&gt;
     for node in xrange(len(graph)):&lt;br /&gt;
         a = findAnchor(anchors, node)  # wegen der Pfadkompression zeigt jeder Knoten jetzt direkt auf seinen Anker&lt;br /&gt;
         if a == node:                  # node ist ein Anker&lt;br /&gt;
             labels[a] = current_label  # =&amp;gt; beginne eine neue Komponente&lt;br /&gt;
             current_label += 1         # und zähle Label für die nächste ZK hoch&lt;br /&gt;
         else:&lt;br /&gt;
             labels[node] = labels[a]   # node ist kein Anker =&amp;gt; setzte das Label des Ankers&lt;br /&gt;
                                        # (wir wissen, dass labels[a] bereits gesetzt ist, weil &lt;br /&gt;
                                        #  der Anker immer der Knoten mit der kleinsten Nummer ist)&lt;br /&gt;
     return anchors, labels&lt;br /&gt;
 &lt;br /&gt;
* Beispiel: ... &amp;lt;b&amp;gt;under construction&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Kürzeste Wege (Pfade) ==&lt;br /&gt;
&lt;br /&gt;
Eine weitere grundlegende Aufgabe in Graphen ist die Bestimmung eines kürzesten Weges zwischen zwei gegebenen Knoten. Dies hat offensichtliche Anwendungen bei Routenplanern und Navigationssystemen und ist darüber hinaus wichtiger Bestandteil anderer Algorithmen, z.B. bei der Berechnung eines maximalen Flusses mit der [http://en.wikipedia.org/wiki/Edmonds%E2%80%93Karp_algorithm Methode von Edmonds und Karp].&lt;br /&gt;
&lt;br /&gt;
=== Kürzeste Wege in ungewichteten Graphen mittels Breitensuche ===&lt;br /&gt;
&lt;br /&gt;
Im Fall eines ungewichteten Graphen ist die Länge eines Weges einfach durch die Anzahl der durchlaufenen Kanten definiert. Daraus folgt, dass kürzeste Pfade mit einer leicht angepassten Version der Breitensuche gefunden werden können: Aufgrund des first in-first out-Verhaltens der Queue betrachtet die Breitensuche alle (erreichbaren) Knoten in der Reihenfolge ihres Abstandes vom Startknoten. Wenn wir den Zielknoten zum ersten Mal erreichen, und der gerade gefundene Weg vom Start zum Ziel hat die Länge L, muss dies der kürzeste Weg sein: Alle möglichen Wege der Länge L' &amp;amp;lt; L hat die Breitensuche ja bereits betrachtet, ohne dass dabei der Zielknoten erreicht wurde. Daraus folgt übrigens eine allgemeine Eigenschaft aller Algorithmen für kürzeste Wege: Wenn der kürzeste Weg vom Start zum Ziel die Länge L hat, finden diese Algorithmen als Nebenprodukt auch die kürzesten Wege zu allen Knoten, für die L' &amp;amp;lt; L gilt. &lt;br /&gt;
&lt;br /&gt;
Um den Algorithmus zu implementieren, passen wir die Breitensuche so an, dass anstelle der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; eine property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; verwendet wird, die für jeden besuchten Knoten den Vaterknoten im Breitensuchbaum speichert. Durch Rückverfolgen der &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette können wir den Pfad vom Ziel zum Start rekonstruieren, und durch Umdrehen der Reihenfolge erhalten wir den gesuchten Pfad vom Start zum Ziel. Sobald der Zielknoten erreicht wurde, können wir die Breitensuche abbrechen (&amp;lt;tt&amp;gt;break&amp;lt;/tt&amp;gt;-Befehl in der ersten &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Schleife). Falls der gegebene Graph unzusammenhängend ist, kann es passieren, dass gar kein Weg gefunden wird, weil Start und Ziel in verschiedenen Zusammenhangskomponenten liegen. Dies erkennen wir daran, dass die Breitensuche beendet wurde, ohne den Zielknoten zu besuchen. Dann gibt die Funktion statt eines Pfades dern Wert &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zurück:&lt;br /&gt;
&lt;br /&gt;
  from collections import deque&lt;br /&gt;
  &lt;br /&gt;
  def shortestPath(graph, startnode, destination):&lt;br /&gt;
      parents = [None]*len(graph)      # Registriere für jeden Knoten den Vaterknoten im Breitensuchbaum&lt;br /&gt;
      parents[startnode] = startnode   # startnode ist die Wurzel des Baums =&amp;gt; verweist auf sich selbst&lt;br /&gt;
      &lt;br /&gt;
      q = deque()                      # Queue für die zu besuchenden Knoten&lt;br /&gt;
      q.append(startnode)              # Startknoten in die Queue einfügen&lt;br /&gt;
      &lt;br /&gt;
      while len(q) &amp;gt; 0:                # Solange es noch unbesuchte Knoten gibt&lt;br /&gt;
          node = q.popleft()           # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
          if node == destination:      # Zielknoten erreicht&lt;br /&gt;
              break                    #   =&amp;gt; Suche beenden&lt;br /&gt;
          for neighbor in graph[node]: # Besuche die Nachbarn von node&lt;br /&gt;
              if parents[neighbor] is None:  # aber nur, wenn sie noch nicht besucht wurden&lt;br /&gt;
                  parents[neighbor] = node   # setze node als Vaterknoten&lt;br /&gt;
                  q.append(neighbor)         # und füge neighbor in die Queue ein&lt;br /&gt;
      &lt;br /&gt;
      if parents[destination] is None: # Breitensuche wurde beendet ohne den Zielknoten zu besuchen&lt;br /&gt;
          return None                  # =&amp;gt; kein Pfad gefunden (unzusammenhängender Graph)&lt;br /&gt;
      &lt;br /&gt;
      # Pfad durch die parents-Kette zurückverfolgen und speichern&lt;br /&gt;
      path = [destination]&lt;br /&gt;
      while path[-1] != startnode:&lt;br /&gt;
          path.append(parents[path[-1]])&lt;br /&gt;
      path.reverse()     # Reihenfolge umdrehen (Ziel =&amp;gt; Start wird zu Start =&amp;gt; Ziel)&lt;br /&gt;
      return path        # gefundenen Pfad zurückgeben&lt;br /&gt;
&lt;br /&gt;
=== Gewichtete Graphen ===&lt;br /&gt;
&lt;br /&gt;
Das Problem der Suche nach kürzesten Wegen wird wesentlich interessanter und realistischer, wenn wir zu gewichteten Graphen übergehen:&lt;br /&gt;
&lt;br /&gt;
; Definition - kantengewichteter Graph&lt;br /&gt;
: Jeder Kante (s,t) des Graphen ist eine reelle oder natürliche Zahl w&amp;lt;sub&amp;gt;st&amp;lt;/sub&amp;gt; zugeordnet, die üblicherweise als ''Kantengewicht'' bezeichnet wird.&lt;br /&gt;
&lt;br /&gt;
; Definition - knotengewichteter Graph&lt;br /&gt;
: Jedem Knoten v des Graphen ist eine reelle oder natürliche Zahl w&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; zugeordnet, die üblicherweise als ''Knotengewicht'' bezeichnet wird.&lt;br /&gt;
&lt;br /&gt;
Je nach Anwendung benötigt man Knoten- oder Kantengewichte oder auch beides zugleich. Wir beschränken uns in der Vorlesung auf kantengewichtete Graphen. Beispiele für die Informationen, die man durch Kantengewichte ausdrücken kann, sind&lt;br /&gt;
* wenn die Knoten Orte sind: Abstand von Anfangs- und Endknoten jeder Kante (z.B. Luftline oder Straßenentfernung), Fahrzeit zwischen den Orten&lt;br /&gt;
* wenn der Knoten ein Rohrnetzwerk beschreibt: Durchflusskapazität der einzelnen Rohre (für max-Flussprobleme), analog bei elektrischen Netzwerken: elektrischer Widerstand&lt;br /&gt;
* wenn die Knoten Währungen repräsentieren, können deren Wechselkurse durch Kantengewichte angegeben werden.&lt;br /&gt;
Bei einigen Beispielen ergeben sich unterschiedliche Kantengewichte, wenn eine Kante von s nach t anstatt von t nach s durchlaufen wird. Beispielsweise können sich die Fahrzeiten erheblich unterscheiden, wenn es in einer Richtung bergauf, in der anderen bergab geht, obwohl die Entfernung in beiden Fällen gleich ist. Hier ergibt sich natürlicherweise ein gerichteter Graph. In anderen Beispielen (z.B. bei Luftlinienentfernungen, in guter Näherung auch bei Straßenentfernungen) sind die Gewichte von der Richtung unabhängig, so dass wir ungerichtete Graphen verwenden können.&lt;br /&gt;
&lt;br /&gt;
Die Repräsentation der Kantengewichte im Programm richtet sich nach der Repräsentation des Graphen selbst. Am einfachsten ist wiederum die Adjazenzmatrix, die aber nur für dichte Graphen (&amp;lt;math&amp;gt;E = O(V^2)&amp;lt;/math&amp;gt;, mit E als Anzahl der Kanten und V als Anzahl der Knoten) effizient ist. Bei gewichteten Graphen gibt das Matrixelement a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; das Gewicht der Kante i &amp;amp;rArr; j (wobei a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; = 0 gesetzt wird, wenn diese Kante nicht existiert). Wie zuvor gilt für ungerichtete Graphen a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; = a&amp;lt;sub&amp;gt;ji&amp;lt;/sub&amp;gt; (symmetrische Matrix), während dies für gerichtete Graphen nicht gelten muss.&lt;br /&gt;
&lt;br /&gt;
Bei Graphen in Adjazenzlistendarstellung hat es sich bewährt, die Gewichte in einer &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; zu speichern. Weiter oben haben wir bereits property maps für Knoteneigenschaften (z.B. &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt;) gesehen. Property maps für Kanten funktionieren ganz analog, allerdings muss man jetzt Paare von Knoten (nämlich Anfangs- und Endknoten der Kante) als Schlüssel verwenden und die Daten entsprechend in einem assoziativen Array ablegen:&lt;br /&gt;
  w = weights[(i,j)]   # Zugriff auf das Gewicht der Kante i &amp;amp;rArr; j&lt;br /&gt;
Alternativ könnte man auch die Graph-Datenstruktur selbst erweitern, aber dies ist weniger zu empfehlen, weil jeder Algorithmus andere Erwiterungen benötigt und damit die Datenstruktur sehr unübersichtlich würde.&lt;br /&gt;
&lt;br /&gt;
Der kürzeste Weg ist nun definiert als der Weg, bei dem die Summe der Kantengewichte minimal ist:&lt;br /&gt;
;Definition - Problem des kürzesten Weges&lt;br /&gt;
: Sei P die Menge aller Wege von u nach v, und &amp;lt;math&amp;gt;p \in P&amp;lt;/math&amp;gt; einer dieser Wege. Wenn der Grpah einfach ist (es also keine Mehrfachkanten zwischen denselben Knoten und keine Schleifen gibt), ist der Weg p durch die Folge der besuchten Knoten eindeutig bestimmt:&lt;br /&gt;
: &amp;lt;math&amp;gt;p : \ \ u = x_0 \rightarrow x_1 \rightarrow x_2 \rightarrow ... \rightarrow v = x_{n_p}&amp;lt;/math&amp;gt;&lt;br /&gt;
:wo &amp;lt;math&amp;gt;n_p&amp;lt;/math&amp;gt; die Anzahl der Kanten im Weg p ist. Seine Kosten W&amp;lt;sub&amp;gt;p&amp;lt;/sub&amp;gt; ergeben sich als Summer der Gewichte der einzelnen Kanten&lt;br /&gt;
: &amp;lt;math&amp;gt;W_p = \sum_{k=1}^{n_p} w_{x_{k-1}x_k}&amp;lt;/math&amp;gt;&lt;br /&gt;
: und ein kürzester Weg &amp;lt;math&amp;gt;p^* \in P&amp;lt;/math&amp;gt; ist ein Weg mit minimalen Kosten&lt;br /&gt;
: &amp;lt;math&amp;gt;p^* = \textrm{argmin}_{p\in P}\ \ W_p&amp;lt;/math&amp;gt;&lt;br /&gt;
: Das Problem des kürzesten Weges besteht darin, einen optimalen Weg &amp;lt;i&amp;gt;p*&amp;lt;/i&amp;gt; zwischen gegebenen Knoten u und v zu finden.&lt;br /&gt;
Die Lösung dieses Problems hängt davon ab, ob alle Kantengewichte positiv sind, oder ob es auch negative Kantengewichte gibt. In letzeren Fall ist es möglich, durch eine Verlängerung des Weges die Kosten zu redizieren, während sich im ersteren Fall die Kosten immer erhöhen, wenn man den Weg verlängert. &lt;br /&gt;
&lt;br /&gt;
Negative Gewichte treten z.B. bei den Währungsgraphen auf. Auf den ersten Blick entsprechen diese Graphen nicht den Anforderungen an das Problem des kürzesten Weges, weil Wechselkurse miteinander (und mit Geldbeträgen) multipliziert anstatt addiert werden. Man beseitigt diese Schwierigkeit aber leicht, indem man die &amp;lt;i&amp;gt;Logarithmen&amp;lt;/i&amp;gt; der Wechselkurse als Kantengewichte verwendet, wodurch sich die Multiplikation in eine Addition der Logarithmen verwandelt. Wechselkurse &amp;amp;lt; 1 führen nun zu negativen Gewichten. &lt;br /&gt;
&lt;br /&gt;
Interessant werden negative Gewichte vor allem in Graphen mit Zyklen. Dann kann es nämlich passieren, dass die Gesamtkosten eines Zyklus ebenfalls negativ sind. Jeder Weg, der den Zyklus enthält, hat dann Kosten von &amp;lt;math&amp;gt;-\infty&amp;lt;/math&amp;gt;, weil man den Zyklus beliebig oft durchlaufen und dadurch die Gesamtkosten immer weiter verkleinern kann:&lt;br /&gt;
&lt;br /&gt;
     /\		1. Durchlauf: Kosten -1&lt;br /&gt;
  1 /  \ -4	2. Durchlauf: Kosten -2&lt;br /&gt;
   /____\	etc.&lt;br /&gt;
      2&lt;br /&gt;
&lt;br /&gt;
Um hier nicht in einer Endlosschleife zu landen, benötigt man spezielle Algorithmen, die mit dieser Situation umgehen können. Der [http://de.wikipedia.org/wiki/Bellman-Ford-Algorithmus Algorithmus von Bellmann und Ford] beispielsweise bricht die Suche nach dem kürzesten Weg ab, sobald er einen negativen Zyklus entdeckt, aber andernfalls kann er negative Gewichte problemlos verarbeiten. &lt;br /&gt;
&lt;br /&gt;
Die Detektion negativer Zyklen hat wiederum eine interessante Anwendung bei Währungsgraphen: Ein Zyklus bedeutet hier, dass man Geld über mehrere Stufen von einer Währung in die nächste und am Schluß wieder in die Originalwährung umtauscht, und ein negativer Zyklus führt dazu, dass man am Ende &amp;lt;i&amp;gt;mehr&amp;lt;/i&amp;gt; Geld besitzt als am Anfang (damit negative Zyklen wirklich einen Gewinn bedeuten und keinen Verlust, müssen die Wechselkurse vor der Logarithmierung in [http://de.wikipedia.org/wiki/Wechselkurs#Nominaler_Wechselkurs Preisnotierung] angegeben sein). Bei Privatpersonen ist dies ausgeschlossen, weil die Umtauschgebühren den möglichen Gewinn mehr als aufzehren. Banken mit direktem weltweitem Börsenzugang hingegen unternehmen große Anstrengungen, um solche negativen Zyklen möglichst schnell (nämlich vor der Konkurrenz) zu entdecken und auszunutzen. Diese Geschäftsmethode bezeichnet man als [http://de.wikipedia.org/wiki/Arbitrage Arbitrage] und die Existenz eines negativen Zyklus als Arbitragegelegenheit. Durch die Kursschwankungen (und durch die ausgleichende Wirkung der Arbitragegeschäfte selbst) existieren die Arbitragegelegenheiten nur für kurze Zeit, und ihre Detektion erfordert leistungsfähige Echtzeitalgorithmen.&lt;br /&gt;
&lt;br /&gt;
In dieser Vorlesung beschränken wir uns hingegen auf Graphen mit ausschließlich positiven Gewichten. In diesem Fall ist der Algorithmus von Dijkstra die Methode der Wahl, weil er wesentlich schneller arbeitet als der Bellmann-Ford-Algorithmus.&lt;br /&gt;
&lt;br /&gt;
=== Algorithmus von Dijkstra ===&lt;br /&gt;
&lt;br /&gt;
==== Edsger Wybe Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
geb. 11. Mai 1930 in Rotterdam&lt;br /&gt;
&lt;br /&gt;
ges. 06. August 2002&lt;br /&gt;
&lt;br /&gt;
Dijkstra war ein niederländischer Informatiker und Wegbereiter der strukturierten Programmierung. 1972 erhielt er für seine Leistung in der Technik und Kunst der Programmiersprachen den Turing Award, der jährlich von der Association for Computing Machinery (ACM) an Personen verliehen wird, die sich besonders um die Entwicklung der Informatik verdient gemacht haben. Zu seinen Beiträgen zur Informatik gehören unter anderem der Dijkstra-Algorithmus zur Berechnung des kürzesten Weges in einem Graphen sowie eine Abhandlung über den go-to-Befehl und warum er nicht benutzt werden sollte. Der go-to-Befehl war in den 60er und 70er Jahren weit verbreitet, führte aber zu Spaghetti-Code. In seinem berühmten Paper &amp;quot;A Case against the GO TO Statement&amp;quot;[http://www.cs.utexas.edu/users/EWD/ewd02xx/EWD215.PDF], das als Brief mit dem Titel &amp;quot;Go-to statement considered harmful&amp;quot; veröffentlicht wurde, argumentiert Dijkstra, dass es umso schwieriger ist, dem Quellcode eines Programmes zu folgen, je mehr go-to-Befehle darin enthalten sind und zeigt, dass man auch ohne diesen Befehl gute Programme schreiben kann.&lt;br /&gt;
&lt;br /&gt;
==== Algorithmus ====&lt;br /&gt;
&lt;br /&gt;
Der Dijkstra-Algorithmus für kürzeste Wege ist dem oben vorgestellten Algorithmus &amp;lt;tt&amp;gt;shortestPath()&amp;lt;/tt&amp;gt; auf der Basis von Breitensuche sehr ähnlich. Insbesondere gilt auch hier, dass neben dem kürzesten Weg vom Start zum Ziel auch alle kürzesten Wege gefunden werden, deren Endknoten dem Start näher sind als der Zielknoten. Aufgrund der Kantengewichte gibt es aber einen wichtigen Unterschied: Der erste gefundene Weg zu einem Knoten ist nicht mehr notwendigerweise der kürzeste. Wir bestimmen deshalb für jeden Knoten mehrere Kandidatenwege und verwenden eine Prioritätswarteschlange (statt einer einfachen First in - First out - Queue), um diese Wege nach ihrer Länge zu sortieren. Die Kandidatenwege für einen gegebenen Knoten werden unterschieden, indem wir auch den Vorgängerknoten im jeweiligen Weg speichern. Wenn ein Knoten &amp;lt;i&amp;gt;erstmals&amp;lt;/i&amp;gt; an die Spitze der Prioritätswarteschlange gelangt, haben wir den kürzesten Weg zu diesem Knoten gefunden (das wird weiter unten formal bewiesen), und der Vorgänger des Knotens in diesem Weg wird zu seinem Vaterknoten. Erscheint derselbe Knoten später nochmals an der Spitze der Prioritätswarteschlange, handelt es sich um einen Kandidatenweg, der sich nicht als kürzester erwiesen hat und deshalb ignoriert werden kann. Wir erkennen dies leicht daran, dass der Vaterknoten in der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; bereits gesetzt ist. &lt;br /&gt;
&lt;br /&gt;
Eine geeignete Datenstruktur für die Prioritätswarteschlange wird durch das Python-Modul [http://docs.python.org/library/heapq.html heapq] realisiert. Es verwendet ein normales Pythonarray als unterliegende Repräsentation für einen Heap und stellt effiziente &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt;-Funktionen zur Verfügung. Dies entspricht genau unserer Vorgehensweise im Kapitel [[Prioritätswarteschlangen]]. Als Datenelement erwartet die Funktion &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; ein Tupel, dessen erstes Element die Priorität sein muss. Die übrigen Elemente des Tupels (und damit auch deren Anzahl) können je nach Anwendung frei festgelegt werden. Wir legen fest, dass das zweite Element den Endknoten des betrachteten Weges und das dritte den Vorgängerknoten speichert. &lt;br /&gt;
&lt;br /&gt;
Die Kantengewichte werden dem Algorithmus in der property map &amp;lt;tt&amp;gt;weights&amp;lt;/tt&amp;gt; übergeben:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;code python&amp;gt;&lt;br /&gt;
    import heapq	                  # heapq implementiert die Funktionen für Heaps&lt;br /&gt;
    &lt;br /&gt;
    def dijkstra(graph, weights, startnode, destination):&lt;br /&gt;
        parents = [None]*len(graph)       # registriere für jeden Knoten den Vaterknoten im Pfadbaum&lt;br /&gt;
      &lt;br /&gt;
        q = []                            # Array q wird als Heap verwendet&lt;br /&gt;
        &amp;lt;font color=red&amp;gt;heapq.heappush(q, (0.0, startnode, startnode))&amp;lt;/font&amp;gt;  # Startknoten in Heap einfügen&lt;br /&gt;
      &lt;br /&gt;
        while len(q) &amp;gt; 0:                 # solange es noch Knoten im Heap gibt:&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;length, node, predecessor = heapq.heappop(q)&amp;lt;/font&amp;gt;   # Knoten aus dem Heap nehmen&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;if parents[node] is not None:&amp;lt;/font&amp;gt; # parent ist schon gesetzt =&amp;gt; es gab einen anderen, kürzeren Weg&lt;br /&gt;
                &amp;lt;font color=red&amp;gt;continue&amp;lt;/font&amp;gt;                  #   =&amp;gt; wir können diesen Weg ignorieren&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;parents[node] = predecessor&amp;lt;/font&amp;gt;   # parent setzen&lt;br /&gt;
            if node == destination:       # Zielknoten erreicht&lt;br /&gt;
                break                     #   =&amp;gt; Suche beenden&lt;br /&gt;
            for neighbor in graph[node]:  # die Nachbarn von node besuchen,&lt;br /&gt;
                if parents[neighbor] is None:   # aber nur, wenn ihr kürzester Weg noch nicht bekannt ist&lt;br /&gt;
                    &amp;lt;font color=red&amp;gt;newLength = length + weights[(node,neighbor)]&amp;lt;/font&amp;gt;   # berechne Pfadlänge zu neighbor              &lt;br /&gt;
                    &amp;lt;font color=red&amp;gt;heapq.heappush(q, (newLength, neighbor, node))&amp;lt;/font&amp;gt;  # und füge neighbor in den Heap ein&lt;br /&gt;
      &lt;br /&gt;
        if parents[destination] is None:  # Suche wurde beendet ohne den Zielknoten zu besuchen&lt;br /&gt;
            return None, None             # =&amp;gt; kein Pfad gefunden (unzusammenhängender Graph)&lt;br /&gt;
      &lt;br /&gt;
        # Pfad durch die parents-Kette zurückverfolgen und speichern&lt;br /&gt;
        path = [destination]&lt;br /&gt;
        while path[-1] != startnode:&lt;br /&gt;
            path.append(parents[path[-1]])&lt;br /&gt;
        path.reverse()                    # Reihenfolge umdrehen (Ziel =&amp;gt; Start wird zu Start =&amp;gt; Ziel)&lt;br /&gt;
        return path, length               # gefundenen Pfad und dessen Länge zurückgeben&lt;br /&gt;
  &amp;lt;/code&amp;gt;&lt;br /&gt;
Die wesentlichen Unterschiede zur Breitensuche sind im Code rot markiert: Anstelle der Queue verwenden wir jetzt einen Heap, und der Startknoten wird mit Pfadlänge 0 als erstes eingefügt. In der Schleife &amp;lt;tt&amp;gt;while len(q) &amp;gt; 0:&amp;lt;/tt&amp;gt; wird jeweils der Knoten &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; mit der aktuell kürzesten Pfadlänge aus dem Heap entfernt. Die Pfadlänge vom Start zu diesem Knoten wird in der Variable &amp;lt;tt&amp;gt;length&amp;lt;/tt&amp;gt; gespeichert, sein Vorgänger in der Variable &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;. Wenn der aktuelle Weg nicht der kürzeste ist (&amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; war bereits gesetzt), wird dieser Weg ignoriert. Andernfalls werden die property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; aktualisiert und die Nachbarn von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; besucht. Beim Scannen der Nachbarn berechnen wir zunächst die Länge &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; das Weges &amp;lt;tt&amp;gt;startnode =&amp;amp;gt; node =&amp;amp;gt; neighbor&amp;lt;/tt&amp;gt; als Summe von &amp;lt;tt&amp;gt;length&amp;lt;/tt&amp;gt; und dem Gewicht der Kante &amp;lt;tt&amp;gt;(node, neighbode)&amp;lt;/tt&amp;gt;. Diese Länge wird beim Einfügen des Nachbarknotens in den Heap zur Priorität des aktuellen Weges.&lt;br /&gt;
&lt;br /&gt;
Die wichtigsten Prinzipien des Dijkstra-Algorithmus noch einmal im Überblick:&lt;br /&gt;
* Der Dijkstra-Algorithmus ist Breitensuche mit Prioritätswarteschlange (Heap) statt einer einfache Warteschlange (Queue).&lt;br /&gt;
* Die Prioritätswarteschlange speichert alle Wege, die bereits gefunden worden sind und ordnet sie aufsteigend nach ihrer Länge. &lt;br /&gt;
* Das Sortieren (und damit der ganze Algorithmus) funktioniert nur mit positiven Kantengewichten korrekt.&lt;br /&gt;
* Da ein Knoten auf mehreren Wegen erreichbar sein kann, kann er auch mehrmals im Heap sein. &lt;br /&gt;
* Wenn ein Knoten &amp;lt;i&amp;gt;erstmals&amp;lt;/i&amp;gt; aus der Prioritätswarteschlange entnommen wird, ist der gefundene Weg der kürzeste zu diesem Knoten. Andernfalls wird der Weg ignoriert.&lt;br /&gt;
* Wenn der Knoten &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; aus dem Heap entnommen wird, ist der kürzeste Weg von Start nach Ziel gefunden, und die Suche kann beendet werden.&lt;br /&gt;
In unserer Implementation können, wie gesagt, mehrere Wege zum selben Knoten gleichzeitig in der Prioritätswarteschlange sein. Im Prinzip wäre es auch möglich, immer nur den besten zur Zeit bekannten Weg zu jedem Enknoten in der Prioritätswarteschlange zu halten - sobald ein besserer Kandidat gefunden wird, ersetzt er den bisherigen Kandidaten, anstatt zusätzlich eingefügt zu werden. Dies erfordert aber eine wesentlich kompliziertere Prioritätswarteschlange, die eine effiziente &amp;lt;tt&amp;gt;updatePriority&amp;lt;/tt&amp;gt;-Funktion anbietet, ohne dass dadurch eine signifikante Beschleunigung erreicht wird. Deshalb verfolgen wir diesen Ansatz nicht.&lt;br /&gt;
&lt;br /&gt;
==== Beispiel ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;under construction&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Image:Bsp.jpg]]&lt;br /&gt;
&lt;br /&gt;
==== Komplexität von Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
Zur Analyse der Komplexität nehmen wir an, dass der Graph V Knoten und E Kanten hat. Die Initialisierung der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; am Anfang der Funktion hat offensichtlich Komplexität O(V), weil Speicher für V Knoten allokiert wird. Der Code am Ende der Funktion, der aus der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; den Pfad extrahiert, hat ebenfalls die Komplexität O(V), weil der Pfad im ungünstigen Fall sämtliche Knoten des Graphen umfasst. Beides wird durch die Komplexität der Hauptschleife dominiert, zu deren Analyse wir den folgenden Codeausschnitt genauer anschauen wollen:&lt;br /&gt;
&lt;br /&gt;
      while len(q) &amp;gt; 0:&lt;br /&gt;
           ... # 1&lt;br /&gt;
           if parents[node] is not None: &lt;br /&gt;
               continue                  &lt;br /&gt;
           parents[node] = predecessor&lt;br /&gt;
           ... # 2&lt;br /&gt;
Wir erkennen, dass der Codeabschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; für jeden Knoten höchstens einmal erreicht werden kann: Da &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; beim ersten Durchlauf gesetzt wird, kann die &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage beim gleichen Knoten nie wieder &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt; liefern, und das nachfolgende &amp;lt;tt&amp;gt;continue&amp;lt;/tt&amp;gt; bewirkt, dass der Abschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; dann übersprungen wird. Man sagt auch, dass jeder Knoten &amp;lt;i&amp;gt;höchstens einmal expandiert&amp;lt;/i&amp;gt; wird, auch wenn er mehrmals im Heap war. &lt;br /&gt;
&lt;br /&gt;
Der Codeabschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; selbst enthält eine Schleife über alle ausgehenden Kanten des Knotens &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt;. Im ungünstigsten Fall iterieren wir bei &amp;lt;i&amp;gt;allen&amp;lt;/i&amp;gt; Knoten über &amp;lt;i&amp;gt;alle&amp;lt;/i&amp;gt; ausgehenden Kanten, aber das sind gerade alle Kanten des Graphen je einmal in den beiden möglichen Richtungen. Die Funktion &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; wird sogar höchstens E Mal aufgerufen, weil eine Kante nur in den Heap eingefügt wird, wenn der kürzeste Weg der jeweiligen Endknotens noch nicht bekannt ist (siehe die &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage in der &amp;lt;tt&amp;gt;for&amp;lt;/tt&amp;gt;-Schleife), und das ist nur ein einer Richtung möglich. Dies hat zwei Konsequenzen:&lt;br /&gt;
* Die Schleife &amp;lt;tt&amp;gt;while len(q) &amp;gt; 0:&amp;lt;/tt&amp;gt; wird nur so oft ausgeführt, wie Elemente im Heap sind, also höchstens E Mal. Das gleiche gilt für den Codeabschnitt &amp;lt;tt&amp;gt;# 1&amp;lt;/tt&amp;gt;, der das &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; enthält.&lt;br /&gt;
* Die Operationen &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; haben logarithmische Komplexität in der Größe des Heaps, sind also in &amp;lt;math&amp;gt;O(\log\,E)&amp;lt;/math&amp;gt;. In einfachen Graphen gilt aber &amp;lt;math&amp;gt;E = O(V^2)&amp;lt;/math&amp;gt;, so dass sich die Komplexität der Heapoperationen vereinfacht zu &amp;lt;math&amp;gt;O(\log\,E)=O(\log\,V^2)=O(2\log\,V)=O(\log\,V)&amp;lt;/math&amp;gt;.&lt;br /&gt;
Zusammenfassend gilt: &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; werden maximal E Mal aufgerufen und haben eine Komplexität in &amp;lt;math&amp;gt;O(\log\,V)&amp;lt;/math&amp;gt;. Folglich hat der Algorithmus von Dijkstra die Komplexität:&lt;br /&gt;
:&amp;lt;math&amp;gt;O(E\,\log\,V)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Vergleich mit Breitensuche und Tiefensuche ====&lt;br /&gt;
&lt;br /&gt;
Der Dijkstra-Algorithmus ist eng mit der Breiten- und Tiefensuche verwandt - man kann diese Algorithmen aus dem Dijkstra-Algorithmus gewinnen, indem man einfach die Regel zur Festlegung der Prioritäten ändert. Anstelle der Länge des Pfades verwenden wir als Priorität den Wert eine Zählvariable &amp;lt;tt&amp;gt;count&amp;lt;/tt&amp;gt;, die nach jeder Einfügung in den Heap (also nach jedem Aufruf von &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt;) aktualisiert wird. Zählen wir die Variable hoch, haben die zuerst eingefügten Kanten die höchste Priorität, der Heap verhält sich also wie eine Queue (First in-First out), und wir erhalten eine Breitensuche. Zählen wir die Variable hingegen (von E beginnend) herunter, haben die zuletzt eingefügten Kanten höchste Priorität. Der Heap verhält sich dann wie ein Stack (Last in-First out), und wir bekommen Tiefensuche. Statt eines Heaps plus Zählvariable kann man jetzt natürlich direkt eine Queue bzw. einen Stack verwenden. Dadurch fällt der Aufwand &amp;lt;math&amp;gt;O(\log\,V)&amp;lt;/math&amp;gt; für die Heapoperationen weg und wird durch die effizienten O(1)-Operationen von Queue bzw. Stack ersetzt. Damit erhalten wir für Breiten- und Tiefensuche die schon bekannte Komplexität O(E).&lt;br /&gt;
&lt;br /&gt;
==== Korrektheit von Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
Wir beweisen mittels vollständiger Induktion die Schleifen-Invariante: Falls &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; gesetzt (also ungleich &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;) ist, dann liefert das Zurückverfolgen des Weges von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; nach &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; den kürzesten Weg. &lt;br /&gt;
;Induktionsanfang: &amp;lt;tt&amp;gt;parents[startnode]&amp;lt;/tt&amp;gt; ist als einziges gesetzt. Zurückverfolgen liefert den trivialen Weg &amp;lt;tt&amp;gt;[startnode]&amp;lt;/tt&amp;gt;, der mit Länge 0 offensichtlich der kürzeste Pfad ist &amp;amp;rarr; die Bedingung ist erfüllt.&lt;br /&gt;
;Induktionsschritt: Wir zeigen mit einem indirektem Beweis, dass wir immer einen kürzesten Weg bekommen, wenn &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; gesetzt wird.&lt;br /&gt;
:Sei &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt; = &amp;lt;tt&amp;gt;{v | parents[v] is not None}&amp;lt;/tt&amp;gt; die Menge aller Knoten, von denen wir den kürzesten Weg schon kennen (Induktionsvoraussetzung), und &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; der Knoten, der sich gerade an der Spitze des Heaps befindet. Dann ist &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; der Vorgänger von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; im aktuellen Weg, und es muss &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt; gelten, weil die Nachbarn von &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; (und damit auch der aktuelle &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt;) erst in den Heap eingefügt werden, wenn der kürzeste Weg für &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; gefunden wurde. Man beachte auch, dass alle Knoten, die noch nicht in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt; enthalten sind, weiter vom Start entfernt sind als alle Knoten in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt;, weil alle neu in den Heap eingefügten Wege länger sind als der kürzeste Weg des jeweiligen Vorgängers. &lt;br /&gt;
:Der indirekte Beweis nimmt jetzt an, dass der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; nicht der kürzeste Weg ist. Dann muss es einen anderen, kürzeren Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; geben. Für den Vorgänger &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; in diesem Weg unterscheiden wir zwei Fälle:&lt;br /&gt;
:* &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt;: In diesem Fall ist die Länge des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; bereits bekannt, und dieser Weg ist in der Prioritätswarteschlange enthalten. Dann kann er aber nicht der kürzeste sein, denn an der Spitze der Warteschlange war nach Voraussetzung der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt;.&lt;br /&gt;
:* &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\notin S&amp;lt;/math&amp;gt;: Die Kosten des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; berechnen sich als &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) + weight[(x, node)]&amp;lt;/tt&amp;gt;, und die Kosten des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; sind analog &amp;lt;tt&amp;gt;Kosten(predecessor &amp;amp;rarr; startnode) + weight[(predecessor, node)]&amp;lt;/tt&amp;gt;. Aufgrund der Induktionsvoraussetzung gilt aber &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt;, und somit &amp;lt;tt&amp;gt;Kosten(predecessor &amp;amp;rarr; startnode) &amp;amp;lt; Kosten(x &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt;, weil &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; andernfalls vor &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; an der Spitze des Heaps gewesen wäre, was mit der Annahme &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\notin S&amp;lt;/math&amp;gt; unverträglich ist. Damit der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; trotzdem der kürzeste Weg sein kann, müsste &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) &amp;amp;lt; Kosten(node &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt; gelten, denn durch die Kante &amp;lt;tt&amp;gt;(x, node)&amp;lt;/tt&amp;gt; kommen ja noch Kosten hinzu. Das wäre aber nur möglich, wenn der Knoten &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; vor dem Knoten &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; an die Spitze des Heaps gelangt, im Widerspruch zur Annahme, dass &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; sich gerade an der Spitze des Heaps befindet. Somit kann die Behauptung, dass der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; der kürzeste Weg ist, nicht stimmen.&lt;br /&gt;
In beiden Fällen erhalten wir einen Widerspruch, und die Behauptung ist somit bewiesen. Da die Invariante insbesondere für den Weg zum Zielknoten &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; erfüllt ist, folgt daraus auch die Korrektheit des Algorithmus von Dijkstra.&lt;br /&gt;
&lt;br /&gt;
===  A*-Algorithmus - Wie kann man Dijkstra noch verbessern? ===&lt;br /&gt;
&lt;br /&gt;
Eine wichtige Eigenschaft des Dijkstra-Algorithmus ist, dass neben dem kürzesten Weg vom Start zum Ziel auch die kürzesten Wege zu allen Knoten berechnet werden, die näher am Startknoten liegen als das Ziel, obwohl uns diese Wege gar nicht interessieren. Sucht man beispielsweise in einem Graphen mit den Straßenverbindungen in Deutschland den kürzesten Weg von Frankfurt (Main) nach Dresden (ca. 460 km), werden auch die kürzesten Wege von Frankfurt nach Köln (190 km), Dortmund (220 km) und Stuttgart (210 km) und vielen anderen Städten gefunden. Aufgrund der geographischen Lage dieser Städte ist eigentlich von vornherein klar, dass sie mit dem kürzesten Weg nach Dresden nicht das geringste zu tun haben. Anders sieht es mit Erfurt (260 km) oder Suhl (210 km) aus - diese Städte liegen zwischen Frankfurt und Dresden und kommen deshalb als Zwischenstationen des gesuchten Weges in Frage.&lt;br /&gt;
&lt;br /&gt;
Damit Dijkstra korrekt funktioniert, würde es im Prinzip ausreichen, wenn man die kürzesten Wege nur für diejenigen Knoten ausrechnet, die auf dem kürzesten Weg vom Start zum Ziel liegen, denn nur diese Knoten braucht man, um den gesuchten Weg über die &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette zurückzuverfolgen. Das Problem ist nur, dass man diese Knoten erst kennt, wenn der Algorithmus fertig durchgelaufen ist. Schließt man Knoten zu früh von der Betrachtung aus, kommt am Ende möglicherweise nicht der korrekte kürzeste Weg heraus. &lt;br /&gt;
&lt;br /&gt;
Der A*-Algorithmus löst dieses Dilemma mit folgender Idee: Ändere die Prioritäten für den Heap so ab, dass unwichtige Knoten nur mit geringerer Wahscheinlichkeit expandiert werden, aber stelle gleichzeitig sicher, dass alle wichtigen Knoten (also diejenigen auf dem korrekten kürzesten Weg) auf jeden Fall expandiert werden. Es zeigt sich, dass man diese Idee umsetzen kann, wenn eine &amp;lt;i&amp;gt;Schätzung für den Restweg&amp;lt;/i&amp;gt; (also für die noch verbleibende Entfernung von jedem Knoten zum Ziel) verfügbar ist:&lt;br /&gt;
 rest = guess(neighbor, destination)&lt;br /&gt;
Diese Schätzung addiert man einfach zur wahren Länge des Weges &amp;lt;tt&amp;gt;startnode &amp;amp;rarr; node&amp;lt;/tt&amp;gt; dazu, um die verbesserte Priorität zu erhalten:&lt;br /&gt;
 priority = newLength + guess(neighbor, destination)&lt;br /&gt;
(Im originalen Dijkstra-Algorithmus wird als Priorität nur &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; allein verwendet. Man beachte, dass man &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; jetzt zusätzlich im Heap speichern muss, weil man es für die Expansion des Knotens später noch benötigt.)&lt;br /&gt;
&lt;br /&gt;
Damit sicher gestellt ist, dass der A*-Algorithmus immer noch die korrekten kürzesten Wege findet, darf die Schätzung den wahren Restweg &amp;lt;i&amp;gt;niemals überschätzen&amp;lt;/i&amp;gt;. Es muss immer gelten:&lt;br /&gt;
 0 &amp;lt;= guess(node, destination) &amp;lt;= trueDistance(node, destination)&lt;br /&gt;
Damit gilt insbesondere &amp;lt;tt&amp;gt;guess(destination, destination) = trueDistance(destination, destination) = 0&amp;lt;/tt&amp;gt;, an der Priorität des Knotens &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; ändert sich also nichts. Die Prioritäten aller anderen Knoten veschlechtern sich hingegen, weil zur bisherigen Priorität noch atwas addiert wird. Für die wichtigen Knoten auf dem kürzesten Weg vom Start nach Ziel gilt jedoch, dass deren neue Priorität immer noch besser ist als die Priorität des Zielknotens selbst. Für diese Knoten gilt nämlich&lt;br /&gt;
 falls node auf dem kürzesten Weg von startnode nach destination liegt:&lt;br /&gt;
 trueDistance(startnode, node) + guess(node, destination) &amp;lt;= trueDistance(startnode, destination)&lt;br /&gt;
weil der Weg von Start nach &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; ein Teil des kürzesten Wegs von Start nach Ziel ist und die Restschätzung die wahre Entfernung immer unterschätzt. Diese Knoten werden deshalb stets vor dem Zielknoten expandiert, so dass wir die &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette immer noch korrekt zurückverfolgen können. Für alle anderen Knoten gilt idealerweise, dass die neue Priorität schlechter ist als die Priorität von &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt;, so dass man sich diese irrelevanten Knotenexpansionen sparen kann.&lt;br /&gt;
&lt;br /&gt;
Für das Beispiel eines Straßennetzwerks bietet sich als Schätzung die Luftlinienentfernung an, weil Straßen nie kürzer sein können als die Luftlinie. Damit erreicht man in der Praxis deutliche Einsparungen. Generell gilt, dass der A*-Algorithmus im typischen Fall schneller ist als der Algorithmus von Dijkstra, aber man kann immer pathologische Fälle konstruieren, wo die Änderung der Prioritäten nichts bringt. Die Komplexität des A*-Algorithmus im ungünstigen Fall ist deshalb nach wie vor &amp;lt;math&amp;gt;O(E\,\log\,V)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=='''Minimaler Spannbaum'''==&lt;br /&gt;
'''(engl.: minimum spanning tree; abgekürzt: MST)'''&lt;br /&gt;
&lt;br /&gt;
[[Image:Minimum_spanning_tree.png‎ |thumb|200px|right|Ein minimal aufspannender Baum verbindet alle Punkte eines Graphen bei minimaler Kantenlänge ([http://de.wikipedia.org/wiki/Spannbaum Quelle])]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: gewichteter Graph G, zusammenhängend&amp;lt;br/&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: Untermenge &amp;lt;math&amp;gt;E'\subseteq E&amp;lt;/math&amp;gt; der Kanten, so dass die Summe der Kantengewichte &amp;lt;math&amp;gt;\sum_{e\in E'} w_e&amp;lt;/math&amp;gt; minimal und der entstehende Graph G' zusammenhängend ist.&amp;lt;br/&amp;gt;&lt;br /&gt;
* G' definiert immer einen Baum, denn andernfalls könnte man eine Kante weglassen und dadurch die Summe &amp;lt;math&amp;gt;\sum_{e\in E'} w_e&amp;lt;/math&amp;gt; verringern, ohne dass sich am Zusammenhang von G' etwas ändert. &amp;lt;br/&amp;gt;&lt;br /&gt;
* Wenn der Graph G nicht zusammenhängend ist, kann man den Spannbaum für jede Zusammenhangskomponente getrennt ausrechnen. Man erhält dann einen aufspannenden Wald. &lt;br /&gt;
* Der MST ist ähnlich wie der Dijkstra-Algorithmus: Dort ist ein Pfad gesucht, bei dem die Summe der Gewichte über den Pfad minimal ist. Beim MST suchen wir eine Lösung, bei der die Summe der Gewichte über den ganzen Graphen minimal ist. &lt;br /&gt;
* Das Problem des MST ist nahe verwandt mit der Bestimmung der Zusammenhangskomponente, z.B. über den Tiefensuchbaum. Für die Zusammenhangskomponenten genügt allerdings ein beliebiger Baum, während beim MST ein minimaler Baum gesucht ist.&lt;br /&gt;
&lt;br /&gt;
=== Anwendungen ===&lt;br /&gt;
==== Wie verbindet man n gegebene Punkte mit möglichst kurzen Straßen (Eisenbahnen, Drähten [bei Schaltungen] usw.)?====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:center&amp;quot; border=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; cellspacing=&amp;quot;0&amp;quot; &lt;br /&gt;
|MST minimale Verbindung (Abb.1)&lt;br /&gt;
|MST = 2 (Länge = Kantengewicht)(Abb.2)&lt;br /&gt;
|- valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| [[Image:mst.png]] &lt;br /&gt;
| [[Image:Gleichseitigesdreieck.png]]&lt;br /&gt;
|}&lt;br /&gt;
*In der Praxis: Die Festlegung, dass man nur die gegebenen Punkte verwenden darf, ist eine ziemliche starke Einschränkung. &lt;br /&gt;
&lt;br /&gt;
* Wenn man sich vorstellt, es sind drei Punkte gegeben, die als gleichseitiges Dreieck angeordnet sind, dann ist der MST (siehe Abb.2, schwarz gezeichnet) und hat die Länge 2. Man kann hier die Länge als Kantengewicht verwenden. &lt;br /&gt;
&lt;br /&gt;
* Wenn es erlaubt ist zusätzliche Punkte einzufügen, dann kann man in der Mitte einen neuen Punkt setzen &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; neuer MST (siehe Abb.2, orange gezeichnet).&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Höhe = &amp;lt;math&amp;gt;\frac{1}{2}\sqrt{3}&amp;lt;/math&amp;gt;, Schwerpunkt: teilt die Höhe des Dreiecks im Verhältnis 2:1; der Abstand von obersten Punkt bis zum neu eingeführten Punkt: &amp;lt;math&amp;gt;\frac{2}{3}h = \frac{\sqrt{3}}{3}&amp;lt;/math&amp;gt;, davon insgesamt 3 Stück, damit (gilt für den MST in orange eingezeichnet): MST = &amp;lt;math&amp;gt;3\left(\frac{1}{3}\right) \sqrt{3} = \sqrt{3} \approx 1,7&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Damit ist der MST in orange kürzer als der schwarz gezeichnete MST. &amp;lt;br\&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt;Folgerung: MST kann kürzer werden, wenn man einen Punkt dazu nimmt. &lt;br /&gt;
* Umgekehrt kann der MST auch kürzer werden, wenn man einen Punkt aus dem Graphen entfernt, aber wie das Beipiel des gleichseitigen Dreiecks zeigt, ist dies nicht immer der Fall.&lt;br /&gt;
&lt;br /&gt;
[[Image: bahn.png|Bahnstrecke Verbindung (Abb.3)]]&lt;br /&gt;
&lt;br /&gt;
* Methode der zusätzlichen Punkteinfügung hat man früher beim Bahnstreckenbau verwendet. Durch Einführung eines Knotenpunktes kann die Streckenlänge verkürzt werden (Dreiecksungleichung).&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Bestimmung von Datenclustern ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Image:cluster.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Daten (in der Abb.: Punkte) bilden Gruppen. &lt;br /&gt;
&lt;br /&gt;
* In der Abbildung hat man 2 verschiedene Messungen gemacht (als x- und y-Achse aufgetragen), bspw. Größe und Gewicht von Personen. Für jede Person i wird ein Punkt an der Koordinate (Größe&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;, Gewicht&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;) gezeichnet (siehe Bild a). Dies bezeichnet man als ''Scatter Plot''. Wenn bestimmte Wertkombinationen häufiger auftreten als andere, bilden sich mitunter Gruppen aus, bspw. eine Gruppe für &amp;quot;klein und schwer&amp;quot; etc.&lt;br /&gt;
&lt;br /&gt;
* Durch Verbinden der Punkte mittels eines MST (siehe Abbildung (b)) sieht man, dass es kurze (innerhalb der Gruppen) und lange Kanten (zwischen den Gruppen) gibt. &lt;br /&gt;
&lt;br /&gt;
* Wenn man geschickt eine Schwelle einführt und alle Kanten löscht, die länger sind als die Schwelle, dann bekommt man als Zusammenhangskomponente die einzelnen Gruppen. &lt;br /&gt;
&lt;br /&gt;
=== Algorithmen ===&lt;br /&gt;
&lt;br /&gt;
Genau wie bei der Bestimmung von Zusammenhangskomponenten kann man auch das MST-Problem entweder nach dem Anlagerungsprinzip oder nach dem Verschmelzungsprinzip lösen (dazu gibt es noch weitere Möglichkeiten, z.B. den [http://de.wikipedia.org/wiki/Algorithmus_von_Bor%C5%AFvka Algorithmus von Boruvka]). Der Anlagerungsalgorithmus für MST wurde zuerst von Prim beschrieben und trägt deshalb seinen Namen, der Verschmelzungsalgorithmus stammt von Kruskal. Im Vergleich zu den Algorithmen für Zusammenhangskomponenten ändert sich im wesentlichen nur die Reihenfolge, in der die Kanten betrachtet werden: Eine Prioritätswarteschlange stellt jetzt sicher, dass am Ende wirklich der Baum mit den geringstmöglichen Kosten herauskommt.&lt;br /&gt;
&lt;br /&gt;
====Algorithmus von Prim====&lt;br /&gt;
[http://de.wikipedia.org/wiki/Algorithmus_von_Prim Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus von Prim geht nach dem Anlagerungsprinzip vor (vgl. den Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Tiefensuche|Zusammenhangskomponenten mit Tiefensuche]]): Starte an der Wurzel (ein willkürlich gewählter Knoten) und füge jeweils die günstigste Kante an die aktuellen Teillösung an, die keinen Zyklus verursacht. Die Sortierung der Kanten nach Priorität erfolgt analog zum Dijsktra-Algorithmus, aber die Definitionen, welche Kante die günstigste ist, unterscheiden sich. Die Konvention für die Bedeutung der Elemente des Heaps ist ebenfalls identisch: ein Tupel mit &amp;lt;tt&amp;gt;(priority, node, predecessor)&amp;lt;/tt&amp;gt;. Die folgende Implementation verdeutlicht sehr schön die Ähnlichkeit der beiden Algorithmen. Das Ergebnis wird als property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; zurückgegeben, in der für jeden Knoten sein Vorgänger im MST steht, wobei die Wurzel wie üblich auf sich selbst verweist.&lt;br /&gt;
&lt;br /&gt;
 import heapq&lt;br /&gt;
 &lt;br /&gt;
 def prim(graph, weights):                # Kantengewichte wie bei Dijkstra als property map&lt;br /&gt;
 	sum = 0.0                         # wird später das Gewicht des Spannbaums sein&lt;br /&gt;
 	start = 0                         # Knoten 0 wird willkürlich als Wurzel gewählt&lt;br /&gt;
        &lt;br /&gt;
        parents = [None]*len(graph)       # property map, die den resultierenden Baum kodiert&lt;br /&gt;
        parents[start] = start            # Wurzel zeigt auf sich selbst&lt;br /&gt;
        &lt;br /&gt;
 	heap = []                         # Heap für die Kanten des Graphen&lt;br /&gt;
 	for neighbor in graph[start]:     # besuche die Nachbarn von start&lt;br /&gt;
 	    heapq.heappush(heap, (weights[(start, neighbor)], neighbor, start))  # und fülle Heap &lt;br /&gt;
 	&lt;br /&gt;
        while len(heap) &amp;gt; 0:&lt;br /&gt;
 	    w, node, predecessor = heapq.heappop(heap) # hole billigste Kante aus dem Heap&lt;br /&gt;
            if parents[node] is not None: # die Kante würde einen Zyklus verursachen&lt;br /&gt;
                continue                  #   =&amp;gt; ignoriere diese Kante&lt;br /&gt;
            parents[node] = predecessor   # füge Kante in den MST ein&lt;br /&gt;
            sum += w                      # und aktualisiere das Gesamtgewicht &lt;br /&gt;
            for neighbor in graph[node]:  # besuche die Nachbarn von node&lt;br /&gt;
                if parents[neighbor] is None:  # aber nur, wenn kein Zyklus entsteht&lt;br /&gt;
                    heapq.heappush(heap, (weights[(node,neighbor)], neighbor, node)) # füge Kandidaten in Heap ein&lt;br /&gt;
        &lt;br /&gt;
 	return parents, sum               # MST und Gesamtgewicht zurückgeben&lt;br /&gt;
&lt;br /&gt;
====Algorithmus von Kruskal====&lt;br /&gt;
[http://de.wikipedia.org/wiki/Algorithmus_von_Kruskal Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Kruskal%27s_algorithm (en)]&lt;br /&gt;
&lt;br /&gt;
Die alternative Vorgehensweise ist das Verschmelzungsprinzip (vgl. den Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Union-Find-Algorithmus|Zusammenhangskomponenten mit Union-Find-Algorithmus]]), das der Algorithmus von Kruskal verwendet. Jeder Knoten wird zunächst als trivialer Baum mit nur einem Knoten betrachtet, und alle Kanten werden aufsteigend nach Gewicht sortiert. Dann wird die billigste noch nicht betrachtete Kante in den MST eingefügt, falls sich dadurch kein Zyklus bildet (erkennbar daran, dass die Endknoten in verschiedenen Zusammenhangskomponenten liegen, das heisst verschiedene Anker haben). Da der fertige Baum (V-1) Kanten haben muss, wird dies (V-1) Mal zutreffen. Andernfalls wird diese Kante ignoriert. Anders ausgedrückt: Der Algorithmus beginnt mit ''V'' Bäumen; in (''V''-1) Verschmelzungsschritten kombiniert er jeweils zwei Bäume (unter Verwendung der kürzesten möglichen Kante), bis nur noch ein Baum übrig bleibt. Der einzige Unterschied zum einfachen Union-Find besteht darin, dass die Kanten in aufsteigender Reihenfolge betrachtet werden müssen, was wir hier durch eine Prioritätswarteschlange realisieren. Der Algorithmus von J.Kruskal ist seit 1956 bekannt. &lt;br /&gt;
&lt;br /&gt;
 def kruskal(graph, weights):&lt;br /&gt;
     anchors = range(len(graph))           # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     results = []                          # result wird später die Kanten des MST enthalten    &lt;br /&gt;
     &lt;br /&gt;
     heap = []                             # Heap zum Sortieren der Kanten nach Gewicht&lt;br /&gt;
     for edge, w in weights.iteritems():   # alle Kanten einfügen&lt;br /&gt;
         heapq.heappush(heap, (w, edge))&lt;br /&gt;
     &lt;br /&gt;
     while len(heap) &amp;gt; 0:                  # solange noch Kanten vorhanden sind&lt;br /&gt;
         w, edge = heapq.heappop(heap)     # billigste Kante aus dem Heap nehmen&lt;br /&gt;
         a1 = findAnchor(anchors, edge[0]) # Anker von Startknoten der Kante&lt;br /&gt;
         a2 = findAnchor(anchors, edge[1]) # ... und Endknoten bestimmen&lt;br /&gt;
         if a1 != a2:                      # wenn die Knoten in verschiedenen Komponenten sind&lt;br /&gt;
             anchors[a2] = a1              # Komponenten verschmelzen&lt;br /&gt;
             result.append(edge)           # ... und Kante in MST einfügen&lt;br /&gt;
     &lt;br /&gt;
     return result                         # Kanten des MST zurückgeben&lt;br /&gt;
&lt;br /&gt;
Die Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt; wurde im Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Union-Find-Algorithmus|Zusammenhangskomponenten mit Union-Find-Algorithmus]] implementiert. Im Unterschied zum Algorithmus von Prim geben wir hier nicht die property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; zurück, sondern einfach eine Liste der Kanten im MST.&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus eignet sich insbesondere für das Clusteringproblem, da der Schwellwert von vornerein als maximales Kantengewicht an den Algorithmus übergeben werden kann. Man hört mit dem Vereinigen auf, wenn das Gewicht der billigste Kante im Heap den Schwellwert überschreitet. Beim Algorithmus von Kruskal kann dann keine bessere Kante als der Schwellwert mehr kommen, da die Kanten vorher sortiert worden sind. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Komplexität:&amp;lt;/b&amp;gt; wie beim Dijkstra-Algorithmus, weil jede Kante genau einmal in den Heap kommt. Der Aufwand für das Sortieren ist somit &amp;lt;math&amp;gt;O\left(E\log E\right)&amp;lt;/math&amp;gt;, was sich zu &amp;lt;math&amp;gt;O \left(E\,\log\,V\right)&amp;lt;/math&amp;gt; reduziert, falls keine Mehrfachkanten vorhanden sind.&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; geeignet für Übungsaufgabe&lt;br /&gt;
&lt;br /&gt;
====Verwendung einer BucketPriorityQueue====&lt;br /&gt;
&lt;br /&gt;
Beide Algorithmen zur Bestimmung des minimalen Spannbaums benötigen eine Prioritätswarteschlange. Wenn die Kantengewichte ganze Zahlen im Bereich &amp;lt;tt&amp;gt;0...(m-1)&amp;lt;/tt&amp;gt; sind, kann man die MST-Algorithmen deutlich beschleunigen, wenn man anstelle des Heaps eine [[Prioritätswarteschlangen#Prioritätssuche mit dem Bucket-Prinzip|&amp;lt;tt&amp;gt;BucketPriorityQueue&amp;lt;/tt&amp;gt;]] verwendet. Die Operationen zum Einfügen einer Kante in die Queue und zum Entfernen der billibsten Kante aus der Queue beschleunigen sich dadurch auf O(1) statt O(log V) (außer wenn die Gewichte sehr ungünstig auf die Kanten verteilt sind). In der Praxis erreicht man durch diese Änderung typischerweise deutliche Verbesserungen. In der Bildverarbeitung können die Prioritäten beispielsweise die Wahrscheinlichkeit kodieren, dass zwei benachbarte Pixel zu verschiedenen Objekten gehören. Bildet man jetzt den MST, und bricht bei einer bestimmten Wahrscheinlichkeit ab, erhält man Cluster von Pixeln, die wahrscheinlich zum selben Objekt gehören (weil der MST ja die Kanten mit minimalem Gewicht bevorzugt, und kleine Gewichte bedeuten kleine Wahrscheinlichkeit, dass benachbarte Pixel von einander getrennt werden). Da man die Wahrscheinlichkeiten nur mit einer Genauigkeit von ca. 1% berechnen kann, reichen hiefür 100 bis 200 Quantisierungstufen aus. Durch Verwendung der schnellen &amp;lt;tt&amp;gt;BucketPriorityQueue&amp;lt;/tt&amp;gt; kann man jetzt wesentlich größere Bilder in akzeptabler Zeit bearbeiten als dies mit einem Heap möglich wäre.&lt;br /&gt;
&lt;br /&gt;
== Algorithmen für gerichtete Graphen ==&lt;br /&gt;
&lt;br /&gt;
Zur Erinnerung: in einem gerichteten Graphen sind die Kanten (i &amp;amp;rarr; j) und (j &amp;amp;rarr; i) voneinander verschieden, und eventuell existiert nur eine der beiden Richtungen. Im allgemeinen unterscheidet sich der [[Graphen_und_Graphenalgorithmen#transposed_graph|transponierte Graph]] G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; also vom Originalgraphen G. Beim Traversieren des Graphen und bei der Pfadsuche dürfen Kanten nur in passender Richtung verwendet werden. Bei gewichteten Graphen tritt häufig der Fall auf, dass zwar Kanten in beiden Richtungen existieren, diese aber unterschiedliche Gewichte haben.&lt;br /&gt;
&lt;br /&gt;
Gerichtete Graphen ergeben sich in natürlicher Weise aus vielen Anwendungsproblemen:&lt;br /&gt;
* Routenplanung&lt;br /&gt;
** Bei Straßennetzwerken enstehen gerichtete Graphen, sobald es Einbahnstraßen gibt.&lt;br /&gt;
** Verwendet man Gewichte, um die erwarteten Fahrzeiten entlang einer Straße zu kodieren, gibt es Asymmetrien z.B. dann, wenn Straßen in einer Richtung bergab, in der anderen bergauf befahren werden. Hier existieren zwar Kanten in beiden Richtungen, sie haben aber unterschiedliche Gewichte. Ähnliches gilt für Flüge: Durch den Gegenwind des Jetstreams braucht man von Frankfurt nach New York länger als umgekehrt von New York nach Frankfurt.&lt;br /&gt;
* zeitliche oder kausale Abhängigkeiten&lt;br /&gt;
** Wenn die Knoten Ereignisse repräsentieren, von denen einige die Ursache von anderen sind, diese wiederum die Ursache der nächsten usw., verbindet man die Knoten zweckmäßig durch gerichtete Kanten, die die Kausalitätsbeziehungen kodieren. Handelt es sich um logische &amp;quot;wenn-dann&amp;quot;-Regeln, erhält man einen [[Graphen_und_Graphenalgorithmen#Anwendung:_Das_Erf.C3.BCllbarkeitsproblem_in_Implikationengraphen|Implikationengraph]] (siehe unten). Handelt es sich hingegen um Wahrscheinlichkeitsaussagen (&amp;quot;Wenn das Wetter schön ist, haben Studenten tendenziell gute Laune, wenn eine Prüfung bevorsteht eher schlechte usw.&amp;quot;), erhält man ein [http://de.wikipedia.org/wiki/Bayessches_Netz Bayessches Netz].&lt;br /&gt;
** Wenn bestimmte Aufgaben erst begonnen werden können, nachdem andere Aufgaben erledigt sind, erhält man einen Abhängigkeitsgraphen. Beispielsweise dürfen Sie erst an der Klausur teilnehmen, nachdem Sie die Übungsaufgaben gelöst haben, und Sie dürfen erst die Abschlussarbeit beginnen, nachdem Sie bestimmte Prüfungen bestanden haben. Ein anderes schönes Beispiel liefern die Regeln für das [[Graphen_und_Graphenalgorithmen#Anwendung:_Abh.C3.A4ngigkeitsgraph|Ankleiden]] weiter unten.&lt;br /&gt;
** Gerichtete Graphen kodieren die Abhängigkeiten zwischen Programmbibliotheken. Beispielsweise benötigt das Pythonmodul &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; die internen Submodule &amp;lt;tt&amp;gt;json.encoder&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;json.decode&amp;lt;/tt&amp;gt; sowie das externe Modul &amp;lt;tt&amp;gt;decimal&amp;lt;/tt&amp;gt;. Die Submodule benötigen wiederum die externen Module &amp;lt;tt&amp;gt;re&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;sys&amp;lt;/tt&amp;gt;, das Modul &amp;lt;tt&amp;gt;decimal&amp;lt;/tt&amp;gt; braucht &amp;lt;tt&amp;gt;copy&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;collections&amp;lt;/tt&amp;gt; usw.&lt;br /&gt;
** Das Internet kann als gerichteter Graph dargestellt werden, wobei die Webseiten die Knoten, und die Hyperlinks die Kanten sind.&lt;br /&gt;
* Sequence Alignment&lt;br /&gt;
** Eine gute Rechtschreibprüfung markiert nicht nur fehlerhafte Wörter, sondern macht auch plausible Vorschläge, was eigentlich gemeint gewesen sein könnte. Dazu muss sie das gegebene Wort mit den Wörtern eines Wörterbuchs vergleichen und die Ähnlichkeit bewerten. Ein analoges Problem ergibt sich, wenn man DNA Fragmente mit der Information in einer Genomdatenbank abgleichen will. &lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Sequence Alignment / Edit Distance ===&lt;br /&gt;
&lt;br /&gt;
:gegeben: zwei Wörter (allgemein: beliebige Zeichenfolgen)&lt;br /&gt;
:gesucht: Wie kann man die Buchstaben am besten in Übereinstimmung bringen?&lt;br /&gt;
&lt;br /&gt;
:Beispiel: WORTE – NORDEN&lt;br /&gt;
&lt;br /&gt;
Zwei mögliche Alignments sind&lt;br /&gt;
&lt;br /&gt;
  W&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;OR&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;T&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;E&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;.          W.ORTE&lt;br /&gt;
  N&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;OR&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;D&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;E&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;N          NORDEN&lt;br /&gt;
&lt;br /&gt;
wobei der Punkt anzeigt, dass der untere Buchstabe keinen Partner hat, und rote Buchstaben oben und unten übereinstimmen. Jede Nicht-Übereinstimmung verursacht nun gewisse Kosten. Dabei unterscheiden wir zwei Fälle:&lt;br /&gt;
# Matche a[i] mit b[j]. Falls a[i] == b[j], ist das gut (rote Buchstaben), und es entstehen keine Kosten. Andernfalls entstehen Kosten U (schwarze Buchstaben).&lt;br /&gt;
# Wir überspringen a[i] oder b[j] (Buchstabe vs. Punkt). Dann entstehen Kosten V. (Manchmal unterscheidet man auch noch Kosten Va und Vb, wenn das Überspringen bei a und b unterschieldiche Signifikanz hat.)&lt;br /&gt;
&lt;br /&gt;
Gesucht ist nun das &amp;lt;b&amp;gt;Alignment mit minimalen Kosten&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Diese Aufgabe kann man sehr schön als gerichteten Graphen darstellen: Wir definieren ein rechteckiges Gitter und schreiben das erste Wort über das Gitter und das andere links davon. Die Gitterpunkte verbinden wir mit Pfeilen (gerichteten Kanten), wobei ein Pfeil nach rechts bedeutet, dass wir beim oberen Wort einen Buchstaben überspringen, ein Pfeil nach unten, dass wir beim linken Wort einen Buchstaben überspringen, und ein diagonaler Pfeil, dass wir zwei Buchstaben matchen (und zwar die am Pfeilende). Die Farben der Pfeile symbolisieren die Kosten: rot für das Überspringen eines Buchstabens (Kosten V), blau für das Matchen, wenn die Buchstaben nicht übereinstimmen (Kosten U), und grün, wenn die Buchstaben übereinstimmen (keine Kosten). &lt;br /&gt;
&lt;br /&gt;
[[Image:sequence-alignment.png|300px]]&lt;br /&gt;
&lt;br /&gt;
Lösung:&lt;br /&gt;
:Suche den kürzesten Pfad vom Knoten &amp;quot;START&amp;quot; (oben links) nach unten rechts. Dazu kann der [[Graphen und Graphenalgorithmen#Algorithmus von Dijkstra|Algorithmus von Dijkstra]] verwendet werden, der auf gerichteten Graphen genauso funktioniert wie auf ungerichteten.&lt;br /&gt;
&lt;br /&gt;
Für unser Beispiel von oben erhalten wir die folgenden Pfade:&lt;br /&gt;
&lt;br /&gt;
[[Image:sequence-alignment-weg1.png|400px]]&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;[[Image:sequence-alignment-weg2.png|400px]]&lt;br /&gt;
&lt;br /&gt;
Durch Addieren der Kosten entsprechend der Farben sieht man, dass der erste Weg die Kosten 2U+V und der zweite die Kosten 5U+V hat. Der erste Weg ist offensichtlich günstiger und entspricht dem besten Alignment.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Abhängigkeitsgraph ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Beispiel: &amp;lt;/b&amp;gt; Wie erklärt man einem zerstreuten Professor, wie er sich morgens anziehen soll? Der folgende Graph enthält einen Knoten für jede Aktion, und eine Kante (i &amp;amp;rarr; j) bedeutet, dass die Aktion i vor der Aktion j abgeschlossen werden muss.&lt;br /&gt;
&lt;br /&gt;
[[Image:anziehen-graph.png|600px]]&lt;br /&gt;
&lt;br /&gt;
In derartigen Abhängigkeitsgraphen ist die wichtigste Frage immer, ob der Graph azyklisch ist. Wäre dies nämlich nicht der Fall, kann es keine Reihenfolge der Aktionen geben, die alle Abhängigkeiten erfüllt. Dies sieht man leicht, wenn man den einfachsten möglichen Zyklus betrachtet: es gibt sowohl eine Kante (i &amp;amp;rarr; j) als auch eine (j &amp;amp;rarr; i). Dann müsste man i vor j erledigen, aber ebenso j vor i, was offensichtlich unmöglich ist - das im Graph kodierte Problem ist dann unlösbar. Wegen ihrer Wichtigkeit wird für gerichtete azyklische Graphen oft die Abkürzung &amp;lt;b&amp;gt;DAG&amp;lt;/b&amp;gt; (von &amp;lt;i&amp;gt;directed acyclic graph&amp;lt;/i&amp;gt;) verwendet. Ein Graph ist genau dann ein DAG, wenn es eine topologische Sortierung gibt:&lt;br /&gt;
;topologische Sortierung: Zeichne die Knoten so auf eine Gerade, dass alle Kanten (Pfeile) nach rechts zeigen. &lt;br /&gt;
Arbeitet man die Aktionen nach einer (beliebigen) topologischen Sortierung ab, werden automatisch alle Abhängigkeiten eingehalten: Da alle Pfeile nach rechts zeigen, werden abhängige Aktionen immer später ausgeführt. Die topologische Sortierung ist im allgemeinen nicht eindeutig. Die folgende Skizze zeigt eine mögliche topologische Sortierung für das Anziehen:&lt;br /&gt;
&lt;br /&gt;
[[Image:anziehen-topologische-sortierung.png|600px]]&lt;br /&gt;
&lt;br /&gt;
Eine solche fest vorgegebene Reihenfolge ist für den zerstreuten Professor sicherlich eine größere Hilfe als der ursprüngliche Graph. Man erkennt, dass die Sortierung nicht eindeutig ist, beispielsweise bei der Uhr: Da für die Uhr keine Abhängigkeiten definiert sind, kann man diese Aktion an beliebiger Stelle einsortieren. Hier wurde willkürlich die letzte Stelle gewählt.&lt;br /&gt;
&lt;br /&gt;
==== Zwei Algorithmen zum Finden der topologischen Sortierung ====&lt;br /&gt;
&lt;br /&gt;
Die folgenden Algorithmen finden entweder eine topologische Sortierung, oder signalisieren, dass der Graph zyklisch ist.&lt;br /&gt;
&lt;br /&gt;
===== Algorithmus 1 =====&lt;br /&gt;
# Suche einen Knoten mit Eingangsgrad 0 (ohne eingehende Pfeile) =&amp;gt; in einem gerichteten azyklischen Graphen gibt es immer einen solchen Knoten&lt;br /&gt;
# Platziere diesen Knoten auf der Geraden (beliebig)&lt;br /&gt;
# Entferne den Knoten aus dem Graphen zusammen mit den ausgehenden Kanten&lt;br /&gt;
# Gehe zu 1., aber platziere in 2. immer rechts der Knoten, die schon auf der Geraden vorhanden sind.&lt;br /&gt;
: =&amp;gt; Wenn noch Knoten übrig sind, aber keiner Eingangsgrad 0 hat, muss der Graph zyklisch sein.&lt;br /&gt;
&lt;br /&gt;
[[Image:bild6.JPG]]&lt;br /&gt;
&lt;br /&gt;
Beispiel für einen zyklischen Graphen: kein Knoten hat Eingangsgrad 0.&lt;br /&gt;
&lt;br /&gt;
Um den Algorithmus zu implementieren, verwenden wir eine property map &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt;, die wir in einem ersten Durchlauf durch den Graphen füllen und die dann für jeden Knoten die Anzahl der eingehenden Kanten speichert. Dann gehen wir sukzessive zu allen Knoten mit &amp;lt;tt&amp;gt;in_degree == 0&amp;lt;/tt&amp;gt;. Anstatt sie aber tatsächlich aus dem Graphen zu entfernen wie im obigen Pseudocode, dekrementieren wir nur den &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt; ihrer Nachbarn. Wird der &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt; eines Nachbarn dadurch 0, wird er ebenfalls in das Array der zu scannenden Knoten aufgenommen. Wenn der Graph azyklisch ist, enthält das Array am Ende alle Knoten des Graphen, und die Reihenfolge der Einfügungen definiert eine topologische Sortierung. Andernfalls ist das Array zu kurz, und wir signalisieren durch Zurückgeben von &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, dass der Graph zyklisch ist:&lt;br /&gt;
&lt;br /&gt;
 def topological_sort(graph):              # ein gerichteter Graph&lt;br /&gt;
     in_degree = [0]*len(graph)            # property map für den Eingangsgrad jeden Knotens&lt;br /&gt;
     for node in xrange(len(graph)):       # besuche alle Knoten&lt;br /&gt;
         for neighbor in graph[node]:      #  ... und deren Nachbarn&lt;br /&gt;
             in_degree[neighbor] += 1      #  ... und inkrementiere den Eingangsgrad&lt;br /&gt;
     &lt;br /&gt;
     result = []                           # wird später die topologische Sortierung enthalten&lt;br /&gt;
     for node in xrange(len(graph)):&lt;br /&gt;
         if in_degree[node] == 0:&lt;br /&gt;
             result.append(node)           # füge alle Knoten mit Eingangsgrad 0 in result ein&lt;br /&gt;
     &lt;br /&gt;
     k = 0&lt;br /&gt;
     while k &amp;lt; len(result):                # besuche alle Knoten mit Eingangsgrad 0&lt;br /&gt;
         node = result[k]&lt;br /&gt;
         k += 1&lt;br /&gt;
         for neighbor in graph[node]:      # besuche alle Nachbarn&lt;br /&gt;
             in_degree[neighbor] -= 1      # entferne 'virtuell' die eingehende Kante&lt;br /&gt;
             if in_degree[neighbor] == 0:  # wenn neighbor jetzt Eingangsgrad 0 hat&lt;br /&gt;
                 result.append(neighbor)   #  ... füge ihn in result ein&lt;br /&gt;
     &lt;br /&gt;
     if len(result) == len(graph):         # wenn alle Knoten jetzt Eingangsgrad 0 haben&lt;br /&gt;
         return result                     # ... ist result eine topologische Sortierung&lt;br /&gt;
     else:&lt;br /&gt;
         return None                       # andernfalls ist der Graph zyklisch&lt;br /&gt;
&lt;br /&gt;
===== Algorithmus 2 =====&lt;br /&gt;
Der obige Algorithmus hat den Nachteil, dass er jeden Knoten zweimal expandiert. Man kann eine topologische Sortierung stattdessen auch mit Tiefensuche bestimmen. Es gilt nämlich der folgende&lt;br /&gt;
;Satz: Wird ein DAG mittels Tiefensuche traversiert, definiert die &amp;lt;i&amp;gt;reverse post-order&amp;lt;/i&amp;gt; eine topologische Sortierung.&lt;br /&gt;
Zur Erinnerung: die post-order erhält man, indem man jeden Knoten ausgibt, &amp;lt;i&amp;gt;nachdem&amp;lt;/i&amp;gt; die Rekursion zu allen seinen Nachbarn beendet ist, siehe unsere [[Graphen_und_Graphenalgorithmen#pre_and_post_order|Diskussion weiter oben]]. Die reverse post-order ist gerade die Umkehrung dieser Reihenfolge. Die folgende Implementation verwendet die rekursive Version der Tiefensuche, in der Praxis wird man meist die iterative Version mit Stack bevorzugen, weil bei großen Graphen die Aufruftiefe sehr groß werden kann:&lt;br /&gt;
&lt;br /&gt;
 def reverse_post_order(graph):               # gerichteter Graph&lt;br /&gt;
     result = []                              # enthält später die reverse post-order&lt;br /&gt;
     visited = [False]*len(graph)             # Flags für bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):                         # besuche node&lt;br /&gt;
         if not visited[node]:                # aber nur, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True             # markiere ihn als besucht&lt;br /&gt;
             for neighbor in graph[node]:     # und besuche die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
             result.append(node)              # alle Nachbarn besucht =&amp;gt; Anhängen an result liefert post-order&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):          # besuche alle Knoten&lt;br /&gt;
         visit(node)&lt;br /&gt;
     &lt;br /&gt;
     result.reverse()                         # post-order =&amp;gt; reverse post-order&lt;br /&gt;
     return result&lt;br /&gt;
&lt;br /&gt;
Die Tatsache, dass die reverse post-order tatsächlich eine topologische Sortierung liefert, leuchtet wahrscheinlich nicht unmittelbar ein. Bevor wir diese Tatsache beweisen. wollen wir uns anhand des Ankleidegraphen klar machen, dass die pre-order (die man intuitiv vielleicht eher wählen würde) keine topologische Sortierung ist. Startet man die Tiefensuche beim Knoten &amp;quot;Unterhemd&amp;quot;, werden die Knoten in der Reihenfolge &amp;quot;Unterhemd&amp;quot;, &amp;quot;Oberhemd&amp;quot;, &amp;quot;Schlips&amp;quot;, &amp;quot;Jackett&amp;quot;, &amp;quot;Gürtel&amp;quot; gefunden. Da dann alle von &amp;quot;Unterhemd&amp;quot; erreichbaren Knoten erschöpft sind, startet man die Tiefensuche als nächstes bei &amp;quot;Unterhose&amp;quot; und erreicht von dort aus &amp;quot;Hose&amp;quot; und &amp;quot;Schuhe&amp;quot;. Man erkennt sofort, dass diese Reihenfolge nicht funktioniert: &amp;quot;Hose&amp;quot; kommt nach &amp;quot;Gürtel&amp;quot;, und &amp;quot;Jackett&amp;quot; kommt vor &amp;quot;Gürtel&amp;quot;. Bei dieser Anordnung gibt es Pfeile nach links, die Abhängigkeitsbedingungen sind somit verletzt.&lt;br /&gt;
&lt;br /&gt;
Damit die reverse post-order eine zulässige Sortierung sein kann, muss stets gelten, dass Knoten u vor Knoten v einsortiert wurde, wenn die Kante (u &amp;amp;rarr; v) existiert. Das ist aber äquivalent zur Forderung, dass in der ursprünglichen post-order (vor dem &amp;lt;tt&amp;gt;reverse&amp;lt;/tt&amp;gt;) u hinter v stehen muss. Wir betrachten den &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt;-Aufruf, bei dem u expandiert wird. Gelangt man jetzt zu u's Nachbarn v, gibt es zwei Möglichkeiten: Wenn v bereits expandiert wurde, befindet es sich bereits im Array &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt; kehrt sofort zurück. Andernfalls wird v ebenfalls expandiert und demzufolge in &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; eingetragen, &amp;lt;i&amp;gt;bevor&amp;lt;/i&amp;gt; der rekursive Aufruf &amp;lt;tt&amp;gt;visit(v)&amp;lt;/tt&amp;gt; zurückkehrt. Knoten u wird aber erst in &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; eingefügt, &amp;lt;i&amp;gt;nachdem&amp;lt;/i&amp;gt; alle rekursiven &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt;-Aufrufe seiner Nachbarn zurückgekehrt sind. In beiden Fällen steht u in der post-order wie gefordert hinter v, und daraus folgt die Behauptung.&lt;br /&gt;
&lt;br /&gt;
Der obige Algorithmus liefert natürlich nur dann eine topologische Sortierung, wenn der Graph wirklich azyklisch ist (man kann ihn aber auch anwenden, um die reverse post-order für einen zyklischen Graphen zu bestimmen, siehe Abschnitt &amp;quot;[[Graphen_und_Graphenalgorithmen#Transitive Hülle und stark zusammenhängende Komponenten|Stark zusammenhängende Komponenten]]&amp;quot;). Dieser Fall tritt in der Praxis häufig auf, weil zyklische Graphen bei vielen Anwendungen gar nicht erst entstehen können. Weiß man allerdings nicht, ob der Graph azyklisch ist oder nicht, muss man einen zusätzlichen Test auf Zyklen in den Algorithmus einbauen. &lt;br /&gt;
&lt;br /&gt;
Zyklische Graphen sind dadurch gekennzeichnet, dass es im obigen Beweis eine dritte Möglichkeit gibt: Während der Expansion von u wird rekursiv v expandiert, und es gibt eine Rückwärtskante (v &amp;amp;rarr; u). (Es spielt dabei keine Rolle, ob v von u aus direkt oder indirekt erreicht wurde.) Ein Zyklus wird also entdeckt, wenn die Tiefensuche zu u zurückkehrt, solange u noch &amp;lt;i&amp;gt;aktiv&amp;lt;/i&amp;gt; ist, d.h. wenn die Rekursion von u aus gestartet und noch nicht beendet wurde. Dies kann man leicht feststellen, wenn man in der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; drei Werte zulässt: 0 für &amp;quot;noch nicht besucht&amp;quot;, 1 für &amp;quot;aktiv&amp;quot; und 2 für &amp;quot;beendet&amp;quot;. Wir signalisieren einen Zyklus, sobald &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt; für einen Knoten aufgerufen wird, der gerade aktiv ist:&lt;br /&gt;
&lt;br /&gt;
 def topological_sort_DFS(graph):             # gerichteter Graph&lt;br /&gt;
     result = []                              # enthält später die topologische Sortierung&lt;br /&gt;
     &lt;br /&gt;
     not_visited, active, finished = 0, 1, 2  # drei Zustände für visited&lt;br /&gt;
     visited = [not_visited]*len(graph)       # Flags für aktive und bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):                         # besuche node (gibt &amp;quot;True&amp;quot; zurück, wenn Zyklus gefunden wurde)&lt;br /&gt;
         if visited[node] == not_visited:     # ... aber nur, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = active           # markiere ihn als aktiv&lt;br /&gt;
             for neighbor in graph[node]:     # und besuche die Nachbarn&lt;br /&gt;
                 if visit(neighbor):          # wenn rekursiv ein Zyklus gefunden wurde&lt;br /&gt;
                     return True              # ... brechen wir ab und signalisieren den Zyklus&lt;br /&gt;
             visited[node] = finished         # Rekursion beendet, node ist nicht mehr aktiv&lt;br /&gt;
             result.append(node)              # alle Nachbarn besucht =&amp;gt; Anhängen an result liefert post-order&lt;br /&gt;
             return False                     # kein Zyklus gefunden&lt;br /&gt;
         elif visited[node] == active:        # Rekursion erreicht einen noch aktiven Knoten&lt;br /&gt;
             return True                      #  =&amp;gt; Zyklus gefunden&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):          # besuche alle Knoten&lt;br /&gt;
         if visit(node):                      # wenn Zyklus gefunden wurde&lt;br /&gt;
             return None                      # ... gibt es keine topologische Sortierung&lt;br /&gt;
     &lt;br /&gt;
     result.reverse()                         # post-order =&amp;gt; reverse post-order (=topologische Sortierung)&lt;br /&gt;
     return result&lt;br /&gt;
&lt;br /&gt;
Man macht sich leicht klar, dass kein Zyklus vorliegt, wenn die Rekursion einen Knoten erreicht, der bereits auf &amp;lt;tt&amp;gt;finished&amp;lt;/tt&amp;gt; gesetzt ist. Nehmen wir an, dass u gerade expandiert wird, und sein Nachbar v ist bereits &amp;lt;tt&amp;gt;finished&amp;lt;/tt&amp;gt;. Wenn es einen Zyklus gäbe, müsste es einen Weg von v nach u geben. Dann wäre u aber bereits während der Expansion von v gefunden worden. Da v nicht mehr im Zustand &amp;lt;tt&amp;gt;active&amp;lt;/tt&amp;gt; ist, muss die Expansion von v schon abgeschlossen gewesen sein, ohne dass u gefunden wurde. Folglich kann es keinen solchen Zyklus geben.&lt;br /&gt;
&lt;br /&gt;
=== Transitive Hülle und stark zusammenhängende Komponenten ===&lt;br /&gt;
&lt;br /&gt;
Auch bei gerichteten Graphen ist die Frage, welche Knoten miteinander zusammenhängen, von großem Interesse. Wir betrachten dazu wieder die Relation &amp;quot;Knoten v ist von Knoten u aus erreichbar&amp;quot;, die anzeigt, ob es einen Weg von u nach v gibt oder nicht. In ungerichteten Graphen ist diese Relation immer symmetrisch, weil jeder Weg in beiden Richtungen benutzt werden kann. In gerichteten Graphen gilt dies nicht. Man muss hier zwei Arten von Zusammenhangskomponenten unterscheiden:&lt;br /&gt;
;Transitive Hülle: Die transitive Hülle eines Knotens u ist die Menge aller Knoten, die von u aus erreichbar sind:&lt;br /&gt;
:&amp;lt;math&amp;gt;T(u) = \{v\ |\ u \rightsquigarrow v\}&amp;lt;/math&amp;gt;&lt;br /&gt;
;Stark zusammenhängende Komponenten: Die stark zusammenhängende Komponenten &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; eines gerichteten Graphen sind maximale Teilgraphen, so dass alle Knoten innerhalb einer Komponente von jedem anderen Knoten der selben Komponente aus erreichbar sind&lt;br /&gt;
:&amp;lt;math&amp;gt;u,v \in C_i\ \ \Leftrightarrow\ \ u \rightsquigarrow v \wedge v \rightsquigarrow u&amp;lt;/math&amp;gt;&lt;br /&gt;
Die erste Definition betrachtet den Zusammenhang asymmetrisch, ohne Beachtung der Frage, ob es auch einen Rückweg von Knoten v nach u gibt, die zweite hingegen symmetrisch.&lt;br /&gt;
&lt;br /&gt;
Die &amp;lt;b&amp;gt;transitive Hülle&amp;lt;/b&amp;gt; benötigt man, wenn man Fragen der Erreichbarkeit besonders effizient beantworten will. Wir hatten bespielsweise oben erwähnt, dass das Python-Modul &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; direkt und indirekt von mehreren anderen Module abhängt, die vorher installiert werden müssen, damit &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; funktioniert. Bittet man den Systemadministrator, das &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt;-Paket zu installieren, will er diese Abhängigkeiten wahrscheinlich nicht erst mühsam rekursiv heraussuchen, sondern er verlangt eine Liste aller Pakete, die installiert werden müssen. Dies ist gerade die transitive Hülle von &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; im Abhängigkeitsgraphen. Damit man diese nicht manuell bestimmen muss, verwendet man Installationsprogramme wie z.B. [http://pypi.python.org/pypi/pip/ pip], die die Abhängigkeiten automatisch herausfinden und installieren. &lt;br /&gt;
&lt;br /&gt;
Bei der Bestimmung der transitiven Hülle modifiziert man den gegebenen Graphen, indem man jedesmal eine neue Kante (u &amp;amp;rarr; v) einfügt, wenn diese Kante noch nicht existiert, aber v von u aus erreichbar ist. Dies gelingt mit einer sehr einfachen Variation der Tiefensuche: Wir rufen &amp;lt;tt&amp;gt;visit(k)&amp;lt;/tt&amp;gt; für jeden Knoten k auf, aber setzen die property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; zuvor auf &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt; zurück. Alle Knoten, die während der Rekursion erreicht werden, sind im modifizierten Graphen Nachbarn von k. Ein etwas effizienterer Ansatz ist der [http://de.wikipedia.org/wiki/Algorithmus_von_Floyd_und_Warshall Algorithmus von Floyd und Warshall].&lt;br /&gt;
&lt;br /&gt;
Die Bestimmung der &amp;lt;b&amp;gt;stark zusammenhängenden Komponenten&amp;lt;/b&amp;gt; ist etwas schwieriger. Es existieren eine ganze Reihe von effizienten Algorithmen (siehe [http://en.wikipedia.org/wiki/Strongly_connected_component WikiPedia]), deren einfachster der Algorithmus von Kosaraju ist:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;gegeben:&amp;lt;/b&amp;gt; gerichteter Graph&lt;br /&gt;
&lt;br /&gt;
# Bestimme die reverse post-order (mit der Funktion &amp;lt;tt&amp;gt;reverse_post_order&amp;lt;/tt&amp;gt;)&lt;br /&gt;
# Bilde den transponierten Graphen &amp;lt;math&amp;gt;G^T&amp;lt;/math&amp;gt; (mit der Funktion &amp;lt;tt&amp;gt;transposeGraph&amp;lt;/tt&amp;gt;)&lt;br /&gt;
# Bestimme die Zusammenhangskomponenten von &amp;lt;math&amp;gt;G^T&amp;lt;/math&amp;gt; mittels Tiefensuche, aber betrachte die Knoten dabei in der reverse post-order aus Schritt 1 (dies kann mit einer minimalen Modifikation der Funktion &amp;lt;tt&amp;gt;connectedComponents&amp;lt;/tt&amp;gt; geschehen, indem man die Zeile &amp;lt;tt&amp;gt;for node in xrange(len(graph)):&amp;lt;/tt&amp;gt; einfach nach &amp;lt;tt&amp;gt;for node in ordered:&amp;lt;/tt&amp;gt; abändert, wobei &amp;lt;tt&amp;gt;ordered&amp;lt;/tt&amp;gt; das Ergebnis der Funktion &amp;lt;tt&amp;gt;reverse_post_order&amp;lt;/tt&amp;gt; ist, also ein Array, das die Knoten in der gewünschten Reihenfolge enthält).&lt;br /&gt;
Die Zusammenhangskomponenten, die man in Schritt 3 findet, sind gerade die stark zusammenhängenden Komponenten des Originalgraphen G. Die folgende Skizze zeigt diese in grün für den schwarz gezeichneten gerichteten Graphen. &lt;br /&gt;
&lt;br /&gt;
[[Image:strongly-connected-components.png|400px]]    &lt;br /&gt;
&lt;br /&gt;
Zum Beweis der Korrektheit des Algorithmus von Kosaraju zeigen wir zwei Implikationen: 1. Wenn die Knoten u und v in der selben stark zusammenhängenden Komponente liegen, werden sie in Schritt 3 des Algorithmus auch der selben Komponente zugewiesen. 2. Wenn die Knoten u und v in Schritt 3 der selben Komponente zugewiesen wurden, müssen sie auch in der selben stark zusammenhängenden Komponente liegen. &lt;br /&gt;
# Knoten u und v gehören zur selben stark zusammenhängenden Komponente von G. Per Definition gilt, dass u von v aus erreichbar ist und umgekehrt. Dies muss auch im transponierten Graphen G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; gelten (der Weg &amp;lt;math&amp;gt;u \rightsquigarrow v&amp;lt;/math&amp;gt; wird jetzt zum Weg &amp;lt;math&amp;gt;v \rightsquigarrow u&amp;lt;/math&amp;gt; und umgekehrt). Wird u bei der Tiefensuche in Schritt 3 vor v expandiert, ist v von u aus erreichbar und gehört somit zur selben Komponente. Das umgekehrte gilt, wenn v vor u expandiert wird. Daraus folgt die Behauptung 1.&lt;br /&gt;
# Knoten u und v werden in Schritt 3 der selben Komponente zugewiesen: Sei x der Anker dieser Komponente. Da u in der gleichen Komponente wie x liegt, muss es in G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; einen Weg &amp;lt;math&amp;gt;x \rightsquigarrow u&amp;lt;/math&amp;gt;, und demnach in G einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow x&amp;lt;/math&amp;gt; geben. Da x der Anker seiner Komponente ist, wissen wir aber auch, dass x in der reverse post-order &amp;lt;i&amp;gt;vor&amp;lt;/i&amp;gt; u liegt (denn der Anker ist der Knoten, mit dem eine neue Komponente gestartet wird; er muss deshalb im Array &amp;lt;tt&amp;gt;ordered&amp;lt;/tt&amp;gt; als erster Konten seiner Komponente gefunden worden sein). Wir unterscheiden jetzt im Schritt 1 des Algorithmus zwei Fälle:&lt;br /&gt;
## u wurde bei der Bestimmung der post-order vor x expandiert. Dann kann x nur dann in der reverse post-order &amp;lt;i&amp;gt;vor&amp;lt;/i&amp;gt; u liegen (oder, einfacher ausgedrückt, x kann nur dann in der post-order &amp;lt;i&amp;gt;hinter&amp;lt;/i&amp;gt; u liegen), wenn x im Graphen G nicht von u aus erreichbar war. Das ist aber unmöglich, weil wir ja schon wissen, dass es in G einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow x&amp;lt;/math&amp;gt; gibt.&lt;br /&gt;
## Folglich wurde u bei der Bestimmung der post-order nach x expandiert. Da x in der post-order hinter u liegt, muss u während der Expansion von x erreicht worden sein. Deshalb muss es in G auch einen Weg &amp;lt;math&amp;gt;x \rightsquigarrow u&amp;lt;/math&amp;gt; geben.&lt;br /&gt;
#:Somit sind x und u in der selben stark zusammenhängenden Komponente. Die gleiche Überlegung gilt für x und v. Wegen der Transitivität der relation &amp;quot;ist erreichbar&amp;quot; folgt daraus, dass auch u und v in der selben Komponente liegen, also die Behauptung 2.&lt;br /&gt;
&lt;br /&gt;
Die folgende Skizze illustriert, dass der Komponentengraph stets azyklisch ist. Den Komponentengraph erhält man, indem man für jede Komponente &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; einen Knoten erzeugt (grün), und die Knoten i und j durch eine gerichtete Kante verbindet (rot), wenn es im Originalgraphen eine Kante (u &amp;amp;rarr; v) mit &amp;lt;math&amp;gt;u \in C_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v \in C_j&amp;lt;/math&amp;gt; gibt. Es ist dann garantiert, dass es keine Kante in umgekehrter Richtung geben kann. Daraus folgt insbesondere, dass ein DAG nur triviale stark verbundene Komponenten haben kann, die aus einzelnen Knoten bestehen.&lt;br /&gt;
&lt;br /&gt;
[[Image:strongly-connected-components-graph.png|400px]]&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Das Erfüllbarkeitsproblem in Implikationengraphen ===&lt;br /&gt;
&lt;br /&gt;
Das Erfüllbarkeitsproblem hat auf den ersten Blick nichts mit Graphen zu tun, denn es geht um Wahrheitswerte logischer Ausdrücke. Man kann logische Ausdrücke jedoch unter bestimmten Bedingungen in eine Graphendarstellung überführen und somit das ursprüngliche Problem auf ein Problem der Graphentheorie reduzieren, für das bereits ein Lösungsverfahren bekannt ist. In diesem Abschnitt wollen wir dies für die sogenannten Implikationengraphen zeigen, ein weiteres Beispiel findet sich im Kapitel [[NP-Vollständigkeit]].&lt;br /&gt;
&lt;br /&gt;
==== Das Erfüllbarkeitsproblem ====&lt;br /&gt;
&lt;br /&gt;
(vgl. [http://de.wikipedia.org/wiki/Erfüllbarkeitsproblem_der_Aussagenlogik WikiPedia (de)])&lt;br /&gt;
&lt;br /&gt;
Das Erfüllbarkeitsproblem (SAT-Problem, von &amp;lt;i&amp;gt;satisfiability&amp;lt;/i&amp;gt;) befasst sich mit logischen (oder Booleschen) Funktionen: Gegeben sei eine Menge &amp;lt;math&amp;gt;\{x_1, ... ,x_n\}&amp;lt;/math&amp;gt; Boolscher Variablen (d.h., die &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; können nur die Werte True oder False annehmen), sowie eine logische Formel, in der die Variablen mit den üblichen logischen Operatoren &lt;br /&gt;
:&amp;lt;math&amp;gt;\neg\quad&amp;lt;/math&amp;gt;: Negation (&amp;quot;nicht&amp;quot;, in Python: &amp;lt;tt&amp;gt;not&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\vee\quad&amp;lt;/math&amp;gt;: Disjunktion (&amp;quot;oder&amp;quot;, in Python: &amp;lt;tt&amp;gt;or&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\wedge\quad&amp;lt;/math&amp;gt;: Konjuktion (&amp;quot;und&amp;quot;, in Python: &amp;lt;tt&amp;gt;and&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\rightarrow\quad&amp;lt;/math&amp;gt;: Implikation (&amp;quot;wenn, dann&amp;quot;, in Python nicht als Operator definiert)&lt;br /&gt;
:&amp;lt;math&amp;gt;\leftrightarrow\quad&amp;lt;/math&amp;gt;: Äquivalenz (&amp;quot;genau dann, wenn&amp;quot;, in Python: &amp;lt;tt&amp;gt;==&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\neq\quad&amp;lt;/math&amp;gt;: exklusive Disjunktion (&amp;quot;entweder oder&amp;quot;, in Python: &amp;lt;tt&amp;gt;!=&amp;lt;/tt&amp;gt;)&lt;br /&gt;
verknüpft sind. Klammern definieren die Reihenfolge der Auswertung der Operationen. Für jede Belegung der Variablen &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; mit True oder False liefert die Formel den Wert der Funktion, der natürlich auch nur True oder False sein kann. Wenn Formel und Belegung gegeben sind, ist die Auswertung der Funktion ein sehr einfaches Problem: Man transformiert die Formel in einen Parse-Baum (siehe Übungsaufgabe &amp;quot;Taschenrechner) und wertet jeden Knoten mit Hilfe der üblichen Wertetabellen für logische Operatoren aus, die wir hier zur Erinnerung noch einmal angeben:&lt;br /&gt;
{| cellspacing=&amp;quot;0&amp;quot; border=&amp;quot;1&amp;quot;&lt;br /&gt;
|- style=&amp;quot;text-align:center;background-color:#ffffcc;width:50px&amp;quot;&lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \vee b &amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \wedge b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \rightarrow b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;b \rightarrow a&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \leftrightarrow b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \neq b&amp;lt;/math&amp;gt; &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 0 || 1 || 1 || 1 || 0&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 0 || 1 || 0 || 0 || 1&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 0 || 1 || 0 || 1&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 || 0&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beim Erfüllbarkeitsproblem wird die Frage umgekehrt gestellt: &lt;br /&gt;
:Gegeben sei eine logische Funktion. Ist es möglich, dass die Funktion jemals den Wert True annimmt? &lt;br /&gt;
Das heisst, kann man die Variablen &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; so mit True oder False belegen, dass die Formel am Ende wahr ist? Im Prinzip kann man diese Frage durch erschöpfende Suche leicht beantworten, indem man die Funktion für alle &amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt; möglichen Belegungen einfach ausrechnet, aber das dauert für große n (ab ca. &amp;lt;math&amp;gt;n\ge 40&amp;lt;/math&amp;gt;) viel zu lange. Erstaunlicherweise ist es aber noch niemanden gelungen, einen Algorithmus zu finden, der für beliebige logische Funktionen schneller funktioniert. Im Gegenteil wurde gezeigt, dass das Erfüllbarkeitsproblem [[NP-Vollständigkeit|NP-vollständig]] ist, so dass wahrscheinlich kein solcher Algorithmus existiert. Trotz (oder gerade wegen) seiner Schwierigkeit hat das Erfüllbarkeitsproblem viele Anwendungen gefunden, vor allem beim Testen logischer Schaltkreise (&amp;quot;Gibt es eine Belegung der Eingänge, so dass am Ausgang der verbotene Wert X entsteht?&amp;quot;) und bei der Planerstellung in der künstlichen Intelligenz (&amp;quot;Kann man ausschließen, dass der generierte Plan Konflikte enthält?&amp;quot;). Es ist außerdem ein beliebtes Modellproblem für die Erforschung neuer Ideen und Algorithmen für schwierige Probleme.&lt;br /&gt;
&lt;br /&gt;
==== Normalformen für logische Ausdrücke ====&lt;br /&gt;
&lt;br /&gt;
Um die Beschreibung von Erfüllbarkeitsproblemen zu vereinfachen und zu vereinheitlichen, hat man verschiedene &amp;lt;i&amp;gt;Normalformen&amp;lt;/i&amp;gt; für logische Ausdrücke eingeführt. Die wichtigste ist die &amp;lt;i&amp;gt;Konjuktionen-Normalform&amp;lt;/i&amp;gt; (CNF - conjunctive normal form). Ein Ausdruck in &amp;lt;i&amp;gt;Konjuktionen-Normalform&amp;lt;/i&amp;gt; ist eine UND-Verknüpfung von M &amp;lt;i&amp;gt;Klauseln&amp;lt;/i&amp;gt;:&lt;br /&gt;
 (CLAUSE&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;) &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; (CLAUSE&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;) &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; ...  &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; (CLAUSE&amp;lt;sub&amp;gt;M&amp;lt;/sub&amp;gt;)&lt;br /&gt;
Jede Klausel ist wiederum ein logischer Ausdruck, der aber sehr einfach sein muss: Er darf nur noch k Variablen enthalten, die nur mit den Operatoren NICHT und ODER verknüpft werden dürfen, z.B.&lt;br /&gt;
  CLAUSE&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; := &amp;lt;math&amp;gt;x_1 \vee \neg x_3 \vee x_8&amp;lt;/math&amp;gt;&lt;br /&gt;
Je nachdem, wie viele Variablen pro Klausel erlaubt sind, spricht man von &amp;lt;b&amp;gt;k-CNF&amp;lt;/b&amp;gt; und entsprechend von einem &amp;lt;b&amp;gt;k-SAT&amp;lt;/b&amp;gt; Problem. Es ist außerdem üblich, die Menge der Variablen und die Menge der negierten Variablen zusammen als Menge der &amp;lt;i&amp;gt;Literale&amp;lt;/i&amp;gt; zu bezeichnen:&lt;br /&gt;
  LITERALS := &amp;lt;math&amp;gt;\{x_1,...,x_n\} \cup \{\neg x_1,...,\neg x_n\}&amp;lt;/math&amp;gt;&lt;br /&gt;
Formal definiert man die &amp;lt;b&amp;gt;k-Konjunktionen-Normalform (k-CNF)&amp;lt;/b&amp;gt; am besten durch eine Grammatik in [http://de.wikipedia.org/wiki/Backus-Naur-Form Backus-Naur-Form]:&lt;br /&gt;
    k_CNF    ::=  CLAUSE | k_CNF &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; CLAUSE&lt;br /&gt;
    CLAUSE   ::= (LITERAL &amp;lt;math&amp;gt;\vee&amp;lt;/math&amp;gt; ... &amp;lt;math&amp;gt;\vee&amp;lt;/math&amp;gt; LITERAL)  # genau k Literale pro Klausel&lt;br /&gt;
    LITERAL  ::=  VARIABLE | &amp;lt;math&amp;gt;\neg&amp;lt;/math&amp;gt;VARIABLE&lt;br /&gt;
    VARIABLE ::=  &amp;lt;math&amp;gt;x_1&amp;lt;/math&amp;gt; | ... | &amp;lt;math&amp;gt;x_n&amp;lt;/math&amp;gt;&lt;br /&gt;
Beispiele:&lt;br /&gt;
* 3-CNF: &amp;lt;math&amp;gt;(x_1 \vee \neg x_2 \vee x_4) \wedge (x_2 \vee x_3 \vee \neg x_4) \wedge (\neg x_1 \vee x_4 \vee \neg x_5)&amp;lt;/math&amp;gt;&lt;br /&gt;
* 2-CNF: &amp;lt;math&amp;gt;(x_1 \vee \neg x_2) \wedge (x_3 \vee x_4)&amp;lt;/math&amp;gt; ...&lt;br /&gt;
&amp;lt;b&amp;gt;Gesucht&amp;lt;/b&amp;gt; ist eine Belegung der Variablen mit True und False, so dass der Ausdruck den Wert True hat. Aus den Eigenschaften der UND- und ODER-Verknüpfungen folgt, dass ein Ausdruck in k-CNF genau dann True ist, wenn jede einzelne Klausel True ist. In jeder Klausel wiederum hat man k Chancen, die Klausel True zu machen, indem man eins der Literale zu True macht. Eventuell werden dadurch aber andere Klauseln wieder zu False, was die Aufgabe so schwierig macht. Die Bedeutung der k-CNF ergibt sich aus folgendem&lt;br /&gt;
;Satz: Jeder logische Ausdruck kann effizient nach 3-CNF transformiert werden, jedoch im allgemeinen nicht nach 2-CNF.&lt;br /&gt;
Man kann sich also auf Algorithmen für 3-SAT-Probleme konzentrieren, ohne dabei an Ausdrucksmächtigkeit zu verlieren. &lt;br /&gt;
&lt;br /&gt;
Leider gilt der entsprechende Satz nicht für k=2: Ausdrücke in 2-CNF sind weit weniger mächtig, weil man in jeder Klausel nur noch zwei Wahlmöglichkeiten hat. Bestimmte logische Ausdrücke sind aber auch nach 2-CNF transformierbar, beispielsweise die Bedingung, dass zwei Literale u und v immer den entgegegesetzten Wert haben müssen. Dies ergibt ein Paar von ODER-Verknüpfungen:&lt;br /&gt;
:&amp;lt;math&amp;gt;(u \leftrightarrow \neg v) \equiv (u \vee \neg v) \wedge (\neg u \vee v)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die 2-CNF hat den Vorteil, dass es effiziente Algorithmen für das 2-SAT-Problem gibt, die wir jetzt kennenlernen wollen. Es zeigt sich, dass man Ausdrücke in 2-CNF als Graphen repräsentieren kann, indem man sie zunächst in die &amp;lt;i&amp;gt;Implikationen-Normalform&amp;lt;/i&amp;gt; (INF für &amp;lt;i&amp;gt;implicative normal form&amp;lt;/i&amp;gt;) überführt. Die Implikationen-Normalform besteht ebenfalls aus einer Menge von Klauseln, die durch UND-Operationen verknüpft sind, aber jede Klausel ist jetzt eine Implikation. &lt;br /&gt;
Die Grammatik der &amp;lt;b&amp;gt;Implikationen-Normalform (INF)&amp;lt;/b&amp;gt; lautet:&lt;br /&gt;
    INF      ::=  CLAUSE | INF &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; CLAUSE&lt;br /&gt;
    CLAUSE   ::= (LITERAL &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; LITERAL)  # genau 2 Literale pro Implikation&lt;br /&gt;
    LITERAL  ::=  VARIABLE | &amp;lt;math&amp;gt;\neg&amp;lt;/math&amp;gt;VARIABLE&lt;br /&gt;
    VARIABLE ::=  &amp;lt;math&amp;gt;x_1&amp;lt;/math&amp;gt; | ... | &amp;lt;math&amp;gt;x_n&amp;lt;/math&amp;gt;&lt;br /&gt;
und ein gültiger Ausdruck wäre z.B.&lt;br /&gt;
:&amp;lt;math&amp;gt;(x_1 \to x_2) \wedge (x_2 \to \neg x_3) \wedge (x_4 \to x_3)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die Umwandlung von 2-CNF nach INF beruht auf folgender Äquivalenz, die man sich aus der obigen Wahrheitstabelle leicht herleitet:&lt;br /&gt;
:&amp;lt;math&amp;gt;(x \vee y) \equiv (\neg x \rightarrow y) \equiv (\neg y \rightarrow x)&amp;lt;/math&amp;gt;&lt;br /&gt;
Aus dieser Äquivalenz folgt der &lt;br /&gt;
;Satz: Ein Ausdruck in 2-CNF kann nach INF transformiert werden, indem man jede Klausel &amp;lt;math&amp;gt;(x \vee y)&amp;lt;/math&amp;gt; durch das Klauselpaar &amp;lt;math&amp;gt;(\neg x \rightarrow y) \wedge (\neg y \rightarrow x)&amp;lt;/math&amp;gt; ersetzt.&lt;br /&gt;
Man beachte, dass man für jede ODER-Klausel des ursprünglichen Ausdrucks &amp;lt;i&amp;gt;zwei&amp;lt;/i&amp;gt; Implikationen (eine für jede Richtung des &amp;quot;wenn, dann&amp;quot;) einfügen muss, um die Symmetrie des Problems zu erhalten.&lt;br /&gt;
&lt;br /&gt;
==== Lösung des 2-SAT-Problems mit Implikationgraphen ====&lt;br /&gt;
&lt;br /&gt;
Jeder Ausdruck in INF kann als gerichteter Graph dargestellt werden:&lt;br /&gt;
# Für jedes Literal wird ein Knoten in den Graphen eingefügt. Es gibt also für jede Variable und für ihre Negation jeweils einen Knoten, d.h. 2n Knoten insgesamt.&lt;br /&gt;
# Jede Implikation ist eine gerichtete Kante.&lt;br /&gt;
Implikationengraphen eignen sich, um Ursache-Folge-Beziehungen oder Konflikte zwischen Aktionen auszudrücken. Beispielsweise kann man die Klausel &amp;lt;math&amp;gt;(x \rightarrow \neg y)&amp;lt;/math&amp;gt; als &amp;quot;wenn man x tut, darf man y nicht tun&amp;quot; interpretieren. Ein anderes schönes Beispiel findet sich in Übung 12.&lt;br /&gt;
&lt;br /&gt;
Für die Implementation eines Implikationengraphen in Python empfiehlt es sich, die Knoten geschickt zu numerieren: Ist die Variable &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; dem Knoten i zugeordnet, so sollte die negierte Variable &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; dem Knoten (i+n) zugeordnet werden. Zu jedem gegebenen Knoten i findet man dann den negierten Partnerknoten j leicht durch die Formel &amp;lt;tt&amp;gt;j = (i + n ) % (2*n)&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die Aufgabe besteht jetzt darin, folgende Fragen zu beantworten:&lt;br /&gt;
# Ist der durch den Implikationengraphen gegebene Ausdruck erfüllbar?&lt;br /&gt;
# Finde eine geeignete Belegung der Variablen, wenn der Ausduck erfüllbar ist.&lt;br /&gt;
Die erste Frage beantwortet man leicht, indem man die stark zusammenhängenden Komponenten des Implikationengraphen bildet. Dann gilt folgender&lt;br /&gt;
;Satz: Seien u und v zwei Literale, die sich in der selben stark zusammenhängenden Komponente befinden. Dann müssen u und v stets den selben Wert haben, damit der Ausdruck erfüllt sein kann.&lt;br /&gt;
Die Korrektheit des Satzes folgt aus der Definition der stark zusammenhängenden Komponenten: Da u und v in der selben Komponente liegen, gibt es im Implikationengraphen einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow v&amp;lt;/math&amp;gt; sowie einen Weg &amp;lt;math&amp;gt;v \rightsquigarrow u&amp;lt;/math&amp;gt;. Wegen der Transitivität der &amp;quot;wenn, dann&amp;quot; Relation kann man die Wege zu zwei Implikationen verkürzen, die gleichzeitig gelten müssen: &amp;lt;math&amp;gt;(u \rightarrow v) \wedge (v \rightarrow u)&amp;lt;/math&amp;gt; (die Verkürzung von Wegen zu direkten Kanten entspricht gerade der Bildung der transitiven Hülle für die Knoten u und v). In der obigen Wertetabelle für logische Operatoren erkennt mann, dass dies äquivalent zur Bedingung &amp;lt;math&amp;gt;(u \leftrightarrow v)&amp;lt;/math&amp;gt; ist. Dies ist aber gerade die Behauptung des Satzes.&lt;br /&gt;
&lt;br /&gt;
Die Erfüllbarkeit des Ausdrucks ist nun ein einfacher Spezialfall dieses Satzes. &lt;br /&gt;
;Korrolar: Der gegebene Ausdruck ist genau dann erfüllbar, wenn die Literale &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; sich für kein i in derselben stark zusammenhängenden Komponente befinden.&lt;br /&gt;
Setzt man nämlich im Satz &amp;lt;math&amp;gt;u = x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v = \neg x_i&amp;lt;/math&amp;gt;, und beide Knoten befinden sich in der selben Komponente, dann müsste gelten &amp;lt;math&amp;gt;x_i \leftrightarrow\neg x_i&amp;lt;/math&amp;gt;, was offensichtlich ein Widerspruch ist. Damit kann der Ausdruck nicht erfüllbar sein. Umgekehrt gilt, dass der Ausdruck immer erfüllbar ist, wenn &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; stets in verschiedenen Komponenten liegen, weil der folgende Algorithmus von Aspvall, Plass und Tarjan in diesem Fall stets eine gültige Belegung aller Variablen liefert:&lt;br /&gt;
# Bestimme die stark zusammenhängenden Komponenten und bilde den Komponentengraphen. Ordne die Knoten des Komponentengraphen (also die stark zusammenhängenden Komponenten des Originalgraphen) in topologische Sortierung an.&lt;br /&gt;
# Betrachte die Komponenten in der topologischen Sortierung von hinten nach vorn und weise ihnen einen Wert nach folgenden Regeln zu (zur Erinnerung: alle Literale in der selben Komponente haben den selben Wert):&lt;br /&gt;
#* Wenn die Komponente noch nicht betrachtet wurde, setze ihren Wert auf True, und den Wert der komplementären Komponente (derjenigen, die die negierten Literale enthält) auf False.&lt;br /&gt;
#* Andernfalls, gehe zur nächsten Komponente weiter.&lt;br /&gt;
Der Algorithmus beruht auf der Symmetrie des Implikationengraphen: Weil Kanten immer paarweise &amp;lt;math&amp;gt;(\neg u \rightarrow v) \wedge (\neg v \rightarrow u)&amp;lt;/math&amp;gt; eingefügt werden, ist der Graph &amp;lt;i&amp;gt;schiefsymmetrisch&amp;lt;/i&amp;gt; (skew symmetric): die eine Hälfte das Graphen ist die transponierte Spiegelung der anderen Hälfte. Enthält eine stark zusammenhängende Komponente &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; die Knoten &amp;lt;tt&amp;gt;i1, i2, ...&amp;lt;/tt&amp;gt;, so gibt es stets eine komplementäre Komponente &amp;lt;math&amp;gt;C_j = \neg C_i&amp;lt;/math&amp;gt;, die die komplementären Knoten &amp;lt;tt&amp;gt;j1 = (i1 + n) % (2*n), j2 = (i2 + n) % (2*n), ...&amp;lt;/tt&amp;gt; enthält. Gilt &amp;lt;math&amp;gt;C_i = \neg C_i&amp;lt;/math&amp;gt; für irgendein i, so ist der Ausdruck nicht erfüllbar. Den Beweis für die Korrektheit des Algorithmus findet man im [http://www.math.ucsd.edu/~sbuss/CourseWeb/Math268_2007WS/2SAT.pdf Originalartikel]. Leider funktioniert dies nicht für k-SAT-Probleme mit &amp;lt;math&amp;gt;k &amp;gt; 2&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Will man nur die Erfüllbarkeit prüfen, vereinfacht sich der Algorithmus zu:&lt;br /&gt;
# Bestimme die stark zusammenhängenden Komponenten.&lt;br /&gt;
# Teste für alle &amp;lt;tt&amp;gt;i = 0,...,n-1&amp;lt;/tt&amp;gt;, dass Knoten &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; und Knoten &amp;lt;tt&amp;gt;(i+n)&amp;lt;/tt&amp;gt; in unterschiedlichen Komponenten liegen.&lt;br /&gt;
Ist der Ausdruck erfüllbar, kann man eine gültige Belegung der Variablen jetzt mit dem randomisierten Algorithmus bestimmen, den wir im Kapitel [[Randomisierte Algorithmen]] behandeln.&lt;br /&gt;
&lt;br /&gt;
== Weitere wichtige Graphenalgorithmen ==&lt;br /&gt;
&lt;br /&gt;
Eins der wichtigsten Einsatzgebiete für Graphen ist die Optimierung, also die Suche nach der &amp;lt;i&amp;gt;besten&amp;lt;/i&amp;gt; Lösung für ein gegebenes Problem:&lt;br /&gt;
* Das &amp;lt;i&amp;gt;interval scheduling&amp;lt;/i&amp;gt; befasst sich damit, aus einer gegebenen Menge von Aufträgen die richtigen auszuwählen und sie geschickt auf die zur Verfügung stehenden Ressourcen aufzuteilen. Damit beschäftigen wir uns im Kapitel [[Greedy-Algorithmen und Dynamische Programmierung]].&lt;br /&gt;
* Beim Problem des Handlungsreisenden sucht man nach der kürzesten Rundreise, die alle gegebenen Städte genau einmal besucht. Dieses Problem behandeln wir im Kapitel [[NP-Vollständigkeit]].&lt;br /&gt;
* Viele weitere Anwendungen können wir leider in der Vorlesung nicht mehr behandeln, z.B.&lt;br /&gt;
** Algorithmen für den [http://en.wikipedia.org/wiki/Maximum_flow_problem maximalen Fluss] beantworten die Frage, wie man die Durchflussmenge durch ein Netzwerk (z.B. von Ölpipelines) maximiert.&lt;br /&gt;
** Beim [http://en.wikipedia.org/wiki/Assignment_problem Problem der optimalen Paarung] (&amp;quot;matching problem&amp;quot; oder &amp;quot;assignment problem&amp;quot;) sucht man nach einer Teilmenge der Kanten (also nach einem Teilgraphen), so dass jeder Knoten in diesem Teilgraphen höchstens den Grad 1 hat. Im neuen Graphen gruppieren die Kanten also je zwei Knoten zu einem Paar, und die Paarung soll nach jeweils anwendungsspezifischen Kriterien optimal sein. Dies benötigt man z.B. bei der optimalen Zuordnung von Gruppen, etwas beim Arbeitsamt (Zuordnung Arbeitssuchender - Stellenangebot) und in der Universität (Zuordnung Studenten - Übungsgruppen).&lt;br /&gt;
** In Statistik und maschinellem Lernen haben in den letzten Jahren die [http://en.wikipedia.org/wiki/Graphical_model graphischen Modelle] große Bedeutung erlangt.&lt;br /&gt;
* usw. usf.&lt;br /&gt;
&lt;br /&gt;
[[Randomisierte Algorithmen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5407</id>
		<title>Graphen und Graphenalgorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5407"/>
				<updated>2012-07-25T17:11:50Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Transitive Hülle und stark zusammenhängende Komponenten */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Einführung zu Graphen ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation -- Königsberger Brückenproblem ===&lt;br /&gt;
Leonhard Euler [http://de.wikipedia.org/wiki/Leonhard_Euler] erfand den Graphen-Formalismus 1736, um eine scheinbar banale Frage zu beantworten: Ist es möglich, in Königsberg (siehe Stadtplan von 1809 und die schematische Darstellung) einen Spaziergang zu unternehmen, bei dem jede der 7 Brücken genau einmal überquert wird?&lt;br /&gt;
&lt;br /&gt;
[[Image:Koenigsberg1809.png]]&amp;lt;br&amp;gt;&lt;br /&gt;
[[Image:Koenigsberg.jpg]]&lt;br /&gt;
&lt;br /&gt;
Ein Graph abstrahiert von der Geometrie des Problems und repräsentiert nur die Topologie. Jeder Stadtteil von Königsberg ist ein Knoten des Graphen, jede Brücke eine Kante. Der zum Brückenproblem gehörende Graph sieht also so aus:&lt;br /&gt;
&lt;br /&gt;
     O&lt;br /&gt;
    /| \&lt;br /&gt;
    \|  \&lt;br /&gt;
     O---O&lt;br /&gt;
    /|  /&lt;br /&gt;
    \| /&lt;br /&gt;
     O&lt;br /&gt;
&lt;br /&gt;
Der gesuchte Spaziergang würde existieren, wenn es maximal 2 Knoten gäbe, an denen sich eine ungerade Zahl von Kanten trifft. Die Frage muss für Königsberg also verneint werden, denn hier gibt es vier solche Knoten. Ein leicht modifiziertes Problem ist allerdings lösbar: Im obigen Stadtplan erkennt man eine Fähre, die die Stadtteile Kneiphof und Altstadt verbindet. Bezieht man dieselbe in den Spaziergang ein, ergibt sich folgender Graph, bei dem nur noch zwei Knoten mit ungerader Kantenzahl existieren:&lt;br /&gt;
&lt;br /&gt;
   --O&lt;br /&gt;
  / /| \&lt;br /&gt;
  \ \|  \&lt;br /&gt;
   --O---O&lt;br /&gt;
    /|  /&lt;br /&gt;
    \| /&lt;br /&gt;
     O&lt;br /&gt;
&lt;br /&gt;
Inzwischen haben Graphen eine riesige Zahl weiterer Anwendungen gefunden. Einige Beispiele:&lt;br /&gt;
&lt;br /&gt;
* Landkarten:&lt;br /&gt;
** Knoten: Länder&lt;br /&gt;
** Kanten: gemeinsame Grenzen&lt;br /&gt;
&lt;br /&gt;
* Logische Schaltkreise:&lt;br /&gt;
** Knoten: Gatter&lt;br /&gt;
** Kanten: Verbindungen&lt;br /&gt;
&lt;br /&gt;
* Chemie (Summenformeln):&lt;br /&gt;
** Knoten: chemische Elemente&lt;br /&gt;
** Kanten: Bindungen &lt;br /&gt;
&lt;br /&gt;
* Soziologie (StudiVZ)&lt;br /&gt;
** Soziogramm&lt;br /&gt;
*** Knoten: Personen&lt;br /&gt;
*** Kanten: Freund von ...&lt;br /&gt;
&lt;br /&gt;
=== Definitionen ===&lt;br /&gt;
&lt;br /&gt;
;Ungerichteter Graph: Ein ungerichteter Graph G = ( V, E ) besteht aus&lt;br /&gt;
:* einer endliche Menge V von Knoten (vertices)&lt;br /&gt;
:* einer endlichen Menge &amp;lt;math&amp;gt;E \subset V \times V&amp;lt;/math&amp;gt; von Kanten (edges)&lt;br /&gt;
:Die Paare (u,v) und (v,u) gelten dabei als nur ''eine'' Kante (somit gilt die Symmetriebeziehung: (u,v) ∈ E =&amp;gt; (v,u) ∈ E ). Die Anzahl der Kanten, die sich an einem Knoten treffen, wird als ''Grad'' (engl. ''degree'') dieses Knotens bezeichnet:&lt;br /&gt;
:::degree(v) = |{v' ∈ V | (v,v') ∈ E}|&lt;br /&gt;
:(Die Syntax |{...}| bezeichnet dabei die Mächtigkeit der angegebenen Menge, also die Anzahl der Elemente in der Menge.)&lt;br /&gt;
&lt;br /&gt;
Der Graph des Königsberger Brückenproblems ist ungerichtet. Bezeichnet man die Knoten entsprechend des folgenden Bildes&lt;br /&gt;
    c&lt;br /&gt;
   /| \&lt;br /&gt;
   \|  \&lt;br /&gt;
    b---d &lt;br /&gt;
   /|  /&lt;br /&gt;
   \| /&lt;br /&gt;
    a&lt;br /&gt;
&lt;br /&gt;
gilt für die Knotengrade: &amp;lt;tt&amp;gt;degree(a) == degree(c) == degree(d) == 3&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;degree(b) == 5&amp;lt;/tt&amp;gt;. Genauer muss man bei diesem Graphen von einem ''Multigraphen'' sprechen, weil es zwischen einigen Knotenpaaren (nämlich (a, b) sowie (b, c)) mehrere Kanten (&amp;quot;Mehrfachkanten&amp;quot;) gibt. Wir werden in dieser Vorlesung nicht näher auf Multigraphen eingehen.&lt;br /&gt;
&lt;br /&gt;
;Gerichteter Graph: Ein Graph heißt ''gerichtet'', wenn die Kanten (u,v) und (v,u) unterschieden werden. Die Kante (u,v) ∈ E wird nun als Kante von u nach v (aber nicht umgekehrt) interpretiert. Entsprechend unterscheidet man jetzt den ''eingehenden'' und den ''ausgehenden Grad'' jedes Knotens:&lt;br /&gt;
:*out_degree(v) = |{v' ∈ V | (v,v') ∈ E}|&amp;lt;br/&amp;gt;&lt;br /&gt;
:*in_degree(v)  = |{v' ∈ V| (v',v) ∈ E}|&lt;br /&gt;
&lt;br /&gt;
Das folgende Bild zeigt einen gerichteten Graphen. Hier gilt &amp;lt;tt&amp;gt;out_degree(1) == out_degree(3) == in_degree(2) == in_degree(4) == 2&amp;lt;/tt&amp;gt; und &lt;br /&gt;
&amp;lt;tt&amp;gt;in_degree(1) == in_degree(3) == out_degree(2) == out_degree(4) == 0&amp;lt;/tt&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
[[Image:digraph.png|gerichteter Graph]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Vollständiger Graph: Ein vollständiger Graph ist ein ungerichteter Graph, bei dem jeder Knoten mit allen anderen Knoten verbunden ist.&lt;br /&gt;
:::&amp;lt;math&amp;gt;E = \{ (v,w) |  v \in V, w \in V, v \ne w \}&amp;lt;/math&amp;gt;&lt;br /&gt;
:Ein vollständiger Graph mit |V| Knoten hat &amp;lt;math&amp;gt;|E| = \frac{|V|(|V|-1)}{2}&amp;lt;/math&amp;gt; Kanten.&lt;br /&gt;
&lt;br /&gt;
Die folgenden Abbildungen zeigen die vollständigen Graphen mit einem bis fünf Knoten (auch als K&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; bis K&amp;lt;sub&amp;gt;5&amp;lt;/sub&amp;gt; bezeichnet).&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;0&amp;quot; style=&amp;quot;margin: 1em auto 1em auto&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
| [[Image:k1.png|frame|k1]]&lt;br /&gt;
| [[Image:k2.png|frame|k2]]&lt;br /&gt;
| [[Image:k3.png|frame|k3]]&lt;br /&gt;
|-&lt;br /&gt;
| [[Image:k4.png|frame|k4]]&lt;br /&gt;
| [[Image:k5.png|frame|k5]]&lt;br /&gt;
|&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
''Rätsel''&amp;lt;br/&amp;gt;&lt;br /&gt;
Auf einer Party sind Leute. Alle stoßen miteinander an. Es hat 78 mal &amp;quot;Pling&amp;quot; gemacht.&lt;br /&gt;
Wieviele Leute waren da? Antwort: Jede Person ist ein Knoten des Graphen, jedes Antoßen eine Kante. &lt;br /&gt;
Da alle miteinander angestoßen haben, handelt es sich um einen vollständigen Graphen. Mit&lt;br /&gt;
|V|(|V|-1)/2 = 78 folgt, dass es 13 Personen waren.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Gewichteter Graph: Ein Graph heißt ''gewichtet'', wenn jeder Kante eine reelle Zahl zugeordnet ist. Bei vielen Anwendungen beschränkt man sich auch auf nichtnegative reelle Gewichte. In einem gerichteten Graphen können die Gewichte der Kanten (u,v) und (v,u) unterschiedlich sein.&lt;br /&gt;
&lt;br /&gt;
Die Gewichte kodieren Eigenschaften der Kanten, die für die jeweilige Anwendung interessant sind. Bei der Berechnung des maximalen Flusses in einem Netzwerk sind die Gewichte z.B. die Durchflusskapazitäten jeder Kante, bei der Suche nach kürzesten Weges kodieren Sie den Abstand zwischen den Endknoten der Kante, bei Währungsnetzwerken (jeder Knoten ist eine Währung) geben sie die Wechselkurse an, usw..&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Teilgraphen: Ein Graph G' = (V',E') ist ein Teilgraph eines Graphen G, wenn gilt:&lt;br /&gt;
:* V' &amp;amp;sube; V &lt;br /&gt;
:* E' &amp;amp;sub; E &lt;br /&gt;
:Er heißt ''(auf)spannender Teilgraph'', wenn gilt:&lt;br /&gt;
:* V' = V&lt;br /&gt;
:Er heißt ''induzierter Teilgraph'', wenn gilt:&lt;br /&gt;
:* e = (u,v) ∈ E' &amp;amp;sub; E &amp;amp;hArr; u ∈ V' und v ∈ V'&lt;br /&gt;
:Den von V' induzierten Teilgraphen erhält man also, indem man aus G alle Knoten löscht, die nicht in V' sind, sowie alle Kanten (und nur diese Kanten), die einen der gelöschten Knoten als Endknoten haben.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Wege, Pfade, Zyklen, Kreise, Erreichbarkeit: Sei G = (V,E) ein Graph (ungerichtet oder gerichteter) Graph. Dann gilt folgende rekursive Definition:&lt;br /&gt;
:* Für v ∈ V ist (v) ein Weg der Länge 0 in G&lt;br /&gt;
:* Falls &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1})&amp;lt;/math&amp;gt; ein Weg ist, und eine Kante &amp;lt;math&amp;gt;(v_{n-1}, v_n)\in E&amp;lt;/math&amp;gt; existiert, dann ist auch &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ein Weg, und er hat die Länge n. &lt;br /&gt;
: Ein Weg ist also eine nichtleere Folge von Knoten, so dass aufeinander folgende Knoten stets durch eine Kante verbunden sind. Die Länge des Weges entspricht der Anzahl der Kanten im Weg (= Anzahl der Knoten - 1).&lt;br /&gt;
:* Ein ''Pfad'' &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ist ein Weg, bei dem alle Knoten v&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; verschieden sind.&lt;br /&gt;
:* ''Ein Zyklus'' &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ist ein Weg, der zum Ausgangspunkt zurückkehrt, wenn also v&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; = v&amp;lt;sub&amp;gt;n&amp;lt;/sub&amp;gt; gilt.&lt;br /&gt;
:* Ein ''Kreis'' ist ein Zyklus ohne Überkreuzungen. Das heisst, es gilt v&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; = v&amp;lt;sub&amp;gt;n&amp;lt;/sub&amp;gt; und &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1})&amp;lt;/math&amp;gt; ist ein Pfad.&lt;br /&gt;
:* Ein Knoten w ∈ V ist von einem anderen Knoten v ∈ V aus ''erreichbar'' genau dann, wenn ein Weg (v, ..., w) existiert. Wir schreiben dann &amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt;.&lt;br /&gt;
In einem ungerichteten Graph ist die Erreichbarkeits-Relation stets symmetrisch, das heisst aus &amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; folgt &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. In einem gerichteten Graphen ist dies im allgemeinen nicht der Fall.&lt;br /&gt;
&lt;br /&gt;
Bestimmte Wege haben spezielle Namen&lt;br /&gt;
&lt;br /&gt;
;Eulerweg: Ein Eulerweg ist ein Weg, der alle '''Kanten''' genau einmal enthält.&lt;br /&gt;
&lt;br /&gt;
Die eingangs erwähnte Frage des Königsberger Brückenproblems ist equivalent zu der Frage, ob der dazugehörige Graph einen Eulerweg besitzt (daher der Name). Ein anderes bekanntes Beispiel ist das &amp;quot;Haus vom Nikolaus&amp;quot;: Wenn man diesen Graphen in üblicher Weise in einem Zug zeichnet, erhält man gerade den Eulerweg. &lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O----O&lt;br /&gt;
  | \/ |&lt;br /&gt;
  | /\ |   &amp;quot;Das Haus vom Nikolaus&amp;quot;: Alle ''Kanten'' werden nur ''einmal'' passiert&lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Hamiltonweg: Ein Hamiltonweg ist ein Weg, der alle '''Knoten''' genau einmal enthält. Das &amp;quot;Haus vom Nikolaus&amp;quot; besitzt auch einen Hamiltonweg:&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /   &lt;br /&gt;
  O----O&lt;br /&gt;
     /  &lt;br /&gt;
    /      Alle ''Knoten'' werden nur ''einmal'' passiert&lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Hamiltonkreis: Ein Hamiltonkreis ist ein Kreis, der alle '''Knoten''' genau einmal enthält. Auch ein solches Gebilde ist im Haus von Nilolaus enthalten:&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O    O&lt;br /&gt;
  |    |   v0 = vn&lt;br /&gt;
  |    |   vi != vj   Für Alle i,j   i !=j; i,j &amp;gt;0; i,j &amp;lt; n&lt;br /&gt;
  O----O     &lt;br /&gt;
&lt;br /&gt;
Die folgende Skizze zeigt hingegen einen Zyklus: Der Knoten rechts unten sowie die untere Kante sind zweimal enthalten (die Kante einmal von links nach rechts und einmal von rechts nach links):&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O    O&lt;br /&gt;
    \  |&lt;br /&gt;
     \ |   Zyklus&lt;br /&gt;
  O====O&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Zusammenhang, Zusammenhangskomponenten: Ein ungerichteter Graph G heißt ''zusammenhängend'', wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt;&lt;br /&gt;
:Ein gerichteter Graph G ist zusammenhängend, wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; '''oder''' &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. &lt;br /&gt;
:Er ist ''stark zusammenhängend'', wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; '''und''' &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. &lt;br /&gt;
:Entsprechende Definitionen gelten für Teilgraphen G'. Ein Teilgraph G' heisst ''Zusammenhangskomponente'' von G, wenn er ein ''maximaler'' zusammenhängender Teilgraph ist, d.h. wenn G' zusammenhängend ist, und man keine Knoten und Kanten aus G mehr zu G' hinzufügen kann, so dass G' immer noch zusammenhängend bleibt. Entsprechend definiert man ''starke Zusammenhangskomponenten'' in einem gerichteten Graphen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Planarer Graph, ebener Graph: Ein Graph heißt ''planar'', wenn er so in einer Ebene gezeichnet werden ''kann'', dass sich die Kanten nicht schneiden (außer an den Knoten). Ein Graph heißt ''eben'', wenn er tatsächlich so gezeichnet ''ist'', dass sich die Kanten nicht schneiden. Die Einbettung in die Ebene ist im allgemeinen nicht eindeutig.&lt;br /&gt;
&lt;br /&gt;
'''Beispiele:'''&lt;br /&gt;
&lt;br /&gt;
Der folgende Graph ist planar und eben:&lt;br /&gt;
 &lt;br /&gt;
      O&lt;br /&gt;
     /|\&lt;br /&gt;
    / O \&lt;br /&gt;
   / / \ \&lt;br /&gt;
   O     O&lt;br /&gt;
&lt;br /&gt;
Das &amp;quot;Haus vom Nikolaus&amp;quot; ist ebenfalls planar, wird aber üblicherweise nicht als ebener Graph gezeichnet, weil sich die Diagonalen auf der Wand überkreuzen:&lt;br /&gt;
 &lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O----O&lt;br /&gt;
  | \/ |&lt;br /&gt;
  | /\ |   &lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
Eine ebene Einbettung dieses Graphen wird erreicht, wenn man eine der Diagonalen ausserhalb des Hauses zeichnet. Der Graph (also die Menge der Knoten und Kanten) ändert sich dadurch nicht.&lt;br /&gt;
 &lt;br /&gt;
      O  &lt;br /&gt;
     /  \&lt;br /&gt;
  --O----O&lt;br /&gt;
 /  |  / |&lt;br /&gt;
 |  | /  |   &lt;br /&gt;
 |  O----O      Das &amp;quot;Haus vom Nikolaus&amp;quot; als ebener Graph gezeichnet.&lt;br /&gt;
  \     /&lt;br /&gt;
   -----&lt;br /&gt;
&lt;br /&gt;
Eine alternative Einbettung erhalten wir, wenn wir die andere Diagonale außerhalb des Hauses zeichnen:&lt;br /&gt;
 &lt;br /&gt;
      O  &lt;br /&gt;
     /  \&lt;br /&gt;
    O----O--|&lt;br /&gt;
    | \  |  |&lt;br /&gt;
    |  \ |  | &lt;br /&gt;
    O----O  |     Alternative Einbettung des &amp;quot;Haus vom Nikolaus&amp;quot;.&lt;br /&gt;
    |       |&lt;br /&gt;
    |-------|&lt;br /&gt;
&lt;br /&gt;
Jede Einbettung eines planaren Graphen (also jeder ebene Graph) definiert eine eindeutige Menge von ''Regionen'':&lt;br /&gt;
&lt;br /&gt;
 |----O   @&lt;br /&gt;
 |   /@ \&lt;br /&gt;
 |  O----O&lt;br /&gt;
 |  |@ / |&lt;br /&gt;
 |  | / @|   &lt;br /&gt;
 |  O----O        @ entspricht jeweils einer ''Region''. Auch ausserhalb der Figur ist eine Region (die sogenannte ''unendliche'' Region).&lt;br /&gt;
 |@      |&lt;br /&gt;
 |-------|&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Der vollständige Graph K5 ist kein planarer Graph, da sich zwangsweise Kanten schneiden, wenn man diesen Graphen in der Ebene zeichnet.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
;Dualer Graph: Jeder ebene Graph G = (V, E) hat einen ''dualen Graphen'' D = (V&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;, E&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;), dessen Knoten und Kanten wie folgt definiert sind:&lt;br /&gt;
:* V&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt; enthält einen Knoten für jede Region des Graphen G&lt;br /&gt;
:* Für jede Kante e ∈ E gibt es eine duale Kante e&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt; ∈ E&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;, die die an e angrenzenden Regionen (genauer: die entsprechenden Knoten in D) verbindet.&lt;br /&gt;
&lt;br /&gt;
Die folgende Abbildung zeigt einen Graphen (grau) und seinen dualen Graphen (schwarz). Die Knoten des dualen Graphen sind mit Zahlen gekennzeichnet und entsprechen den Regionen des Originalgraphen. Jeder (grauen) Kante des Originalgraphen entspricht eine (schwarze) Kante des dualen Graphen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
[[Image:dual-graphs.png]]&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für duale Graphen gilt: Wenn der Originalgraph zusammenhängend ist, enthält jede Region des dualen Graphen genau einen Knoten des Originalgraphen. Deshalb ist der duale Graph des dualen Graphen wieder der Originalgraph. Bei nicht-zusammenhängenden Graphen gilt dies nicht (vgl. das Fenster bei obigem Bild). In diesem Fall hat der duale Graph mehrere mögliche Einbettungen in die Ebene (man kann z.B. die rechte Kante zwischen Knoten 2 und 4 auch links vom Fenster einzeichnen), und man erhält nicht notwendigerweise den Originalgraphen, wenn man den dualen Graphen des dualen berechnet.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Baum: Ein ''Baum'' ist ein zusammenhängender, kreisfreier Graph.&lt;br /&gt;
&lt;br /&gt;
Beispiel: Binärer Suchbaum&lt;br /&gt;
&lt;br /&gt;
;Spannbaum: Ein ''Spannbaum'' eines zusammenhängenden Graphen G ist ein zusammenhängender, kreisfreier Teilgraph von G, der alle Knoten von G enthält&lt;br /&gt;
&lt;br /&gt;
Beispiel: Spannbaum für das &amp;quot;Haus des Nikolaus&amp;quot; &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    O   &lt;br /&gt;
   /       &lt;br /&gt;
  O    O&lt;br /&gt;
  |  /  &lt;br /&gt;
  | /   &lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
Der Spannbaum eines Graphen mit |V| Knoten hat stets |V| - 1 Kanten.&lt;br /&gt;
&lt;br /&gt;
;Wald: Ein ''Wald'' ist ein unzusammenhängender, kreisfreier Graph.&lt;br /&gt;
: Jede Zusammenhangskomponente eines Waldes ist ein Baum.&lt;br /&gt;
&lt;br /&gt;
=== Repräsentation von Graphen ===&lt;br /&gt;
&lt;br /&gt;
Sei G = ( V, E ) gegeben und liege V in einer linearen Sortierung vor.&amp;lt;br/&amp;gt; &lt;br /&gt;
:::&amp;lt;math&amp;gt;V = \{ v_1, ...., v_n \}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Adjazenzmatrix: Ein Graph kann durch eine Adjazenzmatrix repräsentiert werden, die soviele Zeilen und Spalten enthält, wie der Graph Knoten hat. Die Elemente der Adjazenzmatrix sind &amp;quot;1&amp;quot;, falls eine Kante zwischen den zugehörigen Knoten existiert:&lt;br /&gt;
:::&amp;lt;math&amp;gt;\mathrm{\bold A} = a_{ij} = &lt;br /&gt;
\begin{cases}&lt;br /&gt;
1 &amp;amp; \mathrm{falls}\quad (v_i, v_j) \in E \\&lt;br /&gt;
0 &amp;amp; \mathrm{sonst}&lt;br /&gt;
\end{cases} &lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
:Die Indizes der Matrix entsprechen also den Indizes der Knoten gemäß der gegebenen Sortierung. Im Falle eines ungerichteten Graphen ist die Adjazenzmatrix stets symmetrisch (d.h. es gilt &amp;lt;math&amp;gt;a_{ij}=a_{ji}&amp;lt;/math&amp;gt;), bei einem gerichteten Graphen ist sie im allgemeinen unsymmetrisch.&lt;br /&gt;
&lt;br /&gt;
Beispiel für einen ungerichteten Graphen:&lt;br /&gt;
&lt;br /&gt;
 v = { a,b,c,d }     b      d&lt;br /&gt;
                     | \  / |&lt;br /&gt;
                     |  \/  |&lt;br /&gt;
                     |  /\  |&lt;br /&gt;
                     | /  \ |&lt;br /&gt;
                     a      c&lt;br /&gt;
 &lt;br /&gt;
       a b c d&lt;br /&gt;
      -----------&lt;br /&gt;
      (0 1 0 1) |a &lt;br /&gt;
  A = (1 0 1 0) |b&lt;br /&gt;
      (0 1 0 1) |c&lt;br /&gt;
      (1 0 1 0) |d&lt;br /&gt;
&lt;br /&gt;
Die Adjazenzmatrixdarstellung eignet sich besonders für dichte Graphen (d.h. wenn die Zahl der Kanten in O(|V|&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt;) ist.&lt;br /&gt;
&lt;br /&gt;
;Adjazenzlisten: In der Adjazenzlistendarstellung wird der Graph als Liste von Knoten repräsentiert, die für jeden Knoten einen Eintrag enthält. Der Eintrag für jeden Knoten ist wiederum eine Liste, die die Nachbarknoten dieses Knotens enthält:&lt;br /&gt;
:* graph = {adjazencyList(v) | v ∈ V}&lt;br /&gt;
:* adjazencyList(v) = {v' ∈ V | (v, v') ∈ E}&lt;br /&gt;
&lt;br /&gt;
In Python implementieren wir Adjazenzlisten zweckmäßig als Array von Arrays:&lt;br /&gt;
&lt;br /&gt;
                   graph = [[...],[...],...,[...]]&lt;br /&gt;
 Adjazenzliste für Knoten =&amp;gt;  0     1         n&lt;br /&gt;
&lt;br /&gt;
Wenn wir bei dem Graphen oben die Knoten wie bei der Adjazenzmatrix indizieren (also &amp;lt;tt&amp;gt;a =&amp;gt; 0&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;b =&amp;gt; 1&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;c =&amp;gt; 2&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;d =&amp;gt; 3&amp;lt;/tt&amp;gt;), erhalten wir die Adjazenzlistendarstellung:&lt;br /&gt;
&lt;br /&gt;
 graph = [[b, d], [a, c],[b, d], [a, c]]&lt;br /&gt;
&lt;br /&gt;
Auf die Nachbarknoten eines durch seinen Index &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; gegebenen Knotens können wir also wie folgt zugreifen:&lt;br /&gt;
&lt;br /&gt;
      for neighbors in graph[node]:&lt;br /&gt;
          ... # do something with neighbor&lt;br /&gt;
&lt;br /&gt;
Die Adjazenzlistendarstellung ist effizienter, wenn der Graph nicht dicht ist, so dass viele Einträge der Adjazenzmatrix Null wären. In der Vorlesung werden wir nur diese Darstellung verwenden.&lt;br /&gt;
&lt;br /&gt;
;&amp;lt;div id=&amp;quot;transposed_graph&amp;quot;&amp;gt;Transponierter Graph&amp;lt;/div&amp;gt;: Den ''transponierten Graphen'' G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; eines gerichteten Graphen G erhält man, wenn man alle Kantenrichtungen umkehrt.&lt;br /&gt;
&lt;br /&gt;
Bei ungerichteten Graphen hat die Transposition offensichtlich keinen Effekt, weil alle Kanten bereits in beiden Richtungen vorhanden sind, so dass G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; = G gilt. Bei gerichteten Graphen ist die Transposition einfach, wenn der Graph als Adjazenzmatrix implementiert ist, weil man einfach die transponierte Adjazenzmatrix verwenden muss (beachte, dass sich die Reihenfolge der Indizes umkehrt):&lt;br /&gt;
:::A&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; = a&amp;lt;sub&amp;gt;ji&amp;lt;/sub&amp;gt;&lt;br /&gt;
Ist der Graph hingegen durch eine Adjazenzliste repräsentiert, muss etwas mehr Aufwand getrieben werden:&lt;br /&gt;
&lt;br /&gt;
 def transposeGraph(graph):&lt;br /&gt;
      gt = [[] for k in graph]   # zunächst leere Adjazenzlisten von G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt;&lt;br /&gt;
      for node in range(len(graph)):&lt;br /&gt;
           for neighbor in graph[node]:&lt;br /&gt;
               gt[neighbor].append(node)  # füge die umgekehrte Kante in G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; ein&lt;br /&gt;
      return gt&lt;br /&gt;
&lt;br /&gt;
== Durchlaufen von Graphen (Graph Traversal) ==&lt;br /&gt;
&lt;br /&gt;
Wir betrachten zunächst ungerichtete Graphen mit V Knoten und E Kanten. Eine grundlegende Aufgabe in diesen Graphen besteht darin, alle Knoten in einer bestimmten Reihenfolge genau einmal zu besuchen. Hierbei darf man sich von einem gegebenen Startknoten aus nur entlang der Kanten des Graphen bewegen. Die beim Traversieren benutzen Kanten bilden einen Baum, dessen Wurzel der Startknoten ist und der den gesamten Graphen aufspannt, falls der Graph zusammenhängend ist. (Beweis: Da jeder Knoten nur einmal besucht wird, gibt es für jeden besuchten Knoten [mit Ausnahme des Startknotens] genau eine eingehende Kante. Ist der Graph zusammenhängend, wird jeder Knoten tatsächlich erreicht und es gibt genau (V-1) Kanten, exakt soviele wie für einen Baum mit V Knoten notwendig sind.) Ist der Graph nicht zusammenhängend, wird jeder zusammenhängende Teilgraph (jede &amp;lt;i&amp;gt;Zusammenhangskomponente&amp;lt;/i&amp;gt;) getrennt traversiert, und man erhält einen sogenannten &amp;lt;i&amp;gt;Wald&amp;lt;/i&amp;gt; mit einem Baum pro Zusammenhangskomponente. Die beiden grundlegenden Traversierungsmethoden &amp;lt;i&amp;gt;Tiefensuche&amp;lt;/i&amp;gt; und &amp;lt;i&amp;gt;Breitensuche&amp;lt;/i&amp;gt; werden im folgenden vorgestellt.&lt;br /&gt;
&lt;br /&gt;
=== Tiefensuche in Graphen (Depth First Search, DFS) ===&lt;br /&gt;
&lt;br /&gt;
Die Idee der Tiefensuche besteht darin, jeden besuchten Knoten sofort über die erste Kante wieder zu verlassen, die zu einem noch nicht besuchten Knoten führt. Man findet dadurch schnell einen möglichst langen Pfad durch den Graphen, und der Traversierungs-Baum wird zunächst in die Tiefe verfolgt, daher der Name des Verfahrens. Hat ein Knoten keine unbesuchten Nachbarknoten mehr, geht man im Baum zurück (sogenanntes &amp;lt;i&amp;gt;back tracking&amp;lt;/i&amp;gt;), bis man einen Knoten findet, der noch eine unbesuchte Nachbarn besitzt, und traversiert diese nach dem gleichen Muster. Gibt es gar keine unbesuchten Knoten mehr, kehrt die Suche zum Startknoten zurück und endet dort.&lt;br /&gt;
&lt;br /&gt;
WDie folgende rekursive Implementation der Tiefensuche erwartet den Graphen in Adjazenzlistendarstellung und beginnt die Suche beim Knoten &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt;. Die Information, ob ein Knoten bereits besucht wurde, wird im Array &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; gespeichert. Ein solches Array, das zusätzliche Informationen über die Knoten des Graphen bereitstellt, wir häufig &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; genannt.&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)  # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):              # rekursive Hilfsfunktion, die den gegebenen Knoten und dessen Nachbarn besucht&lt;br /&gt;
         if not visited[node]:     # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True  # Markiere node als besucht&lt;br /&gt;
             print node            # Ausgabe der Knotennummer - pre-order&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode)&lt;br /&gt;
&lt;br /&gt;
[[Image:Tiefens.jpg]]&lt;br /&gt;
&lt;br /&gt;
Ausgabe für den Graphen in diesem Bild (es handelt sich um einen ungerichteten Graphen, die Pfeile symbolisieren nur die Suchrichtung beim Traversal):&lt;br /&gt;
&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; dfs(graph, 1)&lt;br /&gt;
 1&lt;br /&gt;
 2&lt;br /&gt;
 4&lt;br /&gt;
 3&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
 5&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div id=&amp;quot;pre_and_post_order&amp;quot;&amp;gt;In dieser Version des Algorithmus werden die Knotennummern ausgegeben, bevor die Nachbarknoten besucht werden. Man bezeichnet die resultierende Sortierung der Knoten als &amp;lt;b&amp;gt;pre-order&amp;lt;/b&amp;gt; oder als &amp;lt;b&amp;gt;discovery order&amp;lt;/b&amp;gt;. Alternativ kann man die Knotennummern erst ausgeben, nachdem alle Nachbarn besucht wurden, also auf dem Rückweg der Rekursion. In diesem Fall spricht man von &amp;lt;b&amp;gt;post-order&amp;lt;/b&amp;gt; oder &amp;lt;b&amp;gt;finishing order&amp;lt;/b&amp;gt;:&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)  # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):              # rekursive Hilfsfunktion, die den gegebenen Knoten und dessen Nachbarn besucht&lt;br /&gt;
         if not visited[node]:     # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True  # Markiere node als besucht&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
             &amp;lt;font color=red&amp;gt;print node            # Ausgabe der Knotennummer - post-order&amp;lt;/font&amp;gt;&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode)&lt;br /&gt;
&lt;br /&gt;
Es ergibt sich jetzt die Ausgabe:&lt;br /&gt;
&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; dfs(graph, 1)&amp;lt;font color=red&amp;gt;&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
 3&lt;br /&gt;
 4&lt;br /&gt;
 5&lt;br /&gt;
 2&lt;br /&gt;
 1&amp;lt;/font&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In realem Code ersetzt man die print-Ausgaben natürlich durch anwendungsspezifische Aktionen und Berechnungen. Einige Anwendungen sind uns im Kapitel [[Suchen]] bereits begegnet. &lt;br /&gt;
; Anwendungen der Pre-Order Traversierung&lt;br /&gt;
* Kopieren eines Graphen: kopiere zuerst den besuchten Knoten, dann seine Nachbarn und die dazugehörigen Kanten (sowie die Kanten zu bereits besuchten Knoten, die in der Grundversion der Tiefensuche ignoriert werden).&lt;br /&gt;
* Bestimmen der Zusammenhangskomponenten eines Graphen (siehe unten)&lt;br /&gt;
* In einem Zeichenprogramm: fülle eine Region mit einer Farbe (&amp;quot;flood fill&amp;quot;). Dabei ist jedes Pixel ein Knoten des Graphen und wird mit seinen 4 Nachbarpixceln verbunden. Die Tiefensuche startet bei der Mausposition und endet am Rand des betreffendcen Gebiets.&lt;br /&gt;
* Falls der Graph ein Baum ist: bestimme den Abstand jedes Knotens von der Wurzel&lt;br /&gt;
* Falls der Graph ein Parse-Baum ist, wobei innere Knoten Funktionsaufrufe, Kindknoten Funktionsargumente, und Blattknoten Werte repräsentieren: drucke den zugehörigen Ausdruck aus (also immer zuerst den Funktionsnamen, dann die Argumente, die wiederum geschachtelte Funktionsaufrufe sein können).&lt;br /&gt;
; Anwendungen der Post-Order Traversierung&lt;br /&gt;
* Löschen eines Graphen: lösche zuerst die Nachbarn, dann den Knoten selbst&lt;br /&gt;
* Bestimmen einer topologischen Sortierung eines azyklischen gerichteten Graphens (siehe unten)&lt;br /&gt;
* Falls der Graph ein Baum ist: bestimme den Abstand jedes Knotens von den Blättern (also die Tiefe des Baumes, siehe Übung 5)&lt;br /&gt;
* Falls der Graph ein Parse-Baum ist: führe die zugehörige Berechnung aus (d.h. berechne zuerst die geschachtelten inneren Funktionen, dann mit diesen Ergebnissen die nächst äußeren usw., siehe Übung 5).&lt;br /&gt;
; Anwendungen, die Pre- und Post-Order benötigen&lt;br /&gt;
* Weg aus einem Labyrinth: die Pre-Order dokumentiert die Suche nach dem Weg, die Post-Order zeigt den Rückweg aus Sackgassen (siehe Übung 9).&lt;br /&gt;
Im Spezialfall, wenn der Graph ein Binärbaum ist, unterscheidet man noch eine dritte Variante der Traversierung, nämlich die &amp;lt;i&amp;gt;in-order&amp;lt;/i&amp;gt; Traversierung. In diesem Fall behandelt man den Vaterknoten nach den linken, aber vor den rechten Kindern. Diese Reihenfolge wird beim [[Suchen#Beziehungen zwischen dem Suchproblem und dem Sortierproblem|Tree Sort Algorithmus]] verwendet. Diese Sortierung verwendet man auch, wenn man einen Parse-Baum mit binären Operatoren (statt Funktionsaufrufen) ausgeben will, siehe Übung 5.&lt;br /&gt;
&lt;br /&gt;
Eine nützliche Erweiterung der Tiefensuche besteht darin, in der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; nicht nur zu dokumentieren, dass ein Knoten bereits besucht wurde, sondern auch, von welchem Knoten aus man den jeweiligen Knoten zuerst erreicht hat. Im entstehenden Tiefensuchbaum ist dies gerade der Vaterknoten, weshalb wir die verbesserte property map zweckmäßigerweise in &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; umbenennen. Für den Startknoten, also die Wurzel des Baumes, wählen wir die Konvention, dass er sein eigener Vaterknoten ist (die Konvention, dafür den Wert &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zu verwenden, scheidet aus, weil dies bereits die Tatsache signalisiert, dass ein Knoten noch nicht besucht wurde):&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     parents = [None]*len(graph)     # Registriere für jeden Knoten den Vaterknoten im Tiefensuchbaum&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, parent):        # rekursive Hilfsfunktion&lt;br /&gt;
         if parents[node] is None:   # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             parents[node] = parent  # Markiere node als besucht und speichere seinen Vaterknoten&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn ...&lt;br /&gt;
                 visit(neighbor, node)      #  ... wobei node zu deren Vaterknoten wird&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode, startnode)     # Konvention für Wurzel: startnode ist sein eigener Vater&lt;br /&gt;
     &lt;br /&gt;
     return parents                  # Rückgabe des berechneten Tiefensuch-Baums&lt;br /&gt;
&lt;br /&gt;
Die Ausgabe für den obigen Beispielgraphen lautet: &lt;br /&gt;
  Knotennummer  |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7&lt;br /&gt;
  --------------+-----+-----+-----+-----+-----+-----+-----+-----&lt;br /&gt;
  Vaterknoten   | None|  1  |  1  |  4  |  2  |  2  |  3  |  3&lt;br /&gt;
&lt;br /&gt;
Dabei ist die Knotennummer der Index im Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt;, und der Vaterknoten ist der dazugehörige Arrayeintrag. Beachte, dass Knoten 0 in diesem Graphen nicht existiert, daher ist sein Eintrag &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;. Per Konvention hat der Wurzelknoten 1 sich selbst als Vater.&lt;br /&gt;
&lt;br /&gt;
=== Breitensuche in Graphen (Breadth First Search, BFS) ===&lt;br /&gt;
&lt;br /&gt;
Im Gegensatz zur Tiefensuche werden bei der Breitensuche alle Nachbarnknoten abgearbeitet, &amp;lt;i&amp;gt;bevor&amp;lt;/i&amp;gt; man rekursiv deren Nachbarn besucht. Man betrachtet somit zuerst alle Knoten, die den Abstand 1 von Startknoten haben, dann diejenigen mit dem Abstand 2 usw. Diese Reihenfolge bezeichnet man als &amp;lt;i&amp;gt;level-order&amp;lt;/i&amp;gt;. Wir sind ihr beispielsweise in Übung 6 begegnet, als die ersten 7 Ebenen eines Treap ausgegeben werden sollten. Man implementiert Breitensuche zweckmäßig mit Hilfe einer Queue, die die Knoten in First In - First Out - Reihenfolge bearbeitet. Eine geeignete Datenstruktur hierfür ist die Klasse &amp;lt;tt&amp;gt;[http://docs.python.org/library/collections.html#collections.deque deque]&amp;lt;/tt&amp;gt; aus dem Python-Modul &amp;lt;tt&amp;gt;[http://docs.python.org/library/collections.html collections]&amp;lt;/tt&amp;gt; (eine Deque implementiert sowohl die Funktionalität einer Queue wie auch die eines Stacks, siehe Übung 3):&lt;br /&gt;
&lt;br /&gt;
 from collections import deque&lt;br /&gt;
 &lt;br /&gt;
 def bfs(graph, startnode)&lt;br /&gt;
     visited = [False]*len(graph)   # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
   &lt;br /&gt;
     q = deque()                    # Queue für die zu besuchenden Knoten&lt;br /&gt;
     q.append(startnode)            # Startknoten in die Queue einfügen&lt;br /&gt;
     &lt;br /&gt;
     while len(q) &amp;gt; 0:              # Solange es noch unbesuchte Knoten gibt&lt;br /&gt;
         node = q.popleft()         # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
         if not visited[node]:      # Falls node noch nicht (auf einem anderen Weg) besucht wurde&lt;br /&gt;
              visited[node] = True  # Markiere node als besucht&lt;br /&gt;
              print node            # Drucke Knotennummer&lt;br /&gt;
              for neighbor in graph[node]:    # Füge Nachbarn in die Queue ein&lt;br /&gt;
                  q.append(neighbor)&lt;br /&gt;
&lt;br /&gt;
[[Image:Breitens.jpg]]&lt;br /&gt;
&lt;br /&gt;
Der Aufruf dieser Funktion liefert die Knoten des obigen Graphens ebenenweise, also zufällig genau in der Reihenfolge der Knotennummern:&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; bfs(graph, 1)&lt;br /&gt;
 1&lt;br /&gt;
 2&lt;br /&gt;
 3&lt;br /&gt;
 4&lt;br /&gt;
 5&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
&lt;br /&gt;
Neben der ebenenweisen Ausgabe hat die Breitensuche viele weitere wichtige Anwendungen, z.B. beim Testen, ob ein gegebener Graph bi-partit ist (siehe [http://en.wikipedia.org/wiki/Breadth-first_search#Testing_bipartiteness WikiPedia]), sowie bei der Suche nach kürzesten Wegen (siehe unten) und kürzesten Zyklen.&lt;br /&gt;
&lt;br /&gt;
== Weitere Anwendungen der Tiefensuche ==&lt;br /&gt;
&lt;br /&gt;
Die Tiefensuche hat zahlreiche Anwendungen, wobei der grundlegende Algorithmus immer wieder leicht modifiziert und an die jeweilige Aufgabe angepasst wird. Wir beschreiben im folgenden einige Beispiele.&lt;br /&gt;
&lt;br /&gt;
=== Damenproblem ===&lt;br /&gt;
&lt;br /&gt;
Tiefensuche wird häufig verwendet, um systematisch nach der Lösung eines logischen Rätsels (oder allgemeiner nach der Lösung eines diskreten Optimierungsproblems) zu suchen. Besonders anschaulich hierfür ist das Damenproblem. Die Aufgabe besteht darin, &amp;lt;math&amp;gt;k&amp;lt;/math&amp;gt; Damen auf einem Schachbrett der Größe &amp;lt;math&amp;gt;k \times k&amp;lt;/math&amp;gt; so zu platzieren, dass sie sich (nach den üblichen Schach-Regeln) nicht gegenseitig schlagen können. Das folgende Diagramm zeigt eine Lösung für den Fall &amp;lt;math&amp;gt;k=4&amp;lt;/math&amp;gt;. Die Positionen der Damen werden dabei wie üblich durch die Angabe der Spalte (Linie) mit Buchstaben und der Zeile (Reihe) mit Zahlen kodiert, hier also A2, B4, C1, D3:&lt;br /&gt;
&lt;br /&gt;
  ---------------&lt;br /&gt;
 |   | X |   |   | 4&lt;br /&gt;
 |---|---|---|---| &lt;br /&gt;
 |   |   |   | X | 3&lt;br /&gt;
 |---|---|---|---|&lt;br /&gt;
 | X |   |   |   | 2&lt;br /&gt;
 |---|---|---|---|&lt;br /&gt;
 |   |   | X |   | 1&lt;br /&gt;
  ---------------&lt;br /&gt;
   A   B   C   D&lt;br /&gt;
&lt;br /&gt;
Um das Problem systematisch zu lösen, konstruieren wir einen gerichteten Graphen, dessen Knoten die möglichen Positionen der Damen kodieren. Wir verbinden Knoten, die zu benachbarten Linien gehören, genau dann mit einer Kante, wenn die zugehörigen Positionen kompatibel sind, also wenn sich die dort positionierten Damen nicht schlagen können. Der resultierende Graph für &amp;lt;math&amp;gt;k=4&amp;lt;/math&amp;gt; hat folgende Gestalt:&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
Knoten, die zur selben Reihe oder Linie gehören, sind beispielsweise nicht direkt verbunden, weil zwei Damen niemals in derselben Linie oder Reihe stehen dürfen. Um eine erlaubte Konfiguration zu finden, verwenden wir nun eine angepasste Version der Tiefensuche: Wir beginnen die Suche beim Knoten &amp;lt;tt&amp;gt;START&amp;lt;/tt&amp;gt;. Sobald wir den Knoten &amp;lt;tt&amp;gt;STOP&amp;lt;/tt&amp;gt; erreichen, beenden wir die Suche und lesen die Lösung am gerade gefundenen Weg von Start nach Stop ab. Zwei kleine Modifikationen des Grundalgorithmus stellen sicher, dass die Bedingungen der Aufgabe eingehalten werden: Wir dürfen bei der Tiefensuche nur dann zu einem Nachbarn weitergehen, wenn die betreffende Position mit allen im Pfad bereits gesetzten Positionen kompatibel ist, andernfalls ist diese Kante tabu. Landen wir aufgrund dieser Regel in einer Sackgasse (also in einem Knoten, wo keine der ausgehenden Kanten erlaubt ist), müssen wir zur nächsten erlaubten Abzweigung zurückgehen (Backtracking). Beim Zurückgehen müssen wir das &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Flag wieder auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zurücksetzen, weil der betreffende Knoten ja möglicherweise auf einem anderen erlaubten Weg erreichbar ist.&lt;br /&gt;
&lt;br /&gt;
Der folgende Graph zeigt einen solchen Fall: Wir haben zwei Damen auf die Felder A1 und B3 positioniert (grüne Pfeile). Die einzig ausgehende Kante von B3 führt zum Knoten C1, welcher aber mit der Position A1 inkompatibel ist, so dass diese Kante nicht verwendet werden darf (roter Pfeil). Das Backtracking muss jetzt zu Knoten A1 zurückgehen (dabei wird das &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Flag von B3 wieder auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; gesetzt), weil A1 mit der Kante nach B4 eine weitere Option hat, die geprüft werden muss (die allerdings hier auch nicht zum Ziel führt).&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph-failure.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
Nach einigen weiteren Sackgassen findet man schließlich den Pfad A2, B4, C1, D3, der im folgenden Graphen grün markiert ist und der obigen Lösung entspricht:&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph-success.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
=== Test, ob ein ungerichteter Graph azyklisch ist ===&lt;br /&gt;
&lt;br /&gt;
Ein zusammenhängender ungerichteter Graph ist azyklisch (also ein Baum) genau dann, wenn es nur einen möglichen Weg von jedem Knoten zu jedem anderen gibt. (Bei gerichteten Graphen sind die Verhältnisse komplizierter. Wir behandeln dies weiter unten.) Das kann man mittels Tiefensuche leicht feststellen: Die Kante, über die wir einen Knoten erstmals erreichen, ist eine &amp;lt;i&amp;gt;Baumkante&amp;lt;/i&amp;gt; des Tiefensuchbaums. Erreichen wir einen bereits besuchten Knoten nochmals über eine andere Kante, haben wir einen Zyklus gefunden. Dabei müssen wir allerdings beachten, dass in einem ungerichteten Graphen jede Baumkante zweimal gefunden wird, einmal in Richtung vom Vater zum Kind und einmal in umgekehrter Richtung. Im zweiten Fall endet die Kante zwar in einem bereits besuchten Knoten (dem Vater), aber es entsteht dadurch kein Zyklus. Den Vaterknoten müssen wir deshalb überspringen, wenn wir über die Nachbarn iterieren:&lt;br /&gt;
&lt;br /&gt;
 def undirected_cycle_test(graph):         # Annahme: der Graph ist zusammenhängend&lt;br /&gt;
                                           # (andernfalls führe den Algorithmus für jede Zusammenhangskomponente aus)&lt;br /&gt;
     visited = [False]*len(graph)          # Flags für bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, from_node):           # rekursive Hilfsfunktion: gibt True zurück, wenn Zyklus gefunden wurde&lt;br /&gt;
         if not visited[node]:             # wenn node noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True          # markiere node als besucht&lt;br /&gt;
             for neighbor in graph[node]:  # besuche die Nachbarn ...&lt;br /&gt;
                 if neighbor == from_node: # ... aber überspringe den Vaterknoten&lt;br /&gt;
                     continue&lt;br /&gt;
                 if visit(neighbor, node): # ... signalisiere, wenn rekursiv ein Zyklus gefunden wurde&lt;br /&gt;
                     return True&lt;br /&gt;
             return False                  # kein Zyklus gefunden&lt;br /&gt;
         else:&lt;br /&gt;
             return True                   # Knoten schon besucht =&amp;gt; Zyklus&lt;br /&gt;
     &lt;br /&gt;
     startnode = 0                         # starte bei beliebigem Knoten (hier: Knoten 0)&lt;br /&gt;
     return visit(startnode, startnode)    # gebe True zurück, wenn ein Zyklus gefunden wurde&lt;br /&gt;
&lt;br /&gt;
Wenn wir einen Zyklus finden, wird das weitere Traversieren das Graphen abgebrochen, denn ein Graph, der einmal zyklisch war, kann später nicht wieder azyklisch werden. Die notwendige Modifikation für unzusammenhängende Graphen erfolgt analog zum Algorithmus für die Detektion von Zusammenhangskomponenten, der im nächsten Abschnitt beschrieben wird.&lt;br /&gt;
&lt;br /&gt;
=== Finden von Zusammenhangskomponenten ===&lt;br /&gt;
&lt;br /&gt;
Das Auffinden und Markieren von Zusammenhangskomponenten (also maximalen zusammenhängenden Teilgraphen) ist eine grundlegende Aufgabe in ungerichteten, unzusammenhängenden Graphen (bei gerichteten Graphen sind die Verhältnisse wiederum komplizierter, siehe unten). Zwei Knoten u und v gehören zur selben Zusammenhangskomponente genau dann, wenn es einen Pfad von u nach v gibt (da der Graph ungerichtet ist, gibt es dann auch einen Pfad von v nach u). Man sagt auch, dass &amp;quot;v von u aus erreichbar&amp;quot; ist. Unzusammenhängende Graphen entstehen in der Praxis häufig, wenn die Kanten gewisse Relationen zwischen den Knoten kodieren: &lt;br /&gt;
* Wenn die Knoten Städte sind und die Kanten Straßen, sind diejenigen Städte in einer Zusammenhangskomponente, die per Auto von einander erreichbar sind. Unzusammenhängende Graphen entstehen hier beispielsweise, wenn eine Insel nicht durch eine Brücke erschlossen ist, wenn Grenzen gesperrt sind oder wenn ein Gebirge zu unwegsam ist, um Straßen zu bauen.&lt;br /&gt;
* Wenn Knoten Personen sind, und Kanten die Eltern-Kind-Relation beschreiben, so umfasst jede Zusammenhangskomponenten die Verwandten (auch wenn sie nur über viele &amp;quot;Ecken&amp;quot; verwandt sind).&lt;br /&gt;
* In der Bildverarbeitung entsprechen Knoten den Pixeln, und dieselben werden durch eine Kante verbunden, wenn sie zum selben Objekt gehören. Die Zusammenhangskomponenten entsprechen somit den Objekten im Bild (siehe Übungsaufgabe).&lt;br /&gt;
Die Zusammenhangskomponenten bilden eine Äquivalenzrelation. Folglich kann für jede Komponente ein Reprässentant bestimmt werden, der sogenannte &amp;quot;Anker&amp;quot;. Kennt jeder Knoten seinen Anker, ist das Problem der Zusammenhangskomponenten gelöst. &lt;br /&gt;
&lt;br /&gt;
==== Lösung mittels Tiefensuche ====&lt;br /&gt;
&lt;br /&gt;
Unser erster Ansatz ist, den Anker mit Hilfe der Tiefensuche zu finden. Anstelle der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; verwenden wir diesmal eine property map &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt;, die für jeden Knoten die Knotennummer des zugehörigen Ankers angibt, oder &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, wenn der Knoten noch nicht besucht wurde. Dabei verwenden wir wieder die Konvention, dass Anker auf sich selbst zeigen. Für viele Anwendungen ist es außerdem (oder stattdessen) zweckmäßig, die Zusammenhangskomponenten mit einer laufenden Nummer, einem sogenannten &amp;lt;i&amp;gt;Label&amp;lt;/i&amp;gt;, durchzuzählen. Dann kann man zusätzliche Informationen zu jeder Komponente (beispielsweise deren Größe) einfach in einem Array speichern, das über die Labels indexiert wird. Die folgende Version der Tiefensuche bestimmt sowohl die Anker als auch die Labels für jeden Knoten:&lt;br /&gt;
&lt;br /&gt;
 def connectedComponents(graph):&lt;br /&gt;
        anchors = [None] * len(graph)             # property map für Anker jedes Knotens&lt;br /&gt;
        labels  = [None] * len(graph)             # property map für Label jedes Knotens&lt;br /&gt;
        &lt;br /&gt;
        def visit(node, anchor):&lt;br /&gt;
                &amp;quot;&amp;quot;&amp;quot;anchor ist der Anker der aktuellen ZK&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
                if anchors[node] is None:         # wenn node noch nicht besucht wurde:&lt;br /&gt;
                    anchors[node] = anchor        # setze seinen Anker&lt;br /&gt;
                    labels[node] = labels[anchor] # und sein Label&lt;br /&gt;
                    for neighbor in graph[node]:  # und besuche die Nachbarn&lt;br /&gt;
                        visit(neighbor, anchor)&lt;br /&gt;
        &lt;br /&gt;
        current_label = 0                         # Zählung der ZK beginnt bei 0&lt;br /&gt;
        for node in xrange(len(graph)):&lt;br /&gt;
            if anchors[node] is None:             # Anker noch nicht bekannt =&amp;gt; neue ZK gefunden&lt;br /&gt;
                labels[node] = current_label      # Label des Ankers setzen&lt;br /&gt;
                visit(node, node)                 # Knoten der neuen ZK rekursiv suchen&lt;br /&gt;
                current_label += 1                # Label für die nächste ZK hochzählen&lt;br /&gt;
        return anchors, labels&lt;br /&gt;
Interessant ist hier die Schleife über alle Knoten des Graphen am Ende des Algorithmus, die bei den bisherigen Versionen der Tiefensuche nicht vorhanden war. Um ihre Funktionsweise zu verstehen, nehmen wir für den Moment an, dass der Graph zusammenhängend ist. Dann findet diese Schleife den ersten Knoten des Graphen und führt die Tiefensuche mit diesem Knoten als Startknoten aus. Sobald die Rekursion zurückkehrt, sind alle Knoten des Graphen besucht (weil der Graph ja zusammenhängend war), so dass die Schleife alle weiteren Knoten überspringt (die if-Anweisung liefert für keinen weiteren Knoten True). Bei unzusammenhängenden Graphen dagegen erreicht die Tiefensuche nur die Knoten derselben Komponente, die im weiteren Verlauf der Schleife übersprungen werden. Findet die if-Anweisung jetzt einen noch nicht besuchten Knoten, muss dieser folglich in einer neuen Komponente liegen. Wir verwenden diesen Knoten als Anker und bestimmen die übrigen Knoten dieser Komponente wiederum mit Tiefensuche.&lt;br /&gt;
&lt;br /&gt;
* Beispiel: ... &amp;lt;b&amp;gt; under construction &amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Man erkennt, dass die Tiefensuche nach dem &amp;lt;i&amp;gt;Anlagerungsprinzip&amp;lt;/i&amp;gt; vorgeht: Beginnend vom einem Startknoten (dem Anker) werden die Knoten der aktuellen Komponente nach und nach an den Tiefensuchbaum angehangen. Erst, wenn nichts mehr angelagert werden kann, geht der Algorithmus zur nächsten Komponente über.&lt;br /&gt;
&lt;br /&gt;
==== Lösung mittels Union-Find-Algorithmus ====&lt;br /&gt;
&lt;br /&gt;
Im Gegensatz zum Anlagerungsprinzip sucht der Union-Find-Algorithmus die Zusammenhangskomponenten mit dem &amp;lt;i&amp;gt;Verschmelzungsprinzip&amp;lt;/i&amp;gt;: Eingangs wird jeder Knoten als ein Teilgraph für sich betrachtet. Dann iteriert man über alle Kanten und verbindet deren Endknoten jeweils zu einem gemeinsamen Teilgraphen (falls die beiden Enden einer Kante bereits im selben Teilgraphen liegen, wird diese Kante ignoriert). Solange noch Kanten vorhanden sind, werden dadurch immer wieder Teilgraphen in größere Teilgraphen verschmolzen. Am Ende bleiben die maximalen zusammenhängenden Teilgraphen (also gerade die Zusammenhangskomponenten) übrig. Dieser Algorithmus kommt ohne Tiefensuche aus und ist daher in der Praxis oft schneller, allerdings auch etwas komplizierter zu implementieren.&lt;br /&gt;
&lt;br /&gt;
Der Schlüssel des Algorithmus ist eine Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt;, die zu jedem Knoten den aktuellen Anker sucht. Der Anker existiert immer, da jeder Knoten von Anfang an zu einem Teilgraphen gehört (anfangs ist jeder Teilgraph trivial und besteht nur aus dem Knoten selbst). Die Verschmelzung wird realisiert, indem der Anker des einen Teilgraphen seine Rolle verliert und stattdessen der Anker des anderen Teilgraphen eingesetzt wird. &lt;br /&gt;
&lt;br /&gt;
Zur Verwaltung der Anker verwenden wir wieder eine property map &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt; mit der Konvention, dass die Anker auf sich selbst verweisen. Es wäre jedoch zu teuer, wenn man bei jeder Verschmelzung alle Anker-Einträge der beteiligten Knoten aktualisieren müsste, da jeder Knoten im Laufe des Algorithmus mehrmals seinen Anker wechseln kann. Statt dessen definiert man Anker rekursiv: Verweist ein Knoten auf einen Anker, der mittlerweile diese Rolle verloren hat, folgt man dem Verweis von diesem Knoten (dem ehemaligen Anker) weiter, bis man einen tatsächlichen Anker gefunden hat - erkennbar daran, dass er auf sich selbst verweist. Diese Suchfunktion kann folgendermassen implementiert werden:&lt;br /&gt;
&lt;br /&gt;
  def findAnchor(anchors, node):&lt;br /&gt;
      while node != anchors[node]:   # wenn node kein Anker ist&lt;br /&gt;
          node = anchors[node]       # ... verfolge die Ankerkette weiter&lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Allerdings kann diese Kette im Laufe vieler Verschmelzungen sehr lang werden, so dass das Verfolgen der Kette teuer wird. Man vermeidet dies durch die sogenannte &amp;lt;i&amp;gt;Pfadkompression&amp;lt;/i&amp;gt;: Immer, wenn man den Anker gefunden hat, aktualisiert man den Eintrag am Anfang der Kette. Die Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt; wird dadurch nur wenig komplizierter:&lt;br /&gt;
&lt;br /&gt;
  def findAnchor(anchors, node):&lt;br /&gt;
      start = node                   # wir merken uns den Anfang der Kette&lt;br /&gt;
      while node != anchors[node]:   # wenn node kein Anker ist&lt;br /&gt;
          node = anchors[node]       # ... verfolge die Ankerkette weiter&lt;br /&gt;
      anchors[start] = node          # Pfadkompression: aktualisiere den Eintrag am Anfang der Kette&lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Man kann zeigen, dass die Ankersuche mit Pfadkompression zu einer fast konstanten amortisierten Laufzeit pro Aufruf führt.&lt;br /&gt;
&lt;br /&gt;
Um mit jeder Kante des (ungerichteten) Graphen nur maximal einmal eine Verschmelzung durchzuführen, betrachten wir jede Kante nur in der Richtung von der kleineren zur größeren Knotennummer, die umgekehrte Richtung wird ignoriert. Außerdem ist es zweckmäßig, bei jeder Verschmelzung denjenigen Anker mit der kleineren Knotennummer als neuen Anker zu übernehmen. Dann gilt für jede Zusammenhangskomponente, dass gerade der Knoten mit der kleinsten Knotennummer der Anker ist (genau wie bei der Lösung mittels Tiefensuche), was die weitere Analyse vereinfacht, z.B. die Zuordnung der Labels zu den Komponenten am Ende des Algorithmus. &lt;br /&gt;
&lt;br /&gt;
 def unionFindConnectedComponents(graph):&lt;br /&gt;
     anchors = range(len(graph))        # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):    # iteriere über alle Knoten&lt;br /&gt;
         for neighbor in graph[node]:   # ... und über deren ausgehende Kanten&lt;br /&gt;
             if neighbor &amp;lt; node:        # ignoriere Kanten, die in falscher Richtung verlaufen&lt;br /&gt;
                 continue&lt;br /&gt;
             # hier landen wir für jede Kante des Graphen genau einmal&lt;br /&gt;
             a1 = findAnchor(anchors, node)       # finde Anker ...&lt;br /&gt;
             a2 = findAnchor(anchors, neighbor)   # ... der beiden Endknoten&lt;br /&gt;
             if a1 &amp;lt; a2:                          # Verschmelze die beiden Teilgraphen&lt;br /&gt;
                 anchors[a2] = a1                 # (verwende den kleineren der beiden Anker als Anker des&lt;br /&gt;
             elif a2 &amp;lt; a1:                        #  entstehenden Teilgraphen. Falls node und neighbor &lt;br /&gt;
                 anchors[a1] = a2                 #  den gleichen Anker haben, waren sie bereits im gleichen&lt;br /&gt;
                                                  #  Teilgraphen, und es passiert hier nichts.)&lt;br /&gt;
     # Bestimme jetzt noch die Labels der Komponenten&lt;br /&gt;
     labels = [None]*len(graph)         # Initialisierung der property map für Labels&lt;br /&gt;
     current_label = 0                  # die Zählung beginnt bei 0&lt;br /&gt;
     for node in xrange(len(graph)):&lt;br /&gt;
         a = findAnchor(anchors, node)  # wegen der Pfadkompression zeigt jeder Knoten jetzt direkt auf seinen Anker&lt;br /&gt;
         if a == node:                  # node ist ein Anker&lt;br /&gt;
             labels[a] = current_label  # =&amp;gt; beginne eine neue Komponente&lt;br /&gt;
             current_label += 1         # und zähle Label für die nächste ZK hoch&lt;br /&gt;
         else:&lt;br /&gt;
             labels[node] = labels[a]   # node ist kein Anker =&amp;gt; setzte das Label des Ankers&lt;br /&gt;
                                        # (wir wissen, dass labels[a] bereits gesetzt ist, weil &lt;br /&gt;
                                        #  der Anker immer der Knoten mit der kleinsten Nummer ist)&lt;br /&gt;
     return anchors, labels&lt;br /&gt;
 &lt;br /&gt;
* Beispiel: ... &amp;lt;b&amp;gt;under construction&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Kürzeste Wege (Pfade) ==&lt;br /&gt;
&lt;br /&gt;
Eine weitere grundlegende Aufgabe in Graphen ist die Bestimmung eines kürzesten Weges zwischen zwei gegebenen Knoten. Dies hat offensichtliche Anwendungen bei Routenplanern und Navigationssystemen und ist darüber hinaus wichtiger Bestandteil anderer Algorithmen, z.B. bei der Berechnung eines maximalen Flusses mit der [http://en.wikipedia.org/wiki/Edmonds%E2%80%93Karp_algorithm Methode von Edmonds und Karp].&lt;br /&gt;
&lt;br /&gt;
=== Kürzeste Wege in ungewichteten Graphen mittels Breitensuche ===&lt;br /&gt;
&lt;br /&gt;
Im Fall eines ungewichteten Graphen ist die Länge eines Weges einfach durch die Anzahl der durchlaufenen Kanten definiert. Daraus folgt, dass kürzeste Pfade mit einer leicht angepassten Version der Breitensuche gefunden werden können: Aufgrund des first in-first out-Verhaltens der Queue betrachtet die Breitensuche alle (erreichbaren) Knoten in der Reihenfolge ihres Abstandes vom Startknoten. Wenn wir den Zielknoten zum ersten Mal erreichen, und der gerade gefundene Weg vom Start zum Ziel hat die Länge L, muss dies der kürzeste Weg sein: Alle möglichen Wege der Länge L' &amp;amp;lt; L hat die Breitensuche ja bereits betrachtet, ohne dass dabei der Zielknoten erreicht wurde. Daraus folgt übrigens eine allgemeine Eigenschaft aller Algorithmen für kürzeste Wege: Wenn der kürzeste Weg vom Start zum Ziel die Länge L hat, finden diese Algorithmen als Nebenprodukt auch die kürzesten Wege zu allen Knoten, für die L' &amp;amp;lt; L gilt. &lt;br /&gt;
&lt;br /&gt;
Um den Algorithmus zu implementieren, passen wir die Breitensuche so an, dass anstelle der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; eine property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; verwendet wird, die für jeden besuchten Knoten den Vaterknoten im Breitensuchbaum speichert. Durch Rückverfolgen der &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette können wir den Pfad vom Ziel zum Start rekonstruieren, und durch Umdrehen der Reihenfolge erhalten wir den gesuchten Pfad vom Start zum Ziel. Sobald der Zielknoten erreicht wurde, können wir die Breitensuche abbrechen (&amp;lt;tt&amp;gt;break&amp;lt;/tt&amp;gt;-Befehl in der ersten &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Schleife). Falls der gegebene Graph unzusammenhängend ist, kann es passieren, dass gar kein Weg gefunden wird, weil Start und Ziel in verschiedenen Zusammenhangskomponenten liegen. Dies erkennen wir daran, dass die Breitensuche beendet wurde, ohne den Zielknoten zu besuchen. Dann gibt die Funktion statt eines Pfades dern Wert &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zurück:&lt;br /&gt;
&lt;br /&gt;
  from collections import deque&lt;br /&gt;
  &lt;br /&gt;
  def shortestPath(graph, startnode, destination):&lt;br /&gt;
      parents = [None]*len(graph)      # Registriere für jeden Knoten den Vaterknoten im Breitensuchbaum&lt;br /&gt;
      parents[startnode] = startnode   # startnode ist die Wurzel des Baums =&amp;gt; verweist auf sich selbst&lt;br /&gt;
      &lt;br /&gt;
      q = deque()                      # Queue für die zu besuchenden Knoten&lt;br /&gt;
      q.append(startnode)              # Startknoten in die Queue einfügen&lt;br /&gt;
      &lt;br /&gt;
      while len(q) &amp;gt; 0:                # Solange es noch unbesuchte Knoten gibt&lt;br /&gt;
          node = q.popleft()           # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
          if node == destination:      # Zielknoten erreicht&lt;br /&gt;
              break                    #   =&amp;gt; Suche beenden&lt;br /&gt;
          for neighbor in graph[node]: # Besuche die Nachbarn von node&lt;br /&gt;
              if parents[neighbor] is None:  # aber nur, wenn sie noch nicht besucht wurden&lt;br /&gt;
                  parents[neighbor] = node   # setze node als Vaterknoten&lt;br /&gt;
                  q.append(neighbor)         # und füge neighbor in die Queue ein&lt;br /&gt;
      &lt;br /&gt;
      if parents[destination] is None: # Breitensuche wurde beendet ohne den Zielknoten zu besuchen&lt;br /&gt;
          return None                  # =&amp;gt; kein Pfad gefunden (unzusammenhängender Graph)&lt;br /&gt;
      &lt;br /&gt;
      # Pfad durch die parents-Kette zurückverfolgen und speichern&lt;br /&gt;
      path = [destination]&lt;br /&gt;
      while path[-1] != startnode:&lt;br /&gt;
          path.append(parents[path[-1]])&lt;br /&gt;
      path.reverse()     # Reihenfolge umdrehen (Ziel =&amp;gt; Start wird zu Start =&amp;gt; Ziel)&lt;br /&gt;
      return path        # gefundenen Pfad zurückgeben&lt;br /&gt;
&lt;br /&gt;
=== Gewichtete Graphen ===&lt;br /&gt;
&lt;br /&gt;
Das Problem der Suche nach kürzesten Wegen wird wesentlich interessanter und realistischer, wenn wir zu gewichteten Graphen übergehen:&lt;br /&gt;
&lt;br /&gt;
; Definition - kantengewichteter Graph&lt;br /&gt;
: Jeder Kante (s,t) des Graphen ist eine reelle oder natürliche Zahl w&amp;lt;sub&amp;gt;st&amp;lt;/sub&amp;gt; zugeordnet, die üblicherweise als ''Kantengewicht'' bezeichnet wird.&lt;br /&gt;
&lt;br /&gt;
; Definition - knotengewichteter Graph&lt;br /&gt;
: Jedem Knoten v des Graphen ist eine reelle oder natürliche Zahl w&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; zugeordnet, die üblicherweise als ''Knotengewicht'' bezeichnet wird.&lt;br /&gt;
&lt;br /&gt;
Je nach Anwendung benötigt man Knoten- oder Kantengewichte oder auch beides zugleich. Wir beschränken uns in der Vorlesung auf kantengewichtete Graphen. Beispiele für die Informationen, die man durch Kantengewichte ausdrücken kann, sind&lt;br /&gt;
* wenn die Knoten Orte sind: Abstand von Anfangs- und Endknoten jeder Kante (z.B. Luftline oder Straßenentfernung), Fahrzeit zwischen den Orten&lt;br /&gt;
* wenn der Knoten ein Rohrnetzwerk beschreibt: Durchflusskapazität der einzelnen Rohre (für max-Flussprobleme), analog bei elektrischen Netzwerken: elektrischer Widerstand&lt;br /&gt;
* wenn die Knoten Währungen repräsentieren, können deren Wechselkurse durch Kantengewichte angegeben werden.&lt;br /&gt;
Bei einigen Beispielen ergeben sich unterschiedliche Kantengewichte, wenn eine Kante von s nach t anstatt von t nach s durchlaufen wird. Beispielsweise können sich die Fahrzeiten erheblich unterscheiden, wenn es in einer Richtung bergauf, in der anderen bergab geht, obwohl die Entfernung in beiden Fällen gleich ist. Hier ergibt sich natürlicherweise ein gerichteter Graph. In anderen Beispielen (z.B. bei Luftlinienentfernungen, in guter Näherung auch bei Straßenentfernungen) sind die Gewichte von der Richtung unabhängig, so dass wir ungerichtete Graphen verwenden können.&lt;br /&gt;
&lt;br /&gt;
Die Repräsentation der Kantengewichte im Programm richtet sich nach der Repräsentation des Graphen selbst. Am einfachsten ist wiederum die Adjazenzmatrix, die aber nur für dichte Graphen (&amp;lt;math&amp;gt;E = O(V^2)&amp;lt;/math&amp;gt;, mit E als Anzahl der Kanten und V als Anzahl der Knoten) effizient ist. Bei gewichteten Graphen gibt das Matrixelement a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; das Gewicht der Kante i &amp;amp;rArr; j (wobei a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; = 0 gesetzt wird, wenn diese Kante nicht existiert). Wie zuvor gilt für ungerichtete Graphen a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; = a&amp;lt;sub&amp;gt;ji&amp;lt;/sub&amp;gt; (symmetrische Matrix), während dies für gerichtete Graphen nicht gelten muss.&lt;br /&gt;
&lt;br /&gt;
Bei Graphen in Adjazenzlistendarstellung hat es sich bewährt, die Gewichte in einer &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; zu speichern. Weiter oben haben wir bereits property maps für Knoteneigenschaften (z.B. &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt;) gesehen. Property maps für Kanten funktionieren ganz analog, allerdings muss man jetzt Paare von Knoten (nämlich Anfangs- und Endknoten der Kante) als Schlüssel verwenden und die Daten entsprechend in einem assoziativen Array ablegen:&lt;br /&gt;
  w = weights[(i,j)]   # Zugriff auf das Gewicht der Kante i &amp;amp;rArr; j&lt;br /&gt;
Alternativ könnte man auch die Graph-Datenstruktur selbst erweitern, aber dies ist weniger zu empfehlen, weil jeder Algorithmus andere Erwiterungen benötigt und damit die Datenstruktur sehr unübersichtlich würde.&lt;br /&gt;
&lt;br /&gt;
Der kürzeste Weg ist nun definiert als der Weg, bei dem die Summe der Kantengewichte minimal ist:&lt;br /&gt;
;Definition - Problem des kürzesten Weges&lt;br /&gt;
: Sei P die Menge aller Wege von u nach v, und &amp;lt;math&amp;gt;p \in P&amp;lt;/math&amp;gt; einer dieser Wege. Wenn der Grpah einfach ist (es also keine Mehrfachkanten zwischen denselben Knoten und keine Schleifen gibt), ist der Weg p durch die Folge der besuchten Knoten eindeutig bestimmt:&lt;br /&gt;
: &amp;lt;math&amp;gt;p : \ \ u = x_0 \rightarrow x_1 \rightarrow x_2 \rightarrow ... \rightarrow v = x_{n_p}&amp;lt;/math&amp;gt;&lt;br /&gt;
:wo &amp;lt;math&amp;gt;n_p&amp;lt;/math&amp;gt; die Anzahl der Kanten im Weg p ist. Seine Kosten W&amp;lt;sub&amp;gt;p&amp;lt;/sub&amp;gt; ergeben sich als Summer der Gewichte der einzelnen Kanten&lt;br /&gt;
: &amp;lt;math&amp;gt;W_p = \sum_{k=1}^{n_p} w_{x_{k-1}x_k}&amp;lt;/math&amp;gt;&lt;br /&gt;
: und ein kürzester Weg &amp;lt;math&amp;gt;p^* \in P&amp;lt;/math&amp;gt; ist ein Weg mit minimalen Kosten&lt;br /&gt;
: &amp;lt;math&amp;gt;p^* = \textrm{argmin}_{p\in P}\ \ W_p&amp;lt;/math&amp;gt;&lt;br /&gt;
: Das Problem des kürzesten Weges besteht darin, einen optimalen Weg &amp;lt;i&amp;gt;p*&amp;lt;/i&amp;gt; zwischen gegebenen Knoten u und v zu finden.&lt;br /&gt;
Die Lösung dieses Problems hängt davon ab, ob alle Kantengewichte positiv sind, oder ob es auch negative Kantengewichte gibt. In letzeren Fall ist es möglich, durch eine Verlängerung des Weges die Kosten zu redizieren, während sich im ersteren Fall die Kosten immer erhöhen, wenn man den Weg verlängert. &lt;br /&gt;
&lt;br /&gt;
Negative Gewichte treten z.B. bei den Währungsgraphen auf. Auf den ersten Blick entsprechen diese Graphen nicht den Anforderungen an das Problem des kürzesten Weges, weil Wechselkurse miteinander (und mit Geldbeträgen) multipliziert anstatt addiert werden. Man beseitigt diese Schwierigkeit aber leicht, indem man die &amp;lt;i&amp;gt;Logarithmen&amp;lt;/i&amp;gt; der Wechselkurse als Kantengewichte verwendet, wodurch sich die Multiplikation in eine Addition der Logarithmen verwandelt. Wechselkurse &amp;amp;lt; 1 führen nun zu negativen Gewichten. &lt;br /&gt;
&lt;br /&gt;
Interessant werden negative Gewichte vor allem in Graphen mit Zyklen. Dann kann es nämlich passieren, dass die Gesamtkosten eines Zyklus ebenfalls negativ sind. Jeder Weg, der den Zyklus enthält, hat dann Kosten von &amp;lt;math&amp;gt;-\infty&amp;lt;/math&amp;gt;, weil man den Zyklus beliebig oft durchlaufen und dadurch die Gesamtkosten immer weiter verkleinern kann:&lt;br /&gt;
&lt;br /&gt;
     /\		1. Durchlauf: Kosten -1&lt;br /&gt;
  1 /  \ -4	2. Durchlauf: Kosten -2&lt;br /&gt;
   /____\	etc.&lt;br /&gt;
      2&lt;br /&gt;
&lt;br /&gt;
Um hier nicht in einer Endlosschleife zu landen, benötigt man spezielle Algorithmen, die mit dieser Situation umgehen können. Der [http://de.wikipedia.org/wiki/Bellman-Ford-Algorithmus Algorithmus von Bellmann und Ford] beispielsweise bricht die Suche nach dem kürzesten Weg ab, sobald er einen negativen Zyklus entdeckt, aber andernfalls kann er negative Gewichte problemlos verarbeiten. &lt;br /&gt;
&lt;br /&gt;
Die Detektion negativer Zyklen hat wiederum eine interessante Anwendung bei Währungsgraphen: Ein Zyklus bedeutet hier, dass man Geld über mehrere Stufen von einer Währung in die nächste und am Schluß wieder in die Originalwährung umtauscht, und ein negativer Zyklus führt dazu, dass man am Ende &amp;lt;i&amp;gt;mehr&amp;lt;/i&amp;gt; Geld besitzt als am Anfang (damit negative Zyklen wirklich einen Gewinn bedeuten und keinen Verlust, müssen die Wechselkurse vor der Logarithmierung in [http://de.wikipedia.org/wiki/Wechselkurs#Nominaler_Wechselkurs Preisnotierung] angegeben sein). Bei Privatpersonen ist dies ausgeschlossen, weil die Umtauschgebühren den möglichen Gewinn mehr als aufzehren. Banken mit direktem weltweitem Börsenzugang hingegen unternehmen große Anstrengungen, um solche negativen Zyklen möglichst schnell (nämlich vor der Konkurrenz) zu entdecken und auszunutzen. Diese Geschäftsmethode bezeichnet man als [http://de.wikipedia.org/wiki/Arbitrage Arbitrage] und die Existenz eines negativen Zyklus als Arbitragegelegenheit. Durch die Kursschwankungen (und durch die ausgleichende Wirkung der Arbitragegeschäfte selbst) existieren die Arbitragegelegenheiten nur für kurze Zeit, und ihre Detektion erfordert leistungsfähige Echtzeitalgorithmen.&lt;br /&gt;
&lt;br /&gt;
In dieser Vorlesung beschränken wir uns hingegen auf Graphen mit ausschließlich positiven Gewichten. In diesem Fall ist der Algorithmus von Dijkstra die Methode der Wahl, weil er wesentlich schneller arbeitet als der Bellmann-Ford-Algorithmus.&lt;br /&gt;
&lt;br /&gt;
=== Algorithmus von Dijkstra ===&lt;br /&gt;
&lt;br /&gt;
==== Edsger Wybe Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
geb. 11. Mai 1930 in Rotterdam&lt;br /&gt;
&lt;br /&gt;
ges. 06. August 2002&lt;br /&gt;
&lt;br /&gt;
Dijkstra war ein niederländischer Informatiker und Wegbereiter der strukturierten Programmierung. 1972 erhielt er für seine Leistung in der Technik und Kunst der Programmiersprachen den Turing Award, der jährlich von der Association for Computing Machinery (ACM) an Personen verliehen wird, die sich besonders um die Entwicklung der Informatik verdient gemacht haben. Zu seinen Beiträgen zur Informatik gehören unter anderem der Dijkstra-Algorithmus zur Berechnung des kürzesten Weges in einem Graphen sowie eine Abhandlung über den go-to-Befehl und warum er nicht benutzt werden sollte. Der go-to-Befehl war in den 60er und 70er Jahren weit verbreitet, führte aber zu Spaghetti-Code. In seinem berühmten Paper &amp;quot;A Case against the GO TO Statement&amp;quot;[http://www.cs.utexas.edu/users/EWD/ewd02xx/EWD215.PDF], das als Brief mit dem Titel &amp;quot;Go-to statement considered harmful&amp;quot; veröffentlicht wurde, argumentiert Dijkstra, dass es umso schwieriger ist, dem Quellcode eines Programmes zu folgen, je mehr go-to-Befehle darin enthalten sind und zeigt, dass man auch ohne diesen Befehl gute Programme schreiben kann.&lt;br /&gt;
&lt;br /&gt;
==== Algorithmus ====&lt;br /&gt;
&lt;br /&gt;
Der Dijkstra-Algorithmus für kürzeste Wege ist dem oben vorgestellten Algorithmus &amp;lt;tt&amp;gt;shortestPath()&amp;lt;/tt&amp;gt; auf der Basis von Breitensuche sehr ähnlich. Insbesondere gilt auch hier, dass neben dem kürzesten Weg vom Start zum Ziel auch alle kürzesten Wege gefunden werden, deren Endknoten dem Start näher sind als der Zielknoten. Aufgrund der Kantengewichte gibt es aber einen wichtigen Unterschied: Der erste gefundene Weg zu einem Knoten ist nicht mehr notwendigerweise der kürzeste. Wir bestimmen deshalb für jeden Knoten mehrere Kandidatenwege und verwenden eine Prioritätswarteschlange (statt einer einfachen First in - First out - Queue), um diese Wege nach ihrer Länge zu sortieren. Die Kandidatenwege für einen gegebenen Knoten werden unterschieden, indem wir auch den Vorgängerknoten im jeweiligen Weg speichern. Wenn ein Knoten &amp;lt;i&amp;gt;erstmals&amp;lt;/i&amp;gt; an die Spitze der Prioritätswarteschlange gelangt, haben wir den kürzesten Weg zu diesem Knoten gefunden (das wird weiter unten formal bewiesen), und der Vorgänger des Knotens in diesem Weg wird zu seinem Vaterknoten. Erscheint derselbe Knoten später nochmals an der Spitze der Prioritätswarteschlange, handelt es sich um einen Kandidatenweg, der sich nicht als kürzester erwiesen hat und deshalb ignoriert werden kann. Wir erkennen dies leicht daran, dass der Vaterknoten in der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; bereits gesetzt ist. &lt;br /&gt;
&lt;br /&gt;
Eine geeignete Datenstruktur für die Prioritätswarteschlange wird durch das Python-Modul [http://docs.python.org/library/heapq.html heapq] realisiert. Es verwendet ein normales Pythonarray als unterliegende Repräsentation für einen Heap und stellt effiziente &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt;-Funktionen zur Verfügung. Dies entspricht genau unserer Vorgehensweise im Kapitel [[Prioritätswarteschlangen]]. Als Datenelement erwartet die Funktion &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; ein Tupel, dessen erstes Element die Priorität sein muss. Die übrigen Elemente des Tupels (und damit auch deren Anzahl) können je nach Anwendung frei festgelegt werden. Wir legen fest, dass das zweite Element den Endknoten des betrachteten Weges und das dritte den Vorgängerknoten speichert. &lt;br /&gt;
&lt;br /&gt;
Die Kantengewichte werden dem Algorithmus in der property map &amp;lt;tt&amp;gt;weights&amp;lt;/tt&amp;gt; übergeben:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;code python&amp;gt;&lt;br /&gt;
    import heapq	                  # heapq implementiert die Funktionen für Heaps&lt;br /&gt;
    &lt;br /&gt;
    def dijkstra(graph, weights, startnode, destination):&lt;br /&gt;
        parents = [None]*len(graph)       # registriere für jeden Knoten den Vaterknoten im Pfadbaum&lt;br /&gt;
      &lt;br /&gt;
        q = []                            # Array q wird als Heap verwendet&lt;br /&gt;
        &amp;lt;font color=red&amp;gt;heapq.heappush(q, (0.0, startnode, startnode))&amp;lt;/font&amp;gt;  # Startknoten in Heap einfügen&lt;br /&gt;
      &lt;br /&gt;
        while len(q) &amp;gt; 0:                 # solange es noch Knoten im Heap gibt:&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;length, node, predecessor = heapq.heappop(q)&amp;lt;/font&amp;gt;   # Knoten aus dem Heap nehmen&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;if parents[node] is not None:&amp;lt;/font&amp;gt; # parent ist schon gesetzt =&amp;gt; es gab einen anderen, kürzeren Weg&lt;br /&gt;
                &amp;lt;font color=red&amp;gt;continue&amp;lt;/font&amp;gt;                  #   =&amp;gt; wir können diesen Weg ignorieren&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;parents[node] = predecessor&amp;lt;/font&amp;gt;   # parent setzen&lt;br /&gt;
            if node == destination:       # Zielknoten erreicht&lt;br /&gt;
                break                     #   =&amp;gt; Suche beenden&lt;br /&gt;
            for neighbor in graph[node]:  # die Nachbarn von node besuchen,&lt;br /&gt;
                if parents[neighbor] is None:   # aber nur, wenn ihr kürzester Weg noch nicht bekannt ist&lt;br /&gt;
                    &amp;lt;font color=red&amp;gt;newLength = length + weights[(node,neighbor)]&amp;lt;/font&amp;gt;   # berechne Pfadlänge zu neighbor              &lt;br /&gt;
                    &amp;lt;font color=red&amp;gt;heapq.heappush(q, (newLength, neighbor, node))&amp;lt;/font&amp;gt;  # und füge neighbor in den Heap ein&lt;br /&gt;
      &lt;br /&gt;
        if parents[destination] is None:  # Suche wurde beendet ohne den Zielknoten zu besuchen&lt;br /&gt;
            return None, None             # =&amp;gt; kein Pfad gefunden (unzusammenhängender Graph)&lt;br /&gt;
      &lt;br /&gt;
        # Pfad durch die parents-Kette zurückverfolgen und speichern&lt;br /&gt;
        path = [destination]&lt;br /&gt;
        while path[-1] != startnode:&lt;br /&gt;
            path.append(parents[path[-1]])&lt;br /&gt;
        path.reverse()                    # Reihenfolge umdrehen (Ziel =&amp;gt; Start wird zu Start =&amp;gt; Ziel)&lt;br /&gt;
        return path, length               # gefundenen Pfad und dessen Länge zurückgeben&lt;br /&gt;
  &amp;lt;/code&amp;gt;&lt;br /&gt;
Die wesentlichen Unterschiede zur Breitensuche sind im Code rot markiert: Anstelle der Queue verwenden wir jetzt einen Heap, und der Startknoten wird mit Pfadlänge 0 als erstes eingefügt. In der Schleife &amp;lt;tt&amp;gt;while len(q) &amp;gt; 0:&amp;lt;/tt&amp;gt; wird jeweils der Knoten &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; mit der aktuell kürzesten Pfadlänge aus dem Heap entfernt. Die Pfadlänge vom Start zu diesem Knoten wird in der Variable &amp;lt;tt&amp;gt;length&amp;lt;/tt&amp;gt; gespeichert, sein Vorgänger in der Variable &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;. Wenn der aktuelle Weg nicht der kürzeste ist (&amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; war bereits gesetzt), wird dieser Weg ignoriert. Andernfalls werden die property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; aktualisiert und die Nachbarn von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; besucht. Beim Scannen der Nachbarn berechnen wir zunächst die Länge &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; das Weges &amp;lt;tt&amp;gt;startnode =&amp;amp;gt; node =&amp;amp;gt; neighbor&amp;lt;/tt&amp;gt; als Summe von &amp;lt;tt&amp;gt;length&amp;lt;/tt&amp;gt; und dem Gewicht der Kante &amp;lt;tt&amp;gt;(node, neighbode)&amp;lt;/tt&amp;gt;. Diese Länge wird beim Einfügen des Nachbarknotens in den Heap zur Priorität des aktuellen Weges.&lt;br /&gt;
&lt;br /&gt;
Die wichtigsten Prinzipien des Dijkstra-Algorithmus noch einmal im Überblick:&lt;br /&gt;
* Der Dijkstra-Algorithmus ist Breitensuche mit Prioritätswarteschlange (Heap) statt einer einfache Warteschlange (Queue).&lt;br /&gt;
* Die Prioritätswarteschlange speichert alle Wege, die bereits gefunden worden sind und ordnet sie aufsteigend nach ihrer Länge. &lt;br /&gt;
* Das Sortieren (und damit der ganze Algorithmus) funktioniert nur mit positiven Kantengewichten korrekt.&lt;br /&gt;
* Da ein Knoten auf mehreren Wegen erreichbar sein kann, kann er auch mehrmals im Heap sein. &lt;br /&gt;
* Wenn ein Knoten &amp;lt;i&amp;gt;erstmals&amp;lt;/i&amp;gt; aus der Prioritätswarteschlange entnommen wird, ist der gefundene Weg der kürzeste zu diesem Knoten. Andernfalls wird der Weg ignoriert.&lt;br /&gt;
* Wenn der Knoten &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; aus dem Heap entnommen wird, ist der kürzeste Weg von Start nach Ziel gefunden, und die Suche kann beendet werden.&lt;br /&gt;
In unserer Implementation können, wie gesagt, mehrere Wege zum selben Knoten gleichzeitig in der Prioritätswarteschlange sein. Im Prinzip wäre es auch möglich, immer nur den besten zur Zeit bekannten Weg zu jedem Enknoten in der Prioritätswarteschlange zu halten - sobald ein besserer Kandidat gefunden wird, ersetzt er den bisherigen Kandidaten, anstatt zusätzlich eingefügt zu werden. Dies erfordert aber eine wesentlich kompliziertere Prioritätswarteschlange, die eine effiziente &amp;lt;tt&amp;gt;updatePriority&amp;lt;/tt&amp;gt;-Funktion anbietet, ohne dass dadurch eine signifikante Beschleunigung erreicht wird. Deshalb verfolgen wir diesen Ansatz nicht.&lt;br /&gt;
&lt;br /&gt;
==== Beispiel ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;under construction&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Image:Bsp.jpg]]&lt;br /&gt;
&lt;br /&gt;
==== Komplexität von Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
Zur Analyse der Komplexität nehmen wir an, dass der Graph V Knoten und E Kanten hat. Die Initialisierung der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; am Anfang der Funktion hat offensichtlich Komplexität O(V), weil Speicher für V Knoten allokiert wird. Der Code am Ende der Funktion, der aus der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; den Pfad extrahiert, hat ebenfalls die Komplexität O(V), weil der Pfad im ungünstigen Fall sämtliche Knoten des Graphen umfasst. Beides wird durch die Komplexität der Hauptschleife dominiert, zu deren Analyse wir den folgenden Codeausschnitt genauer anschauen wollen:&lt;br /&gt;
&lt;br /&gt;
      while len(q) &amp;gt; 0:&lt;br /&gt;
           ... # 1&lt;br /&gt;
           if parents[node] is not None: &lt;br /&gt;
               continue                  &lt;br /&gt;
           parents[node] = predecessor&lt;br /&gt;
           ... # 2&lt;br /&gt;
Wir erkennen, dass der Codeabschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; für jeden Knoten höchstens einmal erreicht werden kann: Da &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; beim ersten Durchlauf gesetzt wird, kann die &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage beim gleichen Knoten nie wieder &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt; liefern, und das nachfolgende &amp;lt;tt&amp;gt;continue&amp;lt;/tt&amp;gt; bewirkt, dass der Abschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; dann übersprungen wird. Man sagt auch, dass jeder Knoten &amp;lt;i&amp;gt;höchstens einmal expandiert&amp;lt;/i&amp;gt; wird, auch wenn er mehrmals im Heap war. &lt;br /&gt;
&lt;br /&gt;
Der Codeabschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; selbst enthält eine Schleife über alle ausgehenden Kanten des Knotens &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt;. Im ungünstigsten Fall iterieren wir bei &amp;lt;i&amp;gt;allen&amp;lt;/i&amp;gt; Knoten über &amp;lt;i&amp;gt;alle&amp;lt;/i&amp;gt; ausgehenden Kanten, aber das sind gerade alle Kanten des Graphen je einmal in den beiden möglichen Richtungen. Die Funktion &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; wird sogar höchstens E Mal aufgerufen, weil eine Kante nur in den Heap eingefügt wird, wenn der kürzeste Weg der jeweiligen Endknotens noch nicht bekannt ist (siehe die &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage in der &amp;lt;tt&amp;gt;for&amp;lt;/tt&amp;gt;-Schleife), und das ist nur ein einer Richtung möglich. Dies hat zwei Konsequenzen:&lt;br /&gt;
* Die Schleife &amp;lt;tt&amp;gt;while len(q) &amp;gt; 0:&amp;lt;/tt&amp;gt; wird nur so oft ausgeführt, wie Elemente im Heap sind, also höchstens E Mal. Das gleiche gilt für den Codeabschnitt &amp;lt;tt&amp;gt;# 1&amp;lt;/tt&amp;gt;, der das &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; enthält.&lt;br /&gt;
* Die Operationen &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; haben logarithmische Komplexität in der Größe des Heaps, sind also in &amp;lt;math&amp;gt;O(\log\,E)&amp;lt;/math&amp;gt;. In einfachen Graphen gilt aber &amp;lt;math&amp;gt;E = O(V^2)&amp;lt;/math&amp;gt;, so dass sich die Komplexität der Heapoperationen vereinfacht zu &amp;lt;math&amp;gt;O(\log\,E)=O(\log\,V^2)=O(2\log\,V)=O(\log\,V)&amp;lt;/math&amp;gt;.&lt;br /&gt;
Zusammenfassend gilt: &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; werden maximal E Mal aufgerufen und haben eine Komplexität in &amp;lt;math&amp;gt;O(\log\,V)&amp;lt;/math&amp;gt;. Folglich hat der Algorithmus von Dijkstra die Komplexität:&lt;br /&gt;
:&amp;lt;math&amp;gt;O(E\,\log\,V)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Vergleich mit Breitensuche und Tiefensuche ====&lt;br /&gt;
&lt;br /&gt;
Der Dijkstra-Algorithmus ist eng mit der Breiten- und Tiefensuche verwandt - man kann diese Algorithmen aus dem Dijkstra-Algorithmus gewinnen, indem man einfach die Regel zur Festlegung der Prioritäten ändert. Anstelle der Länge des Pfades verwenden wir als Priorität den Wert eine Zählvariable &amp;lt;tt&amp;gt;count&amp;lt;/tt&amp;gt;, die nach jeder Einfügung in den Heap (also nach jedem Aufruf von &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt;) aktualisiert wird. Zählen wir die Variable hoch, haben die zuerst eingefügten Kanten die höchste Priorität, der Heap verhält sich also wie eine Queue (First in-First out), und wir erhalten eine Breitensuche. Zählen wir die Variable hingegen (von E beginnend) herunter, haben die zuletzt eingefügten Kanten höchste Priorität. Der Heap verhält sich dann wie ein Stack (Last in-First out), und wir bekommen Tiefensuche. Statt eines Heaps plus Zählvariable kann man jetzt natürlich direkt eine Queue bzw. einen Stack verwenden. Dadurch fällt der Aufwand &amp;lt;math&amp;gt;O(\log\,V)&amp;lt;/math&amp;gt; für die Heapoperationen weg und wird durch die effizienten O(1)-Operationen von Queue bzw. Stack ersetzt. Damit erhalten wir für Breiten- und Tiefensuche die schon bekannte Komplexität O(E).&lt;br /&gt;
&lt;br /&gt;
==== Korrektheit von Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
Wir beweisen mittels vollständiger Induktion die Schleifen-Invariante: Falls &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; gesetzt (also ungleich &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;) ist, dann liefert das Zurückverfolgen des Weges von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; nach &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; den kürzesten Weg. &lt;br /&gt;
;Induktionsanfang: &amp;lt;tt&amp;gt;parents[startnode]&amp;lt;/tt&amp;gt; ist als einziges gesetzt. Zurückverfolgen liefert den trivialen Weg &amp;lt;tt&amp;gt;[startnode]&amp;lt;/tt&amp;gt;, der mit Länge 0 offensichtlich der kürzeste Pfad ist &amp;amp;rarr; die Bedingung ist erfüllt.&lt;br /&gt;
;Induktionsschritt: Wir zeigen mit einem indirektem Beweis, dass wir immer einen kürzesten Weg bekommen, wenn &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; gesetzt wird.&lt;br /&gt;
:Sei &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt; = &amp;lt;tt&amp;gt;{v | parents[v] is not None}&amp;lt;/tt&amp;gt; die Menge aller Knoten, von denen wir den kürzesten Weg schon kennen (Induktionsvoraussetzung), und &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; der Knoten, der sich gerade an der Spitze des Heaps befindet. Dann ist &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; der Vorgänger von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; im aktuellen Weg, und es muss &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt; gelten, weil die Nachbarn von &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; (und damit auch der aktuelle &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt;) erst in den Heap eingefügt werden, wenn der kürzeste Weg für &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; gefunden wurde. Man beachte auch, dass alle Knoten, die noch nicht in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt; enthalten sind, weiter vom Start entfernt sind als alle Knoten in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt;, weil alle neu in den Heap eingefügten Wege länger sind als der kürzeste Weg des jeweiligen Vorgängers. &lt;br /&gt;
:Der indirekte Beweis nimmt jetzt an, dass der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; nicht der kürzeste Weg ist. Dann muss es einen anderen, kürzeren Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; geben. Für den Vorgänger &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; in diesem Weg unterscheiden wir zwei Fälle:&lt;br /&gt;
:* &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt;: In diesem Fall ist die Länge des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; bereits bekannt, und dieser Weg ist in der Prioritätswarteschlange enthalten. Dann kann er aber nicht der kürzeste sein, denn an der Spitze der Warteschlange war nach Voraussetzung der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt;.&lt;br /&gt;
:* &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\notin S&amp;lt;/math&amp;gt;: Die Kosten des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; berechnen sich als &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) + weight[(x, node)]&amp;lt;/tt&amp;gt;, und die Kosten des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; sind analog &amp;lt;tt&amp;gt;Kosten(predecessor &amp;amp;rarr; startnode) + weight[(predecessor, node)]&amp;lt;/tt&amp;gt;. Aufgrund der Induktionsvoraussetzung gilt aber &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt;, und somit &amp;lt;tt&amp;gt;Kosten(predecessor &amp;amp;rarr; startnode) &amp;amp;lt; Kosten(x &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt;, weil &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; andernfalls vor &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; an der Spitze des Heaps gewesen wäre, was mit der Annahme &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\notin S&amp;lt;/math&amp;gt; unverträglich ist. Damit der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; trotzdem der kürzeste Weg sein kann, müsste &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) &amp;amp;lt; Kosten(node &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt; gelten, denn durch die Kante &amp;lt;tt&amp;gt;(x, node)&amp;lt;/tt&amp;gt; kommen ja noch Kosten hinzu. Das wäre aber nur möglich, wenn der Knoten &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; vor dem Knoten &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; an die Spitze des Heaps gelangt, im Widerspruch zur Annahme, dass &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; sich gerade an der Spitze des Heaps befindet. Somit kann die Behauptung, dass der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; der kürzeste Weg ist, nicht stimmen.&lt;br /&gt;
In beiden Fällen erhalten wir einen Widerspruch, und die Behauptung ist somit bewiesen. Da die Invariante insbesondere für den Weg zum Zielknoten &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; erfüllt ist, folgt daraus auch die Korrektheit des Algorithmus von Dijkstra.&lt;br /&gt;
&lt;br /&gt;
===  A*-Algorithmus - Wie kann man Dijkstra noch verbessern? ===&lt;br /&gt;
&lt;br /&gt;
Eine wichtige Eigenschaft des Dijkstra-Algorithmus ist, dass neben dem kürzesten Weg vom Start zum Ziel auch die kürzesten Wege zu allen Knoten berechnet werden, die näher am Startknoten liegen als das Ziel, obwohl uns diese Wege gar nicht interessieren. Sucht man beispielsweise in einem Graphen mit den Straßenverbindungen in Deutschland den kürzesten Weg von Frankfurt (Main) nach Dresden (ca. 460 km), werden auch die kürzesten Wege von Frankfurt nach Köln (190 km), Dortmund (220 km) und Stuttgart (210 km) und vielen anderen Städten gefunden. Aufgrund der geographischen Lage dieser Städte ist eigentlich von vornherein klar, dass sie mit dem kürzesten Weg nach Dresden nicht das geringste zu tun haben. Anders sieht es mit Erfurt (260 km) oder Suhl (210 km) aus - diese Städte liegen zwischen Frankfurt und Dresden und kommen deshalb als Zwischenstationen des gesuchten Weges in Frage.&lt;br /&gt;
&lt;br /&gt;
Damit Dijkstra korrekt funktioniert, würde es im Prinzip ausreichen, wenn man die kürzesten Wege nur für diejenigen Knoten ausrechnet, die auf dem kürzesten Weg vom Start zum Ziel liegen, denn nur diese Knoten braucht man, um den gesuchten Weg über die &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette zurückzuverfolgen. Das Problem ist nur, dass man diese Knoten erst kennt, wenn der Algorithmus fertig durchgelaufen ist. Schließt man Knoten zu früh von der Betrachtung aus, kommt am Ende möglicherweise nicht der korrekte kürzeste Weg heraus. &lt;br /&gt;
&lt;br /&gt;
Der A*-Algorithmus löst dieses Dilemma mit folgender Idee: Ändere die Prioritäten für den Heap so ab, dass unwichtige Knoten nur mit geringerer Wahscheinlichkeit expandiert werden, aber stelle gleichzeitig sicher, dass alle wichtigen Knoten (also diejenigen auf dem korrekten kürzesten Weg) auf jeden Fall expandiert werden. Es zeigt sich, dass man diese Idee umsetzen kann, wenn eine &amp;lt;i&amp;gt;Schätzung für den Restweg&amp;lt;/i&amp;gt; (also für die noch verbleibende Entfernung von jedem Knoten zum Ziel) verfügbar ist:&lt;br /&gt;
 rest = guess(neighbor, destination)&lt;br /&gt;
Diese Schätzung addiert man einfach zur wahren Länge des Weges &amp;lt;tt&amp;gt;startnode &amp;amp;rarr; node&amp;lt;/tt&amp;gt; dazu, um die verbesserte Priorität zu erhalten:&lt;br /&gt;
 priority = newLength + guess(neighbor, destination)&lt;br /&gt;
(Im originalen Dijkstra-Algorithmus wird als Priorität nur &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; allein verwendet. Man beachte, dass man &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; jetzt zusätzlich im Heap speichern muss, weil man es für die Expansion des Knotens später noch benötigt.)&lt;br /&gt;
&lt;br /&gt;
Damit sicher gestellt ist, dass der A*-Algorithmus immer noch die korrekten kürzesten Wege findet, darf die Schätzung den wahren Restweg &amp;lt;i&amp;gt;niemals überschätzen&amp;lt;/i&amp;gt;. Es muss immer gelten:&lt;br /&gt;
 0 &amp;lt;= guess(node, destination) &amp;lt;= trueDistance(node, destination)&lt;br /&gt;
Damit gilt insbesondere &amp;lt;tt&amp;gt;guess(destination, destination) = trueDistance(destination, destination) = 0&amp;lt;/tt&amp;gt;, an der Priorität des Knotens &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; ändert sich also nichts. Die Prioritäten aller anderen Knoten veschlechtern sich hingegen, weil zur bisherigen Priorität noch atwas addiert wird. Für die wichtigen Knoten auf dem kürzesten Weg vom Start nach Ziel gilt jedoch, dass deren neue Priorität immer noch besser ist als die Priorität des Zielknotens selbst. Für diese Knoten gilt nämlich&lt;br /&gt;
 falls node auf dem kürzesten Weg von startnode nach destination liegt:&lt;br /&gt;
 trueDistance(startnode, node) + guess(node, destination) &amp;lt;= trueDistance(startnode, destination)&lt;br /&gt;
weil der Weg von Start nach &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; ein Teil des kürzesten Wegs von Start nach Ziel ist und die Restschätzung die wahre Entfernung immer unterschätzt. Diese Knoten werden deshalb stets vor dem Zielknoten expandiert, so dass wir die &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette immer noch korrekt zurückverfolgen können. Für alle anderen Knoten gilt idealerweise, dass die neue Priorität schlechter ist als die Priorität von &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt;, so dass man sich diese irrelevanten Knotenexpansionen sparen kann.&lt;br /&gt;
&lt;br /&gt;
Für das Beispiel eines Straßennetzwerks bietet sich als Schätzung die Luftlinienentfernung an, weil Straßen nie kürzer sein können als die Luftlinie. Damit erreicht man in der Praxis deutliche Einsparungen. Generell gilt, dass der A*-Algorithmus im typischen Fall schneller ist als der Algorithmus von Dijkstra, aber man kann immer pathologische Fälle konstruieren, wo die Änderung der Prioritäten nichts bringt. Die Komplexität des A*-Algorithmus im ungünstigen Fall ist deshalb nach wie vor &amp;lt;math&amp;gt;O(E\,\log\,V)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=='''Minimaler Spannbaum'''==&lt;br /&gt;
'''(engl.: minimum spanning tree; abgekürzt: MST)'''&lt;br /&gt;
&lt;br /&gt;
[[Image:Minimum_spanning_tree.png‎ |thumb|200px|right|Ein minimal aufspannender Baum verbindet alle Punkte eines Graphen bei minimaler Kantenlänge ([http://de.wikipedia.org/wiki/Spannbaum Quelle])]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: gewichteter Graph G, zusammenhängend&amp;lt;br/&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: Untermenge &amp;lt;math&amp;gt;E'\subseteq E&amp;lt;/math&amp;gt; der Kanten, so dass die Summe der Kantengewichte &amp;lt;math&amp;gt;\sum_{e\in E'} w_e&amp;lt;/math&amp;gt; minimal und der entstehende Graph G' zusammenhängend ist.&amp;lt;br/&amp;gt;&lt;br /&gt;
* G' definiert immer einen Baum, denn andernfalls könnte man eine Kante weglassen und dadurch die Summe &amp;lt;math&amp;gt;\sum_{e\in E'} w_e&amp;lt;/math&amp;gt; verringern, ohne dass sich am Zusammenhang von G' etwas ändert. &amp;lt;br/&amp;gt;&lt;br /&gt;
* Wenn der Graph G nicht zusammenhängend ist, kann man den Spannbaum für jede Zusammenhangskomponente getrennt ausrechnen. Man erhält dann einen aufspannenden Wald. &lt;br /&gt;
* Der MST ist ähnlich wie der Dijkstra-Algorithmus: Dort ist ein Pfad gesucht, bei dem die Summe der Gewichte über den Pfad minimal ist. Beim MST suchen wir eine Lösung, bei der die Summe der Gewichte über den ganzen Graphen minimal ist. &lt;br /&gt;
* Das Problem des MST ist nahe verwandt mit der Bestimmung der Zusammenhangskomponente, z.B. über den Tiefensuchbaum. Für die Zusammenhangskomponenten genügt allerdings ein beliebiger Baum, während beim MST ein minimaler Baum gesucht ist.&lt;br /&gt;
&lt;br /&gt;
=== Anwendungen ===&lt;br /&gt;
==== Wie verbindet man n gegebene Punkte mit möglichst kurzen Straßen (Eisenbahnen, Drähten [bei Schaltungen] usw.)?====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:center&amp;quot; border=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; cellspacing=&amp;quot;0&amp;quot; &lt;br /&gt;
|MST minimale Verbindung (Abb.1)&lt;br /&gt;
|MST = 2 (Länge = Kantengewicht)(Abb.2)&lt;br /&gt;
|- valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| [[Image:mst.png]] &lt;br /&gt;
| [[Image:Gleichseitigesdreieck.png]]&lt;br /&gt;
|}&lt;br /&gt;
*In der Praxis: Die Festlegung, dass man nur die gegebenen Punkte verwenden darf, ist eine ziemliche starke Einschränkung. &lt;br /&gt;
&lt;br /&gt;
* Wenn man sich vorstellt, es sind drei Punkte gegeben, die als gleichseitiges Dreieck angeordnet sind, dann ist der MST (siehe Abb.2, schwarz gezeichnet) und hat die Länge 2. Man kann hier die Länge als Kantengewicht verwenden. &lt;br /&gt;
&lt;br /&gt;
* Wenn es erlaubt ist zusätzliche Punkte einzufügen, dann kann man in der Mitte einen neuen Punkt setzen &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; neuer MST (siehe Abb.2, orange gezeichnet).&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Höhe = &amp;lt;math&amp;gt;\frac{1}{2}\sqrt{3}&amp;lt;/math&amp;gt;, Schwerpunkt: teilt die Höhe des Dreiecks im Verhältnis 2:1; der Abstand von obersten Punkt bis zum neu eingeführten Punkt: &amp;lt;math&amp;gt;\frac{2}{3}h = \frac{\sqrt{3}}{3}&amp;lt;/math&amp;gt;, davon insgesamt 3 Stück, damit (gilt für den MST in orange eingezeichnet): MST = &amp;lt;math&amp;gt;3\left(\frac{1}{3}\right) \sqrt{3} = \sqrt{3} \approx 1,7&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Damit ist der MST in orange kürzer als der schwarz gezeichnete MST. &amp;lt;br\&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt;Folgerung: MST kann kürzer werden, wenn man einen Punkt dazu nimmt. &lt;br /&gt;
* Umgekehrt kann der MST auch kürzer werden, wenn man einen Punkt aus dem Graphen entfernt, aber wie das Beipiel des gleichseitigen Dreiecks zeigt, ist dies nicht immer der Fall.&lt;br /&gt;
&lt;br /&gt;
[[Image: bahn.png|Bahnstrecke Verbindung (Abb.3)]]&lt;br /&gt;
&lt;br /&gt;
* Methode der zusätzlichen Punkteinfügung hat man früher beim Bahnstreckenbau verwendet. Durch Einführung eines Knotenpunktes kann die Streckenlänge verkürzt werden (Dreiecksungleichung).&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Bestimmung von Datenclustern ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Image:cluster.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Daten (in der Abb.: Punkte) bilden Gruppen. &lt;br /&gt;
&lt;br /&gt;
* In der Abbildung hat man 2 verschiedene Messungen gemacht (als x- und y-Achse aufgetragen), bspw. Größe und Gewicht von Personen. Für jede Person i wird ein Punkt an der Koordinate (Größe&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;, Gewicht&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;) gezeichnet (siehe Bild a). Dies bezeichnet man als ''Scatter Plot''. Wenn bestimmte Wertkombinationen häufiger auftreten als andere, bilden sich mitunter Gruppen aus, bspw. eine Gruppe für &amp;quot;klein und schwer&amp;quot; etc.&lt;br /&gt;
&lt;br /&gt;
* Durch Verbinden der Punkte mittels eines MST (siehe Abbildung (b)) sieht man, dass es kurze (innerhalb der Gruppen) und lange Kanten (zwischen den Gruppen) gibt. &lt;br /&gt;
&lt;br /&gt;
* Wenn man geschickt eine Schwelle einführt und alle Kanten löscht, die länger sind als die Schwelle, dann bekommt man als Zusammenhangskomponente die einzelnen Gruppen. &lt;br /&gt;
&lt;br /&gt;
=== Algorithmen ===&lt;br /&gt;
&lt;br /&gt;
Genau wie bei der Bestimmung von Zusammenhangskomponenten kann man auch das MST-Problem entweder nach dem Anlagerungsprinzip oder nach dem Verschmelzungsprinzip lösen (dazu gibt es noch weitere Möglichkeiten, z.B. den [http://de.wikipedia.org/wiki/Algorithmus_von_Bor%C5%AFvka Algorithmus von Boruvka]). Der Anlagerungsalgorithmus für MST wurde zuerst von Prim beschrieben und trägt deshalb seinen Namen, der Verschmelzungsalgorithmus stammt von Kruskal. Im Vergleich zu den Algorithmen für Zusammenhangskomponenten ändert sich im wesentlichen nur die Reihenfolge, in der die Kanten betrachtet werden: Eine Prioritätswarteschlange stellt jetzt sicher, dass am Ende wirklich der Baum mit den geringstmöglichen Kosten herauskommt.&lt;br /&gt;
&lt;br /&gt;
====Algorithmus von Prim====&lt;br /&gt;
[http://de.wikipedia.org/wiki/Algorithmus_von_Prim Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus von Prim geht nach dem Anlagerungsprinzip vor (vgl. den Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Tiefensuche|Zusammenhangskomponenten mit Tiefensuche]]): Starte an der Wurzel (ein willkürlich gewählter Knoten) und füge jeweils die günstigste Kante an die aktuellen Teillösung an, die keinen Zyklus verursacht. Die Sortierung der Kanten nach Priorität erfolgt analog zum Dijsktra-Algorithmus, aber die Definitionen, welche Kante die günstigste ist, unterscheiden sich. Die Konvention für die Bedeutung der Elemente des Heaps ist ebenfalls identisch: ein Tupel mit &amp;lt;tt&amp;gt;(priority, node, predecessor)&amp;lt;/tt&amp;gt;. Die folgende Implementation verdeutlicht sehr schön die Ähnlichkeit der beiden Algorithmen. Das Ergebnis wird als property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; zurückgegeben, in der für jeden Knoten sein Vorgänger im MST steht, wobei die Wurzel wie üblich auf sich selbst verweist.&lt;br /&gt;
&lt;br /&gt;
 import heapq&lt;br /&gt;
 &lt;br /&gt;
 def prim(graph, weights):                # Kantengewichte wie bei Dijkstra als property map&lt;br /&gt;
 	sum = 0.0                         # wird später das Gewicht des Spannbaums sein&lt;br /&gt;
 	start = 0                         # Knoten 0 wird willkürlich als Wurzel gewählt&lt;br /&gt;
        &lt;br /&gt;
        parents = [None]*len(graph)       # property map, die den resultierenden Baum kodiert&lt;br /&gt;
        parents[start] = start            # Wurzel zeigt auf sich selbst&lt;br /&gt;
        &lt;br /&gt;
 	heap = []                         # Heap für die Kanten des Graphen&lt;br /&gt;
 	for neighbor in graph[start]:     # besuche die Nachbarn von start&lt;br /&gt;
 	    heapq.heappush(heap, (weights[(start, neighbor)], neighbor, start))  # und fülle Heap &lt;br /&gt;
 	&lt;br /&gt;
        while len(heap) &amp;gt; 0:&lt;br /&gt;
 	    w, node, predecessor = heapq.heappop(heap) # hole billigste Kante aus dem Heap&lt;br /&gt;
            if parents[node] is not None: # die Kante würde einen Zyklus verursachen&lt;br /&gt;
                continue                  #   =&amp;gt; ignoriere diese Kante&lt;br /&gt;
            parents[node] = predecessor   # füge Kante in den MST ein&lt;br /&gt;
            sum += w                      # und aktualisiere das Gesamtgewicht &lt;br /&gt;
            for neighbor in graph[node]:  # besuche die Nachbarn von node&lt;br /&gt;
                if parents[neighbor] is None:  # aber nur, wenn kein Zyklus entsteht&lt;br /&gt;
                    heapq.heappush(heap, (weights[(node,neighbor)], neighbor, node)) # füge Kandidaten in Heap ein&lt;br /&gt;
        &lt;br /&gt;
 	return parents, sum               # MST und Gesamtgewicht zurückgeben&lt;br /&gt;
&lt;br /&gt;
====Algorithmus von Kruskal====&lt;br /&gt;
[http://de.wikipedia.org/wiki/Algorithmus_von_Kruskal Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Kruskal%27s_algorithm (en)]&lt;br /&gt;
&lt;br /&gt;
Die alternative Vorgehensweise ist das Verschmelzungsprinzip (vgl. den Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Union-Find-Algorithmus|Zusammenhangskomponenten mit Union-Find-Algorithmus]]), das der Algorithmus von Kruskal verwendet. Jeder Knoten wird zunächst als trivialer Baum mit nur einem Knoten betrachtet, und alle Kanten werden aufsteigend nach Gewicht sortiert. Dann wird die billigste noch nicht betrachtete Kante in den MST eingefügt, falls sich dadurch kein Zyklus bildet (erkennbar daran, dass die Endknoten in verschiedenen Zusammenhangskomponenten liegen, das heisst verschiedene Anker haben). Da der fertige Baum (V-1) Kanten haben muss, wird dies (V-1) Mal zutreffen. Andernfalls wird diese Kante ignoriert. Anders ausgedrückt: Der Algorithmus beginnt mit ''V'' Bäumen; in (''V''-1) Verschmelzungsschritten kombiniert er jeweils zwei Bäume (unter Verwendung der kürzesten möglichen Kante), bis nur noch ein Baum übrig bleibt. Der einzige Unterschied zum einfachen Union-Find besteht darin, dass die Kanten in aufsteigender Reihenfolge betrachtet werden müssen, was wir hier durch eine Prioritätswarteschlange realisieren. Der Algorithmus von J.Kruskal ist seit 1956 bekannt. &lt;br /&gt;
&lt;br /&gt;
 def kruskal(graph, weights):&lt;br /&gt;
     anchors = range(len(graph))           # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     results = []                          # result wird später die Kanten des MST enthalten    &lt;br /&gt;
     &lt;br /&gt;
     heap = []                             # Heap zum Sortieren der Kanten nach Gewicht&lt;br /&gt;
     for edge, w in weights.iteritems():   # alle Kanten einfügen&lt;br /&gt;
         heapq.heappush(heap, (w, edge))&lt;br /&gt;
     &lt;br /&gt;
     while len(heap) &amp;gt; 0:                  # solange noch Kanten vorhanden sind&lt;br /&gt;
         w, edge = heapq.heappop(heap)     # billigste Kante aus dem Heap nehmen&lt;br /&gt;
         a1 = findAnchor(anchors, edge[0]) # Anker von Startknoten der Kante&lt;br /&gt;
         a2 = findAnchor(anchors, edge[1]) # ... und Endknoten bestimmen&lt;br /&gt;
         if a1 != a2:                      # wenn die Knoten in verschiedenen Komponenten sind&lt;br /&gt;
             anchors[a2] = a1              # Komponenten verschmelzen&lt;br /&gt;
             result.append(edge)           # ... und Kante in MST einfügen&lt;br /&gt;
     &lt;br /&gt;
     return result                         # Kanten des MST zurückgeben&lt;br /&gt;
&lt;br /&gt;
Die Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt; wurde im Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Union-Find-Algorithmus|Zusammenhangskomponenten mit Union-Find-Algorithmus]] implementiert. Im Unterschied zum Algorithmus von Prim geben wir hier nicht die property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; zurück, sondern einfach eine Liste der Kanten im MST.&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus eignet sich insbesondere für das Clusteringproblem, da der Schwellwert von vornerein als maximales Kantengewicht an den Algorithmus übergeben werden kann. Man hört mit dem Vereinigen auf, wenn das Gewicht der billigste Kante im Heap den Schwellwert überschreitet. Beim Algorithmus von Kruskal kann dann keine bessere Kante als der Schwellwert mehr kommen, da die Kanten vorher sortiert worden sind. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Komplexität:&amp;lt;/b&amp;gt; wie beim Dijkstra-Algorithmus, weil jede Kante genau einmal in den Heap kommt. Der Aufwand für das Sortieren ist somit &amp;lt;math&amp;gt;O\left(E\log E\right)&amp;lt;/math&amp;gt;, was sich zu &amp;lt;math&amp;gt;O \left(E\,\log\,V\right)&amp;lt;/math&amp;gt; reduziert, falls keine Mehrfachkanten vorhanden sind.&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; geeignet für Übungsaufgabe&lt;br /&gt;
&lt;br /&gt;
====Verwendung einer BucketPriorityQueue====&lt;br /&gt;
&lt;br /&gt;
Beide Algorithmen zur Bestimmung des minimalen Spannbaums benötigen eine Prioritätswarteschlange. Wenn die Kantengewichte ganze Zahlen im Bereich &amp;lt;tt&amp;gt;0...(m-1)&amp;lt;/tt&amp;gt; sind, kann man die MST-Algorithmen deutlich beschleunigen, wenn man anstelle des Heaps eine [[Prioritätswarteschlangen#Prioritätssuche mit dem Bucket-Prinzip|&amp;lt;tt&amp;gt;BucketPriorityQueue&amp;lt;/tt&amp;gt;]] verwendet. Die Operationen zum Einfügen einer Kante in die Queue und zum Entfernen der billibsten Kante aus der Queue beschleunigen sich dadurch auf O(1) statt O(log V) (außer wenn die Gewichte sehr ungünstig auf die Kanten verteilt sind). In der Praxis erreicht man durch diese Änderung typischerweise deutliche Verbesserungen. In der Bildverarbeitung können die Prioritäten beispielsweise die Wahrscheinlichkeit kodieren, dass zwei benachbarte Pixel zu verschiedenen Objekten gehören. Bildet man jetzt den MST, und bricht bei einer bestimmten Wahrscheinlichkeit ab, erhält man Cluster von Pixeln, die wahrscheinlich zum selben Objekt gehören (weil der MST ja die Kanten mit minimalem Gewicht bevorzugt, und kleine Gewichte bedeuten kleine Wahrscheinlichkeit, dass benachbarte Pixel von einander getrennt werden). Da man die Wahrscheinlichkeiten nur mit einer Genauigkeit von ca. 1% berechnen kann, reichen hiefür 100 bis 200 Quantisierungstufen aus. Durch Verwendung der schnellen &amp;lt;tt&amp;gt;BucketPriorityQueue&amp;lt;/tt&amp;gt; kann man jetzt wesentlich größere Bilder in akzeptabler Zeit bearbeiten als dies mit einem Heap möglich wäre.&lt;br /&gt;
&lt;br /&gt;
== Algorithmen für gerichtete Graphen ==&lt;br /&gt;
&lt;br /&gt;
Zur Erinnerung: in einem gerichteten Graphen sind die Kanten (i &amp;amp;rarr; j) und (j &amp;amp;rarr; i) voneinander verschieden, und eventuell existiert nur eine der beiden Richtungen. Im allgemeinen unterscheidet sich der [[Graphen_und_Graphenalgorithmen#transposed_graph|transponierte Graph]] G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; also vom Originalgraphen G. Beim Traversieren des Graphen und bei der Pfadsuche dürfen Kanten nur in passender Richtung verwendet werden. Bei gewichteten Graphen tritt häufig der Fall auf, dass zwar Kanten in beiden Richtungen existieren, diese aber unterschiedliche Gewichte haben.&lt;br /&gt;
&lt;br /&gt;
Gerichtete Graphen ergeben sich in natürlicher Weise aus vielen Anwendungsproblemen:&lt;br /&gt;
* Routenplanung&lt;br /&gt;
** Bei Straßennetzwerken enstehen gerichtete Graphen, sobald es Einbahnstraßen gibt.&lt;br /&gt;
** Verwendet man Gewichte, um die erwarteten Fahrzeiten entlang einer Straße zu kodieren, gibt es Asymmetrien z.B. dann, wenn Straßen in einer Richtung bergab, in der anderen bergauf befahren werden. Hier existieren zwar Kanten in beiden Richtungen, sie haben aber unterschiedliche Gewichte. Ähnliches gilt für Flüge: Durch den Gegenwind des Jetstreams braucht man von Frankfurt nach New York länger als umgekehrt von New York nach Frankfurt.&lt;br /&gt;
* zeitliche oder kausale Abhängigkeiten&lt;br /&gt;
** Wenn die Knoten Ereignisse repräsentieren, von denen einige die Ursache von anderen sind, diese wiederum die Ursache der nächsten usw., verbindet man die Knoten zweckmäßig durch gerichtete Kanten, die die Kausalitätsbeziehungen kodieren. Handelt es sich um logische &amp;quot;wenn-dann&amp;quot;-Regeln, erhält man einen [[Graphen_und_Graphenalgorithmen#Anwendung:_Das_Erf.C3.BCllbarkeitsproblem_in_Implikationengraphen|Implikationengraph]] (siehe unten). Handelt es sich hingegen um Wahrscheinlichkeitsaussagen (&amp;quot;Wenn das Wetter schön ist, haben Studenten tendenziell gute Laune, wenn eine Prüfung bevorsteht eher schlechte usw.&amp;quot;), erhält man ein [http://de.wikipedia.org/wiki/Bayessches_Netz Bayessches Netz].&lt;br /&gt;
** Wenn bestimmte Aufgaben erst begonnen werden können, nachdem andere Aufgaben erledigt sind, erhält man einen Abhängigkeitsgraphen. Beispielsweise dürfen Sie erst an der Klausur teilnehmen, nachdem Sie die Übungsaufgaben gelöst haben, und Sie dürfen erst die Abschlussarbeit beginnen, nachdem Sie bestimmte Prüfungen bestanden haben. Ein anderes schönes Beispiel liefern die Regeln für das [[Graphen_und_Graphenalgorithmen#Anwendung:_Abh.C3.A4ngigkeitsgraph|Ankleiden]] weiter unten.&lt;br /&gt;
** Gerichtete Graphen kodieren die Abhängigkeiten zwischen Programmbibliotheken. Beispielsweise benötigt das Pythonmodul &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; die internen Submodule &amp;lt;tt&amp;gt;json.encoder&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;json.decode&amp;lt;/tt&amp;gt; sowie das externe Modul &amp;lt;tt&amp;gt;decimal&amp;lt;/tt&amp;gt;. Die Submodule benötigen wiederum die externen Module &amp;lt;tt&amp;gt;re&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;sys&amp;lt;/tt&amp;gt;, das Modul &amp;lt;tt&amp;gt;decimal&amp;lt;/tt&amp;gt; braucht &amp;lt;tt&amp;gt;copy&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;collections&amp;lt;/tt&amp;gt; usw.&lt;br /&gt;
** Das Internet kann als gerichteter Graph dargestellt werden, wobei die Webseiten die Knoten, und die Hyperlinks die Kanten sind.&lt;br /&gt;
* Sequence Alignment&lt;br /&gt;
** Eine gute Rechtschreibprüfung markiert nicht nur fehlerhafte Wörter, sondern macht auch plausible Vorschläge, was eigentlich gemeint gewesen sein könnte. Dazu muss sie das gegebene Wort mit den Wörtern eines Wörterbuchs vergleichen und die Ähnlichkeit bewerten. Ein analoges Problem ergibt sich, wenn man DNA Fragmente mit der Information in einer Genomdatenbank abgleichen will. &lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Sequence Alignment / Edit Distance ===&lt;br /&gt;
&lt;br /&gt;
:gegeben: zwei Wörter (allgemein: beliebige Zeichenfolgen)&lt;br /&gt;
:gesucht: Wie kann man die Buchstaben am besten in Übereinstimmung bringen?&lt;br /&gt;
&lt;br /&gt;
:Beispiel: WORTE – NORDEN&lt;br /&gt;
&lt;br /&gt;
Zwei mögliche Alignments sind&lt;br /&gt;
&lt;br /&gt;
  W&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;OR&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;T&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;E&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;.          W.ORTE&lt;br /&gt;
  N&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;OR&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;D&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;E&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;N          NORDEN&lt;br /&gt;
&lt;br /&gt;
wobei der Punkt anzeigt, dass der untere Buchstabe keinen Partner hat, und rote Buchstaben oben und unten übereinstimmen. Jede Nicht-Übereinstimmung verursacht nun gewisse Kosten. Dabei unterscheiden wir zwei Fälle:&lt;br /&gt;
# Matche a[i] mit b[j]. Falls a[i] == b[j], ist das gut (rote Buchstaben), und es entstehen keine Kosten. Andernfalls entstehen Kosten U (schwarze Buchstaben).&lt;br /&gt;
# Wir überspringen a[i] oder b[j] (Buchstabe vs. Punkt). Dann entstehen Kosten V. (Manchmal unterscheidet man auch noch Kosten Va und Vb, wenn das Überspringen bei a und b unterschieldiche Signifikanz hat.)&lt;br /&gt;
&lt;br /&gt;
Gesucht ist nun das &amp;lt;b&amp;gt;Alignment mit minimalen Kosten&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Diese Aufgabe kann man sehr schön als gerichteten Graphen darstellen: Wir definieren ein rechteckiges Gitter und schreiben das erste Wort über das Gitter und das andere links davon. Die Gitterpunkte verbinden wir mit Pfeilen (gerichteten Kanten), wobei ein Pfeil nach rechts bedeutet, dass wir beim oberen Wort einen Buchstaben überspringen, ein Pfeil nach unten, dass wir beim linken Wort einen Buchstaben überspringen, und ein diagonaler Pfeil, dass wir zwei Buchstaben matchen (und zwar die am Pfeilende). Die Farben der Pfeile symbolisieren die Kosten: rot für das Überspringen eines Buchstabens (Kosten V), blau für das Matchen, wenn die Buchstaben nicht übereinstimmen (Kosten U), und grün, wenn die Buchstaben übereinstimmen (keine Kosten). &lt;br /&gt;
&lt;br /&gt;
[[Image:sequence-alignment.png|300px]]&lt;br /&gt;
&lt;br /&gt;
Lösung:&lt;br /&gt;
:Suche den kürzesten Pfad vom Knoten &amp;quot;START&amp;quot; (oben links) nach unten rechts. Dazu kann der [[Graphen und Graphenalgorithmen#Algorithmus von Dijkstra|Algorithmus von Dijkstra]] verwendet werden, der auf gerichteten Graphen genauso funktioniert wie auf ungerichteten.&lt;br /&gt;
&lt;br /&gt;
Für unser Beispiel von oben erhalten wir die folgenden Pfade:&lt;br /&gt;
&lt;br /&gt;
[[Image:sequence-alignment-weg1.png|400px]]&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;[[Image:sequence-alignment-weg2.png|400px]]&lt;br /&gt;
&lt;br /&gt;
Durch Addieren der Kosten entsprechend der Farben sieht man, dass der erste Weg die Kosten 2U+V und der zweite die Kosten 5U+V hat. Der erste Weg ist offensichtlich günstiger und entspricht dem besten Alignment.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Abhängigkeitsgraph ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Beispiel: &amp;lt;/b&amp;gt; Wie erklärt man einem zerstreuten Professor, wie er sich morgens anziehen soll? Der folgende Graph enthält einen Knoten für jede Aktion, und eine Kante (i &amp;amp;rarr; j) bedeutet, dass die Aktion i vor der Aktion j abgeschlossen werden muss.&lt;br /&gt;
&lt;br /&gt;
[[Image:anziehen-graph.png|600px]]&lt;br /&gt;
&lt;br /&gt;
In derartigen Abhängigkeitsgraphen ist die wichtigste Frage immer, ob der Graph azyklisch ist. Wäre dies nämlich nicht der Fall, kann es keine Reihenfolge der Aktionen geben, die alle Abhängigkeiten erfüllt. Dies sieht man leicht, wenn man den einfachsten möglichen Zyklus betrachtet: es gibt sowohl eine Kante (i &amp;amp;rarr; j) als auch eine (j &amp;amp;rarr; i). Dann müsste man i vor j erledigen, aber ebenso j vor i, was offensichtlich unmöglich ist - das im Graph kodierte Problem ist dann unlösbar. Wegen ihrer Wichtigkeit wird für gerichtete azyklische Graphen oft die Abkürzung &amp;lt;b&amp;gt;DAG&amp;lt;/b&amp;gt; (von &amp;lt;i&amp;gt;directed acyclic graph&amp;lt;/i&amp;gt;) verwendet. Ein Graph ist genau dann ein DAG, wenn es eine topologische Sortierung gibt:&lt;br /&gt;
;topologische Sortierung: Zeichne die Knoten so auf eine Gerade, dass alle Kanten (Pfeile) nach rechts zeigen. &lt;br /&gt;
Arbeitet man die Aktionen nach einer (beliebigen) topologischen Sortierung ab, werden automatisch alle Abhängigkeiten eingehalten: Da alle Pfeile nach rechts zeigen, werden abhängige Aktionen immer später ausgeführt. Die topologische Sortierung ist im allgemeinen nicht eindeutig. Die folgende Skizze zeigt eine mögliche topologische Sortierung für das Anziehen:&lt;br /&gt;
&lt;br /&gt;
[[Image:anziehen-topologische-sortierung.png|600px]]&lt;br /&gt;
&lt;br /&gt;
Eine solche fest vorgegebene Reihenfolge ist für den zerstreuten Professor sicherlich eine größere Hilfe als der ursprüngliche Graph. Man erkennt, dass die Sortierung nicht eindeutig ist, beispielsweise bei der Uhr: Da für die Uhr keine Abhängigkeiten definiert sind, kann man diese Aktion an beliebiger Stelle einsortieren. Hier wurde willkürlich die letzte Stelle gewählt.&lt;br /&gt;
&lt;br /&gt;
==== Zwei Algorithmen zum Finden der topologischen Sortierung ====&lt;br /&gt;
&lt;br /&gt;
Die folgenden Algorithmen finden entweder eine topologische Sortierung, oder signalisieren, dass der Graph zyklisch ist.&lt;br /&gt;
&lt;br /&gt;
===== Algorithmus 1 =====&lt;br /&gt;
# Suche einen Knoten mit Eingangsgrad 0 (ohne eingehende Pfeile) =&amp;gt; in einem gerichteten azyklischen Graphen gibt es immer einen solchen Knoten&lt;br /&gt;
# Platziere diesen Knoten auf der Geraden (beliebig)&lt;br /&gt;
# Entferne den Knoten aus dem Graphen zusammen mit den ausgehenden Kanten&lt;br /&gt;
# Gehe zu 1., aber platziere in 2. immer rechts der Knoten, die schon auf der Geraden vorhanden sind.&lt;br /&gt;
: =&amp;gt; Wenn noch Knoten übrig sind, aber keiner Eingangsgrad 0 hat, muss der Graph zyklisch sein.&lt;br /&gt;
&lt;br /&gt;
[[Image:bild6.JPG]]&lt;br /&gt;
&lt;br /&gt;
Beispiel für einen zyklischen Graphen: kein Knoten hat Eingangsgrad 0.&lt;br /&gt;
&lt;br /&gt;
Um den Algorithmus zu implementieren, verwenden wir eine property map &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt;, die wir in einem ersten Durchlauf durch den Graphen füllen und die dann für jeden Knoten die Anzahl der eingehenden Kanten speichert. Dann gehen wir sukzessive zu allen Knoten mit &amp;lt;tt&amp;gt;in_degree == 0&amp;lt;/tt&amp;gt;. Anstatt sie aber tatsächlich aus dem Graphen zu entfernen wie im obigen Pseudocode, dekrementieren wir nur den &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt; ihrer Nachbarn. Wird der &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt; eines Nachbarn dadurch 0, wird er ebenfalls in das Array der zu scannenden Knoten aufgenommen. Wenn der Graph azyklisch ist, enthält das Array am Ende alle Knoten des Graphen, und die Reihenfolge der Einfügungen definiert eine topologische Sortierung. Andernfalls ist das Array zu kurz, und wir signalisieren durch Zurückgeben von &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, dass der Graph zyklisch ist:&lt;br /&gt;
&lt;br /&gt;
 def topological_sort(graph):              # ein gerichteter Graph&lt;br /&gt;
     in_degree = [0]*len(graph)            # property map für den Eingangsgrad jeden Knotens&lt;br /&gt;
     for node in xrange(len(graph)):       # besuche alle Knoten&lt;br /&gt;
         for neighbor in graph[node]:      #  ... und deren Nachbarn&lt;br /&gt;
             in_degree[neighbor] += 1      #  ... und inkrementiere den Eingangsgrad&lt;br /&gt;
     &lt;br /&gt;
     result = []                           # wird später die topologische Sortierung enthalten&lt;br /&gt;
     for node in xrange(len(graph)):&lt;br /&gt;
         if in_degree[node] == 0:&lt;br /&gt;
             result.append(node)           # füge alle Knoten mit Eingangsgrad 0 in result ein&lt;br /&gt;
     &lt;br /&gt;
     k = 0&lt;br /&gt;
     while k &amp;lt; len(result):                # besuche alle Knoten mit Eingangsgrad 0&lt;br /&gt;
         node = result[k]&lt;br /&gt;
         k += 1&lt;br /&gt;
         for neighbor in graph[node]:      # besuche alle Nachbarn&lt;br /&gt;
             in_degree[neighbor] -= 1      # entferne 'virtuell' die eingehende Kante&lt;br /&gt;
             if in_degree[neighbor] == 0:  # wenn neighbor jetzt Eingangsgrad 0 hat&lt;br /&gt;
                 result.append(neighbor)   #  ... füge ihn in result ein&lt;br /&gt;
     &lt;br /&gt;
     if len(result) == len(graph):         # wenn alle Knoten jetzt Eingangsgrad 0 haben&lt;br /&gt;
         return result                     # ... ist result eine topologische Sortierung&lt;br /&gt;
     else:&lt;br /&gt;
         return None                       # andernfalls ist der Graph zyklisch&lt;br /&gt;
&lt;br /&gt;
===== Algorithmus 2 =====&lt;br /&gt;
Der obige Algorithmus hat den Nachteil, dass er jeden Knoten zweimal expandiert. Man kann eine topologische Sortierung stattdessen auch mit Tiefensuche bestimmen. Es gilt nämlich der folgende&lt;br /&gt;
;Satz: Wird ein DAG mittels Tiefensuche traversiert, definiert die &amp;lt;i&amp;gt;reverse post-order&amp;lt;/i&amp;gt; eine topologische Sortierung.&lt;br /&gt;
Zur Erinnerung: die post-order erhält man, indem man jeden Knoten ausgibt, &amp;lt;i&amp;gt;nachdem&amp;lt;/i&amp;gt; die Rekursion zu allen seinen Nachbarn beendet ist, siehe unsere [[Graphen_und_Graphenalgorithmen#pre_and_post_order|Diskussion weiter oben]]. Die reverse post-order ist gerade die Umkehrung dieser Reihenfolge. Die folgende Implementation verwendet die rekursive Version der Tiefensuche, in der Praxis wird man meist die iterative Version mit Stack bevorzugen, weil bei großen Graphen die Aufruftiefe sehr groß werden kann:&lt;br /&gt;
&lt;br /&gt;
 def reverse_post_order(graph):               # gerichteter Graph&lt;br /&gt;
     result = []                              # enthält später die reverse post-order&lt;br /&gt;
     visited = [False]*len(graph)             # Flags für bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):                         # besuche node&lt;br /&gt;
         if not visited[node]:                # aber nur, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True             # markiere ihn als besucht&lt;br /&gt;
             for neighbor in graph[node]:     # und besuche die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
             result.append(node)              # alle Nachbarn besucht =&amp;gt; Anhängen an result liefert post-order&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):          # besuche alle Knoten&lt;br /&gt;
         visit(node)&lt;br /&gt;
     &lt;br /&gt;
     result.reverse()                         # post-order =&amp;gt; reverse post-order&lt;br /&gt;
     return result&lt;br /&gt;
&lt;br /&gt;
Die Tatsache, dass die reverse post-order tatsächlich eine topologische Sortierung liefert, leuchtet wahrscheinlich nicht unmittelbar ein. Bevor wir diese Tatsache beweisen. wollen wir uns anhand des Ankleidegraphen klar machen, dass die pre-order (die man intuitiv vielleicht eher wählen würde) keine topologische Sortierung ist. Startet man die Tiefensuche beim Knoten &amp;quot;Unterhemd&amp;quot;, werden die Knoten in der Reihenfolge &amp;quot;Unterhemd&amp;quot;, &amp;quot;Oberhemd&amp;quot;, &amp;quot;Schlips&amp;quot;, &amp;quot;Jackett&amp;quot;, &amp;quot;Gürtel&amp;quot; gefunden. Da dann alle von &amp;quot;Unterhemd&amp;quot; erreichbaren Knoten erschöpft sind, startet man die Tiefensuche als nächstes bei &amp;quot;Unterhose&amp;quot; und erreicht von dort aus &amp;quot;Hose&amp;quot; und &amp;quot;Schuhe&amp;quot;. Man erkennt sofort, dass diese Reihenfolge nicht funktioniert: &amp;quot;Hose&amp;quot; kommt nach &amp;quot;Gürtel&amp;quot;, und &amp;quot;Jackett&amp;quot; kommt vor &amp;quot;Gürtel&amp;quot;. Bei dieser Anordnung gibt es Pfeile nach links, die Abhängigkeitsbedingungen sind somit verletzt.&lt;br /&gt;
&lt;br /&gt;
Damit die reverse post-order eine zulässige Sortierung sein kann, muss stets gelten, dass Knoten u vor Knoten v einsortiert wurde, wenn die Kante (u &amp;amp;rarr; v) existiert. Das ist aber äquivalent zur Forderung, dass in der ursprünglichen post-order (vor dem &amp;lt;tt&amp;gt;reverse&amp;lt;/tt&amp;gt;) u hinter v stehen muss. Wir betrachten den &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt;-Aufruf, bei dem u expandiert wird. Gelangt man jetzt zu u's Nachbarn v, gibt es zwei Möglichkeiten: Wenn v bereits expandiert wurde, befindet es sich bereits im Array &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt; kehrt sofort zurück. Andernfalls wird v ebenfalls expandiert und demzufolge in &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; eingetragen, &amp;lt;i&amp;gt;bevor&amp;lt;/i&amp;gt; der rekursive Aufruf &amp;lt;tt&amp;gt;visit(v)&amp;lt;/tt&amp;gt; zurückkehrt. Knoten u wird aber erst in &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; eingefügt, &amp;lt;i&amp;gt;nachdem&amp;lt;/i&amp;gt; alle rekursiven &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt;-Aufrufe seiner Nachbarn zurückgekehrt sind. In beiden Fällen steht u in der post-order wie gefordert hinter v, und daraus folgt die Behauptung.&lt;br /&gt;
&lt;br /&gt;
Der obige Algorithmus liefert natürlich nur dann eine topologische Sortierung, wenn der Graph wirklich azyklisch ist (man kann ihn aber auch anwenden, um die reverse post-order für einen zyklischen Graphen zu bestimmen, siehe Abschnitt &amp;quot;[[Graphen_und_Graphenalgorithmen#Transitive Hülle und stark zusammenhängende Komponenten|Stark zusammenhängende Komponenten]]&amp;quot;). Dieser Fall tritt in der Praxis häufig auf, weil zyklische Graphen bei vielen Anwendungen gar nicht erst entstehen können. Weiß man allerdings nicht, ob der Graph azyklisch ist oder nicht, muss man einen zusätzlichen Test auf Zyklen in den Algorithmus einbauen. &lt;br /&gt;
&lt;br /&gt;
Zyklische Graphen sind dadurch gekennzeichnet, dass es im obigen Beweis eine dritte Möglichkeit gibt: Während der Expansion von u wird rekursiv v expandiert, und es gibt eine Rückwärtskante (v &amp;amp;rarr; u). (Es spielt dabei keine Rolle, ob v von u aus direkt oder indirekt erreicht wurde.) Ein Zyklus wird also entdeckt, wenn die Tiefensuche zu u zurückkehrt, solange u noch &amp;lt;i&amp;gt;aktiv&amp;lt;/i&amp;gt; ist, d.h. wenn die Rekursion von u aus gestartet und noch nicht beendet wurde. Dies kann man leicht feststellen, wenn man in der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; drei Werte zulässt: 0 für &amp;quot;noch nicht besucht&amp;quot;, 1 für &amp;quot;aktiv&amp;quot; und 2 für &amp;quot;beendet&amp;quot;. Wir signalisieren einen Zyklus, sobald &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt; für einen Knoten aufgerufen wird, der gerade aktiv ist:&lt;br /&gt;
&lt;br /&gt;
 def topological_sort_DFS(graph):             # gerichteter Graph&lt;br /&gt;
     result = []                              # enthält später die topologische Sortierung&lt;br /&gt;
     &lt;br /&gt;
     not_visited, active, finished = 0, 1, 2  # drei Zustände für visited&lt;br /&gt;
     visited = [not_visited]*len(graph)       # Flags für aktive und bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):                         # besuche node (gibt &amp;quot;True&amp;quot; zurück, wenn Zyklus gefunden wurde)&lt;br /&gt;
         if visited[node] == not_visited:     # ... aber nur, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = active           # markiere ihn als aktiv&lt;br /&gt;
             for neighbor in graph[node]:     # und besuche die Nachbarn&lt;br /&gt;
                 if visit(neighbor):          # wenn rekursiv ein Zyklus gefunden wurde&lt;br /&gt;
                     return True              # ... brechen wir ab und signalisieren den Zyklus&lt;br /&gt;
             visited[node] = finished         # Rekursion beendet, node ist nicht mehr aktiv&lt;br /&gt;
             result.append(node)              # alle Nachbarn besucht =&amp;gt; Anhängen an result liefert post-order&lt;br /&gt;
             return False                     # kein Zyklus gefunden&lt;br /&gt;
         elif visited[node] == active:        # Rekursion erreicht einen noch aktiven Knoten&lt;br /&gt;
             return True                      #  =&amp;gt; Zyklus gefunden&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):          # besuche alle Knoten&lt;br /&gt;
         if visit(node):                      # wenn Zyklus gefunden wurde&lt;br /&gt;
             return None                      # ... gibt es keine topologische Sortierung&lt;br /&gt;
     &lt;br /&gt;
     result.reverse()                         # post-order =&amp;gt; reverse post-order (=topologische Sortierung)&lt;br /&gt;
     return result&lt;br /&gt;
&lt;br /&gt;
Man macht sich leicht klar, dass kein Zyklus vorliegt, wenn die Rekursion einen Knoten erreicht, der bereits auf &amp;lt;tt&amp;gt;finished&amp;lt;/tt&amp;gt; gesetzt ist. Nehmen wir an, dass u gerade expandiert wird, und sein Nachbar v ist bereits &amp;lt;tt&amp;gt;finished&amp;lt;/tt&amp;gt;. Wenn es einen Zyklus gäbe, müsste es einen Weg von v nach u geben. Dann wäre u aber bereits während der Expansion von v gefunden worden. Da v nicht mehr im Zustand &amp;lt;tt&amp;gt;active&amp;lt;/tt&amp;gt; ist, muss die Expansion von v schon abgeschlossen gewesen sein, ohne dass u gefunden wurde. Folglich kann es keinen solchen Zyklus geben.&lt;br /&gt;
&lt;br /&gt;
=== Transitive Hülle und stark zusammenhängende Komponenten ===&lt;br /&gt;
&lt;br /&gt;
Auch bei gerichteten Graphen ist die Frage, welche Knoten miteinander zusammenhängen, von großem Interesse. Wir betrachten dazu wieder die Relation &amp;quot;Knoten v ist von Knoten u aus erreichbar&amp;quot;, die anzeigt, ob es einen Weg von u nach v gibt oder nicht. In ungerichteten Graphen ist diese Relation immer symmetrisch, weil jeder Weg in beiden Richtungen benutzt werden kann. In gerichteten Graphen gilt dies nicht. Man muss hier zwei Arten von Zusammenhangskomponenten unterscheiden:&lt;br /&gt;
;Transitive Hülle: Die transitive Hülle eines Knotens u ist die Menge aller Knoten, die von u aus erreichbar sind:&lt;br /&gt;
:&amp;lt;math&amp;gt;T(u) = \{v\ |\ u \rightsquigarrow v\}&amp;lt;/math&amp;gt;&lt;br /&gt;
;Stark zusammenhängende Komponenten: Die stark zusammenhängende Komponenten &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; eines gerichteten Graphen sind maximale Teilgraphen, so dass alle Knoten innerhalb einer Komponente von jedem anderen Knoten der selben Komponente aus erreichbar sind&lt;br /&gt;
:&amp;lt;math&amp;gt;u,v \in C_i\ \ \Leftrightarrow\ \ u \rightsquigarrow v \wedge v \rightsquigarrow u&amp;lt;/math&amp;gt;&lt;br /&gt;
Die erste Definition betrachtet den Zusammenhang asymmetrisch, ohne Beachtung der Frage, ob es auch einen Rückweg von Knoten v nach u gibt, die zweite hingegen symmetrisch.&lt;br /&gt;
&lt;br /&gt;
Die &amp;lt;b&amp;gt;transitive Hülle&amp;lt;/b&amp;gt; benötigt man, wenn man Fragen der Erreichbarkeit besonders effizient beantworten will. Wir hatten bespielsweise oben erwähnt, dass das Python-Modul &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; direkt und indirekt von mehreren anderen Module abhängt, die vorher installiert werden müssen, damit &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; funktioniert. Bittet man den Systemadministrator, das &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt;-Paket zu installieren, will er diese Abhängigkeiten wahrscheinlich nicht erst mühsam rekursiv heraussuchen, sondern er verlangt eine Liste aller Pakete, die installiert werden müssen. Dies ist gerade die transitive Hülle von &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; im Abhängigkeitsgraphen. Damit man diese nicht manuell bestimmen muss, verwendet man Installationsprogrammen wie z.B. [http://pypi.python.org/pypi/pip/ pip], die die Abhängigkeiten automatisch herausfinden und installieren. &lt;br /&gt;
&lt;br /&gt;
Bei der Bestimmung der transitiven Hülle modifiziert man den gegebenen Graphen, indem man jedesmal eine neue Kante (u &amp;amp;rarr; v) einfügt, wenn diese Kante noch nicht existiert, aber v von u aus erreichbar ist. Dies gelingt mit einer sehr einfachen Variation der Tiefensuche: Wir rufen &amp;lt;tt&amp;gt;visit(k)&amp;lt;/tt&amp;gt; für jeden Knoten k auf, aber setzen die property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; zuvor auf &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt; zurück. Alle Knoten, die während der Rekursion erreicht werden, sind im modifizierten Graphen Nachbarn von k. Ein etwas effizienterer Ansatz ist der [http://de.wikipedia.org/wiki/Algorithmus_von_Floyd_und_Warshall Algorithmus von Floyd und Warshall].&lt;br /&gt;
&lt;br /&gt;
Die Bestimmung der &amp;lt;b&amp;gt;stark zusammenhängenden Komponenten&amp;lt;/b&amp;gt; ist etwas schwieriger. Es existieren eine ganze Reihe von effizienten Algorithmen (siehe [http://en.wikipedia.org/wiki/Strongly_connected_component WikiPedia]), deren einfachster der Algorithmus von Kosaraju ist:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;gegeben:&amp;lt;/b&amp;gt; gerichteter Graph&lt;br /&gt;
&lt;br /&gt;
# Bestimme die reverse post-order (mit der Funktion &amp;lt;tt&amp;gt;reverse_post_order&amp;lt;/tt&amp;gt;)&lt;br /&gt;
# Bilde den transponierten Graphen &amp;lt;math&amp;gt;G^T&amp;lt;/math&amp;gt; (mit der Funktion &amp;lt;tt&amp;gt;transposeGraph&amp;lt;/tt&amp;gt;)&lt;br /&gt;
# Bestimme die Zusammenhangskomponenten von &amp;lt;math&amp;gt;G^T&amp;lt;/math&amp;gt; mittels Tiefensuche, aber betrachte die Knoten dabei in der reverse post-order aus Schritt 1 (dies kann mit einer minimalen Modifikation der Funktion &amp;lt;tt&amp;gt;connectedComponents&amp;lt;/tt&amp;gt; geschehen, indem man die Zeile &amp;lt;tt&amp;gt;for node in xrange(len(graph)):&amp;lt;/tt&amp;gt; einfach nach &amp;lt;tt&amp;gt;for node in ordered:&amp;lt;/tt&amp;gt; abändert, wobei &amp;lt;tt&amp;gt;ordered&amp;lt;/tt&amp;gt; das Ergebnis der Funktion &amp;lt;tt&amp;gt;reverse_post_order&amp;lt;/tt&amp;gt; ist, also ein Array, das die Knoten in der gewünschten Reihenfolge enthält).&lt;br /&gt;
Die Zusammenhangskomponenten, die man in Schritt 3 findet, sind gerade die stark zusammenhängenden Komponenten des Originalgraphen G. Die folgende Skizze zeigt diese in grün für den schwarz gezeichneten gerichteten Graphen. &lt;br /&gt;
&lt;br /&gt;
[[Image:strongly-connected-components.png|400px]]    &lt;br /&gt;
&lt;br /&gt;
Zum Beweis der Korrektheit des Algorithmus von Kosaraju zeigen wir zwei Implikationen: 1. Wenn die Knoten u und v in der selben stark zusammenhängenden Komponente liegen, werden sie in Schritt 3 des Algorithmus auch der selben Komponente zugewiesen. 2. Wenn die Knoten u und v in Schritt 3 der selben Komponente zugewiesen wurden, müssen sie auch in der selben stark zusammenhängenden Komponente liegen. &lt;br /&gt;
# Knoten u und v gehören zur selben stark zusammenhängenden Komponente von G. Per Definition gilt, dass u von v aus erreichbar ist und umgekehrt. Dies muss auch im transponierten Graphen G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; gelten (der Weg &amp;lt;math&amp;gt;u \rightsquigarrow v&amp;lt;/math&amp;gt; wird jetzt zum Weg &amp;lt;math&amp;gt;v \rightsquigarrow u&amp;lt;/math&amp;gt; und umgekehrt). Wird u bei der Tiefensuche in Schritt 3 vor v expandiert, ist v von u aus erreichbar und gehört somit zur selben Komponente. Das umgekehrte gilt, wenn v vor u expandiert wird. Daraus folgt die Behauptung 1.&lt;br /&gt;
# Knoten u und v werden in Schritt 3 der selben Komponente zugewiesen: Sei x der Anker dieser Komponente. Da u in der gleichen Komponente wie x liegt, muss es in G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; einen Weg &amp;lt;math&amp;gt;x \rightsquigarrow u&amp;lt;/math&amp;gt;, und demnach in G einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow x&amp;lt;/math&amp;gt; geben. Da x der Anker seiner Komponente ist, wissen wir aber auch, dass x in der reverse post-order &amp;lt;i&amp;gt;vor&amp;lt;/i&amp;gt; u liegt (denn der Anker ist der Knoten, mit dem eine neue Komponente gestartet wird; er muss deshalb im Array &amp;lt;tt&amp;gt;ordered&amp;lt;/tt&amp;gt; als erster Konten seiner Komponente gefunden worden sein). Wir unterscheiden jetzt im Schritt 1 des Algorithmus zwei Fälle:&lt;br /&gt;
## u wurde bei der Bestimmung der post-order vor x expandiert. Dann kann x nur dann in der reverse post-order &amp;lt;i&amp;gt;vor&amp;lt;/i&amp;gt; u liegen (oder, einfacher ausgedrückt, x kann nur dann in der post-order &amp;lt;i&amp;gt;hinter&amp;lt;/i&amp;gt; u liegen), wenn x im Graphen G nicht von u aus erreichbar war. Das ist aber unmöglich, weil wir ja schon wissen, dass es in G einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow x&amp;lt;/math&amp;gt; gibt.&lt;br /&gt;
## Folglich wurde u bei der Bestimmung der post-order nach x expandiert. Da x in der post-order hinter u liegt, muss u während der Expansion von x erreicht worden sein. Deshalb muss es in G auch einen Weg &amp;lt;math&amp;gt;x \rightsquigarrow u&amp;lt;/math&amp;gt; geben.&lt;br /&gt;
#:Somit sind x und u in der selben stark zusammenhängenden Komponente. Die gleiche Überlegung gilt für x und v. Wegen der Transitivität der relation &amp;quot;ist erreichbar&amp;quot; folgt daraus, dass auch u und v in der selben Komponente liegen, also die Behauptung 2.&lt;br /&gt;
&lt;br /&gt;
Die folgende Skizze illustriert, dass der Komponentengraph stets azyklisch ist. Den Komponentengraph erhält man, indem man für jede Komponente &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; einen Knoten erzeugt (grün), und die Knoten i und j durch eine gerichtete Kante verbindet (rot), wenn es im Originalgraphen eine Kante (u &amp;amp;rarr; v) mit &amp;lt;math&amp;gt;u \in C_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v \in C_j&amp;lt;/math&amp;gt; gibt. Es ist dann garantiert, dass es keine Kante in umgekehrter Richtung geben kann. Daraus folgt insbesondere, dass ein DAG nur triviale stark verbundene Komponenten haben kann, die aus einzelnen Knoten bestehen.&lt;br /&gt;
&lt;br /&gt;
[[Image:strongly-connected-components-graph.png|400px]]&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Das Erfüllbarkeitsproblem in Implikationengraphen ===&lt;br /&gt;
&lt;br /&gt;
Das Erfüllbarkeitsproblem hat auf den ersten Blick nichts mit Graphen zu tun, denn es geht um Wahrheitswerte logischer Ausdrücke. Man kann logische Ausdrücke jedoch unter bestimmten Bedingungen in eine Graphendarstellung überführen und somit das ursprüngliche Problem auf ein Problem der Graphentheorie reduzieren, für das bereits ein Lösungsverfahren bekannt ist. In diesem Abschnitt wollen wir dies für die sogenannten Implikationengraphen zeigen, ein weiteres Beispiel findet sich im Kapitel [[NP-Vollständigkeit]].&lt;br /&gt;
&lt;br /&gt;
==== Das Erfüllbarkeitsproblem ====&lt;br /&gt;
&lt;br /&gt;
(vgl. [http://de.wikipedia.org/wiki/Erfüllbarkeitsproblem_der_Aussagenlogik WikiPedia (de)])&lt;br /&gt;
&lt;br /&gt;
Das Erfüllbarkeitsproblem (SAT-Problem, von &amp;lt;i&amp;gt;satisfiability&amp;lt;/i&amp;gt;) befasst sich mit logischen (oder Booleschen) Funktionen: Gegeben sei eine Menge &amp;lt;math&amp;gt;\{x_1, ... ,x_n\}&amp;lt;/math&amp;gt; Boolscher Variablen (d.h., die &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; können nur die Werte True oder False annehmen), sowie eine logische Formel, in der die Variablen mit den üblichen logischen Operatoren &lt;br /&gt;
:&amp;lt;math&amp;gt;\neg\quad&amp;lt;/math&amp;gt;: Negation (&amp;quot;nicht&amp;quot;, in Python: &amp;lt;tt&amp;gt;not&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\vee\quad&amp;lt;/math&amp;gt;: Disjunktion (&amp;quot;oder&amp;quot;, in Python: &amp;lt;tt&amp;gt;or&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\wedge\quad&amp;lt;/math&amp;gt;: Konjuktion (&amp;quot;und&amp;quot;, in Python: &amp;lt;tt&amp;gt;and&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\rightarrow\quad&amp;lt;/math&amp;gt;: Implikation (&amp;quot;wenn, dann&amp;quot;, in Python nicht als Operator definiert)&lt;br /&gt;
:&amp;lt;math&amp;gt;\leftrightarrow\quad&amp;lt;/math&amp;gt;: Äquivalenz (&amp;quot;genau dann, wenn&amp;quot;, in Python: &amp;lt;tt&amp;gt;==&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\neq\quad&amp;lt;/math&amp;gt;: exklusive Disjunktion (&amp;quot;entweder oder&amp;quot;, in Python: &amp;lt;tt&amp;gt;!=&amp;lt;/tt&amp;gt;)&lt;br /&gt;
verknüpft sind. Klammern definieren die Reihenfolge der Auswertung der Operationen. Für jede Belegung der Variablen &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; mit True oder False liefert die Formel den Wert der Funktion, der natürlich auch nur True oder False sein kann. Wenn Formel und Belegung gegeben sind, ist die Auswertung der Funktion ein sehr einfaches Problem: Man transformiert die Formel in einen Parse-Baum (siehe Übungsaufgabe &amp;quot;Taschenrechner) und wertet jeden Knoten mit Hilfe der üblichen Wertetabellen für logische Operatoren aus, die wir hier zur Erinnerung noch einmal angeben:&lt;br /&gt;
{| cellspacing=&amp;quot;0&amp;quot; border=&amp;quot;1&amp;quot;&lt;br /&gt;
|- style=&amp;quot;text-align:center;background-color:#ffffcc;width:50px&amp;quot;&lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \vee b &amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \wedge b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \rightarrow b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;b \rightarrow a&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \leftrightarrow b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \neq b&amp;lt;/math&amp;gt; &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 0 || 1 || 1 || 1 || 0&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 0 || 1 || 0 || 0 || 1&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 0 || 1 || 0 || 1&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 || 0&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beim Erfüllbarkeitsproblem wird die Frage umgekehrt gestellt: &lt;br /&gt;
:Gegeben sei eine logische Funktion. Ist es möglich, dass die Funktion jemals den Wert True annimmt? &lt;br /&gt;
Das heisst, kann man die Variablen &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; so mit True oder False belegen, dass die Formel am Ende wahr ist? Im Prinzip kann man diese Frage durch erschöpfende Suche leicht beantworten, indem man die Funktion für alle &amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt; möglichen Belegungen einfach ausrechnet, aber das dauert für große n (ab ca. &amp;lt;math&amp;gt;n\ge 40&amp;lt;/math&amp;gt;) viel zu lange. Erstaunlicherweise ist es aber noch niemanden gelungen, einen Algorithmus zu finden, der für beliebige logische Funktionen schneller funktioniert. Im Gegenteil wurde gezeigt, dass das Erfüllbarkeitsproblem [[NP-Vollständigkeit|NP-vollständig]] ist, so dass wahrscheinlich kein solcher Algorithmus existiert. Trotz (oder gerade wegen) seiner Schwierigkeit hat das Erfüllbarkeitsproblem viele Anwendungen gefunden, vor allem beim Testen logischer Schaltkreise (&amp;quot;Gibt es eine Belegung der Eingänge, so dass am Ausgang der verbotene Wert X entsteht?&amp;quot;) und bei der Planerstellung in der künstlichen Intelligenz (&amp;quot;Kann man ausschließen, dass der generierte Plan Konflikte enthält?&amp;quot;). Es ist außerdem ein beliebtes Modellproblem für die Erforschung neuer Ideen und Algorithmen für schwierige Probleme.&lt;br /&gt;
&lt;br /&gt;
==== Normalformen für logische Ausdrücke ====&lt;br /&gt;
&lt;br /&gt;
Um die Beschreibung von Erfüllbarkeitsproblemen zu vereinfachen und zu vereinheitlichen, hat man verschiedene &amp;lt;i&amp;gt;Normalformen&amp;lt;/i&amp;gt; für logische Ausdrücke eingeführt. Die wichtigste ist die &amp;lt;i&amp;gt;Konjuktionen-Normalform&amp;lt;/i&amp;gt; (CNF - conjunctive normal form). Ein Ausdruck in &amp;lt;i&amp;gt;Konjuktionen-Normalform&amp;lt;/i&amp;gt; ist eine UND-Verknüpfung von M &amp;lt;i&amp;gt;Klauseln&amp;lt;/i&amp;gt;:&lt;br /&gt;
 (CLAUSE&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;) &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; (CLAUSE&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;) &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; ...  &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; (CLAUSE&amp;lt;sub&amp;gt;M&amp;lt;/sub&amp;gt;)&lt;br /&gt;
Jede Klausel ist wiederum ein logischer Ausdruck, der aber sehr einfach sein muss: Er darf nur noch k Variablen enthalten, die nur mit den Operatoren NICHT und ODER verknüpft werden dürfen, z.B.&lt;br /&gt;
  CLAUSE&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; := &amp;lt;math&amp;gt;x_1 \vee \neg x_3 \vee x_8&amp;lt;/math&amp;gt;&lt;br /&gt;
Je nachdem, wie viele Variablen pro Klausel erlaubt sind, spricht man von &amp;lt;b&amp;gt;k-CNF&amp;lt;/b&amp;gt; und entsprechend von einem &amp;lt;b&amp;gt;k-SAT&amp;lt;/b&amp;gt; Problem. Es ist außerdem üblich, die Menge der Variablen und die Menge der negierten Variablen zusammen als Menge der &amp;lt;i&amp;gt;Literale&amp;lt;/i&amp;gt; zu bezeichnen:&lt;br /&gt;
  LITERALS := &amp;lt;math&amp;gt;\{x_1,...,x_n\} \cup \{\neg x_1,...,\neg x_n\}&amp;lt;/math&amp;gt;&lt;br /&gt;
Formal definiert man die &amp;lt;b&amp;gt;k-Konjunktionen-Normalform (k-CNF)&amp;lt;/b&amp;gt; am besten durch eine Grammatik in [http://de.wikipedia.org/wiki/Backus-Naur-Form Backus-Naur-Form]:&lt;br /&gt;
    k_CNF    ::=  CLAUSE | k_CNF &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; CLAUSE&lt;br /&gt;
    CLAUSE   ::= (LITERAL &amp;lt;math&amp;gt;\vee&amp;lt;/math&amp;gt; ... &amp;lt;math&amp;gt;\vee&amp;lt;/math&amp;gt; LITERAL)  # genau k Literale pro Klausel&lt;br /&gt;
    LITERAL  ::=  VARIABLE | &amp;lt;math&amp;gt;\neg&amp;lt;/math&amp;gt;VARIABLE&lt;br /&gt;
    VARIABLE ::=  &amp;lt;math&amp;gt;x_1&amp;lt;/math&amp;gt; | ... | &amp;lt;math&amp;gt;x_n&amp;lt;/math&amp;gt;&lt;br /&gt;
Beispiele:&lt;br /&gt;
* 3-CNF: &amp;lt;math&amp;gt;(x_1 \vee \neg x_2 \vee x_4) \wedge (x_2 \vee x_3 \vee \neg x_4) \wedge (\neg x_1 \vee x_4 \vee \neg x_5)&amp;lt;/math&amp;gt;&lt;br /&gt;
* 2-CNF: &amp;lt;math&amp;gt;(x_1 \vee \neg x_2) \wedge (x_3 \vee x_4)&amp;lt;/math&amp;gt; ...&lt;br /&gt;
&amp;lt;b&amp;gt;Gesucht&amp;lt;/b&amp;gt; ist eine Belegung der Variablen mit True und False, so dass der Ausdruck den Wert True hat. Aus den Eigenschaften der UND- und ODER-Verknüpfungen folgt, dass ein Ausdruck in k-CNF genau dann True ist, wenn jede einzelne Klausel True ist. In jeder Klausel wiederum hat man k Chancen, die Klausel True zu machen, indem man eins der Literale zu True macht. Eventuell werden dadurch aber andere Klauseln wieder zu False, was die Aufgabe so schwierig macht. Die Bedeutung der k-CNF ergibt sich aus folgendem&lt;br /&gt;
;Satz: Jeder logische Ausdruck kann effizient nach 3-CNF transformiert werden, jedoch im allgemeinen nicht nach 2-CNF.&lt;br /&gt;
Man kann sich also auf Algorithmen für 3-SAT-Probleme konzentrieren, ohne dabei an Ausdrucksmächtigkeit zu verlieren. &lt;br /&gt;
&lt;br /&gt;
Leider gilt der entsprechende Satz nicht für k=2: Ausdrücke in 2-CNF sind weit weniger mächtig, weil man in jeder Klausel nur noch zwei Wahlmöglichkeiten hat. Bestimmte logische Ausdrücke sind aber auch nach 2-CNF transformierbar, beispielsweise die Bedingung, dass zwei Literale u und v immer den entgegegesetzten Wert haben müssen. Dies ergibt ein Paar von ODER-Verknüpfungen:&lt;br /&gt;
:&amp;lt;math&amp;gt;(u \leftrightarrow \neg v) \equiv (u \vee \neg v) \wedge (\neg u \vee v)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die 2-CNF hat den Vorteil, dass es effiziente Algorithmen für das 2-SAT-Problem gibt, die wir jetzt kennenlernen wollen. Es zeigt sich, dass man Ausdrücke in 2-CNF als Graphen repräsentieren kann, indem man sie zunächst in die &amp;lt;i&amp;gt;Implikationen-Normalform&amp;lt;/i&amp;gt; (INF für &amp;lt;i&amp;gt;implicative normal form&amp;lt;/i&amp;gt;) überführt. Die Implikationen-Normalform besteht ebenfalls aus einer Menge von Klauseln, die durch UND-Operationen verknüpft sind, aber jede Klausel ist jetzt eine Implikation. &lt;br /&gt;
Die Grammatik der &amp;lt;b&amp;gt;Implikationen-Normalform (INF)&amp;lt;/b&amp;gt; lautet:&lt;br /&gt;
    INF      ::=  CLAUSE | INF &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; CLAUSE&lt;br /&gt;
    CLAUSE   ::= (LITERAL &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; LITERAL)  # genau 2 Literale pro Implikation&lt;br /&gt;
    LITERAL  ::=  VARIABLE | &amp;lt;math&amp;gt;\neg&amp;lt;/math&amp;gt;VARIABLE&lt;br /&gt;
    VARIABLE ::=  &amp;lt;math&amp;gt;x_1&amp;lt;/math&amp;gt; | ... | &amp;lt;math&amp;gt;x_n&amp;lt;/math&amp;gt;&lt;br /&gt;
und ein gültiger Ausdruck wäre z.B.&lt;br /&gt;
:&amp;lt;math&amp;gt;(x_1 \to x_2) \wedge (x_2 \to \neg x_3) \wedge (x_4 \to x_3)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die Umwandlung von 2-CNF nach INF beruht auf folgender Äquivalenz, die man sich aus der obigen Wahrheitstabelle leicht herleitet:&lt;br /&gt;
:&amp;lt;math&amp;gt;(x \vee y) \equiv (\neg x \rightarrow y) \equiv (\neg y \rightarrow x)&amp;lt;/math&amp;gt;&lt;br /&gt;
Aus dieser Äquivalenz folgt der &lt;br /&gt;
;Satz: Ein Ausdruck in 2-CNF kann nach INF transformiert werden, indem man jede Klausel &amp;lt;math&amp;gt;(x \vee y)&amp;lt;/math&amp;gt; durch das Klauselpaar &amp;lt;math&amp;gt;(\neg x \rightarrow y) \wedge (\neg y \rightarrow x)&amp;lt;/math&amp;gt; ersetzt.&lt;br /&gt;
Man beachte, dass man für jede ODER-Klausel des ursprünglichen Ausdrucks &amp;lt;i&amp;gt;zwei&amp;lt;/i&amp;gt; Implikationen (eine für jede Richtung des &amp;quot;wenn, dann&amp;quot;) einfügen muss, um die Symmetrie des Problems zu erhalten.&lt;br /&gt;
&lt;br /&gt;
==== Lösung des 2-SAT-Problems mit Implikationgraphen ====&lt;br /&gt;
&lt;br /&gt;
Jeder Ausdruck in INF kann als gerichteter Graph dargestellt werden:&lt;br /&gt;
# Für jedes Literal wird ein Knoten in den Graphen eingefügt. Es gibt also für jede Variable und für ihre Negation jeweils einen Knoten, d.h. 2n Knoten insgesamt.&lt;br /&gt;
# Jede Implikation ist eine gerichtete Kante.&lt;br /&gt;
Implikationengraphen eignen sich, um Ursache-Folge-Beziehungen oder Konflikte zwischen Aktionen auszudrücken. Beispielsweise kann man die Klausel &amp;lt;math&amp;gt;(x \rightarrow \neg y)&amp;lt;/math&amp;gt; als &amp;quot;wenn man x tut, darf man y nicht tun&amp;quot; interpretieren. Ein anderes schönes Beispiel findet sich in Übung 12.&lt;br /&gt;
&lt;br /&gt;
Für die Implementation eines Implikationengraphen in Python empfiehlt es sich, die Knoten geschickt zu numerieren: Ist die Variable &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; dem Knoten i zugeordnet, so sollte die negierte Variable &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; dem Knoten (i+n) zugeordnet werden. Zu jedem gegebenen Knoten i findet man dann den negierten Partnerknoten j leicht durch die Formel &amp;lt;tt&amp;gt;j = (i + n ) % (2*n)&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die Aufgabe besteht jetzt darin, folgende Fragen zu beantworten:&lt;br /&gt;
# Ist der durch den Implikationengraphen gegebene Ausdruck erfüllbar?&lt;br /&gt;
# Finde eine geeignete Belegung der Variablen, wenn der Ausduck erfüllbar ist.&lt;br /&gt;
Die erste Frage beantwortet man leicht, indem man die stark zusammenhängenden Komponenten des Implikationengraphen bildet. Dann gilt folgender&lt;br /&gt;
;Satz: Seien u und v zwei Literale, die sich in der selben stark zusammenhängenden Komponente befinden. Dann müssen u und v stets den selben Wert haben, damit der Ausdruck erfüllt sein kann.&lt;br /&gt;
Die Korrektheit des Satzes folgt aus der Definition der stark zusammenhängenden Komponenten: Da u und v in der selben Komponente liegen, gibt es im Implikationengraphen einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow v&amp;lt;/math&amp;gt; sowie einen Weg &amp;lt;math&amp;gt;v \rightsquigarrow u&amp;lt;/math&amp;gt;. Wegen der Transitivität der &amp;quot;wenn, dann&amp;quot; Relation kann man die Wege zu zwei Implikationen verkürzen, die gleichzeitig gelten müssen: &amp;lt;math&amp;gt;(u \rightarrow v) \wedge (v \rightarrow u)&amp;lt;/math&amp;gt; (die Verkürzung von Wegen zu direkten Kanten entspricht gerade der Bildung der transitiven Hülle für die Knoten u und v). In der obigen Wertetabelle für logische Operatoren erkennt mann, dass dies äquivalent zur Bedingung &amp;lt;math&amp;gt;(u \leftrightarrow v)&amp;lt;/math&amp;gt; ist. Dies ist aber gerade die Behauptung des Satzes.&lt;br /&gt;
&lt;br /&gt;
Die Erfüllbarkeit des Ausdrucks ist nun ein einfacher Spezialfall dieses Satzes. &lt;br /&gt;
;Korrolar: Der gegebene Ausdruck ist genau dann erfüllbar, wenn die Literale &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; sich für kein i in derselben stark zusammenhängenden Komponente befinden.&lt;br /&gt;
Setzt man nämlich im Satz &amp;lt;math&amp;gt;u = x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v = \neg x_i&amp;lt;/math&amp;gt;, und beide Knoten befinden sich in der selben Komponente, dann müsste gelten &amp;lt;math&amp;gt;x_i \leftrightarrow\neg x_i&amp;lt;/math&amp;gt;, was offensichtlich ein Widerspruch ist. Damit kann der Ausdruck nicht erfüllbar sein. Umgekehrt gilt, dass der Ausdruck immer erfüllbar ist, wenn &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; stets in verschiedenen Komponenten liegen, weil der folgende Algorithmus von Aspvall, Plass und Tarjan in diesem Fall stets eine gültige Belegung aller Variablen liefert:&lt;br /&gt;
# Bestimme die stark zusammenhängenden Komponenten und bilde den Komponentengraphen. Ordne die Knoten des Komponentengraphen (also die stark zusammenhängenden Komponenten des Originalgraphen) in topologische Sortierung an.&lt;br /&gt;
# Betrachte die Komponenten in der topologischen Sortierung von hinten nach vorn und weise ihnen einen Wert nach folgenden Regeln zu (zur Erinnerung: alle Literale in der selben Komponente haben den selben Wert):&lt;br /&gt;
#* Wenn die Komponente noch nicht betrachtet wurde, setze ihren Wert auf True, und den Wert der komplementären Komponente (derjenigen, die die negierten Literale enthält) auf False.&lt;br /&gt;
#* Andernfalls, gehe zur nächsten Komponente weiter.&lt;br /&gt;
Der Algorithmus beruht auf der Symmetrie des Implikationengraphen: Weil Kanten immer paarweise &amp;lt;math&amp;gt;(\neg u \rightarrow v) \wedge (\neg v \rightarrow u)&amp;lt;/math&amp;gt; eingefügt werden, ist der Graph &amp;lt;i&amp;gt;schiefsymmetrisch&amp;lt;/i&amp;gt; (skew symmetric): die eine Hälfte das Graphen ist die transponierte Spiegelung der anderen Hälfte. Enthält eine stark zusammenhängende Komponente &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; die Knoten &amp;lt;tt&amp;gt;i1, i2, ...&amp;lt;/tt&amp;gt;, so gibt es stets eine komplementäre Komponente &amp;lt;math&amp;gt;C_j = \neg C_i&amp;lt;/math&amp;gt;, die die komplementären Knoten &amp;lt;tt&amp;gt;j1 = (i1 + n) % (2*n), j2 = (i2 + n) % (2*n), ...&amp;lt;/tt&amp;gt; enthält. Gilt &amp;lt;math&amp;gt;C_i = \neg C_i&amp;lt;/math&amp;gt; für irgendein i, so ist der Ausdruck nicht erfüllbar. Den Beweis für die Korrektheit des Algorithmus findet man im [http://www.math.ucsd.edu/~sbuss/CourseWeb/Math268_2007WS/2SAT.pdf Originalartikel]. Leider funktioniert dies nicht für k-SAT-Probleme mit &amp;lt;math&amp;gt;k &amp;gt; 2&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Will man nur die Erfüllbarkeit prüfen, vereinfacht sich der Algorithmus zu:&lt;br /&gt;
# Bestimme die stark zusammenhängenden Komponenten.&lt;br /&gt;
# Teste für alle &amp;lt;tt&amp;gt;i = 0,...,n-1&amp;lt;/tt&amp;gt;, dass Knoten &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; und Knoten &amp;lt;tt&amp;gt;(i+n)&amp;lt;/tt&amp;gt; in unterschiedlichen Komponenten liegen.&lt;br /&gt;
Ist der Ausdruck erfüllbar, kann man eine gültige Belegung der Variablen jetzt mit dem randomisierten Algorithmus bestimmen, den wir im Kapitel [[Randomisierte Algorithmen]] behandeln.&lt;br /&gt;
&lt;br /&gt;
== Weitere wichtige Graphenalgorithmen ==&lt;br /&gt;
&lt;br /&gt;
Eins der wichtigsten Einsatzgebiete für Graphen ist die Optimierung, also die Suche nach der &amp;lt;i&amp;gt;besten&amp;lt;/i&amp;gt; Lösung für ein gegebenes Problem:&lt;br /&gt;
* Das &amp;lt;i&amp;gt;interval scheduling&amp;lt;/i&amp;gt; befasst sich damit, aus einer gegebenen Menge von Aufträgen die richtigen auszuwählen und sie geschickt auf die zur Verfügung stehenden Ressourcen aufzuteilen. Damit beschäftigen wir uns im Kapitel [[Greedy-Algorithmen und Dynamische Programmierung]].&lt;br /&gt;
* Beim Problem des Handlungsreisenden sucht man nach der kürzesten Rundreise, die alle gegebenen Städte genau einmal besucht. Dieses Problem behandeln wir im Kapitel [[NP-Vollständigkeit]].&lt;br /&gt;
* Viele weitere Anwendungen können wir leider in der Vorlesung nicht mehr behandeln, z.B.&lt;br /&gt;
** Algorithmen für den [http://en.wikipedia.org/wiki/Maximum_flow_problem maximalen Fluss] beantworten die Frage, wie man die Durchflussmenge durch ein Netzwerk (z.B. von Ölpipelines) maximiert.&lt;br /&gt;
** Beim [http://en.wikipedia.org/wiki/Assignment_problem Problem der optimalen Paarung] (&amp;quot;matching problem&amp;quot; oder &amp;quot;assignment problem&amp;quot;) sucht man nach einer Teilmenge der Kanten (also nach einem Teilgraphen), so dass jeder Knoten in diesem Teilgraphen höchstens den Grad 1 hat. Im neuen Graphen gruppieren die Kanten also je zwei Knoten zu einem Paar, und die Paarung soll nach jeweils anwendungsspezifischen Kriterien optimal sein. Dies benötigt man z.B. bei der optimalen Zuordnung von Gruppen, etwas beim Arbeitsamt (Zuordnung Arbeitssuchender - Stellenangebot) und in der Universität (Zuordnung Studenten - Übungsgruppen).&lt;br /&gt;
** In Statistik und maschinellem Lernen haben in den letzten Jahren die [http://en.wikipedia.org/wiki/Graphical_model graphischen Modelle] große Bedeutung erlangt.&lt;br /&gt;
* usw. usf.&lt;br /&gt;
&lt;br /&gt;
[[Randomisierte Algorithmen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5406</id>
		<title>Graphen und Graphenalgorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5406"/>
				<updated>2012-07-25T17:09:21Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Algorithmus 2 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Einführung zu Graphen ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation -- Königsberger Brückenproblem ===&lt;br /&gt;
Leonhard Euler [http://de.wikipedia.org/wiki/Leonhard_Euler] erfand den Graphen-Formalismus 1736, um eine scheinbar banale Frage zu beantworten: Ist es möglich, in Königsberg (siehe Stadtplan von 1809 und die schematische Darstellung) einen Spaziergang zu unternehmen, bei dem jede der 7 Brücken genau einmal überquert wird?&lt;br /&gt;
&lt;br /&gt;
[[Image:Koenigsberg1809.png]]&amp;lt;br&amp;gt;&lt;br /&gt;
[[Image:Koenigsberg.jpg]]&lt;br /&gt;
&lt;br /&gt;
Ein Graph abstrahiert von der Geometrie des Problems und repräsentiert nur die Topologie. Jeder Stadtteil von Königsberg ist ein Knoten des Graphen, jede Brücke eine Kante. Der zum Brückenproblem gehörende Graph sieht also so aus:&lt;br /&gt;
&lt;br /&gt;
     O&lt;br /&gt;
    /| \&lt;br /&gt;
    \|  \&lt;br /&gt;
     O---O&lt;br /&gt;
    /|  /&lt;br /&gt;
    \| /&lt;br /&gt;
     O&lt;br /&gt;
&lt;br /&gt;
Der gesuchte Spaziergang würde existieren, wenn es maximal 2 Knoten gäbe, an denen sich eine ungerade Zahl von Kanten trifft. Die Frage muss für Königsberg also verneint werden, denn hier gibt es vier solche Knoten. Ein leicht modifiziertes Problem ist allerdings lösbar: Im obigen Stadtplan erkennt man eine Fähre, die die Stadtteile Kneiphof und Altstadt verbindet. Bezieht man dieselbe in den Spaziergang ein, ergibt sich folgender Graph, bei dem nur noch zwei Knoten mit ungerader Kantenzahl existieren:&lt;br /&gt;
&lt;br /&gt;
   --O&lt;br /&gt;
  / /| \&lt;br /&gt;
  \ \|  \&lt;br /&gt;
   --O---O&lt;br /&gt;
    /|  /&lt;br /&gt;
    \| /&lt;br /&gt;
     O&lt;br /&gt;
&lt;br /&gt;
Inzwischen haben Graphen eine riesige Zahl weiterer Anwendungen gefunden. Einige Beispiele:&lt;br /&gt;
&lt;br /&gt;
* Landkarten:&lt;br /&gt;
** Knoten: Länder&lt;br /&gt;
** Kanten: gemeinsame Grenzen&lt;br /&gt;
&lt;br /&gt;
* Logische Schaltkreise:&lt;br /&gt;
** Knoten: Gatter&lt;br /&gt;
** Kanten: Verbindungen&lt;br /&gt;
&lt;br /&gt;
* Chemie (Summenformeln):&lt;br /&gt;
** Knoten: chemische Elemente&lt;br /&gt;
** Kanten: Bindungen &lt;br /&gt;
&lt;br /&gt;
* Soziologie (StudiVZ)&lt;br /&gt;
** Soziogramm&lt;br /&gt;
*** Knoten: Personen&lt;br /&gt;
*** Kanten: Freund von ...&lt;br /&gt;
&lt;br /&gt;
=== Definitionen ===&lt;br /&gt;
&lt;br /&gt;
;Ungerichteter Graph: Ein ungerichteter Graph G = ( V, E ) besteht aus&lt;br /&gt;
:* einer endliche Menge V von Knoten (vertices)&lt;br /&gt;
:* einer endlichen Menge &amp;lt;math&amp;gt;E \subset V \times V&amp;lt;/math&amp;gt; von Kanten (edges)&lt;br /&gt;
:Die Paare (u,v) und (v,u) gelten dabei als nur ''eine'' Kante (somit gilt die Symmetriebeziehung: (u,v) ∈ E =&amp;gt; (v,u) ∈ E ). Die Anzahl der Kanten, die sich an einem Knoten treffen, wird als ''Grad'' (engl. ''degree'') dieses Knotens bezeichnet:&lt;br /&gt;
:::degree(v) = |{v' ∈ V | (v,v') ∈ E}|&lt;br /&gt;
:(Die Syntax |{...}| bezeichnet dabei die Mächtigkeit der angegebenen Menge, also die Anzahl der Elemente in der Menge.)&lt;br /&gt;
&lt;br /&gt;
Der Graph des Königsberger Brückenproblems ist ungerichtet. Bezeichnet man die Knoten entsprechend des folgenden Bildes&lt;br /&gt;
    c&lt;br /&gt;
   /| \&lt;br /&gt;
   \|  \&lt;br /&gt;
    b---d &lt;br /&gt;
   /|  /&lt;br /&gt;
   \| /&lt;br /&gt;
    a&lt;br /&gt;
&lt;br /&gt;
gilt für die Knotengrade: &amp;lt;tt&amp;gt;degree(a) == degree(c) == degree(d) == 3&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;degree(b) == 5&amp;lt;/tt&amp;gt;. Genauer muss man bei diesem Graphen von einem ''Multigraphen'' sprechen, weil es zwischen einigen Knotenpaaren (nämlich (a, b) sowie (b, c)) mehrere Kanten (&amp;quot;Mehrfachkanten&amp;quot;) gibt. Wir werden in dieser Vorlesung nicht näher auf Multigraphen eingehen.&lt;br /&gt;
&lt;br /&gt;
;Gerichteter Graph: Ein Graph heißt ''gerichtet'', wenn die Kanten (u,v) und (v,u) unterschieden werden. Die Kante (u,v) ∈ E wird nun als Kante von u nach v (aber nicht umgekehrt) interpretiert. Entsprechend unterscheidet man jetzt den ''eingehenden'' und den ''ausgehenden Grad'' jedes Knotens:&lt;br /&gt;
:*out_degree(v) = |{v' ∈ V | (v,v') ∈ E}|&amp;lt;br/&amp;gt;&lt;br /&gt;
:*in_degree(v)  = |{v' ∈ V| (v',v) ∈ E}|&lt;br /&gt;
&lt;br /&gt;
Das folgende Bild zeigt einen gerichteten Graphen. Hier gilt &amp;lt;tt&amp;gt;out_degree(1) == out_degree(3) == in_degree(2) == in_degree(4) == 2&amp;lt;/tt&amp;gt; und &lt;br /&gt;
&amp;lt;tt&amp;gt;in_degree(1) == in_degree(3) == out_degree(2) == out_degree(4) == 0&amp;lt;/tt&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
[[Image:digraph.png|gerichteter Graph]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Vollständiger Graph: Ein vollständiger Graph ist ein ungerichteter Graph, bei dem jeder Knoten mit allen anderen Knoten verbunden ist.&lt;br /&gt;
:::&amp;lt;math&amp;gt;E = \{ (v,w) |  v \in V, w \in V, v \ne w \}&amp;lt;/math&amp;gt;&lt;br /&gt;
:Ein vollständiger Graph mit |V| Knoten hat &amp;lt;math&amp;gt;|E| = \frac{|V|(|V|-1)}{2}&amp;lt;/math&amp;gt; Kanten.&lt;br /&gt;
&lt;br /&gt;
Die folgenden Abbildungen zeigen die vollständigen Graphen mit einem bis fünf Knoten (auch als K&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; bis K&amp;lt;sub&amp;gt;5&amp;lt;/sub&amp;gt; bezeichnet).&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;0&amp;quot; style=&amp;quot;margin: 1em auto 1em auto&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
| [[Image:k1.png|frame|k1]]&lt;br /&gt;
| [[Image:k2.png|frame|k2]]&lt;br /&gt;
| [[Image:k3.png|frame|k3]]&lt;br /&gt;
|-&lt;br /&gt;
| [[Image:k4.png|frame|k4]]&lt;br /&gt;
| [[Image:k5.png|frame|k5]]&lt;br /&gt;
|&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
''Rätsel''&amp;lt;br/&amp;gt;&lt;br /&gt;
Auf einer Party sind Leute. Alle stoßen miteinander an. Es hat 78 mal &amp;quot;Pling&amp;quot; gemacht.&lt;br /&gt;
Wieviele Leute waren da? Antwort: Jede Person ist ein Knoten des Graphen, jedes Antoßen eine Kante. &lt;br /&gt;
Da alle miteinander angestoßen haben, handelt es sich um einen vollständigen Graphen. Mit&lt;br /&gt;
|V|(|V|-1)/2 = 78 folgt, dass es 13 Personen waren.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Gewichteter Graph: Ein Graph heißt ''gewichtet'', wenn jeder Kante eine reelle Zahl zugeordnet ist. Bei vielen Anwendungen beschränkt man sich auch auf nichtnegative reelle Gewichte. In einem gerichteten Graphen können die Gewichte der Kanten (u,v) und (v,u) unterschiedlich sein.&lt;br /&gt;
&lt;br /&gt;
Die Gewichte kodieren Eigenschaften der Kanten, die für die jeweilige Anwendung interessant sind. Bei der Berechnung des maximalen Flusses in einem Netzwerk sind die Gewichte z.B. die Durchflusskapazitäten jeder Kante, bei der Suche nach kürzesten Weges kodieren Sie den Abstand zwischen den Endknoten der Kante, bei Währungsnetzwerken (jeder Knoten ist eine Währung) geben sie die Wechselkurse an, usw..&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Teilgraphen: Ein Graph G' = (V',E') ist ein Teilgraph eines Graphen G, wenn gilt:&lt;br /&gt;
:* V' &amp;amp;sube; V &lt;br /&gt;
:* E' &amp;amp;sub; E &lt;br /&gt;
:Er heißt ''(auf)spannender Teilgraph'', wenn gilt:&lt;br /&gt;
:* V' = V&lt;br /&gt;
:Er heißt ''induzierter Teilgraph'', wenn gilt:&lt;br /&gt;
:* e = (u,v) ∈ E' &amp;amp;sub; E &amp;amp;hArr; u ∈ V' und v ∈ V'&lt;br /&gt;
:Den von V' induzierten Teilgraphen erhält man also, indem man aus G alle Knoten löscht, die nicht in V' sind, sowie alle Kanten (und nur diese Kanten), die einen der gelöschten Knoten als Endknoten haben.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Wege, Pfade, Zyklen, Kreise, Erreichbarkeit: Sei G = (V,E) ein Graph (ungerichtet oder gerichteter) Graph. Dann gilt folgende rekursive Definition:&lt;br /&gt;
:* Für v ∈ V ist (v) ein Weg der Länge 0 in G&lt;br /&gt;
:* Falls &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1})&amp;lt;/math&amp;gt; ein Weg ist, und eine Kante &amp;lt;math&amp;gt;(v_{n-1}, v_n)\in E&amp;lt;/math&amp;gt; existiert, dann ist auch &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ein Weg, und er hat die Länge n. &lt;br /&gt;
: Ein Weg ist also eine nichtleere Folge von Knoten, so dass aufeinander folgende Knoten stets durch eine Kante verbunden sind. Die Länge des Weges entspricht der Anzahl der Kanten im Weg (= Anzahl der Knoten - 1).&lt;br /&gt;
:* Ein ''Pfad'' &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ist ein Weg, bei dem alle Knoten v&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; verschieden sind.&lt;br /&gt;
:* ''Ein Zyklus'' &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ist ein Weg, der zum Ausgangspunkt zurückkehrt, wenn also v&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; = v&amp;lt;sub&amp;gt;n&amp;lt;/sub&amp;gt; gilt.&lt;br /&gt;
:* Ein ''Kreis'' ist ein Zyklus ohne Überkreuzungen. Das heisst, es gilt v&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; = v&amp;lt;sub&amp;gt;n&amp;lt;/sub&amp;gt; und &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1})&amp;lt;/math&amp;gt; ist ein Pfad.&lt;br /&gt;
:* Ein Knoten w ∈ V ist von einem anderen Knoten v ∈ V aus ''erreichbar'' genau dann, wenn ein Weg (v, ..., w) existiert. Wir schreiben dann &amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt;.&lt;br /&gt;
In einem ungerichteten Graph ist die Erreichbarkeits-Relation stets symmetrisch, das heisst aus &amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; folgt &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. In einem gerichteten Graphen ist dies im allgemeinen nicht der Fall.&lt;br /&gt;
&lt;br /&gt;
Bestimmte Wege haben spezielle Namen&lt;br /&gt;
&lt;br /&gt;
;Eulerweg: Ein Eulerweg ist ein Weg, der alle '''Kanten''' genau einmal enthält.&lt;br /&gt;
&lt;br /&gt;
Die eingangs erwähnte Frage des Königsberger Brückenproblems ist equivalent zu der Frage, ob der dazugehörige Graph einen Eulerweg besitzt (daher der Name). Ein anderes bekanntes Beispiel ist das &amp;quot;Haus vom Nikolaus&amp;quot;: Wenn man diesen Graphen in üblicher Weise in einem Zug zeichnet, erhält man gerade den Eulerweg. &lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O----O&lt;br /&gt;
  | \/ |&lt;br /&gt;
  | /\ |   &amp;quot;Das Haus vom Nikolaus&amp;quot;: Alle ''Kanten'' werden nur ''einmal'' passiert&lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Hamiltonweg: Ein Hamiltonweg ist ein Weg, der alle '''Knoten''' genau einmal enthält. Das &amp;quot;Haus vom Nikolaus&amp;quot; besitzt auch einen Hamiltonweg:&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /   &lt;br /&gt;
  O----O&lt;br /&gt;
     /  &lt;br /&gt;
    /      Alle ''Knoten'' werden nur ''einmal'' passiert&lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Hamiltonkreis: Ein Hamiltonkreis ist ein Kreis, der alle '''Knoten''' genau einmal enthält. Auch ein solches Gebilde ist im Haus von Nilolaus enthalten:&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O    O&lt;br /&gt;
  |    |   v0 = vn&lt;br /&gt;
  |    |   vi != vj   Für Alle i,j   i !=j; i,j &amp;gt;0; i,j &amp;lt; n&lt;br /&gt;
  O----O     &lt;br /&gt;
&lt;br /&gt;
Die folgende Skizze zeigt hingegen einen Zyklus: Der Knoten rechts unten sowie die untere Kante sind zweimal enthalten (die Kante einmal von links nach rechts und einmal von rechts nach links):&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O    O&lt;br /&gt;
    \  |&lt;br /&gt;
     \ |   Zyklus&lt;br /&gt;
  O====O&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Zusammenhang, Zusammenhangskomponenten: Ein ungerichteter Graph G heißt ''zusammenhängend'', wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt;&lt;br /&gt;
:Ein gerichteter Graph G ist zusammenhängend, wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; '''oder''' &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. &lt;br /&gt;
:Er ist ''stark zusammenhängend'', wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; '''und''' &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. &lt;br /&gt;
:Entsprechende Definitionen gelten für Teilgraphen G'. Ein Teilgraph G' heisst ''Zusammenhangskomponente'' von G, wenn er ein ''maximaler'' zusammenhängender Teilgraph ist, d.h. wenn G' zusammenhängend ist, und man keine Knoten und Kanten aus G mehr zu G' hinzufügen kann, so dass G' immer noch zusammenhängend bleibt. Entsprechend definiert man ''starke Zusammenhangskomponenten'' in einem gerichteten Graphen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Planarer Graph, ebener Graph: Ein Graph heißt ''planar'', wenn er so in einer Ebene gezeichnet werden ''kann'', dass sich die Kanten nicht schneiden (außer an den Knoten). Ein Graph heißt ''eben'', wenn er tatsächlich so gezeichnet ''ist'', dass sich die Kanten nicht schneiden. Die Einbettung in die Ebene ist im allgemeinen nicht eindeutig.&lt;br /&gt;
&lt;br /&gt;
'''Beispiele:'''&lt;br /&gt;
&lt;br /&gt;
Der folgende Graph ist planar und eben:&lt;br /&gt;
 &lt;br /&gt;
      O&lt;br /&gt;
     /|\&lt;br /&gt;
    / O \&lt;br /&gt;
   / / \ \&lt;br /&gt;
   O     O&lt;br /&gt;
&lt;br /&gt;
Das &amp;quot;Haus vom Nikolaus&amp;quot; ist ebenfalls planar, wird aber üblicherweise nicht als ebener Graph gezeichnet, weil sich die Diagonalen auf der Wand überkreuzen:&lt;br /&gt;
 &lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O----O&lt;br /&gt;
  | \/ |&lt;br /&gt;
  | /\ |   &lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
Eine ebene Einbettung dieses Graphen wird erreicht, wenn man eine der Diagonalen ausserhalb des Hauses zeichnet. Der Graph (also die Menge der Knoten und Kanten) ändert sich dadurch nicht.&lt;br /&gt;
 &lt;br /&gt;
      O  &lt;br /&gt;
     /  \&lt;br /&gt;
  --O----O&lt;br /&gt;
 /  |  / |&lt;br /&gt;
 |  | /  |   &lt;br /&gt;
 |  O----O      Das &amp;quot;Haus vom Nikolaus&amp;quot; als ebener Graph gezeichnet.&lt;br /&gt;
  \     /&lt;br /&gt;
   -----&lt;br /&gt;
&lt;br /&gt;
Eine alternative Einbettung erhalten wir, wenn wir die andere Diagonale außerhalb des Hauses zeichnen:&lt;br /&gt;
 &lt;br /&gt;
      O  &lt;br /&gt;
     /  \&lt;br /&gt;
    O----O--|&lt;br /&gt;
    | \  |  |&lt;br /&gt;
    |  \ |  | &lt;br /&gt;
    O----O  |     Alternative Einbettung des &amp;quot;Haus vom Nikolaus&amp;quot;.&lt;br /&gt;
    |       |&lt;br /&gt;
    |-------|&lt;br /&gt;
&lt;br /&gt;
Jede Einbettung eines planaren Graphen (also jeder ebene Graph) definiert eine eindeutige Menge von ''Regionen'':&lt;br /&gt;
&lt;br /&gt;
 |----O   @&lt;br /&gt;
 |   /@ \&lt;br /&gt;
 |  O----O&lt;br /&gt;
 |  |@ / |&lt;br /&gt;
 |  | / @|   &lt;br /&gt;
 |  O----O        @ entspricht jeweils einer ''Region''. Auch ausserhalb der Figur ist eine Region (die sogenannte ''unendliche'' Region).&lt;br /&gt;
 |@      |&lt;br /&gt;
 |-------|&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Der vollständige Graph K5 ist kein planarer Graph, da sich zwangsweise Kanten schneiden, wenn man diesen Graphen in der Ebene zeichnet.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
;Dualer Graph: Jeder ebene Graph G = (V, E) hat einen ''dualen Graphen'' D = (V&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;, E&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;), dessen Knoten und Kanten wie folgt definiert sind:&lt;br /&gt;
:* V&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt; enthält einen Knoten für jede Region des Graphen G&lt;br /&gt;
:* Für jede Kante e ∈ E gibt es eine duale Kante e&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt; ∈ E&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;, die die an e angrenzenden Regionen (genauer: die entsprechenden Knoten in D) verbindet.&lt;br /&gt;
&lt;br /&gt;
Die folgende Abbildung zeigt einen Graphen (grau) und seinen dualen Graphen (schwarz). Die Knoten des dualen Graphen sind mit Zahlen gekennzeichnet und entsprechen den Regionen des Originalgraphen. Jeder (grauen) Kante des Originalgraphen entspricht eine (schwarze) Kante des dualen Graphen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
[[Image:dual-graphs.png]]&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für duale Graphen gilt: Wenn der Originalgraph zusammenhängend ist, enthält jede Region des dualen Graphen genau einen Knoten des Originalgraphen. Deshalb ist der duale Graph des dualen Graphen wieder der Originalgraph. Bei nicht-zusammenhängenden Graphen gilt dies nicht (vgl. das Fenster bei obigem Bild). In diesem Fall hat der duale Graph mehrere mögliche Einbettungen in die Ebene (man kann z.B. die rechte Kante zwischen Knoten 2 und 4 auch links vom Fenster einzeichnen), und man erhält nicht notwendigerweise den Originalgraphen, wenn man den dualen Graphen des dualen berechnet.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Baum: Ein ''Baum'' ist ein zusammenhängender, kreisfreier Graph.&lt;br /&gt;
&lt;br /&gt;
Beispiel: Binärer Suchbaum&lt;br /&gt;
&lt;br /&gt;
;Spannbaum: Ein ''Spannbaum'' eines zusammenhängenden Graphen G ist ein zusammenhängender, kreisfreier Teilgraph von G, der alle Knoten von G enthält&lt;br /&gt;
&lt;br /&gt;
Beispiel: Spannbaum für das &amp;quot;Haus des Nikolaus&amp;quot; &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    O   &lt;br /&gt;
   /       &lt;br /&gt;
  O    O&lt;br /&gt;
  |  /  &lt;br /&gt;
  | /   &lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
Der Spannbaum eines Graphen mit |V| Knoten hat stets |V| - 1 Kanten.&lt;br /&gt;
&lt;br /&gt;
;Wald: Ein ''Wald'' ist ein unzusammenhängender, kreisfreier Graph.&lt;br /&gt;
: Jede Zusammenhangskomponente eines Waldes ist ein Baum.&lt;br /&gt;
&lt;br /&gt;
=== Repräsentation von Graphen ===&lt;br /&gt;
&lt;br /&gt;
Sei G = ( V, E ) gegeben und liege V in einer linearen Sortierung vor.&amp;lt;br/&amp;gt; &lt;br /&gt;
:::&amp;lt;math&amp;gt;V = \{ v_1, ...., v_n \}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Adjazenzmatrix: Ein Graph kann durch eine Adjazenzmatrix repräsentiert werden, die soviele Zeilen und Spalten enthält, wie der Graph Knoten hat. Die Elemente der Adjazenzmatrix sind &amp;quot;1&amp;quot;, falls eine Kante zwischen den zugehörigen Knoten existiert:&lt;br /&gt;
:::&amp;lt;math&amp;gt;\mathrm{\bold A} = a_{ij} = &lt;br /&gt;
\begin{cases}&lt;br /&gt;
1 &amp;amp; \mathrm{falls}\quad (v_i, v_j) \in E \\&lt;br /&gt;
0 &amp;amp; \mathrm{sonst}&lt;br /&gt;
\end{cases} &lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
:Die Indizes der Matrix entsprechen also den Indizes der Knoten gemäß der gegebenen Sortierung. Im Falle eines ungerichteten Graphen ist die Adjazenzmatrix stets symmetrisch (d.h. es gilt &amp;lt;math&amp;gt;a_{ij}=a_{ji}&amp;lt;/math&amp;gt;), bei einem gerichteten Graphen ist sie im allgemeinen unsymmetrisch.&lt;br /&gt;
&lt;br /&gt;
Beispiel für einen ungerichteten Graphen:&lt;br /&gt;
&lt;br /&gt;
 v = { a,b,c,d }     b      d&lt;br /&gt;
                     | \  / |&lt;br /&gt;
                     |  \/  |&lt;br /&gt;
                     |  /\  |&lt;br /&gt;
                     | /  \ |&lt;br /&gt;
                     a      c&lt;br /&gt;
 &lt;br /&gt;
       a b c d&lt;br /&gt;
      -----------&lt;br /&gt;
      (0 1 0 1) |a &lt;br /&gt;
  A = (1 0 1 0) |b&lt;br /&gt;
      (0 1 0 1) |c&lt;br /&gt;
      (1 0 1 0) |d&lt;br /&gt;
&lt;br /&gt;
Die Adjazenzmatrixdarstellung eignet sich besonders für dichte Graphen (d.h. wenn die Zahl der Kanten in O(|V|&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt;) ist.&lt;br /&gt;
&lt;br /&gt;
;Adjazenzlisten: In der Adjazenzlistendarstellung wird der Graph als Liste von Knoten repräsentiert, die für jeden Knoten einen Eintrag enthält. Der Eintrag für jeden Knoten ist wiederum eine Liste, die die Nachbarknoten dieses Knotens enthält:&lt;br /&gt;
:* graph = {adjazencyList(v) | v ∈ V}&lt;br /&gt;
:* adjazencyList(v) = {v' ∈ V | (v, v') ∈ E}&lt;br /&gt;
&lt;br /&gt;
In Python implementieren wir Adjazenzlisten zweckmäßig als Array von Arrays:&lt;br /&gt;
&lt;br /&gt;
                   graph = [[...],[...],...,[...]]&lt;br /&gt;
 Adjazenzliste für Knoten =&amp;gt;  0     1         n&lt;br /&gt;
&lt;br /&gt;
Wenn wir bei dem Graphen oben die Knoten wie bei der Adjazenzmatrix indizieren (also &amp;lt;tt&amp;gt;a =&amp;gt; 0&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;b =&amp;gt; 1&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;c =&amp;gt; 2&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;d =&amp;gt; 3&amp;lt;/tt&amp;gt;), erhalten wir die Adjazenzlistendarstellung:&lt;br /&gt;
&lt;br /&gt;
 graph = [[b, d], [a, c],[b, d], [a, c]]&lt;br /&gt;
&lt;br /&gt;
Auf die Nachbarknoten eines durch seinen Index &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; gegebenen Knotens können wir also wie folgt zugreifen:&lt;br /&gt;
&lt;br /&gt;
      for neighbors in graph[node]:&lt;br /&gt;
          ... # do something with neighbor&lt;br /&gt;
&lt;br /&gt;
Die Adjazenzlistendarstellung ist effizienter, wenn der Graph nicht dicht ist, so dass viele Einträge der Adjazenzmatrix Null wären. In der Vorlesung werden wir nur diese Darstellung verwenden.&lt;br /&gt;
&lt;br /&gt;
;&amp;lt;div id=&amp;quot;transposed_graph&amp;quot;&amp;gt;Transponierter Graph&amp;lt;/div&amp;gt;: Den ''transponierten Graphen'' G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; eines gerichteten Graphen G erhält man, wenn man alle Kantenrichtungen umkehrt.&lt;br /&gt;
&lt;br /&gt;
Bei ungerichteten Graphen hat die Transposition offensichtlich keinen Effekt, weil alle Kanten bereits in beiden Richtungen vorhanden sind, so dass G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; = G gilt. Bei gerichteten Graphen ist die Transposition einfach, wenn der Graph als Adjazenzmatrix implementiert ist, weil man einfach die transponierte Adjazenzmatrix verwenden muss (beachte, dass sich die Reihenfolge der Indizes umkehrt):&lt;br /&gt;
:::A&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; = a&amp;lt;sub&amp;gt;ji&amp;lt;/sub&amp;gt;&lt;br /&gt;
Ist der Graph hingegen durch eine Adjazenzliste repräsentiert, muss etwas mehr Aufwand getrieben werden:&lt;br /&gt;
&lt;br /&gt;
 def transposeGraph(graph):&lt;br /&gt;
      gt = [[] for k in graph]   # zunächst leere Adjazenzlisten von G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt;&lt;br /&gt;
      for node in range(len(graph)):&lt;br /&gt;
           for neighbor in graph[node]:&lt;br /&gt;
               gt[neighbor].append(node)  # füge die umgekehrte Kante in G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; ein&lt;br /&gt;
      return gt&lt;br /&gt;
&lt;br /&gt;
== Durchlaufen von Graphen (Graph Traversal) ==&lt;br /&gt;
&lt;br /&gt;
Wir betrachten zunächst ungerichtete Graphen mit V Knoten und E Kanten. Eine grundlegende Aufgabe in diesen Graphen besteht darin, alle Knoten in einer bestimmten Reihenfolge genau einmal zu besuchen. Hierbei darf man sich von einem gegebenen Startknoten aus nur entlang der Kanten des Graphen bewegen. Die beim Traversieren benutzen Kanten bilden einen Baum, dessen Wurzel der Startknoten ist und der den gesamten Graphen aufspannt, falls der Graph zusammenhängend ist. (Beweis: Da jeder Knoten nur einmal besucht wird, gibt es für jeden besuchten Knoten [mit Ausnahme des Startknotens] genau eine eingehende Kante. Ist der Graph zusammenhängend, wird jeder Knoten tatsächlich erreicht und es gibt genau (V-1) Kanten, exakt soviele wie für einen Baum mit V Knoten notwendig sind.) Ist der Graph nicht zusammenhängend, wird jeder zusammenhängende Teilgraph (jede &amp;lt;i&amp;gt;Zusammenhangskomponente&amp;lt;/i&amp;gt;) getrennt traversiert, und man erhält einen sogenannten &amp;lt;i&amp;gt;Wald&amp;lt;/i&amp;gt; mit einem Baum pro Zusammenhangskomponente. Die beiden grundlegenden Traversierungsmethoden &amp;lt;i&amp;gt;Tiefensuche&amp;lt;/i&amp;gt; und &amp;lt;i&amp;gt;Breitensuche&amp;lt;/i&amp;gt; werden im folgenden vorgestellt.&lt;br /&gt;
&lt;br /&gt;
=== Tiefensuche in Graphen (Depth First Search, DFS) ===&lt;br /&gt;
&lt;br /&gt;
Die Idee der Tiefensuche besteht darin, jeden besuchten Knoten sofort über die erste Kante wieder zu verlassen, die zu einem noch nicht besuchten Knoten führt. Man findet dadurch schnell einen möglichst langen Pfad durch den Graphen, und der Traversierungs-Baum wird zunächst in die Tiefe verfolgt, daher der Name des Verfahrens. Hat ein Knoten keine unbesuchten Nachbarknoten mehr, geht man im Baum zurück (sogenanntes &amp;lt;i&amp;gt;back tracking&amp;lt;/i&amp;gt;), bis man einen Knoten findet, der noch eine unbesuchte Nachbarn besitzt, und traversiert diese nach dem gleichen Muster. Gibt es gar keine unbesuchten Knoten mehr, kehrt die Suche zum Startknoten zurück und endet dort.&lt;br /&gt;
&lt;br /&gt;
WDie folgende rekursive Implementation der Tiefensuche erwartet den Graphen in Adjazenzlistendarstellung und beginnt die Suche beim Knoten &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt;. Die Information, ob ein Knoten bereits besucht wurde, wird im Array &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; gespeichert. Ein solches Array, das zusätzliche Informationen über die Knoten des Graphen bereitstellt, wir häufig &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; genannt.&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)  # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):              # rekursive Hilfsfunktion, die den gegebenen Knoten und dessen Nachbarn besucht&lt;br /&gt;
         if not visited[node]:     # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True  # Markiere node als besucht&lt;br /&gt;
             print node            # Ausgabe der Knotennummer - pre-order&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode)&lt;br /&gt;
&lt;br /&gt;
[[Image:Tiefens.jpg]]&lt;br /&gt;
&lt;br /&gt;
Ausgabe für den Graphen in diesem Bild (es handelt sich um einen ungerichteten Graphen, die Pfeile symbolisieren nur die Suchrichtung beim Traversal):&lt;br /&gt;
&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; dfs(graph, 1)&lt;br /&gt;
 1&lt;br /&gt;
 2&lt;br /&gt;
 4&lt;br /&gt;
 3&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
 5&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div id=&amp;quot;pre_and_post_order&amp;quot;&amp;gt;In dieser Version des Algorithmus werden die Knotennummern ausgegeben, bevor die Nachbarknoten besucht werden. Man bezeichnet die resultierende Sortierung der Knoten als &amp;lt;b&amp;gt;pre-order&amp;lt;/b&amp;gt; oder als &amp;lt;b&amp;gt;discovery order&amp;lt;/b&amp;gt;. Alternativ kann man die Knotennummern erst ausgeben, nachdem alle Nachbarn besucht wurden, also auf dem Rückweg der Rekursion. In diesem Fall spricht man von &amp;lt;b&amp;gt;post-order&amp;lt;/b&amp;gt; oder &amp;lt;b&amp;gt;finishing order&amp;lt;/b&amp;gt;:&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)  # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):              # rekursive Hilfsfunktion, die den gegebenen Knoten und dessen Nachbarn besucht&lt;br /&gt;
         if not visited[node]:     # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True  # Markiere node als besucht&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
             &amp;lt;font color=red&amp;gt;print node            # Ausgabe der Knotennummer - post-order&amp;lt;/font&amp;gt;&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode)&lt;br /&gt;
&lt;br /&gt;
Es ergibt sich jetzt die Ausgabe:&lt;br /&gt;
&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; dfs(graph, 1)&amp;lt;font color=red&amp;gt;&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
 3&lt;br /&gt;
 4&lt;br /&gt;
 5&lt;br /&gt;
 2&lt;br /&gt;
 1&amp;lt;/font&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In realem Code ersetzt man die print-Ausgaben natürlich durch anwendungsspezifische Aktionen und Berechnungen. Einige Anwendungen sind uns im Kapitel [[Suchen]] bereits begegnet. &lt;br /&gt;
; Anwendungen der Pre-Order Traversierung&lt;br /&gt;
* Kopieren eines Graphen: kopiere zuerst den besuchten Knoten, dann seine Nachbarn und die dazugehörigen Kanten (sowie die Kanten zu bereits besuchten Knoten, die in der Grundversion der Tiefensuche ignoriert werden).&lt;br /&gt;
* Bestimmen der Zusammenhangskomponenten eines Graphen (siehe unten)&lt;br /&gt;
* In einem Zeichenprogramm: fülle eine Region mit einer Farbe (&amp;quot;flood fill&amp;quot;). Dabei ist jedes Pixel ein Knoten des Graphen und wird mit seinen 4 Nachbarpixceln verbunden. Die Tiefensuche startet bei der Mausposition und endet am Rand des betreffendcen Gebiets.&lt;br /&gt;
* Falls der Graph ein Baum ist: bestimme den Abstand jedes Knotens von der Wurzel&lt;br /&gt;
* Falls der Graph ein Parse-Baum ist, wobei innere Knoten Funktionsaufrufe, Kindknoten Funktionsargumente, und Blattknoten Werte repräsentieren: drucke den zugehörigen Ausdruck aus (also immer zuerst den Funktionsnamen, dann die Argumente, die wiederum geschachtelte Funktionsaufrufe sein können).&lt;br /&gt;
; Anwendungen der Post-Order Traversierung&lt;br /&gt;
* Löschen eines Graphen: lösche zuerst die Nachbarn, dann den Knoten selbst&lt;br /&gt;
* Bestimmen einer topologischen Sortierung eines azyklischen gerichteten Graphens (siehe unten)&lt;br /&gt;
* Falls der Graph ein Baum ist: bestimme den Abstand jedes Knotens von den Blättern (also die Tiefe des Baumes, siehe Übung 5)&lt;br /&gt;
* Falls der Graph ein Parse-Baum ist: führe die zugehörige Berechnung aus (d.h. berechne zuerst die geschachtelten inneren Funktionen, dann mit diesen Ergebnissen die nächst äußeren usw., siehe Übung 5).&lt;br /&gt;
; Anwendungen, die Pre- und Post-Order benötigen&lt;br /&gt;
* Weg aus einem Labyrinth: die Pre-Order dokumentiert die Suche nach dem Weg, die Post-Order zeigt den Rückweg aus Sackgassen (siehe Übung 9).&lt;br /&gt;
Im Spezialfall, wenn der Graph ein Binärbaum ist, unterscheidet man noch eine dritte Variante der Traversierung, nämlich die &amp;lt;i&amp;gt;in-order&amp;lt;/i&amp;gt; Traversierung. In diesem Fall behandelt man den Vaterknoten nach den linken, aber vor den rechten Kindern. Diese Reihenfolge wird beim [[Suchen#Beziehungen zwischen dem Suchproblem und dem Sortierproblem|Tree Sort Algorithmus]] verwendet. Diese Sortierung verwendet man auch, wenn man einen Parse-Baum mit binären Operatoren (statt Funktionsaufrufen) ausgeben will, siehe Übung 5.&lt;br /&gt;
&lt;br /&gt;
Eine nützliche Erweiterung der Tiefensuche besteht darin, in der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; nicht nur zu dokumentieren, dass ein Knoten bereits besucht wurde, sondern auch, von welchem Knoten aus man den jeweiligen Knoten zuerst erreicht hat. Im entstehenden Tiefensuchbaum ist dies gerade der Vaterknoten, weshalb wir die verbesserte property map zweckmäßigerweise in &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; umbenennen. Für den Startknoten, also die Wurzel des Baumes, wählen wir die Konvention, dass er sein eigener Vaterknoten ist (die Konvention, dafür den Wert &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zu verwenden, scheidet aus, weil dies bereits die Tatsache signalisiert, dass ein Knoten noch nicht besucht wurde):&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     parents = [None]*len(graph)     # Registriere für jeden Knoten den Vaterknoten im Tiefensuchbaum&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, parent):        # rekursive Hilfsfunktion&lt;br /&gt;
         if parents[node] is None:   # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             parents[node] = parent  # Markiere node als besucht und speichere seinen Vaterknoten&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn ...&lt;br /&gt;
                 visit(neighbor, node)      #  ... wobei node zu deren Vaterknoten wird&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode, startnode)     # Konvention für Wurzel: startnode ist sein eigener Vater&lt;br /&gt;
     &lt;br /&gt;
     return parents                  # Rückgabe des berechneten Tiefensuch-Baums&lt;br /&gt;
&lt;br /&gt;
Die Ausgabe für den obigen Beispielgraphen lautet: &lt;br /&gt;
  Knotennummer  |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7&lt;br /&gt;
  --------------+-----+-----+-----+-----+-----+-----+-----+-----&lt;br /&gt;
  Vaterknoten   | None|  1  |  1  |  4  |  2  |  2  |  3  |  3&lt;br /&gt;
&lt;br /&gt;
Dabei ist die Knotennummer der Index im Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt;, und der Vaterknoten ist der dazugehörige Arrayeintrag. Beachte, dass Knoten 0 in diesem Graphen nicht existiert, daher ist sein Eintrag &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;. Per Konvention hat der Wurzelknoten 1 sich selbst als Vater.&lt;br /&gt;
&lt;br /&gt;
=== Breitensuche in Graphen (Breadth First Search, BFS) ===&lt;br /&gt;
&lt;br /&gt;
Im Gegensatz zur Tiefensuche werden bei der Breitensuche alle Nachbarnknoten abgearbeitet, &amp;lt;i&amp;gt;bevor&amp;lt;/i&amp;gt; man rekursiv deren Nachbarn besucht. Man betrachtet somit zuerst alle Knoten, die den Abstand 1 von Startknoten haben, dann diejenigen mit dem Abstand 2 usw. Diese Reihenfolge bezeichnet man als &amp;lt;i&amp;gt;level-order&amp;lt;/i&amp;gt;. Wir sind ihr beispielsweise in Übung 6 begegnet, als die ersten 7 Ebenen eines Treap ausgegeben werden sollten. Man implementiert Breitensuche zweckmäßig mit Hilfe einer Queue, die die Knoten in First In - First Out - Reihenfolge bearbeitet. Eine geeignete Datenstruktur hierfür ist die Klasse &amp;lt;tt&amp;gt;[http://docs.python.org/library/collections.html#collections.deque deque]&amp;lt;/tt&amp;gt; aus dem Python-Modul &amp;lt;tt&amp;gt;[http://docs.python.org/library/collections.html collections]&amp;lt;/tt&amp;gt; (eine Deque implementiert sowohl die Funktionalität einer Queue wie auch die eines Stacks, siehe Übung 3):&lt;br /&gt;
&lt;br /&gt;
 from collections import deque&lt;br /&gt;
 &lt;br /&gt;
 def bfs(graph, startnode)&lt;br /&gt;
     visited = [False]*len(graph)   # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
   &lt;br /&gt;
     q = deque()                    # Queue für die zu besuchenden Knoten&lt;br /&gt;
     q.append(startnode)            # Startknoten in die Queue einfügen&lt;br /&gt;
     &lt;br /&gt;
     while len(q) &amp;gt; 0:              # Solange es noch unbesuchte Knoten gibt&lt;br /&gt;
         node = q.popleft()         # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
         if not visited[node]:      # Falls node noch nicht (auf einem anderen Weg) besucht wurde&lt;br /&gt;
              visited[node] = True  # Markiere node als besucht&lt;br /&gt;
              print node            # Drucke Knotennummer&lt;br /&gt;
              for neighbor in graph[node]:    # Füge Nachbarn in die Queue ein&lt;br /&gt;
                  q.append(neighbor)&lt;br /&gt;
&lt;br /&gt;
[[Image:Breitens.jpg]]&lt;br /&gt;
&lt;br /&gt;
Der Aufruf dieser Funktion liefert die Knoten des obigen Graphens ebenenweise, also zufällig genau in der Reihenfolge der Knotennummern:&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; bfs(graph, 1)&lt;br /&gt;
 1&lt;br /&gt;
 2&lt;br /&gt;
 3&lt;br /&gt;
 4&lt;br /&gt;
 5&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
&lt;br /&gt;
Neben der ebenenweisen Ausgabe hat die Breitensuche viele weitere wichtige Anwendungen, z.B. beim Testen, ob ein gegebener Graph bi-partit ist (siehe [http://en.wikipedia.org/wiki/Breadth-first_search#Testing_bipartiteness WikiPedia]), sowie bei der Suche nach kürzesten Wegen (siehe unten) und kürzesten Zyklen.&lt;br /&gt;
&lt;br /&gt;
== Weitere Anwendungen der Tiefensuche ==&lt;br /&gt;
&lt;br /&gt;
Die Tiefensuche hat zahlreiche Anwendungen, wobei der grundlegende Algorithmus immer wieder leicht modifiziert und an die jeweilige Aufgabe angepasst wird. Wir beschreiben im folgenden einige Beispiele.&lt;br /&gt;
&lt;br /&gt;
=== Damenproblem ===&lt;br /&gt;
&lt;br /&gt;
Tiefensuche wird häufig verwendet, um systematisch nach der Lösung eines logischen Rätsels (oder allgemeiner nach der Lösung eines diskreten Optimierungsproblems) zu suchen. Besonders anschaulich hierfür ist das Damenproblem. Die Aufgabe besteht darin, &amp;lt;math&amp;gt;k&amp;lt;/math&amp;gt; Damen auf einem Schachbrett der Größe &amp;lt;math&amp;gt;k \times k&amp;lt;/math&amp;gt; so zu platzieren, dass sie sich (nach den üblichen Schach-Regeln) nicht gegenseitig schlagen können. Das folgende Diagramm zeigt eine Lösung für den Fall &amp;lt;math&amp;gt;k=4&amp;lt;/math&amp;gt;. Die Positionen der Damen werden dabei wie üblich durch die Angabe der Spalte (Linie) mit Buchstaben und der Zeile (Reihe) mit Zahlen kodiert, hier also A2, B4, C1, D3:&lt;br /&gt;
&lt;br /&gt;
  ---------------&lt;br /&gt;
 |   | X |   |   | 4&lt;br /&gt;
 |---|---|---|---| &lt;br /&gt;
 |   |   |   | X | 3&lt;br /&gt;
 |---|---|---|---|&lt;br /&gt;
 | X |   |   |   | 2&lt;br /&gt;
 |---|---|---|---|&lt;br /&gt;
 |   |   | X |   | 1&lt;br /&gt;
  ---------------&lt;br /&gt;
   A   B   C   D&lt;br /&gt;
&lt;br /&gt;
Um das Problem systematisch zu lösen, konstruieren wir einen gerichteten Graphen, dessen Knoten die möglichen Positionen der Damen kodieren. Wir verbinden Knoten, die zu benachbarten Linien gehören, genau dann mit einer Kante, wenn die zugehörigen Positionen kompatibel sind, also wenn sich die dort positionierten Damen nicht schlagen können. Der resultierende Graph für &amp;lt;math&amp;gt;k=4&amp;lt;/math&amp;gt; hat folgende Gestalt:&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
Knoten, die zur selben Reihe oder Linie gehören, sind beispielsweise nicht direkt verbunden, weil zwei Damen niemals in derselben Linie oder Reihe stehen dürfen. Um eine erlaubte Konfiguration zu finden, verwenden wir nun eine angepasste Version der Tiefensuche: Wir beginnen die Suche beim Knoten &amp;lt;tt&amp;gt;START&amp;lt;/tt&amp;gt;. Sobald wir den Knoten &amp;lt;tt&amp;gt;STOP&amp;lt;/tt&amp;gt; erreichen, beenden wir die Suche und lesen die Lösung am gerade gefundenen Weg von Start nach Stop ab. Zwei kleine Modifikationen des Grundalgorithmus stellen sicher, dass die Bedingungen der Aufgabe eingehalten werden: Wir dürfen bei der Tiefensuche nur dann zu einem Nachbarn weitergehen, wenn die betreffende Position mit allen im Pfad bereits gesetzten Positionen kompatibel ist, andernfalls ist diese Kante tabu. Landen wir aufgrund dieser Regel in einer Sackgasse (also in einem Knoten, wo keine der ausgehenden Kanten erlaubt ist), müssen wir zur nächsten erlaubten Abzweigung zurückgehen (Backtracking). Beim Zurückgehen müssen wir das &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Flag wieder auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zurücksetzen, weil der betreffende Knoten ja möglicherweise auf einem anderen erlaubten Weg erreichbar ist.&lt;br /&gt;
&lt;br /&gt;
Der folgende Graph zeigt einen solchen Fall: Wir haben zwei Damen auf die Felder A1 und B3 positioniert (grüne Pfeile). Die einzig ausgehende Kante von B3 führt zum Knoten C1, welcher aber mit der Position A1 inkompatibel ist, so dass diese Kante nicht verwendet werden darf (roter Pfeil). Das Backtracking muss jetzt zu Knoten A1 zurückgehen (dabei wird das &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Flag von B3 wieder auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; gesetzt), weil A1 mit der Kante nach B4 eine weitere Option hat, die geprüft werden muss (die allerdings hier auch nicht zum Ziel führt).&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph-failure.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
Nach einigen weiteren Sackgassen findet man schließlich den Pfad A2, B4, C1, D3, der im folgenden Graphen grün markiert ist und der obigen Lösung entspricht:&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph-success.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
=== Test, ob ein ungerichteter Graph azyklisch ist ===&lt;br /&gt;
&lt;br /&gt;
Ein zusammenhängender ungerichteter Graph ist azyklisch (also ein Baum) genau dann, wenn es nur einen möglichen Weg von jedem Knoten zu jedem anderen gibt. (Bei gerichteten Graphen sind die Verhältnisse komplizierter. Wir behandeln dies weiter unten.) Das kann man mittels Tiefensuche leicht feststellen: Die Kante, über die wir einen Knoten erstmals erreichen, ist eine &amp;lt;i&amp;gt;Baumkante&amp;lt;/i&amp;gt; des Tiefensuchbaums. Erreichen wir einen bereits besuchten Knoten nochmals über eine andere Kante, haben wir einen Zyklus gefunden. Dabei müssen wir allerdings beachten, dass in einem ungerichteten Graphen jede Baumkante zweimal gefunden wird, einmal in Richtung vom Vater zum Kind und einmal in umgekehrter Richtung. Im zweiten Fall endet die Kante zwar in einem bereits besuchten Knoten (dem Vater), aber es entsteht dadurch kein Zyklus. Den Vaterknoten müssen wir deshalb überspringen, wenn wir über die Nachbarn iterieren:&lt;br /&gt;
&lt;br /&gt;
 def undirected_cycle_test(graph):         # Annahme: der Graph ist zusammenhängend&lt;br /&gt;
                                           # (andernfalls führe den Algorithmus für jede Zusammenhangskomponente aus)&lt;br /&gt;
     visited = [False]*len(graph)          # Flags für bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, from_node):           # rekursive Hilfsfunktion: gibt True zurück, wenn Zyklus gefunden wurde&lt;br /&gt;
         if not visited[node]:             # wenn node noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True          # markiere node als besucht&lt;br /&gt;
             for neighbor in graph[node]:  # besuche die Nachbarn ...&lt;br /&gt;
                 if neighbor == from_node: # ... aber überspringe den Vaterknoten&lt;br /&gt;
                     continue&lt;br /&gt;
                 if visit(neighbor, node): # ... signalisiere, wenn rekursiv ein Zyklus gefunden wurde&lt;br /&gt;
                     return True&lt;br /&gt;
             return False                  # kein Zyklus gefunden&lt;br /&gt;
         else:&lt;br /&gt;
             return True                   # Knoten schon besucht =&amp;gt; Zyklus&lt;br /&gt;
     &lt;br /&gt;
     startnode = 0                         # starte bei beliebigem Knoten (hier: Knoten 0)&lt;br /&gt;
     return visit(startnode, startnode)    # gebe True zurück, wenn ein Zyklus gefunden wurde&lt;br /&gt;
&lt;br /&gt;
Wenn wir einen Zyklus finden, wird das weitere Traversieren das Graphen abgebrochen, denn ein Graph, der einmal zyklisch war, kann später nicht wieder azyklisch werden. Die notwendige Modifikation für unzusammenhängende Graphen erfolgt analog zum Algorithmus für die Detektion von Zusammenhangskomponenten, der im nächsten Abschnitt beschrieben wird.&lt;br /&gt;
&lt;br /&gt;
=== Finden von Zusammenhangskomponenten ===&lt;br /&gt;
&lt;br /&gt;
Das Auffinden und Markieren von Zusammenhangskomponenten (also maximalen zusammenhängenden Teilgraphen) ist eine grundlegende Aufgabe in ungerichteten, unzusammenhängenden Graphen (bei gerichteten Graphen sind die Verhältnisse wiederum komplizierter, siehe unten). Zwei Knoten u und v gehören zur selben Zusammenhangskomponente genau dann, wenn es einen Pfad von u nach v gibt (da der Graph ungerichtet ist, gibt es dann auch einen Pfad von v nach u). Man sagt auch, dass &amp;quot;v von u aus erreichbar&amp;quot; ist. Unzusammenhängende Graphen entstehen in der Praxis häufig, wenn die Kanten gewisse Relationen zwischen den Knoten kodieren: &lt;br /&gt;
* Wenn die Knoten Städte sind und die Kanten Straßen, sind diejenigen Städte in einer Zusammenhangskomponente, die per Auto von einander erreichbar sind. Unzusammenhängende Graphen entstehen hier beispielsweise, wenn eine Insel nicht durch eine Brücke erschlossen ist, wenn Grenzen gesperrt sind oder wenn ein Gebirge zu unwegsam ist, um Straßen zu bauen.&lt;br /&gt;
* Wenn Knoten Personen sind, und Kanten die Eltern-Kind-Relation beschreiben, so umfasst jede Zusammenhangskomponenten die Verwandten (auch wenn sie nur über viele &amp;quot;Ecken&amp;quot; verwandt sind).&lt;br /&gt;
* In der Bildverarbeitung entsprechen Knoten den Pixeln, und dieselben werden durch eine Kante verbunden, wenn sie zum selben Objekt gehören. Die Zusammenhangskomponenten entsprechen somit den Objekten im Bild (siehe Übungsaufgabe).&lt;br /&gt;
Die Zusammenhangskomponenten bilden eine Äquivalenzrelation. Folglich kann für jede Komponente ein Reprässentant bestimmt werden, der sogenannte &amp;quot;Anker&amp;quot;. Kennt jeder Knoten seinen Anker, ist das Problem der Zusammenhangskomponenten gelöst. &lt;br /&gt;
&lt;br /&gt;
==== Lösung mittels Tiefensuche ====&lt;br /&gt;
&lt;br /&gt;
Unser erster Ansatz ist, den Anker mit Hilfe der Tiefensuche zu finden. Anstelle der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; verwenden wir diesmal eine property map &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt;, die für jeden Knoten die Knotennummer des zugehörigen Ankers angibt, oder &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, wenn der Knoten noch nicht besucht wurde. Dabei verwenden wir wieder die Konvention, dass Anker auf sich selbst zeigen. Für viele Anwendungen ist es außerdem (oder stattdessen) zweckmäßig, die Zusammenhangskomponenten mit einer laufenden Nummer, einem sogenannten &amp;lt;i&amp;gt;Label&amp;lt;/i&amp;gt;, durchzuzählen. Dann kann man zusätzliche Informationen zu jeder Komponente (beispielsweise deren Größe) einfach in einem Array speichern, das über die Labels indexiert wird. Die folgende Version der Tiefensuche bestimmt sowohl die Anker als auch die Labels für jeden Knoten:&lt;br /&gt;
&lt;br /&gt;
 def connectedComponents(graph):&lt;br /&gt;
        anchors = [None] * len(graph)             # property map für Anker jedes Knotens&lt;br /&gt;
        labels  = [None] * len(graph)             # property map für Label jedes Knotens&lt;br /&gt;
        &lt;br /&gt;
        def visit(node, anchor):&lt;br /&gt;
                &amp;quot;&amp;quot;&amp;quot;anchor ist der Anker der aktuellen ZK&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
                if anchors[node] is None:         # wenn node noch nicht besucht wurde:&lt;br /&gt;
                    anchors[node] = anchor        # setze seinen Anker&lt;br /&gt;
                    labels[node] = labels[anchor] # und sein Label&lt;br /&gt;
                    for neighbor in graph[node]:  # und besuche die Nachbarn&lt;br /&gt;
                        visit(neighbor, anchor)&lt;br /&gt;
        &lt;br /&gt;
        current_label = 0                         # Zählung der ZK beginnt bei 0&lt;br /&gt;
        for node in xrange(len(graph)):&lt;br /&gt;
            if anchors[node] is None:             # Anker noch nicht bekannt =&amp;gt; neue ZK gefunden&lt;br /&gt;
                labels[node] = current_label      # Label des Ankers setzen&lt;br /&gt;
                visit(node, node)                 # Knoten der neuen ZK rekursiv suchen&lt;br /&gt;
                current_label += 1                # Label für die nächste ZK hochzählen&lt;br /&gt;
        return anchors, labels&lt;br /&gt;
Interessant ist hier die Schleife über alle Knoten des Graphen am Ende des Algorithmus, die bei den bisherigen Versionen der Tiefensuche nicht vorhanden war. Um ihre Funktionsweise zu verstehen, nehmen wir für den Moment an, dass der Graph zusammenhängend ist. Dann findet diese Schleife den ersten Knoten des Graphen und führt die Tiefensuche mit diesem Knoten als Startknoten aus. Sobald die Rekursion zurückkehrt, sind alle Knoten des Graphen besucht (weil der Graph ja zusammenhängend war), so dass die Schleife alle weiteren Knoten überspringt (die if-Anweisung liefert für keinen weiteren Knoten True). Bei unzusammenhängenden Graphen dagegen erreicht die Tiefensuche nur die Knoten derselben Komponente, die im weiteren Verlauf der Schleife übersprungen werden. Findet die if-Anweisung jetzt einen noch nicht besuchten Knoten, muss dieser folglich in einer neuen Komponente liegen. Wir verwenden diesen Knoten als Anker und bestimmen die übrigen Knoten dieser Komponente wiederum mit Tiefensuche.&lt;br /&gt;
&lt;br /&gt;
* Beispiel: ... &amp;lt;b&amp;gt; under construction &amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Man erkennt, dass die Tiefensuche nach dem &amp;lt;i&amp;gt;Anlagerungsprinzip&amp;lt;/i&amp;gt; vorgeht: Beginnend vom einem Startknoten (dem Anker) werden die Knoten der aktuellen Komponente nach und nach an den Tiefensuchbaum angehangen. Erst, wenn nichts mehr angelagert werden kann, geht der Algorithmus zur nächsten Komponente über.&lt;br /&gt;
&lt;br /&gt;
==== Lösung mittels Union-Find-Algorithmus ====&lt;br /&gt;
&lt;br /&gt;
Im Gegensatz zum Anlagerungsprinzip sucht der Union-Find-Algorithmus die Zusammenhangskomponenten mit dem &amp;lt;i&amp;gt;Verschmelzungsprinzip&amp;lt;/i&amp;gt;: Eingangs wird jeder Knoten als ein Teilgraph für sich betrachtet. Dann iteriert man über alle Kanten und verbindet deren Endknoten jeweils zu einem gemeinsamen Teilgraphen (falls die beiden Enden einer Kante bereits im selben Teilgraphen liegen, wird diese Kante ignoriert). Solange noch Kanten vorhanden sind, werden dadurch immer wieder Teilgraphen in größere Teilgraphen verschmolzen. Am Ende bleiben die maximalen zusammenhängenden Teilgraphen (also gerade die Zusammenhangskomponenten) übrig. Dieser Algorithmus kommt ohne Tiefensuche aus und ist daher in der Praxis oft schneller, allerdings auch etwas komplizierter zu implementieren.&lt;br /&gt;
&lt;br /&gt;
Der Schlüssel des Algorithmus ist eine Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt;, die zu jedem Knoten den aktuellen Anker sucht. Der Anker existiert immer, da jeder Knoten von Anfang an zu einem Teilgraphen gehört (anfangs ist jeder Teilgraph trivial und besteht nur aus dem Knoten selbst). Die Verschmelzung wird realisiert, indem der Anker des einen Teilgraphen seine Rolle verliert und stattdessen der Anker des anderen Teilgraphen eingesetzt wird. &lt;br /&gt;
&lt;br /&gt;
Zur Verwaltung der Anker verwenden wir wieder eine property map &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt; mit der Konvention, dass die Anker auf sich selbst verweisen. Es wäre jedoch zu teuer, wenn man bei jeder Verschmelzung alle Anker-Einträge der beteiligten Knoten aktualisieren müsste, da jeder Knoten im Laufe des Algorithmus mehrmals seinen Anker wechseln kann. Statt dessen definiert man Anker rekursiv: Verweist ein Knoten auf einen Anker, der mittlerweile diese Rolle verloren hat, folgt man dem Verweis von diesem Knoten (dem ehemaligen Anker) weiter, bis man einen tatsächlichen Anker gefunden hat - erkennbar daran, dass er auf sich selbst verweist. Diese Suchfunktion kann folgendermassen implementiert werden:&lt;br /&gt;
&lt;br /&gt;
  def findAnchor(anchors, node):&lt;br /&gt;
      while node != anchors[node]:   # wenn node kein Anker ist&lt;br /&gt;
          node = anchors[node]       # ... verfolge die Ankerkette weiter&lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Allerdings kann diese Kette im Laufe vieler Verschmelzungen sehr lang werden, so dass das Verfolgen der Kette teuer wird. Man vermeidet dies durch die sogenannte &amp;lt;i&amp;gt;Pfadkompression&amp;lt;/i&amp;gt;: Immer, wenn man den Anker gefunden hat, aktualisiert man den Eintrag am Anfang der Kette. Die Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt; wird dadurch nur wenig komplizierter:&lt;br /&gt;
&lt;br /&gt;
  def findAnchor(anchors, node):&lt;br /&gt;
      start = node                   # wir merken uns den Anfang der Kette&lt;br /&gt;
      while node != anchors[node]:   # wenn node kein Anker ist&lt;br /&gt;
          node = anchors[node]       # ... verfolge die Ankerkette weiter&lt;br /&gt;
      anchors[start] = node          # Pfadkompression: aktualisiere den Eintrag am Anfang der Kette&lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Man kann zeigen, dass die Ankersuche mit Pfadkompression zu einer fast konstanten amortisierten Laufzeit pro Aufruf führt.&lt;br /&gt;
&lt;br /&gt;
Um mit jeder Kante des (ungerichteten) Graphen nur maximal einmal eine Verschmelzung durchzuführen, betrachten wir jede Kante nur in der Richtung von der kleineren zur größeren Knotennummer, die umgekehrte Richtung wird ignoriert. Außerdem ist es zweckmäßig, bei jeder Verschmelzung denjenigen Anker mit der kleineren Knotennummer als neuen Anker zu übernehmen. Dann gilt für jede Zusammenhangskomponente, dass gerade der Knoten mit der kleinsten Knotennummer der Anker ist (genau wie bei der Lösung mittels Tiefensuche), was die weitere Analyse vereinfacht, z.B. die Zuordnung der Labels zu den Komponenten am Ende des Algorithmus. &lt;br /&gt;
&lt;br /&gt;
 def unionFindConnectedComponents(graph):&lt;br /&gt;
     anchors = range(len(graph))        # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):    # iteriere über alle Knoten&lt;br /&gt;
         for neighbor in graph[node]:   # ... und über deren ausgehende Kanten&lt;br /&gt;
             if neighbor &amp;lt; node:        # ignoriere Kanten, die in falscher Richtung verlaufen&lt;br /&gt;
                 continue&lt;br /&gt;
             # hier landen wir für jede Kante des Graphen genau einmal&lt;br /&gt;
             a1 = findAnchor(anchors, node)       # finde Anker ...&lt;br /&gt;
             a2 = findAnchor(anchors, neighbor)   # ... der beiden Endknoten&lt;br /&gt;
             if a1 &amp;lt; a2:                          # Verschmelze die beiden Teilgraphen&lt;br /&gt;
                 anchors[a2] = a1                 # (verwende den kleineren der beiden Anker als Anker des&lt;br /&gt;
             elif a2 &amp;lt; a1:                        #  entstehenden Teilgraphen. Falls node und neighbor &lt;br /&gt;
                 anchors[a1] = a2                 #  den gleichen Anker haben, waren sie bereits im gleichen&lt;br /&gt;
                                                  #  Teilgraphen, und es passiert hier nichts.)&lt;br /&gt;
     # Bestimme jetzt noch die Labels der Komponenten&lt;br /&gt;
     labels = [None]*len(graph)         # Initialisierung der property map für Labels&lt;br /&gt;
     current_label = 0                  # die Zählung beginnt bei 0&lt;br /&gt;
     for node in xrange(len(graph)):&lt;br /&gt;
         a = findAnchor(anchors, node)  # wegen der Pfadkompression zeigt jeder Knoten jetzt direkt auf seinen Anker&lt;br /&gt;
         if a == node:                  # node ist ein Anker&lt;br /&gt;
             labels[a] = current_label  # =&amp;gt; beginne eine neue Komponente&lt;br /&gt;
             current_label += 1         # und zähle Label für die nächste ZK hoch&lt;br /&gt;
         else:&lt;br /&gt;
             labels[node] = labels[a]   # node ist kein Anker =&amp;gt; setzte das Label des Ankers&lt;br /&gt;
                                        # (wir wissen, dass labels[a] bereits gesetzt ist, weil &lt;br /&gt;
                                        #  der Anker immer der Knoten mit der kleinsten Nummer ist)&lt;br /&gt;
     return anchors, labels&lt;br /&gt;
 &lt;br /&gt;
* Beispiel: ... &amp;lt;b&amp;gt;under construction&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Kürzeste Wege (Pfade) ==&lt;br /&gt;
&lt;br /&gt;
Eine weitere grundlegende Aufgabe in Graphen ist die Bestimmung eines kürzesten Weges zwischen zwei gegebenen Knoten. Dies hat offensichtliche Anwendungen bei Routenplanern und Navigationssystemen und ist darüber hinaus wichtiger Bestandteil anderer Algorithmen, z.B. bei der Berechnung eines maximalen Flusses mit der [http://en.wikipedia.org/wiki/Edmonds%E2%80%93Karp_algorithm Methode von Edmonds und Karp].&lt;br /&gt;
&lt;br /&gt;
=== Kürzeste Wege in ungewichteten Graphen mittels Breitensuche ===&lt;br /&gt;
&lt;br /&gt;
Im Fall eines ungewichteten Graphen ist die Länge eines Weges einfach durch die Anzahl der durchlaufenen Kanten definiert. Daraus folgt, dass kürzeste Pfade mit einer leicht angepassten Version der Breitensuche gefunden werden können: Aufgrund des first in-first out-Verhaltens der Queue betrachtet die Breitensuche alle (erreichbaren) Knoten in der Reihenfolge ihres Abstandes vom Startknoten. Wenn wir den Zielknoten zum ersten Mal erreichen, und der gerade gefundene Weg vom Start zum Ziel hat die Länge L, muss dies der kürzeste Weg sein: Alle möglichen Wege der Länge L' &amp;amp;lt; L hat die Breitensuche ja bereits betrachtet, ohne dass dabei der Zielknoten erreicht wurde. Daraus folgt übrigens eine allgemeine Eigenschaft aller Algorithmen für kürzeste Wege: Wenn der kürzeste Weg vom Start zum Ziel die Länge L hat, finden diese Algorithmen als Nebenprodukt auch die kürzesten Wege zu allen Knoten, für die L' &amp;amp;lt; L gilt. &lt;br /&gt;
&lt;br /&gt;
Um den Algorithmus zu implementieren, passen wir die Breitensuche so an, dass anstelle der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; eine property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; verwendet wird, die für jeden besuchten Knoten den Vaterknoten im Breitensuchbaum speichert. Durch Rückverfolgen der &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette können wir den Pfad vom Ziel zum Start rekonstruieren, und durch Umdrehen der Reihenfolge erhalten wir den gesuchten Pfad vom Start zum Ziel. Sobald der Zielknoten erreicht wurde, können wir die Breitensuche abbrechen (&amp;lt;tt&amp;gt;break&amp;lt;/tt&amp;gt;-Befehl in der ersten &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Schleife). Falls der gegebene Graph unzusammenhängend ist, kann es passieren, dass gar kein Weg gefunden wird, weil Start und Ziel in verschiedenen Zusammenhangskomponenten liegen. Dies erkennen wir daran, dass die Breitensuche beendet wurde, ohne den Zielknoten zu besuchen. Dann gibt die Funktion statt eines Pfades dern Wert &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zurück:&lt;br /&gt;
&lt;br /&gt;
  from collections import deque&lt;br /&gt;
  &lt;br /&gt;
  def shortestPath(graph, startnode, destination):&lt;br /&gt;
      parents = [None]*len(graph)      # Registriere für jeden Knoten den Vaterknoten im Breitensuchbaum&lt;br /&gt;
      parents[startnode] = startnode   # startnode ist die Wurzel des Baums =&amp;gt; verweist auf sich selbst&lt;br /&gt;
      &lt;br /&gt;
      q = deque()                      # Queue für die zu besuchenden Knoten&lt;br /&gt;
      q.append(startnode)              # Startknoten in die Queue einfügen&lt;br /&gt;
      &lt;br /&gt;
      while len(q) &amp;gt; 0:                # Solange es noch unbesuchte Knoten gibt&lt;br /&gt;
          node = q.popleft()           # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
          if node == destination:      # Zielknoten erreicht&lt;br /&gt;
              break                    #   =&amp;gt; Suche beenden&lt;br /&gt;
          for neighbor in graph[node]: # Besuche die Nachbarn von node&lt;br /&gt;
              if parents[neighbor] is None:  # aber nur, wenn sie noch nicht besucht wurden&lt;br /&gt;
                  parents[neighbor] = node   # setze node als Vaterknoten&lt;br /&gt;
                  q.append(neighbor)         # und füge neighbor in die Queue ein&lt;br /&gt;
      &lt;br /&gt;
      if parents[destination] is None: # Breitensuche wurde beendet ohne den Zielknoten zu besuchen&lt;br /&gt;
          return None                  # =&amp;gt; kein Pfad gefunden (unzusammenhängender Graph)&lt;br /&gt;
      &lt;br /&gt;
      # Pfad durch die parents-Kette zurückverfolgen und speichern&lt;br /&gt;
      path = [destination]&lt;br /&gt;
      while path[-1] != startnode:&lt;br /&gt;
          path.append(parents[path[-1]])&lt;br /&gt;
      path.reverse()     # Reihenfolge umdrehen (Ziel =&amp;gt; Start wird zu Start =&amp;gt; Ziel)&lt;br /&gt;
      return path        # gefundenen Pfad zurückgeben&lt;br /&gt;
&lt;br /&gt;
=== Gewichtete Graphen ===&lt;br /&gt;
&lt;br /&gt;
Das Problem der Suche nach kürzesten Wegen wird wesentlich interessanter und realistischer, wenn wir zu gewichteten Graphen übergehen:&lt;br /&gt;
&lt;br /&gt;
; Definition - kantengewichteter Graph&lt;br /&gt;
: Jeder Kante (s,t) des Graphen ist eine reelle oder natürliche Zahl w&amp;lt;sub&amp;gt;st&amp;lt;/sub&amp;gt; zugeordnet, die üblicherweise als ''Kantengewicht'' bezeichnet wird.&lt;br /&gt;
&lt;br /&gt;
; Definition - knotengewichteter Graph&lt;br /&gt;
: Jedem Knoten v des Graphen ist eine reelle oder natürliche Zahl w&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; zugeordnet, die üblicherweise als ''Knotengewicht'' bezeichnet wird.&lt;br /&gt;
&lt;br /&gt;
Je nach Anwendung benötigt man Knoten- oder Kantengewichte oder auch beides zugleich. Wir beschränken uns in der Vorlesung auf kantengewichtete Graphen. Beispiele für die Informationen, die man durch Kantengewichte ausdrücken kann, sind&lt;br /&gt;
* wenn die Knoten Orte sind: Abstand von Anfangs- und Endknoten jeder Kante (z.B. Luftline oder Straßenentfernung), Fahrzeit zwischen den Orten&lt;br /&gt;
* wenn der Knoten ein Rohrnetzwerk beschreibt: Durchflusskapazität der einzelnen Rohre (für max-Flussprobleme), analog bei elektrischen Netzwerken: elektrischer Widerstand&lt;br /&gt;
* wenn die Knoten Währungen repräsentieren, können deren Wechselkurse durch Kantengewichte angegeben werden.&lt;br /&gt;
Bei einigen Beispielen ergeben sich unterschiedliche Kantengewichte, wenn eine Kante von s nach t anstatt von t nach s durchlaufen wird. Beispielsweise können sich die Fahrzeiten erheblich unterscheiden, wenn es in einer Richtung bergauf, in der anderen bergab geht, obwohl die Entfernung in beiden Fällen gleich ist. Hier ergibt sich natürlicherweise ein gerichteter Graph. In anderen Beispielen (z.B. bei Luftlinienentfernungen, in guter Näherung auch bei Straßenentfernungen) sind die Gewichte von der Richtung unabhängig, so dass wir ungerichtete Graphen verwenden können.&lt;br /&gt;
&lt;br /&gt;
Die Repräsentation der Kantengewichte im Programm richtet sich nach der Repräsentation des Graphen selbst. Am einfachsten ist wiederum die Adjazenzmatrix, die aber nur für dichte Graphen (&amp;lt;math&amp;gt;E = O(V^2)&amp;lt;/math&amp;gt;, mit E als Anzahl der Kanten und V als Anzahl der Knoten) effizient ist. Bei gewichteten Graphen gibt das Matrixelement a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; das Gewicht der Kante i &amp;amp;rArr; j (wobei a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; = 0 gesetzt wird, wenn diese Kante nicht existiert). Wie zuvor gilt für ungerichtete Graphen a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; = a&amp;lt;sub&amp;gt;ji&amp;lt;/sub&amp;gt; (symmetrische Matrix), während dies für gerichtete Graphen nicht gelten muss.&lt;br /&gt;
&lt;br /&gt;
Bei Graphen in Adjazenzlistendarstellung hat es sich bewährt, die Gewichte in einer &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; zu speichern. Weiter oben haben wir bereits property maps für Knoteneigenschaften (z.B. &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt;) gesehen. Property maps für Kanten funktionieren ganz analog, allerdings muss man jetzt Paare von Knoten (nämlich Anfangs- und Endknoten der Kante) als Schlüssel verwenden und die Daten entsprechend in einem assoziativen Array ablegen:&lt;br /&gt;
  w = weights[(i,j)]   # Zugriff auf das Gewicht der Kante i &amp;amp;rArr; j&lt;br /&gt;
Alternativ könnte man auch die Graph-Datenstruktur selbst erweitern, aber dies ist weniger zu empfehlen, weil jeder Algorithmus andere Erwiterungen benötigt und damit die Datenstruktur sehr unübersichtlich würde.&lt;br /&gt;
&lt;br /&gt;
Der kürzeste Weg ist nun definiert als der Weg, bei dem die Summe der Kantengewichte minimal ist:&lt;br /&gt;
;Definition - Problem des kürzesten Weges&lt;br /&gt;
: Sei P die Menge aller Wege von u nach v, und &amp;lt;math&amp;gt;p \in P&amp;lt;/math&amp;gt; einer dieser Wege. Wenn der Grpah einfach ist (es also keine Mehrfachkanten zwischen denselben Knoten und keine Schleifen gibt), ist der Weg p durch die Folge der besuchten Knoten eindeutig bestimmt:&lt;br /&gt;
: &amp;lt;math&amp;gt;p : \ \ u = x_0 \rightarrow x_1 \rightarrow x_2 \rightarrow ... \rightarrow v = x_{n_p}&amp;lt;/math&amp;gt;&lt;br /&gt;
:wo &amp;lt;math&amp;gt;n_p&amp;lt;/math&amp;gt; die Anzahl der Kanten im Weg p ist. Seine Kosten W&amp;lt;sub&amp;gt;p&amp;lt;/sub&amp;gt; ergeben sich als Summer der Gewichte der einzelnen Kanten&lt;br /&gt;
: &amp;lt;math&amp;gt;W_p = \sum_{k=1}^{n_p} w_{x_{k-1}x_k}&amp;lt;/math&amp;gt;&lt;br /&gt;
: und ein kürzester Weg &amp;lt;math&amp;gt;p^* \in P&amp;lt;/math&amp;gt; ist ein Weg mit minimalen Kosten&lt;br /&gt;
: &amp;lt;math&amp;gt;p^* = \textrm{argmin}_{p\in P}\ \ W_p&amp;lt;/math&amp;gt;&lt;br /&gt;
: Das Problem des kürzesten Weges besteht darin, einen optimalen Weg &amp;lt;i&amp;gt;p*&amp;lt;/i&amp;gt; zwischen gegebenen Knoten u und v zu finden.&lt;br /&gt;
Die Lösung dieses Problems hängt davon ab, ob alle Kantengewichte positiv sind, oder ob es auch negative Kantengewichte gibt. In letzeren Fall ist es möglich, durch eine Verlängerung des Weges die Kosten zu redizieren, während sich im ersteren Fall die Kosten immer erhöhen, wenn man den Weg verlängert. &lt;br /&gt;
&lt;br /&gt;
Negative Gewichte treten z.B. bei den Währungsgraphen auf. Auf den ersten Blick entsprechen diese Graphen nicht den Anforderungen an das Problem des kürzesten Weges, weil Wechselkurse miteinander (und mit Geldbeträgen) multipliziert anstatt addiert werden. Man beseitigt diese Schwierigkeit aber leicht, indem man die &amp;lt;i&amp;gt;Logarithmen&amp;lt;/i&amp;gt; der Wechselkurse als Kantengewichte verwendet, wodurch sich die Multiplikation in eine Addition der Logarithmen verwandelt. Wechselkurse &amp;amp;lt; 1 führen nun zu negativen Gewichten. &lt;br /&gt;
&lt;br /&gt;
Interessant werden negative Gewichte vor allem in Graphen mit Zyklen. Dann kann es nämlich passieren, dass die Gesamtkosten eines Zyklus ebenfalls negativ sind. Jeder Weg, der den Zyklus enthält, hat dann Kosten von &amp;lt;math&amp;gt;-\infty&amp;lt;/math&amp;gt;, weil man den Zyklus beliebig oft durchlaufen und dadurch die Gesamtkosten immer weiter verkleinern kann:&lt;br /&gt;
&lt;br /&gt;
     /\		1. Durchlauf: Kosten -1&lt;br /&gt;
  1 /  \ -4	2. Durchlauf: Kosten -2&lt;br /&gt;
   /____\	etc.&lt;br /&gt;
      2&lt;br /&gt;
&lt;br /&gt;
Um hier nicht in einer Endlosschleife zu landen, benötigt man spezielle Algorithmen, die mit dieser Situation umgehen können. Der [http://de.wikipedia.org/wiki/Bellman-Ford-Algorithmus Algorithmus von Bellmann und Ford] beispielsweise bricht die Suche nach dem kürzesten Weg ab, sobald er einen negativen Zyklus entdeckt, aber andernfalls kann er negative Gewichte problemlos verarbeiten. &lt;br /&gt;
&lt;br /&gt;
Die Detektion negativer Zyklen hat wiederum eine interessante Anwendung bei Währungsgraphen: Ein Zyklus bedeutet hier, dass man Geld über mehrere Stufen von einer Währung in die nächste und am Schluß wieder in die Originalwährung umtauscht, und ein negativer Zyklus führt dazu, dass man am Ende &amp;lt;i&amp;gt;mehr&amp;lt;/i&amp;gt; Geld besitzt als am Anfang (damit negative Zyklen wirklich einen Gewinn bedeuten und keinen Verlust, müssen die Wechselkurse vor der Logarithmierung in [http://de.wikipedia.org/wiki/Wechselkurs#Nominaler_Wechselkurs Preisnotierung] angegeben sein). Bei Privatpersonen ist dies ausgeschlossen, weil die Umtauschgebühren den möglichen Gewinn mehr als aufzehren. Banken mit direktem weltweitem Börsenzugang hingegen unternehmen große Anstrengungen, um solche negativen Zyklen möglichst schnell (nämlich vor der Konkurrenz) zu entdecken und auszunutzen. Diese Geschäftsmethode bezeichnet man als [http://de.wikipedia.org/wiki/Arbitrage Arbitrage] und die Existenz eines negativen Zyklus als Arbitragegelegenheit. Durch die Kursschwankungen (und durch die ausgleichende Wirkung der Arbitragegeschäfte selbst) existieren die Arbitragegelegenheiten nur für kurze Zeit, und ihre Detektion erfordert leistungsfähige Echtzeitalgorithmen.&lt;br /&gt;
&lt;br /&gt;
In dieser Vorlesung beschränken wir uns hingegen auf Graphen mit ausschließlich positiven Gewichten. In diesem Fall ist der Algorithmus von Dijkstra die Methode der Wahl, weil er wesentlich schneller arbeitet als der Bellmann-Ford-Algorithmus.&lt;br /&gt;
&lt;br /&gt;
=== Algorithmus von Dijkstra ===&lt;br /&gt;
&lt;br /&gt;
==== Edsger Wybe Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
geb. 11. Mai 1930 in Rotterdam&lt;br /&gt;
&lt;br /&gt;
ges. 06. August 2002&lt;br /&gt;
&lt;br /&gt;
Dijkstra war ein niederländischer Informatiker und Wegbereiter der strukturierten Programmierung. 1972 erhielt er für seine Leistung in der Technik und Kunst der Programmiersprachen den Turing Award, der jährlich von der Association for Computing Machinery (ACM) an Personen verliehen wird, die sich besonders um die Entwicklung der Informatik verdient gemacht haben. Zu seinen Beiträgen zur Informatik gehören unter anderem der Dijkstra-Algorithmus zur Berechnung des kürzesten Weges in einem Graphen sowie eine Abhandlung über den go-to-Befehl und warum er nicht benutzt werden sollte. Der go-to-Befehl war in den 60er und 70er Jahren weit verbreitet, führte aber zu Spaghetti-Code. In seinem berühmten Paper &amp;quot;A Case against the GO TO Statement&amp;quot;[http://www.cs.utexas.edu/users/EWD/ewd02xx/EWD215.PDF], das als Brief mit dem Titel &amp;quot;Go-to statement considered harmful&amp;quot; veröffentlicht wurde, argumentiert Dijkstra, dass es umso schwieriger ist, dem Quellcode eines Programmes zu folgen, je mehr go-to-Befehle darin enthalten sind und zeigt, dass man auch ohne diesen Befehl gute Programme schreiben kann.&lt;br /&gt;
&lt;br /&gt;
==== Algorithmus ====&lt;br /&gt;
&lt;br /&gt;
Der Dijkstra-Algorithmus für kürzeste Wege ist dem oben vorgestellten Algorithmus &amp;lt;tt&amp;gt;shortestPath()&amp;lt;/tt&amp;gt; auf der Basis von Breitensuche sehr ähnlich. Insbesondere gilt auch hier, dass neben dem kürzesten Weg vom Start zum Ziel auch alle kürzesten Wege gefunden werden, deren Endknoten dem Start näher sind als der Zielknoten. Aufgrund der Kantengewichte gibt es aber einen wichtigen Unterschied: Der erste gefundene Weg zu einem Knoten ist nicht mehr notwendigerweise der kürzeste. Wir bestimmen deshalb für jeden Knoten mehrere Kandidatenwege und verwenden eine Prioritätswarteschlange (statt einer einfachen First in - First out - Queue), um diese Wege nach ihrer Länge zu sortieren. Die Kandidatenwege für einen gegebenen Knoten werden unterschieden, indem wir auch den Vorgängerknoten im jeweiligen Weg speichern. Wenn ein Knoten &amp;lt;i&amp;gt;erstmals&amp;lt;/i&amp;gt; an die Spitze der Prioritätswarteschlange gelangt, haben wir den kürzesten Weg zu diesem Knoten gefunden (das wird weiter unten formal bewiesen), und der Vorgänger des Knotens in diesem Weg wird zu seinem Vaterknoten. Erscheint derselbe Knoten später nochmals an der Spitze der Prioritätswarteschlange, handelt es sich um einen Kandidatenweg, der sich nicht als kürzester erwiesen hat und deshalb ignoriert werden kann. Wir erkennen dies leicht daran, dass der Vaterknoten in der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; bereits gesetzt ist. &lt;br /&gt;
&lt;br /&gt;
Eine geeignete Datenstruktur für die Prioritätswarteschlange wird durch das Python-Modul [http://docs.python.org/library/heapq.html heapq] realisiert. Es verwendet ein normales Pythonarray als unterliegende Repräsentation für einen Heap und stellt effiziente &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt;-Funktionen zur Verfügung. Dies entspricht genau unserer Vorgehensweise im Kapitel [[Prioritätswarteschlangen]]. Als Datenelement erwartet die Funktion &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; ein Tupel, dessen erstes Element die Priorität sein muss. Die übrigen Elemente des Tupels (und damit auch deren Anzahl) können je nach Anwendung frei festgelegt werden. Wir legen fest, dass das zweite Element den Endknoten des betrachteten Weges und das dritte den Vorgängerknoten speichert. &lt;br /&gt;
&lt;br /&gt;
Die Kantengewichte werden dem Algorithmus in der property map &amp;lt;tt&amp;gt;weights&amp;lt;/tt&amp;gt; übergeben:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;code python&amp;gt;&lt;br /&gt;
    import heapq	                  # heapq implementiert die Funktionen für Heaps&lt;br /&gt;
    &lt;br /&gt;
    def dijkstra(graph, weights, startnode, destination):&lt;br /&gt;
        parents = [None]*len(graph)       # registriere für jeden Knoten den Vaterknoten im Pfadbaum&lt;br /&gt;
      &lt;br /&gt;
        q = []                            # Array q wird als Heap verwendet&lt;br /&gt;
        &amp;lt;font color=red&amp;gt;heapq.heappush(q, (0.0, startnode, startnode))&amp;lt;/font&amp;gt;  # Startknoten in Heap einfügen&lt;br /&gt;
      &lt;br /&gt;
        while len(q) &amp;gt; 0:                 # solange es noch Knoten im Heap gibt:&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;length, node, predecessor = heapq.heappop(q)&amp;lt;/font&amp;gt;   # Knoten aus dem Heap nehmen&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;if parents[node] is not None:&amp;lt;/font&amp;gt; # parent ist schon gesetzt =&amp;gt; es gab einen anderen, kürzeren Weg&lt;br /&gt;
                &amp;lt;font color=red&amp;gt;continue&amp;lt;/font&amp;gt;                  #   =&amp;gt; wir können diesen Weg ignorieren&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;parents[node] = predecessor&amp;lt;/font&amp;gt;   # parent setzen&lt;br /&gt;
            if node == destination:       # Zielknoten erreicht&lt;br /&gt;
                break                     #   =&amp;gt; Suche beenden&lt;br /&gt;
            for neighbor in graph[node]:  # die Nachbarn von node besuchen,&lt;br /&gt;
                if parents[neighbor] is None:   # aber nur, wenn ihr kürzester Weg noch nicht bekannt ist&lt;br /&gt;
                    &amp;lt;font color=red&amp;gt;newLength = length + weights[(node,neighbor)]&amp;lt;/font&amp;gt;   # berechne Pfadlänge zu neighbor              &lt;br /&gt;
                    &amp;lt;font color=red&amp;gt;heapq.heappush(q, (newLength, neighbor, node))&amp;lt;/font&amp;gt;  # und füge neighbor in den Heap ein&lt;br /&gt;
      &lt;br /&gt;
        if parents[destination] is None:  # Suche wurde beendet ohne den Zielknoten zu besuchen&lt;br /&gt;
            return None, None             # =&amp;gt; kein Pfad gefunden (unzusammenhängender Graph)&lt;br /&gt;
      &lt;br /&gt;
        # Pfad durch die parents-Kette zurückverfolgen und speichern&lt;br /&gt;
        path = [destination]&lt;br /&gt;
        while path[-1] != startnode:&lt;br /&gt;
            path.append(parents[path[-1]])&lt;br /&gt;
        path.reverse()                    # Reihenfolge umdrehen (Ziel =&amp;gt; Start wird zu Start =&amp;gt; Ziel)&lt;br /&gt;
        return path, length               # gefundenen Pfad und dessen Länge zurückgeben&lt;br /&gt;
  &amp;lt;/code&amp;gt;&lt;br /&gt;
Die wesentlichen Unterschiede zur Breitensuche sind im Code rot markiert: Anstelle der Queue verwenden wir jetzt einen Heap, und der Startknoten wird mit Pfadlänge 0 als erstes eingefügt. In der Schleife &amp;lt;tt&amp;gt;while len(q) &amp;gt; 0:&amp;lt;/tt&amp;gt; wird jeweils der Knoten &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; mit der aktuell kürzesten Pfadlänge aus dem Heap entfernt. Die Pfadlänge vom Start zu diesem Knoten wird in der Variable &amp;lt;tt&amp;gt;length&amp;lt;/tt&amp;gt; gespeichert, sein Vorgänger in der Variable &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;. Wenn der aktuelle Weg nicht der kürzeste ist (&amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; war bereits gesetzt), wird dieser Weg ignoriert. Andernfalls werden die property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; aktualisiert und die Nachbarn von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; besucht. Beim Scannen der Nachbarn berechnen wir zunächst die Länge &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; das Weges &amp;lt;tt&amp;gt;startnode =&amp;amp;gt; node =&amp;amp;gt; neighbor&amp;lt;/tt&amp;gt; als Summe von &amp;lt;tt&amp;gt;length&amp;lt;/tt&amp;gt; und dem Gewicht der Kante &amp;lt;tt&amp;gt;(node, neighbode)&amp;lt;/tt&amp;gt;. Diese Länge wird beim Einfügen des Nachbarknotens in den Heap zur Priorität des aktuellen Weges.&lt;br /&gt;
&lt;br /&gt;
Die wichtigsten Prinzipien des Dijkstra-Algorithmus noch einmal im Überblick:&lt;br /&gt;
* Der Dijkstra-Algorithmus ist Breitensuche mit Prioritätswarteschlange (Heap) statt einer einfache Warteschlange (Queue).&lt;br /&gt;
* Die Prioritätswarteschlange speichert alle Wege, die bereits gefunden worden sind und ordnet sie aufsteigend nach ihrer Länge. &lt;br /&gt;
* Das Sortieren (und damit der ganze Algorithmus) funktioniert nur mit positiven Kantengewichten korrekt.&lt;br /&gt;
* Da ein Knoten auf mehreren Wegen erreichbar sein kann, kann er auch mehrmals im Heap sein. &lt;br /&gt;
* Wenn ein Knoten &amp;lt;i&amp;gt;erstmals&amp;lt;/i&amp;gt; aus der Prioritätswarteschlange entnommen wird, ist der gefundene Weg der kürzeste zu diesem Knoten. Andernfalls wird der Weg ignoriert.&lt;br /&gt;
* Wenn der Knoten &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; aus dem Heap entnommen wird, ist der kürzeste Weg von Start nach Ziel gefunden, und die Suche kann beendet werden.&lt;br /&gt;
In unserer Implementation können, wie gesagt, mehrere Wege zum selben Knoten gleichzeitig in der Prioritätswarteschlange sein. Im Prinzip wäre es auch möglich, immer nur den besten zur Zeit bekannten Weg zu jedem Enknoten in der Prioritätswarteschlange zu halten - sobald ein besserer Kandidat gefunden wird, ersetzt er den bisherigen Kandidaten, anstatt zusätzlich eingefügt zu werden. Dies erfordert aber eine wesentlich kompliziertere Prioritätswarteschlange, die eine effiziente &amp;lt;tt&amp;gt;updatePriority&amp;lt;/tt&amp;gt;-Funktion anbietet, ohne dass dadurch eine signifikante Beschleunigung erreicht wird. Deshalb verfolgen wir diesen Ansatz nicht.&lt;br /&gt;
&lt;br /&gt;
==== Beispiel ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;under construction&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Image:Bsp.jpg]]&lt;br /&gt;
&lt;br /&gt;
==== Komplexität von Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
Zur Analyse der Komplexität nehmen wir an, dass der Graph V Knoten und E Kanten hat. Die Initialisierung der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; am Anfang der Funktion hat offensichtlich Komplexität O(V), weil Speicher für V Knoten allokiert wird. Der Code am Ende der Funktion, der aus der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; den Pfad extrahiert, hat ebenfalls die Komplexität O(V), weil der Pfad im ungünstigen Fall sämtliche Knoten des Graphen umfasst. Beides wird durch die Komplexität der Hauptschleife dominiert, zu deren Analyse wir den folgenden Codeausschnitt genauer anschauen wollen:&lt;br /&gt;
&lt;br /&gt;
      while len(q) &amp;gt; 0:&lt;br /&gt;
           ... # 1&lt;br /&gt;
           if parents[node] is not None: &lt;br /&gt;
               continue                  &lt;br /&gt;
           parents[node] = predecessor&lt;br /&gt;
           ... # 2&lt;br /&gt;
Wir erkennen, dass der Codeabschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; für jeden Knoten höchstens einmal erreicht werden kann: Da &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; beim ersten Durchlauf gesetzt wird, kann die &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage beim gleichen Knoten nie wieder &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt; liefern, und das nachfolgende &amp;lt;tt&amp;gt;continue&amp;lt;/tt&amp;gt; bewirkt, dass der Abschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; dann übersprungen wird. Man sagt auch, dass jeder Knoten &amp;lt;i&amp;gt;höchstens einmal expandiert&amp;lt;/i&amp;gt; wird, auch wenn er mehrmals im Heap war. &lt;br /&gt;
&lt;br /&gt;
Der Codeabschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; selbst enthält eine Schleife über alle ausgehenden Kanten des Knotens &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt;. Im ungünstigsten Fall iterieren wir bei &amp;lt;i&amp;gt;allen&amp;lt;/i&amp;gt; Knoten über &amp;lt;i&amp;gt;alle&amp;lt;/i&amp;gt; ausgehenden Kanten, aber das sind gerade alle Kanten des Graphen je einmal in den beiden möglichen Richtungen. Die Funktion &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; wird sogar höchstens E Mal aufgerufen, weil eine Kante nur in den Heap eingefügt wird, wenn der kürzeste Weg der jeweiligen Endknotens noch nicht bekannt ist (siehe die &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage in der &amp;lt;tt&amp;gt;for&amp;lt;/tt&amp;gt;-Schleife), und das ist nur ein einer Richtung möglich. Dies hat zwei Konsequenzen:&lt;br /&gt;
* Die Schleife &amp;lt;tt&amp;gt;while len(q) &amp;gt; 0:&amp;lt;/tt&amp;gt; wird nur so oft ausgeführt, wie Elemente im Heap sind, also höchstens E Mal. Das gleiche gilt für den Codeabschnitt &amp;lt;tt&amp;gt;# 1&amp;lt;/tt&amp;gt;, der das &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; enthält.&lt;br /&gt;
* Die Operationen &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; haben logarithmische Komplexität in der Größe des Heaps, sind also in &amp;lt;math&amp;gt;O(\log\,E)&amp;lt;/math&amp;gt;. In einfachen Graphen gilt aber &amp;lt;math&amp;gt;E = O(V^2)&amp;lt;/math&amp;gt;, so dass sich die Komplexität der Heapoperationen vereinfacht zu &amp;lt;math&amp;gt;O(\log\,E)=O(\log\,V^2)=O(2\log\,V)=O(\log\,V)&amp;lt;/math&amp;gt;.&lt;br /&gt;
Zusammenfassend gilt: &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; werden maximal E Mal aufgerufen und haben eine Komplexität in &amp;lt;math&amp;gt;O(\log\,V)&amp;lt;/math&amp;gt;. Folglich hat der Algorithmus von Dijkstra die Komplexität:&lt;br /&gt;
:&amp;lt;math&amp;gt;O(E\,\log\,V)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Vergleich mit Breitensuche und Tiefensuche ====&lt;br /&gt;
&lt;br /&gt;
Der Dijkstra-Algorithmus ist eng mit der Breiten- und Tiefensuche verwandt - man kann diese Algorithmen aus dem Dijkstra-Algorithmus gewinnen, indem man einfach die Regel zur Festlegung der Prioritäten ändert. Anstelle der Länge des Pfades verwenden wir als Priorität den Wert eine Zählvariable &amp;lt;tt&amp;gt;count&amp;lt;/tt&amp;gt;, die nach jeder Einfügung in den Heap (also nach jedem Aufruf von &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt;) aktualisiert wird. Zählen wir die Variable hoch, haben die zuerst eingefügten Kanten die höchste Priorität, der Heap verhält sich also wie eine Queue (First in-First out), und wir erhalten eine Breitensuche. Zählen wir die Variable hingegen (von E beginnend) herunter, haben die zuletzt eingefügten Kanten höchste Priorität. Der Heap verhält sich dann wie ein Stack (Last in-First out), und wir bekommen Tiefensuche. Statt eines Heaps plus Zählvariable kann man jetzt natürlich direkt eine Queue bzw. einen Stack verwenden. Dadurch fällt der Aufwand &amp;lt;math&amp;gt;O(\log\,V)&amp;lt;/math&amp;gt; für die Heapoperationen weg und wird durch die effizienten O(1)-Operationen von Queue bzw. Stack ersetzt. Damit erhalten wir für Breiten- und Tiefensuche die schon bekannte Komplexität O(E).&lt;br /&gt;
&lt;br /&gt;
==== Korrektheit von Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
Wir beweisen mittels vollständiger Induktion die Schleifen-Invariante: Falls &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; gesetzt (also ungleich &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;) ist, dann liefert das Zurückverfolgen des Weges von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; nach &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; den kürzesten Weg. &lt;br /&gt;
;Induktionsanfang: &amp;lt;tt&amp;gt;parents[startnode]&amp;lt;/tt&amp;gt; ist als einziges gesetzt. Zurückverfolgen liefert den trivialen Weg &amp;lt;tt&amp;gt;[startnode]&amp;lt;/tt&amp;gt;, der mit Länge 0 offensichtlich der kürzeste Pfad ist &amp;amp;rarr; die Bedingung ist erfüllt.&lt;br /&gt;
;Induktionsschritt: Wir zeigen mit einem indirektem Beweis, dass wir immer einen kürzesten Weg bekommen, wenn &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; gesetzt wird.&lt;br /&gt;
:Sei &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt; = &amp;lt;tt&amp;gt;{v | parents[v] is not None}&amp;lt;/tt&amp;gt; die Menge aller Knoten, von denen wir den kürzesten Weg schon kennen (Induktionsvoraussetzung), und &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; der Knoten, der sich gerade an der Spitze des Heaps befindet. Dann ist &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; der Vorgänger von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; im aktuellen Weg, und es muss &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt; gelten, weil die Nachbarn von &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; (und damit auch der aktuelle &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt;) erst in den Heap eingefügt werden, wenn der kürzeste Weg für &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; gefunden wurde. Man beachte auch, dass alle Knoten, die noch nicht in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt; enthalten sind, weiter vom Start entfernt sind als alle Knoten in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt;, weil alle neu in den Heap eingefügten Wege länger sind als der kürzeste Weg des jeweiligen Vorgängers. &lt;br /&gt;
:Der indirekte Beweis nimmt jetzt an, dass der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; nicht der kürzeste Weg ist. Dann muss es einen anderen, kürzeren Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; geben. Für den Vorgänger &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; in diesem Weg unterscheiden wir zwei Fälle:&lt;br /&gt;
:* &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt;: In diesem Fall ist die Länge des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; bereits bekannt, und dieser Weg ist in der Prioritätswarteschlange enthalten. Dann kann er aber nicht der kürzeste sein, denn an der Spitze der Warteschlange war nach Voraussetzung der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt;.&lt;br /&gt;
:* &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\notin S&amp;lt;/math&amp;gt;: Die Kosten des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; berechnen sich als &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) + weight[(x, node)]&amp;lt;/tt&amp;gt;, und die Kosten des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; sind analog &amp;lt;tt&amp;gt;Kosten(predecessor &amp;amp;rarr; startnode) + weight[(predecessor, node)]&amp;lt;/tt&amp;gt;. Aufgrund der Induktionsvoraussetzung gilt aber &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt;, und somit &amp;lt;tt&amp;gt;Kosten(predecessor &amp;amp;rarr; startnode) &amp;amp;lt; Kosten(x &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt;, weil &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; andernfalls vor &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; an der Spitze des Heaps gewesen wäre, was mit der Annahme &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\notin S&amp;lt;/math&amp;gt; unverträglich ist. Damit der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; trotzdem der kürzeste Weg sein kann, müsste &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) &amp;amp;lt; Kosten(node &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt; gelten, denn durch die Kante &amp;lt;tt&amp;gt;(x, node)&amp;lt;/tt&amp;gt; kommen ja noch Kosten hinzu. Das wäre aber nur möglich, wenn der Knoten &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; vor dem Knoten &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; an die Spitze des Heaps gelangt, im Widerspruch zur Annahme, dass &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; sich gerade an der Spitze des Heaps befindet. Somit kann die Behauptung, dass der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; der kürzeste Weg ist, nicht stimmen.&lt;br /&gt;
In beiden Fällen erhalten wir einen Widerspruch, und die Behauptung ist somit bewiesen. Da die Invariante insbesondere für den Weg zum Zielknoten &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; erfüllt ist, folgt daraus auch die Korrektheit des Algorithmus von Dijkstra.&lt;br /&gt;
&lt;br /&gt;
===  A*-Algorithmus - Wie kann man Dijkstra noch verbessern? ===&lt;br /&gt;
&lt;br /&gt;
Eine wichtige Eigenschaft des Dijkstra-Algorithmus ist, dass neben dem kürzesten Weg vom Start zum Ziel auch die kürzesten Wege zu allen Knoten berechnet werden, die näher am Startknoten liegen als das Ziel, obwohl uns diese Wege gar nicht interessieren. Sucht man beispielsweise in einem Graphen mit den Straßenverbindungen in Deutschland den kürzesten Weg von Frankfurt (Main) nach Dresden (ca. 460 km), werden auch die kürzesten Wege von Frankfurt nach Köln (190 km), Dortmund (220 km) und Stuttgart (210 km) und vielen anderen Städten gefunden. Aufgrund der geographischen Lage dieser Städte ist eigentlich von vornherein klar, dass sie mit dem kürzesten Weg nach Dresden nicht das geringste zu tun haben. Anders sieht es mit Erfurt (260 km) oder Suhl (210 km) aus - diese Städte liegen zwischen Frankfurt und Dresden und kommen deshalb als Zwischenstationen des gesuchten Weges in Frage.&lt;br /&gt;
&lt;br /&gt;
Damit Dijkstra korrekt funktioniert, würde es im Prinzip ausreichen, wenn man die kürzesten Wege nur für diejenigen Knoten ausrechnet, die auf dem kürzesten Weg vom Start zum Ziel liegen, denn nur diese Knoten braucht man, um den gesuchten Weg über die &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette zurückzuverfolgen. Das Problem ist nur, dass man diese Knoten erst kennt, wenn der Algorithmus fertig durchgelaufen ist. Schließt man Knoten zu früh von der Betrachtung aus, kommt am Ende möglicherweise nicht der korrekte kürzeste Weg heraus. &lt;br /&gt;
&lt;br /&gt;
Der A*-Algorithmus löst dieses Dilemma mit folgender Idee: Ändere die Prioritäten für den Heap so ab, dass unwichtige Knoten nur mit geringerer Wahscheinlichkeit expandiert werden, aber stelle gleichzeitig sicher, dass alle wichtigen Knoten (also diejenigen auf dem korrekten kürzesten Weg) auf jeden Fall expandiert werden. Es zeigt sich, dass man diese Idee umsetzen kann, wenn eine &amp;lt;i&amp;gt;Schätzung für den Restweg&amp;lt;/i&amp;gt; (also für die noch verbleibende Entfernung von jedem Knoten zum Ziel) verfügbar ist:&lt;br /&gt;
 rest = guess(neighbor, destination)&lt;br /&gt;
Diese Schätzung addiert man einfach zur wahren Länge des Weges &amp;lt;tt&amp;gt;startnode &amp;amp;rarr; node&amp;lt;/tt&amp;gt; dazu, um die verbesserte Priorität zu erhalten:&lt;br /&gt;
 priority = newLength + guess(neighbor, destination)&lt;br /&gt;
(Im originalen Dijkstra-Algorithmus wird als Priorität nur &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; allein verwendet. Man beachte, dass man &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; jetzt zusätzlich im Heap speichern muss, weil man es für die Expansion des Knotens später noch benötigt.)&lt;br /&gt;
&lt;br /&gt;
Damit sicher gestellt ist, dass der A*-Algorithmus immer noch die korrekten kürzesten Wege findet, darf die Schätzung den wahren Restweg &amp;lt;i&amp;gt;niemals überschätzen&amp;lt;/i&amp;gt;. Es muss immer gelten:&lt;br /&gt;
 0 &amp;lt;= guess(node, destination) &amp;lt;= trueDistance(node, destination)&lt;br /&gt;
Damit gilt insbesondere &amp;lt;tt&amp;gt;guess(destination, destination) = trueDistance(destination, destination) = 0&amp;lt;/tt&amp;gt;, an der Priorität des Knotens &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; ändert sich also nichts. Die Prioritäten aller anderen Knoten veschlechtern sich hingegen, weil zur bisherigen Priorität noch atwas addiert wird. Für die wichtigen Knoten auf dem kürzesten Weg vom Start nach Ziel gilt jedoch, dass deren neue Priorität immer noch besser ist als die Priorität des Zielknotens selbst. Für diese Knoten gilt nämlich&lt;br /&gt;
 falls node auf dem kürzesten Weg von startnode nach destination liegt:&lt;br /&gt;
 trueDistance(startnode, node) + guess(node, destination) &amp;lt;= trueDistance(startnode, destination)&lt;br /&gt;
weil der Weg von Start nach &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; ein Teil des kürzesten Wegs von Start nach Ziel ist und die Restschätzung die wahre Entfernung immer unterschätzt. Diese Knoten werden deshalb stets vor dem Zielknoten expandiert, so dass wir die &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette immer noch korrekt zurückverfolgen können. Für alle anderen Knoten gilt idealerweise, dass die neue Priorität schlechter ist als die Priorität von &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt;, so dass man sich diese irrelevanten Knotenexpansionen sparen kann.&lt;br /&gt;
&lt;br /&gt;
Für das Beispiel eines Straßennetzwerks bietet sich als Schätzung die Luftlinienentfernung an, weil Straßen nie kürzer sein können als die Luftlinie. Damit erreicht man in der Praxis deutliche Einsparungen. Generell gilt, dass der A*-Algorithmus im typischen Fall schneller ist als der Algorithmus von Dijkstra, aber man kann immer pathologische Fälle konstruieren, wo die Änderung der Prioritäten nichts bringt. Die Komplexität des A*-Algorithmus im ungünstigen Fall ist deshalb nach wie vor &amp;lt;math&amp;gt;O(E\,\log\,V)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=='''Minimaler Spannbaum'''==&lt;br /&gt;
'''(engl.: minimum spanning tree; abgekürzt: MST)'''&lt;br /&gt;
&lt;br /&gt;
[[Image:Minimum_spanning_tree.png‎ |thumb|200px|right|Ein minimal aufspannender Baum verbindet alle Punkte eines Graphen bei minimaler Kantenlänge ([http://de.wikipedia.org/wiki/Spannbaum Quelle])]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: gewichteter Graph G, zusammenhängend&amp;lt;br/&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: Untermenge &amp;lt;math&amp;gt;E'\subseteq E&amp;lt;/math&amp;gt; der Kanten, so dass die Summe der Kantengewichte &amp;lt;math&amp;gt;\sum_{e\in E'} w_e&amp;lt;/math&amp;gt; minimal und der entstehende Graph G' zusammenhängend ist.&amp;lt;br/&amp;gt;&lt;br /&gt;
* G' definiert immer einen Baum, denn andernfalls könnte man eine Kante weglassen und dadurch die Summe &amp;lt;math&amp;gt;\sum_{e\in E'} w_e&amp;lt;/math&amp;gt; verringern, ohne dass sich am Zusammenhang von G' etwas ändert. &amp;lt;br/&amp;gt;&lt;br /&gt;
* Wenn der Graph G nicht zusammenhängend ist, kann man den Spannbaum für jede Zusammenhangskomponente getrennt ausrechnen. Man erhält dann einen aufspannenden Wald. &lt;br /&gt;
* Der MST ist ähnlich wie der Dijkstra-Algorithmus: Dort ist ein Pfad gesucht, bei dem die Summe der Gewichte über den Pfad minimal ist. Beim MST suchen wir eine Lösung, bei der die Summe der Gewichte über den ganzen Graphen minimal ist. &lt;br /&gt;
* Das Problem des MST ist nahe verwandt mit der Bestimmung der Zusammenhangskomponente, z.B. über den Tiefensuchbaum. Für die Zusammenhangskomponenten genügt allerdings ein beliebiger Baum, während beim MST ein minimaler Baum gesucht ist.&lt;br /&gt;
&lt;br /&gt;
=== Anwendungen ===&lt;br /&gt;
==== Wie verbindet man n gegebene Punkte mit möglichst kurzen Straßen (Eisenbahnen, Drähten [bei Schaltungen] usw.)?====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:center&amp;quot; border=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; cellspacing=&amp;quot;0&amp;quot; &lt;br /&gt;
|MST minimale Verbindung (Abb.1)&lt;br /&gt;
|MST = 2 (Länge = Kantengewicht)(Abb.2)&lt;br /&gt;
|- valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| [[Image:mst.png]] &lt;br /&gt;
| [[Image:Gleichseitigesdreieck.png]]&lt;br /&gt;
|}&lt;br /&gt;
*In der Praxis: Die Festlegung, dass man nur die gegebenen Punkte verwenden darf, ist eine ziemliche starke Einschränkung. &lt;br /&gt;
&lt;br /&gt;
* Wenn man sich vorstellt, es sind drei Punkte gegeben, die als gleichseitiges Dreieck angeordnet sind, dann ist der MST (siehe Abb.2, schwarz gezeichnet) und hat die Länge 2. Man kann hier die Länge als Kantengewicht verwenden. &lt;br /&gt;
&lt;br /&gt;
* Wenn es erlaubt ist zusätzliche Punkte einzufügen, dann kann man in der Mitte einen neuen Punkt setzen &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; neuer MST (siehe Abb.2, orange gezeichnet).&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Höhe = &amp;lt;math&amp;gt;\frac{1}{2}\sqrt{3}&amp;lt;/math&amp;gt;, Schwerpunkt: teilt die Höhe des Dreiecks im Verhältnis 2:1; der Abstand von obersten Punkt bis zum neu eingeführten Punkt: &amp;lt;math&amp;gt;\frac{2}{3}h = \frac{\sqrt{3}}{3}&amp;lt;/math&amp;gt;, davon insgesamt 3 Stück, damit (gilt für den MST in orange eingezeichnet): MST = &amp;lt;math&amp;gt;3\left(\frac{1}{3}\right) \sqrt{3} = \sqrt{3} \approx 1,7&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Damit ist der MST in orange kürzer als der schwarz gezeichnete MST. &amp;lt;br\&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt;Folgerung: MST kann kürzer werden, wenn man einen Punkt dazu nimmt. &lt;br /&gt;
* Umgekehrt kann der MST auch kürzer werden, wenn man einen Punkt aus dem Graphen entfernt, aber wie das Beipiel des gleichseitigen Dreiecks zeigt, ist dies nicht immer der Fall.&lt;br /&gt;
&lt;br /&gt;
[[Image: bahn.png|Bahnstrecke Verbindung (Abb.3)]]&lt;br /&gt;
&lt;br /&gt;
* Methode der zusätzlichen Punkteinfügung hat man früher beim Bahnstreckenbau verwendet. Durch Einführung eines Knotenpunktes kann die Streckenlänge verkürzt werden (Dreiecksungleichung).&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Bestimmung von Datenclustern ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Image:cluster.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Daten (in der Abb.: Punkte) bilden Gruppen. &lt;br /&gt;
&lt;br /&gt;
* In der Abbildung hat man 2 verschiedene Messungen gemacht (als x- und y-Achse aufgetragen), bspw. Größe und Gewicht von Personen. Für jede Person i wird ein Punkt an der Koordinate (Größe&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;, Gewicht&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;) gezeichnet (siehe Bild a). Dies bezeichnet man als ''Scatter Plot''. Wenn bestimmte Wertkombinationen häufiger auftreten als andere, bilden sich mitunter Gruppen aus, bspw. eine Gruppe für &amp;quot;klein und schwer&amp;quot; etc.&lt;br /&gt;
&lt;br /&gt;
* Durch Verbinden der Punkte mittels eines MST (siehe Abbildung (b)) sieht man, dass es kurze (innerhalb der Gruppen) und lange Kanten (zwischen den Gruppen) gibt. &lt;br /&gt;
&lt;br /&gt;
* Wenn man geschickt eine Schwelle einführt und alle Kanten löscht, die länger sind als die Schwelle, dann bekommt man als Zusammenhangskomponente die einzelnen Gruppen. &lt;br /&gt;
&lt;br /&gt;
=== Algorithmen ===&lt;br /&gt;
&lt;br /&gt;
Genau wie bei der Bestimmung von Zusammenhangskomponenten kann man auch das MST-Problem entweder nach dem Anlagerungsprinzip oder nach dem Verschmelzungsprinzip lösen (dazu gibt es noch weitere Möglichkeiten, z.B. den [http://de.wikipedia.org/wiki/Algorithmus_von_Bor%C5%AFvka Algorithmus von Boruvka]). Der Anlagerungsalgorithmus für MST wurde zuerst von Prim beschrieben und trägt deshalb seinen Namen, der Verschmelzungsalgorithmus stammt von Kruskal. Im Vergleich zu den Algorithmen für Zusammenhangskomponenten ändert sich im wesentlichen nur die Reihenfolge, in der die Kanten betrachtet werden: Eine Prioritätswarteschlange stellt jetzt sicher, dass am Ende wirklich der Baum mit den geringstmöglichen Kosten herauskommt.&lt;br /&gt;
&lt;br /&gt;
====Algorithmus von Prim====&lt;br /&gt;
[http://de.wikipedia.org/wiki/Algorithmus_von_Prim Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus von Prim geht nach dem Anlagerungsprinzip vor (vgl. den Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Tiefensuche|Zusammenhangskomponenten mit Tiefensuche]]): Starte an der Wurzel (ein willkürlich gewählter Knoten) und füge jeweils die günstigste Kante an die aktuellen Teillösung an, die keinen Zyklus verursacht. Die Sortierung der Kanten nach Priorität erfolgt analog zum Dijsktra-Algorithmus, aber die Definitionen, welche Kante die günstigste ist, unterscheiden sich. Die Konvention für die Bedeutung der Elemente des Heaps ist ebenfalls identisch: ein Tupel mit &amp;lt;tt&amp;gt;(priority, node, predecessor)&amp;lt;/tt&amp;gt;. Die folgende Implementation verdeutlicht sehr schön die Ähnlichkeit der beiden Algorithmen. Das Ergebnis wird als property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; zurückgegeben, in der für jeden Knoten sein Vorgänger im MST steht, wobei die Wurzel wie üblich auf sich selbst verweist.&lt;br /&gt;
&lt;br /&gt;
 import heapq&lt;br /&gt;
 &lt;br /&gt;
 def prim(graph, weights):                # Kantengewichte wie bei Dijkstra als property map&lt;br /&gt;
 	sum = 0.0                         # wird später das Gewicht des Spannbaums sein&lt;br /&gt;
 	start = 0                         # Knoten 0 wird willkürlich als Wurzel gewählt&lt;br /&gt;
        &lt;br /&gt;
        parents = [None]*len(graph)       # property map, die den resultierenden Baum kodiert&lt;br /&gt;
        parents[start] = start            # Wurzel zeigt auf sich selbst&lt;br /&gt;
        &lt;br /&gt;
 	heap = []                         # Heap für die Kanten des Graphen&lt;br /&gt;
 	for neighbor in graph[start]:     # besuche die Nachbarn von start&lt;br /&gt;
 	    heapq.heappush(heap, (weights[(start, neighbor)], neighbor, start))  # und fülle Heap &lt;br /&gt;
 	&lt;br /&gt;
        while len(heap) &amp;gt; 0:&lt;br /&gt;
 	    w, node, predecessor = heapq.heappop(heap) # hole billigste Kante aus dem Heap&lt;br /&gt;
            if parents[node] is not None: # die Kante würde einen Zyklus verursachen&lt;br /&gt;
                continue                  #   =&amp;gt; ignoriere diese Kante&lt;br /&gt;
            parents[node] = predecessor   # füge Kante in den MST ein&lt;br /&gt;
            sum += w                      # und aktualisiere das Gesamtgewicht &lt;br /&gt;
            for neighbor in graph[node]:  # besuche die Nachbarn von node&lt;br /&gt;
                if parents[neighbor] is None:  # aber nur, wenn kein Zyklus entsteht&lt;br /&gt;
                    heapq.heappush(heap, (weights[(node,neighbor)], neighbor, node)) # füge Kandidaten in Heap ein&lt;br /&gt;
        &lt;br /&gt;
 	return parents, sum               # MST und Gesamtgewicht zurückgeben&lt;br /&gt;
&lt;br /&gt;
====Algorithmus von Kruskal====&lt;br /&gt;
[http://de.wikipedia.org/wiki/Algorithmus_von_Kruskal Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Kruskal%27s_algorithm (en)]&lt;br /&gt;
&lt;br /&gt;
Die alternative Vorgehensweise ist das Verschmelzungsprinzip (vgl. den Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Union-Find-Algorithmus|Zusammenhangskomponenten mit Union-Find-Algorithmus]]), das der Algorithmus von Kruskal verwendet. Jeder Knoten wird zunächst als trivialer Baum mit nur einem Knoten betrachtet, und alle Kanten werden aufsteigend nach Gewicht sortiert. Dann wird die billigste noch nicht betrachtete Kante in den MST eingefügt, falls sich dadurch kein Zyklus bildet (erkennbar daran, dass die Endknoten in verschiedenen Zusammenhangskomponenten liegen, das heisst verschiedene Anker haben). Da der fertige Baum (V-1) Kanten haben muss, wird dies (V-1) Mal zutreffen. Andernfalls wird diese Kante ignoriert. Anders ausgedrückt: Der Algorithmus beginnt mit ''V'' Bäumen; in (''V''-1) Verschmelzungsschritten kombiniert er jeweils zwei Bäume (unter Verwendung der kürzesten möglichen Kante), bis nur noch ein Baum übrig bleibt. Der einzige Unterschied zum einfachen Union-Find besteht darin, dass die Kanten in aufsteigender Reihenfolge betrachtet werden müssen, was wir hier durch eine Prioritätswarteschlange realisieren. Der Algorithmus von J.Kruskal ist seit 1956 bekannt. &lt;br /&gt;
&lt;br /&gt;
 def kruskal(graph, weights):&lt;br /&gt;
     anchors = range(len(graph))           # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     results = []                          # result wird später die Kanten des MST enthalten    &lt;br /&gt;
     &lt;br /&gt;
     heap = []                             # Heap zum Sortieren der Kanten nach Gewicht&lt;br /&gt;
     for edge, w in weights.iteritems():   # alle Kanten einfügen&lt;br /&gt;
         heapq.heappush(heap, (w, edge))&lt;br /&gt;
     &lt;br /&gt;
     while len(heap) &amp;gt; 0:                  # solange noch Kanten vorhanden sind&lt;br /&gt;
         w, edge = heapq.heappop(heap)     # billigste Kante aus dem Heap nehmen&lt;br /&gt;
         a1 = findAnchor(anchors, edge[0]) # Anker von Startknoten der Kante&lt;br /&gt;
         a2 = findAnchor(anchors, edge[1]) # ... und Endknoten bestimmen&lt;br /&gt;
         if a1 != a2:                      # wenn die Knoten in verschiedenen Komponenten sind&lt;br /&gt;
             anchors[a2] = a1              # Komponenten verschmelzen&lt;br /&gt;
             result.append(edge)           # ... und Kante in MST einfügen&lt;br /&gt;
     &lt;br /&gt;
     return result                         # Kanten des MST zurückgeben&lt;br /&gt;
&lt;br /&gt;
Die Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt; wurde im Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Union-Find-Algorithmus|Zusammenhangskomponenten mit Union-Find-Algorithmus]] implementiert. Im Unterschied zum Algorithmus von Prim geben wir hier nicht die property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; zurück, sondern einfach eine Liste der Kanten im MST.&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus eignet sich insbesondere für das Clusteringproblem, da der Schwellwert von vornerein als maximales Kantengewicht an den Algorithmus übergeben werden kann. Man hört mit dem Vereinigen auf, wenn das Gewicht der billigste Kante im Heap den Schwellwert überschreitet. Beim Algorithmus von Kruskal kann dann keine bessere Kante als der Schwellwert mehr kommen, da die Kanten vorher sortiert worden sind. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Komplexität:&amp;lt;/b&amp;gt; wie beim Dijkstra-Algorithmus, weil jede Kante genau einmal in den Heap kommt. Der Aufwand für das Sortieren ist somit &amp;lt;math&amp;gt;O\left(E\log E\right)&amp;lt;/math&amp;gt;, was sich zu &amp;lt;math&amp;gt;O \left(E\,\log\,V\right)&amp;lt;/math&amp;gt; reduziert, falls keine Mehrfachkanten vorhanden sind.&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; geeignet für Übungsaufgabe&lt;br /&gt;
&lt;br /&gt;
====Verwendung einer BucketPriorityQueue====&lt;br /&gt;
&lt;br /&gt;
Beide Algorithmen zur Bestimmung des minimalen Spannbaums benötigen eine Prioritätswarteschlange. Wenn die Kantengewichte ganze Zahlen im Bereich &amp;lt;tt&amp;gt;0...(m-1)&amp;lt;/tt&amp;gt; sind, kann man die MST-Algorithmen deutlich beschleunigen, wenn man anstelle des Heaps eine [[Prioritätswarteschlangen#Prioritätssuche mit dem Bucket-Prinzip|&amp;lt;tt&amp;gt;BucketPriorityQueue&amp;lt;/tt&amp;gt;]] verwendet. Die Operationen zum Einfügen einer Kante in die Queue und zum Entfernen der billibsten Kante aus der Queue beschleunigen sich dadurch auf O(1) statt O(log V) (außer wenn die Gewichte sehr ungünstig auf die Kanten verteilt sind). In der Praxis erreicht man durch diese Änderung typischerweise deutliche Verbesserungen. In der Bildverarbeitung können die Prioritäten beispielsweise die Wahrscheinlichkeit kodieren, dass zwei benachbarte Pixel zu verschiedenen Objekten gehören. Bildet man jetzt den MST, und bricht bei einer bestimmten Wahrscheinlichkeit ab, erhält man Cluster von Pixeln, die wahrscheinlich zum selben Objekt gehören (weil der MST ja die Kanten mit minimalem Gewicht bevorzugt, und kleine Gewichte bedeuten kleine Wahrscheinlichkeit, dass benachbarte Pixel von einander getrennt werden). Da man die Wahrscheinlichkeiten nur mit einer Genauigkeit von ca. 1% berechnen kann, reichen hiefür 100 bis 200 Quantisierungstufen aus. Durch Verwendung der schnellen &amp;lt;tt&amp;gt;BucketPriorityQueue&amp;lt;/tt&amp;gt; kann man jetzt wesentlich größere Bilder in akzeptabler Zeit bearbeiten als dies mit einem Heap möglich wäre.&lt;br /&gt;
&lt;br /&gt;
== Algorithmen für gerichtete Graphen ==&lt;br /&gt;
&lt;br /&gt;
Zur Erinnerung: in einem gerichteten Graphen sind die Kanten (i &amp;amp;rarr; j) und (j &amp;amp;rarr; i) voneinander verschieden, und eventuell existiert nur eine der beiden Richtungen. Im allgemeinen unterscheidet sich der [[Graphen_und_Graphenalgorithmen#transposed_graph|transponierte Graph]] G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; also vom Originalgraphen G. Beim Traversieren des Graphen und bei der Pfadsuche dürfen Kanten nur in passender Richtung verwendet werden. Bei gewichteten Graphen tritt häufig der Fall auf, dass zwar Kanten in beiden Richtungen existieren, diese aber unterschiedliche Gewichte haben.&lt;br /&gt;
&lt;br /&gt;
Gerichtete Graphen ergeben sich in natürlicher Weise aus vielen Anwendungsproblemen:&lt;br /&gt;
* Routenplanung&lt;br /&gt;
** Bei Straßennetzwerken enstehen gerichtete Graphen, sobald es Einbahnstraßen gibt.&lt;br /&gt;
** Verwendet man Gewichte, um die erwarteten Fahrzeiten entlang einer Straße zu kodieren, gibt es Asymmetrien z.B. dann, wenn Straßen in einer Richtung bergab, in der anderen bergauf befahren werden. Hier existieren zwar Kanten in beiden Richtungen, sie haben aber unterschiedliche Gewichte. Ähnliches gilt für Flüge: Durch den Gegenwind des Jetstreams braucht man von Frankfurt nach New York länger als umgekehrt von New York nach Frankfurt.&lt;br /&gt;
* zeitliche oder kausale Abhängigkeiten&lt;br /&gt;
** Wenn die Knoten Ereignisse repräsentieren, von denen einige die Ursache von anderen sind, diese wiederum die Ursache der nächsten usw., verbindet man die Knoten zweckmäßig durch gerichtete Kanten, die die Kausalitätsbeziehungen kodieren. Handelt es sich um logische &amp;quot;wenn-dann&amp;quot;-Regeln, erhält man einen [[Graphen_und_Graphenalgorithmen#Anwendung:_Das_Erf.C3.BCllbarkeitsproblem_in_Implikationengraphen|Implikationengraph]] (siehe unten). Handelt es sich hingegen um Wahrscheinlichkeitsaussagen (&amp;quot;Wenn das Wetter schön ist, haben Studenten tendenziell gute Laune, wenn eine Prüfung bevorsteht eher schlechte usw.&amp;quot;), erhält man ein [http://de.wikipedia.org/wiki/Bayessches_Netz Bayessches Netz].&lt;br /&gt;
** Wenn bestimmte Aufgaben erst begonnen werden können, nachdem andere Aufgaben erledigt sind, erhält man einen Abhängigkeitsgraphen. Beispielsweise dürfen Sie erst an der Klausur teilnehmen, nachdem Sie die Übungsaufgaben gelöst haben, und Sie dürfen erst die Abschlussarbeit beginnen, nachdem Sie bestimmte Prüfungen bestanden haben. Ein anderes schönes Beispiel liefern die Regeln für das [[Graphen_und_Graphenalgorithmen#Anwendung:_Abh.C3.A4ngigkeitsgraph|Ankleiden]] weiter unten.&lt;br /&gt;
** Gerichtete Graphen kodieren die Abhängigkeiten zwischen Programmbibliotheken. Beispielsweise benötigt das Pythonmodul &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; die internen Submodule &amp;lt;tt&amp;gt;json.encoder&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;json.decode&amp;lt;/tt&amp;gt; sowie das externe Modul &amp;lt;tt&amp;gt;decimal&amp;lt;/tt&amp;gt;. Die Submodule benötigen wiederum die externen Module &amp;lt;tt&amp;gt;re&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;sys&amp;lt;/tt&amp;gt;, das Modul &amp;lt;tt&amp;gt;decimal&amp;lt;/tt&amp;gt; braucht &amp;lt;tt&amp;gt;copy&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;collections&amp;lt;/tt&amp;gt; usw.&lt;br /&gt;
** Das Internet kann als gerichteter Graph dargestellt werden, wobei die Webseiten die Knoten, und die Hyperlinks die Kanten sind.&lt;br /&gt;
* Sequence Alignment&lt;br /&gt;
** Eine gute Rechtschreibprüfung markiert nicht nur fehlerhafte Wörter, sondern macht auch plausible Vorschläge, was eigentlich gemeint gewesen sein könnte. Dazu muss sie das gegebene Wort mit den Wörtern eines Wörterbuchs vergleichen und die Ähnlichkeit bewerten. Ein analoges Problem ergibt sich, wenn man DNA Fragmente mit der Information in einer Genomdatenbank abgleichen will. &lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Sequence Alignment / Edit Distance ===&lt;br /&gt;
&lt;br /&gt;
:gegeben: zwei Wörter (allgemein: beliebige Zeichenfolgen)&lt;br /&gt;
:gesucht: Wie kann man die Buchstaben am besten in Übereinstimmung bringen?&lt;br /&gt;
&lt;br /&gt;
:Beispiel: WORTE – NORDEN&lt;br /&gt;
&lt;br /&gt;
Zwei mögliche Alignments sind&lt;br /&gt;
&lt;br /&gt;
  W&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;OR&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;T&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;E&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;.          W.ORTE&lt;br /&gt;
  N&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;OR&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;D&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;E&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;N          NORDEN&lt;br /&gt;
&lt;br /&gt;
wobei der Punkt anzeigt, dass der untere Buchstabe keinen Partner hat, und rote Buchstaben oben und unten übereinstimmen. Jede Nicht-Übereinstimmung verursacht nun gewisse Kosten. Dabei unterscheiden wir zwei Fälle:&lt;br /&gt;
# Matche a[i] mit b[j]. Falls a[i] == b[j], ist das gut (rote Buchstaben), und es entstehen keine Kosten. Andernfalls entstehen Kosten U (schwarze Buchstaben).&lt;br /&gt;
# Wir überspringen a[i] oder b[j] (Buchstabe vs. Punkt). Dann entstehen Kosten V. (Manchmal unterscheidet man auch noch Kosten Va und Vb, wenn das Überspringen bei a und b unterschieldiche Signifikanz hat.)&lt;br /&gt;
&lt;br /&gt;
Gesucht ist nun das &amp;lt;b&amp;gt;Alignment mit minimalen Kosten&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Diese Aufgabe kann man sehr schön als gerichteten Graphen darstellen: Wir definieren ein rechteckiges Gitter und schreiben das erste Wort über das Gitter und das andere links davon. Die Gitterpunkte verbinden wir mit Pfeilen (gerichteten Kanten), wobei ein Pfeil nach rechts bedeutet, dass wir beim oberen Wort einen Buchstaben überspringen, ein Pfeil nach unten, dass wir beim linken Wort einen Buchstaben überspringen, und ein diagonaler Pfeil, dass wir zwei Buchstaben matchen (und zwar die am Pfeilende). Die Farben der Pfeile symbolisieren die Kosten: rot für das Überspringen eines Buchstabens (Kosten V), blau für das Matchen, wenn die Buchstaben nicht übereinstimmen (Kosten U), und grün, wenn die Buchstaben übereinstimmen (keine Kosten). &lt;br /&gt;
&lt;br /&gt;
[[Image:sequence-alignment.png|300px]]&lt;br /&gt;
&lt;br /&gt;
Lösung:&lt;br /&gt;
:Suche den kürzesten Pfad vom Knoten &amp;quot;START&amp;quot; (oben links) nach unten rechts. Dazu kann der [[Graphen und Graphenalgorithmen#Algorithmus von Dijkstra|Algorithmus von Dijkstra]] verwendet werden, der auf gerichteten Graphen genauso funktioniert wie auf ungerichteten.&lt;br /&gt;
&lt;br /&gt;
Für unser Beispiel von oben erhalten wir die folgenden Pfade:&lt;br /&gt;
&lt;br /&gt;
[[Image:sequence-alignment-weg1.png|400px]]&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;[[Image:sequence-alignment-weg2.png|400px]]&lt;br /&gt;
&lt;br /&gt;
Durch Addieren der Kosten entsprechend der Farben sieht man, dass der erste Weg die Kosten 2U+V und der zweite die Kosten 5U+V hat. Der erste Weg ist offensichtlich günstiger und entspricht dem besten Alignment.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Abhängigkeitsgraph ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Beispiel: &amp;lt;/b&amp;gt; Wie erklärt man einem zerstreuten Professor, wie er sich morgens anziehen soll? Der folgende Graph enthält einen Knoten für jede Aktion, und eine Kante (i &amp;amp;rarr; j) bedeutet, dass die Aktion i vor der Aktion j abgeschlossen werden muss.&lt;br /&gt;
&lt;br /&gt;
[[Image:anziehen-graph.png|600px]]&lt;br /&gt;
&lt;br /&gt;
In derartigen Abhängigkeitsgraphen ist die wichtigste Frage immer, ob der Graph azyklisch ist. Wäre dies nämlich nicht der Fall, kann es keine Reihenfolge der Aktionen geben, die alle Abhängigkeiten erfüllt. Dies sieht man leicht, wenn man den einfachsten möglichen Zyklus betrachtet: es gibt sowohl eine Kante (i &amp;amp;rarr; j) als auch eine (j &amp;amp;rarr; i). Dann müsste man i vor j erledigen, aber ebenso j vor i, was offensichtlich unmöglich ist - das im Graph kodierte Problem ist dann unlösbar. Wegen ihrer Wichtigkeit wird für gerichtete azyklische Graphen oft die Abkürzung &amp;lt;b&amp;gt;DAG&amp;lt;/b&amp;gt; (von &amp;lt;i&amp;gt;directed acyclic graph&amp;lt;/i&amp;gt;) verwendet. Ein Graph ist genau dann ein DAG, wenn es eine topologische Sortierung gibt:&lt;br /&gt;
;topologische Sortierung: Zeichne die Knoten so auf eine Gerade, dass alle Kanten (Pfeile) nach rechts zeigen. &lt;br /&gt;
Arbeitet man die Aktionen nach einer (beliebigen) topologischen Sortierung ab, werden automatisch alle Abhängigkeiten eingehalten: Da alle Pfeile nach rechts zeigen, werden abhängige Aktionen immer später ausgeführt. Die topologische Sortierung ist im allgemeinen nicht eindeutig. Die folgende Skizze zeigt eine mögliche topologische Sortierung für das Anziehen:&lt;br /&gt;
&lt;br /&gt;
[[Image:anziehen-topologische-sortierung.png|600px]]&lt;br /&gt;
&lt;br /&gt;
Eine solche fest vorgegebene Reihenfolge ist für den zerstreuten Professor sicherlich eine größere Hilfe als der ursprüngliche Graph. Man erkennt, dass die Sortierung nicht eindeutig ist, beispielsweise bei der Uhr: Da für die Uhr keine Abhängigkeiten definiert sind, kann man diese Aktion an beliebiger Stelle einsortieren. Hier wurde willkürlich die letzte Stelle gewählt.&lt;br /&gt;
&lt;br /&gt;
==== Zwei Algorithmen zum Finden der topologischen Sortierung ====&lt;br /&gt;
&lt;br /&gt;
Die folgenden Algorithmen finden entweder eine topologische Sortierung, oder signalisieren, dass der Graph zyklisch ist.&lt;br /&gt;
&lt;br /&gt;
===== Algorithmus 1 =====&lt;br /&gt;
# Suche einen Knoten mit Eingangsgrad 0 (ohne eingehende Pfeile) =&amp;gt; in einem gerichteten azyklischen Graphen gibt es immer einen solchen Knoten&lt;br /&gt;
# Platziere diesen Knoten auf der Geraden (beliebig)&lt;br /&gt;
# Entferne den Knoten aus dem Graphen zusammen mit den ausgehenden Kanten&lt;br /&gt;
# Gehe zu 1., aber platziere in 2. immer rechts der Knoten, die schon auf der Geraden vorhanden sind.&lt;br /&gt;
: =&amp;gt; Wenn noch Knoten übrig sind, aber keiner Eingangsgrad 0 hat, muss der Graph zyklisch sein.&lt;br /&gt;
&lt;br /&gt;
[[Image:bild6.JPG]]&lt;br /&gt;
&lt;br /&gt;
Beispiel für einen zyklischen Graphen: kein Knoten hat Eingangsgrad 0.&lt;br /&gt;
&lt;br /&gt;
Um den Algorithmus zu implementieren, verwenden wir eine property map &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt;, die wir in einem ersten Durchlauf durch den Graphen füllen und die dann für jeden Knoten die Anzahl der eingehenden Kanten speichert. Dann gehen wir sukzessive zu allen Knoten mit &amp;lt;tt&amp;gt;in_degree == 0&amp;lt;/tt&amp;gt;. Anstatt sie aber tatsächlich aus dem Graphen zu entfernen wie im obigen Pseudocode, dekrementieren wir nur den &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt; ihrer Nachbarn. Wird der &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt; eines Nachbarn dadurch 0, wird er ebenfalls in das Array der zu scannenden Knoten aufgenommen. Wenn der Graph azyklisch ist, enthält das Array am Ende alle Knoten des Graphen, und die Reihenfolge der Einfügungen definiert eine topologische Sortierung. Andernfalls ist das Array zu kurz, und wir signalisieren durch Zurückgeben von &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, dass der Graph zyklisch ist:&lt;br /&gt;
&lt;br /&gt;
 def topological_sort(graph):              # ein gerichteter Graph&lt;br /&gt;
     in_degree = [0]*len(graph)            # property map für den Eingangsgrad jeden Knotens&lt;br /&gt;
     for node in xrange(len(graph)):       # besuche alle Knoten&lt;br /&gt;
         for neighbor in graph[node]:      #  ... und deren Nachbarn&lt;br /&gt;
             in_degree[neighbor] += 1      #  ... und inkrementiere den Eingangsgrad&lt;br /&gt;
     &lt;br /&gt;
     result = []                           # wird später die topologische Sortierung enthalten&lt;br /&gt;
     for node in xrange(len(graph)):&lt;br /&gt;
         if in_degree[node] == 0:&lt;br /&gt;
             result.append(node)           # füge alle Knoten mit Eingangsgrad 0 in result ein&lt;br /&gt;
     &lt;br /&gt;
     k = 0&lt;br /&gt;
     while k &amp;lt; len(result):                # besuche alle Knoten mit Eingangsgrad 0&lt;br /&gt;
         node = result[k]&lt;br /&gt;
         k += 1&lt;br /&gt;
         for neighbor in graph[node]:      # besuche alle Nachbarn&lt;br /&gt;
             in_degree[neighbor] -= 1      # entferne 'virtuell' die eingehende Kante&lt;br /&gt;
             if in_degree[neighbor] == 0:  # wenn neighbor jetzt Eingangsgrad 0 hat&lt;br /&gt;
                 result.append(neighbor)   #  ... füge ihn in result ein&lt;br /&gt;
     &lt;br /&gt;
     if len(result) == len(graph):         # wenn alle Knoten jetzt Eingangsgrad 0 haben&lt;br /&gt;
         return result                     # ... ist result eine topologische Sortierung&lt;br /&gt;
     else:&lt;br /&gt;
         return None                       # andernfalls ist der Graph zyklisch&lt;br /&gt;
&lt;br /&gt;
===== Algorithmus 2 =====&lt;br /&gt;
Der obige Algorithmus hat den Nachteil, dass er jeden Knoten zweimal expandiert. Man kann eine topologische Sortierung stattdessen auch mit Tiefensuche bestimmen. Es gilt nämlich der folgende&lt;br /&gt;
;Satz: Wird ein DAG mittels Tiefensuche traversiert, definiert die &amp;lt;i&amp;gt;reverse post-order&amp;lt;/i&amp;gt; eine topologische Sortierung.&lt;br /&gt;
Zur Erinnerung: die post-order erhält man, indem man jeden Knoten ausgibt, &amp;lt;i&amp;gt;nachdem&amp;lt;/i&amp;gt; die Rekursion zu allen seinen Nachbarn beendet ist, siehe unsere [[Graphen_und_Graphenalgorithmen#pre_and_post_order|Diskussion weiter oben]]. Die reverse post-order ist gerade die Umkehrung dieser Reihenfolge. Die folgende Implementation verwendet die rekursive Version der Tiefensuche, in der Praxis wird man meist die iterative Version mit Stack bevorzugen, weil bei großen Graphen die Aufruftiefe sehr groß werden kann:&lt;br /&gt;
&lt;br /&gt;
 def reverse_post_order(graph):               # gerichteter Graph&lt;br /&gt;
     result = []                              # enthält später die reverse post-order&lt;br /&gt;
     visited = [False]*len(graph)             # Flags für bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):                         # besuche node&lt;br /&gt;
         if not visited[node]:                # aber nur, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True             # markiere ihn als besucht&lt;br /&gt;
             for neighbor in graph[node]:     # und besuche die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
             result.append(node)              # alle Nachbarn besucht =&amp;gt; Anhängen an result liefert post-order&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):          # besuche alle Knoten&lt;br /&gt;
         visit(node)&lt;br /&gt;
     &lt;br /&gt;
     result.reverse()                         # post-order =&amp;gt; reverse post-order&lt;br /&gt;
     return result&lt;br /&gt;
&lt;br /&gt;
Die Tatsache, dass die reverse post-order tatsächlich eine topologische Sortierung liefert, leuchtet wahrscheinlich nicht unmittelbar ein. Bevor wir diese Tatsache beweisen. wollen wir uns anhand des Ankleidegraphen klar machen, dass die pre-order (die man intuitiv vielleicht eher wählen würde) keine topologische Sortierung ist. Startet man die Tiefensuche beim Knoten &amp;quot;Unterhemd&amp;quot;, werden die Knoten in der Reihenfolge &amp;quot;Unterhemd&amp;quot;, &amp;quot;Oberhemd&amp;quot;, &amp;quot;Schlips&amp;quot;, &amp;quot;Jackett&amp;quot;, &amp;quot;Gürtel&amp;quot; gefunden. Da dann alle von &amp;quot;Unterhemd&amp;quot; erreichbaren Knoten erschöpft sind, startet man die Tiefensuche als nächstes bei &amp;quot;Unterhose&amp;quot; und erreicht von dort aus &amp;quot;Hose&amp;quot; und &amp;quot;Schuhe&amp;quot;. Man erkennt sofort, dass diese Reihenfolge nicht funktioniert: &amp;quot;Hose&amp;quot; kommt nach &amp;quot;Gürtel&amp;quot;, und &amp;quot;Jackett&amp;quot; kommt vor &amp;quot;Gürtel&amp;quot;. Bei dieser Anordnung gibt es Pfeile nach links, die Abhängigkeitsbedingungen sind somit verletzt.&lt;br /&gt;
&lt;br /&gt;
Damit die reverse post-order eine zulässige Sortierung sein kann, muss stets gelten, dass Knoten u vor Knoten v einsortiert wurde, wenn die Kante (u &amp;amp;rarr; v) existiert. Das ist aber äquivalent zur Forderung, dass in der ursprünglichen post-order (vor dem &amp;lt;tt&amp;gt;reverse&amp;lt;/tt&amp;gt;) u hinter v stehen muss. Wir betrachten den &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt;-Aufruf, bei dem u expandiert wird. Gelangt man jetzt zu u's Nachbarn v, gibt es zwei Möglichkeiten: Wenn v bereits expandiert wurde, befindet es sich bereits im Array &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt; kehrt sofort zurück. Andernfalls wird v ebenfalls expandiert und demzufolge in &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; eingetragen, &amp;lt;i&amp;gt;bevor&amp;lt;/i&amp;gt; der rekursive Aufruf &amp;lt;tt&amp;gt;visit(v)&amp;lt;/tt&amp;gt; zurückkehrt. Knoten u wird aber erst in &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; eingefügt, &amp;lt;i&amp;gt;nachdem&amp;lt;/i&amp;gt; alle rekursiven &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt;-Aufrufe seiner Nachbarn zurückgekehrt sind. In beiden Fällen steht u in der post-order wie gefordert hinter v, und daraus folgt die Behauptung.&lt;br /&gt;
&lt;br /&gt;
Der obige Algorithmus liefert natürlich nur dann eine topologische Sortierung, wenn der Graph wirklich azyklisch ist (man kann ihn aber auch anwenden, um die reverse post-order für einen zyklischen Graphen zu bestimmen, siehe Abschnitt &amp;quot;[[Graphen_und_Graphenalgorithmen#Transitive Hülle und stark zusammenhängende Komponenten|Stark zusammenhängende Komponenten]]&amp;quot;). Dieser Fall tritt in der Praxis häufig auf, weil zyklische Graphen bei vielen Anwendungen gar nicht erst entstehen können. Weiß man allerdings nicht, ob der Graph azyklisch ist oder nicht, muss man einen zusätzlichen Test auf Zyklen in den Algorithmus einbauen. &lt;br /&gt;
&lt;br /&gt;
Zyklische Graphen sind dadurch gekennzeichnet, dass es im obigen Beweis eine dritte Möglichkeit gibt: Während der Expansion von u wird rekursiv v expandiert, und es gibt eine Rückwärtskante (v &amp;amp;rarr; u). (Es spielt dabei keine Rolle, ob v von u aus direkt oder indirekt erreicht wurde.) Ein Zyklus wird also entdeckt, wenn die Tiefensuche zu u zurückkehrt, solange u noch &amp;lt;i&amp;gt;aktiv&amp;lt;/i&amp;gt; ist, d.h. wenn die Rekursion von u aus gestartet und noch nicht beendet wurde. Dies kann man leicht feststellen, wenn man in der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; drei Werte zulässt: 0 für &amp;quot;noch nicht besucht&amp;quot;, 1 für &amp;quot;aktiv&amp;quot; und 2 für &amp;quot;beendet&amp;quot;. Wir signalisieren einen Zyklus, sobald &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt; für einen Knoten aufgerufen wird, der gerade aktiv ist:&lt;br /&gt;
&lt;br /&gt;
 def topological_sort_DFS(graph):             # gerichteter Graph&lt;br /&gt;
     result = []                              # enthält später die topologische Sortierung&lt;br /&gt;
     &lt;br /&gt;
     not_visited, active, finished = 0, 1, 2  # drei Zustände für visited&lt;br /&gt;
     visited = [not_visited]*len(graph)       # Flags für aktive und bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):                         # besuche node (gibt &amp;quot;True&amp;quot; zurück, wenn Zyklus gefunden wurde)&lt;br /&gt;
         if visited[node] == not_visited:     # ... aber nur, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = active           # markiere ihn als aktiv&lt;br /&gt;
             for neighbor in graph[node]:     # und besuche die Nachbarn&lt;br /&gt;
                 if visit(neighbor):          # wenn rekursiv ein Zyklus gefunden wurde&lt;br /&gt;
                     return True              # ... brechen wir ab und signalisieren den Zyklus&lt;br /&gt;
             visited[node] = finished         # Rekursion beendet, node ist nicht mehr aktiv&lt;br /&gt;
             result.append(node)              # alle Nachbarn besucht =&amp;gt; Anhängen an result liefert post-order&lt;br /&gt;
             return False                     # kein Zyklus gefunden&lt;br /&gt;
         elif visited[node] == active:        # Rekursion erreicht einen noch aktiven Knoten&lt;br /&gt;
             return True                      #  =&amp;gt; Zyklus gefunden&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):          # besuche alle Knoten&lt;br /&gt;
         if visit(node):                      # wenn Zyklus gefunden wurde&lt;br /&gt;
             return None                      # ... gibt es keine topologische Sortierung&lt;br /&gt;
     &lt;br /&gt;
     result.reverse()                         # post-order =&amp;gt; reverse post-order (=topologische Sortierung)&lt;br /&gt;
     return result&lt;br /&gt;
&lt;br /&gt;
Man macht sich leicht klar, dass kein Zyklus vorliegt, wenn die Rekursion einen Knoten erreicht, der bereits auf &amp;lt;tt&amp;gt;finished&amp;lt;/tt&amp;gt; gesetzt ist. Nehmen wir an, dass u gerade expandiert wird, und sein Nachbar v ist bereits &amp;lt;tt&amp;gt;finished&amp;lt;/tt&amp;gt;. Wenn es einen Zyklus gäbe, müsste es einen Weg von v nach u geben. Dann wäre u aber bereits während der Expansion von v gefunden worden. Da v nicht mehr im Zustand &amp;lt;tt&amp;gt;active&amp;lt;/tt&amp;gt; ist, muss die Expansion von v schon abgeschlossen gewesen sein, ohne dass u gefunden wurde. Folglich kann es keinen solchen Zyklus geben.&lt;br /&gt;
&lt;br /&gt;
=== Transitive Hülle und stark zusammenhängende Komponenten ===&lt;br /&gt;
&lt;br /&gt;
Auch bei gerichteten Graphen ist die Frage, welche Knoten miteinander zusammenhängen, von großem Interesse. Wir betrachten dazu wieder die Relation &amp;quot;Knoten v ist von Knoten u aus erreichbar&amp;quot;, die anzeigt, ob es einen Weg von u nach v gibt oder nicht. In ungerichteten Graphen ist diese Relation immer symmetrisch, weil jeder Weg in beiden Richtungen benutzt werden kann. In gerichteten Graphen gilt dies nicht. Man muss hier zwei Arten von Zusammenhangskomponenten unterscheiden:&lt;br /&gt;
;Transitive Hülle: Die transitive Hülle eines Knotens u ist die Menge aller Knoten, die von u aus erreichbar sind:&lt;br /&gt;
:&amp;lt;math&amp;gt;T(u) = \{v\ |\ u \rightsquigarrow v\}&amp;lt;/math&amp;gt;&lt;br /&gt;
;Stark zusammenhängende Komponenten: Die stark zusammenhängende Komponenten &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; eines gerichteten Graphen sind maximale Teilgraphen, so dass alle Knoten innerhalb einer Komponente von jedem anderen Knoten der selben Komponente aus erreichbar sind&lt;br /&gt;
:&amp;lt;math&amp;gt;u,v \in C_i\ \ \Leftrightarrow\ \ u \rightsquigarrow v \wedge v \rightsquigarrow u&amp;lt;/math&amp;gt;&lt;br /&gt;
Die erste Definition betrachtet den Zusammenhang asymmetrisch, ohne Beachtung der Frage, ob es auch einen Rückweg von Knoten v nach u gibt, die zweite hingegen symmetrisch.&lt;br /&gt;
&lt;br /&gt;
Die &amp;lt;b&amp;gt;transitive Hülle&amp;lt;/b&amp;gt; benötigt man, wenn man Fragen der Erreichbarkeit besonders effizient beantworten will. Wir hatten bespielsweise oben erwähnt, dass das Python-Modul &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; direkt und indirekt von mehreren anderen Module abhängt, die vorher installiert werden müssen, damit &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; funktioniert. Bittet man den Systemadministrator, das &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt;-Paket zu installieren, will er diese Abhängigkeiten wahrscheinlich nicht erst mühsam rekursiv heraussuchen, sonder er verlangt eine Liste aller Pakete, die installiert werden müssen. Dies ist gerade die transitive Hülle von &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; im Abhängigkeitsgraphen. Damit man diese nicht manuell bestimmen muss, verwendet man Installationsprogrammen wie z.B. [http://pypi.python.org/pypi/pip/ pip], die die Abhängigkeiten automatisch herausfinden und installieren. &lt;br /&gt;
&lt;br /&gt;
Bei der Bestimmung der transitiven Hülle modifiziert man den gegebenen Graphen, indem man jedesmal eine neue Kante (u &amp;amp;rarr; v) einfügt, wenn diese Kante noch nicht existiert, aber v von u aus erreichbar ist. Dies gelingt mit einer sehr einfachen Variation der Tiefensuche: Wir rufen &amp;lt;tt&amp;gt;visit(k)&amp;lt;/tt&amp;gt; für jeden Knoten k auf, aber setzen die property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; zuvor auf &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt; zurück. Alle Knoten, die während der Rekursion erreicht werden, sind im modifizierten Graphen Nachbarn von k. Ein etwas effizienterer Ansatz ist der [http://de.wikipedia.org/wiki/Algorithmus_von_Floyd_und_Warshall Algorithmus von Floyd und Warshall].&lt;br /&gt;
&lt;br /&gt;
Die Bestimmung der &amp;lt;b&amp;gt;stark zusammenhängenden Komponenten&amp;lt;/b&amp;gt; ist etwas schwieriger. Es existieren eine ganze Reihe von effizienten Algorithmen (siehe [http://en.wikipedia.org/wiki/Strongly_connected_component WikiPedia]), deren einfachster der Algorithmus von Kosaraju ist:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;gegeben:&amp;lt;/b&amp;gt; gerichteter Graph&lt;br /&gt;
&lt;br /&gt;
# Bestimme die reverse post-order (mit der Funktion &amp;lt;tt&amp;gt;reverse_post_order&amp;lt;/tt&amp;gt;)&lt;br /&gt;
# Bilde den transponierten Graphen &amp;lt;math&amp;gt;G^T&amp;lt;/math&amp;gt; (mit der Funktion &amp;lt;tt&amp;gt;transposeGraph&amp;lt;/tt&amp;gt;)&lt;br /&gt;
# Bestimme die Zusammenhangskomponenten von &amp;lt;math&amp;gt;G^T&amp;lt;/math&amp;gt; mittels Tiefensuche, aber betrachte die Knoten dabei in der reverse post-order aus Schritt 1 (dies kann mit einer minimalen Modifikation der Funktion &amp;lt;tt&amp;gt;connectedComponents&amp;lt;/tt&amp;gt; geschehen, indem man die Zeile &amp;lt;tt&amp;gt;for node in xrange(len(graph)):&amp;lt;/tt&amp;gt; einfach nach &amp;lt;tt&amp;gt;for node in ordered:&amp;lt;/tt&amp;gt; abändert, wobei &amp;lt;tt&amp;gt;ordered&amp;lt;/tt&amp;gt; das Ergebnis der Funktion &amp;lt;tt&amp;gt;reverse_post_order&amp;lt;/tt&amp;gt; ist, also ein Array, das die Knoten in der gewünschten Reihenfolge enthält).&lt;br /&gt;
Die Zusammenhangskomponenten, die man in Schritt 3 findet, sind gerade die stark zusammenhängenden Komponenten des Originalgraphen G. Die folgende Skizze zeigt diese in grün für den schwarz gezeichneten gerichteten Graphen. &lt;br /&gt;
&lt;br /&gt;
[[Image:strongly-connected-components.png|400px]]    &lt;br /&gt;
&lt;br /&gt;
Zum Beweis der Korrektheit des Algorithmus von Kosaraju zeigen wir zwei Implikationen: 1. Wenn die Knoten u und v in der selben stark zusammenhängenden Komponente liegen, werden sie in Schritt 3 des Algorithmus auch der selben Komponente zugewiesen. 2. Wenn die Knoten u und v in Schritt 3 der selben Komponente zugewiesen wurden, müssen sie auch in der selben stark zusammenhängenden Komponente liegen. &lt;br /&gt;
# Knoten u und v gehören zur selben stark zusammenhängenden Komponente von G. Per Definition gilt, dass u von v aus erreichbar ist und umgekehrt. Dies muss auch im transponierten Graphen G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; gelten (der Weg &amp;lt;math&amp;gt;u \rightsquigarrow v&amp;lt;/math&amp;gt; wird jetzt zum Weg &amp;lt;math&amp;gt;v \rightsquigarrow u&amp;lt;/math&amp;gt; und umgekehrt). Wird u bei der Tiefensuche in Schritt 3 vor v expandiert, ist v von u aus erreichbar und gehört somit zur selben Komponente. Das umgekehrte gilt, wenn v vor u expandiert wird. Daraus folgt die Behauptung 1.&lt;br /&gt;
# Knoten u und v werden in Schritt 3 der selben Komponente zugewiesen: Sei x der Anker dieser Komponente. Da u in der gleichen Komponente wie x liegt, muss es in G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; einen Weg &amp;lt;math&amp;gt;x \rightsquigarrow u&amp;lt;/math&amp;gt;, und demnach in G einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow x&amp;lt;/math&amp;gt; geben. Da x der Anker seiner Komponente ist, wissen wir aber auch, dass x in der reverse post-order &amp;lt;i&amp;gt;vor&amp;lt;/i&amp;gt; u liegt (denn der Anker ist der Knoten, mit dem eine neue Komponente gestartet wird; er muss deshalb im Array &amp;lt;tt&amp;gt;ordered&amp;lt;/tt&amp;gt; als erster Konten seiner Komponente gefunden worden sein). Wir unterscheiden jetzt im Schritt 1 des Algorithmus zwei Fälle:&lt;br /&gt;
## u wurde bei der Bestimmung der post-order vor x expandiert. Dann kann x nur dann in der reverse post-order &amp;lt;i&amp;gt;vor&amp;lt;/i&amp;gt; u liegen (oder, einfacher ausgedrückt, x kann nur dann in der post-order &amp;lt;i&amp;gt;hinter&amp;lt;/i&amp;gt; u liegen), wenn x im Graphen G nicht von u aus erreichbar war. Das ist aber unmöglich, weil wir ja schon wissen, dass es in G einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow x&amp;lt;/math&amp;gt; gibt.&lt;br /&gt;
## Folglich wurde u bei der Bestimmung der post-order nach x expandiert. Da x in der post-order hinter u liegt, muss u während der Expansion von x erreicht worden sein. Deshalb muss es in G auch einen Weg &amp;lt;math&amp;gt;x \rightsquigarrow u&amp;lt;/math&amp;gt; geben.&lt;br /&gt;
#:Somit sind x und u in der selben stark zusammenhängenden Komponente. Die gleiche Überlegung gilt für x und v. Wegen der Transitivität der relation &amp;quot;ist erreichbar&amp;quot; folgt daraus, dass auch u und v in der selben Komponente liegen, also die Behauptung 2.&lt;br /&gt;
&lt;br /&gt;
Die folgende Skizze illustriert, dass der Komponentengraph stets azyklisch ist. Den Komponentengraph erhält man, indem man für jede Komponente &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; einen Knoten erzeugt (grün), und die Knoten i und j durch eine gerichtete Kante verbindet (rot), wenn es im Originalgraphen eine Kante (u &amp;amp;rarr; v) mit &amp;lt;math&amp;gt;u \in C_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v \in C_j&amp;lt;/math&amp;gt; gibt. Es ist dann garantiert, dass es keine Kante in umgekehrter Richtung geben kann. Daraus folgt insbesondere, dass ein DAG nur triviale stark verbundene Komponenten haben kann, die aus einzelnen Knoten bestehen.&lt;br /&gt;
&lt;br /&gt;
[[Image:strongly-connected-components-graph.png|400px]]&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Das Erfüllbarkeitsproblem in Implikationengraphen ===&lt;br /&gt;
&lt;br /&gt;
Das Erfüllbarkeitsproblem hat auf den ersten Blick nichts mit Graphen zu tun, denn es geht um Wahrheitswerte logischer Ausdrücke. Man kann logische Ausdrücke jedoch unter bestimmten Bedingungen in eine Graphendarstellung überführen und somit das ursprüngliche Problem auf ein Problem der Graphentheorie reduzieren, für das bereits ein Lösungsverfahren bekannt ist. In diesem Abschnitt wollen wir dies für die sogenannten Implikationengraphen zeigen, ein weiteres Beispiel findet sich im Kapitel [[NP-Vollständigkeit]].&lt;br /&gt;
&lt;br /&gt;
==== Das Erfüllbarkeitsproblem ====&lt;br /&gt;
&lt;br /&gt;
(vgl. [http://de.wikipedia.org/wiki/Erfüllbarkeitsproblem_der_Aussagenlogik WikiPedia (de)])&lt;br /&gt;
&lt;br /&gt;
Das Erfüllbarkeitsproblem (SAT-Problem, von &amp;lt;i&amp;gt;satisfiability&amp;lt;/i&amp;gt;) befasst sich mit logischen (oder Booleschen) Funktionen: Gegeben sei eine Menge &amp;lt;math&amp;gt;\{x_1, ... ,x_n\}&amp;lt;/math&amp;gt; Boolscher Variablen (d.h., die &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; können nur die Werte True oder False annehmen), sowie eine logische Formel, in der die Variablen mit den üblichen logischen Operatoren &lt;br /&gt;
:&amp;lt;math&amp;gt;\neg\quad&amp;lt;/math&amp;gt;: Negation (&amp;quot;nicht&amp;quot;, in Python: &amp;lt;tt&amp;gt;not&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\vee\quad&amp;lt;/math&amp;gt;: Disjunktion (&amp;quot;oder&amp;quot;, in Python: &amp;lt;tt&amp;gt;or&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\wedge\quad&amp;lt;/math&amp;gt;: Konjuktion (&amp;quot;und&amp;quot;, in Python: &amp;lt;tt&amp;gt;and&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\rightarrow\quad&amp;lt;/math&amp;gt;: Implikation (&amp;quot;wenn, dann&amp;quot;, in Python nicht als Operator definiert)&lt;br /&gt;
:&amp;lt;math&amp;gt;\leftrightarrow\quad&amp;lt;/math&amp;gt;: Äquivalenz (&amp;quot;genau dann, wenn&amp;quot;, in Python: &amp;lt;tt&amp;gt;==&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\neq\quad&amp;lt;/math&amp;gt;: exklusive Disjunktion (&amp;quot;entweder oder&amp;quot;, in Python: &amp;lt;tt&amp;gt;!=&amp;lt;/tt&amp;gt;)&lt;br /&gt;
verknüpft sind. Klammern definieren die Reihenfolge der Auswertung der Operationen. Für jede Belegung der Variablen &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; mit True oder False liefert die Formel den Wert der Funktion, der natürlich auch nur True oder False sein kann. Wenn Formel und Belegung gegeben sind, ist die Auswertung der Funktion ein sehr einfaches Problem: Man transformiert die Formel in einen Parse-Baum (siehe Übungsaufgabe &amp;quot;Taschenrechner) und wertet jeden Knoten mit Hilfe der üblichen Wertetabellen für logische Operatoren aus, die wir hier zur Erinnerung noch einmal angeben:&lt;br /&gt;
{| cellspacing=&amp;quot;0&amp;quot; border=&amp;quot;1&amp;quot;&lt;br /&gt;
|- style=&amp;quot;text-align:center;background-color:#ffffcc;width:50px&amp;quot;&lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \vee b &amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \wedge b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \rightarrow b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;b \rightarrow a&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \leftrightarrow b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \neq b&amp;lt;/math&amp;gt; &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 0 || 1 || 1 || 1 || 0&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 0 || 1 || 0 || 0 || 1&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 0 || 1 || 0 || 1&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 || 0&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beim Erfüllbarkeitsproblem wird die Frage umgekehrt gestellt: &lt;br /&gt;
:Gegeben sei eine logische Funktion. Ist es möglich, dass die Funktion jemals den Wert True annimmt? &lt;br /&gt;
Das heisst, kann man die Variablen &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; so mit True oder False belegen, dass die Formel am Ende wahr ist? Im Prinzip kann man diese Frage durch erschöpfende Suche leicht beantworten, indem man die Funktion für alle &amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt; möglichen Belegungen einfach ausrechnet, aber das dauert für große n (ab ca. &amp;lt;math&amp;gt;n\ge 40&amp;lt;/math&amp;gt;) viel zu lange. Erstaunlicherweise ist es aber noch niemanden gelungen, einen Algorithmus zu finden, der für beliebige logische Funktionen schneller funktioniert. Im Gegenteil wurde gezeigt, dass das Erfüllbarkeitsproblem [[NP-Vollständigkeit|NP-vollständig]] ist, so dass wahrscheinlich kein solcher Algorithmus existiert. Trotz (oder gerade wegen) seiner Schwierigkeit hat das Erfüllbarkeitsproblem viele Anwendungen gefunden, vor allem beim Testen logischer Schaltkreise (&amp;quot;Gibt es eine Belegung der Eingänge, so dass am Ausgang der verbotene Wert X entsteht?&amp;quot;) und bei der Planerstellung in der künstlichen Intelligenz (&amp;quot;Kann man ausschließen, dass der generierte Plan Konflikte enthält?&amp;quot;). Es ist außerdem ein beliebtes Modellproblem für die Erforschung neuer Ideen und Algorithmen für schwierige Probleme.&lt;br /&gt;
&lt;br /&gt;
==== Normalformen für logische Ausdrücke ====&lt;br /&gt;
&lt;br /&gt;
Um die Beschreibung von Erfüllbarkeitsproblemen zu vereinfachen und zu vereinheitlichen, hat man verschiedene &amp;lt;i&amp;gt;Normalformen&amp;lt;/i&amp;gt; für logische Ausdrücke eingeführt. Die wichtigste ist die &amp;lt;i&amp;gt;Konjuktionen-Normalform&amp;lt;/i&amp;gt; (CNF - conjunctive normal form). Ein Ausdruck in &amp;lt;i&amp;gt;Konjuktionen-Normalform&amp;lt;/i&amp;gt; ist eine UND-Verknüpfung von M &amp;lt;i&amp;gt;Klauseln&amp;lt;/i&amp;gt;:&lt;br /&gt;
 (CLAUSE&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;) &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; (CLAUSE&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;) &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; ...  &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; (CLAUSE&amp;lt;sub&amp;gt;M&amp;lt;/sub&amp;gt;)&lt;br /&gt;
Jede Klausel ist wiederum ein logischer Ausdruck, der aber sehr einfach sein muss: Er darf nur noch k Variablen enthalten, die nur mit den Operatoren NICHT und ODER verknüpft werden dürfen, z.B.&lt;br /&gt;
  CLAUSE&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; := &amp;lt;math&amp;gt;x_1 \vee \neg x_3 \vee x_8&amp;lt;/math&amp;gt;&lt;br /&gt;
Je nachdem, wie viele Variablen pro Klausel erlaubt sind, spricht man von &amp;lt;b&amp;gt;k-CNF&amp;lt;/b&amp;gt; und entsprechend von einem &amp;lt;b&amp;gt;k-SAT&amp;lt;/b&amp;gt; Problem. Es ist außerdem üblich, die Menge der Variablen und die Menge der negierten Variablen zusammen als Menge der &amp;lt;i&amp;gt;Literale&amp;lt;/i&amp;gt; zu bezeichnen:&lt;br /&gt;
  LITERALS := &amp;lt;math&amp;gt;\{x_1,...,x_n\} \cup \{\neg x_1,...,\neg x_n\}&amp;lt;/math&amp;gt;&lt;br /&gt;
Formal definiert man die &amp;lt;b&amp;gt;k-Konjunktionen-Normalform (k-CNF)&amp;lt;/b&amp;gt; am besten durch eine Grammatik in [http://de.wikipedia.org/wiki/Backus-Naur-Form Backus-Naur-Form]:&lt;br /&gt;
    k_CNF    ::=  CLAUSE | k_CNF &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; CLAUSE&lt;br /&gt;
    CLAUSE   ::= (LITERAL &amp;lt;math&amp;gt;\vee&amp;lt;/math&amp;gt; ... &amp;lt;math&amp;gt;\vee&amp;lt;/math&amp;gt; LITERAL)  # genau k Literale pro Klausel&lt;br /&gt;
    LITERAL  ::=  VARIABLE | &amp;lt;math&amp;gt;\neg&amp;lt;/math&amp;gt;VARIABLE&lt;br /&gt;
    VARIABLE ::=  &amp;lt;math&amp;gt;x_1&amp;lt;/math&amp;gt; | ... | &amp;lt;math&amp;gt;x_n&amp;lt;/math&amp;gt;&lt;br /&gt;
Beispiele:&lt;br /&gt;
* 3-CNF: &amp;lt;math&amp;gt;(x_1 \vee \neg x_2 \vee x_4) \wedge (x_2 \vee x_3 \vee \neg x_4) \wedge (\neg x_1 \vee x_4 \vee \neg x_5)&amp;lt;/math&amp;gt;&lt;br /&gt;
* 2-CNF: &amp;lt;math&amp;gt;(x_1 \vee \neg x_2) \wedge (x_3 \vee x_4)&amp;lt;/math&amp;gt; ...&lt;br /&gt;
&amp;lt;b&amp;gt;Gesucht&amp;lt;/b&amp;gt; ist eine Belegung der Variablen mit True und False, so dass der Ausdruck den Wert True hat. Aus den Eigenschaften der UND- und ODER-Verknüpfungen folgt, dass ein Ausdruck in k-CNF genau dann True ist, wenn jede einzelne Klausel True ist. In jeder Klausel wiederum hat man k Chancen, die Klausel True zu machen, indem man eins der Literale zu True macht. Eventuell werden dadurch aber andere Klauseln wieder zu False, was die Aufgabe so schwierig macht. Die Bedeutung der k-CNF ergibt sich aus folgendem&lt;br /&gt;
;Satz: Jeder logische Ausdruck kann effizient nach 3-CNF transformiert werden, jedoch im allgemeinen nicht nach 2-CNF.&lt;br /&gt;
Man kann sich also auf Algorithmen für 3-SAT-Probleme konzentrieren, ohne dabei an Ausdrucksmächtigkeit zu verlieren. &lt;br /&gt;
&lt;br /&gt;
Leider gilt der entsprechende Satz nicht für k=2: Ausdrücke in 2-CNF sind weit weniger mächtig, weil man in jeder Klausel nur noch zwei Wahlmöglichkeiten hat. Bestimmte logische Ausdrücke sind aber auch nach 2-CNF transformierbar, beispielsweise die Bedingung, dass zwei Literale u und v immer den entgegegesetzten Wert haben müssen. Dies ergibt ein Paar von ODER-Verknüpfungen:&lt;br /&gt;
:&amp;lt;math&amp;gt;(u \leftrightarrow \neg v) \equiv (u \vee \neg v) \wedge (\neg u \vee v)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die 2-CNF hat den Vorteil, dass es effiziente Algorithmen für das 2-SAT-Problem gibt, die wir jetzt kennenlernen wollen. Es zeigt sich, dass man Ausdrücke in 2-CNF als Graphen repräsentieren kann, indem man sie zunächst in die &amp;lt;i&amp;gt;Implikationen-Normalform&amp;lt;/i&amp;gt; (INF für &amp;lt;i&amp;gt;implicative normal form&amp;lt;/i&amp;gt;) überführt. Die Implikationen-Normalform besteht ebenfalls aus einer Menge von Klauseln, die durch UND-Operationen verknüpft sind, aber jede Klausel ist jetzt eine Implikation. &lt;br /&gt;
Die Grammatik der &amp;lt;b&amp;gt;Implikationen-Normalform (INF)&amp;lt;/b&amp;gt; lautet:&lt;br /&gt;
    INF      ::=  CLAUSE | INF &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; CLAUSE&lt;br /&gt;
    CLAUSE   ::= (LITERAL &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; LITERAL)  # genau 2 Literale pro Implikation&lt;br /&gt;
    LITERAL  ::=  VARIABLE | &amp;lt;math&amp;gt;\neg&amp;lt;/math&amp;gt;VARIABLE&lt;br /&gt;
    VARIABLE ::=  &amp;lt;math&amp;gt;x_1&amp;lt;/math&amp;gt; | ... | &amp;lt;math&amp;gt;x_n&amp;lt;/math&amp;gt;&lt;br /&gt;
und ein gültiger Ausdruck wäre z.B.&lt;br /&gt;
:&amp;lt;math&amp;gt;(x_1 \to x_2) \wedge (x_2 \to \neg x_3) \wedge (x_4 \to x_3)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die Umwandlung von 2-CNF nach INF beruht auf folgender Äquivalenz, die man sich aus der obigen Wahrheitstabelle leicht herleitet:&lt;br /&gt;
:&amp;lt;math&amp;gt;(x \vee y) \equiv (\neg x \rightarrow y) \equiv (\neg y \rightarrow x)&amp;lt;/math&amp;gt;&lt;br /&gt;
Aus dieser Äquivalenz folgt der &lt;br /&gt;
;Satz: Ein Ausdruck in 2-CNF kann nach INF transformiert werden, indem man jede Klausel &amp;lt;math&amp;gt;(x \vee y)&amp;lt;/math&amp;gt; durch das Klauselpaar &amp;lt;math&amp;gt;(\neg x \rightarrow y) \wedge (\neg y \rightarrow x)&amp;lt;/math&amp;gt; ersetzt.&lt;br /&gt;
Man beachte, dass man für jede ODER-Klausel des ursprünglichen Ausdrucks &amp;lt;i&amp;gt;zwei&amp;lt;/i&amp;gt; Implikationen (eine für jede Richtung des &amp;quot;wenn, dann&amp;quot;) einfügen muss, um die Symmetrie des Problems zu erhalten.&lt;br /&gt;
&lt;br /&gt;
==== Lösung des 2-SAT-Problems mit Implikationgraphen ====&lt;br /&gt;
&lt;br /&gt;
Jeder Ausdruck in INF kann als gerichteter Graph dargestellt werden:&lt;br /&gt;
# Für jedes Literal wird ein Knoten in den Graphen eingefügt. Es gibt also für jede Variable und für ihre Negation jeweils einen Knoten, d.h. 2n Knoten insgesamt.&lt;br /&gt;
# Jede Implikation ist eine gerichtete Kante.&lt;br /&gt;
Implikationengraphen eignen sich, um Ursache-Folge-Beziehungen oder Konflikte zwischen Aktionen auszudrücken. Beispielsweise kann man die Klausel &amp;lt;math&amp;gt;(x \rightarrow \neg y)&amp;lt;/math&amp;gt; als &amp;quot;wenn man x tut, darf man y nicht tun&amp;quot; interpretieren. Ein anderes schönes Beispiel findet sich in Übung 12.&lt;br /&gt;
&lt;br /&gt;
Für die Implementation eines Implikationengraphen in Python empfiehlt es sich, die Knoten geschickt zu numerieren: Ist die Variable &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; dem Knoten i zugeordnet, so sollte die negierte Variable &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; dem Knoten (i+n) zugeordnet werden. Zu jedem gegebenen Knoten i findet man dann den negierten Partnerknoten j leicht durch die Formel &amp;lt;tt&amp;gt;j = (i + n ) % (2*n)&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die Aufgabe besteht jetzt darin, folgende Fragen zu beantworten:&lt;br /&gt;
# Ist der durch den Implikationengraphen gegebene Ausdruck erfüllbar?&lt;br /&gt;
# Finde eine geeignete Belegung der Variablen, wenn der Ausduck erfüllbar ist.&lt;br /&gt;
Die erste Frage beantwortet man leicht, indem man die stark zusammenhängenden Komponenten des Implikationengraphen bildet. Dann gilt folgender&lt;br /&gt;
;Satz: Seien u und v zwei Literale, die sich in der selben stark zusammenhängenden Komponente befinden. Dann müssen u und v stets den selben Wert haben, damit der Ausdruck erfüllt sein kann.&lt;br /&gt;
Die Korrektheit des Satzes folgt aus der Definition der stark zusammenhängenden Komponenten: Da u und v in der selben Komponente liegen, gibt es im Implikationengraphen einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow v&amp;lt;/math&amp;gt; sowie einen Weg &amp;lt;math&amp;gt;v \rightsquigarrow u&amp;lt;/math&amp;gt;. Wegen der Transitivität der &amp;quot;wenn, dann&amp;quot; Relation kann man die Wege zu zwei Implikationen verkürzen, die gleichzeitig gelten müssen: &amp;lt;math&amp;gt;(u \rightarrow v) \wedge (v \rightarrow u)&amp;lt;/math&amp;gt; (die Verkürzung von Wegen zu direkten Kanten entspricht gerade der Bildung der transitiven Hülle für die Knoten u und v). In der obigen Wertetabelle für logische Operatoren erkennt mann, dass dies äquivalent zur Bedingung &amp;lt;math&amp;gt;(u \leftrightarrow v)&amp;lt;/math&amp;gt; ist. Dies ist aber gerade die Behauptung des Satzes.&lt;br /&gt;
&lt;br /&gt;
Die Erfüllbarkeit des Ausdrucks ist nun ein einfacher Spezialfall dieses Satzes. &lt;br /&gt;
;Korrolar: Der gegebene Ausdruck ist genau dann erfüllbar, wenn die Literale &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; sich für kein i in derselben stark zusammenhängenden Komponente befinden.&lt;br /&gt;
Setzt man nämlich im Satz &amp;lt;math&amp;gt;u = x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v = \neg x_i&amp;lt;/math&amp;gt;, und beide Knoten befinden sich in der selben Komponente, dann müsste gelten &amp;lt;math&amp;gt;x_i \leftrightarrow\neg x_i&amp;lt;/math&amp;gt;, was offensichtlich ein Widerspruch ist. Damit kann der Ausdruck nicht erfüllbar sein. Umgekehrt gilt, dass der Ausdruck immer erfüllbar ist, wenn &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; stets in verschiedenen Komponenten liegen, weil der folgende Algorithmus von Aspvall, Plass und Tarjan in diesem Fall stets eine gültige Belegung aller Variablen liefert:&lt;br /&gt;
# Bestimme die stark zusammenhängenden Komponenten und bilde den Komponentengraphen. Ordne die Knoten des Komponentengraphen (also die stark zusammenhängenden Komponenten des Originalgraphen) in topologische Sortierung an.&lt;br /&gt;
# Betrachte die Komponenten in der topologischen Sortierung von hinten nach vorn und weise ihnen einen Wert nach folgenden Regeln zu (zur Erinnerung: alle Literale in der selben Komponente haben den selben Wert):&lt;br /&gt;
#* Wenn die Komponente noch nicht betrachtet wurde, setze ihren Wert auf True, und den Wert der komplementären Komponente (derjenigen, die die negierten Literale enthält) auf False.&lt;br /&gt;
#* Andernfalls, gehe zur nächsten Komponente weiter.&lt;br /&gt;
Der Algorithmus beruht auf der Symmetrie des Implikationengraphen: Weil Kanten immer paarweise &amp;lt;math&amp;gt;(\neg u \rightarrow v) \wedge (\neg v \rightarrow u)&amp;lt;/math&amp;gt; eingefügt werden, ist der Graph &amp;lt;i&amp;gt;schiefsymmetrisch&amp;lt;/i&amp;gt; (skew symmetric): die eine Hälfte das Graphen ist die transponierte Spiegelung der anderen Hälfte. Enthält eine stark zusammenhängende Komponente &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; die Knoten &amp;lt;tt&amp;gt;i1, i2, ...&amp;lt;/tt&amp;gt;, so gibt es stets eine komplementäre Komponente &amp;lt;math&amp;gt;C_j = \neg C_i&amp;lt;/math&amp;gt;, die die komplementären Knoten &amp;lt;tt&amp;gt;j1 = (i1 + n) % (2*n), j2 = (i2 + n) % (2*n), ...&amp;lt;/tt&amp;gt; enthält. Gilt &amp;lt;math&amp;gt;C_i = \neg C_i&amp;lt;/math&amp;gt; für irgendein i, so ist der Ausdruck nicht erfüllbar. Den Beweis für die Korrektheit des Algorithmus findet man im [http://www.math.ucsd.edu/~sbuss/CourseWeb/Math268_2007WS/2SAT.pdf Originalartikel]. Leider funktioniert dies nicht für k-SAT-Probleme mit &amp;lt;math&amp;gt;k &amp;gt; 2&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Will man nur die Erfüllbarkeit prüfen, vereinfacht sich der Algorithmus zu:&lt;br /&gt;
# Bestimme die stark zusammenhängenden Komponenten.&lt;br /&gt;
# Teste für alle &amp;lt;tt&amp;gt;i = 0,...,n-1&amp;lt;/tt&amp;gt;, dass Knoten &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; und Knoten &amp;lt;tt&amp;gt;(i+n)&amp;lt;/tt&amp;gt; in unterschiedlichen Komponenten liegen.&lt;br /&gt;
Ist der Ausdruck erfüllbar, kann man eine gültige Belegung der Variablen jetzt mit dem randomisierten Algorithmus bestimmen, den wir im Kapitel [[Randomisierte Algorithmen]] behandeln.&lt;br /&gt;
&lt;br /&gt;
== Weitere wichtige Graphenalgorithmen ==&lt;br /&gt;
&lt;br /&gt;
Eins der wichtigsten Einsatzgebiete für Graphen ist die Optimierung, also die Suche nach der &amp;lt;i&amp;gt;besten&amp;lt;/i&amp;gt; Lösung für ein gegebenes Problem:&lt;br /&gt;
* Das &amp;lt;i&amp;gt;interval scheduling&amp;lt;/i&amp;gt; befasst sich damit, aus einer gegebenen Menge von Aufträgen die richtigen auszuwählen und sie geschickt auf die zur Verfügung stehenden Ressourcen aufzuteilen. Damit beschäftigen wir uns im Kapitel [[Greedy-Algorithmen und Dynamische Programmierung]].&lt;br /&gt;
* Beim Problem des Handlungsreisenden sucht man nach der kürzesten Rundreise, die alle gegebenen Städte genau einmal besucht. Dieses Problem behandeln wir im Kapitel [[NP-Vollständigkeit]].&lt;br /&gt;
* Viele weitere Anwendungen können wir leider in der Vorlesung nicht mehr behandeln, z.B.&lt;br /&gt;
** Algorithmen für den [http://en.wikipedia.org/wiki/Maximum_flow_problem maximalen Fluss] beantworten die Frage, wie man die Durchflussmenge durch ein Netzwerk (z.B. von Ölpipelines) maximiert.&lt;br /&gt;
** Beim [http://en.wikipedia.org/wiki/Assignment_problem Problem der optimalen Paarung] (&amp;quot;matching problem&amp;quot; oder &amp;quot;assignment problem&amp;quot;) sucht man nach einer Teilmenge der Kanten (also nach einem Teilgraphen), so dass jeder Knoten in diesem Teilgraphen höchstens den Grad 1 hat. Im neuen Graphen gruppieren die Kanten also je zwei Knoten zu einem Paar, und die Paarung soll nach jeweils anwendungsspezifischen Kriterien optimal sein. Dies benötigt man z.B. bei der optimalen Zuordnung von Gruppen, etwas beim Arbeitsamt (Zuordnung Arbeitssuchender - Stellenangebot) und in der Universität (Zuordnung Studenten - Übungsgruppen).&lt;br /&gt;
** In Statistik und maschinellem Lernen haben in den letzten Jahren die [http://en.wikipedia.org/wiki/Graphical_model graphischen Modelle] große Bedeutung erlangt.&lt;br /&gt;
* usw. usf.&lt;br /&gt;
&lt;br /&gt;
[[Randomisierte Algorithmen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5405</id>
		<title>Graphen und Graphenalgorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5405"/>
				<updated>2012-07-25T17:04:53Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Algorithmus 2 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Einführung zu Graphen ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation -- Königsberger Brückenproblem ===&lt;br /&gt;
Leonhard Euler [http://de.wikipedia.org/wiki/Leonhard_Euler] erfand den Graphen-Formalismus 1736, um eine scheinbar banale Frage zu beantworten: Ist es möglich, in Königsberg (siehe Stadtplan von 1809 und die schematische Darstellung) einen Spaziergang zu unternehmen, bei dem jede der 7 Brücken genau einmal überquert wird?&lt;br /&gt;
&lt;br /&gt;
[[Image:Koenigsberg1809.png]]&amp;lt;br&amp;gt;&lt;br /&gt;
[[Image:Koenigsberg.jpg]]&lt;br /&gt;
&lt;br /&gt;
Ein Graph abstrahiert von der Geometrie des Problems und repräsentiert nur die Topologie. Jeder Stadtteil von Königsberg ist ein Knoten des Graphen, jede Brücke eine Kante. Der zum Brückenproblem gehörende Graph sieht also so aus:&lt;br /&gt;
&lt;br /&gt;
     O&lt;br /&gt;
    /| \&lt;br /&gt;
    \|  \&lt;br /&gt;
     O---O&lt;br /&gt;
    /|  /&lt;br /&gt;
    \| /&lt;br /&gt;
     O&lt;br /&gt;
&lt;br /&gt;
Der gesuchte Spaziergang würde existieren, wenn es maximal 2 Knoten gäbe, an denen sich eine ungerade Zahl von Kanten trifft. Die Frage muss für Königsberg also verneint werden, denn hier gibt es vier solche Knoten. Ein leicht modifiziertes Problem ist allerdings lösbar: Im obigen Stadtplan erkennt man eine Fähre, die die Stadtteile Kneiphof und Altstadt verbindet. Bezieht man dieselbe in den Spaziergang ein, ergibt sich folgender Graph, bei dem nur noch zwei Knoten mit ungerader Kantenzahl existieren:&lt;br /&gt;
&lt;br /&gt;
   --O&lt;br /&gt;
  / /| \&lt;br /&gt;
  \ \|  \&lt;br /&gt;
   --O---O&lt;br /&gt;
    /|  /&lt;br /&gt;
    \| /&lt;br /&gt;
     O&lt;br /&gt;
&lt;br /&gt;
Inzwischen haben Graphen eine riesige Zahl weiterer Anwendungen gefunden. Einige Beispiele:&lt;br /&gt;
&lt;br /&gt;
* Landkarten:&lt;br /&gt;
** Knoten: Länder&lt;br /&gt;
** Kanten: gemeinsame Grenzen&lt;br /&gt;
&lt;br /&gt;
* Logische Schaltkreise:&lt;br /&gt;
** Knoten: Gatter&lt;br /&gt;
** Kanten: Verbindungen&lt;br /&gt;
&lt;br /&gt;
* Chemie (Summenformeln):&lt;br /&gt;
** Knoten: chemische Elemente&lt;br /&gt;
** Kanten: Bindungen &lt;br /&gt;
&lt;br /&gt;
* Soziologie (StudiVZ)&lt;br /&gt;
** Soziogramm&lt;br /&gt;
*** Knoten: Personen&lt;br /&gt;
*** Kanten: Freund von ...&lt;br /&gt;
&lt;br /&gt;
=== Definitionen ===&lt;br /&gt;
&lt;br /&gt;
;Ungerichteter Graph: Ein ungerichteter Graph G = ( V, E ) besteht aus&lt;br /&gt;
:* einer endliche Menge V von Knoten (vertices)&lt;br /&gt;
:* einer endlichen Menge &amp;lt;math&amp;gt;E \subset V \times V&amp;lt;/math&amp;gt; von Kanten (edges)&lt;br /&gt;
:Die Paare (u,v) und (v,u) gelten dabei als nur ''eine'' Kante (somit gilt die Symmetriebeziehung: (u,v) ∈ E =&amp;gt; (v,u) ∈ E ). Die Anzahl der Kanten, die sich an einem Knoten treffen, wird als ''Grad'' (engl. ''degree'') dieses Knotens bezeichnet:&lt;br /&gt;
:::degree(v) = |{v' ∈ V | (v,v') ∈ E}|&lt;br /&gt;
:(Die Syntax |{...}| bezeichnet dabei die Mächtigkeit der angegebenen Menge, also die Anzahl der Elemente in der Menge.)&lt;br /&gt;
&lt;br /&gt;
Der Graph des Königsberger Brückenproblems ist ungerichtet. Bezeichnet man die Knoten entsprechend des folgenden Bildes&lt;br /&gt;
    c&lt;br /&gt;
   /| \&lt;br /&gt;
   \|  \&lt;br /&gt;
    b---d &lt;br /&gt;
   /|  /&lt;br /&gt;
   \| /&lt;br /&gt;
    a&lt;br /&gt;
&lt;br /&gt;
gilt für die Knotengrade: &amp;lt;tt&amp;gt;degree(a) == degree(c) == degree(d) == 3&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;degree(b) == 5&amp;lt;/tt&amp;gt;. Genauer muss man bei diesem Graphen von einem ''Multigraphen'' sprechen, weil es zwischen einigen Knotenpaaren (nämlich (a, b) sowie (b, c)) mehrere Kanten (&amp;quot;Mehrfachkanten&amp;quot;) gibt. Wir werden in dieser Vorlesung nicht näher auf Multigraphen eingehen.&lt;br /&gt;
&lt;br /&gt;
;Gerichteter Graph: Ein Graph heißt ''gerichtet'', wenn die Kanten (u,v) und (v,u) unterschieden werden. Die Kante (u,v) ∈ E wird nun als Kante von u nach v (aber nicht umgekehrt) interpretiert. Entsprechend unterscheidet man jetzt den ''eingehenden'' und den ''ausgehenden Grad'' jedes Knotens:&lt;br /&gt;
:*out_degree(v) = |{v' ∈ V | (v,v') ∈ E}|&amp;lt;br/&amp;gt;&lt;br /&gt;
:*in_degree(v)  = |{v' ∈ V| (v',v) ∈ E}|&lt;br /&gt;
&lt;br /&gt;
Das folgende Bild zeigt einen gerichteten Graphen. Hier gilt &amp;lt;tt&amp;gt;out_degree(1) == out_degree(3) == in_degree(2) == in_degree(4) == 2&amp;lt;/tt&amp;gt; und &lt;br /&gt;
&amp;lt;tt&amp;gt;in_degree(1) == in_degree(3) == out_degree(2) == out_degree(4) == 0&amp;lt;/tt&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
[[Image:digraph.png|gerichteter Graph]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Vollständiger Graph: Ein vollständiger Graph ist ein ungerichteter Graph, bei dem jeder Knoten mit allen anderen Knoten verbunden ist.&lt;br /&gt;
:::&amp;lt;math&amp;gt;E = \{ (v,w) |  v \in V, w \in V, v \ne w \}&amp;lt;/math&amp;gt;&lt;br /&gt;
:Ein vollständiger Graph mit |V| Knoten hat &amp;lt;math&amp;gt;|E| = \frac{|V|(|V|-1)}{2}&amp;lt;/math&amp;gt; Kanten.&lt;br /&gt;
&lt;br /&gt;
Die folgenden Abbildungen zeigen die vollständigen Graphen mit einem bis fünf Knoten (auch als K&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; bis K&amp;lt;sub&amp;gt;5&amp;lt;/sub&amp;gt; bezeichnet).&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;0&amp;quot; style=&amp;quot;margin: 1em auto 1em auto&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
| [[Image:k1.png|frame|k1]]&lt;br /&gt;
| [[Image:k2.png|frame|k2]]&lt;br /&gt;
| [[Image:k3.png|frame|k3]]&lt;br /&gt;
|-&lt;br /&gt;
| [[Image:k4.png|frame|k4]]&lt;br /&gt;
| [[Image:k5.png|frame|k5]]&lt;br /&gt;
|&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
''Rätsel''&amp;lt;br/&amp;gt;&lt;br /&gt;
Auf einer Party sind Leute. Alle stoßen miteinander an. Es hat 78 mal &amp;quot;Pling&amp;quot; gemacht.&lt;br /&gt;
Wieviele Leute waren da? Antwort: Jede Person ist ein Knoten des Graphen, jedes Antoßen eine Kante. &lt;br /&gt;
Da alle miteinander angestoßen haben, handelt es sich um einen vollständigen Graphen. Mit&lt;br /&gt;
|V|(|V|-1)/2 = 78 folgt, dass es 13 Personen waren.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Gewichteter Graph: Ein Graph heißt ''gewichtet'', wenn jeder Kante eine reelle Zahl zugeordnet ist. Bei vielen Anwendungen beschränkt man sich auch auf nichtnegative reelle Gewichte. In einem gerichteten Graphen können die Gewichte der Kanten (u,v) und (v,u) unterschiedlich sein.&lt;br /&gt;
&lt;br /&gt;
Die Gewichte kodieren Eigenschaften der Kanten, die für die jeweilige Anwendung interessant sind. Bei der Berechnung des maximalen Flusses in einem Netzwerk sind die Gewichte z.B. die Durchflusskapazitäten jeder Kante, bei der Suche nach kürzesten Weges kodieren Sie den Abstand zwischen den Endknoten der Kante, bei Währungsnetzwerken (jeder Knoten ist eine Währung) geben sie die Wechselkurse an, usw..&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Teilgraphen: Ein Graph G' = (V',E') ist ein Teilgraph eines Graphen G, wenn gilt:&lt;br /&gt;
:* V' &amp;amp;sube; V &lt;br /&gt;
:* E' &amp;amp;sub; E &lt;br /&gt;
:Er heißt ''(auf)spannender Teilgraph'', wenn gilt:&lt;br /&gt;
:* V' = V&lt;br /&gt;
:Er heißt ''induzierter Teilgraph'', wenn gilt:&lt;br /&gt;
:* e = (u,v) ∈ E' &amp;amp;sub; E &amp;amp;hArr; u ∈ V' und v ∈ V'&lt;br /&gt;
:Den von V' induzierten Teilgraphen erhält man also, indem man aus G alle Knoten löscht, die nicht in V' sind, sowie alle Kanten (und nur diese Kanten), die einen der gelöschten Knoten als Endknoten haben.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Wege, Pfade, Zyklen, Kreise, Erreichbarkeit: Sei G = (V,E) ein Graph (ungerichtet oder gerichteter) Graph. Dann gilt folgende rekursive Definition:&lt;br /&gt;
:* Für v ∈ V ist (v) ein Weg der Länge 0 in G&lt;br /&gt;
:* Falls &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1})&amp;lt;/math&amp;gt; ein Weg ist, und eine Kante &amp;lt;math&amp;gt;(v_{n-1}, v_n)\in E&amp;lt;/math&amp;gt; existiert, dann ist auch &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ein Weg, und er hat die Länge n. &lt;br /&gt;
: Ein Weg ist also eine nichtleere Folge von Knoten, so dass aufeinander folgende Knoten stets durch eine Kante verbunden sind. Die Länge des Weges entspricht der Anzahl der Kanten im Weg (= Anzahl der Knoten - 1).&lt;br /&gt;
:* Ein ''Pfad'' &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ist ein Weg, bei dem alle Knoten v&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; verschieden sind.&lt;br /&gt;
:* ''Ein Zyklus'' &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ist ein Weg, der zum Ausgangspunkt zurückkehrt, wenn also v&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; = v&amp;lt;sub&amp;gt;n&amp;lt;/sub&amp;gt; gilt.&lt;br /&gt;
:* Ein ''Kreis'' ist ein Zyklus ohne Überkreuzungen. Das heisst, es gilt v&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; = v&amp;lt;sub&amp;gt;n&amp;lt;/sub&amp;gt; und &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1})&amp;lt;/math&amp;gt; ist ein Pfad.&lt;br /&gt;
:* Ein Knoten w ∈ V ist von einem anderen Knoten v ∈ V aus ''erreichbar'' genau dann, wenn ein Weg (v, ..., w) existiert. Wir schreiben dann &amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt;.&lt;br /&gt;
In einem ungerichteten Graph ist die Erreichbarkeits-Relation stets symmetrisch, das heisst aus &amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; folgt &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. In einem gerichteten Graphen ist dies im allgemeinen nicht der Fall.&lt;br /&gt;
&lt;br /&gt;
Bestimmte Wege haben spezielle Namen&lt;br /&gt;
&lt;br /&gt;
;Eulerweg: Ein Eulerweg ist ein Weg, der alle '''Kanten''' genau einmal enthält.&lt;br /&gt;
&lt;br /&gt;
Die eingangs erwähnte Frage des Königsberger Brückenproblems ist equivalent zu der Frage, ob der dazugehörige Graph einen Eulerweg besitzt (daher der Name). Ein anderes bekanntes Beispiel ist das &amp;quot;Haus vom Nikolaus&amp;quot;: Wenn man diesen Graphen in üblicher Weise in einem Zug zeichnet, erhält man gerade den Eulerweg. &lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O----O&lt;br /&gt;
  | \/ |&lt;br /&gt;
  | /\ |   &amp;quot;Das Haus vom Nikolaus&amp;quot;: Alle ''Kanten'' werden nur ''einmal'' passiert&lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Hamiltonweg: Ein Hamiltonweg ist ein Weg, der alle '''Knoten''' genau einmal enthält. Das &amp;quot;Haus vom Nikolaus&amp;quot; besitzt auch einen Hamiltonweg:&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /   &lt;br /&gt;
  O----O&lt;br /&gt;
     /  &lt;br /&gt;
    /      Alle ''Knoten'' werden nur ''einmal'' passiert&lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Hamiltonkreis: Ein Hamiltonkreis ist ein Kreis, der alle '''Knoten''' genau einmal enthält. Auch ein solches Gebilde ist im Haus von Nilolaus enthalten:&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O    O&lt;br /&gt;
  |    |   v0 = vn&lt;br /&gt;
  |    |   vi != vj   Für Alle i,j   i !=j; i,j &amp;gt;0; i,j &amp;lt; n&lt;br /&gt;
  O----O     &lt;br /&gt;
&lt;br /&gt;
Die folgende Skizze zeigt hingegen einen Zyklus: Der Knoten rechts unten sowie die untere Kante sind zweimal enthalten (die Kante einmal von links nach rechts und einmal von rechts nach links):&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O    O&lt;br /&gt;
    \  |&lt;br /&gt;
     \ |   Zyklus&lt;br /&gt;
  O====O&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Zusammenhang, Zusammenhangskomponenten: Ein ungerichteter Graph G heißt ''zusammenhängend'', wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt;&lt;br /&gt;
:Ein gerichteter Graph G ist zusammenhängend, wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; '''oder''' &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. &lt;br /&gt;
:Er ist ''stark zusammenhängend'', wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; '''und''' &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. &lt;br /&gt;
:Entsprechende Definitionen gelten für Teilgraphen G'. Ein Teilgraph G' heisst ''Zusammenhangskomponente'' von G, wenn er ein ''maximaler'' zusammenhängender Teilgraph ist, d.h. wenn G' zusammenhängend ist, und man keine Knoten und Kanten aus G mehr zu G' hinzufügen kann, so dass G' immer noch zusammenhängend bleibt. Entsprechend definiert man ''starke Zusammenhangskomponenten'' in einem gerichteten Graphen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Planarer Graph, ebener Graph: Ein Graph heißt ''planar'', wenn er so in einer Ebene gezeichnet werden ''kann'', dass sich die Kanten nicht schneiden (außer an den Knoten). Ein Graph heißt ''eben'', wenn er tatsächlich so gezeichnet ''ist'', dass sich die Kanten nicht schneiden. Die Einbettung in die Ebene ist im allgemeinen nicht eindeutig.&lt;br /&gt;
&lt;br /&gt;
'''Beispiele:'''&lt;br /&gt;
&lt;br /&gt;
Der folgende Graph ist planar und eben:&lt;br /&gt;
 &lt;br /&gt;
      O&lt;br /&gt;
     /|\&lt;br /&gt;
    / O \&lt;br /&gt;
   / / \ \&lt;br /&gt;
   O     O&lt;br /&gt;
&lt;br /&gt;
Das &amp;quot;Haus vom Nikolaus&amp;quot; ist ebenfalls planar, wird aber üblicherweise nicht als ebener Graph gezeichnet, weil sich die Diagonalen auf der Wand überkreuzen:&lt;br /&gt;
 &lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O----O&lt;br /&gt;
  | \/ |&lt;br /&gt;
  | /\ |   &lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
Eine ebene Einbettung dieses Graphen wird erreicht, wenn man eine der Diagonalen ausserhalb des Hauses zeichnet. Der Graph (also die Menge der Knoten und Kanten) ändert sich dadurch nicht.&lt;br /&gt;
 &lt;br /&gt;
      O  &lt;br /&gt;
     /  \&lt;br /&gt;
  --O----O&lt;br /&gt;
 /  |  / |&lt;br /&gt;
 |  | /  |   &lt;br /&gt;
 |  O----O      Das &amp;quot;Haus vom Nikolaus&amp;quot; als ebener Graph gezeichnet.&lt;br /&gt;
  \     /&lt;br /&gt;
   -----&lt;br /&gt;
&lt;br /&gt;
Eine alternative Einbettung erhalten wir, wenn wir die andere Diagonale außerhalb des Hauses zeichnen:&lt;br /&gt;
 &lt;br /&gt;
      O  &lt;br /&gt;
     /  \&lt;br /&gt;
    O----O--|&lt;br /&gt;
    | \  |  |&lt;br /&gt;
    |  \ |  | &lt;br /&gt;
    O----O  |     Alternative Einbettung des &amp;quot;Haus vom Nikolaus&amp;quot;.&lt;br /&gt;
    |       |&lt;br /&gt;
    |-------|&lt;br /&gt;
&lt;br /&gt;
Jede Einbettung eines planaren Graphen (also jeder ebene Graph) definiert eine eindeutige Menge von ''Regionen'':&lt;br /&gt;
&lt;br /&gt;
 |----O   @&lt;br /&gt;
 |   /@ \&lt;br /&gt;
 |  O----O&lt;br /&gt;
 |  |@ / |&lt;br /&gt;
 |  | / @|   &lt;br /&gt;
 |  O----O        @ entspricht jeweils einer ''Region''. Auch ausserhalb der Figur ist eine Region (die sogenannte ''unendliche'' Region).&lt;br /&gt;
 |@      |&lt;br /&gt;
 |-------|&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Der vollständige Graph K5 ist kein planarer Graph, da sich zwangsweise Kanten schneiden, wenn man diesen Graphen in der Ebene zeichnet.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
;Dualer Graph: Jeder ebene Graph G = (V, E) hat einen ''dualen Graphen'' D = (V&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;, E&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;), dessen Knoten und Kanten wie folgt definiert sind:&lt;br /&gt;
:* V&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt; enthält einen Knoten für jede Region des Graphen G&lt;br /&gt;
:* Für jede Kante e ∈ E gibt es eine duale Kante e&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt; ∈ E&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;, die die an e angrenzenden Regionen (genauer: die entsprechenden Knoten in D) verbindet.&lt;br /&gt;
&lt;br /&gt;
Die folgende Abbildung zeigt einen Graphen (grau) und seinen dualen Graphen (schwarz). Die Knoten des dualen Graphen sind mit Zahlen gekennzeichnet und entsprechen den Regionen des Originalgraphen. Jeder (grauen) Kante des Originalgraphen entspricht eine (schwarze) Kante des dualen Graphen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
[[Image:dual-graphs.png]]&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für duale Graphen gilt: Wenn der Originalgraph zusammenhängend ist, enthält jede Region des dualen Graphen genau einen Knoten des Originalgraphen. Deshalb ist der duale Graph des dualen Graphen wieder der Originalgraph. Bei nicht-zusammenhängenden Graphen gilt dies nicht (vgl. das Fenster bei obigem Bild). In diesem Fall hat der duale Graph mehrere mögliche Einbettungen in die Ebene (man kann z.B. die rechte Kante zwischen Knoten 2 und 4 auch links vom Fenster einzeichnen), und man erhält nicht notwendigerweise den Originalgraphen, wenn man den dualen Graphen des dualen berechnet.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Baum: Ein ''Baum'' ist ein zusammenhängender, kreisfreier Graph.&lt;br /&gt;
&lt;br /&gt;
Beispiel: Binärer Suchbaum&lt;br /&gt;
&lt;br /&gt;
;Spannbaum: Ein ''Spannbaum'' eines zusammenhängenden Graphen G ist ein zusammenhängender, kreisfreier Teilgraph von G, der alle Knoten von G enthält&lt;br /&gt;
&lt;br /&gt;
Beispiel: Spannbaum für das &amp;quot;Haus des Nikolaus&amp;quot; &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    O   &lt;br /&gt;
   /       &lt;br /&gt;
  O    O&lt;br /&gt;
  |  /  &lt;br /&gt;
  | /   &lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
Der Spannbaum eines Graphen mit |V| Knoten hat stets |V| - 1 Kanten.&lt;br /&gt;
&lt;br /&gt;
;Wald: Ein ''Wald'' ist ein unzusammenhängender, kreisfreier Graph.&lt;br /&gt;
: Jede Zusammenhangskomponente eines Waldes ist ein Baum.&lt;br /&gt;
&lt;br /&gt;
=== Repräsentation von Graphen ===&lt;br /&gt;
&lt;br /&gt;
Sei G = ( V, E ) gegeben und liege V in einer linearen Sortierung vor.&amp;lt;br/&amp;gt; &lt;br /&gt;
:::&amp;lt;math&amp;gt;V = \{ v_1, ...., v_n \}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Adjazenzmatrix: Ein Graph kann durch eine Adjazenzmatrix repräsentiert werden, die soviele Zeilen und Spalten enthält, wie der Graph Knoten hat. Die Elemente der Adjazenzmatrix sind &amp;quot;1&amp;quot;, falls eine Kante zwischen den zugehörigen Knoten existiert:&lt;br /&gt;
:::&amp;lt;math&amp;gt;\mathrm{\bold A} = a_{ij} = &lt;br /&gt;
\begin{cases}&lt;br /&gt;
1 &amp;amp; \mathrm{falls}\quad (v_i, v_j) \in E \\&lt;br /&gt;
0 &amp;amp; \mathrm{sonst}&lt;br /&gt;
\end{cases} &lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
:Die Indizes der Matrix entsprechen also den Indizes der Knoten gemäß der gegebenen Sortierung. Im Falle eines ungerichteten Graphen ist die Adjazenzmatrix stets symmetrisch (d.h. es gilt &amp;lt;math&amp;gt;a_{ij}=a_{ji}&amp;lt;/math&amp;gt;), bei einem gerichteten Graphen ist sie im allgemeinen unsymmetrisch.&lt;br /&gt;
&lt;br /&gt;
Beispiel für einen ungerichteten Graphen:&lt;br /&gt;
&lt;br /&gt;
 v = { a,b,c,d }     b      d&lt;br /&gt;
                     | \  / |&lt;br /&gt;
                     |  \/  |&lt;br /&gt;
                     |  /\  |&lt;br /&gt;
                     | /  \ |&lt;br /&gt;
                     a      c&lt;br /&gt;
 &lt;br /&gt;
       a b c d&lt;br /&gt;
      -----------&lt;br /&gt;
      (0 1 0 1) |a &lt;br /&gt;
  A = (1 0 1 0) |b&lt;br /&gt;
      (0 1 0 1) |c&lt;br /&gt;
      (1 0 1 0) |d&lt;br /&gt;
&lt;br /&gt;
Die Adjazenzmatrixdarstellung eignet sich besonders für dichte Graphen (d.h. wenn die Zahl der Kanten in O(|V|&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt;) ist.&lt;br /&gt;
&lt;br /&gt;
;Adjazenzlisten: In der Adjazenzlistendarstellung wird der Graph als Liste von Knoten repräsentiert, die für jeden Knoten einen Eintrag enthält. Der Eintrag für jeden Knoten ist wiederum eine Liste, die die Nachbarknoten dieses Knotens enthält:&lt;br /&gt;
:* graph = {adjazencyList(v) | v ∈ V}&lt;br /&gt;
:* adjazencyList(v) = {v' ∈ V | (v, v') ∈ E}&lt;br /&gt;
&lt;br /&gt;
In Python implementieren wir Adjazenzlisten zweckmäßig als Array von Arrays:&lt;br /&gt;
&lt;br /&gt;
                   graph = [[...],[...],...,[...]]&lt;br /&gt;
 Adjazenzliste für Knoten =&amp;gt;  0     1         n&lt;br /&gt;
&lt;br /&gt;
Wenn wir bei dem Graphen oben die Knoten wie bei der Adjazenzmatrix indizieren (also &amp;lt;tt&amp;gt;a =&amp;gt; 0&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;b =&amp;gt; 1&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;c =&amp;gt; 2&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;d =&amp;gt; 3&amp;lt;/tt&amp;gt;), erhalten wir die Adjazenzlistendarstellung:&lt;br /&gt;
&lt;br /&gt;
 graph = [[b, d], [a, c],[b, d], [a, c]]&lt;br /&gt;
&lt;br /&gt;
Auf die Nachbarknoten eines durch seinen Index &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; gegebenen Knotens können wir also wie folgt zugreifen:&lt;br /&gt;
&lt;br /&gt;
      for neighbors in graph[node]:&lt;br /&gt;
          ... # do something with neighbor&lt;br /&gt;
&lt;br /&gt;
Die Adjazenzlistendarstellung ist effizienter, wenn der Graph nicht dicht ist, so dass viele Einträge der Adjazenzmatrix Null wären. In der Vorlesung werden wir nur diese Darstellung verwenden.&lt;br /&gt;
&lt;br /&gt;
;&amp;lt;div id=&amp;quot;transposed_graph&amp;quot;&amp;gt;Transponierter Graph&amp;lt;/div&amp;gt;: Den ''transponierten Graphen'' G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; eines gerichteten Graphen G erhält man, wenn man alle Kantenrichtungen umkehrt.&lt;br /&gt;
&lt;br /&gt;
Bei ungerichteten Graphen hat die Transposition offensichtlich keinen Effekt, weil alle Kanten bereits in beiden Richtungen vorhanden sind, so dass G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; = G gilt. Bei gerichteten Graphen ist die Transposition einfach, wenn der Graph als Adjazenzmatrix implementiert ist, weil man einfach die transponierte Adjazenzmatrix verwenden muss (beachte, dass sich die Reihenfolge der Indizes umkehrt):&lt;br /&gt;
:::A&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; = a&amp;lt;sub&amp;gt;ji&amp;lt;/sub&amp;gt;&lt;br /&gt;
Ist der Graph hingegen durch eine Adjazenzliste repräsentiert, muss etwas mehr Aufwand getrieben werden:&lt;br /&gt;
&lt;br /&gt;
 def transposeGraph(graph):&lt;br /&gt;
      gt = [[] for k in graph]   # zunächst leere Adjazenzlisten von G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt;&lt;br /&gt;
      for node in range(len(graph)):&lt;br /&gt;
           for neighbor in graph[node]:&lt;br /&gt;
               gt[neighbor].append(node)  # füge die umgekehrte Kante in G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; ein&lt;br /&gt;
      return gt&lt;br /&gt;
&lt;br /&gt;
== Durchlaufen von Graphen (Graph Traversal) ==&lt;br /&gt;
&lt;br /&gt;
Wir betrachten zunächst ungerichtete Graphen mit V Knoten und E Kanten. Eine grundlegende Aufgabe in diesen Graphen besteht darin, alle Knoten in einer bestimmten Reihenfolge genau einmal zu besuchen. Hierbei darf man sich von einem gegebenen Startknoten aus nur entlang der Kanten des Graphen bewegen. Die beim Traversieren benutzen Kanten bilden einen Baum, dessen Wurzel der Startknoten ist und der den gesamten Graphen aufspannt, falls der Graph zusammenhängend ist. (Beweis: Da jeder Knoten nur einmal besucht wird, gibt es für jeden besuchten Knoten [mit Ausnahme des Startknotens] genau eine eingehende Kante. Ist der Graph zusammenhängend, wird jeder Knoten tatsächlich erreicht und es gibt genau (V-1) Kanten, exakt soviele wie für einen Baum mit V Knoten notwendig sind.) Ist der Graph nicht zusammenhängend, wird jeder zusammenhängende Teilgraph (jede &amp;lt;i&amp;gt;Zusammenhangskomponente&amp;lt;/i&amp;gt;) getrennt traversiert, und man erhält einen sogenannten &amp;lt;i&amp;gt;Wald&amp;lt;/i&amp;gt; mit einem Baum pro Zusammenhangskomponente. Die beiden grundlegenden Traversierungsmethoden &amp;lt;i&amp;gt;Tiefensuche&amp;lt;/i&amp;gt; und &amp;lt;i&amp;gt;Breitensuche&amp;lt;/i&amp;gt; werden im folgenden vorgestellt.&lt;br /&gt;
&lt;br /&gt;
=== Tiefensuche in Graphen (Depth First Search, DFS) ===&lt;br /&gt;
&lt;br /&gt;
Die Idee der Tiefensuche besteht darin, jeden besuchten Knoten sofort über die erste Kante wieder zu verlassen, die zu einem noch nicht besuchten Knoten führt. Man findet dadurch schnell einen möglichst langen Pfad durch den Graphen, und der Traversierungs-Baum wird zunächst in die Tiefe verfolgt, daher der Name des Verfahrens. Hat ein Knoten keine unbesuchten Nachbarknoten mehr, geht man im Baum zurück (sogenanntes &amp;lt;i&amp;gt;back tracking&amp;lt;/i&amp;gt;), bis man einen Knoten findet, der noch eine unbesuchte Nachbarn besitzt, und traversiert diese nach dem gleichen Muster. Gibt es gar keine unbesuchten Knoten mehr, kehrt die Suche zum Startknoten zurück und endet dort.&lt;br /&gt;
&lt;br /&gt;
WDie folgende rekursive Implementation der Tiefensuche erwartet den Graphen in Adjazenzlistendarstellung und beginnt die Suche beim Knoten &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt;. Die Information, ob ein Knoten bereits besucht wurde, wird im Array &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; gespeichert. Ein solches Array, das zusätzliche Informationen über die Knoten des Graphen bereitstellt, wir häufig &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; genannt.&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)  # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):              # rekursive Hilfsfunktion, die den gegebenen Knoten und dessen Nachbarn besucht&lt;br /&gt;
         if not visited[node]:     # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True  # Markiere node als besucht&lt;br /&gt;
             print node            # Ausgabe der Knotennummer - pre-order&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode)&lt;br /&gt;
&lt;br /&gt;
[[Image:Tiefens.jpg]]&lt;br /&gt;
&lt;br /&gt;
Ausgabe für den Graphen in diesem Bild (es handelt sich um einen ungerichteten Graphen, die Pfeile symbolisieren nur die Suchrichtung beim Traversal):&lt;br /&gt;
&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; dfs(graph, 1)&lt;br /&gt;
 1&lt;br /&gt;
 2&lt;br /&gt;
 4&lt;br /&gt;
 3&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
 5&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div id=&amp;quot;pre_and_post_order&amp;quot;&amp;gt;In dieser Version des Algorithmus werden die Knotennummern ausgegeben, bevor die Nachbarknoten besucht werden. Man bezeichnet die resultierende Sortierung der Knoten als &amp;lt;b&amp;gt;pre-order&amp;lt;/b&amp;gt; oder als &amp;lt;b&amp;gt;discovery order&amp;lt;/b&amp;gt;. Alternativ kann man die Knotennummern erst ausgeben, nachdem alle Nachbarn besucht wurden, also auf dem Rückweg der Rekursion. In diesem Fall spricht man von &amp;lt;b&amp;gt;post-order&amp;lt;/b&amp;gt; oder &amp;lt;b&amp;gt;finishing order&amp;lt;/b&amp;gt;:&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)  # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):              # rekursive Hilfsfunktion, die den gegebenen Knoten und dessen Nachbarn besucht&lt;br /&gt;
         if not visited[node]:     # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True  # Markiere node als besucht&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
             &amp;lt;font color=red&amp;gt;print node            # Ausgabe der Knotennummer - post-order&amp;lt;/font&amp;gt;&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode)&lt;br /&gt;
&lt;br /&gt;
Es ergibt sich jetzt die Ausgabe:&lt;br /&gt;
&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; dfs(graph, 1)&amp;lt;font color=red&amp;gt;&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
 3&lt;br /&gt;
 4&lt;br /&gt;
 5&lt;br /&gt;
 2&lt;br /&gt;
 1&amp;lt;/font&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In realem Code ersetzt man die print-Ausgaben natürlich durch anwendungsspezifische Aktionen und Berechnungen. Einige Anwendungen sind uns im Kapitel [[Suchen]] bereits begegnet. &lt;br /&gt;
; Anwendungen der Pre-Order Traversierung&lt;br /&gt;
* Kopieren eines Graphen: kopiere zuerst den besuchten Knoten, dann seine Nachbarn und die dazugehörigen Kanten (sowie die Kanten zu bereits besuchten Knoten, die in der Grundversion der Tiefensuche ignoriert werden).&lt;br /&gt;
* Bestimmen der Zusammenhangskomponenten eines Graphen (siehe unten)&lt;br /&gt;
* In einem Zeichenprogramm: fülle eine Region mit einer Farbe (&amp;quot;flood fill&amp;quot;). Dabei ist jedes Pixel ein Knoten des Graphen und wird mit seinen 4 Nachbarpixceln verbunden. Die Tiefensuche startet bei der Mausposition und endet am Rand des betreffendcen Gebiets.&lt;br /&gt;
* Falls der Graph ein Baum ist: bestimme den Abstand jedes Knotens von der Wurzel&lt;br /&gt;
* Falls der Graph ein Parse-Baum ist, wobei innere Knoten Funktionsaufrufe, Kindknoten Funktionsargumente, und Blattknoten Werte repräsentieren: drucke den zugehörigen Ausdruck aus (also immer zuerst den Funktionsnamen, dann die Argumente, die wiederum geschachtelte Funktionsaufrufe sein können).&lt;br /&gt;
; Anwendungen der Post-Order Traversierung&lt;br /&gt;
* Löschen eines Graphen: lösche zuerst die Nachbarn, dann den Knoten selbst&lt;br /&gt;
* Bestimmen einer topologischen Sortierung eines azyklischen gerichteten Graphens (siehe unten)&lt;br /&gt;
* Falls der Graph ein Baum ist: bestimme den Abstand jedes Knotens von den Blättern (also die Tiefe des Baumes, siehe Übung 5)&lt;br /&gt;
* Falls der Graph ein Parse-Baum ist: führe die zugehörige Berechnung aus (d.h. berechne zuerst die geschachtelten inneren Funktionen, dann mit diesen Ergebnissen die nächst äußeren usw., siehe Übung 5).&lt;br /&gt;
; Anwendungen, die Pre- und Post-Order benötigen&lt;br /&gt;
* Weg aus einem Labyrinth: die Pre-Order dokumentiert die Suche nach dem Weg, die Post-Order zeigt den Rückweg aus Sackgassen (siehe Übung 9).&lt;br /&gt;
Im Spezialfall, wenn der Graph ein Binärbaum ist, unterscheidet man noch eine dritte Variante der Traversierung, nämlich die &amp;lt;i&amp;gt;in-order&amp;lt;/i&amp;gt; Traversierung. In diesem Fall behandelt man den Vaterknoten nach den linken, aber vor den rechten Kindern. Diese Reihenfolge wird beim [[Suchen#Beziehungen zwischen dem Suchproblem und dem Sortierproblem|Tree Sort Algorithmus]] verwendet. Diese Sortierung verwendet man auch, wenn man einen Parse-Baum mit binären Operatoren (statt Funktionsaufrufen) ausgeben will, siehe Übung 5.&lt;br /&gt;
&lt;br /&gt;
Eine nützliche Erweiterung der Tiefensuche besteht darin, in der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; nicht nur zu dokumentieren, dass ein Knoten bereits besucht wurde, sondern auch, von welchem Knoten aus man den jeweiligen Knoten zuerst erreicht hat. Im entstehenden Tiefensuchbaum ist dies gerade der Vaterknoten, weshalb wir die verbesserte property map zweckmäßigerweise in &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; umbenennen. Für den Startknoten, also die Wurzel des Baumes, wählen wir die Konvention, dass er sein eigener Vaterknoten ist (die Konvention, dafür den Wert &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zu verwenden, scheidet aus, weil dies bereits die Tatsache signalisiert, dass ein Knoten noch nicht besucht wurde):&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     parents = [None]*len(graph)     # Registriere für jeden Knoten den Vaterknoten im Tiefensuchbaum&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, parent):        # rekursive Hilfsfunktion&lt;br /&gt;
         if parents[node] is None:   # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             parents[node] = parent  # Markiere node als besucht und speichere seinen Vaterknoten&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn ...&lt;br /&gt;
                 visit(neighbor, node)      #  ... wobei node zu deren Vaterknoten wird&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode, startnode)     # Konvention für Wurzel: startnode ist sein eigener Vater&lt;br /&gt;
     &lt;br /&gt;
     return parents                  # Rückgabe des berechneten Tiefensuch-Baums&lt;br /&gt;
&lt;br /&gt;
Die Ausgabe für den obigen Beispielgraphen lautet: &lt;br /&gt;
  Knotennummer  |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7&lt;br /&gt;
  --------------+-----+-----+-----+-----+-----+-----+-----+-----&lt;br /&gt;
  Vaterknoten   | None|  1  |  1  |  4  |  2  |  2  |  3  |  3&lt;br /&gt;
&lt;br /&gt;
Dabei ist die Knotennummer der Index im Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt;, und der Vaterknoten ist der dazugehörige Arrayeintrag. Beachte, dass Knoten 0 in diesem Graphen nicht existiert, daher ist sein Eintrag &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;. Per Konvention hat der Wurzelknoten 1 sich selbst als Vater.&lt;br /&gt;
&lt;br /&gt;
=== Breitensuche in Graphen (Breadth First Search, BFS) ===&lt;br /&gt;
&lt;br /&gt;
Im Gegensatz zur Tiefensuche werden bei der Breitensuche alle Nachbarnknoten abgearbeitet, &amp;lt;i&amp;gt;bevor&amp;lt;/i&amp;gt; man rekursiv deren Nachbarn besucht. Man betrachtet somit zuerst alle Knoten, die den Abstand 1 von Startknoten haben, dann diejenigen mit dem Abstand 2 usw. Diese Reihenfolge bezeichnet man als &amp;lt;i&amp;gt;level-order&amp;lt;/i&amp;gt;. Wir sind ihr beispielsweise in Übung 6 begegnet, als die ersten 7 Ebenen eines Treap ausgegeben werden sollten. Man implementiert Breitensuche zweckmäßig mit Hilfe einer Queue, die die Knoten in First In - First Out - Reihenfolge bearbeitet. Eine geeignete Datenstruktur hierfür ist die Klasse &amp;lt;tt&amp;gt;[http://docs.python.org/library/collections.html#collections.deque deque]&amp;lt;/tt&amp;gt; aus dem Python-Modul &amp;lt;tt&amp;gt;[http://docs.python.org/library/collections.html collections]&amp;lt;/tt&amp;gt; (eine Deque implementiert sowohl die Funktionalität einer Queue wie auch die eines Stacks, siehe Übung 3):&lt;br /&gt;
&lt;br /&gt;
 from collections import deque&lt;br /&gt;
 &lt;br /&gt;
 def bfs(graph, startnode)&lt;br /&gt;
     visited = [False]*len(graph)   # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
   &lt;br /&gt;
     q = deque()                    # Queue für die zu besuchenden Knoten&lt;br /&gt;
     q.append(startnode)            # Startknoten in die Queue einfügen&lt;br /&gt;
     &lt;br /&gt;
     while len(q) &amp;gt; 0:              # Solange es noch unbesuchte Knoten gibt&lt;br /&gt;
         node = q.popleft()         # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
         if not visited[node]:      # Falls node noch nicht (auf einem anderen Weg) besucht wurde&lt;br /&gt;
              visited[node] = True  # Markiere node als besucht&lt;br /&gt;
              print node            # Drucke Knotennummer&lt;br /&gt;
              for neighbor in graph[node]:    # Füge Nachbarn in die Queue ein&lt;br /&gt;
                  q.append(neighbor)&lt;br /&gt;
&lt;br /&gt;
[[Image:Breitens.jpg]]&lt;br /&gt;
&lt;br /&gt;
Der Aufruf dieser Funktion liefert die Knoten des obigen Graphens ebenenweise, also zufällig genau in der Reihenfolge der Knotennummern:&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; bfs(graph, 1)&lt;br /&gt;
 1&lt;br /&gt;
 2&lt;br /&gt;
 3&lt;br /&gt;
 4&lt;br /&gt;
 5&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
&lt;br /&gt;
Neben der ebenenweisen Ausgabe hat die Breitensuche viele weitere wichtige Anwendungen, z.B. beim Testen, ob ein gegebener Graph bi-partit ist (siehe [http://en.wikipedia.org/wiki/Breadth-first_search#Testing_bipartiteness WikiPedia]), sowie bei der Suche nach kürzesten Wegen (siehe unten) und kürzesten Zyklen.&lt;br /&gt;
&lt;br /&gt;
== Weitere Anwendungen der Tiefensuche ==&lt;br /&gt;
&lt;br /&gt;
Die Tiefensuche hat zahlreiche Anwendungen, wobei der grundlegende Algorithmus immer wieder leicht modifiziert und an die jeweilige Aufgabe angepasst wird. Wir beschreiben im folgenden einige Beispiele.&lt;br /&gt;
&lt;br /&gt;
=== Damenproblem ===&lt;br /&gt;
&lt;br /&gt;
Tiefensuche wird häufig verwendet, um systematisch nach der Lösung eines logischen Rätsels (oder allgemeiner nach der Lösung eines diskreten Optimierungsproblems) zu suchen. Besonders anschaulich hierfür ist das Damenproblem. Die Aufgabe besteht darin, &amp;lt;math&amp;gt;k&amp;lt;/math&amp;gt; Damen auf einem Schachbrett der Größe &amp;lt;math&amp;gt;k \times k&amp;lt;/math&amp;gt; so zu platzieren, dass sie sich (nach den üblichen Schach-Regeln) nicht gegenseitig schlagen können. Das folgende Diagramm zeigt eine Lösung für den Fall &amp;lt;math&amp;gt;k=4&amp;lt;/math&amp;gt;. Die Positionen der Damen werden dabei wie üblich durch die Angabe der Spalte (Linie) mit Buchstaben und der Zeile (Reihe) mit Zahlen kodiert, hier also A2, B4, C1, D3:&lt;br /&gt;
&lt;br /&gt;
  ---------------&lt;br /&gt;
 |   | X |   |   | 4&lt;br /&gt;
 |---|---|---|---| &lt;br /&gt;
 |   |   |   | X | 3&lt;br /&gt;
 |---|---|---|---|&lt;br /&gt;
 | X |   |   |   | 2&lt;br /&gt;
 |---|---|---|---|&lt;br /&gt;
 |   |   | X |   | 1&lt;br /&gt;
  ---------------&lt;br /&gt;
   A   B   C   D&lt;br /&gt;
&lt;br /&gt;
Um das Problem systematisch zu lösen, konstruieren wir einen gerichteten Graphen, dessen Knoten die möglichen Positionen der Damen kodieren. Wir verbinden Knoten, die zu benachbarten Linien gehören, genau dann mit einer Kante, wenn die zugehörigen Positionen kompatibel sind, also wenn sich die dort positionierten Damen nicht schlagen können. Der resultierende Graph für &amp;lt;math&amp;gt;k=4&amp;lt;/math&amp;gt; hat folgende Gestalt:&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
Knoten, die zur selben Reihe oder Linie gehören, sind beispielsweise nicht direkt verbunden, weil zwei Damen niemals in derselben Linie oder Reihe stehen dürfen. Um eine erlaubte Konfiguration zu finden, verwenden wir nun eine angepasste Version der Tiefensuche: Wir beginnen die Suche beim Knoten &amp;lt;tt&amp;gt;START&amp;lt;/tt&amp;gt;. Sobald wir den Knoten &amp;lt;tt&amp;gt;STOP&amp;lt;/tt&amp;gt; erreichen, beenden wir die Suche und lesen die Lösung am gerade gefundenen Weg von Start nach Stop ab. Zwei kleine Modifikationen des Grundalgorithmus stellen sicher, dass die Bedingungen der Aufgabe eingehalten werden: Wir dürfen bei der Tiefensuche nur dann zu einem Nachbarn weitergehen, wenn die betreffende Position mit allen im Pfad bereits gesetzten Positionen kompatibel ist, andernfalls ist diese Kante tabu. Landen wir aufgrund dieser Regel in einer Sackgasse (also in einem Knoten, wo keine der ausgehenden Kanten erlaubt ist), müssen wir zur nächsten erlaubten Abzweigung zurückgehen (Backtracking). Beim Zurückgehen müssen wir das &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Flag wieder auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zurücksetzen, weil der betreffende Knoten ja möglicherweise auf einem anderen erlaubten Weg erreichbar ist.&lt;br /&gt;
&lt;br /&gt;
Der folgende Graph zeigt einen solchen Fall: Wir haben zwei Damen auf die Felder A1 und B3 positioniert (grüne Pfeile). Die einzig ausgehende Kante von B3 führt zum Knoten C1, welcher aber mit der Position A1 inkompatibel ist, so dass diese Kante nicht verwendet werden darf (roter Pfeil). Das Backtracking muss jetzt zu Knoten A1 zurückgehen (dabei wird das &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Flag von B3 wieder auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; gesetzt), weil A1 mit der Kante nach B4 eine weitere Option hat, die geprüft werden muss (die allerdings hier auch nicht zum Ziel führt).&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph-failure.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
Nach einigen weiteren Sackgassen findet man schließlich den Pfad A2, B4, C1, D3, der im folgenden Graphen grün markiert ist und der obigen Lösung entspricht:&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph-success.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
=== Test, ob ein ungerichteter Graph azyklisch ist ===&lt;br /&gt;
&lt;br /&gt;
Ein zusammenhängender ungerichteter Graph ist azyklisch (also ein Baum) genau dann, wenn es nur einen möglichen Weg von jedem Knoten zu jedem anderen gibt. (Bei gerichteten Graphen sind die Verhältnisse komplizierter. Wir behandeln dies weiter unten.) Das kann man mittels Tiefensuche leicht feststellen: Die Kante, über die wir einen Knoten erstmals erreichen, ist eine &amp;lt;i&amp;gt;Baumkante&amp;lt;/i&amp;gt; des Tiefensuchbaums. Erreichen wir einen bereits besuchten Knoten nochmals über eine andere Kante, haben wir einen Zyklus gefunden. Dabei müssen wir allerdings beachten, dass in einem ungerichteten Graphen jede Baumkante zweimal gefunden wird, einmal in Richtung vom Vater zum Kind und einmal in umgekehrter Richtung. Im zweiten Fall endet die Kante zwar in einem bereits besuchten Knoten (dem Vater), aber es entsteht dadurch kein Zyklus. Den Vaterknoten müssen wir deshalb überspringen, wenn wir über die Nachbarn iterieren:&lt;br /&gt;
&lt;br /&gt;
 def undirected_cycle_test(graph):         # Annahme: der Graph ist zusammenhängend&lt;br /&gt;
                                           # (andernfalls führe den Algorithmus für jede Zusammenhangskomponente aus)&lt;br /&gt;
     visited = [False]*len(graph)          # Flags für bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, from_node):           # rekursive Hilfsfunktion: gibt True zurück, wenn Zyklus gefunden wurde&lt;br /&gt;
         if not visited[node]:             # wenn node noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True          # markiere node als besucht&lt;br /&gt;
             for neighbor in graph[node]:  # besuche die Nachbarn ...&lt;br /&gt;
                 if neighbor == from_node: # ... aber überspringe den Vaterknoten&lt;br /&gt;
                     continue&lt;br /&gt;
                 if visit(neighbor, node): # ... signalisiere, wenn rekursiv ein Zyklus gefunden wurde&lt;br /&gt;
                     return True&lt;br /&gt;
             return False                  # kein Zyklus gefunden&lt;br /&gt;
         else:&lt;br /&gt;
             return True                   # Knoten schon besucht =&amp;gt; Zyklus&lt;br /&gt;
     &lt;br /&gt;
     startnode = 0                         # starte bei beliebigem Knoten (hier: Knoten 0)&lt;br /&gt;
     return visit(startnode, startnode)    # gebe True zurück, wenn ein Zyklus gefunden wurde&lt;br /&gt;
&lt;br /&gt;
Wenn wir einen Zyklus finden, wird das weitere Traversieren das Graphen abgebrochen, denn ein Graph, der einmal zyklisch war, kann später nicht wieder azyklisch werden. Die notwendige Modifikation für unzusammenhängende Graphen erfolgt analog zum Algorithmus für die Detektion von Zusammenhangskomponenten, der im nächsten Abschnitt beschrieben wird.&lt;br /&gt;
&lt;br /&gt;
=== Finden von Zusammenhangskomponenten ===&lt;br /&gt;
&lt;br /&gt;
Das Auffinden und Markieren von Zusammenhangskomponenten (also maximalen zusammenhängenden Teilgraphen) ist eine grundlegende Aufgabe in ungerichteten, unzusammenhängenden Graphen (bei gerichteten Graphen sind die Verhältnisse wiederum komplizierter, siehe unten). Zwei Knoten u und v gehören zur selben Zusammenhangskomponente genau dann, wenn es einen Pfad von u nach v gibt (da der Graph ungerichtet ist, gibt es dann auch einen Pfad von v nach u). Man sagt auch, dass &amp;quot;v von u aus erreichbar&amp;quot; ist. Unzusammenhängende Graphen entstehen in der Praxis häufig, wenn die Kanten gewisse Relationen zwischen den Knoten kodieren: &lt;br /&gt;
* Wenn die Knoten Städte sind und die Kanten Straßen, sind diejenigen Städte in einer Zusammenhangskomponente, die per Auto von einander erreichbar sind. Unzusammenhängende Graphen entstehen hier beispielsweise, wenn eine Insel nicht durch eine Brücke erschlossen ist, wenn Grenzen gesperrt sind oder wenn ein Gebirge zu unwegsam ist, um Straßen zu bauen.&lt;br /&gt;
* Wenn Knoten Personen sind, und Kanten die Eltern-Kind-Relation beschreiben, so umfasst jede Zusammenhangskomponenten die Verwandten (auch wenn sie nur über viele &amp;quot;Ecken&amp;quot; verwandt sind).&lt;br /&gt;
* In der Bildverarbeitung entsprechen Knoten den Pixeln, und dieselben werden durch eine Kante verbunden, wenn sie zum selben Objekt gehören. Die Zusammenhangskomponenten entsprechen somit den Objekten im Bild (siehe Übungsaufgabe).&lt;br /&gt;
Die Zusammenhangskomponenten bilden eine Äquivalenzrelation. Folglich kann für jede Komponente ein Reprässentant bestimmt werden, der sogenannte &amp;quot;Anker&amp;quot;. Kennt jeder Knoten seinen Anker, ist das Problem der Zusammenhangskomponenten gelöst. &lt;br /&gt;
&lt;br /&gt;
==== Lösung mittels Tiefensuche ====&lt;br /&gt;
&lt;br /&gt;
Unser erster Ansatz ist, den Anker mit Hilfe der Tiefensuche zu finden. Anstelle der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; verwenden wir diesmal eine property map &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt;, die für jeden Knoten die Knotennummer des zugehörigen Ankers angibt, oder &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, wenn der Knoten noch nicht besucht wurde. Dabei verwenden wir wieder die Konvention, dass Anker auf sich selbst zeigen. Für viele Anwendungen ist es außerdem (oder stattdessen) zweckmäßig, die Zusammenhangskomponenten mit einer laufenden Nummer, einem sogenannten &amp;lt;i&amp;gt;Label&amp;lt;/i&amp;gt;, durchzuzählen. Dann kann man zusätzliche Informationen zu jeder Komponente (beispielsweise deren Größe) einfach in einem Array speichern, das über die Labels indexiert wird. Die folgende Version der Tiefensuche bestimmt sowohl die Anker als auch die Labels für jeden Knoten:&lt;br /&gt;
&lt;br /&gt;
 def connectedComponents(graph):&lt;br /&gt;
        anchors = [None] * len(graph)             # property map für Anker jedes Knotens&lt;br /&gt;
        labels  = [None] * len(graph)             # property map für Label jedes Knotens&lt;br /&gt;
        &lt;br /&gt;
        def visit(node, anchor):&lt;br /&gt;
                &amp;quot;&amp;quot;&amp;quot;anchor ist der Anker der aktuellen ZK&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
                if anchors[node] is None:         # wenn node noch nicht besucht wurde:&lt;br /&gt;
                    anchors[node] = anchor        # setze seinen Anker&lt;br /&gt;
                    labels[node] = labels[anchor] # und sein Label&lt;br /&gt;
                    for neighbor in graph[node]:  # und besuche die Nachbarn&lt;br /&gt;
                        visit(neighbor, anchor)&lt;br /&gt;
        &lt;br /&gt;
        current_label = 0                         # Zählung der ZK beginnt bei 0&lt;br /&gt;
        for node in xrange(len(graph)):&lt;br /&gt;
            if anchors[node] is None:             # Anker noch nicht bekannt =&amp;gt; neue ZK gefunden&lt;br /&gt;
                labels[node] = current_label      # Label des Ankers setzen&lt;br /&gt;
                visit(node, node)                 # Knoten der neuen ZK rekursiv suchen&lt;br /&gt;
                current_label += 1                # Label für die nächste ZK hochzählen&lt;br /&gt;
        return anchors, labels&lt;br /&gt;
Interessant ist hier die Schleife über alle Knoten des Graphen am Ende des Algorithmus, die bei den bisherigen Versionen der Tiefensuche nicht vorhanden war. Um ihre Funktionsweise zu verstehen, nehmen wir für den Moment an, dass der Graph zusammenhängend ist. Dann findet diese Schleife den ersten Knoten des Graphen und führt die Tiefensuche mit diesem Knoten als Startknoten aus. Sobald die Rekursion zurückkehrt, sind alle Knoten des Graphen besucht (weil der Graph ja zusammenhängend war), so dass die Schleife alle weiteren Knoten überspringt (die if-Anweisung liefert für keinen weiteren Knoten True). Bei unzusammenhängenden Graphen dagegen erreicht die Tiefensuche nur die Knoten derselben Komponente, die im weiteren Verlauf der Schleife übersprungen werden. Findet die if-Anweisung jetzt einen noch nicht besuchten Knoten, muss dieser folglich in einer neuen Komponente liegen. Wir verwenden diesen Knoten als Anker und bestimmen die übrigen Knoten dieser Komponente wiederum mit Tiefensuche.&lt;br /&gt;
&lt;br /&gt;
* Beispiel: ... &amp;lt;b&amp;gt; under construction &amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Man erkennt, dass die Tiefensuche nach dem &amp;lt;i&amp;gt;Anlagerungsprinzip&amp;lt;/i&amp;gt; vorgeht: Beginnend vom einem Startknoten (dem Anker) werden die Knoten der aktuellen Komponente nach und nach an den Tiefensuchbaum angehangen. Erst, wenn nichts mehr angelagert werden kann, geht der Algorithmus zur nächsten Komponente über.&lt;br /&gt;
&lt;br /&gt;
==== Lösung mittels Union-Find-Algorithmus ====&lt;br /&gt;
&lt;br /&gt;
Im Gegensatz zum Anlagerungsprinzip sucht der Union-Find-Algorithmus die Zusammenhangskomponenten mit dem &amp;lt;i&amp;gt;Verschmelzungsprinzip&amp;lt;/i&amp;gt;: Eingangs wird jeder Knoten als ein Teilgraph für sich betrachtet. Dann iteriert man über alle Kanten und verbindet deren Endknoten jeweils zu einem gemeinsamen Teilgraphen (falls die beiden Enden einer Kante bereits im selben Teilgraphen liegen, wird diese Kante ignoriert). Solange noch Kanten vorhanden sind, werden dadurch immer wieder Teilgraphen in größere Teilgraphen verschmolzen. Am Ende bleiben die maximalen zusammenhängenden Teilgraphen (also gerade die Zusammenhangskomponenten) übrig. Dieser Algorithmus kommt ohne Tiefensuche aus und ist daher in der Praxis oft schneller, allerdings auch etwas komplizierter zu implementieren.&lt;br /&gt;
&lt;br /&gt;
Der Schlüssel des Algorithmus ist eine Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt;, die zu jedem Knoten den aktuellen Anker sucht. Der Anker existiert immer, da jeder Knoten von Anfang an zu einem Teilgraphen gehört (anfangs ist jeder Teilgraph trivial und besteht nur aus dem Knoten selbst). Die Verschmelzung wird realisiert, indem der Anker des einen Teilgraphen seine Rolle verliert und stattdessen der Anker des anderen Teilgraphen eingesetzt wird. &lt;br /&gt;
&lt;br /&gt;
Zur Verwaltung der Anker verwenden wir wieder eine property map &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt; mit der Konvention, dass die Anker auf sich selbst verweisen. Es wäre jedoch zu teuer, wenn man bei jeder Verschmelzung alle Anker-Einträge der beteiligten Knoten aktualisieren müsste, da jeder Knoten im Laufe des Algorithmus mehrmals seinen Anker wechseln kann. Statt dessen definiert man Anker rekursiv: Verweist ein Knoten auf einen Anker, der mittlerweile diese Rolle verloren hat, folgt man dem Verweis von diesem Knoten (dem ehemaligen Anker) weiter, bis man einen tatsächlichen Anker gefunden hat - erkennbar daran, dass er auf sich selbst verweist. Diese Suchfunktion kann folgendermassen implementiert werden:&lt;br /&gt;
&lt;br /&gt;
  def findAnchor(anchors, node):&lt;br /&gt;
      while node != anchors[node]:   # wenn node kein Anker ist&lt;br /&gt;
          node = anchors[node]       # ... verfolge die Ankerkette weiter&lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Allerdings kann diese Kette im Laufe vieler Verschmelzungen sehr lang werden, so dass das Verfolgen der Kette teuer wird. Man vermeidet dies durch die sogenannte &amp;lt;i&amp;gt;Pfadkompression&amp;lt;/i&amp;gt;: Immer, wenn man den Anker gefunden hat, aktualisiert man den Eintrag am Anfang der Kette. Die Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt; wird dadurch nur wenig komplizierter:&lt;br /&gt;
&lt;br /&gt;
  def findAnchor(anchors, node):&lt;br /&gt;
      start = node                   # wir merken uns den Anfang der Kette&lt;br /&gt;
      while node != anchors[node]:   # wenn node kein Anker ist&lt;br /&gt;
          node = anchors[node]       # ... verfolge die Ankerkette weiter&lt;br /&gt;
      anchors[start] = node          # Pfadkompression: aktualisiere den Eintrag am Anfang der Kette&lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Man kann zeigen, dass die Ankersuche mit Pfadkompression zu einer fast konstanten amortisierten Laufzeit pro Aufruf führt.&lt;br /&gt;
&lt;br /&gt;
Um mit jeder Kante des (ungerichteten) Graphen nur maximal einmal eine Verschmelzung durchzuführen, betrachten wir jede Kante nur in der Richtung von der kleineren zur größeren Knotennummer, die umgekehrte Richtung wird ignoriert. Außerdem ist es zweckmäßig, bei jeder Verschmelzung denjenigen Anker mit der kleineren Knotennummer als neuen Anker zu übernehmen. Dann gilt für jede Zusammenhangskomponente, dass gerade der Knoten mit der kleinsten Knotennummer der Anker ist (genau wie bei der Lösung mittels Tiefensuche), was die weitere Analyse vereinfacht, z.B. die Zuordnung der Labels zu den Komponenten am Ende des Algorithmus. &lt;br /&gt;
&lt;br /&gt;
 def unionFindConnectedComponents(graph):&lt;br /&gt;
     anchors = range(len(graph))        # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):    # iteriere über alle Knoten&lt;br /&gt;
         for neighbor in graph[node]:   # ... und über deren ausgehende Kanten&lt;br /&gt;
             if neighbor &amp;lt; node:        # ignoriere Kanten, die in falscher Richtung verlaufen&lt;br /&gt;
                 continue&lt;br /&gt;
             # hier landen wir für jede Kante des Graphen genau einmal&lt;br /&gt;
             a1 = findAnchor(anchors, node)       # finde Anker ...&lt;br /&gt;
             a2 = findAnchor(anchors, neighbor)   # ... der beiden Endknoten&lt;br /&gt;
             if a1 &amp;lt; a2:                          # Verschmelze die beiden Teilgraphen&lt;br /&gt;
                 anchors[a2] = a1                 # (verwende den kleineren der beiden Anker als Anker des&lt;br /&gt;
             elif a2 &amp;lt; a1:                        #  entstehenden Teilgraphen. Falls node und neighbor &lt;br /&gt;
                 anchors[a1] = a2                 #  den gleichen Anker haben, waren sie bereits im gleichen&lt;br /&gt;
                                                  #  Teilgraphen, und es passiert hier nichts.)&lt;br /&gt;
     # Bestimme jetzt noch die Labels der Komponenten&lt;br /&gt;
     labels = [None]*len(graph)         # Initialisierung der property map für Labels&lt;br /&gt;
     current_label = 0                  # die Zählung beginnt bei 0&lt;br /&gt;
     for node in xrange(len(graph)):&lt;br /&gt;
         a = findAnchor(anchors, node)  # wegen der Pfadkompression zeigt jeder Knoten jetzt direkt auf seinen Anker&lt;br /&gt;
         if a == node:                  # node ist ein Anker&lt;br /&gt;
             labels[a] = current_label  # =&amp;gt; beginne eine neue Komponente&lt;br /&gt;
             current_label += 1         # und zähle Label für die nächste ZK hoch&lt;br /&gt;
         else:&lt;br /&gt;
             labels[node] = labels[a]   # node ist kein Anker =&amp;gt; setzte das Label des Ankers&lt;br /&gt;
                                        # (wir wissen, dass labels[a] bereits gesetzt ist, weil &lt;br /&gt;
                                        #  der Anker immer der Knoten mit der kleinsten Nummer ist)&lt;br /&gt;
     return anchors, labels&lt;br /&gt;
 &lt;br /&gt;
* Beispiel: ... &amp;lt;b&amp;gt;under construction&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Kürzeste Wege (Pfade) ==&lt;br /&gt;
&lt;br /&gt;
Eine weitere grundlegende Aufgabe in Graphen ist die Bestimmung eines kürzesten Weges zwischen zwei gegebenen Knoten. Dies hat offensichtliche Anwendungen bei Routenplanern und Navigationssystemen und ist darüber hinaus wichtiger Bestandteil anderer Algorithmen, z.B. bei der Berechnung eines maximalen Flusses mit der [http://en.wikipedia.org/wiki/Edmonds%E2%80%93Karp_algorithm Methode von Edmonds und Karp].&lt;br /&gt;
&lt;br /&gt;
=== Kürzeste Wege in ungewichteten Graphen mittels Breitensuche ===&lt;br /&gt;
&lt;br /&gt;
Im Fall eines ungewichteten Graphen ist die Länge eines Weges einfach durch die Anzahl der durchlaufenen Kanten definiert. Daraus folgt, dass kürzeste Pfade mit einer leicht angepassten Version der Breitensuche gefunden werden können: Aufgrund des first in-first out-Verhaltens der Queue betrachtet die Breitensuche alle (erreichbaren) Knoten in der Reihenfolge ihres Abstandes vom Startknoten. Wenn wir den Zielknoten zum ersten Mal erreichen, und der gerade gefundene Weg vom Start zum Ziel hat die Länge L, muss dies der kürzeste Weg sein: Alle möglichen Wege der Länge L' &amp;amp;lt; L hat die Breitensuche ja bereits betrachtet, ohne dass dabei der Zielknoten erreicht wurde. Daraus folgt übrigens eine allgemeine Eigenschaft aller Algorithmen für kürzeste Wege: Wenn der kürzeste Weg vom Start zum Ziel die Länge L hat, finden diese Algorithmen als Nebenprodukt auch die kürzesten Wege zu allen Knoten, für die L' &amp;amp;lt; L gilt. &lt;br /&gt;
&lt;br /&gt;
Um den Algorithmus zu implementieren, passen wir die Breitensuche so an, dass anstelle der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; eine property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; verwendet wird, die für jeden besuchten Knoten den Vaterknoten im Breitensuchbaum speichert. Durch Rückverfolgen der &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette können wir den Pfad vom Ziel zum Start rekonstruieren, und durch Umdrehen der Reihenfolge erhalten wir den gesuchten Pfad vom Start zum Ziel. Sobald der Zielknoten erreicht wurde, können wir die Breitensuche abbrechen (&amp;lt;tt&amp;gt;break&amp;lt;/tt&amp;gt;-Befehl in der ersten &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Schleife). Falls der gegebene Graph unzusammenhängend ist, kann es passieren, dass gar kein Weg gefunden wird, weil Start und Ziel in verschiedenen Zusammenhangskomponenten liegen. Dies erkennen wir daran, dass die Breitensuche beendet wurde, ohne den Zielknoten zu besuchen. Dann gibt die Funktion statt eines Pfades dern Wert &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zurück:&lt;br /&gt;
&lt;br /&gt;
  from collections import deque&lt;br /&gt;
  &lt;br /&gt;
  def shortestPath(graph, startnode, destination):&lt;br /&gt;
      parents = [None]*len(graph)      # Registriere für jeden Knoten den Vaterknoten im Breitensuchbaum&lt;br /&gt;
      parents[startnode] = startnode   # startnode ist die Wurzel des Baums =&amp;gt; verweist auf sich selbst&lt;br /&gt;
      &lt;br /&gt;
      q = deque()                      # Queue für die zu besuchenden Knoten&lt;br /&gt;
      q.append(startnode)              # Startknoten in die Queue einfügen&lt;br /&gt;
      &lt;br /&gt;
      while len(q) &amp;gt; 0:                # Solange es noch unbesuchte Knoten gibt&lt;br /&gt;
          node = q.popleft()           # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
          if node == destination:      # Zielknoten erreicht&lt;br /&gt;
              break                    #   =&amp;gt; Suche beenden&lt;br /&gt;
          for neighbor in graph[node]: # Besuche die Nachbarn von node&lt;br /&gt;
              if parents[neighbor] is None:  # aber nur, wenn sie noch nicht besucht wurden&lt;br /&gt;
                  parents[neighbor] = node   # setze node als Vaterknoten&lt;br /&gt;
                  q.append(neighbor)         # und füge neighbor in die Queue ein&lt;br /&gt;
      &lt;br /&gt;
      if parents[destination] is None: # Breitensuche wurde beendet ohne den Zielknoten zu besuchen&lt;br /&gt;
          return None                  # =&amp;gt; kein Pfad gefunden (unzusammenhängender Graph)&lt;br /&gt;
      &lt;br /&gt;
      # Pfad durch die parents-Kette zurückverfolgen und speichern&lt;br /&gt;
      path = [destination]&lt;br /&gt;
      while path[-1] != startnode:&lt;br /&gt;
          path.append(parents[path[-1]])&lt;br /&gt;
      path.reverse()     # Reihenfolge umdrehen (Ziel =&amp;gt; Start wird zu Start =&amp;gt; Ziel)&lt;br /&gt;
      return path        # gefundenen Pfad zurückgeben&lt;br /&gt;
&lt;br /&gt;
=== Gewichtete Graphen ===&lt;br /&gt;
&lt;br /&gt;
Das Problem der Suche nach kürzesten Wegen wird wesentlich interessanter und realistischer, wenn wir zu gewichteten Graphen übergehen:&lt;br /&gt;
&lt;br /&gt;
; Definition - kantengewichteter Graph&lt;br /&gt;
: Jeder Kante (s,t) des Graphen ist eine reelle oder natürliche Zahl w&amp;lt;sub&amp;gt;st&amp;lt;/sub&amp;gt; zugeordnet, die üblicherweise als ''Kantengewicht'' bezeichnet wird.&lt;br /&gt;
&lt;br /&gt;
; Definition - knotengewichteter Graph&lt;br /&gt;
: Jedem Knoten v des Graphen ist eine reelle oder natürliche Zahl w&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; zugeordnet, die üblicherweise als ''Knotengewicht'' bezeichnet wird.&lt;br /&gt;
&lt;br /&gt;
Je nach Anwendung benötigt man Knoten- oder Kantengewichte oder auch beides zugleich. Wir beschränken uns in der Vorlesung auf kantengewichtete Graphen. Beispiele für die Informationen, die man durch Kantengewichte ausdrücken kann, sind&lt;br /&gt;
* wenn die Knoten Orte sind: Abstand von Anfangs- und Endknoten jeder Kante (z.B. Luftline oder Straßenentfernung), Fahrzeit zwischen den Orten&lt;br /&gt;
* wenn der Knoten ein Rohrnetzwerk beschreibt: Durchflusskapazität der einzelnen Rohre (für max-Flussprobleme), analog bei elektrischen Netzwerken: elektrischer Widerstand&lt;br /&gt;
* wenn die Knoten Währungen repräsentieren, können deren Wechselkurse durch Kantengewichte angegeben werden.&lt;br /&gt;
Bei einigen Beispielen ergeben sich unterschiedliche Kantengewichte, wenn eine Kante von s nach t anstatt von t nach s durchlaufen wird. Beispielsweise können sich die Fahrzeiten erheblich unterscheiden, wenn es in einer Richtung bergauf, in der anderen bergab geht, obwohl die Entfernung in beiden Fällen gleich ist. Hier ergibt sich natürlicherweise ein gerichteter Graph. In anderen Beispielen (z.B. bei Luftlinienentfernungen, in guter Näherung auch bei Straßenentfernungen) sind die Gewichte von der Richtung unabhängig, so dass wir ungerichtete Graphen verwenden können.&lt;br /&gt;
&lt;br /&gt;
Die Repräsentation der Kantengewichte im Programm richtet sich nach der Repräsentation des Graphen selbst. Am einfachsten ist wiederum die Adjazenzmatrix, die aber nur für dichte Graphen (&amp;lt;math&amp;gt;E = O(V^2)&amp;lt;/math&amp;gt;, mit E als Anzahl der Kanten und V als Anzahl der Knoten) effizient ist. Bei gewichteten Graphen gibt das Matrixelement a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; das Gewicht der Kante i &amp;amp;rArr; j (wobei a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; = 0 gesetzt wird, wenn diese Kante nicht existiert). Wie zuvor gilt für ungerichtete Graphen a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; = a&amp;lt;sub&amp;gt;ji&amp;lt;/sub&amp;gt; (symmetrische Matrix), während dies für gerichtete Graphen nicht gelten muss.&lt;br /&gt;
&lt;br /&gt;
Bei Graphen in Adjazenzlistendarstellung hat es sich bewährt, die Gewichte in einer &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; zu speichern. Weiter oben haben wir bereits property maps für Knoteneigenschaften (z.B. &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt;) gesehen. Property maps für Kanten funktionieren ganz analog, allerdings muss man jetzt Paare von Knoten (nämlich Anfangs- und Endknoten der Kante) als Schlüssel verwenden und die Daten entsprechend in einem assoziativen Array ablegen:&lt;br /&gt;
  w = weights[(i,j)]   # Zugriff auf das Gewicht der Kante i &amp;amp;rArr; j&lt;br /&gt;
Alternativ könnte man auch die Graph-Datenstruktur selbst erweitern, aber dies ist weniger zu empfehlen, weil jeder Algorithmus andere Erwiterungen benötigt und damit die Datenstruktur sehr unübersichtlich würde.&lt;br /&gt;
&lt;br /&gt;
Der kürzeste Weg ist nun definiert als der Weg, bei dem die Summe der Kantengewichte minimal ist:&lt;br /&gt;
;Definition - Problem des kürzesten Weges&lt;br /&gt;
: Sei P die Menge aller Wege von u nach v, und &amp;lt;math&amp;gt;p \in P&amp;lt;/math&amp;gt; einer dieser Wege. Wenn der Grpah einfach ist (es also keine Mehrfachkanten zwischen denselben Knoten und keine Schleifen gibt), ist der Weg p durch die Folge der besuchten Knoten eindeutig bestimmt:&lt;br /&gt;
: &amp;lt;math&amp;gt;p : \ \ u = x_0 \rightarrow x_1 \rightarrow x_2 \rightarrow ... \rightarrow v = x_{n_p}&amp;lt;/math&amp;gt;&lt;br /&gt;
:wo &amp;lt;math&amp;gt;n_p&amp;lt;/math&amp;gt; die Anzahl der Kanten im Weg p ist. Seine Kosten W&amp;lt;sub&amp;gt;p&amp;lt;/sub&amp;gt; ergeben sich als Summer der Gewichte der einzelnen Kanten&lt;br /&gt;
: &amp;lt;math&amp;gt;W_p = \sum_{k=1}^{n_p} w_{x_{k-1}x_k}&amp;lt;/math&amp;gt;&lt;br /&gt;
: und ein kürzester Weg &amp;lt;math&amp;gt;p^* \in P&amp;lt;/math&amp;gt; ist ein Weg mit minimalen Kosten&lt;br /&gt;
: &amp;lt;math&amp;gt;p^* = \textrm{argmin}_{p\in P}\ \ W_p&amp;lt;/math&amp;gt;&lt;br /&gt;
: Das Problem des kürzesten Weges besteht darin, einen optimalen Weg &amp;lt;i&amp;gt;p*&amp;lt;/i&amp;gt; zwischen gegebenen Knoten u und v zu finden.&lt;br /&gt;
Die Lösung dieses Problems hängt davon ab, ob alle Kantengewichte positiv sind, oder ob es auch negative Kantengewichte gibt. In letzeren Fall ist es möglich, durch eine Verlängerung des Weges die Kosten zu redizieren, während sich im ersteren Fall die Kosten immer erhöhen, wenn man den Weg verlängert. &lt;br /&gt;
&lt;br /&gt;
Negative Gewichte treten z.B. bei den Währungsgraphen auf. Auf den ersten Blick entsprechen diese Graphen nicht den Anforderungen an das Problem des kürzesten Weges, weil Wechselkurse miteinander (und mit Geldbeträgen) multipliziert anstatt addiert werden. Man beseitigt diese Schwierigkeit aber leicht, indem man die &amp;lt;i&amp;gt;Logarithmen&amp;lt;/i&amp;gt; der Wechselkurse als Kantengewichte verwendet, wodurch sich die Multiplikation in eine Addition der Logarithmen verwandelt. Wechselkurse &amp;amp;lt; 1 führen nun zu negativen Gewichten. &lt;br /&gt;
&lt;br /&gt;
Interessant werden negative Gewichte vor allem in Graphen mit Zyklen. Dann kann es nämlich passieren, dass die Gesamtkosten eines Zyklus ebenfalls negativ sind. Jeder Weg, der den Zyklus enthält, hat dann Kosten von &amp;lt;math&amp;gt;-\infty&amp;lt;/math&amp;gt;, weil man den Zyklus beliebig oft durchlaufen und dadurch die Gesamtkosten immer weiter verkleinern kann:&lt;br /&gt;
&lt;br /&gt;
     /\		1. Durchlauf: Kosten -1&lt;br /&gt;
  1 /  \ -4	2. Durchlauf: Kosten -2&lt;br /&gt;
   /____\	etc.&lt;br /&gt;
      2&lt;br /&gt;
&lt;br /&gt;
Um hier nicht in einer Endlosschleife zu landen, benötigt man spezielle Algorithmen, die mit dieser Situation umgehen können. Der [http://de.wikipedia.org/wiki/Bellman-Ford-Algorithmus Algorithmus von Bellmann und Ford] beispielsweise bricht die Suche nach dem kürzesten Weg ab, sobald er einen negativen Zyklus entdeckt, aber andernfalls kann er negative Gewichte problemlos verarbeiten. &lt;br /&gt;
&lt;br /&gt;
Die Detektion negativer Zyklen hat wiederum eine interessante Anwendung bei Währungsgraphen: Ein Zyklus bedeutet hier, dass man Geld über mehrere Stufen von einer Währung in die nächste und am Schluß wieder in die Originalwährung umtauscht, und ein negativer Zyklus führt dazu, dass man am Ende &amp;lt;i&amp;gt;mehr&amp;lt;/i&amp;gt; Geld besitzt als am Anfang (damit negative Zyklen wirklich einen Gewinn bedeuten und keinen Verlust, müssen die Wechselkurse vor der Logarithmierung in [http://de.wikipedia.org/wiki/Wechselkurs#Nominaler_Wechselkurs Preisnotierung] angegeben sein). Bei Privatpersonen ist dies ausgeschlossen, weil die Umtauschgebühren den möglichen Gewinn mehr als aufzehren. Banken mit direktem weltweitem Börsenzugang hingegen unternehmen große Anstrengungen, um solche negativen Zyklen möglichst schnell (nämlich vor der Konkurrenz) zu entdecken und auszunutzen. Diese Geschäftsmethode bezeichnet man als [http://de.wikipedia.org/wiki/Arbitrage Arbitrage] und die Existenz eines negativen Zyklus als Arbitragegelegenheit. Durch die Kursschwankungen (und durch die ausgleichende Wirkung der Arbitragegeschäfte selbst) existieren die Arbitragegelegenheiten nur für kurze Zeit, und ihre Detektion erfordert leistungsfähige Echtzeitalgorithmen.&lt;br /&gt;
&lt;br /&gt;
In dieser Vorlesung beschränken wir uns hingegen auf Graphen mit ausschließlich positiven Gewichten. In diesem Fall ist der Algorithmus von Dijkstra die Methode der Wahl, weil er wesentlich schneller arbeitet als der Bellmann-Ford-Algorithmus.&lt;br /&gt;
&lt;br /&gt;
=== Algorithmus von Dijkstra ===&lt;br /&gt;
&lt;br /&gt;
==== Edsger Wybe Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
geb. 11. Mai 1930 in Rotterdam&lt;br /&gt;
&lt;br /&gt;
ges. 06. August 2002&lt;br /&gt;
&lt;br /&gt;
Dijkstra war ein niederländischer Informatiker und Wegbereiter der strukturierten Programmierung. 1972 erhielt er für seine Leistung in der Technik und Kunst der Programmiersprachen den Turing Award, der jährlich von der Association for Computing Machinery (ACM) an Personen verliehen wird, die sich besonders um die Entwicklung der Informatik verdient gemacht haben. Zu seinen Beiträgen zur Informatik gehören unter anderem der Dijkstra-Algorithmus zur Berechnung des kürzesten Weges in einem Graphen sowie eine Abhandlung über den go-to-Befehl und warum er nicht benutzt werden sollte. Der go-to-Befehl war in den 60er und 70er Jahren weit verbreitet, führte aber zu Spaghetti-Code. In seinem berühmten Paper &amp;quot;A Case against the GO TO Statement&amp;quot;[http://www.cs.utexas.edu/users/EWD/ewd02xx/EWD215.PDF], das als Brief mit dem Titel &amp;quot;Go-to statement considered harmful&amp;quot; veröffentlicht wurde, argumentiert Dijkstra, dass es umso schwieriger ist, dem Quellcode eines Programmes zu folgen, je mehr go-to-Befehle darin enthalten sind und zeigt, dass man auch ohne diesen Befehl gute Programme schreiben kann.&lt;br /&gt;
&lt;br /&gt;
==== Algorithmus ====&lt;br /&gt;
&lt;br /&gt;
Der Dijkstra-Algorithmus für kürzeste Wege ist dem oben vorgestellten Algorithmus &amp;lt;tt&amp;gt;shortestPath()&amp;lt;/tt&amp;gt; auf der Basis von Breitensuche sehr ähnlich. Insbesondere gilt auch hier, dass neben dem kürzesten Weg vom Start zum Ziel auch alle kürzesten Wege gefunden werden, deren Endknoten dem Start näher sind als der Zielknoten. Aufgrund der Kantengewichte gibt es aber einen wichtigen Unterschied: Der erste gefundene Weg zu einem Knoten ist nicht mehr notwendigerweise der kürzeste. Wir bestimmen deshalb für jeden Knoten mehrere Kandidatenwege und verwenden eine Prioritätswarteschlange (statt einer einfachen First in - First out - Queue), um diese Wege nach ihrer Länge zu sortieren. Die Kandidatenwege für einen gegebenen Knoten werden unterschieden, indem wir auch den Vorgängerknoten im jeweiligen Weg speichern. Wenn ein Knoten &amp;lt;i&amp;gt;erstmals&amp;lt;/i&amp;gt; an die Spitze der Prioritätswarteschlange gelangt, haben wir den kürzesten Weg zu diesem Knoten gefunden (das wird weiter unten formal bewiesen), und der Vorgänger des Knotens in diesem Weg wird zu seinem Vaterknoten. Erscheint derselbe Knoten später nochmals an der Spitze der Prioritätswarteschlange, handelt es sich um einen Kandidatenweg, der sich nicht als kürzester erwiesen hat und deshalb ignoriert werden kann. Wir erkennen dies leicht daran, dass der Vaterknoten in der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; bereits gesetzt ist. &lt;br /&gt;
&lt;br /&gt;
Eine geeignete Datenstruktur für die Prioritätswarteschlange wird durch das Python-Modul [http://docs.python.org/library/heapq.html heapq] realisiert. Es verwendet ein normales Pythonarray als unterliegende Repräsentation für einen Heap und stellt effiziente &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt;-Funktionen zur Verfügung. Dies entspricht genau unserer Vorgehensweise im Kapitel [[Prioritätswarteschlangen]]. Als Datenelement erwartet die Funktion &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; ein Tupel, dessen erstes Element die Priorität sein muss. Die übrigen Elemente des Tupels (und damit auch deren Anzahl) können je nach Anwendung frei festgelegt werden. Wir legen fest, dass das zweite Element den Endknoten des betrachteten Weges und das dritte den Vorgängerknoten speichert. &lt;br /&gt;
&lt;br /&gt;
Die Kantengewichte werden dem Algorithmus in der property map &amp;lt;tt&amp;gt;weights&amp;lt;/tt&amp;gt; übergeben:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;code python&amp;gt;&lt;br /&gt;
    import heapq	                  # heapq implementiert die Funktionen für Heaps&lt;br /&gt;
    &lt;br /&gt;
    def dijkstra(graph, weights, startnode, destination):&lt;br /&gt;
        parents = [None]*len(graph)       # registriere für jeden Knoten den Vaterknoten im Pfadbaum&lt;br /&gt;
      &lt;br /&gt;
        q = []                            # Array q wird als Heap verwendet&lt;br /&gt;
        &amp;lt;font color=red&amp;gt;heapq.heappush(q, (0.0, startnode, startnode))&amp;lt;/font&amp;gt;  # Startknoten in Heap einfügen&lt;br /&gt;
      &lt;br /&gt;
        while len(q) &amp;gt; 0:                 # solange es noch Knoten im Heap gibt:&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;length, node, predecessor = heapq.heappop(q)&amp;lt;/font&amp;gt;   # Knoten aus dem Heap nehmen&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;if parents[node] is not None:&amp;lt;/font&amp;gt; # parent ist schon gesetzt =&amp;gt; es gab einen anderen, kürzeren Weg&lt;br /&gt;
                &amp;lt;font color=red&amp;gt;continue&amp;lt;/font&amp;gt;                  #   =&amp;gt; wir können diesen Weg ignorieren&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;parents[node] = predecessor&amp;lt;/font&amp;gt;   # parent setzen&lt;br /&gt;
            if node == destination:       # Zielknoten erreicht&lt;br /&gt;
                break                     #   =&amp;gt; Suche beenden&lt;br /&gt;
            for neighbor in graph[node]:  # die Nachbarn von node besuchen,&lt;br /&gt;
                if parents[neighbor] is None:   # aber nur, wenn ihr kürzester Weg noch nicht bekannt ist&lt;br /&gt;
                    &amp;lt;font color=red&amp;gt;newLength = length + weights[(node,neighbor)]&amp;lt;/font&amp;gt;   # berechne Pfadlänge zu neighbor              &lt;br /&gt;
                    &amp;lt;font color=red&amp;gt;heapq.heappush(q, (newLength, neighbor, node))&amp;lt;/font&amp;gt;  # und füge neighbor in den Heap ein&lt;br /&gt;
      &lt;br /&gt;
        if parents[destination] is None:  # Suche wurde beendet ohne den Zielknoten zu besuchen&lt;br /&gt;
            return None, None             # =&amp;gt; kein Pfad gefunden (unzusammenhängender Graph)&lt;br /&gt;
      &lt;br /&gt;
        # Pfad durch die parents-Kette zurückverfolgen und speichern&lt;br /&gt;
        path = [destination]&lt;br /&gt;
        while path[-1] != startnode:&lt;br /&gt;
            path.append(parents[path[-1]])&lt;br /&gt;
        path.reverse()                    # Reihenfolge umdrehen (Ziel =&amp;gt; Start wird zu Start =&amp;gt; Ziel)&lt;br /&gt;
        return path, length               # gefundenen Pfad und dessen Länge zurückgeben&lt;br /&gt;
  &amp;lt;/code&amp;gt;&lt;br /&gt;
Die wesentlichen Unterschiede zur Breitensuche sind im Code rot markiert: Anstelle der Queue verwenden wir jetzt einen Heap, und der Startknoten wird mit Pfadlänge 0 als erstes eingefügt. In der Schleife &amp;lt;tt&amp;gt;while len(q) &amp;gt; 0:&amp;lt;/tt&amp;gt; wird jeweils der Knoten &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; mit der aktuell kürzesten Pfadlänge aus dem Heap entfernt. Die Pfadlänge vom Start zu diesem Knoten wird in der Variable &amp;lt;tt&amp;gt;length&amp;lt;/tt&amp;gt; gespeichert, sein Vorgänger in der Variable &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;. Wenn der aktuelle Weg nicht der kürzeste ist (&amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; war bereits gesetzt), wird dieser Weg ignoriert. Andernfalls werden die property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; aktualisiert und die Nachbarn von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; besucht. Beim Scannen der Nachbarn berechnen wir zunächst die Länge &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; das Weges &amp;lt;tt&amp;gt;startnode =&amp;amp;gt; node =&amp;amp;gt; neighbor&amp;lt;/tt&amp;gt; als Summe von &amp;lt;tt&amp;gt;length&amp;lt;/tt&amp;gt; und dem Gewicht der Kante &amp;lt;tt&amp;gt;(node, neighbode)&amp;lt;/tt&amp;gt;. Diese Länge wird beim Einfügen des Nachbarknotens in den Heap zur Priorität des aktuellen Weges.&lt;br /&gt;
&lt;br /&gt;
Die wichtigsten Prinzipien des Dijkstra-Algorithmus noch einmal im Überblick:&lt;br /&gt;
* Der Dijkstra-Algorithmus ist Breitensuche mit Prioritätswarteschlange (Heap) statt einer einfache Warteschlange (Queue).&lt;br /&gt;
* Die Prioritätswarteschlange speichert alle Wege, die bereits gefunden worden sind und ordnet sie aufsteigend nach ihrer Länge. &lt;br /&gt;
* Das Sortieren (und damit der ganze Algorithmus) funktioniert nur mit positiven Kantengewichten korrekt.&lt;br /&gt;
* Da ein Knoten auf mehreren Wegen erreichbar sein kann, kann er auch mehrmals im Heap sein. &lt;br /&gt;
* Wenn ein Knoten &amp;lt;i&amp;gt;erstmals&amp;lt;/i&amp;gt; aus der Prioritätswarteschlange entnommen wird, ist der gefundene Weg der kürzeste zu diesem Knoten. Andernfalls wird der Weg ignoriert.&lt;br /&gt;
* Wenn der Knoten &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; aus dem Heap entnommen wird, ist der kürzeste Weg von Start nach Ziel gefunden, und die Suche kann beendet werden.&lt;br /&gt;
In unserer Implementation können, wie gesagt, mehrere Wege zum selben Knoten gleichzeitig in der Prioritätswarteschlange sein. Im Prinzip wäre es auch möglich, immer nur den besten zur Zeit bekannten Weg zu jedem Enknoten in der Prioritätswarteschlange zu halten - sobald ein besserer Kandidat gefunden wird, ersetzt er den bisherigen Kandidaten, anstatt zusätzlich eingefügt zu werden. Dies erfordert aber eine wesentlich kompliziertere Prioritätswarteschlange, die eine effiziente &amp;lt;tt&amp;gt;updatePriority&amp;lt;/tt&amp;gt;-Funktion anbietet, ohne dass dadurch eine signifikante Beschleunigung erreicht wird. Deshalb verfolgen wir diesen Ansatz nicht.&lt;br /&gt;
&lt;br /&gt;
==== Beispiel ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;under construction&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Image:Bsp.jpg]]&lt;br /&gt;
&lt;br /&gt;
==== Komplexität von Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
Zur Analyse der Komplexität nehmen wir an, dass der Graph V Knoten und E Kanten hat. Die Initialisierung der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; am Anfang der Funktion hat offensichtlich Komplexität O(V), weil Speicher für V Knoten allokiert wird. Der Code am Ende der Funktion, der aus der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; den Pfad extrahiert, hat ebenfalls die Komplexität O(V), weil der Pfad im ungünstigen Fall sämtliche Knoten des Graphen umfasst. Beides wird durch die Komplexität der Hauptschleife dominiert, zu deren Analyse wir den folgenden Codeausschnitt genauer anschauen wollen:&lt;br /&gt;
&lt;br /&gt;
      while len(q) &amp;gt; 0:&lt;br /&gt;
           ... # 1&lt;br /&gt;
           if parents[node] is not None: &lt;br /&gt;
               continue                  &lt;br /&gt;
           parents[node] = predecessor&lt;br /&gt;
           ... # 2&lt;br /&gt;
Wir erkennen, dass der Codeabschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; für jeden Knoten höchstens einmal erreicht werden kann: Da &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; beim ersten Durchlauf gesetzt wird, kann die &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage beim gleichen Knoten nie wieder &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt; liefern, und das nachfolgende &amp;lt;tt&amp;gt;continue&amp;lt;/tt&amp;gt; bewirkt, dass der Abschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; dann übersprungen wird. Man sagt auch, dass jeder Knoten &amp;lt;i&amp;gt;höchstens einmal expandiert&amp;lt;/i&amp;gt; wird, auch wenn er mehrmals im Heap war. &lt;br /&gt;
&lt;br /&gt;
Der Codeabschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; selbst enthält eine Schleife über alle ausgehenden Kanten des Knotens &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt;. Im ungünstigsten Fall iterieren wir bei &amp;lt;i&amp;gt;allen&amp;lt;/i&amp;gt; Knoten über &amp;lt;i&amp;gt;alle&amp;lt;/i&amp;gt; ausgehenden Kanten, aber das sind gerade alle Kanten des Graphen je einmal in den beiden möglichen Richtungen. Die Funktion &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; wird sogar höchstens E Mal aufgerufen, weil eine Kante nur in den Heap eingefügt wird, wenn der kürzeste Weg der jeweiligen Endknotens noch nicht bekannt ist (siehe die &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage in der &amp;lt;tt&amp;gt;for&amp;lt;/tt&amp;gt;-Schleife), und das ist nur ein einer Richtung möglich. Dies hat zwei Konsequenzen:&lt;br /&gt;
* Die Schleife &amp;lt;tt&amp;gt;while len(q) &amp;gt; 0:&amp;lt;/tt&amp;gt; wird nur so oft ausgeführt, wie Elemente im Heap sind, also höchstens E Mal. Das gleiche gilt für den Codeabschnitt &amp;lt;tt&amp;gt;# 1&amp;lt;/tt&amp;gt;, der das &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; enthält.&lt;br /&gt;
* Die Operationen &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; haben logarithmische Komplexität in der Größe des Heaps, sind also in &amp;lt;math&amp;gt;O(\log\,E)&amp;lt;/math&amp;gt;. In einfachen Graphen gilt aber &amp;lt;math&amp;gt;E = O(V^2)&amp;lt;/math&amp;gt;, so dass sich die Komplexität der Heapoperationen vereinfacht zu &amp;lt;math&amp;gt;O(\log\,E)=O(\log\,V^2)=O(2\log\,V)=O(\log\,V)&amp;lt;/math&amp;gt;.&lt;br /&gt;
Zusammenfassend gilt: &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; werden maximal E Mal aufgerufen und haben eine Komplexität in &amp;lt;math&amp;gt;O(\log\,V)&amp;lt;/math&amp;gt;. Folglich hat der Algorithmus von Dijkstra die Komplexität:&lt;br /&gt;
:&amp;lt;math&amp;gt;O(E\,\log\,V)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Vergleich mit Breitensuche und Tiefensuche ====&lt;br /&gt;
&lt;br /&gt;
Der Dijkstra-Algorithmus ist eng mit der Breiten- und Tiefensuche verwandt - man kann diese Algorithmen aus dem Dijkstra-Algorithmus gewinnen, indem man einfach die Regel zur Festlegung der Prioritäten ändert. Anstelle der Länge des Pfades verwenden wir als Priorität den Wert eine Zählvariable &amp;lt;tt&amp;gt;count&amp;lt;/tt&amp;gt;, die nach jeder Einfügung in den Heap (also nach jedem Aufruf von &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt;) aktualisiert wird. Zählen wir die Variable hoch, haben die zuerst eingefügten Kanten die höchste Priorität, der Heap verhält sich also wie eine Queue (First in-First out), und wir erhalten eine Breitensuche. Zählen wir die Variable hingegen (von E beginnend) herunter, haben die zuletzt eingefügten Kanten höchste Priorität. Der Heap verhält sich dann wie ein Stack (Last in-First out), und wir bekommen Tiefensuche. Statt eines Heaps plus Zählvariable kann man jetzt natürlich direkt eine Queue bzw. einen Stack verwenden. Dadurch fällt der Aufwand &amp;lt;math&amp;gt;O(\log\,V)&amp;lt;/math&amp;gt; für die Heapoperationen weg und wird durch die effizienten O(1)-Operationen von Queue bzw. Stack ersetzt. Damit erhalten wir für Breiten- und Tiefensuche die schon bekannte Komplexität O(E).&lt;br /&gt;
&lt;br /&gt;
==== Korrektheit von Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
Wir beweisen mittels vollständiger Induktion die Schleifen-Invariante: Falls &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; gesetzt (also ungleich &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;) ist, dann liefert das Zurückverfolgen des Weges von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; nach &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; den kürzesten Weg. &lt;br /&gt;
;Induktionsanfang: &amp;lt;tt&amp;gt;parents[startnode]&amp;lt;/tt&amp;gt; ist als einziges gesetzt. Zurückverfolgen liefert den trivialen Weg &amp;lt;tt&amp;gt;[startnode]&amp;lt;/tt&amp;gt;, der mit Länge 0 offensichtlich der kürzeste Pfad ist &amp;amp;rarr; die Bedingung ist erfüllt.&lt;br /&gt;
;Induktionsschritt: Wir zeigen mit einem indirektem Beweis, dass wir immer einen kürzesten Weg bekommen, wenn &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; gesetzt wird.&lt;br /&gt;
:Sei &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt; = &amp;lt;tt&amp;gt;{v | parents[v] is not None}&amp;lt;/tt&amp;gt; die Menge aller Knoten, von denen wir den kürzesten Weg schon kennen (Induktionsvoraussetzung), und &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; der Knoten, der sich gerade an der Spitze des Heaps befindet. Dann ist &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; der Vorgänger von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; im aktuellen Weg, und es muss &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt; gelten, weil die Nachbarn von &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; (und damit auch der aktuelle &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt;) erst in den Heap eingefügt werden, wenn der kürzeste Weg für &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; gefunden wurde. Man beachte auch, dass alle Knoten, die noch nicht in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt; enthalten sind, weiter vom Start entfernt sind als alle Knoten in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt;, weil alle neu in den Heap eingefügten Wege länger sind als der kürzeste Weg des jeweiligen Vorgängers. &lt;br /&gt;
:Der indirekte Beweis nimmt jetzt an, dass der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; nicht der kürzeste Weg ist. Dann muss es einen anderen, kürzeren Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; geben. Für den Vorgänger &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; in diesem Weg unterscheiden wir zwei Fälle:&lt;br /&gt;
:* &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt;: In diesem Fall ist die Länge des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; bereits bekannt, und dieser Weg ist in der Prioritätswarteschlange enthalten. Dann kann er aber nicht der kürzeste sein, denn an der Spitze der Warteschlange war nach Voraussetzung der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt;.&lt;br /&gt;
:* &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\notin S&amp;lt;/math&amp;gt;: Die Kosten des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; berechnen sich als &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) + weight[(x, node)]&amp;lt;/tt&amp;gt;, und die Kosten des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; sind analog &amp;lt;tt&amp;gt;Kosten(predecessor &amp;amp;rarr; startnode) + weight[(predecessor, node)]&amp;lt;/tt&amp;gt;. Aufgrund der Induktionsvoraussetzung gilt aber &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt;, und somit &amp;lt;tt&amp;gt;Kosten(predecessor &amp;amp;rarr; startnode) &amp;amp;lt; Kosten(x &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt;, weil &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; andernfalls vor &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; an der Spitze des Heaps gewesen wäre, was mit der Annahme &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\notin S&amp;lt;/math&amp;gt; unverträglich ist. Damit der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; trotzdem der kürzeste Weg sein kann, müsste &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) &amp;amp;lt; Kosten(node &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt; gelten, denn durch die Kante &amp;lt;tt&amp;gt;(x, node)&amp;lt;/tt&amp;gt; kommen ja noch Kosten hinzu. Das wäre aber nur möglich, wenn der Knoten &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; vor dem Knoten &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; an die Spitze des Heaps gelangt, im Widerspruch zur Annahme, dass &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; sich gerade an der Spitze des Heaps befindet. Somit kann die Behauptung, dass der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; der kürzeste Weg ist, nicht stimmen.&lt;br /&gt;
In beiden Fällen erhalten wir einen Widerspruch, und die Behauptung ist somit bewiesen. Da die Invariante insbesondere für den Weg zum Zielknoten &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; erfüllt ist, folgt daraus auch die Korrektheit des Algorithmus von Dijkstra.&lt;br /&gt;
&lt;br /&gt;
===  A*-Algorithmus - Wie kann man Dijkstra noch verbessern? ===&lt;br /&gt;
&lt;br /&gt;
Eine wichtige Eigenschaft des Dijkstra-Algorithmus ist, dass neben dem kürzesten Weg vom Start zum Ziel auch die kürzesten Wege zu allen Knoten berechnet werden, die näher am Startknoten liegen als das Ziel, obwohl uns diese Wege gar nicht interessieren. Sucht man beispielsweise in einem Graphen mit den Straßenverbindungen in Deutschland den kürzesten Weg von Frankfurt (Main) nach Dresden (ca. 460 km), werden auch die kürzesten Wege von Frankfurt nach Köln (190 km), Dortmund (220 km) und Stuttgart (210 km) und vielen anderen Städten gefunden. Aufgrund der geographischen Lage dieser Städte ist eigentlich von vornherein klar, dass sie mit dem kürzesten Weg nach Dresden nicht das geringste zu tun haben. Anders sieht es mit Erfurt (260 km) oder Suhl (210 km) aus - diese Städte liegen zwischen Frankfurt und Dresden und kommen deshalb als Zwischenstationen des gesuchten Weges in Frage.&lt;br /&gt;
&lt;br /&gt;
Damit Dijkstra korrekt funktioniert, würde es im Prinzip ausreichen, wenn man die kürzesten Wege nur für diejenigen Knoten ausrechnet, die auf dem kürzesten Weg vom Start zum Ziel liegen, denn nur diese Knoten braucht man, um den gesuchten Weg über die &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette zurückzuverfolgen. Das Problem ist nur, dass man diese Knoten erst kennt, wenn der Algorithmus fertig durchgelaufen ist. Schließt man Knoten zu früh von der Betrachtung aus, kommt am Ende möglicherweise nicht der korrekte kürzeste Weg heraus. &lt;br /&gt;
&lt;br /&gt;
Der A*-Algorithmus löst dieses Dilemma mit folgender Idee: Ändere die Prioritäten für den Heap so ab, dass unwichtige Knoten nur mit geringerer Wahscheinlichkeit expandiert werden, aber stelle gleichzeitig sicher, dass alle wichtigen Knoten (also diejenigen auf dem korrekten kürzesten Weg) auf jeden Fall expandiert werden. Es zeigt sich, dass man diese Idee umsetzen kann, wenn eine &amp;lt;i&amp;gt;Schätzung für den Restweg&amp;lt;/i&amp;gt; (also für die noch verbleibende Entfernung von jedem Knoten zum Ziel) verfügbar ist:&lt;br /&gt;
 rest = guess(neighbor, destination)&lt;br /&gt;
Diese Schätzung addiert man einfach zur wahren Länge des Weges &amp;lt;tt&amp;gt;startnode &amp;amp;rarr; node&amp;lt;/tt&amp;gt; dazu, um die verbesserte Priorität zu erhalten:&lt;br /&gt;
 priority = newLength + guess(neighbor, destination)&lt;br /&gt;
(Im originalen Dijkstra-Algorithmus wird als Priorität nur &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; allein verwendet. Man beachte, dass man &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; jetzt zusätzlich im Heap speichern muss, weil man es für die Expansion des Knotens später noch benötigt.)&lt;br /&gt;
&lt;br /&gt;
Damit sicher gestellt ist, dass der A*-Algorithmus immer noch die korrekten kürzesten Wege findet, darf die Schätzung den wahren Restweg &amp;lt;i&amp;gt;niemals überschätzen&amp;lt;/i&amp;gt;. Es muss immer gelten:&lt;br /&gt;
 0 &amp;lt;= guess(node, destination) &amp;lt;= trueDistance(node, destination)&lt;br /&gt;
Damit gilt insbesondere &amp;lt;tt&amp;gt;guess(destination, destination) = trueDistance(destination, destination) = 0&amp;lt;/tt&amp;gt;, an der Priorität des Knotens &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; ändert sich also nichts. Die Prioritäten aller anderen Knoten veschlechtern sich hingegen, weil zur bisherigen Priorität noch atwas addiert wird. Für die wichtigen Knoten auf dem kürzesten Weg vom Start nach Ziel gilt jedoch, dass deren neue Priorität immer noch besser ist als die Priorität des Zielknotens selbst. Für diese Knoten gilt nämlich&lt;br /&gt;
 falls node auf dem kürzesten Weg von startnode nach destination liegt:&lt;br /&gt;
 trueDistance(startnode, node) + guess(node, destination) &amp;lt;= trueDistance(startnode, destination)&lt;br /&gt;
weil der Weg von Start nach &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; ein Teil des kürzesten Wegs von Start nach Ziel ist und die Restschätzung die wahre Entfernung immer unterschätzt. Diese Knoten werden deshalb stets vor dem Zielknoten expandiert, so dass wir die &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette immer noch korrekt zurückverfolgen können. Für alle anderen Knoten gilt idealerweise, dass die neue Priorität schlechter ist als die Priorität von &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt;, so dass man sich diese irrelevanten Knotenexpansionen sparen kann.&lt;br /&gt;
&lt;br /&gt;
Für das Beispiel eines Straßennetzwerks bietet sich als Schätzung die Luftlinienentfernung an, weil Straßen nie kürzer sein können als die Luftlinie. Damit erreicht man in der Praxis deutliche Einsparungen. Generell gilt, dass der A*-Algorithmus im typischen Fall schneller ist als der Algorithmus von Dijkstra, aber man kann immer pathologische Fälle konstruieren, wo die Änderung der Prioritäten nichts bringt. Die Komplexität des A*-Algorithmus im ungünstigen Fall ist deshalb nach wie vor &amp;lt;math&amp;gt;O(E\,\log\,V)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=='''Minimaler Spannbaum'''==&lt;br /&gt;
'''(engl.: minimum spanning tree; abgekürzt: MST)'''&lt;br /&gt;
&lt;br /&gt;
[[Image:Minimum_spanning_tree.png‎ |thumb|200px|right|Ein minimal aufspannender Baum verbindet alle Punkte eines Graphen bei minimaler Kantenlänge ([http://de.wikipedia.org/wiki/Spannbaum Quelle])]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: gewichteter Graph G, zusammenhängend&amp;lt;br/&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: Untermenge &amp;lt;math&amp;gt;E'\subseteq E&amp;lt;/math&amp;gt; der Kanten, so dass die Summe der Kantengewichte &amp;lt;math&amp;gt;\sum_{e\in E'} w_e&amp;lt;/math&amp;gt; minimal und der entstehende Graph G' zusammenhängend ist.&amp;lt;br/&amp;gt;&lt;br /&gt;
* G' definiert immer einen Baum, denn andernfalls könnte man eine Kante weglassen und dadurch die Summe &amp;lt;math&amp;gt;\sum_{e\in E'} w_e&amp;lt;/math&amp;gt; verringern, ohne dass sich am Zusammenhang von G' etwas ändert. &amp;lt;br/&amp;gt;&lt;br /&gt;
* Wenn der Graph G nicht zusammenhängend ist, kann man den Spannbaum für jede Zusammenhangskomponente getrennt ausrechnen. Man erhält dann einen aufspannenden Wald. &lt;br /&gt;
* Der MST ist ähnlich wie der Dijkstra-Algorithmus: Dort ist ein Pfad gesucht, bei dem die Summe der Gewichte über den Pfad minimal ist. Beim MST suchen wir eine Lösung, bei der die Summe der Gewichte über den ganzen Graphen minimal ist. &lt;br /&gt;
* Das Problem des MST ist nahe verwandt mit der Bestimmung der Zusammenhangskomponente, z.B. über den Tiefensuchbaum. Für die Zusammenhangskomponenten genügt allerdings ein beliebiger Baum, während beim MST ein minimaler Baum gesucht ist.&lt;br /&gt;
&lt;br /&gt;
=== Anwendungen ===&lt;br /&gt;
==== Wie verbindet man n gegebene Punkte mit möglichst kurzen Straßen (Eisenbahnen, Drähten [bei Schaltungen] usw.)?====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:center&amp;quot; border=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; cellspacing=&amp;quot;0&amp;quot; &lt;br /&gt;
|MST minimale Verbindung (Abb.1)&lt;br /&gt;
|MST = 2 (Länge = Kantengewicht)(Abb.2)&lt;br /&gt;
|- valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| [[Image:mst.png]] &lt;br /&gt;
| [[Image:Gleichseitigesdreieck.png]]&lt;br /&gt;
|}&lt;br /&gt;
*In der Praxis: Die Festlegung, dass man nur die gegebenen Punkte verwenden darf, ist eine ziemliche starke Einschränkung. &lt;br /&gt;
&lt;br /&gt;
* Wenn man sich vorstellt, es sind drei Punkte gegeben, die als gleichseitiges Dreieck angeordnet sind, dann ist der MST (siehe Abb.2, schwarz gezeichnet) und hat die Länge 2. Man kann hier die Länge als Kantengewicht verwenden. &lt;br /&gt;
&lt;br /&gt;
* Wenn es erlaubt ist zusätzliche Punkte einzufügen, dann kann man in der Mitte einen neuen Punkt setzen &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; neuer MST (siehe Abb.2, orange gezeichnet).&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Höhe = &amp;lt;math&amp;gt;\frac{1}{2}\sqrt{3}&amp;lt;/math&amp;gt;, Schwerpunkt: teilt die Höhe des Dreiecks im Verhältnis 2:1; der Abstand von obersten Punkt bis zum neu eingeführten Punkt: &amp;lt;math&amp;gt;\frac{2}{3}h = \frac{\sqrt{3}}{3}&amp;lt;/math&amp;gt;, davon insgesamt 3 Stück, damit (gilt für den MST in orange eingezeichnet): MST = &amp;lt;math&amp;gt;3\left(\frac{1}{3}\right) \sqrt{3} = \sqrt{3} \approx 1,7&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Damit ist der MST in orange kürzer als der schwarz gezeichnete MST. &amp;lt;br\&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt;Folgerung: MST kann kürzer werden, wenn man einen Punkt dazu nimmt. &lt;br /&gt;
* Umgekehrt kann der MST auch kürzer werden, wenn man einen Punkt aus dem Graphen entfernt, aber wie das Beipiel des gleichseitigen Dreiecks zeigt, ist dies nicht immer der Fall.&lt;br /&gt;
&lt;br /&gt;
[[Image: bahn.png|Bahnstrecke Verbindung (Abb.3)]]&lt;br /&gt;
&lt;br /&gt;
* Methode der zusätzlichen Punkteinfügung hat man früher beim Bahnstreckenbau verwendet. Durch Einführung eines Knotenpunktes kann die Streckenlänge verkürzt werden (Dreiecksungleichung).&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Bestimmung von Datenclustern ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Image:cluster.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Daten (in der Abb.: Punkte) bilden Gruppen. &lt;br /&gt;
&lt;br /&gt;
* In der Abbildung hat man 2 verschiedene Messungen gemacht (als x- und y-Achse aufgetragen), bspw. Größe und Gewicht von Personen. Für jede Person i wird ein Punkt an der Koordinate (Größe&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;, Gewicht&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;) gezeichnet (siehe Bild a). Dies bezeichnet man als ''Scatter Plot''. Wenn bestimmte Wertkombinationen häufiger auftreten als andere, bilden sich mitunter Gruppen aus, bspw. eine Gruppe für &amp;quot;klein und schwer&amp;quot; etc.&lt;br /&gt;
&lt;br /&gt;
* Durch Verbinden der Punkte mittels eines MST (siehe Abbildung (b)) sieht man, dass es kurze (innerhalb der Gruppen) und lange Kanten (zwischen den Gruppen) gibt. &lt;br /&gt;
&lt;br /&gt;
* Wenn man geschickt eine Schwelle einführt und alle Kanten löscht, die länger sind als die Schwelle, dann bekommt man als Zusammenhangskomponente die einzelnen Gruppen. &lt;br /&gt;
&lt;br /&gt;
=== Algorithmen ===&lt;br /&gt;
&lt;br /&gt;
Genau wie bei der Bestimmung von Zusammenhangskomponenten kann man auch das MST-Problem entweder nach dem Anlagerungsprinzip oder nach dem Verschmelzungsprinzip lösen (dazu gibt es noch weitere Möglichkeiten, z.B. den [http://de.wikipedia.org/wiki/Algorithmus_von_Bor%C5%AFvka Algorithmus von Boruvka]). Der Anlagerungsalgorithmus für MST wurde zuerst von Prim beschrieben und trägt deshalb seinen Namen, der Verschmelzungsalgorithmus stammt von Kruskal. Im Vergleich zu den Algorithmen für Zusammenhangskomponenten ändert sich im wesentlichen nur die Reihenfolge, in der die Kanten betrachtet werden: Eine Prioritätswarteschlange stellt jetzt sicher, dass am Ende wirklich der Baum mit den geringstmöglichen Kosten herauskommt.&lt;br /&gt;
&lt;br /&gt;
====Algorithmus von Prim====&lt;br /&gt;
[http://de.wikipedia.org/wiki/Algorithmus_von_Prim Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus von Prim geht nach dem Anlagerungsprinzip vor (vgl. den Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Tiefensuche|Zusammenhangskomponenten mit Tiefensuche]]): Starte an der Wurzel (ein willkürlich gewählter Knoten) und füge jeweils die günstigste Kante an die aktuellen Teillösung an, die keinen Zyklus verursacht. Die Sortierung der Kanten nach Priorität erfolgt analog zum Dijsktra-Algorithmus, aber die Definitionen, welche Kante die günstigste ist, unterscheiden sich. Die Konvention für die Bedeutung der Elemente des Heaps ist ebenfalls identisch: ein Tupel mit &amp;lt;tt&amp;gt;(priority, node, predecessor)&amp;lt;/tt&amp;gt;. Die folgende Implementation verdeutlicht sehr schön die Ähnlichkeit der beiden Algorithmen. Das Ergebnis wird als property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; zurückgegeben, in der für jeden Knoten sein Vorgänger im MST steht, wobei die Wurzel wie üblich auf sich selbst verweist.&lt;br /&gt;
&lt;br /&gt;
 import heapq&lt;br /&gt;
 &lt;br /&gt;
 def prim(graph, weights):                # Kantengewichte wie bei Dijkstra als property map&lt;br /&gt;
 	sum = 0.0                         # wird später das Gewicht des Spannbaums sein&lt;br /&gt;
 	start = 0                         # Knoten 0 wird willkürlich als Wurzel gewählt&lt;br /&gt;
        &lt;br /&gt;
        parents = [None]*len(graph)       # property map, die den resultierenden Baum kodiert&lt;br /&gt;
        parents[start] = start            # Wurzel zeigt auf sich selbst&lt;br /&gt;
        &lt;br /&gt;
 	heap = []                         # Heap für die Kanten des Graphen&lt;br /&gt;
 	for neighbor in graph[start]:     # besuche die Nachbarn von start&lt;br /&gt;
 	    heapq.heappush(heap, (weights[(start, neighbor)], neighbor, start))  # und fülle Heap &lt;br /&gt;
 	&lt;br /&gt;
        while len(heap) &amp;gt; 0:&lt;br /&gt;
 	    w, node, predecessor = heapq.heappop(heap) # hole billigste Kante aus dem Heap&lt;br /&gt;
            if parents[node] is not None: # die Kante würde einen Zyklus verursachen&lt;br /&gt;
                continue                  #   =&amp;gt; ignoriere diese Kante&lt;br /&gt;
            parents[node] = predecessor   # füge Kante in den MST ein&lt;br /&gt;
            sum += w                      # und aktualisiere das Gesamtgewicht &lt;br /&gt;
            for neighbor in graph[node]:  # besuche die Nachbarn von node&lt;br /&gt;
                if parents[neighbor] is None:  # aber nur, wenn kein Zyklus entsteht&lt;br /&gt;
                    heapq.heappush(heap, (weights[(node,neighbor)], neighbor, node)) # füge Kandidaten in Heap ein&lt;br /&gt;
        &lt;br /&gt;
 	return parents, sum               # MST und Gesamtgewicht zurückgeben&lt;br /&gt;
&lt;br /&gt;
====Algorithmus von Kruskal====&lt;br /&gt;
[http://de.wikipedia.org/wiki/Algorithmus_von_Kruskal Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Kruskal%27s_algorithm (en)]&lt;br /&gt;
&lt;br /&gt;
Die alternative Vorgehensweise ist das Verschmelzungsprinzip (vgl. den Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Union-Find-Algorithmus|Zusammenhangskomponenten mit Union-Find-Algorithmus]]), das der Algorithmus von Kruskal verwendet. Jeder Knoten wird zunächst als trivialer Baum mit nur einem Knoten betrachtet, und alle Kanten werden aufsteigend nach Gewicht sortiert. Dann wird die billigste noch nicht betrachtete Kante in den MST eingefügt, falls sich dadurch kein Zyklus bildet (erkennbar daran, dass die Endknoten in verschiedenen Zusammenhangskomponenten liegen, das heisst verschiedene Anker haben). Da der fertige Baum (V-1) Kanten haben muss, wird dies (V-1) Mal zutreffen. Andernfalls wird diese Kante ignoriert. Anders ausgedrückt: Der Algorithmus beginnt mit ''V'' Bäumen; in (''V''-1) Verschmelzungsschritten kombiniert er jeweils zwei Bäume (unter Verwendung der kürzesten möglichen Kante), bis nur noch ein Baum übrig bleibt. Der einzige Unterschied zum einfachen Union-Find besteht darin, dass die Kanten in aufsteigender Reihenfolge betrachtet werden müssen, was wir hier durch eine Prioritätswarteschlange realisieren. Der Algorithmus von J.Kruskal ist seit 1956 bekannt. &lt;br /&gt;
&lt;br /&gt;
 def kruskal(graph, weights):&lt;br /&gt;
     anchors = range(len(graph))           # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     results = []                          # result wird später die Kanten des MST enthalten    &lt;br /&gt;
     &lt;br /&gt;
     heap = []                             # Heap zum Sortieren der Kanten nach Gewicht&lt;br /&gt;
     for edge, w in weights.iteritems():   # alle Kanten einfügen&lt;br /&gt;
         heapq.heappush(heap, (w, edge))&lt;br /&gt;
     &lt;br /&gt;
     while len(heap) &amp;gt; 0:                  # solange noch Kanten vorhanden sind&lt;br /&gt;
         w, edge = heapq.heappop(heap)     # billigste Kante aus dem Heap nehmen&lt;br /&gt;
         a1 = findAnchor(anchors, edge[0]) # Anker von Startknoten der Kante&lt;br /&gt;
         a2 = findAnchor(anchors, edge[1]) # ... und Endknoten bestimmen&lt;br /&gt;
         if a1 != a2:                      # wenn die Knoten in verschiedenen Komponenten sind&lt;br /&gt;
             anchors[a2] = a1              # Komponenten verschmelzen&lt;br /&gt;
             result.append(edge)           # ... und Kante in MST einfügen&lt;br /&gt;
     &lt;br /&gt;
     return result                         # Kanten des MST zurückgeben&lt;br /&gt;
&lt;br /&gt;
Die Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt; wurde im Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Union-Find-Algorithmus|Zusammenhangskomponenten mit Union-Find-Algorithmus]] implementiert. Im Unterschied zum Algorithmus von Prim geben wir hier nicht die property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; zurück, sondern einfach eine Liste der Kanten im MST.&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus eignet sich insbesondere für das Clusteringproblem, da der Schwellwert von vornerein als maximales Kantengewicht an den Algorithmus übergeben werden kann. Man hört mit dem Vereinigen auf, wenn das Gewicht der billigste Kante im Heap den Schwellwert überschreitet. Beim Algorithmus von Kruskal kann dann keine bessere Kante als der Schwellwert mehr kommen, da die Kanten vorher sortiert worden sind. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Komplexität:&amp;lt;/b&amp;gt; wie beim Dijkstra-Algorithmus, weil jede Kante genau einmal in den Heap kommt. Der Aufwand für das Sortieren ist somit &amp;lt;math&amp;gt;O\left(E\log E\right)&amp;lt;/math&amp;gt;, was sich zu &amp;lt;math&amp;gt;O \left(E\,\log\,V\right)&amp;lt;/math&amp;gt; reduziert, falls keine Mehrfachkanten vorhanden sind.&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; geeignet für Übungsaufgabe&lt;br /&gt;
&lt;br /&gt;
====Verwendung einer BucketPriorityQueue====&lt;br /&gt;
&lt;br /&gt;
Beide Algorithmen zur Bestimmung des minimalen Spannbaums benötigen eine Prioritätswarteschlange. Wenn die Kantengewichte ganze Zahlen im Bereich &amp;lt;tt&amp;gt;0...(m-1)&amp;lt;/tt&amp;gt; sind, kann man die MST-Algorithmen deutlich beschleunigen, wenn man anstelle des Heaps eine [[Prioritätswarteschlangen#Prioritätssuche mit dem Bucket-Prinzip|&amp;lt;tt&amp;gt;BucketPriorityQueue&amp;lt;/tt&amp;gt;]] verwendet. Die Operationen zum Einfügen einer Kante in die Queue und zum Entfernen der billibsten Kante aus der Queue beschleunigen sich dadurch auf O(1) statt O(log V) (außer wenn die Gewichte sehr ungünstig auf die Kanten verteilt sind). In der Praxis erreicht man durch diese Änderung typischerweise deutliche Verbesserungen. In der Bildverarbeitung können die Prioritäten beispielsweise die Wahrscheinlichkeit kodieren, dass zwei benachbarte Pixel zu verschiedenen Objekten gehören. Bildet man jetzt den MST, und bricht bei einer bestimmten Wahrscheinlichkeit ab, erhält man Cluster von Pixeln, die wahrscheinlich zum selben Objekt gehören (weil der MST ja die Kanten mit minimalem Gewicht bevorzugt, und kleine Gewichte bedeuten kleine Wahrscheinlichkeit, dass benachbarte Pixel von einander getrennt werden). Da man die Wahrscheinlichkeiten nur mit einer Genauigkeit von ca. 1% berechnen kann, reichen hiefür 100 bis 200 Quantisierungstufen aus. Durch Verwendung der schnellen &amp;lt;tt&amp;gt;BucketPriorityQueue&amp;lt;/tt&amp;gt; kann man jetzt wesentlich größere Bilder in akzeptabler Zeit bearbeiten als dies mit einem Heap möglich wäre.&lt;br /&gt;
&lt;br /&gt;
== Algorithmen für gerichtete Graphen ==&lt;br /&gt;
&lt;br /&gt;
Zur Erinnerung: in einem gerichteten Graphen sind die Kanten (i &amp;amp;rarr; j) und (j &amp;amp;rarr; i) voneinander verschieden, und eventuell existiert nur eine der beiden Richtungen. Im allgemeinen unterscheidet sich der [[Graphen_und_Graphenalgorithmen#transposed_graph|transponierte Graph]] G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; also vom Originalgraphen G. Beim Traversieren des Graphen und bei der Pfadsuche dürfen Kanten nur in passender Richtung verwendet werden. Bei gewichteten Graphen tritt häufig der Fall auf, dass zwar Kanten in beiden Richtungen existieren, diese aber unterschiedliche Gewichte haben.&lt;br /&gt;
&lt;br /&gt;
Gerichtete Graphen ergeben sich in natürlicher Weise aus vielen Anwendungsproblemen:&lt;br /&gt;
* Routenplanung&lt;br /&gt;
** Bei Straßennetzwerken enstehen gerichtete Graphen, sobald es Einbahnstraßen gibt.&lt;br /&gt;
** Verwendet man Gewichte, um die erwarteten Fahrzeiten entlang einer Straße zu kodieren, gibt es Asymmetrien z.B. dann, wenn Straßen in einer Richtung bergab, in der anderen bergauf befahren werden. Hier existieren zwar Kanten in beiden Richtungen, sie haben aber unterschiedliche Gewichte. Ähnliches gilt für Flüge: Durch den Gegenwind des Jetstreams braucht man von Frankfurt nach New York länger als umgekehrt von New York nach Frankfurt.&lt;br /&gt;
* zeitliche oder kausale Abhängigkeiten&lt;br /&gt;
** Wenn die Knoten Ereignisse repräsentieren, von denen einige die Ursache von anderen sind, diese wiederum die Ursache der nächsten usw., verbindet man die Knoten zweckmäßig durch gerichtete Kanten, die die Kausalitätsbeziehungen kodieren. Handelt es sich um logische &amp;quot;wenn-dann&amp;quot;-Regeln, erhält man einen [[Graphen_und_Graphenalgorithmen#Anwendung:_Das_Erf.C3.BCllbarkeitsproblem_in_Implikationengraphen|Implikationengraph]] (siehe unten). Handelt es sich hingegen um Wahrscheinlichkeitsaussagen (&amp;quot;Wenn das Wetter schön ist, haben Studenten tendenziell gute Laune, wenn eine Prüfung bevorsteht eher schlechte usw.&amp;quot;), erhält man ein [http://de.wikipedia.org/wiki/Bayessches_Netz Bayessches Netz].&lt;br /&gt;
** Wenn bestimmte Aufgaben erst begonnen werden können, nachdem andere Aufgaben erledigt sind, erhält man einen Abhängigkeitsgraphen. Beispielsweise dürfen Sie erst an der Klausur teilnehmen, nachdem Sie die Übungsaufgaben gelöst haben, und Sie dürfen erst die Abschlussarbeit beginnen, nachdem Sie bestimmte Prüfungen bestanden haben. Ein anderes schönes Beispiel liefern die Regeln für das [[Graphen_und_Graphenalgorithmen#Anwendung:_Abh.C3.A4ngigkeitsgraph|Ankleiden]] weiter unten.&lt;br /&gt;
** Gerichtete Graphen kodieren die Abhängigkeiten zwischen Programmbibliotheken. Beispielsweise benötigt das Pythonmodul &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; die internen Submodule &amp;lt;tt&amp;gt;json.encoder&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;json.decode&amp;lt;/tt&amp;gt; sowie das externe Modul &amp;lt;tt&amp;gt;decimal&amp;lt;/tt&amp;gt;. Die Submodule benötigen wiederum die externen Module &amp;lt;tt&amp;gt;re&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;sys&amp;lt;/tt&amp;gt;, das Modul &amp;lt;tt&amp;gt;decimal&amp;lt;/tt&amp;gt; braucht &amp;lt;tt&amp;gt;copy&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;collections&amp;lt;/tt&amp;gt; usw.&lt;br /&gt;
** Das Internet kann als gerichteter Graph dargestellt werden, wobei die Webseiten die Knoten, und die Hyperlinks die Kanten sind.&lt;br /&gt;
* Sequence Alignment&lt;br /&gt;
** Eine gute Rechtschreibprüfung markiert nicht nur fehlerhafte Wörter, sondern macht auch plausible Vorschläge, was eigentlich gemeint gewesen sein könnte. Dazu muss sie das gegebene Wort mit den Wörtern eines Wörterbuchs vergleichen und die Ähnlichkeit bewerten. Ein analoges Problem ergibt sich, wenn man DNA Fragmente mit der Information in einer Genomdatenbank abgleichen will. &lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Sequence Alignment / Edit Distance ===&lt;br /&gt;
&lt;br /&gt;
:gegeben: zwei Wörter (allgemein: beliebige Zeichenfolgen)&lt;br /&gt;
:gesucht: Wie kann man die Buchstaben am besten in Übereinstimmung bringen?&lt;br /&gt;
&lt;br /&gt;
:Beispiel: WORTE – NORDEN&lt;br /&gt;
&lt;br /&gt;
Zwei mögliche Alignments sind&lt;br /&gt;
&lt;br /&gt;
  W&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;OR&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;T&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;E&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;.          W.ORTE&lt;br /&gt;
  N&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;OR&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;D&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;E&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;N          NORDEN&lt;br /&gt;
&lt;br /&gt;
wobei der Punkt anzeigt, dass der untere Buchstabe keinen Partner hat, und rote Buchstaben oben und unten übereinstimmen. Jede Nicht-Übereinstimmung verursacht nun gewisse Kosten. Dabei unterscheiden wir zwei Fälle:&lt;br /&gt;
# Matche a[i] mit b[j]. Falls a[i] == b[j], ist das gut (rote Buchstaben), und es entstehen keine Kosten. Andernfalls entstehen Kosten U (schwarze Buchstaben).&lt;br /&gt;
# Wir überspringen a[i] oder b[j] (Buchstabe vs. Punkt). Dann entstehen Kosten V. (Manchmal unterscheidet man auch noch Kosten Va und Vb, wenn das Überspringen bei a und b unterschieldiche Signifikanz hat.)&lt;br /&gt;
&lt;br /&gt;
Gesucht ist nun das &amp;lt;b&amp;gt;Alignment mit minimalen Kosten&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Diese Aufgabe kann man sehr schön als gerichteten Graphen darstellen: Wir definieren ein rechteckiges Gitter und schreiben das erste Wort über das Gitter und das andere links davon. Die Gitterpunkte verbinden wir mit Pfeilen (gerichteten Kanten), wobei ein Pfeil nach rechts bedeutet, dass wir beim oberen Wort einen Buchstaben überspringen, ein Pfeil nach unten, dass wir beim linken Wort einen Buchstaben überspringen, und ein diagonaler Pfeil, dass wir zwei Buchstaben matchen (und zwar die am Pfeilende). Die Farben der Pfeile symbolisieren die Kosten: rot für das Überspringen eines Buchstabens (Kosten V), blau für das Matchen, wenn die Buchstaben nicht übereinstimmen (Kosten U), und grün, wenn die Buchstaben übereinstimmen (keine Kosten). &lt;br /&gt;
&lt;br /&gt;
[[Image:sequence-alignment.png|300px]]&lt;br /&gt;
&lt;br /&gt;
Lösung:&lt;br /&gt;
:Suche den kürzesten Pfad vom Knoten &amp;quot;START&amp;quot; (oben links) nach unten rechts. Dazu kann der [[Graphen und Graphenalgorithmen#Algorithmus von Dijkstra|Algorithmus von Dijkstra]] verwendet werden, der auf gerichteten Graphen genauso funktioniert wie auf ungerichteten.&lt;br /&gt;
&lt;br /&gt;
Für unser Beispiel von oben erhalten wir die folgenden Pfade:&lt;br /&gt;
&lt;br /&gt;
[[Image:sequence-alignment-weg1.png|400px]]&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;[[Image:sequence-alignment-weg2.png|400px]]&lt;br /&gt;
&lt;br /&gt;
Durch Addieren der Kosten entsprechend der Farben sieht man, dass der erste Weg die Kosten 2U+V und der zweite die Kosten 5U+V hat. Der erste Weg ist offensichtlich günstiger und entspricht dem besten Alignment.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Abhängigkeitsgraph ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Beispiel: &amp;lt;/b&amp;gt; Wie erklärt man einem zerstreuten Professor, wie er sich morgens anziehen soll? Der folgende Graph enthält einen Knoten für jede Aktion, und eine Kante (i &amp;amp;rarr; j) bedeutet, dass die Aktion i vor der Aktion j abgeschlossen werden muss.&lt;br /&gt;
&lt;br /&gt;
[[Image:anziehen-graph.png|600px]]&lt;br /&gt;
&lt;br /&gt;
In derartigen Abhängigkeitsgraphen ist die wichtigste Frage immer, ob der Graph azyklisch ist. Wäre dies nämlich nicht der Fall, kann es keine Reihenfolge der Aktionen geben, die alle Abhängigkeiten erfüllt. Dies sieht man leicht, wenn man den einfachsten möglichen Zyklus betrachtet: es gibt sowohl eine Kante (i &amp;amp;rarr; j) als auch eine (j &amp;amp;rarr; i). Dann müsste man i vor j erledigen, aber ebenso j vor i, was offensichtlich unmöglich ist - das im Graph kodierte Problem ist dann unlösbar. Wegen ihrer Wichtigkeit wird für gerichtete azyklische Graphen oft die Abkürzung &amp;lt;b&amp;gt;DAG&amp;lt;/b&amp;gt; (von &amp;lt;i&amp;gt;directed acyclic graph&amp;lt;/i&amp;gt;) verwendet. Ein Graph ist genau dann ein DAG, wenn es eine topologische Sortierung gibt:&lt;br /&gt;
;topologische Sortierung: Zeichne die Knoten so auf eine Gerade, dass alle Kanten (Pfeile) nach rechts zeigen. &lt;br /&gt;
Arbeitet man die Aktionen nach einer (beliebigen) topologischen Sortierung ab, werden automatisch alle Abhängigkeiten eingehalten: Da alle Pfeile nach rechts zeigen, werden abhängige Aktionen immer später ausgeführt. Die topologische Sortierung ist im allgemeinen nicht eindeutig. Die folgende Skizze zeigt eine mögliche topologische Sortierung für das Anziehen:&lt;br /&gt;
&lt;br /&gt;
[[Image:anziehen-topologische-sortierung.png|600px]]&lt;br /&gt;
&lt;br /&gt;
Eine solche fest vorgegebene Reihenfolge ist für den zerstreuten Professor sicherlich eine größere Hilfe als der ursprüngliche Graph. Man erkennt, dass die Sortierung nicht eindeutig ist, beispielsweise bei der Uhr: Da für die Uhr keine Abhängigkeiten definiert sind, kann man diese Aktion an beliebiger Stelle einsortieren. Hier wurde willkürlich die letzte Stelle gewählt.&lt;br /&gt;
&lt;br /&gt;
==== Zwei Algorithmen zum Finden der topologischen Sortierung ====&lt;br /&gt;
&lt;br /&gt;
Die folgenden Algorithmen finden entweder eine topologische Sortierung, oder signalisieren, dass der Graph zyklisch ist.&lt;br /&gt;
&lt;br /&gt;
===== Algorithmus 1 =====&lt;br /&gt;
# Suche einen Knoten mit Eingangsgrad 0 (ohne eingehende Pfeile) =&amp;gt; in einem gerichteten azyklischen Graphen gibt es immer einen solchen Knoten&lt;br /&gt;
# Platziere diesen Knoten auf der Geraden (beliebig)&lt;br /&gt;
# Entferne den Knoten aus dem Graphen zusammen mit den ausgehenden Kanten&lt;br /&gt;
# Gehe zu 1., aber platziere in 2. immer rechts der Knoten, die schon auf der Geraden vorhanden sind.&lt;br /&gt;
: =&amp;gt; Wenn noch Knoten übrig sind, aber keiner Eingangsgrad 0 hat, muss der Graph zyklisch sein.&lt;br /&gt;
&lt;br /&gt;
[[Image:bild6.JPG]]&lt;br /&gt;
&lt;br /&gt;
Beispiel für einen zyklischen Graphen: kein Knoten hat Eingangsgrad 0.&lt;br /&gt;
&lt;br /&gt;
Um den Algorithmus zu implementieren, verwenden wir eine property map &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt;, die wir in einem ersten Durchlauf durch den Graphen füllen und die dann für jeden Knoten die Anzahl der eingehenden Kanten speichert. Dann gehen wir sukzessive zu allen Knoten mit &amp;lt;tt&amp;gt;in_degree == 0&amp;lt;/tt&amp;gt;. Anstatt sie aber tatsächlich aus dem Graphen zu entfernen wie im obigen Pseudocode, dekrementieren wir nur den &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt; ihrer Nachbarn. Wird der &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt; eines Nachbarn dadurch 0, wird er ebenfalls in das Array der zu scannenden Knoten aufgenommen. Wenn der Graph azyklisch ist, enthält das Array am Ende alle Knoten des Graphen, und die Reihenfolge der Einfügungen definiert eine topologische Sortierung. Andernfalls ist das Array zu kurz, und wir signalisieren durch Zurückgeben von &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, dass der Graph zyklisch ist:&lt;br /&gt;
&lt;br /&gt;
 def topological_sort(graph):              # ein gerichteter Graph&lt;br /&gt;
     in_degree = [0]*len(graph)            # property map für den Eingangsgrad jeden Knotens&lt;br /&gt;
     for node in xrange(len(graph)):       # besuche alle Knoten&lt;br /&gt;
         for neighbor in graph[node]:      #  ... und deren Nachbarn&lt;br /&gt;
             in_degree[neighbor] += 1      #  ... und inkrementiere den Eingangsgrad&lt;br /&gt;
     &lt;br /&gt;
     result = []                           # wird später die topologische Sortierung enthalten&lt;br /&gt;
     for node in xrange(len(graph)):&lt;br /&gt;
         if in_degree[node] == 0:&lt;br /&gt;
             result.append(node)           # füge alle Knoten mit Eingangsgrad 0 in result ein&lt;br /&gt;
     &lt;br /&gt;
     k = 0&lt;br /&gt;
     while k &amp;lt; len(result):                # besuche alle Knoten mit Eingangsgrad 0&lt;br /&gt;
         node = result[k]&lt;br /&gt;
         k += 1&lt;br /&gt;
         for neighbor in graph[node]:      # besuche alle Nachbarn&lt;br /&gt;
             in_degree[neighbor] -= 1      # entferne 'virtuell' die eingehende Kante&lt;br /&gt;
             if in_degree[neighbor] == 0:  # wenn neighbor jetzt Eingangsgrad 0 hat&lt;br /&gt;
                 result.append(neighbor)   #  ... füge ihn in result ein&lt;br /&gt;
     &lt;br /&gt;
     if len(result) == len(graph):         # wenn alle Knoten jetzt Eingangsgrad 0 haben&lt;br /&gt;
         return result                     # ... ist result eine topologische Sortierung&lt;br /&gt;
     else:&lt;br /&gt;
         return None                       # andernfalls ist der Graph zyklisch&lt;br /&gt;
&lt;br /&gt;
===== Algorithmus 2 =====&lt;br /&gt;
Der obige Algorithmus hat den Nachteil, dass er jeden Knoten zweimal expandiert. Man kann eine topologische Sortierung stattdessen auch mit Tiefensuche bestimmen. Es gilt nämlich der folgende&lt;br /&gt;
;Satz: Wird ein DAG mittels Tiefensuche traversiert, definiert die &amp;lt;i&amp;gt;reverse post-order&amp;lt;/i&amp;gt; eine topologische Sortierung.&lt;br /&gt;
Zur Erinnerung: die post-order erhält man, indem man jeden Knoten ausgibt, &amp;lt;i&amp;gt;nachdem&amp;lt;/i&amp;gt; die Rekursion zu allen seinen Nachbarn beendet ist, siehe unsere [[Graphen_und_Graphenalgorithmen#pre_and_post_order|Diskussion weiter oben]]. Die reverse post-order ist gerade die Umkehrung dieser Reihenfolge. Die folgende Implementation verwendet die rekursive Version der Tiefensuche, in der Praxis wird man meist die iterative Version mit Stack bevorzugen, weil bei großen Graphen die Aufruftiefe sehr groß werden kann:&lt;br /&gt;
&lt;br /&gt;
 def reverse_post_order(graph):               # gerichteter Graph&lt;br /&gt;
     result = []                              # enthält später die reverse post-order&lt;br /&gt;
     visited = [False]*len(graph)             # Flags für bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):                         # besuche node&lt;br /&gt;
         if not visited[node]:                # aber nur, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True             # markiere ihn als besucht&lt;br /&gt;
             for neighbor in graph[node]:     # und besuche die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
             result.append(node)              # alle Nachbarn besucht =&amp;gt; Anhängen an result liefert post-order&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):          # besuche alle Knoten&lt;br /&gt;
         visit(node)&lt;br /&gt;
     &lt;br /&gt;
     result.reverse()                         # post-order =&amp;gt; reverse post-order&lt;br /&gt;
     return result&lt;br /&gt;
&lt;br /&gt;
Die Tatsache, dass die reverse post-order tatsächlich eine topologische Sortierung liefert, leuchtet wahrscheinlich nicht unmittelbar ein. Bevor wir diese Tatsache beweisen. wollen wir uns anhand des Ankleidegraphen klar machen, dass die pre-order (die man intuitiv vielleicht eher wählen würde) keine topologische Sortierung ist. Startet man die Tiefensuche beim Knoten &amp;quot;Unterhemd&amp;quot;, werden die Knoten in der Reihenfolge &amp;quot;Unterhemd&amp;quot;, &amp;quot;Oberhemd&amp;quot;, &amp;quot;Schlips&amp;quot;, &amp;quot;Jackett&amp;quot;, &amp;quot;Gürtel&amp;quot; gefunden. Da dann alle von &amp;quot;Unterhemd&amp;quot; erreichbaren Knoten erschöpft sind, startet man die Tiefensuche als nächstes bei &amp;quot;Unterhose&amp;quot; und erreicht von dort aus &amp;quot;Hose&amp;quot; und &amp;quot;Schuhe&amp;quot;. Man erkennt sofort, dass diese Reihenfolge nicht funktioniert: &amp;quot;Hose&amp;quot; kommt nach &amp;quot;Gürtel&amp;quot;, und &amp;quot;Jackett&amp;quot; kommt vor &amp;quot;Gürtel&amp;quot;. Bei dieser Anordnung gibt es Pfeile nach links, die Abhängigkeitsbedingungen sind somit verletzt.&lt;br /&gt;
&lt;br /&gt;
Damit die reverse post-order eine zulässige Sortierung sein kann, muss stets gelten, dass Knoten u vor Knoten v einsortiert wurde, wenn die Kante (u &amp;amp;rarr; v) existiert. Das ist aber äquivalent zur Forderung, dass in der ursprünglichen post-order (vor dem &amp;lt;tt&amp;gt;reverse&amp;lt;/tt&amp;gt;) u hinter v stehen muss. Wir betrachten den &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt;-Aufruf, bei dem u expandiert wird. Gelangt man jetzt zu u's Nachbarn v, gibt es zwei Möglichkeiten: Wenn v bereits expandiert wurde, befindet es sich bereits im Array &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt; kehrt sofort zurück. Andernfalls wird v ebenfalls expandiert und demzufolge in &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; eingetragen, &amp;lt;i&amp;gt;bevor&amp;lt;/i&amp;gt; der rekursive Aufruf &amp;lt;tt&amp;gt;visit(v)&amp;lt;/tt&amp;gt; zurückkehrt. Knoten u wird aber erst in &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; eingefügt, &amp;lt;i&amp;gt;nachdem&amp;lt;/i&amp;gt; alle rekursiven &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt;-Aufrufe seiner Nachbarn zurückgekehrt sind. In beiden Fällen steht u in der post-order wie gefordert hinter v, und daraus folgt die Behauptung.&lt;br /&gt;
&lt;br /&gt;
Der obige Algorithmus liefert natürlich nur dann eine topologische Sortierung, wenn der Graph wirklich azyklisch ist (man kann ihn aber auch anwenden, um die reverse post-order für einen zyklischen Graphen zu bestimmen, siehe Abschnitt &amp;quot;[[Graphen_und_Graphenalgorithmen#Transitive Hülle und stark zusammenhängende Komponenten|Stark zusammenhängende Komponenten]]&amp;quot;). Dieser Fall tritt in der Praxis häufig auf, weil zyklische Graphen bei vielen Anwendungen gar nicht erst entstehen können. Weiß man allerdings nicht, ob der Graph azyklisch ist oder nicht, muss man einen zusätzlichen Test auf Zyklen in den Algorithmus einbauen. &lt;br /&gt;
&lt;br /&gt;
Zyklische Graphen sind dadurch gekennzeichnet, dass es im obigen Beweis eine dritte Möglichkeit gibt: Während der Expansion von u wird rekursiv v expandiert, und es gibt eine Rückwärtskante (v &amp;amp;rarr; u). (Es spielt dabei keine Rolle, ob v von u aus direkt oder indirekt erreicht wurde.) Ein Zyklus wird also entdeckt, wenn die Tiefensuche zu u zurückkehrt, solange u noch &amp;lt;i&amp;gt;aktiv&amp;lt;/i&amp;gt; ist, d.h. wenn die Rekursion von u aus gestartet und noch nicht beendet wurde. Dies kann man leicht feststellen, wenn man in der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; drei Werte zulässt: 0 für &amp;quot;noch nicht besucht&amp;quot;, 1 für &amp;quot;aktiv&amp;quot; und 2 für &amp;quot;beendet&amp;quot;. Wir signalisieren einen Zyklus, sobald &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt; für einen Knoten aufgerufen wird, der gerade aktiv ist:&lt;br /&gt;
&lt;br /&gt;
 def topological_sort_DFS(graph):             # gerichteter Graph&lt;br /&gt;
     result = []                              # enthält später die topologische Sortierung&lt;br /&gt;
     &lt;br /&gt;
     not_visited, active, finished = 0, 1, 2  # drei Zustände für visited&lt;br /&gt;
     visited = [not_visited]*len(graph)       # Flags für aktive und bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):                         # besuche node (gibt &amp;quot;True&amp;quot; zurück, wenn Zyklus gefunden wurde)&lt;br /&gt;
         if not visited[node]:                # ... aber nur, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = active           # markiere ihn als aktiv&lt;br /&gt;
             for neighbor in graph[node]:     # und besuche die Nachbarn&lt;br /&gt;
                 if visit(neighbor):          # wenn rekursiv ein Zyklus gefunden wurde&lt;br /&gt;
                     return True              # ... brechen wir ab und signalisieren den Zyklus&lt;br /&gt;
             visited[node] = finished         # Rekursion beendet, node ist nicht mehr aktiv&lt;br /&gt;
             result.append(node)              # alle Nachbarn besucht =&amp;gt; Anhängen an result liefert post-order&lt;br /&gt;
             return False                     # kein Zyklus gefunden&lt;br /&gt;
         elif visited[node] == active:        # Rekursion erreicht einen noch aktiven Knoten&lt;br /&gt;
             return True                      #  =&amp;gt; Zyklus gefunden&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):          # besuche alle Knoten&lt;br /&gt;
         if visit(node):                      # wenn Zyklus gefunden wurde&lt;br /&gt;
             return None                      # ... gibt es keine topologische Sortierung&lt;br /&gt;
     &lt;br /&gt;
     result.reverse()                         # post-order =&amp;gt; reverse post-order (=topologische Sortierung)&lt;br /&gt;
     return result&lt;br /&gt;
&lt;br /&gt;
Man macht sich leicht klar, dass kein Zyklus vorliegt, wenn die Rekursion einen Knoten erreicht, der bereits auf &amp;lt;tt&amp;gt;finished&amp;lt;/tt&amp;gt; gesetzt ist. Nehmen wir an, dass u gerade expandiert wird, und sein Nachbar v ist bereits &amp;lt;tt&amp;gt;finished&amp;lt;/tt&amp;gt;. Wenn es einen Zyklus gäbe, müsste es einen Weg von v nach u geben. Dann wäre u aber bereits während der Expansion von v gefunden worden. Da v nicht mehr im Zustand &amp;lt;tt&amp;gt;active&amp;lt;/tt&amp;gt; ist, muss die Expansion von v schon abgeschlossen gewesen sein, ohne dass u gefunden wurde. Folglich kann es keinen solchen Zyklus geben.&lt;br /&gt;
&lt;br /&gt;
=== Transitive Hülle und stark zusammenhängende Komponenten ===&lt;br /&gt;
&lt;br /&gt;
Auch bei gerichteten Graphen ist die Frage, welche Knoten miteinander zusammenhängen, von großem Interesse. Wir betrachten dazu wieder die Relation &amp;quot;Knoten v ist von Knoten u aus erreichbar&amp;quot;, die anzeigt, ob es einen Weg von u nach v gibt oder nicht. In ungerichteten Graphen ist diese Relation immer symmetrisch, weil jeder Weg in beiden Richtungen benutzt werden kann. In gerichteten Graphen gilt dies nicht. Man muss hier zwei Arten von Zusammenhangskomponenten unterscheiden:&lt;br /&gt;
;Transitive Hülle: Die transitive Hülle eines Knotens u ist die Menge aller Knoten, die von u aus erreichbar sind:&lt;br /&gt;
:&amp;lt;math&amp;gt;T(u) = \{v\ |\ u \rightsquigarrow v\}&amp;lt;/math&amp;gt;&lt;br /&gt;
;Stark zusammenhängende Komponenten: Die stark zusammenhängende Komponenten &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; eines gerichteten Graphen sind maximale Teilgraphen, so dass alle Knoten innerhalb einer Komponente von jedem anderen Knoten der selben Komponente aus erreichbar sind&lt;br /&gt;
:&amp;lt;math&amp;gt;u,v \in C_i\ \ \Leftrightarrow\ \ u \rightsquigarrow v \wedge v \rightsquigarrow u&amp;lt;/math&amp;gt;&lt;br /&gt;
Die erste Definition betrachtet den Zusammenhang asymmetrisch, ohne Beachtung der Frage, ob es auch einen Rückweg von Knoten v nach u gibt, die zweite hingegen symmetrisch.&lt;br /&gt;
&lt;br /&gt;
Die &amp;lt;b&amp;gt;transitive Hülle&amp;lt;/b&amp;gt; benötigt man, wenn man Fragen der Erreichbarkeit besonders effizient beantworten will. Wir hatten bespielsweise oben erwähnt, dass das Python-Modul &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; direkt und indirekt von mehreren anderen Module abhängt, die vorher installiert werden müssen, damit &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; funktioniert. Bittet man den Systemadministrator, das &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt;-Paket zu installieren, will er diese Abhängigkeiten wahrscheinlich nicht erst mühsam rekursiv heraussuchen, sonder er verlangt eine Liste aller Pakete, die installiert werden müssen. Dies ist gerade die transitive Hülle von &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; im Abhängigkeitsgraphen. Damit man diese nicht manuell bestimmen muss, verwendet man Installationsprogrammen wie z.B. [http://pypi.python.org/pypi/pip/ pip], die die Abhängigkeiten automatisch herausfinden und installieren. &lt;br /&gt;
&lt;br /&gt;
Bei der Bestimmung der transitiven Hülle modifiziert man den gegebenen Graphen, indem man jedesmal eine neue Kante (u &amp;amp;rarr; v) einfügt, wenn diese Kante noch nicht existiert, aber v von u aus erreichbar ist. Dies gelingt mit einer sehr einfachen Variation der Tiefensuche: Wir rufen &amp;lt;tt&amp;gt;visit(k)&amp;lt;/tt&amp;gt; für jeden Knoten k auf, aber setzen die property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; zuvor auf &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt; zurück. Alle Knoten, die während der Rekursion erreicht werden, sind im modifizierten Graphen Nachbarn von k. Ein etwas effizienterer Ansatz ist der [http://de.wikipedia.org/wiki/Algorithmus_von_Floyd_und_Warshall Algorithmus von Floyd und Warshall].&lt;br /&gt;
&lt;br /&gt;
Die Bestimmung der &amp;lt;b&amp;gt;stark zusammenhängenden Komponenten&amp;lt;/b&amp;gt; ist etwas schwieriger. Es existieren eine ganze Reihe von effizienten Algorithmen (siehe [http://en.wikipedia.org/wiki/Strongly_connected_component WikiPedia]), deren einfachster der Algorithmus von Kosaraju ist:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;gegeben:&amp;lt;/b&amp;gt; gerichteter Graph&lt;br /&gt;
&lt;br /&gt;
# Bestimme die reverse post-order (mit der Funktion &amp;lt;tt&amp;gt;reverse_post_order&amp;lt;/tt&amp;gt;)&lt;br /&gt;
# Bilde den transponierten Graphen &amp;lt;math&amp;gt;G^T&amp;lt;/math&amp;gt; (mit der Funktion &amp;lt;tt&amp;gt;transposeGraph&amp;lt;/tt&amp;gt;)&lt;br /&gt;
# Bestimme die Zusammenhangskomponenten von &amp;lt;math&amp;gt;G^T&amp;lt;/math&amp;gt; mittels Tiefensuche, aber betrachte die Knoten dabei in der reverse post-order aus Schritt 1 (dies kann mit einer minimalen Modifikation der Funktion &amp;lt;tt&amp;gt;connectedComponents&amp;lt;/tt&amp;gt; geschehen, indem man die Zeile &amp;lt;tt&amp;gt;for node in xrange(len(graph)):&amp;lt;/tt&amp;gt; einfach nach &amp;lt;tt&amp;gt;for node in ordered:&amp;lt;/tt&amp;gt; abändert, wobei &amp;lt;tt&amp;gt;ordered&amp;lt;/tt&amp;gt; das Ergebnis der Funktion &amp;lt;tt&amp;gt;reverse_post_order&amp;lt;/tt&amp;gt; ist, also ein Array, das die Knoten in der gewünschten Reihenfolge enthält).&lt;br /&gt;
Die Zusammenhangskomponenten, die man in Schritt 3 findet, sind gerade die stark zusammenhängenden Komponenten des Originalgraphen G. Die folgende Skizze zeigt diese in grün für den schwarz gezeichneten gerichteten Graphen. &lt;br /&gt;
&lt;br /&gt;
[[Image:strongly-connected-components.png|400px]]    &lt;br /&gt;
&lt;br /&gt;
Zum Beweis der Korrektheit des Algorithmus von Kosaraju zeigen wir zwei Implikationen: 1. Wenn die Knoten u und v in der selben stark zusammenhängenden Komponente liegen, werden sie in Schritt 3 des Algorithmus auch der selben Komponente zugewiesen. 2. Wenn die Knoten u und v in Schritt 3 der selben Komponente zugewiesen wurden, müssen sie auch in der selben stark zusammenhängenden Komponente liegen. &lt;br /&gt;
# Knoten u und v gehören zur selben stark zusammenhängenden Komponente von G. Per Definition gilt, dass u von v aus erreichbar ist und umgekehrt. Dies muss auch im transponierten Graphen G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; gelten (der Weg &amp;lt;math&amp;gt;u \rightsquigarrow v&amp;lt;/math&amp;gt; wird jetzt zum Weg &amp;lt;math&amp;gt;v \rightsquigarrow u&amp;lt;/math&amp;gt; und umgekehrt). Wird u bei der Tiefensuche in Schritt 3 vor v expandiert, ist v von u aus erreichbar und gehört somit zur selben Komponente. Das umgekehrte gilt, wenn v vor u expandiert wird. Daraus folgt die Behauptung 1.&lt;br /&gt;
# Knoten u und v werden in Schritt 3 der selben Komponente zugewiesen: Sei x der Anker dieser Komponente. Da u in der gleichen Komponente wie x liegt, muss es in G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; einen Weg &amp;lt;math&amp;gt;x \rightsquigarrow u&amp;lt;/math&amp;gt;, und demnach in G einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow x&amp;lt;/math&amp;gt; geben. Da x der Anker seiner Komponente ist, wissen wir aber auch, dass x in der reverse post-order &amp;lt;i&amp;gt;vor&amp;lt;/i&amp;gt; u liegt (denn der Anker ist der Knoten, mit dem eine neue Komponente gestartet wird; er muss deshalb im Array &amp;lt;tt&amp;gt;ordered&amp;lt;/tt&amp;gt; als erster Konten seiner Komponente gefunden worden sein). Wir unterscheiden jetzt im Schritt 1 des Algorithmus zwei Fälle:&lt;br /&gt;
## u wurde bei der Bestimmung der post-order vor x expandiert. Dann kann x nur dann in der reverse post-order &amp;lt;i&amp;gt;vor&amp;lt;/i&amp;gt; u liegen (oder, einfacher ausgedrückt, x kann nur dann in der post-order &amp;lt;i&amp;gt;hinter&amp;lt;/i&amp;gt; u liegen), wenn x im Graphen G nicht von u aus erreichbar war. Das ist aber unmöglich, weil wir ja schon wissen, dass es in G einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow x&amp;lt;/math&amp;gt; gibt.&lt;br /&gt;
## Folglich wurde u bei der Bestimmung der post-order nach x expandiert. Da x in der post-order hinter u liegt, muss u während der Expansion von x erreicht worden sein. Deshalb muss es in G auch einen Weg &amp;lt;math&amp;gt;x \rightsquigarrow u&amp;lt;/math&amp;gt; geben.&lt;br /&gt;
#:Somit sind x und u in der selben stark zusammenhängenden Komponente. Die gleiche Überlegung gilt für x und v. Wegen der Transitivität der relation &amp;quot;ist erreichbar&amp;quot; folgt daraus, dass auch u und v in der selben Komponente liegen, also die Behauptung 2.&lt;br /&gt;
&lt;br /&gt;
Die folgende Skizze illustriert, dass der Komponentengraph stets azyklisch ist. Den Komponentengraph erhält man, indem man für jede Komponente &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; einen Knoten erzeugt (grün), und die Knoten i und j durch eine gerichtete Kante verbindet (rot), wenn es im Originalgraphen eine Kante (u &amp;amp;rarr; v) mit &amp;lt;math&amp;gt;u \in C_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v \in C_j&amp;lt;/math&amp;gt; gibt. Es ist dann garantiert, dass es keine Kante in umgekehrter Richtung geben kann. Daraus folgt insbesondere, dass ein DAG nur triviale stark verbundene Komponenten haben kann, die aus einzelnen Knoten bestehen.&lt;br /&gt;
&lt;br /&gt;
[[Image:strongly-connected-components-graph.png|400px]]&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Das Erfüllbarkeitsproblem in Implikationengraphen ===&lt;br /&gt;
&lt;br /&gt;
Das Erfüllbarkeitsproblem hat auf den ersten Blick nichts mit Graphen zu tun, denn es geht um Wahrheitswerte logischer Ausdrücke. Man kann logische Ausdrücke jedoch unter bestimmten Bedingungen in eine Graphendarstellung überführen und somit das ursprüngliche Problem auf ein Problem der Graphentheorie reduzieren, für das bereits ein Lösungsverfahren bekannt ist. In diesem Abschnitt wollen wir dies für die sogenannten Implikationengraphen zeigen, ein weiteres Beispiel findet sich im Kapitel [[NP-Vollständigkeit]].&lt;br /&gt;
&lt;br /&gt;
==== Das Erfüllbarkeitsproblem ====&lt;br /&gt;
&lt;br /&gt;
(vgl. [http://de.wikipedia.org/wiki/Erfüllbarkeitsproblem_der_Aussagenlogik WikiPedia (de)])&lt;br /&gt;
&lt;br /&gt;
Das Erfüllbarkeitsproblem (SAT-Problem, von &amp;lt;i&amp;gt;satisfiability&amp;lt;/i&amp;gt;) befasst sich mit logischen (oder Booleschen) Funktionen: Gegeben sei eine Menge &amp;lt;math&amp;gt;\{x_1, ... ,x_n\}&amp;lt;/math&amp;gt; Boolscher Variablen (d.h., die &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; können nur die Werte True oder False annehmen), sowie eine logische Formel, in der die Variablen mit den üblichen logischen Operatoren &lt;br /&gt;
:&amp;lt;math&amp;gt;\neg\quad&amp;lt;/math&amp;gt;: Negation (&amp;quot;nicht&amp;quot;, in Python: &amp;lt;tt&amp;gt;not&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\vee\quad&amp;lt;/math&amp;gt;: Disjunktion (&amp;quot;oder&amp;quot;, in Python: &amp;lt;tt&amp;gt;or&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\wedge\quad&amp;lt;/math&amp;gt;: Konjuktion (&amp;quot;und&amp;quot;, in Python: &amp;lt;tt&amp;gt;and&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\rightarrow\quad&amp;lt;/math&amp;gt;: Implikation (&amp;quot;wenn, dann&amp;quot;, in Python nicht als Operator definiert)&lt;br /&gt;
:&amp;lt;math&amp;gt;\leftrightarrow\quad&amp;lt;/math&amp;gt;: Äquivalenz (&amp;quot;genau dann, wenn&amp;quot;, in Python: &amp;lt;tt&amp;gt;==&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\neq\quad&amp;lt;/math&amp;gt;: exklusive Disjunktion (&amp;quot;entweder oder&amp;quot;, in Python: &amp;lt;tt&amp;gt;!=&amp;lt;/tt&amp;gt;)&lt;br /&gt;
verknüpft sind. Klammern definieren die Reihenfolge der Auswertung der Operationen. Für jede Belegung der Variablen &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; mit True oder False liefert die Formel den Wert der Funktion, der natürlich auch nur True oder False sein kann. Wenn Formel und Belegung gegeben sind, ist die Auswertung der Funktion ein sehr einfaches Problem: Man transformiert die Formel in einen Parse-Baum (siehe Übungsaufgabe &amp;quot;Taschenrechner) und wertet jeden Knoten mit Hilfe der üblichen Wertetabellen für logische Operatoren aus, die wir hier zur Erinnerung noch einmal angeben:&lt;br /&gt;
{| cellspacing=&amp;quot;0&amp;quot; border=&amp;quot;1&amp;quot;&lt;br /&gt;
|- style=&amp;quot;text-align:center;background-color:#ffffcc;width:50px&amp;quot;&lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \vee b &amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \wedge b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \rightarrow b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;b \rightarrow a&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \leftrightarrow b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \neq b&amp;lt;/math&amp;gt; &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 0 || 1 || 1 || 1 || 0&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 0 || 1 || 0 || 0 || 1&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 0 || 1 || 0 || 1&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 || 0&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beim Erfüllbarkeitsproblem wird die Frage umgekehrt gestellt: &lt;br /&gt;
:Gegeben sei eine logische Funktion. Ist es möglich, dass die Funktion jemals den Wert True annimmt? &lt;br /&gt;
Das heisst, kann man die Variablen &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; so mit True oder False belegen, dass die Formel am Ende wahr ist? Im Prinzip kann man diese Frage durch erschöpfende Suche leicht beantworten, indem man die Funktion für alle &amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt; möglichen Belegungen einfach ausrechnet, aber das dauert für große n (ab ca. &amp;lt;math&amp;gt;n\ge 40&amp;lt;/math&amp;gt;) viel zu lange. Erstaunlicherweise ist es aber noch niemanden gelungen, einen Algorithmus zu finden, der für beliebige logische Funktionen schneller funktioniert. Im Gegenteil wurde gezeigt, dass das Erfüllbarkeitsproblem [[NP-Vollständigkeit|NP-vollständig]] ist, so dass wahrscheinlich kein solcher Algorithmus existiert. Trotz (oder gerade wegen) seiner Schwierigkeit hat das Erfüllbarkeitsproblem viele Anwendungen gefunden, vor allem beim Testen logischer Schaltkreise (&amp;quot;Gibt es eine Belegung der Eingänge, so dass am Ausgang der verbotene Wert X entsteht?&amp;quot;) und bei der Planerstellung in der künstlichen Intelligenz (&amp;quot;Kann man ausschließen, dass der generierte Plan Konflikte enthält?&amp;quot;). Es ist außerdem ein beliebtes Modellproblem für die Erforschung neuer Ideen und Algorithmen für schwierige Probleme.&lt;br /&gt;
&lt;br /&gt;
==== Normalformen für logische Ausdrücke ====&lt;br /&gt;
&lt;br /&gt;
Um die Beschreibung von Erfüllbarkeitsproblemen zu vereinfachen und zu vereinheitlichen, hat man verschiedene &amp;lt;i&amp;gt;Normalformen&amp;lt;/i&amp;gt; für logische Ausdrücke eingeführt. Die wichtigste ist die &amp;lt;i&amp;gt;Konjuktionen-Normalform&amp;lt;/i&amp;gt; (CNF - conjunctive normal form). Ein Ausdruck in &amp;lt;i&amp;gt;Konjuktionen-Normalform&amp;lt;/i&amp;gt; ist eine UND-Verknüpfung von M &amp;lt;i&amp;gt;Klauseln&amp;lt;/i&amp;gt;:&lt;br /&gt;
 (CLAUSE&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;) &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; (CLAUSE&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;) &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; ...  &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; (CLAUSE&amp;lt;sub&amp;gt;M&amp;lt;/sub&amp;gt;)&lt;br /&gt;
Jede Klausel ist wiederum ein logischer Ausdruck, der aber sehr einfach sein muss: Er darf nur noch k Variablen enthalten, die nur mit den Operatoren NICHT und ODER verknüpft werden dürfen, z.B.&lt;br /&gt;
  CLAUSE&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; := &amp;lt;math&amp;gt;x_1 \vee \neg x_3 \vee x_8&amp;lt;/math&amp;gt;&lt;br /&gt;
Je nachdem, wie viele Variablen pro Klausel erlaubt sind, spricht man von &amp;lt;b&amp;gt;k-CNF&amp;lt;/b&amp;gt; und entsprechend von einem &amp;lt;b&amp;gt;k-SAT&amp;lt;/b&amp;gt; Problem. Es ist außerdem üblich, die Menge der Variablen und die Menge der negierten Variablen zusammen als Menge der &amp;lt;i&amp;gt;Literale&amp;lt;/i&amp;gt; zu bezeichnen:&lt;br /&gt;
  LITERALS := &amp;lt;math&amp;gt;\{x_1,...,x_n\} \cup \{\neg x_1,...,\neg x_n\}&amp;lt;/math&amp;gt;&lt;br /&gt;
Formal definiert man die &amp;lt;b&amp;gt;k-Konjunktionen-Normalform (k-CNF)&amp;lt;/b&amp;gt; am besten durch eine Grammatik in [http://de.wikipedia.org/wiki/Backus-Naur-Form Backus-Naur-Form]:&lt;br /&gt;
    k_CNF    ::=  CLAUSE | k_CNF &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; CLAUSE&lt;br /&gt;
    CLAUSE   ::= (LITERAL &amp;lt;math&amp;gt;\vee&amp;lt;/math&amp;gt; ... &amp;lt;math&amp;gt;\vee&amp;lt;/math&amp;gt; LITERAL)  # genau k Literale pro Klausel&lt;br /&gt;
    LITERAL  ::=  VARIABLE | &amp;lt;math&amp;gt;\neg&amp;lt;/math&amp;gt;VARIABLE&lt;br /&gt;
    VARIABLE ::=  &amp;lt;math&amp;gt;x_1&amp;lt;/math&amp;gt; | ... | &amp;lt;math&amp;gt;x_n&amp;lt;/math&amp;gt;&lt;br /&gt;
Beispiele:&lt;br /&gt;
* 3-CNF: &amp;lt;math&amp;gt;(x_1 \vee \neg x_2 \vee x_4) \wedge (x_2 \vee x_3 \vee \neg x_4) \wedge (\neg x_1 \vee x_4 \vee \neg x_5)&amp;lt;/math&amp;gt;&lt;br /&gt;
* 2-CNF: &amp;lt;math&amp;gt;(x_1 \vee \neg x_2) \wedge (x_3 \vee x_4)&amp;lt;/math&amp;gt; ...&lt;br /&gt;
&amp;lt;b&amp;gt;Gesucht&amp;lt;/b&amp;gt; ist eine Belegung der Variablen mit True und False, so dass der Ausdruck den Wert True hat. Aus den Eigenschaften der UND- und ODER-Verknüpfungen folgt, dass ein Ausdruck in k-CNF genau dann True ist, wenn jede einzelne Klausel True ist. In jeder Klausel wiederum hat man k Chancen, die Klausel True zu machen, indem man eins der Literale zu True macht. Eventuell werden dadurch aber andere Klauseln wieder zu False, was die Aufgabe so schwierig macht. Die Bedeutung der k-CNF ergibt sich aus folgendem&lt;br /&gt;
;Satz: Jeder logische Ausdruck kann effizient nach 3-CNF transformiert werden, jedoch im allgemeinen nicht nach 2-CNF.&lt;br /&gt;
Man kann sich also auf Algorithmen für 3-SAT-Probleme konzentrieren, ohne dabei an Ausdrucksmächtigkeit zu verlieren. &lt;br /&gt;
&lt;br /&gt;
Leider gilt der entsprechende Satz nicht für k=2: Ausdrücke in 2-CNF sind weit weniger mächtig, weil man in jeder Klausel nur noch zwei Wahlmöglichkeiten hat. Bestimmte logische Ausdrücke sind aber auch nach 2-CNF transformierbar, beispielsweise die Bedingung, dass zwei Literale u und v immer den entgegegesetzten Wert haben müssen. Dies ergibt ein Paar von ODER-Verknüpfungen:&lt;br /&gt;
:&amp;lt;math&amp;gt;(u \leftrightarrow \neg v) \equiv (u \vee \neg v) \wedge (\neg u \vee v)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die 2-CNF hat den Vorteil, dass es effiziente Algorithmen für das 2-SAT-Problem gibt, die wir jetzt kennenlernen wollen. Es zeigt sich, dass man Ausdrücke in 2-CNF als Graphen repräsentieren kann, indem man sie zunächst in die &amp;lt;i&amp;gt;Implikationen-Normalform&amp;lt;/i&amp;gt; (INF für &amp;lt;i&amp;gt;implicative normal form&amp;lt;/i&amp;gt;) überführt. Die Implikationen-Normalform besteht ebenfalls aus einer Menge von Klauseln, die durch UND-Operationen verknüpft sind, aber jede Klausel ist jetzt eine Implikation. &lt;br /&gt;
Die Grammatik der &amp;lt;b&amp;gt;Implikationen-Normalform (INF)&amp;lt;/b&amp;gt; lautet:&lt;br /&gt;
    INF      ::=  CLAUSE | INF &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; CLAUSE&lt;br /&gt;
    CLAUSE   ::= (LITERAL &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; LITERAL)  # genau 2 Literale pro Implikation&lt;br /&gt;
    LITERAL  ::=  VARIABLE | &amp;lt;math&amp;gt;\neg&amp;lt;/math&amp;gt;VARIABLE&lt;br /&gt;
    VARIABLE ::=  &amp;lt;math&amp;gt;x_1&amp;lt;/math&amp;gt; | ... | &amp;lt;math&amp;gt;x_n&amp;lt;/math&amp;gt;&lt;br /&gt;
und ein gültiger Ausdruck wäre z.B.&lt;br /&gt;
:&amp;lt;math&amp;gt;(x_1 \to x_2) \wedge (x_2 \to \neg x_3) \wedge (x_4 \to x_3)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die Umwandlung von 2-CNF nach INF beruht auf folgender Äquivalenz, die man sich aus der obigen Wahrheitstabelle leicht herleitet:&lt;br /&gt;
:&amp;lt;math&amp;gt;(x \vee y) \equiv (\neg x \rightarrow y) \equiv (\neg y \rightarrow x)&amp;lt;/math&amp;gt;&lt;br /&gt;
Aus dieser Äquivalenz folgt der &lt;br /&gt;
;Satz: Ein Ausdruck in 2-CNF kann nach INF transformiert werden, indem man jede Klausel &amp;lt;math&amp;gt;(x \vee y)&amp;lt;/math&amp;gt; durch das Klauselpaar &amp;lt;math&amp;gt;(\neg x \rightarrow y) \wedge (\neg y \rightarrow x)&amp;lt;/math&amp;gt; ersetzt.&lt;br /&gt;
Man beachte, dass man für jede ODER-Klausel des ursprünglichen Ausdrucks &amp;lt;i&amp;gt;zwei&amp;lt;/i&amp;gt; Implikationen (eine für jede Richtung des &amp;quot;wenn, dann&amp;quot;) einfügen muss, um die Symmetrie des Problems zu erhalten.&lt;br /&gt;
&lt;br /&gt;
==== Lösung des 2-SAT-Problems mit Implikationgraphen ====&lt;br /&gt;
&lt;br /&gt;
Jeder Ausdruck in INF kann als gerichteter Graph dargestellt werden:&lt;br /&gt;
# Für jedes Literal wird ein Knoten in den Graphen eingefügt. Es gibt also für jede Variable und für ihre Negation jeweils einen Knoten, d.h. 2n Knoten insgesamt.&lt;br /&gt;
# Jede Implikation ist eine gerichtete Kante.&lt;br /&gt;
Implikationengraphen eignen sich, um Ursache-Folge-Beziehungen oder Konflikte zwischen Aktionen auszudrücken. Beispielsweise kann man die Klausel &amp;lt;math&amp;gt;(x \rightarrow \neg y)&amp;lt;/math&amp;gt; als &amp;quot;wenn man x tut, darf man y nicht tun&amp;quot; interpretieren. Ein anderes schönes Beispiel findet sich in Übung 12.&lt;br /&gt;
&lt;br /&gt;
Für die Implementation eines Implikationengraphen in Python empfiehlt es sich, die Knoten geschickt zu numerieren: Ist die Variable &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; dem Knoten i zugeordnet, so sollte die negierte Variable &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; dem Knoten (i+n) zugeordnet werden. Zu jedem gegebenen Knoten i findet man dann den negierten Partnerknoten j leicht durch die Formel &amp;lt;tt&amp;gt;j = (i + n ) % (2*n)&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die Aufgabe besteht jetzt darin, folgende Fragen zu beantworten:&lt;br /&gt;
# Ist der durch den Implikationengraphen gegebene Ausdruck erfüllbar?&lt;br /&gt;
# Finde eine geeignete Belegung der Variablen, wenn der Ausduck erfüllbar ist.&lt;br /&gt;
Die erste Frage beantwortet man leicht, indem man die stark zusammenhängenden Komponenten des Implikationengraphen bildet. Dann gilt folgender&lt;br /&gt;
;Satz: Seien u und v zwei Literale, die sich in der selben stark zusammenhängenden Komponente befinden. Dann müssen u und v stets den selben Wert haben, damit der Ausdruck erfüllt sein kann.&lt;br /&gt;
Die Korrektheit des Satzes folgt aus der Definition der stark zusammenhängenden Komponenten: Da u und v in der selben Komponente liegen, gibt es im Implikationengraphen einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow v&amp;lt;/math&amp;gt; sowie einen Weg &amp;lt;math&amp;gt;v \rightsquigarrow u&amp;lt;/math&amp;gt;. Wegen der Transitivität der &amp;quot;wenn, dann&amp;quot; Relation kann man die Wege zu zwei Implikationen verkürzen, die gleichzeitig gelten müssen: &amp;lt;math&amp;gt;(u \rightarrow v) \wedge (v \rightarrow u)&amp;lt;/math&amp;gt; (die Verkürzung von Wegen zu direkten Kanten entspricht gerade der Bildung der transitiven Hülle für die Knoten u und v). In der obigen Wertetabelle für logische Operatoren erkennt mann, dass dies äquivalent zur Bedingung &amp;lt;math&amp;gt;(u \leftrightarrow v)&amp;lt;/math&amp;gt; ist. Dies ist aber gerade die Behauptung des Satzes.&lt;br /&gt;
&lt;br /&gt;
Die Erfüllbarkeit des Ausdrucks ist nun ein einfacher Spezialfall dieses Satzes. &lt;br /&gt;
;Korrolar: Der gegebene Ausdruck ist genau dann erfüllbar, wenn die Literale &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; sich für kein i in derselben stark zusammenhängenden Komponente befinden.&lt;br /&gt;
Setzt man nämlich im Satz &amp;lt;math&amp;gt;u = x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v = \neg x_i&amp;lt;/math&amp;gt;, und beide Knoten befinden sich in der selben Komponente, dann müsste gelten &amp;lt;math&amp;gt;x_i \leftrightarrow\neg x_i&amp;lt;/math&amp;gt;, was offensichtlich ein Widerspruch ist. Damit kann der Ausdruck nicht erfüllbar sein. Umgekehrt gilt, dass der Ausdruck immer erfüllbar ist, wenn &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; stets in verschiedenen Komponenten liegen, weil der folgende Algorithmus von Aspvall, Plass und Tarjan in diesem Fall stets eine gültige Belegung aller Variablen liefert:&lt;br /&gt;
# Bestimme die stark zusammenhängenden Komponenten und bilde den Komponentengraphen. Ordne die Knoten des Komponentengraphen (also die stark zusammenhängenden Komponenten des Originalgraphen) in topologische Sortierung an.&lt;br /&gt;
# Betrachte die Komponenten in der topologischen Sortierung von hinten nach vorn und weise ihnen einen Wert nach folgenden Regeln zu (zur Erinnerung: alle Literale in der selben Komponente haben den selben Wert):&lt;br /&gt;
#* Wenn die Komponente noch nicht betrachtet wurde, setze ihren Wert auf True, und den Wert der komplementären Komponente (derjenigen, die die negierten Literale enthält) auf False.&lt;br /&gt;
#* Andernfalls, gehe zur nächsten Komponente weiter.&lt;br /&gt;
Der Algorithmus beruht auf der Symmetrie des Implikationengraphen: Weil Kanten immer paarweise &amp;lt;math&amp;gt;(\neg u \rightarrow v) \wedge (\neg v \rightarrow u)&amp;lt;/math&amp;gt; eingefügt werden, ist der Graph &amp;lt;i&amp;gt;schiefsymmetrisch&amp;lt;/i&amp;gt; (skew symmetric): die eine Hälfte das Graphen ist die transponierte Spiegelung der anderen Hälfte. Enthält eine stark zusammenhängende Komponente &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; die Knoten &amp;lt;tt&amp;gt;i1, i2, ...&amp;lt;/tt&amp;gt;, so gibt es stets eine komplementäre Komponente &amp;lt;math&amp;gt;C_j = \neg C_i&amp;lt;/math&amp;gt;, die die komplementären Knoten &amp;lt;tt&amp;gt;j1 = (i1 + n) % (2*n), j2 = (i2 + n) % (2*n), ...&amp;lt;/tt&amp;gt; enthält. Gilt &amp;lt;math&amp;gt;C_i = \neg C_i&amp;lt;/math&amp;gt; für irgendein i, so ist der Ausdruck nicht erfüllbar. Den Beweis für die Korrektheit des Algorithmus findet man im [http://www.math.ucsd.edu/~sbuss/CourseWeb/Math268_2007WS/2SAT.pdf Originalartikel]. Leider funktioniert dies nicht für k-SAT-Probleme mit &amp;lt;math&amp;gt;k &amp;gt; 2&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Will man nur die Erfüllbarkeit prüfen, vereinfacht sich der Algorithmus zu:&lt;br /&gt;
# Bestimme die stark zusammenhängenden Komponenten.&lt;br /&gt;
# Teste für alle &amp;lt;tt&amp;gt;i = 0,...,n-1&amp;lt;/tt&amp;gt;, dass Knoten &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; und Knoten &amp;lt;tt&amp;gt;(i+n)&amp;lt;/tt&amp;gt; in unterschiedlichen Komponenten liegen.&lt;br /&gt;
Ist der Ausdruck erfüllbar, kann man eine gültige Belegung der Variablen jetzt mit dem randomisierten Algorithmus bestimmen, den wir im Kapitel [[Randomisierte Algorithmen]] behandeln.&lt;br /&gt;
&lt;br /&gt;
== Weitere wichtige Graphenalgorithmen ==&lt;br /&gt;
&lt;br /&gt;
Eins der wichtigsten Einsatzgebiete für Graphen ist die Optimierung, also die Suche nach der &amp;lt;i&amp;gt;besten&amp;lt;/i&amp;gt; Lösung für ein gegebenes Problem:&lt;br /&gt;
* Das &amp;lt;i&amp;gt;interval scheduling&amp;lt;/i&amp;gt; befasst sich damit, aus einer gegebenen Menge von Aufträgen die richtigen auszuwählen und sie geschickt auf die zur Verfügung stehenden Ressourcen aufzuteilen. Damit beschäftigen wir uns im Kapitel [[Greedy-Algorithmen und Dynamische Programmierung]].&lt;br /&gt;
* Beim Problem des Handlungsreisenden sucht man nach der kürzesten Rundreise, die alle gegebenen Städte genau einmal besucht. Dieses Problem behandeln wir im Kapitel [[NP-Vollständigkeit]].&lt;br /&gt;
* Viele weitere Anwendungen können wir leider in der Vorlesung nicht mehr behandeln, z.B.&lt;br /&gt;
** Algorithmen für den [http://en.wikipedia.org/wiki/Maximum_flow_problem maximalen Fluss] beantworten die Frage, wie man die Durchflussmenge durch ein Netzwerk (z.B. von Ölpipelines) maximiert.&lt;br /&gt;
** Beim [http://en.wikipedia.org/wiki/Assignment_problem Problem der optimalen Paarung] (&amp;quot;matching problem&amp;quot; oder &amp;quot;assignment problem&amp;quot;) sucht man nach einer Teilmenge der Kanten (also nach einem Teilgraphen), so dass jeder Knoten in diesem Teilgraphen höchstens den Grad 1 hat. Im neuen Graphen gruppieren die Kanten also je zwei Knoten zu einem Paar, und die Paarung soll nach jeweils anwendungsspezifischen Kriterien optimal sein. Dies benötigt man z.B. bei der optimalen Zuordnung von Gruppen, etwas beim Arbeitsamt (Zuordnung Arbeitssuchender - Stellenangebot) und in der Universität (Zuordnung Studenten - Übungsgruppen).&lt;br /&gt;
** In Statistik und maschinellem Lernen haben in den letzten Jahren die [http://en.wikipedia.org/wiki/Graphical_model graphischen Modelle] große Bedeutung erlangt.&lt;br /&gt;
* usw. usf.&lt;br /&gt;
&lt;br /&gt;
[[Randomisierte Algorithmen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5404</id>
		<title>Graphen und Graphenalgorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5404"/>
				<updated>2012-07-25T17:02:47Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Algorithmus 2 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Einführung zu Graphen ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation -- Königsberger Brückenproblem ===&lt;br /&gt;
Leonhard Euler [http://de.wikipedia.org/wiki/Leonhard_Euler] erfand den Graphen-Formalismus 1736, um eine scheinbar banale Frage zu beantworten: Ist es möglich, in Königsberg (siehe Stadtplan von 1809 und die schematische Darstellung) einen Spaziergang zu unternehmen, bei dem jede der 7 Brücken genau einmal überquert wird?&lt;br /&gt;
&lt;br /&gt;
[[Image:Koenigsberg1809.png]]&amp;lt;br&amp;gt;&lt;br /&gt;
[[Image:Koenigsberg.jpg]]&lt;br /&gt;
&lt;br /&gt;
Ein Graph abstrahiert von der Geometrie des Problems und repräsentiert nur die Topologie. Jeder Stadtteil von Königsberg ist ein Knoten des Graphen, jede Brücke eine Kante. Der zum Brückenproblem gehörende Graph sieht also so aus:&lt;br /&gt;
&lt;br /&gt;
     O&lt;br /&gt;
    /| \&lt;br /&gt;
    \|  \&lt;br /&gt;
     O---O&lt;br /&gt;
    /|  /&lt;br /&gt;
    \| /&lt;br /&gt;
     O&lt;br /&gt;
&lt;br /&gt;
Der gesuchte Spaziergang würde existieren, wenn es maximal 2 Knoten gäbe, an denen sich eine ungerade Zahl von Kanten trifft. Die Frage muss für Königsberg also verneint werden, denn hier gibt es vier solche Knoten. Ein leicht modifiziertes Problem ist allerdings lösbar: Im obigen Stadtplan erkennt man eine Fähre, die die Stadtteile Kneiphof und Altstadt verbindet. Bezieht man dieselbe in den Spaziergang ein, ergibt sich folgender Graph, bei dem nur noch zwei Knoten mit ungerader Kantenzahl existieren:&lt;br /&gt;
&lt;br /&gt;
   --O&lt;br /&gt;
  / /| \&lt;br /&gt;
  \ \|  \&lt;br /&gt;
   --O---O&lt;br /&gt;
    /|  /&lt;br /&gt;
    \| /&lt;br /&gt;
     O&lt;br /&gt;
&lt;br /&gt;
Inzwischen haben Graphen eine riesige Zahl weiterer Anwendungen gefunden. Einige Beispiele:&lt;br /&gt;
&lt;br /&gt;
* Landkarten:&lt;br /&gt;
** Knoten: Länder&lt;br /&gt;
** Kanten: gemeinsame Grenzen&lt;br /&gt;
&lt;br /&gt;
* Logische Schaltkreise:&lt;br /&gt;
** Knoten: Gatter&lt;br /&gt;
** Kanten: Verbindungen&lt;br /&gt;
&lt;br /&gt;
* Chemie (Summenformeln):&lt;br /&gt;
** Knoten: chemische Elemente&lt;br /&gt;
** Kanten: Bindungen &lt;br /&gt;
&lt;br /&gt;
* Soziologie (StudiVZ)&lt;br /&gt;
** Soziogramm&lt;br /&gt;
*** Knoten: Personen&lt;br /&gt;
*** Kanten: Freund von ...&lt;br /&gt;
&lt;br /&gt;
=== Definitionen ===&lt;br /&gt;
&lt;br /&gt;
;Ungerichteter Graph: Ein ungerichteter Graph G = ( V, E ) besteht aus&lt;br /&gt;
:* einer endliche Menge V von Knoten (vertices)&lt;br /&gt;
:* einer endlichen Menge &amp;lt;math&amp;gt;E \subset V \times V&amp;lt;/math&amp;gt; von Kanten (edges)&lt;br /&gt;
:Die Paare (u,v) und (v,u) gelten dabei als nur ''eine'' Kante (somit gilt die Symmetriebeziehung: (u,v) ∈ E =&amp;gt; (v,u) ∈ E ). Die Anzahl der Kanten, die sich an einem Knoten treffen, wird als ''Grad'' (engl. ''degree'') dieses Knotens bezeichnet:&lt;br /&gt;
:::degree(v) = |{v' ∈ V | (v,v') ∈ E}|&lt;br /&gt;
:(Die Syntax |{...}| bezeichnet dabei die Mächtigkeit der angegebenen Menge, also die Anzahl der Elemente in der Menge.)&lt;br /&gt;
&lt;br /&gt;
Der Graph des Königsberger Brückenproblems ist ungerichtet. Bezeichnet man die Knoten entsprechend des folgenden Bildes&lt;br /&gt;
    c&lt;br /&gt;
   /| \&lt;br /&gt;
   \|  \&lt;br /&gt;
    b---d &lt;br /&gt;
   /|  /&lt;br /&gt;
   \| /&lt;br /&gt;
    a&lt;br /&gt;
&lt;br /&gt;
gilt für die Knotengrade: &amp;lt;tt&amp;gt;degree(a) == degree(c) == degree(d) == 3&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;degree(b) == 5&amp;lt;/tt&amp;gt;. Genauer muss man bei diesem Graphen von einem ''Multigraphen'' sprechen, weil es zwischen einigen Knotenpaaren (nämlich (a, b) sowie (b, c)) mehrere Kanten (&amp;quot;Mehrfachkanten&amp;quot;) gibt. Wir werden in dieser Vorlesung nicht näher auf Multigraphen eingehen.&lt;br /&gt;
&lt;br /&gt;
;Gerichteter Graph: Ein Graph heißt ''gerichtet'', wenn die Kanten (u,v) und (v,u) unterschieden werden. Die Kante (u,v) ∈ E wird nun als Kante von u nach v (aber nicht umgekehrt) interpretiert. Entsprechend unterscheidet man jetzt den ''eingehenden'' und den ''ausgehenden Grad'' jedes Knotens:&lt;br /&gt;
:*out_degree(v) = |{v' ∈ V | (v,v') ∈ E}|&amp;lt;br/&amp;gt;&lt;br /&gt;
:*in_degree(v)  = |{v' ∈ V| (v',v) ∈ E}|&lt;br /&gt;
&lt;br /&gt;
Das folgende Bild zeigt einen gerichteten Graphen. Hier gilt &amp;lt;tt&amp;gt;out_degree(1) == out_degree(3) == in_degree(2) == in_degree(4) == 2&amp;lt;/tt&amp;gt; und &lt;br /&gt;
&amp;lt;tt&amp;gt;in_degree(1) == in_degree(3) == out_degree(2) == out_degree(4) == 0&amp;lt;/tt&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
[[Image:digraph.png|gerichteter Graph]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Vollständiger Graph: Ein vollständiger Graph ist ein ungerichteter Graph, bei dem jeder Knoten mit allen anderen Knoten verbunden ist.&lt;br /&gt;
:::&amp;lt;math&amp;gt;E = \{ (v,w) |  v \in V, w \in V, v \ne w \}&amp;lt;/math&amp;gt;&lt;br /&gt;
:Ein vollständiger Graph mit |V| Knoten hat &amp;lt;math&amp;gt;|E| = \frac{|V|(|V|-1)}{2}&amp;lt;/math&amp;gt; Kanten.&lt;br /&gt;
&lt;br /&gt;
Die folgenden Abbildungen zeigen die vollständigen Graphen mit einem bis fünf Knoten (auch als K&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; bis K&amp;lt;sub&amp;gt;5&amp;lt;/sub&amp;gt; bezeichnet).&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;0&amp;quot; style=&amp;quot;margin: 1em auto 1em auto&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
| [[Image:k1.png|frame|k1]]&lt;br /&gt;
| [[Image:k2.png|frame|k2]]&lt;br /&gt;
| [[Image:k3.png|frame|k3]]&lt;br /&gt;
|-&lt;br /&gt;
| [[Image:k4.png|frame|k4]]&lt;br /&gt;
| [[Image:k5.png|frame|k5]]&lt;br /&gt;
|&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
''Rätsel''&amp;lt;br/&amp;gt;&lt;br /&gt;
Auf einer Party sind Leute. Alle stoßen miteinander an. Es hat 78 mal &amp;quot;Pling&amp;quot; gemacht.&lt;br /&gt;
Wieviele Leute waren da? Antwort: Jede Person ist ein Knoten des Graphen, jedes Antoßen eine Kante. &lt;br /&gt;
Da alle miteinander angestoßen haben, handelt es sich um einen vollständigen Graphen. Mit&lt;br /&gt;
|V|(|V|-1)/2 = 78 folgt, dass es 13 Personen waren.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Gewichteter Graph: Ein Graph heißt ''gewichtet'', wenn jeder Kante eine reelle Zahl zugeordnet ist. Bei vielen Anwendungen beschränkt man sich auch auf nichtnegative reelle Gewichte. In einem gerichteten Graphen können die Gewichte der Kanten (u,v) und (v,u) unterschiedlich sein.&lt;br /&gt;
&lt;br /&gt;
Die Gewichte kodieren Eigenschaften der Kanten, die für die jeweilige Anwendung interessant sind. Bei der Berechnung des maximalen Flusses in einem Netzwerk sind die Gewichte z.B. die Durchflusskapazitäten jeder Kante, bei der Suche nach kürzesten Weges kodieren Sie den Abstand zwischen den Endknoten der Kante, bei Währungsnetzwerken (jeder Knoten ist eine Währung) geben sie die Wechselkurse an, usw..&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Teilgraphen: Ein Graph G' = (V',E') ist ein Teilgraph eines Graphen G, wenn gilt:&lt;br /&gt;
:* V' &amp;amp;sube; V &lt;br /&gt;
:* E' &amp;amp;sub; E &lt;br /&gt;
:Er heißt ''(auf)spannender Teilgraph'', wenn gilt:&lt;br /&gt;
:* V' = V&lt;br /&gt;
:Er heißt ''induzierter Teilgraph'', wenn gilt:&lt;br /&gt;
:* e = (u,v) ∈ E' &amp;amp;sub; E &amp;amp;hArr; u ∈ V' und v ∈ V'&lt;br /&gt;
:Den von V' induzierten Teilgraphen erhält man also, indem man aus G alle Knoten löscht, die nicht in V' sind, sowie alle Kanten (und nur diese Kanten), die einen der gelöschten Knoten als Endknoten haben.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Wege, Pfade, Zyklen, Kreise, Erreichbarkeit: Sei G = (V,E) ein Graph (ungerichtet oder gerichteter) Graph. Dann gilt folgende rekursive Definition:&lt;br /&gt;
:* Für v ∈ V ist (v) ein Weg der Länge 0 in G&lt;br /&gt;
:* Falls &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1})&amp;lt;/math&amp;gt; ein Weg ist, und eine Kante &amp;lt;math&amp;gt;(v_{n-1}, v_n)\in E&amp;lt;/math&amp;gt; existiert, dann ist auch &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ein Weg, und er hat die Länge n. &lt;br /&gt;
: Ein Weg ist also eine nichtleere Folge von Knoten, so dass aufeinander folgende Knoten stets durch eine Kante verbunden sind. Die Länge des Weges entspricht der Anzahl der Kanten im Weg (= Anzahl der Knoten - 1).&lt;br /&gt;
:* Ein ''Pfad'' &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ist ein Weg, bei dem alle Knoten v&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; verschieden sind.&lt;br /&gt;
:* ''Ein Zyklus'' &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ist ein Weg, der zum Ausgangspunkt zurückkehrt, wenn also v&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; = v&amp;lt;sub&amp;gt;n&amp;lt;/sub&amp;gt; gilt.&lt;br /&gt;
:* Ein ''Kreis'' ist ein Zyklus ohne Überkreuzungen. Das heisst, es gilt v&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; = v&amp;lt;sub&amp;gt;n&amp;lt;/sub&amp;gt; und &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1})&amp;lt;/math&amp;gt; ist ein Pfad.&lt;br /&gt;
:* Ein Knoten w ∈ V ist von einem anderen Knoten v ∈ V aus ''erreichbar'' genau dann, wenn ein Weg (v, ..., w) existiert. Wir schreiben dann &amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt;.&lt;br /&gt;
In einem ungerichteten Graph ist die Erreichbarkeits-Relation stets symmetrisch, das heisst aus &amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; folgt &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. In einem gerichteten Graphen ist dies im allgemeinen nicht der Fall.&lt;br /&gt;
&lt;br /&gt;
Bestimmte Wege haben spezielle Namen&lt;br /&gt;
&lt;br /&gt;
;Eulerweg: Ein Eulerweg ist ein Weg, der alle '''Kanten''' genau einmal enthält.&lt;br /&gt;
&lt;br /&gt;
Die eingangs erwähnte Frage des Königsberger Brückenproblems ist equivalent zu der Frage, ob der dazugehörige Graph einen Eulerweg besitzt (daher der Name). Ein anderes bekanntes Beispiel ist das &amp;quot;Haus vom Nikolaus&amp;quot;: Wenn man diesen Graphen in üblicher Weise in einem Zug zeichnet, erhält man gerade den Eulerweg. &lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O----O&lt;br /&gt;
  | \/ |&lt;br /&gt;
  | /\ |   &amp;quot;Das Haus vom Nikolaus&amp;quot;: Alle ''Kanten'' werden nur ''einmal'' passiert&lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Hamiltonweg: Ein Hamiltonweg ist ein Weg, der alle '''Knoten''' genau einmal enthält. Das &amp;quot;Haus vom Nikolaus&amp;quot; besitzt auch einen Hamiltonweg:&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /   &lt;br /&gt;
  O----O&lt;br /&gt;
     /  &lt;br /&gt;
    /      Alle ''Knoten'' werden nur ''einmal'' passiert&lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Hamiltonkreis: Ein Hamiltonkreis ist ein Kreis, der alle '''Knoten''' genau einmal enthält. Auch ein solches Gebilde ist im Haus von Nilolaus enthalten:&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O    O&lt;br /&gt;
  |    |   v0 = vn&lt;br /&gt;
  |    |   vi != vj   Für Alle i,j   i !=j; i,j &amp;gt;0; i,j &amp;lt; n&lt;br /&gt;
  O----O     &lt;br /&gt;
&lt;br /&gt;
Die folgende Skizze zeigt hingegen einen Zyklus: Der Knoten rechts unten sowie die untere Kante sind zweimal enthalten (die Kante einmal von links nach rechts und einmal von rechts nach links):&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O    O&lt;br /&gt;
    \  |&lt;br /&gt;
     \ |   Zyklus&lt;br /&gt;
  O====O&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Zusammenhang, Zusammenhangskomponenten: Ein ungerichteter Graph G heißt ''zusammenhängend'', wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt;&lt;br /&gt;
:Ein gerichteter Graph G ist zusammenhängend, wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; '''oder''' &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. &lt;br /&gt;
:Er ist ''stark zusammenhängend'', wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; '''und''' &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. &lt;br /&gt;
:Entsprechende Definitionen gelten für Teilgraphen G'. Ein Teilgraph G' heisst ''Zusammenhangskomponente'' von G, wenn er ein ''maximaler'' zusammenhängender Teilgraph ist, d.h. wenn G' zusammenhängend ist, und man keine Knoten und Kanten aus G mehr zu G' hinzufügen kann, so dass G' immer noch zusammenhängend bleibt. Entsprechend definiert man ''starke Zusammenhangskomponenten'' in einem gerichteten Graphen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Planarer Graph, ebener Graph: Ein Graph heißt ''planar'', wenn er so in einer Ebene gezeichnet werden ''kann'', dass sich die Kanten nicht schneiden (außer an den Knoten). Ein Graph heißt ''eben'', wenn er tatsächlich so gezeichnet ''ist'', dass sich die Kanten nicht schneiden. Die Einbettung in die Ebene ist im allgemeinen nicht eindeutig.&lt;br /&gt;
&lt;br /&gt;
'''Beispiele:'''&lt;br /&gt;
&lt;br /&gt;
Der folgende Graph ist planar und eben:&lt;br /&gt;
 &lt;br /&gt;
      O&lt;br /&gt;
     /|\&lt;br /&gt;
    / O \&lt;br /&gt;
   / / \ \&lt;br /&gt;
   O     O&lt;br /&gt;
&lt;br /&gt;
Das &amp;quot;Haus vom Nikolaus&amp;quot; ist ebenfalls planar, wird aber üblicherweise nicht als ebener Graph gezeichnet, weil sich die Diagonalen auf der Wand überkreuzen:&lt;br /&gt;
 &lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O----O&lt;br /&gt;
  | \/ |&lt;br /&gt;
  | /\ |   &lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
Eine ebene Einbettung dieses Graphen wird erreicht, wenn man eine der Diagonalen ausserhalb des Hauses zeichnet. Der Graph (also die Menge der Knoten und Kanten) ändert sich dadurch nicht.&lt;br /&gt;
 &lt;br /&gt;
      O  &lt;br /&gt;
     /  \&lt;br /&gt;
  --O----O&lt;br /&gt;
 /  |  / |&lt;br /&gt;
 |  | /  |   &lt;br /&gt;
 |  O----O      Das &amp;quot;Haus vom Nikolaus&amp;quot; als ebener Graph gezeichnet.&lt;br /&gt;
  \     /&lt;br /&gt;
   -----&lt;br /&gt;
&lt;br /&gt;
Eine alternative Einbettung erhalten wir, wenn wir die andere Diagonale außerhalb des Hauses zeichnen:&lt;br /&gt;
 &lt;br /&gt;
      O  &lt;br /&gt;
     /  \&lt;br /&gt;
    O----O--|&lt;br /&gt;
    | \  |  |&lt;br /&gt;
    |  \ |  | &lt;br /&gt;
    O----O  |     Alternative Einbettung des &amp;quot;Haus vom Nikolaus&amp;quot;.&lt;br /&gt;
    |       |&lt;br /&gt;
    |-------|&lt;br /&gt;
&lt;br /&gt;
Jede Einbettung eines planaren Graphen (also jeder ebene Graph) definiert eine eindeutige Menge von ''Regionen'':&lt;br /&gt;
&lt;br /&gt;
 |----O   @&lt;br /&gt;
 |   /@ \&lt;br /&gt;
 |  O----O&lt;br /&gt;
 |  |@ / |&lt;br /&gt;
 |  | / @|   &lt;br /&gt;
 |  O----O        @ entspricht jeweils einer ''Region''. Auch ausserhalb der Figur ist eine Region (die sogenannte ''unendliche'' Region).&lt;br /&gt;
 |@      |&lt;br /&gt;
 |-------|&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Der vollständige Graph K5 ist kein planarer Graph, da sich zwangsweise Kanten schneiden, wenn man diesen Graphen in der Ebene zeichnet.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
;Dualer Graph: Jeder ebene Graph G = (V, E) hat einen ''dualen Graphen'' D = (V&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;, E&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;), dessen Knoten und Kanten wie folgt definiert sind:&lt;br /&gt;
:* V&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt; enthält einen Knoten für jede Region des Graphen G&lt;br /&gt;
:* Für jede Kante e ∈ E gibt es eine duale Kante e&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt; ∈ E&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;, die die an e angrenzenden Regionen (genauer: die entsprechenden Knoten in D) verbindet.&lt;br /&gt;
&lt;br /&gt;
Die folgende Abbildung zeigt einen Graphen (grau) und seinen dualen Graphen (schwarz). Die Knoten des dualen Graphen sind mit Zahlen gekennzeichnet und entsprechen den Regionen des Originalgraphen. Jeder (grauen) Kante des Originalgraphen entspricht eine (schwarze) Kante des dualen Graphen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
[[Image:dual-graphs.png]]&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für duale Graphen gilt: Wenn der Originalgraph zusammenhängend ist, enthält jede Region des dualen Graphen genau einen Knoten des Originalgraphen. Deshalb ist der duale Graph des dualen Graphen wieder der Originalgraph. Bei nicht-zusammenhängenden Graphen gilt dies nicht (vgl. das Fenster bei obigem Bild). In diesem Fall hat der duale Graph mehrere mögliche Einbettungen in die Ebene (man kann z.B. die rechte Kante zwischen Knoten 2 und 4 auch links vom Fenster einzeichnen), und man erhält nicht notwendigerweise den Originalgraphen, wenn man den dualen Graphen des dualen berechnet.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Baum: Ein ''Baum'' ist ein zusammenhängender, kreisfreier Graph.&lt;br /&gt;
&lt;br /&gt;
Beispiel: Binärer Suchbaum&lt;br /&gt;
&lt;br /&gt;
;Spannbaum: Ein ''Spannbaum'' eines zusammenhängenden Graphen G ist ein zusammenhängender, kreisfreier Teilgraph von G, der alle Knoten von G enthält&lt;br /&gt;
&lt;br /&gt;
Beispiel: Spannbaum für das &amp;quot;Haus des Nikolaus&amp;quot; &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    O   &lt;br /&gt;
   /       &lt;br /&gt;
  O    O&lt;br /&gt;
  |  /  &lt;br /&gt;
  | /   &lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
Der Spannbaum eines Graphen mit |V| Knoten hat stets |V| - 1 Kanten.&lt;br /&gt;
&lt;br /&gt;
;Wald: Ein ''Wald'' ist ein unzusammenhängender, kreisfreier Graph.&lt;br /&gt;
: Jede Zusammenhangskomponente eines Waldes ist ein Baum.&lt;br /&gt;
&lt;br /&gt;
=== Repräsentation von Graphen ===&lt;br /&gt;
&lt;br /&gt;
Sei G = ( V, E ) gegeben und liege V in einer linearen Sortierung vor.&amp;lt;br/&amp;gt; &lt;br /&gt;
:::&amp;lt;math&amp;gt;V = \{ v_1, ...., v_n \}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Adjazenzmatrix: Ein Graph kann durch eine Adjazenzmatrix repräsentiert werden, die soviele Zeilen und Spalten enthält, wie der Graph Knoten hat. Die Elemente der Adjazenzmatrix sind &amp;quot;1&amp;quot;, falls eine Kante zwischen den zugehörigen Knoten existiert:&lt;br /&gt;
:::&amp;lt;math&amp;gt;\mathrm{\bold A} = a_{ij} = &lt;br /&gt;
\begin{cases}&lt;br /&gt;
1 &amp;amp; \mathrm{falls}\quad (v_i, v_j) \in E \\&lt;br /&gt;
0 &amp;amp; \mathrm{sonst}&lt;br /&gt;
\end{cases} &lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
:Die Indizes der Matrix entsprechen also den Indizes der Knoten gemäß der gegebenen Sortierung. Im Falle eines ungerichteten Graphen ist die Adjazenzmatrix stets symmetrisch (d.h. es gilt &amp;lt;math&amp;gt;a_{ij}=a_{ji}&amp;lt;/math&amp;gt;), bei einem gerichteten Graphen ist sie im allgemeinen unsymmetrisch.&lt;br /&gt;
&lt;br /&gt;
Beispiel für einen ungerichteten Graphen:&lt;br /&gt;
&lt;br /&gt;
 v = { a,b,c,d }     b      d&lt;br /&gt;
                     | \  / |&lt;br /&gt;
                     |  \/  |&lt;br /&gt;
                     |  /\  |&lt;br /&gt;
                     | /  \ |&lt;br /&gt;
                     a      c&lt;br /&gt;
 &lt;br /&gt;
       a b c d&lt;br /&gt;
      -----------&lt;br /&gt;
      (0 1 0 1) |a &lt;br /&gt;
  A = (1 0 1 0) |b&lt;br /&gt;
      (0 1 0 1) |c&lt;br /&gt;
      (1 0 1 0) |d&lt;br /&gt;
&lt;br /&gt;
Die Adjazenzmatrixdarstellung eignet sich besonders für dichte Graphen (d.h. wenn die Zahl der Kanten in O(|V|&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt;) ist.&lt;br /&gt;
&lt;br /&gt;
;Adjazenzlisten: In der Adjazenzlistendarstellung wird der Graph als Liste von Knoten repräsentiert, die für jeden Knoten einen Eintrag enthält. Der Eintrag für jeden Knoten ist wiederum eine Liste, die die Nachbarknoten dieses Knotens enthält:&lt;br /&gt;
:* graph = {adjazencyList(v) | v ∈ V}&lt;br /&gt;
:* adjazencyList(v) = {v' ∈ V | (v, v') ∈ E}&lt;br /&gt;
&lt;br /&gt;
In Python implementieren wir Adjazenzlisten zweckmäßig als Array von Arrays:&lt;br /&gt;
&lt;br /&gt;
                   graph = [[...],[...],...,[...]]&lt;br /&gt;
 Adjazenzliste für Knoten =&amp;gt;  0     1         n&lt;br /&gt;
&lt;br /&gt;
Wenn wir bei dem Graphen oben die Knoten wie bei der Adjazenzmatrix indizieren (also &amp;lt;tt&amp;gt;a =&amp;gt; 0&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;b =&amp;gt; 1&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;c =&amp;gt; 2&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;d =&amp;gt; 3&amp;lt;/tt&amp;gt;), erhalten wir die Adjazenzlistendarstellung:&lt;br /&gt;
&lt;br /&gt;
 graph = [[b, d], [a, c],[b, d], [a, c]]&lt;br /&gt;
&lt;br /&gt;
Auf die Nachbarknoten eines durch seinen Index &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; gegebenen Knotens können wir also wie folgt zugreifen:&lt;br /&gt;
&lt;br /&gt;
      for neighbors in graph[node]:&lt;br /&gt;
          ... # do something with neighbor&lt;br /&gt;
&lt;br /&gt;
Die Adjazenzlistendarstellung ist effizienter, wenn der Graph nicht dicht ist, so dass viele Einträge der Adjazenzmatrix Null wären. In der Vorlesung werden wir nur diese Darstellung verwenden.&lt;br /&gt;
&lt;br /&gt;
;&amp;lt;div id=&amp;quot;transposed_graph&amp;quot;&amp;gt;Transponierter Graph&amp;lt;/div&amp;gt;: Den ''transponierten Graphen'' G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; eines gerichteten Graphen G erhält man, wenn man alle Kantenrichtungen umkehrt.&lt;br /&gt;
&lt;br /&gt;
Bei ungerichteten Graphen hat die Transposition offensichtlich keinen Effekt, weil alle Kanten bereits in beiden Richtungen vorhanden sind, so dass G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; = G gilt. Bei gerichteten Graphen ist die Transposition einfach, wenn der Graph als Adjazenzmatrix implementiert ist, weil man einfach die transponierte Adjazenzmatrix verwenden muss (beachte, dass sich die Reihenfolge der Indizes umkehrt):&lt;br /&gt;
:::A&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; = a&amp;lt;sub&amp;gt;ji&amp;lt;/sub&amp;gt;&lt;br /&gt;
Ist der Graph hingegen durch eine Adjazenzliste repräsentiert, muss etwas mehr Aufwand getrieben werden:&lt;br /&gt;
&lt;br /&gt;
 def transposeGraph(graph):&lt;br /&gt;
      gt = [[] for k in graph]   # zunächst leere Adjazenzlisten von G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt;&lt;br /&gt;
      for node in range(len(graph)):&lt;br /&gt;
           for neighbor in graph[node]:&lt;br /&gt;
               gt[neighbor].append(node)  # füge die umgekehrte Kante in G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; ein&lt;br /&gt;
      return gt&lt;br /&gt;
&lt;br /&gt;
== Durchlaufen von Graphen (Graph Traversal) ==&lt;br /&gt;
&lt;br /&gt;
Wir betrachten zunächst ungerichtete Graphen mit V Knoten und E Kanten. Eine grundlegende Aufgabe in diesen Graphen besteht darin, alle Knoten in einer bestimmten Reihenfolge genau einmal zu besuchen. Hierbei darf man sich von einem gegebenen Startknoten aus nur entlang der Kanten des Graphen bewegen. Die beim Traversieren benutzen Kanten bilden einen Baum, dessen Wurzel der Startknoten ist und der den gesamten Graphen aufspannt, falls der Graph zusammenhängend ist. (Beweis: Da jeder Knoten nur einmal besucht wird, gibt es für jeden besuchten Knoten [mit Ausnahme des Startknotens] genau eine eingehende Kante. Ist der Graph zusammenhängend, wird jeder Knoten tatsächlich erreicht und es gibt genau (V-1) Kanten, exakt soviele wie für einen Baum mit V Knoten notwendig sind.) Ist der Graph nicht zusammenhängend, wird jeder zusammenhängende Teilgraph (jede &amp;lt;i&amp;gt;Zusammenhangskomponente&amp;lt;/i&amp;gt;) getrennt traversiert, und man erhält einen sogenannten &amp;lt;i&amp;gt;Wald&amp;lt;/i&amp;gt; mit einem Baum pro Zusammenhangskomponente. Die beiden grundlegenden Traversierungsmethoden &amp;lt;i&amp;gt;Tiefensuche&amp;lt;/i&amp;gt; und &amp;lt;i&amp;gt;Breitensuche&amp;lt;/i&amp;gt; werden im folgenden vorgestellt.&lt;br /&gt;
&lt;br /&gt;
=== Tiefensuche in Graphen (Depth First Search, DFS) ===&lt;br /&gt;
&lt;br /&gt;
Die Idee der Tiefensuche besteht darin, jeden besuchten Knoten sofort über die erste Kante wieder zu verlassen, die zu einem noch nicht besuchten Knoten führt. Man findet dadurch schnell einen möglichst langen Pfad durch den Graphen, und der Traversierungs-Baum wird zunächst in die Tiefe verfolgt, daher der Name des Verfahrens. Hat ein Knoten keine unbesuchten Nachbarknoten mehr, geht man im Baum zurück (sogenanntes &amp;lt;i&amp;gt;back tracking&amp;lt;/i&amp;gt;), bis man einen Knoten findet, der noch eine unbesuchte Nachbarn besitzt, und traversiert diese nach dem gleichen Muster. Gibt es gar keine unbesuchten Knoten mehr, kehrt die Suche zum Startknoten zurück und endet dort.&lt;br /&gt;
&lt;br /&gt;
WDie folgende rekursive Implementation der Tiefensuche erwartet den Graphen in Adjazenzlistendarstellung und beginnt die Suche beim Knoten &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt;. Die Information, ob ein Knoten bereits besucht wurde, wird im Array &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; gespeichert. Ein solches Array, das zusätzliche Informationen über die Knoten des Graphen bereitstellt, wir häufig &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; genannt.&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)  # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):              # rekursive Hilfsfunktion, die den gegebenen Knoten und dessen Nachbarn besucht&lt;br /&gt;
         if not visited[node]:     # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True  # Markiere node als besucht&lt;br /&gt;
             print node            # Ausgabe der Knotennummer - pre-order&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode)&lt;br /&gt;
&lt;br /&gt;
[[Image:Tiefens.jpg]]&lt;br /&gt;
&lt;br /&gt;
Ausgabe für den Graphen in diesem Bild (es handelt sich um einen ungerichteten Graphen, die Pfeile symbolisieren nur die Suchrichtung beim Traversal):&lt;br /&gt;
&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; dfs(graph, 1)&lt;br /&gt;
 1&lt;br /&gt;
 2&lt;br /&gt;
 4&lt;br /&gt;
 3&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
 5&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div id=&amp;quot;pre_and_post_order&amp;quot;&amp;gt;In dieser Version des Algorithmus werden die Knotennummern ausgegeben, bevor die Nachbarknoten besucht werden. Man bezeichnet die resultierende Sortierung der Knoten als &amp;lt;b&amp;gt;pre-order&amp;lt;/b&amp;gt; oder als &amp;lt;b&amp;gt;discovery order&amp;lt;/b&amp;gt;. Alternativ kann man die Knotennummern erst ausgeben, nachdem alle Nachbarn besucht wurden, also auf dem Rückweg der Rekursion. In diesem Fall spricht man von &amp;lt;b&amp;gt;post-order&amp;lt;/b&amp;gt; oder &amp;lt;b&amp;gt;finishing order&amp;lt;/b&amp;gt;:&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)  # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):              # rekursive Hilfsfunktion, die den gegebenen Knoten und dessen Nachbarn besucht&lt;br /&gt;
         if not visited[node]:     # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True  # Markiere node als besucht&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
             &amp;lt;font color=red&amp;gt;print node            # Ausgabe der Knotennummer - post-order&amp;lt;/font&amp;gt;&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode)&lt;br /&gt;
&lt;br /&gt;
Es ergibt sich jetzt die Ausgabe:&lt;br /&gt;
&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; dfs(graph, 1)&amp;lt;font color=red&amp;gt;&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
 3&lt;br /&gt;
 4&lt;br /&gt;
 5&lt;br /&gt;
 2&lt;br /&gt;
 1&amp;lt;/font&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In realem Code ersetzt man die print-Ausgaben natürlich durch anwendungsspezifische Aktionen und Berechnungen. Einige Anwendungen sind uns im Kapitel [[Suchen]] bereits begegnet. &lt;br /&gt;
; Anwendungen der Pre-Order Traversierung&lt;br /&gt;
* Kopieren eines Graphen: kopiere zuerst den besuchten Knoten, dann seine Nachbarn und die dazugehörigen Kanten (sowie die Kanten zu bereits besuchten Knoten, die in der Grundversion der Tiefensuche ignoriert werden).&lt;br /&gt;
* Bestimmen der Zusammenhangskomponenten eines Graphen (siehe unten)&lt;br /&gt;
* In einem Zeichenprogramm: fülle eine Region mit einer Farbe (&amp;quot;flood fill&amp;quot;). Dabei ist jedes Pixel ein Knoten des Graphen und wird mit seinen 4 Nachbarpixceln verbunden. Die Tiefensuche startet bei der Mausposition und endet am Rand des betreffendcen Gebiets.&lt;br /&gt;
* Falls der Graph ein Baum ist: bestimme den Abstand jedes Knotens von der Wurzel&lt;br /&gt;
* Falls der Graph ein Parse-Baum ist, wobei innere Knoten Funktionsaufrufe, Kindknoten Funktionsargumente, und Blattknoten Werte repräsentieren: drucke den zugehörigen Ausdruck aus (also immer zuerst den Funktionsnamen, dann die Argumente, die wiederum geschachtelte Funktionsaufrufe sein können).&lt;br /&gt;
; Anwendungen der Post-Order Traversierung&lt;br /&gt;
* Löschen eines Graphen: lösche zuerst die Nachbarn, dann den Knoten selbst&lt;br /&gt;
* Bestimmen einer topologischen Sortierung eines azyklischen gerichteten Graphens (siehe unten)&lt;br /&gt;
* Falls der Graph ein Baum ist: bestimme den Abstand jedes Knotens von den Blättern (also die Tiefe des Baumes, siehe Übung 5)&lt;br /&gt;
* Falls der Graph ein Parse-Baum ist: führe die zugehörige Berechnung aus (d.h. berechne zuerst die geschachtelten inneren Funktionen, dann mit diesen Ergebnissen die nächst äußeren usw., siehe Übung 5).&lt;br /&gt;
; Anwendungen, die Pre- und Post-Order benötigen&lt;br /&gt;
* Weg aus einem Labyrinth: die Pre-Order dokumentiert die Suche nach dem Weg, die Post-Order zeigt den Rückweg aus Sackgassen (siehe Übung 9).&lt;br /&gt;
Im Spezialfall, wenn der Graph ein Binärbaum ist, unterscheidet man noch eine dritte Variante der Traversierung, nämlich die &amp;lt;i&amp;gt;in-order&amp;lt;/i&amp;gt; Traversierung. In diesem Fall behandelt man den Vaterknoten nach den linken, aber vor den rechten Kindern. Diese Reihenfolge wird beim [[Suchen#Beziehungen zwischen dem Suchproblem und dem Sortierproblem|Tree Sort Algorithmus]] verwendet. Diese Sortierung verwendet man auch, wenn man einen Parse-Baum mit binären Operatoren (statt Funktionsaufrufen) ausgeben will, siehe Übung 5.&lt;br /&gt;
&lt;br /&gt;
Eine nützliche Erweiterung der Tiefensuche besteht darin, in der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; nicht nur zu dokumentieren, dass ein Knoten bereits besucht wurde, sondern auch, von welchem Knoten aus man den jeweiligen Knoten zuerst erreicht hat. Im entstehenden Tiefensuchbaum ist dies gerade der Vaterknoten, weshalb wir die verbesserte property map zweckmäßigerweise in &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; umbenennen. Für den Startknoten, also die Wurzel des Baumes, wählen wir die Konvention, dass er sein eigener Vaterknoten ist (die Konvention, dafür den Wert &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zu verwenden, scheidet aus, weil dies bereits die Tatsache signalisiert, dass ein Knoten noch nicht besucht wurde):&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     parents = [None]*len(graph)     # Registriere für jeden Knoten den Vaterknoten im Tiefensuchbaum&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, parent):        # rekursive Hilfsfunktion&lt;br /&gt;
         if parents[node] is None:   # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             parents[node] = parent  # Markiere node als besucht und speichere seinen Vaterknoten&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn ...&lt;br /&gt;
                 visit(neighbor, node)      #  ... wobei node zu deren Vaterknoten wird&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode, startnode)     # Konvention für Wurzel: startnode ist sein eigener Vater&lt;br /&gt;
     &lt;br /&gt;
     return parents                  # Rückgabe des berechneten Tiefensuch-Baums&lt;br /&gt;
&lt;br /&gt;
Die Ausgabe für den obigen Beispielgraphen lautet: &lt;br /&gt;
  Knotennummer  |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7&lt;br /&gt;
  --------------+-----+-----+-----+-----+-----+-----+-----+-----&lt;br /&gt;
  Vaterknoten   | None|  1  |  1  |  4  |  2  |  2  |  3  |  3&lt;br /&gt;
&lt;br /&gt;
Dabei ist die Knotennummer der Index im Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt;, und der Vaterknoten ist der dazugehörige Arrayeintrag. Beachte, dass Knoten 0 in diesem Graphen nicht existiert, daher ist sein Eintrag &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;. Per Konvention hat der Wurzelknoten 1 sich selbst als Vater.&lt;br /&gt;
&lt;br /&gt;
=== Breitensuche in Graphen (Breadth First Search, BFS) ===&lt;br /&gt;
&lt;br /&gt;
Im Gegensatz zur Tiefensuche werden bei der Breitensuche alle Nachbarnknoten abgearbeitet, &amp;lt;i&amp;gt;bevor&amp;lt;/i&amp;gt; man rekursiv deren Nachbarn besucht. Man betrachtet somit zuerst alle Knoten, die den Abstand 1 von Startknoten haben, dann diejenigen mit dem Abstand 2 usw. Diese Reihenfolge bezeichnet man als &amp;lt;i&amp;gt;level-order&amp;lt;/i&amp;gt;. Wir sind ihr beispielsweise in Übung 6 begegnet, als die ersten 7 Ebenen eines Treap ausgegeben werden sollten. Man implementiert Breitensuche zweckmäßig mit Hilfe einer Queue, die die Knoten in First In - First Out - Reihenfolge bearbeitet. Eine geeignete Datenstruktur hierfür ist die Klasse &amp;lt;tt&amp;gt;[http://docs.python.org/library/collections.html#collections.deque deque]&amp;lt;/tt&amp;gt; aus dem Python-Modul &amp;lt;tt&amp;gt;[http://docs.python.org/library/collections.html collections]&amp;lt;/tt&amp;gt; (eine Deque implementiert sowohl die Funktionalität einer Queue wie auch die eines Stacks, siehe Übung 3):&lt;br /&gt;
&lt;br /&gt;
 from collections import deque&lt;br /&gt;
 &lt;br /&gt;
 def bfs(graph, startnode)&lt;br /&gt;
     visited = [False]*len(graph)   # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
   &lt;br /&gt;
     q = deque()                    # Queue für die zu besuchenden Knoten&lt;br /&gt;
     q.append(startnode)            # Startknoten in die Queue einfügen&lt;br /&gt;
     &lt;br /&gt;
     while len(q) &amp;gt; 0:              # Solange es noch unbesuchte Knoten gibt&lt;br /&gt;
         node = q.popleft()         # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
         if not visited[node]:      # Falls node noch nicht (auf einem anderen Weg) besucht wurde&lt;br /&gt;
              visited[node] = True  # Markiere node als besucht&lt;br /&gt;
              print node            # Drucke Knotennummer&lt;br /&gt;
              for neighbor in graph[node]:    # Füge Nachbarn in die Queue ein&lt;br /&gt;
                  q.append(neighbor)&lt;br /&gt;
&lt;br /&gt;
[[Image:Breitens.jpg]]&lt;br /&gt;
&lt;br /&gt;
Der Aufruf dieser Funktion liefert die Knoten des obigen Graphens ebenenweise, also zufällig genau in der Reihenfolge der Knotennummern:&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; bfs(graph, 1)&lt;br /&gt;
 1&lt;br /&gt;
 2&lt;br /&gt;
 3&lt;br /&gt;
 4&lt;br /&gt;
 5&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
&lt;br /&gt;
Neben der ebenenweisen Ausgabe hat die Breitensuche viele weitere wichtige Anwendungen, z.B. beim Testen, ob ein gegebener Graph bi-partit ist (siehe [http://en.wikipedia.org/wiki/Breadth-first_search#Testing_bipartiteness WikiPedia]), sowie bei der Suche nach kürzesten Wegen (siehe unten) und kürzesten Zyklen.&lt;br /&gt;
&lt;br /&gt;
== Weitere Anwendungen der Tiefensuche ==&lt;br /&gt;
&lt;br /&gt;
Die Tiefensuche hat zahlreiche Anwendungen, wobei der grundlegende Algorithmus immer wieder leicht modifiziert und an die jeweilige Aufgabe angepasst wird. Wir beschreiben im folgenden einige Beispiele.&lt;br /&gt;
&lt;br /&gt;
=== Damenproblem ===&lt;br /&gt;
&lt;br /&gt;
Tiefensuche wird häufig verwendet, um systematisch nach der Lösung eines logischen Rätsels (oder allgemeiner nach der Lösung eines diskreten Optimierungsproblems) zu suchen. Besonders anschaulich hierfür ist das Damenproblem. Die Aufgabe besteht darin, &amp;lt;math&amp;gt;k&amp;lt;/math&amp;gt; Damen auf einem Schachbrett der Größe &amp;lt;math&amp;gt;k \times k&amp;lt;/math&amp;gt; so zu platzieren, dass sie sich (nach den üblichen Schach-Regeln) nicht gegenseitig schlagen können. Das folgende Diagramm zeigt eine Lösung für den Fall &amp;lt;math&amp;gt;k=4&amp;lt;/math&amp;gt;. Die Positionen der Damen werden dabei wie üblich durch die Angabe der Spalte (Linie) mit Buchstaben und der Zeile (Reihe) mit Zahlen kodiert, hier also A2, B4, C1, D3:&lt;br /&gt;
&lt;br /&gt;
  ---------------&lt;br /&gt;
 |   | X |   |   | 4&lt;br /&gt;
 |---|---|---|---| &lt;br /&gt;
 |   |   |   | X | 3&lt;br /&gt;
 |---|---|---|---|&lt;br /&gt;
 | X |   |   |   | 2&lt;br /&gt;
 |---|---|---|---|&lt;br /&gt;
 |   |   | X |   | 1&lt;br /&gt;
  ---------------&lt;br /&gt;
   A   B   C   D&lt;br /&gt;
&lt;br /&gt;
Um das Problem systematisch zu lösen, konstruieren wir einen gerichteten Graphen, dessen Knoten die möglichen Positionen der Damen kodieren. Wir verbinden Knoten, die zu benachbarten Linien gehören, genau dann mit einer Kante, wenn die zugehörigen Positionen kompatibel sind, also wenn sich die dort positionierten Damen nicht schlagen können. Der resultierende Graph für &amp;lt;math&amp;gt;k=4&amp;lt;/math&amp;gt; hat folgende Gestalt:&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
Knoten, die zur selben Reihe oder Linie gehören, sind beispielsweise nicht direkt verbunden, weil zwei Damen niemals in derselben Linie oder Reihe stehen dürfen. Um eine erlaubte Konfiguration zu finden, verwenden wir nun eine angepasste Version der Tiefensuche: Wir beginnen die Suche beim Knoten &amp;lt;tt&amp;gt;START&amp;lt;/tt&amp;gt;. Sobald wir den Knoten &amp;lt;tt&amp;gt;STOP&amp;lt;/tt&amp;gt; erreichen, beenden wir die Suche und lesen die Lösung am gerade gefundenen Weg von Start nach Stop ab. Zwei kleine Modifikationen des Grundalgorithmus stellen sicher, dass die Bedingungen der Aufgabe eingehalten werden: Wir dürfen bei der Tiefensuche nur dann zu einem Nachbarn weitergehen, wenn die betreffende Position mit allen im Pfad bereits gesetzten Positionen kompatibel ist, andernfalls ist diese Kante tabu. Landen wir aufgrund dieser Regel in einer Sackgasse (also in einem Knoten, wo keine der ausgehenden Kanten erlaubt ist), müssen wir zur nächsten erlaubten Abzweigung zurückgehen (Backtracking). Beim Zurückgehen müssen wir das &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Flag wieder auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zurücksetzen, weil der betreffende Knoten ja möglicherweise auf einem anderen erlaubten Weg erreichbar ist.&lt;br /&gt;
&lt;br /&gt;
Der folgende Graph zeigt einen solchen Fall: Wir haben zwei Damen auf die Felder A1 und B3 positioniert (grüne Pfeile). Die einzig ausgehende Kante von B3 führt zum Knoten C1, welcher aber mit der Position A1 inkompatibel ist, so dass diese Kante nicht verwendet werden darf (roter Pfeil). Das Backtracking muss jetzt zu Knoten A1 zurückgehen (dabei wird das &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Flag von B3 wieder auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; gesetzt), weil A1 mit der Kante nach B4 eine weitere Option hat, die geprüft werden muss (die allerdings hier auch nicht zum Ziel führt).&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph-failure.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
Nach einigen weiteren Sackgassen findet man schließlich den Pfad A2, B4, C1, D3, der im folgenden Graphen grün markiert ist und der obigen Lösung entspricht:&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph-success.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
=== Test, ob ein ungerichteter Graph azyklisch ist ===&lt;br /&gt;
&lt;br /&gt;
Ein zusammenhängender ungerichteter Graph ist azyklisch (also ein Baum) genau dann, wenn es nur einen möglichen Weg von jedem Knoten zu jedem anderen gibt. (Bei gerichteten Graphen sind die Verhältnisse komplizierter. Wir behandeln dies weiter unten.) Das kann man mittels Tiefensuche leicht feststellen: Die Kante, über die wir einen Knoten erstmals erreichen, ist eine &amp;lt;i&amp;gt;Baumkante&amp;lt;/i&amp;gt; des Tiefensuchbaums. Erreichen wir einen bereits besuchten Knoten nochmals über eine andere Kante, haben wir einen Zyklus gefunden. Dabei müssen wir allerdings beachten, dass in einem ungerichteten Graphen jede Baumkante zweimal gefunden wird, einmal in Richtung vom Vater zum Kind und einmal in umgekehrter Richtung. Im zweiten Fall endet die Kante zwar in einem bereits besuchten Knoten (dem Vater), aber es entsteht dadurch kein Zyklus. Den Vaterknoten müssen wir deshalb überspringen, wenn wir über die Nachbarn iterieren:&lt;br /&gt;
&lt;br /&gt;
 def undirected_cycle_test(graph):         # Annahme: der Graph ist zusammenhängend&lt;br /&gt;
                                           # (andernfalls führe den Algorithmus für jede Zusammenhangskomponente aus)&lt;br /&gt;
     visited = [False]*len(graph)          # Flags für bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, from_node):           # rekursive Hilfsfunktion: gibt True zurück, wenn Zyklus gefunden wurde&lt;br /&gt;
         if not visited[node]:             # wenn node noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True          # markiere node als besucht&lt;br /&gt;
             for neighbor in graph[node]:  # besuche die Nachbarn ...&lt;br /&gt;
                 if neighbor == from_node: # ... aber überspringe den Vaterknoten&lt;br /&gt;
                     continue&lt;br /&gt;
                 if visit(neighbor, node): # ... signalisiere, wenn rekursiv ein Zyklus gefunden wurde&lt;br /&gt;
                     return True&lt;br /&gt;
             return False                  # kein Zyklus gefunden&lt;br /&gt;
         else:&lt;br /&gt;
             return True                   # Knoten schon besucht =&amp;gt; Zyklus&lt;br /&gt;
     &lt;br /&gt;
     startnode = 0                         # starte bei beliebigem Knoten (hier: Knoten 0)&lt;br /&gt;
     return visit(startnode, startnode)    # gebe True zurück, wenn ein Zyklus gefunden wurde&lt;br /&gt;
&lt;br /&gt;
Wenn wir einen Zyklus finden, wird das weitere Traversieren das Graphen abgebrochen, denn ein Graph, der einmal zyklisch war, kann später nicht wieder azyklisch werden. Die notwendige Modifikation für unzusammenhängende Graphen erfolgt analog zum Algorithmus für die Detektion von Zusammenhangskomponenten, der im nächsten Abschnitt beschrieben wird.&lt;br /&gt;
&lt;br /&gt;
=== Finden von Zusammenhangskomponenten ===&lt;br /&gt;
&lt;br /&gt;
Das Auffinden und Markieren von Zusammenhangskomponenten (also maximalen zusammenhängenden Teilgraphen) ist eine grundlegende Aufgabe in ungerichteten, unzusammenhängenden Graphen (bei gerichteten Graphen sind die Verhältnisse wiederum komplizierter, siehe unten). Zwei Knoten u und v gehören zur selben Zusammenhangskomponente genau dann, wenn es einen Pfad von u nach v gibt (da der Graph ungerichtet ist, gibt es dann auch einen Pfad von v nach u). Man sagt auch, dass &amp;quot;v von u aus erreichbar&amp;quot; ist. Unzusammenhängende Graphen entstehen in der Praxis häufig, wenn die Kanten gewisse Relationen zwischen den Knoten kodieren: &lt;br /&gt;
* Wenn die Knoten Städte sind und die Kanten Straßen, sind diejenigen Städte in einer Zusammenhangskomponente, die per Auto von einander erreichbar sind. Unzusammenhängende Graphen entstehen hier beispielsweise, wenn eine Insel nicht durch eine Brücke erschlossen ist, wenn Grenzen gesperrt sind oder wenn ein Gebirge zu unwegsam ist, um Straßen zu bauen.&lt;br /&gt;
* Wenn Knoten Personen sind, und Kanten die Eltern-Kind-Relation beschreiben, so umfasst jede Zusammenhangskomponenten die Verwandten (auch wenn sie nur über viele &amp;quot;Ecken&amp;quot; verwandt sind).&lt;br /&gt;
* In der Bildverarbeitung entsprechen Knoten den Pixeln, und dieselben werden durch eine Kante verbunden, wenn sie zum selben Objekt gehören. Die Zusammenhangskomponenten entsprechen somit den Objekten im Bild (siehe Übungsaufgabe).&lt;br /&gt;
Die Zusammenhangskomponenten bilden eine Äquivalenzrelation. Folglich kann für jede Komponente ein Reprässentant bestimmt werden, der sogenannte &amp;quot;Anker&amp;quot;. Kennt jeder Knoten seinen Anker, ist das Problem der Zusammenhangskomponenten gelöst. &lt;br /&gt;
&lt;br /&gt;
==== Lösung mittels Tiefensuche ====&lt;br /&gt;
&lt;br /&gt;
Unser erster Ansatz ist, den Anker mit Hilfe der Tiefensuche zu finden. Anstelle der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; verwenden wir diesmal eine property map &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt;, die für jeden Knoten die Knotennummer des zugehörigen Ankers angibt, oder &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, wenn der Knoten noch nicht besucht wurde. Dabei verwenden wir wieder die Konvention, dass Anker auf sich selbst zeigen. Für viele Anwendungen ist es außerdem (oder stattdessen) zweckmäßig, die Zusammenhangskomponenten mit einer laufenden Nummer, einem sogenannten &amp;lt;i&amp;gt;Label&amp;lt;/i&amp;gt;, durchzuzählen. Dann kann man zusätzliche Informationen zu jeder Komponente (beispielsweise deren Größe) einfach in einem Array speichern, das über die Labels indexiert wird. Die folgende Version der Tiefensuche bestimmt sowohl die Anker als auch die Labels für jeden Knoten:&lt;br /&gt;
&lt;br /&gt;
 def connectedComponents(graph):&lt;br /&gt;
        anchors = [None] * len(graph)             # property map für Anker jedes Knotens&lt;br /&gt;
        labels  = [None] * len(graph)             # property map für Label jedes Knotens&lt;br /&gt;
        &lt;br /&gt;
        def visit(node, anchor):&lt;br /&gt;
                &amp;quot;&amp;quot;&amp;quot;anchor ist der Anker der aktuellen ZK&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
                if anchors[node] is None:         # wenn node noch nicht besucht wurde:&lt;br /&gt;
                    anchors[node] = anchor        # setze seinen Anker&lt;br /&gt;
                    labels[node] = labels[anchor] # und sein Label&lt;br /&gt;
                    for neighbor in graph[node]:  # und besuche die Nachbarn&lt;br /&gt;
                        visit(neighbor, anchor)&lt;br /&gt;
        &lt;br /&gt;
        current_label = 0                         # Zählung der ZK beginnt bei 0&lt;br /&gt;
        for node in xrange(len(graph)):&lt;br /&gt;
            if anchors[node] is None:             # Anker noch nicht bekannt =&amp;gt; neue ZK gefunden&lt;br /&gt;
                labels[node] = current_label      # Label des Ankers setzen&lt;br /&gt;
                visit(node, node)                 # Knoten der neuen ZK rekursiv suchen&lt;br /&gt;
                current_label += 1                # Label für die nächste ZK hochzählen&lt;br /&gt;
        return anchors, labels&lt;br /&gt;
Interessant ist hier die Schleife über alle Knoten des Graphen am Ende des Algorithmus, die bei den bisherigen Versionen der Tiefensuche nicht vorhanden war. Um ihre Funktionsweise zu verstehen, nehmen wir für den Moment an, dass der Graph zusammenhängend ist. Dann findet diese Schleife den ersten Knoten des Graphen und führt die Tiefensuche mit diesem Knoten als Startknoten aus. Sobald die Rekursion zurückkehrt, sind alle Knoten des Graphen besucht (weil der Graph ja zusammenhängend war), so dass die Schleife alle weiteren Knoten überspringt (die if-Anweisung liefert für keinen weiteren Knoten True). Bei unzusammenhängenden Graphen dagegen erreicht die Tiefensuche nur die Knoten derselben Komponente, die im weiteren Verlauf der Schleife übersprungen werden. Findet die if-Anweisung jetzt einen noch nicht besuchten Knoten, muss dieser folglich in einer neuen Komponente liegen. Wir verwenden diesen Knoten als Anker und bestimmen die übrigen Knoten dieser Komponente wiederum mit Tiefensuche.&lt;br /&gt;
&lt;br /&gt;
* Beispiel: ... &amp;lt;b&amp;gt; under construction &amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Man erkennt, dass die Tiefensuche nach dem &amp;lt;i&amp;gt;Anlagerungsprinzip&amp;lt;/i&amp;gt; vorgeht: Beginnend vom einem Startknoten (dem Anker) werden die Knoten der aktuellen Komponente nach und nach an den Tiefensuchbaum angehangen. Erst, wenn nichts mehr angelagert werden kann, geht der Algorithmus zur nächsten Komponente über.&lt;br /&gt;
&lt;br /&gt;
==== Lösung mittels Union-Find-Algorithmus ====&lt;br /&gt;
&lt;br /&gt;
Im Gegensatz zum Anlagerungsprinzip sucht der Union-Find-Algorithmus die Zusammenhangskomponenten mit dem &amp;lt;i&amp;gt;Verschmelzungsprinzip&amp;lt;/i&amp;gt;: Eingangs wird jeder Knoten als ein Teilgraph für sich betrachtet. Dann iteriert man über alle Kanten und verbindet deren Endknoten jeweils zu einem gemeinsamen Teilgraphen (falls die beiden Enden einer Kante bereits im selben Teilgraphen liegen, wird diese Kante ignoriert). Solange noch Kanten vorhanden sind, werden dadurch immer wieder Teilgraphen in größere Teilgraphen verschmolzen. Am Ende bleiben die maximalen zusammenhängenden Teilgraphen (also gerade die Zusammenhangskomponenten) übrig. Dieser Algorithmus kommt ohne Tiefensuche aus und ist daher in der Praxis oft schneller, allerdings auch etwas komplizierter zu implementieren.&lt;br /&gt;
&lt;br /&gt;
Der Schlüssel des Algorithmus ist eine Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt;, die zu jedem Knoten den aktuellen Anker sucht. Der Anker existiert immer, da jeder Knoten von Anfang an zu einem Teilgraphen gehört (anfangs ist jeder Teilgraph trivial und besteht nur aus dem Knoten selbst). Die Verschmelzung wird realisiert, indem der Anker des einen Teilgraphen seine Rolle verliert und stattdessen der Anker des anderen Teilgraphen eingesetzt wird. &lt;br /&gt;
&lt;br /&gt;
Zur Verwaltung der Anker verwenden wir wieder eine property map &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt; mit der Konvention, dass die Anker auf sich selbst verweisen. Es wäre jedoch zu teuer, wenn man bei jeder Verschmelzung alle Anker-Einträge der beteiligten Knoten aktualisieren müsste, da jeder Knoten im Laufe des Algorithmus mehrmals seinen Anker wechseln kann. Statt dessen definiert man Anker rekursiv: Verweist ein Knoten auf einen Anker, der mittlerweile diese Rolle verloren hat, folgt man dem Verweis von diesem Knoten (dem ehemaligen Anker) weiter, bis man einen tatsächlichen Anker gefunden hat - erkennbar daran, dass er auf sich selbst verweist. Diese Suchfunktion kann folgendermassen implementiert werden:&lt;br /&gt;
&lt;br /&gt;
  def findAnchor(anchors, node):&lt;br /&gt;
      while node != anchors[node]:   # wenn node kein Anker ist&lt;br /&gt;
          node = anchors[node]       # ... verfolge die Ankerkette weiter&lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Allerdings kann diese Kette im Laufe vieler Verschmelzungen sehr lang werden, so dass das Verfolgen der Kette teuer wird. Man vermeidet dies durch die sogenannte &amp;lt;i&amp;gt;Pfadkompression&amp;lt;/i&amp;gt;: Immer, wenn man den Anker gefunden hat, aktualisiert man den Eintrag am Anfang der Kette. Die Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt; wird dadurch nur wenig komplizierter:&lt;br /&gt;
&lt;br /&gt;
  def findAnchor(anchors, node):&lt;br /&gt;
      start = node                   # wir merken uns den Anfang der Kette&lt;br /&gt;
      while node != anchors[node]:   # wenn node kein Anker ist&lt;br /&gt;
          node = anchors[node]       # ... verfolge die Ankerkette weiter&lt;br /&gt;
      anchors[start] = node          # Pfadkompression: aktualisiere den Eintrag am Anfang der Kette&lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Man kann zeigen, dass die Ankersuche mit Pfadkompression zu einer fast konstanten amortisierten Laufzeit pro Aufruf führt.&lt;br /&gt;
&lt;br /&gt;
Um mit jeder Kante des (ungerichteten) Graphen nur maximal einmal eine Verschmelzung durchzuführen, betrachten wir jede Kante nur in der Richtung von der kleineren zur größeren Knotennummer, die umgekehrte Richtung wird ignoriert. Außerdem ist es zweckmäßig, bei jeder Verschmelzung denjenigen Anker mit der kleineren Knotennummer als neuen Anker zu übernehmen. Dann gilt für jede Zusammenhangskomponente, dass gerade der Knoten mit der kleinsten Knotennummer der Anker ist (genau wie bei der Lösung mittels Tiefensuche), was die weitere Analyse vereinfacht, z.B. die Zuordnung der Labels zu den Komponenten am Ende des Algorithmus. &lt;br /&gt;
&lt;br /&gt;
 def unionFindConnectedComponents(graph):&lt;br /&gt;
     anchors = range(len(graph))        # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):    # iteriere über alle Knoten&lt;br /&gt;
         for neighbor in graph[node]:   # ... und über deren ausgehende Kanten&lt;br /&gt;
             if neighbor &amp;lt; node:        # ignoriere Kanten, die in falscher Richtung verlaufen&lt;br /&gt;
                 continue&lt;br /&gt;
             # hier landen wir für jede Kante des Graphen genau einmal&lt;br /&gt;
             a1 = findAnchor(anchors, node)       # finde Anker ...&lt;br /&gt;
             a2 = findAnchor(anchors, neighbor)   # ... der beiden Endknoten&lt;br /&gt;
             if a1 &amp;lt; a2:                          # Verschmelze die beiden Teilgraphen&lt;br /&gt;
                 anchors[a2] = a1                 # (verwende den kleineren der beiden Anker als Anker des&lt;br /&gt;
             elif a2 &amp;lt; a1:                        #  entstehenden Teilgraphen. Falls node und neighbor &lt;br /&gt;
                 anchors[a1] = a2                 #  den gleichen Anker haben, waren sie bereits im gleichen&lt;br /&gt;
                                                  #  Teilgraphen, und es passiert hier nichts.)&lt;br /&gt;
     # Bestimme jetzt noch die Labels der Komponenten&lt;br /&gt;
     labels = [None]*len(graph)         # Initialisierung der property map für Labels&lt;br /&gt;
     current_label = 0                  # die Zählung beginnt bei 0&lt;br /&gt;
     for node in xrange(len(graph)):&lt;br /&gt;
         a = findAnchor(anchors, node)  # wegen der Pfadkompression zeigt jeder Knoten jetzt direkt auf seinen Anker&lt;br /&gt;
         if a == node:                  # node ist ein Anker&lt;br /&gt;
             labels[a] = current_label  # =&amp;gt; beginne eine neue Komponente&lt;br /&gt;
             current_label += 1         # und zähle Label für die nächste ZK hoch&lt;br /&gt;
         else:&lt;br /&gt;
             labels[node] = labels[a]   # node ist kein Anker =&amp;gt; setzte das Label des Ankers&lt;br /&gt;
                                        # (wir wissen, dass labels[a] bereits gesetzt ist, weil &lt;br /&gt;
                                        #  der Anker immer der Knoten mit der kleinsten Nummer ist)&lt;br /&gt;
     return anchors, labels&lt;br /&gt;
 &lt;br /&gt;
* Beispiel: ... &amp;lt;b&amp;gt;under construction&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Kürzeste Wege (Pfade) ==&lt;br /&gt;
&lt;br /&gt;
Eine weitere grundlegende Aufgabe in Graphen ist die Bestimmung eines kürzesten Weges zwischen zwei gegebenen Knoten. Dies hat offensichtliche Anwendungen bei Routenplanern und Navigationssystemen und ist darüber hinaus wichtiger Bestandteil anderer Algorithmen, z.B. bei der Berechnung eines maximalen Flusses mit der [http://en.wikipedia.org/wiki/Edmonds%E2%80%93Karp_algorithm Methode von Edmonds und Karp].&lt;br /&gt;
&lt;br /&gt;
=== Kürzeste Wege in ungewichteten Graphen mittels Breitensuche ===&lt;br /&gt;
&lt;br /&gt;
Im Fall eines ungewichteten Graphen ist die Länge eines Weges einfach durch die Anzahl der durchlaufenen Kanten definiert. Daraus folgt, dass kürzeste Pfade mit einer leicht angepassten Version der Breitensuche gefunden werden können: Aufgrund des first in-first out-Verhaltens der Queue betrachtet die Breitensuche alle (erreichbaren) Knoten in der Reihenfolge ihres Abstandes vom Startknoten. Wenn wir den Zielknoten zum ersten Mal erreichen, und der gerade gefundene Weg vom Start zum Ziel hat die Länge L, muss dies der kürzeste Weg sein: Alle möglichen Wege der Länge L' &amp;amp;lt; L hat die Breitensuche ja bereits betrachtet, ohne dass dabei der Zielknoten erreicht wurde. Daraus folgt übrigens eine allgemeine Eigenschaft aller Algorithmen für kürzeste Wege: Wenn der kürzeste Weg vom Start zum Ziel die Länge L hat, finden diese Algorithmen als Nebenprodukt auch die kürzesten Wege zu allen Knoten, für die L' &amp;amp;lt; L gilt. &lt;br /&gt;
&lt;br /&gt;
Um den Algorithmus zu implementieren, passen wir die Breitensuche so an, dass anstelle der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; eine property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; verwendet wird, die für jeden besuchten Knoten den Vaterknoten im Breitensuchbaum speichert. Durch Rückverfolgen der &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette können wir den Pfad vom Ziel zum Start rekonstruieren, und durch Umdrehen der Reihenfolge erhalten wir den gesuchten Pfad vom Start zum Ziel. Sobald der Zielknoten erreicht wurde, können wir die Breitensuche abbrechen (&amp;lt;tt&amp;gt;break&amp;lt;/tt&amp;gt;-Befehl in der ersten &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Schleife). Falls der gegebene Graph unzusammenhängend ist, kann es passieren, dass gar kein Weg gefunden wird, weil Start und Ziel in verschiedenen Zusammenhangskomponenten liegen. Dies erkennen wir daran, dass die Breitensuche beendet wurde, ohne den Zielknoten zu besuchen. Dann gibt die Funktion statt eines Pfades dern Wert &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zurück:&lt;br /&gt;
&lt;br /&gt;
  from collections import deque&lt;br /&gt;
  &lt;br /&gt;
  def shortestPath(graph, startnode, destination):&lt;br /&gt;
      parents = [None]*len(graph)      # Registriere für jeden Knoten den Vaterknoten im Breitensuchbaum&lt;br /&gt;
      parents[startnode] = startnode   # startnode ist die Wurzel des Baums =&amp;gt; verweist auf sich selbst&lt;br /&gt;
      &lt;br /&gt;
      q = deque()                      # Queue für die zu besuchenden Knoten&lt;br /&gt;
      q.append(startnode)              # Startknoten in die Queue einfügen&lt;br /&gt;
      &lt;br /&gt;
      while len(q) &amp;gt; 0:                # Solange es noch unbesuchte Knoten gibt&lt;br /&gt;
          node = q.popleft()           # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
          if node == destination:      # Zielknoten erreicht&lt;br /&gt;
              break                    #   =&amp;gt; Suche beenden&lt;br /&gt;
          for neighbor in graph[node]: # Besuche die Nachbarn von node&lt;br /&gt;
              if parents[neighbor] is None:  # aber nur, wenn sie noch nicht besucht wurden&lt;br /&gt;
                  parents[neighbor] = node   # setze node als Vaterknoten&lt;br /&gt;
                  q.append(neighbor)         # und füge neighbor in die Queue ein&lt;br /&gt;
      &lt;br /&gt;
      if parents[destination] is None: # Breitensuche wurde beendet ohne den Zielknoten zu besuchen&lt;br /&gt;
          return None                  # =&amp;gt; kein Pfad gefunden (unzusammenhängender Graph)&lt;br /&gt;
      &lt;br /&gt;
      # Pfad durch die parents-Kette zurückverfolgen und speichern&lt;br /&gt;
      path = [destination]&lt;br /&gt;
      while path[-1] != startnode:&lt;br /&gt;
          path.append(parents[path[-1]])&lt;br /&gt;
      path.reverse()     # Reihenfolge umdrehen (Ziel =&amp;gt; Start wird zu Start =&amp;gt; Ziel)&lt;br /&gt;
      return path        # gefundenen Pfad zurückgeben&lt;br /&gt;
&lt;br /&gt;
=== Gewichtete Graphen ===&lt;br /&gt;
&lt;br /&gt;
Das Problem der Suche nach kürzesten Wegen wird wesentlich interessanter und realistischer, wenn wir zu gewichteten Graphen übergehen:&lt;br /&gt;
&lt;br /&gt;
; Definition - kantengewichteter Graph&lt;br /&gt;
: Jeder Kante (s,t) des Graphen ist eine reelle oder natürliche Zahl w&amp;lt;sub&amp;gt;st&amp;lt;/sub&amp;gt; zugeordnet, die üblicherweise als ''Kantengewicht'' bezeichnet wird.&lt;br /&gt;
&lt;br /&gt;
; Definition - knotengewichteter Graph&lt;br /&gt;
: Jedem Knoten v des Graphen ist eine reelle oder natürliche Zahl w&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; zugeordnet, die üblicherweise als ''Knotengewicht'' bezeichnet wird.&lt;br /&gt;
&lt;br /&gt;
Je nach Anwendung benötigt man Knoten- oder Kantengewichte oder auch beides zugleich. Wir beschränken uns in der Vorlesung auf kantengewichtete Graphen. Beispiele für die Informationen, die man durch Kantengewichte ausdrücken kann, sind&lt;br /&gt;
* wenn die Knoten Orte sind: Abstand von Anfangs- und Endknoten jeder Kante (z.B. Luftline oder Straßenentfernung), Fahrzeit zwischen den Orten&lt;br /&gt;
* wenn der Knoten ein Rohrnetzwerk beschreibt: Durchflusskapazität der einzelnen Rohre (für max-Flussprobleme), analog bei elektrischen Netzwerken: elektrischer Widerstand&lt;br /&gt;
* wenn die Knoten Währungen repräsentieren, können deren Wechselkurse durch Kantengewichte angegeben werden.&lt;br /&gt;
Bei einigen Beispielen ergeben sich unterschiedliche Kantengewichte, wenn eine Kante von s nach t anstatt von t nach s durchlaufen wird. Beispielsweise können sich die Fahrzeiten erheblich unterscheiden, wenn es in einer Richtung bergauf, in der anderen bergab geht, obwohl die Entfernung in beiden Fällen gleich ist. Hier ergibt sich natürlicherweise ein gerichteter Graph. In anderen Beispielen (z.B. bei Luftlinienentfernungen, in guter Näherung auch bei Straßenentfernungen) sind die Gewichte von der Richtung unabhängig, so dass wir ungerichtete Graphen verwenden können.&lt;br /&gt;
&lt;br /&gt;
Die Repräsentation der Kantengewichte im Programm richtet sich nach der Repräsentation des Graphen selbst. Am einfachsten ist wiederum die Adjazenzmatrix, die aber nur für dichte Graphen (&amp;lt;math&amp;gt;E = O(V^2)&amp;lt;/math&amp;gt;, mit E als Anzahl der Kanten und V als Anzahl der Knoten) effizient ist. Bei gewichteten Graphen gibt das Matrixelement a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; das Gewicht der Kante i &amp;amp;rArr; j (wobei a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; = 0 gesetzt wird, wenn diese Kante nicht existiert). Wie zuvor gilt für ungerichtete Graphen a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; = a&amp;lt;sub&amp;gt;ji&amp;lt;/sub&amp;gt; (symmetrische Matrix), während dies für gerichtete Graphen nicht gelten muss.&lt;br /&gt;
&lt;br /&gt;
Bei Graphen in Adjazenzlistendarstellung hat es sich bewährt, die Gewichte in einer &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; zu speichern. Weiter oben haben wir bereits property maps für Knoteneigenschaften (z.B. &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt;) gesehen. Property maps für Kanten funktionieren ganz analog, allerdings muss man jetzt Paare von Knoten (nämlich Anfangs- und Endknoten der Kante) als Schlüssel verwenden und die Daten entsprechend in einem assoziativen Array ablegen:&lt;br /&gt;
  w = weights[(i,j)]   # Zugriff auf das Gewicht der Kante i &amp;amp;rArr; j&lt;br /&gt;
Alternativ könnte man auch die Graph-Datenstruktur selbst erweitern, aber dies ist weniger zu empfehlen, weil jeder Algorithmus andere Erwiterungen benötigt und damit die Datenstruktur sehr unübersichtlich würde.&lt;br /&gt;
&lt;br /&gt;
Der kürzeste Weg ist nun definiert als der Weg, bei dem die Summe der Kantengewichte minimal ist:&lt;br /&gt;
;Definition - Problem des kürzesten Weges&lt;br /&gt;
: Sei P die Menge aller Wege von u nach v, und &amp;lt;math&amp;gt;p \in P&amp;lt;/math&amp;gt; einer dieser Wege. Wenn der Grpah einfach ist (es also keine Mehrfachkanten zwischen denselben Knoten und keine Schleifen gibt), ist der Weg p durch die Folge der besuchten Knoten eindeutig bestimmt:&lt;br /&gt;
: &amp;lt;math&amp;gt;p : \ \ u = x_0 \rightarrow x_1 \rightarrow x_2 \rightarrow ... \rightarrow v = x_{n_p}&amp;lt;/math&amp;gt;&lt;br /&gt;
:wo &amp;lt;math&amp;gt;n_p&amp;lt;/math&amp;gt; die Anzahl der Kanten im Weg p ist. Seine Kosten W&amp;lt;sub&amp;gt;p&amp;lt;/sub&amp;gt; ergeben sich als Summer der Gewichte der einzelnen Kanten&lt;br /&gt;
: &amp;lt;math&amp;gt;W_p = \sum_{k=1}^{n_p} w_{x_{k-1}x_k}&amp;lt;/math&amp;gt;&lt;br /&gt;
: und ein kürzester Weg &amp;lt;math&amp;gt;p^* \in P&amp;lt;/math&amp;gt; ist ein Weg mit minimalen Kosten&lt;br /&gt;
: &amp;lt;math&amp;gt;p^* = \textrm{argmin}_{p\in P}\ \ W_p&amp;lt;/math&amp;gt;&lt;br /&gt;
: Das Problem des kürzesten Weges besteht darin, einen optimalen Weg &amp;lt;i&amp;gt;p*&amp;lt;/i&amp;gt; zwischen gegebenen Knoten u und v zu finden.&lt;br /&gt;
Die Lösung dieses Problems hängt davon ab, ob alle Kantengewichte positiv sind, oder ob es auch negative Kantengewichte gibt. In letzeren Fall ist es möglich, durch eine Verlängerung des Weges die Kosten zu redizieren, während sich im ersteren Fall die Kosten immer erhöhen, wenn man den Weg verlängert. &lt;br /&gt;
&lt;br /&gt;
Negative Gewichte treten z.B. bei den Währungsgraphen auf. Auf den ersten Blick entsprechen diese Graphen nicht den Anforderungen an das Problem des kürzesten Weges, weil Wechselkurse miteinander (und mit Geldbeträgen) multipliziert anstatt addiert werden. Man beseitigt diese Schwierigkeit aber leicht, indem man die &amp;lt;i&amp;gt;Logarithmen&amp;lt;/i&amp;gt; der Wechselkurse als Kantengewichte verwendet, wodurch sich die Multiplikation in eine Addition der Logarithmen verwandelt. Wechselkurse &amp;amp;lt; 1 führen nun zu negativen Gewichten. &lt;br /&gt;
&lt;br /&gt;
Interessant werden negative Gewichte vor allem in Graphen mit Zyklen. Dann kann es nämlich passieren, dass die Gesamtkosten eines Zyklus ebenfalls negativ sind. Jeder Weg, der den Zyklus enthält, hat dann Kosten von &amp;lt;math&amp;gt;-\infty&amp;lt;/math&amp;gt;, weil man den Zyklus beliebig oft durchlaufen und dadurch die Gesamtkosten immer weiter verkleinern kann:&lt;br /&gt;
&lt;br /&gt;
     /\		1. Durchlauf: Kosten -1&lt;br /&gt;
  1 /  \ -4	2. Durchlauf: Kosten -2&lt;br /&gt;
   /____\	etc.&lt;br /&gt;
      2&lt;br /&gt;
&lt;br /&gt;
Um hier nicht in einer Endlosschleife zu landen, benötigt man spezielle Algorithmen, die mit dieser Situation umgehen können. Der [http://de.wikipedia.org/wiki/Bellman-Ford-Algorithmus Algorithmus von Bellmann und Ford] beispielsweise bricht die Suche nach dem kürzesten Weg ab, sobald er einen negativen Zyklus entdeckt, aber andernfalls kann er negative Gewichte problemlos verarbeiten. &lt;br /&gt;
&lt;br /&gt;
Die Detektion negativer Zyklen hat wiederum eine interessante Anwendung bei Währungsgraphen: Ein Zyklus bedeutet hier, dass man Geld über mehrere Stufen von einer Währung in die nächste und am Schluß wieder in die Originalwährung umtauscht, und ein negativer Zyklus führt dazu, dass man am Ende &amp;lt;i&amp;gt;mehr&amp;lt;/i&amp;gt; Geld besitzt als am Anfang (damit negative Zyklen wirklich einen Gewinn bedeuten und keinen Verlust, müssen die Wechselkurse vor der Logarithmierung in [http://de.wikipedia.org/wiki/Wechselkurs#Nominaler_Wechselkurs Preisnotierung] angegeben sein). Bei Privatpersonen ist dies ausgeschlossen, weil die Umtauschgebühren den möglichen Gewinn mehr als aufzehren. Banken mit direktem weltweitem Börsenzugang hingegen unternehmen große Anstrengungen, um solche negativen Zyklen möglichst schnell (nämlich vor der Konkurrenz) zu entdecken und auszunutzen. Diese Geschäftsmethode bezeichnet man als [http://de.wikipedia.org/wiki/Arbitrage Arbitrage] und die Existenz eines negativen Zyklus als Arbitragegelegenheit. Durch die Kursschwankungen (und durch die ausgleichende Wirkung der Arbitragegeschäfte selbst) existieren die Arbitragegelegenheiten nur für kurze Zeit, und ihre Detektion erfordert leistungsfähige Echtzeitalgorithmen.&lt;br /&gt;
&lt;br /&gt;
In dieser Vorlesung beschränken wir uns hingegen auf Graphen mit ausschließlich positiven Gewichten. In diesem Fall ist der Algorithmus von Dijkstra die Methode der Wahl, weil er wesentlich schneller arbeitet als der Bellmann-Ford-Algorithmus.&lt;br /&gt;
&lt;br /&gt;
=== Algorithmus von Dijkstra ===&lt;br /&gt;
&lt;br /&gt;
==== Edsger Wybe Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
geb. 11. Mai 1930 in Rotterdam&lt;br /&gt;
&lt;br /&gt;
ges. 06. August 2002&lt;br /&gt;
&lt;br /&gt;
Dijkstra war ein niederländischer Informatiker und Wegbereiter der strukturierten Programmierung. 1972 erhielt er für seine Leistung in der Technik und Kunst der Programmiersprachen den Turing Award, der jährlich von der Association for Computing Machinery (ACM) an Personen verliehen wird, die sich besonders um die Entwicklung der Informatik verdient gemacht haben. Zu seinen Beiträgen zur Informatik gehören unter anderem der Dijkstra-Algorithmus zur Berechnung des kürzesten Weges in einem Graphen sowie eine Abhandlung über den go-to-Befehl und warum er nicht benutzt werden sollte. Der go-to-Befehl war in den 60er und 70er Jahren weit verbreitet, führte aber zu Spaghetti-Code. In seinem berühmten Paper &amp;quot;A Case against the GO TO Statement&amp;quot;[http://www.cs.utexas.edu/users/EWD/ewd02xx/EWD215.PDF], das als Brief mit dem Titel &amp;quot;Go-to statement considered harmful&amp;quot; veröffentlicht wurde, argumentiert Dijkstra, dass es umso schwieriger ist, dem Quellcode eines Programmes zu folgen, je mehr go-to-Befehle darin enthalten sind und zeigt, dass man auch ohne diesen Befehl gute Programme schreiben kann.&lt;br /&gt;
&lt;br /&gt;
==== Algorithmus ====&lt;br /&gt;
&lt;br /&gt;
Der Dijkstra-Algorithmus für kürzeste Wege ist dem oben vorgestellten Algorithmus &amp;lt;tt&amp;gt;shortestPath()&amp;lt;/tt&amp;gt; auf der Basis von Breitensuche sehr ähnlich. Insbesondere gilt auch hier, dass neben dem kürzesten Weg vom Start zum Ziel auch alle kürzesten Wege gefunden werden, deren Endknoten dem Start näher sind als der Zielknoten. Aufgrund der Kantengewichte gibt es aber einen wichtigen Unterschied: Der erste gefundene Weg zu einem Knoten ist nicht mehr notwendigerweise der kürzeste. Wir bestimmen deshalb für jeden Knoten mehrere Kandidatenwege und verwenden eine Prioritätswarteschlange (statt einer einfachen First in - First out - Queue), um diese Wege nach ihrer Länge zu sortieren. Die Kandidatenwege für einen gegebenen Knoten werden unterschieden, indem wir auch den Vorgängerknoten im jeweiligen Weg speichern. Wenn ein Knoten &amp;lt;i&amp;gt;erstmals&amp;lt;/i&amp;gt; an die Spitze der Prioritätswarteschlange gelangt, haben wir den kürzesten Weg zu diesem Knoten gefunden (das wird weiter unten formal bewiesen), und der Vorgänger des Knotens in diesem Weg wird zu seinem Vaterknoten. Erscheint derselbe Knoten später nochmals an der Spitze der Prioritätswarteschlange, handelt es sich um einen Kandidatenweg, der sich nicht als kürzester erwiesen hat und deshalb ignoriert werden kann. Wir erkennen dies leicht daran, dass der Vaterknoten in der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; bereits gesetzt ist. &lt;br /&gt;
&lt;br /&gt;
Eine geeignete Datenstruktur für die Prioritätswarteschlange wird durch das Python-Modul [http://docs.python.org/library/heapq.html heapq] realisiert. Es verwendet ein normales Pythonarray als unterliegende Repräsentation für einen Heap und stellt effiziente &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt;-Funktionen zur Verfügung. Dies entspricht genau unserer Vorgehensweise im Kapitel [[Prioritätswarteschlangen]]. Als Datenelement erwartet die Funktion &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; ein Tupel, dessen erstes Element die Priorität sein muss. Die übrigen Elemente des Tupels (und damit auch deren Anzahl) können je nach Anwendung frei festgelegt werden. Wir legen fest, dass das zweite Element den Endknoten des betrachteten Weges und das dritte den Vorgängerknoten speichert. &lt;br /&gt;
&lt;br /&gt;
Die Kantengewichte werden dem Algorithmus in der property map &amp;lt;tt&amp;gt;weights&amp;lt;/tt&amp;gt; übergeben:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;code python&amp;gt;&lt;br /&gt;
    import heapq	                  # heapq implementiert die Funktionen für Heaps&lt;br /&gt;
    &lt;br /&gt;
    def dijkstra(graph, weights, startnode, destination):&lt;br /&gt;
        parents = [None]*len(graph)       # registriere für jeden Knoten den Vaterknoten im Pfadbaum&lt;br /&gt;
      &lt;br /&gt;
        q = []                            # Array q wird als Heap verwendet&lt;br /&gt;
        &amp;lt;font color=red&amp;gt;heapq.heappush(q, (0.0, startnode, startnode))&amp;lt;/font&amp;gt;  # Startknoten in Heap einfügen&lt;br /&gt;
      &lt;br /&gt;
        while len(q) &amp;gt; 0:                 # solange es noch Knoten im Heap gibt:&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;length, node, predecessor = heapq.heappop(q)&amp;lt;/font&amp;gt;   # Knoten aus dem Heap nehmen&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;if parents[node] is not None:&amp;lt;/font&amp;gt; # parent ist schon gesetzt =&amp;gt; es gab einen anderen, kürzeren Weg&lt;br /&gt;
                &amp;lt;font color=red&amp;gt;continue&amp;lt;/font&amp;gt;                  #   =&amp;gt; wir können diesen Weg ignorieren&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;parents[node] = predecessor&amp;lt;/font&amp;gt;   # parent setzen&lt;br /&gt;
            if node == destination:       # Zielknoten erreicht&lt;br /&gt;
                break                     #   =&amp;gt; Suche beenden&lt;br /&gt;
            for neighbor in graph[node]:  # die Nachbarn von node besuchen,&lt;br /&gt;
                if parents[neighbor] is None:   # aber nur, wenn ihr kürzester Weg noch nicht bekannt ist&lt;br /&gt;
                    &amp;lt;font color=red&amp;gt;newLength = length + weights[(node,neighbor)]&amp;lt;/font&amp;gt;   # berechne Pfadlänge zu neighbor              &lt;br /&gt;
                    &amp;lt;font color=red&amp;gt;heapq.heappush(q, (newLength, neighbor, node))&amp;lt;/font&amp;gt;  # und füge neighbor in den Heap ein&lt;br /&gt;
      &lt;br /&gt;
        if parents[destination] is None:  # Suche wurde beendet ohne den Zielknoten zu besuchen&lt;br /&gt;
            return None, None             # =&amp;gt; kein Pfad gefunden (unzusammenhängender Graph)&lt;br /&gt;
      &lt;br /&gt;
        # Pfad durch die parents-Kette zurückverfolgen und speichern&lt;br /&gt;
        path = [destination]&lt;br /&gt;
        while path[-1] != startnode:&lt;br /&gt;
            path.append(parents[path[-1]])&lt;br /&gt;
        path.reverse()                    # Reihenfolge umdrehen (Ziel =&amp;gt; Start wird zu Start =&amp;gt; Ziel)&lt;br /&gt;
        return path, length               # gefundenen Pfad und dessen Länge zurückgeben&lt;br /&gt;
  &amp;lt;/code&amp;gt;&lt;br /&gt;
Die wesentlichen Unterschiede zur Breitensuche sind im Code rot markiert: Anstelle der Queue verwenden wir jetzt einen Heap, und der Startknoten wird mit Pfadlänge 0 als erstes eingefügt. In der Schleife &amp;lt;tt&amp;gt;while len(q) &amp;gt; 0:&amp;lt;/tt&amp;gt; wird jeweils der Knoten &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; mit der aktuell kürzesten Pfadlänge aus dem Heap entfernt. Die Pfadlänge vom Start zu diesem Knoten wird in der Variable &amp;lt;tt&amp;gt;length&amp;lt;/tt&amp;gt; gespeichert, sein Vorgänger in der Variable &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;. Wenn der aktuelle Weg nicht der kürzeste ist (&amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; war bereits gesetzt), wird dieser Weg ignoriert. Andernfalls werden die property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; aktualisiert und die Nachbarn von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; besucht. Beim Scannen der Nachbarn berechnen wir zunächst die Länge &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; das Weges &amp;lt;tt&amp;gt;startnode =&amp;amp;gt; node =&amp;amp;gt; neighbor&amp;lt;/tt&amp;gt; als Summe von &amp;lt;tt&amp;gt;length&amp;lt;/tt&amp;gt; und dem Gewicht der Kante &amp;lt;tt&amp;gt;(node, neighbode)&amp;lt;/tt&amp;gt;. Diese Länge wird beim Einfügen des Nachbarknotens in den Heap zur Priorität des aktuellen Weges.&lt;br /&gt;
&lt;br /&gt;
Die wichtigsten Prinzipien des Dijkstra-Algorithmus noch einmal im Überblick:&lt;br /&gt;
* Der Dijkstra-Algorithmus ist Breitensuche mit Prioritätswarteschlange (Heap) statt einer einfache Warteschlange (Queue).&lt;br /&gt;
* Die Prioritätswarteschlange speichert alle Wege, die bereits gefunden worden sind und ordnet sie aufsteigend nach ihrer Länge. &lt;br /&gt;
* Das Sortieren (und damit der ganze Algorithmus) funktioniert nur mit positiven Kantengewichten korrekt.&lt;br /&gt;
* Da ein Knoten auf mehreren Wegen erreichbar sein kann, kann er auch mehrmals im Heap sein. &lt;br /&gt;
* Wenn ein Knoten &amp;lt;i&amp;gt;erstmals&amp;lt;/i&amp;gt; aus der Prioritätswarteschlange entnommen wird, ist der gefundene Weg der kürzeste zu diesem Knoten. Andernfalls wird der Weg ignoriert.&lt;br /&gt;
* Wenn der Knoten &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; aus dem Heap entnommen wird, ist der kürzeste Weg von Start nach Ziel gefunden, und die Suche kann beendet werden.&lt;br /&gt;
In unserer Implementation können, wie gesagt, mehrere Wege zum selben Knoten gleichzeitig in der Prioritätswarteschlange sein. Im Prinzip wäre es auch möglich, immer nur den besten zur Zeit bekannten Weg zu jedem Enknoten in der Prioritätswarteschlange zu halten - sobald ein besserer Kandidat gefunden wird, ersetzt er den bisherigen Kandidaten, anstatt zusätzlich eingefügt zu werden. Dies erfordert aber eine wesentlich kompliziertere Prioritätswarteschlange, die eine effiziente &amp;lt;tt&amp;gt;updatePriority&amp;lt;/tt&amp;gt;-Funktion anbietet, ohne dass dadurch eine signifikante Beschleunigung erreicht wird. Deshalb verfolgen wir diesen Ansatz nicht.&lt;br /&gt;
&lt;br /&gt;
==== Beispiel ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;under construction&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Image:Bsp.jpg]]&lt;br /&gt;
&lt;br /&gt;
==== Komplexität von Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
Zur Analyse der Komplexität nehmen wir an, dass der Graph V Knoten und E Kanten hat. Die Initialisierung der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; am Anfang der Funktion hat offensichtlich Komplexität O(V), weil Speicher für V Knoten allokiert wird. Der Code am Ende der Funktion, der aus der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; den Pfad extrahiert, hat ebenfalls die Komplexität O(V), weil der Pfad im ungünstigen Fall sämtliche Knoten des Graphen umfasst. Beides wird durch die Komplexität der Hauptschleife dominiert, zu deren Analyse wir den folgenden Codeausschnitt genauer anschauen wollen:&lt;br /&gt;
&lt;br /&gt;
      while len(q) &amp;gt; 0:&lt;br /&gt;
           ... # 1&lt;br /&gt;
           if parents[node] is not None: &lt;br /&gt;
               continue                  &lt;br /&gt;
           parents[node] = predecessor&lt;br /&gt;
           ... # 2&lt;br /&gt;
Wir erkennen, dass der Codeabschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; für jeden Knoten höchstens einmal erreicht werden kann: Da &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; beim ersten Durchlauf gesetzt wird, kann die &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage beim gleichen Knoten nie wieder &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt; liefern, und das nachfolgende &amp;lt;tt&amp;gt;continue&amp;lt;/tt&amp;gt; bewirkt, dass der Abschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; dann übersprungen wird. Man sagt auch, dass jeder Knoten &amp;lt;i&amp;gt;höchstens einmal expandiert&amp;lt;/i&amp;gt; wird, auch wenn er mehrmals im Heap war. &lt;br /&gt;
&lt;br /&gt;
Der Codeabschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; selbst enthält eine Schleife über alle ausgehenden Kanten des Knotens &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt;. Im ungünstigsten Fall iterieren wir bei &amp;lt;i&amp;gt;allen&amp;lt;/i&amp;gt; Knoten über &amp;lt;i&amp;gt;alle&amp;lt;/i&amp;gt; ausgehenden Kanten, aber das sind gerade alle Kanten des Graphen je einmal in den beiden möglichen Richtungen. Die Funktion &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; wird sogar höchstens E Mal aufgerufen, weil eine Kante nur in den Heap eingefügt wird, wenn der kürzeste Weg der jeweiligen Endknotens noch nicht bekannt ist (siehe die &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage in der &amp;lt;tt&amp;gt;for&amp;lt;/tt&amp;gt;-Schleife), und das ist nur ein einer Richtung möglich. Dies hat zwei Konsequenzen:&lt;br /&gt;
* Die Schleife &amp;lt;tt&amp;gt;while len(q) &amp;gt; 0:&amp;lt;/tt&amp;gt; wird nur so oft ausgeführt, wie Elemente im Heap sind, also höchstens E Mal. Das gleiche gilt für den Codeabschnitt &amp;lt;tt&amp;gt;# 1&amp;lt;/tt&amp;gt;, der das &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; enthält.&lt;br /&gt;
* Die Operationen &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; haben logarithmische Komplexität in der Größe des Heaps, sind also in &amp;lt;math&amp;gt;O(\log\,E)&amp;lt;/math&amp;gt;. In einfachen Graphen gilt aber &amp;lt;math&amp;gt;E = O(V^2)&amp;lt;/math&amp;gt;, so dass sich die Komplexität der Heapoperationen vereinfacht zu &amp;lt;math&amp;gt;O(\log\,E)=O(\log\,V^2)=O(2\log\,V)=O(\log\,V)&amp;lt;/math&amp;gt;.&lt;br /&gt;
Zusammenfassend gilt: &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; werden maximal E Mal aufgerufen und haben eine Komplexität in &amp;lt;math&amp;gt;O(\log\,V)&amp;lt;/math&amp;gt;. Folglich hat der Algorithmus von Dijkstra die Komplexität:&lt;br /&gt;
:&amp;lt;math&amp;gt;O(E\,\log\,V)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Vergleich mit Breitensuche und Tiefensuche ====&lt;br /&gt;
&lt;br /&gt;
Der Dijkstra-Algorithmus ist eng mit der Breiten- und Tiefensuche verwandt - man kann diese Algorithmen aus dem Dijkstra-Algorithmus gewinnen, indem man einfach die Regel zur Festlegung der Prioritäten ändert. Anstelle der Länge des Pfades verwenden wir als Priorität den Wert eine Zählvariable &amp;lt;tt&amp;gt;count&amp;lt;/tt&amp;gt;, die nach jeder Einfügung in den Heap (also nach jedem Aufruf von &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt;) aktualisiert wird. Zählen wir die Variable hoch, haben die zuerst eingefügten Kanten die höchste Priorität, der Heap verhält sich also wie eine Queue (First in-First out), und wir erhalten eine Breitensuche. Zählen wir die Variable hingegen (von E beginnend) herunter, haben die zuletzt eingefügten Kanten höchste Priorität. Der Heap verhält sich dann wie ein Stack (Last in-First out), und wir bekommen Tiefensuche. Statt eines Heaps plus Zählvariable kann man jetzt natürlich direkt eine Queue bzw. einen Stack verwenden. Dadurch fällt der Aufwand &amp;lt;math&amp;gt;O(\log\,V)&amp;lt;/math&amp;gt; für die Heapoperationen weg und wird durch die effizienten O(1)-Operationen von Queue bzw. Stack ersetzt. Damit erhalten wir für Breiten- und Tiefensuche die schon bekannte Komplexität O(E).&lt;br /&gt;
&lt;br /&gt;
==== Korrektheit von Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
Wir beweisen mittels vollständiger Induktion die Schleifen-Invariante: Falls &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; gesetzt (also ungleich &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;) ist, dann liefert das Zurückverfolgen des Weges von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; nach &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; den kürzesten Weg. &lt;br /&gt;
;Induktionsanfang: &amp;lt;tt&amp;gt;parents[startnode]&amp;lt;/tt&amp;gt; ist als einziges gesetzt. Zurückverfolgen liefert den trivialen Weg &amp;lt;tt&amp;gt;[startnode]&amp;lt;/tt&amp;gt;, der mit Länge 0 offensichtlich der kürzeste Pfad ist &amp;amp;rarr; die Bedingung ist erfüllt.&lt;br /&gt;
;Induktionsschritt: Wir zeigen mit einem indirektem Beweis, dass wir immer einen kürzesten Weg bekommen, wenn &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; gesetzt wird.&lt;br /&gt;
:Sei &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt; = &amp;lt;tt&amp;gt;{v | parents[v] is not None}&amp;lt;/tt&amp;gt; die Menge aller Knoten, von denen wir den kürzesten Weg schon kennen (Induktionsvoraussetzung), und &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; der Knoten, der sich gerade an der Spitze des Heaps befindet. Dann ist &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; der Vorgänger von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; im aktuellen Weg, und es muss &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt; gelten, weil die Nachbarn von &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; (und damit auch der aktuelle &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt;) erst in den Heap eingefügt werden, wenn der kürzeste Weg für &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; gefunden wurde. Man beachte auch, dass alle Knoten, die noch nicht in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt; enthalten sind, weiter vom Start entfernt sind als alle Knoten in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt;, weil alle neu in den Heap eingefügten Wege länger sind als der kürzeste Weg des jeweiligen Vorgängers. &lt;br /&gt;
:Der indirekte Beweis nimmt jetzt an, dass der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; nicht der kürzeste Weg ist. Dann muss es einen anderen, kürzeren Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; geben. Für den Vorgänger &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; in diesem Weg unterscheiden wir zwei Fälle:&lt;br /&gt;
:* &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt;: In diesem Fall ist die Länge des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; bereits bekannt, und dieser Weg ist in der Prioritätswarteschlange enthalten. Dann kann er aber nicht der kürzeste sein, denn an der Spitze der Warteschlange war nach Voraussetzung der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt;.&lt;br /&gt;
:* &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\notin S&amp;lt;/math&amp;gt;: Die Kosten des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; berechnen sich als &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) + weight[(x, node)]&amp;lt;/tt&amp;gt;, und die Kosten des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; sind analog &amp;lt;tt&amp;gt;Kosten(predecessor &amp;amp;rarr; startnode) + weight[(predecessor, node)]&amp;lt;/tt&amp;gt;. Aufgrund der Induktionsvoraussetzung gilt aber &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt;, und somit &amp;lt;tt&amp;gt;Kosten(predecessor &amp;amp;rarr; startnode) &amp;amp;lt; Kosten(x &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt;, weil &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; andernfalls vor &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; an der Spitze des Heaps gewesen wäre, was mit der Annahme &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\notin S&amp;lt;/math&amp;gt; unverträglich ist. Damit der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; trotzdem der kürzeste Weg sein kann, müsste &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) &amp;amp;lt; Kosten(node &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt; gelten, denn durch die Kante &amp;lt;tt&amp;gt;(x, node)&amp;lt;/tt&amp;gt; kommen ja noch Kosten hinzu. Das wäre aber nur möglich, wenn der Knoten &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; vor dem Knoten &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; an die Spitze des Heaps gelangt, im Widerspruch zur Annahme, dass &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; sich gerade an der Spitze des Heaps befindet. Somit kann die Behauptung, dass der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; der kürzeste Weg ist, nicht stimmen.&lt;br /&gt;
In beiden Fällen erhalten wir einen Widerspruch, und die Behauptung ist somit bewiesen. Da die Invariante insbesondere für den Weg zum Zielknoten &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; erfüllt ist, folgt daraus auch die Korrektheit des Algorithmus von Dijkstra.&lt;br /&gt;
&lt;br /&gt;
===  A*-Algorithmus - Wie kann man Dijkstra noch verbessern? ===&lt;br /&gt;
&lt;br /&gt;
Eine wichtige Eigenschaft des Dijkstra-Algorithmus ist, dass neben dem kürzesten Weg vom Start zum Ziel auch die kürzesten Wege zu allen Knoten berechnet werden, die näher am Startknoten liegen als das Ziel, obwohl uns diese Wege gar nicht interessieren. Sucht man beispielsweise in einem Graphen mit den Straßenverbindungen in Deutschland den kürzesten Weg von Frankfurt (Main) nach Dresden (ca. 460 km), werden auch die kürzesten Wege von Frankfurt nach Köln (190 km), Dortmund (220 km) und Stuttgart (210 km) und vielen anderen Städten gefunden. Aufgrund der geographischen Lage dieser Städte ist eigentlich von vornherein klar, dass sie mit dem kürzesten Weg nach Dresden nicht das geringste zu tun haben. Anders sieht es mit Erfurt (260 km) oder Suhl (210 km) aus - diese Städte liegen zwischen Frankfurt und Dresden und kommen deshalb als Zwischenstationen des gesuchten Weges in Frage.&lt;br /&gt;
&lt;br /&gt;
Damit Dijkstra korrekt funktioniert, würde es im Prinzip ausreichen, wenn man die kürzesten Wege nur für diejenigen Knoten ausrechnet, die auf dem kürzesten Weg vom Start zum Ziel liegen, denn nur diese Knoten braucht man, um den gesuchten Weg über die &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette zurückzuverfolgen. Das Problem ist nur, dass man diese Knoten erst kennt, wenn der Algorithmus fertig durchgelaufen ist. Schließt man Knoten zu früh von der Betrachtung aus, kommt am Ende möglicherweise nicht der korrekte kürzeste Weg heraus. &lt;br /&gt;
&lt;br /&gt;
Der A*-Algorithmus löst dieses Dilemma mit folgender Idee: Ändere die Prioritäten für den Heap so ab, dass unwichtige Knoten nur mit geringerer Wahscheinlichkeit expandiert werden, aber stelle gleichzeitig sicher, dass alle wichtigen Knoten (also diejenigen auf dem korrekten kürzesten Weg) auf jeden Fall expandiert werden. Es zeigt sich, dass man diese Idee umsetzen kann, wenn eine &amp;lt;i&amp;gt;Schätzung für den Restweg&amp;lt;/i&amp;gt; (also für die noch verbleibende Entfernung von jedem Knoten zum Ziel) verfügbar ist:&lt;br /&gt;
 rest = guess(neighbor, destination)&lt;br /&gt;
Diese Schätzung addiert man einfach zur wahren Länge des Weges &amp;lt;tt&amp;gt;startnode &amp;amp;rarr; node&amp;lt;/tt&amp;gt; dazu, um die verbesserte Priorität zu erhalten:&lt;br /&gt;
 priority = newLength + guess(neighbor, destination)&lt;br /&gt;
(Im originalen Dijkstra-Algorithmus wird als Priorität nur &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; allein verwendet. Man beachte, dass man &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; jetzt zusätzlich im Heap speichern muss, weil man es für die Expansion des Knotens später noch benötigt.)&lt;br /&gt;
&lt;br /&gt;
Damit sicher gestellt ist, dass der A*-Algorithmus immer noch die korrekten kürzesten Wege findet, darf die Schätzung den wahren Restweg &amp;lt;i&amp;gt;niemals überschätzen&amp;lt;/i&amp;gt;. Es muss immer gelten:&lt;br /&gt;
 0 &amp;lt;= guess(node, destination) &amp;lt;= trueDistance(node, destination)&lt;br /&gt;
Damit gilt insbesondere &amp;lt;tt&amp;gt;guess(destination, destination) = trueDistance(destination, destination) = 0&amp;lt;/tt&amp;gt;, an der Priorität des Knotens &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; ändert sich also nichts. Die Prioritäten aller anderen Knoten veschlechtern sich hingegen, weil zur bisherigen Priorität noch atwas addiert wird. Für die wichtigen Knoten auf dem kürzesten Weg vom Start nach Ziel gilt jedoch, dass deren neue Priorität immer noch besser ist als die Priorität des Zielknotens selbst. Für diese Knoten gilt nämlich&lt;br /&gt;
 falls node auf dem kürzesten Weg von startnode nach destination liegt:&lt;br /&gt;
 trueDistance(startnode, node) + guess(node, destination) &amp;lt;= trueDistance(startnode, destination)&lt;br /&gt;
weil der Weg von Start nach &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; ein Teil des kürzesten Wegs von Start nach Ziel ist und die Restschätzung die wahre Entfernung immer unterschätzt. Diese Knoten werden deshalb stets vor dem Zielknoten expandiert, so dass wir die &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette immer noch korrekt zurückverfolgen können. Für alle anderen Knoten gilt idealerweise, dass die neue Priorität schlechter ist als die Priorität von &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt;, so dass man sich diese irrelevanten Knotenexpansionen sparen kann.&lt;br /&gt;
&lt;br /&gt;
Für das Beispiel eines Straßennetzwerks bietet sich als Schätzung die Luftlinienentfernung an, weil Straßen nie kürzer sein können als die Luftlinie. Damit erreicht man in der Praxis deutliche Einsparungen. Generell gilt, dass der A*-Algorithmus im typischen Fall schneller ist als der Algorithmus von Dijkstra, aber man kann immer pathologische Fälle konstruieren, wo die Änderung der Prioritäten nichts bringt. Die Komplexität des A*-Algorithmus im ungünstigen Fall ist deshalb nach wie vor &amp;lt;math&amp;gt;O(E\,\log\,V)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=='''Minimaler Spannbaum'''==&lt;br /&gt;
'''(engl.: minimum spanning tree; abgekürzt: MST)'''&lt;br /&gt;
&lt;br /&gt;
[[Image:Minimum_spanning_tree.png‎ |thumb|200px|right|Ein minimal aufspannender Baum verbindet alle Punkte eines Graphen bei minimaler Kantenlänge ([http://de.wikipedia.org/wiki/Spannbaum Quelle])]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: gewichteter Graph G, zusammenhängend&amp;lt;br/&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: Untermenge &amp;lt;math&amp;gt;E'\subseteq E&amp;lt;/math&amp;gt; der Kanten, so dass die Summe der Kantengewichte &amp;lt;math&amp;gt;\sum_{e\in E'} w_e&amp;lt;/math&amp;gt; minimal und der entstehende Graph G' zusammenhängend ist.&amp;lt;br/&amp;gt;&lt;br /&gt;
* G' definiert immer einen Baum, denn andernfalls könnte man eine Kante weglassen und dadurch die Summe &amp;lt;math&amp;gt;\sum_{e\in E'} w_e&amp;lt;/math&amp;gt; verringern, ohne dass sich am Zusammenhang von G' etwas ändert. &amp;lt;br/&amp;gt;&lt;br /&gt;
* Wenn der Graph G nicht zusammenhängend ist, kann man den Spannbaum für jede Zusammenhangskomponente getrennt ausrechnen. Man erhält dann einen aufspannenden Wald. &lt;br /&gt;
* Der MST ist ähnlich wie der Dijkstra-Algorithmus: Dort ist ein Pfad gesucht, bei dem die Summe der Gewichte über den Pfad minimal ist. Beim MST suchen wir eine Lösung, bei der die Summe der Gewichte über den ganzen Graphen minimal ist. &lt;br /&gt;
* Das Problem des MST ist nahe verwandt mit der Bestimmung der Zusammenhangskomponente, z.B. über den Tiefensuchbaum. Für die Zusammenhangskomponenten genügt allerdings ein beliebiger Baum, während beim MST ein minimaler Baum gesucht ist.&lt;br /&gt;
&lt;br /&gt;
=== Anwendungen ===&lt;br /&gt;
==== Wie verbindet man n gegebene Punkte mit möglichst kurzen Straßen (Eisenbahnen, Drähten [bei Schaltungen] usw.)?====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:center&amp;quot; border=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; cellspacing=&amp;quot;0&amp;quot; &lt;br /&gt;
|MST minimale Verbindung (Abb.1)&lt;br /&gt;
|MST = 2 (Länge = Kantengewicht)(Abb.2)&lt;br /&gt;
|- valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| [[Image:mst.png]] &lt;br /&gt;
| [[Image:Gleichseitigesdreieck.png]]&lt;br /&gt;
|}&lt;br /&gt;
*In der Praxis: Die Festlegung, dass man nur die gegebenen Punkte verwenden darf, ist eine ziemliche starke Einschränkung. &lt;br /&gt;
&lt;br /&gt;
* Wenn man sich vorstellt, es sind drei Punkte gegeben, die als gleichseitiges Dreieck angeordnet sind, dann ist der MST (siehe Abb.2, schwarz gezeichnet) und hat die Länge 2. Man kann hier die Länge als Kantengewicht verwenden. &lt;br /&gt;
&lt;br /&gt;
* Wenn es erlaubt ist zusätzliche Punkte einzufügen, dann kann man in der Mitte einen neuen Punkt setzen &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; neuer MST (siehe Abb.2, orange gezeichnet).&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Höhe = &amp;lt;math&amp;gt;\frac{1}{2}\sqrt{3}&amp;lt;/math&amp;gt;, Schwerpunkt: teilt die Höhe des Dreiecks im Verhältnis 2:1; der Abstand von obersten Punkt bis zum neu eingeführten Punkt: &amp;lt;math&amp;gt;\frac{2}{3}h = \frac{\sqrt{3}}{3}&amp;lt;/math&amp;gt;, davon insgesamt 3 Stück, damit (gilt für den MST in orange eingezeichnet): MST = &amp;lt;math&amp;gt;3\left(\frac{1}{3}\right) \sqrt{3} = \sqrt{3} \approx 1,7&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Damit ist der MST in orange kürzer als der schwarz gezeichnete MST. &amp;lt;br\&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt;Folgerung: MST kann kürzer werden, wenn man einen Punkt dazu nimmt. &lt;br /&gt;
* Umgekehrt kann der MST auch kürzer werden, wenn man einen Punkt aus dem Graphen entfernt, aber wie das Beipiel des gleichseitigen Dreiecks zeigt, ist dies nicht immer der Fall.&lt;br /&gt;
&lt;br /&gt;
[[Image: bahn.png|Bahnstrecke Verbindung (Abb.3)]]&lt;br /&gt;
&lt;br /&gt;
* Methode der zusätzlichen Punkteinfügung hat man früher beim Bahnstreckenbau verwendet. Durch Einführung eines Knotenpunktes kann die Streckenlänge verkürzt werden (Dreiecksungleichung).&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Bestimmung von Datenclustern ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Image:cluster.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Daten (in der Abb.: Punkte) bilden Gruppen. &lt;br /&gt;
&lt;br /&gt;
* In der Abbildung hat man 2 verschiedene Messungen gemacht (als x- und y-Achse aufgetragen), bspw. Größe und Gewicht von Personen. Für jede Person i wird ein Punkt an der Koordinate (Größe&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;, Gewicht&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;) gezeichnet (siehe Bild a). Dies bezeichnet man als ''Scatter Plot''. Wenn bestimmte Wertkombinationen häufiger auftreten als andere, bilden sich mitunter Gruppen aus, bspw. eine Gruppe für &amp;quot;klein und schwer&amp;quot; etc.&lt;br /&gt;
&lt;br /&gt;
* Durch Verbinden der Punkte mittels eines MST (siehe Abbildung (b)) sieht man, dass es kurze (innerhalb der Gruppen) und lange Kanten (zwischen den Gruppen) gibt. &lt;br /&gt;
&lt;br /&gt;
* Wenn man geschickt eine Schwelle einführt und alle Kanten löscht, die länger sind als die Schwelle, dann bekommt man als Zusammenhangskomponente die einzelnen Gruppen. &lt;br /&gt;
&lt;br /&gt;
=== Algorithmen ===&lt;br /&gt;
&lt;br /&gt;
Genau wie bei der Bestimmung von Zusammenhangskomponenten kann man auch das MST-Problem entweder nach dem Anlagerungsprinzip oder nach dem Verschmelzungsprinzip lösen (dazu gibt es noch weitere Möglichkeiten, z.B. den [http://de.wikipedia.org/wiki/Algorithmus_von_Bor%C5%AFvka Algorithmus von Boruvka]). Der Anlagerungsalgorithmus für MST wurde zuerst von Prim beschrieben und trägt deshalb seinen Namen, der Verschmelzungsalgorithmus stammt von Kruskal. Im Vergleich zu den Algorithmen für Zusammenhangskomponenten ändert sich im wesentlichen nur die Reihenfolge, in der die Kanten betrachtet werden: Eine Prioritätswarteschlange stellt jetzt sicher, dass am Ende wirklich der Baum mit den geringstmöglichen Kosten herauskommt.&lt;br /&gt;
&lt;br /&gt;
====Algorithmus von Prim====&lt;br /&gt;
[http://de.wikipedia.org/wiki/Algorithmus_von_Prim Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus von Prim geht nach dem Anlagerungsprinzip vor (vgl. den Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Tiefensuche|Zusammenhangskomponenten mit Tiefensuche]]): Starte an der Wurzel (ein willkürlich gewählter Knoten) und füge jeweils die günstigste Kante an die aktuellen Teillösung an, die keinen Zyklus verursacht. Die Sortierung der Kanten nach Priorität erfolgt analog zum Dijsktra-Algorithmus, aber die Definitionen, welche Kante die günstigste ist, unterscheiden sich. Die Konvention für die Bedeutung der Elemente des Heaps ist ebenfalls identisch: ein Tupel mit &amp;lt;tt&amp;gt;(priority, node, predecessor)&amp;lt;/tt&amp;gt;. Die folgende Implementation verdeutlicht sehr schön die Ähnlichkeit der beiden Algorithmen. Das Ergebnis wird als property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; zurückgegeben, in der für jeden Knoten sein Vorgänger im MST steht, wobei die Wurzel wie üblich auf sich selbst verweist.&lt;br /&gt;
&lt;br /&gt;
 import heapq&lt;br /&gt;
 &lt;br /&gt;
 def prim(graph, weights):                # Kantengewichte wie bei Dijkstra als property map&lt;br /&gt;
 	sum = 0.0                         # wird später das Gewicht des Spannbaums sein&lt;br /&gt;
 	start = 0                         # Knoten 0 wird willkürlich als Wurzel gewählt&lt;br /&gt;
        &lt;br /&gt;
        parents = [None]*len(graph)       # property map, die den resultierenden Baum kodiert&lt;br /&gt;
        parents[start] = start            # Wurzel zeigt auf sich selbst&lt;br /&gt;
        &lt;br /&gt;
 	heap = []                         # Heap für die Kanten des Graphen&lt;br /&gt;
 	for neighbor in graph[start]:     # besuche die Nachbarn von start&lt;br /&gt;
 	    heapq.heappush(heap, (weights[(start, neighbor)], neighbor, start))  # und fülle Heap &lt;br /&gt;
 	&lt;br /&gt;
        while len(heap) &amp;gt; 0:&lt;br /&gt;
 	    w, node, predecessor = heapq.heappop(heap) # hole billigste Kante aus dem Heap&lt;br /&gt;
            if parents[node] is not None: # die Kante würde einen Zyklus verursachen&lt;br /&gt;
                continue                  #   =&amp;gt; ignoriere diese Kante&lt;br /&gt;
            parents[node] = predecessor   # füge Kante in den MST ein&lt;br /&gt;
            sum += w                      # und aktualisiere das Gesamtgewicht &lt;br /&gt;
            for neighbor in graph[node]:  # besuche die Nachbarn von node&lt;br /&gt;
                if parents[neighbor] is None:  # aber nur, wenn kein Zyklus entsteht&lt;br /&gt;
                    heapq.heappush(heap, (weights[(node,neighbor)], neighbor, node)) # füge Kandidaten in Heap ein&lt;br /&gt;
        &lt;br /&gt;
 	return parents, sum               # MST und Gesamtgewicht zurückgeben&lt;br /&gt;
&lt;br /&gt;
====Algorithmus von Kruskal====&lt;br /&gt;
[http://de.wikipedia.org/wiki/Algorithmus_von_Kruskal Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Kruskal%27s_algorithm (en)]&lt;br /&gt;
&lt;br /&gt;
Die alternative Vorgehensweise ist das Verschmelzungsprinzip (vgl. den Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Union-Find-Algorithmus|Zusammenhangskomponenten mit Union-Find-Algorithmus]]), das der Algorithmus von Kruskal verwendet. Jeder Knoten wird zunächst als trivialer Baum mit nur einem Knoten betrachtet, und alle Kanten werden aufsteigend nach Gewicht sortiert. Dann wird die billigste noch nicht betrachtete Kante in den MST eingefügt, falls sich dadurch kein Zyklus bildet (erkennbar daran, dass die Endknoten in verschiedenen Zusammenhangskomponenten liegen, das heisst verschiedene Anker haben). Da der fertige Baum (V-1) Kanten haben muss, wird dies (V-1) Mal zutreffen. Andernfalls wird diese Kante ignoriert. Anders ausgedrückt: Der Algorithmus beginnt mit ''V'' Bäumen; in (''V''-1) Verschmelzungsschritten kombiniert er jeweils zwei Bäume (unter Verwendung der kürzesten möglichen Kante), bis nur noch ein Baum übrig bleibt. Der einzige Unterschied zum einfachen Union-Find besteht darin, dass die Kanten in aufsteigender Reihenfolge betrachtet werden müssen, was wir hier durch eine Prioritätswarteschlange realisieren. Der Algorithmus von J.Kruskal ist seit 1956 bekannt. &lt;br /&gt;
&lt;br /&gt;
 def kruskal(graph, weights):&lt;br /&gt;
     anchors = range(len(graph))           # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     results = []                          # result wird später die Kanten des MST enthalten    &lt;br /&gt;
     &lt;br /&gt;
     heap = []                             # Heap zum Sortieren der Kanten nach Gewicht&lt;br /&gt;
     for edge, w in weights.iteritems():   # alle Kanten einfügen&lt;br /&gt;
         heapq.heappush(heap, (w, edge))&lt;br /&gt;
     &lt;br /&gt;
     while len(heap) &amp;gt; 0:                  # solange noch Kanten vorhanden sind&lt;br /&gt;
         w, edge = heapq.heappop(heap)     # billigste Kante aus dem Heap nehmen&lt;br /&gt;
         a1 = findAnchor(anchors, edge[0]) # Anker von Startknoten der Kante&lt;br /&gt;
         a2 = findAnchor(anchors, edge[1]) # ... und Endknoten bestimmen&lt;br /&gt;
         if a1 != a2:                      # wenn die Knoten in verschiedenen Komponenten sind&lt;br /&gt;
             anchors[a2] = a1              # Komponenten verschmelzen&lt;br /&gt;
             result.append(edge)           # ... und Kante in MST einfügen&lt;br /&gt;
     &lt;br /&gt;
     return result                         # Kanten des MST zurückgeben&lt;br /&gt;
&lt;br /&gt;
Die Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt; wurde im Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Union-Find-Algorithmus|Zusammenhangskomponenten mit Union-Find-Algorithmus]] implementiert. Im Unterschied zum Algorithmus von Prim geben wir hier nicht die property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; zurück, sondern einfach eine Liste der Kanten im MST.&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus eignet sich insbesondere für das Clusteringproblem, da der Schwellwert von vornerein als maximales Kantengewicht an den Algorithmus übergeben werden kann. Man hört mit dem Vereinigen auf, wenn das Gewicht der billigste Kante im Heap den Schwellwert überschreitet. Beim Algorithmus von Kruskal kann dann keine bessere Kante als der Schwellwert mehr kommen, da die Kanten vorher sortiert worden sind. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Komplexität:&amp;lt;/b&amp;gt; wie beim Dijkstra-Algorithmus, weil jede Kante genau einmal in den Heap kommt. Der Aufwand für das Sortieren ist somit &amp;lt;math&amp;gt;O\left(E\log E\right)&amp;lt;/math&amp;gt;, was sich zu &amp;lt;math&amp;gt;O \left(E\,\log\,V\right)&amp;lt;/math&amp;gt; reduziert, falls keine Mehrfachkanten vorhanden sind.&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; geeignet für Übungsaufgabe&lt;br /&gt;
&lt;br /&gt;
====Verwendung einer BucketPriorityQueue====&lt;br /&gt;
&lt;br /&gt;
Beide Algorithmen zur Bestimmung des minimalen Spannbaums benötigen eine Prioritätswarteschlange. Wenn die Kantengewichte ganze Zahlen im Bereich &amp;lt;tt&amp;gt;0...(m-1)&amp;lt;/tt&amp;gt; sind, kann man die MST-Algorithmen deutlich beschleunigen, wenn man anstelle des Heaps eine [[Prioritätswarteschlangen#Prioritätssuche mit dem Bucket-Prinzip|&amp;lt;tt&amp;gt;BucketPriorityQueue&amp;lt;/tt&amp;gt;]] verwendet. Die Operationen zum Einfügen einer Kante in die Queue und zum Entfernen der billibsten Kante aus der Queue beschleunigen sich dadurch auf O(1) statt O(log V) (außer wenn die Gewichte sehr ungünstig auf die Kanten verteilt sind). In der Praxis erreicht man durch diese Änderung typischerweise deutliche Verbesserungen. In der Bildverarbeitung können die Prioritäten beispielsweise die Wahrscheinlichkeit kodieren, dass zwei benachbarte Pixel zu verschiedenen Objekten gehören. Bildet man jetzt den MST, und bricht bei einer bestimmten Wahrscheinlichkeit ab, erhält man Cluster von Pixeln, die wahrscheinlich zum selben Objekt gehören (weil der MST ja die Kanten mit minimalem Gewicht bevorzugt, und kleine Gewichte bedeuten kleine Wahrscheinlichkeit, dass benachbarte Pixel von einander getrennt werden). Da man die Wahrscheinlichkeiten nur mit einer Genauigkeit von ca. 1% berechnen kann, reichen hiefür 100 bis 200 Quantisierungstufen aus. Durch Verwendung der schnellen &amp;lt;tt&amp;gt;BucketPriorityQueue&amp;lt;/tt&amp;gt; kann man jetzt wesentlich größere Bilder in akzeptabler Zeit bearbeiten als dies mit einem Heap möglich wäre.&lt;br /&gt;
&lt;br /&gt;
== Algorithmen für gerichtete Graphen ==&lt;br /&gt;
&lt;br /&gt;
Zur Erinnerung: in einem gerichteten Graphen sind die Kanten (i &amp;amp;rarr; j) und (j &amp;amp;rarr; i) voneinander verschieden, und eventuell existiert nur eine der beiden Richtungen. Im allgemeinen unterscheidet sich der [[Graphen_und_Graphenalgorithmen#transposed_graph|transponierte Graph]] G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; also vom Originalgraphen G. Beim Traversieren des Graphen und bei der Pfadsuche dürfen Kanten nur in passender Richtung verwendet werden. Bei gewichteten Graphen tritt häufig der Fall auf, dass zwar Kanten in beiden Richtungen existieren, diese aber unterschiedliche Gewichte haben.&lt;br /&gt;
&lt;br /&gt;
Gerichtete Graphen ergeben sich in natürlicher Weise aus vielen Anwendungsproblemen:&lt;br /&gt;
* Routenplanung&lt;br /&gt;
** Bei Straßennetzwerken enstehen gerichtete Graphen, sobald es Einbahnstraßen gibt.&lt;br /&gt;
** Verwendet man Gewichte, um die erwarteten Fahrzeiten entlang einer Straße zu kodieren, gibt es Asymmetrien z.B. dann, wenn Straßen in einer Richtung bergab, in der anderen bergauf befahren werden. Hier existieren zwar Kanten in beiden Richtungen, sie haben aber unterschiedliche Gewichte. Ähnliches gilt für Flüge: Durch den Gegenwind des Jetstreams braucht man von Frankfurt nach New York länger als umgekehrt von New York nach Frankfurt.&lt;br /&gt;
* zeitliche oder kausale Abhängigkeiten&lt;br /&gt;
** Wenn die Knoten Ereignisse repräsentieren, von denen einige die Ursache von anderen sind, diese wiederum die Ursache der nächsten usw., verbindet man die Knoten zweckmäßig durch gerichtete Kanten, die die Kausalitätsbeziehungen kodieren. Handelt es sich um logische &amp;quot;wenn-dann&amp;quot;-Regeln, erhält man einen [[Graphen_und_Graphenalgorithmen#Anwendung:_Das_Erf.C3.BCllbarkeitsproblem_in_Implikationengraphen|Implikationengraph]] (siehe unten). Handelt es sich hingegen um Wahrscheinlichkeitsaussagen (&amp;quot;Wenn das Wetter schön ist, haben Studenten tendenziell gute Laune, wenn eine Prüfung bevorsteht eher schlechte usw.&amp;quot;), erhält man ein [http://de.wikipedia.org/wiki/Bayessches_Netz Bayessches Netz].&lt;br /&gt;
** Wenn bestimmte Aufgaben erst begonnen werden können, nachdem andere Aufgaben erledigt sind, erhält man einen Abhängigkeitsgraphen. Beispielsweise dürfen Sie erst an der Klausur teilnehmen, nachdem Sie die Übungsaufgaben gelöst haben, und Sie dürfen erst die Abschlussarbeit beginnen, nachdem Sie bestimmte Prüfungen bestanden haben. Ein anderes schönes Beispiel liefern die Regeln für das [[Graphen_und_Graphenalgorithmen#Anwendung:_Abh.C3.A4ngigkeitsgraph|Ankleiden]] weiter unten.&lt;br /&gt;
** Gerichtete Graphen kodieren die Abhängigkeiten zwischen Programmbibliotheken. Beispielsweise benötigt das Pythonmodul &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; die internen Submodule &amp;lt;tt&amp;gt;json.encoder&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;json.decode&amp;lt;/tt&amp;gt; sowie das externe Modul &amp;lt;tt&amp;gt;decimal&amp;lt;/tt&amp;gt;. Die Submodule benötigen wiederum die externen Module &amp;lt;tt&amp;gt;re&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;sys&amp;lt;/tt&amp;gt;, das Modul &amp;lt;tt&amp;gt;decimal&amp;lt;/tt&amp;gt; braucht &amp;lt;tt&amp;gt;copy&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;collections&amp;lt;/tt&amp;gt; usw.&lt;br /&gt;
** Das Internet kann als gerichteter Graph dargestellt werden, wobei die Webseiten die Knoten, und die Hyperlinks die Kanten sind.&lt;br /&gt;
* Sequence Alignment&lt;br /&gt;
** Eine gute Rechtschreibprüfung markiert nicht nur fehlerhafte Wörter, sondern macht auch plausible Vorschläge, was eigentlich gemeint gewesen sein könnte. Dazu muss sie das gegebene Wort mit den Wörtern eines Wörterbuchs vergleichen und die Ähnlichkeit bewerten. Ein analoges Problem ergibt sich, wenn man DNA Fragmente mit der Information in einer Genomdatenbank abgleichen will. &lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Sequence Alignment / Edit Distance ===&lt;br /&gt;
&lt;br /&gt;
:gegeben: zwei Wörter (allgemein: beliebige Zeichenfolgen)&lt;br /&gt;
:gesucht: Wie kann man die Buchstaben am besten in Übereinstimmung bringen?&lt;br /&gt;
&lt;br /&gt;
:Beispiel: WORTE – NORDEN&lt;br /&gt;
&lt;br /&gt;
Zwei mögliche Alignments sind&lt;br /&gt;
&lt;br /&gt;
  W&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;OR&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;T&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;E&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;.          W.ORTE&lt;br /&gt;
  N&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;OR&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;D&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;E&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;N          NORDEN&lt;br /&gt;
&lt;br /&gt;
wobei der Punkt anzeigt, dass der untere Buchstabe keinen Partner hat, und rote Buchstaben oben und unten übereinstimmen. Jede Nicht-Übereinstimmung verursacht nun gewisse Kosten. Dabei unterscheiden wir zwei Fälle:&lt;br /&gt;
# Matche a[i] mit b[j]. Falls a[i] == b[j], ist das gut (rote Buchstaben), und es entstehen keine Kosten. Andernfalls entstehen Kosten U (schwarze Buchstaben).&lt;br /&gt;
# Wir überspringen a[i] oder b[j] (Buchstabe vs. Punkt). Dann entstehen Kosten V. (Manchmal unterscheidet man auch noch Kosten Va und Vb, wenn das Überspringen bei a und b unterschieldiche Signifikanz hat.)&lt;br /&gt;
&lt;br /&gt;
Gesucht ist nun das &amp;lt;b&amp;gt;Alignment mit minimalen Kosten&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Diese Aufgabe kann man sehr schön als gerichteten Graphen darstellen: Wir definieren ein rechteckiges Gitter und schreiben das erste Wort über das Gitter und das andere links davon. Die Gitterpunkte verbinden wir mit Pfeilen (gerichteten Kanten), wobei ein Pfeil nach rechts bedeutet, dass wir beim oberen Wort einen Buchstaben überspringen, ein Pfeil nach unten, dass wir beim linken Wort einen Buchstaben überspringen, und ein diagonaler Pfeil, dass wir zwei Buchstaben matchen (und zwar die am Pfeilende). Die Farben der Pfeile symbolisieren die Kosten: rot für das Überspringen eines Buchstabens (Kosten V), blau für das Matchen, wenn die Buchstaben nicht übereinstimmen (Kosten U), und grün, wenn die Buchstaben übereinstimmen (keine Kosten). &lt;br /&gt;
&lt;br /&gt;
[[Image:sequence-alignment.png|300px]]&lt;br /&gt;
&lt;br /&gt;
Lösung:&lt;br /&gt;
:Suche den kürzesten Pfad vom Knoten &amp;quot;START&amp;quot; (oben links) nach unten rechts. Dazu kann der [[Graphen und Graphenalgorithmen#Algorithmus von Dijkstra|Algorithmus von Dijkstra]] verwendet werden, der auf gerichteten Graphen genauso funktioniert wie auf ungerichteten.&lt;br /&gt;
&lt;br /&gt;
Für unser Beispiel von oben erhalten wir die folgenden Pfade:&lt;br /&gt;
&lt;br /&gt;
[[Image:sequence-alignment-weg1.png|400px]]&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;[[Image:sequence-alignment-weg2.png|400px]]&lt;br /&gt;
&lt;br /&gt;
Durch Addieren der Kosten entsprechend der Farben sieht man, dass der erste Weg die Kosten 2U+V und der zweite die Kosten 5U+V hat. Der erste Weg ist offensichtlich günstiger und entspricht dem besten Alignment.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Abhängigkeitsgraph ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Beispiel: &amp;lt;/b&amp;gt; Wie erklärt man einem zerstreuten Professor, wie er sich morgens anziehen soll? Der folgende Graph enthält einen Knoten für jede Aktion, und eine Kante (i &amp;amp;rarr; j) bedeutet, dass die Aktion i vor der Aktion j abgeschlossen werden muss.&lt;br /&gt;
&lt;br /&gt;
[[Image:anziehen-graph.png|600px]]&lt;br /&gt;
&lt;br /&gt;
In derartigen Abhängigkeitsgraphen ist die wichtigste Frage immer, ob der Graph azyklisch ist. Wäre dies nämlich nicht der Fall, kann es keine Reihenfolge der Aktionen geben, die alle Abhängigkeiten erfüllt. Dies sieht man leicht, wenn man den einfachsten möglichen Zyklus betrachtet: es gibt sowohl eine Kante (i &amp;amp;rarr; j) als auch eine (j &amp;amp;rarr; i). Dann müsste man i vor j erledigen, aber ebenso j vor i, was offensichtlich unmöglich ist - das im Graph kodierte Problem ist dann unlösbar. Wegen ihrer Wichtigkeit wird für gerichtete azyklische Graphen oft die Abkürzung &amp;lt;b&amp;gt;DAG&amp;lt;/b&amp;gt; (von &amp;lt;i&amp;gt;directed acyclic graph&amp;lt;/i&amp;gt;) verwendet. Ein Graph ist genau dann ein DAG, wenn es eine topologische Sortierung gibt:&lt;br /&gt;
;topologische Sortierung: Zeichne die Knoten so auf eine Gerade, dass alle Kanten (Pfeile) nach rechts zeigen. &lt;br /&gt;
Arbeitet man die Aktionen nach einer (beliebigen) topologischen Sortierung ab, werden automatisch alle Abhängigkeiten eingehalten: Da alle Pfeile nach rechts zeigen, werden abhängige Aktionen immer später ausgeführt. Die topologische Sortierung ist im allgemeinen nicht eindeutig. Die folgende Skizze zeigt eine mögliche topologische Sortierung für das Anziehen:&lt;br /&gt;
&lt;br /&gt;
[[Image:anziehen-topologische-sortierung.png|600px]]&lt;br /&gt;
&lt;br /&gt;
Eine solche fest vorgegebene Reihenfolge ist für den zerstreuten Professor sicherlich eine größere Hilfe als der ursprüngliche Graph. Man erkennt, dass die Sortierung nicht eindeutig ist, beispielsweise bei der Uhr: Da für die Uhr keine Abhängigkeiten definiert sind, kann man diese Aktion an beliebiger Stelle einsortieren. Hier wurde willkürlich die letzte Stelle gewählt.&lt;br /&gt;
&lt;br /&gt;
==== Zwei Algorithmen zum Finden der topologischen Sortierung ====&lt;br /&gt;
&lt;br /&gt;
Die folgenden Algorithmen finden entweder eine topologische Sortierung, oder signalisieren, dass der Graph zyklisch ist.&lt;br /&gt;
&lt;br /&gt;
===== Algorithmus 1 =====&lt;br /&gt;
# Suche einen Knoten mit Eingangsgrad 0 (ohne eingehende Pfeile) =&amp;gt; in einem gerichteten azyklischen Graphen gibt es immer einen solchen Knoten&lt;br /&gt;
# Platziere diesen Knoten auf der Geraden (beliebig)&lt;br /&gt;
# Entferne den Knoten aus dem Graphen zusammen mit den ausgehenden Kanten&lt;br /&gt;
# Gehe zu 1., aber platziere in 2. immer rechts der Knoten, die schon auf der Geraden vorhanden sind.&lt;br /&gt;
: =&amp;gt; Wenn noch Knoten übrig sind, aber keiner Eingangsgrad 0 hat, muss der Graph zyklisch sein.&lt;br /&gt;
&lt;br /&gt;
[[Image:bild6.JPG]]&lt;br /&gt;
&lt;br /&gt;
Beispiel für einen zyklischen Graphen: kein Knoten hat Eingangsgrad 0.&lt;br /&gt;
&lt;br /&gt;
Um den Algorithmus zu implementieren, verwenden wir eine property map &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt;, die wir in einem ersten Durchlauf durch den Graphen füllen und die dann für jeden Knoten die Anzahl der eingehenden Kanten speichert. Dann gehen wir sukzessive zu allen Knoten mit &amp;lt;tt&amp;gt;in_degree == 0&amp;lt;/tt&amp;gt;. Anstatt sie aber tatsächlich aus dem Graphen zu entfernen wie im obigen Pseudocode, dekrementieren wir nur den &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt; ihrer Nachbarn. Wird der &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt; eines Nachbarn dadurch 0, wird er ebenfalls in das Array der zu scannenden Knoten aufgenommen. Wenn der Graph azyklisch ist, enthält das Array am Ende alle Knoten des Graphen, und die Reihenfolge der Einfügungen definiert eine topologische Sortierung. Andernfalls ist das Array zu kurz, und wir signalisieren durch Zurückgeben von &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, dass der Graph zyklisch ist:&lt;br /&gt;
&lt;br /&gt;
 def topological_sort(graph):              # ein gerichteter Graph&lt;br /&gt;
     in_degree = [0]*len(graph)            # property map für den Eingangsgrad jeden Knotens&lt;br /&gt;
     for node in xrange(len(graph)):       # besuche alle Knoten&lt;br /&gt;
         for neighbor in graph[node]:      #  ... und deren Nachbarn&lt;br /&gt;
             in_degree[neighbor] += 1      #  ... und inkrementiere den Eingangsgrad&lt;br /&gt;
     &lt;br /&gt;
     result = []                           # wird später die topologische Sortierung enthalten&lt;br /&gt;
     for node in xrange(len(graph)):&lt;br /&gt;
         if in_degree[node] == 0:&lt;br /&gt;
             result.append(node)           # füge alle Knoten mit Eingangsgrad 0 in result ein&lt;br /&gt;
     &lt;br /&gt;
     k = 0&lt;br /&gt;
     while k &amp;lt; len(result):                # besuche alle Knoten mit Eingangsgrad 0&lt;br /&gt;
         node = result[k]&lt;br /&gt;
         k += 1&lt;br /&gt;
         for neighbor in graph[node]:      # besuche alle Nachbarn&lt;br /&gt;
             in_degree[neighbor] -= 1      # entferne 'virtuell' die eingehende Kante&lt;br /&gt;
             if in_degree[neighbor] == 0:  # wenn neighbor jetzt Eingangsgrad 0 hat&lt;br /&gt;
                 result.append(neighbor)   #  ... füge ihn in result ein&lt;br /&gt;
     &lt;br /&gt;
     if len(result) == len(graph):         # wenn alle Knoten jetzt Eingangsgrad 0 haben&lt;br /&gt;
         return result                     # ... ist result eine topologische Sortierung&lt;br /&gt;
     else:&lt;br /&gt;
         return None                       # andernfalls ist der Graph zyklisch&lt;br /&gt;
&lt;br /&gt;
===== Algorithmus 2 =====&lt;br /&gt;
Der obige Algorithmus hat den Nachteil, dass er jeden Knoten zweimal expandiert. Man kann eine topologische Sortierung stattdessen auch mit Tiefensuche bestimmen. Es gilt nämlich der folgende&lt;br /&gt;
;Satz: Wird ein DAG mittels Tiefensuche traversiert, definiert die &amp;lt;i&amp;gt;reverse post-order&amp;lt;/i&amp;gt; eine topologische Sortierung.&lt;br /&gt;
Zur Erinnerung: die post-order erhält man, indem man jeden Knoten ausgibt, &amp;lt;i&amp;gt;nachdem&amp;lt;/i&amp;gt; die Rekursion zu allen seinen Nachbarn beendet ist, siehe unsere [[Graphen_und_Graphenalgorithmen#pre_and_post_order|Diskussion weiter oben]]. Die reverse post-order ist gerade die Umkehrung dieser Reihenfolge. Die folgende Implementation verwendet die rekursive Version der Tiefensuche, in der Praxis wird man meist die iterative Version mit Stack bevorzugen, weil bei großen Graphen die Aufruftiefe sehr groß werden kann:&lt;br /&gt;
&lt;br /&gt;
 def reverse_post_order(graph):               # gerichteter Graph&lt;br /&gt;
     result = []                              # enthält später die reverse post-order&lt;br /&gt;
     visited = [False]*len(graph)             # Flags für bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):                         # besuche node&lt;br /&gt;
         if not visited[node]:                # aber nur, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True             # markiere ihn als besucht&lt;br /&gt;
             for neighbor in graph[node]:     # und besuche die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
             result.append(node)              # alle Nachbarn besucht =&amp;gt; Anhängen an result liefert post-order&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):          # besuche alle Knoten&lt;br /&gt;
         visit(node)&lt;br /&gt;
     &lt;br /&gt;
     result.reverse()                         # post-order =&amp;gt; reverse post-order&lt;br /&gt;
     return result&lt;br /&gt;
&lt;br /&gt;
Die Tatsache, dass die reverse post-order tatsächlich eine topologische Sortierung liefert, leuchtet wahrscheinlich nicht unmittelbar ein. Bevor wir diese Tatsache beweisen. wollen wir uns anhand des Ankleidegraphen klar machen, dass die pre-order (die man intuitiv vielleicht eher wählen würde) keine topologische Sortierung ist. Startet man die Tiefensuche beim Knoten &amp;quot;Unterhemd&amp;quot;, werden die Knoten in der Reihenfolge &amp;quot;Unterhemd&amp;quot;, &amp;quot;Oberhemd&amp;quot;, &amp;quot;Schlips&amp;quot;, &amp;quot;Jackett&amp;quot;, &amp;quot;Gürtel&amp;quot; gefunden. Da dann alle von &amp;quot;Unterhemd&amp;quot; erreichbaren Knoten erschöpft sind, startet man die Tiefensuche als nächstes bei &amp;quot;Unterhose&amp;quot; und erreicht von dort aus &amp;quot;Hose&amp;quot; und &amp;quot;Schuhe&amp;quot;. Man erkennt sofort, dass diese Reihenfolge nicht funktioniert: &amp;quot;Hose&amp;quot; kommt nach &amp;quot;Gürtel&amp;quot;, und &amp;quot;Jackett&amp;quot; kommt vor &amp;quot;Gürtel&amp;quot;. Bei dieser Anordnung gibt es Pfeile nach links, die Abhängigkeitsbedingungen sind somit verletzt.&lt;br /&gt;
&lt;br /&gt;
Damit die reverse post-order eine zulässige Sortierung sein kann, muss stets gelten, dass Knoten u vor Knoten v einsortiert wurde, wenn die Kante (u &amp;amp;rarr; v) existiert. Das ist aber äquivalent zur Forderung, dass in der ursprünglichen post-order (vor dem &amp;lt;tt&amp;gt;reverse&amp;lt;/tt&amp;gt;) u hinter v stehen muss. Wir betrachten den &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt;-Aufruf, bei dem u expandiert wird. Gelangt man jetzt zu u's Nachbarn v, gibt es zwei Möglichkeiten: Wenn v bereits expandiert wurde, befindet es sich bereits im Array &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt; kehrt sofort zurück. Andernfalls wird v ebenfalls expandiert und damit in &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; eingetragen, &amp;lt;i&amp;gt;bevor&amp;lt;/i&amp;gt; der rekursive Aufruf &amp;lt;tt&amp;gt;visit(v)&amp;lt;/tt&amp;gt; zurückkehrt. Knoten u wird aber erst in &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; eingefügt, &amp;lt;i&amp;gt;nachdem&amp;lt;/i&amp;gt; alle rekursiven &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt;-Aufrufe seiner Nachbarn zurückgekehrt sind. In beiden Fällen steht u in der post-order wie gefordert hinter v, und daraus folgt die Behauptung.&lt;br /&gt;
&lt;br /&gt;
Der obige Algorithmus liefert natürlich nur dann eine topologische Sortierung, wenn der Graph wirklich azyklisch ist (man kann ihn aber auch anwenden, um die reverse post-order für einen zyklischen Graphen zu bestimmen, siehe Abschnitt &amp;quot;[[Graphen_und_Graphenalgorithmen#Transitive Hülle und stark zusammenhängende Komponenten|Stark zusammenhängende Komponenten]]&amp;quot;). Dieser Fall tritt in der Praxis häufig auf, weil zyklische Graphen bei vielen Anwendungen gar nicht erst entstehen können. Weiß man allerdings nicht, ob der Graph azyklisch ist oder nicht, muss man einen zusätzlichen Test auf Zyklen in den Algorithmus einbauen. &lt;br /&gt;
&lt;br /&gt;
Zyklische Graphen sind dadurch gekennzeichnet, dass es im obigen Beweis eine dritte Möglichkeit gibt: Während der Expansion von u wird rekursiv v expandiert, und es gibt eine Rückwärtskante (v &amp;amp;rarr; u). (Es spielt dabei keine Rolle, ob v von u aus direkt oder indirekt erreicht wurde.) Ein Zyklus wird also entdeckt, wenn die Tiefensuche zu u zurückkehrt, solange u noch &amp;lt;i&amp;gt;aktiv&amp;lt;/i&amp;gt; ist, d.h. wenn die Rekursion von u aus gestartet und noch nicht beendet wurde. Dies kann man leicht feststellen, wenn man in der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; drei Werte zulässt: 0 für &amp;quot;noch nicht besucht&amp;quot;, 1 für &amp;quot;aktiv&amp;quot; und 2 für &amp;quot;beendet&amp;quot;. Wir signalisieren einen Zyklus, sobald &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt; für einen Knoten aufgerufen wird, der gerade aktiv ist:&lt;br /&gt;
&lt;br /&gt;
 def topological_sort_DFS(graph):             # gerichteter Graph&lt;br /&gt;
     result = []                              # enthält später die topologische Sortierung&lt;br /&gt;
     &lt;br /&gt;
     not_visited, active, finished = 0, 1, 2  # drei Zustände für visited&lt;br /&gt;
     visited = [not_visited]*len(graph)       # Flags für aktive und bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):                         # besuche node (gibt &amp;quot;True&amp;quot; zurück, wenn Zyklus gefunden wurde)&lt;br /&gt;
         if not visited[node]:                # ... aber nur, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = active           # markiere ihn als aktiv&lt;br /&gt;
             for neighbor in graph[node]:     # und besuche die Nachbarn&lt;br /&gt;
                 if visit(neighbor):          # wenn rekursiv ein Zyklus gefunden wurde&lt;br /&gt;
                     return True              # ... brechen wir ab und signalisieren den Zyklus&lt;br /&gt;
             visited[node] = finished         # Rekursion beendet, node ist nicht mehr aktiv&lt;br /&gt;
             result.append(node)              # alle Nachbarn besucht =&amp;gt; Anhängen an result liefert post-order&lt;br /&gt;
             return False                     # kein Zyklus gefunden&lt;br /&gt;
         elif visited[node] == active:        # Rekursion erreicht einen noch aktiven Knoten&lt;br /&gt;
             return True                      #  =&amp;gt; Zyklus gefunden&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):          # besuche alle Knoten&lt;br /&gt;
         if visit(node):                      # wenn Zyklus gefunden wurde&lt;br /&gt;
             return None                      # ... gibt es keine topologische Sortierung&lt;br /&gt;
     &lt;br /&gt;
     result.reverse()                         # post-order =&amp;gt; reverse post-order (=topologische Sortierung)&lt;br /&gt;
     return result&lt;br /&gt;
&lt;br /&gt;
Man macht sich leicht klar, dass kein Zyklus vorliegt, wenn die Rekursion einen Knoten erreicht, der bereits auf &amp;lt;tt&amp;gt;finished&amp;lt;/tt&amp;gt; gesetzt ist. Nehmen wir an, dass u gerade expandiert wird, und sein Nachbar v ist bereits &amp;lt;tt&amp;gt;finished&amp;lt;/tt&amp;gt;. Wenn es einen Zyklus gäbe, müsste es einen Weg von v nach u geben. Dann wäre u aber bereits während der Expansion von v gefunden worden. Da v nicht mehr im Zustand &amp;lt;tt&amp;gt;active&amp;lt;/tt&amp;gt; ist, muss die Expansion von v schon abgeschlossen gewesen sein, ohne dass u gefunden wurde. Folglich kann es keinen solchen Zyklus geben.&lt;br /&gt;
&lt;br /&gt;
=== Transitive Hülle und stark zusammenhängende Komponenten ===&lt;br /&gt;
&lt;br /&gt;
Auch bei gerichteten Graphen ist die Frage, welche Knoten miteinander zusammenhängen, von großem Interesse. Wir betrachten dazu wieder die Relation &amp;quot;Knoten v ist von Knoten u aus erreichbar&amp;quot;, die anzeigt, ob es einen Weg von u nach v gibt oder nicht. In ungerichteten Graphen ist diese Relation immer symmetrisch, weil jeder Weg in beiden Richtungen benutzt werden kann. In gerichteten Graphen gilt dies nicht. Man muss hier zwei Arten von Zusammenhangskomponenten unterscheiden:&lt;br /&gt;
;Transitive Hülle: Die transitive Hülle eines Knotens u ist die Menge aller Knoten, die von u aus erreichbar sind:&lt;br /&gt;
:&amp;lt;math&amp;gt;T(u) = \{v\ |\ u \rightsquigarrow v\}&amp;lt;/math&amp;gt;&lt;br /&gt;
;Stark zusammenhängende Komponenten: Die stark zusammenhängende Komponenten &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; eines gerichteten Graphen sind maximale Teilgraphen, so dass alle Knoten innerhalb einer Komponente von jedem anderen Knoten der selben Komponente aus erreichbar sind&lt;br /&gt;
:&amp;lt;math&amp;gt;u,v \in C_i\ \ \Leftrightarrow\ \ u \rightsquigarrow v \wedge v \rightsquigarrow u&amp;lt;/math&amp;gt;&lt;br /&gt;
Die erste Definition betrachtet den Zusammenhang asymmetrisch, ohne Beachtung der Frage, ob es auch einen Rückweg von Knoten v nach u gibt, die zweite hingegen symmetrisch.&lt;br /&gt;
&lt;br /&gt;
Die &amp;lt;b&amp;gt;transitive Hülle&amp;lt;/b&amp;gt; benötigt man, wenn man Fragen der Erreichbarkeit besonders effizient beantworten will. Wir hatten bespielsweise oben erwähnt, dass das Python-Modul &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; direkt und indirekt von mehreren anderen Module abhängt, die vorher installiert werden müssen, damit &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; funktioniert. Bittet man den Systemadministrator, das &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt;-Paket zu installieren, will er diese Abhängigkeiten wahrscheinlich nicht erst mühsam rekursiv heraussuchen, sonder er verlangt eine Liste aller Pakete, die installiert werden müssen. Dies ist gerade die transitive Hülle von &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; im Abhängigkeitsgraphen. Damit man diese nicht manuell bestimmen muss, verwendet man Installationsprogrammen wie z.B. [http://pypi.python.org/pypi/pip/ pip], die die Abhängigkeiten automatisch herausfinden und installieren. &lt;br /&gt;
&lt;br /&gt;
Bei der Bestimmung der transitiven Hülle modifiziert man den gegebenen Graphen, indem man jedesmal eine neue Kante (u &amp;amp;rarr; v) einfügt, wenn diese Kante noch nicht existiert, aber v von u aus erreichbar ist. Dies gelingt mit einer sehr einfachen Variation der Tiefensuche: Wir rufen &amp;lt;tt&amp;gt;visit(k)&amp;lt;/tt&amp;gt; für jeden Knoten k auf, aber setzen die property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; zuvor auf &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt; zurück. Alle Knoten, die während der Rekursion erreicht werden, sind im modifizierten Graphen Nachbarn von k. Ein etwas effizienterer Ansatz ist der [http://de.wikipedia.org/wiki/Algorithmus_von_Floyd_und_Warshall Algorithmus von Floyd und Warshall].&lt;br /&gt;
&lt;br /&gt;
Die Bestimmung der &amp;lt;b&amp;gt;stark zusammenhängenden Komponenten&amp;lt;/b&amp;gt; ist etwas schwieriger. Es existieren eine ganze Reihe von effizienten Algorithmen (siehe [http://en.wikipedia.org/wiki/Strongly_connected_component WikiPedia]), deren einfachster der Algorithmus von Kosaraju ist:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;gegeben:&amp;lt;/b&amp;gt; gerichteter Graph&lt;br /&gt;
&lt;br /&gt;
# Bestimme die reverse post-order (mit der Funktion &amp;lt;tt&amp;gt;reverse_post_order&amp;lt;/tt&amp;gt;)&lt;br /&gt;
# Bilde den transponierten Graphen &amp;lt;math&amp;gt;G^T&amp;lt;/math&amp;gt; (mit der Funktion &amp;lt;tt&amp;gt;transposeGraph&amp;lt;/tt&amp;gt;)&lt;br /&gt;
# Bestimme die Zusammenhangskomponenten von &amp;lt;math&amp;gt;G^T&amp;lt;/math&amp;gt; mittels Tiefensuche, aber betrachte die Knoten dabei in der reverse post-order aus Schritt 1 (dies kann mit einer minimalen Modifikation der Funktion &amp;lt;tt&amp;gt;connectedComponents&amp;lt;/tt&amp;gt; geschehen, indem man die Zeile &amp;lt;tt&amp;gt;for node in xrange(len(graph)):&amp;lt;/tt&amp;gt; einfach nach &amp;lt;tt&amp;gt;for node in ordered:&amp;lt;/tt&amp;gt; abändert, wobei &amp;lt;tt&amp;gt;ordered&amp;lt;/tt&amp;gt; das Ergebnis der Funktion &amp;lt;tt&amp;gt;reverse_post_order&amp;lt;/tt&amp;gt; ist, also ein Array, das die Knoten in der gewünschten Reihenfolge enthält).&lt;br /&gt;
Die Zusammenhangskomponenten, die man in Schritt 3 findet, sind gerade die stark zusammenhängenden Komponenten des Originalgraphen G. Die folgende Skizze zeigt diese in grün für den schwarz gezeichneten gerichteten Graphen. &lt;br /&gt;
&lt;br /&gt;
[[Image:strongly-connected-components.png|400px]]    &lt;br /&gt;
&lt;br /&gt;
Zum Beweis der Korrektheit des Algorithmus von Kosaraju zeigen wir zwei Implikationen: 1. Wenn die Knoten u und v in der selben stark zusammenhängenden Komponente liegen, werden sie in Schritt 3 des Algorithmus auch der selben Komponente zugewiesen. 2. Wenn die Knoten u und v in Schritt 3 der selben Komponente zugewiesen wurden, müssen sie auch in der selben stark zusammenhängenden Komponente liegen. &lt;br /&gt;
# Knoten u und v gehören zur selben stark zusammenhängenden Komponente von G. Per Definition gilt, dass u von v aus erreichbar ist und umgekehrt. Dies muss auch im transponierten Graphen G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; gelten (der Weg &amp;lt;math&amp;gt;u \rightsquigarrow v&amp;lt;/math&amp;gt; wird jetzt zum Weg &amp;lt;math&amp;gt;v \rightsquigarrow u&amp;lt;/math&amp;gt; und umgekehrt). Wird u bei der Tiefensuche in Schritt 3 vor v expandiert, ist v von u aus erreichbar und gehört somit zur selben Komponente. Das umgekehrte gilt, wenn v vor u expandiert wird. Daraus folgt die Behauptung 1.&lt;br /&gt;
# Knoten u und v werden in Schritt 3 der selben Komponente zugewiesen: Sei x der Anker dieser Komponente. Da u in der gleichen Komponente wie x liegt, muss es in G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; einen Weg &amp;lt;math&amp;gt;x \rightsquigarrow u&amp;lt;/math&amp;gt;, und demnach in G einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow x&amp;lt;/math&amp;gt; geben. Da x der Anker seiner Komponente ist, wissen wir aber auch, dass x in der reverse post-order &amp;lt;i&amp;gt;vor&amp;lt;/i&amp;gt; u liegt (denn der Anker ist der Knoten, mit dem eine neue Komponente gestartet wird; er muss deshalb im Array &amp;lt;tt&amp;gt;ordered&amp;lt;/tt&amp;gt; als erster Konten seiner Komponente gefunden worden sein). Wir unterscheiden jetzt im Schritt 1 des Algorithmus zwei Fälle:&lt;br /&gt;
## u wurde bei der Bestimmung der post-order vor x expandiert. Dann kann x nur dann in der reverse post-order &amp;lt;i&amp;gt;vor&amp;lt;/i&amp;gt; u liegen (oder, einfacher ausgedrückt, x kann nur dann in der post-order &amp;lt;i&amp;gt;hinter&amp;lt;/i&amp;gt; u liegen), wenn x im Graphen G nicht von u aus erreichbar war. Das ist aber unmöglich, weil wir ja schon wissen, dass es in G einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow x&amp;lt;/math&amp;gt; gibt.&lt;br /&gt;
## Folglich wurde u bei der Bestimmung der post-order nach x expandiert. Da x in der post-order hinter u liegt, muss u während der Expansion von x erreicht worden sein. Deshalb muss es in G auch einen Weg &amp;lt;math&amp;gt;x \rightsquigarrow u&amp;lt;/math&amp;gt; geben.&lt;br /&gt;
#:Somit sind x und u in der selben stark zusammenhängenden Komponente. Die gleiche Überlegung gilt für x und v. Wegen der Transitivität der relation &amp;quot;ist erreichbar&amp;quot; folgt daraus, dass auch u und v in der selben Komponente liegen, also die Behauptung 2.&lt;br /&gt;
&lt;br /&gt;
Die folgende Skizze illustriert, dass der Komponentengraph stets azyklisch ist. Den Komponentengraph erhält man, indem man für jede Komponente &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; einen Knoten erzeugt (grün), und die Knoten i und j durch eine gerichtete Kante verbindet (rot), wenn es im Originalgraphen eine Kante (u &amp;amp;rarr; v) mit &amp;lt;math&amp;gt;u \in C_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v \in C_j&amp;lt;/math&amp;gt; gibt. Es ist dann garantiert, dass es keine Kante in umgekehrter Richtung geben kann. Daraus folgt insbesondere, dass ein DAG nur triviale stark verbundene Komponenten haben kann, die aus einzelnen Knoten bestehen.&lt;br /&gt;
&lt;br /&gt;
[[Image:strongly-connected-components-graph.png|400px]]&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Das Erfüllbarkeitsproblem in Implikationengraphen ===&lt;br /&gt;
&lt;br /&gt;
Das Erfüllbarkeitsproblem hat auf den ersten Blick nichts mit Graphen zu tun, denn es geht um Wahrheitswerte logischer Ausdrücke. Man kann logische Ausdrücke jedoch unter bestimmten Bedingungen in eine Graphendarstellung überführen und somit das ursprüngliche Problem auf ein Problem der Graphentheorie reduzieren, für das bereits ein Lösungsverfahren bekannt ist. In diesem Abschnitt wollen wir dies für die sogenannten Implikationengraphen zeigen, ein weiteres Beispiel findet sich im Kapitel [[NP-Vollständigkeit]].&lt;br /&gt;
&lt;br /&gt;
==== Das Erfüllbarkeitsproblem ====&lt;br /&gt;
&lt;br /&gt;
(vgl. [http://de.wikipedia.org/wiki/Erfüllbarkeitsproblem_der_Aussagenlogik WikiPedia (de)])&lt;br /&gt;
&lt;br /&gt;
Das Erfüllbarkeitsproblem (SAT-Problem, von &amp;lt;i&amp;gt;satisfiability&amp;lt;/i&amp;gt;) befasst sich mit logischen (oder Booleschen) Funktionen: Gegeben sei eine Menge &amp;lt;math&amp;gt;\{x_1, ... ,x_n\}&amp;lt;/math&amp;gt; Boolscher Variablen (d.h., die &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; können nur die Werte True oder False annehmen), sowie eine logische Formel, in der die Variablen mit den üblichen logischen Operatoren &lt;br /&gt;
:&amp;lt;math&amp;gt;\neg\quad&amp;lt;/math&amp;gt;: Negation (&amp;quot;nicht&amp;quot;, in Python: &amp;lt;tt&amp;gt;not&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\vee\quad&amp;lt;/math&amp;gt;: Disjunktion (&amp;quot;oder&amp;quot;, in Python: &amp;lt;tt&amp;gt;or&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\wedge\quad&amp;lt;/math&amp;gt;: Konjuktion (&amp;quot;und&amp;quot;, in Python: &amp;lt;tt&amp;gt;and&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\rightarrow\quad&amp;lt;/math&amp;gt;: Implikation (&amp;quot;wenn, dann&amp;quot;, in Python nicht als Operator definiert)&lt;br /&gt;
:&amp;lt;math&amp;gt;\leftrightarrow\quad&amp;lt;/math&amp;gt;: Äquivalenz (&amp;quot;genau dann, wenn&amp;quot;, in Python: &amp;lt;tt&amp;gt;==&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\neq\quad&amp;lt;/math&amp;gt;: exklusive Disjunktion (&amp;quot;entweder oder&amp;quot;, in Python: &amp;lt;tt&amp;gt;!=&amp;lt;/tt&amp;gt;)&lt;br /&gt;
verknüpft sind. Klammern definieren die Reihenfolge der Auswertung der Operationen. Für jede Belegung der Variablen &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; mit True oder False liefert die Formel den Wert der Funktion, der natürlich auch nur True oder False sein kann. Wenn Formel und Belegung gegeben sind, ist die Auswertung der Funktion ein sehr einfaches Problem: Man transformiert die Formel in einen Parse-Baum (siehe Übungsaufgabe &amp;quot;Taschenrechner) und wertet jeden Knoten mit Hilfe der üblichen Wertetabellen für logische Operatoren aus, die wir hier zur Erinnerung noch einmal angeben:&lt;br /&gt;
{| cellspacing=&amp;quot;0&amp;quot; border=&amp;quot;1&amp;quot;&lt;br /&gt;
|- style=&amp;quot;text-align:center;background-color:#ffffcc;width:50px&amp;quot;&lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \vee b &amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \wedge b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \rightarrow b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;b \rightarrow a&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \leftrightarrow b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \neq b&amp;lt;/math&amp;gt; &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 0 || 1 || 1 || 1 || 0&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 0 || 1 || 0 || 0 || 1&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 0 || 1 || 0 || 1&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 || 0&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beim Erfüllbarkeitsproblem wird die Frage umgekehrt gestellt: &lt;br /&gt;
:Gegeben sei eine logische Funktion. Ist es möglich, dass die Funktion jemals den Wert True annimmt? &lt;br /&gt;
Das heisst, kann man die Variablen &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; so mit True oder False belegen, dass die Formel am Ende wahr ist? Im Prinzip kann man diese Frage durch erschöpfende Suche leicht beantworten, indem man die Funktion für alle &amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt; möglichen Belegungen einfach ausrechnet, aber das dauert für große n (ab ca. &amp;lt;math&amp;gt;n\ge 40&amp;lt;/math&amp;gt;) viel zu lange. Erstaunlicherweise ist es aber noch niemanden gelungen, einen Algorithmus zu finden, der für beliebige logische Funktionen schneller funktioniert. Im Gegenteil wurde gezeigt, dass das Erfüllbarkeitsproblem [[NP-Vollständigkeit|NP-vollständig]] ist, so dass wahrscheinlich kein solcher Algorithmus existiert. Trotz (oder gerade wegen) seiner Schwierigkeit hat das Erfüllbarkeitsproblem viele Anwendungen gefunden, vor allem beim Testen logischer Schaltkreise (&amp;quot;Gibt es eine Belegung der Eingänge, so dass am Ausgang der verbotene Wert X entsteht?&amp;quot;) und bei der Planerstellung in der künstlichen Intelligenz (&amp;quot;Kann man ausschließen, dass der generierte Plan Konflikte enthält?&amp;quot;). Es ist außerdem ein beliebtes Modellproblem für die Erforschung neuer Ideen und Algorithmen für schwierige Probleme.&lt;br /&gt;
&lt;br /&gt;
==== Normalformen für logische Ausdrücke ====&lt;br /&gt;
&lt;br /&gt;
Um die Beschreibung von Erfüllbarkeitsproblemen zu vereinfachen und zu vereinheitlichen, hat man verschiedene &amp;lt;i&amp;gt;Normalformen&amp;lt;/i&amp;gt; für logische Ausdrücke eingeführt. Die wichtigste ist die &amp;lt;i&amp;gt;Konjuktionen-Normalform&amp;lt;/i&amp;gt; (CNF - conjunctive normal form). Ein Ausdruck in &amp;lt;i&amp;gt;Konjuktionen-Normalform&amp;lt;/i&amp;gt; ist eine UND-Verknüpfung von M &amp;lt;i&amp;gt;Klauseln&amp;lt;/i&amp;gt;:&lt;br /&gt;
 (CLAUSE&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;) &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; (CLAUSE&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;) &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; ...  &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; (CLAUSE&amp;lt;sub&amp;gt;M&amp;lt;/sub&amp;gt;)&lt;br /&gt;
Jede Klausel ist wiederum ein logischer Ausdruck, der aber sehr einfach sein muss: Er darf nur noch k Variablen enthalten, die nur mit den Operatoren NICHT und ODER verknüpft werden dürfen, z.B.&lt;br /&gt;
  CLAUSE&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; := &amp;lt;math&amp;gt;x_1 \vee \neg x_3 \vee x_8&amp;lt;/math&amp;gt;&lt;br /&gt;
Je nachdem, wie viele Variablen pro Klausel erlaubt sind, spricht man von &amp;lt;b&amp;gt;k-CNF&amp;lt;/b&amp;gt; und entsprechend von einem &amp;lt;b&amp;gt;k-SAT&amp;lt;/b&amp;gt; Problem. Es ist außerdem üblich, die Menge der Variablen und die Menge der negierten Variablen zusammen als Menge der &amp;lt;i&amp;gt;Literale&amp;lt;/i&amp;gt; zu bezeichnen:&lt;br /&gt;
  LITERALS := &amp;lt;math&amp;gt;\{x_1,...,x_n\} \cup \{\neg x_1,...,\neg x_n\}&amp;lt;/math&amp;gt;&lt;br /&gt;
Formal definiert man die &amp;lt;b&amp;gt;k-Konjunktionen-Normalform (k-CNF)&amp;lt;/b&amp;gt; am besten durch eine Grammatik in [http://de.wikipedia.org/wiki/Backus-Naur-Form Backus-Naur-Form]:&lt;br /&gt;
    k_CNF    ::=  CLAUSE | k_CNF &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; CLAUSE&lt;br /&gt;
    CLAUSE   ::= (LITERAL &amp;lt;math&amp;gt;\vee&amp;lt;/math&amp;gt; ... &amp;lt;math&amp;gt;\vee&amp;lt;/math&amp;gt; LITERAL)  # genau k Literale pro Klausel&lt;br /&gt;
    LITERAL  ::=  VARIABLE | &amp;lt;math&amp;gt;\neg&amp;lt;/math&amp;gt;VARIABLE&lt;br /&gt;
    VARIABLE ::=  &amp;lt;math&amp;gt;x_1&amp;lt;/math&amp;gt; | ... | &amp;lt;math&amp;gt;x_n&amp;lt;/math&amp;gt;&lt;br /&gt;
Beispiele:&lt;br /&gt;
* 3-CNF: &amp;lt;math&amp;gt;(x_1 \vee \neg x_2 \vee x_4) \wedge (x_2 \vee x_3 \vee \neg x_4) \wedge (\neg x_1 \vee x_4 \vee \neg x_5)&amp;lt;/math&amp;gt;&lt;br /&gt;
* 2-CNF: &amp;lt;math&amp;gt;(x_1 \vee \neg x_2) \wedge (x_3 \vee x_4)&amp;lt;/math&amp;gt; ...&lt;br /&gt;
&amp;lt;b&amp;gt;Gesucht&amp;lt;/b&amp;gt; ist eine Belegung der Variablen mit True und False, so dass der Ausdruck den Wert True hat. Aus den Eigenschaften der UND- und ODER-Verknüpfungen folgt, dass ein Ausdruck in k-CNF genau dann True ist, wenn jede einzelne Klausel True ist. In jeder Klausel wiederum hat man k Chancen, die Klausel True zu machen, indem man eins der Literale zu True macht. Eventuell werden dadurch aber andere Klauseln wieder zu False, was die Aufgabe so schwierig macht. Die Bedeutung der k-CNF ergibt sich aus folgendem&lt;br /&gt;
;Satz: Jeder logische Ausdruck kann effizient nach 3-CNF transformiert werden, jedoch im allgemeinen nicht nach 2-CNF.&lt;br /&gt;
Man kann sich also auf Algorithmen für 3-SAT-Probleme konzentrieren, ohne dabei an Ausdrucksmächtigkeit zu verlieren. &lt;br /&gt;
&lt;br /&gt;
Leider gilt der entsprechende Satz nicht für k=2: Ausdrücke in 2-CNF sind weit weniger mächtig, weil man in jeder Klausel nur noch zwei Wahlmöglichkeiten hat. Bestimmte logische Ausdrücke sind aber auch nach 2-CNF transformierbar, beispielsweise die Bedingung, dass zwei Literale u und v immer den entgegegesetzten Wert haben müssen. Dies ergibt ein Paar von ODER-Verknüpfungen:&lt;br /&gt;
:&amp;lt;math&amp;gt;(u \leftrightarrow \neg v) \equiv (u \vee \neg v) \wedge (\neg u \vee v)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die 2-CNF hat den Vorteil, dass es effiziente Algorithmen für das 2-SAT-Problem gibt, die wir jetzt kennenlernen wollen. Es zeigt sich, dass man Ausdrücke in 2-CNF als Graphen repräsentieren kann, indem man sie zunächst in die &amp;lt;i&amp;gt;Implikationen-Normalform&amp;lt;/i&amp;gt; (INF für &amp;lt;i&amp;gt;implicative normal form&amp;lt;/i&amp;gt;) überführt. Die Implikationen-Normalform besteht ebenfalls aus einer Menge von Klauseln, die durch UND-Operationen verknüpft sind, aber jede Klausel ist jetzt eine Implikation. &lt;br /&gt;
Die Grammatik der &amp;lt;b&amp;gt;Implikationen-Normalform (INF)&amp;lt;/b&amp;gt; lautet:&lt;br /&gt;
    INF      ::=  CLAUSE | INF &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; CLAUSE&lt;br /&gt;
    CLAUSE   ::= (LITERAL &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; LITERAL)  # genau 2 Literale pro Implikation&lt;br /&gt;
    LITERAL  ::=  VARIABLE | &amp;lt;math&amp;gt;\neg&amp;lt;/math&amp;gt;VARIABLE&lt;br /&gt;
    VARIABLE ::=  &amp;lt;math&amp;gt;x_1&amp;lt;/math&amp;gt; | ... | &amp;lt;math&amp;gt;x_n&amp;lt;/math&amp;gt;&lt;br /&gt;
und ein gültiger Ausdruck wäre z.B.&lt;br /&gt;
:&amp;lt;math&amp;gt;(x_1 \to x_2) \wedge (x_2 \to \neg x_3) \wedge (x_4 \to x_3)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die Umwandlung von 2-CNF nach INF beruht auf folgender Äquivalenz, die man sich aus der obigen Wahrheitstabelle leicht herleitet:&lt;br /&gt;
:&amp;lt;math&amp;gt;(x \vee y) \equiv (\neg x \rightarrow y) \equiv (\neg y \rightarrow x)&amp;lt;/math&amp;gt;&lt;br /&gt;
Aus dieser Äquivalenz folgt der &lt;br /&gt;
;Satz: Ein Ausdruck in 2-CNF kann nach INF transformiert werden, indem man jede Klausel &amp;lt;math&amp;gt;(x \vee y)&amp;lt;/math&amp;gt; durch das Klauselpaar &amp;lt;math&amp;gt;(\neg x \rightarrow y) \wedge (\neg y \rightarrow x)&amp;lt;/math&amp;gt; ersetzt.&lt;br /&gt;
Man beachte, dass man für jede ODER-Klausel des ursprünglichen Ausdrucks &amp;lt;i&amp;gt;zwei&amp;lt;/i&amp;gt; Implikationen (eine für jede Richtung des &amp;quot;wenn, dann&amp;quot;) einfügen muss, um die Symmetrie des Problems zu erhalten.&lt;br /&gt;
&lt;br /&gt;
==== Lösung des 2-SAT-Problems mit Implikationgraphen ====&lt;br /&gt;
&lt;br /&gt;
Jeder Ausdruck in INF kann als gerichteter Graph dargestellt werden:&lt;br /&gt;
# Für jedes Literal wird ein Knoten in den Graphen eingefügt. Es gibt also für jede Variable und für ihre Negation jeweils einen Knoten, d.h. 2n Knoten insgesamt.&lt;br /&gt;
# Jede Implikation ist eine gerichtete Kante.&lt;br /&gt;
Implikationengraphen eignen sich, um Ursache-Folge-Beziehungen oder Konflikte zwischen Aktionen auszudrücken. Beispielsweise kann man die Klausel &amp;lt;math&amp;gt;(x \rightarrow \neg y)&amp;lt;/math&amp;gt; als &amp;quot;wenn man x tut, darf man y nicht tun&amp;quot; interpretieren. Ein anderes schönes Beispiel findet sich in Übung 12.&lt;br /&gt;
&lt;br /&gt;
Für die Implementation eines Implikationengraphen in Python empfiehlt es sich, die Knoten geschickt zu numerieren: Ist die Variable &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; dem Knoten i zugeordnet, so sollte die negierte Variable &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; dem Knoten (i+n) zugeordnet werden. Zu jedem gegebenen Knoten i findet man dann den negierten Partnerknoten j leicht durch die Formel &amp;lt;tt&amp;gt;j = (i + n ) % (2*n)&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die Aufgabe besteht jetzt darin, folgende Fragen zu beantworten:&lt;br /&gt;
# Ist der durch den Implikationengraphen gegebene Ausdruck erfüllbar?&lt;br /&gt;
# Finde eine geeignete Belegung der Variablen, wenn der Ausduck erfüllbar ist.&lt;br /&gt;
Die erste Frage beantwortet man leicht, indem man die stark zusammenhängenden Komponenten des Implikationengraphen bildet. Dann gilt folgender&lt;br /&gt;
;Satz: Seien u und v zwei Literale, die sich in der selben stark zusammenhängenden Komponente befinden. Dann müssen u und v stets den selben Wert haben, damit der Ausdruck erfüllt sein kann.&lt;br /&gt;
Die Korrektheit des Satzes folgt aus der Definition der stark zusammenhängenden Komponenten: Da u und v in der selben Komponente liegen, gibt es im Implikationengraphen einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow v&amp;lt;/math&amp;gt; sowie einen Weg &amp;lt;math&amp;gt;v \rightsquigarrow u&amp;lt;/math&amp;gt;. Wegen der Transitivität der &amp;quot;wenn, dann&amp;quot; Relation kann man die Wege zu zwei Implikationen verkürzen, die gleichzeitig gelten müssen: &amp;lt;math&amp;gt;(u \rightarrow v) \wedge (v \rightarrow u)&amp;lt;/math&amp;gt; (die Verkürzung von Wegen zu direkten Kanten entspricht gerade der Bildung der transitiven Hülle für die Knoten u und v). In der obigen Wertetabelle für logische Operatoren erkennt mann, dass dies äquivalent zur Bedingung &amp;lt;math&amp;gt;(u \leftrightarrow v)&amp;lt;/math&amp;gt; ist. Dies ist aber gerade die Behauptung des Satzes.&lt;br /&gt;
&lt;br /&gt;
Die Erfüllbarkeit des Ausdrucks ist nun ein einfacher Spezialfall dieses Satzes. &lt;br /&gt;
;Korrolar: Der gegebene Ausdruck ist genau dann erfüllbar, wenn die Literale &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; sich für kein i in derselben stark zusammenhängenden Komponente befinden.&lt;br /&gt;
Setzt man nämlich im Satz &amp;lt;math&amp;gt;u = x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v = \neg x_i&amp;lt;/math&amp;gt;, und beide Knoten befinden sich in der selben Komponente, dann müsste gelten &amp;lt;math&amp;gt;x_i \leftrightarrow\neg x_i&amp;lt;/math&amp;gt;, was offensichtlich ein Widerspruch ist. Damit kann der Ausdruck nicht erfüllbar sein. Umgekehrt gilt, dass der Ausdruck immer erfüllbar ist, wenn &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; stets in verschiedenen Komponenten liegen, weil der folgende Algorithmus von Aspvall, Plass und Tarjan in diesem Fall stets eine gültige Belegung aller Variablen liefert:&lt;br /&gt;
# Bestimme die stark zusammenhängenden Komponenten und bilde den Komponentengraphen. Ordne die Knoten des Komponentengraphen (also die stark zusammenhängenden Komponenten des Originalgraphen) in topologische Sortierung an.&lt;br /&gt;
# Betrachte die Komponenten in der topologischen Sortierung von hinten nach vorn und weise ihnen einen Wert nach folgenden Regeln zu (zur Erinnerung: alle Literale in der selben Komponente haben den selben Wert):&lt;br /&gt;
#* Wenn die Komponente noch nicht betrachtet wurde, setze ihren Wert auf True, und den Wert der komplementären Komponente (derjenigen, die die negierten Literale enthält) auf False.&lt;br /&gt;
#* Andernfalls, gehe zur nächsten Komponente weiter.&lt;br /&gt;
Der Algorithmus beruht auf der Symmetrie des Implikationengraphen: Weil Kanten immer paarweise &amp;lt;math&amp;gt;(\neg u \rightarrow v) \wedge (\neg v \rightarrow u)&amp;lt;/math&amp;gt; eingefügt werden, ist der Graph &amp;lt;i&amp;gt;schiefsymmetrisch&amp;lt;/i&amp;gt; (skew symmetric): die eine Hälfte das Graphen ist die transponierte Spiegelung der anderen Hälfte. Enthält eine stark zusammenhängende Komponente &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; die Knoten &amp;lt;tt&amp;gt;i1, i2, ...&amp;lt;/tt&amp;gt;, so gibt es stets eine komplementäre Komponente &amp;lt;math&amp;gt;C_j = \neg C_i&amp;lt;/math&amp;gt;, die die komplementären Knoten &amp;lt;tt&amp;gt;j1 = (i1 + n) % (2*n), j2 = (i2 + n) % (2*n), ...&amp;lt;/tt&amp;gt; enthält. Gilt &amp;lt;math&amp;gt;C_i = \neg C_i&amp;lt;/math&amp;gt; für irgendein i, so ist der Ausdruck nicht erfüllbar. Den Beweis für die Korrektheit des Algorithmus findet man im [http://www.math.ucsd.edu/~sbuss/CourseWeb/Math268_2007WS/2SAT.pdf Originalartikel]. Leider funktioniert dies nicht für k-SAT-Probleme mit &amp;lt;math&amp;gt;k &amp;gt; 2&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Will man nur die Erfüllbarkeit prüfen, vereinfacht sich der Algorithmus zu:&lt;br /&gt;
# Bestimme die stark zusammenhängenden Komponenten.&lt;br /&gt;
# Teste für alle &amp;lt;tt&amp;gt;i = 0,...,n-1&amp;lt;/tt&amp;gt;, dass Knoten &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; und Knoten &amp;lt;tt&amp;gt;(i+n)&amp;lt;/tt&amp;gt; in unterschiedlichen Komponenten liegen.&lt;br /&gt;
Ist der Ausdruck erfüllbar, kann man eine gültige Belegung der Variablen jetzt mit dem randomisierten Algorithmus bestimmen, den wir im Kapitel [[Randomisierte Algorithmen]] behandeln.&lt;br /&gt;
&lt;br /&gt;
== Weitere wichtige Graphenalgorithmen ==&lt;br /&gt;
&lt;br /&gt;
Eins der wichtigsten Einsatzgebiete für Graphen ist die Optimierung, also die Suche nach der &amp;lt;i&amp;gt;besten&amp;lt;/i&amp;gt; Lösung für ein gegebenes Problem:&lt;br /&gt;
* Das &amp;lt;i&amp;gt;interval scheduling&amp;lt;/i&amp;gt; befasst sich damit, aus einer gegebenen Menge von Aufträgen die richtigen auszuwählen und sie geschickt auf die zur Verfügung stehenden Ressourcen aufzuteilen. Damit beschäftigen wir uns im Kapitel [[Greedy-Algorithmen und Dynamische Programmierung]].&lt;br /&gt;
* Beim Problem des Handlungsreisenden sucht man nach der kürzesten Rundreise, die alle gegebenen Städte genau einmal besucht. Dieses Problem behandeln wir im Kapitel [[NP-Vollständigkeit]].&lt;br /&gt;
* Viele weitere Anwendungen können wir leider in der Vorlesung nicht mehr behandeln, z.B.&lt;br /&gt;
** Algorithmen für den [http://en.wikipedia.org/wiki/Maximum_flow_problem maximalen Fluss] beantworten die Frage, wie man die Durchflussmenge durch ein Netzwerk (z.B. von Ölpipelines) maximiert.&lt;br /&gt;
** Beim [http://en.wikipedia.org/wiki/Assignment_problem Problem der optimalen Paarung] (&amp;quot;matching problem&amp;quot; oder &amp;quot;assignment problem&amp;quot;) sucht man nach einer Teilmenge der Kanten (also nach einem Teilgraphen), so dass jeder Knoten in diesem Teilgraphen höchstens den Grad 1 hat. Im neuen Graphen gruppieren die Kanten also je zwei Knoten zu einem Paar, und die Paarung soll nach jeweils anwendungsspezifischen Kriterien optimal sein. Dies benötigt man z.B. bei der optimalen Zuordnung von Gruppen, etwas beim Arbeitsamt (Zuordnung Arbeitssuchender - Stellenangebot) und in der Universität (Zuordnung Studenten - Übungsgruppen).&lt;br /&gt;
** In Statistik und maschinellem Lernen haben in den letzten Jahren die [http://en.wikipedia.org/wiki/Graphical_model graphischen Modelle] große Bedeutung erlangt.&lt;br /&gt;
* usw. usf.&lt;br /&gt;
&lt;br /&gt;
[[Randomisierte Algorithmen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5403</id>
		<title>Graphen und Graphenalgorithmen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5403"/>
				<updated>2012-07-25T16:56:25Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Das Erfüllbarkeitsproblem */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Einführung zu Graphen ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation -- Königsberger Brückenproblem ===&lt;br /&gt;
Leonhard Euler [http://de.wikipedia.org/wiki/Leonhard_Euler] erfand den Graphen-Formalismus 1736, um eine scheinbar banale Frage zu beantworten: Ist es möglich, in Königsberg (siehe Stadtplan von 1809 und die schematische Darstellung) einen Spaziergang zu unternehmen, bei dem jede der 7 Brücken genau einmal überquert wird?&lt;br /&gt;
&lt;br /&gt;
[[Image:Koenigsberg1809.png]]&amp;lt;br&amp;gt;&lt;br /&gt;
[[Image:Koenigsberg.jpg]]&lt;br /&gt;
&lt;br /&gt;
Ein Graph abstrahiert von der Geometrie des Problems und repräsentiert nur die Topologie. Jeder Stadtteil von Königsberg ist ein Knoten des Graphen, jede Brücke eine Kante. Der zum Brückenproblem gehörende Graph sieht also so aus:&lt;br /&gt;
&lt;br /&gt;
     O&lt;br /&gt;
    /| \&lt;br /&gt;
    \|  \&lt;br /&gt;
     O---O&lt;br /&gt;
    /|  /&lt;br /&gt;
    \| /&lt;br /&gt;
     O&lt;br /&gt;
&lt;br /&gt;
Der gesuchte Spaziergang würde existieren, wenn es maximal 2 Knoten gäbe, an denen sich eine ungerade Zahl von Kanten trifft. Die Frage muss für Königsberg also verneint werden, denn hier gibt es vier solche Knoten. Ein leicht modifiziertes Problem ist allerdings lösbar: Im obigen Stadtplan erkennt man eine Fähre, die die Stadtteile Kneiphof und Altstadt verbindet. Bezieht man dieselbe in den Spaziergang ein, ergibt sich folgender Graph, bei dem nur noch zwei Knoten mit ungerader Kantenzahl existieren:&lt;br /&gt;
&lt;br /&gt;
   --O&lt;br /&gt;
  / /| \&lt;br /&gt;
  \ \|  \&lt;br /&gt;
   --O---O&lt;br /&gt;
    /|  /&lt;br /&gt;
    \| /&lt;br /&gt;
     O&lt;br /&gt;
&lt;br /&gt;
Inzwischen haben Graphen eine riesige Zahl weiterer Anwendungen gefunden. Einige Beispiele:&lt;br /&gt;
&lt;br /&gt;
* Landkarten:&lt;br /&gt;
** Knoten: Länder&lt;br /&gt;
** Kanten: gemeinsame Grenzen&lt;br /&gt;
&lt;br /&gt;
* Logische Schaltkreise:&lt;br /&gt;
** Knoten: Gatter&lt;br /&gt;
** Kanten: Verbindungen&lt;br /&gt;
&lt;br /&gt;
* Chemie (Summenformeln):&lt;br /&gt;
** Knoten: chemische Elemente&lt;br /&gt;
** Kanten: Bindungen &lt;br /&gt;
&lt;br /&gt;
* Soziologie (StudiVZ)&lt;br /&gt;
** Soziogramm&lt;br /&gt;
*** Knoten: Personen&lt;br /&gt;
*** Kanten: Freund von ...&lt;br /&gt;
&lt;br /&gt;
=== Definitionen ===&lt;br /&gt;
&lt;br /&gt;
;Ungerichteter Graph: Ein ungerichteter Graph G = ( V, E ) besteht aus&lt;br /&gt;
:* einer endliche Menge V von Knoten (vertices)&lt;br /&gt;
:* einer endlichen Menge &amp;lt;math&amp;gt;E \subset V \times V&amp;lt;/math&amp;gt; von Kanten (edges)&lt;br /&gt;
:Die Paare (u,v) und (v,u) gelten dabei als nur ''eine'' Kante (somit gilt die Symmetriebeziehung: (u,v) ∈ E =&amp;gt; (v,u) ∈ E ). Die Anzahl der Kanten, die sich an einem Knoten treffen, wird als ''Grad'' (engl. ''degree'') dieses Knotens bezeichnet:&lt;br /&gt;
:::degree(v) = |{v' ∈ V | (v,v') ∈ E}|&lt;br /&gt;
:(Die Syntax |{...}| bezeichnet dabei die Mächtigkeit der angegebenen Menge, also die Anzahl der Elemente in der Menge.)&lt;br /&gt;
&lt;br /&gt;
Der Graph des Königsberger Brückenproblems ist ungerichtet. Bezeichnet man die Knoten entsprechend des folgenden Bildes&lt;br /&gt;
    c&lt;br /&gt;
   /| \&lt;br /&gt;
   \|  \&lt;br /&gt;
    b---d &lt;br /&gt;
   /|  /&lt;br /&gt;
   \| /&lt;br /&gt;
    a&lt;br /&gt;
&lt;br /&gt;
gilt für die Knotengrade: &amp;lt;tt&amp;gt;degree(a) == degree(c) == degree(d) == 3&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;degree(b) == 5&amp;lt;/tt&amp;gt;. Genauer muss man bei diesem Graphen von einem ''Multigraphen'' sprechen, weil es zwischen einigen Knotenpaaren (nämlich (a, b) sowie (b, c)) mehrere Kanten (&amp;quot;Mehrfachkanten&amp;quot;) gibt. Wir werden in dieser Vorlesung nicht näher auf Multigraphen eingehen.&lt;br /&gt;
&lt;br /&gt;
;Gerichteter Graph: Ein Graph heißt ''gerichtet'', wenn die Kanten (u,v) und (v,u) unterschieden werden. Die Kante (u,v) ∈ E wird nun als Kante von u nach v (aber nicht umgekehrt) interpretiert. Entsprechend unterscheidet man jetzt den ''eingehenden'' und den ''ausgehenden Grad'' jedes Knotens:&lt;br /&gt;
:*out_degree(v) = |{v' ∈ V | (v,v') ∈ E}|&amp;lt;br/&amp;gt;&lt;br /&gt;
:*in_degree(v)  = |{v' ∈ V| (v',v) ∈ E}|&lt;br /&gt;
&lt;br /&gt;
Das folgende Bild zeigt einen gerichteten Graphen. Hier gilt &amp;lt;tt&amp;gt;out_degree(1) == out_degree(3) == in_degree(2) == in_degree(4) == 2&amp;lt;/tt&amp;gt; und &lt;br /&gt;
&amp;lt;tt&amp;gt;in_degree(1) == in_degree(3) == out_degree(2) == out_degree(4) == 0&amp;lt;/tt&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
[[Image:digraph.png|gerichteter Graph]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Vollständiger Graph: Ein vollständiger Graph ist ein ungerichteter Graph, bei dem jeder Knoten mit allen anderen Knoten verbunden ist.&lt;br /&gt;
:::&amp;lt;math&amp;gt;E = \{ (v,w) |  v \in V, w \in V, v \ne w \}&amp;lt;/math&amp;gt;&lt;br /&gt;
:Ein vollständiger Graph mit |V| Knoten hat &amp;lt;math&amp;gt;|E| = \frac{|V|(|V|-1)}{2}&amp;lt;/math&amp;gt; Kanten.&lt;br /&gt;
&lt;br /&gt;
Die folgenden Abbildungen zeigen die vollständigen Graphen mit einem bis fünf Knoten (auch als K&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; bis K&amp;lt;sub&amp;gt;5&amp;lt;/sub&amp;gt; bezeichnet).&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;0&amp;quot; style=&amp;quot;margin: 1em auto 1em auto&amp;quot;&lt;br /&gt;
|- &lt;br /&gt;
| [[Image:k1.png|frame|k1]]&lt;br /&gt;
| [[Image:k2.png|frame|k2]]&lt;br /&gt;
| [[Image:k3.png|frame|k3]]&lt;br /&gt;
|-&lt;br /&gt;
| [[Image:k4.png|frame|k4]]&lt;br /&gt;
| [[Image:k5.png|frame|k5]]&lt;br /&gt;
|&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
''Rätsel''&amp;lt;br/&amp;gt;&lt;br /&gt;
Auf einer Party sind Leute. Alle stoßen miteinander an. Es hat 78 mal &amp;quot;Pling&amp;quot; gemacht.&lt;br /&gt;
Wieviele Leute waren da? Antwort: Jede Person ist ein Knoten des Graphen, jedes Antoßen eine Kante. &lt;br /&gt;
Da alle miteinander angestoßen haben, handelt es sich um einen vollständigen Graphen. Mit&lt;br /&gt;
|V|(|V|-1)/2 = 78 folgt, dass es 13 Personen waren.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Gewichteter Graph: Ein Graph heißt ''gewichtet'', wenn jeder Kante eine reelle Zahl zugeordnet ist. Bei vielen Anwendungen beschränkt man sich auch auf nichtnegative reelle Gewichte. In einem gerichteten Graphen können die Gewichte der Kanten (u,v) und (v,u) unterschiedlich sein.&lt;br /&gt;
&lt;br /&gt;
Die Gewichte kodieren Eigenschaften der Kanten, die für die jeweilige Anwendung interessant sind. Bei der Berechnung des maximalen Flusses in einem Netzwerk sind die Gewichte z.B. die Durchflusskapazitäten jeder Kante, bei der Suche nach kürzesten Weges kodieren Sie den Abstand zwischen den Endknoten der Kante, bei Währungsnetzwerken (jeder Knoten ist eine Währung) geben sie die Wechselkurse an, usw..&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Teilgraphen: Ein Graph G' = (V',E') ist ein Teilgraph eines Graphen G, wenn gilt:&lt;br /&gt;
:* V' &amp;amp;sube; V &lt;br /&gt;
:* E' &amp;amp;sub; E &lt;br /&gt;
:Er heißt ''(auf)spannender Teilgraph'', wenn gilt:&lt;br /&gt;
:* V' = V&lt;br /&gt;
:Er heißt ''induzierter Teilgraph'', wenn gilt:&lt;br /&gt;
:* e = (u,v) ∈ E' &amp;amp;sub; E &amp;amp;hArr; u ∈ V' und v ∈ V'&lt;br /&gt;
:Den von V' induzierten Teilgraphen erhält man also, indem man aus G alle Knoten löscht, die nicht in V' sind, sowie alle Kanten (und nur diese Kanten), die einen der gelöschten Knoten als Endknoten haben.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Wege, Pfade, Zyklen, Kreise, Erreichbarkeit: Sei G = (V,E) ein Graph (ungerichtet oder gerichteter) Graph. Dann gilt folgende rekursive Definition:&lt;br /&gt;
:* Für v ∈ V ist (v) ein Weg der Länge 0 in G&lt;br /&gt;
:* Falls &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1})&amp;lt;/math&amp;gt; ein Weg ist, und eine Kante &amp;lt;math&amp;gt;(v_{n-1}, v_n)\in E&amp;lt;/math&amp;gt; existiert, dann ist auch &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ein Weg, und er hat die Länge n. &lt;br /&gt;
: Ein Weg ist also eine nichtleere Folge von Knoten, so dass aufeinander folgende Knoten stets durch eine Kante verbunden sind. Die Länge des Weges entspricht der Anzahl der Kanten im Weg (= Anzahl der Knoten - 1).&lt;br /&gt;
:* Ein ''Pfad'' &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ist ein Weg, bei dem alle Knoten v&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt; verschieden sind.&lt;br /&gt;
:* ''Ein Zyklus'' &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1}, v_n)&amp;lt;/math&amp;gt; ist ein Weg, der zum Ausgangspunkt zurückkehrt, wenn also v&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; = v&amp;lt;sub&amp;gt;n&amp;lt;/sub&amp;gt; gilt.&lt;br /&gt;
:* Ein ''Kreis'' ist ein Zyklus ohne Überkreuzungen. Das heisst, es gilt v&amp;lt;sub&amp;gt;0&amp;lt;/sub&amp;gt; = v&amp;lt;sub&amp;gt;n&amp;lt;/sub&amp;gt; und &amp;lt;math&amp;gt;(v_0, v_1, ..., v_{n-1})&amp;lt;/math&amp;gt; ist ein Pfad.&lt;br /&gt;
:* Ein Knoten w ∈ V ist von einem anderen Knoten v ∈ V aus ''erreichbar'' genau dann, wenn ein Weg (v, ..., w) existiert. Wir schreiben dann &amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt;.&lt;br /&gt;
In einem ungerichteten Graph ist die Erreichbarkeits-Relation stets symmetrisch, das heisst aus &amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; folgt &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. In einem gerichteten Graphen ist dies im allgemeinen nicht der Fall.&lt;br /&gt;
&lt;br /&gt;
Bestimmte Wege haben spezielle Namen&lt;br /&gt;
&lt;br /&gt;
;Eulerweg: Ein Eulerweg ist ein Weg, der alle '''Kanten''' genau einmal enthält.&lt;br /&gt;
&lt;br /&gt;
Die eingangs erwähnte Frage des Königsberger Brückenproblems ist equivalent zu der Frage, ob der dazugehörige Graph einen Eulerweg besitzt (daher der Name). Ein anderes bekanntes Beispiel ist das &amp;quot;Haus vom Nikolaus&amp;quot;: Wenn man diesen Graphen in üblicher Weise in einem Zug zeichnet, erhält man gerade den Eulerweg. &lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O----O&lt;br /&gt;
  | \/ |&lt;br /&gt;
  | /\ |   &amp;quot;Das Haus vom Nikolaus&amp;quot;: Alle ''Kanten'' werden nur ''einmal'' passiert&lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Hamiltonweg: Ein Hamiltonweg ist ein Weg, der alle '''Knoten''' genau einmal enthält. Das &amp;quot;Haus vom Nikolaus&amp;quot; besitzt auch einen Hamiltonweg:&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /   &lt;br /&gt;
  O----O&lt;br /&gt;
     /  &lt;br /&gt;
    /      Alle ''Knoten'' werden nur ''einmal'' passiert&lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Hamiltonkreis: Ein Hamiltonkreis ist ein Kreis, der alle '''Knoten''' genau einmal enthält. Auch ein solches Gebilde ist im Haus von Nilolaus enthalten:&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O    O&lt;br /&gt;
  |    |   v0 = vn&lt;br /&gt;
  |    |   vi != vj   Für Alle i,j   i !=j; i,j &amp;gt;0; i,j &amp;lt; n&lt;br /&gt;
  O----O     &lt;br /&gt;
&lt;br /&gt;
Die folgende Skizze zeigt hingegen einen Zyklus: Der Knoten rechts unten sowie die untere Kante sind zweimal enthalten (die Kante einmal von links nach rechts und einmal von rechts nach links):&lt;br /&gt;
&lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O    O&lt;br /&gt;
    \  |&lt;br /&gt;
     \ |   Zyklus&lt;br /&gt;
  O====O&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Zusammenhang, Zusammenhangskomponenten: Ein ungerichteter Graph G heißt ''zusammenhängend'', wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt;&lt;br /&gt;
:Ein gerichteter Graph G ist zusammenhängend, wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; '''oder''' &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. &lt;br /&gt;
:Er ist ''stark zusammenhängend'', wenn für alle v,w ∈ V gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;v \rightsquigarrow w&amp;lt;/math&amp;gt; '''und''' &amp;lt;math&amp;gt;w \rightsquigarrow v&amp;lt;/math&amp;gt;. &lt;br /&gt;
:Entsprechende Definitionen gelten für Teilgraphen G'. Ein Teilgraph G' heisst ''Zusammenhangskomponente'' von G, wenn er ein ''maximaler'' zusammenhängender Teilgraph ist, d.h. wenn G' zusammenhängend ist, und man keine Knoten und Kanten aus G mehr zu G' hinzufügen kann, so dass G' immer noch zusammenhängend bleibt. Entsprechend definiert man ''starke Zusammenhangskomponenten'' in einem gerichteten Graphen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Planarer Graph, ebener Graph: Ein Graph heißt ''planar'', wenn er so in einer Ebene gezeichnet werden ''kann'', dass sich die Kanten nicht schneiden (außer an den Knoten). Ein Graph heißt ''eben'', wenn er tatsächlich so gezeichnet ''ist'', dass sich die Kanten nicht schneiden. Die Einbettung in die Ebene ist im allgemeinen nicht eindeutig.&lt;br /&gt;
&lt;br /&gt;
'''Beispiele:'''&lt;br /&gt;
&lt;br /&gt;
Der folgende Graph ist planar und eben:&lt;br /&gt;
 &lt;br /&gt;
      O&lt;br /&gt;
     /|\&lt;br /&gt;
    / O \&lt;br /&gt;
   / / \ \&lt;br /&gt;
   O     O&lt;br /&gt;
&lt;br /&gt;
Das &amp;quot;Haus vom Nikolaus&amp;quot; ist ebenfalls planar, wird aber üblicherweise nicht als ebener Graph gezeichnet, weil sich die Diagonalen auf der Wand überkreuzen:&lt;br /&gt;
 &lt;br /&gt;
    O&lt;br /&gt;
   /  \&lt;br /&gt;
  O----O&lt;br /&gt;
  | \/ |&lt;br /&gt;
  | /\ |   &lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
Eine ebene Einbettung dieses Graphen wird erreicht, wenn man eine der Diagonalen ausserhalb des Hauses zeichnet. Der Graph (also die Menge der Knoten und Kanten) ändert sich dadurch nicht.&lt;br /&gt;
 &lt;br /&gt;
      O  &lt;br /&gt;
     /  \&lt;br /&gt;
  --O----O&lt;br /&gt;
 /  |  / |&lt;br /&gt;
 |  | /  |   &lt;br /&gt;
 |  O----O      Das &amp;quot;Haus vom Nikolaus&amp;quot; als ebener Graph gezeichnet.&lt;br /&gt;
  \     /&lt;br /&gt;
   -----&lt;br /&gt;
&lt;br /&gt;
Eine alternative Einbettung erhalten wir, wenn wir die andere Diagonale außerhalb des Hauses zeichnen:&lt;br /&gt;
 &lt;br /&gt;
      O  &lt;br /&gt;
     /  \&lt;br /&gt;
    O----O--|&lt;br /&gt;
    | \  |  |&lt;br /&gt;
    |  \ |  | &lt;br /&gt;
    O----O  |     Alternative Einbettung des &amp;quot;Haus vom Nikolaus&amp;quot;.&lt;br /&gt;
    |       |&lt;br /&gt;
    |-------|&lt;br /&gt;
&lt;br /&gt;
Jede Einbettung eines planaren Graphen (also jeder ebene Graph) definiert eine eindeutige Menge von ''Regionen'':&lt;br /&gt;
&lt;br /&gt;
 |----O   @&lt;br /&gt;
 |   /@ \&lt;br /&gt;
 |  O----O&lt;br /&gt;
 |  |@ / |&lt;br /&gt;
 |  | / @|   &lt;br /&gt;
 |  O----O        @ entspricht jeweils einer ''Region''. Auch ausserhalb der Figur ist eine Region (die sogenannte ''unendliche'' Region).&lt;br /&gt;
 |@      |&lt;br /&gt;
 |-------|&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Der vollständige Graph K5 ist kein planarer Graph, da sich zwangsweise Kanten schneiden, wenn man diesen Graphen in der Ebene zeichnet.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
;Dualer Graph: Jeder ebene Graph G = (V, E) hat einen ''dualen Graphen'' D = (V&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;, E&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;), dessen Knoten und Kanten wie folgt definiert sind:&lt;br /&gt;
:* V&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt; enthält einen Knoten für jede Region des Graphen G&lt;br /&gt;
:* Für jede Kante e ∈ E gibt es eine duale Kante e&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt; ∈ E&amp;lt;sub&amp;gt;D&amp;lt;/sub&amp;gt;, die die an e angrenzenden Regionen (genauer: die entsprechenden Knoten in D) verbindet.&lt;br /&gt;
&lt;br /&gt;
Die folgende Abbildung zeigt einen Graphen (grau) und seinen dualen Graphen (schwarz). Die Knoten des dualen Graphen sind mit Zahlen gekennzeichnet und entsprechen den Regionen des Originalgraphen. Jeder (grauen) Kante des Originalgraphen entspricht eine (schwarze) Kante des dualen Graphen.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
[[Image:dual-graphs.png]]&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für duale Graphen gilt: Wenn der Originalgraph zusammenhängend ist, enthält jede Region des dualen Graphen genau einen Knoten des Originalgraphen. Deshalb ist der duale Graph des dualen Graphen wieder der Originalgraph. Bei nicht-zusammenhängenden Graphen gilt dies nicht (vgl. das Fenster bei obigem Bild). In diesem Fall hat der duale Graph mehrere mögliche Einbettungen in die Ebene (man kann z.B. die rechte Kante zwischen Knoten 2 und 4 auch links vom Fenster einzeichnen), und man erhält nicht notwendigerweise den Originalgraphen, wenn man den dualen Graphen des dualen berechnet.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Baum: Ein ''Baum'' ist ein zusammenhängender, kreisfreier Graph.&lt;br /&gt;
&lt;br /&gt;
Beispiel: Binärer Suchbaum&lt;br /&gt;
&lt;br /&gt;
;Spannbaum: Ein ''Spannbaum'' eines zusammenhängenden Graphen G ist ein zusammenhängender, kreisfreier Teilgraph von G, der alle Knoten von G enthält&lt;br /&gt;
&lt;br /&gt;
Beispiel: Spannbaum für das &amp;quot;Haus des Nikolaus&amp;quot; &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
    O   &lt;br /&gt;
   /       &lt;br /&gt;
  O    O&lt;br /&gt;
  |  /  &lt;br /&gt;
  | /   &lt;br /&gt;
  O----O&lt;br /&gt;
&lt;br /&gt;
Der Spannbaum eines Graphen mit |V| Knoten hat stets |V| - 1 Kanten.&lt;br /&gt;
&lt;br /&gt;
;Wald: Ein ''Wald'' ist ein unzusammenhängender, kreisfreier Graph.&lt;br /&gt;
: Jede Zusammenhangskomponente eines Waldes ist ein Baum.&lt;br /&gt;
&lt;br /&gt;
=== Repräsentation von Graphen ===&lt;br /&gt;
&lt;br /&gt;
Sei G = ( V, E ) gegeben und liege V in einer linearen Sortierung vor.&amp;lt;br/&amp;gt; &lt;br /&gt;
:::&amp;lt;math&amp;gt;V = \{ v_1, ...., v_n \}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Adjazenzmatrix: Ein Graph kann durch eine Adjazenzmatrix repräsentiert werden, die soviele Zeilen und Spalten enthält, wie der Graph Knoten hat. Die Elemente der Adjazenzmatrix sind &amp;quot;1&amp;quot;, falls eine Kante zwischen den zugehörigen Knoten existiert:&lt;br /&gt;
:::&amp;lt;math&amp;gt;\mathrm{\bold A} = a_{ij} = &lt;br /&gt;
\begin{cases}&lt;br /&gt;
1 &amp;amp; \mathrm{falls}\quad (v_i, v_j) \in E \\&lt;br /&gt;
0 &amp;amp; \mathrm{sonst}&lt;br /&gt;
\end{cases} &lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
:Die Indizes der Matrix entsprechen also den Indizes der Knoten gemäß der gegebenen Sortierung. Im Falle eines ungerichteten Graphen ist die Adjazenzmatrix stets symmetrisch (d.h. es gilt &amp;lt;math&amp;gt;a_{ij}=a_{ji}&amp;lt;/math&amp;gt;), bei einem gerichteten Graphen ist sie im allgemeinen unsymmetrisch.&lt;br /&gt;
&lt;br /&gt;
Beispiel für einen ungerichteten Graphen:&lt;br /&gt;
&lt;br /&gt;
 v = { a,b,c,d }     b      d&lt;br /&gt;
                     | \  / |&lt;br /&gt;
                     |  \/  |&lt;br /&gt;
                     |  /\  |&lt;br /&gt;
                     | /  \ |&lt;br /&gt;
                     a      c&lt;br /&gt;
 &lt;br /&gt;
       a b c d&lt;br /&gt;
      -----------&lt;br /&gt;
      (0 1 0 1) |a &lt;br /&gt;
  A = (1 0 1 0) |b&lt;br /&gt;
      (0 1 0 1) |c&lt;br /&gt;
      (1 0 1 0) |d&lt;br /&gt;
&lt;br /&gt;
Die Adjazenzmatrixdarstellung eignet sich besonders für dichte Graphen (d.h. wenn die Zahl der Kanten in O(|V|&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt;) ist.&lt;br /&gt;
&lt;br /&gt;
;Adjazenzlisten: In der Adjazenzlistendarstellung wird der Graph als Liste von Knoten repräsentiert, die für jeden Knoten einen Eintrag enthält. Der Eintrag für jeden Knoten ist wiederum eine Liste, die die Nachbarknoten dieses Knotens enthält:&lt;br /&gt;
:* graph = {adjazencyList(v) | v ∈ V}&lt;br /&gt;
:* adjazencyList(v) = {v' ∈ V | (v, v') ∈ E}&lt;br /&gt;
&lt;br /&gt;
In Python implementieren wir Adjazenzlisten zweckmäßig als Array von Arrays:&lt;br /&gt;
&lt;br /&gt;
                   graph = [[...],[...],...,[...]]&lt;br /&gt;
 Adjazenzliste für Knoten =&amp;gt;  0     1         n&lt;br /&gt;
&lt;br /&gt;
Wenn wir bei dem Graphen oben die Knoten wie bei der Adjazenzmatrix indizieren (also &amp;lt;tt&amp;gt;a =&amp;gt; 0&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;b =&amp;gt; 1&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;c =&amp;gt; 2&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;d =&amp;gt; 3&amp;lt;/tt&amp;gt;), erhalten wir die Adjazenzlistendarstellung:&lt;br /&gt;
&lt;br /&gt;
 graph = [[b, d], [a, c],[b, d], [a, c]]&lt;br /&gt;
&lt;br /&gt;
Auf die Nachbarknoten eines durch seinen Index &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; gegebenen Knotens können wir also wie folgt zugreifen:&lt;br /&gt;
&lt;br /&gt;
      for neighbors in graph[node]:&lt;br /&gt;
          ... # do something with neighbor&lt;br /&gt;
&lt;br /&gt;
Die Adjazenzlistendarstellung ist effizienter, wenn der Graph nicht dicht ist, so dass viele Einträge der Adjazenzmatrix Null wären. In der Vorlesung werden wir nur diese Darstellung verwenden.&lt;br /&gt;
&lt;br /&gt;
;&amp;lt;div id=&amp;quot;transposed_graph&amp;quot;&amp;gt;Transponierter Graph&amp;lt;/div&amp;gt;: Den ''transponierten Graphen'' G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; eines gerichteten Graphen G erhält man, wenn man alle Kantenrichtungen umkehrt.&lt;br /&gt;
&lt;br /&gt;
Bei ungerichteten Graphen hat die Transposition offensichtlich keinen Effekt, weil alle Kanten bereits in beiden Richtungen vorhanden sind, so dass G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; = G gilt. Bei gerichteten Graphen ist die Transposition einfach, wenn der Graph als Adjazenzmatrix implementiert ist, weil man einfach die transponierte Adjazenzmatrix verwenden muss (beachte, dass sich die Reihenfolge der Indizes umkehrt):&lt;br /&gt;
:::A&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; = a&amp;lt;sub&amp;gt;ji&amp;lt;/sub&amp;gt;&lt;br /&gt;
Ist der Graph hingegen durch eine Adjazenzliste repräsentiert, muss etwas mehr Aufwand getrieben werden:&lt;br /&gt;
&lt;br /&gt;
 def transposeGraph(graph):&lt;br /&gt;
      gt = [[] for k in graph]   # zunächst leere Adjazenzlisten von G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt;&lt;br /&gt;
      for node in range(len(graph)):&lt;br /&gt;
           for neighbor in graph[node]:&lt;br /&gt;
               gt[neighbor].append(node)  # füge die umgekehrte Kante in G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; ein&lt;br /&gt;
      return gt&lt;br /&gt;
&lt;br /&gt;
== Durchlaufen von Graphen (Graph Traversal) ==&lt;br /&gt;
&lt;br /&gt;
Wir betrachten zunächst ungerichtete Graphen mit V Knoten und E Kanten. Eine grundlegende Aufgabe in diesen Graphen besteht darin, alle Knoten in einer bestimmten Reihenfolge genau einmal zu besuchen. Hierbei darf man sich von einem gegebenen Startknoten aus nur entlang der Kanten des Graphen bewegen. Die beim Traversieren benutzen Kanten bilden einen Baum, dessen Wurzel der Startknoten ist und der den gesamten Graphen aufspannt, falls der Graph zusammenhängend ist. (Beweis: Da jeder Knoten nur einmal besucht wird, gibt es für jeden besuchten Knoten [mit Ausnahme des Startknotens] genau eine eingehende Kante. Ist der Graph zusammenhängend, wird jeder Knoten tatsächlich erreicht und es gibt genau (V-1) Kanten, exakt soviele wie für einen Baum mit V Knoten notwendig sind.) Ist der Graph nicht zusammenhängend, wird jeder zusammenhängende Teilgraph (jede &amp;lt;i&amp;gt;Zusammenhangskomponente&amp;lt;/i&amp;gt;) getrennt traversiert, und man erhält einen sogenannten &amp;lt;i&amp;gt;Wald&amp;lt;/i&amp;gt; mit einem Baum pro Zusammenhangskomponente. Die beiden grundlegenden Traversierungsmethoden &amp;lt;i&amp;gt;Tiefensuche&amp;lt;/i&amp;gt; und &amp;lt;i&amp;gt;Breitensuche&amp;lt;/i&amp;gt; werden im folgenden vorgestellt.&lt;br /&gt;
&lt;br /&gt;
=== Tiefensuche in Graphen (Depth First Search, DFS) ===&lt;br /&gt;
&lt;br /&gt;
Die Idee der Tiefensuche besteht darin, jeden besuchten Knoten sofort über die erste Kante wieder zu verlassen, die zu einem noch nicht besuchten Knoten führt. Man findet dadurch schnell einen möglichst langen Pfad durch den Graphen, und der Traversierungs-Baum wird zunächst in die Tiefe verfolgt, daher der Name des Verfahrens. Hat ein Knoten keine unbesuchten Nachbarknoten mehr, geht man im Baum zurück (sogenanntes &amp;lt;i&amp;gt;back tracking&amp;lt;/i&amp;gt;), bis man einen Knoten findet, der noch eine unbesuchte Nachbarn besitzt, und traversiert diese nach dem gleichen Muster. Gibt es gar keine unbesuchten Knoten mehr, kehrt die Suche zum Startknoten zurück und endet dort.&lt;br /&gt;
&lt;br /&gt;
WDie folgende rekursive Implementation der Tiefensuche erwartet den Graphen in Adjazenzlistendarstellung und beginnt die Suche beim Knoten &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt;. Die Information, ob ein Knoten bereits besucht wurde, wird im Array &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; gespeichert. Ein solches Array, das zusätzliche Informationen über die Knoten des Graphen bereitstellt, wir häufig &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; genannt.&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)  # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):              # rekursive Hilfsfunktion, die den gegebenen Knoten und dessen Nachbarn besucht&lt;br /&gt;
         if not visited[node]:     # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True  # Markiere node als besucht&lt;br /&gt;
             print node            # Ausgabe der Knotennummer - pre-order&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode)&lt;br /&gt;
&lt;br /&gt;
[[Image:Tiefens.jpg]]&lt;br /&gt;
&lt;br /&gt;
Ausgabe für den Graphen in diesem Bild (es handelt sich um einen ungerichteten Graphen, die Pfeile symbolisieren nur die Suchrichtung beim Traversal):&lt;br /&gt;
&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; dfs(graph, 1)&lt;br /&gt;
 1&lt;br /&gt;
 2&lt;br /&gt;
 4&lt;br /&gt;
 3&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
 5&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div id=&amp;quot;pre_and_post_order&amp;quot;&amp;gt;In dieser Version des Algorithmus werden die Knotennummern ausgegeben, bevor die Nachbarknoten besucht werden. Man bezeichnet die resultierende Sortierung der Knoten als &amp;lt;b&amp;gt;pre-order&amp;lt;/b&amp;gt; oder als &amp;lt;b&amp;gt;discovery order&amp;lt;/b&amp;gt;. Alternativ kann man die Knotennummern erst ausgeben, nachdem alle Nachbarn besucht wurden, also auf dem Rückweg der Rekursion. In diesem Fall spricht man von &amp;lt;b&amp;gt;post-order&amp;lt;/b&amp;gt; oder &amp;lt;b&amp;gt;finishing order&amp;lt;/b&amp;gt;:&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)  # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):              # rekursive Hilfsfunktion, die den gegebenen Knoten und dessen Nachbarn besucht&lt;br /&gt;
         if not visited[node]:     # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True  # Markiere node als besucht&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
             &amp;lt;font color=red&amp;gt;print node            # Ausgabe der Knotennummer - post-order&amp;lt;/font&amp;gt;&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode)&lt;br /&gt;
&lt;br /&gt;
Es ergibt sich jetzt die Ausgabe:&lt;br /&gt;
&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; dfs(graph, 1)&amp;lt;font color=red&amp;gt;&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
 3&lt;br /&gt;
 4&lt;br /&gt;
 5&lt;br /&gt;
 2&lt;br /&gt;
 1&amp;lt;/font&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In realem Code ersetzt man die print-Ausgaben natürlich durch anwendungsspezifische Aktionen und Berechnungen. Einige Anwendungen sind uns im Kapitel [[Suchen]] bereits begegnet. &lt;br /&gt;
; Anwendungen der Pre-Order Traversierung&lt;br /&gt;
* Kopieren eines Graphen: kopiere zuerst den besuchten Knoten, dann seine Nachbarn und die dazugehörigen Kanten (sowie die Kanten zu bereits besuchten Knoten, die in der Grundversion der Tiefensuche ignoriert werden).&lt;br /&gt;
* Bestimmen der Zusammenhangskomponenten eines Graphen (siehe unten)&lt;br /&gt;
* In einem Zeichenprogramm: fülle eine Region mit einer Farbe (&amp;quot;flood fill&amp;quot;). Dabei ist jedes Pixel ein Knoten des Graphen und wird mit seinen 4 Nachbarpixceln verbunden. Die Tiefensuche startet bei der Mausposition und endet am Rand des betreffendcen Gebiets.&lt;br /&gt;
* Falls der Graph ein Baum ist: bestimme den Abstand jedes Knotens von der Wurzel&lt;br /&gt;
* Falls der Graph ein Parse-Baum ist, wobei innere Knoten Funktionsaufrufe, Kindknoten Funktionsargumente, und Blattknoten Werte repräsentieren: drucke den zugehörigen Ausdruck aus (also immer zuerst den Funktionsnamen, dann die Argumente, die wiederum geschachtelte Funktionsaufrufe sein können).&lt;br /&gt;
; Anwendungen der Post-Order Traversierung&lt;br /&gt;
* Löschen eines Graphen: lösche zuerst die Nachbarn, dann den Knoten selbst&lt;br /&gt;
* Bestimmen einer topologischen Sortierung eines azyklischen gerichteten Graphens (siehe unten)&lt;br /&gt;
* Falls der Graph ein Baum ist: bestimme den Abstand jedes Knotens von den Blättern (also die Tiefe des Baumes, siehe Übung 5)&lt;br /&gt;
* Falls der Graph ein Parse-Baum ist: führe die zugehörige Berechnung aus (d.h. berechne zuerst die geschachtelten inneren Funktionen, dann mit diesen Ergebnissen die nächst äußeren usw., siehe Übung 5).&lt;br /&gt;
; Anwendungen, die Pre- und Post-Order benötigen&lt;br /&gt;
* Weg aus einem Labyrinth: die Pre-Order dokumentiert die Suche nach dem Weg, die Post-Order zeigt den Rückweg aus Sackgassen (siehe Übung 9).&lt;br /&gt;
Im Spezialfall, wenn der Graph ein Binärbaum ist, unterscheidet man noch eine dritte Variante der Traversierung, nämlich die &amp;lt;i&amp;gt;in-order&amp;lt;/i&amp;gt; Traversierung. In diesem Fall behandelt man den Vaterknoten nach den linken, aber vor den rechten Kindern. Diese Reihenfolge wird beim [[Suchen#Beziehungen zwischen dem Suchproblem und dem Sortierproblem|Tree Sort Algorithmus]] verwendet. Diese Sortierung verwendet man auch, wenn man einen Parse-Baum mit binären Operatoren (statt Funktionsaufrufen) ausgeben will, siehe Übung 5.&lt;br /&gt;
&lt;br /&gt;
Eine nützliche Erweiterung der Tiefensuche besteht darin, in der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; nicht nur zu dokumentieren, dass ein Knoten bereits besucht wurde, sondern auch, von welchem Knoten aus man den jeweiligen Knoten zuerst erreicht hat. Im entstehenden Tiefensuchbaum ist dies gerade der Vaterknoten, weshalb wir die verbesserte property map zweckmäßigerweise in &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; umbenennen. Für den Startknoten, also die Wurzel des Baumes, wählen wir die Konvention, dass er sein eigener Vaterknoten ist (die Konvention, dafür den Wert &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zu verwenden, scheidet aus, weil dies bereits die Tatsache signalisiert, dass ein Knoten noch nicht besucht wurde):&lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     parents = [None]*len(graph)     # Registriere für jeden Knoten den Vaterknoten im Tiefensuchbaum&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, parent):        # rekursive Hilfsfunktion&lt;br /&gt;
         if parents[node] is None:   # Besuche node, wenn er noch nicht besucht wurde&lt;br /&gt;
             parents[node] = parent  # Markiere node als besucht und speichere seinen Vaterknoten&lt;br /&gt;
             for neighbor in graph[node]:   # Besuche rekursiv die Nachbarn ...&lt;br /&gt;
                 visit(neighbor, node)      #  ... wobei node zu deren Vaterknoten wird&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode, startnode)     # Konvention für Wurzel: startnode ist sein eigener Vater&lt;br /&gt;
     &lt;br /&gt;
     return parents                  # Rückgabe des berechneten Tiefensuch-Baums&lt;br /&gt;
&lt;br /&gt;
Die Ausgabe für den obigen Beispielgraphen lautet: &lt;br /&gt;
  Knotennummer  |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7&lt;br /&gt;
  --------------+-----+-----+-----+-----+-----+-----+-----+-----&lt;br /&gt;
  Vaterknoten   | None|  1  |  1  |  4  |  2  |  2  |  3  |  3&lt;br /&gt;
&lt;br /&gt;
Dabei ist die Knotennummer der Index im Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt;, und der Vaterknoten ist der dazugehörige Arrayeintrag. Beachte, dass Knoten 0 in diesem Graphen nicht existiert, daher ist sein Eintrag &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;. Per Konvention hat der Wurzelknoten 1 sich selbst als Vater.&lt;br /&gt;
&lt;br /&gt;
=== Breitensuche in Graphen (Breadth First Search, BFS) ===&lt;br /&gt;
&lt;br /&gt;
Im Gegensatz zur Tiefensuche werden bei der Breitensuche alle Nachbarnknoten abgearbeitet, &amp;lt;i&amp;gt;bevor&amp;lt;/i&amp;gt; man rekursiv deren Nachbarn besucht. Man betrachtet somit zuerst alle Knoten, die den Abstand 1 von Startknoten haben, dann diejenigen mit dem Abstand 2 usw. Diese Reihenfolge bezeichnet man als &amp;lt;i&amp;gt;level-order&amp;lt;/i&amp;gt;. Wir sind ihr beispielsweise in Übung 6 begegnet, als die ersten 7 Ebenen eines Treap ausgegeben werden sollten. Man implementiert Breitensuche zweckmäßig mit Hilfe einer Queue, die die Knoten in First In - First Out - Reihenfolge bearbeitet. Eine geeignete Datenstruktur hierfür ist die Klasse &amp;lt;tt&amp;gt;[http://docs.python.org/library/collections.html#collections.deque deque]&amp;lt;/tt&amp;gt; aus dem Python-Modul &amp;lt;tt&amp;gt;[http://docs.python.org/library/collections.html collections]&amp;lt;/tt&amp;gt; (eine Deque implementiert sowohl die Funktionalität einer Queue wie auch die eines Stacks, siehe Übung 3):&lt;br /&gt;
&lt;br /&gt;
 from collections import deque&lt;br /&gt;
 &lt;br /&gt;
 def bfs(graph, startnode)&lt;br /&gt;
     visited = [False]*len(graph)   # Flags, welche Knoten bereits besucht wurden&lt;br /&gt;
   &lt;br /&gt;
     q = deque()                    # Queue für die zu besuchenden Knoten&lt;br /&gt;
     q.append(startnode)            # Startknoten in die Queue einfügen&lt;br /&gt;
     &lt;br /&gt;
     while len(q) &amp;gt; 0:              # Solange es noch unbesuchte Knoten gibt&lt;br /&gt;
         node = q.popleft()         # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
         if not visited[node]:      # Falls node noch nicht (auf einem anderen Weg) besucht wurde&lt;br /&gt;
              visited[node] = True  # Markiere node als besucht&lt;br /&gt;
              print node            # Drucke Knotennummer&lt;br /&gt;
              for neighbor in graph[node]:    # Füge Nachbarn in die Queue ein&lt;br /&gt;
                  q.append(neighbor)&lt;br /&gt;
&lt;br /&gt;
[[Image:Breitens.jpg]]&lt;br /&gt;
&lt;br /&gt;
Der Aufruf dieser Funktion liefert die Knoten des obigen Graphens ebenenweise, also zufällig genau in der Reihenfolge der Knotennummern:&lt;br /&gt;
 &amp;gt;&amp;gt;&amp;gt; bfs(graph, 1)&lt;br /&gt;
 1&lt;br /&gt;
 2&lt;br /&gt;
 3&lt;br /&gt;
 4&lt;br /&gt;
 5&lt;br /&gt;
 6&lt;br /&gt;
 7&lt;br /&gt;
&lt;br /&gt;
Neben der ebenenweisen Ausgabe hat die Breitensuche viele weitere wichtige Anwendungen, z.B. beim Testen, ob ein gegebener Graph bi-partit ist (siehe [http://en.wikipedia.org/wiki/Breadth-first_search#Testing_bipartiteness WikiPedia]), sowie bei der Suche nach kürzesten Wegen (siehe unten) und kürzesten Zyklen.&lt;br /&gt;
&lt;br /&gt;
== Weitere Anwendungen der Tiefensuche ==&lt;br /&gt;
&lt;br /&gt;
Die Tiefensuche hat zahlreiche Anwendungen, wobei der grundlegende Algorithmus immer wieder leicht modifiziert und an die jeweilige Aufgabe angepasst wird. Wir beschreiben im folgenden einige Beispiele.&lt;br /&gt;
&lt;br /&gt;
=== Damenproblem ===&lt;br /&gt;
&lt;br /&gt;
Tiefensuche wird häufig verwendet, um systematisch nach der Lösung eines logischen Rätsels (oder allgemeiner nach der Lösung eines diskreten Optimierungsproblems) zu suchen. Besonders anschaulich hierfür ist das Damenproblem. Die Aufgabe besteht darin, &amp;lt;math&amp;gt;k&amp;lt;/math&amp;gt; Damen auf einem Schachbrett der Größe &amp;lt;math&amp;gt;k \times k&amp;lt;/math&amp;gt; so zu platzieren, dass sie sich (nach den üblichen Schach-Regeln) nicht gegenseitig schlagen können. Das folgende Diagramm zeigt eine Lösung für den Fall &amp;lt;math&amp;gt;k=4&amp;lt;/math&amp;gt;. Die Positionen der Damen werden dabei wie üblich durch die Angabe der Spalte (Linie) mit Buchstaben und der Zeile (Reihe) mit Zahlen kodiert, hier also A2, B4, C1, D3:&lt;br /&gt;
&lt;br /&gt;
  ---------------&lt;br /&gt;
 |   | X |   |   | 4&lt;br /&gt;
 |---|---|---|---| &lt;br /&gt;
 |   |   |   | X | 3&lt;br /&gt;
 |---|---|---|---|&lt;br /&gt;
 | X |   |   |   | 2&lt;br /&gt;
 |---|---|---|---|&lt;br /&gt;
 |   |   | X |   | 1&lt;br /&gt;
  ---------------&lt;br /&gt;
   A   B   C   D&lt;br /&gt;
&lt;br /&gt;
Um das Problem systematisch zu lösen, konstruieren wir einen gerichteten Graphen, dessen Knoten die möglichen Positionen der Damen kodieren. Wir verbinden Knoten, die zu benachbarten Linien gehören, genau dann mit einer Kante, wenn die zugehörigen Positionen kompatibel sind, also wenn sich die dort positionierten Damen nicht schlagen können. Der resultierende Graph für &amp;lt;math&amp;gt;k=4&amp;lt;/math&amp;gt; hat folgende Gestalt:&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
Knoten, die zur selben Reihe oder Linie gehören, sind beispielsweise nicht direkt verbunden, weil zwei Damen niemals in derselben Linie oder Reihe stehen dürfen. Um eine erlaubte Konfiguration zu finden, verwenden wir nun eine angepasste Version der Tiefensuche: Wir beginnen die Suche beim Knoten &amp;lt;tt&amp;gt;START&amp;lt;/tt&amp;gt;. Sobald wir den Knoten &amp;lt;tt&amp;gt;STOP&amp;lt;/tt&amp;gt; erreichen, beenden wir die Suche und lesen die Lösung am gerade gefundenen Weg von Start nach Stop ab. Zwei kleine Modifikationen des Grundalgorithmus stellen sicher, dass die Bedingungen der Aufgabe eingehalten werden: Wir dürfen bei der Tiefensuche nur dann zu einem Nachbarn weitergehen, wenn die betreffende Position mit allen im Pfad bereits gesetzten Positionen kompatibel ist, andernfalls ist diese Kante tabu. Landen wir aufgrund dieser Regel in einer Sackgasse (also in einem Knoten, wo keine der ausgehenden Kanten erlaubt ist), müssen wir zur nächsten erlaubten Abzweigung zurückgehen (Backtracking). Beim Zurückgehen müssen wir das &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Flag wieder auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zurücksetzen, weil der betreffende Knoten ja möglicherweise auf einem anderen erlaubten Weg erreichbar ist.&lt;br /&gt;
&lt;br /&gt;
Der folgende Graph zeigt einen solchen Fall: Wir haben zwei Damen auf die Felder A1 und B3 positioniert (grüne Pfeile). Die einzig ausgehende Kante von B3 führt zum Knoten C1, welcher aber mit der Position A1 inkompatibel ist, so dass diese Kante nicht verwendet werden darf (roter Pfeil). Das Backtracking muss jetzt zu Knoten A1 zurückgehen (dabei wird das &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Flag von B3 wieder auf &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; gesetzt), weil A1 mit der Kante nach B4 eine weitere Option hat, die geprüft werden muss (die allerdings hier auch nicht zum Ziel führt).&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph-failure.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
Nach einigen weiteren Sackgassen findet man schließlich den Pfad A2, B4, C1, D3, der im folgenden Graphen grün markiert ist und der obigen Lösung entspricht:&lt;br /&gt;
&lt;br /&gt;
[[Image:damenproblem-graph-success.png|500px|center]]&lt;br /&gt;
&lt;br /&gt;
=== Test, ob ein ungerichteter Graph azyklisch ist ===&lt;br /&gt;
&lt;br /&gt;
Ein zusammenhängender ungerichteter Graph ist azyklisch (also ein Baum) genau dann, wenn es nur einen möglichen Weg von jedem Knoten zu jedem anderen gibt. (Bei gerichteten Graphen sind die Verhältnisse komplizierter. Wir behandeln dies weiter unten.) Das kann man mittels Tiefensuche leicht feststellen: Die Kante, über die wir einen Knoten erstmals erreichen, ist eine &amp;lt;i&amp;gt;Baumkante&amp;lt;/i&amp;gt; des Tiefensuchbaums. Erreichen wir einen bereits besuchten Knoten nochmals über eine andere Kante, haben wir einen Zyklus gefunden. Dabei müssen wir allerdings beachten, dass in einem ungerichteten Graphen jede Baumkante zweimal gefunden wird, einmal in Richtung vom Vater zum Kind und einmal in umgekehrter Richtung. Im zweiten Fall endet die Kante zwar in einem bereits besuchten Knoten (dem Vater), aber es entsteht dadurch kein Zyklus. Den Vaterknoten müssen wir deshalb überspringen, wenn wir über die Nachbarn iterieren:&lt;br /&gt;
&lt;br /&gt;
 def undirected_cycle_test(graph):         # Annahme: der Graph ist zusammenhängend&lt;br /&gt;
                                           # (andernfalls führe den Algorithmus für jede Zusammenhangskomponente aus)&lt;br /&gt;
     visited = [False]*len(graph)          # Flags für bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, from_node):           # rekursive Hilfsfunktion: gibt True zurück, wenn Zyklus gefunden wurde&lt;br /&gt;
         if not visited[node]:             # wenn node noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True          # markiere node als besucht&lt;br /&gt;
             for neighbor in graph[node]:  # besuche die Nachbarn ...&lt;br /&gt;
                 if neighbor == from_node: # ... aber überspringe den Vaterknoten&lt;br /&gt;
                     continue&lt;br /&gt;
                 if visit(neighbor, node): # ... signalisiere, wenn rekursiv ein Zyklus gefunden wurde&lt;br /&gt;
                     return True&lt;br /&gt;
             return False                  # kein Zyklus gefunden&lt;br /&gt;
         else:&lt;br /&gt;
             return True                   # Knoten schon besucht =&amp;gt; Zyklus&lt;br /&gt;
     &lt;br /&gt;
     startnode = 0                         # starte bei beliebigem Knoten (hier: Knoten 0)&lt;br /&gt;
     return visit(startnode, startnode)    # gebe True zurück, wenn ein Zyklus gefunden wurde&lt;br /&gt;
&lt;br /&gt;
Wenn wir einen Zyklus finden, wird das weitere Traversieren das Graphen abgebrochen, denn ein Graph, der einmal zyklisch war, kann später nicht wieder azyklisch werden. Die notwendige Modifikation für unzusammenhängende Graphen erfolgt analog zum Algorithmus für die Detektion von Zusammenhangskomponenten, der im nächsten Abschnitt beschrieben wird.&lt;br /&gt;
&lt;br /&gt;
=== Finden von Zusammenhangskomponenten ===&lt;br /&gt;
&lt;br /&gt;
Das Auffinden und Markieren von Zusammenhangskomponenten (also maximalen zusammenhängenden Teilgraphen) ist eine grundlegende Aufgabe in ungerichteten, unzusammenhängenden Graphen (bei gerichteten Graphen sind die Verhältnisse wiederum komplizierter, siehe unten). Zwei Knoten u und v gehören zur selben Zusammenhangskomponente genau dann, wenn es einen Pfad von u nach v gibt (da der Graph ungerichtet ist, gibt es dann auch einen Pfad von v nach u). Man sagt auch, dass &amp;quot;v von u aus erreichbar&amp;quot; ist. Unzusammenhängende Graphen entstehen in der Praxis häufig, wenn die Kanten gewisse Relationen zwischen den Knoten kodieren: &lt;br /&gt;
* Wenn die Knoten Städte sind und die Kanten Straßen, sind diejenigen Städte in einer Zusammenhangskomponente, die per Auto von einander erreichbar sind. Unzusammenhängende Graphen entstehen hier beispielsweise, wenn eine Insel nicht durch eine Brücke erschlossen ist, wenn Grenzen gesperrt sind oder wenn ein Gebirge zu unwegsam ist, um Straßen zu bauen.&lt;br /&gt;
* Wenn Knoten Personen sind, und Kanten die Eltern-Kind-Relation beschreiben, so umfasst jede Zusammenhangskomponenten die Verwandten (auch wenn sie nur über viele &amp;quot;Ecken&amp;quot; verwandt sind).&lt;br /&gt;
* In der Bildverarbeitung entsprechen Knoten den Pixeln, und dieselben werden durch eine Kante verbunden, wenn sie zum selben Objekt gehören. Die Zusammenhangskomponenten entsprechen somit den Objekten im Bild (siehe Übungsaufgabe).&lt;br /&gt;
Die Zusammenhangskomponenten bilden eine Äquivalenzrelation. Folglich kann für jede Komponente ein Reprässentant bestimmt werden, der sogenannte &amp;quot;Anker&amp;quot;. Kennt jeder Knoten seinen Anker, ist das Problem der Zusammenhangskomponenten gelöst. &lt;br /&gt;
&lt;br /&gt;
==== Lösung mittels Tiefensuche ====&lt;br /&gt;
&lt;br /&gt;
Unser erster Ansatz ist, den Anker mit Hilfe der Tiefensuche zu finden. Anstelle der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; verwenden wir diesmal eine property map &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt;, die für jeden Knoten die Knotennummer des zugehörigen Ankers angibt, oder &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, wenn der Knoten noch nicht besucht wurde. Dabei verwenden wir wieder die Konvention, dass Anker auf sich selbst zeigen. Für viele Anwendungen ist es außerdem (oder stattdessen) zweckmäßig, die Zusammenhangskomponenten mit einer laufenden Nummer, einem sogenannten &amp;lt;i&amp;gt;Label&amp;lt;/i&amp;gt;, durchzuzählen. Dann kann man zusätzliche Informationen zu jeder Komponente (beispielsweise deren Größe) einfach in einem Array speichern, das über die Labels indexiert wird. Die folgende Version der Tiefensuche bestimmt sowohl die Anker als auch die Labels für jeden Knoten:&lt;br /&gt;
&lt;br /&gt;
 def connectedComponents(graph):&lt;br /&gt;
        anchors = [None] * len(graph)             # property map für Anker jedes Knotens&lt;br /&gt;
        labels  = [None] * len(graph)             # property map für Label jedes Knotens&lt;br /&gt;
        &lt;br /&gt;
        def visit(node, anchor):&lt;br /&gt;
                &amp;quot;&amp;quot;&amp;quot;anchor ist der Anker der aktuellen ZK&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
                if anchors[node] is None:         # wenn node noch nicht besucht wurde:&lt;br /&gt;
                    anchors[node] = anchor        # setze seinen Anker&lt;br /&gt;
                    labels[node] = labels[anchor] # und sein Label&lt;br /&gt;
                    for neighbor in graph[node]:  # und besuche die Nachbarn&lt;br /&gt;
                        visit(neighbor, anchor)&lt;br /&gt;
        &lt;br /&gt;
        current_label = 0                         # Zählung der ZK beginnt bei 0&lt;br /&gt;
        for node in xrange(len(graph)):&lt;br /&gt;
            if anchors[node] is None:             # Anker noch nicht bekannt =&amp;gt; neue ZK gefunden&lt;br /&gt;
                labels[node] = current_label      # Label des Ankers setzen&lt;br /&gt;
                visit(node, node)                 # Knoten der neuen ZK rekursiv suchen&lt;br /&gt;
                current_label += 1                # Label für die nächste ZK hochzählen&lt;br /&gt;
        return anchors, labels&lt;br /&gt;
Interessant ist hier die Schleife über alle Knoten des Graphen am Ende des Algorithmus, die bei den bisherigen Versionen der Tiefensuche nicht vorhanden war. Um ihre Funktionsweise zu verstehen, nehmen wir für den Moment an, dass der Graph zusammenhängend ist. Dann findet diese Schleife den ersten Knoten des Graphen und führt die Tiefensuche mit diesem Knoten als Startknoten aus. Sobald die Rekursion zurückkehrt, sind alle Knoten des Graphen besucht (weil der Graph ja zusammenhängend war), so dass die Schleife alle weiteren Knoten überspringt (die if-Anweisung liefert für keinen weiteren Knoten True). Bei unzusammenhängenden Graphen dagegen erreicht die Tiefensuche nur die Knoten derselben Komponente, die im weiteren Verlauf der Schleife übersprungen werden. Findet die if-Anweisung jetzt einen noch nicht besuchten Knoten, muss dieser folglich in einer neuen Komponente liegen. Wir verwenden diesen Knoten als Anker und bestimmen die übrigen Knoten dieser Komponente wiederum mit Tiefensuche.&lt;br /&gt;
&lt;br /&gt;
* Beispiel: ... &amp;lt;b&amp;gt; under construction &amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Man erkennt, dass die Tiefensuche nach dem &amp;lt;i&amp;gt;Anlagerungsprinzip&amp;lt;/i&amp;gt; vorgeht: Beginnend vom einem Startknoten (dem Anker) werden die Knoten der aktuellen Komponente nach und nach an den Tiefensuchbaum angehangen. Erst, wenn nichts mehr angelagert werden kann, geht der Algorithmus zur nächsten Komponente über.&lt;br /&gt;
&lt;br /&gt;
==== Lösung mittels Union-Find-Algorithmus ====&lt;br /&gt;
&lt;br /&gt;
Im Gegensatz zum Anlagerungsprinzip sucht der Union-Find-Algorithmus die Zusammenhangskomponenten mit dem &amp;lt;i&amp;gt;Verschmelzungsprinzip&amp;lt;/i&amp;gt;: Eingangs wird jeder Knoten als ein Teilgraph für sich betrachtet. Dann iteriert man über alle Kanten und verbindet deren Endknoten jeweils zu einem gemeinsamen Teilgraphen (falls die beiden Enden einer Kante bereits im selben Teilgraphen liegen, wird diese Kante ignoriert). Solange noch Kanten vorhanden sind, werden dadurch immer wieder Teilgraphen in größere Teilgraphen verschmolzen. Am Ende bleiben die maximalen zusammenhängenden Teilgraphen (also gerade die Zusammenhangskomponenten) übrig. Dieser Algorithmus kommt ohne Tiefensuche aus und ist daher in der Praxis oft schneller, allerdings auch etwas komplizierter zu implementieren.&lt;br /&gt;
&lt;br /&gt;
Der Schlüssel des Algorithmus ist eine Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt;, die zu jedem Knoten den aktuellen Anker sucht. Der Anker existiert immer, da jeder Knoten von Anfang an zu einem Teilgraphen gehört (anfangs ist jeder Teilgraph trivial und besteht nur aus dem Knoten selbst). Die Verschmelzung wird realisiert, indem der Anker des einen Teilgraphen seine Rolle verliert und stattdessen der Anker des anderen Teilgraphen eingesetzt wird. &lt;br /&gt;
&lt;br /&gt;
Zur Verwaltung der Anker verwenden wir wieder eine property map &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt; mit der Konvention, dass die Anker auf sich selbst verweisen. Es wäre jedoch zu teuer, wenn man bei jeder Verschmelzung alle Anker-Einträge der beteiligten Knoten aktualisieren müsste, da jeder Knoten im Laufe des Algorithmus mehrmals seinen Anker wechseln kann. Statt dessen definiert man Anker rekursiv: Verweist ein Knoten auf einen Anker, der mittlerweile diese Rolle verloren hat, folgt man dem Verweis von diesem Knoten (dem ehemaligen Anker) weiter, bis man einen tatsächlichen Anker gefunden hat - erkennbar daran, dass er auf sich selbst verweist. Diese Suchfunktion kann folgendermassen implementiert werden:&lt;br /&gt;
&lt;br /&gt;
  def findAnchor(anchors, node):&lt;br /&gt;
      while node != anchors[node]:   # wenn node kein Anker ist&lt;br /&gt;
          node = anchors[node]       # ... verfolge die Ankerkette weiter&lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Allerdings kann diese Kette im Laufe vieler Verschmelzungen sehr lang werden, so dass das Verfolgen der Kette teuer wird. Man vermeidet dies durch die sogenannte &amp;lt;i&amp;gt;Pfadkompression&amp;lt;/i&amp;gt;: Immer, wenn man den Anker gefunden hat, aktualisiert man den Eintrag am Anfang der Kette. Die Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt; wird dadurch nur wenig komplizierter:&lt;br /&gt;
&lt;br /&gt;
  def findAnchor(anchors, node):&lt;br /&gt;
      start = node                   # wir merken uns den Anfang der Kette&lt;br /&gt;
      while node != anchors[node]:   # wenn node kein Anker ist&lt;br /&gt;
          node = anchors[node]       # ... verfolge die Ankerkette weiter&lt;br /&gt;
      anchors[start] = node          # Pfadkompression: aktualisiere den Eintrag am Anfang der Kette&lt;br /&gt;
      return node&lt;br /&gt;
&lt;br /&gt;
Man kann zeigen, dass die Ankersuche mit Pfadkompression zu einer fast konstanten amortisierten Laufzeit pro Aufruf führt.&lt;br /&gt;
&lt;br /&gt;
Um mit jeder Kante des (ungerichteten) Graphen nur maximal einmal eine Verschmelzung durchzuführen, betrachten wir jede Kante nur in der Richtung von der kleineren zur größeren Knotennummer, die umgekehrte Richtung wird ignoriert. Außerdem ist es zweckmäßig, bei jeder Verschmelzung denjenigen Anker mit der kleineren Knotennummer als neuen Anker zu übernehmen. Dann gilt für jede Zusammenhangskomponente, dass gerade der Knoten mit der kleinsten Knotennummer der Anker ist (genau wie bei der Lösung mittels Tiefensuche), was die weitere Analyse vereinfacht, z.B. die Zuordnung der Labels zu den Komponenten am Ende des Algorithmus. &lt;br /&gt;
&lt;br /&gt;
 def unionFindConnectedComponents(graph):&lt;br /&gt;
     anchors = range(len(graph))        # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):    # iteriere über alle Knoten&lt;br /&gt;
         for neighbor in graph[node]:   # ... und über deren ausgehende Kanten&lt;br /&gt;
             if neighbor &amp;lt; node:        # ignoriere Kanten, die in falscher Richtung verlaufen&lt;br /&gt;
                 continue&lt;br /&gt;
             # hier landen wir für jede Kante des Graphen genau einmal&lt;br /&gt;
             a1 = findAnchor(anchors, node)       # finde Anker ...&lt;br /&gt;
             a2 = findAnchor(anchors, neighbor)   # ... der beiden Endknoten&lt;br /&gt;
             if a1 &amp;lt; a2:                          # Verschmelze die beiden Teilgraphen&lt;br /&gt;
                 anchors[a2] = a1                 # (verwende den kleineren der beiden Anker als Anker des&lt;br /&gt;
             elif a2 &amp;lt; a1:                        #  entstehenden Teilgraphen. Falls node und neighbor &lt;br /&gt;
                 anchors[a1] = a2                 #  den gleichen Anker haben, waren sie bereits im gleichen&lt;br /&gt;
                                                  #  Teilgraphen, und es passiert hier nichts.)&lt;br /&gt;
     # Bestimme jetzt noch die Labels der Komponenten&lt;br /&gt;
     labels = [None]*len(graph)         # Initialisierung der property map für Labels&lt;br /&gt;
     current_label = 0                  # die Zählung beginnt bei 0&lt;br /&gt;
     for node in xrange(len(graph)):&lt;br /&gt;
         a = findAnchor(anchors, node)  # wegen der Pfadkompression zeigt jeder Knoten jetzt direkt auf seinen Anker&lt;br /&gt;
         if a == node:                  # node ist ein Anker&lt;br /&gt;
             labels[a] = current_label  # =&amp;gt; beginne eine neue Komponente&lt;br /&gt;
             current_label += 1         # und zähle Label für die nächste ZK hoch&lt;br /&gt;
         else:&lt;br /&gt;
             labels[node] = labels[a]   # node ist kein Anker =&amp;gt; setzte das Label des Ankers&lt;br /&gt;
                                        # (wir wissen, dass labels[a] bereits gesetzt ist, weil &lt;br /&gt;
                                        #  der Anker immer der Knoten mit der kleinsten Nummer ist)&lt;br /&gt;
     return anchors, labels&lt;br /&gt;
 &lt;br /&gt;
* Beispiel: ... &amp;lt;b&amp;gt;under construction&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Kürzeste Wege (Pfade) ==&lt;br /&gt;
&lt;br /&gt;
Eine weitere grundlegende Aufgabe in Graphen ist die Bestimmung eines kürzesten Weges zwischen zwei gegebenen Knoten. Dies hat offensichtliche Anwendungen bei Routenplanern und Navigationssystemen und ist darüber hinaus wichtiger Bestandteil anderer Algorithmen, z.B. bei der Berechnung eines maximalen Flusses mit der [http://en.wikipedia.org/wiki/Edmonds%E2%80%93Karp_algorithm Methode von Edmonds und Karp].&lt;br /&gt;
&lt;br /&gt;
=== Kürzeste Wege in ungewichteten Graphen mittels Breitensuche ===&lt;br /&gt;
&lt;br /&gt;
Im Fall eines ungewichteten Graphen ist die Länge eines Weges einfach durch die Anzahl der durchlaufenen Kanten definiert. Daraus folgt, dass kürzeste Pfade mit einer leicht angepassten Version der Breitensuche gefunden werden können: Aufgrund des first in-first out-Verhaltens der Queue betrachtet die Breitensuche alle (erreichbaren) Knoten in der Reihenfolge ihres Abstandes vom Startknoten. Wenn wir den Zielknoten zum ersten Mal erreichen, und der gerade gefundene Weg vom Start zum Ziel hat die Länge L, muss dies der kürzeste Weg sein: Alle möglichen Wege der Länge L' &amp;amp;lt; L hat die Breitensuche ja bereits betrachtet, ohne dass dabei der Zielknoten erreicht wurde. Daraus folgt übrigens eine allgemeine Eigenschaft aller Algorithmen für kürzeste Wege: Wenn der kürzeste Weg vom Start zum Ziel die Länge L hat, finden diese Algorithmen als Nebenprodukt auch die kürzesten Wege zu allen Knoten, für die L' &amp;amp;lt; L gilt. &lt;br /&gt;
&lt;br /&gt;
Um den Algorithmus zu implementieren, passen wir die Breitensuche so an, dass anstelle der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; eine property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; verwendet wird, die für jeden besuchten Knoten den Vaterknoten im Breitensuchbaum speichert. Durch Rückverfolgen der &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette können wir den Pfad vom Ziel zum Start rekonstruieren, und durch Umdrehen der Reihenfolge erhalten wir den gesuchten Pfad vom Start zum Ziel. Sobald der Zielknoten erreicht wurde, können wir die Breitensuche abbrechen (&amp;lt;tt&amp;gt;break&amp;lt;/tt&amp;gt;-Befehl in der ersten &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Schleife). Falls der gegebene Graph unzusammenhängend ist, kann es passieren, dass gar kein Weg gefunden wird, weil Start und Ziel in verschiedenen Zusammenhangskomponenten liegen. Dies erkennen wir daran, dass die Breitensuche beendet wurde, ohne den Zielknoten zu besuchen. Dann gibt die Funktion statt eines Pfades dern Wert &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; zurück:&lt;br /&gt;
&lt;br /&gt;
  from collections import deque&lt;br /&gt;
  &lt;br /&gt;
  def shortestPath(graph, startnode, destination):&lt;br /&gt;
      parents = [None]*len(graph)      # Registriere für jeden Knoten den Vaterknoten im Breitensuchbaum&lt;br /&gt;
      parents[startnode] = startnode   # startnode ist die Wurzel des Baums =&amp;gt; verweist auf sich selbst&lt;br /&gt;
      &lt;br /&gt;
      q = deque()                      # Queue für die zu besuchenden Knoten&lt;br /&gt;
      q.append(startnode)              # Startknoten in die Queue einfügen&lt;br /&gt;
      &lt;br /&gt;
      while len(q) &amp;gt; 0:                # Solange es noch unbesuchte Knoten gibt&lt;br /&gt;
          node = q.popleft()           # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
          if node == destination:      # Zielknoten erreicht&lt;br /&gt;
              break                    #   =&amp;gt; Suche beenden&lt;br /&gt;
          for neighbor in graph[node]: # Besuche die Nachbarn von node&lt;br /&gt;
              if parents[neighbor] is None:  # aber nur, wenn sie noch nicht besucht wurden&lt;br /&gt;
                  parents[neighbor] = node   # setze node als Vaterknoten&lt;br /&gt;
                  q.append(neighbor)         # und füge neighbor in die Queue ein&lt;br /&gt;
      &lt;br /&gt;
      if parents[destination] is None: # Breitensuche wurde beendet ohne den Zielknoten zu besuchen&lt;br /&gt;
          return None                  # =&amp;gt; kein Pfad gefunden (unzusammenhängender Graph)&lt;br /&gt;
      &lt;br /&gt;
      # Pfad durch die parents-Kette zurückverfolgen und speichern&lt;br /&gt;
      path = [destination]&lt;br /&gt;
      while path[-1] != startnode:&lt;br /&gt;
          path.append(parents[path[-1]])&lt;br /&gt;
      path.reverse()     # Reihenfolge umdrehen (Ziel =&amp;gt; Start wird zu Start =&amp;gt; Ziel)&lt;br /&gt;
      return path        # gefundenen Pfad zurückgeben&lt;br /&gt;
&lt;br /&gt;
=== Gewichtete Graphen ===&lt;br /&gt;
&lt;br /&gt;
Das Problem der Suche nach kürzesten Wegen wird wesentlich interessanter und realistischer, wenn wir zu gewichteten Graphen übergehen:&lt;br /&gt;
&lt;br /&gt;
; Definition - kantengewichteter Graph&lt;br /&gt;
: Jeder Kante (s,t) des Graphen ist eine reelle oder natürliche Zahl w&amp;lt;sub&amp;gt;st&amp;lt;/sub&amp;gt; zugeordnet, die üblicherweise als ''Kantengewicht'' bezeichnet wird.&lt;br /&gt;
&lt;br /&gt;
; Definition - knotengewichteter Graph&lt;br /&gt;
: Jedem Knoten v des Graphen ist eine reelle oder natürliche Zahl w&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; zugeordnet, die üblicherweise als ''Knotengewicht'' bezeichnet wird.&lt;br /&gt;
&lt;br /&gt;
Je nach Anwendung benötigt man Knoten- oder Kantengewichte oder auch beides zugleich. Wir beschränken uns in der Vorlesung auf kantengewichtete Graphen. Beispiele für die Informationen, die man durch Kantengewichte ausdrücken kann, sind&lt;br /&gt;
* wenn die Knoten Orte sind: Abstand von Anfangs- und Endknoten jeder Kante (z.B. Luftline oder Straßenentfernung), Fahrzeit zwischen den Orten&lt;br /&gt;
* wenn der Knoten ein Rohrnetzwerk beschreibt: Durchflusskapazität der einzelnen Rohre (für max-Flussprobleme), analog bei elektrischen Netzwerken: elektrischer Widerstand&lt;br /&gt;
* wenn die Knoten Währungen repräsentieren, können deren Wechselkurse durch Kantengewichte angegeben werden.&lt;br /&gt;
Bei einigen Beispielen ergeben sich unterschiedliche Kantengewichte, wenn eine Kante von s nach t anstatt von t nach s durchlaufen wird. Beispielsweise können sich die Fahrzeiten erheblich unterscheiden, wenn es in einer Richtung bergauf, in der anderen bergab geht, obwohl die Entfernung in beiden Fällen gleich ist. Hier ergibt sich natürlicherweise ein gerichteter Graph. In anderen Beispielen (z.B. bei Luftlinienentfernungen, in guter Näherung auch bei Straßenentfernungen) sind die Gewichte von der Richtung unabhängig, so dass wir ungerichtete Graphen verwenden können.&lt;br /&gt;
&lt;br /&gt;
Die Repräsentation der Kantengewichte im Programm richtet sich nach der Repräsentation des Graphen selbst. Am einfachsten ist wiederum die Adjazenzmatrix, die aber nur für dichte Graphen (&amp;lt;math&amp;gt;E = O(V^2)&amp;lt;/math&amp;gt;, mit E als Anzahl der Kanten und V als Anzahl der Knoten) effizient ist. Bei gewichteten Graphen gibt das Matrixelement a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; das Gewicht der Kante i &amp;amp;rArr; j (wobei a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; = 0 gesetzt wird, wenn diese Kante nicht existiert). Wie zuvor gilt für ungerichtete Graphen a&amp;lt;sub&amp;gt;ij&amp;lt;/sub&amp;gt; = a&amp;lt;sub&amp;gt;ji&amp;lt;/sub&amp;gt; (symmetrische Matrix), während dies für gerichtete Graphen nicht gelten muss.&lt;br /&gt;
&lt;br /&gt;
Bei Graphen in Adjazenzlistendarstellung hat es sich bewährt, die Gewichte in einer &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; zu speichern. Weiter oben haben wir bereits property maps für Knoteneigenschaften (z.B. &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;anchors&amp;lt;/tt&amp;gt;) gesehen. Property maps für Kanten funktionieren ganz analog, allerdings muss man jetzt Paare von Knoten (nämlich Anfangs- und Endknoten der Kante) als Schlüssel verwenden und die Daten entsprechend in einem assoziativen Array ablegen:&lt;br /&gt;
  w = weights[(i,j)]   # Zugriff auf das Gewicht der Kante i &amp;amp;rArr; j&lt;br /&gt;
Alternativ könnte man auch die Graph-Datenstruktur selbst erweitern, aber dies ist weniger zu empfehlen, weil jeder Algorithmus andere Erwiterungen benötigt und damit die Datenstruktur sehr unübersichtlich würde.&lt;br /&gt;
&lt;br /&gt;
Der kürzeste Weg ist nun definiert als der Weg, bei dem die Summe der Kantengewichte minimal ist:&lt;br /&gt;
;Definition - Problem des kürzesten Weges&lt;br /&gt;
: Sei P die Menge aller Wege von u nach v, und &amp;lt;math&amp;gt;p \in P&amp;lt;/math&amp;gt; einer dieser Wege. Wenn der Grpah einfach ist (es also keine Mehrfachkanten zwischen denselben Knoten und keine Schleifen gibt), ist der Weg p durch die Folge der besuchten Knoten eindeutig bestimmt:&lt;br /&gt;
: &amp;lt;math&amp;gt;p : \ \ u = x_0 \rightarrow x_1 \rightarrow x_2 \rightarrow ... \rightarrow v = x_{n_p}&amp;lt;/math&amp;gt;&lt;br /&gt;
:wo &amp;lt;math&amp;gt;n_p&amp;lt;/math&amp;gt; die Anzahl der Kanten im Weg p ist. Seine Kosten W&amp;lt;sub&amp;gt;p&amp;lt;/sub&amp;gt; ergeben sich als Summer der Gewichte der einzelnen Kanten&lt;br /&gt;
: &amp;lt;math&amp;gt;W_p = \sum_{k=1}^{n_p} w_{x_{k-1}x_k}&amp;lt;/math&amp;gt;&lt;br /&gt;
: und ein kürzester Weg &amp;lt;math&amp;gt;p^* \in P&amp;lt;/math&amp;gt; ist ein Weg mit minimalen Kosten&lt;br /&gt;
: &amp;lt;math&amp;gt;p^* = \textrm{argmin}_{p\in P}\ \ W_p&amp;lt;/math&amp;gt;&lt;br /&gt;
: Das Problem des kürzesten Weges besteht darin, einen optimalen Weg &amp;lt;i&amp;gt;p*&amp;lt;/i&amp;gt; zwischen gegebenen Knoten u und v zu finden.&lt;br /&gt;
Die Lösung dieses Problems hängt davon ab, ob alle Kantengewichte positiv sind, oder ob es auch negative Kantengewichte gibt. In letzeren Fall ist es möglich, durch eine Verlängerung des Weges die Kosten zu redizieren, während sich im ersteren Fall die Kosten immer erhöhen, wenn man den Weg verlängert. &lt;br /&gt;
&lt;br /&gt;
Negative Gewichte treten z.B. bei den Währungsgraphen auf. Auf den ersten Blick entsprechen diese Graphen nicht den Anforderungen an das Problem des kürzesten Weges, weil Wechselkurse miteinander (und mit Geldbeträgen) multipliziert anstatt addiert werden. Man beseitigt diese Schwierigkeit aber leicht, indem man die &amp;lt;i&amp;gt;Logarithmen&amp;lt;/i&amp;gt; der Wechselkurse als Kantengewichte verwendet, wodurch sich die Multiplikation in eine Addition der Logarithmen verwandelt. Wechselkurse &amp;amp;lt; 1 führen nun zu negativen Gewichten. &lt;br /&gt;
&lt;br /&gt;
Interessant werden negative Gewichte vor allem in Graphen mit Zyklen. Dann kann es nämlich passieren, dass die Gesamtkosten eines Zyklus ebenfalls negativ sind. Jeder Weg, der den Zyklus enthält, hat dann Kosten von &amp;lt;math&amp;gt;-\infty&amp;lt;/math&amp;gt;, weil man den Zyklus beliebig oft durchlaufen und dadurch die Gesamtkosten immer weiter verkleinern kann:&lt;br /&gt;
&lt;br /&gt;
     /\		1. Durchlauf: Kosten -1&lt;br /&gt;
  1 /  \ -4	2. Durchlauf: Kosten -2&lt;br /&gt;
   /____\	etc.&lt;br /&gt;
      2&lt;br /&gt;
&lt;br /&gt;
Um hier nicht in einer Endlosschleife zu landen, benötigt man spezielle Algorithmen, die mit dieser Situation umgehen können. Der [http://de.wikipedia.org/wiki/Bellman-Ford-Algorithmus Algorithmus von Bellmann und Ford] beispielsweise bricht die Suche nach dem kürzesten Weg ab, sobald er einen negativen Zyklus entdeckt, aber andernfalls kann er negative Gewichte problemlos verarbeiten. &lt;br /&gt;
&lt;br /&gt;
Die Detektion negativer Zyklen hat wiederum eine interessante Anwendung bei Währungsgraphen: Ein Zyklus bedeutet hier, dass man Geld über mehrere Stufen von einer Währung in die nächste und am Schluß wieder in die Originalwährung umtauscht, und ein negativer Zyklus führt dazu, dass man am Ende &amp;lt;i&amp;gt;mehr&amp;lt;/i&amp;gt; Geld besitzt als am Anfang (damit negative Zyklen wirklich einen Gewinn bedeuten und keinen Verlust, müssen die Wechselkurse vor der Logarithmierung in [http://de.wikipedia.org/wiki/Wechselkurs#Nominaler_Wechselkurs Preisnotierung] angegeben sein). Bei Privatpersonen ist dies ausgeschlossen, weil die Umtauschgebühren den möglichen Gewinn mehr als aufzehren. Banken mit direktem weltweitem Börsenzugang hingegen unternehmen große Anstrengungen, um solche negativen Zyklen möglichst schnell (nämlich vor der Konkurrenz) zu entdecken und auszunutzen. Diese Geschäftsmethode bezeichnet man als [http://de.wikipedia.org/wiki/Arbitrage Arbitrage] und die Existenz eines negativen Zyklus als Arbitragegelegenheit. Durch die Kursschwankungen (und durch die ausgleichende Wirkung der Arbitragegeschäfte selbst) existieren die Arbitragegelegenheiten nur für kurze Zeit, und ihre Detektion erfordert leistungsfähige Echtzeitalgorithmen.&lt;br /&gt;
&lt;br /&gt;
In dieser Vorlesung beschränken wir uns hingegen auf Graphen mit ausschließlich positiven Gewichten. In diesem Fall ist der Algorithmus von Dijkstra die Methode der Wahl, weil er wesentlich schneller arbeitet als der Bellmann-Ford-Algorithmus.&lt;br /&gt;
&lt;br /&gt;
=== Algorithmus von Dijkstra ===&lt;br /&gt;
&lt;br /&gt;
==== Edsger Wybe Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
geb. 11. Mai 1930 in Rotterdam&lt;br /&gt;
&lt;br /&gt;
ges. 06. August 2002&lt;br /&gt;
&lt;br /&gt;
Dijkstra war ein niederländischer Informatiker und Wegbereiter der strukturierten Programmierung. 1972 erhielt er für seine Leistung in der Technik und Kunst der Programmiersprachen den Turing Award, der jährlich von der Association for Computing Machinery (ACM) an Personen verliehen wird, die sich besonders um die Entwicklung der Informatik verdient gemacht haben. Zu seinen Beiträgen zur Informatik gehören unter anderem der Dijkstra-Algorithmus zur Berechnung des kürzesten Weges in einem Graphen sowie eine Abhandlung über den go-to-Befehl und warum er nicht benutzt werden sollte. Der go-to-Befehl war in den 60er und 70er Jahren weit verbreitet, führte aber zu Spaghetti-Code. In seinem berühmten Paper &amp;quot;A Case against the GO TO Statement&amp;quot;[http://www.cs.utexas.edu/users/EWD/ewd02xx/EWD215.PDF], das als Brief mit dem Titel &amp;quot;Go-to statement considered harmful&amp;quot; veröffentlicht wurde, argumentiert Dijkstra, dass es umso schwieriger ist, dem Quellcode eines Programmes zu folgen, je mehr go-to-Befehle darin enthalten sind und zeigt, dass man auch ohne diesen Befehl gute Programme schreiben kann.&lt;br /&gt;
&lt;br /&gt;
==== Algorithmus ====&lt;br /&gt;
&lt;br /&gt;
Der Dijkstra-Algorithmus für kürzeste Wege ist dem oben vorgestellten Algorithmus &amp;lt;tt&amp;gt;shortestPath()&amp;lt;/tt&amp;gt; auf der Basis von Breitensuche sehr ähnlich. Insbesondere gilt auch hier, dass neben dem kürzesten Weg vom Start zum Ziel auch alle kürzesten Wege gefunden werden, deren Endknoten dem Start näher sind als der Zielknoten. Aufgrund der Kantengewichte gibt es aber einen wichtigen Unterschied: Der erste gefundene Weg zu einem Knoten ist nicht mehr notwendigerweise der kürzeste. Wir bestimmen deshalb für jeden Knoten mehrere Kandidatenwege und verwenden eine Prioritätswarteschlange (statt einer einfachen First in - First out - Queue), um diese Wege nach ihrer Länge zu sortieren. Die Kandidatenwege für einen gegebenen Knoten werden unterschieden, indem wir auch den Vorgängerknoten im jeweiligen Weg speichern. Wenn ein Knoten &amp;lt;i&amp;gt;erstmals&amp;lt;/i&amp;gt; an die Spitze der Prioritätswarteschlange gelangt, haben wir den kürzesten Weg zu diesem Knoten gefunden (das wird weiter unten formal bewiesen), und der Vorgänger des Knotens in diesem Weg wird zu seinem Vaterknoten. Erscheint derselbe Knoten später nochmals an der Spitze der Prioritätswarteschlange, handelt es sich um einen Kandidatenweg, der sich nicht als kürzester erwiesen hat und deshalb ignoriert werden kann. Wir erkennen dies leicht daran, dass der Vaterknoten in der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; bereits gesetzt ist. &lt;br /&gt;
&lt;br /&gt;
Eine geeignete Datenstruktur für die Prioritätswarteschlange wird durch das Python-Modul [http://docs.python.org/library/heapq.html heapq] realisiert. Es verwendet ein normales Pythonarray als unterliegende Repräsentation für einen Heap und stellt effiziente &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt;-Funktionen zur Verfügung. Dies entspricht genau unserer Vorgehensweise im Kapitel [[Prioritätswarteschlangen]]. Als Datenelement erwartet die Funktion &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; ein Tupel, dessen erstes Element die Priorität sein muss. Die übrigen Elemente des Tupels (und damit auch deren Anzahl) können je nach Anwendung frei festgelegt werden. Wir legen fest, dass das zweite Element den Endknoten des betrachteten Weges und das dritte den Vorgängerknoten speichert. &lt;br /&gt;
&lt;br /&gt;
Die Kantengewichte werden dem Algorithmus in der property map &amp;lt;tt&amp;gt;weights&amp;lt;/tt&amp;gt; übergeben:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;code python&amp;gt;&lt;br /&gt;
    import heapq	                  # heapq implementiert die Funktionen für Heaps&lt;br /&gt;
    &lt;br /&gt;
    def dijkstra(graph, weights, startnode, destination):&lt;br /&gt;
        parents = [None]*len(graph)       # registriere für jeden Knoten den Vaterknoten im Pfadbaum&lt;br /&gt;
      &lt;br /&gt;
        q = []                            # Array q wird als Heap verwendet&lt;br /&gt;
        &amp;lt;font color=red&amp;gt;heapq.heappush(q, (0.0, startnode, startnode))&amp;lt;/font&amp;gt;  # Startknoten in Heap einfügen&lt;br /&gt;
      &lt;br /&gt;
        while len(q) &amp;gt; 0:                 # solange es noch Knoten im Heap gibt:&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;length, node, predecessor = heapq.heappop(q)&amp;lt;/font&amp;gt;   # Knoten aus dem Heap nehmen&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;if parents[node] is not None:&amp;lt;/font&amp;gt; # parent ist schon gesetzt =&amp;gt; es gab einen anderen, kürzeren Weg&lt;br /&gt;
                &amp;lt;font color=red&amp;gt;continue&amp;lt;/font&amp;gt;                  #   =&amp;gt; wir können diesen Weg ignorieren&lt;br /&gt;
            &amp;lt;font color=red&amp;gt;parents[node] = predecessor&amp;lt;/font&amp;gt;   # parent setzen&lt;br /&gt;
            if node == destination:       # Zielknoten erreicht&lt;br /&gt;
                break                     #   =&amp;gt; Suche beenden&lt;br /&gt;
            for neighbor in graph[node]:  # die Nachbarn von node besuchen,&lt;br /&gt;
                if parents[neighbor] is None:   # aber nur, wenn ihr kürzester Weg noch nicht bekannt ist&lt;br /&gt;
                    &amp;lt;font color=red&amp;gt;newLength = length + weights[(node,neighbor)]&amp;lt;/font&amp;gt;   # berechne Pfadlänge zu neighbor              &lt;br /&gt;
                    &amp;lt;font color=red&amp;gt;heapq.heappush(q, (newLength, neighbor, node))&amp;lt;/font&amp;gt;  # und füge neighbor in den Heap ein&lt;br /&gt;
      &lt;br /&gt;
        if parents[destination] is None:  # Suche wurde beendet ohne den Zielknoten zu besuchen&lt;br /&gt;
            return None, None             # =&amp;gt; kein Pfad gefunden (unzusammenhängender Graph)&lt;br /&gt;
      &lt;br /&gt;
        # Pfad durch die parents-Kette zurückverfolgen und speichern&lt;br /&gt;
        path = [destination]&lt;br /&gt;
        while path[-1] != startnode:&lt;br /&gt;
            path.append(parents[path[-1]])&lt;br /&gt;
        path.reverse()                    # Reihenfolge umdrehen (Ziel =&amp;gt; Start wird zu Start =&amp;gt; Ziel)&lt;br /&gt;
        return path, length               # gefundenen Pfad und dessen Länge zurückgeben&lt;br /&gt;
  &amp;lt;/code&amp;gt;&lt;br /&gt;
Die wesentlichen Unterschiede zur Breitensuche sind im Code rot markiert: Anstelle der Queue verwenden wir jetzt einen Heap, und der Startknoten wird mit Pfadlänge 0 als erstes eingefügt. In der Schleife &amp;lt;tt&amp;gt;while len(q) &amp;gt; 0:&amp;lt;/tt&amp;gt; wird jeweils der Knoten &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; mit der aktuell kürzesten Pfadlänge aus dem Heap entfernt. Die Pfadlänge vom Start zu diesem Knoten wird in der Variable &amp;lt;tt&amp;gt;length&amp;lt;/tt&amp;gt; gespeichert, sein Vorgänger in der Variable &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;. Wenn der aktuelle Weg nicht der kürzeste ist (&amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; war bereits gesetzt), wird dieser Weg ignoriert. Andernfalls werden die property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; aktualisiert und die Nachbarn von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; besucht. Beim Scannen der Nachbarn berechnen wir zunächst die Länge &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; das Weges &amp;lt;tt&amp;gt;startnode =&amp;amp;gt; node =&amp;amp;gt; neighbor&amp;lt;/tt&amp;gt; als Summe von &amp;lt;tt&amp;gt;length&amp;lt;/tt&amp;gt; und dem Gewicht der Kante &amp;lt;tt&amp;gt;(node, neighbode)&amp;lt;/tt&amp;gt;. Diese Länge wird beim Einfügen des Nachbarknotens in den Heap zur Priorität des aktuellen Weges.&lt;br /&gt;
&lt;br /&gt;
Die wichtigsten Prinzipien des Dijkstra-Algorithmus noch einmal im Überblick:&lt;br /&gt;
* Der Dijkstra-Algorithmus ist Breitensuche mit Prioritätswarteschlange (Heap) statt einer einfache Warteschlange (Queue).&lt;br /&gt;
* Die Prioritätswarteschlange speichert alle Wege, die bereits gefunden worden sind und ordnet sie aufsteigend nach ihrer Länge. &lt;br /&gt;
* Das Sortieren (und damit der ganze Algorithmus) funktioniert nur mit positiven Kantengewichten korrekt.&lt;br /&gt;
* Da ein Knoten auf mehreren Wegen erreichbar sein kann, kann er auch mehrmals im Heap sein. &lt;br /&gt;
* Wenn ein Knoten &amp;lt;i&amp;gt;erstmals&amp;lt;/i&amp;gt; aus der Prioritätswarteschlange entnommen wird, ist der gefundene Weg der kürzeste zu diesem Knoten. Andernfalls wird der Weg ignoriert.&lt;br /&gt;
* Wenn der Knoten &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; aus dem Heap entnommen wird, ist der kürzeste Weg von Start nach Ziel gefunden, und die Suche kann beendet werden.&lt;br /&gt;
In unserer Implementation können, wie gesagt, mehrere Wege zum selben Knoten gleichzeitig in der Prioritätswarteschlange sein. Im Prinzip wäre es auch möglich, immer nur den besten zur Zeit bekannten Weg zu jedem Enknoten in der Prioritätswarteschlange zu halten - sobald ein besserer Kandidat gefunden wird, ersetzt er den bisherigen Kandidaten, anstatt zusätzlich eingefügt zu werden. Dies erfordert aber eine wesentlich kompliziertere Prioritätswarteschlange, die eine effiziente &amp;lt;tt&amp;gt;updatePriority&amp;lt;/tt&amp;gt;-Funktion anbietet, ohne dass dadurch eine signifikante Beschleunigung erreicht wird. Deshalb verfolgen wir diesen Ansatz nicht.&lt;br /&gt;
&lt;br /&gt;
==== Beispiel ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;under construction&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Image:Bsp.jpg]]&lt;br /&gt;
&lt;br /&gt;
==== Komplexität von Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
Zur Analyse der Komplexität nehmen wir an, dass der Graph V Knoten und E Kanten hat. Die Initialisierung der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; am Anfang der Funktion hat offensichtlich Komplexität O(V), weil Speicher für V Knoten allokiert wird. Der Code am Ende der Funktion, der aus der property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; den Pfad extrahiert, hat ebenfalls die Komplexität O(V), weil der Pfad im ungünstigen Fall sämtliche Knoten des Graphen umfasst. Beides wird durch die Komplexität der Hauptschleife dominiert, zu deren Analyse wir den folgenden Codeausschnitt genauer anschauen wollen:&lt;br /&gt;
&lt;br /&gt;
      while len(q) &amp;gt; 0:&lt;br /&gt;
           ... # 1&lt;br /&gt;
           if parents[node] is not None: &lt;br /&gt;
               continue                  &lt;br /&gt;
           parents[node] = predecessor&lt;br /&gt;
           ... # 2&lt;br /&gt;
Wir erkennen, dass der Codeabschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; für jeden Knoten höchstens einmal erreicht werden kann: Da &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; beim ersten Durchlauf gesetzt wird, kann die &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage beim gleichen Knoten nie wieder &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt; liefern, und das nachfolgende &amp;lt;tt&amp;gt;continue&amp;lt;/tt&amp;gt; bewirkt, dass der Abschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; dann übersprungen wird. Man sagt auch, dass jeder Knoten &amp;lt;i&amp;gt;höchstens einmal expandiert&amp;lt;/i&amp;gt; wird, auch wenn er mehrmals im Heap war. &lt;br /&gt;
&lt;br /&gt;
Der Codeabschnitt &amp;lt;tt&amp;gt;# 2&amp;lt;/tt&amp;gt; selbst enthält eine Schleife über alle ausgehenden Kanten des Knotens &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt;. Im ungünstigsten Fall iterieren wir bei &amp;lt;i&amp;gt;allen&amp;lt;/i&amp;gt; Knoten über &amp;lt;i&amp;gt;alle&amp;lt;/i&amp;gt; ausgehenden Kanten, aber das sind gerade alle Kanten des Graphen je einmal in den beiden möglichen Richtungen. Die Funktion &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; wird sogar höchstens E Mal aufgerufen, weil eine Kante nur in den Heap eingefügt wird, wenn der kürzeste Weg der jeweiligen Endknotens noch nicht bekannt ist (siehe die &amp;lt;tt&amp;gt;if&amp;lt;/tt&amp;gt;-Abfrage in der &amp;lt;tt&amp;gt;for&amp;lt;/tt&amp;gt;-Schleife), und das ist nur ein einer Richtung möglich. Dies hat zwei Konsequenzen:&lt;br /&gt;
* Die Schleife &amp;lt;tt&amp;gt;while len(q) &amp;gt; 0:&amp;lt;/tt&amp;gt; wird nur so oft ausgeführt, wie Elemente im Heap sind, also höchstens E Mal. Das gleiche gilt für den Codeabschnitt &amp;lt;tt&amp;gt;# 1&amp;lt;/tt&amp;gt;, der das &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; enthält.&lt;br /&gt;
* Die Operationen &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; haben logarithmische Komplexität in der Größe des Heaps, sind also in &amp;lt;math&amp;gt;O(\log\,E)&amp;lt;/math&amp;gt;. In einfachen Graphen gilt aber &amp;lt;math&amp;gt;E = O(V^2)&amp;lt;/math&amp;gt;, so dass sich die Komplexität der Heapoperationen vereinfacht zu &amp;lt;math&amp;gt;O(\log\,E)=O(\log\,V^2)=O(2\log\,V)=O(\log\,V)&amp;lt;/math&amp;gt;.&lt;br /&gt;
Zusammenfassend gilt: &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; werden maximal E Mal aufgerufen und haben eine Komplexität in &amp;lt;math&amp;gt;O(\log\,V)&amp;lt;/math&amp;gt;. Folglich hat der Algorithmus von Dijkstra die Komplexität:&lt;br /&gt;
:&amp;lt;math&amp;gt;O(E\,\log\,V)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Vergleich mit Breitensuche und Tiefensuche ====&lt;br /&gt;
&lt;br /&gt;
Der Dijkstra-Algorithmus ist eng mit der Breiten- und Tiefensuche verwandt - man kann diese Algorithmen aus dem Dijkstra-Algorithmus gewinnen, indem man einfach die Regel zur Festlegung der Prioritäten ändert. Anstelle der Länge des Pfades verwenden wir als Priorität den Wert eine Zählvariable &amp;lt;tt&amp;gt;count&amp;lt;/tt&amp;gt;, die nach jeder Einfügung in den Heap (also nach jedem Aufruf von &amp;lt;tt&amp;gt;heappush&amp;lt;/tt&amp;gt;) aktualisiert wird. Zählen wir die Variable hoch, haben die zuerst eingefügten Kanten die höchste Priorität, der Heap verhält sich also wie eine Queue (First in-First out), und wir erhalten eine Breitensuche. Zählen wir die Variable hingegen (von E beginnend) herunter, haben die zuletzt eingefügten Kanten höchste Priorität. Der Heap verhält sich dann wie ein Stack (Last in-First out), und wir bekommen Tiefensuche. Statt eines Heaps plus Zählvariable kann man jetzt natürlich direkt eine Queue bzw. einen Stack verwenden. Dadurch fällt der Aufwand &amp;lt;math&amp;gt;O(\log\,V)&amp;lt;/math&amp;gt; für die Heapoperationen weg und wird durch die effizienten O(1)-Operationen von Queue bzw. Stack ersetzt. Damit erhalten wir für Breiten- und Tiefensuche die schon bekannte Komplexität O(E).&lt;br /&gt;
&lt;br /&gt;
==== Korrektheit von Dijkstra ====&lt;br /&gt;
&lt;br /&gt;
Wir beweisen mittels vollständiger Induktion die Schleifen-Invariante: Falls &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; gesetzt (also ungleich &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;) ist, dann liefert das Zurückverfolgen des Weges von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; nach &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; den kürzesten Weg. &lt;br /&gt;
;Induktionsanfang: &amp;lt;tt&amp;gt;parents[startnode]&amp;lt;/tt&amp;gt; ist als einziges gesetzt. Zurückverfolgen liefert den trivialen Weg &amp;lt;tt&amp;gt;[startnode]&amp;lt;/tt&amp;gt;, der mit Länge 0 offensichtlich der kürzeste Pfad ist &amp;amp;rarr; die Bedingung ist erfüllt.&lt;br /&gt;
;Induktionsschritt: Wir zeigen mit einem indirektem Beweis, dass wir immer einen kürzesten Weg bekommen, wenn &amp;lt;tt&amp;gt;parents[node]&amp;lt;/tt&amp;gt; gesetzt wird.&lt;br /&gt;
:Sei &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt; = &amp;lt;tt&amp;gt;{v | parents[v] is not None}&amp;lt;/tt&amp;gt; die Menge aller Knoten, von denen wir den kürzesten Weg schon kennen (Induktionsvoraussetzung), und &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; der Knoten, der sich gerade an der Spitze des Heaps befindet. Dann ist &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; der Vorgänger von &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; im aktuellen Weg, und es muss &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt; gelten, weil die Nachbarn von &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; (und damit auch der aktuelle &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt;) erst in den Heap eingefügt werden, wenn der kürzeste Weg für &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; gefunden wurde. Man beachte auch, dass alle Knoten, die noch nicht in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt; enthalten sind, weiter vom Start entfernt sind als alle Knoten in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt;, weil alle neu in den Heap eingefügten Wege länger sind als der kürzeste Weg des jeweiligen Vorgängers. &lt;br /&gt;
:Der indirekte Beweis nimmt jetzt an, dass der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; nicht der kürzeste Weg ist. Dann muss es einen anderen, kürzeren Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; geben. Für den Vorgänger &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; in diesem Weg unterscheiden wir zwei Fälle:&lt;br /&gt;
:* &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt;: In diesem Fall ist die Länge des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; bereits bekannt, und dieser Weg ist in der Prioritätswarteschlange enthalten. Dann kann er aber nicht der kürzeste sein, denn an der Spitze der Warteschlange war nach Voraussetzung der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt;.&lt;br /&gt;
:* &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\notin S&amp;lt;/math&amp;gt;: Die Kosten des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; berechnen sich als &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) + weight[(x, node)]&amp;lt;/tt&amp;gt;, und die Kosten des Weges &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; sind analog &amp;lt;tt&amp;gt;Kosten(predecessor &amp;amp;rarr; startnode) + weight[(predecessor, node)]&amp;lt;/tt&amp;gt;. Aufgrund der Induktionsvoraussetzung gilt aber &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\in S&amp;lt;/math&amp;gt;, und somit &amp;lt;tt&amp;gt;Kosten(predecessor &amp;amp;rarr; startnode) &amp;amp;lt; Kosten(x &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt;, weil &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; andernfalls vor &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; an der Spitze des Heaps gewesen wäre, was mit der Annahme &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;&amp;lt;math&amp;gt;\notin S&amp;lt;/math&amp;gt; unverträglich ist. Damit der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; trotzdem der kürzeste Weg sein kann, müsste &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) &amp;amp;lt; Kosten(node &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt; gelten, denn durch die Kante &amp;lt;tt&amp;gt;(x, node)&amp;lt;/tt&amp;gt; kommen ja noch Kosten hinzu. Das wäre aber nur möglich, wenn der Knoten &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; vor dem Knoten &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; an die Spitze des Heaps gelangt, im Widerspruch zur Annahme, dass &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; sich gerade an der Spitze des Heaps befindet. Somit kann die Behauptung, dass der Weg &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; &amp;amp;rarr; &amp;lt;tt&amp;gt;startnode&amp;lt;/tt&amp;gt; der kürzeste Weg ist, nicht stimmen.&lt;br /&gt;
In beiden Fällen erhalten wir einen Widerspruch, und die Behauptung ist somit bewiesen. Da die Invariante insbesondere für den Weg zum Zielknoten &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; erfüllt ist, folgt daraus auch die Korrektheit des Algorithmus von Dijkstra.&lt;br /&gt;
&lt;br /&gt;
===  A*-Algorithmus - Wie kann man Dijkstra noch verbessern? ===&lt;br /&gt;
&lt;br /&gt;
Eine wichtige Eigenschaft des Dijkstra-Algorithmus ist, dass neben dem kürzesten Weg vom Start zum Ziel auch die kürzesten Wege zu allen Knoten berechnet werden, die näher am Startknoten liegen als das Ziel, obwohl uns diese Wege gar nicht interessieren. Sucht man beispielsweise in einem Graphen mit den Straßenverbindungen in Deutschland den kürzesten Weg von Frankfurt (Main) nach Dresden (ca. 460 km), werden auch die kürzesten Wege von Frankfurt nach Köln (190 km), Dortmund (220 km) und Stuttgart (210 km) und vielen anderen Städten gefunden. Aufgrund der geographischen Lage dieser Städte ist eigentlich von vornherein klar, dass sie mit dem kürzesten Weg nach Dresden nicht das geringste zu tun haben. Anders sieht es mit Erfurt (260 km) oder Suhl (210 km) aus - diese Städte liegen zwischen Frankfurt und Dresden und kommen deshalb als Zwischenstationen des gesuchten Weges in Frage.&lt;br /&gt;
&lt;br /&gt;
Damit Dijkstra korrekt funktioniert, würde es im Prinzip ausreichen, wenn man die kürzesten Wege nur für diejenigen Knoten ausrechnet, die auf dem kürzesten Weg vom Start zum Ziel liegen, denn nur diese Knoten braucht man, um den gesuchten Weg über die &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette zurückzuverfolgen. Das Problem ist nur, dass man diese Knoten erst kennt, wenn der Algorithmus fertig durchgelaufen ist. Schließt man Knoten zu früh von der Betrachtung aus, kommt am Ende möglicherweise nicht der korrekte kürzeste Weg heraus. &lt;br /&gt;
&lt;br /&gt;
Der A*-Algorithmus löst dieses Dilemma mit folgender Idee: Ändere die Prioritäten für den Heap so ab, dass unwichtige Knoten nur mit geringerer Wahscheinlichkeit expandiert werden, aber stelle gleichzeitig sicher, dass alle wichtigen Knoten (also diejenigen auf dem korrekten kürzesten Weg) auf jeden Fall expandiert werden. Es zeigt sich, dass man diese Idee umsetzen kann, wenn eine &amp;lt;i&amp;gt;Schätzung für den Restweg&amp;lt;/i&amp;gt; (also für die noch verbleibende Entfernung von jedem Knoten zum Ziel) verfügbar ist:&lt;br /&gt;
 rest = guess(neighbor, destination)&lt;br /&gt;
Diese Schätzung addiert man einfach zur wahren Länge des Weges &amp;lt;tt&amp;gt;startnode &amp;amp;rarr; node&amp;lt;/tt&amp;gt; dazu, um die verbesserte Priorität zu erhalten:&lt;br /&gt;
 priority = newLength + guess(neighbor, destination)&lt;br /&gt;
(Im originalen Dijkstra-Algorithmus wird als Priorität nur &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; allein verwendet. Man beachte, dass man &amp;lt;tt&amp;gt;newLength&amp;lt;/tt&amp;gt; jetzt zusätzlich im Heap speichern muss, weil man es für die Expansion des Knotens später noch benötigt.)&lt;br /&gt;
&lt;br /&gt;
Damit sicher gestellt ist, dass der A*-Algorithmus immer noch die korrekten kürzesten Wege findet, darf die Schätzung den wahren Restweg &amp;lt;i&amp;gt;niemals überschätzen&amp;lt;/i&amp;gt;. Es muss immer gelten:&lt;br /&gt;
 0 &amp;lt;= guess(node, destination) &amp;lt;= trueDistance(node, destination)&lt;br /&gt;
Damit gilt insbesondere &amp;lt;tt&amp;gt;guess(destination, destination) = trueDistance(destination, destination) = 0&amp;lt;/tt&amp;gt;, an der Priorität des Knotens &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt; ändert sich also nichts. Die Prioritäten aller anderen Knoten veschlechtern sich hingegen, weil zur bisherigen Priorität noch atwas addiert wird. Für die wichtigen Knoten auf dem kürzesten Weg vom Start nach Ziel gilt jedoch, dass deren neue Priorität immer noch besser ist als die Priorität des Zielknotens selbst. Für diese Knoten gilt nämlich&lt;br /&gt;
 falls node auf dem kürzesten Weg von startnode nach destination liegt:&lt;br /&gt;
 trueDistance(startnode, node) + guess(node, destination) &amp;lt;= trueDistance(startnode, destination)&lt;br /&gt;
weil der Weg von Start nach &amp;lt;tt&amp;gt;node&amp;lt;/tt&amp;gt; ein Teil des kürzesten Wegs von Start nach Ziel ist und die Restschätzung die wahre Entfernung immer unterschätzt. Diese Knoten werden deshalb stets vor dem Zielknoten expandiert, so dass wir die &amp;lt;tt&amp;gt;parent&amp;lt;/tt&amp;gt;-Kette immer noch korrekt zurückverfolgen können. Für alle anderen Knoten gilt idealerweise, dass die neue Priorität schlechter ist als die Priorität von &amp;lt;tt&amp;gt;destination&amp;lt;/tt&amp;gt;, so dass man sich diese irrelevanten Knotenexpansionen sparen kann.&lt;br /&gt;
&lt;br /&gt;
Für das Beispiel eines Straßennetzwerks bietet sich als Schätzung die Luftlinienentfernung an, weil Straßen nie kürzer sein können als die Luftlinie. Damit erreicht man in der Praxis deutliche Einsparungen. Generell gilt, dass der A*-Algorithmus im typischen Fall schneller ist als der Algorithmus von Dijkstra, aber man kann immer pathologische Fälle konstruieren, wo die Änderung der Prioritäten nichts bringt. Die Komplexität des A*-Algorithmus im ungünstigen Fall ist deshalb nach wie vor &amp;lt;math&amp;gt;O(E\,\log\,V)&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=='''Minimaler Spannbaum'''==&lt;br /&gt;
'''(engl.: minimum spanning tree; abgekürzt: MST)'''&lt;br /&gt;
&lt;br /&gt;
[[Image:Minimum_spanning_tree.png‎ |thumb|200px|right|Ein minimal aufspannender Baum verbindet alle Punkte eines Graphen bei minimaler Kantenlänge ([http://de.wikipedia.org/wiki/Spannbaum Quelle])]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: gewichteter Graph G, zusammenhängend&amp;lt;br/&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: Untermenge &amp;lt;math&amp;gt;E'\subseteq E&amp;lt;/math&amp;gt; der Kanten, so dass die Summe der Kantengewichte &amp;lt;math&amp;gt;\sum_{e\in E'} w_e&amp;lt;/math&amp;gt; minimal und der entstehende Graph G' zusammenhängend ist.&amp;lt;br/&amp;gt;&lt;br /&gt;
* G' definiert immer einen Baum, denn andernfalls könnte man eine Kante weglassen und dadurch die Summe &amp;lt;math&amp;gt;\sum_{e\in E'} w_e&amp;lt;/math&amp;gt; verringern, ohne dass sich am Zusammenhang von G' etwas ändert. &amp;lt;br/&amp;gt;&lt;br /&gt;
* Wenn der Graph G nicht zusammenhängend ist, kann man den Spannbaum für jede Zusammenhangskomponente getrennt ausrechnen. Man erhält dann einen aufspannenden Wald. &lt;br /&gt;
* Der MST ist ähnlich wie der Dijkstra-Algorithmus: Dort ist ein Pfad gesucht, bei dem die Summe der Gewichte über den Pfad minimal ist. Beim MST suchen wir eine Lösung, bei der die Summe der Gewichte über den ganzen Graphen minimal ist. &lt;br /&gt;
* Das Problem des MST ist nahe verwandt mit der Bestimmung der Zusammenhangskomponente, z.B. über den Tiefensuchbaum. Für die Zusammenhangskomponenten genügt allerdings ein beliebiger Baum, während beim MST ein minimaler Baum gesucht ist.&lt;br /&gt;
&lt;br /&gt;
=== Anwendungen ===&lt;br /&gt;
==== Wie verbindet man n gegebene Punkte mit möglichst kurzen Straßen (Eisenbahnen, Drähten [bei Schaltungen] usw.)?====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:center&amp;quot; border=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; cellspacing=&amp;quot;0&amp;quot; &lt;br /&gt;
|MST minimale Verbindung (Abb.1)&lt;br /&gt;
|MST = 2 (Länge = Kantengewicht)(Abb.2)&lt;br /&gt;
|- valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| [[Image:mst.png]] &lt;br /&gt;
| [[Image:Gleichseitigesdreieck.png]]&lt;br /&gt;
|}&lt;br /&gt;
*In der Praxis: Die Festlegung, dass man nur die gegebenen Punkte verwenden darf, ist eine ziemliche starke Einschränkung. &lt;br /&gt;
&lt;br /&gt;
* Wenn man sich vorstellt, es sind drei Punkte gegeben, die als gleichseitiges Dreieck angeordnet sind, dann ist der MST (siehe Abb.2, schwarz gezeichnet) und hat die Länge 2. Man kann hier die Länge als Kantengewicht verwenden. &lt;br /&gt;
&lt;br /&gt;
* Wenn es erlaubt ist zusätzliche Punkte einzufügen, dann kann man in der Mitte einen neuen Punkt setzen &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; neuer MST (siehe Abb.2, orange gezeichnet).&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Höhe = &amp;lt;math&amp;gt;\frac{1}{2}\sqrt{3}&amp;lt;/math&amp;gt;, Schwerpunkt: teilt die Höhe des Dreiecks im Verhältnis 2:1; der Abstand von obersten Punkt bis zum neu eingeführten Punkt: &amp;lt;math&amp;gt;\frac{2}{3}h = \frac{\sqrt{3}}{3}&amp;lt;/math&amp;gt;, davon insgesamt 3 Stück, damit (gilt für den MST in orange eingezeichnet): MST = &amp;lt;math&amp;gt;3\left(\frac{1}{3}\right) \sqrt{3} = \sqrt{3} \approx 1,7&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Damit ist der MST in orange kürzer als der schwarz gezeichnete MST. &amp;lt;br\&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt;Folgerung: MST kann kürzer werden, wenn man einen Punkt dazu nimmt. &lt;br /&gt;
* Umgekehrt kann der MST auch kürzer werden, wenn man einen Punkt aus dem Graphen entfernt, aber wie das Beipiel des gleichseitigen Dreiecks zeigt, ist dies nicht immer der Fall.&lt;br /&gt;
&lt;br /&gt;
[[Image: bahn.png|Bahnstrecke Verbindung (Abb.3)]]&lt;br /&gt;
&lt;br /&gt;
* Methode der zusätzlichen Punkteinfügung hat man früher beim Bahnstreckenbau verwendet. Durch Einführung eines Knotenpunktes kann die Streckenlänge verkürzt werden (Dreiecksungleichung).&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Bestimmung von Datenclustern ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Image:cluster.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Daten (in der Abb.: Punkte) bilden Gruppen. &lt;br /&gt;
&lt;br /&gt;
* In der Abbildung hat man 2 verschiedene Messungen gemacht (als x- und y-Achse aufgetragen), bspw. Größe und Gewicht von Personen. Für jede Person i wird ein Punkt an der Koordinate (Größe&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;, Gewicht&amp;lt;sub&amp;gt;i&amp;lt;/sub&amp;gt;) gezeichnet (siehe Bild a). Dies bezeichnet man als ''Scatter Plot''. Wenn bestimmte Wertkombinationen häufiger auftreten als andere, bilden sich mitunter Gruppen aus, bspw. eine Gruppe für &amp;quot;klein und schwer&amp;quot; etc.&lt;br /&gt;
&lt;br /&gt;
* Durch Verbinden der Punkte mittels eines MST (siehe Abbildung (b)) sieht man, dass es kurze (innerhalb der Gruppen) und lange Kanten (zwischen den Gruppen) gibt. &lt;br /&gt;
&lt;br /&gt;
* Wenn man geschickt eine Schwelle einführt und alle Kanten löscht, die länger sind als die Schwelle, dann bekommt man als Zusammenhangskomponente die einzelnen Gruppen. &lt;br /&gt;
&lt;br /&gt;
=== Algorithmen ===&lt;br /&gt;
&lt;br /&gt;
Genau wie bei der Bestimmung von Zusammenhangskomponenten kann man auch das MST-Problem entweder nach dem Anlagerungsprinzip oder nach dem Verschmelzungsprinzip lösen (dazu gibt es noch weitere Möglichkeiten, z.B. den [http://de.wikipedia.org/wiki/Algorithmus_von_Bor%C5%AFvka Algorithmus von Boruvka]). Der Anlagerungsalgorithmus für MST wurde zuerst von Prim beschrieben und trägt deshalb seinen Namen, der Verschmelzungsalgorithmus stammt von Kruskal. Im Vergleich zu den Algorithmen für Zusammenhangskomponenten ändert sich im wesentlichen nur die Reihenfolge, in der die Kanten betrachtet werden: Eine Prioritätswarteschlange stellt jetzt sicher, dass am Ende wirklich der Baum mit den geringstmöglichen Kosten herauskommt.&lt;br /&gt;
&lt;br /&gt;
====Algorithmus von Prim====&lt;br /&gt;
[http://de.wikipedia.org/wiki/Algorithmus_von_Prim Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus von Prim geht nach dem Anlagerungsprinzip vor (vgl. den Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Tiefensuche|Zusammenhangskomponenten mit Tiefensuche]]): Starte an der Wurzel (ein willkürlich gewählter Knoten) und füge jeweils die günstigste Kante an die aktuellen Teillösung an, die keinen Zyklus verursacht. Die Sortierung der Kanten nach Priorität erfolgt analog zum Dijsktra-Algorithmus, aber die Definitionen, welche Kante die günstigste ist, unterscheiden sich. Die Konvention für die Bedeutung der Elemente des Heaps ist ebenfalls identisch: ein Tupel mit &amp;lt;tt&amp;gt;(priority, node, predecessor)&amp;lt;/tt&amp;gt;. Die folgende Implementation verdeutlicht sehr schön die Ähnlichkeit der beiden Algorithmen. Das Ergebnis wird als property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; zurückgegeben, in der für jeden Knoten sein Vorgänger im MST steht, wobei die Wurzel wie üblich auf sich selbst verweist.&lt;br /&gt;
&lt;br /&gt;
 import heapq&lt;br /&gt;
 &lt;br /&gt;
 def prim(graph, weights):                # Kantengewichte wie bei Dijkstra als property map&lt;br /&gt;
 	sum = 0.0                         # wird später das Gewicht des Spannbaums sein&lt;br /&gt;
 	start = 0                         # Knoten 0 wird willkürlich als Wurzel gewählt&lt;br /&gt;
        &lt;br /&gt;
        parents = [None]*len(graph)       # property map, die den resultierenden Baum kodiert&lt;br /&gt;
        parents[start] = start            # Wurzel zeigt auf sich selbst&lt;br /&gt;
        &lt;br /&gt;
 	heap = []                         # Heap für die Kanten des Graphen&lt;br /&gt;
 	for neighbor in graph[start]:     # besuche die Nachbarn von start&lt;br /&gt;
 	    heapq.heappush(heap, (weights[(start, neighbor)], neighbor, start))  # und fülle Heap &lt;br /&gt;
 	&lt;br /&gt;
        while len(heap) &amp;gt; 0:&lt;br /&gt;
 	    w, node, predecessor = heapq.heappop(heap) # hole billigste Kante aus dem Heap&lt;br /&gt;
            if parents[node] is not None: # die Kante würde einen Zyklus verursachen&lt;br /&gt;
                continue                  #   =&amp;gt; ignoriere diese Kante&lt;br /&gt;
            parents[node] = predecessor   # füge Kante in den MST ein&lt;br /&gt;
            sum += w                      # und aktualisiere das Gesamtgewicht &lt;br /&gt;
            for neighbor in graph[node]:  # besuche die Nachbarn von node&lt;br /&gt;
                if parents[neighbor] is None:  # aber nur, wenn kein Zyklus entsteht&lt;br /&gt;
                    heapq.heappush(heap, (weights[(node,neighbor)], neighbor, node)) # füge Kandidaten in Heap ein&lt;br /&gt;
        &lt;br /&gt;
 	return parents, sum               # MST und Gesamtgewicht zurückgeben&lt;br /&gt;
&lt;br /&gt;
====Algorithmus von Kruskal====&lt;br /&gt;
[http://de.wikipedia.org/wiki/Algorithmus_von_Kruskal Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Kruskal%27s_algorithm (en)]&lt;br /&gt;
&lt;br /&gt;
Die alternative Vorgehensweise ist das Verschmelzungsprinzip (vgl. den Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Union-Find-Algorithmus|Zusammenhangskomponenten mit Union-Find-Algorithmus]]), das der Algorithmus von Kruskal verwendet. Jeder Knoten wird zunächst als trivialer Baum mit nur einem Knoten betrachtet, und alle Kanten werden aufsteigend nach Gewicht sortiert. Dann wird die billigste noch nicht betrachtete Kante in den MST eingefügt, falls sich dadurch kein Zyklus bildet (erkennbar daran, dass die Endknoten in verschiedenen Zusammenhangskomponenten liegen, das heisst verschiedene Anker haben). Da der fertige Baum (V-1) Kanten haben muss, wird dies (V-1) Mal zutreffen. Andernfalls wird diese Kante ignoriert. Anders ausgedrückt: Der Algorithmus beginnt mit ''V'' Bäumen; in (''V''-1) Verschmelzungsschritten kombiniert er jeweils zwei Bäume (unter Verwendung der kürzesten möglichen Kante), bis nur noch ein Baum übrig bleibt. Der einzige Unterschied zum einfachen Union-Find besteht darin, dass die Kanten in aufsteigender Reihenfolge betrachtet werden müssen, was wir hier durch eine Prioritätswarteschlange realisieren. Der Algorithmus von J.Kruskal ist seit 1956 bekannt. &lt;br /&gt;
&lt;br /&gt;
 def kruskal(graph, weights):&lt;br /&gt;
     anchors = range(len(graph))           # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     results = []                          # result wird später die Kanten des MST enthalten    &lt;br /&gt;
     &lt;br /&gt;
     heap = []                             # Heap zum Sortieren der Kanten nach Gewicht&lt;br /&gt;
     for edge, w in weights.iteritems():   # alle Kanten einfügen&lt;br /&gt;
         heapq.heappush(heap, (w, edge))&lt;br /&gt;
     &lt;br /&gt;
     while len(heap) &amp;gt; 0:                  # solange noch Kanten vorhanden sind&lt;br /&gt;
         w, edge = heapq.heappop(heap)     # billigste Kante aus dem Heap nehmen&lt;br /&gt;
         a1 = findAnchor(anchors, edge[0]) # Anker von Startknoten der Kante&lt;br /&gt;
         a2 = findAnchor(anchors, edge[1]) # ... und Endknoten bestimmen&lt;br /&gt;
         if a1 != a2:                      # wenn die Knoten in verschiedenen Komponenten sind&lt;br /&gt;
             anchors[a2] = a1              # Komponenten verschmelzen&lt;br /&gt;
             result.append(edge)           # ... und Kante in MST einfügen&lt;br /&gt;
     &lt;br /&gt;
     return result                         # Kanten des MST zurückgeben&lt;br /&gt;
&lt;br /&gt;
Die Funktion &amp;lt;tt&amp;gt;findAnchor()&amp;lt;/tt&amp;gt; wurde im Abschnitt [[Graphen_und_Graphenalgorithmen#Lösung mittels Union-Find-Algorithmus|Zusammenhangskomponenten mit Union-Find-Algorithmus]] implementiert. Im Unterschied zum Algorithmus von Prim geben wir hier nicht die property map &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; zurück, sondern einfach eine Liste der Kanten im MST.&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus eignet sich insbesondere für das Clusteringproblem, da der Schwellwert von vornerein als maximales Kantengewicht an den Algorithmus übergeben werden kann. Man hört mit dem Vereinigen auf, wenn das Gewicht der billigste Kante im Heap den Schwellwert überschreitet. Beim Algorithmus von Kruskal kann dann keine bessere Kante als der Schwellwert mehr kommen, da die Kanten vorher sortiert worden sind. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Komplexität:&amp;lt;/b&amp;gt; wie beim Dijkstra-Algorithmus, weil jede Kante genau einmal in den Heap kommt. Der Aufwand für das Sortieren ist somit &amp;lt;math&amp;gt;O\left(E\log E\right)&amp;lt;/math&amp;gt;, was sich zu &amp;lt;math&amp;gt;O \left(E\,\log\,V\right)&amp;lt;/math&amp;gt; reduziert, falls keine Mehrfachkanten vorhanden sind.&lt;br /&gt;
&lt;br /&gt;
=&amp;gt; geeignet für Übungsaufgabe&lt;br /&gt;
&lt;br /&gt;
====Verwendung einer BucketPriorityQueue====&lt;br /&gt;
&lt;br /&gt;
Beide Algorithmen zur Bestimmung des minimalen Spannbaums benötigen eine Prioritätswarteschlange. Wenn die Kantengewichte ganze Zahlen im Bereich &amp;lt;tt&amp;gt;0...(m-1)&amp;lt;/tt&amp;gt; sind, kann man die MST-Algorithmen deutlich beschleunigen, wenn man anstelle des Heaps eine [[Prioritätswarteschlangen#Prioritätssuche mit dem Bucket-Prinzip|&amp;lt;tt&amp;gt;BucketPriorityQueue&amp;lt;/tt&amp;gt;]] verwendet. Die Operationen zum Einfügen einer Kante in die Queue und zum Entfernen der billibsten Kante aus der Queue beschleunigen sich dadurch auf O(1) statt O(log V) (außer wenn die Gewichte sehr ungünstig auf die Kanten verteilt sind). In der Praxis erreicht man durch diese Änderung typischerweise deutliche Verbesserungen. In der Bildverarbeitung können die Prioritäten beispielsweise die Wahrscheinlichkeit kodieren, dass zwei benachbarte Pixel zu verschiedenen Objekten gehören. Bildet man jetzt den MST, und bricht bei einer bestimmten Wahrscheinlichkeit ab, erhält man Cluster von Pixeln, die wahrscheinlich zum selben Objekt gehören (weil der MST ja die Kanten mit minimalem Gewicht bevorzugt, und kleine Gewichte bedeuten kleine Wahrscheinlichkeit, dass benachbarte Pixel von einander getrennt werden). Da man die Wahrscheinlichkeiten nur mit einer Genauigkeit von ca. 1% berechnen kann, reichen hiefür 100 bis 200 Quantisierungstufen aus. Durch Verwendung der schnellen &amp;lt;tt&amp;gt;BucketPriorityQueue&amp;lt;/tt&amp;gt; kann man jetzt wesentlich größere Bilder in akzeptabler Zeit bearbeiten als dies mit einem Heap möglich wäre.&lt;br /&gt;
&lt;br /&gt;
== Algorithmen für gerichtete Graphen ==&lt;br /&gt;
&lt;br /&gt;
Zur Erinnerung: in einem gerichteten Graphen sind die Kanten (i &amp;amp;rarr; j) und (j &amp;amp;rarr; i) voneinander verschieden, und eventuell existiert nur eine der beiden Richtungen. Im allgemeinen unterscheidet sich der [[Graphen_und_Graphenalgorithmen#transposed_graph|transponierte Graph]] G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; also vom Originalgraphen G. Beim Traversieren des Graphen und bei der Pfadsuche dürfen Kanten nur in passender Richtung verwendet werden. Bei gewichteten Graphen tritt häufig der Fall auf, dass zwar Kanten in beiden Richtungen existieren, diese aber unterschiedliche Gewichte haben.&lt;br /&gt;
&lt;br /&gt;
Gerichtete Graphen ergeben sich in natürlicher Weise aus vielen Anwendungsproblemen:&lt;br /&gt;
* Routenplanung&lt;br /&gt;
** Bei Straßennetzwerken enstehen gerichtete Graphen, sobald es Einbahnstraßen gibt.&lt;br /&gt;
** Verwendet man Gewichte, um die erwarteten Fahrzeiten entlang einer Straße zu kodieren, gibt es Asymmetrien z.B. dann, wenn Straßen in einer Richtung bergab, in der anderen bergauf befahren werden. Hier existieren zwar Kanten in beiden Richtungen, sie haben aber unterschiedliche Gewichte. Ähnliches gilt für Flüge: Durch den Gegenwind des Jetstreams braucht man von Frankfurt nach New York länger als umgekehrt von New York nach Frankfurt.&lt;br /&gt;
* zeitliche oder kausale Abhängigkeiten&lt;br /&gt;
** Wenn die Knoten Ereignisse repräsentieren, von denen einige die Ursache von anderen sind, diese wiederum die Ursache der nächsten usw., verbindet man die Knoten zweckmäßig durch gerichtete Kanten, die die Kausalitätsbeziehungen kodieren. Handelt es sich um logische &amp;quot;wenn-dann&amp;quot;-Regeln, erhält man einen [[Graphen_und_Graphenalgorithmen#Anwendung:_Das_Erf.C3.BCllbarkeitsproblem_in_Implikationengraphen|Implikationengraph]] (siehe unten). Handelt es sich hingegen um Wahrscheinlichkeitsaussagen (&amp;quot;Wenn das Wetter schön ist, haben Studenten tendenziell gute Laune, wenn eine Prüfung bevorsteht eher schlechte usw.&amp;quot;), erhält man ein [http://de.wikipedia.org/wiki/Bayessches_Netz Bayessches Netz].&lt;br /&gt;
** Wenn bestimmte Aufgaben erst begonnen werden können, nachdem andere Aufgaben erledigt sind, erhält man einen Abhängigkeitsgraphen. Beispielsweise dürfen Sie erst an der Klausur teilnehmen, nachdem Sie die Übungsaufgaben gelöst haben, und Sie dürfen erst die Abschlussarbeit beginnen, nachdem Sie bestimmte Prüfungen bestanden haben. Ein anderes schönes Beispiel liefern die Regeln für das [[Graphen_und_Graphenalgorithmen#Anwendung:_Abh.C3.A4ngigkeitsgraph|Ankleiden]] weiter unten.&lt;br /&gt;
** Gerichtete Graphen kodieren die Abhängigkeiten zwischen Programmbibliotheken. Beispielsweise benötigt das Pythonmodul &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; die internen Submodule &amp;lt;tt&amp;gt;json.encoder&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;json.decode&amp;lt;/tt&amp;gt; sowie das externe Modul &amp;lt;tt&amp;gt;decimal&amp;lt;/tt&amp;gt;. Die Submodule benötigen wiederum die externen Module &amp;lt;tt&amp;gt;re&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;sys&amp;lt;/tt&amp;gt;, das Modul &amp;lt;tt&amp;gt;decimal&amp;lt;/tt&amp;gt; braucht &amp;lt;tt&amp;gt;copy&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;collections&amp;lt;/tt&amp;gt; usw.&lt;br /&gt;
** Das Internet kann als gerichteter Graph dargestellt werden, wobei die Webseiten die Knoten, und die Hyperlinks die Kanten sind.&lt;br /&gt;
* Sequence Alignment&lt;br /&gt;
** Eine gute Rechtschreibprüfung markiert nicht nur fehlerhafte Wörter, sondern macht auch plausible Vorschläge, was eigentlich gemeint gewesen sein könnte. Dazu muss sie das gegebene Wort mit den Wörtern eines Wörterbuchs vergleichen und die Ähnlichkeit bewerten. Ein analoges Problem ergibt sich, wenn man DNA Fragmente mit der Information in einer Genomdatenbank abgleichen will. &lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Sequence Alignment / Edit Distance ===&lt;br /&gt;
&lt;br /&gt;
:gegeben: zwei Wörter (allgemein: beliebige Zeichenfolgen)&lt;br /&gt;
:gesucht: Wie kann man die Buchstaben am besten in Übereinstimmung bringen?&lt;br /&gt;
&lt;br /&gt;
:Beispiel: WORTE – NORDEN&lt;br /&gt;
&lt;br /&gt;
Zwei mögliche Alignments sind&lt;br /&gt;
&lt;br /&gt;
  W&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;OR&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;T&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;E&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;.          W.ORTE&lt;br /&gt;
  N&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;OR&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;D&amp;lt;font color=red&amp;gt;&amp;lt;b&amp;gt;E&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;N          NORDEN&lt;br /&gt;
&lt;br /&gt;
wobei der Punkt anzeigt, dass der untere Buchstabe keinen Partner hat, und rote Buchstaben oben und unten übereinstimmen. Jede Nicht-Übereinstimmung verursacht nun gewisse Kosten. Dabei unterscheiden wir zwei Fälle:&lt;br /&gt;
# Matche a[i] mit b[j]. Falls a[i] == b[j], ist das gut (rote Buchstaben), und es entstehen keine Kosten. Andernfalls entstehen Kosten U (schwarze Buchstaben).&lt;br /&gt;
# Wir überspringen a[i] oder b[j] (Buchstabe vs. Punkt). Dann entstehen Kosten V. (Manchmal unterscheidet man auch noch Kosten Va und Vb, wenn das Überspringen bei a und b unterschieldiche Signifikanz hat.)&lt;br /&gt;
&lt;br /&gt;
Gesucht ist nun das &amp;lt;b&amp;gt;Alignment mit minimalen Kosten&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Diese Aufgabe kann man sehr schön als gerichteten Graphen darstellen: Wir definieren ein rechteckiges Gitter und schreiben das erste Wort über das Gitter und das andere links davon. Die Gitterpunkte verbinden wir mit Pfeilen (gerichteten Kanten), wobei ein Pfeil nach rechts bedeutet, dass wir beim oberen Wort einen Buchstaben überspringen, ein Pfeil nach unten, dass wir beim linken Wort einen Buchstaben überspringen, und ein diagonaler Pfeil, dass wir zwei Buchstaben matchen (und zwar die am Pfeilende). Die Farben der Pfeile symbolisieren die Kosten: rot für das Überspringen eines Buchstabens (Kosten V), blau für das Matchen, wenn die Buchstaben nicht übereinstimmen (Kosten U), und grün, wenn die Buchstaben übereinstimmen (keine Kosten). &lt;br /&gt;
&lt;br /&gt;
[[Image:sequence-alignment.png|300px]]&lt;br /&gt;
&lt;br /&gt;
Lösung:&lt;br /&gt;
:Suche den kürzesten Pfad vom Knoten &amp;quot;START&amp;quot; (oben links) nach unten rechts. Dazu kann der [[Graphen und Graphenalgorithmen#Algorithmus von Dijkstra|Algorithmus von Dijkstra]] verwendet werden, der auf gerichteten Graphen genauso funktioniert wie auf ungerichteten.&lt;br /&gt;
&lt;br /&gt;
Für unser Beispiel von oben erhalten wir die folgenden Pfade:&lt;br /&gt;
&lt;br /&gt;
[[Image:sequence-alignment-weg1.png|400px]]&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;[[Image:sequence-alignment-weg2.png|400px]]&lt;br /&gt;
&lt;br /&gt;
Durch Addieren der Kosten entsprechend der Farben sieht man, dass der erste Weg die Kosten 2U+V und der zweite die Kosten 5U+V hat. Der erste Weg ist offensichtlich günstiger und entspricht dem besten Alignment.&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Abhängigkeitsgraph ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Beispiel: &amp;lt;/b&amp;gt; Wie erklärt man einem zerstreuten Professor, wie er sich morgens anziehen soll? Der folgende Graph enthält einen Knoten für jede Aktion, und eine Kante (i &amp;amp;rarr; j) bedeutet, dass die Aktion i vor der Aktion j abgeschlossen werden muss.&lt;br /&gt;
&lt;br /&gt;
[[Image:anziehen-graph.png|600px]]&lt;br /&gt;
&lt;br /&gt;
In derartigen Abhängigkeitsgraphen ist die wichtigste Frage immer, ob der Graph azyklisch ist. Wäre dies nämlich nicht der Fall, kann es keine Reihenfolge der Aktionen geben, die alle Abhängigkeiten erfüllt. Dies sieht man leicht, wenn man den einfachsten möglichen Zyklus betrachtet: es gibt sowohl eine Kante (i &amp;amp;rarr; j) als auch eine (j &amp;amp;rarr; i). Dann müsste man i vor j erledigen, aber ebenso j vor i, was offensichtlich unmöglich ist - das im Graph kodierte Problem ist dann unlösbar. Wegen ihrer Wichtigkeit wird für gerichtete azyklische Graphen oft die Abkürzung &amp;lt;b&amp;gt;DAG&amp;lt;/b&amp;gt; (von &amp;lt;i&amp;gt;directed acyclic graph&amp;lt;/i&amp;gt;) verwendet. Ein Graph ist genau dann ein DAG, wenn es eine topologische Sortierung gibt:&lt;br /&gt;
;topologische Sortierung: Zeichne die Knoten so auf eine Gerade, dass alle Kanten (Pfeile) nach rechts zeigen. &lt;br /&gt;
Arbeitet man die Aktionen nach einer (beliebigen) topologischen Sortierung ab, werden automatisch alle Abhängigkeiten eingehalten: Da alle Pfeile nach rechts zeigen, werden abhängige Aktionen immer später ausgeführt. Die topologische Sortierung ist im allgemeinen nicht eindeutig. Die folgende Skizze zeigt eine mögliche topologische Sortierung für das Anziehen:&lt;br /&gt;
&lt;br /&gt;
[[Image:anziehen-topologische-sortierung.png|600px]]&lt;br /&gt;
&lt;br /&gt;
Eine solche fest vorgegebene Reihenfolge ist für den zerstreuten Professor sicherlich eine größere Hilfe als der ursprüngliche Graph. Man erkennt, dass die Sortierung nicht eindeutig ist, beispielsweise bei der Uhr: Da für die Uhr keine Abhängigkeiten definiert sind, kann man diese Aktion an beliebiger Stelle einsortieren. Hier wurde willkürlich die letzte Stelle gewählt.&lt;br /&gt;
&lt;br /&gt;
==== Zwei Algorithmen zum Finden der topologischen Sortierung ====&lt;br /&gt;
&lt;br /&gt;
Die folgenden Algorithmen finden entweder eine topologische Sortierung, oder signalisieren, dass der Graph zyklisch ist.&lt;br /&gt;
&lt;br /&gt;
===== Algorithmus 1 =====&lt;br /&gt;
# Suche einen Knoten mit Eingangsgrad 0 (ohne eingehende Pfeile) =&amp;gt; in einem gerichteten azyklischen Graphen gibt es immer einen solchen Knoten&lt;br /&gt;
# Platziere diesen Knoten auf der Geraden (beliebig)&lt;br /&gt;
# Entferne den Knoten aus dem Graphen zusammen mit den ausgehenden Kanten&lt;br /&gt;
# Gehe zu 1., aber platziere in 2. immer rechts der Knoten, die schon auf der Geraden vorhanden sind.&lt;br /&gt;
: =&amp;gt; Wenn noch Knoten übrig sind, aber keiner Eingangsgrad 0 hat, muss der Graph zyklisch sein.&lt;br /&gt;
&lt;br /&gt;
[[Image:bild6.JPG]]&lt;br /&gt;
&lt;br /&gt;
Beispiel für einen zyklischen Graphen: kein Knoten hat Eingangsgrad 0.&lt;br /&gt;
&lt;br /&gt;
Um den Algorithmus zu implementieren, verwenden wir eine property map &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt;, die wir in einem ersten Durchlauf durch den Graphen füllen und die dann für jeden Knoten die Anzahl der eingehenden Kanten speichert. Dann gehen wir sukzessive zu allen Knoten mit &amp;lt;tt&amp;gt;in_degree == 0&amp;lt;/tt&amp;gt;. Anstatt sie aber tatsächlich aus dem Graphen zu entfernen wie im obigen Pseudocode, dekrementieren wir nur den &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt; ihrer Nachbarn. Wird der &amp;lt;tt&amp;gt;in_degree&amp;lt;/tt&amp;gt; eines Nachbarn dadurch 0, wird er ebenfalls in das Array der zu scannenden Knoten aufgenommen. Wenn der Graph azyklisch ist, enthält das Array am Ende alle Knoten des Graphen, und die Reihenfolge der Einfügungen definiert eine topologische Sortierung. Andernfalls ist das Array zu kurz, und wir signalisieren durch Zurückgeben von &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, dass der Graph zyklisch ist:&lt;br /&gt;
&lt;br /&gt;
 def topological_sort(graph):              # ein gerichteter Graph&lt;br /&gt;
     in_degree = [0]*len(graph)            # property map für den Eingangsgrad jeden Knotens&lt;br /&gt;
     for node in xrange(len(graph)):       # besuche alle Knoten&lt;br /&gt;
         for neighbor in graph[node]:      #  ... und deren Nachbarn&lt;br /&gt;
             in_degree[neighbor] += 1      #  ... und inkrementiere den Eingangsgrad&lt;br /&gt;
     &lt;br /&gt;
     result = []                           # wird später die topologische Sortierung enthalten&lt;br /&gt;
     for node in xrange(len(graph)):&lt;br /&gt;
         if in_degree[node] == 0:&lt;br /&gt;
             result.append(node)           # füge alle Knoten mit Eingangsgrad 0 in result ein&lt;br /&gt;
     &lt;br /&gt;
     k = 0&lt;br /&gt;
     while k &amp;lt; len(result):                # besuche alle Knoten mit Eingangsgrad 0&lt;br /&gt;
         node = result[k]&lt;br /&gt;
         k += 1&lt;br /&gt;
         for neighbor in graph[node]:      # besuche alle Nachbarn&lt;br /&gt;
             in_degree[neighbor] -= 1      # entferne 'virtuell' die eingehende Kante&lt;br /&gt;
             if in_degree[neighbor] == 0:  # wenn neighbor jetzt Eingangsgrad 0 hat&lt;br /&gt;
                 result.append(neighbor)   #  ... füge ihn in result ein&lt;br /&gt;
     &lt;br /&gt;
     if len(result) == len(graph):         # wenn alle Knoten jetzt Eingangsgrad 0 haben&lt;br /&gt;
         return result                     # ... ist result eine topologische Sortierung&lt;br /&gt;
     else:&lt;br /&gt;
         return None                       # andernfalls ist der Graph zyklisch&lt;br /&gt;
&lt;br /&gt;
===== Algorithmus 2 =====&lt;br /&gt;
Der obige Algorithmus hat den Nachteil, dass er jeden Knoten zweimal expandiert. Man kann eine topologische Sortierung stattdessen auch mit Tiefensuche bestimmen. Es gilt nämlich der folgende&lt;br /&gt;
;Satz: Wird ein DAG mittels Tiefensuche traversiert, definiert die &amp;lt;i&amp;gt;reverse post-order&amp;lt;/i&amp;gt; eine topologische Sortierung.&lt;br /&gt;
Zur Erinnerung: die post-order erhält man, indem man jeden Knoten ausgibt, &amp;lt;i&amp;gt;nachdem&amp;lt;/i&amp;gt; die Rekursion zu allen seinen Nachbarn beendet ist, siehe unsere [[Graphen_und_Graphenalgorithmen#pre_and_post_order|Diskussion weiter oben]]. Die reverse post-order ist gerade die Umkehrung dieser Reihenfolge. Die folgende Implementation verwendet die rekursive Version der Tiefensuche, in der Praxis wird man meist die iterative Version mit Stack bevorzugen, weil bei großen Graphen die Aufruftiefe sehr große werden kann:&lt;br /&gt;
&lt;br /&gt;
 def reverse_post_order(graph):               # gerichteter Graph&lt;br /&gt;
     result = []                              # enthält später die reverse post-order&lt;br /&gt;
     visited = [False]*len(graph)             # Flags für bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):                         # besuche node&lt;br /&gt;
         if not visited[node]:                # aber nur, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True             # markiere ihn als besucht&lt;br /&gt;
             for neighbor in graph[node]:     # und besuche die Nachbarn&lt;br /&gt;
                 visit(neighbor)&lt;br /&gt;
             result.append(node)              # alle Nachbarn besucht =&amp;gt; Anhängen an result liefert post-order&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):          # besuche alle Knoten&lt;br /&gt;
         visit(node)&lt;br /&gt;
     &lt;br /&gt;
     result.reverse()                         # post-order =&amp;gt; reverse post-order&lt;br /&gt;
     return result&lt;br /&gt;
&lt;br /&gt;
Die Tatsache, dass die reverse post-order tatsächlich eine topologische Sortierung liefert, leuchtet wahrscheinlich nicht unmittelbar ein. Bevor wir diese Tatsache beweisen. wollen wir uns anhand des Ankleidegraphen klar machen, dass die pre-order (die man intuitiv vielleicht eher wählen würde) keine topologische Sortierung ist. Startet man die Tiefensuche beim Knoten &amp;quot;Unterhemd&amp;quot;, werden die Knoten in der Reihenfolge &amp;quot;Unterhemd&amp;quot;, &amp;quot;Oberhemd&amp;quot;, &amp;quot;Schlips&amp;quot;, &amp;quot;Jackett&amp;quot;, &amp;quot;Gürtel&amp;quot; gefunden. Da dann alle von &amp;quot;Unterhemd&amp;quot; erreichbaren Knoten erschöpft sind, startet man die Tiefensuche als nächstes bei &amp;quot;Unterhose&amp;quot; und erreicht von dort aus &amp;quot;Hose&amp;quot; und &amp;quot;Schuhe&amp;quot;. Man erkennt sofort, dass diese Reihenfolge nicht funktioniert: &amp;quot;Hose&amp;quot; kommt nach &amp;quot;Gürtel&amp;quot;, und &amp;quot;Jackett&amp;quot; kommt vor &amp;quot;Gürtel&amp;quot;. Bei dieser Anordnung gibt es Pfeile nach links, die Abhängigkeitsbedingungen sind somit verletzt.&lt;br /&gt;
&lt;br /&gt;
Damit die reverse post-order eine zulässige Sortierung sein kann, muss stets gelten, dass Knoten u vor Knoten v einsortiert wurde, wenn die Kante (u &amp;amp;rarr; v) existiert. Das ist aber äquivalent zur Forderung, dass in der ursprünglichen post-order (vor dem &amp;lt;tt&amp;gt;reverse&amp;lt;/tt&amp;gt;) u hinter v stehen muss. Wir betrachten den &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt;-Aufruf, bei dem u expandiert wird. Gelangt man jetzt zu u's Nachbarn v, gibt es zwei Möglichkeiten: Wenn v bereits expandiert wurde, befindet es sich bereits im Array &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt; kehrt sofort zurück. Andernfalls wird v ebenfalls expandiert und damit in &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; eingetragen, &amp;lt;i&amp;gt;bevor&amp;lt;/i&amp;gt; der rekursive Aufruf &amp;lt;tt&amp;gt;visit(v)&amp;lt;/tt&amp;gt; zurückkehrt. Knoten u wird aber erst in &amp;lt;tt&amp;gt;result&amp;lt;/tt&amp;gt; eingefügt, &amp;lt;i&amp;gt;nachdem&amp;lt;/i&amp;gt; alle rekursiven &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt;-Aufrufe seiner Nachbarn zurückgekehrt sind. In beiden Fällen steht u in der post-order wie gefordert hinter v, und daraus folgt die Behauptung.&lt;br /&gt;
&lt;br /&gt;
Der obige Algorithmus liefert natürlich nur dann eine topologische Sortierung, wenn der Graph wirklich azyklisch ist (man kann ihn aber auch anwenden, um die reverse post-order für einen zyklischen Graphen zu bestimmen, siehe Abschnitt &amp;quot;[[Graphen_und_Graphenalgorithmen#Transitive Hülle und stark zusammenhängende Komponenten|Stark zusammenhängende Komponenten]]&amp;quot;). Dieser Fall tritt in der Praxis häufig auf, weil zyklische Graphen bei vielen Anwendungen gar nicht erst entstehen können. Weiß man allerdings nicht, ob der Graph azyklisch ist oder nicht, muss man einen zusätzlichen Test auf Zyklen in den Algorithmus einbauen. &lt;br /&gt;
&lt;br /&gt;
Zyklische Graphen sind dadurch gekennzeichnet, dass es im obigen Beweis eine dritte Möglichkeit gibt: Während der Expansion von u wird rekursiv v expandiert, und es gibt eine Rückwärtskante (v &amp;amp;rarr; u). (Es spielt dabei keine Rolle, ob v von u aus direkt oder indirekt erreicht wurde.) Ein Zyklus wird also entdeckt, wenn die Tiefensuche zu u zurückkehrt, solange u noch &amp;lt;i&amp;gt;aktiv&amp;lt;/i&amp;gt; ist, d.h. wenn die Rekursion von u aus gestartet und noch nicht beendet wurde. Dies kann man leicht feststellen, wenn man in der property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; drei Werte zulässt: 0 für &amp;quot;noch nicht besucht&amp;quot;, 1 für &amp;quot;aktiv&amp;quot; und 2 für &amp;quot;beendet&amp;quot;. Wir signalisieren einen Zyklus, sobald &amp;lt;tt&amp;gt;visit&amp;lt;/tt&amp;gt; für einen Knoten aufgerufen wird, der gerade aktiv ist:&lt;br /&gt;
&lt;br /&gt;
 def topological_sort_DFS(graph):             # gerichteter Graph&lt;br /&gt;
     result = []                              # enthält später die topologische Sortierung&lt;br /&gt;
     &lt;br /&gt;
     not_visited, active, finished = 0, 1, 2  # drei Zustände für visited&lt;br /&gt;
     visited = [not_visited]*len(graph)       # Flags für aktive und bereits besuchte Knoten&lt;br /&gt;
     &lt;br /&gt;
     def visit(node):                         # besuche node (gibt &amp;quot;True&amp;quot; zurück, wenn Zyklus gefunden wurde)&lt;br /&gt;
         if not visited[node]:                # ... aber nur, wenn er noch nicht besucht wurde&lt;br /&gt;
             visited[node] = active           # markiere ihn als aktiv&lt;br /&gt;
             for neighbor in graph[node]:     # und besuche die Nachbarn&lt;br /&gt;
                 if visit(neighbor):          # wenn rekursiv ein Zyklus gefunden wurde&lt;br /&gt;
                     return True              # ... brechen wir ab und signalisieren den Zyklus&lt;br /&gt;
             visited[node] = finished         # Rekursion beendet, node ist nicht mehr aktiv&lt;br /&gt;
             result.append(node)              # alle Nachbarn besucht =&amp;gt; Anhängen an result liefert post-order&lt;br /&gt;
             return False                     # kein Zyklus gefunden&lt;br /&gt;
         elif visited[node] == active:        # Rekursion erreicht einen noch aktiven Knoten&lt;br /&gt;
             return True                      #  =&amp;gt; Zyklus gefunden&lt;br /&gt;
     &lt;br /&gt;
     for node in xrange(len(graph)):          # besuche alle Knoten&lt;br /&gt;
         if visit(node):                      # wenn Zyklus gefunden wurde&lt;br /&gt;
             return None                      # ... gibt es keine topologische Sortierung&lt;br /&gt;
     &lt;br /&gt;
     result.reverse()                         # post-order =&amp;gt; reverse post-order (=topologische Sortierung)&lt;br /&gt;
     return result&lt;br /&gt;
&lt;br /&gt;
Man macht sich leicht klar, dass kein Zyklus vorliegt, wenn die Rekursion einen Knoten erreicht, der bereits auf &amp;lt;tt&amp;gt;finished&amp;lt;/tt&amp;gt; gesetzt ist. Nehmen wir an, dass u gerade expandiert wird, und sein Nachbar v ist bereits &amp;lt;tt&amp;gt;finished&amp;lt;/tt&amp;gt;. Wenn es einen Zyklus gäbe, müsste es einen Weg von v nach u geben. Dann wäre u aber bereits während der Expansion von v gefunden worden. Da v nicht mehr im Zustand &amp;lt;tt&amp;gt;active&amp;lt;/tt&amp;gt; ist, muss die Expansion von v schon abgeschlossen gewesen sein, ohne dass u gefunden wurde. Folglich kann es keinen solchen Zyklus geben.&lt;br /&gt;
&lt;br /&gt;
=== Transitive Hülle und stark zusammenhängende Komponenten ===&lt;br /&gt;
&lt;br /&gt;
Auch bei gerichteten Graphen ist die Frage, welche Knoten miteinander zusammenhängen, von großem Interesse. Wir betrachten dazu wieder die Relation &amp;quot;Knoten v ist von Knoten u aus erreichbar&amp;quot;, die anzeigt, ob es einen Weg von u nach v gibt oder nicht. In ungerichteten Graphen ist diese Relation immer symmetrisch, weil jeder Weg in beiden Richtungen benutzt werden kann. In gerichteten Graphen gilt dies nicht. Man muss hier zwei Arten von Zusammenhangskomponenten unterscheiden:&lt;br /&gt;
;Transitive Hülle: Die transitive Hülle eines Knotens u ist die Menge aller Knoten, die von u aus erreichbar sind:&lt;br /&gt;
:&amp;lt;math&amp;gt;T(u) = \{v\ |\ u \rightsquigarrow v\}&amp;lt;/math&amp;gt;&lt;br /&gt;
;Stark zusammenhängende Komponenten: Die stark zusammenhängende Komponenten &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; eines gerichteten Graphen sind maximale Teilgraphen, so dass alle Knoten innerhalb einer Komponente von jedem anderen Knoten der selben Komponente aus erreichbar sind&lt;br /&gt;
:&amp;lt;math&amp;gt;u,v \in C_i\ \ \Leftrightarrow\ \ u \rightsquigarrow v \wedge v \rightsquigarrow u&amp;lt;/math&amp;gt;&lt;br /&gt;
Die erste Definition betrachtet den Zusammenhang asymmetrisch, ohne Beachtung der Frage, ob es auch einen Rückweg von Knoten v nach u gibt, die zweite hingegen symmetrisch.&lt;br /&gt;
&lt;br /&gt;
Die &amp;lt;b&amp;gt;transitive Hülle&amp;lt;/b&amp;gt; benötigt man, wenn man Fragen der Erreichbarkeit besonders effizient beantworten will. Wir hatten bespielsweise oben erwähnt, dass das Python-Modul &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; direkt und indirekt von mehreren anderen Module abhängt, die vorher installiert werden müssen, damit &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; funktioniert. Bittet man den Systemadministrator, das &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt;-Paket zu installieren, will er diese Abhängigkeiten wahrscheinlich nicht erst mühsam rekursiv heraussuchen, sonder er verlangt eine Liste aller Pakete, die installiert werden müssen. Dies ist gerade die transitive Hülle von &amp;lt;tt&amp;gt;json&amp;lt;/tt&amp;gt; im Abhängigkeitsgraphen. Damit man diese nicht manuell bestimmen muss, verwendet man Installationsprogrammen wie z.B. [http://pypi.python.org/pypi/pip/ pip], die die Abhängigkeiten automatisch herausfinden und installieren. &lt;br /&gt;
&lt;br /&gt;
Bei der Bestimmung der transitiven Hülle modifiziert man den gegebenen Graphen, indem man jedesmal eine neue Kante (u &amp;amp;rarr; v) einfügt, wenn diese Kante noch nicht existiert, aber v von u aus erreichbar ist. Dies gelingt mit einer sehr einfachen Variation der Tiefensuche: Wir rufen &amp;lt;tt&amp;gt;visit(k)&amp;lt;/tt&amp;gt; für jeden Knoten k auf, aber setzen die property map &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; zuvor auf &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt; zurück. Alle Knoten, die während der Rekursion erreicht werden, sind im modifizierten Graphen Nachbarn von k. Ein etwas effizienterer Ansatz ist der [http://de.wikipedia.org/wiki/Algorithmus_von_Floyd_und_Warshall Algorithmus von Floyd und Warshall].&lt;br /&gt;
&lt;br /&gt;
Die Bestimmung der &amp;lt;b&amp;gt;stark zusammenhängenden Komponenten&amp;lt;/b&amp;gt; ist etwas schwieriger. Es existieren eine ganze Reihe von effizienten Algorithmen (siehe [http://en.wikipedia.org/wiki/Strongly_connected_component WikiPedia]), deren einfachster der Algorithmus von Kosaraju ist:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;gegeben:&amp;lt;/b&amp;gt; gerichteter Graph&lt;br /&gt;
&lt;br /&gt;
# Bestimme die reverse post-order (mit der Funktion &amp;lt;tt&amp;gt;reverse_post_order&amp;lt;/tt&amp;gt;)&lt;br /&gt;
# Bilde den transponierten Graphen &amp;lt;math&amp;gt;G^T&amp;lt;/math&amp;gt; (mit der Funktion &amp;lt;tt&amp;gt;transposeGraph&amp;lt;/tt&amp;gt;)&lt;br /&gt;
# Bestimme die Zusammenhangskomponenten von &amp;lt;math&amp;gt;G^T&amp;lt;/math&amp;gt; mittels Tiefensuche, aber betrachte die Knoten dabei in der reverse post-order aus Schritt 1 (dies kann mit einer minimalen Modifikation der Funktion &amp;lt;tt&amp;gt;connectedComponents&amp;lt;/tt&amp;gt; geschehen, indem man die Zeile &amp;lt;tt&amp;gt;for node in xrange(len(graph)):&amp;lt;/tt&amp;gt; einfach nach &amp;lt;tt&amp;gt;for node in ordered:&amp;lt;/tt&amp;gt; abändert, wobei &amp;lt;tt&amp;gt;ordered&amp;lt;/tt&amp;gt; das Ergebnis der Funktion &amp;lt;tt&amp;gt;reverse_post_order&amp;lt;/tt&amp;gt; ist, also ein Array, das die Knoten in der gewünschten Reihenfolge enthält).&lt;br /&gt;
Die Zusammenhangskomponenten, die man in Schritt 3 findet, sind gerade die stark zusammenhängenden Komponenten des Originalgraphen G. Die folgende Skizze zeigt diese in grün für den schwarz gezeichneten gerichteten Graphen. &lt;br /&gt;
&lt;br /&gt;
[[Image:strongly-connected-components.png|400px]]    &lt;br /&gt;
&lt;br /&gt;
Zum Beweis der Korrektheit des Algorithmus von Kosaraju zeigen wir zwei Implikationen: 1. Wenn die Knoten u und v in der selben stark zusammenhängenden Komponente liegen, werden sie in Schritt 3 des Algorithmus auch der selben Komponente zugewiesen. 2. Wenn die Knoten u und v in Schritt 3 der selben Komponente zugewiesen wurden, müssen sie auch in der selben stark zusammenhängenden Komponente liegen. &lt;br /&gt;
# Knoten u und v gehören zur selben stark zusammenhängenden Komponente von G. Per Definition gilt, dass u von v aus erreichbar ist und umgekehrt. Dies muss auch im transponierten Graphen G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; gelten (der Weg &amp;lt;math&amp;gt;u \rightsquigarrow v&amp;lt;/math&amp;gt; wird jetzt zum Weg &amp;lt;math&amp;gt;v \rightsquigarrow u&amp;lt;/math&amp;gt; und umgekehrt). Wird u bei der Tiefensuche in Schritt 3 vor v expandiert, ist v von u aus erreichbar und gehört somit zur selben Komponente. Das umgekehrte gilt, wenn v vor u expandiert wird. Daraus folgt die Behauptung 1.&lt;br /&gt;
# Knoten u und v werden in Schritt 3 der selben Komponente zugewiesen: Sei x der Anker dieser Komponente. Da u in der gleichen Komponente wie x liegt, muss es in G&amp;lt;sup&amp;gt;T&amp;lt;/sup&amp;gt; einen Weg &amp;lt;math&amp;gt;x \rightsquigarrow u&amp;lt;/math&amp;gt;, und demnach in G einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow x&amp;lt;/math&amp;gt; geben. Da x der Anker seiner Komponente ist, wissen wir aber auch, dass x in der reverse post-order &amp;lt;i&amp;gt;vor&amp;lt;/i&amp;gt; u liegt (denn der Anker ist der Knoten, mit dem eine neue Komponente gestartet wird; er muss deshalb im Array &amp;lt;tt&amp;gt;ordered&amp;lt;/tt&amp;gt; als erster Konten seiner Komponente gefunden worden sein). Wir unterscheiden jetzt im Schritt 1 des Algorithmus zwei Fälle:&lt;br /&gt;
## u wurde bei der Bestimmung der post-order vor x expandiert. Dann kann x nur dann in der reverse post-order &amp;lt;i&amp;gt;vor&amp;lt;/i&amp;gt; u liegen (oder, einfacher ausgedrückt, x kann nur dann in der post-order &amp;lt;i&amp;gt;hinter&amp;lt;/i&amp;gt; u liegen), wenn x im Graphen G nicht von u aus erreichbar war. Das ist aber unmöglich, weil wir ja schon wissen, dass es in G einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow x&amp;lt;/math&amp;gt; gibt.&lt;br /&gt;
## Folglich wurde u bei der Bestimmung der post-order nach x expandiert. Da x in der post-order hinter u liegt, muss u während der Expansion von x erreicht worden sein. Deshalb muss es in G auch einen Weg &amp;lt;math&amp;gt;x \rightsquigarrow u&amp;lt;/math&amp;gt; geben.&lt;br /&gt;
#:Somit sind x und u in der selben stark zusammenhängenden Komponente. Die gleiche Überlegung gilt für x und v. Wegen der Transitivität der relation &amp;quot;ist erreichbar&amp;quot; folgt daraus, dass auch u und v in der selben Komponente liegen, also die Behauptung 2.&lt;br /&gt;
&lt;br /&gt;
Die folgende Skizze illustriert, dass der Komponentengraph stets azyklisch ist. Den Komponentengraph erhält man, indem man für jede Komponente &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; einen Knoten erzeugt (grün), und die Knoten i und j durch eine gerichtete Kante verbindet (rot), wenn es im Originalgraphen eine Kante (u &amp;amp;rarr; v) mit &amp;lt;math&amp;gt;u \in C_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v \in C_j&amp;lt;/math&amp;gt; gibt. Es ist dann garantiert, dass es keine Kante in umgekehrter Richtung geben kann. Daraus folgt insbesondere, dass ein DAG nur triviale stark verbundene Komponenten haben kann, die aus einzelnen Knoten bestehen.&lt;br /&gt;
&lt;br /&gt;
[[Image:strongly-connected-components-graph.png|400px]]&lt;br /&gt;
&lt;br /&gt;
=== Anwendung: Das Erfüllbarkeitsproblem in Implikationengraphen ===&lt;br /&gt;
&lt;br /&gt;
Das Erfüllbarkeitsproblem hat auf den ersten Blick nichts mit Graphen zu tun, denn es geht um Wahrheitswerte logischer Ausdrücke. Man kann logische Ausdrücke jedoch unter bestimmten Bedingungen in eine Graphendarstellung überführen und somit das ursprüngliche Problem auf ein Problem der Graphentheorie reduzieren, für das bereits ein Lösungsverfahren bekannt ist. In diesem Abschnitt wollen wir dies für die sogenannten Implikationengraphen zeigen, ein weiteres Beispiel findet sich im Kapitel [[NP-Vollständigkeit]].&lt;br /&gt;
&lt;br /&gt;
==== Das Erfüllbarkeitsproblem ====&lt;br /&gt;
&lt;br /&gt;
(vgl. [http://de.wikipedia.org/wiki/Erfüllbarkeitsproblem_der_Aussagenlogik WikiPedia (de)])&lt;br /&gt;
&lt;br /&gt;
Das Erfüllbarkeitsproblem (SAT-Problem, von &amp;lt;i&amp;gt;satisfiability&amp;lt;/i&amp;gt;) befasst sich mit logischen (oder Booleschen) Funktionen: Gegeben sei eine Menge &amp;lt;math&amp;gt;\{x_1, ... ,x_n\}&amp;lt;/math&amp;gt; Boolscher Variablen (d.h., die &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; können nur die Werte True oder False annehmen), sowie eine logische Formel, in der die Variablen mit den üblichen logischen Operatoren &lt;br /&gt;
:&amp;lt;math&amp;gt;\neg\quad&amp;lt;/math&amp;gt;: Negation (&amp;quot;nicht&amp;quot;, in Python: &amp;lt;tt&amp;gt;not&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\vee\quad&amp;lt;/math&amp;gt;: Disjunktion (&amp;quot;oder&amp;quot;, in Python: &amp;lt;tt&amp;gt;or&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\wedge\quad&amp;lt;/math&amp;gt;: Konjuktion (&amp;quot;und&amp;quot;, in Python: &amp;lt;tt&amp;gt;and&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\rightarrow\quad&amp;lt;/math&amp;gt;: Implikation (&amp;quot;wenn, dann&amp;quot;, in Python nicht als Operator definiert)&lt;br /&gt;
:&amp;lt;math&amp;gt;\leftrightarrow\quad&amp;lt;/math&amp;gt;: Äquivalenz (&amp;quot;genau dann, wenn&amp;quot;, in Python: &amp;lt;tt&amp;gt;==&amp;lt;/tt&amp;gt;)&lt;br /&gt;
:&amp;lt;math&amp;gt;\neq\quad&amp;lt;/math&amp;gt;: exklusive Disjunktion (&amp;quot;entweder oder&amp;quot;, in Python: &amp;lt;tt&amp;gt;!=&amp;lt;/tt&amp;gt;)&lt;br /&gt;
verknüpft sind. Klammern definieren die Reihenfolge der Auswertung der Operationen. Für jede Belegung der Variablen &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; mit True oder False liefert die Formel den Wert der Funktion, der natürlich auch nur True oder False sein kann. Wenn Formel und Belegung gegeben sind, ist die Auswertung der Funktion ein sehr einfaches Problem: Man transformiert die Formel in einen Parse-Baum (siehe Übungsaufgabe &amp;quot;Taschenrechner) und wertet jeden Knoten mit Hilfe der üblichen Wertetabellen für logische Operatoren aus, die wir hier zur Erinnerung noch einmal angeben:&lt;br /&gt;
{| cellspacing=&amp;quot;0&amp;quot; border=&amp;quot;1&amp;quot;&lt;br /&gt;
|- style=&amp;quot;text-align:center;background-color:#ffffcc;width:50px&amp;quot;&lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \vee b &amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \wedge b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \rightarrow b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;b \rightarrow a&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \leftrightarrow b&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;a \neq b&amp;lt;/math&amp;gt; &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 0 || 1 || 1 || 1 || 0&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 0 || 1 || 0 || 0 || 1&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 0 || 1 || 0 || 1&lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 || 0&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Beim Erfüllbarkeitsproblem wird die Frage umgekehrt gestellt: &lt;br /&gt;
:Gegeben sei eine logische Funktion. Ist es möglich, dass die Funktion jemals den Wert True annimmt? &lt;br /&gt;
Das heisst, kann man die Variablen &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; so mit True oder False belegen, dass die Formel am Ende wahr ist? Im Prinzip kann man diese Frage durch erschöpfende Suche leicht beantworten, indem man die Funktion für alle &amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt; möglichen Belegungen einfach ausrechnet, aber das dauert für große n (ab ca. &amp;lt;math&amp;gt;n\ge 40&amp;lt;/math&amp;gt;) viel zu lange. Erstaunlicherweise ist es aber noch niemanden gelungen, einen Algorithmus zu finden, der für beliebige logische Funktionen schneller funktioniert. Im Gegenteil wurde gezeigt, dass das Erfüllbarkeitsproblem [[NP-Vollständigkeit|NP-vollständig]] ist, so dass wahrscheinlich kein solcher Algorithmus existiert. Trotz (oder gerade wegen) seiner Schwierigkeit hat das Erfüllbarkeitsproblem viele Anwendungen gefunden, vor allem beim Testen logischer Schaltkreise (&amp;quot;Gibt es eine Belegung der Eingänge, so dass am Ausgang der verbotene Wert X entsteht?&amp;quot;) und bei der Planerstellung in der künstlichen Intelligenz (&amp;quot;Kann man ausschließen, dass der generierte Plan Konflikte enthält?&amp;quot;). Es ist außerdem ein beliebtes Modellproblem für die Erforschung neuer Ideen und Algorithmen für schwierige Probleme.&lt;br /&gt;
&lt;br /&gt;
==== Normalformen für logische Ausdrücke ====&lt;br /&gt;
&lt;br /&gt;
Um die Beschreibung von Erfüllbarkeitsproblemen zu vereinfachen und zu vereinheitlichen, hat man verschiedene &amp;lt;i&amp;gt;Normalformen&amp;lt;/i&amp;gt; für logische Ausdrücke eingeführt. Die wichtigste ist die &amp;lt;i&amp;gt;Konjuktionen-Normalform&amp;lt;/i&amp;gt; (CNF - conjunctive normal form). Ein Ausdruck in &amp;lt;i&amp;gt;Konjuktionen-Normalform&amp;lt;/i&amp;gt; ist eine UND-Verknüpfung von M &amp;lt;i&amp;gt;Klauseln&amp;lt;/i&amp;gt;:&lt;br /&gt;
 (CLAUSE&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;) &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; (CLAUSE&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;) &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; ...  &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; (CLAUSE&amp;lt;sub&amp;gt;M&amp;lt;/sub&amp;gt;)&lt;br /&gt;
Jede Klausel ist wiederum ein logischer Ausdruck, der aber sehr einfach sein muss: Er darf nur noch k Variablen enthalten, die nur mit den Operatoren NICHT und ODER verknüpft werden dürfen, z.B.&lt;br /&gt;
  CLAUSE&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; := &amp;lt;math&amp;gt;x_1 \vee \neg x_3 \vee x_8&amp;lt;/math&amp;gt;&lt;br /&gt;
Je nachdem, wie viele Variablen pro Klausel erlaubt sind, spricht man von &amp;lt;b&amp;gt;k-CNF&amp;lt;/b&amp;gt; und entsprechend von einem &amp;lt;b&amp;gt;k-SAT&amp;lt;/b&amp;gt; Problem. Es ist außerdem üblich, die Menge der Variablen und die Menge der negierten Variablen zusammen als Menge der &amp;lt;i&amp;gt;Literale&amp;lt;/i&amp;gt; zu bezeichnen:&lt;br /&gt;
  LITERALS := &amp;lt;math&amp;gt;\{x_1,...,x_n\} \cup \{\neg x_1,...,\neg x_n\}&amp;lt;/math&amp;gt;&lt;br /&gt;
Formal definiert man die &amp;lt;b&amp;gt;k-Konjunktionen-Normalform (k-CNF)&amp;lt;/b&amp;gt; am besten durch eine Grammatik in [http://de.wikipedia.org/wiki/Backus-Naur-Form Backus-Naur-Form]:&lt;br /&gt;
    k_CNF    ::=  CLAUSE | k_CNF &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; CLAUSE&lt;br /&gt;
    CLAUSE   ::= (LITERAL &amp;lt;math&amp;gt;\vee&amp;lt;/math&amp;gt; ... &amp;lt;math&amp;gt;\vee&amp;lt;/math&amp;gt; LITERAL)  # genau k Literale pro Klausel&lt;br /&gt;
    LITERAL  ::=  VARIABLE | &amp;lt;math&amp;gt;\neg&amp;lt;/math&amp;gt;VARIABLE&lt;br /&gt;
    VARIABLE ::=  &amp;lt;math&amp;gt;x_1&amp;lt;/math&amp;gt; | ... | &amp;lt;math&amp;gt;x_n&amp;lt;/math&amp;gt;&lt;br /&gt;
Beispiele:&lt;br /&gt;
* 3-CNF: &amp;lt;math&amp;gt;(x_1 \vee \neg x_2 \vee x_4) \wedge (x_2 \vee x_3 \vee \neg x_4) \wedge (\neg x_1 \vee x_4 \vee \neg x_5)&amp;lt;/math&amp;gt;&lt;br /&gt;
* 2-CNF: &amp;lt;math&amp;gt;(x_1 \vee \neg x_2) \wedge (x_3 \vee x_4)&amp;lt;/math&amp;gt; ...&lt;br /&gt;
&amp;lt;b&amp;gt;Gesucht&amp;lt;/b&amp;gt; ist eine Belegung der Variablen mit True und False, so dass der Ausdruck den Wert True hat. Aus den Eigenschaften der UND- und ODER-Verknüpfungen folgt, dass ein Ausdruck in k-CNF genau dann True ist, wenn jede einzelne Klausel True ist. In jeder Klausel wiederum hat man k Chancen, die Klausel True zu machen, indem man eins der Literale zu True macht. Eventuell werden dadurch aber andere Klauseln wieder zu False, was die Aufgabe so schwierig macht. Die Bedeutung der k-CNF ergibt sich aus folgendem&lt;br /&gt;
;Satz: Jeder logische Ausdruck kann effizient nach 3-CNF transformiert werden, jedoch im allgemeinen nicht nach 2-CNF.&lt;br /&gt;
Man kann sich also auf Algorithmen für 3-SAT-Probleme konzentrieren, ohne dabei an Ausdrucksmächtigkeit zu verlieren. &lt;br /&gt;
&lt;br /&gt;
Leider gilt der entsprechende Satz nicht für k=2: Ausdrücke in 2-CNF sind weit weniger mächtig, weil man in jeder Klausel nur noch zwei Wahlmöglichkeiten hat. Bestimmte logische Ausdrücke sind aber auch nach 2-CNF transformierbar, beispielsweise die Bedingung, dass zwei Literale u und v immer den entgegegesetzten Wert haben müssen. Dies ergibt ein Paar von ODER-Verknüpfungen:&lt;br /&gt;
:&amp;lt;math&amp;gt;(u \leftrightarrow \neg v) \equiv (u \vee \neg v) \wedge (\neg u \vee v)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die 2-CNF hat den Vorteil, dass es effiziente Algorithmen für das 2-SAT-Problem gibt, die wir jetzt kennenlernen wollen. Es zeigt sich, dass man Ausdrücke in 2-CNF als Graphen repräsentieren kann, indem man sie zunächst in die &amp;lt;i&amp;gt;Implikationen-Normalform&amp;lt;/i&amp;gt; (INF für &amp;lt;i&amp;gt;implicative normal form&amp;lt;/i&amp;gt;) überführt. Die Implikationen-Normalform besteht ebenfalls aus einer Menge von Klauseln, die durch UND-Operationen verknüpft sind, aber jede Klausel ist jetzt eine Implikation. &lt;br /&gt;
Die Grammatik der &amp;lt;b&amp;gt;Implikationen-Normalform (INF)&amp;lt;/b&amp;gt; lautet:&lt;br /&gt;
    INF      ::=  CLAUSE | INF &amp;lt;math&amp;gt;\wedge&amp;lt;/math&amp;gt; CLAUSE&lt;br /&gt;
    CLAUSE   ::= (LITERAL &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; LITERAL)  # genau 2 Literale pro Implikation&lt;br /&gt;
    LITERAL  ::=  VARIABLE | &amp;lt;math&amp;gt;\neg&amp;lt;/math&amp;gt;VARIABLE&lt;br /&gt;
    VARIABLE ::=  &amp;lt;math&amp;gt;x_1&amp;lt;/math&amp;gt; | ... | &amp;lt;math&amp;gt;x_n&amp;lt;/math&amp;gt;&lt;br /&gt;
und ein gültiger Ausdruck wäre z.B.&lt;br /&gt;
:&amp;lt;math&amp;gt;(x_1 \to x_2) \wedge (x_2 \to \neg x_3) \wedge (x_4 \to x_3)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die Umwandlung von 2-CNF nach INF beruht auf folgender Äquivalenz, die man sich aus der obigen Wahrheitstabelle leicht herleitet:&lt;br /&gt;
:&amp;lt;math&amp;gt;(x \vee y) \equiv (\neg x \rightarrow y) \equiv (\neg y \rightarrow x)&amp;lt;/math&amp;gt;&lt;br /&gt;
Aus dieser Äquivalenz folgt der &lt;br /&gt;
;Satz: Ein Ausdruck in 2-CNF kann nach INF transformiert werden, indem man jede Klausel &amp;lt;math&amp;gt;(x \vee y)&amp;lt;/math&amp;gt; durch das Klauselpaar &amp;lt;math&amp;gt;(\neg x \rightarrow y) \wedge (\neg y \rightarrow x)&amp;lt;/math&amp;gt; ersetzt.&lt;br /&gt;
Man beachte, dass man für jede ODER-Klausel des ursprünglichen Ausdrucks &amp;lt;i&amp;gt;zwei&amp;lt;/i&amp;gt; Implikationen (eine für jede Richtung des &amp;quot;wenn, dann&amp;quot;) einfügen muss, um die Symmetrie des Problems zu erhalten.&lt;br /&gt;
&lt;br /&gt;
==== Lösung des 2-SAT-Problems mit Implikationgraphen ====&lt;br /&gt;
&lt;br /&gt;
Jeder Ausdruck in INF kann als gerichteter Graph dargestellt werden:&lt;br /&gt;
# Für jedes Literal wird ein Knoten in den Graphen eingefügt. Es gibt also für jede Variable und für ihre Negation jeweils einen Knoten, d.h. 2n Knoten insgesamt.&lt;br /&gt;
# Jede Implikation ist eine gerichtete Kante.&lt;br /&gt;
Implikationengraphen eignen sich, um Ursache-Folge-Beziehungen oder Konflikte zwischen Aktionen auszudrücken. Beispielsweise kann man die Klausel &amp;lt;math&amp;gt;(x \rightarrow \neg y)&amp;lt;/math&amp;gt; als &amp;quot;wenn man x tut, darf man y nicht tun&amp;quot; interpretieren. Ein anderes schönes Beispiel findet sich in Übung 12.&lt;br /&gt;
&lt;br /&gt;
Für die Implementation eines Implikationengraphen in Python empfiehlt es sich, die Knoten geschickt zu numerieren: Ist die Variable &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; dem Knoten i zugeordnet, so sollte die negierte Variable &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; dem Knoten (i+n) zugeordnet werden. Zu jedem gegebenen Knoten i findet man dann den negierten Partnerknoten j leicht durch die Formel &amp;lt;tt&amp;gt;j = (i + n ) % (2*n)&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die Aufgabe besteht jetzt darin, folgende Fragen zu beantworten:&lt;br /&gt;
# Ist der durch den Implikationengraphen gegebene Ausdruck erfüllbar?&lt;br /&gt;
# Finde eine geeignete Belegung der Variablen, wenn der Ausduck erfüllbar ist.&lt;br /&gt;
Die erste Frage beantwortet man leicht, indem man die stark zusammenhängenden Komponenten des Implikationengraphen bildet. Dann gilt folgender&lt;br /&gt;
;Satz: Seien u und v zwei Literale, die sich in der selben stark zusammenhängenden Komponente befinden. Dann müssen u und v stets den selben Wert haben, damit der Ausdruck erfüllt sein kann.&lt;br /&gt;
Die Korrektheit des Satzes folgt aus der Definition der stark zusammenhängenden Komponenten: Da u und v in der selben Komponente liegen, gibt es im Implikationengraphen einen Weg &amp;lt;math&amp;gt;u \rightsquigarrow v&amp;lt;/math&amp;gt; sowie einen Weg &amp;lt;math&amp;gt;v \rightsquigarrow u&amp;lt;/math&amp;gt;. Wegen der Transitivität der &amp;quot;wenn, dann&amp;quot; Relation kann man die Wege zu zwei Implikationen verkürzen, die gleichzeitig gelten müssen: &amp;lt;math&amp;gt;(u \rightarrow v) \wedge (v \rightarrow u)&amp;lt;/math&amp;gt; (die Verkürzung von Wegen zu direkten Kanten entspricht gerade der Bildung der transitiven Hülle für die Knoten u und v). In der obigen Wertetabelle für logische Operatoren erkennt mann, dass dies äquivalent zur Bedingung &amp;lt;math&amp;gt;(u \leftrightarrow v)&amp;lt;/math&amp;gt; ist. Dies ist aber gerade die Behauptung des Satzes.&lt;br /&gt;
&lt;br /&gt;
Die Erfüllbarkeit des Ausdrucks ist nun ein einfacher Spezialfall dieses Satzes. &lt;br /&gt;
;Korrolar: Der gegebene Ausdruck ist genau dann erfüllbar, wenn die Literale &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; sich für kein i in derselben stark zusammenhängenden Komponente befinden.&lt;br /&gt;
Setzt man nämlich im Satz &amp;lt;math&amp;gt;u = x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v = \neg x_i&amp;lt;/math&amp;gt;, und beide Knoten befinden sich in der selben Komponente, dann müsste gelten &amp;lt;math&amp;gt;x_i \leftrightarrow\neg x_i&amp;lt;/math&amp;gt;, was offensichtlich ein Widerspruch ist. Damit kann der Ausdruck nicht erfüllbar sein. Umgekehrt gilt, dass der Ausdruck immer erfüllbar ist, wenn &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\neg x_i&amp;lt;/math&amp;gt; stets in verschiedenen Komponenten liegen, weil der folgende Algorithmus von Aspvall, Plass und Tarjan in diesem Fall stets eine gültige Belegung aller Variablen liefert:&lt;br /&gt;
# Bestimme die stark zusammenhängenden Komponenten und bilde den Komponentengraphen. Ordne die Knoten des Komponentengraphen (also die stark zusammenhängenden Komponenten des Originalgraphen) in topologische Sortierung an.&lt;br /&gt;
# Betrachte die Komponenten in der topologischen Sortierung von hinten nach vorn und weise ihnen einen Wert nach folgenden Regeln zu (zur Erinnerung: alle Literale in der selben Komponente haben den selben Wert):&lt;br /&gt;
#* Wenn die Komponente noch nicht betrachtet wurde, setze ihren Wert auf True, und den Wert der komplementären Komponente (derjenigen, die die negierten Literale enthält) auf False.&lt;br /&gt;
#* Andernfalls, gehe zur nächsten Komponente weiter.&lt;br /&gt;
Der Algorithmus beruht auf der Symmetrie des Implikationengraphen: Weil Kanten immer paarweise &amp;lt;math&amp;gt;(\neg u \rightarrow v) \wedge (\neg v \rightarrow u)&amp;lt;/math&amp;gt; eingefügt werden, ist der Graph &amp;lt;i&amp;gt;schiefsymmetrisch&amp;lt;/i&amp;gt; (skew symmetric): die eine Hälfte das Graphen ist die transponierte Spiegelung der anderen Hälfte. Enthält eine stark zusammenhängende Komponente &amp;lt;math&amp;gt;C_i&amp;lt;/math&amp;gt; die Knoten &amp;lt;tt&amp;gt;i1, i2, ...&amp;lt;/tt&amp;gt;, so gibt es stets eine komplementäre Komponente &amp;lt;math&amp;gt;C_j = \neg C_i&amp;lt;/math&amp;gt;, die die komplementären Knoten &amp;lt;tt&amp;gt;j1 = (i1 + n) % (2*n), j2 = (i2 + n) % (2*n), ...&amp;lt;/tt&amp;gt; enthält. Gilt &amp;lt;math&amp;gt;C_i = \neg C_i&amp;lt;/math&amp;gt; für irgendein i, so ist der Ausdruck nicht erfüllbar. Den Beweis für die Korrektheit des Algorithmus findet man im [http://www.math.ucsd.edu/~sbuss/CourseWeb/Math268_2007WS/2SAT.pdf Originalartikel]. Leider funktioniert dies nicht für k-SAT-Probleme mit &amp;lt;math&amp;gt;k &amp;gt; 2&amp;lt;/math&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Will man nur die Erfüllbarkeit prüfen, vereinfacht sich der Algorithmus zu:&lt;br /&gt;
# Bestimme die stark zusammenhängenden Komponenten.&lt;br /&gt;
# Teste für alle &amp;lt;tt&amp;gt;i = 0,...,n-1&amp;lt;/tt&amp;gt;, dass Knoten &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; und Knoten &amp;lt;tt&amp;gt;(i+n)&amp;lt;/tt&amp;gt; in unterschiedlichen Komponenten liegen.&lt;br /&gt;
Ist der Ausdruck erfüllbar, kann man eine gültige Belegung der Variablen jetzt mit dem randomisierten Algorithmus bestimmen, den wir im Kapitel [[Randomisierte Algorithmen]] behandeln.&lt;br /&gt;
&lt;br /&gt;
== Weitere wichtige Graphenalgorithmen ==&lt;br /&gt;
&lt;br /&gt;
Eins der wichtigsten Einsatzgebiete für Graphen ist die Optimierung, also die Suche nach der &amp;lt;i&amp;gt;besten&amp;lt;/i&amp;gt; Lösung für ein gegebenes Problem:&lt;br /&gt;
* Das &amp;lt;i&amp;gt;interval scheduling&amp;lt;/i&amp;gt; befasst sich damit, aus einer gegebenen Menge von Aufträgen die richtigen auszuwählen und sie geschickt auf die zur Verfügung stehenden Ressourcen aufzuteilen. Damit beschäftigen wir uns im Kapitel [[Greedy-Algorithmen und Dynamische Programmierung]].&lt;br /&gt;
* Beim Problem des Handlungsreisenden sucht man nach der kürzesten Rundreise, die alle gegebenen Städte genau einmal besucht. Dieses Problem behandeln wir im Kapitel [[NP-Vollständigkeit]].&lt;br /&gt;
* Viele weitere Anwendungen können wir leider in der Vorlesung nicht mehr behandeln, z.B.&lt;br /&gt;
** Algorithmen für den [http://en.wikipedia.org/wiki/Maximum_flow_problem maximalen Fluss] beantworten die Frage, wie man die Durchflussmenge durch ein Netzwerk (z.B. von Ölpipelines) maximiert.&lt;br /&gt;
** Beim [http://en.wikipedia.org/wiki/Assignment_problem Problem der optimalen Paarung] (&amp;quot;matching problem&amp;quot; oder &amp;quot;assignment problem&amp;quot;) sucht man nach einer Teilmenge der Kanten (also nach einem Teilgraphen), so dass jeder Knoten in diesem Teilgraphen höchstens den Grad 1 hat. Im neuen Graphen gruppieren die Kanten also je zwei Knoten zu einem Paar, und die Paarung soll nach jeweils anwendungsspezifischen Kriterien optimal sein. Dies benötigt man z.B. bei der optimalen Zuordnung von Gruppen, etwas beim Arbeitsamt (Zuordnung Arbeitssuchender - Stellenangebot) und in der Universität (Zuordnung Studenten - Übungsgruppen).&lt;br /&gt;
** In Statistik und maschinellem Lernen haben in den letzten Jahren die [http://en.wikipedia.org/wiki/Graphical_model graphischen Modelle] große Bedeutung erlangt.&lt;br /&gt;
* usw. usf.&lt;br /&gt;
&lt;br /&gt;
[[Randomisierte Algorithmen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Main_Page&amp;diff=5402</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Main_Page&amp;diff=5402"/>
				<updated>2012-07-25T13:18:48Z</updated>
		
		<summary type="html">&lt;p&gt;Ukoethe: /* Klausur und Nachprüfung */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Vorlesung Algorithmen und Datenstrukturen ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Dr. Ullrich Köthe, Universität Heidelberg, Sommersemester 2012&lt;br /&gt;
&lt;br /&gt;
Die Vorlesung findet '''dienstags''' und '''donnerstags''' jeweils um 14:15 Uhr in INF 227 (KIP), HS 2 statt. &lt;br /&gt;
&lt;br /&gt;
=== Klausur und Nachprüfung ===&lt;br /&gt;
&lt;br /&gt;
Die '''Abschlussklausur''' findet am Dienstag, dem 31.7.2012 von 10:00 bis 12:00 Uhr im HS 1 in INF 306 statt. Zur Klausur wird zugelassen, wer mindestens 50% der Übungspunkte erreicht. (Hinweis: Sie benötigen einen Lichtbildausweis, um sich bei der Klausur zu indentifizieren!) Falls notwendig, wird eine Nachklausur kurz vor Beginn des neuen Semesters stattfinden, näheres wird noch bekanntgegeben.&lt;br /&gt;
&amp;lt;!---&lt;br /&gt;
* '''[[Media:Prüfungsteilnehmer.pdf|Liste der Studenten]], die sich verbindlich zur Klausur angemeldet und die notwendige Übungspunktzahl erreicht haben.'''&lt;br /&gt;
* '''[[Media:Ergebnis-Klausur-23-07-2008.pdf|Ergebnis der Klausur vom 23.7.2008]]''' (anonymisiert)&lt;br /&gt;
* '''Scheine''' können ab 1.9.2008 im Sekretariat Informatik bei Frau Tenschert abgeholt werden.&lt;br /&gt;
* Die '''Wiederholungsklausur''' findet am 1.10.2008 um 9:00 Uhr im Seminarraum des [http://hci.iwr.uni-heidelberg.de/contact.php HCI, Speyerer Str. 4], statt.&lt;br /&gt;
* '''[[Media:Ergebnis-Klausur-01-10-2008.pdf|Ergebnis der Wiederholungsklausur vom 1.10.2008]]''' (anonymisiert)&lt;br /&gt;
---&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Leistungsnachweise ===&lt;br /&gt;
Für alle Leistungsnachweise ist die erfolgreiche Teilnahme an den Übungen erforderlich. Für Leistungspunkte bzw. den Klausurschein muss außerdem die schriftliche Prüfung bestanden werden. Einzelheiten werden noch bekanntgegeben.&lt;br /&gt;
&amp;lt;!-------&lt;br /&gt;
Im einzelnen können erworben werden:&lt;br /&gt;
* ein unbenoteter Übungsschein, falls jemand nicht an der Klausur teilnimmt bzw. die Klausur nicht bestanden wurde.&lt;br /&gt;
* ein benoteter Übungsschein (Magister mit Computerlinguistik im ''Nebenfach'', Physik Diplom)&lt;br /&gt;
* ein Klausurschein (Magister mit Computerlinguistik im ''Hauptfach'')&lt;br /&gt;
* ein Leistungsnachweis über 9 Leistungspunkte (B.A. Computerlinguistik - alte Studienordnung) &lt;br /&gt;
* ein Leistungsnachweis über 8 Leistungspunkte (B.Sc. Informatik, B.A. Computerlinguistik - neue Studienordnung) &lt;br /&gt;
* ein Leistungsnachweis über 7 Leistungspunkte (B.Sc. Physik).&lt;br /&gt;
---------&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Übungsbetrieb ===&lt;br /&gt;
* Termine und Räume: &lt;br /&gt;
** Mo 14:00 - 16:00 Uhr, INF 227 (KIP), Seminarraum 2.402 (Tutor: Sven Ebser [mailto:sven@ebsers.de sven AT ebsers.de])&lt;br /&gt;
** Di  9:00 - 11:00 Uhr, INF 227 (KIP), Seminarraum 2.403 (Tutor: Christoph Koke [mailto:koke@kip.uni-heidelberg.de koke AT kip.uni-heidelberg.de])&lt;br /&gt;
** Di 11:00 - 13:00 Uhr, INF 227 (KIP), Seminarraum 2.403 (Tutor: Kai Karius [mailto:kai.karius@googlemail.com kai.karius AT googlemail.com])&lt;br /&gt;
** Mi 14:00 - 16:00 Uhr, INF 227 (KIP), Seminarraum 2.401 (Tutor: Stephan Meister [mailto:stephan.meister@iwr.uni-heidelberg.de stephan.meister AT iwr.uni-heidelberg.de])  &lt;br /&gt;
* Die Übungsgruppen werden über &amp;lt;b&amp;gt;[https://www.mathi.uni-heidelberg.de/muesli/lecture/view/169 MÜSLI]&amp;lt;/b&amp;gt; verwaltet. Dort erfolgt auch die &amp;lt;b&amp;gt;Anmeldung&amp;lt;/b&amp;gt;.&lt;br /&gt;
&amp;lt;!------&lt;br /&gt;
* [[Media:Punktestand.pdf|aktueller Punktestand]] (PDF, anonymisiert, so aktuell, wie von den Tutoren an mich übermittelt -- UK)&lt;br /&gt;
-------&amp;gt;&lt;br /&gt;
* &amp;lt;b&amp;gt;[[Main Page#Übungsaufgaben|Übungsaufgaben]]&amp;lt;/b&amp;gt; (Übungszettel mit Abgabetermin, Musterlösungen). Lösungen bitte per Email an den jeweiligen Übungsgruppenleiter.&lt;br /&gt;
* Zur Klausur wird zugelassen, wer mindestens 50% der Übungspunkte erreicht. Außerdem muss jeder Teilnehmer eine Lösung (bzw. einen Teil davon) in der Übungsgruppe vorrechnen. &lt;br /&gt;
* Durch das Lösen von Bonusaufgaben und gute Mitarbeit in den Übungen können Sie Zusatzpunkte erlangen. Zusatzpunkte werden auch vergeben, wenn Sie größere Verbesserungen an diesem Wiki vornehmen. Damit solche Verbesserungen der richtigen Person zugeordnet werden, sollten Sie dafür ein eigenes Wiki-Login verwenden, das Ihnen Stephan Meister oder Ullrich Köthe auf Anfrage gerne einrichten.&lt;br /&gt;
&lt;br /&gt;
=== Prüfungsvorbereitung ===&lt;br /&gt;
&lt;br /&gt;
Zur Hilfe bei der Prüfungsvorbereitung hat Andreas Fay [http://de.neemoy.com/quizcategories/31/ Quizfragen] erstellt.&lt;br /&gt;
&lt;br /&gt;
=== Literatur ===&lt;br /&gt;
&lt;br /&gt;
* R. Sedgewick: Algorithmen (empfohlen für den ersten Teil, bis einschließlich Graphenalgorithmen)&lt;br /&gt;
* J. Kleinberg, E.Tardos: Algorithm Design (empfohlen für den zweiten Teil, einschließlich Graphenalgorithmen)&lt;br /&gt;
* T. Cormen, C. Leiserson, R.Rivest: Algorithmen - eine Einführung (empfohlen zum Thema Komplexität)&lt;br /&gt;
* Wikipedia und andere Internetseiten (sehr gute Seiten über viele Algorithmen und Datenstrukturen)&lt;br /&gt;
&lt;br /&gt;
=== Gliederung der Vorlesung ===&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Einführung]] (17.4.2012) &lt;br /&gt;
#* Definition von Algorithmen und Datenstrukturen, Geschichte&lt;br /&gt;
#* Fundamentale Algorithmen: create, assign, copy, swap, compare etc.&lt;br /&gt;
#* Fundamentale Datenstrukturen: Zahlen, Container, Handles&lt;br /&gt;
#* Python-Grundlagen&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Container]] (19.4.2012)&lt;br /&gt;
#* Anforderungen von Algorithmen an Container&lt;br /&gt;
#* Einteilung der Container&lt;br /&gt;
#* Grundlegende Container: Array, verkettete Liste, Stack und Queue&lt;br /&gt;
#* Sequenzen und Intervalle (Ranges)&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Sortieren]] (24. und 26.4.2012)&lt;br /&gt;
#* Spezifikation des Sortierproblems&lt;br /&gt;
#* Selection Sort und Insertion Sort&lt;br /&gt;
#* Merge Sort&lt;br /&gt;
#* Quick Sort und seine Varianten&lt;br /&gt;
#* Vergleich der Anzahl der benötigten Schritte&lt;br /&gt;
#* Laufzeitmessung in Python&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Korrektheit]] (3. und 8.5.2012)&lt;br /&gt;
#* Definition von Korrektheit, Algorithmen-Spezifikation&lt;br /&gt;
#* Korrektheitsbeweise versus Testen&lt;br /&gt;
#* Vor- und Nachbedingungen, Invarianten, Programming by contract&lt;br /&gt;
#* Testen, Execution paths, Unit Tests in Python&lt;br /&gt;
#* Ausnahmen (exceptions) und Ausnahmebehandlung in Python&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Effizienz]] (10. und 15.5.2012)&lt;br /&gt;
#* Laufzeit und Optimierung: Innere Schleife, Caches, locality of reference&lt;br /&gt;
#* Laufzeit versus Komplexität&lt;br /&gt;
#* Landausymbole (O-Notation, &amp;lt;math&amp;gt;\Omega&amp;lt;/math&amp;gt;-Notation, &amp;lt;math&amp;gt;\Theta&amp;lt;/math&amp;gt;-Notation), Komplexitätsklassen&lt;br /&gt;
#* Bester, schlechtester, durchschnittlicher Fall&lt;br /&gt;
#* Amortisierte Komplexität&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Suchen]] (22. und 24.5.2012)&lt;br /&gt;
#* Sequentielle Suche&lt;br /&gt;
#* Binäre Suche in sortierten Arrays, Medianproblem&lt;br /&gt;
#* Suchbäume, balancierte Bäume&lt;br /&gt;
#* selbst-balancierende Bäume, Rotationen&lt;br /&gt;
#* Komplexität der Suche&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Sortieren in linearer Zeit]] (29.5.2012)&lt;br /&gt;
#* Permutationen&lt;br /&gt;
#* Sortieren als Suchproblem&lt;br /&gt;
#* Bucket Prinzip, Bucket Sort&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Prioritätswarteschlangen]] (31.5.2012)&lt;br /&gt;
#* Heap-Datenstruktur&lt;br /&gt;
#* Einfüge- und Löschoperationen&lt;br /&gt;
#* Heapsort&lt;br /&gt;
#* Komplexität des Heaps&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Assoziative Arrays]] (5.6.2012)&lt;br /&gt;
#* Datenstruktur-Dreieck für assoziative Arrays&lt;br /&gt;
#* Definition des abstrakten Datentyps&lt;br /&gt;
#* JSON-Datenformat&lt;br /&gt;
#* Realisierung durch sequentielle Suche und durch Suchbäume&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Hashing und Hashtabellen]] (5.6.und 12.6.2012)&lt;br /&gt;
#* Implementation assoziativer Arrays mit Bäumen&lt;br /&gt;
#* Hashing und Hashfunktionen&lt;br /&gt;
#* Implementation assoziativer Arrays als Hashtabelle mit linearer Verkettung bzw. mit offener Adressierung&lt;br /&gt;
#* Anwendung des Hashing zur String-Suche: Rabin-Karp-Algorithmus&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Iteration versus Rekursion]] (14.6.2012)&lt;br /&gt;
#* Typen der Rekursion und ihre Umwandlung in Iteration&lt;br /&gt;
#* Auflösung rekursiver Formeln mittels Master-Methode und Substitutionsmethode&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Generizität]] (19.6.2012)&lt;br /&gt;
#* Abstrakte Datentypen, Typspezifikation&lt;br /&gt;
#* Required Interface versus Offered Interface&lt;br /&gt;
#* Adapter und Typattribute, Funktoren&lt;br /&gt;
#* Beispiel: Algebraische Konzepte und Zahlendatentypen&lt;br /&gt;
#* Operator overloading in Python&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Graphen und Graphenalgorithmen]] (21.6. bis 5.7.2012)&lt;br /&gt;
#* Einführung&lt;br /&gt;
#* Graphendatenstrukturen, Adjazenzlisten und Adjazenzmatrizen&lt;br /&gt;
#* Gerichtete und ungerichtete Graphen&lt;br /&gt;
#* Vollständige Graphen&lt;br /&gt;
#* Planare Graphen, duale Graphen&lt;br /&gt;
#* Pfade, Zyklen&lt;br /&gt;
#* Tiefensuche und Breitensuche&lt;br /&gt;
#* Zusammenhang, Komponenten&lt;br /&gt;
#* Gewichtete Graphen&lt;br /&gt;
#* Minimaler Spannbaum&lt;br /&gt;
#* Kürzeste Wege, Best-first search (Dijkstra)&lt;br /&gt;
#* Most-Promising-first search (A*)&lt;br /&gt;
#* Problem des Handlungsreisenden, exakte Algorithmen (erschöpfende Suche, Branch-and-Bound-Methode) und Approximationen&lt;br /&gt;
#* Erfüllbarkeitsproblem, Darstellung des 2-SAT-Problems durch gerichtete Graphen, stark zusammenhängende Komponenten&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Repetition---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Orthogonale Zerlegung des Problems---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Hierarchische Zerlegung der Daten (Divide and Conquer)---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Randomisierung---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Optimierung, Zielfunktionen---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Systematisierung von Algorithmen aus der bisherigen Vorlesung---&amp;gt;&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
&amp;lt;!---# [[Analytische Optimierung]] (25.6.2008)---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Methode der kleinsten Quadrate---&amp;gt;&lt;br /&gt;
&amp;lt;!---#* Approximation von Geraden---&amp;gt;&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Randomisierte Algorithmen]] (10. und 12.7.2012)&lt;br /&gt;
#* Zufallszahlen, Zyklenlänge, Pitfalls&lt;br /&gt;
#* Zufallszahlengeneratoren: linear congruential generator, Mersenne Twister&lt;br /&gt;
#* Randomisierte vs. deterministische Algorithmen&lt;br /&gt;
#* Las Vegas vs. Monte Carlo Algorithmen&lt;br /&gt;
#* Beispiel für Las Vegas: Randomisiertes Quicksort&lt;br /&gt;
#* Beispiele für Monte Carlo: Randomisierte Lösung des k-SAT Problems &lt;br /&gt;
#* RANSAC-Algorithmus, Erfolgswahrscheinlichkeit, Vergleich mit analytischer Optimierung (Methode der kleinsten Quadrate)&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Greedy-Algorithmen und Dynamische Programmierung]] (17.7.2012)&lt;br /&gt;
#* Prinzipien, Aufwandsreduktion in Entscheidungsbäumen&lt;br /&gt;
#* bereits bekannte Algorithmen: minimale Spannbäume nach Kruskal, kürzeste Wege nach Dijkstra&lt;br /&gt;
#* Beispiel: Interval Scheduling Problem und Weighted Interval Scheduling Problem&lt;br /&gt;
#* Beweis der Optimalität beim Scheduling Problem: &amp;quot;greedy stays ahead&amp;quot;-Prinzip, Directed Acyclic Graph bei dynamischer Programmierung&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[NP-Vollständigkeit]] (19.7.2012)&lt;br /&gt;
#* die Klassen P und NP&lt;br /&gt;
#* NP-Vollständigkeit und Problemreduktion&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# Reserve und/oder Wiederholung (24. und 26.7.2012)&lt;br /&gt;
&lt;br /&gt;
== Übungsaufgaben ==&lt;br /&gt;
&lt;br /&gt;
(im PDF Format). Die Abgabe erfolgt am angegebenen Tag bis 14:00 Uhr per Email an den jeweiligen Übungsgruppenleiter. Bei Abgabe bis zum folgenden Montag 11:00 Uhr werden noch 50% der erreichten Punkte angerechnet. Danach wird die Musterlösung freigeschaltet. Erreichbare Punkte (ohne Bonusaufgaben): 466.&lt;br /&gt;
&lt;br /&gt;
# [[Media:Übung-1.pdf|Übung]] (Abgabe 24.4.2012) und [[Media:Uebung-1-Musterloesung.pdf|Musterlösung]]&lt;br /&gt;
#* Python-Tutorial&lt;br /&gt;
#* Sieb des Eratosthenes&lt;br /&gt;
#* Wert- und Referenzsemantik&lt;br /&gt;
#* Dynamisches Array&lt;br /&gt;
# [[Media:Uebung-2.pdf|Übung]] (Abgabe 3.5.2012) und [[Media:Uebung-2-Musterloesung.pdf|Musterlösung]]&lt;br /&gt;
#* Sortieren: Implementation und Geschwindigkeitsvergleich (Diagramme in Abhängigkeit von der Problemgröße)&lt;br /&gt;
#* Entwicklung eines Gewinnalgorithmus für ein Spiel&lt;br /&gt;
#* Bonus: Dynamisches Array mit verringertem Speicherverbrauch&lt;br /&gt;
# [[Media:Uebung-3.pdf|Übung]] (Abgabe 10.5.2012) und [[Media:Uebung-3-Musterlösung.pdf|Musterlösung]]&lt;br /&gt;
#* Experimente zur Effektivität von Unit Tests&lt;br /&gt;
#* Bestimmung von Pi mit dem Algorithmus von Archimedes&lt;br /&gt;
#* Deque-Datenstruktur: Vor- und Nachbedingungen der Operationen, Implementation und Unit Tests&lt;br /&gt;
# [[Media:Uebung-4.pdf|Übung]] (Abgabe '''Montag''' 21.5.2012) und [[Media:muster_blatt4.pdf|Musterlösung]]&lt;br /&gt;
#* Theoretische Aufgaben zur Komplexität&lt;br /&gt;
#* Amortisierte Komplexität von array.append()&lt;br /&gt;
#* Optimierung der Matrizenmultiplikation&lt;br /&gt;
# [[Media:Uebung-5.pdf|Übung]] (31.5.2012) und [[Media:muster_blatt5.pdf|Musterlösung]]&lt;br /&gt;
#* Implementation und Analyse eines Binärbaumes&lt;br /&gt;
#* Anwendung: einfacher Taschenrechner&lt;br /&gt;
# [[Media:Uebung-6.pdf|Übung]] (Abgabe '''Freitag''' 8.6.2012) und [[Media:muster_blatt6.pdf|Musterlösung]]&lt;br /&gt;
#* Treap-Datenstruktur: Verbindung von Suchbaum und Heap&lt;br /&gt;
#* Anwendung: Worthäufigkeiten (Dazu benötigen Sie das File  [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/die-drei-musketiere.txt die-drei-musketiere.txt]. Die Zeichenkodierung in diesem File ist Latin-1.)&lt;br /&gt;
#* BucketSort&lt;br /&gt;
# [[Media:Uebung-7.pdf|Übung]] (Abgabe 14.6.2012) und [[Media:muster_blatt07.pdf|Musterlösung]]&lt;br /&gt;
#* Absichtliche Konstruktion von Kollisionen für eine Hashfunktion&lt;br /&gt;
#* Übungen zum Assoziativen Array und zum JSON-Format: Cocktail-Datenbank (Dazu benötigen Sie das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/cocktails.json cocktails.json]. Die Zeichenkodierung in diesem File ist UTF-8.)&lt;br /&gt;
# [[Media:Uebung-8.pdf|Übung]] (Abgabe 21.6.2012) und [[Media:muster_blatt8.pdf|Musterlösung]]&lt;br /&gt;
#* Übungen zu Rekursion und Iteration: Fibonaccizahlen, Koch-Schneeflocke, Komplexität rekursiver Algorithmen, Umwandlung von Rekursion in Iteration&lt;br /&gt;
# [[Media:Uebung-9.pdf|Übung]] (Abgabe 28.6.2012) und [[Media:muster_blatt9.pdf|Musterlösung]]&lt;br /&gt;
#* Planare Graphen: Aufstellen von Adjazenzmatrizen und Adjazenzlisten, obere Schranke für die Zahl der Kanten&lt;br /&gt;
#* Übungen zur Generizität: Sortieren mit veränderter Ordnung, Iterator für Tiefensuche&lt;br /&gt;
# [[Media:Uebung-10.pdf|Übung]] (Abgabe 5.7.2012) und [[Media:muster_blatt10.pdf|Musterlösung]]&lt;br /&gt;
#* Fortgeschrittene Graphenaufgaben: Erzeugen einer perfekten Hashfunktion, Routenplaner (Dazu benötigen Sie das File  [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/entfernungen.json entfernungen.json]. Die Zeichenkodierung in diesem File ist UTF-8.)&lt;br /&gt;
# [[Media:Uebung-11.pdf|Übung]] (Abgabe 12.7.2012) und [[Media:muster_blatt11.pdf|Musterlösung]] sowie schöne [[Media:ballungsgebiete.pdf|Visualisierung der Ballungsgebiete]] von Thorben Kröger&lt;br /&gt;
#* Fortgeschrittene Graphenaufgaben 2: Clusterung mittels minimaler Spannbäume, Bildverarbeitung mit Graphen (Dazu benötigen Sie wieder das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/entfernungen.json entfernungen.json] sowie die Files [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/cells.pgm cells.pgm] und [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/pgm.py pgm.py].)&lt;br /&gt;
# [[Media:Uebung-12.pdf|Übung]] (Abgabe 19.7.2012)&lt;br /&gt;
#* Erfüllbarkeitsproblem, Anwendung: Heim- und Auswärtsspiele im Fussball (Dazu benötigen sie das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/bundesliga-paarungen-12-13.json bundesliga-paarungen-12-13.json].)&lt;br /&gt;
#* Randomisierte Algorithmen: RANSAC für Kreise (Dazu benötigen sie das File [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/noisy-circles.txt noisy-circles.txt].)&lt;br /&gt;
# [[Media:Bonusuebung.pdf|Übung (Bonus)]] (&amp;lt;font color=red&amp;gt;Achtung: Abgabe bereits am Dienstag, 24.7.2012&amp;lt;/font&amp;gt;)&lt;br /&gt;
#* Greedy-Algorithmus&lt;br /&gt;
#* Weg durch einen Graphen&lt;br /&gt;
#* Wiederholungsaufgaben für die Klausur&lt;br /&gt;
&lt;br /&gt;
== Sonstiges ==&lt;br /&gt;
* [[Gnuplot| Gnuplot Kurztutorial]]&lt;br /&gt;
* [[Git Kurztutorial]]&lt;br /&gt;
* [[neue Startseite|mögliche neue Startseite]]&lt;/div&gt;</summary>
		<author><name>Ukoethe</name></author>	</entry>

	</feed>