<?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=Alda</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=Alda"/>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php/Special:Contributions/Alda"/>
		<updated>2026-05-08T17:06:17Z</updated>
		<subtitle>User contributions</subtitle>
		<generator>MediaWiki 1.30.0</generator>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Einf%C3%BChrung&amp;diff=5720</id>
		<title>Einführung</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Einf%C3%BChrung&amp;diff=5720"/>
				<updated>2021-02-11T16:02:19Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Zur Frage der elementaren Schritte */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Definition von Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
Es gibt viele Definitionen von Algorithmen. Hier sind die Ergebnisse einer Google-Suche auf  [http://www.google.de/search?hl=de&amp;amp;defl=en&amp;amp;q=define:Algorithm&amp;amp;sa=X&amp;amp;oi=glossary_definition&amp;amp;ct=title englisch] und auf&lt;br /&gt;
[http://www.google.de/search?hl=de&amp;amp;defl=de&amp;amp;q=define:Algorithmus&amp;amp;sa=X&amp;amp;oi=glossary_definition&amp;amp;ct=title deutsch]. Die Grundidee ist aber immer gleich:&lt;br /&gt;
&lt;br /&gt;
Ein '''Algorithmus''' ist eine Problemlösung durch endlich viele elementare Schritte. Die Teile der Definition bedürfen näherer Erläuterung:&lt;br /&gt;
&lt;br /&gt;
;Problemlösung: Damit ein Algorithmus ein Problem (genauer: eine Menge von gleichartigen Problemen) lösen kann, muss das Problem zunächst definiert (''spezifiziert'') werden. Die '''Spezifikation''' legt fest, ''was'' der Algorithmus erreichen soll, sagt aber nichts über das ''wie''. Die Spezifikation beschreibt somit relevante Eigenschaften des Systemzustands ''vor'' und ''nach'' der Ausführung des Algorithmus (sogenannte '''Vor-''' und '''Nachbedingungen'''), während der Algorithmus einen bestimmten ''Lösungsweg'' repräsentiert. Mit Hilfe der Spezifikation kann getestet werden, ob der Algorithmus tatsächlich eine Lösung des gestellten Problems liefert. Diese Frage untersuchen wir im Kapitel [[Korrektheit]].&lt;br /&gt;
;Endlich viele Schritte: Die Forderung nach endlich vielen Schritten unterstellt, dass jeder einzelne Schritt eine gewisse Zeit benötigt, also nicht unendlich schnell ausgeführt werden kann. Damit ist diese Forderung äquivalent zu der Forderung, dass der Algorithmus in endlicher Zeit zum Ergebnis kommen muss. Der Sinn einer solchen Forderung leuchtet aus praktischer Sicht unmittelbar ein. Interessant ist darüber hinaus die Frage, wie man mit möglichst wenigen Schritten, also möglichst schnell, zur Lösung kommt. Diese Frage untersuchen wir im Kapitel [[Effizienz]].&lt;br /&gt;
;Elementare Schritte: Im weiteren Sinne verstehen wir unter einem elementaren Schritt ein Teilproblem, für das bereits ein Algorithmus bekannt ist. Im engeren Sinne ist die Menge der elementaren Schritte durch die Hilfsmittel vorgegeben, mit denen der Algorithmus ausgeführt werden soll, also z.B. durch die Hardware oder die Programmiersprache. Wir gehen darauf im nächsten Abschnitt näher ein.&lt;br /&gt;
&lt;br /&gt;
=== Zur Frage der elementaren Schritte ===&lt;br /&gt;
&lt;br /&gt;
Welche Schritte als elementar angesehen werden können, hängt sehr stark vom Kontext der Aufgabe und den Hilfsmitteln zu ihrer Lösung ab. Ein interessantes Beispiel ist die Geometrie der alten Griechen, wo geometrische Probleme in der Ebene allein mit Zirkel und Lineal gelöst werden. In diesem Fall sind folgende elementare Operationen erlaubt:&lt;br /&gt;
* das Markieren eines Punktes (beliebig in der Ebene oder als Schnittpunkt zwischen bereits gezeichneten Linien),&lt;br /&gt;
* das Zeichnen einer Geraden durch zwei Punkte,&lt;br /&gt;
* das Zeichnen eines Kreises um einen Punkt,&lt;br /&gt;
* das Abgreifen des Abstands zwischen zwei Punkten mit dem Zirkel.&lt;br /&gt;
Auf der Basis dieser Operationen kann zum Beispiel kein Algorithmus für die Dreiteilung eines beliebigen Winkels definiert werden, während der Algorithmus für die Zweiteilung sehr einfach ist. &lt;br /&gt;
&lt;br /&gt;
Eine völlig andere Menge von elementaren Operationen ergibt sich für arithmetische Berechnungen mit Hilfe des Abacus (Rechenbrett), der seit der Römerzeit in Europa weit verbreitet war. Hier werden Zahlen durch die Positionen von Perlen auf Rillen oder Drähten dargestellt und Berechnungen durch deren Verschiebung. Eine ausführliche Beschreibung der wichtigsten Abacus-Algorithmen findet sich unter [http://totton.idirect.com/abacus/ The Bead Unbaffled] von Totton Heffelfinger und Gary Flom.&lt;br /&gt;
&lt;br /&gt;
Die moderne Auffassung von elementaren Operationen wird durch die Berechenbarkeitstheorie (ein Teilgebiet der theoretischen Informatik) bestimmt. Verschiedene Mathematiker (darunter die Pioniere Alan Turing, Alonso Church, Kurt Gödel, Stephen Kleene und Emil Post) haben seit den 1930er Jahren versucht, den intuitiven Begriff der Berechenbarkeit einer Funktion zu formalisieren und sind dabei zu völlig verschiedenen Lösungen gelangt (z.B. Turingmaschine, Lambda-Kalkül, μ-Rekursion und WHILE-Programm). Interessanterweise stellte sich heraus, dass diese Lösungen alle die gleiche Mächtigkeit haben: Obwohl die elementaren Operationen jeweils ganz anders definiert sind, ist die Menge der damit berechenbaren Funktionen immer gleich. Die [http://en.wikipedia.org/wiki/Church_thesis Church-Turing-These] besagt, dass es prinzipiell unmöglich ist, eine mächtigere Definition von elementaren Operationen zu finden, aber dies ist unbewiesen. Am bequemsten für die Praxis sind die  [http://de.wikipedia.org/wiki/WHILE-Programm WHILE-Programme], da sie sich direkt auf die heute gebräuchliche Hardware-Architektur abbilden lassen. Die elementaren Operationen eines WHILE-Programms lauten in erweiterter Backus-Naur Notation:&lt;br /&gt;
 P ::= x[i] = x[j] + c            # Addition einer Konstanten zur Variable x[i]&lt;br /&gt;
     | x[i] = x[j] - c            # Subtraktion einer Konstanten von x[i]&lt;br /&gt;
     | P; P                       # Nacheinanderausführung von zwei Anweisungen&lt;br /&gt;
     | WHILE x[i] != 0 DO P DONE  # Wiederholte Ausführung der Anweisung(en) P &lt;br /&gt;
                                  # (x[i] muss sich innerhalb von P ändern, um eine Endlosschleife zu vermeiden)&lt;br /&gt;
wobei &amp;lt;tt&amp;gt;c&amp;lt;/tt&amp;gt; eine beliebige ganzahlige Konstante (eine ausgeschriebene ganze Zahl) und &amp;lt;tt&amp;gt;x[i]&amp;lt;/tt&amp;gt; die Speicherzelle &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; bezeichnen. Alle Speicherzellen können ganze Zahlen aufnehmen und sind anfangs mit Null belegt. Darüber hinaus wird vorausgesetzt, dass mindestens soviele Speicherzellen vorhanden sind, wie der gegebene Algorithmus benötigt, und jede Speicherzelle groß genug ist, um die größte auftretende Zahl aufzunehmen. Beide Annahmen sind in der Praxis nicht immer erfüllt.&lt;br /&gt;
&lt;br /&gt;
In einem WHILE-Programm gibt es keine elementare Funktion, um die Summe von zwei &amp;lt;i&amp;gt;Variablen&amp;lt;/i&amp;gt; zu berechnen. Diese Operation muss man bereits als Algorithmus implementieren. Der folgende Code berechnet die Summe unter der Voraussetzung, dass &amp;lt;tt&amp;gt;x[j]&amp;lt;/tt&amp;gt; nicht negativ ist, indem &amp;lt;tt&amp;gt;x[j]&amp;lt;/tt&amp;gt; solange dekrementiert (um 1 erniedrigt) wird, bis es den Wert 0 annimmt, und &amp;lt;tt&amp;gt;x[i]&amp;lt;/tt&amp;gt; entsprechend bei jedem Schritt inkrementiert (um 1 erhöht) wird. Die alten Werte der Variablen gehen bei der Berechnung verloren:&lt;br /&gt;
 Algorithmus: x[i] = x[i] + x[j] als WHILE-Programm (Vorbedingung: x[j] &amp;gt;= 0)&lt;br /&gt;
     WHILE x[j] != 0 DO&lt;br /&gt;
         x[i] = x[i] + 1;&lt;br /&gt;
         x[j] = x[j] - 1&lt;br /&gt;
     DONE&lt;br /&gt;
Man erkennt, dass tatsächlich nur die vier elementaren Operationen (Addition/Subtraktion einer Konstanten, Nacheinanderausführung von Anweisungen, WHILE-Schleife) vorkommen. Allerdings ist dieser Algorithmus sehr langsam. Außerdem ist die Zerlegung in Form eines WHILE-Programms (oder eines äquivalenten Formalismus der Berechenbarkeitstheorie) für unsere Zwecke zu feinkörnig: Sie würde bedeuten, dass alle Algorithmen auf einem extrem einfachen Prozessor in Assembler programmiert werden müssten. Bereits eine so einfache Operation wie die Summe von zwei Variablen erfordert vier Codezeilen! &lt;br /&gt;
&lt;br /&gt;
Deshalb definiert man ''höhere Programmiersprachen'', die wichtige Algorithmen wie z.B. die arithmetischen Operationen mit ganzen Zahlen und Gleitkomma-Zahlen bereits als elementare Operationen enthalten. Weitere nicht ganz so wichtige Funktionen wie die Wurzel oder der Logarithmus werden in Programmbibliotheken angeboten, die standardmäßig mitgeliefert werden. In der Praxis betrachtet man eine Operation deshalb als elementar, wenn sie von einer typischen Programmiersprache oder einer typischen Standardbibliothek unterstützt wird. In dieser Vorlesung wählen wir die Operationen und Bibliotheken der Programmiersprache [http://www.python.org Python]. Wenn ein Algorithmus Anforderungen stellt, die nicht selbstverständlich sind, müssen sie als ''Requirements'' explizit angegeben werden. Wir werden darauf im Kapitel [[Generizität]] zurückkommen.&lt;br /&gt;
&lt;br /&gt;
=== Zur Geschichte ===&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|-valign=&amp;quot;top&amp;quot; &lt;br /&gt;
| Algorithmen wurden bereits im Altertum verwendet. Besonders die alten Griechen haben Pionierarbeit geleistet, z.B. auf dem Gebiet der Arithmetik (Euklidischer Algorithmus für den größten gemeinsamen Teiler von zwei Zahlen, Sieb des Eratosthenes zur Bestimmung von Primzahlen) und der Geometrie (Teilung einer Strecke oder eines Winkels nur mit Zirkel und Lineal). Der Begriff ''Algorithmus'' ist vom Namen des arabischen Gelehrten Muhammed Al Chwarizmi (ca. 783-850) abgeleitet, der in seinem Werk „Über das Rechnen mit indischen Ziffern“ (um 825) grundlegende Verfahren für das Rechnen im dekadischen Positionssystem beschrieben hat. Im 12. Jahrhundert wurde dieses Buch ins Lateinische übersetzt, und die Einleitung begann mit den Worten „Dixit Algorismi“ (Al Chwarizmi hat gesagt). Ab etwa 1200 wurden die neuen Rechenmethoden als „Algorismus de integris“ bzw. „Algorismus vulgaris“ (Rechnen mit ganzen Zahlen, d.h. Grundrechenarten und Wurzelziehen) sowie „Algorismus de minutiis“ (Bruchrechnung) zum festen Bestandteil der mathematischen Ausbildung im Rahmen der sieben freien Künste. Dabei diente der Begriff Algorithmus ursprünglich vor allem zur Abgrenzung des schriftlichen Rechnens mit indischen/arabischen Zahlen (wie wir es noch heute in der Schule lernen) vom traditionellen mechanischen Rechnen mit Abacus und römischen Zahlen, das noch bis ca. 1500 in Europa vorherrschend blieb. &lt;br /&gt;
&lt;br /&gt;
Die allgemeinere Bedeutung des Wortes Algorithmus als systematische Rechenvorschrift war jedoch ebenfalls schon früh gebräuchlich. Dies zeigt zum Beispiel der Titel des Buches „Algorismus proportionum“ (Rechenkunst mit Proportionen, ca. 1350) von Nicole Oresme, wo erstmals die Rechenregeln für Potenzen mit rationalen Exponenten beschrieben werden. Durch die steigenden Anforderungen des kaufmännischen Rechnens und der Navigation verbreitete sich die algorithmische Denkweise ab etwa 1500 rasch. Der Buchdruck machte mit Werken wie Adam Ries' „Rechenung auff der linihen und federn“ (d.h. mit Abacus und mit indischen/arabischen Zahlen, zuerst 1522) die grundlegenden Rechenalgorithmen einem breiten Bevölkerungskreis bekannt. Umfangreiche gedruckte Tafelwerke, z.B. der „Canon“ von G.J. Rhaeticus (1551) mit bis zu siebenstelligen Tabellen der trigonometrischen Funktionen, erlaubten es, komplizierte Berechnungen auf einfache Schritte (Addition, Subtraktion sowie Nachschlagen in der Tabelle) zurückzuführen. Unsere heutige Verwendung des Begriffs geht wohl auf Alonso Church's Aufsatz „An Unsolvable Problem of Elementary Number Theory“ (1936) zurück, wo die Berechenbarkeit einer Funktion mit der Existenz eines terminierenden Berechnungsalgorithmus gleichgesetzt wird.&lt;br /&gt;
| [[Image:Al-Khwarizmi.jpg]] &amp;lt;br&amp;gt; Al Chwarizmi-Denkmal in Teheran&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Definition von Datenstrukturen ==&lt;br /&gt;
&lt;br /&gt;
=== Beispiele für Datenformate ===&lt;br /&gt;
&lt;br /&gt;
Der Speicher eines Computers enthält eine Folge von Zeichen aus einem gegebenen Alphabet. Bei fast allen heutigen Computern ist dies eine Folge von Bits aus dem Alphabet {0,1}. Ein '''Datenformat''' ordnet eine Bitfolge in Gruppen und gibt jeder Gruppe eine Bedeutung. Der Gruppierungsprozess kann dann hierarchisch fortgesetzt werden.&lt;br /&gt;
&lt;br /&gt;
Die selben Bits können somit völlig verschiedene Bedeutungen annehmen, ja nachdem in welchem Datenformat sie sich befinden. Man betrachte z.B. die Folge von 16 Bits:&lt;br /&gt;
 1101011001101100&lt;br /&gt;
Wenn wir diese Folge als eine zusammengehörende Gruppe betrachten und als positive ganze Zahl in Binärdarstellung interpretieren (unsigned integer, &amp;lt;tt&amp;gt;uint16&amp;lt;/tt&amp;gt;), ergibt sich die Dezimalzahl &lt;br /&gt;
 54892 = 1*2&amp;lt;sup&amp;gt;15&amp;lt;/sup&amp;gt; + ... + 1*2&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt; + 1*2&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; + 0*2&amp;lt;sup&amp;gt;1&amp;lt;/sup&amp;gt; + 0*2&amp;lt;sup&amp;gt;0&amp;lt;/sup&amp;gt;&lt;br /&gt;
Interpretieren wir dieselbe Gruppe als vorzeichenbehaftete ganze Zahl in [http://de.wikipedia.org/wiki/Zweierkomplement Zweierkomplement]-Darstellung (signed integer, &amp;lt;tt&amp;gt;int16&amp;lt;/tt&amp;gt;), ergibt sich eine andere Dezimalzahl: Da das linke (höchstwertige) Bit Eins ist, handelt es sich um eine negative Zahl. Das Zweierkomplement erhält man durch Negieren aller Bits und nachfolgende Addition von 1:&lt;br /&gt;
 Zweierkomplement von 1101011001101100:&lt;br /&gt;
                      0010100110010011 + 1 = 0010100110010100&lt;br /&gt;
Die resultierende Dezimalzahl ist somit&lt;br /&gt;
 -10644 = -(0*2&amp;lt;sup&amp;gt;15&amp;lt;/sup&amp;gt; + ... + 0*2&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt; + 1*2&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; + 0*2&amp;lt;sup&amp;gt;1&amp;lt;/sup&amp;gt; + 0*2&amp;lt;sup&amp;gt;0&amp;lt;/sup&amp;gt;)&lt;br /&gt;
Alternativ können wir die Folge in zwei Gruppen zu 8 Bit gruppieren, und die Gruppen als Zeichencodes im Windows-Zeichensatz interpretieren. Wir erhalten die Zeichenkette &amp;quot;Öl&amp;quot;:&lt;br /&gt;
 11010110  01101100 = char[214] char[108] =&amp;gt; Öl&lt;br /&gt;
Eine weitere Interpretation ist diejenige als 16-Bit Gleitkommazahl (&amp;lt;tt&amp;gt;float16&amp;lt;/tt&amp;gt;) gemäß [http://en.wikipedia.org/wiki/IEEE_floating-point_standard IEEE Standard 754]. Dabei wird die Folge in Gruppen zu 1 Bit, 5 Bit und 10 Bit eingeteilt:&lt;br /&gt;
 1  10101  1001101100&lt;br /&gt;
Die Gruppen werden als nicht-negative Binärzahlen gelesen, wobei die erste Gruppe das Vorzeichen &amp;lt;tt&amp;gt;s&amp;lt;/tt&amp;gt; der Gleitkommazahl ist (0 bedeutet &amp;quot;+&amp;quot;, 1 bedeutet &amp;quot;-&amp;quot;), die zweite ist ihr Exponent &amp;lt;tt&amp;gt;exp&amp;lt;/tt&amp;gt; und die dritte die Mantisse &amp;lt;tt&amp;gt;m&amp;lt;/tt&amp;gt;. In unserem Beispiel gilt &amp;lt;tt&amp;gt;s = 1&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;exp = 21&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;m = 620&amp;lt;/tt&amp;gt;). Die Umrechnung in eine Gleitkommazahl erfolgt, gemäß IEEE Standard, nach folgender Formel:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;tt&amp;gt;z = (1 - 2*s) * 2&amp;lt;sup&amp;gt;exp-15&amp;lt;/sup&amp;gt; * (1 + m * 2&amp;lt;sup&amp;gt;-10&amp;lt;/sup&amp;gt;)&amp;lt;/tt&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
In Dezimaldarstellung ist dies &amp;lt;tt&amp;gt;-102.75&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Das analoge Beispiel für eine Folge von 32 Bits ist vielleicht realistischer, weil 32-bit Zahlen (integer und float) in der Praxis häufiger vorkommen. Wir betrachten die Bitfolge:&lt;br /&gt;
 11111100011000100110010101101110&lt;br /&gt;
Als positive ganze Zahl in Binärdarstellung (unsigned integer, &amp;lt;tt&amp;gt;uint32&amp;lt;/tt&amp;gt;) ergibt sich die Dezimalzahl 4234306926. Dieselben Bits als vorzeichenbehaftete ganze Zahl in Zweierkomplement-Darstellung (signed integer, &amp;lt;tt&amp;gt;int32&amp;lt;/tt&amp;gt;) ergiben die Dezimalzahl -60660370. Als Zeichenfolge (vier Gruppen zu 8 Bit) bekommen wir die Zeichenkette &amp;quot;üben&amp;quot;. Eine weitere mögliche Interpretation ist diejenige als Farbe im RGBA System (8 Bit pro Farbkanal, 8 Bit Transparenzwert), und wir erhalten ein halbtransparentes Rosa (Rot: 252, Grün: 98, Blau: 101, Alpha: 110).&amp;lt;br&amp;gt;&lt;br /&gt;
Eine 32-Bit Gleitkommazahl (&amp;lt;tt&amp;gt;float32&amp;lt;/tt&amp;gt;) ist gemäß IEEE Standard 754 definiert durch Gruppen zu 1 Bit für das Vorzeichen, 8 Bit für den Exponenten und 23 Bit für die Mantisse, d.h:&lt;br /&gt;
 1 11111000 11000100110010101101110&lt;br /&gt;
Hier gilt also &amp;lt;tt&amp;gt;s = 1&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;exp = 248&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;m = 6448494&amp;lt;/tt&amp;gt;). Die Umrechnung in eine Gleitkommazahl erfolgt jetzt nach der Formel:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;tt&amp;gt;z = (1 - 2*s) * 2&amp;lt;sup&amp;gt;exp-127&amp;lt;/sup&amp;gt; * (1 + m * 2&amp;lt;sup&amp;gt;-23&amp;lt;/sup&amp;gt;)&amp;lt;/tt&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
In Dezimaldarstellung ist dies rund &amp;lt;tt&amp;gt;-4.7020653*10&amp;lt;sup&amp;gt;36&amp;lt;/sup&amp;gt;&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Im Sinne einer hierarchischen Gruppierung können wir jetzt z.B. eine Datenstruktur &amp;quot;Farbbild&amp;quot; definieren, indem wir viele RGBA-Werte zu einem 2-dimensionalen Array zusammenfassen. Eine Datenstruktur &amp;quot;komplexe Zahl&amp;quot; wird durch ein geordnetes Paar von Gleitkommazahlen gebildet, eine &amp;quot;Meßreihe&amp;quot; als Liste von ganzen Zahlen oder Gleitkommawerten (je nach Art der Messung), usw.&lt;br /&gt;
&lt;br /&gt;
=== Varianten der Datenstrukturdefinition ===&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|-valign=&amp;quot;bottom&amp;quot; &lt;br /&gt;
| Bei den Beispielen im vorigen Abschnitt habe wir das Speicherlayout und die Bedeutung der einzelnen Bits bzw. Bit-Gruppen festgelegt. Wir bezeichnen eine auf diese Weise definierte Datenstruktur als &amp;lt;b&amp;gt;Datenformat&amp;lt;/b&amp;gt;. Datenformate werden vor allem verwendet, um Datenstrukturen auf Festplatte oder in einer Datenbank zu speichern und Daten über ein Netzwerk auszutauschen (vgl. den Eintrag [http://de.wikipedia.org/wiki/Dateitypen Dateityp] in der WikiPedia). Aus Sicht des Betriebssystems ist ein File einfach eine Folge von Bits, deren Bedeutung aus anderen Informationen geschlossen werden muss, z.B. aus der Endung des Filenames (.jpg, .png, .xml usw.) oder aus dem mit dem File assoziierten [http://de.wikipedia.org/wiki/Internet_Media_Type MIME-Type]. Viele Fileformate beginnen zudem mit bestimmten Bitfolgen (&amp;quot;[http://de.wikipedia.org/wiki/Magische_Zahl_%28Informatik%29 magischen Zahlen]&amp;quot;), die für das betreffende Fileformat charakteristisch sind. Jedes JPEG-File beginnt z.B. mit dem Bytemuster &amp;lt;tt&amp;gt;255 216 255&amp;lt;/tt&amp;gt;, jedes PNG-File mit der Folge &amp;lt;tt&amp;gt;137 80 78 71&amp;lt;/tt&amp;gt;, jedes XML-File mit dem String &amp;lt;tt&amp;gt;&amp;quot;&amp;amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot; ?&amp;amp;gt;&amp;lt;/tt&amp;gt; (wobei Versionsnummer und Zeichensatzdefinition natürlich verschieden sein können, je nach Fileinhalt). Wann immer möglich sollte man bei der Verwendung von Datenformaten auf vorhandene Standards (wie z.B. IEEE 754 für Gleitkommazahlen oder XML für hierarchisch strukturierte Dokumente) zurückgreifen, weil sonst beim Einlesen und Interpretieren der gespeicherten Bitfolgen sehr leicht Fehler passieren.&lt;br /&gt;
&lt;br /&gt;
Innerhalb einer Programmiersprache werden Datenstrukturen typischerweise nicht als Datenformate definiert, sondern durch die Verknüpfung eines Speicherlayouts mit einer &amp;lt;i&amp;gt;Menge erlaubter Operationen&amp;lt;/i&amp;gt; auf diesen Daten. Die Interpretation ergibt sich implizit aus der Definition dieser Operationen. Verwendet man beispielsweise eine Folge von 32 Bits zusammen mit den arithmetischen Operationen für natürliche Zahlen (inklusive der zugehörigen Vor- und Nachbedingungen), ist die Interpretation als &amp;lt;tt&amp;gt;uint32&amp;lt;/tt&amp;gt; dadurch gegeben. Eine Folge von Bytes mit den Operationen &amp;lt;tt&amp;gt;print&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;toLowerCase&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;toUpperCase&amp;lt;/tt&amp;gt; usw. weist auf die Interpretation &amp;quot;Zeichenkette&amp;quot; (&amp;lt;tt&amp;gt;string&amp;lt;/tt&amp;gt;). Eine solche Verknüpfung von Datenrepräsentation mit Operationen bezeichnen wir als '''(Daten-)Typ''' oder '''Klasse'''. Klassen sind für den Programmierer das wichtigste Mittel, um eigene Datenstrukturen zu definieren, und wir werden in der Vorlesung ausführlich darauf eingehen.&lt;br /&gt;
&lt;br /&gt;
Die dritte Möglichkeit ist schließlich die Kombination einer Interpretation mit einer Menge erlaubter Operationen, ohne ein bestimmtes Speicherlayout oder eine konkrete Implementation der Operationen festzulegen. In diesem Fall sprechen wir von '''Abstrakten Datentypen''' (ADTs). Diese spielen beim Entwurf von anwendungsübergreifenden Programmierschnittstellen und bei der theoretischen Analyse von Algorithmen und Datenstrukturen eine wichtige Rolle. Da von den Besonderheiten einer bestimmten Implementation und eines bestimmten Computers abstrahiert wird, sind die gewonnen Erkenntnisse auf viele Anwendungen übertragbar. Konzepte, die als abstrakte Datentypen definiert sind, können je nach Kontext immer wieder anders implementiert werden, ohne dass die übergreifenden (abstrakten) Eigenschaften verloren gehen. Viele der konkreten Datenstrukturen, die wir behandeln werden, kann man zu abstrakten Datenstrukturen verallgemeinern. Dies ist eine Schlüsselaufgabe beim Entwurf wiederverwendbarer Programmbibliotheken. Wir kommen im Kapitel [[Generizität]] auf ADTs zurück.&lt;br /&gt;
&lt;br /&gt;
Man kann sich die drei Möglichkeiten &amp;quot;Speicherlayout&amp;quot;, &amp;quot;Bedeutung&amp;quot; und &amp;quot;Menge der darauf ausführbaren Operatoren&amp;quot; als Ecken eines Dreiecks wie in der nebenstehenden Skizze vorstellen. Definiert man zwei Ecken des Dreiecks, ist auch die dritte weitgehend (oder zumindest zu einem gewissen Grade, wie bei ADTs) festgelegt. Die drei Kanten entsprechen den drei Arten der Datenstrukturen: Legt man &amp;quot;Speicherlayout&amp;quot; und &amp;quot;Bedeutung&amp;quot; fest, erhalten wir ein Datenformat, bei &amp;quot;Speicherlayout&amp;quot; plus &amp;quot;Operatoren&amp;quot; einen Klasse bzw. einen Typ, und aus &amp;quot;Operatoren&amp;quot; plus &amp;quot;Bedeutung&amp;quot; folgt ein abstrakter Datentyp.&lt;br /&gt;
| [[Image:Dt dreieck.png|400px]] &amp;lt;br&amp;gt; &amp;lt;center&amp;gt;Datenstruktur-Dreieck&amp;lt;/center&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Wichtige Begriffe ===&lt;br /&gt;
&lt;br /&gt;
Programmiersprachen, die ausgereifte Mechanismen zur Definition von Klassen bieten, werden als ''objekt-orientiert'' bezeichnet. Sprachen heißen ''streng typisiert'', wenn der Compiler bzw. Interpreter der Sprache sicherstellt, dass auf jeder Datenstruktur nur die jeweils explizit erlaubten Operationen ausgeführt werden (jeder Versuch, eine illegale Operation auszuführen, wird hier als Fehler signalisiert). Erfolgt diese Prüfung während der Compilierung (also während der Übersetzung des Quellcodes in eine Maschinensprache), spricht man von einer ''statisch typisierten Sprache''. Wird die Prüfung hingegen während der Ausführung des Programms durchgeführt, handelt es sich um eine ''dynamisch typisierte Sprache''. Python ist eine dynamisch-typisierte, objekt-orientierte Sprache. Streng typisiert ist sie allerdings nur für die vordefinierten Klassen. Bei benutzerdefinierten Klassen gibt es (wie bei den meisten anderen Programmiersprachen auch) Möglichkeiten, die erlaubten Operationen zu umgehen. Dies sollte man allerdings nur dann tun, wenn es einen wichtigen Grund gibt. Solange man sich nämlich auf die erlaubten Operationen beschränkt, ist eine große Menge von Fehlerquellen von vornherein ausgeschlossen. &lt;br /&gt;
&lt;br /&gt;
Ein bestimmter Speicherbereich, der den Anforderungen an eine Klasse genügt (wo also die Bits in entsprechender Weise gruppiert und interpretiert werden), wird als '''Objekt''' dieser Klasse oder als '''Instanz''' bezeichnet. Jede Instanz hat eine eindeutige Identität, einen ''Schlüssel''. Innerhalb eines Programms wird dafür gewöhnlich die Speicheradresse des ersten Bytes der Instanz (also der Index der ersten Speicherzelle) verwendet. Dies ist besonders effizient, weil die Speicheradresse für jedes Objekt eindeutig und leicht feststellbar ist. Ist das Objekt hingegen als Datei gespeichert, benötigt man einen expliziten Schlüssel, z.B. den Dateinamen oder die URL. &lt;br /&gt;
&lt;br /&gt;
Das Bitmuster selbst bzw. die daraus folgende Interpretation wird als '''Zustand''' oder '''Wert''' der Instanz bezeichnet. Daraus folgt, dass verschiedene Instanzen einer Klasse dennoch gleiche Werte haben können. Die Menge aller legalen Werte bilden den ''Wertebereich'' der Klasse. Werden Instanzen ausschließlich mit den explizit erlaubten Operationen ihrer Klasse manipuliert, können niemals illegale Werte entstehen. Es liegt auf der Hand, dass illegale Werte schwerwiegende Programmfehler darstellen, die man auf diese Weise vermeidet. [Computerviren tun genau das Gegenteil: Sie verwenden absichtlich verbotene Operationen, um das Programm in einen illegalen, vom Angreifer gewünschten Zustand zu bringen. Dies ist möglich, weil nicht alle verbotenen Operationen automatisch als Fehler erkannt werden, siehe oben.]&lt;br /&gt;
&lt;br /&gt;
Die meisten Programmiersprachen haben einen oder mehrere spezielle Typen für das Speichern von Objektschlüsseln. Die gebräuchlichsten Namen für diese Typen sind ''Zeiger'' (pointer), ''Referenz'' (reference) und ''Handle''. Wir verwenden das Wort '''Referenz'''. Ein Objekt der Klasse Referenz enthält also den Schlüssel eines anderen Objekts. Man sagt, dass die Referenz ''auf das andere Objekt verweist''. Diese Art der Indirektion ist uns heutzutage durch das Internet bestens vertraut: Jede WWW-Seite ist ein Objekt, und seine URL ist der dazugehörige Schlüssel. Hyperlinks und Lesezeichen (bookmarks) hingegen sind Referenzen, die mittels der URL auf andere Seiten verweisen.&lt;br /&gt;
&lt;br /&gt;
Aus der Unterscheidung von Werten und Referenzen ergibt sich die wichtige Unterscheidung von ''Wertsemantik'' und ''Referenzsemantik''. Wird nämlich ein Objekt an eine Variable zugewiesen&lt;br /&gt;
 x = anObject&lt;br /&gt;
so hängt die korrekte Verwendung der Variablen &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; davon ab, ob sie das Objekt in Form eines Wertes oder einer Referenz speichert. Im ersten Fall wird das Objekt selbst kopiert, und es entsteht ein neues Objekt mit neuer Identität, aber gleichem Zustand. Im anderen Fall wird nur der Schlüssel kopiert, und die Referenz verweist nach wie vor auf das ursprüngliche Objekt. Ist &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; ein Wert, so verändert eine Manipulation von &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; nur das neue Objekt (das ursprüngliche bleibt erhalten). Ist &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; hingegen eine Referenz, wird immer das ürsprüngliche Objekt manipuliert (denn es gibt ja keine Kopie). Ob eine Variable einen Wert oder eine Referenz enthält, wird in jeder Programmiersprache anderes festgelegt. In Python gilt&lt;br /&gt;
* Zahlen (Typen &amp;lt;tt&amp;gt;bool&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;int&amp;lt;/tt&amp;gt;, und &amp;lt;tt&amp;gt;float&amp;lt;/tt&amp;gt;) werden immer als Werte gespeichert und kopiert.&lt;br /&gt;
* Alle anderen Typen werden als Referenzen gespeichert und kopiert.&lt;br /&gt;
* Für alle Typen kann Wertsemantik mit Hilfe des Python-Moduls [http://docs.python.org/lib/module-copy.html copy] erzwungen werden.&lt;br /&gt;
Das Verständnis von Werten und Referenzen wird in der 1. Übung vertieft.&lt;br /&gt;
&lt;br /&gt;
Der Entwurf von Datentypen bzw. Klassen wird uns im Laufe der Vorlesung immer wieder beschäftigen.&lt;br /&gt;
&lt;br /&gt;
== Fundamentale Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
Einige Algorithmen werden praktisch bei jeder Klasse benötigt, unabhängig vom eigentlichem Verwendungszweck der Klasse. Es ist wichtig, diese fundamentalen Algorithmen zu kennen. Außerdem eignen sie sich gut zur Einführung der Grundprinzipien der Algorithmen-Spezifikation mittels Vor- und Nachbedingungen. Diese Bedingungen beschreiben Eigenschaften, die die Variablen des Systems ''vor'' bzw. ''nach'' der Ausführung des Algorithmus haben sollen. Damit man außerdem die Veränderungen durch den Algorithmus beschreiben kann, führt man zu jeder Variablen (z.B. &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;) eine Hilfsvariable (z.B. &amp;lt;tt&amp;gt;x&amp;lt;sub&amp;gt;o&amp;lt;/sub&amp;gt;&amp;lt;/tt&amp;gt;, sprich &amp;quot;x-old&amp;quot;) ein. In den Hilfsvariablen wird der Zustand ''vor'' der Ausführung des Algorithmus gespeichert, so dass man diesen noch abfragen kann, wenn Variablen durch den Algorithmus verändert werden. Wenn der Algorithmus beispielsweise die Variable &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; inkrementiert (um eins erhöht), gilt die Nachbedingung &amp;lt;tt&amp;gt;x == x&amp;lt;sub&amp;gt;o&amp;lt;/sub&amp;gt; + 1&amp;lt;/tt&amp;gt; (darin ist &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; der neue, und &amp;lt;tt&amp;gt;x&amp;lt;sub&amp;gt;o&amp;lt;/sub&amp;gt;&amp;lt;/tt&amp;gt; der alte Wert der Variablen). Falls &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; hingegen nicht verändert wird, gilt &amp;lt;tt&amp;gt;x == x&amp;lt;sub&amp;gt;o&amp;lt;/sub&amp;gt;&amp;lt;/tt&amp;gt;. (Man beachte, dass dies in der Literatur nicht einheitlich gehandhabt wird -- einige Autoren verwenden z.B. &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; für den Zustand vor Ausführung des Algorithmus, und &amp;lt;tt&amp;gt;x'&amp;lt;/tt&amp;gt; für denjenigen danach. Diese Syntax ist jedoch mit den meisten Programmiersprachen inkompatibel.)&lt;br /&gt;
&lt;br /&gt;
Die wichtigste Gruppe von fundamentalen Funktionen sind die '''Konstruktoren''', die einen vorher unbenutzten Speicherbereich in eine Datenstruktur mit einem wohldefinierten Anfangswert transformieren. In Python haben die Konstruktoren im allgemeinen den gleichen Namen wie die dazugehörige Klasse, also z.B.&lt;br /&gt;
 i = int()   # erzeuge eine ganze Zahl mit Anfangswert 0&lt;br /&gt;
 f = float() # erzeuge eine Gleitkommazahl mit Anfangswert 0&lt;br /&gt;
 a = list()  # erzeuge ein leeres Array&lt;br /&gt;
usw. (Man beachte, dass das Python-Array den Klassennamen &amp;lt;tt&amp;gt;list&amp;lt;/tt&amp;gt; hat. Dies hat nichts mit verketteten Listen zu tun.) Konstruktoren ohne Argumente bezeichnet man als ''Standard-Konstruktoren'' (default constructors). Ja nach Typ gibt es meist noch weitere Konstruktoren, die Objekte mit anderen Anfangswerten erzeugen, z.B.&lt;br /&gt;
 i = int(2)     # erzeuge eine ganze Zahl mit Anfangswert 2&lt;br /&gt;
 i = 2          # ebenso (abgekürzte Schreibweise)&lt;br /&gt;
 f = float(1.5) # erzeuge eine Gleitkommazahl mit Anfangswert 1.5&lt;br /&gt;
 f = 1.5        # ebenso (abgekürzte Schreibweise)&lt;br /&gt;
 a = [i, f]     # erzeuge ein Array mit Kopien der Werte von i und f&lt;br /&gt;
(Das Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; enthält Kopien der Werte, weil Zahlen immer mit Wertsemantik zugewiesen werden.) Die allgemeine Spezifikation eines Standard-Konstruktors lautet&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
\mathrm{Precondition: } &amp;amp; T \in \mathrm{Types}\\&lt;br /&gt;
\mathrm{Constructor: } &amp;amp; t = T() \\&lt;br /&gt;
\mathrm{Postcondition: } &amp;amp; t \in T&lt;br /&gt;
\end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Der Ausdruck &amp;lt;math&amp;gt;t \in T&amp;lt;/math&amp;gt; besagt, dass t nach Ausführung des Konstruktors eine legale Instanz des Typs T (oder eine Referenz auf einen solche Instanz) sein muss. In Pythonsyntax kann dies folgendermassen geschrieben werden&lt;br /&gt;
 import inspect           # wir brauchen das inspect-Modul&lt;br /&gt;
 &lt;br /&gt;
 if inspect.isclass(T):   # prüfe, dass T ein Type ist&lt;br /&gt;
      t = T()&lt;br /&gt;
 assert isinstance(t, T)&lt;br /&gt;
Natürlich funktioniert der Code nur, wenn die Klasse &amp;lt;tt&amp;gt;T&amp;lt;/tt&amp;gt; tatsächlich existiert und dafür ein Standardkonstruktor definiert wurde. Das Gegenstück zu Konstruktoren sind die '''Destruktoren''', die den Speicher der Datenstruktur wieder frei geben. Da Python automatisches Speichermanagment unterstützt, werden die Destruktoren automatisch aufgerufen. Wir können sie deshalb hier übergehen.&lt;br /&gt;
&lt;br /&gt;
Sehr wichtig sind auch die '''Vergleichsoperatoren'''. Wir müssen dabei unterscheiden, ob auf Gleichheit der Referenzen (''identity'') oder auf Gleichkeit der Werte (''equality'') geprüft werden soll. In Python werden dazu die Operatoren &amp;lt;tt&amp;gt;is&amp;lt;/tt&amp;gt; bzw. &amp;lt;tt&amp;gt;==&amp;lt;/tt&amp;gt; verwendet. Die Negation erhält man durch &amp;lt;tt&amp;gt;is not&amp;lt;/tt&amp;gt; bzw. &lt;br /&gt;
&amp;lt;tt&amp;gt;!=&amp;lt;/tt&amp;gt;&lt;br /&gt;
 a = [1, 2]&lt;br /&gt;
 b = [1, 2]&lt;br /&gt;
 &lt;br /&gt;
 a == b      # True  weil gleiche Werte&lt;br /&gt;
 a != b      # False weil Negation&lt;br /&gt;
 a is b      # False weil unterschiedliche Identität&lt;br /&gt;
 a is not b  # True  weil Negation&lt;br /&gt;
&lt;br /&gt;
(Beachte: beim Vergleich von Zahlen des gleichen Typs liefern &amp;lt;tt&amp;gt;is&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;==&amp;lt;/tt&amp;gt; immer dasselbe Ergebnis.) Natürlich impliziert die Gleichheit der Schlüssel (Identität der Objekte) die Gleichheit der Werte.&lt;br /&gt;
&lt;br /&gt;
Ebenso wichtig sind die '''Zuweisungen'''. Hier zeigt sich besonders der Unterschied zwischen Wert- und Referenzsemantik. Im Falle von Wertsemantik gilt&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
\mathrm{Preconditions: } &amp;amp; s,t \in T \\&lt;br /&gt;
                         &amp;amp; s \mathrm{\ is\ not\ } t \\&lt;br /&gt;
\mathrm{Assign\ by\ value: } &amp;amp; s = t \\&lt;br /&gt;
\mathrm{Postconditions: } &amp;amp; t \mathrm{\ is\ } t_o \\&lt;br /&gt;
                          &amp;amp; s \mathrm{\ is\ not\ } t \\&lt;br /&gt;
                          &amp;amp; s == t &lt;br /&gt;
\end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Das heisst, t darf sich nicht verändern, und s hat nach der Zuweisung den gleichen Wert wie t. Bei Referenzsemantik gilt sogar&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
\mathrm{Precondition: } &amp;amp; t \in T \\&lt;br /&gt;
\mathrm{Assign\ by\ reference: } &amp;amp; s = t \\&lt;br /&gt;
\mathrm{Postconditions: } &amp;amp; t \mathrm{\ is\ } t_o \\&lt;br /&gt;
                          &amp;amp; s \mathrm{\ is\ } t&lt;br /&gt;
\end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Dies entspricht dem Pythoncode&lt;br /&gt;
 x = y&lt;br /&gt;
 assert x is y&lt;br /&gt;
Die Wertsemantik muss man in Python explizit erzwingen&lt;br /&gt;
 import copy  # wir brauchen das copy-Modul&lt;br /&gt;
 &lt;br /&gt;
 x = copy.deepcopy(y)&lt;br /&gt;
 assert x == y&lt;br /&gt;
 assert x is not y&lt;br /&gt;
&lt;br /&gt;
Mit der Zuweisung eng verwandt ist die Funktion &amp;lt;tt&amp;gt;swap&amp;lt;/tt&amp;gt;, die den Inhalt von zwei Variablen vertauscht:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
\mathrm{Precondition: } &amp;amp; t \in T, s \in S \\&lt;br /&gt;
\mathrm{Algorithm\ swap: } &amp;amp; \mathrm{swap}(s, t) \\&lt;br /&gt;
\mathrm{Postconditions: } &amp;amp; t \mathrm{\ is\ } s_o \\&lt;br /&gt;
                          &amp;amp; s \mathrm{\ is\ } t_o&lt;br /&gt;
\end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Diese Funktion wird sich beim Sortieren als sehr nützlich erweisen, weil dort das Vertauschen von zwei Datenelementen eine Grundoperation ist. In Python kann man dies so implementieren:&lt;br /&gt;
 t, s = s, t  # swap&lt;br /&gt;
Dabei macht man sich zunutze, dass Python mehrere Variablen in einem einzigen Statement zuweisen kann.&lt;br /&gt;
&lt;br /&gt;
[[Container|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Einf%C3%BChrung&amp;diff=5719</id>
		<title>Einführung</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Einf%C3%BChrung&amp;diff=5719"/>
				<updated>2021-02-11T16:01:52Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Zur Frage der elementaren Schritte */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Definition von Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
Es gibt viele Definitionen von Algorithmen. Hier sind die Ergebnisse einer Google-Suche auf  [http://www.google.de/search?hl=de&amp;amp;defl=en&amp;amp;q=define:Algorithm&amp;amp;sa=X&amp;amp;oi=glossary_definition&amp;amp;ct=title englisch] und auf&lt;br /&gt;
[http://www.google.de/search?hl=de&amp;amp;defl=de&amp;amp;q=define:Algorithmus&amp;amp;sa=X&amp;amp;oi=glossary_definition&amp;amp;ct=title deutsch]. Die Grundidee ist aber immer gleich:&lt;br /&gt;
&lt;br /&gt;
Ein '''Algorithmus''' ist eine Problemlösung durch endlich viele elementare Schritte. Die Teile der Definition bedürfen näherer Erläuterung:&lt;br /&gt;
&lt;br /&gt;
;Problemlösung: Damit ein Algorithmus ein Problem (genauer: eine Menge von gleichartigen Problemen) lösen kann, muss das Problem zunächst definiert (''spezifiziert'') werden. Die '''Spezifikation''' legt fest, ''was'' der Algorithmus erreichen soll, sagt aber nichts über das ''wie''. Die Spezifikation beschreibt somit relevante Eigenschaften des Systemzustands ''vor'' und ''nach'' der Ausführung des Algorithmus (sogenannte '''Vor-''' und '''Nachbedingungen'''), während der Algorithmus einen bestimmten ''Lösungsweg'' repräsentiert. Mit Hilfe der Spezifikation kann getestet werden, ob der Algorithmus tatsächlich eine Lösung des gestellten Problems liefert. Diese Frage untersuchen wir im Kapitel [[Korrektheit]].&lt;br /&gt;
;Endlich viele Schritte: Die Forderung nach endlich vielen Schritten unterstellt, dass jeder einzelne Schritt eine gewisse Zeit benötigt, also nicht unendlich schnell ausgeführt werden kann. Damit ist diese Forderung äquivalent zu der Forderung, dass der Algorithmus in endlicher Zeit zum Ergebnis kommen muss. Der Sinn einer solchen Forderung leuchtet aus praktischer Sicht unmittelbar ein. Interessant ist darüber hinaus die Frage, wie man mit möglichst wenigen Schritten, also möglichst schnell, zur Lösung kommt. Diese Frage untersuchen wir im Kapitel [[Effizienz]].&lt;br /&gt;
;Elementare Schritte: Im weiteren Sinne verstehen wir unter einem elementaren Schritt ein Teilproblem, für das bereits ein Algorithmus bekannt ist. Im engeren Sinne ist die Menge der elementaren Schritte durch die Hilfsmittel vorgegeben, mit denen der Algorithmus ausgeführt werden soll, also z.B. durch die Hardware oder die Programmiersprache. Wir gehen darauf im nächsten Abschnitt näher ein.&lt;br /&gt;
&lt;br /&gt;
=== Zur Frage der elementaren Schritte ===&lt;br /&gt;
&lt;br /&gt;
Welche Schritte als elementar angesehen werden können, hängt sehr stark vom Kontext der Aufgabe und den Hilfsmitteln zu ihrer Lösung ab. Ein interessantes Beispiel ist die Geometrie der alten Griechen, wo geometrische Probleme in der Ebene allein mit Zirkel und Lineal gelöst werden. In diesem Fall sind folgende elementare Operationen erlaubt:&lt;br /&gt;
* das Markieren eines Punktes (beliebig in der Ebene oder als Schnittpunkt zwischen bereits gezeichneten Linien),&lt;br /&gt;
* das Zeichnen einer Geraden durch zwei Punkte,&lt;br /&gt;
* das Zeichnen eines Kreises um einen Punkt,&lt;br /&gt;
* das Abgreifen des Abstands zwischen zwei Punkten mit dem Zirkel.&lt;br /&gt;
Auf der Basis dieser Operationen kann zum Beispiel kein Algorithmus für die Dreiteilung eines beliebigen Winkels definiert werden, während der Algorithmus für die Zweiteilung sehr einfach ist. &lt;br /&gt;
&lt;br /&gt;
Eine völlig andere Menge von elementaren Operationen ergibt sich für arithmetische Berechnungen mit Hilfe des Abacus (Rechenbrett), der seit der Römerzeit in Europa weit verbreitet war. Hier werden Zahlen durch die Positionen von Perlen auf Rillen oder Drähten dargestellt und Berechnungen durch deren Verschiebung. Eine ausführliche Beschreibung der wichtigsten Abacus-Algorithmen findet sich unter [http://totton.idirect.com/abacus/ The Bead Unbuffled] von Totton Heffelfinger und Gary Flom.&lt;br /&gt;
&lt;br /&gt;
Die moderne Auffassung von elementaren Operationen wird durch die Berechenbarkeitstheorie (ein Teilgebiet der theoretischen Informatik) bestimmt. Verschiedene Mathematiker (darunter die Pioniere Alan Turing, Alonso Church, Kurt Gödel, Stephen Kleene und Emil Post) haben seit den 1930er Jahren versucht, den intuitiven Begriff der Berechenbarkeit einer Funktion zu formalisieren und sind dabei zu völlig verschiedenen Lösungen gelangt (z.B. Turingmaschine, Lambda-Kalkül, μ-Rekursion und WHILE-Programm). Interessanterweise stellte sich heraus, dass diese Lösungen alle die gleiche Mächtigkeit haben: Obwohl die elementaren Operationen jeweils ganz anders definiert sind, ist die Menge der damit berechenbaren Funktionen immer gleich. Die [http://en.wikipedia.org/wiki/Church_thesis Church-Turing-These] besagt, dass es prinzipiell unmöglich ist, eine mächtigere Definition von elementaren Operationen zu finden, aber dies ist unbewiesen. Am bequemsten für die Praxis sind die  [http://de.wikipedia.org/wiki/WHILE-Programm WHILE-Programme], da sie sich direkt auf die heute gebräuchliche Hardware-Architektur abbilden lassen. Die elementaren Operationen eines WHILE-Programms lauten in erweiterter Backus-Naur Notation:&lt;br /&gt;
 P ::= x[i] = x[j] + c            # Addition einer Konstanten zur Variable x[i]&lt;br /&gt;
     | x[i] = x[j] - c            # Subtraktion einer Konstanten von x[i]&lt;br /&gt;
     | P; P                       # Nacheinanderausführung von zwei Anweisungen&lt;br /&gt;
     | WHILE x[i] != 0 DO P DONE  # Wiederholte Ausführung der Anweisung(en) P &lt;br /&gt;
                                  # (x[i] muss sich innerhalb von P ändern, um eine Endlosschleife zu vermeiden)&lt;br /&gt;
wobei &amp;lt;tt&amp;gt;c&amp;lt;/tt&amp;gt; eine beliebige ganzahlige Konstante (eine ausgeschriebene ganze Zahl) und &amp;lt;tt&amp;gt;x[i]&amp;lt;/tt&amp;gt; die Speicherzelle &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; bezeichnen. Alle Speicherzellen können ganze Zahlen aufnehmen und sind anfangs mit Null belegt. Darüber hinaus wird vorausgesetzt, dass mindestens soviele Speicherzellen vorhanden sind, wie der gegebene Algorithmus benötigt, und jede Speicherzelle groß genug ist, um die größte auftretende Zahl aufzunehmen. Beide Annahmen sind in der Praxis nicht immer erfüllt.&lt;br /&gt;
&lt;br /&gt;
In einem WHILE-Programm gibt es keine elementare Funktion, um die Summe von zwei &amp;lt;i&amp;gt;Variablen&amp;lt;/i&amp;gt; zu berechnen. Diese Operation muss man bereits als Algorithmus implementieren. Der folgende Code berechnet die Summe unter der Voraussetzung, dass &amp;lt;tt&amp;gt;x[j]&amp;lt;/tt&amp;gt; nicht negativ ist, indem &amp;lt;tt&amp;gt;x[j]&amp;lt;/tt&amp;gt; solange dekrementiert (um 1 erniedrigt) wird, bis es den Wert 0 annimmt, und &amp;lt;tt&amp;gt;x[i]&amp;lt;/tt&amp;gt; entsprechend bei jedem Schritt inkrementiert (um 1 erhöht) wird. Die alten Werte der Variablen gehen bei der Berechnung verloren:&lt;br /&gt;
 Algorithmus: x[i] = x[i] + x[j] als WHILE-Programm (Vorbedingung: x[j] &amp;gt;= 0)&lt;br /&gt;
     WHILE x[j] != 0 DO&lt;br /&gt;
         x[i] = x[i] + 1;&lt;br /&gt;
         x[j] = x[j] - 1&lt;br /&gt;
     DONE&lt;br /&gt;
Man erkennt, dass tatsächlich nur die vier elementaren Operationen (Addition/Subtraktion einer Konstanten, Nacheinanderausführung von Anweisungen, WHILE-Schleife) vorkommen. Allerdings ist dieser Algorithmus sehr langsam. Außerdem ist die Zerlegung in Form eines WHILE-Programms (oder eines äquivalenten Formalismus der Berechenbarkeitstheorie) für unsere Zwecke zu feinkörnig: Sie würde bedeuten, dass alle Algorithmen auf einem extrem einfachen Prozessor in Assembler programmiert werden müssten. Bereits eine so einfache Operation wie die Summe von zwei Variablen erfordert vier Codezeilen! &lt;br /&gt;
&lt;br /&gt;
Deshalb definiert man ''höhere Programmiersprachen'', die wichtige Algorithmen wie z.B. die arithmetischen Operationen mit ganzen Zahlen und Gleitkomma-Zahlen bereits als elementare Operationen enthalten. Weitere nicht ganz so wichtige Funktionen wie die Wurzel oder der Logarithmus werden in Programmbibliotheken angeboten, die standardmäßig mitgeliefert werden. In der Praxis betrachtet man eine Operation deshalb als elementar, wenn sie von einer typischen Programmiersprache oder einer typischen Standardbibliothek unterstützt wird. In dieser Vorlesung wählen wir die Operationen und Bibliotheken der Programmiersprache [http://www.python.org Python]. Wenn ein Algorithmus Anforderungen stellt, die nicht selbstverständlich sind, müssen sie als ''Requirements'' explizit angegeben werden. Wir werden darauf im Kapitel [[Generizität]] zurückkommen.&lt;br /&gt;
&lt;br /&gt;
=== Zur Geschichte ===&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|-valign=&amp;quot;top&amp;quot; &lt;br /&gt;
| Algorithmen wurden bereits im Altertum verwendet. Besonders die alten Griechen haben Pionierarbeit geleistet, z.B. auf dem Gebiet der Arithmetik (Euklidischer Algorithmus für den größten gemeinsamen Teiler von zwei Zahlen, Sieb des Eratosthenes zur Bestimmung von Primzahlen) und der Geometrie (Teilung einer Strecke oder eines Winkels nur mit Zirkel und Lineal). Der Begriff ''Algorithmus'' ist vom Namen des arabischen Gelehrten Muhammed Al Chwarizmi (ca. 783-850) abgeleitet, der in seinem Werk „Über das Rechnen mit indischen Ziffern“ (um 825) grundlegende Verfahren für das Rechnen im dekadischen Positionssystem beschrieben hat. Im 12. Jahrhundert wurde dieses Buch ins Lateinische übersetzt, und die Einleitung begann mit den Worten „Dixit Algorismi“ (Al Chwarizmi hat gesagt). Ab etwa 1200 wurden die neuen Rechenmethoden als „Algorismus de integris“ bzw. „Algorismus vulgaris“ (Rechnen mit ganzen Zahlen, d.h. Grundrechenarten und Wurzelziehen) sowie „Algorismus de minutiis“ (Bruchrechnung) zum festen Bestandteil der mathematischen Ausbildung im Rahmen der sieben freien Künste. Dabei diente der Begriff Algorithmus ursprünglich vor allem zur Abgrenzung des schriftlichen Rechnens mit indischen/arabischen Zahlen (wie wir es noch heute in der Schule lernen) vom traditionellen mechanischen Rechnen mit Abacus und römischen Zahlen, das noch bis ca. 1500 in Europa vorherrschend blieb. &lt;br /&gt;
&lt;br /&gt;
Die allgemeinere Bedeutung des Wortes Algorithmus als systematische Rechenvorschrift war jedoch ebenfalls schon früh gebräuchlich. Dies zeigt zum Beispiel der Titel des Buches „Algorismus proportionum“ (Rechenkunst mit Proportionen, ca. 1350) von Nicole Oresme, wo erstmals die Rechenregeln für Potenzen mit rationalen Exponenten beschrieben werden. Durch die steigenden Anforderungen des kaufmännischen Rechnens und der Navigation verbreitete sich die algorithmische Denkweise ab etwa 1500 rasch. Der Buchdruck machte mit Werken wie Adam Ries' „Rechenung auff der linihen und federn“ (d.h. mit Abacus und mit indischen/arabischen Zahlen, zuerst 1522) die grundlegenden Rechenalgorithmen einem breiten Bevölkerungskreis bekannt. Umfangreiche gedruckte Tafelwerke, z.B. der „Canon“ von G.J. Rhaeticus (1551) mit bis zu siebenstelligen Tabellen der trigonometrischen Funktionen, erlaubten es, komplizierte Berechnungen auf einfache Schritte (Addition, Subtraktion sowie Nachschlagen in der Tabelle) zurückzuführen. Unsere heutige Verwendung des Begriffs geht wohl auf Alonso Church's Aufsatz „An Unsolvable Problem of Elementary Number Theory“ (1936) zurück, wo die Berechenbarkeit einer Funktion mit der Existenz eines terminierenden Berechnungsalgorithmus gleichgesetzt wird.&lt;br /&gt;
| [[Image:Al-Khwarizmi.jpg]] &amp;lt;br&amp;gt; Al Chwarizmi-Denkmal in Teheran&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Definition von Datenstrukturen ==&lt;br /&gt;
&lt;br /&gt;
=== Beispiele für Datenformate ===&lt;br /&gt;
&lt;br /&gt;
Der Speicher eines Computers enthält eine Folge von Zeichen aus einem gegebenen Alphabet. Bei fast allen heutigen Computern ist dies eine Folge von Bits aus dem Alphabet {0,1}. Ein '''Datenformat''' ordnet eine Bitfolge in Gruppen und gibt jeder Gruppe eine Bedeutung. Der Gruppierungsprozess kann dann hierarchisch fortgesetzt werden.&lt;br /&gt;
&lt;br /&gt;
Die selben Bits können somit völlig verschiedene Bedeutungen annehmen, ja nachdem in welchem Datenformat sie sich befinden. Man betrachte z.B. die Folge von 16 Bits:&lt;br /&gt;
 1101011001101100&lt;br /&gt;
Wenn wir diese Folge als eine zusammengehörende Gruppe betrachten und als positive ganze Zahl in Binärdarstellung interpretieren (unsigned integer, &amp;lt;tt&amp;gt;uint16&amp;lt;/tt&amp;gt;), ergibt sich die Dezimalzahl &lt;br /&gt;
 54892 = 1*2&amp;lt;sup&amp;gt;15&amp;lt;/sup&amp;gt; + ... + 1*2&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt; + 1*2&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; + 0*2&amp;lt;sup&amp;gt;1&amp;lt;/sup&amp;gt; + 0*2&amp;lt;sup&amp;gt;0&amp;lt;/sup&amp;gt;&lt;br /&gt;
Interpretieren wir dieselbe Gruppe als vorzeichenbehaftete ganze Zahl in [http://de.wikipedia.org/wiki/Zweierkomplement Zweierkomplement]-Darstellung (signed integer, &amp;lt;tt&amp;gt;int16&amp;lt;/tt&amp;gt;), ergibt sich eine andere Dezimalzahl: Da das linke (höchstwertige) Bit Eins ist, handelt es sich um eine negative Zahl. Das Zweierkomplement erhält man durch Negieren aller Bits und nachfolgende Addition von 1:&lt;br /&gt;
 Zweierkomplement von 1101011001101100:&lt;br /&gt;
                      0010100110010011 + 1 = 0010100110010100&lt;br /&gt;
Die resultierende Dezimalzahl ist somit&lt;br /&gt;
 -10644 = -(0*2&amp;lt;sup&amp;gt;15&amp;lt;/sup&amp;gt; + ... + 0*2&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt; + 1*2&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; + 0*2&amp;lt;sup&amp;gt;1&amp;lt;/sup&amp;gt; + 0*2&amp;lt;sup&amp;gt;0&amp;lt;/sup&amp;gt;)&lt;br /&gt;
Alternativ können wir die Folge in zwei Gruppen zu 8 Bit gruppieren, und die Gruppen als Zeichencodes im Windows-Zeichensatz interpretieren. Wir erhalten die Zeichenkette &amp;quot;Öl&amp;quot;:&lt;br /&gt;
 11010110  01101100 = char[214] char[108] =&amp;gt; Öl&lt;br /&gt;
Eine weitere Interpretation ist diejenige als 16-Bit Gleitkommazahl (&amp;lt;tt&amp;gt;float16&amp;lt;/tt&amp;gt;) gemäß [http://en.wikipedia.org/wiki/IEEE_floating-point_standard IEEE Standard 754]. Dabei wird die Folge in Gruppen zu 1 Bit, 5 Bit und 10 Bit eingeteilt:&lt;br /&gt;
 1  10101  1001101100&lt;br /&gt;
Die Gruppen werden als nicht-negative Binärzahlen gelesen, wobei die erste Gruppe das Vorzeichen &amp;lt;tt&amp;gt;s&amp;lt;/tt&amp;gt; der Gleitkommazahl ist (0 bedeutet &amp;quot;+&amp;quot;, 1 bedeutet &amp;quot;-&amp;quot;), die zweite ist ihr Exponent &amp;lt;tt&amp;gt;exp&amp;lt;/tt&amp;gt; und die dritte die Mantisse &amp;lt;tt&amp;gt;m&amp;lt;/tt&amp;gt;. In unserem Beispiel gilt &amp;lt;tt&amp;gt;s = 1&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;exp = 21&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;m = 620&amp;lt;/tt&amp;gt;). Die Umrechnung in eine Gleitkommazahl erfolgt, gemäß IEEE Standard, nach folgender Formel:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;tt&amp;gt;z = (1 - 2*s) * 2&amp;lt;sup&amp;gt;exp-15&amp;lt;/sup&amp;gt; * (1 + m * 2&amp;lt;sup&amp;gt;-10&amp;lt;/sup&amp;gt;)&amp;lt;/tt&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
In Dezimaldarstellung ist dies &amp;lt;tt&amp;gt;-102.75&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Das analoge Beispiel für eine Folge von 32 Bits ist vielleicht realistischer, weil 32-bit Zahlen (integer und float) in der Praxis häufiger vorkommen. Wir betrachten die Bitfolge:&lt;br /&gt;
 11111100011000100110010101101110&lt;br /&gt;
Als positive ganze Zahl in Binärdarstellung (unsigned integer, &amp;lt;tt&amp;gt;uint32&amp;lt;/tt&amp;gt;) ergibt sich die Dezimalzahl 4234306926. Dieselben Bits als vorzeichenbehaftete ganze Zahl in Zweierkomplement-Darstellung (signed integer, &amp;lt;tt&amp;gt;int32&amp;lt;/tt&amp;gt;) ergiben die Dezimalzahl -60660370. Als Zeichenfolge (vier Gruppen zu 8 Bit) bekommen wir die Zeichenkette &amp;quot;üben&amp;quot;. Eine weitere mögliche Interpretation ist diejenige als Farbe im RGBA System (8 Bit pro Farbkanal, 8 Bit Transparenzwert), und wir erhalten ein halbtransparentes Rosa (Rot: 252, Grün: 98, Blau: 101, Alpha: 110).&amp;lt;br&amp;gt;&lt;br /&gt;
Eine 32-Bit Gleitkommazahl (&amp;lt;tt&amp;gt;float32&amp;lt;/tt&amp;gt;) ist gemäß IEEE Standard 754 definiert durch Gruppen zu 1 Bit für das Vorzeichen, 8 Bit für den Exponenten und 23 Bit für die Mantisse, d.h:&lt;br /&gt;
 1 11111000 11000100110010101101110&lt;br /&gt;
Hier gilt also &amp;lt;tt&amp;gt;s = 1&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;exp = 248&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;m = 6448494&amp;lt;/tt&amp;gt;). Die Umrechnung in eine Gleitkommazahl erfolgt jetzt nach der Formel:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;tt&amp;gt;z = (1 - 2*s) * 2&amp;lt;sup&amp;gt;exp-127&amp;lt;/sup&amp;gt; * (1 + m * 2&amp;lt;sup&amp;gt;-23&amp;lt;/sup&amp;gt;)&amp;lt;/tt&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
In Dezimaldarstellung ist dies rund &amp;lt;tt&amp;gt;-4.7020653*10&amp;lt;sup&amp;gt;36&amp;lt;/sup&amp;gt;&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Im Sinne einer hierarchischen Gruppierung können wir jetzt z.B. eine Datenstruktur &amp;quot;Farbbild&amp;quot; definieren, indem wir viele RGBA-Werte zu einem 2-dimensionalen Array zusammenfassen. Eine Datenstruktur &amp;quot;komplexe Zahl&amp;quot; wird durch ein geordnetes Paar von Gleitkommazahlen gebildet, eine &amp;quot;Meßreihe&amp;quot; als Liste von ganzen Zahlen oder Gleitkommawerten (je nach Art der Messung), usw.&lt;br /&gt;
&lt;br /&gt;
=== Varianten der Datenstrukturdefinition ===&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|-valign=&amp;quot;bottom&amp;quot; &lt;br /&gt;
| Bei den Beispielen im vorigen Abschnitt habe wir das Speicherlayout und die Bedeutung der einzelnen Bits bzw. Bit-Gruppen festgelegt. Wir bezeichnen eine auf diese Weise definierte Datenstruktur als &amp;lt;b&amp;gt;Datenformat&amp;lt;/b&amp;gt;. Datenformate werden vor allem verwendet, um Datenstrukturen auf Festplatte oder in einer Datenbank zu speichern und Daten über ein Netzwerk auszutauschen (vgl. den Eintrag [http://de.wikipedia.org/wiki/Dateitypen Dateityp] in der WikiPedia). Aus Sicht des Betriebssystems ist ein File einfach eine Folge von Bits, deren Bedeutung aus anderen Informationen geschlossen werden muss, z.B. aus der Endung des Filenames (.jpg, .png, .xml usw.) oder aus dem mit dem File assoziierten [http://de.wikipedia.org/wiki/Internet_Media_Type MIME-Type]. Viele Fileformate beginnen zudem mit bestimmten Bitfolgen (&amp;quot;[http://de.wikipedia.org/wiki/Magische_Zahl_%28Informatik%29 magischen Zahlen]&amp;quot;), die für das betreffende Fileformat charakteristisch sind. Jedes JPEG-File beginnt z.B. mit dem Bytemuster &amp;lt;tt&amp;gt;255 216 255&amp;lt;/tt&amp;gt;, jedes PNG-File mit der Folge &amp;lt;tt&amp;gt;137 80 78 71&amp;lt;/tt&amp;gt;, jedes XML-File mit dem String &amp;lt;tt&amp;gt;&amp;quot;&amp;amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot; ?&amp;amp;gt;&amp;lt;/tt&amp;gt; (wobei Versionsnummer und Zeichensatzdefinition natürlich verschieden sein können, je nach Fileinhalt). Wann immer möglich sollte man bei der Verwendung von Datenformaten auf vorhandene Standards (wie z.B. IEEE 754 für Gleitkommazahlen oder XML für hierarchisch strukturierte Dokumente) zurückgreifen, weil sonst beim Einlesen und Interpretieren der gespeicherten Bitfolgen sehr leicht Fehler passieren.&lt;br /&gt;
&lt;br /&gt;
Innerhalb einer Programmiersprache werden Datenstrukturen typischerweise nicht als Datenformate definiert, sondern durch die Verknüpfung eines Speicherlayouts mit einer &amp;lt;i&amp;gt;Menge erlaubter Operationen&amp;lt;/i&amp;gt; auf diesen Daten. Die Interpretation ergibt sich implizit aus der Definition dieser Operationen. Verwendet man beispielsweise eine Folge von 32 Bits zusammen mit den arithmetischen Operationen für natürliche Zahlen (inklusive der zugehörigen Vor- und Nachbedingungen), ist die Interpretation als &amp;lt;tt&amp;gt;uint32&amp;lt;/tt&amp;gt; dadurch gegeben. Eine Folge von Bytes mit den Operationen &amp;lt;tt&amp;gt;print&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;toLowerCase&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;toUpperCase&amp;lt;/tt&amp;gt; usw. weist auf die Interpretation &amp;quot;Zeichenkette&amp;quot; (&amp;lt;tt&amp;gt;string&amp;lt;/tt&amp;gt;). Eine solche Verknüpfung von Datenrepräsentation mit Operationen bezeichnen wir als '''(Daten-)Typ''' oder '''Klasse'''. Klassen sind für den Programmierer das wichtigste Mittel, um eigene Datenstrukturen zu definieren, und wir werden in der Vorlesung ausführlich darauf eingehen.&lt;br /&gt;
&lt;br /&gt;
Die dritte Möglichkeit ist schließlich die Kombination einer Interpretation mit einer Menge erlaubter Operationen, ohne ein bestimmtes Speicherlayout oder eine konkrete Implementation der Operationen festzulegen. In diesem Fall sprechen wir von '''Abstrakten Datentypen''' (ADTs). Diese spielen beim Entwurf von anwendungsübergreifenden Programmierschnittstellen und bei der theoretischen Analyse von Algorithmen und Datenstrukturen eine wichtige Rolle. Da von den Besonderheiten einer bestimmten Implementation und eines bestimmten Computers abstrahiert wird, sind die gewonnen Erkenntnisse auf viele Anwendungen übertragbar. Konzepte, die als abstrakte Datentypen definiert sind, können je nach Kontext immer wieder anders implementiert werden, ohne dass die übergreifenden (abstrakten) Eigenschaften verloren gehen. Viele der konkreten Datenstrukturen, die wir behandeln werden, kann man zu abstrakten Datenstrukturen verallgemeinern. Dies ist eine Schlüsselaufgabe beim Entwurf wiederverwendbarer Programmbibliotheken. Wir kommen im Kapitel [[Generizität]] auf ADTs zurück.&lt;br /&gt;
&lt;br /&gt;
Man kann sich die drei Möglichkeiten &amp;quot;Speicherlayout&amp;quot;, &amp;quot;Bedeutung&amp;quot; und &amp;quot;Menge der darauf ausführbaren Operatoren&amp;quot; als Ecken eines Dreiecks wie in der nebenstehenden Skizze vorstellen. Definiert man zwei Ecken des Dreiecks, ist auch die dritte weitgehend (oder zumindest zu einem gewissen Grade, wie bei ADTs) festgelegt. Die drei Kanten entsprechen den drei Arten der Datenstrukturen: Legt man &amp;quot;Speicherlayout&amp;quot; und &amp;quot;Bedeutung&amp;quot; fest, erhalten wir ein Datenformat, bei &amp;quot;Speicherlayout&amp;quot; plus &amp;quot;Operatoren&amp;quot; einen Klasse bzw. einen Typ, und aus &amp;quot;Operatoren&amp;quot; plus &amp;quot;Bedeutung&amp;quot; folgt ein abstrakter Datentyp.&lt;br /&gt;
| [[Image:Dt dreieck.png|400px]] &amp;lt;br&amp;gt; &amp;lt;center&amp;gt;Datenstruktur-Dreieck&amp;lt;/center&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Wichtige Begriffe ===&lt;br /&gt;
&lt;br /&gt;
Programmiersprachen, die ausgereifte Mechanismen zur Definition von Klassen bieten, werden als ''objekt-orientiert'' bezeichnet. Sprachen heißen ''streng typisiert'', wenn der Compiler bzw. Interpreter der Sprache sicherstellt, dass auf jeder Datenstruktur nur die jeweils explizit erlaubten Operationen ausgeführt werden (jeder Versuch, eine illegale Operation auszuführen, wird hier als Fehler signalisiert). Erfolgt diese Prüfung während der Compilierung (also während der Übersetzung des Quellcodes in eine Maschinensprache), spricht man von einer ''statisch typisierten Sprache''. Wird die Prüfung hingegen während der Ausführung des Programms durchgeführt, handelt es sich um eine ''dynamisch typisierte Sprache''. Python ist eine dynamisch-typisierte, objekt-orientierte Sprache. Streng typisiert ist sie allerdings nur für die vordefinierten Klassen. Bei benutzerdefinierten Klassen gibt es (wie bei den meisten anderen Programmiersprachen auch) Möglichkeiten, die erlaubten Operationen zu umgehen. Dies sollte man allerdings nur dann tun, wenn es einen wichtigen Grund gibt. Solange man sich nämlich auf die erlaubten Operationen beschränkt, ist eine große Menge von Fehlerquellen von vornherein ausgeschlossen. &lt;br /&gt;
&lt;br /&gt;
Ein bestimmter Speicherbereich, der den Anforderungen an eine Klasse genügt (wo also die Bits in entsprechender Weise gruppiert und interpretiert werden), wird als '''Objekt''' dieser Klasse oder als '''Instanz''' bezeichnet. Jede Instanz hat eine eindeutige Identität, einen ''Schlüssel''. Innerhalb eines Programms wird dafür gewöhnlich die Speicheradresse des ersten Bytes der Instanz (also der Index der ersten Speicherzelle) verwendet. Dies ist besonders effizient, weil die Speicheradresse für jedes Objekt eindeutig und leicht feststellbar ist. Ist das Objekt hingegen als Datei gespeichert, benötigt man einen expliziten Schlüssel, z.B. den Dateinamen oder die URL. &lt;br /&gt;
&lt;br /&gt;
Das Bitmuster selbst bzw. die daraus folgende Interpretation wird als '''Zustand''' oder '''Wert''' der Instanz bezeichnet. Daraus folgt, dass verschiedene Instanzen einer Klasse dennoch gleiche Werte haben können. Die Menge aller legalen Werte bilden den ''Wertebereich'' der Klasse. Werden Instanzen ausschließlich mit den explizit erlaubten Operationen ihrer Klasse manipuliert, können niemals illegale Werte entstehen. Es liegt auf der Hand, dass illegale Werte schwerwiegende Programmfehler darstellen, die man auf diese Weise vermeidet. [Computerviren tun genau das Gegenteil: Sie verwenden absichtlich verbotene Operationen, um das Programm in einen illegalen, vom Angreifer gewünschten Zustand zu bringen. Dies ist möglich, weil nicht alle verbotenen Operationen automatisch als Fehler erkannt werden, siehe oben.]&lt;br /&gt;
&lt;br /&gt;
Die meisten Programmiersprachen haben einen oder mehrere spezielle Typen für das Speichern von Objektschlüsseln. Die gebräuchlichsten Namen für diese Typen sind ''Zeiger'' (pointer), ''Referenz'' (reference) und ''Handle''. Wir verwenden das Wort '''Referenz'''. Ein Objekt der Klasse Referenz enthält also den Schlüssel eines anderen Objekts. Man sagt, dass die Referenz ''auf das andere Objekt verweist''. Diese Art der Indirektion ist uns heutzutage durch das Internet bestens vertraut: Jede WWW-Seite ist ein Objekt, und seine URL ist der dazugehörige Schlüssel. Hyperlinks und Lesezeichen (bookmarks) hingegen sind Referenzen, die mittels der URL auf andere Seiten verweisen.&lt;br /&gt;
&lt;br /&gt;
Aus der Unterscheidung von Werten und Referenzen ergibt sich die wichtige Unterscheidung von ''Wertsemantik'' und ''Referenzsemantik''. Wird nämlich ein Objekt an eine Variable zugewiesen&lt;br /&gt;
 x = anObject&lt;br /&gt;
so hängt die korrekte Verwendung der Variablen &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; davon ab, ob sie das Objekt in Form eines Wertes oder einer Referenz speichert. Im ersten Fall wird das Objekt selbst kopiert, und es entsteht ein neues Objekt mit neuer Identität, aber gleichem Zustand. Im anderen Fall wird nur der Schlüssel kopiert, und die Referenz verweist nach wie vor auf das ursprüngliche Objekt. Ist &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; ein Wert, so verändert eine Manipulation von &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; nur das neue Objekt (das ursprüngliche bleibt erhalten). Ist &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; hingegen eine Referenz, wird immer das ürsprüngliche Objekt manipuliert (denn es gibt ja keine Kopie). Ob eine Variable einen Wert oder eine Referenz enthält, wird in jeder Programmiersprache anderes festgelegt. In Python gilt&lt;br /&gt;
* Zahlen (Typen &amp;lt;tt&amp;gt;bool&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;int&amp;lt;/tt&amp;gt;, und &amp;lt;tt&amp;gt;float&amp;lt;/tt&amp;gt;) werden immer als Werte gespeichert und kopiert.&lt;br /&gt;
* Alle anderen Typen werden als Referenzen gespeichert und kopiert.&lt;br /&gt;
* Für alle Typen kann Wertsemantik mit Hilfe des Python-Moduls [http://docs.python.org/lib/module-copy.html copy] erzwungen werden.&lt;br /&gt;
Das Verständnis von Werten und Referenzen wird in der 1. Übung vertieft.&lt;br /&gt;
&lt;br /&gt;
Der Entwurf von Datentypen bzw. Klassen wird uns im Laufe der Vorlesung immer wieder beschäftigen.&lt;br /&gt;
&lt;br /&gt;
== Fundamentale Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
Einige Algorithmen werden praktisch bei jeder Klasse benötigt, unabhängig vom eigentlichem Verwendungszweck der Klasse. Es ist wichtig, diese fundamentalen Algorithmen zu kennen. Außerdem eignen sie sich gut zur Einführung der Grundprinzipien der Algorithmen-Spezifikation mittels Vor- und Nachbedingungen. Diese Bedingungen beschreiben Eigenschaften, die die Variablen des Systems ''vor'' bzw. ''nach'' der Ausführung des Algorithmus haben sollen. Damit man außerdem die Veränderungen durch den Algorithmus beschreiben kann, führt man zu jeder Variablen (z.B. &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;) eine Hilfsvariable (z.B. &amp;lt;tt&amp;gt;x&amp;lt;sub&amp;gt;o&amp;lt;/sub&amp;gt;&amp;lt;/tt&amp;gt;, sprich &amp;quot;x-old&amp;quot;) ein. In den Hilfsvariablen wird der Zustand ''vor'' der Ausführung des Algorithmus gespeichert, so dass man diesen noch abfragen kann, wenn Variablen durch den Algorithmus verändert werden. Wenn der Algorithmus beispielsweise die Variable &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; inkrementiert (um eins erhöht), gilt die Nachbedingung &amp;lt;tt&amp;gt;x == x&amp;lt;sub&amp;gt;o&amp;lt;/sub&amp;gt; + 1&amp;lt;/tt&amp;gt; (darin ist &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; der neue, und &amp;lt;tt&amp;gt;x&amp;lt;sub&amp;gt;o&amp;lt;/sub&amp;gt;&amp;lt;/tt&amp;gt; der alte Wert der Variablen). Falls &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; hingegen nicht verändert wird, gilt &amp;lt;tt&amp;gt;x == x&amp;lt;sub&amp;gt;o&amp;lt;/sub&amp;gt;&amp;lt;/tt&amp;gt;. (Man beachte, dass dies in der Literatur nicht einheitlich gehandhabt wird -- einige Autoren verwenden z.B. &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; für den Zustand vor Ausführung des Algorithmus, und &amp;lt;tt&amp;gt;x'&amp;lt;/tt&amp;gt; für denjenigen danach. Diese Syntax ist jedoch mit den meisten Programmiersprachen inkompatibel.)&lt;br /&gt;
&lt;br /&gt;
Die wichtigste Gruppe von fundamentalen Funktionen sind die '''Konstruktoren''', die einen vorher unbenutzten Speicherbereich in eine Datenstruktur mit einem wohldefinierten Anfangswert transformieren. In Python haben die Konstruktoren im allgemeinen den gleichen Namen wie die dazugehörige Klasse, also z.B.&lt;br /&gt;
 i = int()   # erzeuge eine ganze Zahl mit Anfangswert 0&lt;br /&gt;
 f = float() # erzeuge eine Gleitkommazahl mit Anfangswert 0&lt;br /&gt;
 a = list()  # erzeuge ein leeres Array&lt;br /&gt;
usw. (Man beachte, dass das Python-Array den Klassennamen &amp;lt;tt&amp;gt;list&amp;lt;/tt&amp;gt; hat. Dies hat nichts mit verketteten Listen zu tun.) Konstruktoren ohne Argumente bezeichnet man als ''Standard-Konstruktoren'' (default constructors). Ja nach Typ gibt es meist noch weitere Konstruktoren, die Objekte mit anderen Anfangswerten erzeugen, z.B.&lt;br /&gt;
 i = int(2)     # erzeuge eine ganze Zahl mit Anfangswert 2&lt;br /&gt;
 i = 2          # ebenso (abgekürzte Schreibweise)&lt;br /&gt;
 f = float(1.5) # erzeuge eine Gleitkommazahl mit Anfangswert 1.5&lt;br /&gt;
 f = 1.5        # ebenso (abgekürzte Schreibweise)&lt;br /&gt;
 a = [i, f]     # erzeuge ein Array mit Kopien der Werte von i und f&lt;br /&gt;
(Das Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; enthält Kopien der Werte, weil Zahlen immer mit Wertsemantik zugewiesen werden.) Die allgemeine Spezifikation eines Standard-Konstruktors lautet&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
\mathrm{Precondition: } &amp;amp; T \in \mathrm{Types}\\&lt;br /&gt;
\mathrm{Constructor: } &amp;amp; t = T() \\&lt;br /&gt;
\mathrm{Postcondition: } &amp;amp; t \in T&lt;br /&gt;
\end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Der Ausdruck &amp;lt;math&amp;gt;t \in T&amp;lt;/math&amp;gt; besagt, dass t nach Ausführung des Konstruktors eine legale Instanz des Typs T (oder eine Referenz auf einen solche Instanz) sein muss. In Pythonsyntax kann dies folgendermassen geschrieben werden&lt;br /&gt;
 import inspect           # wir brauchen das inspect-Modul&lt;br /&gt;
 &lt;br /&gt;
 if inspect.isclass(T):   # prüfe, dass T ein Type ist&lt;br /&gt;
      t = T()&lt;br /&gt;
 assert isinstance(t, T)&lt;br /&gt;
Natürlich funktioniert der Code nur, wenn die Klasse &amp;lt;tt&amp;gt;T&amp;lt;/tt&amp;gt; tatsächlich existiert und dafür ein Standardkonstruktor definiert wurde. Das Gegenstück zu Konstruktoren sind die '''Destruktoren''', die den Speicher der Datenstruktur wieder frei geben. Da Python automatisches Speichermanagment unterstützt, werden die Destruktoren automatisch aufgerufen. Wir können sie deshalb hier übergehen.&lt;br /&gt;
&lt;br /&gt;
Sehr wichtig sind auch die '''Vergleichsoperatoren'''. Wir müssen dabei unterscheiden, ob auf Gleichheit der Referenzen (''identity'') oder auf Gleichkeit der Werte (''equality'') geprüft werden soll. In Python werden dazu die Operatoren &amp;lt;tt&amp;gt;is&amp;lt;/tt&amp;gt; bzw. &amp;lt;tt&amp;gt;==&amp;lt;/tt&amp;gt; verwendet. Die Negation erhält man durch &amp;lt;tt&amp;gt;is not&amp;lt;/tt&amp;gt; bzw. &lt;br /&gt;
&amp;lt;tt&amp;gt;!=&amp;lt;/tt&amp;gt;&lt;br /&gt;
 a = [1, 2]&lt;br /&gt;
 b = [1, 2]&lt;br /&gt;
 &lt;br /&gt;
 a == b      # True  weil gleiche Werte&lt;br /&gt;
 a != b      # False weil Negation&lt;br /&gt;
 a is b      # False weil unterschiedliche Identität&lt;br /&gt;
 a is not b  # True  weil Negation&lt;br /&gt;
&lt;br /&gt;
(Beachte: beim Vergleich von Zahlen des gleichen Typs liefern &amp;lt;tt&amp;gt;is&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;==&amp;lt;/tt&amp;gt; immer dasselbe Ergebnis.) Natürlich impliziert die Gleichheit der Schlüssel (Identität der Objekte) die Gleichheit der Werte.&lt;br /&gt;
&lt;br /&gt;
Ebenso wichtig sind die '''Zuweisungen'''. Hier zeigt sich besonders der Unterschied zwischen Wert- und Referenzsemantik. Im Falle von Wertsemantik gilt&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
\mathrm{Preconditions: } &amp;amp; s,t \in T \\&lt;br /&gt;
                         &amp;amp; s \mathrm{\ is\ not\ } t \\&lt;br /&gt;
\mathrm{Assign\ by\ value: } &amp;amp; s = t \\&lt;br /&gt;
\mathrm{Postconditions: } &amp;amp; t \mathrm{\ is\ } t_o \\&lt;br /&gt;
                          &amp;amp; s \mathrm{\ is\ not\ } t \\&lt;br /&gt;
                          &amp;amp; s == t &lt;br /&gt;
\end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Das heisst, t darf sich nicht verändern, und s hat nach der Zuweisung den gleichen Wert wie t. Bei Referenzsemantik gilt sogar&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
\mathrm{Precondition: } &amp;amp; t \in T \\&lt;br /&gt;
\mathrm{Assign\ by\ reference: } &amp;amp; s = t \\&lt;br /&gt;
\mathrm{Postconditions: } &amp;amp; t \mathrm{\ is\ } t_o \\&lt;br /&gt;
                          &amp;amp; s \mathrm{\ is\ } t&lt;br /&gt;
\end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Dies entspricht dem Pythoncode&lt;br /&gt;
 x = y&lt;br /&gt;
 assert x is y&lt;br /&gt;
Die Wertsemantik muss man in Python explizit erzwingen&lt;br /&gt;
 import copy  # wir brauchen das copy-Modul&lt;br /&gt;
 &lt;br /&gt;
 x = copy.deepcopy(y)&lt;br /&gt;
 assert x == y&lt;br /&gt;
 assert x is not y&lt;br /&gt;
&lt;br /&gt;
Mit der Zuweisung eng verwandt ist die Funktion &amp;lt;tt&amp;gt;swap&amp;lt;/tt&amp;gt;, die den Inhalt von zwei Variablen vertauscht:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
\mathrm{Precondition: } &amp;amp; t \in T, s \in S \\&lt;br /&gt;
\mathrm{Algorithm\ swap: } &amp;amp; \mathrm{swap}(s, t) \\&lt;br /&gt;
\mathrm{Postconditions: } &amp;amp; t \mathrm{\ is\ } s_o \\&lt;br /&gt;
                          &amp;amp; s \mathrm{\ is\ } t_o&lt;br /&gt;
\end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Diese Funktion wird sich beim Sortieren als sehr nützlich erweisen, weil dort das Vertauschen von zwei Datenelementen eine Grundoperation ist. In Python kann man dies so implementieren:&lt;br /&gt;
 t, s = s, t  # swap&lt;br /&gt;
Dabei macht man sich zunutze, dass Python mehrere Variablen in einem einzigen Statement zuweisen kann.&lt;br /&gt;
&lt;br /&gt;
[[Container|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Main_Page&amp;diff=5718</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=5718"/>
				<updated>2020-10-23T11:21:02Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Gliederung der Vorlesung */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Vorlesung Algorithmen und Datenstrukturen ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
apl. Prof. Dr. Ullrich Köthe, Universität Heidelberg, Sommersemester 2020&lt;br /&gt;
&lt;br /&gt;
Die Vorlesung findet '''dienstags''' um 14:15 Uhr und '''donnerstags''' um 16:15 Uhr online auf Discord und Twitch statt. Die Links haben in Müsli angemeldete Teilnehmer per Email erhalten.&lt;br /&gt;
&lt;br /&gt;
=== Klausur und Nachprüfung ===&lt;br /&gt;
&lt;br /&gt;
Der Termin der '''Abschlussklausur''' steht noch nicht fest.&lt;br /&gt;
&amp;lt;!--Die '''Klausur 1''' findet am Donnerstag, dem 20.7.2017 von 13:00 bis 14:30 Uhr im Großen Hörsaal Chemie (INF 252) statt. &amp;lt;b&amp;gt;Bitte melden Sie sich in [https://muesli.mathi.uni-heidelberg.de/lecture/view/707 MÜSLI] für die Klausur an.&amp;lt;/b&amp;gt; Zur Klausur wird zugelassen, wer mindestens 50% der Übungspunkte erreicht und eine Aufgabe in der Übungsgruppe vorgerechnet hat. (Hinweis: Sie benötigen einen Lichtbildausweis, um sich bei der Klausur zu indentifizieren!)--&amp;gt;&lt;br /&gt;
&amp;lt;!---&lt;br /&gt;
* '''[[Media:2014-Klausur-1.pdf|Ergebnis der Klausur 1 vom 29.7.2014]]''' (anonymisiert)&lt;br /&gt;
Die '''Klausur 2''' findet am Mittwoch, dem 8.10.2014 von 10:00 bis 12:00 Uhr im Seminarraum des [http://hci.iwr.uni-heidelberg.de/contact.php HCI, Speyerer Str. 6], statt. Teilnahmeberechtigt sind diejenigen, die Klausur 1 nicht bestanden haben (bitte unbedingt per Email &amp;lt;b&amp;gt;anmelden!&amp;lt;/b&amp;gt;) sowie diejenigen, die mich vorab informiert haben (Sie brauchen sich nicht nochmals anzumelden).&lt;br /&gt;
* '''[[Media:2014-Klausur-2.pdf|Ergebnis der Klausur 2 vom 8.10.2014]]''' (anonymisiert)&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;
&amp;lt;!-------&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;
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;
&amp;lt;!---&lt;br /&gt;
* Wegen der großen Nachfrage haben wir zusätzliche Tutoren/Korrektoren eingestellt und die Gruppengröße auf 32 Teilnehmer erhöht. Bitte melden Sie sich im [https://www.mathi.uni-heidelberg.de/muesli/lecture/view/355 MÜSLI] an.&lt;br /&gt;
* Termine und Räume: &lt;br /&gt;
** Mo 14:00 - 16:00 Uhr, INF 501, R-102 (Tutor und Korrektor: Niels Buwen [mailto:buwen@stud.uni-heidelberg.de buwen AT stud.uni-heidelberg.de])&lt;br /&gt;
** Mo 16:00 - 18:00 Uhr, INF 288, HS6 (Tutor: Niels Buwen, Korrektor: Katrin Honauer [mailto:katrin.honauer@iwr.uni-heidelberg.de katrin.honauer AT iwr.uni-heidelberg.de])&lt;br /&gt;
** Di  9:00 - 11:00 Uhr, &amp;lt;b&amp;gt;neuer Raum:&amp;lt;/b&amp;gt; INF 294, R-103 (Tutor und Korrektor: Marius Killinger [mailto:marius.felix.killinger@stud.uni-heidelberg.de marius.felix.killinger AT stud.uni-heidelberg.de])&lt;br /&gt;
** Di 11:00 - 13:00 Uhr, INF 294, R-102 (Tutor und Korrektor: Fynn Beuttenmüller: [mailto:f.beuttenmueller@gmx.de f.beuttenmueller AT gmx.de])&lt;br /&gt;
** Mi 14:00 - 16:00 Uhr, INF 294, R-113 (Tutor: Axel Wagner, Korrektor: Philipp Schubert [mailto:phil.jo.schubert@gmail.com phil.jo.schubert AT gmail.com])&lt;br /&gt;
** Mi 16:00 - 18:00 Uhr, INF 294, R-113 (Tutor und Korrektor: Axel Wagner: [mailto:axel.wagner@stud.uni-heidelberg.de axel.wagner AT stud.uni-heidelberg.de])&lt;br /&gt;
---&amp;gt;&lt;br /&gt;
* Die Übungsgruppen werden über &amp;lt;b&amp;gt;[https://muesli.mathi.uni-heidelberg.de/lecture/view/1171 MÜSLI]&amp;lt;/b&amp;gt; verwaltet. &lt;br /&gt;
* Übungsblätter werden auf [https://moodle.uni-heidelberg.de/course/view.php?id=2239 Moodle] veröffentlicht.&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;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 Korrektor.&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 Ullrich Köthe auf Anfrage gerne einrichtet.&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;
-------&amp;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;
(Termine werden nach und nach aktualisiert)&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Einführung]] (21. und 23.4.2020) &lt;br /&gt;
#* Definition von Algorithmen und Datenstrukturen, Geschichte&lt;br /&gt;
#* Fundamentale Algorithmen: Konstruktoren, Kopierfunktionen, swap.&lt;br /&gt;
#* Fundamentale Datenstrukturen: Zahlen, Container, Handles&lt;br /&gt;
#* Python-Grundlagen&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Container]] (28.4.2020)&lt;br /&gt;
#* Abstrakte Datentypen und algebraische Spezifikation&lt;br /&gt;
#* Grundlegende Container: Array, Stack, Queue, assoziatives Array&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Sortieren]] (some day in 2020)&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;
#* Anzahl der benötigten Vergleiche&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Korrektheit]] (29.4. und 6.5.2014 -- ab hier altes Datum)&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]] (8. und 13.5.2014)&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]] (15. und 20.5.2014)&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]] (22.5.2014)&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]] (27.5.2014)&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]] (3.6.2014)&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. und 10.6.2014)&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]] (12.6.2014)&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]] (17.6.2014)&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]] (24.6. bis 10.7.2014)&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 15.7.2014)&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.2014)&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]] (22.7.2014)&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;
# Wiederholung (24.7.2014)&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 verspäteter Abgabe bis zu drei Tagen werden noch 50% der erreichten Punkte angerechnet. Danach wird die Musterlösung freigeschaltet. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;i&amp;gt;Die Übungsaufgaben sind zur Zeit nicht freigeschaltet.&amp;lt;/i&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!------&lt;br /&gt;
# [[Media:Uebung-1.pdf|Übung]] (Abgabe 29.4.2014) und [[Media:muster_blatt1.pdf|Musterlösung]]&lt;br /&gt;
#* Python-Tutorial&lt;br /&gt;
#* Sieb des Eratosthenes&lt;br /&gt;
#* Wert- und Referenzsemantik&lt;br /&gt;
#* Freitag der 13.&lt;br /&gt;
#* Dynamisches Array&lt;br /&gt;
# [[Media:Uebung-2.pdf|Übung]] (Abgabe 6.5.2014) und [[Media:muster_blatt2.pdf|Musterlösung]]&lt;br /&gt;
#* Sortieren: Implementation und Geschwindigkeitsvergleich (Diagramme in Abhängigkeit von der Problemgröße)&lt;br /&gt;
#* Zelluläre Automaten&lt;br /&gt;
# [[Media:Uebung-3.pdf|Übung]] (Abgabe 13.5.2014) und [[Media:muster_blatt3.pdf|Musterlösung]]&lt;br /&gt;
#* Manuelles Debuggen&lt;br /&gt;
#* Einführung in 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 20.5.2014) 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]] (Abgabe 27.5.2014) 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 '''Donnerstag''' 5.6.2014) 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 die Files [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/die-drei-musketiere.txt die-drei-musketiere.txt] und [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/stopwords.txt stopwords.txt]. Die Zeichenkodierung in diesen Files ist Latin-1.)&lt;br /&gt;
#* BucketSort&lt;br /&gt;
# [[Media:Uebung-7.pdf|Übung]] (Abgabe 12.6.2014) und [[Media:muster_blatt7.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 19.6.2014) 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 '''Dienstag''' 1.7.2014) und Musterlösung für [[Media:muster_blatt9-1+3.pdf|Aufgaben 1 und 3]] sowie für [[Media:muster_blatt9-2.pdf|Aufgabe 2]]&lt;br /&gt;
#* Übungen zur Generizität: Sortieren mit veränderter Ordnung, Iterator für Tiefensuche&lt;br /&gt;
#* Graphenaufgaben: Weg aus einem Labyrinth, Erzeugen einer perfekten Hashfunktion (Dazu benötigen Sie den Artikel [http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.5566 &amp;lt;i&amp;gt;&amp;quot;An optimal algorithm for generating minimal perfect hash functions&amp;quot;&amp;lt;/i&amp;gt;] sowie 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-10.pdf|Übung]] (Abgabe 8.7.2014) und Musterlösung für [[Media:muster_blatt10-1.pdf|Aufgabe 1]] sowie für [[Media:muster_blatt10-2.pdf|Aufgabe 2]]&lt;br /&gt;
#* Fortgeschrittene Graphenaufgaben: Routenplaner (Dazu benötigen Sie wieder das File  [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/entfernungen.json entfernungen.json]. Die Zeichenkodierung in diesem File ist UTF-8.) und Bildverarbeitung (Dazu benötigen Sie 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-11.pdf|Übung]] (Abgabe 15.7.2014) &lt;br /&gt;
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, Seam Carving (Dazu benötigen Sie wieder die Files [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/entfernungen.json entfernungen.json] und [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/pgm.py pgm.py] aowie das Bild [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/coast.pgm coast.pgm].)&lt;br /&gt;
# [[Media:Uebung-12.pdf|Übung]] (Abgabe 22.7.2014)&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;
#* Bonusaufgaben: indirektes Sortieren und Prüfungswiederholung&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;
# [[Media:Bonusuebung.pdf|Übung (Bonus)]] (&amp;lt;font color=red&amp;gt;Achtung: Abgabe bereits am Dienstag, 24.7.2014&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>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5717</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5717"/>
				<updated>2020-07-21T22:40:52Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Systematisches Erzeugen aller Permutationen */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann mit dem Algorithmus von Pandita (Indien, 1325-1400) -- dem (mehrmals wiederentdeckten) Standardalgorithmus für diese Aufgabe -- implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i+1] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage Y und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei Y impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern: [[Image:Bild 11.jpg]]&lt;br /&gt;
: Jede logische Schaltung kann als SAT-Ausdruck geschrieben werden.&lt;br /&gt;
* Bsp.: Zuordnung von Heim und Auswärtsspielen beim Fußball&lt;br /&gt;
: &amp;lt;math&amp;gt;x_{it} = \begin{cases} &lt;br /&gt;
true,  &amp;amp; \mbox{Mannschaft i hat am Spieltag t Heimspiel} \\&lt;br /&gt;
false, &amp;amp; \mbox{Mannschaft i hat am Spieltag t Auswärtsspiel} &lt;br /&gt;
\end{cases}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
: 1. Nebenbedingung: spielt Mannschaft i am Spieltag t gegen Mannschaft j, muss gelten &amp;lt;math&amp;gt; x_{it} = \lnot x_{jt}&amp;lt;/math&amp;gt; &lt;br /&gt;
: 2. Nebenbedingung: Jede Mannschaft spielt gegen jede&lt;br /&gt;
: 3. Nebenbedingung: Jede Mannschaft spielt abwechselnd Heim und auswärts &amp;lt;math&amp;gt; x_{it} \neq x_{i(t+1)} &amp;lt;/math&amp;gt;&lt;br /&gt;
: Alle Bedingungen sollen gleichzeitig Erfüllt sein: &lt;br /&gt;
:: &amp;lt;math&amp;gt;y = \begin{cases} (x_{11} \neq x_{21}) \and (x_{31} \neq x_{41}) \and ... \\ &lt;br /&gt;
( x_{12} \neq x_{32} ) \and ... \\&lt;br /&gt;
( x_{11} \neq x_{12} ) \and ( x_{12} \neq x_{13} ) \and ...&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&lt;br /&gt;
: Frage: Kann man X so belegen, dass Y wahr ist? &lt;br /&gt;
: &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Nein, nur möglich wenn es nur 2 Mannschaften gibt und diese abwechselnd gegeneinander antreten. &lt;br /&gt;
&lt;br /&gt;
Normalformen für logische Ausdrücke zur Vereinfachung und Systematisierung&lt;br /&gt;
* 3-CNF (Konjunktionen-NF)&lt;br /&gt;
** jede Klausel enthält max 3 Variablen (genau 3 mit dummy Variablen)&lt;br /&gt;
** jede Klausel enthält nur &amp;lt;math&amp;gt; \or &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; /lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
** alle Klauseln sind durch &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft.&lt;br /&gt;
z.B. &amp;lt;math&amp;gt; ( x_1 \or x_2 \or \lnot x_4 ) \and ( \lnot x_2 \or x_3 \or x_4) \and (...) \and &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \Rightarrow &amp;lt;/math&amp;gt; Ausdruck ist wahr, wenn jede Klausel wahr ist. &lt;br /&gt;
: In jeder Klausel hat man 3 Chancen die Klausel wahr zu machen. &lt;br /&gt;
: Aber: Klauseln können sich widersprechen und nicht erfüllbar sein!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz:&amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; Jeder logische Ausdruck effizient (in pol. Zeit) in 3-CNF umwandelbar.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz v. Cook: &amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; 3-SAT (Erfüllbarkeitsproblem für Ausdrücke in 3-CNF) ist NP-vollständig&lt;br /&gt;
&lt;br /&gt;
zur Zeit ist kein effizienterer Algorithmus bekannt, als im schlechtesten Fall alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Belegungen von {&amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt;} auszuprobieren &lt;br /&gt;
&lt;br /&gt;
2-CNF: wie 3-CNF, nur 2 Variablen pro Klausel&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; effiziente Alg existieren, aber nicht jeder logische Ausdruck in 2-CNF transformierbar.&lt;br /&gt;
: z.B. Heim-Auswärtsproblem&lt;br /&gt;
&lt;br /&gt;
INF (Implikationen-NF):&lt;br /&gt;
* 2 Variablen pro Klausel, Operatoren &amp;lt;math&amp;gt; \implies und \lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
* Klauseln mit &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft. &lt;br /&gt;
&lt;br /&gt;
Satz: jede 2-CNF effizient in INF umwandelbat.&lt;br /&gt;
: &amp;lt;math&amp;gt; ( x_i \or x_j ) \rightsquigarrow ( \lnot x_i \implies x_j ) \and ( \lnot x_j \implies x_i )&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;  INF als gerichteter Graph schreibbar und mittels starker Zusammenhangskomponenten lösbar. &lt;br /&gt;
&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;x_1&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \or x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \implies x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_1 \implies x_2 (A)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_2 \implies x_1 (B)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;A \and B&amp;lt;/math&amp;gt;  &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 1 || 0 || 0 || 0 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden.&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== k-SAT, k=2 in pol. Zeit lösbar === &lt;br /&gt;
&lt;br /&gt;
==== Alg. 1 ====&lt;br /&gt;
(f. bei k) (nur für k=2 effizient) '''Randomisiert'''&lt;br /&gt;
* (0) initialisiere &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; beliebig&lt;br /&gt;
* (1) wiederhole &amp;lt;math&amp;gt;T_{max}&amp;lt;/math&amp;gt; - mal&lt;br /&gt;
** (a) wenn das aktuelle x den Ausdruck erfüllt: return x (x=[&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;
** (b) wähle zufällig eine Klausel, die nicht erfüllt ist&lt;br /&gt;
** (c) wähle in dieser Klausel zufällig eine der k Variablen und invertiere sie =&amp;gt; Klausel ist jetzt erfüllt&lt;br /&gt;
::: (andere können jetzt false geworden sein) &lt;br /&gt;
::: (&amp;lt;math&amp;gt;x_1 \or x_2 ) \and ( x_1 \or \lnot x_2 )&amp;lt;/math&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;math&amp;gt; x_1 = 0, x_2 = 0,&amp;lt;/math&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;math&amp;gt; x_2&amp;lt;/math&amp;gt; auf 1 =&amp;gt; 1. Klausel wahr, 2. falsch&lt;br /&gt;
* (2) return &amp;quot;keine Lösung gefunden&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Nach wie vielen Iterationen wird im Mittel eine Lösung gefunden?&lt;br /&gt;
* Ausdruck unerfüllbar =&amp;gt; Endlosschleife, Timeout nach &amp;lt;math&amp;gt;T_{max}&amp;lt;/math&amp;gt; Iterationen &lt;br /&gt;
* Ausrduck erfüllbar:&lt;br /&gt;
** falls k&amp;lt;math&amp;gt;\geq&amp;lt;/math&amp;gt;3: nach &amp;lt;math&amp;gt;O((\frac{2(k-1)}{k})^N)&amp;lt;/math&amp;gt; Iterationen wird Lösung gefunden&lt;br /&gt;
** k=3: &amp;lt;math&amp;gt;O((\frac{4}{3})^N)&amp;lt;/math&amp;gt; exponentielle Zeit, wie zu erwarten für NP-vollständiges Problem&lt;br /&gt;
** k=2: &amp;lt;math&amp;gt;O(N^2)&amp;lt;/math&amp;gt; Iterationen bis Lösung&lt;br /&gt;
&lt;br /&gt;
Beweis: Algorithmus entspricht im Wesentlichen dem '''Random Walk'''&lt;br /&gt;
: Sei &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; die korrekte Lösung und x die aktuelle Belegung&lt;br /&gt;
: RW: Stuhl i &amp;lt;math&amp;gt;\mathrel{\hat=}&amp;lt;/math&amp;gt; i Variablen zwischen &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; und x stimmen überein =&amp;gt; Ziel: erreiche Stuhl N &lt;br /&gt;
* (c):&lt;br /&gt;
** Fall 1: beide Variablen falsch =&amp;gt; egal welche wir invertieren, bewegen wir uns von Stuhl i zu i+1&lt;br /&gt;
** Fall 2: eine Variable ist falsch: &lt;br /&gt;
*** mit Wahrscheinlichkeit 1/2 wählen wir diese und gehen von i nach i+1&lt;br /&gt;
*** mit Wahrscheinlichkeit 1/2 wählen wir die andere und gehen von i nach i-1&lt;br /&gt;
&lt;br /&gt;
schlechtester Fall: Es existiert keine Lösung &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; und wir haben immer Fall 2&lt;br /&gt;
: =&amp;gt;RW braucht &amp;lt;math&amp;gt;O(N^2 - i^2)&amp;lt;/math&amp;gt; Schritte zum Stuhl N &amp;lt;math&amp;gt;\mathrel{\hat=} O(N^2)&amp;lt;/math&amp;gt; falls i anfangs zufällig ist &lt;br /&gt;
&lt;br /&gt;
==== Alg. 2 (det. Alg. für k=2 mittels SZK in gerichtetem Graphen) ====&lt;br /&gt;
: geg.: Ausdruck 2-CNF&lt;br /&gt;
* (1) wandle nach INF: ersetze jede Klausel &amp;lt;math&amp;gt;(x_i \or x_j)&amp;lt;/math&amp;gt; durch &amp;lt;math&amp;gt;(\lnot x_i \Rightarrow x_j) \and (\lnot x_j \Rightarrow x_i)&amp;lt;/math&amp;gt;&lt;br /&gt;
: (entsprechend, wenn in Originalklausel &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt; vorkommen)&lt;br /&gt;
* (2) repräsentiere den Ausdruck als Graph: &lt;br /&gt;
** (a) 2 Knoten pro Var:&amp;lt;math&amp;gt;v_i \mathrel{\hat=} x_i , v_{i+N} \mathrel{\hat=} \lnot x_i &amp;lt;/math&amp;gt;&lt;br /&gt;
** (b) Verbindung für jede Implikation durch korrespondierenden Knoten durch gerichtete Kante&lt;br /&gt;
&lt;br /&gt;
Bsp.:&lt;br /&gt;
&amp;lt;math&amp;gt;C_1 \and C_2 \Leftrightarrow (\lnot x_1 \Rightarrow x_2 ) \and (\lnot x_2 \Rightarrow x_1) \and (x_2 \Rightarrow x_3) \and (\lnot x_3 \Rightarrow \lnot x_2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* (3) Prüfe ob der Ausdruck erfüllbar ist. Bilde SZK des Graphen&lt;br /&gt;
: '''Satz''': Ausdruck erfüllbar &amp;lt;math&amp;gt;\Leftrightarrow \forall&amp;lt;/math&amp;gt;i: &amp;lt;math&amp;gt; v_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v_{i+N}&amp;lt;/math&amp;gt; sind in verschiedenen Komponenten&lt;br /&gt;
&lt;br /&gt;
Beweis: in jeder SZK gilt: &amp;lt;math&amp;gt;u,v \in SZK: \exists u \rightsquigarrow v und v \rightsquigarrow u&amp;lt;/math&amp;gt;&lt;br /&gt;
: Kanten &amp;lt;math&amp;gt;\to&amp;lt;/math&amp;gt; Implikationen, Implikationen sind transitiv&lt;br /&gt;
: &amp;lt;math&amp;gt;\Rightarrow u \rightsquigarrow v \mathrel{\hat=} u \to v &amp;lt;/math&amp;gt; &amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp; &amp;lt;math&amp;gt;\to u \leftrightarrow v&amp;lt;/math&amp;gt; &amp;amp;nbsp; bzw. &amp;amp;nbsp; u == v&lt;br /&gt;
:: &amp;lt;math&amp;gt; v \rightsquigarrow u \mathrel{\hat=} v \to u &amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt; alle Knoten in einer SZK haben den gleichen Wahrheitswert true oder false&lt;br /&gt;
: aber &amp;lt;math&amp;gt;v_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v_{i+N} \mathrel{\hat=} x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\lnot x_i&amp;lt;/math&amp;gt; haben immer verschiedene Werte&lt;br /&gt;
: &amp;lt;math&amp;gt;\Rightarrow v_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v_{i+N}&amp;lt;/math&amp;gt; dürfen nicht in selber SZK sein, andernfalls fordert der Graph &amp;lt;math&amp;gt;x_i == \lnot x_i&amp;lt;/math&amp;gt;, was unmöglich ist.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* (4) Bilde den Komponentengraphen &amp;lt;math&amp;gt;\to&amp;lt;/math&amp;gt; azyklisch (zu jedem Knoten existiert Komplementärknoten mit negierter Variable)[jede SZK in je 1 Knoten kontrahieren]&lt;br /&gt;
** (b) bestehende topologische Sortierung&lt;br /&gt;
** (c) gehe in topologischer Sortierung von hinten nach vorne &lt;br /&gt;
*** (I) wenn aktueller Knoten noch keinen Wert hat: setze ihn auf true und Komplementoren false&lt;br /&gt;
*** (II) sonst: überspringe Knoten&lt;br /&gt;
&lt;br /&gt;
Beweis, dass ein Problem aus NP auch NP-vollständig ist&lt;br /&gt;
* Möglichkeit 1: z.B. 3-SAT (Satz von Cook): mühsam, aber mindestens für ein Problem unbermeidbar (für erstes)&lt;br /&gt;
* Möglichkeit 2: zeige dass  jedes Problem vom Typ A in eines von Typ B umwandelbar (in pol. Zeit)&lt;br /&gt;
** &amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt; Problem Type B nicht einfacher als Typ A&lt;br /&gt;
** falls Typ A NP-vollständig &amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt; Typ B auch&lt;br /&gt;
&lt;br /&gt;
==== Anwendung auf TSP ====&lt;br /&gt;
3-SAT &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; Hamiltonzyklus im gerichteten Graph &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; Hamiltonzyklus im ungerichteten Graph &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; TSP im gerwichteten ungerichteten Graph&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5716</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5716"/>
				<updated>2020-07-21T22:40:17Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Systematisches Erzeugen aller Permutationen */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann mit dem Algorithmus von Pandita (Indien, 1325-1400) -- dem (mehrmals wiederentdeckten) Standardalgorithmus für diese Aufgabe -- implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage Y und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei Y impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern: [[Image:Bild 11.jpg]]&lt;br /&gt;
: Jede logische Schaltung kann als SAT-Ausdruck geschrieben werden.&lt;br /&gt;
* Bsp.: Zuordnung von Heim und Auswärtsspielen beim Fußball&lt;br /&gt;
: &amp;lt;math&amp;gt;x_{it} = \begin{cases} &lt;br /&gt;
true,  &amp;amp; \mbox{Mannschaft i hat am Spieltag t Heimspiel} \\&lt;br /&gt;
false, &amp;amp; \mbox{Mannschaft i hat am Spieltag t Auswärtsspiel} &lt;br /&gt;
\end{cases}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
: 1. Nebenbedingung: spielt Mannschaft i am Spieltag t gegen Mannschaft j, muss gelten &amp;lt;math&amp;gt; x_{it} = \lnot x_{jt}&amp;lt;/math&amp;gt; &lt;br /&gt;
: 2. Nebenbedingung: Jede Mannschaft spielt gegen jede&lt;br /&gt;
: 3. Nebenbedingung: Jede Mannschaft spielt abwechselnd Heim und auswärts &amp;lt;math&amp;gt; x_{it} \neq x_{i(t+1)} &amp;lt;/math&amp;gt;&lt;br /&gt;
: Alle Bedingungen sollen gleichzeitig Erfüllt sein: &lt;br /&gt;
:: &amp;lt;math&amp;gt;y = \begin{cases} (x_{11} \neq x_{21}) \and (x_{31} \neq x_{41}) \and ... \\ &lt;br /&gt;
( x_{12} \neq x_{32} ) \and ... \\&lt;br /&gt;
( x_{11} \neq x_{12} ) \and ( x_{12} \neq x_{13} ) \and ...&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&lt;br /&gt;
: Frage: Kann man X so belegen, dass Y wahr ist? &lt;br /&gt;
: &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Nein, nur möglich wenn es nur 2 Mannschaften gibt und diese abwechselnd gegeneinander antreten. &lt;br /&gt;
&lt;br /&gt;
Normalformen für logische Ausdrücke zur Vereinfachung und Systematisierung&lt;br /&gt;
* 3-CNF (Konjunktionen-NF)&lt;br /&gt;
** jede Klausel enthält max 3 Variablen (genau 3 mit dummy Variablen)&lt;br /&gt;
** jede Klausel enthält nur &amp;lt;math&amp;gt; \or &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; /lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
** alle Klauseln sind durch &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft.&lt;br /&gt;
z.B. &amp;lt;math&amp;gt; ( x_1 \or x_2 \or \lnot x_4 ) \and ( \lnot x_2 \or x_3 \or x_4) \and (...) \and &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \Rightarrow &amp;lt;/math&amp;gt; Ausdruck ist wahr, wenn jede Klausel wahr ist. &lt;br /&gt;
: In jeder Klausel hat man 3 Chancen die Klausel wahr zu machen. &lt;br /&gt;
: Aber: Klauseln können sich widersprechen und nicht erfüllbar sein!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz:&amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; Jeder logische Ausdruck effizient (in pol. Zeit) in 3-CNF umwandelbar.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz v. Cook: &amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; 3-SAT (Erfüllbarkeitsproblem für Ausdrücke in 3-CNF) ist NP-vollständig&lt;br /&gt;
&lt;br /&gt;
zur Zeit ist kein effizienterer Algorithmus bekannt, als im schlechtesten Fall alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Belegungen von {&amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt;} auszuprobieren &lt;br /&gt;
&lt;br /&gt;
2-CNF: wie 3-CNF, nur 2 Variablen pro Klausel&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; effiziente Alg existieren, aber nicht jeder logische Ausdruck in 2-CNF transformierbar.&lt;br /&gt;
: z.B. Heim-Auswärtsproblem&lt;br /&gt;
&lt;br /&gt;
INF (Implikationen-NF):&lt;br /&gt;
* 2 Variablen pro Klausel, Operatoren &amp;lt;math&amp;gt; \implies und \lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
* Klauseln mit &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft. &lt;br /&gt;
&lt;br /&gt;
Satz: jede 2-CNF effizient in INF umwandelbat.&lt;br /&gt;
: &amp;lt;math&amp;gt; ( x_i \or x_j ) \rightsquigarrow ( \lnot x_i \implies x_j ) \and ( \lnot x_j \implies x_i )&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;  INF als gerichteter Graph schreibbar und mittels starker Zusammenhangskomponenten lösbar. &lt;br /&gt;
&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;x_1&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \or x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \implies x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_1 \implies x_2 (A)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_2 \implies x_1 (B)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;A \and B&amp;lt;/math&amp;gt;  &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 1 || 0 || 0 || 0 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden.&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== k-SAT, k=2 in pol. Zeit lösbar === &lt;br /&gt;
&lt;br /&gt;
==== Alg. 1 ====&lt;br /&gt;
(f. bei k) (nur für k=2 effizient) '''Randomisiert'''&lt;br /&gt;
* (0) initialisiere &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; beliebig&lt;br /&gt;
* (1) wiederhole &amp;lt;math&amp;gt;T_{max}&amp;lt;/math&amp;gt; - mal&lt;br /&gt;
** (a) wenn das aktuelle x den Ausdruck erfüllt: return x (x=[&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;
** (b) wähle zufällig eine Klausel, die nicht erfüllt ist&lt;br /&gt;
** (c) wähle in dieser Klausel zufällig eine der k Variablen und invertiere sie =&amp;gt; Klausel ist jetzt erfüllt&lt;br /&gt;
::: (andere können jetzt false geworden sein) &lt;br /&gt;
::: (&amp;lt;math&amp;gt;x_1 \or x_2 ) \and ( x_1 \or \lnot x_2 )&amp;lt;/math&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;math&amp;gt; x_1 = 0, x_2 = 0,&amp;lt;/math&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;math&amp;gt; x_2&amp;lt;/math&amp;gt; auf 1 =&amp;gt; 1. Klausel wahr, 2. falsch&lt;br /&gt;
* (2) return &amp;quot;keine Lösung gefunden&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Nach wie vielen Iterationen wird im Mittel eine Lösung gefunden?&lt;br /&gt;
* Ausdruck unerfüllbar =&amp;gt; Endlosschleife, Timeout nach &amp;lt;math&amp;gt;T_{max}&amp;lt;/math&amp;gt; Iterationen &lt;br /&gt;
* Ausrduck erfüllbar:&lt;br /&gt;
** falls k&amp;lt;math&amp;gt;\geq&amp;lt;/math&amp;gt;3: nach &amp;lt;math&amp;gt;O((\frac{2(k-1)}{k})^N)&amp;lt;/math&amp;gt; Iterationen wird Lösung gefunden&lt;br /&gt;
** k=3: &amp;lt;math&amp;gt;O((\frac{4}{3})^N)&amp;lt;/math&amp;gt; exponentielle Zeit, wie zu erwarten für NP-vollständiges Problem&lt;br /&gt;
** k=2: &amp;lt;math&amp;gt;O(N^2)&amp;lt;/math&amp;gt; Iterationen bis Lösung&lt;br /&gt;
&lt;br /&gt;
Beweis: Algorithmus entspricht im Wesentlichen dem '''Random Walk'''&lt;br /&gt;
: Sei &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; die korrekte Lösung und x die aktuelle Belegung&lt;br /&gt;
: RW: Stuhl i &amp;lt;math&amp;gt;\mathrel{\hat=}&amp;lt;/math&amp;gt; i Variablen zwischen &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; und x stimmen überein =&amp;gt; Ziel: erreiche Stuhl N &lt;br /&gt;
* (c):&lt;br /&gt;
** Fall 1: beide Variablen falsch =&amp;gt; egal welche wir invertieren, bewegen wir uns von Stuhl i zu i+1&lt;br /&gt;
** Fall 2: eine Variable ist falsch: &lt;br /&gt;
*** mit Wahrscheinlichkeit 1/2 wählen wir diese und gehen von i nach i+1&lt;br /&gt;
*** mit Wahrscheinlichkeit 1/2 wählen wir die andere und gehen von i nach i-1&lt;br /&gt;
&lt;br /&gt;
schlechtester Fall: Es existiert keine Lösung &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; und wir haben immer Fall 2&lt;br /&gt;
: =&amp;gt;RW braucht &amp;lt;math&amp;gt;O(N^2 - i^2)&amp;lt;/math&amp;gt; Schritte zum Stuhl N &amp;lt;math&amp;gt;\mathrel{\hat=} O(N^2)&amp;lt;/math&amp;gt; falls i anfangs zufällig ist &lt;br /&gt;
&lt;br /&gt;
==== Alg. 2 (det. Alg. für k=2 mittels SZK in gerichtetem Graphen) ====&lt;br /&gt;
: geg.: Ausdruck 2-CNF&lt;br /&gt;
* (1) wandle nach INF: ersetze jede Klausel &amp;lt;math&amp;gt;(x_i \or x_j)&amp;lt;/math&amp;gt; durch &amp;lt;math&amp;gt;(\lnot x_i \Rightarrow x_j) \and (\lnot x_j \Rightarrow x_i)&amp;lt;/math&amp;gt;&lt;br /&gt;
: (entsprechend, wenn in Originalklausel &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt; vorkommen)&lt;br /&gt;
* (2) repräsentiere den Ausdruck als Graph: &lt;br /&gt;
** (a) 2 Knoten pro Var:&amp;lt;math&amp;gt;v_i \mathrel{\hat=} x_i , v_{i+N} \mathrel{\hat=} \lnot x_i &amp;lt;/math&amp;gt;&lt;br /&gt;
** (b) Verbindung für jede Implikation durch korrespondierenden Knoten durch gerichtete Kante&lt;br /&gt;
&lt;br /&gt;
Bsp.:&lt;br /&gt;
&amp;lt;math&amp;gt;C_1 \and C_2 \Leftrightarrow (\lnot x_1 \Rightarrow x_2 ) \and (\lnot x_2 \Rightarrow x_1) \and (x_2 \Rightarrow x_3) \and (\lnot x_3 \Rightarrow \lnot x_2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* (3) Prüfe ob der Ausdruck erfüllbar ist. Bilde SZK des Graphen&lt;br /&gt;
: '''Satz''': Ausdruck erfüllbar &amp;lt;math&amp;gt;\Leftrightarrow \forall&amp;lt;/math&amp;gt;i: &amp;lt;math&amp;gt; v_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v_{i+N}&amp;lt;/math&amp;gt; sind in verschiedenen Komponenten&lt;br /&gt;
&lt;br /&gt;
Beweis: in jeder SZK gilt: &amp;lt;math&amp;gt;u,v \in SZK: \exists u \rightsquigarrow v und v \rightsquigarrow u&amp;lt;/math&amp;gt;&lt;br /&gt;
: Kanten &amp;lt;math&amp;gt;\to&amp;lt;/math&amp;gt; Implikationen, Implikationen sind transitiv&lt;br /&gt;
: &amp;lt;math&amp;gt;\Rightarrow u \rightsquigarrow v \mathrel{\hat=} u \to v &amp;lt;/math&amp;gt; &amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp; &amp;lt;math&amp;gt;\to u \leftrightarrow v&amp;lt;/math&amp;gt; &amp;amp;nbsp; bzw. &amp;amp;nbsp; u == v&lt;br /&gt;
:: &amp;lt;math&amp;gt; v \rightsquigarrow u \mathrel{\hat=} v \to u &amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt; alle Knoten in einer SZK haben den gleichen Wahrheitswert true oder false&lt;br /&gt;
: aber &amp;lt;math&amp;gt;v_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v_{i+N} \mathrel{\hat=} x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\lnot x_i&amp;lt;/math&amp;gt; haben immer verschiedene Werte&lt;br /&gt;
: &amp;lt;math&amp;gt;\Rightarrow v_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v_{i+N}&amp;lt;/math&amp;gt; dürfen nicht in selber SZK sein, andernfalls fordert der Graph &amp;lt;math&amp;gt;x_i == \lnot x_i&amp;lt;/math&amp;gt;, was unmöglich ist.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* (4) Bilde den Komponentengraphen &amp;lt;math&amp;gt;\to&amp;lt;/math&amp;gt; azyklisch (zu jedem Knoten existiert Komplementärknoten mit negierter Variable)[jede SZK in je 1 Knoten kontrahieren]&lt;br /&gt;
** (b) bestehende topologische Sortierung&lt;br /&gt;
** (c) gehe in topologischer Sortierung von hinten nach vorne &lt;br /&gt;
*** (I) wenn aktueller Knoten noch keinen Wert hat: setze ihn auf true und Komplementoren false&lt;br /&gt;
*** (II) sonst: überspringe Knoten&lt;br /&gt;
&lt;br /&gt;
Beweis, dass ein Problem aus NP auch NP-vollständig ist&lt;br /&gt;
* Möglichkeit 1: z.B. 3-SAT (Satz von Cook): mühsam, aber mindestens für ein Problem unbermeidbar (für erstes)&lt;br /&gt;
* Möglichkeit 2: zeige dass  jedes Problem vom Typ A in eines von Typ B umwandelbar (in pol. Zeit)&lt;br /&gt;
** &amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt; Problem Type B nicht einfacher als Typ A&lt;br /&gt;
** falls Typ A NP-vollständig &amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt; Typ B auch&lt;br /&gt;
&lt;br /&gt;
==== Anwendung auf TSP ====&lt;br /&gt;
3-SAT &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; Hamiltonzyklus im gerichteten Graph &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; Hamiltonzyklus im ungerichteten Graph &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; TSP im gerwichteten ungerichteten Graph&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5715</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5715"/>
				<updated>2020-07-21T22:37:26Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Systematisches Erzeugen aller Permutationen */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann mit dem Algorithmus von Pandit (Indien, 1325-1400) -- dem (mehrmals wiederentdeckten) Standardalgorithmus für diese Aufgabe -- implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage Y und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei Y impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern: [[Image:Bild 11.jpg]]&lt;br /&gt;
: Jede logische Schaltung kann als SAT-Ausdruck geschrieben werden.&lt;br /&gt;
* Bsp.: Zuordnung von Heim und Auswärtsspielen beim Fußball&lt;br /&gt;
: &amp;lt;math&amp;gt;x_{it} = \begin{cases} &lt;br /&gt;
true,  &amp;amp; \mbox{Mannschaft i hat am Spieltag t Heimspiel} \\&lt;br /&gt;
false, &amp;amp; \mbox{Mannschaft i hat am Spieltag t Auswärtsspiel} &lt;br /&gt;
\end{cases}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
: 1. Nebenbedingung: spielt Mannschaft i am Spieltag t gegen Mannschaft j, muss gelten &amp;lt;math&amp;gt; x_{it} = \lnot x_{jt}&amp;lt;/math&amp;gt; &lt;br /&gt;
: 2. Nebenbedingung: Jede Mannschaft spielt gegen jede&lt;br /&gt;
: 3. Nebenbedingung: Jede Mannschaft spielt abwechselnd Heim und auswärts &amp;lt;math&amp;gt; x_{it} \neq x_{i(t+1)} &amp;lt;/math&amp;gt;&lt;br /&gt;
: Alle Bedingungen sollen gleichzeitig Erfüllt sein: &lt;br /&gt;
:: &amp;lt;math&amp;gt;y = \begin{cases} (x_{11} \neq x_{21}) \and (x_{31} \neq x_{41}) \and ... \\ &lt;br /&gt;
( x_{12} \neq x_{32} ) \and ... \\&lt;br /&gt;
( x_{11} \neq x_{12} ) \and ( x_{12} \neq x_{13} ) \and ...&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&lt;br /&gt;
: Frage: Kann man X so belegen, dass Y wahr ist? &lt;br /&gt;
: &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Nein, nur möglich wenn es nur 2 Mannschaften gibt und diese abwechselnd gegeneinander antreten. &lt;br /&gt;
&lt;br /&gt;
Normalformen für logische Ausdrücke zur Vereinfachung und Systematisierung&lt;br /&gt;
* 3-CNF (Konjunktionen-NF)&lt;br /&gt;
** jede Klausel enthält max 3 Variablen (genau 3 mit dummy Variablen)&lt;br /&gt;
** jede Klausel enthält nur &amp;lt;math&amp;gt; \or &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; /lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
** alle Klauseln sind durch &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft.&lt;br /&gt;
z.B. &amp;lt;math&amp;gt; ( x_1 \or x_2 \or \lnot x_4 ) \and ( \lnot x_2 \or x_3 \or x_4) \and (...) \and &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \Rightarrow &amp;lt;/math&amp;gt; Ausdruck ist wahr, wenn jede Klausel wahr ist. &lt;br /&gt;
: In jeder Klausel hat man 3 Chancen die Klausel wahr zu machen. &lt;br /&gt;
: Aber: Klauseln können sich widersprechen und nicht erfüllbar sein!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz:&amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; Jeder logische Ausdruck effizient (in pol. Zeit) in 3-CNF umwandelbar.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz v. Cook: &amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; 3-SAT (Erfüllbarkeitsproblem für Ausdrücke in 3-CNF) ist NP-vollständig&lt;br /&gt;
&lt;br /&gt;
zur Zeit ist kein effizienterer Algorithmus bekannt, als im schlechtesten Fall alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Belegungen von {&amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt;} auszuprobieren &lt;br /&gt;
&lt;br /&gt;
2-CNF: wie 3-CNF, nur 2 Variablen pro Klausel&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; effiziente Alg existieren, aber nicht jeder logische Ausdruck in 2-CNF transformierbar.&lt;br /&gt;
: z.B. Heim-Auswärtsproblem&lt;br /&gt;
&lt;br /&gt;
INF (Implikationen-NF):&lt;br /&gt;
* 2 Variablen pro Klausel, Operatoren &amp;lt;math&amp;gt; \implies und \lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
* Klauseln mit &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft. &lt;br /&gt;
&lt;br /&gt;
Satz: jede 2-CNF effizient in INF umwandelbat.&lt;br /&gt;
: &amp;lt;math&amp;gt; ( x_i \or x_j ) \rightsquigarrow ( \lnot x_i \implies x_j ) \and ( \lnot x_j \implies x_i )&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;  INF als gerichteter Graph schreibbar und mittels starker Zusammenhangskomponenten lösbar. &lt;br /&gt;
&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;x_1&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \or x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \implies x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_1 \implies x_2 (A)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_2 \implies x_1 (B)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;A \and B&amp;lt;/math&amp;gt;  &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 1 || 0 || 0 || 0 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden.&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== k-SAT, k=2 in pol. Zeit lösbar === &lt;br /&gt;
&lt;br /&gt;
==== Alg. 1 ====&lt;br /&gt;
(f. bei k) (nur für k=2 effizient) '''Randomisiert'''&lt;br /&gt;
* (0) initialisiere &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; beliebig&lt;br /&gt;
* (1) wiederhole &amp;lt;math&amp;gt;T_{max}&amp;lt;/math&amp;gt; - mal&lt;br /&gt;
** (a) wenn das aktuelle x den Ausdruck erfüllt: return x (x=[&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;
** (b) wähle zufällig eine Klausel, die nicht erfüllt ist&lt;br /&gt;
** (c) wähle in dieser Klausel zufällig eine der k Variablen und invertiere sie =&amp;gt; Klausel ist jetzt erfüllt&lt;br /&gt;
::: (andere können jetzt false geworden sein) &lt;br /&gt;
::: (&amp;lt;math&amp;gt;x_1 \or x_2 ) \and ( x_1 \or \lnot x_2 )&amp;lt;/math&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;math&amp;gt; x_1 = 0, x_2 = 0,&amp;lt;/math&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;math&amp;gt; x_2&amp;lt;/math&amp;gt; auf 1 =&amp;gt; 1. Klausel wahr, 2. falsch&lt;br /&gt;
* (2) return &amp;quot;keine Lösung gefunden&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Nach wie vielen Iterationen wird im Mittel eine Lösung gefunden?&lt;br /&gt;
* Ausdruck unerfüllbar =&amp;gt; Endlosschleife, Timeout nach &amp;lt;math&amp;gt;T_{max}&amp;lt;/math&amp;gt; Iterationen &lt;br /&gt;
* Ausrduck erfüllbar:&lt;br /&gt;
** falls k&amp;lt;math&amp;gt;\geq&amp;lt;/math&amp;gt;3: nach &amp;lt;math&amp;gt;O((\frac{2(k-1)}{k})^N)&amp;lt;/math&amp;gt; Iterationen wird Lösung gefunden&lt;br /&gt;
** k=3: &amp;lt;math&amp;gt;O((\frac{4}{3})^N)&amp;lt;/math&amp;gt; exponentielle Zeit, wie zu erwarten für NP-vollständiges Problem&lt;br /&gt;
** k=2: &amp;lt;math&amp;gt;O(N^2)&amp;lt;/math&amp;gt; Iterationen bis Lösung&lt;br /&gt;
&lt;br /&gt;
Beweis: Algorithmus entspricht im Wesentlichen dem '''Random Walk'''&lt;br /&gt;
: Sei &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; die korrekte Lösung und x die aktuelle Belegung&lt;br /&gt;
: RW: Stuhl i &amp;lt;math&amp;gt;\mathrel{\hat=}&amp;lt;/math&amp;gt; i Variablen zwischen &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; und x stimmen überein =&amp;gt; Ziel: erreiche Stuhl N &lt;br /&gt;
* (c):&lt;br /&gt;
** Fall 1: beide Variablen falsch =&amp;gt; egal welche wir invertieren, bewegen wir uns von Stuhl i zu i+1&lt;br /&gt;
** Fall 2: eine Variable ist falsch: &lt;br /&gt;
*** mit Wahrscheinlichkeit 1/2 wählen wir diese und gehen von i nach i+1&lt;br /&gt;
*** mit Wahrscheinlichkeit 1/2 wählen wir die andere und gehen von i nach i-1&lt;br /&gt;
&lt;br /&gt;
schlechtester Fall: Es existiert keine Lösung &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; und wir haben immer Fall 2&lt;br /&gt;
: =&amp;gt;RW braucht &amp;lt;math&amp;gt;O(N^2 - i^2)&amp;lt;/math&amp;gt; Schritte zum Stuhl N &amp;lt;math&amp;gt;\mathrel{\hat=} O(N^2)&amp;lt;/math&amp;gt; falls i anfangs zufällig ist &lt;br /&gt;
&lt;br /&gt;
==== Alg. 2 (det. Alg. für k=2 mittels SZK in gerichtetem Graphen) ====&lt;br /&gt;
: geg.: Ausdruck 2-CNF&lt;br /&gt;
* (1) wandle nach INF: ersetze jede Klausel &amp;lt;math&amp;gt;(x_i \or x_j)&amp;lt;/math&amp;gt; durch &amp;lt;math&amp;gt;(\lnot x_i \Rightarrow x_j) \and (\lnot x_j \Rightarrow x_i)&amp;lt;/math&amp;gt;&lt;br /&gt;
: (entsprechend, wenn in Originalklausel &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt; vorkommen)&lt;br /&gt;
* (2) repräsentiere den Ausdruck als Graph: &lt;br /&gt;
** (a) 2 Knoten pro Var:&amp;lt;math&amp;gt;v_i \mathrel{\hat=} x_i , v_{i+N} \mathrel{\hat=} \lnot x_i &amp;lt;/math&amp;gt;&lt;br /&gt;
** (b) Verbindung für jede Implikation durch korrespondierenden Knoten durch gerichtete Kante&lt;br /&gt;
&lt;br /&gt;
Bsp.:&lt;br /&gt;
&amp;lt;math&amp;gt;C_1 \and C_2 \Leftrightarrow (\lnot x_1 \Rightarrow x_2 ) \and (\lnot x_2 \Rightarrow x_1) \and (x_2 \Rightarrow x_3) \and (\lnot x_3 \Rightarrow \lnot x_2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* (3) Prüfe ob der Ausdruck erfüllbar ist. Bilde SZK des Graphen&lt;br /&gt;
: '''Satz''': Ausdruck erfüllbar &amp;lt;math&amp;gt;\Leftrightarrow \forall&amp;lt;/math&amp;gt;i: &amp;lt;math&amp;gt; v_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v_{i+N}&amp;lt;/math&amp;gt; sind in verschiedenen Komponenten&lt;br /&gt;
&lt;br /&gt;
Beweis: in jeder SZK gilt: &amp;lt;math&amp;gt;u,v \in SZK: \exists u \rightsquigarrow v und v \rightsquigarrow u&amp;lt;/math&amp;gt;&lt;br /&gt;
: Kanten &amp;lt;math&amp;gt;\to&amp;lt;/math&amp;gt; Implikationen, Implikationen sind transitiv&lt;br /&gt;
: &amp;lt;math&amp;gt;\Rightarrow u \rightsquigarrow v \mathrel{\hat=} u \to v &amp;lt;/math&amp;gt; &amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp; &amp;lt;math&amp;gt;\to u \leftrightarrow v&amp;lt;/math&amp;gt; &amp;amp;nbsp; bzw. &amp;amp;nbsp; u == v&lt;br /&gt;
:: &amp;lt;math&amp;gt; v \rightsquigarrow u \mathrel{\hat=} v \to u &amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt; alle Knoten in einer SZK haben den gleichen Wahrheitswert true oder false&lt;br /&gt;
: aber &amp;lt;math&amp;gt;v_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v_{i+N} \mathrel{\hat=} x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\lnot x_i&amp;lt;/math&amp;gt; haben immer verschiedene Werte&lt;br /&gt;
: &amp;lt;math&amp;gt;\Rightarrow v_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v_{i+N}&amp;lt;/math&amp;gt; dürfen nicht in selber SZK sein, andernfalls fordert der Graph &amp;lt;math&amp;gt;x_i == \lnot x_i&amp;lt;/math&amp;gt;, was unmöglich ist.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* (4) Bilde den Komponentengraphen &amp;lt;math&amp;gt;\to&amp;lt;/math&amp;gt; azyklisch (zu jedem Knoten existiert Komplementärknoten mit negierter Variable)[jede SZK in je 1 Knoten kontrahieren]&lt;br /&gt;
** (b) bestehende topologische Sortierung&lt;br /&gt;
** (c) gehe in topologischer Sortierung von hinten nach vorne &lt;br /&gt;
*** (I) wenn aktueller Knoten noch keinen Wert hat: setze ihn auf true und Komplementoren false&lt;br /&gt;
*** (II) sonst: überspringe Knoten&lt;br /&gt;
&lt;br /&gt;
Beweis, dass ein Problem aus NP auch NP-vollständig ist&lt;br /&gt;
* Möglichkeit 1: z.B. 3-SAT (Satz von Cook): mühsam, aber mindestens für ein Problem unbermeidbar (für erstes)&lt;br /&gt;
* Möglichkeit 2: zeige dass  jedes Problem vom Typ A in eines von Typ B umwandelbar (in pol. Zeit)&lt;br /&gt;
** &amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt; Problem Type B nicht einfacher als Typ A&lt;br /&gt;
** falls Typ A NP-vollständig &amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt; Typ B auch&lt;br /&gt;
&lt;br /&gt;
==== Anwendung auf TSP ====&lt;br /&gt;
3-SAT &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; Hamiltonzyklus im gerichteten Graph &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; Hamiltonzyklus im ungerichteten Graph &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; TSP im gerwichteten ungerichteten Graph&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Assoziative_Arrays&amp;diff=5714</id>
		<title>Assoziative Arrays</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Assoziative_Arrays&amp;diff=5714"/>
				<updated>2020-07-09T19:09:19Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Das JSON-Datenformat */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Datenstruktur-Dreieck für assoziative Arrays ==&lt;br /&gt;
&lt;br /&gt;
Assoziative Arrays sind eine der wichtigsten Anwendungen für Suchalgorithmen und Suchbäume. Bevor wir dies im Detail erklären, wollen wir jedoch noch einmal einen Blick auf das Datenstruktur-Dreieck aus der ersten Vorlesung werfen, das am Beispiel der assoziativen Arrays sehr schön illustriert werden kann. Wir zeigen es hier noch einmal:&lt;br /&gt;
&lt;br /&gt;
[[Image:Dt dreieck.png|300px]]&lt;br /&gt;
&lt;br /&gt;
Wir erinnern daran, dass man zwei Ecken des Dreicks wählen muss, um eine Datenstruktur zu definieren. Wir werden im Folgenden zeigen, wie Python durch Festlegen der erlaubten Operationen und deren Bedeutung den abstrakten Datentyp &amp;quot;Assoziatives Array&amp;quot; definiert, wie durch Festlegen eines Speicherlayouts und der Bedeutung der gespeicherten Entitäten das Standard-Datenformat &amp;quot;JSON&amp;quot; definiert ist, und wie durch effiziente Implementation der festgelegten Operationen mit jeweils passendem Speicherlayout die Datenstruktur auf unterschiedliche Arten realisiert werden kann.&lt;br /&gt;
&lt;br /&gt;
== Definition des abstrakten Datentyps  ==&lt;br /&gt;
&lt;br /&gt;
Assoziative Arrays können genau wie gewöhnliche Arrays benutzt werden, sie unterstützen also den lesenden und schreibenden Zugriff über den Indexoperator &amp;lt;tt&amp;gt;a[...]&amp;lt;/tt&amp;gt;. Im Unterschied zum gewöhnlichen Array, wo die Indizes ganze Zahlen im Bereich &amp;lt;math&amp;gt; i \in 0 \ldots N-1&amp;lt;/math&amp;gt; sein muss, kann der Typ der Indizes jetzt ''beliebig'' sein. Wir verwenden dafür den Begriff &amp;quot;Schlüssel&amp;quot; (engl.: key):&lt;br /&gt;
    a[key] = value   # Speichern des Wertes 'value' unter dem Schlüssel 'key'&lt;br /&gt;
    value  = a[key]  # Auslesen des unter dem Schlüssel 'key' gespeicherten Wertes&lt;br /&gt;
Eine typische Anwendung ist ein Wörterbuch&lt;br /&gt;
    x = toEnglish['Baum']   # ergibt 'tree'&lt;br /&gt;
In diesem Fall ist der Typ des Schlüssels &amp;lt;tt&amp;gt;string&amp;lt;/tt&amp;gt;. Dies ist in der Praxis der häufigste Fall, weshalb assoziative Arrays oft als ''Dictionary'' bezeichnet werden (so auch in Python, hier heißt der Typ &amp;lt;tt&amp;gt;dict&amp;lt;/tt&amp;gt;). Im allgemeinen kann aber jeder Typ als Schlüssel benutzt werden, der eine der folgenden Anforderungen erfüllt:&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|-align=&amp;quot;center&amp;quot;  &lt;br /&gt;
! unterstützte Vergleichsoperationen für Schlüssel&lt;br /&gt;
! mögliche Implementation des assoziativen Arrays&lt;br /&gt;
|-  &lt;br /&gt;
| Identitätstest: &amp;lt;br&amp;gt;&amp;lt;tt&amp;gt;key1 == key2&amp;lt;/tt&amp;gt;&lt;br /&gt;
| sequentielle Suche&lt;br /&gt;
|-&lt;br /&gt;
| Ordnungsrelation: &amp;lt;br&amp;gt;&amp;lt;tt&amp;gt;key1 &amp;amp;lt; key2&amp;lt;/tt&amp;gt; oder&amp;lt;br&amp;gt;&amp;lt;tt&amp;gt;cmp(key1, key2)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| Suchbaum (auch binäre Suche, falls keine neuen Schlüssel eingefügt und keine gelöscht werden)&lt;br /&gt;
|-&lt;br /&gt;
| Identitätstest und Hashfunktion:&amp;lt;br&amp;gt;&amp;lt;tt&amp;gt;key1 == key2&amp;lt;/tt&amp;gt; und &amp;lt;br&amp;gt;&amp;lt;tt&amp;gt;hash(key1) == hash(key2)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| Hashtabelle&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Wenn über die Schlüssel mehr bekannt ist (eine Ordnungsrelation oder eine Hashfunktion statt einer bloßen Indentitätsprüfung), kann man entsprechend bessere Datenstrukturen (Suchbäume oder Hashtabellen statt sequentieller Suche) verwenden, deren Zugriffsfunktionen wesentlich effizienter sind (sequentielle Suche ist ja nur in O(N)). &lt;br /&gt;
&lt;br /&gt;
Zu den beiden obigen Zugriffsfunktionen treten in Python noch drei weitere Funktionen hinzu: eine um zu testen, ob ein Schlüssel vorhanden ist, eine um einen Schlüssel und die darunter gespeicherten Daten zu löschen, sowie eine, die die Größe des Arrays (Anzahl der gespeicherten Schlüssel/Wert-Paare) zurückgibt:&lt;br /&gt;
    if a.has_key(key): # Testen, ob Schlüssel 'key' existiert&lt;br /&gt;
        del a[key]     # Schlüssel 'key' und zugehörige Daten aus dem Array entfernen&lt;br /&gt;
    print len(a)       # Größe des Arrays ausgeben&lt;br /&gt;
&lt;br /&gt;
Die Syntax der aufgeführten Funktionen gilt für die ''Benutzung'' eines assoziativen Arrays. Will man einen solchen Datentyp implementieren, muss man die entsprechende Funktionalität als Methoden der jeweiligen Klasse zur Verfügung stellen. Der Python-Interpreter transformiert den Index-/Schlüsselzugriff &amp;lt;tt&amp;gt;a[key]&amp;lt;/tt&amp;gt; sowie die &amp;lt;tt&amp;gt;len&amp;lt;/tt&amp;gt;- und &amp;lt;tt&amp;gt;del&amp;lt;/tt&amp;gt;-Operatoren automatisch in Aufrufe der jeweiligen Methode, wie die folgende Tabelle verdeutlicht. Zur vollständigen Definition der Bedeutung der einzelnen Operationen (wie vom Datenstruktur-Dreieck gefordert) gehört außerdem die Spezifikation des Verhaltens im Fehlerfall (wenn z.B. ein angeforderter Schlüssel nicht existiert).&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|-align=&amp;quot;center&amp;quot;  &lt;br /&gt;
! Operation&lt;br /&gt;
! von Python intern generierter Methodenaufruf&lt;br /&gt;
! zu implementierende Methodensignatur&lt;br /&gt;
! Bedeutung&lt;br /&gt;
|-  &lt;br /&gt;
| &amp;lt;tt&amp;gt;a[key] = value&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;a.__setitem__(key, value)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;def __setitem__(self, key, value):&amp;lt;/tt&amp;gt;&lt;br /&gt;
| * wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; bereits existiert: ersetze die zugehörigen Daten durch &amp;lt;tt&amp;gt;value&amp;lt;/tt&amp;gt;&amp;lt;br&amp;gt;* wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; noch nicht existiert: lege einen neuen Schlüssel an und speichere &amp;lt;tt&amp;gt;value&amp;lt;/tt&amp;gt; als zugehörigeaten&lt;br /&gt;
|-  &lt;br /&gt;
| &amp;lt;tt&amp;gt;value = a[key]&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;a.__getitem__(key)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;def __getitem__(self, key):&amp;lt;/tt&amp;gt;&lt;br /&gt;
| * wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; existiert: gebe die zugehörigen Daten zurück&amp;lt;br&amp;gt;* wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; nicht existiert: löse &amp;lt;tt&amp;gt;KeyError&amp;lt;/tt&amp;gt;-Exception aus&lt;br /&gt;
|-  &lt;br /&gt;
| &amp;lt;tt&amp;gt;a.has_key(key)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;a.has_key(key)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;def has_key(self, key):&amp;lt;/tt&amp;gt;&lt;br /&gt;
| gibt &amp;lt;tt&amp;gt;True&amp;lt;/tt&amp;gt; zurück wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; existiert, sonst &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-  &lt;br /&gt;
| &amp;lt;tt&amp;gt;del a[key]&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;a.__delitem__(key)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;def __delitem__(self, key):&amp;lt;/tt&amp;gt;&lt;br /&gt;
| * wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; existiert: entferne diesen Schlüssel und die zugehörigen Daten&amp;lt;br&amp;gt;* wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; nicht existiert: löse &amp;lt;tt&amp;gt;KeyError&amp;lt;/tt&amp;gt;-Exception aus&lt;br /&gt;
|-  &lt;br /&gt;
| &amp;lt;tt&amp;gt;len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;a.__len__()&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;def __len__(self):&amp;lt;/tt&amp;gt;&lt;br /&gt;
| gibt die Größe des Arrays zurück&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Aufgrund der Definition ist klar, das jeder Schlüssel nur einmal im Array vorkommen kann. Die Definition des Abstrakten Datentyps &amp;quot;assoziatives Array&amp;quot; erlaubt es uns, derartige Arrays auf verschiedenste Art zu implementieren, ohne dass sich an der Benutzung (also daran, wie man die Arrayfunktionalität später aufruft) irgend etwas ändert.&lt;br /&gt;
&lt;br /&gt;
== Das JSON-Datenformat ==&lt;br /&gt;
&lt;br /&gt;
Die zweite Kante des Datenstruktur-Dreiecks ist das &amp;quot;Datenformat&amp;quot;. Hier legt man Speicherlayout und Bedeutung fest. Ein Datenformat dient vor allem zum Speichern von Daten auf Festplatte und zum Austausch von Daten zwischen verschiedenen Programmen bzw. Internetseiten. Im Fall von assoziativen Arrays setzt sich dafür das [http://www.json.org/ JSON-Format] immer mehr durch, weil es einfach und trotzdem mächtig ist. Es eignet sich sehr gut zum Speichern von assoziativen Arrays (also von Schlüssel/Wert Paaren) und unterstützt außerdem gewöhnliche Arrays und hierarchische Strukturen, weil die Werte wiederum (gewöhnliche oder assoziative) Arrays sein dürfen.&lt;br /&gt;
&lt;br /&gt;
Das Speicherlayout einer JSON-Dateien ist definiert als eine Bytefolge, die als Zeichenfolge gemäß [http://de.wikipedia.org/wiki/UTF-8 UTF-8-Standard] interpretiert wird. Dies hat zwei Vorteile: einerseits ist das Format dadurch mit allen gängigen Systemen kompatibel und überall gleich, andererseits kann jedes JSON-File einfach in einem Texteditor geöffnet und editiert werden und ist für Menschen und Maschinen gleichermaßen leicht lesbar.&lt;br /&gt;
&lt;br /&gt;
Die Zuordung einer Bedeutung zu einem gegebenen Speicherinhalt erfolgt in JSON mit Hilfe einer Grammatik. Ein JSON-File enthält entweder ein gewöhnliches Array oder ein assoziatives Array (Dictionary):&lt;br /&gt;
  JSON_file := array&lt;br /&gt;
             | dictionary&lt;br /&gt;
Ein gewöhnliches Array wird als Folge von einem oder mehreren Elementen geschrieben, die durch Komma getrennt und in eckigen Klammern eingeschlossen sind (Zeichen, die in der Grammatik in einfache Anführungszeichen eingeschlossen sind, müssen explizit im JSON-File stehen). Leere Arrays sind ebenfalls erlaubt:&lt;br /&gt;
  array    := '[' elements ']'&lt;br /&gt;
            | '[' ']'&lt;br /&gt;
  elements := value&lt;br /&gt;
            | value ',' elements&lt;br /&gt;
Ein Dictionary wird in ähnlicher Weise als Folge von Schlüssel/Wert-Paaren geschrieben, die durch Komma getrennt und in geschweiften Klammern eingeschlossen sind. Leere Dictionaries sind erlaubt. Die Schlüssel müssen immer Strings sein, gefolgt von einem Doppelpunkt:&lt;br /&gt;
  dictionary := '{' pairs '}'&lt;br /&gt;
              | '{' '}'&lt;br /&gt;
  pairs      := string ':' value&lt;br /&gt;
              | string ':' value ',' pairs&lt;br /&gt;
Strings sind Zeichenfolgen (inklusive einiger Sonderzeichen wie &amp;quot;\n&amp;quot; für einen Zeilenumbruch), die in doppelte Anführungszeichen eingeschlossen sind, oder der Leerstring:&lt;br /&gt;
  string := '&amp;quot;' '&amp;quot;'  &lt;br /&gt;
          | '&amp;quot;'characters'&amp;quot;'&lt;br /&gt;
Werte können Zahlen (ganze oder reelle Zahlen, definiert wie in Python), Boolesche Werte ('true' oder 'false'), Strings oder 'null' sein. Außerdem können Arrays und Dictionaries wiederum als Werte verwendet werden, wodurch sich beliebig tief geschachtelte, hierarchische Datenstrukturen ergeben:&lt;br /&gt;
  value := number | string | boolean | null | array | dictionary&lt;br /&gt;
Hier ist ein einfaches Beispiel für ein JSON-File, das Ausschnitte aus einer Studenten-Datenbank enthält:&lt;br /&gt;
   {&lt;br /&gt;
       &amp;quot;Müller, Friedrich&amp;quot; : {&lt;br /&gt;
            &amp;quot;Mathematik&amp;quot; : 2.0,&lt;br /&gt;
            &amp;quot;ALDA&amp;quot; : 1.3&lt;br /&gt;
       },&lt;br /&gt;
       &amp;quot;Weise, Anna&amp;quot; : {&lt;br /&gt;
            &amp;quot;Mathematik&amp;quot; : 1.0,&lt;br /&gt;
            &amp;quot;Philosophie&amp;quot;: 1.3&lt;br /&gt;
       }&lt;br /&gt;
   }&lt;br /&gt;
Das JSON-Format ist syntaktisch der Sprache Python sehr nahe, und kann mit einigen verherigen Definitionen direkt durch die &amp;lt;tt&amp;gt;eval()&amp;lt;/tt&amp;gt;-Funktion in ein Python-Dictionary oder -Array umgewandelt werden.&lt;br /&gt;
   # don't do this - it is highly '''unsafe''' and dangerous&lt;br /&gt;
   true, false, null = True, False, None                  # fehlende Konstanten defininieren&lt;br /&gt;
   res = eval(file(&amp;quot;test.json&amp;quot;).read().decode(&amp;quot;utf_8&amp;quot;))   # File einlesen und als Python-Code ausführen&lt;br /&gt;
Dies sollte man jedoch '''auf keinen Fall''' tun, weil ein Hacker dadurch beliebigen Code ausführen könnte, den er vorher in das File 'test.json' eingeschmuggelt hat. Da die Funktion &amp;lt;tt&amp;gt;eval()&amp;lt;/tt&amp;gt; nur prüft, ob der Ausdruck gültiges Python ist, aber nicht, ob das File gültiges JSON (also nur Daten, aber keinen ausführbaren Code) enthält, kann man dies nicht erkennen oder verhindern. Deshalb sollte man zum Einlesen von JSON stets das Python-Modul [http://docs.python.org/library/json.html json] verwenden, das ein manipuliertes File einfach zurückweisen würde:&lt;br /&gt;
   # sicheres Einlesen und Konvertieren&lt;br /&gt;
   import json&lt;br /&gt;
   &lt;br /&gt;
   with open('test.json', encoding='utf-8') as f:    # File im UTF-8 Format öffnen &lt;br /&gt;
       res = json.load(f)                            # und als json einlesen&lt;br /&gt;
&lt;br /&gt;
==Implementation von assoziativen Array-Klassen==&lt;br /&gt;
&lt;br /&gt;
Die dritte Kante des Datenstruktur-Dreiecks bezieht sich schließlich auf die Realisierung der Datenstruktur als Klasse, indem man auf geeignet organisiertem Speicher die geforderten Operationen implementiert. In Python ist mit der Klasse &amp;lt;tt&amp;gt;dict&amp;lt;/tt&amp;gt; eine sehr leistungsfähige Implementation eines assoziativen Arrays integraler Bestandteil der Sprache. Diese Implementation beruht auf dem Konzept der Hashtabellen, das wir in der Vorlesung [[Hashing und Hashtabellen|später behandeln]]. Man benötigt dafür eine Funktion &amp;lt;tt&amp;gt;hash(key)&amp;lt;/tt&amp;gt;, die in Python für alle Standarddatentypen bereits implementiert ist. In diesem Abschnitt wollen wir zwei alternative Implementationen auf der Basis von sequentieller Suche und auf der Basis von Suchbäumen betrachten.&lt;br /&gt;
&lt;br /&gt;
=== Realisierung durch sequentielle Suche===&lt;br /&gt;
&lt;br /&gt;
Wenn für die Schlüssel nur ein Identitätsvergleich &lt;br /&gt;
   key1 == key2&lt;br /&gt;
definiert ist, hat man keine Möglichkeit, die Schlüsselsuche durch eine spezielle Datenstruktur zu beschleunigen. Man speichert die Schlüssel/Wert-Paare dann einfach in einem gewöhnlichen Array, das man sequentiell durchsucht. Dazu implementieren wir zunächst eine Hilfsklasse, die Schlüssel/Wert-Paare aufnimmt:&lt;br /&gt;
   class KeyValuePair:&lt;br /&gt;
       def __init__(self, key, value):&lt;br /&gt;
           self.key = key&lt;br /&gt;
           self.value = value&lt;br /&gt;
Die Arrayklasse speichert die Paare in einem Array &amp;lt;tt&amp;gt;self.data&amp;lt;/tt&amp;gt;, dessen aktuelle Länge der Größe des assoziativen Arrays entspricht. Damit ist das Speicherlayout der Klasse festgelegt:&lt;br /&gt;
   class SequentialSearchArray:&lt;br /&gt;
       def __init__(self):&lt;br /&gt;
           self.data = []&lt;br /&gt;
       def __len__(self):&lt;br /&gt;
           return len(self.data)&lt;br /&gt;
Um auf die Daten zugreifen zu können, müssen wir nach dem richtigen Schlüssel suchen. Dazu implementieren wir die Hilfsfunktion &amp;lt;tt&amp;gt;findKey&amp;lt;/tt&amp;gt;, die den Index des Schlüssels zurückgibt, oder &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, wenn der Schlüssel nicht existiert:&lt;br /&gt;
       def findKey(self, key):&lt;br /&gt;
           for k in xrange(len(self.data)):&lt;br /&gt;
               if key == self.data[k].key:&lt;br /&gt;
                   return k&lt;br /&gt;
           return None&lt;br /&gt;
Beim Einfügen eines Elements müssen wir zuerst prüfen, ob es den Schlüssel schon gibt, und dann entweder die daten überschreiben oder ein neues Element anfügen:&lt;br /&gt;
       def __setitem__(self, key, value):&lt;br /&gt;
           k = self.findKey(key)&lt;br /&gt;
           if k is None:&lt;br /&gt;
               self.data.append(KeyValuePair(key, value))  # neues Paar einfügen&lt;br /&gt;
           else:&lt;br /&gt;
               self.data[k].value = value                  # Daten ersetzen&lt;br /&gt;
Die Suche hingegen löst eine Exception aus, wenn der Schlüssel nicht gefunden wurde:&lt;br /&gt;
       def __getitem__(self, key):&lt;br /&gt;
           k = self.findKey(key)&lt;br /&gt;
           if k is None:&lt;br /&gt;
               raise KeyError(key)        # Schlüssel nicht gefunden =&amp;gt; Fehler&lt;br /&gt;
           else:&lt;br /&gt;
               return self.data[k].value  # Schlüssel gefunden =&amp;gt; Daten zurückgeben&lt;br /&gt;
Die übrigen geforderten Funktionen sind ebenso einfach zu implementieren:&lt;br /&gt;
       def has_key(self, key):&lt;br /&gt;
           return (self.findKey(key) is not None)&lt;br /&gt;
       &lt;br /&gt;
       def __delitem__(self, key):&lt;br /&gt;
           k = self.findKey(key)&lt;br /&gt;
           if k is None:&lt;br /&gt;
               raise KeyError(key)     # Schlüssel nicht gefunden =&amp;gt; Fehler&lt;br /&gt;
           else:&lt;br /&gt;
               del self.data[k]        # Schlüssel gefunden =&amp;gt; löschen&lt;br /&gt;
Wegen der sequentiellen Suche hat der Zugriff auf ein Element in dieser Datenstruktur die Komplexität O(len(a)).&lt;br /&gt;
&lt;br /&gt;
=== Realisierung als Suchbaum ===&lt;br /&gt;
&lt;br /&gt;
Wenn für den Schlüsseltyp des assoziativen Arrays eine Ordnung definiert ist (wenn also &amp;lt;tt&amp;gt;key1 &amp;lt; key2&amp;lt;/tt&amp;gt; oder &amp;lt;tt&amp;gt;cmp(key1, key2)&amp;lt;/tt&amp;gt; unterstützt werden), kann man das Indexierungsproblem auf das Suchproblem zurückführen. Dann kann das assoziative Array effizient als selbst-balancierender Suchbaum imlementiert werden, so dass die Zugriffsfunktionen nur noch eine Komplexität in O(log(len(a))) haben. Die Datenstruktur des Suchbaums muss dafür so erweitert werden, dass zu jedem Schlüssel auch die zugehörigen Daten gespeichert werden. Man erweitert die Node-Klasse deshalb um ein Feld &amp;quot;value&amp;quot;:&lt;br /&gt;
    class Node:&lt;br /&gt;
        def __init__(self, key, value):&lt;br /&gt;
            self.key = key&lt;br /&gt;
            self.data = value&lt;br /&gt;
            self.left = self.right = None&lt;br /&gt;
Dann kann man eine Klasse &amp;lt;tt&amp;gt;TreeSearchArray&amp;lt;/tt&amp;gt; realisieren, deren Konstruktor einen leeren Suchbaum initialisiert:&lt;br /&gt;
    class TreeSearchArray:&lt;br /&gt;
        def __init__(self):&lt;br /&gt;
            self.root = None&lt;br /&gt;
            self.size = 0&lt;br /&gt;
        def __len__(self):&lt;br /&gt;
            return self.size&lt;br /&gt;
Die Funktion &amp;lt;tt&amp;gt;__setitem__&amp;lt;/tt&amp;gt; schaut nach, ob ein Eintrag mit dem betreffenden Schlüssel bereits existiert. Wenn ja, werden seine Daten mit den neuen Daten überschrieben, andernfalls wird ein neuer Eintrag angelegt. Intern werden dazu die bereits bekannten Funktionen &amp;lt;tt&amp;gt;treeSearch&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;treeInsert&amp;lt;/tt&amp;gt; verwendet (siehe Abschnitt [[Suchen#Suchbäume|Suchbäume]]):&lt;br /&gt;
        def __setitem__(self, key, value):&lt;br /&gt;
             node = treeSearch(self.root, key)&lt;br /&gt;
             if node is None:&lt;br /&gt;
                 self.root = treeInsert(self.root, key)&lt;br /&gt;
                 self.size += 1&lt;br /&gt;
                 node = treeSearch(self.root, key) &lt;br /&gt;
             node.value = value&lt;br /&gt;
(Eine geschicktere Implementation würde natürlich den zweiten Aufruf von &amp;lt;tt&amp;gt;treeSearch&amp;lt;/tt&amp;gt; eliminieren und das Setzen der Daten gleich in &amp;lt;tt&amp;gt;treeInsert&amp;lt;/tt&amp;gt; erledigen. Dies ändert aber nichts an der Komplexität der Funktion.) Die Funktion &amp;lt;tt&amp;gt;__getitem__&amp;lt;/tt&amp;gt; sucht ebenfalls einen Eintrag mit dem gegebenen Schlüssel. Wenn er gefunden wird, gibt sie die zugehörigen Daten zurück, andernfalls eine Fehlermeldung:&lt;br /&gt;
        def __getitem__(self, key):&lt;br /&gt;
            node = treeSearch(self.root, key)&lt;br /&gt;
            if node is None:&lt;br /&gt;
                raise KeyError(key)&lt;br /&gt;
            else:&lt;br /&gt;
                return node.value&lt;br /&gt;
Die Indexoperationen haben bei der Realisierung mit Baumsuche eine Komplexität in O(log n).&lt;br /&gt;
&lt;br /&gt;
Ein wichtiges Beispiel für ein assoziatives Array, das auf diese Weise realisiert wurde, ist die C++ Standardklasse &amp;lt;tt&amp;gt;[http://www.sgi.com/tech/stl/Map.html std::map]&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
[[Hashing und Hashtabellen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Assoziative_Arrays&amp;diff=5713</id>
		<title>Assoziative Arrays</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Assoziative_Arrays&amp;diff=5713"/>
				<updated>2020-07-09T19:07:57Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Datenstruktur-Dreieck für assoziative Arrays ==&lt;br /&gt;
&lt;br /&gt;
Assoziative Arrays sind eine der wichtigsten Anwendungen für Suchalgorithmen und Suchbäume. Bevor wir dies im Detail erklären, wollen wir jedoch noch einmal einen Blick auf das Datenstruktur-Dreieck aus der ersten Vorlesung werfen, das am Beispiel der assoziativen Arrays sehr schön illustriert werden kann. Wir zeigen es hier noch einmal:&lt;br /&gt;
&lt;br /&gt;
[[Image:Dt dreieck.png|300px]]&lt;br /&gt;
&lt;br /&gt;
Wir erinnern daran, dass man zwei Ecken des Dreicks wählen muss, um eine Datenstruktur zu definieren. Wir werden im Folgenden zeigen, wie Python durch Festlegen der erlaubten Operationen und deren Bedeutung den abstrakten Datentyp &amp;quot;Assoziatives Array&amp;quot; definiert, wie durch Festlegen eines Speicherlayouts und der Bedeutung der gespeicherten Entitäten das Standard-Datenformat &amp;quot;JSON&amp;quot; definiert ist, und wie durch effiziente Implementation der festgelegten Operationen mit jeweils passendem Speicherlayout die Datenstruktur auf unterschiedliche Arten realisiert werden kann.&lt;br /&gt;
&lt;br /&gt;
== Definition des abstrakten Datentyps  ==&lt;br /&gt;
&lt;br /&gt;
Assoziative Arrays können genau wie gewöhnliche Arrays benutzt werden, sie unterstützen also den lesenden und schreibenden Zugriff über den Indexoperator &amp;lt;tt&amp;gt;a[...]&amp;lt;/tt&amp;gt;. Im Unterschied zum gewöhnlichen Array, wo die Indizes ganze Zahlen im Bereich &amp;lt;math&amp;gt; i \in 0 \ldots N-1&amp;lt;/math&amp;gt; sein muss, kann der Typ der Indizes jetzt ''beliebig'' sein. Wir verwenden dafür den Begriff &amp;quot;Schlüssel&amp;quot; (engl.: key):&lt;br /&gt;
    a[key] = value   # Speichern des Wertes 'value' unter dem Schlüssel 'key'&lt;br /&gt;
    value  = a[key]  # Auslesen des unter dem Schlüssel 'key' gespeicherten Wertes&lt;br /&gt;
Eine typische Anwendung ist ein Wörterbuch&lt;br /&gt;
    x = toEnglish['Baum']   # ergibt 'tree'&lt;br /&gt;
In diesem Fall ist der Typ des Schlüssels &amp;lt;tt&amp;gt;string&amp;lt;/tt&amp;gt;. Dies ist in der Praxis der häufigste Fall, weshalb assoziative Arrays oft als ''Dictionary'' bezeichnet werden (so auch in Python, hier heißt der Typ &amp;lt;tt&amp;gt;dict&amp;lt;/tt&amp;gt;). Im allgemeinen kann aber jeder Typ als Schlüssel benutzt werden, der eine der folgenden Anforderungen erfüllt:&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|-align=&amp;quot;center&amp;quot;  &lt;br /&gt;
! unterstützte Vergleichsoperationen für Schlüssel&lt;br /&gt;
! mögliche Implementation des assoziativen Arrays&lt;br /&gt;
|-  &lt;br /&gt;
| Identitätstest: &amp;lt;br&amp;gt;&amp;lt;tt&amp;gt;key1 == key2&amp;lt;/tt&amp;gt;&lt;br /&gt;
| sequentielle Suche&lt;br /&gt;
|-&lt;br /&gt;
| Ordnungsrelation: &amp;lt;br&amp;gt;&amp;lt;tt&amp;gt;key1 &amp;amp;lt; key2&amp;lt;/tt&amp;gt; oder&amp;lt;br&amp;gt;&amp;lt;tt&amp;gt;cmp(key1, key2)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| Suchbaum (auch binäre Suche, falls keine neuen Schlüssel eingefügt und keine gelöscht werden)&lt;br /&gt;
|-&lt;br /&gt;
| Identitätstest und Hashfunktion:&amp;lt;br&amp;gt;&amp;lt;tt&amp;gt;key1 == key2&amp;lt;/tt&amp;gt; und &amp;lt;br&amp;gt;&amp;lt;tt&amp;gt;hash(key1) == hash(key2)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| Hashtabelle&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Wenn über die Schlüssel mehr bekannt ist (eine Ordnungsrelation oder eine Hashfunktion statt einer bloßen Indentitätsprüfung), kann man entsprechend bessere Datenstrukturen (Suchbäume oder Hashtabellen statt sequentieller Suche) verwenden, deren Zugriffsfunktionen wesentlich effizienter sind (sequentielle Suche ist ja nur in O(N)). &lt;br /&gt;
&lt;br /&gt;
Zu den beiden obigen Zugriffsfunktionen treten in Python noch drei weitere Funktionen hinzu: eine um zu testen, ob ein Schlüssel vorhanden ist, eine um einen Schlüssel und die darunter gespeicherten Daten zu löschen, sowie eine, die die Größe des Arrays (Anzahl der gespeicherten Schlüssel/Wert-Paare) zurückgibt:&lt;br /&gt;
    if a.has_key(key): # Testen, ob Schlüssel 'key' existiert&lt;br /&gt;
        del a[key]     # Schlüssel 'key' und zugehörige Daten aus dem Array entfernen&lt;br /&gt;
    print len(a)       # Größe des Arrays ausgeben&lt;br /&gt;
&lt;br /&gt;
Die Syntax der aufgeführten Funktionen gilt für die ''Benutzung'' eines assoziativen Arrays. Will man einen solchen Datentyp implementieren, muss man die entsprechende Funktionalität als Methoden der jeweiligen Klasse zur Verfügung stellen. Der Python-Interpreter transformiert den Index-/Schlüsselzugriff &amp;lt;tt&amp;gt;a[key]&amp;lt;/tt&amp;gt; sowie die &amp;lt;tt&amp;gt;len&amp;lt;/tt&amp;gt;- und &amp;lt;tt&amp;gt;del&amp;lt;/tt&amp;gt;-Operatoren automatisch in Aufrufe der jeweiligen Methode, wie die folgende Tabelle verdeutlicht. Zur vollständigen Definition der Bedeutung der einzelnen Operationen (wie vom Datenstruktur-Dreieck gefordert) gehört außerdem die Spezifikation des Verhaltens im Fehlerfall (wenn z.B. ein angeforderter Schlüssel nicht existiert).&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|-align=&amp;quot;center&amp;quot;  &lt;br /&gt;
! Operation&lt;br /&gt;
! von Python intern generierter Methodenaufruf&lt;br /&gt;
! zu implementierende Methodensignatur&lt;br /&gt;
! Bedeutung&lt;br /&gt;
|-  &lt;br /&gt;
| &amp;lt;tt&amp;gt;a[key] = value&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;a.__setitem__(key, value)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;def __setitem__(self, key, value):&amp;lt;/tt&amp;gt;&lt;br /&gt;
| * wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; bereits existiert: ersetze die zugehörigen Daten durch &amp;lt;tt&amp;gt;value&amp;lt;/tt&amp;gt;&amp;lt;br&amp;gt;* wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; noch nicht existiert: lege einen neuen Schlüssel an und speichere &amp;lt;tt&amp;gt;value&amp;lt;/tt&amp;gt; als zugehörigeaten&lt;br /&gt;
|-  &lt;br /&gt;
| &amp;lt;tt&amp;gt;value = a[key]&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;a.__getitem__(key)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;def __getitem__(self, key):&amp;lt;/tt&amp;gt;&lt;br /&gt;
| * wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; existiert: gebe die zugehörigen Daten zurück&amp;lt;br&amp;gt;* wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; nicht existiert: löse &amp;lt;tt&amp;gt;KeyError&amp;lt;/tt&amp;gt;-Exception aus&lt;br /&gt;
|-  &lt;br /&gt;
| &amp;lt;tt&amp;gt;a.has_key(key)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;a.has_key(key)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;def has_key(self, key):&amp;lt;/tt&amp;gt;&lt;br /&gt;
| gibt &amp;lt;tt&amp;gt;True&amp;lt;/tt&amp;gt; zurück wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; existiert, sonst &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-  &lt;br /&gt;
| &amp;lt;tt&amp;gt;del a[key]&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;a.__delitem__(key)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;def __delitem__(self, key):&amp;lt;/tt&amp;gt;&lt;br /&gt;
| * wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; existiert: entferne diesen Schlüssel und die zugehörigen Daten&amp;lt;br&amp;gt;* wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; nicht existiert: löse &amp;lt;tt&amp;gt;KeyError&amp;lt;/tt&amp;gt;-Exception aus&lt;br /&gt;
|-  &lt;br /&gt;
| &amp;lt;tt&amp;gt;len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;a.__len__()&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;def __len__(self):&amp;lt;/tt&amp;gt;&lt;br /&gt;
| gibt die Größe des Arrays zurück&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Aufgrund der Definition ist klar, das jeder Schlüssel nur einmal im Array vorkommen kann. Die Definition des Abstrakten Datentyps &amp;quot;assoziatives Array&amp;quot; erlaubt es uns, derartige Arrays auf verschiedenste Art zu implementieren, ohne dass sich an der Benutzung (also daran, wie man die Arrayfunktionalität später aufruft) irgend etwas ändert.&lt;br /&gt;
&lt;br /&gt;
== Das JSON-Datenformat ==&lt;br /&gt;
&lt;br /&gt;
Die zweite Kante des Datenstruktur-Dreiecks ist das &amp;quot;Datenformat&amp;quot;. Hier legt man Speicherlayout und Bedeutung fest. Ein Datenformat dient vor allem zum Speichern von Daten auf Festplatte und zum Austausch von Daten zwischen verschiedenen Programmen bzw. Internetseiten. Im Fall von assoziativen Arrays setzt sich dafür das [http://www.json.org/ JSON-Format] immer mehr durch, weil es einfach und trotzdem mächtig ist. Es eignet sich sehr gut zum Speichern von assoziativen Arrays (also von Schlüssel/Wert Paaren) und unterstützt außerdem gewöhnliche Arrays und hierarchische Strukturen, weil die Werte wiederum (gewöhnliche oder assoziative) Arrays sein dürfen.&lt;br /&gt;
&lt;br /&gt;
Das Speicherlayout einer JSON-Dateien ist definiert als eine Bytefolge, die als Zeichenfolge gemäß [http://de.wikipedia.org/wiki/UTF-8 UTF-8-Standard] interpretiert wird. Dies hat zwei Vorteile: einerseits ist das Format dadurch mit allen gängigen Systemen kompatibel und überall gleich, andererseits kann jedes JSON-File einfach in einem Texteditor geöffnet und editiert werden und ist für Menschen und Maschinen gleichermaßen leicht lesbar.&lt;br /&gt;
&lt;br /&gt;
Die Zuordung einer Bedeutung zu einem gegebenen Speicherinhalt erfolgt in JSON mit Hilfe einer Grammatik. Ein JSON-File enthält entweder ein gewöhnliches Array oder ein assoziatives Array (Dictionary):&lt;br /&gt;
  JSON_file := array&lt;br /&gt;
             | dictionary&lt;br /&gt;
Ein gewöhnliches Array wird als Folge von einem oder mehreren Elementen geschrieben, die durch Komma getrennt und in eckigen Klammern eingeschlossen sind (Zeichen, die in der Grammatik in einfache Anführungszeichen eingeschlossen sind, müssen explizit im JSON-File stehen). Leere Arrays sind ebenfalls erlaubt:&lt;br /&gt;
  array    := '[' elements ']'&lt;br /&gt;
            | '[' ']'&lt;br /&gt;
  elements := value&lt;br /&gt;
            | value ',' elements&lt;br /&gt;
Ein Dictionary wird in ähnlicher Weise als Folge von Schlüssel/Wert-Paaren geschrieben, die durch Komma getrennt und in geschweiften Klammern eingeschlossen sind. Leere Dictionaries sind erlaubt. Die Schlüssel müssen immer Strings sein, gefolgt von einem Doppelpunkt:&lt;br /&gt;
  dictionary := '{' pairs '}'&lt;br /&gt;
              | '{' '}'&lt;br /&gt;
  pairs      := string ':' value&lt;br /&gt;
              | string ':' value ',' pairs&lt;br /&gt;
Strings sind Zeichenfolgen (inklusive einiger Sonderzeichen wie &amp;quot;\n&amp;quot; für einen Zeilenumbruch), die in doppelte Anführungszeichen eingeschlossen sind, oder der Leerstring:&lt;br /&gt;
  string := '&amp;quot;' '&amp;quot;'  &lt;br /&gt;
          | '&amp;quot;'characters'&amp;quot;'&lt;br /&gt;
Werte können Zahlen (ganze oder reelle Zahlen, definiert wie in Python), Boolesche Werte ('true' oder 'false'), Strings oder 'null' sein. Außerdem können Arrays und Dictionaries wiederum als Werte verwendet werden, wodurch sich beliebig tief geschachtelte, hierarchische Datenstrukturen ergeben:&lt;br /&gt;
  value := number | string | boolean | null | array | dictionary&lt;br /&gt;
Hier ist ein einfaches Beispiel für ein JSON-File, das Ausschnitte aus einer Studenten-Datenbank enthält:&lt;br /&gt;
   {&lt;br /&gt;
       &amp;quot;Müller, Friedrich&amp;quot; : {&lt;br /&gt;
            &amp;quot;Mathematik&amp;quot; : 2.0,&lt;br /&gt;
            &amp;quot;ALDA&amp;quot; : 1.3&lt;br /&gt;
       },&lt;br /&gt;
       &amp;quot;Weise, Anna&amp;quot; : {&lt;br /&gt;
            &amp;quot;Mathematik&amp;quot; : 1.0,&lt;br /&gt;
            &amp;quot;Philosophie&amp;quot;: 1.3&lt;br /&gt;
       }&lt;br /&gt;
   }&lt;br /&gt;
Das JSON-Format ist syntaktisch der Sprache Python sehr nahe, und kann mit einigen verherigen Definitionen direkt durch die &amp;lt;tt&amp;gt;eval()&amp;lt;/tt&amp;gt;-Funktion in ein Python-Dictionary oder -Array umgewandelt werden.&lt;br /&gt;
   # don't do this - it is highly '''unsafe''' and dangerous&lt;br /&gt;
   true, false, null = True, False, None                  # fehlende Konstanten defininieren&lt;br /&gt;
   res = eval(file(&amp;quot;test.json&amp;quot;).read().decode(&amp;quot;utf_8&amp;quot;))   # File einlesen und nach Python konvertieren&lt;br /&gt;
Dies sollte man jedoch '''auf keinen Fall''' tun, weil ein Hacker dadurch beliebigen Code ausführen könnte, den er vorher in das File 'test.json' eingeschmuggelt hat. Da die Funktion &amp;lt;tt&amp;gt;eval()&amp;lt;/tt&amp;gt; nur prüft, ob der Ausdruck gültiges Python ist, aber nicht, ob das File gültiges JSON (also nur Daten, aber keinen ausführbaren Code) enthält, kann man dies nicht erkennen oder verhindern. Deshalb sollte man zum Einlesen von JSON stets das Python-Modul [http://docs.python.org/library/json.html json] verwenden, das ein manipuliertes File einfach zurückweisen würde:&lt;br /&gt;
   # sicheres Einlesen und Konvertieren&lt;br /&gt;
   import json&lt;br /&gt;
   &lt;br /&gt;
   with open('test.json', encoding='utf-8') as f:    # File im UTF-8 Format öffnen &lt;br /&gt;
       res = json.load(f)                            # und als json einlesen&lt;br /&gt;
&lt;br /&gt;
==Implementation von assoziativen Array-Klassen==&lt;br /&gt;
&lt;br /&gt;
Die dritte Kante des Datenstruktur-Dreiecks bezieht sich schließlich auf die Realisierung der Datenstruktur als Klasse, indem man auf geeignet organisiertem Speicher die geforderten Operationen implementiert. In Python ist mit der Klasse &amp;lt;tt&amp;gt;dict&amp;lt;/tt&amp;gt; eine sehr leistungsfähige Implementation eines assoziativen Arrays integraler Bestandteil der Sprache. Diese Implementation beruht auf dem Konzept der Hashtabellen, das wir in der Vorlesung [[Hashing und Hashtabellen|später behandeln]]. Man benötigt dafür eine Funktion &amp;lt;tt&amp;gt;hash(key)&amp;lt;/tt&amp;gt;, die in Python für alle Standarddatentypen bereits implementiert ist. In diesem Abschnitt wollen wir zwei alternative Implementationen auf der Basis von sequentieller Suche und auf der Basis von Suchbäumen betrachten.&lt;br /&gt;
&lt;br /&gt;
=== Realisierung durch sequentielle Suche===&lt;br /&gt;
&lt;br /&gt;
Wenn für die Schlüssel nur ein Identitätsvergleich &lt;br /&gt;
   key1 == key2&lt;br /&gt;
definiert ist, hat man keine Möglichkeit, die Schlüsselsuche durch eine spezielle Datenstruktur zu beschleunigen. Man speichert die Schlüssel/Wert-Paare dann einfach in einem gewöhnlichen Array, das man sequentiell durchsucht. Dazu implementieren wir zunächst eine Hilfsklasse, die Schlüssel/Wert-Paare aufnimmt:&lt;br /&gt;
   class KeyValuePair:&lt;br /&gt;
       def __init__(self, key, value):&lt;br /&gt;
           self.key = key&lt;br /&gt;
           self.value = value&lt;br /&gt;
Die Arrayklasse speichert die Paare in einem Array &amp;lt;tt&amp;gt;self.data&amp;lt;/tt&amp;gt;, dessen aktuelle Länge der Größe des assoziativen Arrays entspricht. Damit ist das Speicherlayout der Klasse festgelegt:&lt;br /&gt;
   class SequentialSearchArray:&lt;br /&gt;
       def __init__(self):&lt;br /&gt;
           self.data = []&lt;br /&gt;
       def __len__(self):&lt;br /&gt;
           return len(self.data)&lt;br /&gt;
Um auf die Daten zugreifen zu können, müssen wir nach dem richtigen Schlüssel suchen. Dazu implementieren wir die Hilfsfunktion &amp;lt;tt&amp;gt;findKey&amp;lt;/tt&amp;gt;, die den Index des Schlüssels zurückgibt, oder &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, wenn der Schlüssel nicht existiert:&lt;br /&gt;
       def findKey(self, key):&lt;br /&gt;
           for k in xrange(len(self.data)):&lt;br /&gt;
               if key == self.data[k].key:&lt;br /&gt;
                   return k&lt;br /&gt;
           return None&lt;br /&gt;
Beim Einfügen eines Elements müssen wir zuerst prüfen, ob es den Schlüssel schon gibt, und dann entweder die daten überschreiben oder ein neues Element anfügen:&lt;br /&gt;
       def __setitem__(self, key, value):&lt;br /&gt;
           k = self.findKey(key)&lt;br /&gt;
           if k is None:&lt;br /&gt;
               self.data.append(KeyValuePair(key, value))  # neues Paar einfügen&lt;br /&gt;
           else:&lt;br /&gt;
               self.data[k].value = value                  # Daten ersetzen&lt;br /&gt;
Die Suche hingegen löst eine Exception aus, wenn der Schlüssel nicht gefunden wurde:&lt;br /&gt;
       def __getitem__(self, key):&lt;br /&gt;
           k = self.findKey(key)&lt;br /&gt;
           if k is None:&lt;br /&gt;
               raise KeyError(key)        # Schlüssel nicht gefunden =&amp;gt; Fehler&lt;br /&gt;
           else:&lt;br /&gt;
               return self.data[k].value  # Schlüssel gefunden =&amp;gt; Daten zurückgeben&lt;br /&gt;
Die übrigen geforderten Funktionen sind ebenso einfach zu implementieren:&lt;br /&gt;
       def has_key(self, key):&lt;br /&gt;
           return (self.findKey(key) is not None)&lt;br /&gt;
       &lt;br /&gt;
       def __delitem__(self, key):&lt;br /&gt;
           k = self.findKey(key)&lt;br /&gt;
           if k is None:&lt;br /&gt;
               raise KeyError(key)     # Schlüssel nicht gefunden =&amp;gt; Fehler&lt;br /&gt;
           else:&lt;br /&gt;
               del self.data[k]        # Schlüssel gefunden =&amp;gt; löschen&lt;br /&gt;
Wegen der sequentiellen Suche hat der Zugriff auf ein Element in dieser Datenstruktur die Komplexität O(len(a)).&lt;br /&gt;
&lt;br /&gt;
=== Realisierung als Suchbaum ===&lt;br /&gt;
&lt;br /&gt;
Wenn für den Schlüsseltyp des assoziativen Arrays eine Ordnung definiert ist (wenn also &amp;lt;tt&amp;gt;key1 &amp;lt; key2&amp;lt;/tt&amp;gt; oder &amp;lt;tt&amp;gt;cmp(key1, key2)&amp;lt;/tt&amp;gt; unterstützt werden), kann man das Indexierungsproblem auf das Suchproblem zurückführen. Dann kann das assoziative Array effizient als selbst-balancierender Suchbaum imlementiert werden, so dass die Zugriffsfunktionen nur noch eine Komplexität in O(log(len(a))) haben. Die Datenstruktur des Suchbaums muss dafür so erweitert werden, dass zu jedem Schlüssel auch die zugehörigen Daten gespeichert werden. Man erweitert die Node-Klasse deshalb um ein Feld &amp;quot;value&amp;quot;:&lt;br /&gt;
    class Node:&lt;br /&gt;
        def __init__(self, key, value):&lt;br /&gt;
            self.key = key&lt;br /&gt;
            self.data = value&lt;br /&gt;
            self.left = self.right = None&lt;br /&gt;
Dann kann man eine Klasse &amp;lt;tt&amp;gt;TreeSearchArray&amp;lt;/tt&amp;gt; realisieren, deren Konstruktor einen leeren Suchbaum initialisiert:&lt;br /&gt;
    class TreeSearchArray:&lt;br /&gt;
        def __init__(self):&lt;br /&gt;
            self.root = None&lt;br /&gt;
            self.size = 0&lt;br /&gt;
        def __len__(self):&lt;br /&gt;
            return self.size&lt;br /&gt;
Die Funktion &amp;lt;tt&amp;gt;__setitem__&amp;lt;/tt&amp;gt; schaut nach, ob ein Eintrag mit dem betreffenden Schlüssel bereits existiert. Wenn ja, werden seine Daten mit den neuen Daten überschrieben, andernfalls wird ein neuer Eintrag angelegt. Intern werden dazu die bereits bekannten Funktionen &amp;lt;tt&amp;gt;treeSearch&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;treeInsert&amp;lt;/tt&amp;gt; verwendet (siehe Abschnitt [[Suchen#Suchbäume|Suchbäume]]):&lt;br /&gt;
        def __setitem__(self, key, value):&lt;br /&gt;
             node = treeSearch(self.root, key)&lt;br /&gt;
             if node is None:&lt;br /&gt;
                 self.root = treeInsert(self.root, key)&lt;br /&gt;
                 self.size += 1&lt;br /&gt;
                 node = treeSearch(self.root, key) &lt;br /&gt;
             node.value = value&lt;br /&gt;
(Eine geschicktere Implementation würde natürlich den zweiten Aufruf von &amp;lt;tt&amp;gt;treeSearch&amp;lt;/tt&amp;gt; eliminieren und das Setzen der Daten gleich in &amp;lt;tt&amp;gt;treeInsert&amp;lt;/tt&amp;gt; erledigen. Dies ändert aber nichts an der Komplexität der Funktion.) Die Funktion &amp;lt;tt&amp;gt;__getitem__&amp;lt;/tt&amp;gt; sucht ebenfalls einen Eintrag mit dem gegebenen Schlüssel. Wenn er gefunden wird, gibt sie die zugehörigen Daten zurück, andernfalls eine Fehlermeldung:&lt;br /&gt;
        def __getitem__(self, key):&lt;br /&gt;
            node = treeSearch(self.root, key)&lt;br /&gt;
            if node is None:&lt;br /&gt;
                raise KeyError(key)&lt;br /&gt;
            else:&lt;br /&gt;
                return node.value&lt;br /&gt;
Die Indexoperationen haben bei der Realisierung mit Baumsuche eine Komplexität in O(log n).&lt;br /&gt;
&lt;br /&gt;
Ein wichtiges Beispiel für ein assoziatives Array, das auf diese Weise realisiert wurde, ist die C++ Standardklasse &amp;lt;tt&amp;gt;[http://www.sgi.com/tech/stl/Map.html std::map]&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
[[Hashing und Hashtabellen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5712</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=5712"/>
				<updated>2020-07-02T18:26:53Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Breitensuche in Graphen (Breadth First Search, BFS) */&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 auf demselben Weg zurück (sogenanntes &amp;lt;i&amp;gt;back tracking&amp;lt;/i&amp;gt;), bis man einen Knoten findet, der noch einen unbesuchten 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;
Die 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 &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; genannt. (Die Verwendung von property maps hat sich gegenüber der alternativen Idee durchgesetzt, solche Eigenschaften in speziellen Knotenklassen zu speichern. Im letzteren Fall braucht man nämlich für jede Anwendung eine angepasste Knotenklasse mit den jeweils gewünschten Attributen und damit auch angepasste Implementationen der Graphenfunktionen, was sich als sehr aufwändig erwiesen hat.) &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 Nachbarpixeln 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, Informationen über den Verlauf der Suche zu sammeln und am Ende zurückzugeben, so dass andere Algorithmen diese Information nutzen können. Typische Beispiele dafür sind eine Reihenfolge der Knoten (in discovery oder finishing order) oder die Vorgänger jedes Knotens im Tiefensuchbaum (also  von welchem Knoten aus man den jeweiligen Knoten zuerst erreicht hat). Wir führen dafür drei neue Arrays ein. &lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)    # wurde ein Knoten bereits besucht?&lt;br /&gt;
     parents = [None]*len(graph)     # registriere für jeden Knoten den Vorgänger im Tiefensuchbaum&lt;br /&gt;
     discovery_order = []            # enthält am Ende die pre-order Sortierung&lt;br /&gt;
     finishing_order = []            # enthält am Ende die post-order Sortierung&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, parent):        # rekursive Hilfsfunktion&lt;br /&gt;
         if not visited[node]:       # besuche 'node', wenn noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True           # markiere 'node' als besucht&lt;br /&gt;
             parents[node] = parent         # speichere den Vorgänger von 'node'&lt;br /&gt;
             discovery_order.append(node)   # registriere, dass 'node' jetzt entdeckt wurde&lt;br /&gt;
             for neighbor in graph[node]:   # besuche rekursiv die Nachbarn ...&lt;br /&gt;
                 visit(neighbor, node)      #  ... wobei 'node' zu deren Vorgänger wird&lt;br /&gt;
             finishing_order.append(node)   # registriere, dass 'node' jetzt fertiggestellt wurde&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode, None)          # beginne bei 'startnode', der keinen Vorgänger hat&lt;br /&gt;
     &lt;br /&gt;
     return parents, discovery_order, finishing_order # gib die zusätzliche Informationen zurück&lt;br /&gt;
&lt;br /&gt;
Beginnt man die Suche bei Knoten 1, entsprechen die Inhalte der Arrays &amp;lt;tt&amp;gt;discovery_order&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;finishing_order&amp;lt;/tt&amp;gt; für den obigen Beispielgraphen gerade den vorher angeführten &amp;lt;tt&amp;gt;print&amp;lt;/tt&amp;gt;-Ausgaben. Die Vorgänger im Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; lauten: &lt;br /&gt;
  Knotennummer  |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7&lt;br /&gt;
  --------------+-----+-----+-----+-----+-----+-----+-----+-----&lt;br /&gt;
  Vorgänger     | None| None|  1  |  4  |  2  |  2  |  3  |  3&lt;br /&gt;
&lt;br /&gt;
Die Knotennummern dienen hier als Array-Indizes, und die dazugehörigen Arrayeinträge verweisen auf die Vorgänger. Man kann mit diesen Informationen den Weg von jedem Knoten zur Wurzel zurückverfolgen und damit den Tiefensuchbaum von unten nach oben rekonstruieren. Man beachte, dass &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; den Eintrag &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; für die Knoten 0 umd 1 enthält, weil Knoten 0 in diesem Graphen nicht existiert und Knoten 1 als Wurzel der Suche keinen Vorgänger hat.&lt;br /&gt;
&lt;br /&gt;
Wird das Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; verwendet, kann man den Code vereinfachen, indem man das Array &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; einspart: Sobald ein Knoten erstmals besucht wurde, ist sein Vorgänger bekannt und damit ungleich &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;. Die Abfrage &amp;lt;tt&amp;gt;if parents[node] is None:&amp;lt;/tt&amp;gt; liefert damit das gleiche Resultat wie die Abfrage &amp;lt;tt&amp;gt;if not visited[node]:&amp;lt;/tt&amp;gt;. Einzige Ausnahme ist der Startknoten der Suche, dessen Vorgänger bisher &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; war. Dieses Problem löst man leicht mit der Konvention, dass man den Startknoten zu seinem eigenen Vorgänger erklärt. Man startet die Suche also mit &amp;lt;tt&amp;gt;visit(startnode, startnode)&amp;lt;/tt&amp;gt; statt mit &amp;lt;tt&amp;gt;visit(startnode, None)&amp;lt;/tt&amp;gt;.&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 Nachbarknoten 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;
     parents = [None]*len(graph)            # speichere für jeden Knoten den Vorgänger im Breitensuchbaum&lt;br /&gt;
     parents[startnode] = startnode         # Konvention: der Startknoten hat sich selbst als Vorgänger &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 noch Knoten zu bearbeiten sind&lt;br /&gt;
         node = q.popleft()                 # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
     &amp;lt;font color=red&amp;gt;                                       # Beachte: mit q.pop() bekommen wir DFS&amp;lt;/font&amp;gt;&lt;br /&gt;
         print(node)                        # den Knoten bearbeiten (hier: Knotennummer drucken)&lt;br /&gt;
         for neighbor in graph[node]:       # die Nachbarn expandieren&lt;br /&gt;
             if parents[neighbor] is None:  # Nachbar wurde noch nicht besucht&lt;br /&gt;
                 parents[neighbor] = node   # =&amp;gt; Vorgänger merken, Knoten dadurch als &amp;quot;besucht&amp;quot; markieren&lt;br /&gt;
                 q.append(neighbor)         #    und in die Queue aufnehmen&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;
=== 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;
=== 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;
=== 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 range(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 = list(range(len(graph)))  # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     &lt;br /&gt;
     for node in range(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 range(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 zunächst eine wichtige Eigenschaft des Algorithmus: Die Priorität (=Pfadlänge) des Knotens an der Spitze des Heaps wächst im Laufe des Algorithmus monoton an (aber nicht notwendigerweise streng monoton). Mit anderen Worten: liefert &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; in der i-ten Iteration der &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Schleife den Knoten u mit der Pfadlänge l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt;, und in der (i+1)-ten Iteration den Knoten v mit der Pfadlänge l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt;, so gilt stets l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; &amp;amp;ge; l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt;. Wir zeigen dies mit der Technik des indirekten Beweises, d.h. wir nehmen das Gegenteil an und führen diese Annahme zum Widerspruch. Wäre also l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; &amp;lt; l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt;, gäbe es zwei Möglichkeiten:&lt;br /&gt;
&amp;lt;ol&amp;gt;&lt;br /&gt;
&amp;lt;li&amp;gt;Der Weg nach v mit der Länge l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; war in der i-ten Iteration schon bekannt und somit bereits im Heap enthalten. Dann hätte &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; in dieser Iteration aber v zurückgegeben, im Widerspruch zur Annahme, dass u zurückgegeben wurde.&amp;lt;/li&amp;gt;&lt;br /&gt;
&amp;lt;li&amp;gt;Der Weg wurde erst bei der Expansion von u in der i-ten Iteration gefunden. Dann muss v ein Nachbar von u sein, und seine Weglänge berechnet sich als l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; = l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt; + w&amp;lt;sub&amp;gt;u,v&amp;lt;/sub&amp;gt;. Da für die Kantengewichte aber w&amp;lt;sub&amp;gt;u,v&amp;lt;/sub&amp;gt; &amp;amp;ge; 0 gefordert ist, kann l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; &amp;lt; l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt; nicht gelten.&amp;lt;/li&amp;gt;&lt;br /&gt;
&amp;lt;/ol&amp;gt;&lt;br /&gt;
Diese Monotonieeigenschaft hat eine interessante Konsequenz: Beträgt der Abstand vom Start zum Zielknoten l&amp;lt;sub&amp;gt;z&amp;lt;/sub&amp;gt;, so findet Dijsktra's Algorithmus als Nebenprodukt auch die kürzesten Wege zu allen näher gelegenen Knoten, also zu allen Knoten u, für deren Abstand l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt; &amp;lt; l&amp;lt;sub&amp;gt;z&amp;lt;/sub&amp;gt; gilt. Dies trifft auch dann zu, wenn diese Wege für den Benutzer gar nicht von Interesse sind. Der A*-Algorithmus, der weiter unten erklärt wird, versucht dem abzuhelfen.&lt;br /&gt;
&lt;br /&gt;
Wir können nun mittels vollständiger Induktion die folgende Schleifen-Invariante beweisen: 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 wieder 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 dem Momemnt in den Heap eingefügt werden, wo der kürzeste Weg für &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; gefunden wurde. Man beachte auch, dass wegen der Monotonieeigenschaft 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 die Knoten in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt;. &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 im Heap 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;: Wegen der Monotonieeigenschaft muss jetzt &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) &amp;gt; Kosten(node &amp;amp;rarr; predecessor &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt; gelten. 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 aber als &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) + weight[(x, node)]&amp;lt;/tt&amp;gt;, und deshalb kann dieser Weg keinesfalls kürzer sein.&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 range(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 range(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 range(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:     # neuer Knoten gefunden:&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;
         else:&lt;br /&gt;
             return False                     # node war bereits 'finished' =&amp;gt; kein Zyklus&lt;br /&gt;
     &lt;br /&gt;
     for node in range(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 range(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;
== 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>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Assoziative_Arrays&amp;diff=5711</id>
		<title>Assoziative Arrays</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Assoziative_Arrays&amp;diff=5711"/>
				<updated>2020-07-02T16:27:41Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Das JSON-Datenformat */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Datenstruktur-Dreieck für assoziative Arrays ==&lt;br /&gt;
&lt;br /&gt;
Assoziative Arrays sind eine der wichtigsten Anwendungen für Suchalgorithmen und Suchbäume. Bevor wir dies im Detail erklären, wollen wir jedoch noch einmal einen Blick auf das Datenstruktur-Dreieck aus der ersten Vorlesung werfen, das am Beispiel der assoziativen Arrays sehr schön illustriert werden kann. Wir zeigen es hier noch einmal:&lt;br /&gt;
&lt;br /&gt;
[[Image:Dt dreieck.png|300px]]&lt;br /&gt;
&lt;br /&gt;
Wir erinnern daran, dass man zwei Ecken des Dreicks wählen muss, um eine Datenstruktur zu definieren. Wir werden im Folgenden zeigen, wie Python durch Festlegen der erlaubten Operationen und deren Bedeutung den abstrakten Datentyp &amp;quot;Assoziatives Array&amp;quot; definiert, wie durch Festlegen eines Speicherlayouts und der Bedeutung der gespeicherten Entitäten das Standard-Datenformat &amp;quot;JSON&amp;quot; definiert ist, und wie durch effiziente Implementation der festgelegten Operationen mit jeweils passendem Speicherlayout die Datenstruktur auf unterschiedliche Arten realisiert werden kann.&lt;br /&gt;
&lt;br /&gt;
== Definition des abstrakten Datentyps  ==&lt;br /&gt;
&lt;br /&gt;
Assoziative Arrays können genau wie gewöhnliche Arrays benutzt werden, sie unterstützen also den lesenden und schreibenden Zugriff über den Indexoperator &amp;lt;tt&amp;gt;a[...]&amp;lt;/tt&amp;gt;. Im Unterschied zum gewöhnlichen Array, wo die Indizes ganze Zahlen im Bereich &amp;lt;math&amp;gt; i \in 0 \ldots N-1&amp;lt;/math&amp;gt; sein muss, kann der Typ der Indizes jetzt ''beliebig'' sein. Wir verwenden dafür den Begriff &amp;quot;Schlüssel&amp;quot; (engl.: key):&lt;br /&gt;
    a[key] = value   # Speichern des Wertes 'value' unter dem Schlüssel 'key'&lt;br /&gt;
    value  = a[key]  # Auslesen des unter dem Schlüssel 'key' gespeicherten Wertes&lt;br /&gt;
Eine typische Anwendung ist ein Wörterbuch&lt;br /&gt;
    x = toEnglish['Baum']   # ergibt 'tree'&lt;br /&gt;
In diesem Fall ist der Typ des Schlüssels &amp;lt;tt&amp;gt;string&amp;lt;/tt&amp;gt;. Dies ist in der Praxis der häufigste Fall, weshalb assoziative Arrays oft als ''Dictionary'' bezeichnet werden (so auch in Python, hier heißt der Typ &amp;lt;tt&amp;gt;dict&amp;lt;/tt&amp;gt;). Im allgemeinen kann aber jeder Typ als Schlüssel benutzt werden, der eine der folgenden Anforderungen erfüllt:&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|-align=&amp;quot;center&amp;quot;  &lt;br /&gt;
! unterstützte Vergleichsoperationen für Schlüssel&lt;br /&gt;
! mögliche Implementation des assoziativen Arrays&lt;br /&gt;
|-  &lt;br /&gt;
| Identitätstest: &amp;lt;br&amp;gt;&amp;lt;tt&amp;gt;key1 == key2&amp;lt;/tt&amp;gt;&lt;br /&gt;
| sequentielle Suche&lt;br /&gt;
|-&lt;br /&gt;
| Ordnungsrelation: &amp;lt;br&amp;gt;&amp;lt;tt&amp;gt;key1 &amp;amp;lt; key2&amp;lt;/tt&amp;gt; oder&amp;lt;br&amp;gt;&amp;lt;tt&amp;gt;cmp(key1, key2)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| Suchbaum (auch binäre Suche, falls keine neuen Schlüssel eingefügt und keine gelöscht werden)&lt;br /&gt;
|-&lt;br /&gt;
| Identitätstest und Hashfunktion:&amp;lt;br&amp;gt;&amp;lt;tt&amp;gt;key1 == key2&amp;lt;/tt&amp;gt; und &amp;lt;br&amp;gt;&amp;lt;tt&amp;gt;hash(key1) == hash(key2)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| Hashtabelle&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Wenn über die Schlüssel mehr bekannt ist (eine Ordnungsrelation oder eine Hashfunktion statt einer bloßen Indentitätsprüfung), kann man entsprechend bessere Datenstrukturen (Suchbäume oder Hashtabellen statt sequentieller Suche) verwenden, deren Zugriffsfunktionen wesentlich effizienter sind (sequentielle Suche ist ja nur in O(N)). &lt;br /&gt;
&lt;br /&gt;
Zu den beiden obigen Zugriffsfunktionen treten in Python noch drei weitere Funktionen hinzu: eine um zu testen, ob ein Schlüssel vorhanden ist, eine um einen Schlüssel und die darunter gespeicherten Daten zu löschen, sowie eine, die die Größe des Arrays (Anzahl der gespeicherten Schlüssel/Wert-Paare) zurückgibt:&lt;br /&gt;
    if a.has_key(key): # Testen, ob Schlüssel 'key' existiert&lt;br /&gt;
        del a[key]     # Schlüssel 'key' und zugehörige Daten aus dem Array entfernen&lt;br /&gt;
    print len(a)       # Größe des Arrays ausgeben&lt;br /&gt;
&lt;br /&gt;
Die Syntax der aufgeführten Funktionen gilt für die ''Benutzung'' eines assoziativen Arrays. Will man einen solchen Datentyp implementieren, muss man die entsprechende Funktionalität als Methoden der jeweiligen Klasse zur Verfügung stellen. Der Python-Interpreter transformiert den Index-/Schlüsselzugriff &amp;lt;tt&amp;gt;a[key]&amp;lt;/tt&amp;gt; sowie die &amp;lt;tt&amp;gt;len&amp;lt;/tt&amp;gt;- und &amp;lt;tt&amp;gt;del&amp;lt;/tt&amp;gt;-Operatoren automatisch in Aufrufe der jeweiligen Methode, wie die folgende Tabelle verdeutlicht. Zur vollständigen Definition der Bedeutung der einzelnen Operationen (wie vom Datenstruktur-Dreieck gefordert) gehört außerdem die Spezifikation des Verhaltens im Fehlerfall (wenn z.B. ein angeforderter Schlüssel nicht existiert).&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|-align=&amp;quot;center&amp;quot;  &lt;br /&gt;
! Operation&lt;br /&gt;
! von Python intern generierter Methodenaufruf&lt;br /&gt;
! zu implementierende Methodensignatur&lt;br /&gt;
! Bedeutung&lt;br /&gt;
|-  &lt;br /&gt;
| &amp;lt;tt&amp;gt;a[key] = value&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;a.__setitem__(key, value)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;def __setitem__(self, key, value):&amp;lt;/tt&amp;gt;&lt;br /&gt;
| * wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; bereits existiert: ersetze die zugehörigen Daten durch &amp;lt;tt&amp;gt;value&amp;lt;/tt&amp;gt;&amp;lt;br&amp;gt;* wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; noch nicht existiert: lege einen neuen Schlüssel an und speichere &amp;lt;tt&amp;gt;value&amp;lt;/tt&amp;gt; als zugehörigeaten&lt;br /&gt;
|-  &lt;br /&gt;
| &amp;lt;tt&amp;gt;value = a[key]&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;a.__getitem__(key)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;def __getitem__(self, key):&amp;lt;/tt&amp;gt;&lt;br /&gt;
| * wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; existiert: gebe die zugehörigen Daten zurück&amp;lt;br&amp;gt;* wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; nicht existiert: löse &amp;lt;tt&amp;gt;KeyError&amp;lt;/tt&amp;gt;-Exception aus&lt;br /&gt;
|-  &lt;br /&gt;
| &amp;lt;tt&amp;gt;a.has_key(key)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;a.has_key(key)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;def has_key(self, key):&amp;lt;/tt&amp;gt;&lt;br /&gt;
| gibt &amp;lt;tt&amp;gt;True&amp;lt;/tt&amp;gt; zurück wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; existiert, sonst &amp;lt;tt&amp;gt;False&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-  &lt;br /&gt;
| &amp;lt;tt&amp;gt;del a[key]&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;a.__delitem__(key)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;def __delitem__(self, key):&amp;lt;/tt&amp;gt;&lt;br /&gt;
| * wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; existiert: entferne diesen Schlüssel und die zugehörigen Daten&amp;lt;br&amp;gt;* wenn &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; nicht existiert: löse &amp;lt;tt&amp;gt;KeyError&amp;lt;/tt&amp;gt;-Exception aus&lt;br /&gt;
|-  &lt;br /&gt;
| &amp;lt;tt&amp;gt;len(a)&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;a.__len__()&amp;lt;/tt&amp;gt;&lt;br /&gt;
| &amp;lt;tt&amp;gt;def __len__(self):&amp;lt;/tt&amp;gt;&lt;br /&gt;
| gibt die Größe des Arrays zurück&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Aufgrund der Definition ist klar, das jeder Schlüssel nur einmal im Array vorkommen kann. Die Definition des Abstrakten Datentyps &amp;quot;assoziatives Array&amp;quot; erlaubt es uns, derartige Arrays auf verschiedenste Art zu implementieren, ohne dass sich an der Benutzung (also daran, wie man die Arrayfunktionalität später aufruft) irgend etwas ändert.&lt;br /&gt;
&lt;br /&gt;
== Das JSON-Datenformat ==&lt;br /&gt;
&lt;br /&gt;
Die zweite Kante des Datenstruktur-Dreiecks ist das &amp;quot;Datenformat&amp;quot;. Hier legt man Speicherlayout und Bedeutung fest. Ein Datenformat dient vor allem zum Speichern von Daten auf Festplatte und zum Austausch von Daten zwischen verschiedenen Programmen bzw. Internetseiten. Im Fall von assoziativen Arrays setzt sich dafür das [http://www.json.org/ JSON-Format] immer mehr durch, weil es einfach und trotzdem mächtig ist. Es eignet sich sehr gut zum Speichern von assoziativen Arrays (also von Schlüssel/Wert Paaren) und unterstützt außerdem gewöhnliche Arrays und hierarchische Strukturen, weil die Werte wiederum (gewöhnliche oder assoziative) Arrays sein dürfen.&lt;br /&gt;
&lt;br /&gt;
Das Speicherlayout einer JSON-Dateien ist definiert als eine Bytefolge, die als Zeichenfolge gemäß [http://de.wikipedia.org/wiki/UTF-8 UTF-8-Standard] interpretiert wird. Dies hat zwei Vorteile: einerseits ist das Format dadurch mit allen gängigen Systemen kompatibel und überall gleich, andererseits kann jedes JSON-File einfach in einem Texteditor geöffnet und editiert werden und ist für Menschen und Maschinen gleichermaßen leicht lesbar.&lt;br /&gt;
&lt;br /&gt;
Die Zuordung einer Bedeutung zu einem gegebenen Speicherinhalt erfolgt in JSON mit Hilfe einer Grammatik. Ein JSON-File enthält entweder ein gewöhnliches Array oder ein assoziatives Array (Dictionary):&lt;br /&gt;
  JSON_file := array&lt;br /&gt;
             | dictionary&lt;br /&gt;
Ein gewöhnliches Array wird als Folge von einem oder mehreren Elementen geschrieben, die durch Komma getrennt und in eckigen Klammern eingeschlossen sind (Zeichen, die in der Grammatik in einfache Anführungszeichen eingeschlossen sind, müssen explizit im JSON-File stehen). Leere Arrays sind ebenfalls erlaubt:&lt;br /&gt;
  array    := '[' elements ']'&lt;br /&gt;
            | '[' ']'&lt;br /&gt;
  elements := value&lt;br /&gt;
            | value ',' elements&lt;br /&gt;
Ein Dictionary wird in ähnlicher Weise als Folge von Schlüssel/Wert-Paaren geschrieben, die durch Komma getrennt und in geschweiften Klammern eingeschlossen sind. Leere Dictionaries sind erlaubt. Die Schlüssel müssen immer Strings sein, gefolgt von einem Doppelpunkt:&lt;br /&gt;
  dictionary := '{' pairs '}'&lt;br /&gt;
              | '{' '}'&lt;br /&gt;
  pairs      := string ':' value&lt;br /&gt;
              | string ':' value ',' pairs&lt;br /&gt;
Strings sind Zeichenfolgen (inklusive einiger Sonderzeichen wie &amp;quot;\n&amp;quot; für einen Zeilenumbruch), die in doppelte Anführungszeichen eingeschlossen sind, oder der Leerstring:&lt;br /&gt;
  string := '&amp;quot;' '&amp;quot;'  &lt;br /&gt;
          | '&amp;quot;'characters'&amp;quot;'&lt;br /&gt;
Werte können Zahlen (ganze oder reelle Zahlen, definiert wie in Python), Boolesche Werte ('true' oder 'false'), Strings oder 'null' sein. Außerdem können Arrays und Dictionaries wiederum als Werte verwendet werden, wodurch sich beliebig tief geschachtelte, hierarchische Datenstrukturen ergeben:&lt;br /&gt;
  value := number | string | boolean | null | array | dictionary&lt;br /&gt;
Hier ist ein einfaches Beispiel für ein JSON-File, das Ausschnitte aus einer Studenten-Datenbank enthält:&lt;br /&gt;
   {&lt;br /&gt;
       &amp;quot;Müller, Friedrich&amp;quot; : {&lt;br /&gt;
            &amp;quot;Mathematik&amp;quot; : 2.0,&lt;br /&gt;
            &amp;quot;ALDA&amp;quot; : 1.3&lt;br /&gt;
       },&lt;br /&gt;
       &amp;quot;Weise, Anna&amp;quot; : {&lt;br /&gt;
            &amp;quot;Mathematik&amp;quot; : 1.0,&lt;br /&gt;
            &amp;quot;Philosophie&amp;quot;: 1.3&lt;br /&gt;
       }&lt;br /&gt;
   }&lt;br /&gt;
Das JSON-Format ist syntaktisch der Sprache Python sehr nahe, und kann mit einigen verherigen Definitionen direkt durch die &amp;lt;tt&amp;gt;eval()&amp;lt;/tt&amp;gt;-Funktion in ein Python-Dictionary oder -Array umgewandelt werden.&lt;br /&gt;
   # don't do this - it is highly '''unsafe''' and dangerous&lt;br /&gt;
   true, false, null = True, False, None                  # fehlende Konstanten defininieren&lt;br /&gt;
   res = eval(file(&amp;quot;test.json&amp;quot;).read().decode(&amp;quot;utf_8&amp;quot;))   # File einlesen und nach Python konvertieren&lt;br /&gt;
Dies sollte man jedoch '''auf keinen Fall''' tun, weil ein Hacker dadurch beliebigen Code ausführen könnte, den er vorher in das File 'test.json' eingeschmuggelt hat. Da die Funktion &amp;lt;tt&amp;gt;eval()&amp;lt;/tt&amp;gt; nur prüft, ob der Ausdruck gültiges Python ist, aber nicht, ob das File gültiges JSON (also nur Daten, aber keinen ausführbaren Code) enthält, kann man dies nicht erkennen oder verhindern. Deshalb sollte man zum Einlesen von JSON stets das Python-Modul [http://docs.python.org/library/json.html json] verwenden, das ein manipuliertes File einfach zurückweisen würde:&lt;br /&gt;
   # sicheres Einlesen und Konvertieren&lt;br /&gt;
   import json, codecs&lt;br /&gt;
   &lt;br /&gt;
   with codecs.open(&amp;quot;test.json&amp;quot;, 'r', encoding='utf-8') as f: # File im UTF-8 Format öffnen &lt;br /&gt;
       res = json.load(f)                                     # und als json einlesen&lt;br /&gt;
&lt;br /&gt;
==Implementation von assoziativen Array-Klassen==&lt;br /&gt;
&lt;br /&gt;
Die dritte Kante des Datenstruktur-Dreiecks bezieht sich schließlich auf die Realisierung der Datenstruktur als Klasse, indem man auf geeignet organisiertem Speicher die geforderten Operationen implementiert. In Python ist mit der Klasse &amp;lt;tt&amp;gt;dict&amp;lt;/tt&amp;gt; eine sehr leistungsfähige Implementation eines assoziativen Arrays integraler Bestandteil der Sprache. Diese Implementation beruht auf dem Konzept der Hashtabellen, das wir in der Vorlesung [[Hashing und Hashtabellen|später behandeln]]. Man benötigt dafür eine Funktion &amp;lt;tt&amp;gt;hash(key)&amp;lt;/tt&amp;gt;, die in Python für alle Standarddatentypen bereits implementiert ist. In diesem Abschnitt wollen wir zwei alternative Implementationen auf der Basis von sequentieller Suche und auf der Basis von Suchbäumen betrachten.&lt;br /&gt;
&lt;br /&gt;
=== Realisierung durch sequentielle Suche===&lt;br /&gt;
&lt;br /&gt;
Wenn für die Schlüssel nur ein Identitätsvergleich &lt;br /&gt;
   key1 == key2&lt;br /&gt;
definiert ist, hat man keine Möglichkeit, die Schlüsselsuche durch eine spezielle Datenstruktur zu beschleunigen. Man speichert die Schlüssel/Wert-Paare dann einfach in einem gewöhnlichen Array, das man sequentiell durchsucht. Dazu implementieren wir zunächst eine Hilfsklasse, die Schlüssel/Wert-Paare aufnimmt:&lt;br /&gt;
   class KeyValuePair:&lt;br /&gt;
       def __init__(self, key, value):&lt;br /&gt;
           self.key = key&lt;br /&gt;
           self.value = value&lt;br /&gt;
Die Arrayklasse speichert die Paare in einem Array &amp;lt;tt&amp;gt;self.data&amp;lt;/tt&amp;gt;, dessen aktuelle Länge der Größe des assoziativen Arrays entspricht. Damit ist das Speicherlayout der Klasse festgelegt:&lt;br /&gt;
   class SequentialSearchArray:&lt;br /&gt;
       def __init__(self):&lt;br /&gt;
           self.data = []&lt;br /&gt;
       def __len__(self):&lt;br /&gt;
           return len(self.data)&lt;br /&gt;
Um auf die Daten zugreifen zu können, müssen wir nach dem richtigen Schlüssel suchen. Dazu implementieren wir die Hilfsfunktion &amp;lt;tt&amp;gt;findKey&amp;lt;/tt&amp;gt;, die den Index des Schlüssels zurückgibt, oder &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;, wenn der Schlüssel nicht existiert:&lt;br /&gt;
       def findKey(self, key):&lt;br /&gt;
           for k in xrange(len(self.data)):&lt;br /&gt;
               if key == self.data[k].key:&lt;br /&gt;
                   return k&lt;br /&gt;
           return None&lt;br /&gt;
Beim Einfügen eines Elements müssen wir zuerst prüfen, ob es den Schlüssel schon gibt, und dann entweder die daten überschreiben oder ein neues Element anfügen:&lt;br /&gt;
       def __setitem__(self, key, value):&lt;br /&gt;
           k = self.findKey(key)&lt;br /&gt;
           if k is None:&lt;br /&gt;
               self.data.append(KeyValuePair(key, value))  # neues Paar einfügen&lt;br /&gt;
           else:&lt;br /&gt;
               self.data[k].value = value                  # Daten ersetzen&lt;br /&gt;
Die Suche hingegen löst eine Exception aus, wenn der Schlüssel nicht gefunden wurde:&lt;br /&gt;
       def __getitem__(self, key):&lt;br /&gt;
           k = self.findKey(key)&lt;br /&gt;
           if k is None:&lt;br /&gt;
               raise KeyError(key)        # Schlüssel nicht gefunden =&amp;gt; Fehler&lt;br /&gt;
           else:&lt;br /&gt;
               return self.data[k].value  # Schlüssel gefunden =&amp;gt; Daten zurückgeben&lt;br /&gt;
Die übrigen geforderten Funktionen sind ebenso einfach zu implementieren:&lt;br /&gt;
       def has_key(self, key):&lt;br /&gt;
           return (self.findKey(key) is not None)&lt;br /&gt;
       &lt;br /&gt;
       def __delitem__(self, key):&lt;br /&gt;
           k = self.findKey(key)&lt;br /&gt;
           if k is None:&lt;br /&gt;
               raise KeyError(key)     # Schlüssel nicht gefunden =&amp;gt; Fehler&lt;br /&gt;
           else:&lt;br /&gt;
               del self.data[k]        # Schlüssel gefunden =&amp;gt; löschen&lt;br /&gt;
Wegen der sequentiellen Suche hat der Zugriff auf ein Element in dieser Datenstruktur die Komplexität O(len(a)).&lt;br /&gt;
&lt;br /&gt;
=== Realisierung als Suchbaum ===&lt;br /&gt;
&lt;br /&gt;
Wenn für den Schlüsseltyp des assoziativen Arrays eine Ordnung definiert ist (wenn also &amp;lt;tt&amp;gt;key1 &amp;lt; key2&amp;lt;/tt&amp;gt; oder &amp;lt;tt&amp;gt;cmp(key1, key2)&amp;lt;/tt&amp;gt; unterstützt werden), kann man das Indexierungsproblem auf das Suchproblem zurückführen. Dann kann das assoziative Array effizient als selbst-balancierender Suchbaum imlementiert werden, so dass die Zugriffsfunktionen nur noch eine Komplexität in O(log(len(a))) haben. Die Datenstruktur des Suchbaums muss dafür so erweitert werden, dass zu jedem Schlüssel auch die zugehörigen Daten gespeichert werden. Man erweitert die Node-Klasse deshalb um ein Feld &amp;quot;value&amp;quot;:&lt;br /&gt;
    class Node:&lt;br /&gt;
        def __init__(self, key, value):&lt;br /&gt;
            self.key = key&lt;br /&gt;
            self.data = value&lt;br /&gt;
            self.left = self.right = None&lt;br /&gt;
Dann kann man eine Klasse &amp;lt;tt&amp;gt;TreeSearchArray&amp;lt;/tt&amp;gt; realisieren, deren Konstruktor einen leeren Suchbaum initialisiert:&lt;br /&gt;
    class TreeSearchArray:&lt;br /&gt;
        def __init__(self):&lt;br /&gt;
            self.root = None&lt;br /&gt;
            self.size = 0&lt;br /&gt;
        def __len__(self):&lt;br /&gt;
            return self.size&lt;br /&gt;
Die Funktion &amp;lt;tt&amp;gt;__setitem__&amp;lt;/tt&amp;gt; schaut nach, ob ein Eintrag mit dem betreffenden Schlüssel bereits existiert. Wenn ja, werden seine Daten mit den neuen Daten überschrieben, andernfalls wird ein neuer Eintrag angelegt. Intern werden dazu die bereits bekannten Funktionen &amp;lt;tt&amp;gt;treeSearch&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;treeInsert&amp;lt;/tt&amp;gt; verwendet (siehe Abschnitt [[Suchen#Suchbäume|Suchbäume]]):&lt;br /&gt;
        def __setitem__(self, key, value):&lt;br /&gt;
             node = treeSearch(self.root, key)&lt;br /&gt;
             if node is None:&lt;br /&gt;
                 self.root = treeInsert(self.root, key)&lt;br /&gt;
                 self.size += 1&lt;br /&gt;
                 node = treeSearch(self.root, key) &lt;br /&gt;
             node.value = value&lt;br /&gt;
(Eine geschicktere Implementation würde natürlich den zweiten Aufruf von &amp;lt;tt&amp;gt;treeSearch&amp;lt;/tt&amp;gt; eliminieren und das Setzen der Daten gleich in &amp;lt;tt&amp;gt;treeInsert&amp;lt;/tt&amp;gt; erledigen. Dies ändert aber nichts an der Komplexität der Funktion.) Die Funktion &amp;lt;tt&amp;gt;__getitem__&amp;lt;/tt&amp;gt; sucht ebenfalls einen Eintrag mit dem gegebenen Schlüssel. Wenn er gefunden wird, gibt sie die zugehörigen Daten zurück, andernfalls eine Fehlermeldung:&lt;br /&gt;
        def __getitem__(self, key):&lt;br /&gt;
            node = treeSearch(self.root, key)&lt;br /&gt;
            if node is None:&lt;br /&gt;
                raise KeyError(key)&lt;br /&gt;
            else:&lt;br /&gt;
                return node.value&lt;br /&gt;
Die Indexoperationen haben bei der Realisierung mit Baumsuche eine Komplexität in O(log n).&lt;br /&gt;
&lt;br /&gt;
Ein wichtiges Beispiel für ein assoziatives Array, das auf diese Weise realisiert wurde, ist die C++ Standardklasse &amp;lt;tt&amp;gt;[http://www.sgi.com/tech/stl/Map.html std::map]&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
[[Hashing und Hashtabellen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Sortieren_in_linearer_Zeit&amp;diff=5710</id>
		<title>Sortieren in linearer Zeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Sortieren_in_linearer_Zeit&amp;diff=5710"/>
				<updated>2020-07-02T16:24:23Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Bucket Sort */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Wir kehren an dieser Stelle nochmals zum Sortierproblem zurück und stellen uns die Frage, ob wir noch schnellere Algorithmen finden können, die eventuell sogar in O(N) statt in O(N*log(N)) zum Ziel kommen. Mit Hilfe der gerade eingeführten Suchbäumen werden wir zeigen, dass dies nicht möglich ist, solange für die Sortierschlüssel nur eine paarweise Vergleichsfunktion definiert ist. Besitzen wir jedoch zusätzliche Informationen über die Schlüssel, die uns die Anwendung des ''Bucket-Prinzips'' erlauben, ist das Sortieren in linearer Zeit möglich.&lt;br /&gt;
&lt;br /&gt;
== Sortieren und Permutationen ==&lt;br /&gt;
&lt;br /&gt;
Bevor wir die Grenzen des Sortierens mit Paarvergleichen analysieren, wollen wir noch etwas näher beleuchten, was beim Sortieren eigentlich geschieht. Dazu gehen wir noch einen Schritt zurück und schauen uns an, was beim Mischen eines zunächst sortierten Arrays passiert. Wir betrachten das Array mit den drei Element A, B, C sowie ein korrespondierendes ''Indexarray'', das angibt, an welcher Position im sortierten Array die drei Elemente gehören. Solange das Hauptarray noch sortiert ist, enthält das Indexarray einfach die aufsteigende Folge 0, 1, 2:&lt;br /&gt;
   L = A B C     # Hauptarray sortiert (0. Permutation)&lt;br /&gt;
   I = 0 1 2     # Indexarray&lt;br /&gt;
Es gibt jetzt 5 weitere Anordnungsmöglichkeiten (imsgesamt also 6 = 3! Permutationen) für die drei Elementa A, B und C. Immer, wenn wir diese drei Elemente umordnen, ordnen wir das Indexarray so, dass das Element &amp;lt;tt&amp;gt;I[k]&amp;lt;/tt&amp;gt; in jedem Indexarray uns angibt, wo sich der Buchstabe jetzt befindet, der ursprünglich (also in der sortierten Anordnung) an Position &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; stand:&lt;br /&gt;
   L = A C B     # 1. Permutation&lt;br /&gt;
   I = 0 2 1 &lt;br /&gt;
&lt;br /&gt;
   L = B A C     # 2. Permutation&lt;br /&gt;
   I = 1 0 2&lt;br /&gt;
&lt;br /&gt;
   L = B C A     # 3. Permutation&lt;br /&gt;
   I = 2 0 1&lt;br /&gt;
&lt;br /&gt;
   L = C A B     # 4. Permutation&lt;br /&gt;
   I = 1 2 0&lt;br /&gt;
&lt;br /&gt;
   L = C B A     # 5. Permutation&lt;br /&gt;
   I = 2 1 0&lt;br /&gt;
In der 5. Permutation sagt beispielsweise &amp;lt;tt&amp;gt;I[0] = 2&amp;lt;/tt&amp;gt;, dass der ursprünglich 0. Buchstabe (das A) jetzt an Position 2 ist. Daraus folgt, dass wir ein permutiertes Array in linearer Zeit sortieren können, wenn uns das Indexarray bekannt ist. Wir müssen einfach einmal durch das Indexarray gehen und jedes Element von Position &amp;lt;tt&amp;gt;I[k]&amp;lt;/tt&amp;gt; wieder an Position &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; verschieben:&lt;br /&gt;
   def sortByIndexArray(L, I):&lt;br /&gt;
       R = [None]*len(L)        # zunächst leeres Ergebnisarray&lt;br /&gt;
       for k in xrange(len(L)):&lt;br /&gt;
           R[k] = L[I[k]]       # Elemente sortiert in R einfügen&lt;br /&gt;
       return R&lt;br /&gt;
Da man nur einmal über den Bereich k = 0 ... len(L)-1 gehen muss, ist der Aufwand dieser Funktion O(len(L)), also linear. Dieser Algorithmus ist z.B. nützlich, wenn man mehrere Arrays in der gleichen Weise sortieren muss, z.B. die Liste der Studentennamen und die Liste der dazugehörigen Übungspunkte. Man kann dann einfach einmal die Permutation des Indexarrays bestimmen, und dann alle Listen entsprechend sortieren. Wie man das Indexarray mit dem Standard-Sortieralgorithmus &amp;lt;tt&amp;gt;array.sort()&amp;lt;/tt&amp;gt; bestimmen kann, ist Aufgabe im Übungsblatt 9.&lt;br /&gt;
&lt;br /&gt;
==Sortieren als Suchproblem==&lt;br /&gt;
&lt;br /&gt;
Wir haben gesehen, dass wir in linearer Zeit sortieren können, wenn uns die Permutation bzw. das zugehörige Indexarray bekannt ist. Die nächste Frage lautet deshalb: Wie viele Schritte brauchen wir, um die Permutation zu finden? Dabei ist es nur erlaubt, Schlüssel paarweise zu vergleichen, und man erhält jeweils eine ja/nein Antwort. Ein solches Vorgehen kann als Entscheidungsbaum dargestellt werden. Jeder Knoten ist eine Frage, und wir gehen zum linken Kind weiter, wenn die Frage mit &amp;quot;ja&amp;quot; beantwortet wurde, ansonsten zum rechten Kind. An jeder Kante stehen die jetzt noch in Frage kommenden Permutationen, und der jeweilige Kindknoten gibt uns die nächste Frage vor. Die Blätter enthalten das Indexarray, das der Permutation entspricht:&lt;br /&gt;
                                  (L[0] &amp;lt; L[1])&lt;br /&gt;
                          ja     /             \  nein&lt;br /&gt;
                        ABC     /               \    BAC&lt;br /&gt;
                        ACB    /                 \   CAB&lt;br /&gt;
                        BCA   /                   \  CBA&lt;br /&gt;
                             /                     \&lt;br /&gt;
                      (L[0] &amp;lt; L[2])           (L[0] &amp;lt; L[2])&lt;br /&gt;
                     /            \           /            \&lt;br /&gt;
               ja   /        nein  \         /  ja          \  nein&lt;br /&gt;
             ABC   /          BCA   |       |   BAC          \   CAB&lt;br /&gt;
             ACB  /                 |       |                 \  CBA&lt;br /&gt;
                 /               (2 0 1) (1 0 2)               \&lt;br /&gt;
          (L[1] &amp;lt; L[2])                                   (L[1] &amp;lt; L[2])&lt;br /&gt;
          /            \                                 /             \&lt;br /&gt;
     ja  /              \  nein                    ja   /               \  nein&lt;br /&gt;
   ABC  /                \   ACB                 CAB   /                 \   CBA&lt;br /&gt;
       /                  \                           /                   \&lt;br /&gt;
    (0 1 2)             (0 2 1)                   (1 2 0)               (2 1 0)&lt;br /&gt;
&lt;br /&gt;
Der Suchaufwand im schlechtesten Fall entspricht offensichtlich der Tiefe des Baumes. Bei Arrays mit drei Elementen ist die Tiefe gerade 3, wir benötigen maximal 3 Fragen bis zum Ziel. Für Arrays der Länge n gilt allgemein: Es gibt N = n! verschiedene Permutationen, der Baum muss also n! Blätter haben. Wir haben im Abschnitt [[Suchen#Balance_eines_Suchbaumes|Suchen]] gesehen, dass die Tiefe eines Baumes minimal wird, wenn der Baum ''perfekt balanciert'' ist, und dass der balancierte Baum mit den meisten Blättern der ''vollständige Baum ist''. Die Tiefe des vollständigen Baums mit n! Blättern gibt uns also eine untere Schranke für die minimale Anzahl der Vergleiche im schlechtesten Fall. &lt;br /&gt;
&lt;br /&gt;
Ein vollständiger Baum der Tiefe d hat 2&amp;lt;sup&amp;gt;d+1&amp;lt;/sup&amp;gt;-1 Knoten und 2&amp;lt;sup&amp;gt;d&amp;lt;/sup&amp;gt; Blätter:&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|-valign=&amp;quot;center&amp;quot; &lt;br /&gt;
|[[Image:vollbaum.png|left]]&lt;br /&gt;
| vollständiger Baum &amp;lt;br&amp;gt;2&amp;lt;sup&amp;gt;d+1&amp;lt;/sup&amp;gt;-1 Knoten&amp;lt;br&amp;gt;2&amp;lt;sup&amp;gt;d&amp;lt;/sup&amp;gt; Blätter&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Im Fall des Sortierens von n Elementen gilt, dass es N = n! mögliche Permutation gibt. Ein Baum mit n! Blättern hat mindestens die Tiefe log(n!). Im obigen Beispiel für n=3 gilt 3! = 1*2*3 = 6 und damit für die Tiefe d&lt;br /&gt;
:::&amp;lt;math&amp;gt;d = \lceil \log_2(6)\rceil \approx \lceil 2.6\rceil = 3&amp;lt;/math&amp;gt;&lt;br /&gt;
Im ungünstigsten Fall braucht man bei dem Frage-Baum drei Schritte. Weil aber &amp;lt;math&amp;gt;\log(6)\approx 2.6 &amp;lt; 3&amp;lt;/math&amp;gt; muss nicht jeder Pfad zu Ende durchlaufen werden, um die Lösung zu bekommen.&lt;br /&gt;
&lt;br /&gt;
Allgemein gilt&lt;br /&gt;
::&amp;lt;math&amp;gt;d \ge \log_2(n!)&amp;lt;/math&amp;gt;&lt;br /&gt;
Wir können die Tiefe am einfachsten durch die ''Stirlingsche Näherungsformel für die Fakultät'' abschätzen:&lt;br /&gt;
::&amp;lt;math&amp;gt;n! \approx \sqrt{2\pi n} \left(\frac{n}{e}\right)^n&amp;lt;/math&amp;gt;,&lt;br /&gt;
die asymptitisch für große n gilt. Einsetzen liefert&lt;br /&gt;
::&amp;lt;math&amp;gt;d \ge \log_2(n!) \in \Omega\left(\log_2\left(\sqrt{2\pi n} \left(\frac{n}{e}\right)^n\right)\right)&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Logarithmus eines Produkts ist gleich der Summe der Logarithmen der einzelnen Faktoren:&lt;br /&gt;
::&amp;lt;math&amp;gt;\Omega\left(\log_2\left(\sqrt{2\pi n} \left(\frac{n}{e}\right)^n\right)\right) = \Omega(\log_2(\sqrt{2\pi})) + \Omega(\log_2(\sqrt{n})) + \Omega(\log_2(n^n)) - \Omega(\log_2(e^n))&amp;lt;/math&amp;gt;&lt;br /&gt;
Wir vereinfachen die rechte Seite nach den Regeln der O-Notation: nur der am schnellsten wachsende Term bleibt übrig:&lt;br /&gt;
::&amp;lt;math&amp;gt;\Omega\left(\log_2\left(\sqrt{2\pi n} \left(\frac{n}{e}\right)^n\right)\right) = \Omega(\log_2(n^n))&amp;lt;/math&amp;gt;.&lt;br /&gt;
Den Exponenten in n&amp;lt;sup&amp;gt;n&amp;lt;/sup&amp;gt; kann man vor den Logarithmus ziehen, und die Basis des Logarithmus spielt keine Rolle. Wir erhalten somit:&lt;br /&gt;
::&amp;lt;math&amp;gt;d \in \Omega(n \log n)&amp;lt;/math&amp;gt;.&lt;br /&gt;
Somit braucht man im schlechtesten Fall mindestens &amp;lt;math&amp;gt;\Omega(n \log n)&amp;lt;/math&amp;gt; Vergleiche, und Merge Sort ist somit optimal und kann nicht weiter verbessert werden, solange man sich auf paarweise Vergleiche von Schlüsseln beschränkt.&lt;br /&gt;
&lt;br /&gt;
Eine exakte Herleitung dieser Tatsache, ohne Verwendung der Stirlingschen Formel, ist möglich durch &lt;br /&gt;
&lt;br /&gt;
===Abschätzung von Summen durch Integrale===&lt;br /&gt;
&lt;br /&gt;
Schreibt man die Fakultät als Produkt aus, und transformiert den Logarithmus des Produkts in eine Summe von Logarithmen, erhalten wir:&lt;br /&gt;
::&amp;lt;math&amp;gt;d \ge \log_2(n!) = \log_2(1\cdot 2\cdot ... \cdot n) = \log_2(1) + \log_2(2) + ... + \log_2(n) = \sum_{k=1}^n \log_2(k) = \frac{1}{\ln(2)}\sum_{k=1}^n \ln(k) = \frac{1}{\ln(2)}\sum_{k=2}^n \ln(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die letzte Identität gilt, weil &amp;lt;math&amp;gt;\ln(1) = 0&amp;lt;/math&amp;gt; in der Summe weggelassen werden kann. Eine untere Schranke für die Tiefe kann man explizit bestimmen durch die Methode der&lt;br /&gt;
 &lt;br /&gt;
Gegeben sei eine monoton wachsende Funktion f(x) (blaue Kurve). Das bestimmte Integral über die Funktion sei&lt;br /&gt;
:::&amp;lt;math&amp;gt;\int_{x_1}^{x_2} f(x)dx&amp;lt;/math&amp;gt;. &lt;br /&gt;
Wenn wir das Funktionsargument x abrunden (schwarze Kurve), entsteht ein Integral, das einen kleineren Wert als das ursprüngliche Integral hat. Runden wir auf (rote Kurve), entsteht ein Integral mit einem größeren Wert:&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|-valign=&amp;quot;center&amp;quot; &lt;br /&gt;
|[[Image:integralGraph.png|400px|left]]&lt;br /&gt;
| &amp;lt;math&amp;gt;\int_{x_1}^{x_2} f(\lfloor x \rfloor)dx \le \int_{x_1}^{x_2} f(x)dx \le \int_{x_1}^{x_2} f(\lceil x \rceil)dx&amp;lt;/math&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
In unserem Zusammenhang sind x&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; und x&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; positive ganze Zahlen. Deshalb gilt&lt;br /&gt;
:::&amp;lt;math&amp;gt;f(\lfloor x \rfloor)_{x_1}^{x_1+1}= f(x_1),&amp;lt;/math&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;f(\lfloor x \rfloor)_{x_1+1}^{x_1+2}= f(x_1+1)&amp;lt;/math&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;...&amp;lt;/math&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;f(\lfloor x \rfloor)_{x_2-1}^{x_2}= f(x_2-1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Wir können die obigen Integrale daher folgendermaßen vereinfachen:&lt;br /&gt;
:::&amp;lt;math&amp;gt;\begin{array}{lcl}&lt;br /&gt;
\int_{x_1}^{x_2} f(\lfloor x \rfloor) dx &amp;amp;=&amp;amp; \int_{x_1}^{x_1 + 1} f(\lfloor x \rfloor) dx + ...+ \int_{x_2-1}^{x_2} f(\lfloor x \rfloor) dx \\&lt;br /&gt;
&amp;amp; = &amp;amp; \int_{x_1}^{x_1 + 1} f(x_1) dx + ...+ \int_{x_2-1}^{x_2} f(x_2-1) dx \\&lt;br /&gt;
&amp;amp; = &amp;amp; f(x_1) \int_{x_1}^{x_1 + 1} dx + ...+ f(x_2-1) \int_{x_2-1}^{x_2} dx \\&lt;br /&gt;
&amp;amp; = &amp;amp; f(x_1)  + ...+ f(x_2-1) \\&lt;br /&gt;
&amp;amp; = &amp;amp; \sum_{k=x_1}^{x_2-1} f(k)&lt;br /&gt;
\end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
für die Fläche unter den schwarzen Rechtecken sowie&lt;br /&gt;
:::&amp;lt;math&amp;gt;\begin{array}{lcl}&lt;br /&gt;
\int_{x_1}^{x_2} f(\lceil x \rceil) dx &amp;amp;=&amp;amp; \int_{x_1}^{x_1 + 1} f(\lceil x \rceil) dx + ...+ \int_{x_2-1}^{x_2} f(\lceil x \rceil) dx \\&lt;br /&gt;
&amp;amp; = &amp;amp; \int_{x_1}^{x_1 + 1} f(x_1+1) dx + ...+ \int_{x_2-1}^{x_2} f(x_2) dx \\&lt;br /&gt;
&amp;amp; = &amp;amp; f(x_1+1) \int_{x_1}^{x_1 + 1} dx + ...+ f(x_2) \int_{x_2-1}^{x_2} dx \\&lt;br /&gt;
&amp;amp; = &amp;amp; f(x_1+1)  + ...+ f(x_2) \\&lt;br /&gt;
&amp;amp; = &amp;amp; \sum_{k=x_1+1}^{x_2} f(k)&lt;br /&gt;
\end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
für die Fläche unter den roten Rechtecken. Zusammenfassend gilt also&lt;br /&gt;
&amp;lt;math&amp;gt; \sum_{k=x_1}^{x_2-1} f(k) \le \int_{x_1}^{x_2} f(x)dx&amp;lt;/math&amp;gt; und &lt;br /&gt;
&amp;lt;math&amp;gt; \sum_{k=x_1+1}^{x_2} f(k) \ge \int_{x_1}^{x_2} f(x)dx&amp;lt;/math&amp;gt;&lt;br /&gt;
Für unser Problem setzen wir f(k) = ln(k), x&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;+1 = 2, und x&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; = n. Also können wir abschätzen&lt;br /&gt;
:::&amp;lt;math&amp;gt;\sum_{k=x_1+1}^{x_2} f(k) = \frac{1}{\ln(2)}\sum_{k=2}^{n} \ln(k) \ge \frac{1}{\ln(2)}\int_1^n \ln(x) dx&amp;lt;/math&amp;gt;&lt;br /&gt;
Das Integral ist leicht zu lösen, und wir erhalten&lt;br /&gt;
:::&amp;lt;math&amp;gt;\frac{1}{\ln(2)}\sum_{k=2}^{n} \ln(k) \ge \frac{1}{\ln(2)}\left[x\ln(x)-x\right]_{x=1}^{n} = \frac{1}{\ln(2)}(n\ln(n)-n+1)=n\log_2(n) - \frac{n-1}{\ln(2)} \in \Omega(n \log(n))&amp;lt;/math&amp;gt;&lt;br /&gt;
Folglich gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;d\ge\log_2(n!) = \frac{1}{\ln(2)}\sum_{k=2}^{n} \ln(k) \in \Omega(n \log(n))&amp;lt;/math&amp;gt;&lt;br /&gt;
Mit anderen Worten: '''Kein Sortieralgorithmus auf Basis paarweise Vergleiche ist asymptotisch schneller als Mergesort, denn die Anzahl der Vergleiche (= Tiefe des Entscheidungsbaumes) ist &amp;lt;math&amp;gt;\Omega(n \log(n))&amp;lt;/math&amp;gt;'''. Falls man einen schnelleren Sortieralgorithmus benötigt, muss man ein anderes algorithmisches Prinzip verwenden.&lt;br /&gt;
&lt;br /&gt;
==Effizientere Sortieralgorithmen==&lt;br /&gt;
&lt;br /&gt;
Wir haben gezeigt, dass mit paarweisen Größenvergleichen allein kein Sortieralgorithmus schneller als &amp;lt;math&amp;gt;\Omega(n \log n)&amp;lt;/math&amp;gt; sein kann. Um einen besseren Algorithmus zu finden, dürfen wir nicht nur die relative Größe der Schlüssel (also die Ordnung) berücksichtigen, sondern müssen die Werte selbst verwenden. Der entscheidende Trick dabei ist das &lt;br /&gt;
&lt;br /&gt;
=== Bucket-Prinzip ===&lt;br /&gt;
&lt;br /&gt;
Man definiert eine Funktion &amp;lt;tt&amp;gt;quantize(key, M)&amp;lt;/tt&amp;gt;, die jeden Schlüssel auf eine ganze Zahl im Bereich &amp;lt;tt&amp;gt;[0,...,M-1]&amp;lt;/tt&amp;gt; abbildet. Mit Hilfe dieser Zahlen werden die Schlüssel auf M ''Buckets'' aufgeteilt, und das Sortieren kann dann in jedem Bucket getrennt erfolgen. Am Ende setzt man aus den Inhalten der Buckets das gesamte Array (sortiert) wieder zusammen. Wir zeigen unten, dass man damit lineare Zeit erreicht.&lt;br /&gt;
&lt;br /&gt;
Um für das Sortieren brauchbar zu sein, muss die Funktion &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt; ''Ordnung erhaltend'' definiert sein:&lt;br /&gt;
   wenn key1 &amp;lt;= key2, gilt auch quantize(key1, M) &amp;lt;= quantize(key2, M)&lt;br /&gt;
Eine solche Abbildung nennt man ''Quantisierung''. Allgemein bekannt ist der Prozess der Quantisierung z.B. bei Digitalkameras: Hier wird in jedem Pixel eine reell-wertige Lichtintensität gemessen, die im resultierenden Bild mit nur 256 Intensitätsabstufungen pro Farbe abgespeichert wird (bzw. mit bis zu 65536 Abstufungen bei Kameras mit sogenanntem &amp;quot;high dynamic range&amp;quot;). Bleibt die Ordnung bei der Abbildung von Schlüsseln auf natürliche Zahlen nicht erhalten, spricht man von ''Hashing''. Hashing wird zur Implementation von [[Hashing und Hashtabellen|Hashtabellen]] nutzbringend eingesetzt.&lt;br /&gt;
&lt;br /&gt;
Der einfachste Fall liegt vor, wenn die Schlüssel bereits genze Zahlen im Bereich &amp;lt;tt&amp;gt;[0,...,M-1]&amp;lt;/tt&amp;gt; sind. Dann ist die &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt; einfach die Identität. Wir nehmen an, dass die Daten als Schlüssel/Wert-Paare in einem Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; gespeichert sind, und wir können das Sortieren wie folgt implementieren:&lt;br /&gt;
   def integerSort(a, M):&lt;br /&gt;
       # erzeuge M leere Buckets&lt;br /&gt;
       buckets = [[] for k in range(M)]  &lt;br /&gt;
       &lt;br /&gt;
       # verteile die Daten auf die Buckets&lt;br /&gt;
       for k in range(len(a)):           &lt;br /&gt;
           buckets[a[k].key].append(a[k]) # a[k].key sind Integer-Schlüssel in [0,...,M-1]&lt;br /&gt;
      &lt;br /&gt;
       # setze das Array a aus den Buckets sortiert wieder zusammen&lt;br /&gt;
       start = 0                          # Anfangsindex des ersten Buckets&lt;br /&gt;
       for k in range(M):&lt;br /&gt;
           end = start + len(buckets[k])  # Endindex des aktuellen Buckets&lt;br /&gt;
           a[start:end] = buckets[k]      # Daten an der richtigen Position in a einfügen&lt;br /&gt;
           start = end                    # Anfangsindex für das nächste Bucket aktualisieren&lt;br /&gt;
Das Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; ist am Ende sortiert, weil wir den Inhalt der Buckets nach aufsteigendem Bucket-Index, und damit automatisch nach aufsteigenden Schlüsseln, in &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; einfügen. Das Sortieren ist außerdem ''stabil'', da Daten mit gleichem Schlüssel immer hinten an das jeweilige Bucket angefügt werden. &lt;br /&gt;
&lt;br /&gt;
Die Komplexität des Algorithmus ist &amp;lt;math&amp;gt;O(N)&amp;lt;/math&amp;gt; mit &amp;lt;tt&amp;gt;N = len(a)&amp;lt;/tt&amp;gt;, solange &lt;br /&gt;
::&amp;lt;math&amp;gt;M \in O(N)&amp;lt;/math&amp;gt; &lt;br /&gt;
gilt: Das Erzeugen der Buckets erfordert &amp;lt;math&amp;gt;O(M)&amp;lt;/math&amp;gt; Schritte und das Verteilen der Daten auf die Buckets &amp;lt;math&amp;gt;O(N)&amp;lt;/math&amp;gt; Schritte (weil &amp;lt;tt&amp;gt;bucket[k].append()&amp;lt;/tt&amp;gt; amortisiert konstante Komplexität hat). Das Zusammensetzen des sortierten Arrays wird vom Kopieren der Daten dominiert, welches die Komplexität &lt;br /&gt;
::&amp;lt;math&amp;gt;\sum_{k=0}^{M-1} O(N_k)= O\left(\sum_{k=0}^{M-1} N_k\right)&amp;lt;/math&amp;gt; &lt;br /&gt;
besitzt, wobei &amp;lt;tt&amp;gt;N&amp;lt;sub&amp;gt;k&amp;lt;/sub&amp;gt;=len(buckets[k])&amp;lt;/tt&amp;gt; die Größe des k-ten Buckets ist. Die Gesamtanzahl der Daten in allen Buckets zusammen ist aber gerade wieder die Größe von &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt;, also &lt;br /&gt;
::&amp;lt;math&amp;gt;O\left(\sum_{k=0}^{M-1} N_k\right) = O(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
Nach der Sequenzregel ist die Gesamtkomplexität somit &amp;lt;math&amp;gt;O(M + N)=O(N)&amp;lt;/math&amp;gt;, falls &amp;lt;math&amp;gt;M \in O(N)&amp;lt;/math&amp;gt; gilt.&lt;br /&gt;
&lt;br /&gt;
===Bucket Sort===&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus wird nur wenig komplizierter, wenn die Schlüssel beliebig sein können, aber eine ordnung-erhaltende &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt;-Funktion vorhanden ist. Allerdings geht bei der Quantisierung, also der Abbildung von Schlüsseln auf Bucket-Indizes, ein Teil der Schlüsselinformation verloren. Die Elemente im selben Bucket haben im Allgemeinen nicht exakt den gleichen Schlüssel, so dass jeder Bucket noch explizit sortiert werden muss. Diese Tatsache führt zu einer zusätzlichen Einschränkung: einerseits muss für die Anzahl der Buckets nach wie vor &lt;br /&gt;
::&amp;lt;math&amp;gt;M \in O(N)&amp;lt;/math&amp;gt; &lt;br /&gt;
gelten, aber andererseits sollte jeder Bucket nur wenige Daten enthalten, so dass das Sortieren innerhalb der Buckets effizient ist. Wir fordern deshalb, dass &amp;lt;math&amp;gt;N_k \in O(1)&amp;lt;/math&amp;gt; sein soll. Unter der Voraussetzung, dass &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt; die Daten gleichmäßig auf alle Buckets verteilt (dazu unten mehr), gilt für die Bucketgrößen &lt;br /&gt;
::&amp;lt;math&amp;gt;N_k \in O\left(\frac{N}{M}\right)&amp;lt;/math&amp;gt;&lt;br /&gt;
denn wir verteilen N Elemente auf M Buckets. Beide Bedingungen sind erfüllt, wenn&lt;br /&gt;
::&amp;lt;math&amp;gt;M = \frac{N}{d}&amp;lt;/math&amp;gt;&lt;br /&gt;
gilt, wobei d eine Konstante unabhängig von N ist. In der Praxis erzielt man gute Resultate mit d &amp;amp;asymp; 10 (die beste Wahl hängt im konkreten Fall von der Schlüsselverteilung und von der &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt;-Funktion ab). Wir übergeben die Konstante d und die &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt;-Funktion als Parameter an den Algorithmus:&lt;br /&gt;
   def bucketSort(a, quant, d):&lt;br /&gt;
       N = len(a)&lt;br /&gt;
       M = int(N // d) + 1  # Anzahl der Buckets festlegen (+1, damit es mindestens ein Bucket gibt)&lt;br /&gt;
       &lt;br /&gt;
       # M leere Buckets erzeugen&lt;br /&gt;
       buckets = [[] for k in range(M)]&lt;br /&gt;
       &lt;br /&gt;
       # Daten auf die Buckets verteilen&lt;br /&gt;
       for k in range(len(a)):&lt;br /&gt;
           index = quant(a[k].key, M)    # Bucket-Index berechnen&lt;br /&gt;
           buckets[index].append(a[k])   # a[k] im passenden Bucket einfügen&lt;br /&gt;
       &lt;br /&gt;
       # Daten sortiert wieder in a einfügen&lt;br /&gt;
       start = 0                          # Anfangsindex des ersten Buckets &lt;br /&gt;
       for k in range(M):&lt;br /&gt;
           insertionSort(buckets[k])      # Daten innerhalb des aktuellen Buckets sortieren&lt;br /&gt;
           end = start + len(buckets[k])  # Endindex des aktuellen Buckets&lt;br /&gt;
           a[start:end] = buckets[k]      # Daten an der richtigen Position in a einfügen&lt;br /&gt;
           start += len(buckets[k])       # Anfangsindex für nächsten Bucket aktualisieren&lt;br /&gt;
Wir verwenden zum Sortieren der Daten in jedem Bucket &amp;lt;tt&amp;gt;insertionSort()&amp;lt;/tt&amp;gt;. Dies ist aus zwei Gründen eine gute Wahl: Erstens haben wir die Buckets so konstruiert, dass jeder Bucket nur wenige Elemente enthält (&amp;lt;math&amp;gt;N_k \in O(1)&amp;lt;/math&amp;gt;), und für kleine Arrays ist Insertion Sort der schnellste Algorithmus. Zweitens ist Insertion Sort ein ''stabiler'' Sortieralgorithmus, und demzufolge ist auch das gesamte &amp;lt;tt&amp;gt;bucketSort()&amp;lt;/tt&amp;gt; stabil.&lt;br /&gt;
&lt;br /&gt;
Unter der Voraussetzung, dass &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt; konstante Zeit für die Quantisierung eines Schlüssels benötigt, unterscheidet sich die Komplexitätsanalyse von &amp;lt;tt&amp;gt;bucketSort()&amp;lt;/tt&amp;gt; nur in einem Punkt von &amp;lt;tt&amp;gt;integerSort()&amp;lt;/tt&amp;gt;, nämlich durch das zusätzliche Sortieren in jedem Bucket. Bei Verwendung von Insertion Sort hat dies quadratische Komplexität in &amp;lt;math&amp;gt;N_k&amp;lt;/math&amp;gt;, aber wenn &amp;lt;math&amp;gt;N_k \in O(1)&amp;lt;/math&amp;gt; erfüllt ist, gilt &amp;lt;math&amp;gt;O(N_k^2) = O(1^2) = O(1)&amp;lt;/math&amp;gt;. Das Sortieren hat also konstante Komplexität pro Bucket, und somit ist die Gesamtkomplexität von &amp;lt;tt&amp;gt;bucketSort()&amp;lt;/tt&amp;gt; linear in N, wie am Anfang des Abschnitts gewünscht.&lt;br /&gt;
&lt;br /&gt;
Allerdings steht und fällt diese Analyse damit, dass die &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt;-Funktion die Daten tatsächlich gleichmäßig auf die Buckets verteilt. Andernfalls könnten im schlechtesten Fall alle Daten in einem einzigen Bucket landen, und dann hätte &amp;lt;tt&amp;gt;bucketSort()&amp;lt;/tt&amp;gt; quadratische Komplexität. Die &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt;-Funktion muss deshalb je nach der Wahrscheinlichkeitsverteilung der Schlüssel immer wieder anders festgelegt werden. Sehr einfach ist dies, wenn die Schlüssel in einem gewissen Intervall &amp;lt;tt&amp;gt;[U,...,V)&amp;lt;/tt&amp;gt; gleichverteilt sind: dann kann man einfach das Intervall &amp;lt;tt&amp;gt;[U,...,V)&amp;lt;/tt&amp;gt; durch eine lineare Gleichung auf das Intervall &amp;lt;tt&amp;gt;[0,...M-1]&amp;lt;/tt&amp;gt; abbilden und dann abrunden. Für &amp;lt;tt&amp;gt;U = 0&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;V = 1&amp;lt;/tt&amp;gt; erhalten wir beilspielsweise:&lt;br /&gt;
    '''Beispiel:'''&lt;br /&gt;
    # keys sind reelle Zahlen in [0, 1)&lt;br /&gt;
    &lt;br /&gt;
    def quantize(key, M):&lt;br /&gt;
        return int(key * M)&lt;br /&gt;
Die Definition einer geeigneten &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt;-Funktion für eine andere Schlüsselverteilung ist Bestandteil einer Übungsaufgabe. In der Praxis findet man allerdings, dass die Verteilung der Daten auf die Buckets nicht übermäßig kritisch ist -- &amp;lt;tt&amp;gt;bucketSort()&amp;lt;/tt&amp;gt; bleibt auch dann ein sehr schneller Algorithmus, wenn die Verteilung nicht ganz gleichmäßig gelingt. Die obige Implementation ist somit ein guter Default für viele Anwendungen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Prioritätswarteschlangen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Sortieren_in_linearer_Zeit&amp;diff=5709</id>
		<title>Sortieren in linearer Zeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Sortieren_in_linearer_Zeit&amp;diff=5709"/>
				<updated>2020-07-02T16:20:15Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Bucket Sort */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Wir kehren an dieser Stelle nochmals zum Sortierproblem zurück und stellen uns die Frage, ob wir noch schnellere Algorithmen finden können, die eventuell sogar in O(N) statt in O(N*log(N)) zum Ziel kommen. Mit Hilfe der gerade eingeführten Suchbäumen werden wir zeigen, dass dies nicht möglich ist, solange für die Sortierschlüssel nur eine paarweise Vergleichsfunktion definiert ist. Besitzen wir jedoch zusätzliche Informationen über die Schlüssel, die uns die Anwendung des ''Bucket-Prinzips'' erlauben, ist das Sortieren in linearer Zeit möglich.&lt;br /&gt;
&lt;br /&gt;
== Sortieren und Permutationen ==&lt;br /&gt;
&lt;br /&gt;
Bevor wir die Grenzen des Sortierens mit Paarvergleichen analysieren, wollen wir noch etwas näher beleuchten, was beim Sortieren eigentlich geschieht. Dazu gehen wir noch einen Schritt zurück und schauen uns an, was beim Mischen eines zunächst sortierten Arrays passiert. Wir betrachten das Array mit den drei Element A, B, C sowie ein korrespondierendes ''Indexarray'', das angibt, an welcher Position im sortierten Array die drei Elemente gehören. Solange das Hauptarray noch sortiert ist, enthält das Indexarray einfach die aufsteigende Folge 0, 1, 2:&lt;br /&gt;
   L = A B C     # Hauptarray sortiert (0. Permutation)&lt;br /&gt;
   I = 0 1 2     # Indexarray&lt;br /&gt;
Es gibt jetzt 5 weitere Anordnungsmöglichkeiten (imsgesamt also 6 = 3! Permutationen) für die drei Elementa A, B und C. Immer, wenn wir diese drei Elemente umordnen, ordnen wir das Indexarray so, dass das Element &amp;lt;tt&amp;gt;I[k]&amp;lt;/tt&amp;gt; in jedem Indexarray uns angibt, wo sich der Buchstabe jetzt befindet, der ursprünglich (also in der sortierten Anordnung) an Position &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; stand:&lt;br /&gt;
   L = A C B     # 1. Permutation&lt;br /&gt;
   I = 0 2 1 &lt;br /&gt;
&lt;br /&gt;
   L = B A C     # 2. Permutation&lt;br /&gt;
   I = 1 0 2&lt;br /&gt;
&lt;br /&gt;
   L = B C A     # 3. Permutation&lt;br /&gt;
   I = 2 0 1&lt;br /&gt;
&lt;br /&gt;
   L = C A B     # 4. Permutation&lt;br /&gt;
   I = 1 2 0&lt;br /&gt;
&lt;br /&gt;
   L = C B A     # 5. Permutation&lt;br /&gt;
   I = 2 1 0&lt;br /&gt;
In der 5. Permutation sagt beispielsweise &amp;lt;tt&amp;gt;I[0] = 2&amp;lt;/tt&amp;gt;, dass der ursprünglich 0. Buchstabe (das A) jetzt an Position 2 ist. Daraus folgt, dass wir ein permutiertes Array in linearer Zeit sortieren können, wenn uns das Indexarray bekannt ist. Wir müssen einfach einmal durch das Indexarray gehen und jedes Element von Position &amp;lt;tt&amp;gt;I[k]&amp;lt;/tt&amp;gt; wieder an Position &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; verschieben:&lt;br /&gt;
   def sortByIndexArray(L, I):&lt;br /&gt;
       R = [None]*len(L)        # zunächst leeres Ergebnisarray&lt;br /&gt;
       for k in xrange(len(L)):&lt;br /&gt;
           R[k] = L[I[k]]       # Elemente sortiert in R einfügen&lt;br /&gt;
       return R&lt;br /&gt;
Da man nur einmal über den Bereich k = 0 ... len(L)-1 gehen muss, ist der Aufwand dieser Funktion O(len(L)), also linear. Dieser Algorithmus ist z.B. nützlich, wenn man mehrere Arrays in der gleichen Weise sortieren muss, z.B. die Liste der Studentennamen und die Liste der dazugehörigen Übungspunkte. Man kann dann einfach einmal die Permutation des Indexarrays bestimmen, und dann alle Listen entsprechend sortieren. Wie man das Indexarray mit dem Standard-Sortieralgorithmus &amp;lt;tt&amp;gt;array.sort()&amp;lt;/tt&amp;gt; bestimmen kann, ist Aufgabe im Übungsblatt 9.&lt;br /&gt;
&lt;br /&gt;
==Sortieren als Suchproblem==&lt;br /&gt;
&lt;br /&gt;
Wir haben gesehen, dass wir in linearer Zeit sortieren können, wenn uns die Permutation bzw. das zugehörige Indexarray bekannt ist. Die nächste Frage lautet deshalb: Wie viele Schritte brauchen wir, um die Permutation zu finden? Dabei ist es nur erlaubt, Schlüssel paarweise zu vergleichen, und man erhält jeweils eine ja/nein Antwort. Ein solches Vorgehen kann als Entscheidungsbaum dargestellt werden. Jeder Knoten ist eine Frage, und wir gehen zum linken Kind weiter, wenn die Frage mit &amp;quot;ja&amp;quot; beantwortet wurde, ansonsten zum rechten Kind. An jeder Kante stehen die jetzt noch in Frage kommenden Permutationen, und der jeweilige Kindknoten gibt uns die nächste Frage vor. Die Blätter enthalten das Indexarray, das der Permutation entspricht:&lt;br /&gt;
                                  (L[0] &amp;lt; L[1])&lt;br /&gt;
                          ja     /             \  nein&lt;br /&gt;
                        ABC     /               \    BAC&lt;br /&gt;
                        ACB    /                 \   CAB&lt;br /&gt;
                        BCA   /                   \  CBA&lt;br /&gt;
                             /                     \&lt;br /&gt;
                      (L[0] &amp;lt; L[2])           (L[0] &amp;lt; L[2])&lt;br /&gt;
                     /            \           /            \&lt;br /&gt;
               ja   /        nein  \         /  ja          \  nein&lt;br /&gt;
             ABC   /          BCA   |       |   BAC          \   CAB&lt;br /&gt;
             ACB  /                 |       |                 \  CBA&lt;br /&gt;
                 /               (2 0 1) (1 0 2)               \&lt;br /&gt;
          (L[1] &amp;lt; L[2])                                   (L[1] &amp;lt; L[2])&lt;br /&gt;
          /            \                                 /             \&lt;br /&gt;
     ja  /              \  nein                    ja   /               \  nein&lt;br /&gt;
   ABC  /                \   ACB                 CAB   /                 \   CBA&lt;br /&gt;
       /                  \                           /                   \&lt;br /&gt;
    (0 1 2)             (0 2 1)                   (1 2 0)               (2 1 0)&lt;br /&gt;
&lt;br /&gt;
Der Suchaufwand im schlechtesten Fall entspricht offensichtlich der Tiefe des Baumes. Bei Arrays mit drei Elementen ist die Tiefe gerade 3, wir benötigen maximal 3 Fragen bis zum Ziel. Für Arrays der Länge n gilt allgemein: Es gibt N = n! verschiedene Permutationen, der Baum muss also n! Blätter haben. Wir haben im Abschnitt [[Suchen#Balance_eines_Suchbaumes|Suchen]] gesehen, dass die Tiefe eines Baumes minimal wird, wenn der Baum ''perfekt balanciert'' ist, und dass der balancierte Baum mit den meisten Blättern der ''vollständige Baum ist''. Die Tiefe des vollständigen Baums mit n! Blättern gibt uns also eine untere Schranke für die minimale Anzahl der Vergleiche im schlechtesten Fall. &lt;br /&gt;
&lt;br /&gt;
Ein vollständiger Baum der Tiefe d hat 2&amp;lt;sup&amp;gt;d+1&amp;lt;/sup&amp;gt;-1 Knoten und 2&amp;lt;sup&amp;gt;d&amp;lt;/sup&amp;gt; Blätter:&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|-valign=&amp;quot;center&amp;quot; &lt;br /&gt;
|[[Image:vollbaum.png|left]]&lt;br /&gt;
| vollständiger Baum &amp;lt;br&amp;gt;2&amp;lt;sup&amp;gt;d+1&amp;lt;/sup&amp;gt;-1 Knoten&amp;lt;br&amp;gt;2&amp;lt;sup&amp;gt;d&amp;lt;/sup&amp;gt; Blätter&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Im Fall des Sortierens von n Elementen gilt, dass es N = n! mögliche Permutation gibt. Ein Baum mit n! Blättern hat mindestens die Tiefe log(n!). Im obigen Beispiel für n=3 gilt 3! = 1*2*3 = 6 und damit für die Tiefe d&lt;br /&gt;
:::&amp;lt;math&amp;gt;d = \lceil \log_2(6)\rceil \approx \lceil 2.6\rceil = 3&amp;lt;/math&amp;gt;&lt;br /&gt;
Im ungünstigsten Fall braucht man bei dem Frage-Baum drei Schritte. Weil aber &amp;lt;math&amp;gt;\log(6)\approx 2.6 &amp;lt; 3&amp;lt;/math&amp;gt; muss nicht jeder Pfad zu Ende durchlaufen werden, um die Lösung zu bekommen.&lt;br /&gt;
&lt;br /&gt;
Allgemein gilt&lt;br /&gt;
::&amp;lt;math&amp;gt;d \ge \log_2(n!)&amp;lt;/math&amp;gt;&lt;br /&gt;
Wir können die Tiefe am einfachsten durch die ''Stirlingsche Näherungsformel für die Fakultät'' abschätzen:&lt;br /&gt;
::&amp;lt;math&amp;gt;n! \approx \sqrt{2\pi n} \left(\frac{n}{e}\right)^n&amp;lt;/math&amp;gt;,&lt;br /&gt;
die asymptitisch für große n gilt. Einsetzen liefert&lt;br /&gt;
::&amp;lt;math&amp;gt;d \ge \log_2(n!) \in \Omega\left(\log_2\left(\sqrt{2\pi n} \left(\frac{n}{e}\right)^n\right)\right)&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Logarithmus eines Produkts ist gleich der Summe der Logarithmen der einzelnen Faktoren:&lt;br /&gt;
::&amp;lt;math&amp;gt;\Omega\left(\log_2\left(\sqrt{2\pi n} \left(\frac{n}{e}\right)^n\right)\right) = \Omega(\log_2(\sqrt{2\pi})) + \Omega(\log_2(\sqrt{n})) + \Omega(\log_2(n^n)) - \Omega(\log_2(e^n))&amp;lt;/math&amp;gt;&lt;br /&gt;
Wir vereinfachen die rechte Seite nach den Regeln der O-Notation: nur der am schnellsten wachsende Term bleibt übrig:&lt;br /&gt;
::&amp;lt;math&amp;gt;\Omega\left(\log_2\left(\sqrt{2\pi n} \left(\frac{n}{e}\right)^n\right)\right) = \Omega(\log_2(n^n))&amp;lt;/math&amp;gt;.&lt;br /&gt;
Den Exponenten in n&amp;lt;sup&amp;gt;n&amp;lt;/sup&amp;gt; kann man vor den Logarithmus ziehen, und die Basis des Logarithmus spielt keine Rolle. Wir erhalten somit:&lt;br /&gt;
::&amp;lt;math&amp;gt;d \in \Omega(n \log n)&amp;lt;/math&amp;gt;.&lt;br /&gt;
Somit braucht man im schlechtesten Fall mindestens &amp;lt;math&amp;gt;\Omega(n \log n)&amp;lt;/math&amp;gt; Vergleiche, und Merge Sort ist somit optimal und kann nicht weiter verbessert werden, solange man sich auf paarweise Vergleiche von Schlüsseln beschränkt.&lt;br /&gt;
&lt;br /&gt;
Eine exakte Herleitung dieser Tatsache, ohne Verwendung der Stirlingschen Formel, ist möglich durch &lt;br /&gt;
&lt;br /&gt;
===Abschätzung von Summen durch Integrale===&lt;br /&gt;
&lt;br /&gt;
Schreibt man die Fakultät als Produkt aus, und transformiert den Logarithmus des Produkts in eine Summe von Logarithmen, erhalten wir:&lt;br /&gt;
::&amp;lt;math&amp;gt;d \ge \log_2(n!) = \log_2(1\cdot 2\cdot ... \cdot n) = \log_2(1) + \log_2(2) + ... + \log_2(n) = \sum_{k=1}^n \log_2(k) = \frac{1}{\ln(2)}\sum_{k=1}^n \ln(k) = \frac{1}{\ln(2)}\sum_{k=2}^n \ln(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die letzte Identität gilt, weil &amp;lt;math&amp;gt;\ln(1) = 0&amp;lt;/math&amp;gt; in der Summe weggelassen werden kann. Eine untere Schranke für die Tiefe kann man explizit bestimmen durch die Methode der&lt;br /&gt;
 &lt;br /&gt;
Gegeben sei eine monoton wachsende Funktion f(x) (blaue Kurve). Das bestimmte Integral über die Funktion sei&lt;br /&gt;
:::&amp;lt;math&amp;gt;\int_{x_1}^{x_2} f(x)dx&amp;lt;/math&amp;gt;. &lt;br /&gt;
Wenn wir das Funktionsargument x abrunden (schwarze Kurve), entsteht ein Integral, das einen kleineren Wert als das ursprüngliche Integral hat. Runden wir auf (rote Kurve), entsteht ein Integral mit einem größeren Wert:&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|-valign=&amp;quot;center&amp;quot; &lt;br /&gt;
|[[Image:integralGraph.png|400px|left]]&lt;br /&gt;
| &amp;lt;math&amp;gt;\int_{x_1}^{x_2} f(\lfloor x \rfloor)dx \le \int_{x_1}^{x_2} f(x)dx \le \int_{x_1}^{x_2} f(\lceil x \rceil)dx&amp;lt;/math&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
In unserem Zusammenhang sind x&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; und x&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; positive ganze Zahlen. Deshalb gilt&lt;br /&gt;
:::&amp;lt;math&amp;gt;f(\lfloor x \rfloor)_{x_1}^{x_1+1}= f(x_1),&amp;lt;/math&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;f(\lfloor x \rfloor)_{x_1+1}^{x_1+2}= f(x_1+1)&amp;lt;/math&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;...&amp;lt;/math&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;f(\lfloor x \rfloor)_{x_2-1}^{x_2}= f(x_2-1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Wir können die obigen Integrale daher folgendermaßen vereinfachen:&lt;br /&gt;
:::&amp;lt;math&amp;gt;\begin{array}{lcl}&lt;br /&gt;
\int_{x_1}^{x_2} f(\lfloor x \rfloor) dx &amp;amp;=&amp;amp; \int_{x_1}^{x_1 + 1} f(\lfloor x \rfloor) dx + ...+ \int_{x_2-1}^{x_2} f(\lfloor x \rfloor) dx \\&lt;br /&gt;
&amp;amp; = &amp;amp; \int_{x_1}^{x_1 + 1} f(x_1) dx + ...+ \int_{x_2-1}^{x_2} f(x_2-1) dx \\&lt;br /&gt;
&amp;amp; = &amp;amp; f(x_1) \int_{x_1}^{x_1 + 1} dx + ...+ f(x_2-1) \int_{x_2-1}^{x_2} dx \\&lt;br /&gt;
&amp;amp; = &amp;amp; f(x_1)  + ...+ f(x_2-1) \\&lt;br /&gt;
&amp;amp; = &amp;amp; \sum_{k=x_1}^{x_2-1} f(k)&lt;br /&gt;
\end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
für die Fläche unter den schwarzen Rechtecken sowie&lt;br /&gt;
:::&amp;lt;math&amp;gt;\begin{array}{lcl}&lt;br /&gt;
\int_{x_1}^{x_2} f(\lceil x \rceil) dx &amp;amp;=&amp;amp; \int_{x_1}^{x_1 + 1} f(\lceil x \rceil) dx + ...+ \int_{x_2-1}^{x_2} f(\lceil x \rceil) dx \\&lt;br /&gt;
&amp;amp; = &amp;amp; \int_{x_1}^{x_1 + 1} f(x_1+1) dx + ...+ \int_{x_2-1}^{x_2} f(x_2) dx \\&lt;br /&gt;
&amp;amp; = &amp;amp; f(x_1+1) \int_{x_1}^{x_1 + 1} dx + ...+ f(x_2) \int_{x_2-1}^{x_2} dx \\&lt;br /&gt;
&amp;amp; = &amp;amp; f(x_1+1)  + ...+ f(x_2) \\&lt;br /&gt;
&amp;amp; = &amp;amp; \sum_{k=x_1+1}^{x_2} f(k)&lt;br /&gt;
\end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
für die Fläche unter den roten Rechtecken. Zusammenfassend gilt also&lt;br /&gt;
&amp;lt;math&amp;gt; \sum_{k=x_1}^{x_2-1} f(k) \le \int_{x_1}^{x_2} f(x)dx&amp;lt;/math&amp;gt; und &lt;br /&gt;
&amp;lt;math&amp;gt; \sum_{k=x_1+1}^{x_2} f(k) \ge \int_{x_1}^{x_2} f(x)dx&amp;lt;/math&amp;gt;&lt;br /&gt;
Für unser Problem setzen wir f(k) = ln(k), x&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;+1 = 2, und x&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; = n. Also können wir abschätzen&lt;br /&gt;
:::&amp;lt;math&amp;gt;\sum_{k=x_1+1}^{x_2} f(k) = \frac{1}{\ln(2)}\sum_{k=2}^{n} \ln(k) \ge \frac{1}{\ln(2)}\int_1^n \ln(x) dx&amp;lt;/math&amp;gt;&lt;br /&gt;
Das Integral ist leicht zu lösen, und wir erhalten&lt;br /&gt;
:::&amp;lt;math&amp;gt;\frac{1}{\ln(2)}\sum_{k=2}^{n} \ln(k) \ge \frac{1}{\ln(2)}\left[x\ln(x)-x\right]_{x=1}^{n} = \frac{1}{\ln(2)}(n\ln(n)-n+1)=n\log_2(n) - \frac{n-1}{\ln(2)} \in \Omega(n \log(n))&amp;lt;/math&amp;gt;&lt;br /&gt;
Folglich gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;d\ge\log_2(n!) = \frac{1}{\ln(2)}\sum_{k=2}^{n} \ln(k) \in \Omega(n \log(n))&amp;lt;/math&amp;gt;&lt;br /&gt;
Mit anderen Worten: '''Kein Sortieralgorithmus auf Basis paarweise Vergleiche ist asymptotisch schneller als Mergesort, denn die Anzahl der Vergleiche (= Tiefe des Entscheidungsbaumes) ist &amp;lt;math&amp;gt;\Omega(n \log(n))&amp;lt;/math&amp;gt;'''. Falls man einen schnelleren Sortieralgorithmus benötigt, muss man ein anderes algorithmisches Prinzip verwenden.&lt;br /&gt;
&lt;br /&gt;
==Effizientere Sortieralgorithmen==&lt;br /&gt;
&lt;br /&gt;
Wir haben gezeigt, dass mit paarweisen Größenvergleichen allein kein Sortieralgorithmus schneller als &amp;lt;math&amp;gt;\Omega(n \log n)&amp;lt;/math&amp;gt; sein kann. Um einen besseren Algorithmus zu finden, dürfen wir nicht nur die relative Größe der Schlüssel (also die Ordnung) berücksichtigen, sondern müssen die Werte selbst verwenden. Der entscheidende Trick dabei ist das &lt;br /&gt;
&lt;br /&gt;
=== Bucket-Prinzip ===&lt;br /&gt;
&lt;br /&gt;
Man definiert eine Funktion &amp;lt;tt&amp;gt;quantize(key, M)&amp;lt;/tt&amp;gt;, die jeden Schlüssel auf eine ganze Zahl im Bereich &amp;lt;tt&amp;gt;[0,...,M-1]&amp;lt;/tt&amp;gt; abbildet. Mit Hilfe dieser Zahlen werden die Schlüssel auf M ''Buckets'' aufgeteilt, und das Sortieren kann dann in jedem Bucket getrennt erfolgen. Am Ende setzt man aus den Inhalten der Buckets das gesamte Array (sortiert) wieder zusammen. Wir zeigen unten, dass man damit lineare Zeit erreicht.&lt;br /&gt;
&lt;br /&gt;
Um für das Sortieren brauchbar zu sein, muss die Funktion &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt; ''Ordnung erhaltend'' definiert sein:&lt;br /&gt;
   wenn key1 &amp;lt;= key2, gilt auch quantize(key1, M) &amp;lt;= quantize(key2, M)&lt;br /&gt;
Eine solche Abbildung nennt man ''Quantisierung''. Allgemein bekannt ist der Prozess der Quantisierung z.B. bei Digitalkameras: Hier wird in jedem Pixel eine reell-wertige Lichtintensität gemessen, die im resultierenden Bild mit nur 256 Intensitätsabstufungen pro Farbe abgespeichert wird (bzw. mit bis zu 65536 Abstufungen bei Kameras mit sogenanntem &amp;quot;high dynamic range&amp;quot;). Bleibt die Ordnung bei der Abbildung von Schlüsseln auf natürliche Zahlen nicht erhalten, spricht man von ''Hashing''. Hashing wird zur Implementation von [[Hashing und Hashtabellen|Hashtabellen]] nutzbringend eingesetzt.&lt;br /&gt;
&lt;br /&gt;
Der einfachste Fall liegt vor, wenn die Schlüssel bereits genze Zahlen im Bereich &amp;lt;tt&amp;gt;[0,...,M-1]&amp;lt;/tt&amp;gt; sind. Dann ist die &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt; einfach die Identität. Wir nehmen an, dass die Daten als Schlüssel/Wert-Paare in einem Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; gespeichert sind, und wir können das Sortieren wie folgt implementieren:&lt;br /&gt;
   def integerSort(a, M):&lt;br /&gt;
       # erzeuge M leere Buckets&lt;br /&gt;
       buckets = [[] for k in range(M)]  &lt;br /&gt;
       &lt;br /&gt;
       # verteile die Daten auf die Buckets&lt;br /&gt;
       for k in range(len(a)):           &lt;br /&gt;
           buckets[a[k].key].append(a[k]) # a[k].key sind Integer-Schlüssel in [0,...,M-1]&lt;br /&gt;
      &lt;br /&gt;
       # setze das Array a aus den Buckets sortiert wieder zusammen&lt;br /&gt;
       start = 0                          # Anfangsindex des ersten Buckets&lt;br /&gt;
       for k in range(M):&lt;br /&gt;
           end = start + len(buckets[k])  # Endindex des aktuellen Buckets&lt;br /&gt;
           a[start:end] = buckets[k]      # Daten an der richtigen Position in a einfügen&lt;br /&gt;
           start = end                    # Anfangsindex für das nächste Bucket aktualisieren&lt;br /&gt;
Das Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; ist am Ende sortiert, weil wir den Inhalt der Buckets nach aufsteigendem Bucket-Index, und damit automatisch nach aufsteigenden Schlüsseln, in &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; einfügen. Das Sortieren ist außerdem ''stabil'', da Daten mit gleichem Schlüssel immer hinten an das jeweilige Bucket angefügt werden. &lt;br /&gt;
&lt;br /&gt;
Die Komplexität des Algorithmus ist &amp;lt;math&amp;gt;O(N)&amp;lt;/math&amp;gt; mit &amp;lt;tt&amp;gt;N = len(a)&amp;lt;/tt&amp;gt;, solange &lt;br /&gt;
::&amp;lt;math&amp;gt;M \in O(N)&amp;lt;/math&amp;gt; &lt;br /&gt;
gilt: Das Erzeugen der Buckets erfordert &amp;lt;math&amp;gt;O(M)&amp;lt;/math&amp;gt; Schritte und das Verteilen der Daten auf die Buckets &amp;lt;math&amp;gt;O(N)&amp;lt;/math&amp;gt; Schritte (weil &amp;lt;tt&amp;gt;bucket[k].append()&amp;lt;/tt&amp;gt; amortisiert konstante Komplexität hat). Das Zusammensetzen des sortierten Arrays wird vom Kopieren der Daten dominiert, welches die Komplexität &lt;br /&gt;
::&amp;lt;math&amp;gt;\sum_{k=0}^{M-1} O(N_k)= O\left(\sum_{k=0}^{M-1} N_k\right)&amp;lt;/math&amp;gt; &lt;br /&gt;
besitzt, wobei &amp;lt;tt&amp;gt;N&amp;lt;sub&amp;gt;k&amp;lt;/sub&amp;gt;=len(buckets[k])&amp;lt;/tt&amp;gt; die Größe des k-ten Buckets ist. Die Gesamtanzahl der Daten in allen Buckets zusammen ist aber gerade wieder die Größe von &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt;, also &lt;br /&gt;
::&amp;lt;math&amp;gt;O\left(\sum_{k=0}^{M-1} N_k\right) = O(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
Nach der Sequenzregel ist die Gesamtkomplexität somit &amp;lt;math&amp;gt;O(M + N)=O(N)&amp;lt;/math&amp;gt;, falls &amp;lt;math&amp;gt;M \in O(N)&amp;lt;/math&amp;gt; gilt.&lt;br /&gt;
&lt;br /&gt;
===Bucket Sort===&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus wird nur wenig komplizierter, wenn die Schlüssel beliebig sein können, aber eine ordnung-erhaltende &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt;-Funktion vorhanden ist. Allerdings geht bei der Quantisierung, also der Abbildung von Schlüsseln auf Bucket-Indizes, ein Teil der Schlüsselinformation verloren. Die Elemente im selben Bucket haben im Allgemeinen nicht exakt den gleichen Schlüssel, so dass jeder Bucket noch explizit sortiert werden muss. Diese Tatsache führt zu einer zusätzlichen Einschränkung: einerseits muss für die Anzahl der Buckets nach wie vor &lt;br /&gt;
::&amp;lt;math&amp;gt;M \in O(N)&amp;lt;/math&amp;gt; &lt;br /&gt;
gelten, aber andererseits sollte jeder Bucket nur wenige Daten enthalten, so dass das Sortieren innerhalb der Buckets effizient ist. Wir fordern deshalb, dass &amp;lt;math&amp;gt;N_k \in O(1)&amp;lt;/math&amp;gt; sein soll. Unter der Voraussetzung, dass &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt; die Daten gleichmäßig auf alle Buckets verteilt (dazu unten mehr), gilt für die Bucketgrößen &lt;br /&gt;
::&amp;lt;math&amp;gt;N_k \in O\left(\frac{N}{M}\right)&amp;lt;/math&amp;gt;&lt;br /&gt;
denn wir verteilen N Elemente auf M Buckets. Beide Bedingungen sind erfüllt, wenn&lt;br /&gt;
::&amp;lt;math&amp;gt;M = \frac{N}{c}&amp;lt;/math&amp;gt;&lt;br /&gt;
gilt, wobei c eine Konstante unabhängig von N ist. In der Praxis erzielt man die besten Resultate mit &amp;lt;math&amp;gt;c \approx 10&amp;lt;/math&amp;gt; (die beste Wahl hängt im konkreten Fall von der Schlüsselverteilung und von der &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt;-Funktion ab). Wir übergeben die Konstante c und die &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt;-Funktion als Parameter an den Algorithmus:&lt;br /&gt;
   def bucketSort(a, quant, c):&lt;br /&gt;
       N = len(a)&lt;br /&gt;
       M = int(N // c) + 1  # Anzahl der Buckets festlegen (+1, damit es mindestens ein Bucket gibt)&lt;br /&gt;
       &lt;br /&gt;
       # M leere Buckets erzeugen&lt;br /&gt;
       buckets = [[] for k in range(M)]&lt;br /&gt;
       &lt;br /&gt;
       # Daten auf die Buckets verteilen&lt;br /&gt;
       for k in range(len(a)):&lt;br /&gt;
           index = quant(a[k].key, M)    # Bucket-Index berechnen&lt;br /&gt;
           buckets[index].append(a[k])   # a[k] im passenden Bucket einfügen&lt;br /&gt;
       &lt;br /&gt;
       # Daten sortiert wieder in a einfügen&lt;br /&gt;
       start = 0                          # Anfangsindex des ersten Buckets &lt;br /&gt;
       for k in range(M):&lt;br /&gt;
           insertionSort(buckets[k])      # Daten innerhalb des aktuellen Buckets sortieren&lt;br /&gt;
           end = start + len(buckets[k])  # Endindex des aktuellen Buckets&lt;br /&gt;
           a[start:end] = buckets[k]      # Daten an der richtigen Position in a einfügen&lt;br /&gt;
           start += len(buckets[k])       # Anfangsindex für nächsten Bucket aktualisieren&lt;br /&gt;
Wir verwenden zum Sortieren der Daten in jedem Bucket &amp;lt;tt&amp;gt;insertionSort()&amp;lt;/tt&amp;gt;. Dies ist aus zwei Gründen eine gute Wahl: Erstens haben wir die Buckets so konstruiert, dass jeder Bucket nur wenige Elemente enthält (&amp;lt;math&amp;gt;N_k \in O(1)&amp;lt;/math&amp;gt;), und für kleine Arrays ist Insertion Sort der schnellste Algorithmus. Zweitens ist Insertion Sort ein ''stabiler'' Sortieralgorithmus, und demzufolge ist auch das gesamte &amp;lt;tt&amp;gt;bucketSort()&amp;lt;/tt&amp;gt; stabil.&lt;br /&gt;
&lt;br /&gt;
Unter der Voraussetzung, dass &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt; konstante Zeit für die Quantisierung eines Schlüssels benötigt, unterscheidet sich die Komplexitätsanalyse von &amp;lt;tt&amp;gt;bucketSort()&amp;lt;/tt&amp;gt; nur in einem Punkt von &amp;lt;tt&amp;gt;integerSort()&amp;lt;/tt&amp;gt;, nämlich durch das zusätzliche Sortieren in jedem Bucket. Bei Verwendung von Insertion Sort hat dies quadratische Komplexität in &amp;lt;math&amp;gt;N_k&amp;lt;/math&amp;gt;, aber wenn &amp;lt;math&amp;gt;N_k \in O(1)&amp;lt;/math&amp;gt; erfüllt ist, gilt &amp;lt;math&amp;gt;O(N_k^2) = O(1^2) = O(1)&amp;lt;/math&amp;gt;. Das Sortieren hat also konstante Komplexität pro Bucket, und somit ist die Gesamtkomplexität von &amp;lt;tt&amp;gt;bucketSort()&amp;lt;/tt&amp;gt; linear in N, wie am Anfang des Abschnitts gewünscht.&lt;br /&gt;
&lt;br /&gt;
Allerdings steht und fällt diese Analyse damit, dass die &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt;-Funktion die Daten tatsächlich gleichmäßig auf die Buckets verteilt. Andernfalls könnten im schlechtesten Fall alle Daten in einem einzigen Bucket landen, und dann hätte &amp;lt;tt&amp;gt;bucketSort()&amp;lt;/tt&amp;gt; quadratische Komplexität. Die &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt;-Funktion muss deshalb je nach der Wahrscheinlichkeitsverteilung der Schlüssel immer wieder anders festgelegt werden. Sehr einfach ist dies, wenn die Schlüssel in einem gewissen Intervall &amp;lt;tt&amp;gt;[U,...,V)&amp;lt;/tt&amp;gt; gleichverteilt sind: dann kann man einfach das Intervall &amp;lt;tt&amp;gt;[U,...,V)&amp;lt;/tt&amp;gt; durch eine lineare Gleichung auf das Intervall &amp;lt;tt&amp;gt;[0,...M-1]&amp;lt;/tt&amp;gt; abbilden und dann abrunden. Für &amp;lt;tt&amp;gt;U = 0&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;V = 1&amp;lt;/tt&amp;gt; erhalten wir beilspielsweise:&lt;br /&gt;
    '''Beispiel:'''&lt;br /&gt;
    # keys sind reelle Zahlen in [0, 1)&lt;br /&gt;
    &lt;br /&gt;
    def quantize(key, M):&lt;br /&gt;
        return int(key * M)&lt;br /&gt;
Die Definition einer geeigneten &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt;-Funktion für eine andere Schlüsselverteilung ist Bestandteil einer Übungsaufgabe. In der Praxis findet man allerdings, dass die Verteilung der Daten auf die Buckets nicht übermäßig kritisch ist -- &amp;lt;tt&amp;gt;bucketSort()&amp;lt;/tt&amp;gt; bleibt auch dann ein sehr schneller Algorithmus, wenn die Verteilung nicht ganz gleichmäßig gelingt. Die obige Implementation ist somit ein guter Default für viele Anwendungen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Prioritätswarteschlangen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Sortieren_in_linearer_Zeit&amp;diff=5708</id>
		<title>Sortieren in linearer Zeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Sortieren_in_linearer_Zeit&amp;diff=5708"/>
				<updated>2020-07-02T16:14:46Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Bucket-Prinzip */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Wir kehren an dieser Stelle nochmals zum Sortierproblem zurück und stellen uns die Frage, ob wir noch schnellere Algorithmen finden können, die eventuell sogar in O(N) statt in O(N*log(N)) zum Ziel kommen. Mit Hilfe der gerade eingeführten Suchbäumen werden wir zeigen, dass dies nicht möglich ist, solange für die Sortierschlüssel nur eine paarweise Vergleichsfunktion definiert ist. Besitzen wir jedoch zusätzliche Informationen über die Schlüssel, die uns die Anwendung des ''Bucket-Prinzips'' erlauben, ist das Sortieren in linearer Zeit möglich.&lt;br /&gt;
&lt;br /&gt;
== Sortieren und Permutationen ==&lt;br /&gt;
&lt;br /&gt;
Bevor wir die Grenzen des Sortierens mit Paarvergleichen analysieren, wollen wir noch etwas näher beleuchten, was beim Sortieren eigentlich geschieht. Dazu gehen wir noch einen Schritt zurück und schauen uns an, was beim Mischen eines zunächst sortierten Arrays passiert. Wir betrachten das Array mit den drei Element A, B, C sowie ein korrespondierendes ''Indexarray'', das angibt, an welcher Position im sortierten Array die drei Elemente gehören. Solange das Hauptarray noch sortiert ist, enthält das Indexarray einfach die aufsteigende Folge 0, 1, 2:&lt;br /&gt;
   L = A B C     # Hauptarray sortiert (0. Permutation)&lt;br /&gt;
   I = 0 1 2     # Indexarray&lt;br /&gt;
Es gibt jetzt 5 weitere Anordnungsmöglichkeiten (imsgesamt also 6 = 3! Permutationen) für die drei Elementa A, B und C. Immer, wenn wir diese drei Elemente umordnen, ordnen wir das Indexarray so, dass das Element &amp;lt;tt&amp;gt;I[k]&amp;lt;/tt&amp;gt; in jedem Indexarray uns angibt, wo sich der Buchstabe jetzt befindet, der ursprünglich (also in der sortierten Anordnung) an Position &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; stand:&lt;br /&gt;
   L = A C B     # 1. Permutation&lt;br /&gt;
   I = 0 2 1 &lt;br /&gt;
&lt;br /&gt;
   L = B A C     # 2. Permutation&lt;br /&gt;
   I = 1 0 2&lt;br /&gt;
&lt;br /&gt;
   L = B C A     # 3. Permutation&lt;br /&gt;
   I = 2 0 1&lt;br /&gt;
&lt;br /&gt;
   L = C A B     # 4. Permutation&lt;br /&gt;
   I = 1 2 0&lt;br /&gt;
&lt;br /&gt;
   L = C B A     # 5. Permutation&lt;br /&gt;
   I = 2 1 0&lt;br /&gt;
In der 5. Permutation sagt beispielsweise &amp;lt;tt&amp;gt;I[0] = 2&amp;lt;/tt&amp;gt;, dass der ursprünglich 0. Buchstabe (das A) jetzt an Position 2 ist. Daraus folgt, dass wir ein permutiertes Array in linearer Zeit sortieren können, wenn uns das Indexarray bekannt ist. Wir müssen einfach einmal durch das Indexarray gehen und jedes Element von Position &amp;lt;tt&amp;gt;I[k]&amp;lt;/tt&amp;gt; wieder an Position &amp;lt;tt&amp;gt;k&amp;lt;/tt&amp;gt; verschieben:&lt;br /&gt;
   def sortByIndexArray(L, I):&lt;br /&gt;
       R = [None]*len(L)        # zunächst leeres Ergebnisarray&lt;br /&gt;
       for k in xrange(len(L)):&lt;br /&gt;
           R[k] = L[I[k]]       # Elemente sortiert in R einfügen&lt;br /&gt;
       return R&lt;br /&gt;
Da man nur einmal über den Bereich k = 0 ... len(L)-1 gehen muss, ist der Aufwand dieser Funktion O(len(L)), also linear. Dieser Algorithmus ist z.B. nützlich, wenn man mehrere Arrays in der gleichen Weise sortieren muss, z.B. die Liste der Studentennamen und die Liste der dazugehörigen Übungspunkte. Man kann dann einfach einmal die Permutation des Indexarrays bestimmen, und dann alle Listen entsprechend sortieren. Wie man das Indexarray mit dem Standard-Sortieralgorithmus &amp;lt;tt&amp;gt;array.sort()&amp;lt;/tt&amp;gt; bestimmen kann, ist Aufgabe im Übungsblatt 9.&lt;br /&gt;
&lt;br /&gt;
==Sortieren als Suchproblem==&lt;br /&gt;
&lt;br /&gt;
Wir haben gesehen, dass wir in linearer Zeit sortieren können, wenn uns die Permutation bzw. das zugehörige Indexarray bekannt ist. Die nächste Frage lautet deshalb: Wie viele Schritte brauchen wir, um die Permutation zu finden? Dabei ist es nur erlaubt, Schlüssel paarweise zu vergleichen, und man erhält jeweils eine ja/nein Antwort. Ein solches Vorgehen kann als Entscheidungsbaum dargestellt werden. Jeder Knoten ist eine Frage, und wir gehen zum linken Kind weiter, wenn die Frage mit &amp;quot;ja&amp;quot; beantwortet wurde, ansonsten zum rechten Kind. An jeder Kante stehen die jetzt noch in Frage kommenden Permutationen, und der jeweilige Kindknoten gibt uns die nächste Frage vor. Die Blätter enthalten das Indexarray, das der Permutation entspricht:&lt;br /&gt;
                                  (L[0] &amp;lt; L[1])&lt;br /&gt;
                          ja     /             \  nein&lt;br /&gt;
                        ABC     /               \    BAC&lt;br /&gt;
                        ACB    /                 \   CAB&lt;br /&gt;
                        BCA   /                   \  CBA&lt;br /&gt;
                             /                     \&lt;br /&gt;
                      (L[0] &amp;lt; L[2])           (L[0] &amp;lt; L[2])&lt;br /&gt;
                     /            \           /            \&lt;br /&gt;
               ja   /        nein  \         /  ja          \  nein&lt;br /&gt;
             ABC   /          BCA   |       |   BAC          \   CAB&lt;br /&gt;
             ACB  /                 |       |                 \  CBA&lt;br /&gt;
                 /               (2 0 1) (1 0 2)               \&lt;br /&gt;
          (L[1] &amp;lt; L[2])                                   (L[1] &amp;lt; L[2])&lt;br /&gt;
          /            \                                 /             \&lt;br /&gt;
     ja  /              \  nein                    ja   /               \  nein&lt;br /&gt;
   ABC  /                \   ACB                 CAB   /                 \   CBA&lt;br /&gt;
       /                  \                           /                   \&lt;br /&gt;
    (0 1 2)             (0 2 1)                   (1 2 0)               (2 1 0)&lt;br /&gt;
&lt;br /&gt;
Der Suchaufwand im schlechtesten Fall entspricht offensichtlich der Tiefe des Baumes. Bei Arrays mit drei Elementen ist die Tiefe gerade 3, wir benötigen maximal 3 Fragen bis zum Ziel. Für Arrays der Länge n gilt allgemein: Es gibt N = n! verschiedene Permutationen, der Baum muss also n! Blätter haben. Wir haben im Abschnitt [[Suchen#Balance_eines_Suchbaumes|Suchen]] gesehen, dass die Tiefe eines Baumes minimal wird, wenn der Baum ''perfekt balanciert'' ist, und dass der balancierte Baum mit den meisten Blättern der ''vollständige Baum ist''. Die Tiefe des vollständigen Baums mit n! Blättern gibt uns also eine untere Schranke für die minimale Anzahl der Vergleiche im schlechtesten Fall. &lt;br /&gt;
&lt;br /&gt;
Ein vollständiger Baum der Tiefe d hat 2&amp;lt;sup&amp;gt;d+1&amp;lt;/sup&amp;gt;-1 Knoten und 2&amp;lt;sup&amp;gt;d&amp;lt;/sup&amp;gt; Blätter:&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|-valign=&amp;quot;center&amp;quot; &lt;br /&gt;
|[[Image:vollbaum.png|left]]&lt;br /&gt;
| vollständiger Baum &amp;lt;br&amp;gt;2&amp;lt;sup&amp;gt;d+1&amp;lt;/sup&amp;gt;-1 Knoten&amp;lt;br&amp;gt;2&amp;lt;sup&amp;gt;d&amp;lt;/sup&amp;gt; Blätter&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Im Fall des Sortierens von n Elementen gilt, dass es N = n! mögliche Permutation gibt. Ein Baum mit n! Blättern hat mindestens die Tiefe log(n!). Im obigen Beispiel für n=3 gilt 3! = 1*2*3 = 6 und damit für die Tiefe d&lt;br /&gt;
:::&amp;lt;math&amp;gt;d = \lceil \log_2(6)\rceil \approx \lceil 2.6\rceil = 3&amp;lt;/math&amp;gt;&lt;br /&gt;
Im ungünstigsten Fall braucht man bei dem Frage-Baum drei Schritte. Weil aber &amp;lt;math&amp;gt;\log(6)\approx 2.6 &amp;lt; 3&amp;lt;/math&amp;gt; muss nicht jeder Pfad zu Ende durchlaufen werden, um die Lösung zu bekommen.&lt;br /&gt;
&lt;br /&gt;
Allgemein gilt&lt;br /&gt;
::&amp;lt;math&amp;gt;d \ge \log_2(n!)&amp;lt;/math&amp;gt;&lt;br /&gt;
Wir können die Tiefe am einfachsten durch die ''Stirlingsche Näherungsformel für die Fakultät'' abschätzen:&lt;br /&gt;
::&amp;lt;math&amp;gt;n! \approx \sqrt{2\pi n} \left(\frac{n}{e}\right)^n&amp;lt;/math&amp;gt;,&lt;br /&gt;
die asymptitisch für große n gilt. Einsetzen liefert&lt;br /&gt;
::&amp;lt;math&amp;gt;d \ge \log_2(n!) \in \Omega\left(\log_2\left(\sqrt{2\pi n} \left(\frac{n}{e}\right)^n\right)\right)&amp;lt;/math&amp;gt;&lt;br /&gt;
Der Logarithmus eines Produkts ist gleich der Summe der Logarithmen der einzelnen Faktoren:&lt;br /&gt;
::&amp;lt;math&amp;gt;\Omega\left(\log_2\left(\sqrt{2\pi n} \left(\frac{n}{e}\right)^n\right)\right) = \Omega(\log_2(\sqrt{2\pi})) + \Omega(\log_2(\sqrt{n})) + \Omega(\log_2(n^n)) - \Omega(\log_2(e^n))&amp;lt;/math&amp;gt;&lt;br /&gt;
Wir vereinfachen die rechte Seite nach den Regeln der O-Notation: nur der am schnellsten wachsende Term bleibt übrig:&lt;br /&gt;
::&amp;lt;math&amp;gt;\Omega\left(\log_2\left(\sqrt{2\pi n} \left(\frac{n}{e}\right)^n\right)\right) = \Omega(\log_2(n^n))&amp;lt;/math&amp;gt;.&lt;br /&gt;
Den Exponenten in n&amp;lt;sup&amp;gt;n&amp;lt;/sup&amp;gt; kann man vor den Logarithmus ziehen, und die Basis des Logarithmus spielt keine Rolle. Wir erhalten somit:&lt;br /&gt;
::&amp;lt;math&amp;gt;d \in \Omega(n \log n)&amp;lt;/math&amp;gt;.&lt;br /&gt;
Somit braucht man im schlechtesten Fall mindestens &amp;lt;math&amp;gt;\Omega(n \log n)&amp;lt;/math&amp;gt; Vergleiche, und Merge Sort ist somit optimal und kann nicht weiter verbessert werden, solange man sich auf paarweise Vergleiche von Schlüsseln beschränkt.&lt;br /&gt;
&lt;br /&gt;
Eine exakte Herleitung dieser Tatsache, ohne Verwendung der Stirlingschen Formel, ist möglich durch &lt;br /&gt;
&lt;br /&gt;
===Abschätzung von Summen durch Integrale===&lt;br /&gt;
&lt;br /&gt;
Schreibt man die Fakultät als Produkt aus, und transformiert den Logarithmus des Produkts in eine Summe von Logarithmen, erhalten wir:&lt;br /&gt;
::&amp;lt;math&amp;gt;d \ge \log_2(n!) = \log_2(1\cdot 2\cdot ... \cdot n) = \log_2(1) + \log_2(2) + ... + \log_2(n) = \sum_{k=1}^n \log_2(k) = \frac{1}{\ln(2)}\sum_{k=1}^n \ln(k) = \frac{1}{\ln(2)}\sum_{k=2}^n \ln(k)&amp;lt;/math&amp;gt;&lt;br /&gt;
Die letzte Identität gilt, weil &amp;lt;math&amp;gt;\ln(1) = 0&amp;lt;/math&amp;gt; in der Summe weggelassen werden kann. Eine untere Schranke für die Tiefe kann man explizit bestimmen durch die Methode der&lt;br /&gt;
 &lt;br /&gt;
Gegeben sei eine monoton wachsende Funktion f(x) (blaue Kurve). Das bestimmte Integral über die Funktion sei&lt;br /&gt;
:::&amp;lt;math&amp;gt;\int_{x_1}^{x_2} f(x)dx&amp;lt;/math&amp;gt;. &lt;br /&gt;
Wenn wir das Funktionsargument x abrunden (schwarze Kurve), entsteht ein Integral, das einen kleineren Wert als das ursprüngliche Integral hat. Runden wir auf (rote Kurve), entsteht ein Integral mit einem größeren Wert:&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|-valign=&amp;quot;center&amp;quot; &lt;br /&gt;
|[[Image:integralGraph.png|400px|left]]&lt;br /&gt;
| &amp;lt;math&amp;gt;\int_{x_1}^{x_2} f(\lfloor x \rfloor)dx \le \int_{x_1}^{x_2} f(x)dx \le \int_{x_1}^{x_2} f(\lceil x \rceil)dx&amp;lt;/math&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
In unserem Zusammenhang sind x&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; und x&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; positive ganze Zahlen. Deshalb gilt&lt;br /&gt;
:::&amp;lt;math&amp;gt;f(\lfloor x \rfloor)_{x_1}^{x_1+1}= f(x_1),&amp;lt;/math&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;f(\lfloor x \rfloor)_{x_1+1}^{x_1+2}= f(x_1+1)&amp;lt;/math&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;...&amp;lt;/math&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;f(\lfloor x \rfloor)_{x_2-1}^{x_2}= f(x_2-1)&amp;lt;/math&amp;gt;&lt;br /&gt;
Wir können die obigen Integrale daher folgendermaßen vereinfachen:&lt;br /&gt;
:::&amp;lt;math&amp;gt;\begin{array}{lcl}&lt;br /&gt;
\int_{x_1}^{x_2} f(\lfloor x \rfloor) dx &amp;amp;=&amp;amp; \int_{x_1}^{x_1 + 1} f(\lfloor x \rfloor) dx + ...+ \int_{x_2-1}^{x_2} f(\lfloor x \rfloor) dx \\&lt;br /&gt;
&amp;amp; = &amp;amp; \int_{x_1}^{x_1 + 1} f(x_1) dx + ...+ \int_{x_2-1}^{x_2} f(x_2-1) dx \\&lt;br /&gt;
&amp;amp; = &amp;amp; f(x_1) \int_{x_1}^{x_1 + 1} dx + ...+ f(x_2-1) \int_{x_2-1}^{x_2} dx \\&lt;br /&gt;
&amp;amp; = &amp;amp; f(x_1)  + ...+ f(x_2-1) \\&lt;br /&gt;
&amp;amp; = &amp;amp; \sum_{k=x_1}^{x_2-1} f(k)&lt;br /&gt;
\end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
für die Fläche unter den schwarzen Rechtecken sowie&lt;br /&gt;
:::&amp;lt;math&amp;gt;\begin{array}{lcl}&lt;br /&gt;
\int_{x_1}^{x_2} f(\lceil x \rceil) dx &amp;amp;=&amp;amp; \int_{x_1}^{x_1 + 1} f(\lceil x \rceil) dx + ...+ \int_{x_2-1}^{x_2} f(\lceil x \rceil) dx \\&lt;br /&gt;
&amp;amp; = &amp;amp; \int_{x_1}^{x_1 + 1} f(x_1+1) dx + ...+ \int_{x_2-1}^{x_2} f(x_2) dx \\&lt;br /&gt;
&amp;amp; = &amp;amp; f(x_1+1) \int_{x_1}^{x_1 + 1} dx + ...+ f(x_2) \int_{x_2-1}^{x_2} dx \\&lt;br /&gt;
&amp;amp; = &amp;amp; f(x_1+1)  + ...+ f(x_2) \\&lt;br /&gt;
&amp;amp; = &amp;amp; \sum_{k=x_1+1}^{x_2} f(k)&lt;br /&gt;
\end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
für die Fläche unter den roten Rechtecken. Zusammenfassend gilt also&lt;br /&gt;
&amp;lt;math&amp;gt; \sum_{k=x_1}^{x_2-1} f(k) \le \int_{x_1}^{x_2} f(x)dx&amp;lt;/math&amp;gt; und &lt;br /&gt;
&amp;lt;math&amp;gt; \sum_{k=x_1+1}^{x_2} f(k) \ge \int_{x_1}^{x_2} f(x)dx&amp;lt;/math&amp;gt;&lt;br /&gt;
Für unser Problem setzen wir f(k) = ln(k), x&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt;+1 = 2, und x&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt; = n. Also können wir abschätzen&lt;br /&gt;
:::&amp;lt;math&amp;gt;\sum_{k=x_1+1}^{x_2} f(k) = \frac{1}{\ln(2)}\sum_{k=2}^{n} \ln(k) \ge \frac{1}{\ln(2)}\int_1^n \ln(x) dx&amp;lt;/math&amp;gt;&lt;br /&gt;
Das Integral ist leicht zu lösen, und wir erhalten&lt;br /&gt;
:::&amp;lt;math&amp;gt;\frac{1}{\ln(2)}\sum_{k=2}^{n} \ln(k) \ge \frac{1}{\ln(2)}\left[x\ln(x)-x\right]_{x=1}^{n} = \frac{1}{\ln(2)}(n\ln(n)-n+1)=n\log_2(n) - \frac{n-1}{\ln(2)} \in \Omega(n \log(n))&amp;lt;/math&amp;gt;&lt;br /&gt;
Folglich gilt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;d\ge\log_2(n!) = \frac{1}{\ln(2)}\sum_{k=2}^{n} \ln(k) \in \Omega(n \log(n))&amp;lt;/math&amp;gt;&lt;br /&gt;
Mit anderen Worten: '''Kein Sortieralgorithmus auf Basis paarweise Vergleiche ist asymptotisch schneller als Mergesort, denn die Anzahl der Vergleiche (= Tiefe des Entscheidungsbaumes) ist &amp;lt;math&amp;gt;\Omega(n \log(n))&amp;lt;/math&amp;gt;'''. Falls man einen schnelleren Sortieralgorithmus benötigt, muss man ein anderes algorithmisches Prinzip verwenden.&lt;br /&gt;
&lt;br /&gt;
==Effizientere Sortieralgorithmen==&lt;br /&gt;
&lt;br /&gt;
Wir haben gezeigt, dass mit paarweisen Größenvergleichen allein kein Sortieralgorithmus schneller als &amp;lt;math&amp;gt;\Omega(n \log n)&amp;lt;/math&amp;gt; sein kann. Um einen besseren Algorithmus zu finden, dürfen wir nicht nur die relative Größe der Schlüssel (also die Ordnung) berücksichtigen, sondern müssen die Werte selbst verwenden. Der entscheidende Trick dabei ist das &lt;br /&gt;
&lt;br /&gt;
=== Bucket-Prinzip ===&lt;br /&gt;
&lt;br /&gt;
Man definiert eine Funktion &amp;lt;tt&amp;gt;quantize(key, M)&amp;lt;/tt&amp;gt;, die jeden Schlüssel auf eine ganze Zahl im Bereich &amp;lt;tt&amp;gt;[0,...,M-1]&amp;lt;/tt&amp;gt; abbildet. Mit Hilfe dieser Zahlen werden die Schlüssel auf M ''Buckets'' aufgeteilt, und das Sortieren kann dann in jedem Bucket getrennt erfolgen. Am Ende setzt man aus den Inhalten der Buckets das gesamte Array (sortiert) wieder zusammen. Wir zeigen unten, dass man damit lineare Zeit erreicht.&lt;br /&gt;
&lt;br /&gt;
Um für das Sortieren brauchbar zu sein, muss die Funktion &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt; ''Ordnung erhaltend'' definiert sein:&lt;br /&gt;
   wenn key1 &amp;lt;= key2, gilt auch quantize(key1, M) &amp;lt;= quantize(key2, M)&lt;br /&gt;
Eine solche Abbildung nennt man ''Quantisierung''. Allgemein bekannt ist der Prozess der Quantisierung z.B. bei Digitalkameras: Hier wird in jedem Pixel eine reell-wertige Lichtintensität gemessen, die im resultierenden Bild mit nur 256 Intensitätsabstufungen pro Farbe abgespeichert wird (bzw. mit bis zu 65536 Abstufungen bei Kameras mit sogenanntem &amp;quot;high dynamic range&amp;quot;). Bleibt die Ordnung bei der Abbildung von Schlüsseln auf natürliche Zahlen nicht erhalten, spricht man von ''Hashing''. Hashing wird zur Implementation von [[Hashing und Hashtabellen|Hashtabellen]] nutzbringend eingesetzt.&lt;br /&gt;
&lt;br /&gt;
Der einfachste Fall liegt vor, wenn die Schlüssel bereits genze Zahlen im Bereich &amp;lt;tt&amp;gt;[0,...,M-1]&amp;lt;/tt&amp;gt; sind. Dann ist die &amp;lt;tt&amp;gt;quantize()&amp;lt;/tt&amp;gt; einfach die Identität. Wir nehmen an, dass die Daten als Schlüssel/Wert-Paare in einem Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; gespeichert sind, und wir können das Sortieren wie folgt implementieren:&lt;br /&gt;
   def integerSort(a, M):&lt;br /&gt;
       # erzeuge M leere Buckets&lt;br /&gt;
       buckets = [[] for k in range(M)]  &lt;br /&gt;
       &lt;br /&gt;
       # verteile die Daten auf die Buckets&lt;br /&gt;
       for k in range(len(a)):           &lt;br /&gt;
           buckets[a[k].key].append(a[k]) # a[k].key sind Integer-Schlüssel in [0,...,M-1]&lt;br /&gt;
      &lt;br /&gt;
       # setze das Array a aus den Buckets sortiert wieder zusammen&lt;br /&gt;
       start = 0                          # Anfangsindex des ersten Buckets&lt;br /&gt;
       for k in range(M):&lt;br /&gt;
           end = start + len(buckets[k])  # Endindex des aktuellen Buckets&lt;br /&gt;
           a[start:end] = buckets[k]      # Daten an der richtigen Position in a einfügen&lt;br /&gt;
           start = end                    # Anfangsindex für das nächste Bucket aktualisieren&lt;br /&gt;
Das Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; ist am Ende sortiert, weil wir den Inhalt der Buckets nach aufsteigendem Bucket-Index, und damit automatisch nach aufsteigenden Schlüsseln, in &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; einfügen. Das Sortieren ist außerdem ''stabil'', da Daten mit gleichem Schlüssel immer hinten an das jeweilige Bucket angefügt werden. &lt;br /&gt;
&lt;br /&gt;
Die Komplexität des Algorithmus ist &amp;lt;math&amp;gt;O(N)&amp;lt;/math&amp;gt; mit &amp;lt;tt&amp;gt;N = len(a)&amp;lt;/tt&amp;gt;, solange &lt;br /&gt;
::&amp;lt;math&amp;gt;M \in O(N)&amp;lt;/math&amp;gt; &lt;br /&gt;
gilt: Das Erzeugen der Buckets erfordert &amp;lt;math&amp;gt;O(M)&amp;lt;/math&amp;gt; Schritte und das Verteilen der Daten auf die Buckets &amp;lt;math&amp;gt;O(N)&amp;lt;/math&amp;gt; Schritte (weil &amp;lt;tt&amp;gt;bucket[k].append()&amp;lt;/tt&amp;gt; amortisiert konstante Komplexität hat). Das Zusammensetzen des sortierten Arrays wird vom Kopieren der Daten dominiert, welches die Komplexität &lt;br /&gt;
::&amp;lt;math&amp;gt;\sum_{k=0}^{M-1} O(N_k)= O\left(\sum_{k=0}^{M-1} N_k\right)&amp;lt;/math&amp;gt; &lt;br /&gt;
besitzt, wobei &amp;lt;tt&amp;gt;N&amp;lt;sub&amp;gt;k&amp;lt;/sub&amp;gt;=len(buckets[k])&amp;lt;/tt&amp;gt; die Größe des k-ten Buckets ist. Die Gesamtanzahl der Daten in allen Buckets zusammen ist aber gerade wieder die Größe von &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt;, also &lt;br /&gt;
::&amp;lt;math&amp;gt;O\left(\sum_{k=0}^{M-1} N_k\right) = O(N)&amp;lt;/math&amp;gt;&lt;br /&gt;
Nach der Sequenzregel ist die Gesamtkomplexität somit &amp;lt;math&amp;gt;O(M + N)=O(N)&amp;lt;/math&amp;gt;, falls &amp;lt;math&amp;gt;M \in O(N)&amp;lt;/math&amp;gt; gilt.&lt;br /&gt;
&lt;br /&gt;
===Bucket Sort===&lt;br /&gt;
&lt;br /&gt;
Der Algorithmus wird nur wenig komplizierter, wenn die Schlüssel beliebig sein können, aber eine Ordnung-erhaltende &amp;lt;tt&amp;gt;bucketMap()&amp;lt;/tt&amp;gt;-Funktion vorhanden ist. Allerdings geht bei der Abbildung von Schlüsseln auf Bucket-Indizes im allgemeinen ein Teil der Schlüsselinformation verloren. Die Elemente im selben Bucket haben nicht (wie oben) automatisch den gleichen Schlüssel, so dass jeder Bucket noch explizit sortiert werden muss. Diese Tatsache führt zu einer zusaätzlichen Einschränkung: einerseits muss für die Anzahl der Buckets nach wie vor &lt;br /&gt;
::&amp;lt;math&amp;gt;M \in O(N)&amp;lt;/math&amp;gt; &lt;br /&gt;
gelten, aber andererseits sollte jeder Bucket nur wenige Daten enthalten, so dass das Sortieren innerhalb der Buckets effizient ist. Wir fordern deshalb, dass &amp;lt;math&amp;gt;N_k \in O(1)&amp;lt;/math&amp;gt; sein soll. Unter der Voraussetzung, dass &amp;lt;tt&amp;gt;bucketMap()&amp;lt;/tt&amp;gt; die daten gleichmäßig auf alle Buckets verteilt (dazu unten mehr), gilt für die Bucketgrößen &lt;br /&gt;
::&amp;lt;math&amp;gt;N_k \in O\left(\frac{N}{M}\right)&amp;lt;/math&amp;gt;&lt;br /&gt;
denn wir verteilen N Elemente auf M Buckets. Beide Bedingungen sind erfüllt, wenn&lt;br /&gt;
::&amp;lt;math&amp;gt;M = \frac{N}{d}&amp;lt;/math&amp;gt;&lt;br /&gt;
gilt, wobei d eine Konstante unabhängig von N ist. In der Praxis erzielt man die besten Resultate mit &amp;lt;math&amp;gt;1 \le d \le 10&amp;lt;/math&amp;gt; (die beste Wahl hängt im konkreten Fall von der Schlüsselverteilung und von der &amp;lt;tt&amp;gt;bucketMap()&amp;lt;/tt&amp;gt;-Funktion ab). Wir übergeben die Konstante d und die &amp;lt;tt&amp;gt;bucketMap()&amp;lt;/tt&amp;gt;-Funktion als Parameter an den Algorithmus:&lt;br /&gt;
   def bucketSort(a, bucketMap, d):&lt;br /&gt;
       N = len(a)&lt;br /&gt;
       M = int(N / float(d))  # Anzahl der Buckets festlegen&lt;br /&gt;
       &lt;br /&gt;
       # M leere Buckets erzeugen&lt;br /&gt;
       buckets = [[] for k in range(M)]&lt;br /&gt;
       &lt;br /&gt;
       # Daten auf die Buckets verteilen&lt;br /&gt;
       for k in range(len(a)):&lt;br /&gt;
           index = bucketMap(a[k].key, M) # Bucket-Index berechnen&lt;br /&gt;
           buckets[index].append(a[k])    # a[k] im passenden Bucket einfügen&lt;br /&gt;
       &lt;br /&gt;
       # Daten sortiert wieder in a einfügen&lt;br /&gt;
       start = 0                          # Anfangsindex des ersten Buckets &lt;br /&gt;
       for k in range(M):&lt;br /&gt;
           insertionSort(buckets[k])      # Daten innerhalb des aktuellen Buckets sortieren&lt;br /&gt;
           end = start + len(buckets[k])  # Endindex des aktuellen Buckets&lt;br /&gt;
           a[start:end] = buckets[k]      # Daten an der richtigen Position in a einfügen&lt;br /&gt;
           start += len(buckets[k])       # Anfangsindex für nächsten Bucket aktualisieren&lt;br /&gt;
Wir verwenden zum Sortieren der Daten in jedem Bucket &amp;lt;tt&amp;gt;insertionSort()&amp;lt;/tt&amp;gt;. Dies ist aus zwei Gründen eine gute Wahl: Erstens haben wir die Buckets so konstruiert, dass jeder Bucket nur wenige Elemente enthält (&amp;lt;math&amp;gt;N_k \in O(1)&amp;lt;/math&amp;gt;), und für kleine Arrays ist Insertion Sort der schnellste Algorithmus. Zweitens ist Insertion Sort ein ''stabiler'' Sortieralgorithmus, und demzufolge ist auch das gesamte &amp;lt;tt&amp;gt;bucketSort()&amp;lt;/tt&amp;gt; stabil.&lt;br /&gt;
&lt;br /&gt;
Unter der Voraussetzung, dass &amp;lt;tt&amp;gt;bucketMap()&amp;lt;/tt&amp;gt; konstante Zeit für die Quantisierung eines Schlüssels benötigt, unterscheidet sich die Komplexitätsanalyse von &amp;lt;tt&amp;gt;bucketSort()&amp;lt;/tt&amp;gt; nur in einem Punkt von &amp;lt;tt&amp;gt;integerSort()&amp;lt;/tt&amp;gt;, nämlich durch das zusätzliche Sortieren in jedem Bucket. Bei Verwendung von Insertion Sort hat dies quadratische Komplexität in &amp;lt;math&amp;gt;N_k&amp;lt;/math&amp;gt;, aber wenn &amp;lt;math&amp;gt;N_k \in O(1)&amp;lt;/math&amp;gt; erfüllt ist, gilt &amp;lt;math&amp;gt;O(N_k^2) = O(1^2) = O(1)&amp;lt;/math&amp;gt;. Das Sortieren hat also konstante Komplexität pro Bucket, und somit ist die Gesamtkomplexität von &amp;lt;tt&amp;gt;bucketSort()&amp;lt;/tt&amp;gt; linear in N, wie am Anfang des Abschnitts gewünscht.&lt;br /&gt;
&lt;br /&gt;
Allerdings steht und fällt diese Analyse damit, dass die &amp;lt;tt&amp;gt;bucketMap()&amp;lt;/tt&amp;gt;-Funktion die Daten tatsächlich gleichmäßig auf die Buckets verteilt. Andernfalls könnten im schlechtesten Fall alle Daten in einem einzigen Bucket landen, und dann hätte &amp;lt;tt&amp;gt;bucketSort()&amp;lt;/tt&amp;gt; quadratische Komplexität. Die &amp;lt;tt&amp;gt;bucketMap()&amp;lt;/tt&amp;gt;-Funktion muss deshalb je nach der Wahrscheinlichkeitsverteilung der Schlüssel immer wieder anders festgelegt werden. Sehr einfach ist dies, wenn die Schlüssel in einem gewissen Intervall &amp;lt;tt&amp;gt;[U,...,V)&amp;lt;/tt&amp;gt; gleichverteilt sind: dann kann man einfach das Intervall &amp;lt;tt&amp;gt;[U,...,V)&amp;lt;/tt&amp;gt; durch eine lineare Gleichung auf das Intervall &amp;lt;tt&amp;gt;[0,...M-1]&amp;lt;/tt&amp;gt; abbilden und dann abrunden. Für &amp;lt;tt&amp;gt;U = 0&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;V = 1&amp;lt;/tt&amp;gt; erhalten wir beilspielsweise:&lt;br /&gt;
    '''Beispiel:'''&lt;br /&gt;
    # keys sind reelle Zahlen in [0, 1)&lt;br /&gt;
    &lt;br /&gt;
    def bucketMap(key, M):&lt;br /&gt;
        return int(key * M)&lt;br /&gt;
Die Definition einer geeigneten &amp;lt;tt&amp;gt;bucketMap()&amp;lt;/tt&amp;gt;-Funktion für eine andere Schlüsselverteilung ist Bestandteil einer Übungsaufgabe. In der Praxis findet man allerdings, dass die Verteilung der Daten auf die Buckets nicht übermäßig kritisch ist -- &amp;lt;tt&amp;gt;bucketSort()&amp;lt;/tt&amp;gt; blaibt auch dann ein sehr schneller Algorithmus, wenn die Verteilung nicht ganz gleichmäßig gelingt.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Prioritätswarteschlangen|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5707</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=5707"/>
				<updated>2020-07-02T16:10:52Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Breitensuche in Graphen (Breadth First Search, BFS) */&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 auf demselben Weg zurück (sogenanntes &amp;lt;i&amp;gt;back tracking&amp;lt;/i&amp;gt;), bis man einen Knoten findet, der noch einen unbesuchten 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;
Die 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 &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; genannt. (Die Verwendung von property maps hat sich gegenüber der alternativen Idee durchgesetzt, solche Eigenschaften in speziellen Knotenklassen zu speichern. Im letzteren Fall braucht man nämlich für jede Anwendung eine angepasste Knotenklasse mit den jeweils gewünschten Attributen und damit auch angepasste Implementationen der Graphenfunktionen, was sich als sehr aufwändig erwiesen hat.) &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 Nachbarpixeln 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, Informationen über den Verlauf der Suche zu sammeln und am Ende zurückzugeben, so dass andere Algorithmen diese Information nutzen können. Typische Beispiele dafür sind eine Reihenfolge der Knoten (in discovery oder finishing order) oder die Vorgänger jedes Knotens im Tiefensuchbaum (also  von welchem Knoten aus man den jeweiligen Knoten zuerst erreicht hat). Wir führen dafür drei neue Arrays ein. &lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)    # wurde ein Knoten bereits besucht?&lt;br /&gt;
     parents = [None]*len(graph)     # registriere für jeden Knoten den Vorgänger im Tiefensuchbaum&lt;br /&gt;
     discovery_order = []            # enthält am Ende die pre-order Sortierung&lt;br /&gt;
     finishing_order = []            # enthält am Ende die post-order Sortierung&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, parent):        # rekursive Hilfsfunktion&lt;br /&gt;
         if not visited[node]:       # besuche 'node', wenn noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True           # markiere 'node' als besucht&lt;br /&gt;
             parents[node] = parent         # speichere den Vorgänger von 'node'&lt;br /&gt;
             discovery_order.append(node)   # registriere, dass 'node' jetzt entdeckt wurde&lt;br /&gt;
             for neighbor in graph[node]:   # besuche rekursiv die Nachbarn ...&lt;br /&gt;
                 visit(neighbor, node)      #  ... wobei 'node' zu deren Vorgänger wird&lt;br /&gt;
             finishing_order.append(node)   # registriere, dass 'node' jetzt fertiggestellt wurde&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode, None)          # beginne bei 'startnode', der keinen Vorgänger hat&lt;br /&gt;
     &lt;br /&gt;
     return parents, discovery_order, finishing_order # gib die zusätzliche Informationen zurück&lt;br /&gt;
&lt;br /&gt;
Beginnt man die Suche bei Knoten 1, entsprechen die Inhalte der Arrays &amp;lt;tt&amp;gt;discovery_order&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;finishing_order&amp;lt;/tt&amp;gt; für den obigen Beispielgraphen gerade den vorher angeführten &amp;lt;tt&amp;gt;print&amp;lt;/tt&amp;gt;-Ausgaben. Die Vorgänger im Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; lauten: &lt;br /&gt;
  Knotennummer  |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7&lt;br /&gt;
  --------------+-----+-----+-----+-----+-----+-----+-----+-----&lt;br /&gt;
  Vorgänger     | None| None|  1  |  4  |  2  |  2  |  3  |  3&lt;br /&gt;
&lt;br /&gt;
Die Knotennummern dienen hier als Array-Indizes, und die dazugehörigen Arrayeinträge verweisen auf die Vorgänger. Man kann mit diesen Informationen den Weg von jedem Knoten zur Wurzel zurückverfolgen und damit den Tiefensuchbaum von unten nach oben rekonstruieren. Man beachte, dass &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; den Eintrag &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; für die Knoten 0 umd 1 enthält, weil Knoten 0 in diesem Graphen nicht existiert und Knoten 1 als Wurzel der Suche keinen Vorgänger hat.&lt;br /&gt;
&lt;br /&gt;
Wird das Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; verwendet, kann man den Code vereinfachen, indem man das Array &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; einspart: Sobald ein Knoten erstmals besucht wurde, ist sein Vorgänger bekannt und damit ungleich &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;. Die Abfrage &amp;lt;tt&amp;gt;if parents[node] is None:&amp;lt;/tt&amp;gt; liefert damit das gleiche Resultat wie die Abfrage &amp;lt;tt&amp;gt;if not visited[node]:&amp;lt;/tt&amp;gt;. Einzige Ausnahme ist der Startknoten der Suche, dessen Vorgänger bisher &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; war. Dieses Problem löst man leicht mit der Konvention, dass man den Startknoten zu seinem eigenen Vorgänger erklärt. Man startet die Suche also mit &amp;lt;tt&amp;gt;visit(startnode, startnode)&amp;lt;/tt&amp;gt; statt mit &amp;lt;tt&amp;gt;visit(startnode, None)&amp;lt;/tt&amp;gt;.&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 Nachbarknoten 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;
     parents = [None]*len(graph)            # speichere für jeden Knoten den Vorgänger im Breitensuchbaum&lt;br /&gt;
     parents[startnode] = startnode         # Konvention: der Startknoten hat sich selbst als Vorgänger &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 noch Knoten zu bearbeiten sind&lt;br /&gt;
         node = q.popleft()                 # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
     &amp;lt;font color=red&amp;gt;                                       # Beachte: mit q.popright() bekommen wir DFS&amp;lt;/font&amp;gt;&lt;br /&gt;
         print(node)                        # den Knoten bearbeiten (hier: Knotennummer drucken)&lt;br /&gt;
         for neighbor in graph[node]:       # die Nachbarn expandieren&lt;br /&gt;
             if parents[neighbor] is None:  # Nachbar wurde noch nicht besucht&lt;br /&gt;
                 parents[neighbor] = node   # =&amp;gt; Vorgänger merken, Knoten dadurch als &amp;quot;besucht&amp;quot; markieren&lt;br /&gt;
                 q.append(neighbor)         #    und in die Queue aufnehmen&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;
=== 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;
=== 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;
=== 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 range(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 = list(range(len(graph)))  # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     &lt;br /&gt;
     for node in range(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 range(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 zunächst eine wichtige Eigenschaft des Algorithmus: Die Priorität (=Pfadlänge) des Knotens an der Spitze des Heaps wächst im Laufe des Algorithmus monoton an (aber nicht notwendigerweise streng monoton). Mit anderen Worten: liefert &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; in der i-ten Iteration der &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Schleife den Knoten u mit der Pfadlänge l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt;, und in der (i+1)-ten Iteration den Knoten v mit der Pfadlänge l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt;, so gilt stets l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; &amp;amp;ge; l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt;. Wir zeigen dies mit der Technik des indirekten Beweises, d.h. wir nehmen das Gegenteil an und führen diese Annahme zum Widerspruch. Wäre also l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; &amp;lt; l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt;, gäbe es zwei Möglichkeiten:&lt;br /&gt;
&amp;lt;ol&amp;gt;&lt;br /&gt;
&amp;lt;li&amp;gt;Der Weg nach v mit der Länge l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; war in der i-ten Iteration schon bekannt und somit bereits im Heap enthalten. Dann hätte &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; in dieser Iteration aber v zurückgegeben, im Widerspruch zur Annahme, dass u zurückgegeben wurde.&amp;lt;/li&amp;gt;&lt;br /&gt;
&amp;lt;li&amp;gt;Der Weg wurde erst bei der Expansion von u in der i-ten Iteration gefunden. Dann muss v ein Nachbar von u sein, und seine Weglänge berechnet sich als l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; = l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt; + w&amp;lt;sub&amp;gt;u,v&amp;lt;/sub&amp;gt;. Da für die Kantengewichte aber w&amp;lt;sub&amp;gt;u,v&amp;lt;/sub&amp;gt; &amp;amp;ge; 0 gefordert ist, kann l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; &amp;lt; l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt; nicht gelten.&amp;lt;/li&amp;gt;&lt;br /&gt;
&amp;lt;/ol&amp;gt;&lt;br /&gt;
Diese Monotonieeigenschaft hat eine interessante Konsequenz: Beträgt der Abstand vom Start zum Zielknoten l&amp;lt;sub&amp;gt;z&amp;lt;/sub&amp;gt;, so findet Dijsktra's Algorithmus als Nebenprodukt auch die kürzesten Wege zu allen näher gelegenen Knoten, also zu allen Knoten u, für deren Abstand l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt; &amp;lt; l&amp;lt;sub&amp;gt;z&amp;lt;/sub&amp;gt; gilt. Dies trifft auch dann zu, wenn diese Wege für den Benutzer gar nicht von Interesse sind. Der A*-Algorithmus, der weiter unten erklärt wird, versucht dem abzuhelfen.&lt;br /&gt;
&lt;br /&gt;
Wir können nun mittels vollständiger Induktion die folgende Schleifen-Invariante beweisen: 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 wieder 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 dem Momemnt in den Heap eingefügt werden, wo der kürzeste Weg für &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; gefunden wurde. Man beachte auch, dass wegen der Monotonieeigenschaft 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 die Knoten in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt;. &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 im Heap 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;: Wegen der Monotonieeigenschaft muss jetzt &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) &amp;gt; Kosten(node &amp;amp;rarr; predecessor &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt; gelten. 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 aber als &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) + weight[(x, node)]&amp;lt;/tt&amp;gt;, und deshalb kann dieser Weg keinesfalls kürzer sein.&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 range(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 range(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 range(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:     # neuer Knoten gefunden:&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;
         else:&lt;br /&gt;
             return False                     # node war bereits 'finished' =&amp;gt; kein Zyklus&lt;br /&gt;
     &lt;br /&gt;
     for node in range(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 range(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;
== 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>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5706</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=5706"/>
				<updated>2020-07-02T16:10:31Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Breitensuche in Graphen (Breadth First Search, BFS) */&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 auf demselben Weg zurück (sogenanntes &amp;lt;i&amp;gt;back tracking&amp;lt;/i&amp;gt;), bis man einen Knoten findet, der noch einen unbesuchten 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;
Die 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 &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; genannt. (Die Verwendung von property maps hat sich gegenüber der alternativen Idee durchgesetzt, solche Eigenschaften in speziellen Knotenklassen zu speichern. Im letzteren Fall braucht man nämlich für jede Anwendung eine angepasste Knotenklasse mit den jeweils gewünschten Attributen und damit auch angepasste Implementationen der Graphenfunktionen, was sich als sehr aufwändig erwiesen hat.) &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 Nachbarpixeln 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, Informationen über den Verlauf der Suche zu sammeln und am Ende zurückzugeben, so dass andere Algorithmen diese Information nutzen können. Typische Beispiele dafür sind eine Reihenfolge der Knoten (in discovery oder finishing order) oder die Vorgänger jedes Knotens im Tiefensuchbaum (also  von welchem Knoten aus man den jeweiligen Knoten zuerst erreicht hat). Wir führen dafür drei neue Arrays ein. &lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)    # wurde ein Knoten bereits besucht?&lt;br /&gt;
     parents = [None]*len(graph)     # registriere für jeden Knoten den Vorgänger im Tiefensuchbaum&lt;br /&gt;
     discovery_order = []            # enthält am Ende die pre-order Sortierung&lt;br /&gt;
     finishing_order = []            # enthält am Ende die post-order Sortierung&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, parent):        # rekursive Hilfsfunktion&lt;br /&gt;
         if not visited[node]:       # besuche 'node', wenn noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True           # markiere 'node' als besucht&lt;br /&gt;
             parents[node] = parent         # speichere den Vorgänger von 'node'&lt;br /&gt;
             discovery_order.append(node)   # registriere, dass 'node' jetzt entdeckt wurde&lt;br /&gt;
             for neighbor in graph[node]:   # besuche rekursiv die Nachbarn ...&lt;br /&gt;
                 visit(neighbor, node)      #  ... wobei 'node' zu deren Vorgänger wird&lt;br /&gt;
             finishing_order.append(node)   # registriere, dass 'node' jetzt fertiggestellt wurde&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode, None)          # beginne bei 'startnode', der keinen Vorgänger hat&lt;br /&gt;
     &lt;br /&gt;
     return parents, discovery_order, finishing_order # gib die zusätzliche Informationen zurück&lt;br /&gt;
&lt;br /&gt;
Beginnt man die Suche bei Knoten 1, entsprechen die Inhalte der Arrays &amp;lt;tt&amp;gt;discovery_order&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;finishing_order&amp;lt;/tt&amp;gt; für den obigen Beispielgraphen gerade den vorher angeführten &amp;lt;tt&amp;gt;print&amp;lt;/tt&amp;gt;-Ausgaben. Die Vorgänger im Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; lauten: &lt;br /&gt;
  Knotennummer  |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7&lt;br /&gt;
  --------------+-----+-----+-----+-----+-----+-----+-----+-----&lt;br /&gt;
  Vorgänger     | None| None|  1  |  4  |  2  |  2  |  3  |  3&lt;br /&gt;
&lt;br /&gt;
Die Knotennummern dienen hier als Array-Indizes, und die dazugehörigen Arrayeinträge verweisen auf die Vorgänger. Man kann mit diesen Informationen den Weg von jedem Knoten zur Wurzel zurückverfolgen und damit den Tiefensuchbaum von unten nach oben rekonstruieren. Man beachte, dass &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; den Eintrag &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; für die Knoten 0 umd 1 enthält, weil Knoten 0 in diesem Graphen nicht existiert und Knoten 1 als Wurzel der Suche keinen Vorgänger hat.&lt;br /&gt;
&lt;br /&gt;
Wird das Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; verwendet, kann man den Code vereinfachen, indem man das Array &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; einspart: Sobald ein Knoten erstmals besucht wurde, ist sein Vorgänger bekannt und damit ungleich &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;. Die Abfrage &amp;lt;tt&amp;gt;if parents[node] is None:&amp;lt;/tt&amp;gt; liefert damit das gleiche Resultat wie die Abfrage &amp;lt;tt&amp;gt;if not visited[node]:&amp;lt;/tt&amp;gt;. Einzige Ausnahme ist der Startknoten der Suche, dessen Vorgänger bisher &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; war. Dieses Problem löst man leicht mit der Konvention, dass man den Startknoten zu seinem eigenen Vorgänger erklärt. Man startet die Suche also mit &amp;lt;tt&amp;gt;visit(startnode, startnode)&amp;lt;/tt&amp;gt; statt mit &amp;lt;tt&amp;gt;visit(startnode, None)&amp;lt;/tt&amp;gt;.&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 Nachbarknoten 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;
     parents = [None]*len(graph)            # speichere für jeden Knoten den Vorgänger im Breitensuchbaum&lt;br /&gt;
     parents[startnode] = startnode         # Konvention: der Startknoten hat sich selbst als Vorgänger &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 noch Knoten zu bearbeiten sind&lt;br /&gt;
         node = q.popleft()                 # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
&amp;lt;font color=red&amp;gt;                                            # Beachte: mit q.popright() bekommen wir DFS&amp;lt;/font&amp;gt;&lt;br /&gt;
         print(node)                        # den Knoten bearbeiten (hier: Knotennummer drucken)&lt;br /&gt;
         for neighbor in graph[node]:       # die Nachbarn expandieren&lt;br /&gt;
             if parents[neighbor] is None:  # Nachbar wurde noch nicht besucht&lt;br /&gt;
                 parents[neighbor] = node   # =&amp;gt; Vorgänger merken, Knoten dadurch als &amp;quot;besucht&amp;quot; markieren&lt;br /&gt;
                 q.append(neighbor)         #    und in die Queue aufnehmen&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;
=== 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;
=== 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;
=== 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 range(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 = list(range(len(graph)))  # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     &lt;br /&gt;
     for node in range(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 range(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 zunächst eine wichtige Eigenschaft des Algorithmus: Die Priorität (=Pfadlänge) des Knotens an der Spitze des Heaps wächst im Laufe des Algorithmus monoton an (aber nicht notwendigerweise streng monoton). Mit anderen Worten: liefert &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; in der i-ten Iteration der &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Schleife den Knoten u mit der Pfadlänge l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt;, und in der (i+1)-ten Iteration den Knoten v mit der Pfadlänge l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt;, so gilt stets l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; &amp;amp;ge; l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt;. Wir zeigen dies mit der Technik des indirekten Beweises, d.h. wir nehmen das Gegenteil an und führen diese Annahme zum Widerspruch. Wäre also l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; &amp;lt; l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt;, gäbe es zwei Möglichkeiten:&lt;br /&gt;
&amp;lt;ol&amp;gt;&lt;br /&gt;
&amp;lt;li&amp;gt;Der Weg nach v mit der Länge l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; war in der i-ten Iteration schon bekannt und somit bereits im Heap enthalten. Dann hätte &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; in dieser Iteration aber v zurückgegeben, im Widerspruch zur Annahme, dass u zurückgegeben wurde.&amp;lt;/li&amp;gt;&lt;br /&gt;
&amp;lt;li&amp;gt;Der Weg wurde erst bei der Expansion von u in der i-ten Iteration gefunden. Dann muss v ein Nachbar von u sein, und seine Weglänge berechnet sich als l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; = l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt; + w&amp;lt;sub&amp;gt;u,v&amp;lt;/sub&amp;gt;. Da für die Kantengewichte aber w&amp;lt;sub&amp;gt;u,v&amp;lt;/sub&amp;gt; &amp;amp;ge; 0 gefordert ist, kann l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; &amp;lt; l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt; nicht gelten.&amp;lt;/li&amp;gt;&lt;br /&gt;
&amp;lt;/ol&amp;gt;&lt;br /&gt;
Diese Monotonieeigenschaft hat eine interessante Konsequenz: Beträgt der Abstand vom Start zum Zielknoten l&amp;lt;sub&amp;gt;z&amp;lt;/sub&amp;gt;, so findet Dijsktra's Algorithmus als Nebenprodukt auch die kürzesten Wege zu allen näher gelegenen Knoten, also zu allen Knoten u, für deren Abstand l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt; &amp;lt; l&amp;lt;sub&amp;gt;z&amp;lt;/sub&amp;gt; gilt. Dies trifft auch dann zu, wenn diese Wege für den Benutzer gar nicht von Interesse sind. Der A*-Algorithmus, der weiter unten erklärt wird, versucht dem abzuhelfen.&lt;br /&gt;
&lt;br /&gt;
Wir können nun mittels vollständiger Induktion die folgende Schleifen-Invariante beweisen: 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 wieder 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 dem Momemnt in den Heap eingefügt werden, wo der kürzeste Weg für &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; gefunden wurde. Man beachte auch, dass wegen der Monotonieeigenschaft 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 die Knoten in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt;. &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 im Heap 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;: Wegen der Monotonieeigenschaft muss jetzt &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) &amp;gt; Kosten(node &amp;amp;rarr; predecessor &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt; gelten. 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 aber als &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) + weight[(x, node)]&amp;lt;/tt&amp;gt;, und deshalb kann dieser Weg keinesfalls kürzer sein.&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 range(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 range(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 range(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:     # neuer Knoten gefunden:&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;
         else:&lt;br /&gt;
             return False                     # node war bereits 'finished' =&amp;gt; kein Zyklus&lt;br /&gt;
     &lt;br /&gt;
     for node in range(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 range(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;
== 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>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5705</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=5705"/>
				<updated>2020-07-02T16:08:19Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Tiefensuche in Graphen (Depth First Search, DFS) */&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 auf demselben Weg zurück (sogenanntes &amp;lt;i&amp;gt;back tracking&amp;lt;/i&amp;gt;), bis man einen Knoten findet, der noch einen unbesuchten 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;
Die 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 &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; genannt. (Die Verwendung von property maps hat sich gegenüber der alternativen Idee durchgesetzt, solche Eigenschaften in speziellen Knotenklassen zu speichern. Im letzteren Fall braucht man nämlich für jede Anwendung eine angepasste Knotenklasse mit den jeweils gewünschten Attributen und damit auch angepasste Implementationen der Graphenfunktionen, was sich als sehr aufwändig erwiesen hat.) &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 Nachbarpixeln 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, Informationen über den Verlauf der Suche zu sammeln und am Ende zurückzugeben, so dass andere Algorithmen diese Information nutzen können. Typische Beispiele dafür sind eine Reihenfolge der Knoten (in discovery oder finishing order) oder die Vorgänger jedes Knotens im Tiefensuchbaum (also  von welchem Knoten aus man den jeweiligen Knoten zuerst erreicht hat). Wir führen dafür drei neue Arrays ein. &lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)    # wurde ein Knoten bereits besucht?&lt;br /&gt;
     parents = [None]*len(graph)     # registriere für jeden Knoten den Vorgänger im Tiefensuchbaum&lt;br /&gt;
     discovery_order = []            # enthält am Ende die pre-order Sortierung&lt;br /&gt;
     finishing_order = []            # enthält am Ende die post-order Sortierung&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, parent):        # rekursive Hilfsfunktion&lt;br /&gt;
         if not visited[node]:       # besuche 'node', wenn noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True           # markiere 'node' als besucht&lt;br /&gt;
             parents[node] = parent         # speichere den Vorgänger von 'node'&lt;br /&gt;
             discovery_order.append(node)   # registriere, dass 'node' jetzt entdeckt wurde&lt;br /&gt;
             for neighbor in graph[node]:   # besuche rekursiv die Nachbarn ...&lt;br /&gt;
                 visit(neighbor, node)      #  ... wobei 'node' zu deren Vorgänger wird&lt;br /&gt;
             finishing_order.append(node)   # registriere, dass 'node' jetzt fertiggestellt wurde&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode, None)          # beginne bei 'startnode', der keinen Vorgänger hat&lt;br /&gt;
     &lt;br /&gt;
     return parents, discovery_order, finishing_order # gib die zusätzliche Informationen zurück&lt;br /&gt;
&lt;br /&gt;
Beginnt man die Suche bei Knoten 1, entsprechen die Inhalte der Arrays &amp;lt;tt&amp;gt;discovery_order&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;finishing_order&amp;lt;/tt&amp;gt; für den obigen Beispielgraphen gerade den vorher angeführten &amp;lt;tt&amp;gt;print&amp;lt;/tt&amp;gt;-Ausgaben. Die Vorgänger im Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; lauten: &lt;br /&gt;
  Knotennummer  |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7&lt;br /&gt;
  --------------+-----+-----+-----+-----+-----+-----+-----+-----&lt;br /&gt;
  Vorgänger     | None| None|  1  |  4  |  2  |  2  |  3  |  3&lt;br /&gt;
&lt;br /&gt;
Die Knotennummern dienen hier als Array-Indizes, und die dazugehörigen Arrayeinträge verweisen auf die Vorgänger. Man kann mit diesen Informationen den Weg von jedem Knoten zur Wurzel zurückverfolgen und damit den Tiefensuchbaum von unten nach oben rekonstruieren. Man beachte, dass &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; den Eintrag &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; für die Knoten 0 umd 1 enthält, weil Knoten 0 in diesem Graphen nicht existiert und Knoten 1 als Wurzel der Suche keinen Vorgänger hat.&lt;br /&gt;
&lt;br /&gt;
Wird das Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; verwendet, kann man den Code vereinfachen, indem man das Array &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; einspart: Sobald ein Knoten erstmals besucht wurde, ist sein Vorgänger bekannt und damit ungleich &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;. Die Abfrage &amp;lt;tt&amp;gt;if parents[node] is None:&amp;lt;/tt&amp;gt; liefert damit das gleiche Resultat wie die Abfrage &amp;lt;tt&amp;gt;if not visited[node]:&amp;lt;/tt&amp;gt;. Einzige Ausnahme ist der Startknoten der Suche, dessen Vorgänger bisher &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; war. Dieses Problem löst man leicht mit der Konvention, dass man den Startknoten zu seinem eigenen Vorgänger erklärt. Man startet die Suche also mit &amp;lt;tt&amp;gt;visit(startnode, startnode)&amp;lt;/tt&amp;gt; statt mit &amp;lt;tt&amp;gt;visit(startnode, None)&amp;lt;/tt&amp;gt;.&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 Nachbarknoten 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;
     parents = [None]*len(graph)            # speichere für jeden Knoten den Vorgänger im Breitensuchbaum&lt;br /&gt;
     parents[startnode] = startnode         # Konvention: der Startknoten hat sich selbst als Vorgänger &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 noch Knoten zu bearbeiten sind&lt;br /&gt;
         node = q.popleft()                 # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
         print(node)                        # den Knoten bearbeiten (hier: Knotennummer drucken)&lt;br /&gt;
         for neighbor in graph[node]:       # die Nachbarn expandieren&lt;br /&gt;
             if parents[neighbor] is None:  # Nachbar wurde noch nicht besucht&lt;br /&gt;
                 parents[neighbor] = node   # =&amp;gt; Vorgänger merken, Knoten dadurch als &amp;quot;besucht&amp;quot; markieren&lt;br /&gt;
                 q.append(neighbor)         #    und in die Queue aufnehmen&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;
=== 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;
=== 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;
=== 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 range(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 = list(range(len(graph)))  # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     &lt;br /&gt;
     for node in range(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 range(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 zunächst eine wichtige Eigenschaft des Algorithmus: Die Priorität (=Pfadlänge) des Knotens an der Spitze des Heaps wächst im Laufe des Algorithmus monoton an (aber nicht notwendigerweise streng monoton). Mit anderen Worten: liefert &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; in der i-ten Iteration der &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Schleife den Knoten u mit der Pfadlänge l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt;, und in der (i+1)-ten Iteration den Knoten v mit der Pfadlänge l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt;, so gilt stets l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; &amp;amp;ge; l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt;. Wir zeigen dies mit der Technik des indirekten Beweises, d.h. wir nehmen das Gegenteil an und führen diese Annahme zum Widerspruch. Wäre also l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; &amp;lt; l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt;, gäbe es zwei Möglichkeiten:&lt;br /&gt;
&amp;lt;ol&amp;gt;&lt;br /&gt;
&amp;lt;li&amp;gt;Der Weg nach v mit der Länge l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; war in der i-ten Iteration schon bekannt und somit bereits im Heap enthalten. Dann hätte &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; in dieser Iteration aber v zurückgegeben, im Widerspruch zur Annahme, dass u zurückgegeben wurde.&amp;lt;/li&amp;gt;&lt;br /&gt;
&amp;lt;li&amp;gt;Der Weg wurde erst bei der Expansion von u in der i-ten Iteration gefunden. Dann muss v ein Nachbar von u sein, und seine Weglänge berechnet sich als l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; = l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt; + w&amp;lt;sub&amp;gt;u,v&amp;lt;/sub&amp;gt;. Da für die Kantengewichte aber w&amp;lt;sub&amp;gt;u,v&amp;lt;/sub&amp;gt; &amp;amp;ge; 0 gefordert ist, kann l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; &amp;lt; l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt; nicht gelten.&amp;lt;/li&amp;gt;&lt;br /&gt;
&amp;lt;/ol&amp;gt;&lt;br /&gt;
Diese Monotonieeigenschaft hat eine interessante Konsequenz: Beträgt der Abstand vom Start zum Zielknoten l&amp;lt;sub&amp;gt;z&amp;lt;/sub&amp;gt;, so findet Dijsktra's Algorithmus als Nebenprodukt auch die kürzesten Wege zu allen näher gelegenen Knoten, also zu allen Knoten u, für deren Abstand l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt; &amp;lt; l&amp;lt;sub&amp;gt;z&amp;lt;/sub&amp;gt; gilt. Dies trifft auch dann zu, wenn diese Wege für den Benutzer gar nicht von Interesse sind. Der A*-Algorithmus, der weiter unten erklärt wird, versucht dem abzuhelfen.&lt;br /&gt;
&lt;br /&gt;
Wir können nun mittels vollständiger Induktion die folgende Schleifen-Invariante beweisen: 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 wieder 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 dem Momemnt in den Heap eingefügt werden, wo der kürzeste Weg für &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; gefunden wurde. Man beachte auch, dass wegen der Monotonieeigenschaft 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 die Knoten in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt;. &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 im Heap 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;: Wegen der Monotonieeigenschaft muss jetzt &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) &amp;gt; Kosten(node &amp;amp;rarr; predecessor &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt; gelten. 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 aber als &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) + weight[(x, node)]&amp;lt;/tt&amp;gt;, und deshalb kann dieser Weg keinesfalls kürzer sein.&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 range(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 range(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 range(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:     # neuer Knoten gefunden:&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;
         else:&lt;br /&gt;
             return False                     # node war bereits 'finished' =&amp;gt; kein Zyklus&lt;br /&gt;
     &lt;br /&gt;
     for node in range(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 range(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;
== 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>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5704</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=5704"/>
				<updated>2020-07-02T16:07:31Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: &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 auf demselben Weg zurück (sogenanntes &amp;lt;i&amp;gt;back tracking&amp;lt;/i&amp;gt;), bis man einen Knoten findet, der noch einen unbesuchten 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;
Die 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 &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; genannt. (Die Verwendung von property maps hat sich gegenüber der alternativen Idee durchgesetzt, solche Eigenschaften in speziellen Knotenklassen zu speichern. Im letzteren Fall braucht man nämlich für jede Anwendung eine angepasste Knotenklasse mit den jeweils gewünschten Attributen und damit auch angepasste Implementationen der Graphenfunktionen, was sich als sehr aufwändig erwiesen hat.) &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 Nachbarpixeln 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, Informationen über den Verlauf der Suche zu sammeln und am Ende zurückzugeben, so dass andere Algorithmen diese Information nutzen können. Typische Beispiele dafür sind eine Reihenfolge der Knoten (in discovery oder finishing order) oder die Vorgänger jedes Knotens im Tiefensuchbaum (also  von welchem Knoten aus man den jeweiligen Knoten zuerst erreicht hat). Wir führen dafür drei neue Arrays ein. &lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)    # wurde ein Knoten bereits besucht?&lt;br /&gt;
     parents = [None]*len(graph)     # registriere für jeden Knoten den Vorgänger im Tiefensuchbaum&lt;br /&gt;
     discovery_order = []            # enthält am Ende die pre-order Sortierung&lt;br /&gt;
     finishing_order = []            # enthält am Ende die post-order Sortierung&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, parent):        # rekursive Hilfsfunktion&lt;br /&gt;
         if not visited[node]:       # besuche 'node', wenn noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True           # markiere 'node' als besucht&lt;br /&gt;
             parents[node] = parent         # speichere den Vorgänger von 'node'&lt;br /&gt;
             discovery_order.append(node)   # registriere, dass 'node' jetzt entdeckt wurde&lt;br /&gt;
             for neighbor in graph[node]:   # besuche rekursiv die Nachbarn ...&lt;br /&gt;
                 visit(neighbor, node)      #  ... wobei 'node' zu deren Vorgänger wird&lt;br /&gt;
             finishing_order.append(node)   # registriere, dass 'node' jetzt fertiggestellt wurde&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode, None)          # beginne bei 'startnode', der keinen Vorgänger hat&lt;br /&gt;
     &lt;br /&gt;
     return parents, discovery_order, finishing_order # gib die zusätzliche Informationen zurück&lt;br /&gt;
&lt;br /&gt;
Beginnt man die Suche bei Knoten 1, entsprechen die Inhalte der Arrays &amp;lt;tt&amp;gt;discovery_order&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;finishing_order&amp;lt;/tt&amp;gt; für den obigen Beispielgraphen gerade den vorher angeführten &amp;lt;tt&amp;gt;print&amp;lt;/tt&amp;gt;-Ausgaben. Die Vorgänger im Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; lauten: &lt;br /&gt;
  Knotennummer  |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7&lt;br /&gt;
  --------------+-----+-----+-----+-----+-----+-----+-----+-----&lt;br /&gt;
  Vorgänger     | None| None|  1  |  4  |  2  |  2  |  3  |  3&lt;br /&gt;
&lt;br /&gt;
Die Knotennummern dienen hier als Array-Indizes, und die dazugehörigen Arrayeinträge verweisen auf die Vorgänger. Man kann mit diesen Informationen den Weg von jedem Knoten zur Wurzel zurückverfolgen und damit den Tiefensuchbaum von unten nach oben rekonstruieren. Man beachte, dass &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; den Eintrag &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; für die Knoten 0 umd 1 enthält, weil Knoten 0 in diesem Graphen nicht existiert und Knoten 1 als Wurzel der Suche keinen Vorgänger hat.&lt;br /&gt;
&lt;br /&gt;
Wird das Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; verwendet, kann man den Code vereinfachen, indem man das Array &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; einspart: Sobald ein Knoten erstmals besucht wurde, ist sein Vorgänger bekannt und damit ungleich &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;. Die Abfrage &amp;lt;tt&amp;gt;if parents[node] is None:&amp;lt;/tt&amp;gt; liefert damit das gleiche Resultat wie die Abfrage &amp;lt;tt&amp;gt;if not visited[node]:&amp;lt;/tt&amp;gt;. Einzige Ausnahme ist der Startknoten der Suche, dessen Vorgänger bisher &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; war. Dieses Problem löst man leicht mit der Konvention, dass man den Startknoten zu seinem eigenen Vorgänger erklärt. Man startet die Suche also mit &amp;lt;tt&amp;gt;visit(startnode, startnode)&amp;lt;/tt&amp;gt; statt mit &amp;lt;tt&amp;gt;visit(startnode, None)&amp;lt;/tt&amp;gt;.&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 Nachbarknoten 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;
     parents = [None]*len(graph)            # speichere für jeden Knoten den Vorgänger im Breitensuchbaum&lt;br /&gt;
     parents[startnode] = startnode         # Konvention: der Startknoten hat sich selbst als Vorgänger &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 noch Knoten zu bearbeiten sind&lt;br /&gt;
         node = q.popleft()                 # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
         print(node)                        # den Knoten bearbeiten (hier: Knotennummer drucken)&lt;br /&gt;
         for neighbor in graph[node]:       # die Nachbarn expandieren&lt;br /&gt;
             if parents[neighbor] is None:  # Nachbar wurde noch nicht besucht&lt;br /&gt;
                 parents[neighbor] = node   # =&amp;gt; Vorgänger merken, Knoten dadurch als &amp;quot;besucht&amp;quot; markieren&lt;br /&gt;
                 q.append(neighbor)         #    und in die Queue aufnehmen&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;
=== 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;
=== 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;
=== 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 range(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 = list(range(len(graph)))  # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     &lt;br /&gt;
     for node in range(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 range(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 zunächst eine wichtige Eigenschaft des Algorithmus: Die Priorität (=Pfadlänge) des Knotens an der Spitze des Heaps wächst im Laufe des Algorithmus monoton an (aber nicht notwendigerweise streng monoton). Mit anderen Worten: liefert &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; in der i-ten Iteration der &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Schleife den Knoten u mit der Pfadlänge l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt;, und in der (i+1)-ten Iteration den Knoten v mit der Pfadlänge l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt;, so gilt stets l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; &amp;amp;ge; l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt;. Wir zeigen dies mit der Technik des indirekten Beweises, d.h. wir nehmen das Gegenteil an und führen diese Annahme zum Widerspruch. Wäre also l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; &amp;lt; l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt;, gäbe es zwei Möglichkeiten:&lt;br /&gt;
&amp;lt;ol&amp;gt;&lt;br /&gt;
&amp;lt;li&amp;gt;Der Weg nach v mit der Länge l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; war in der i-ten Iteration schon bekannt und somit bereits im Heap enthalten. Dann hätte &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; in dieser Iteration aber v zurückgegeben, im Widerspruch zur Annahme, dass u zurückgegeben wurde.&amp;lt;/li&amp;gt;&lt;br /&gt;
&amp;lt;li&amp;gt;Der Weg wurde erst bei der Expansion von u in der i-ten Iteration gefunden. Dann muss v ein Nachbar von u sein, und seine Weglänge berechnet sich als l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; = l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt; + w&amp;lt;sub&amp;gt;u,v&amp;lt;/sub&amp;gt;. Da für die Kantengewichte aber w&amp;lt;sub&amp;gt;u,v&amp;lt;/sub&amp;gt; &amp;amp;ge; 0 gefordert ist, kann l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; &amp;lt; l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt; nicht gelten.&amp;lt;/li&amp;gt;&lt;br /&gt;
&amp;lt;/ol&amp;gt;&lt;br /&gt;
Diese Monotonieeigenschaft hat eine interessante Konsequenz: Beträgt der Abstand vom Start zum Zielknoten l&amp;lt;sub&amp;gt;z&amp;lt;/sub&amp;gt;, so findet Dijsktra's Algorithmus als Nebenprodukt auch die kürzesten Wege zu allen näher gelegenen Knoten, also zu allen Knoten u, für deren Abstand l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt; &amp;lt; l&amp;lt;sub&amp;gt;z&amp;lt;/sub&amp;gt; gilt. Dies trifft auch dann zu, wenn diese Wege für den Benutzer gar nicht von Interesse sind. Der A*-Algorithmus, der weiter unten erklärt wird, versucht dem abzuhelfen.&lt;br /&gt;
&lt;br /&gt;
Wir können nun mittels vollständiger Induktion die folgende Schleifen-Invariante beweisen: 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 wieder 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 dem Momemnt in den Heap eingefügt werden, wo der kürzeste Weg für &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; gefunden wurde. Man beachte auch, dass wegen der Monotonieeigenschaft 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 die Knoten in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt;. &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 im Heap 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;: Wegen der Monotonieeigenschaft muss jetzt &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) &amp;gt; Kosten(node &amp;amp;rarr; predecessor &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt; gelten. 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 aber als &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) + weight[(x, node)]&amp;lt;/tt&amp;gt;, und deshalb kann dieser Weg keinesfalls kürzer sein.&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 range(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 range(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 range(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:     # neuer Knoten gefunden:&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;
         else:&lt;br /&gt;
             return False                     # node war bereits 'finished' =&amp;gt; kein Zyklus&lt;br /&gt;
     &lt;br /&gt;
     for node in range(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 range(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;
== 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>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Hashing_und_Hashtabellen&amp;diff=5703</id>
		<title>Hashing und Hashtabellen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Hashing_und_Hashtabellen&amp;diff=5703"/>
				<updated>2020-06-23T14:11:22Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Doppeltes Hashing */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Die Mitschrift gibts auch als [http://hci.iwr.uni-heidelberg.de/alda/images/AlDa.pdf PDF].&lt;br /&gt;
== Hashing ==&lt;br /&gt;
&lt;br /&gt;
Wir haben im Abschnitt [[Assoziative Arrays]] gezeigt, dass man assoziative Arrays effizient mit Hilfe von Suchbäumen realisieren kann, so dass die Zugriffszeit auf ein Element in O(log(len(a))) ist. Genau wie beim Sortierproblem stellt sich jetzt die Frage, ob die Zugriffszeit noch verbessert werden kann, idealerseise auf O(1) wie beim gewöhnlichen Array. Die Antwort lautet: Ja, wenn für die Schlüssel eine Hashfunktion definiert ist.&lt;br /&gt;
&lt;br /&gt;
===Hashfunktionen===&lt;br /&gt;
&lt;br /&gt;
Hashfunktionen sind eine weitere Anwendung des [[Sortieren in linearer Zeit#Bucket-Prinzip|Bucket-Prinzips]], das wir im Zusammenhang mit dem Sortieren in linearer Zeit eingeführt haben. man bildet die Schlüssel wiederum auf Bucket-Indizes ab, um die Suche zu beschleunigen (von O(log N) nach O(1)). Im Unterschied zum Sortieren verzichtet man hier allerdings darauf, dass die Abbildung auf Bucket-Indizes die Ordnung der Schlüssel erhalten muss (es muss nicht einmal eine Ordnung definiert sein), weil diese Forderung es erschwert, die Schlüssel gleichmäßig auf die Buckets zu verteilen. Letzteres ist aber bei Hashtabellen extrem wichtig.&lt;br /&gt;
&lt;br /&gt;
Gegeben sei ein Universum U, dass die Menge aller legalen Schlüssel darstellt. Die Mächtigkeit |U| der Menge U ist im allgemeinen sehr groß. Beispielsweise kann man mit Strings der Länge 9 bis zu 27&amp;lt;sup&amp;gt;9&amp;lt;/sup&amp;gt;&amp;amp;asymp;10&amp;lt;sup&amp;gt;13&amp;lt;/sup&amp;gt;&amp;amp;asymp;2&amp;lt;sup&amp;gt;43&amp;lt;/sup&amp;gt; verschiedene Schlüssel generieren, wenn 27 Zeichen erlaubt sind (Kleinbuchstaben und Leerzeichen). Die Grundannahme von Hashing ist jetzt, dass in jeder gegebenen Anwendung nur ein (kleiner) Teil der erlaubten Schlüssel tatsächlich verwendet wird. Man definiert eine Hashfunktion, die jeden Schlüssel auf eine natürliche Zahl im Bereich 0...(M-1) abbildet, wobei M viel kleiner als |U| ist. &lt;br /&gt;
;Definition einer Hashfunktion: &lt;br /&gt;
:::&amp;lt;math&amp;gt; f: U  \rightarrow [0, 1, \ldots, M-1] \subset \mathbb{N} &amp;lt;/math&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt; f(u \in U) = h \in [0, 1, \ldots, M-1]&amp;lt;/math&amp;gt;&lt;br /&gt;
h wird als ''Hashwert'' von u bezeichnet. Da M &amp;lt; |U|, werden notwendigerweise einige Schlüssel auf dieselbe Zahl abgebildet. Man bezeichnet den Fall &amp;lt;math&amp;gt; f(u_1 \in U) = f(u_2 \in U) &amp;lt;/math&amp;gt; als ''Kollision'' zwischen den Schlüsseln u&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; und u&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die '''Aufgabe''' besteht jetzt darin, ein Hash-Funktion zu entwerfen, die möglichst wenige Kollisionen hat. Hashfunktionen ähneln damit einem Zufallszahlengenerator, weil jede Zahl &amp;lt;math&amp;gt; h \in 0 \ldots (M-1) &amp;lt;/math&amp;gt; nach Möglichkeit mit gleicher Wahrscheinlichkeit herauskommen soll. Wird dieses Ziel erreicht, spricht man vom ''uniformen Hashing''.&lt;br /&gt;
&lt;br /&gt;
In der Regel ist aber nicht vorher bekannt, welche Schlüssel in einer Anwendung verwendet werden. Es kann deshalb immer vorkommen, dass die verwendete Schlüsselmenge sehr viele Kollisionen verursacht. Man sieht in der Tat leicht ein, dass für jede gegebene Hashfunktion ungünstige Schlüsselmengen &amp;lt;math&amp;gt; U_f \subset U&amp;lt;/math&amp;gt; existieren, bei denen es sehr viele Kollisionen gibt. Im ungünstigsten Fall könnte U&amp;lt;sub&amp;gt;f&amp;lt;/sub&amp;gt; so gewählt sein, dass f(U&amp;lt;sub&amp;gt;f&amp;lt;/sub&amp;gt;) = k = const. gilt. Ein Hacker, der die verwendete Hashfunktion kennt, kann z.B. U&amp;lt;sub&amp;gt;f&amp;lt;/sub&amp;gt; absichtlich so wählen, um eine denial-of-service-Attacke gegen einen hash-basierten Webservice zu starten. Ein anderes anschauliches Beispiel wäre eine Party, zu der nur Leute eingeladen werden, die an einem 8ten im Monat Geburtstag haben. Auf dieser Party ist es viel wahrscheinlicher, Leute zu finden, die am selben (oder gleichen) Tag Geburtstag haben, als wenn man alle einlädt.&lt;br /&gt;
&lt;br /&gt;
D.h. die Wahl einer guten Hashfunktion ist eine Kunst, und man muss (wenn möglich) die Daten analysieren um ein gutes f zu finden.&lt;br /&gt;
&lt;br /&gt;
====Perfektes Hashing====&lt;br /&gt;
&lt;br /&gt;
Kennt man die Untermenge der tatsächlich vorkommenden Schlüssel &amp;lt;math&amp;gt;U_f \subset U&amp;lt;/math&amp;gt; schon im voraus, hat man die Möglichkeit, eine ''perfekte Hashfunktion'' ohne Kollisionen zu entwerfen.&lt;br /&gt;
&lt;br /&gt;
;Beispiel anhand der Monatsnamen &lt;br /&gt;
&lt;br /&gt;
U ist in diesem Fall eine Menge von Strings der Länge 9 (weil der September als längster Monatsname 9 Zeichen hat). Es ergeben sich also &amp;lt;math&amp;gt;60^{9}&amp;lt;/math&amp;gt;&amp;gt;&amp;amp;asymp;10&amp;lt;sup&amp;gt;16&amp;lt;/sup&amp;gt;&amp;amp;asymp;2&amp;lt;sup&amp;gt;54&amp;lt;/sup&amp;gt; mögliche Strings, da mit Groß- und Kleinbuchstaben, Umlauten, ß und Leerzeichen 60 Zeichen im deutschen Alphabet vorhanden sind. Von all diesen Möglichkeiten werden genau 12 benutzt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;U_f&amp;lt;/math&amp;gt; = {&amp;quot;Januar&amp;quot;; &amp;quot;Februar&amp;quot;; ... ; &amp;quot;Dezember&amp;quot;}&lt;br /&gt;
* Benutzt man nun als Hashfunktion die Anfangsbuchstaben der Monatsnamen, benötigt man dafür 6 bit. M ist somit 64. &lt;br /&gt;
:::{&amp;quot;Januar&amp;quot;; &amp;quot;Februar&amp;quot;; ... ; &amp;quot;Dezember&amp;quot;} &amp;amp;rarr; {&amp;quot;J&amp;quot;; &amp;quot;F&amp;quot;; &amp;quot;M&amp;quot;; &amp;quot;A&amp;quot;; &amp;quot;M&amp;quot;; &amp;quot;J&amp;quot;; &amp;quot;J&amp;quot;; &amp;quot;A&amp;quot;; &amp;quot;S&amp;quot;; &amp;quot;O&amp;quot;; &amp;quot;N&amp;quot;; &amp;quot;D&amp;quot;}&lt;br /&gt;
:Dabei enstehen viele Kollisionen (J wird 3x verwendet, M 2x, A 2x), die gewählte ist also keine gute Hashfunktion&lt;br /&gt;
* Benutzt man als Hashfunktion die ersten 3 Buchstaben benötigt man 18 bit, M = &amp;lt;math&amp;gt;2^{18}&amp;lt;/math&amp;gt; &lt;br /&gt;
:::{&amp;quot;Januar&amp;quot;; &amp;quot;Februar&amp;quot;; ... ; &amp;quot;Dezember&amp;quot;} &amp;amp;rarr; {&amp;quot;Jan&amp;quot;, &amp;quot;Feb&amp;quot;, &amp;quot;März&amp;quot;, &amp;quot;Apr&amp;quot;, &amp;quot;Mai&amp;quot;, &amp;quot;Jun&amp;quot;, &amp;quot;Jul&amp;quot;, &amp;quot;Aug&amp;quot;, &amp;quot;Sep&amp;quot;, &amp;quot;Okt&amp;quot;, &amp;quot;Nov&amp;quot;, &amp;quot;Dez&amp;quot;}&lt;br /&gt;
:Nun entstehen keine Kollision mehr. Diese Hashfunktion ist deshalb beim Ausfüllen von Formularen und dergleichen sehr beliebt. Dafür ist M aber recht groß.&lt;br /&gt;
&lt;br /&gt;
Die Aufgabe wird also präzisiert: man sucht für &amp;lt;math&amp;gt;U_f&amp;lt;/math&amp;gt; eine '''minimale, perfekte Hashfunktion''', für die &amp;lt;math&amp;gt;|U_f| = M&amp;lt;/math&amp;gt; gilt. Ein Verfahren hierfür ist Gegenstand von Übungsblatt 9.&lt;br /&gt;
&lt;br /&gt;
====Universelles Hashing====&lt;br /&gt;
&lt;br /&gt;
Hier wählt man für eine gegebene Hashtabelle die Hashfunktion per Zufallszahl aus einer (großen) Menge erlaubter Hashfunktion &amp;amp;rarr; Die Wahrscheinlichkeit, dass die Hashfunktion für die Schlüssel ungünstig ist, wird dadruch minimiert. Die oben erwähnte denial-of-service-Attacke ist jetzt nicht mehr möglich, weil kein Hacker die Hashfunktion im voraus kennen kann. Näheres zum universellen Hashing finden Sie in der [http://en.wikipedia.org/wiki/Universal_hashing Wikpedia].&lt;br /&gt;
&lt;br /&gt;
====Kryptographische Hashfunktionen====&lt;br /&gt;
&lt;br /&gt;
In kryptographischen Anwendungen treten neben dem Hauptziel, die Größe des Universums auf eine überschaubare Zahl von Integer-Werten zu reduzieren, zwei weitere Anforderungen, die für Verschlüsselung bzw. verschlüsselte Kommunikation wichtig sind: erstens will man Kollisionen unbedingt vermeiden (damit zwei verschiedene Dokumente oder Passwörter nicht auf den gleichen Hashwert abgebildet werden), und zweitens darf es nicht möglich sein, aus dem Hashwert die urpsrüngliche Nachricht (also das Dokument oder Passwort) zu rekonstruieren. Man wählt deshalb relative große M (128 bit und mehr) sowie spezielle, für diesen Zweck optimierte Hashfunktionen, wie z.B. [http://de.wikipedia.org/wiki/Message-Digest_Algorithm_5 md5] und [http://de.wikipedia.org/wiki/SHA1 sha1]. Weitere Einzelheiten finden Sie in der [http://en.wikipedia.org/wiki/Cryptographic_hash_function Wikipedia].&lt;br /&gt;
&lt;br /&gt;
====Beliebte Standard-Hashfunktionen====&lt;br /&gt;
&lt;br /&gt;
In der Praxis definiert man Hashfunktionen gewöhnlich zweistufig: Zunächst bildet man den Schlüssel auf einen 32 bit Integerwert ab, M' ist damit 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;. Dieser &amp;quot;rohe&amp;quot; Hashwert wird dann mittels der Modulo-Operation auf die eigentliche Größe M des assoziativen Arrays abgebildet:&lt;br /&gt;
:::&amp;lt;math&amp;gt; f(u \in U) = f'(u \in U)\,\%\,M\,=\,h \in [0, 1, \ldots, M-1] &amp;lt;/math&amp;gt;&lt;br /&gt;
mit&lt;br /&gt;
:::&amp;lt;math&amp;gt; f'(u \in U) = h' \in [0, 1, \ldots, 2^{32}-1] &amp;lt;/math&amp;gt;&lt;br /&gt;
Der große Wert von M' sichert, dass man bei der Wahl von M großen Spielraum hat, so dass die Größe des assoziativen Arrays sehr gut an die Menge der zu speichernden Daten angepaßt werden kann. Die Funktion f'(u) definiert man wie folgt:&lt;br /&gt;
* Falls U = &amp;lt;tt&amp;gt;unsigned int&amp;lt;/tt&amp;gt; (32bit int Datentyp) &amp;amp;rArr; f'(u) = u&lt;br /&gt;
* Falls U = &amp;lt;tt&amp;gt;signed int&amp;lt;/tt&amp;gt; &amp;amp;rArr; Typkonvertierung nach &amp;lt;tt&amp;gt;unsigned int&amp;lt;/tt&amp;gt; &amp;amp;rArr; f'(u) = (unsigned int)u&lt;br /&gt;
* Andere Schlüsseltypen (also insbesondere Strings) interpretiert man als Array of byte &amp;amp;rArr; f'(u) konvertiert Array of Byte nach &amp;lt;tt&amp;gt;unsigned int&amp;lt;/tt&amp;gt;. Beispiele für solche Funktionen:&lt;br /&gt;
:: '''Bernsteinfunktion:'''&lt;br /&gt;
     def bHash(u):     # u: Array of Byte&lt;br /&gt;
         h=0&lt;br /&gt;
         for k in u:&lt;br /&gt;
             h = 33 * h + k &lt;br /&gt;
         return h&lt;br /&gt;
:: '''modifizierte Bernsteinfunktion:'''&lt;br /&gt;
     def mbHash(u):    # u: Array of Byte&lt;br /&gt;
         h=0&lt;br /&gt;
         for k in u:&lt;br /&gt;
             h = (33 * h) ^ k  # ^ ist bitweises Xor&lt;br /&gt;
         return h&lt;br /&gt;
:: '''Shift-Add-Xor-Funktion:'''&lt;br /&gt;
     def saxhash(u):   # u: Array of Byte&lt;br /&gt;
         h=0&lt;br /&gt;
         for k in u:&lt;br /&gt;
             h ^= (h &amp;lt;&amp;lt; 5) + (h &amp;gt;&amp;gt; 2) + k  # &amp;lt;&amp;lt; und &amp;gt;&amp;gt; sind Links- bzw. Rechtsshift der Bits, ^= ist bitweise Xor-Zuweisung&lt;br /&gt;
         return h&lt;br /&gt;
:: '''Fowler/Noll/Vo-Funktion:'''&lt;br /&gt;
     def FNVhash(u):   # u: Array of Byte&lt;br /&gt;
         h = 2166136261&lt;br /&gt;
         for k in u:&lt;br /&gt;
             h = (16777619 * h) ^ k   # ähnlich der modifizierten Bernsteinfunktion, aber mit anderen Konstanten&lt;br /&gt;
         return h&lt;br /&gt;
:: Die verwendeten Konstanten sind experimentell so gewählt worden, dass die Hashfunktionen in typischen Praxisanwendungen relativ wenige Kollisionen verursachen. Der tiefere Grund, warum z.B. 33 in der Bernsteinfunktion eine gute Wahl darstellt, ist unbekannt. Es empfielt sich, in einer gegebenen Anwendung mit mehreren Hashfunktionen zu experimentieren. Weitere solche Funktionen und andere nützliche Informationen findet man auf der Seite [http://www.eternallyconfuzzled.com/tuts/algorithms/jsw_tut_hashing.aspx eternallyconfuzzled.com].&lt;br /&gt;
&lt;br /&gt;
== Hashtabellen ==&lt;br /&gt;
&lt;br /&gt;
Eine Hashtabelle ist eine Datenstruktur, die die Funktionalität des assoziativen Arrays mit Hilfe von Hashing realisiert. Das Grundprinzip besteht darin, dass die Hashtabelle intern ein (dynamisches) Array der Größe &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; verwaltet, so dass die Hashwerte als Indizes in diesem Array verwendet werden können (&amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; entspricht der Zahl M aus der mathematischen Definition oben). Eine naive Implementation der Einfügeoperation sieht also so aus &lt;br /&gt;
     def __setitem__(self, key, value):    # naive Implementation, funktioniert so nicht&lt;br /&gt;
         index = self.hash(key) % self.capacity&lt;br /&gt;
         self.array[index] = value&lt;br /&gt;
Diese Implementation ist allerdings zu einfach. Wenn nämlich die Schlüssel aus dem Universum U beliebig gewählt werden dürfen, sind Kollisionen unvermeidlich. Tritt aber eine Kollision auf, werden die Daten eines Schlüssels mit den Daten eines anderen Schlüssels überschrieben. Um Kollisionen geschickt zu behandeln gibt es zwei Ansätze:&lt;br /&gt;
* lineare Verkettung&lt;br /&gt;
* offene Adressierung&lt;br /&gt;
&lt;br /&gt;
=== Hashtabelle mit linearer Verkettung (offenes Hashing/geschlossene Adressierung) ===&lt;br /&gt;
&lt;br /&gt;
Man kann dies als die pessimistische Lösung bezeichnen: Man nimmt an, dass Kollisionen häufig auftreten. Deshalb wird unter jedem&lt;br /&gt;
Hashindex gleich eine Liste angelegt, in der Einträge mit gleichem Hashindex aufgenommen werden können. Die Hashtabelle verwaltet ein Array von Listen, und jedes Arrayfeld kann beliebig viele Elemente speichern: Wird ein Element auf den Index &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; abgebildet, werden die Daten einfach an die betreffende Liste angehängt. Bei Zugriff auf ein Element wird zunächst die passende Liste gesucht (mit Hilfe des Hashwerts), danach erfolgt in dieser Liste eine sequentielle Suche nach dem richtigen Schlüssel.&lt;br /&gt;
&lt;br /&gt;
Um diese Idee implementieren zu können, benötigen wir zunächst eine Hilfsklasse &amp;lt;tt&amp;gt;HashNode&amp;lt;/tt&amp;gt;, die (Schlüssel, Wert)-Paare speichert und mit Hilfe von &amp;lt;tt&amp;gt;next&amp;lt;/tt&amp;gt; eine verkettete Liste realisiert:&lt;br /&gt;
  class HashNode:&lt;br /&gt;
      def __init__(self,key,data,next):&lt;br /&gt;
          self.key = key&lt;br /&gt;
          self.data = data&lt;br /&gt;
          self.next = next    # Verkettung!&lt;br /&gt;
Die eigentliche Hashtabelle wird in der Klasse ''HashTable'' implementiert:&lt;br /&gt;
  class HashTable:&lt;br /&gt;
      def __init__(self):&lt;br /&gt;
         self.capacity = ... # Geeignete Werte siehe unten&lt;br /&gt;
         self.size = 0       # Anzahl der Werte, die zur Zeit tatsächlich gespeichert sind&lt;br /&gt;
         self.array = [None]*self.capacity&lt;br /&gt;
Wie oben bereits erwähnt, werden die Zugriffsoperatoren ''[ ]'' für eine Datenstruktur in Python durch die Funktionen &amp;lt;tt&amp;gt;__setitem__&amp;lt;/tt&amp;gt; bzw. &amp;lt;tt&amp;gt;__getitem__&amp;lt;/tt&amp;gt; implementiert. &lt;br /&gt;
Die &amp;lt;tt&amp;gt;__setitem__&amp;lt;/tt&amp;gt;-Funktion speichert die gegebenen Daten unter dem Schlüssel &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; in der &amp;lt;tt&amp;gt;HashTable&amp;lt;/tt&amp;gt;-Klasse:&lt;br /&gt;
     def __setitem__(self, key, value):&lt;br /&gt;
         index = hash(key) % self.capacity  # hash(...) ist in Python eine vordefinierte Funktion&lt;br /&gt;
         node  = self.array[index]          # finde die zu 'key' gehörende Liste&lt;br /&gt;
         while node is not None:            # sequentielle Suche nach 'key' in dieser Liste&lt;br /&gt;
             if node.key == key:&lt;br /&gt;
                 # Element 'key' ist schon in der Tabelle&lt;br /&gt;
                 # =&amp;gt; überschreibe die Daten mit dem neuen Wert&lt;br /&gt;
                 node.data = value&lt;br /&gt;
                 return&lt;br /&gt;
             # andernfalls: Kollision des Hashwerts, probiere nächsten 'key' aus&lt;br /&gt;
             node = node.next&lt;br /&gt;
         # kein Element hatte den richtigen Schlüssel.&lt;br /&gt;
         # =&amp;gt; es gibt diesen Schlüssel noch nicht&lt;br /&gt;
         #    füge also ein neues Element in die Hashtabelle ein&lt;br /&gt;
         self.array[index] = HashNode(key, value, self.array[index]) # der alte Anfang der Liste wird zum&lt;br /&gt;
                                                                     # Nachfolger des neu eingefügten ersten Elements&lt;br /&gt;
         self.size += 1&lt;br /&gt;
         ... # eventuell muss jetzt noch die Kapazität optimiert werden&lt;br /&gt;
Die Funktion &amp;lt;tt&amp;gt;__getitem__&amp;lt;/tt&amp;gt; gibt die unter dem Schlüssel &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; abgelegten Daten zurück, oder eine Fehlermeldung, falls dieser Schlüssel nicht existiert:&lt;br /&gt;
     def __getitem__(self, key):&lt;br /&gt;
         index = hash(key) % self.capacity&lt;br /&gt;
         node = self.array[index]     # finde die zu 'key' gehörende Liste&lt;br /&gt;
         while node is not None:      # sequentielle Suche nach 'key' in dieser Liste&lt;br /&gt;
              if node.key == key:     # gefunden!&lt;br /&gt;
                  return node.data    # =&amp;gt; Daten zurückgeben&lt;br /&gt;
              node = node.next        # nächsten Schlüssel probieren&lt;br /&gt;
         raise KeyError(key)          # Schlüssel nicht gefunden =&amp;gt; Fehler&lt;br /&gt;
&lt;br /&gt;
==== Komplexität der linearen Verkettung und Wahl der Kapazität ====&lt;br /&gt;
&lt;br /&gt;
Die Komplexität wird durch zwei Operationen bestimmt: erstens das Auffinden der zu einem Schlüssel gehörenden Liste (die in O(1) erfolgt), zweitens das sequentielle Durchsuchen der Liste, die Zeit in O(L) erfordert, wobei L die mittlere Länge der Listen ist. Die Hashtabelle ist also nur schnell, wenn die Länge der Listen möglichst klein ist. Unter der Annahme des ''uniformen Hashings'', wenn also alle Indizes gleich häufig verwendet werden, ist L gleich dem '''Füllstand''' der Hashtabelle:&lt;br /&gt;
:::&amp;lt;math&amp;gt;\alpha = \frac{N}{M} = \frac{\text{size}}{\text{capacity}}&amp;lt;/math&amp;gt; wobei N die Größe &amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt; der Hashtabelle und M die Größe &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; des Arrays ist.&lt;br /&gt;
Wenn die Hashwerte uniform sind, entfallen auf jede Liste im Mittel N/M Einträge (N Einträge, verteilt auf M Listen). Die Gesamtkomplexität berechnet sich nach der Sequenzregel zu&lt;br /&gt;
:::&amp;lt;math&amp;gt;O(1+\alpha)&amp;lt;/math&amp;gt;&lt;br /&gt;
Für eine effiziente Suche muss demnach &amp;lt;math&amp;gt;\alpha \in O(1)&amp;lt;/math&amp;gt; gewählt werden. Dies erreicht man, indem man, wie beim dynamischen Array, &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; immer wieder anpasst, falls &amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt; zu groß wird. Üblicherweise verdoppelt man &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt;, sobald &amp;lt;tt&amp;gt;size == capacity&amp;lt;/tt&amp;gt; erreicht wird. Analog zum dynamischen Array werden die Daten dann aus dem alten Array (&amp;lt;tt&amp;gt;self.array&amp;lt;/tt&amp;gt;) in ein entsprechend vergrößertes neues Array kopiert.&lt;br /&gt;
&lt;br /&gt;
In der C++ Standardbibliothek (Klasse &amp;lt;tt&amp;gt; [http://www.cplusplus.com/reference/stl/unordered_map/ std::unordered_map]&amp;lt;/tt&amp;gt;, siehe auch [http://gcc.gnu.org/viewcvs/trunk/libstdc%2B%2B-v3/src/shared/hashtable-aux.cc?view=markup GCC hashtable_aux.cc (Primzahlen)] und [http://gcc.gnu.org/viewcvs/trunk/libstdc%2B%2B-v3/include/bits/hashtable_policy.h?view=markup GCC Hash Implementation]) wird die Hashtabelle häufig so&lt;br /&gt;
implementiert. Dabei wird &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; immer als ''Primzahl'' gewählt, wobei sich aufeinanderfolgende Kapazitäten immer ungefähr verdoppeln. Dazu wählt man aus einer vorberechneten Liste von Primzahlen die kleinste Zahl, so dass &amp;lt;tT&amp;gt;new_capacity &amp;gt;= 2*capacity&amp;lt;/tt&amp;gt; gilt, und beginnt z.B. mit einer Default-Kapazität von 11:&lt;br /&gt;
  11, 23, 47, 97, 199, 409, 823, ...&lt;br /&gt;
Die Wahl von Primzahlen hat zur Folge, dass &amp;lt;tt&amp;gt;hash(key) % self.capacity&amp;lt;/tt&amp;gt; ''alle'' Bits von h benutzt (Eigenschaft aus der Zahlentheorie). Die Kapazität wird vergrößert, wenn &amp;lt;tt&amp;gt;size == capacity&amp;lt;/tt&amp;gt; erreicht wird, und die ungefähre Verdoppelung sichert, dass die amortisierte Komplexität der Einfügeoperation in O(1) ist (wie beim dynamischen Array).&lt;br /&gt;
&lt;br /&gt;
=== Hashtabelle mit offener Adressierung (geschlossenes Hashing) ===&lt;br /&gt;
[[Image:HASHTB12.svg.png|frame|Prinzip ([http://en.wikipedia.org/wiki/Hash_table Quelle])]]&lt;br /&gt;
&lt;br /&gt;
Dies kann als die optimistische Variante betrachtet werden: man nimmt an, dass Kollisionen nicht so häufig auftreten, um eine komplexe Datenstruktur wie das &amp;quot;Array von Listen&amp;quot; zu rechtfertigen. Stattdessen behandelt man Kollisionen mit einer einfachen '''Idee''': Wenn &amp;lt;tt&amp;gt;array[index]&amp;lt;/tt&amp;gt; durch Kollision bereits vergeben ist, probiere einen&lt;br /&gt;
anderen Index aus (siehe auch [http://de.wikipedia.org/wiki/Hashtabelle#Hashing_mit_offener_Adressierung Wikipedia (de)] und&lt;br /&gt;
[http://en.wikipedia.org/wiki/Open_addressing Wikipedia (en)]). Dabei muss man folgendes beachten:&lt;br /&gt;
&lt;br /&gt;
* Das Array enthält pro Element höchstens ein (key,value)-Paar&lt;br /&gt;
* Das Array muss stets mindestens ''einen'' freien Platz haben (sonst gäbe es beim Ausprobieren anderer Indizes eine Endlosschleife). Es gilt immer &amp;lt;tt&amp;gt;self.size &amp;lt; self.capacity&amp;lt;/tt&amp;gt;. Dies war bei der vorigen Hash-Implementation mit linearer Verkettung nicht notwendig (aber im Sinne schneller Zugriffszeiten trotzdem wünschenswert).&lt;br /&gt;
&lt;br /&gt;
==== Vorgehen bei Kollisionen ====&lt;br /&gt;
&lt;br /&gt;
=====Sequentielles Sondieren=====&lt;br /&gt;
&lt;br /&gt;
Probiere den nächsten Index: &amp;lt;tt&amp;gt;index = (index+1) % capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Vorteil: einfach&lt;br /&gt;
* Nachteil: Clusterbildung&lt;br /&gt;
&lt;br /&gt;
Clusterbildung heißt, dass sich größere zusammenhängende Bereiche bilden die belegt sind, unterbrochen von Bereichen die komplett frei sind. Beim Versuch des Einfügens eines Elements an einen Platz, der schon belegt ist, muss jetzt das ganze Cluster sequentiell durchlaufen werden, bis ein freier Platz gefunden wird. Damit entspricht die Komplexität der Suche der mittleren Länge der belegten Bereiche, was sich entsprechend in einer langsamen Suche widerspiegelt.&lt;br /&gt;
&lt;br /&gt;
=====Doppeltes Hashing=====&lt;br /&gt;
&lt;br /&gt;
[http://de.wikipedia.org/wiki/Doppel-Hashing Wikipedia (de)] [http://en.wikipedia.org/wiki/Double_hashing Wikipedia (en)]&lt;br /&gt;
&lt;br /&gt;
Bestimme einen neuen Index (bei Kollisionen) durch eine ''2. Hashfunktion''.&lt;br /&gt;
&lt;br /&gt;
Das doppelte Hashing wird typischerweise in der Praxis angewendet und liegt auch der Python Implementierung des Datentyps [http://docs.python.org/tut/node7.html#SECTION007500000000000000000 Dictionary] (Syntax &amp;lt;tt&amp;gt;{'a':1, 'b':2, 'c':3}&amp;lt;/tt&amp;gt; zugrunde.&lt;br /&gt;
&lt;br /&gt;
Eine effiziente Implementierung dieses Datentyps ist für die Performance der Skriptsprache Python extrem wichtig, da z.B. beim Aufruf einer Funktion der auszuführunde Code in einem Dictionary unter dem Schlüssel ''Funktionsname'' nachgeschlagen wird oder die Werte lokaler Variablen innerhalb einer Funktion ebenfalls in einem Dictionary zu finden sind.&lt;br /&gt;
&lt;br /&gt;
Für die Implementierung in Python werden wieder die obigen Klassen &amp;lt;tt&amp;gt;HashNode&amp;lt;/tt&amp;gt; (das Attribut &amp;lt;tt&amp;gt;next&amp;lt;/tt&amp;gt; kann allerdings jetzt entfernt werden) und &amp;lt;tt&amp;gt;HashTable&amp;lt;/tt&amp;gt; benötigt, es folgen die angepassten Implementationen von &amp;lt;tt&amp;gt;__setitem__&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;__getitem__&amp;lt;/tt&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
  def __setitem__(self, key, value):&lt;br /&gt;
      h = hash(key)&lt;br /&gt;
      index = h % self.capacity&lt;br /&gt;
      # erste Schleife: teste, ob key schon vorhanden ist&lt;br /&gt;
      while True:&lt;br /&gt;
          if self.array[index] is None:     # freies Feld gefunden =&amp;gt; key nicht vorhanden&lt;br /&gt;
               break&lt;br /&gt;
          if self.array[index].key == key:  # key gefunden =&amp;gt; Daten aktualisieren&lt;br /&gt;
               self.array[index].data = value&lt;br /&gt;
               return&lt;br /&gt;
          # self.array[index].key ist anderer Schlüssel oder als gelöscht markiert&lt;br /&gt;
          # =&amp;gt; neuen Index durch 2. Hashfunktion berechnen&lt;br /&gt;
          index = (5*index+1+h) % self.capacity&lt;br /&gt;
          h = h // 32&lt;br /&gt;
      # wenn wir hier landen, wurde key nicht gefunden&lt;br /&gt;
      h = hash(key)&lt;br /&gt;
      index = h % self.capacity&lt;br /&gt;
      # zweite Schleife: neues Element einfügen&lt;br /&gt;
      while True:&lt;br /&gt;
          if self.array[index] is None or self.array[index].key is None:&lt;br /&gt;
               # index ist frei (1. Bedingung) oder als gelöscht markiert (2. Bedingung)&lt;br /&gt;
               # =&amp;gt; hier gehört key hin&lt;br /&gt;
               self.array[index] = HashNode(key, value)&lt;br /&gt;
               self.size +=1&lt;br /&gt;
               ... # eventuell muss hier die Kapazität optimiert werden&lt;br /&gt;
               return&lt;br /&gt;
          # index ist schon belegt =&amp;gt; neuen Index durch 2. Hashfunktion berechnen&lt;br /&gt;
          index = (5*index+1+h) % self.capacity&lt;br /&gt;
          h = h // 32&lt;br /&gt;
&lt;br /&gt;
Wir nehmen bei dieser Implementation an, dass gelöschte Elemente dadurch markiert werden, dass &amp;lt;tt&amp;gt;self.array[index].key&amp;lt;/tt&amp;gt; auf einen Schlüssel gesetzt wird, der sonst nicht vorkommen kann (z.B. &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;). Dann wird die if-Abfrage &amp;lt;tt&amp;gt;self.array[index].key == key&amp;lt;/tt&amp;gt; niemals wahr, und es wird weitergesucht. Würde man hingegen das Element vollständig löschen, könnte die Bedingung &amp;lt;tt&amp;gt;self.array[index] is None&amp;lt;/tt&amp;gt; zu früh wahr werden, so dass die Schleife vorzeitig abgebrochen und das vorhandene Element für &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; nicht erreicht würde.&lt;br /&gt;
 &lt;br /&gt;
 def __getitem__(self, key):&lt;br /&gt;
     h = hash(key)&lt;br /&gt;
     index = h % self.capacity&lt;br /&gt;
     while True:&lt;br /&gt;
         if self.array[index] is None:    # die Suchkette bricht ab =&amp;gt; key existiert nicht&lt;br /&gt;
             raise KeyError(key)&lt;br /&gt;
         if self.array[index].key == key: # key gefunden =&amp;gt; zugehörige Daten zurückgeben&lt;br /&gt;
             return self.array[index].data&lt;br /&gt;
         # index enthält nicht den passenden kay =&amp;gt; neuen Index durch 2. Hashfunktion berechnen&lt;br /&gt;
         index = (5*index+1+h) % self.capacity&lt;br /&gt;
         h = h // 32&lt;br /&gt;
&lt;br /&gt;
Die vorgestellte Implementierung orientiert sich an Pythons interner Dictionary Implementierung, der zugehörige Quellcode (mit ausführlichem Kommentar) findet sich im File [https://github.com/python/cpython/blob/master/Objects/dictobject.c dictobject.c] der Python Implementation.&lt;br /&gt;
&lt;br /&gt;
===== Beispiel für doppeltes Hashing =====&lt;br /&gt;
&lt;br /&gt;
Der Übersichtlichkeit wegen wählen wir M'=2&amp;lt;sup&amp;gt;5&amp;lt;/sup&amp;gt; (statt 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;) und eine Kapazität von M=8.&lt;br /&gt;
&lt;br /&gt;
Roher Hashwert (für das Beispiel willkürlich gewählt):&lt;br /&gt;
  h=25&lt;br /&gt;
Erster Index:&lt;br /&gt;
  i0 = h % capacity = 25 % 8 = 1&lt;br /&gt;
Es finde eine Kollision statt. Es wird ein zweiter Index berechnet:&lt;br /&gt;
  i1 = (5*i0 + 1 + h) % 8 = (5*1 + 1 + 25) % 8 = 31 % 8 = 7&lt;br /&gt;
Der Hashwert wird aktualisiert um die höherwertigen Bits von &amp;lt;tt&amp;gt;h&amp;lt;/tt&amp;gt; ins Spiel zu bringen (hier durch &amp;lt;tt&amp;gt;h &amp;gt;&amp;gt; 2&amp;lt;/tt&amp;gt; anstelle von &amp;lt;tt&amp;gt;h &amp;gt;&amp;gt; 5&amp;lt;/tt&amp;gt; im originalen Pythoncode). Wir stellen &amp;lt;tt&amp;gt;h&amp;lt;/tt&amp;gt; als Binärzahl dar, damit der Rechtsshift besser sichtbar wird:&lt;br /&gt;
  h = h &amp;gt;&amp;gt; 2 &lt;br /&gt;
  ==&amp;gt; h = (11001 &amp;gt;&amp;gt; 2) = 00110 = 6&lt;br /&gt;
Es finde wieder eine Kollision statt, so dass ein dritter Index berechnet werden muss.&lt;br /&gt;
  i2 = (5*i1 + 1 + h) % 8 = (5*7 + 1 + 6) % 8 = 42 % 8 = 2&lt;br /&gt;
Der Hashwert wird wiederum aktualisiert:&lt;br /&gt;
  h = h &amp;gt;&amp;gt; 2&lt;br /&gt;
  ==&amp;gt; h = (00110 &amp;gt;&amp;gt; 2) = 00001 = 1&lt;br /&gt;
Es finde eine Kollision statt, und wir berechnen den vierten Index:&lt;br /&gt;
  i3 = (5*i2 + 1 + h) % 8 = (5*2 + 1 + 1) % 8 = 12 % 8 = 4&lt;br /&gt;
Der Hashwert wird nochmals aktualisiert und erreicht jetzt den Wert 0 (der sich dann nicht mehr ändert):&lt;br /&gt;
  h = h &amp;gt;&amp;gt; 2 &lt;br /&gt;
  ==&amp;gt; h = (00110 &amp;gt;&amp;gt; 2) = 0&lt;br /&gt;
Es finde eine Kollision statt. Da jetzt &amp;lt;tt&amp;gt;h = 0&amp;lt;/tt&amp;gt; gilt, und die Zahlen 5 (Multiplikator) und 8 (capacity) teilerfremd sind, werden ab jetzt systematisch alle Indizes von 0 bis 7 durchprobiert (in der durch die Modulo-Operation bestimmten Reihenfolge):&lt;br /&gt;
  i4  = (5*i3  + 1 + h) % 8 = (5*4 + 1 + 0) % 8 = 21 % 8 = 5&lt;br /&gt;
  i5  = (5*i4  + 1 + h) % 8 = (5*5 + 1 + 0) % 8 = 26 % 8 = 2&lt;br /&gt;
  i6  = (5*i5  + 1 + h) % 8 = (5*2 + 1 + 0) % 8 = 11 &amp;amp; 8 = 3&lt;br /&gt;
  i7  = (5*i6  + 1 + h) % 8 = (5*3 + 1 + 0) % 8 = 16 &amp;amp; 8 = 0&lt;br /&gt;
  i8  = (5*i7  + 1 + h) % 8 = (5*0 + 1 + 0) % 8 =  1 &amp;amp; 8 = 1&lt;br /&gt;
  i9  = (5*i8  + 1 + h) % 8 = (5*1 + 1 + 0) % 8 =  6 &amp;amp; 8 = 6&lt;br /&gt;
  i10 = (5*i9  + 1 + h) % 8 = (5*6 + 1 + 0) % 8 = 31 &amp;amp; 8 = 7&lt;br /&gt;
  i11 = (5*i10 + 1 + h) % 8 = (5*7 + 1 + 0) % 8 = 36 &amp;amp; 8 = 4&lt;br /&gt;
Allen Indizes werden also erreicht, bevor sich die Folge wiederholt. Da man &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; immer so wählt, dass mindestens ein Arrayfeld noch frei ist, wird dadurch immer ein geeigneter Platz für das einzufügende Element gefunden.&lt;br /&gt;
&lt;br /&gt;
==== Komplexität der offenen Adressierung ====&lt;br /&gt;
&lt;br /&gt;
* Annahme: uniformes Hashing, das heißt alle Indizes haben gleiche Wahrscheinlichkeit&lt;br /&gt;
* Füllstand &amp;lt;math&amp;gt;\alpha =\frac{N}{M} = \frac{\text{size}}{\text{capacity}}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* '''Erfolglose Suche''' (d.h. es wird entweder ein neues Element eingefügt oder ein &amp;lt;tt&amp;gt;KeyError&amp;lt;/tt&amp;gt; geworfen): Untere Schranke für die Komplexität ist &amp;lt;math&amp;gt;\Omega\left(\frac{1}{1-\alpha}\right)&amp;lt;/math&amp;gt; Schritte (= Anzahl der notwendigen index-Berechnungen).&lt;br /&gt;
* '''Erfolgreiche Suche''' &amp;lt;math&amp;gt;\Omega\left(\frac{1}{\alpha}\ln\left(\frac{1} {1-\alpha}\right)\right)&amp;lt;/math&amp;gt; Schritte.&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;
! &amp;lt;math&amp;gt;\alpha&amp;lt;/math&amp;gt;&lt;br /&gt;
! 0.5&lt;br /&gt;
! 0.9&lt;br /&gt;
|- &lt;br /&gt;
| erfolglos&lt;br /&gt;
| 2.0&lt;br /&gt;
| 10&lt;br /&gt;
|-&lt;br /&gt;
| erfolgreich&lt;br /&gt;
| 1.4&lt;br /&gt;
| 2.6&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Wahl der Kapazität ====&lt;br /&gt;
Man sieht an der obigen Tabelle, dass die erfolglose Suche (und damit das Einfügen) sehr langsam wird, wenn der Füllstand hoch ist. In Python wird &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; deshalb so gewählt, dass &amp;lt;math&amp;gt;\alpha \leq 2/3&amp;lt;/math&amp;gt;. Falls &amp;lt;math&amp;gt;\alpha&amp;lt;/math&amp;gt; größer werden sollte, verdopple die Kapazität und kopiere das alte array in das neue Array (analog zum dynamischen Array)&lt;br /&gt;
&lt;br /&gt;
In Python werden die Kapazitätsgrößen als Zweierpotenzen gewählt, also 4,8,16,32,...,&lt;br /&gt;
so dass &amp;lt;tt&amp;gt;h % self.capacity&amp;lt;/tt&amp;gt; nur die unteren Bits von &amp;lt;tt&amp;gt;h&amp;lt;/tt&amp;gt; benutzt. Die oberen Bits von &amp;lt;tt&amp;gt;h&amp;lt;/tt&amp;gt; kommen erst ins Spiel, wenn bei der Berechnung der 2. Hashfunktion die Aktualisierung &amp;lt;tt&amp;gt;h = h &amp;gt;&amp;gt; 5&amp;lt;/tt&amp;gt; erfolgt. Dies hat sich bei umfangreichen Experimenten als sehr gute Lösung erwiesen.&lt;br /&gt;
&lt;br /&gt;
== Anwendungen von Hashing ==&lt;br /&gt;
&lt;br /&gt;
* Hashtabelle, assoziatives Aray&lt;br /&gt;
* Sortieren in linearer Zeit (Übungsaufgabe 6.2)&lt;br /&gt;
* Suchen von Strings in Texten: Rabin-Karp-Algorithmus&lt;br /&gt;
* ...&lt;br /&gt;
&lt;br /&gt;
=== Rabin Karp Algorithmus ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Rabin-Karp-Algorithmus Wikipedia (de)] [http://en.wikipedia.org/wiki/Rabin-Karp_string_search_algorithm Wikipedia (en)]&lt;br /&gt;
&lt;br /&gt;
In Textverarbeitungsanwendungen ist eine häufig benutzte Funktion die ''Search &amp;amp; Replace'' Funktionalität. Die Suche sollte in O(len(text)) möglich sein, aber ein naiver Algorithmus braucht O(len(text)*len(searchstring))&lt;br /&gt;
&lt;br /&gt;
==== Naive Implementierung der Textsuche ====&lt;br /&gt;
  def search(text, s):&lt;br /&gt;
      M, N = len(text), len(s)&lt;br /&gt;
      for k in range(M-N):&lt;br /&gt;
          if s==text[k:k+N]:   # O(N), da N Zeichen verglichen werden müssen&lt;br /&gt;
              return k&lt;br /&gt;
      return -1 #nicht gefunden&lt;br /&gt;
&lt;br /&gt;
==== Idee des Rabin Karp Algorithmus ====&lt;br /&gt;
Statt Vergleichen &amp;lt;tt&amp;gt;s==text[k:k+N]&amp;lt;/tt&amp;gt;, die O(N) benötigen, weil N Vergleiche der Buchstaben durchgeführt werden müssen, vergleichen wir die Hashs von Suchstring und dem zu untersuchenden Textabschnitt: &amp;lt;tt&amp;gt;hash(s) == hash(text[k:k+N])&amp;lt;/tt&amp;gt;. Dabei muss natürlich &amp;lt;tt&amp;gt;hash(s)&amp;lt;/tt&amp;gt; nur einmal berechnet werden, wohingegen &amp;lt;tt&amp;gt;hash(text[k:k+N])&amp;lt;/tt&amp;gt; immer wieder neu berechnet werden muss. Damit der Vergleich O(1) sein kann, ist es deswegen erforderlich, eine solche Hashfunktion zu haben, die nicht alle Zeichen (das wäre O(N) ) einlesen muss, sondern die den vorhergehenden Hashwert mit einbezieht.&lt;br /&gt;
&lt;br /&gt;
Eine solche Hashfunktion heißt ''Running Hash'' und funktioniert analog zum ''Sliding Mean''.&lt;br /&gt;
&lt;br /&gt;
Die Running Hash Funktion berechnet in O(1) den hash von &amp;lt;tt&amp;gt;text[k+1:k+1+N]&amp;lt;/tt&amp;gt; ausgehend vom hash für &amp;lt;tt&amp;gt;text[k:k+N]&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Idee: Interpretiere den Text als Ziffern in einer base d Darstellung:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;h_k = \text{text}[k]\cdot d^{N-1} + \text{text}[k+1]\cdot d^{N-2} + \cdots + \text{text}[k+N-1]&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für die Basis 10 (Dezimalsystem) ergibt sich also&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;h_k = \text{text}[k]\cdot {10}^{N-1} + \text{text}[k+1]\cdot {10}^{N-2} + \cdots + \text{text}[k+N-1]&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Daraus folgt&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;h_{k+1} = 10 \cdot h_k - \text {text}[k]\cdot {10}^{N} + \text {text}[k+N]&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexität dieses Updates ist O(1), falls man &amp;lt;math&amp;gt;{10}^{N}&amp;lt;/math&amp;gt; vorberechnet hat.&lt;br /&gt;
&lt;br /&gt;
In der Realität wählt man dann d=32 und benutzt noch an einigen Stellen modulo Operationen, um die Zahlen nicht zu groß werden zu lassen (siehe die Zahl &amp;lt;tt&amp;gt;q&amp;lt;/tt&amp;gt; in der folgenden Implementation).&lt;br /&gt;
&lt;br /&gt;
==== Implementation ====&lt;br /&gt;
  def searchRabinKarp(text, s):&lt;br /&gt;
      M, N = len(text), len(s)&lt;br /&gt;
      d = 32&lt;br /&gt;
      q = 33554393  # q ist eine große Primzahl, aber so,&lt;br /&gt;
                    # dass d*q &amp;lt; 2**32 (um Überlauf bei 32-bit Integerarithmetik zu vermeiden)&lt;br /&gt;
      dN = d**N % q # Vorberechnung des Vorfaktors für das Entfernen aus dem Hash&lt;br /&gt;
      &lt;br /&gt;
      # Initialisierung      &lt;br /&gt;
      hs, ht = 0, 0&lt;br /&gt;
      for k in range(N):&lt;br /&gt;
          # ord() gibt die Zeichen-Nummer (z.B. ASCII- oder UTF-8-Code) des &lt;br /&gt;
          # übergebenen Zeichens zurück&lt;br /&gt;
          hs = (hs*d + ord( s[k] )) % q&lt;br /&gt;
          ht = (ht*d + ord(text[k])) % q&lt;br /&gt;
      # Die Variablen sind jetzt wie folgt initialisiert:&lt;br /&gt;
      # hs = hash(s)&lt;br /&gt;
      # ht = hash(text[0:N])&lt;br /&gt;
      &lt;br /&gt;
      # Hauptschleife&lt;br /&gt;
      k = 0 &lt;br /&gt;
      while k &amp;lt; M-N:&lt;br /&gt;
          if hs == ht:  # übereinstimmende Hashs =&amp;gt; prüfe, dass es nicht nur&lt;br /&gt;
                        # eine Kollision ist&lt;br /&gt;
              if s == text[k:k+N]:  # O(N)-Vergleich nur nötig, wenn Hashs übereinstimmen&lt;br /&gt;
                   return k         # search string an Position k gefunden&lt;br /&gt;
          # nicht gefunden =&amp;gt; aktualisiere Hash für den nächsten Teilabschnitt von text:&lt;br /&gt;
          ht = (d*ht + ord(text[k+N])) % q # neues Zeichen text[k+N] in Hash einfügen&lt;br /&gt;
          ht = (ht - dN*ord(text[k])) % q  # Zeichen text[k] aus Hash entfernen.&lt;br /&gt;
          k +=1&lt;br /&gt;
      return -1  # search string nicht gefunden&lt;br /&gt;
&lt;br /&gt;
[[Iteration versus Rekursion|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Hashing_und_Hashtabellen&amp;diff=5702</id>
		<title>Hashing und Hashtabellen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Hashing_und_Hashtabellen&amp;diff=5702"/>
				<updated>2020-06-23T14:08:35Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Doppeltes Hashing */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Die Mitschrift gibts auch als [http://hci.iwr.uni-heidelberg.de/alda/images/AlDa.pdf PDF].&lt;br /&gt;
== Hashing ==&lt;br /&gt;
&lt;br /&gt;
Wir haben im Abschnitt [[Assoziative Arrays]] gezeigt, dass man assoziative Arrays effizient mit Hilfe von Suchbäumen realisieren kann, so dass die Zugriffszeit auf ein Element in O(log(len(a))) ist. Genau wie beim Sortierproblem stellt sich jetzt die Frage, ob die Zugriffszeit noch verbessert werden kann, idealerseise auf O(1) wie beim gewöhnlichen Array. Die Antwort lautet: Ja, wenn für die Schlüssel eine Hashfunktion definiert ist.&lt;br /&gt;
&lt;br /&gt;
===Hashfunktionen===&lt;br /&gt;
&lt;br /&gt;
Hashfunktionen sind eine weitere Anwendung des [[Sortieren in linearer Zeit#Bucket-Prinzip|Bucket-Prinzips]], das wir im Zusammenhang mit dem Sortieren in linearer Zeit eingeführt haben. man bildet die Schlüssel wiederum auf Bucket-Indizes ab, um die Suche zu beschleunigen (von O(log N) nach O(1)). Im Unterschied zum Sortieren verzichtet man hier allerdings darauf, dass die Abbildung auf Bucket-Indizes die Ordnung der Schlüssel erhalten muss (es muss nicht einmal eine Ordnung definiert sein), weil diese Forderung es erschwert, die Schlüssel gleichmäßig auf die Buckets zu verteilen. Letzteres ist aber bei Hashtabellen extrem wichtig.&lt;br /&gt;
&lt;br /&gt;
Gegeben sei ein Universum U, dass die Menge aller legalen Schlüssel darstellt. Die Mächtigkeit |U| der Menge U ist im allgemeinen sehr groß. Beispielsweise kann man mit Strings der Länge 9 bis zu 27&amp;lt;sup&amp;gt;9&amp;lt;/sup&amp;gt;&amp;amp;asymp;10&amp;lt;sup&amp;gt;13&amp;lt;/sup&amp;gt;&amp;amp;asymp;2&amp;lt;sup&amp;gt;43&amp;lt;/sup&amp;gt; verschiedene Schlüssel generieren, wenn 27 Zeichen erlaubt sind (Kleinbuchstaben und Leerzeichen). Die Grundannahme von Hashing ist jetzt, dass in jeder gegebenen Anwendung nur ein (kleiner) Teil der erlaubten Schlüssel tatsächlich verwendet wird. Man definiert eine Hashfunktion, die jeden Schlüssel auf eine natürliche Zahl im Bereich 0...(M-1) abbildet, wobei M viel kleiner als |U| ist. &lt;br /&gt;
;Definition einer Hashfunktion: &lt;br /&gt;
:::&amp;lt;math&amp;gt; f: U  \rightarrow [0, 1, \ldots, M-1] \subset \mathbb{N} &amp;lt;/math&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt; f(u \in U) = h \in [0, 1, \ldots, M-1]&amp;lt;/math&amp;gt;&lt;br /&gt;
h wird als ''Hashwert'' von u bezeichnet. Da M &amp;lt; |U|, werden notwendigerweise einige Schlüssel auf dieselbe Zahl abgebildet. Man bezeichnet den Fall &amp;lt;math&amp;gt; f(u_1 \in U) = f(u_2 \in U) &amp;lt;/math&amp;gt; als ''Kollision'' zwischen den Schlüsseln u&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; und u&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die '''Aufgabe''' besteht jetzt darin, ein Hash-Funktion zu entwerfen, die möglichst wenige Kollisionen hat. Hashfunktionen ähneln damit einem Zufallszahlengenerator, weil jede Zahl &amp;lt;math&amp;gt; h \in 0 \ldots (M-1) &amp;lt;/math&amp;gt; nach Möglichkeit mit gleicher Wahrscheinlichkeit herauskommen soll. Wird dieses Ziel erreicht, spricht man vom ''uniformen Hashing''.&lt;br /&gt;
&lt;br /&gt;
In der Regel ist aber nicht vorher bekannt, welche Schlüssel in einer Anwendung verwendet werden. Es kann deshalb immer vorkommen, dass die verwendete Schlüsselmenge sehr viele Kollisionen verursacht. Man sieht in der Tat leicht ein, dass für jede gegebene Hashfunktion ungünstige Schlüsselmengen &amp;lt;math&amp;gt; U_f \subset U&amp;lt;/math&amp;gt; existieren, bei denen es sehr viele Kollisionen gibt. Im ungünstigsten Fall könnte U&amp;lt;sub&amp;gt;f&amp;lt;/sub&amp;gt; so gewählt sein, dass f(U&amp;lt;sub&amp;gt;f&amp;lt;/sub&amp;gt;) = k = const. gilt. Ein Hacker, der die verwendete Hashfunktion kennt, kann z.B. U&amp;lt;sub&amp;gt;f&amp;lt;/sub&amp;gt; absichtlich so wählen, um eine denial-of-service-Attacke gegen einen hash-basierten Webservice zu starten. Ein anderes anschauliches Beispiel wäre eine Party, zu der nur Leute eingeladen werden, die an einem 8ten im Monat Geburtstag haben. Auf dieser Party ist es viel wahrscheinlicher, Leute zu finden, die am selben (oder gleichen) Tag Geburtstag haben, als wenn man alle einlädt.&lt;br /&gt;
&lt;br /&gt;
D.h. die Wahl einer guten Hashfunktion ist eine Kunst, und man muss (wenn möglich) die Daten analysieren um ein gutes f zu finden.&lt;br /&gt;
&lt;br /&gt;
====Perfektes Hashing====&lt;br /&gt;
&lt;br /&gt;
Kennt man die Untermenge der tatsächlich vorkommenden Schlüssel &amp;lt;math&amp;gt;U_f \subset U&amp;lt;/math&amp;gt; schon im voraus, hat man die Möglichkeit, eine ''perfekte Hashfunktion'' ohne Kollisionen zu entwerfen.&lt;br /&gt;
&lt;br /&gt;
;Beispiel anhand der Monatsnamen &lt;br /&gt;
&lt;br /&gt;
U ist in diesem Fall eine Menge von Strings der Länge 9 (weil der September als längster Monatsname 9 Zeichen hat). Es ergeben sich also &amp;lt;math&amp;gt;60^{9}&amp;lt;/math&amp;gt;&amp;gt;&amp;amp;asymp;10&amp;lt;sup&amp;gt;16&amp;lt;/sup&amp;gt;&amp;amp;asymp;2&amp;lt;sup&amp;gt;54&amp;lt;/sup&amp;gt; mögliche Strings, da mit Groß- und Kleinbuchstaben, Umlauten, ß und Leerzeichen 60 Zeichen im deutschen Alphabet vorhanden sind. Von all diesen Möglichkeiten werden genau 12 benutzt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;U_f&amp;lt;/math&amp;gt; = {&amp;quot;Januar&amp;quot;; &amp;quot;Februar&amp;quot;; ... ; &amp;quot;Dezember&amp;quot;}&lt;br /&gt;
* Benutzt man nun als Hashfunktion die Anfangsbuchstaben der Monatsnamen, benötigt man dafür 6 bit. M ist somit 64. &lt;br /&gt;
:::{&amp;quot;Januar&amp;quot;; &amp;quot;Februar&amp;quot;; ... ; &amp;quot;Dezember&amp;quot;} &amp;amp;rarr; {&amp;quot;J&amp;quot;; &amp;quot;F&amp;quot;; &amp;quot;M&amp;quot;; &amp;quot;A&amp;quot;; &amp;quot;M&amp;quot;; &amp;quot;J&amp;quot;; &amp;quot;J&amp;quot;; &amp;quot;A&amp;quot;; &amp;quot;S&amp;quot;; &amp;quot;O&amp;quot;; &amp;quot;N&amp;quot;; &amp;quot;D&amp;quot;}&lt;br /&gt;
:Dabei enstehen viele Kollisionen (J wird 3x verwendet, M 2x, A 2x), die gewählte ist also keine gute Hashfunktion&lt;br /&gt;
* Benutzt man als Hashfunktion die ersten 3 Buchstaben benötigt man 18 bit, M = &amp;lt;math&amp;gt;2^{18}&amp;lt;/math&amp;gt; &lt;br /&gt;
:::{&amp;quot;Januar&amp;quot;; &amp;quot;Februar&amp;quot;; ... ; &amp;quot;Dezember&amp;quot;} &amp;amp;rarr; {&amp;quot;Jan&amp;quot;, &amp;quot;Feb&amp;quot;, &amp;quot;März&amp;quot;, &amp;quot;Apr&amp;quot;, &amp;quot;Mai&amp;quot;, &amp;quot;Jun&amp;quot;, &amp;quot;Jul&amp;quot;, &amp;quot;Aug&amp;quot;, &amp;quot;Sep&amp;quot;, &amp;quot;Okt&amp;quot;, &amp;quot;Nov&amp;quot;, &amp;quot;Dez&amp;quot;}&lt;br /&gt;
:Nun entstehen keine Kollision mehr. Diese Hashfunktion ist deshalb beim Ausfüllen von Formularen und dergleichen sehr beliebt. Dafür ist M aber recht groß.&lt;br /&gt;
&lt;br /&gt;
Die Aufgabe wird also präzisiert: man sucht für &amp;lt;math&amp;gt;U_f&amp;lt;/math&amp;gt; eine '''minimale, perfekte Hashfunktion''', für die &amp;lt;math&amp;gt;|U_f| = M&amp;lt;/math&amp;gt; gilt. Ein Verfahren hierfür ist Gegenstand von Übungsblatt 9.&lt;br /&gt;
&lt;br /&gt;
====Universelles Hashing====&lt;br /&gt;
&lt;br /&gt;
Hier wählt man für eine gegebene Hashtabelle die Hashfunktion per Zufallszahl aus einer (großen) Menge erlaubter Hashfunktion &amp;amp;rarr; Die Wahrscheinlichkeit, dass die Hashfunktion für die Schlüssel ungünstig ist, wird dadruch minimiert. Die oben erwähnte denial-of-service-Attacke ist jetzt nicht mehr möglich, weil kein Hacker die Hashfunktion im voraus kennen kann. Näheres zum universellen Hashing finden Sie in der [http://en.wikipedia.org/wiki/Universal_hashing Wikpedia].&lt;br /&gt;
&lt;br /&gt;
====Kryptographische Hashfunktionen====&lt;br /&gt;
&lt;br /&gt;
In kryptographischen Anwendungen treten neben dem Hauptziel, die Größe des Universums auf eine überschaubare Zahl von Integer-Werten zu reduzieren, zwei weitere Anforderungen, die für Verschlüsselung bzw. verschlüsselte Kommunikation wichtig sind: erstens will man Kollisionen unbedingt vermeiden (damit zwei verschiedene Dokumente oder Passwörter nicht auf den gleichen Hashwert abgebildet werden), und zweitens darf es nicht möglich sein, aus dem Hashwert die urpsrüngliche Nachricht (also das Dokument oder Passwort) zu rekonstruieren. Man wählt deshalb relative große M (128 bit und mehr) sowie spezielle, für diesen Zweck optimierte Hashfunktionen, wie z.B. [http://de.wikipedia.org/wiki/Message-Digest_Algorithm_5 md5] und [http://de.wikipedia.org/wiki/SHA1 sha1]. Weitere Einzelheiten finden Sie in der [http://en.wikipedia.org/wiki/Cryptographic_hash_function Wikipedia].&lt;br /&gt;
&lt;br /&gt;
====Beliebte Standard-Hashfunktionen====&lt;br /&gt;
&lt;br /&gt;
In der Praxis definiert man Hashfunktionen gewöhnlich zweistufig: Zunächst bildet man den Schlüssel auf einen 32 bit Integerwert ab, M' ist damit 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;. Dieser &amp;quot;rohe&amp;quot; Hashwert wird dann mittels der Modulo-Operation auf die eigentliche Größe M des assoziativen Arrays abgebildet:&lt;br /&gt;
:::&amp;lt;math&amp;gt; f(u \in U) = f'(u \in U)\,\%\,M\,=\,h \in [0, 1, \ldots, M-1] &amp;lt;/math&amp;gt;&lt;br /&gt;
mit&lt;br /&gt;
:::&amp;lt;math&amp;gt; f'(u \in U) = h' \in [0, 1, \ldots, 2^{32}-1] &amp;lt;/math&amp;gt;&lt;br /&gt;
Der große Wert von M' sichert, dass man bei der Wahl von M großen Spielraum hat, so dass die Größe des assoziativen Arrays sehr gut an die Menge der zu speichernden Daten angepaßt werden kann. Die Funktion f'(u) definiert man wie folgt:&lt;br /&gt;
* Falls U = &amp;lt;tt&amp;gt;unsigned int&amp;lt;/tt&amp;gt; (32bit int Datentyp) &amp;amp;rArr; f'(u) = u&lt;br /&gt;
* Falls U = &amp;lt;tt&amp;gt;signed int&amp;lt;/tt&amp;gt; &amp;amp;rArr; Typkonvertierung nach &amp;lt;tt&amp;gt;unsigned int&amp;lt;/tt&amp;gt; &amp;amp;rArr; f'(u) = (unsigned int)u&lt;br /&gt;
* Andere Schlüsseltypen (also insbesondere Strings) interpretiert man als Array of byte &amp;amp;rArr; f'(u) konvertiert Array of Byte nach &amp;lt;tt&amp;gt;unsigned int&amp;lt;/tt&amp;gt;. Beispiele für solche Funktionen:&lt;br /&gt;
:: '''Bernsteinfunktion:'''&lt;br /&gt;
     def bHash(u):     # u: Array of Byte&lt;br /&gt;
         h=0&lt;br /&gt;
         for k in u:&lt;br /&gt;
             h = 33 * h + k &lt;br /&gt;
         return h&lt;br /&gt;
:: '''modifizierte Bernsteinfunktion:'''&lt;br /&gt;
     def mbHash(u):    # u: Array of Byte&lt;br /&gt;
         h=0&lt;br /&gt;
         for k in u:&lt;br /&gt;
             h = (33 * h) ^ k  # ^ ist bitweises Xor&lt;br /&gt;
         return h&lt;br /&gt;
:: '''Shift-Add-Xor-Funktion:'''&lt;br /&gt;
     def saxhash(u):   # u: Array of Byte&lt;br /&gt;
         h=0&lt;br /&gt;
         for k in u:&lt;br /&gt;
             h ^= (h &amp;lt;&amp;lt; 5) + (h &amp;gt;&amp;gt; 2) + k  # &amp;lt;&amp;lt; und &amp;gt;&amp;gt; sind Links- bzw. Rechtsshift der Bits, ^= ist bitweise Xor-Zuweisung&lt;br /&gt;
         return h&lt;br /&gt;
:: '''Fowler/Noll/Vo-Funktion:'''&lt;br /&gt;
     def FNVhash(u):   # u: Array of Byte&lt;br /&gt;
         h = 2166136261&lt;br /&gt;
         for k in u:&lt;br /&gt;
             h = (16777619 * h) ^ k   # ähnlich der modifizierten Bernsteinfunktion, aber mit anderen Konstanten&lt;br /&gt;
         return h&lt;br /&gt;
:: Die verwendeten Konstanten sind experimentell so gewählt worden, dass die Hashfunktionen in typischen Praxisanwendungen relativ wenige Kollisionen verursachen. Der tiefere Grund, warum z.B. 33 in der Bernsteinfunktion eine gute Wahl darstellt, ist unbekannt. Es empfielt sich, in einer gegebenen Anwendung mit mehreren Hashfunktionen zu experimentieren. Weitere solche Funktionen und andere nützliche Informationen findet man auf der Seite [http://www.eternallyconfuzzled.com/tuts/algorithms/jsw_tut_hashing.aspx eternallyconfuzzled.com].&lt;br /&gt;
&lt;br /&gt;
== Hashtabellen ==&lt;br /&gt;
&lt;br /&gt;
Eine Hashtabelle ist eine Datenstruktur, die die Funktionalität des assoziativen Arrays mit Hilfe von Hashing realisiert. Das Grundprinzip besteht darin, dass die Hashtabelle intern ein (dynamisches) Array der Größe &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; verwaltet, so dass die Hashwerte als Indizes in diesem Array verwendet werden können (&amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; entspricht der Zahl M aus der mathematischen Definition oben). Eine naive Implementation der Einfügeoperation sieht also so aus &lt;br /&gt;
     def __setitem__(self, key, value):    # naive Implementation, funktioniert so nicht&lt;br /&gt;
         index = self.hash(key) % self.capacity&lt;br /&gt;
         self.array[index] = value&lt;br /&gt;
Diese Implementation ist allerdings zu einfach. Wenn nämlich die Schlüssel aus dem Universum U beliebig gewählt werden dürfen, sind Kollisionen unvermeidlich. Tritt aber eine Kollision auf, werden die Daten eines Schlüssels mit den Daten eines anderen Schlüssels überschrieben. Um Kollisionen geschickt zu behandeln gibt es zwei Ansätze:&lt;br /&gt;
* lineare Verkettung&lt;br /&gt;
* offene Adressierung&lt;br /&gt;
&lt;br /&gt;
=== Hashtabelle mit linearer Verkettung (offenes Hashing/geschlossene Adressierung) ===&lt;br /&gt;
&lt;br /&gt;
Man kann dies als die pessimistische Lösung bezeichnen: Man nimmt an, dass Kollisionen häufig auftreten. Deshalb wird unter jedem&lt;br /&gt;
Hashindex gleich eine Liste angelegt, in der Einträge mit gleichem Hashindex aufgenommen werden können. Die Hashtabelle verwaltet ein Array von Listen, und jedes Arrayfeld kann beliebig viele Elemente speichern: Wird ein Element auf den Index &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; abgebildet, werden die Daten einfach an die betreffende Liste angehängt. Bei Zugriff auf ein Element wird zunächst die passende Liste gesucht (mit Hilfe des Hashwerts), danach erfolgt in dieser Liste eine sequentielle Suche nach dem richtigen Schlüssel.&lt;br /&gt;
&lt;br /&gt;
Um diese Idee implementieren zu können, benötigen wir zunächst eine Hilfsklasse &amp;lt;tt&amp;gt;HashNode&amp;lt;/tt&amp;gt;, die (Schlüssel, Wert)-Paare speichert und mit Hilfe von &amp;lt;tt&amp;gt;next&amp;lt;/tt&amp;gt; eine verkettete Liste realisiert:&lt;br /&gt;
  class HashNode:&lt;br /&gt;
      def __init__(self,key,data,next):&lt;br /&gt;
          self.key = key&lt;br /&gt;
          self.data = data&lt;br /&gt;
          self.next = next    # Verkettung!&lt;br /&gt;
Die eigentliche Hashtabelle wird in der Klasse ''HashTable'' implementiert:&lt;br /&gt;
  class HashTable:&lt;br /&gt;
      def __init__(self):&lt;br /&gt;
         self.capacity = ... # Geeignete Werte siehe unten&lt;br /&gt;
         self.size = 0       # Anzahl der Werte, die zur Zeit tatsächlich gespeichert sind&lt;br /&gt;
         self.array = [None]*self.capacity&lt;br /&gt;
Wie oben bereits erwähnt, werden die Zugriffsoperatoren ''[ ]'' für eine Datenstruktur in Python durch die Funktionen &amp;lt;tt&amp;gt;__setitem__&amp;lt;/tt&amp;gt; bzw. &amp;lt;tt&amp;gt;__getitem__&amp;lt;/tt&amp;gt; implementiert. &lt;br /&gt;
Die &amp;lt;tt&amp;gt;__setitem__&amp;lt;/tt&amp;gt;-Funktion speichert die gegebenen Daten unter dem Schlüssel &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; in der &amp;lt;tt&amp;gt;HashTable&amp;lt;/tt&amp;gt;-Klasse:&lt;br /&gt;
     def __setitem__(self, key, value):&lt;br /&gt;
         index = hash(key) % self.capacity  # hash(...) ist in Python eine vordefinierte Funktion&lt;br /&gt;
         node  = self.array[index]          # finde die zu 'key' gehörende Liste&lt;br /&gt;
         while node is not None:            # sequentielle Suche nach 'key' in dieser Liste&lt;br /&gt;
             if node.key == key:&lt;br /&gt;
                 # Element 'key' ist schon in der Tabelle&lt;br /&gt;
                 # =&amp;gt; überschreibe die Daten mit dem neuen Wert&lt;br /&gt;
                 node.data = value&lt;br /&gt;
                 return&lt;br /&gt;
             # andernfalls: Kollision des Hashwerts, probiere nächsten 'key' aus&lt;br /&gt;
             node = node.next&lt;br /&gt;
         # kein Element hatte den richtigen Schlüssel.&lt;br /&gt;
         # =&amp;gt; es gibt diesen Schlüssel noch nicht&lt;br /&gt;
         #    füge also ein neues Element in die Hashtabelle ein&lt;br /&gt;
         self.array[index] = HashNode(key, value, self.array[index]) # der alte Anfang der Liste wird zum&lt;br /&gt;
                                                                     # Nachfolger des neu eingefügten ersten Elements&lt;br /&gt;
         self.size += 1&lt;br /&gt;
         ... # eventuell muss jetzt noch die Kapazität optimiert werden&lt;br /&gt;
Die Funktion &amp;lt;tt&amp;gt;__getitem__&amp;lt;/tt&amp;gt; gibt die unter dem Schlüssel &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; abgelegten Daten zurück, oder eine Fehlermeldung, falls dieser Schlüssel nicht existiert:&lt;br /&gt;
     def __getitem__(self, key):&lt;br /&gt;
         index = hash(key) % self.capacity&lt;br /&gt;
         node = self.array[index]     # finde die zu 'key' gehörende Liste&lt;br /&gt;
         while node is not None:      # sequentielle Suche nach 'key' in dieser Liste&lt;br /&gt;
              if node.key == key:     # gefunden!&lt;br /&gt;
                  return node.data    # =&amp;gt; Daten zurückgeben&lt;br /&gt;
              node = node.next        # nächsten Schlüssel probieren&lt;br /&gt;
         raise KeyError(key)          # Schlüssel nicht gefunden =&amp;gt; Fehler&lt;br /&gt;
&lt;br /&gt;
==== Komplexität der linearen Verkettung und Wahl der Kapazität ====&lt;br /&gt;
&lt;br /&gt;
Die Komplexität wird durch zwei Operationen bestimmt: erstens das Auffinden der zu einem Schlüssel gehörenden Liste (die in O(1) erfolgt), zweitens das sequentielle Durchsuchen der Liste, die Zeit in O(L) erfordert, wobei L die mittlere Länge der Listen ist. Die Hashtabelle ist also nur schnell, wenn die Länge der Listen möglichst klein ist. Unter der Annahme des ''uniformen Hashings'', wenn also alle Indizes gleich häufig verwendet werden, ist L gleich dem '''Füllstand''' der Hashtabelle:&lt;br /&gt;
:::&amp;lt;math&amp;gt;\alpha = \frac{N}{M} = \frac{\text{size}}{\text{capacity}}&amp;lt;/math&amp;gt; wobei N die Größe &amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt; der Hashtabelle und M die Größe &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; des Arrays ist.&lt;br /&gt;
Wenn die Hashwerte uniform sind, entfallen auf jede Liste im Mittel N/M Einträge (N Einträge, verteilt auf M Listen). Die Gesamtkomplexität berechnet sich nach der Sequenzregel zu&lt;br /&gt;
:::&amp;lt;math&amp;gt;O(1+\alpha)&amp;lt;/math&amp;gt;&lt;br /&gt;
Für eine effiziente Suche muss demnach &amp;lt;math&amp;gt;\alpha \in O(1)&amp;lt;/math&amp;gt; gewählt werden. Dies erreicht man, indem man, wie beim dynamischen Array, &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; immer wieder anpasst, falls &amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt; zu groß wird. Üblicherweise verdoppelt man &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt;, sobald &amp;lt;tt&amp;gt;size == capacity&amp;lt;/tt&amp;gt; erreicht wird. Analog zum dynamischen Array werden die Daten dann aus dem alten Array (&amp;lt;tt&amp;gt;self.array&amp;lt;/tt&amp;gt;) in ein entsprechend vergrößertes neues Array kopiert.&lt;br /&gt;
&lt;br /&gt;
In der C++ Standardbibliothek (Klasse &amp;lt;tt&amp;gt; [http://www.cplusplus.com/reference/stl/unordered_map/ std::unordered_map]&amp;lt;/tt&amp;gt;, siehe auch [http://gcc.gnu.org/viewcvs/trunk/libstdc%2B%2B-v3/src/shared/hashtable-aux.cc?view=markup GCC hashtable_aux.cc (Primzahlen)] und [http://gcc.gnu.org/viewcvs/trunk/libstdc%2B%2B-v3/include/bits/hashtable_policy.h?view=markup GCC Hash Implementation]) wird die Hashtabelle häufig so&lt;br /&gt;
implementiert. Dabei wird &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; immer als ''Primzahl'' gewählt, wobei sich aufeinanderfolgende Kapazitäten immer ungefähr verdoppeln. Dazu wählt man aus einer vorberechneten Liste von Primzahlen die kleinste Zahl, so dass &amp;lt;tT&amp;gt;new_capacity &amp;gt;= 2*capacity&amp;lt;/tt&amp;gt; gilt, und beginnt z.B. mit einer Default-Kapazität von 11:&lt;br /&gt;
  11, 23, 47, 97, 199, 409, 823, ...&lt;br /&gt;
Die Wahl von Primzahlen hat zur Folge, dass &amp;lt;tt&amp;gt;hash(key) % self.capacity&amp;lt;/tt&amp;gt; ''alle'' Bits von h benutzt (Eigenschaft aus der Zahlentheorie). Die Kapazität wird vergrößert, wenn &amp;lt;tt&amp;gt;size == capacity&amp;lt;/tt&amp;gt; erreicht wird, und die ungefähre Verdoppelung sichert, dass die amortisierte Komplexität der Einfügeoperation in O(1) ist (wie beim dynamischen Array).&lt;br /&gt;
&lt;br /&gt;
=== Hashtabelle mit offener Adressierung (geschlossenes Hashing) ===&lt;br /&gt;
[[Image:HASHTB12.svg.png|frame|Prinzip ([http://en.wikipedia.org/wiki/Hash_table Quelle])]]&lt;br /&gt;
&lt;br /&gt;
Dies kann als die optimistische Variante betrachtet werden: man nimmt an, dass Kollisionen nicht so häufig auftreten, um eine komplexe Datenstruktur wie das &amp;quot;Array von Listen&amp;quot; zu rechtfertigen. Stattdessen behandelt man Kollisionen mit einer einfachen '''Idee''': Wenn &amp;lt;tt&amp;gt;array[index]&amp;lt;/tt&amp;gt; durch Kollision bereits vergeben ist, probiere einen&lt;br /&gt;
anderen Index aus (siehe auch [http://de.wikipedia.org/wiki/Hashtabelle#Hashing_mit_offener_Adressierung Wikipedia (de)] und&lt;br /&gt;
[http://en.wikipedia.org/wiki/Open_addressing Wikipedia (en)]). Dabei muss man folgendes beachten:&lt;br /&gt;
&lt;br /&gt;
* Das Array enthält pro Element höchstens ein (key,value)-Paar&lt;br /&gt;
* Das Array muss stets mindestens ''einen'' freien Platz haben (sonst gäbe es beim Ausprobieren anderer Indizes eine Endlosschleife). Es gilt immer &amp;lt;tt&amp;gt;self.size &amp;lt; self.capacity&amp;lt;/tt&amp;gt;. Dies war bei der vorigen Hash-Implementation mit linearer Verkettung nicht notwendig (aber im Sinne schneller Zugriffszeiten trotzdem wünschenswert).&lt;br /&gt;
&lt;br /&gt;
==== Vorgehen bei Kollisionen ====&lt;br /&gt;
&lt;br /&gt;
=====Sequentielles Sondieren=====&lt;br /&gt;
&lt;br /&gt;
Probiere den nächsten Index: &amp;lt;tt&amp;gt;index = (index+1) % capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Vorteil: einfach&lt;br /&gt;
* Nachteil: Clusterbildung&lt;br /&gt;
&lt;br /&gt;
Clusterbildung heißt, dass sich größere zusammenhängende Bereiche bilden die belegt sind, unterbrochen von Bereichen die komplett frei sind. Beim Versuch des Einfügens eines Elements an einen Platz, der schon belegt ist, muss jetzt das ganze Cluster sequentiell durchlaufen werden, bis ein freier Platz gefunden wird. Damit entspricht die Komplexität der Suche der mittleren Länge der belegten Bereiche, was sich entsprechend in einer langsamen Suche widerspiegelt.&lt;br /&gt;
&lt;br /&gt;
=====Doppeltes Hashing=====&lt;br /&gt;
&lt;br /&gt;
[http://de.wikipedia.org/wiki/Doppel-Hashing Wikipedia (de)] [http://en.wikipedia.org/wiki/Double_hashing Wikipedia (en)]&lt;br /&gt;
&lt;br /&gt;
Bestimme einen neuen Index (bei Kollisionen) durch eine ''2. Hashfunktion''.&lt;br /&gt;
&lt;br /&gt;
Das doppelte Hashing wird typischerweise in der Praxis angewendet und liegt auch der Python Implementierung des Datentyps [http://docs.python.org/tut/node7.html#SECTION007500000000000000000 Dictionary] (Syntax &amp;lt;tt&amp;gt;{'a':1, 'b':2, 'c':3}&amp;lt;/tt&amp;gt; zugrunde.&lt;br /&gt;
&lt;br /&gt;
Eine effiziente Implementierung dieses Datentyps ist für die Performance der Skriptsprache Python extrem wichtig, da z.B. beim Aufruf einer Funktion der auszuführunde Code in einem Dictionary unter dem Schlüssel ''Funktionsname'' nachgeschlagen wird oder die Werte lokaler Variablen innerhalb einer Funktion ebenfalls in einem Dictionary zu finden sind.&lt;br /&gt;
&lt;br /&gt;
Für die Implementierung in Python werden wieder die obigen Klassen &amp;lt;tt&amp;gt;HashNode&amp;lt;/tt&amp;gt; (das Attribut &amp;lt;tt&amp;gt;next&amp;lt;/tt&amp;gt; kann allerdings jetzt entfernt werden) und &amp;lt;tt&amp;gt;HashTable&amp;lt;/tt&amp;gt; benötigt, es folgen die angepassten Implementationen von &amp;lt;tt&amp;gt;__setitem__&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;__getitem__&amp;lt;/tt&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
  def __setitem__(self, key, value):&lt;br /&gt;
      h = hash(key)&lt;br /&gt;
      index = h % self.capacity&lt;br /&gt;
      # erste Schleife: teste, ob key schon vorhanden ist&lt;br /&gt;
      while True:&lt;br /&gt;
          if self.array[index] is None:     # freies Feld gefunden =&amp;gt; key nicht vorhanden&lt;br /&gt;
               break&lt;br /&gt;
          if self.array[index].key == key:  # key gefunden =&amp;gt; Daten aktualisieren&lt;br /&gt;
               self.array[index].data = value&lt;br /&gt;
               return&lt;br /&gt;
          # self.array[index].key ist anderer Schlüssel oder als gelöscht markiert&lt;br /&gt;
          # =&amp;gt; neuen Index durch 2. Hashfunktion berechnen&lt;br /&gt;
          index = (5*index+1+h) % self.capacity&lt;br /&gt;
          h = h // 32&lt;br /&gt;
      # wenn wir hier landen, wurde key nicht gefunden&lt;br /&gt;
      h = hash(key)&lt;br /&gt;
      index = h % self.capacity&lt;br /&gt;
      # zweite Schleife: neues Element einfügen&lt;br /&gt;
      while True:&lt;br /&gt;
          if self.array[index] is None or self.array[index].key is None:&lt;br /&gt;
               # index ist frei (1. Bedingung) oder als gelöscht markiert (2. Bedingung)&lt;br /&gt;
               # =&amp;gt; hier gehört key hin&lt;br /&gt;
               self.array[index] = HashNode(key, value)&lt;br /&gt;
               self.size +=1&lt;br /&gt;
               ... # eventuell muss hier die Kapazität optimiert werden&lt;br /&gt;
               return&lt;br /&gt;
          # index ist schon belegt =&amp;gt; neuen Index durch 2. Hashfunktion berechnen&lt;br /&gt;
          index = (5*index+1+h) % self.capacity&lt;br /&gt;
          h = h // 32&lt;br /&gt;
&lt;br /&gt;
Wir nehmen bei dieser Implementation an, dass gelöschte Elemente dadurch markiert werden, dass &amp;lt;tt&amp;gt;self.array[index].key&amp;lt;/tt&amp;gt; auf einen Schlüssel gesetzt wird, der sonst nicht vorkommen kann (z.B. &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;). Dann wird die if-Abfrage &amp;lt;tt&amp;gt;self.array[index].key == key&amp;lt;/tt&amp;gt; niemals wahr, und es wird weitergesucht. Würde man hingegen das Element vollständig löschen, könnte die Bedingung &amp;lt;tt&amp;gt;self.array[index] is None&amp;lt;/tt&amp;gt; zu früh wahr werden, so dass die Schleife vorzeitig abgebrochen und das vorhandene Element &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; nicht erreicht würde.&lt;br /&gt;
 &lt;br /&gt;
 def __getitem__(self, key):&lt;br /&gt;
     h = hash(key)&lt;br /&gt;
     index = h % self.capacity&lt;br /&gt;
     while True:&lt;br /&gt;
         if self.array[index] is None:    # die Suchkette bricht ab =&amp;gt; key existiert nicht&lt;br /&gt;
             raise KeyError(key)&lt;br /&gt;
         if self.array[index].key == key: # key gefunden =&amp;gt; zugehörige Daten zurückgeben&lt;br /&gt;
             return self.array[index].data&lt;br /&gt;
         # index enthält nicht den passenden kay =&amp;gt; neuen Index durch 2. Hashfunktion berechnen&lt;br /&gt;
         index = (5*index+1+h) % self.capacity&lt;br /&gt;
         h = h // 32&lt;br /&gt;
&lt;br /&gt;
Die vorgestellte Implementierung orientiert sich an Pythons interner Dictionary Implementierung, der zugehörige Quellcode (mit ausführlichem Kommentar) findet sich im File [https://github.com/python/cpython/blob/master/Objects/dictobject.c dictobject.c] der Python Implementation.&lt;br /&gt;
&lt;br /&gt;
===== Beispiel für doppeltes Hashing =====&lt;br /&gt;
&lt;br /&gt;
Der Übersichtlichkeit wegen wählen wir M'=2&amp;lt;sup&amp;gt;5&amp;lt;/sup&amp;gt; (statt 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;) und eine Kapazität von M=8.&lt;br /&gt;
&lt;br /&gt;
Roher Hashwert (für das Beispiel willkürlich gewählt):&lt;br /&gt;
  h=25&lt;br /&gt;
Erster Index:&lt;br /&gt;
  i0 = h % capacity = 25 % 8 = 1&lt;br /&gt;
Es finde eine Kollision statt. Es wird ein zweiter Index berechnet:&lt;br /&gt;
  i1 = (5*i0 + 1 + h) % 8 = (5*1 + 1 + 25) % 8 = 31 % 8 = 7&lt;br /&gt;
Der Hashwert wird aktualisiert um die höherwertigen Bits von &amp;lt;tt&amp;gt;h&amp;lt;/tt&amp;gt; ins Spiel zu bringen (hier durch &amp;lt;tt&amp;gt;h &amp;gt;&amp;gt; 2&amp;lt;/tt&amp;gt; anstelle von &amp;lt;tt&amp;gt;h &amp;gt;&amp;gt; 5&amp;lt;/tt&amp;gt; im originalen Pythoncode). Wir stellen &amp;lt;tt&amp;gt;h&amp;lt;/tt&amp;gt; als Binärzahl dar, damit der Rechtsshift besser sichtbar wird:&lt;br /&gt;
  h = h &amp;gt;&amp;gt; 2 &lt;br /&gt;
  ==&amp;gt; h = (11001 &amp;gt;&amp;gt; 2) = 00110 = 6&lt;br /&gt;
Es finde wieder eine Kollision statt, so dass ein dritter Index berechnet werden muss.&lt;br /&gt;
  i2 = (5*i1 + 1 + h) % 8 = (5*7 + 1 + 6) % 8 = 42 % 8 = 2&lt;br /&gt;
Der Hashwert wird wiederum aktualisiert:&lt;br /&gt;
  h = h &amp;gt;&amp;gt; 2&lt;br /&gt;
  ==&amp;gt; h = (00110 &amp;gt;&amp;gt; 2) = 00001 = 1&lt;br /&gt;
Es finde eine Kollision statt, und wir berechnen den vierten Index:&lt;br /&gt;
  i3 = (5*i2 + 1 + h) % 8 = (5*2 + 1 + 1) % 8 = 12 % 8 = 4&lt;br /&gt;
Der Hashwert wird nochmals aktualisiert und erreicht jetzt den Wert 0 (der sich dann nicht mehr ändert):&lt;br /&gt;
  h = h &amp;gt;&amp;gt; 2 &lt;br /&gt;
  ==&amp;gt; h = (00110 &amp;gt;&amp;gt; 2) = 0&lt;br /&gt;
Es finde eine Kollision statt. Da jetzt &amp;lt;tt&amp;gt;h = 0&amp;lt;/tt&amp;gt; gilt, und die Zahlen 5 (Multiplikator) und 8 (capacity) teilerfremd sind, werden ab jetzt systematisch alle Indizes von 0 bis 7 durchprobiert (in der durch die Modulo-Operation bestimmten Reihenfolge):&lt;br /&gt;
  i4  = (5*i3  + 1 + h) % 8 = (5*4 + 1 + 0) % 8 = 21 % 8 = 5&lt;br /&gt;
  i5  = (5*i4  + 1 + h) % 8 = (5*5 + 1 + 0) % 8 = 26 % 8 = 2&lt;br /&gt;
  i6  = (5*i5  + 1 + h) % 8 = (5*2 + 1 + 0) % 8 = 11 &amp;amp; 8 = 3&lt;br /&gt;
  i7  = (5*i6  + 1 + h) % 8 = (5*3 + 1 + 0) % 8 = 16 &amp;amp; 8 = 0&lt;br /&gt;
  i8  = (5*i7  + 1 + h) % 8 = (5*0 + 1 + 0) % 8 =  1 &amp;amp; 8 = 1&lt;br /&gt;
  i9  = (5*i8  + 1 + h) % 8 = (5*1 + 1 + 0) % 8 =  6 &amp;amp; 8 = 6&lt;br /&gt;
  i10 = (5*i9  + 1 + h) % 8 = (5*6 + 1 + 0) % 8 = 31 &amp;amp; 8 = 7&lt;br /&gt;
  i11 = (5*i10 + 1 + h) % 8 = (5*7 + 1 + 0) % 8 = 36 &amp;amp; 8 = 4&lt;br /&gt;
Allen Indizes werden also erreicht, bevor sich die Folge wiederholt. Da man &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; immer so wählt, dass mindestens ein Arrayfeld noch frei ist, wird dadurch immer ein geeigneter Platz für das einzufügende Element gefunden.&lt;br /&gt;
&lt;br /&gt;
==== Komplexität der offenen Adressierung ====&lt;br /&gt;
&lt;br /&gt;
* Annahme: uniformes Hashing, das heißt alle Indizes haben gleiche Wahrscheinlichkeit&lt;br /&gt;
* Füllstand &amp;lt;math&amp;gt;\alpha =\frac{N}{M} = \frac{\text{size}}{\text{capacity}}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* '''Erfolglose Suche''' (d.h. es wird entweder ein neues Element eingefügt oder ein &amp;lt;tt&amp;gt;KeyError&amp;lt;/tt&amp;gt; geworfen): Untere Schranke für die Komplexität ist &amp;lt;math&amp;gt;\Omega\left(\frac{1}{1-\alpha}\right)&amp;lt;/math&amp;gt; Schritte (= Anzahl der notwendigen index-Berechnungen).&lt;br /&gt;
* '''Erfolgreiche Suche''' &amp;lt;math&amp;gt;\Omega\left(\frac{1}{\alpha}\ln\left(\frac{1} {1-\alpha}\right)\right)&amp;lt;/math&amp;gt; Schritte.&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;
! &amp;lt;math&amp;gt;\alpha&amp;lt;/math&amp;gt;&lt;br /&gt;
! 0.5&lt;br /&gt;
! 0.9&lt;br /&gt;
|- &lt;br /&gt;
| erfolglos&lt;br /&gt;
| 2.0&lt;br /&gt;
| 10&lt;br /&gt;
|-&lt;br /&gt;
| erfolgreich&lt;br /&gt;
| 1.4&lt;br /&gt;
| 2.6&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Wahl der Kapazität ====&lt;br /&gt;
Man sieht an der obigen Tabelle, dass die erfolglose Suche (und damit das Einfügen) sehr langsam wird, wenn der Füllstand hoch ist. In Python wird &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; deshalb so gewählt, dass &amp;lt;math&amp;gt;\alpha \leq 2/3&amp;lt;/math&amp;gt;. Falls &amp;lt;math&amp;gt;\alpha&amp;lt;/math&amp;gt; größer werden sollte, verdopple die Kapazität und kopiere das alte array in das neue Array (analog zum dynamischen Array)&lt;br /&gt;
&lt;br /&gt;
In Python werden die Kapazitätsgrößen als Zweierpotenzen gewählt, also 4,8,16,32,...,&lt;br /&gt;
so dass &amp;lt;tt&amp;gt;h % self.capacity&amp;lt;/tt&amp;gt; nur die unteren Bits von &amp;lt;tt&amp;gt;h&amp;lt;/tt&amp;gt; benutzt. Die oberen Bits von &amp;lt;tt&amp;gt;h&amp;lt;/tt&amp;gt; kommen erst ins Spiel, wenn bei der Berechnung der 2. Hashfunktion die Aktualisierung &amp;lt;tt&amp;gt;h = h &amp;gt;&amp;gt; 5&amp;lt;/tt&amp;gt; erfolgt. Dies hat sich bei umfangreichen Experimenten als sehr gute Lösung erwiesen.&lt;br /&gt;
&lt;br /&gt;
== Anwendungen von Hashing ==&lt;br /&gt;
&lt;br /&gt;
* Hashtabelle, assoziatives Aray&lt;br /&gt;
* Sortieren in linearer Zeit (Übungsaufgabe 6.2)&lt;br /&gt;
* Suchen von Strings in Texten: Rabin-Karp-Algorithmus&lt;br /&gt;
* ...&lt;br /&gt;
&lt;br /&gt;
=== Rabin Karp Algorithmus ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Rabin-Karp-Algorithmus Wikipedia (de)] [http://en.wikipedia.org/wiki/Rabin-Karp_string_search_algorithm Wikipedia (en)]&lt;br /&gt;
&lt;br /&gt;
In Textverarbeitungsanwendungen ist eine häufig benutzte Funktion die ''Search &amp;amp; Replace'' Funktionalität. Die Suche sollte in O(len(text)) möglich sein, aber ein naiver Algorithmus braucht O(len(text)*len(searchstring))&lt;br /&gt;
&lt;br /&gt;
==== Naive Implementierung der Textsuche ====&lt;br /&gt;
  def search(text, s):&lt;br /&gt;
      M, N = len(text), len(s)&lt;br /&gt;
      for k in range(M-N):&lt;br /&gt;
          if s==text[k:k+N]:   # O(N), da N Zeichen verglichen werden müssen&lt;br /&gt;
              return k&lt;br /&gt;
      return -1 #nicht gefunden&lt;br /&gt;
&lt;br /&gt;
==== Idee des Rabin Karp Algorithmus ====&lt;br /&gt;
Statt Vergleichen &amp;lt;tt&amp;gt;s==text[k:k+N]&amp;lt;/tt&amp;gt;, die O(N) benötigen, weil N Vergleiche der Buchstaben durchgeführt werden müssen, vergleichen wir die Hashs von Suchstring und dem zu untersuchenden Textabschnitt: &amp;lt;tt&amp;gt;hash(s) == hash(text[k:k+N])&amp;lt;/tt&amp;gt;. Dabei muss natürlich &amp;lt;tt&amp;gt;hash(s)&amp;lt;/tt&amp;gt; nur einmal berechnet werden, wohingegen &amp;lt;tt&amp;gt;hash(text[k:k+N])&amp;lt;/tt&amp;gt; immer wieder neu berechnet werden muss. Damit der Vergleich O(1) sein kann, ist es deswegen erforderlich, eine solche Hashfunktion zu haben, die nicht alle Zeichen (das wäre O(N) ) einlesen muss, sondern die den vorhergehenden Hashwert mit einbezieht.&lt;br /&gt;
&lt;br /&gt;
Eine solche Hashfunktion heißt ''Running Hash'' und funktioniert analog zum ''Sliding Mean''.&lt;br /&gt;
&lt;br /&gt;
Die Running Hash Funktion berechnet in O(1) den hash von &amp;lt;tt&amp;gt;text[k+1:k+1+N]&amp;lt;/tt&amp;gt; ausgehend vom hash für &amp;lt;tt&amp;gt;text[k:k+N]&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Idee: Interpretiere den Text als Ziffern in einer base d Darstellung:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;h_k = \text{text}[k]\cdot d^{N-1} + \text{text}[k+1]\cdot d^{N-2} + \cdots + \text{text}[k+N-1]&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für die Basis 10 (Dezimalsystem) ergibt sich also&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;h_k = \text{text}[k]\cdot {10}^{N-1} + \text{text}[k+1]\cdot {10}^{N-2} + \cdots + \text{text}[k+N-1]&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Daraus folgt&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;h_{k+1} = 10 \cdot h_k - \text {text}[k]\cdot {10}^{N} + \text {text}[k+N]&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexität dieses Updates ist O(1), falls man &amp;lt;math&amp;gt;{10}^{N}&amp;lt;/math&amp;gt; vorberechnet hat.&lt;br /&gt;
&lt;br /&gt;
In der Realität wählt man dann d=32 und benutzt noch an einigen Stellen modulo Operationen, um die Zahlen nicht zu groß werden zu lassen (siehe die Zahl &amp;lt;tt&amp;gt;q&amp;lt;/tt&amp;gt; in der folgenden Implementation).&lt;br /&gt;
&lt;br /&gt;
==== Implementation ====&lt;br /&gt;
  def searchRabinKarp(text, s):&lt;br /&gt;
      M, N = len(text), len(s)&lt;br /&gt;
      d = 32&lt;br /&gt;
      q = 33554393  # q ist eine große Primzahl, aber so,&lt;br /&gt;
                    # dass d*q &amp;lt; 2**32 (um Überlauf bei 32-bit Integerarithmetik zu vermeiden)&lt;br /&gt;
      dN = d**N % q # Vorberechnung des Vorfaktors für das Entfernen aus dem Hash&lt;br /&gt;
      &lt;br /&gt;
      # Initialisierung      &lt;br /&gt;
      hs, ht = 0, 0&lt;br /&gt;
      for k in range(N):&lt;br /&gt;
          # ord() gibt die Zeichen-Nummer (z.B. ASCII- oder UTF-8-Code) des &lt;br /&gt;
          # übergebenen Zeichens zurück&lt;br /&gt;
          hs = (hs*d + ord( s[k] )) % q&lt;br /&gt;
          ht = (ht*d + ord(text[k])) % q&lt;br /&gt;
      # Die Variablen sind jetzt wie folgt initialisiert:&lt;br /&gt;
      # hs = hash(s)&lt;br /&gt;
      # ht = hash(text[0:N])&lt;br /&gt;
      &lt;br /&gt;
      # Hauptschleife&lt;br /&gt;
      k = 0 &lt;br /&gt;
      while k &amp;lt; M-N:&lt;br /&gt;
          if hs == ht:  # übereinstimmende Hashs =&amp;gt; prüfe, dass es nicht nur&lt;br /&gt;
                        # eine Kollision ist&lt;br /&gt;
              if s == text[k:k+N]:  # O(N)-Vergleich nur nötig, wenn Hashs übereinstimmen&lt;br /&gt;
                   return k         # search string an Position k gefunden&lt;br /&gt;
          # nicht gefunden =&amp;gt; aktualisiere Hash für den nächsten Teilabschnitt von text:&lt;br /&gt;
          ht = (d*ht + ord(text[k+N])) % q # neues Zeichen text[k+N] in Hash einfügen&lt;br /&gt;
          ht = (ht - dN*ord(text[k])) % q  # Zeichen text[k] aus Hash entfernen.&lt;br /&gt;
          k +=1&lt;br /&gt;
      return -1  # search string nicht gefunden&lt;br /&gt;
&lt;br /&gt;
[[Iteration versus Rekursion|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Hashing_und_Hashtabellen&amp;diff=5701</id>
		<title>Hashing und Hashtabellen</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Hashing_und_Hashtabellen&amp;diff=5701"/>
				<updated>2020-06-23T14:05:58Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Doppeltes Hashing */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Die Mitschrift gibts auch als [http://hci.iwr.uni-heidelberg.de/alda/images/AlDa.pdf PDF].&lt;br /&gt;
== Hashing ==&lt;br /&gt;
&lt;br /&gt;
Wir haben im Abschnitt [[Assoziative Arrays]] gezeigt, dass man assoziative Arrays effizient mit Hilfe von Suchbäumen realisieren kann, so dass die Zugriffszeit auf ein Element in O(log(len(a))) ist. Genau wie beim Sortierproblem stellt sich jetzt die Frage, ob die Zugriffszeit noch verbessert werden kann, idealerseise auf O(1) wie beim gewöhnlichen Array. Die Antwort lautet: Ja, wenn für die Schlüssel eine Hashfunktion definiert ist.&lt;br /&gt;
&lt;br /&gt;
===Hashfunktionen===&lt;br /&gt;
&lt;br /&gt;
Hashfunktionen sind eine weitere Anwendung des [[Sortieren in linearer Zeit#Bucket-Prinzip|Bucket-Prinzips]], das wir im Zusammenhang mit dem Sortieren in linearer Zeit eingeführt haben. man bildet die Schlüssel wiederum auf Bucket-Indizes ab, um die Suche zu beschleunigen (von O(log N) nach O(1)). Im Unterschied zum Sortieren verzichtet man hier allerdings darauf, dass die Abbildung auf Bucket-Indizes die Ordnung der Schlüssel erhalten muss (es muss nicht einmal eine Ordnung definiert sein), weil diese Forderung es erschwert, die Schlüssel gleichmäßig auf die Buckets zu verteilen. Letzteres ist aber bei Hashtabellen extrem wichtig.&lt;br /&gt;
&lt;br /&gt;
Gegeben sei ein Universum U, dass die Menge aller legalen Schlüssel darstellt. Die Mächtigkeit |U| der Menge U ist im allgemeinen sehr groß. Beispielsweise kann man mit Strings der Länge 9 bis zu 27&amp;lt;sup&amp;gt;9&amp;lt;/sup&amp;gt;&amp;amp;asymp;10&amp;lt;sup&amp;gt;13&amp;lt;/sup&amp;gt;&amp;amp;asymp;2&amp;lt;sup&amp;gt;43&amp;lt;/sup&amp;gt; verschiedene Schlüssel generieren, wenn 27 Zeichen erlaubt sind (Kleinbuchstaben und Leerzeichen). Die Grundannahme von Hashing ist jetzt, dass in jeder gegebenen Anwendung nur ein (kleiner) Teil der erlaubten Schlüssel tatsächlich verwendet wird. Man definiert eine Hashfunktion, die jeden Schlüssel auf eine natürliche Zahl im Bereich 0...(M-1) abbildet, wobei M viel kleiner als |U| ist. &lt;br /&gt;
;Definition einer Hashfunktion: &lt;br /&gt;
:::&amp;lt;math&amp;gt; f: U  \rightarrow [0, 1, \ldots, M-1] \subset \mathbb{N} &amp;lt;/math&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt; f(u \in U) = h \in [0, 1, \ldots, M-1]&amp;lt;/math&amp;gt;&lt;br /&gt;
h wird als ''Hashwert'' von u bezeichnet. Da M &amp;lt; |U|, werden notwendigerweise einige Schlüssel auf dieselbe Zahl abgebildet. Man bezeichnet den Fall &amp;lt;math&amp;gt; f(u_1 \in U) = f(u_2 \in U) &amp;lt;/math&amp;gt; als ''Kollision'' zwischen den Schlüsseln u&amp;lt;sub&amp;gt;1&amp;lt;/sub&amp;gt; und u&amp;lt;sub&amp;gt;2&amp;lt;/sub&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Die '''Aufgabe''' besteht jetzt darin, ein Hash-Funktion zu entwerfen, die möglichst wenige Kollisionen hat. Hashfunktionen ähneln damit einem Zufallszahlengenerator, weil jede Zahl &amp;lt;math&amp;gt; h \in 0 \ldots (M-1) &amp;lt;/math&amp;gt; nach Möglichkeit mit gleicher Wahrscheinlichkeit herauskommen soll. Wird dieses Ziel erreicht, spricht man vom ''uniformen Hashing''.&lt;br /&gt;
&lt;br /&gt;
In der Regel ist aber nicht vorher bekannt, welche Schlüssel in einer Anwendung verwendet werden. Es kann deshalb immer vorkommen, dass die verwendete Schlüsselmenge sehr viele Kollisionen verursacht. Man sieht in der Tat leicht ein, dass für jede gegebene Hashfunktion ungünstige Schlüsselmengen &amp;lt;math&amp;gt; U_f \subset U&amp;lt;/math&amp;gt; existieren, bei denen es sehr viele Kollisionen gibt. Im ungünstigsten Fall könnte U&amp;lt;sub&amp;gt;f&amp;lt;/sub&amp;gt; so gewählt sein, dass f(U&amp;lt;sub&amp;gt;f&amp;lt;/sub&amp;gt;) = k = const. gilt. Ein Hacker, der die verwendete Hashfunktion kennt, kann z.B. U&amp;lt;sub&amp;gt;f&amp;lt;/sub&amp;gt; absichtlich so wählen, um eine denial-of-service-Attacke gegen einen hash-basierten Webservice zu starten. Ein anderes anschauliches Beispiel wäre eine Party, zu der nur Leute eingeladen werden, die an einem 8ten im Monat Geburtstag haben. Auf dieser Party ist es viel wahrscheinlicher, Leute zu finden, die am selben (oder gleichen) Tag Geburtstag haben, als wenn man alle einlädt.&lt;br /&gt;
&lt;br /&gt;
D.h. die Wahl einer guten Hashfunktion ist eine Kunst, und man muss (wenn möglich) die Daten analysieren um ein gutes f zu finden.&lt;br /&gt;
&lt;br /&gt;
====Perfektes Hashing====&lt;br /&gt;
&lt;br /&gt;
Kennt man die Untermenge der tatsächlich vorkommenden Schlüssel &amp;lt;math&amp;gt;U_f \subset U&amp;lt;/math&amp;gt; schon im voraus, hat man die Möglichkeit, eine ''perfekte Hashfunktion'' ohne Kollisionen zu entwerfen.&lt;br /&gt;
&lt;br /&gt;
;Beispiel anhand der Monatsnamen &lt;br /&gt;
&lt;br /&gt;
U ist in diesem Fall eine Menge von Strings der Länge 9 (weil der September als längster Monatsname 9 Zeichen hat). Es ergeben sich also &amp;lt;math&amp;gt;60^{9}&amp;lt;/math&amp;gt;&amp;gt;&amp;amp;asymp;10&amp;lt;sup&amp;gt;16&amp;lt;/sup&amp;gt;&amp;amp;asymp;2&amp;lt;sup&amp;gt;54&amp;lt;/sup&amp;gt; mögliche Strings, da mit Groß- und Kleinbuchstaben, Umlauten, ß und Leerzeichen 60 Zeichen im deutschen Alphabet vorhanden sind. Von all diesen Möglichkeiten werden genau 12 benutzt:&lt;br /&gt;
:::&amp;lt;math&amp;gt;U_f&amp;lt;/math&amp;gt; = {&amp;quot;Januar&amp;quot;; &amp;quot;Februar&amp;quot;; ... ; &amp;quot;Dezember&amp;quot;}&lt;br /&gt;
* Benutzt man nun als Hashfunktion die Anfangsbuchstaben der Monatsnamen, benötigt man dafür 6 bit. M ist somit 64. &lt;br /&gt;
:::{&amp;quot;Januar&amp;quot;; &amp;quot;Februar&amp;quot;; ... ; &amp;quot;Dezember&amp;quot;} &amp;amp;rarr; {&amp;quot;J&amp;quot;; &amp;quot;F&amp;quot;; &amp;quot;M&amp;quot;; &amp;quot;A&amp;quot;; &amp;quot;M&amp;quot;; &amp;quot;J&amp;quot;; &amp;quot;J&amp;quot;; &amp;quot;A&amp;quot;; &amp;quot;S&amp;quot;; &amp;quot;O&amp;quot;; &amp;quot;N&amp;quot;; &amp;quot;D&amp;quot;}&lt;br /&gt;
:Dabei enstehen viele Kollisionen (J wird 3x verwendet, M 2x, A 2x), die gewählte ist also keine gute Hashfunktion&lt;br /&gt;
* Benutzt man als Hashfunktion die ersten 3 Buchstaben benötigt man 18 bit, M = &amp;lt;math&amp;gt;2^{18}&amp;lt;/math&amp;gt; &lt;br /&gt;
:::{&amp;quot;Januar&amp;quot;; &amp;quot;Februar&amp;quot;; ... ; &amp;quot;Dezember&amp;quot;} &amp;amp;rarr; {&amp;quot;Jan&amp;quot;, &amp;quot;Feb&amp;quot;, &amp;quot;März&amp;quot;, &amp;quot;Apr&amp;quot;, &amp;quot;Mai&amp;quot;, &amp;quot;Jun&amp;quot;, &amp;quot;Jul&amp;quot;, &amp;quot;Aug&amp;quot;, &amp;quot;Sep&amp;quot;, &amp;quot;Okt&amp;quot;, &amp;quot;Nov&amp;quot;, &amp;quot;Dez&amp;quot;}&lt;br /&gt;
:Nun entstehen keine Kollision mehr. Diese Hashfunktion ist deshalb beim Ausfüllen von Formularen und dergleichen sehr beliebt. Dafür ist M aber recht groß.&lt;br /&gt;
&lt;br /&gt;
Die Aufgabe wird also präzisiert: man sucht für &amp;lt;math&amp;gt;U_f&amp;lt;/math&amp;gt; eine '''minimale, perfekte Hashfunktion''', für die &amp;lt;math&amp;gt;|U_f| = M&amp;lt;/math&amp;gt; gilt. Ein Verfahren hierfür ist Gegenstand von Übungsblatt 9.&lt;br /&gt;
&lt;br /&gt;
====Universelles Hashing====&lt;br /&gt;
&lt;br /&gt;
Hier wählt man für eine gegebene Hashtabelle die Hashfunktion per Zufallszahl aus einer (großen) Menge erlaubter Hashfunktion &amp;amp;rarr; Die Wahrscheinlichkeit, dass die Hashfunktion für die Schlüssel ungünstig ist, wird dadruch minimiert. Die oben erwähnte denial-of-service-Attacke ist jetzt nicht mehr möglich, weil kein Hacker die Hashfunktion im voraus kennen kann. Näheres zum universellen Hashing finden Sie in der [http://en.wikipedia.org/wiki/Universal_hashing Wikpedia].&lt;br /&gt;
&lt;br /&gt;
====Kryptographische Hashfunktionen====&lt;br /&gt;
&lt;br /&gt;
In kryptographischen Anwendungen treten neben dem Hauptziel, die Größe des Universums auf eine überschaubare Zahl von Integer-Werten zu reduzieren, zwei weitere Anforderungen, die für Verschlüsselung bzw. verschlüsselte Kommunikation wichtig sind: erstens will man Kollisionen unbedingt vermeiden (damit zwei verschiedene Dokumente oder Passwörter nicht auf den gleichen Hashwert abgebildet werden), und zweitens darf es nicht möglich sein, aus dem Hashwert die urpsrüngliche Nachricht (also das Dokument oder Passwort) zu rekonstruieren. Man wählt deshalb relative große M (128 bit und mehr) sowie spezielle, für diesen Zweck optimierte Hashfunktionen, wie z.B. [http://de.wikipedia.org/wiki/Message-Digest_Algorithm_5 md5] und [http://de.wikipedia.org/wiki/SHA1 sha1]. Weitere Einzelheiten finden Sie in der [http://en.wikipedia.org/wiki/Cryptographic_hash_function Wikipedia].&lt;br /&gt;
&lt;br /&gt;
====Beliebte Standard-Hashfunktionen====&lt;br /&gt;
&lt;br /&gt;
In der Praxis definiert man Hashfunktionen gewöhnlich zweistufig: Zunächst bildet man den Schlüssel auf einen 32 bit Integerwert ab, M' ist damit 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;. Dieser &amp;quot;rohe&amp;quot; Hashwert wird dann mittels der Modulo-Operation auf die eigentliche Größe M des assoziativen Arrays abgebildet:&lt;br /&gt;
:::&amp;lt;math&amp;gt; f(u \in U) = f'(u \in U)\,\%\,M\,=\,h \in [0, 1, \ldots, M-1] &amp;lt;/math&amp;gt;&lt;br /&gt;
mit&lt;br /&gt;
:::&amp;lt;math&amp;gt; f'(u \in U) = h' \in [0, 1, \ldots, 2^{32}-1] &amp;lt;/math&amp;gt;&lt;br /&gt;
Der große Wert von M' sichert, dass man bei der Wahl von M großen Spielraum hat, so dass die Größe des assoziativen Arrays sehr gut an die Menge der zu speichernden Daten angepaßt werden kann. Die Funktion f'(u) definiert man wie folgt:&lt;br /&gt;
* Falls U = &amp;lt;tt&amp;gt;unsigned int&amp;lt;/tt&amp;gt; (32bit int Datentyp) &amp;amp;rArr; f'(u) = u&lt;br /&gt;
* Falls U = &amp;lt;tt&amp;gt;signed int&amp;lt;/tt&amp;gt; &amp;amp;rArr; Typkonvertierung nach &amp;lt;tt&amp;gt;unsigned int&amp;lt;/tt&amp;gt; &amp;amp;rArr; f'(u) = (unsigned int)u&lt;br /&gt;
* Andere Schlüsseltypen (also insbesondere Strings) interpretiert man als Array of byte &amp;amp;rArr; f'(u) konvertiert Array of Byte nach &amp;lt;tt&amp;gt;unsigned int&amp;lt;/tt&amp;gt;. Beispiele für solche Funktionen:&lt;br /&gt;
:: '''Bernsteinfunktion:'''&lt;br /&gt;
     def bHash(u):     # u: Array of Byte&lt;br /&gt;
         h=0&lt;br /&gt;
         for k in u:&lt;br /&gt;
             h = 33 * h + k &lt;br /&gt;
         return h&lt;br /&gt;
:: '''modifizierte Bernsteinfunktion:'''&lt;br /&gt;
     def mbHash(u):    # u: Array of Byte&lt;br /&gt;
         h=0&lt;br /&gt;
         for k in u:&lt;br /&gt;
             h = (33 * h) ^ k  # ^ ist bitweises Xor&lt;br /&gt;
         return h&lt;br /&gt;
:: '''Shift-Add-Xor-Funktion:'''&lt;br /&gt;
     def saxhash(u):   # u: Array of Byte&lt;br /&gt;
         h=0&lt;br /&gt;
         for k in u:&lt;br /&gt;
             h ^= (h &amp;lt;&amp;lt; 5) + (h &amp;gt;&amp;gt; 2) + k  # &amp;lt;&amp;lt; und &amp;gt;&amp;gt; sind Links- bzw. Rechtsshift der Bits, ^= ist bitweise Xor-Zuweisung&lt;br /&gt;
         return h&lt;br /&gt;
:: '''Fowler/Noll/Vo-Funktion:'''&lt;br /&gt;
     def FNVhash(u):   # u: Array of Byte&lt;br /&gt;
         h = 2166136261&lt;br /&gt;
         for k in u:&lt;br /&gt;
             h = (16777619 * h) ^ k   # ähnlich der modifizierten Bernsteinfunktion, aber mit anderen Konstanten&lt;br /&gt;
         return h&lt;br /&gt;
:: Die verwendeten Konstanten sind experimentell so gewählt worden, dass die Hashfunktionen in typischen Praxisanwendungen relativ wenige Kollisionen verursachen. Der tiefere Grund, warum z.B. 33 in der Bernsteinfunktion eine gute Wahl darstellt, ist unbekannt. Es empfielt sich, in einer gegebenen Anwendung mit mehreren Hashfunktionen zu experimentieren. Weitere solche Funktionen und andere nützliche Informationen findet man auf der Seite [http://www.eternallyconfuzzled.com/tuts/algorithms/jsw_tut_hashing.aspx eternallyconfuzzled.com].&lt;br /&gt;
&lt;br /&gt;
== Hashtabellen ==&lt;br /&gt;
&lt;br /&gt;
Eine Hashtabelle ist eine Datenstruktur, die die Funktionalität des assoziativen Arrays mit Hilfe von Hashing realisiert. Das Grundprinzip besteht darin, dass die Hashtabelle intern ein (dynamisches) Array der Größe &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; verwaltet, so dass die Hashwerte als Indizes in diesem Array verwendet werden können (&amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; entspricht der Zahl M aus der mathematischen Definition oben). Eine naive Implementation der Einfügeoperation sieht also so aus &lt;br /&gt;
     def __setitem__(self, key, value):    # naive Implementation, funktioniert so nicht&lt;br /&gt;
         index = self.hash(key) % self.capacity&lt;br /&gt;
         self.array[index] = value&lt;br /&gt;
Diese Implementation ist allerdings zu einfach. Wenn nämlich die Schlüssel aus dem Universum U beliebig gewählt werden dürfen, sind Kollisionen unvermeidlich. Tritt aber eine Kollision auf, werden die Daten eines Schlüssels mit den Daten eines anderen Schlüssels überschrieben. Um Kollisionen geschickt zu behandeln gibt es zwei Ansätze:&lt;br /&gt;
* lineare Verkettung&lt;br /&gt;
* offene Adressierung&lt;br /&gt;
&lt;br /&gt;
=== Hashtabelle mit linearer Verkettung (offenes Hashing/geschlossene Adressierung) ===&lt;br /&gt;
&lt;br /&gt;
Man kann dies als die pessimistische Lösung bezeichnen: Man nimmt an, dass Kollisionen häufig auftreten. Deshalb wird unter jedem&lt;br /&gt;
Hashindex gleich eine Liste angelegt, in der Einträge mit gleichem Hashindex aufgenommen werden können. Die Hashtabelle verwaltet ein Array von Listen, und jedes Arrayfeld kann beliebig viele Elemente speichern: Wird ein Element auf den Index &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; abgebildet, werden die Daten einfach an die betreffende Liste angehängt. Bei Zugriff auf ein Element wird zunächst die passende Liste gesucht (mit Hilfe des Hashwerts), danach erfolgt in dieser Liste eine sequentielle Suche nach dem richtigen Schlüssel.&lt;br /&gt;
&lt;br /&gt;
Um diese Idee implementieren zu können, benötigen wir zunächst eine Hilfsklasse &amp;lt;tt&amp;gt;HashNode&amp;lt;/tt&amp;gt;, die (Schlüssel, Wert)-Paare speichert und mit Hilfe von &amp;lt;tt&amp;gt;next&amp;lt;/tt&amp;gt; eine verkettete Liste realisiert:&lt;br /&gt;
  class HashNode:&lt;br /&gt;
      def __init__(self,key,data,next):&lt;br /&gt;
          self.key = key&lt;br /&gt;
          self.data = data&lt;br /&gt;
          self.next = next    # Verkettung!&lt;br /&gt;
Die eigentliche Hashtabelle wird in der Klasse ''HashTable'' implementiert:&lt;br /&gt;
  class HashTable:&lt;br /&gt;
      def __init__(self):&lt;br /&gt;
         self.capacity = ... # Geeignete Werte siehe unten&lt;br /&gt;
         self.size = 0       # Anzahl der Werte, die zur Zeit tatsächlich gespeichert sind&lt;br /&gt;
         self.array = [None]*self.capacity&lt;br /&gt;
Wie oben bereits erwähnt, werden die Zugriffsoperatoren ''[ ]'' für eine Datenstruktur in Python durch die Funktionen &amp;lt;tt&amp;gt;__setitem__&amp;lt;/tt&amp;gt; bzw. &amp;lt;tt&amp;gt;__getitem__&amp;lt;/tt&amp;gt; implementiert. &lt;br /&gt;
Die &amp;lt;tt&amp;gt;__setitem__&amp;lt;/tt&amp;gt;-Funktion speichert die gegebenen Daten unter dem Schlüssel &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; in der &amp;lt;tt&amp;gt;HashTable&amp;lt;/tt&amp;gt;-Klasse:&lt;br /&gt;
     def __setitem__(self, key, value):&lt;br /&gt;
         index = hash(key) % self.capacity  # hash(...) ist in Python eine vordefinierte Funktion&lt;br /&gt;
         node  = self.array[index]          # finde die zu 'key' gehörende Liste&lt;br /&gt;
         while node is not None:            # sequentielle Suche nach 'key' in dieser Liste&lt;br /&gt;
             if node.key == key:&lt;br /&gt;
                 # Element 'key' ist schon in der Tabelle&lt;br /&gt;
                 # =&amp;gt; überschreibe die Daten mit dem neuen Wert&lt;br /&gt;
                 node.data = value&lt;br /&gt;
                 return&lt;br /&gt;
             # andernfalls: Kollision des Hashwerts, probiere nächsten 'key' aus&lt;br /&gt;
             node = node.next&lt;br /&gt;
         # kein Element hatte den richtigen Schlüssel.&lt;br /&gt;
         # =&amp;gt; es gibt diesen Schlüssel noch nicht&lt;br /&gt;
         #    füge also ein neues Element in die Hashtabelle ein&lt;br /&gt;
         self.array[index] = HashNode(key, value, self.array[index]) # der alte Anfang der Liste wird zum&lt;br /&gt;
                                                                     # Nachfolger des neu eingefügten ersten Elements&lt;br /&gt;
         self.size += 1&lt;br /&gt;
         ... # eventuell muss jetzt noch die Kapazität optimiert werden&lt;br /&gt;
Die Funktion &amp;lt;tt&amp;gt;__getitem__&amp;lt;/tt&amp;gt; gibt die unter dem Schlüssel &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; abgelegten Daten zurück, oder eine Fehlermeldung, falls dieser Schlüssel nicht existiert:&lt;br /&gt;
     def __getitem__(self, key):&lt;br /&gt;
         index = hash(key) % self.capacity&lt;br /&gt;
         node = self.array[index]     # finde die zu 'key' gehörende Liste&lt;br /&gt;
         while node is not None:      # sequentielle Suche nach 'key' in dieser Liste&lt;br /&gt;
              if node.key == key:     # gefunden!&lt;br /&gt;
                  return node.data    # =&amp;gt; Daten zurückgeben&lt;br /&gt;
              node = node.next        # nächsten Schlüssel probieren&lt;br /&gt;
         raise KeyError(key)          # Schlüssel nicht gefunden =&amp;gt; Fehler&lt;br /&gt;
&lt;br /&gt;
==== Komplexität der linearen Verkettung und Wahl der Kapazität ====&lt;br /&gt;
&lt;br /&gt;
Die Komplexität wird durch zwei Operationen bestimmt: erstens das Auffinden der zu einem Schlüssel gehörenden Liste (die in O(1) erfolgt), zweitens das sequentielle Durchsuchen der Liste, die Zeit in O(L) erfordert, wobei L die mittlere Länge der Listen ist. Die Hashtabelle ist also nur schnell, wenn die Länge der Listen möglichst klein ist. Unter der Annahme des ''uniformen Hashings'', wenn also alle Indizes gleich häufig verwendet werden, ist L gleich dem '''Füllstand''' der Hashtabelle:&lt;br /&gt;
:::&amp;lt;math&amp;gt;\alpha = \frac{N}{M} = \frac{\text{size}}{\text{capacity}}&amp;lt;/math&amp;gt; wobei N die Größe &amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt; der Hashtabelle und M die Größe &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; des Arrays ist.&lt;br /&gt;
Wenn die Hashwerte uniform sind, entfallen auf jede Liste im Mittel N/M Einträge (N Einträge, verteilt auf M Listen). Die Gesamtkomplexität berechnet sich nach der Sequenzregel zu&lt;br /&gt;
:::&amp;lt;math&amp;gt;O(1+\alpha)&amp;lt;/math&amp;gt;&lt;br /&gt;
Für eine effiziente Suche muss demnach &amp;lt;math&amp;gt;\alpha \in O(1)&amp;lt;/math&amp;gt; gewählt werden. Dies erreicht man, indem man, wie beim dynamischen Array, &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; immer wieder anpasst, falls &amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt; zu groß wird. Üblicherweise verdoppelt man &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt;, sobald &amp;lt;tt&amp;gt;size == capacity&amp;lt;/tt&amp;gt; erreicht wird. Analog zum dynamischen Array werden die Daten dann aus dem alten Array (&amp;lt;tt&amp;gt;self.array&amp;lt;/tt&amp;gt;) in ein entsprechend vergrößertes neues Array kopiert.&lt;br /&gt;
&lt;br /&gt;
In der C++ Standardbibliothek (Klasse &amp;lt;tt&amp;gt; [http://www.cplusplus.com/reference/stl/unordered_map/ std::unordered_map]&amp;lt;/tt&amp;gt;, siehe auch [http://gcc.gnu.org/viewcvs/trunk/libstdc%2B%2B-v3/src/shared/hashtable-aux.cc?view=markup GCC hashtable_aux.cc (Primzahlen)] und [http://gcc.gnu.org/viewcvs/trunk/libstdc%2B%2B-v3/include/bits/hashtable_policy.h?view=markup GCC Hash Implementation]) wird die Hashtabelle häufig so&lt;br /&gt;
implementiert. Dabei wird &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; immer als ''Primzahl'' gewählt, wobei sich aufeinanderfolgende Kapazitäten immer ungefähr verdoppeln. Dazu wählt man aus einer vorberechneten Liste von Primzahlen die kleinste Zahl, so dass &amp;lt;tT&amp;gt;new_capacity &amp;gt;= 2*capacity&amp;lt;/tt&amp;gt; gilt, und beginnt z.B. mit einer Default-Kapazität von 11:&lt;br /&gt;
  11, 23, 47, 97, 199, 409, 823, ...&lt;br /&gt;
Die Wahl von Primzahlen hat zur Folge, dass &amp;lt;tt&amp;gt;hash(key) % self.capacity&amp;lt;/tt&amp;gt; ''alle'' Bits von h benutzt (Eigenschaft aus der Zahlentheorie). Die Kapazität wird vergrößert, wenn &amp;lt;tt&amp;gt;size == capacity&amp;lt;/tt&amp;gt; erreicht wird, und die ungefähre Verdoppelung sichert, dass die amortisierte Komplexität der Einfügeoperation in O(1) ist (wie beim dynamischen Array).&lt;br /&gt;
&lt;br /&gt;
=== Hashtabelle mit offener Adressierung (geschlossenes Hashing) ===&lt;br /&gt;
[[Image:HASHTB12.svg.png|frame|Prinzip ([http://en.wikipedia.org/wiki/Hash_table Quelle])]]&lt;br /&gt;
&lt;br /&gt;
Dies kann als die optimistische Variante betrachtet werden: man nimmt an, dass Kollisionen nicht so häufig auftreten, um eine komplexe Datenstruktur wie das &amp;quot;Array von Listen&amp;quot; zu rechtfertigen. Stattdessen behandelt man Kollisionen mit einer einfachen '''Idee''': Wenn &amp;lt;tt&amp;gt;array[index]&amp;lt;/tt&amp;gt; durch Kollision bereits vergeben ist, probiere einen&lt;br /&gt;
anderen Index aus (siehe auch [http://de.wikipedia.org/wiki/Hashtabelle#Hashing_mit_offener_Adressierung Wikipedia (de)] und&lt;br /&gt;
[http://en.wikipedia.org/wiki/Open_addressing Wikipedia (en)]). Dabei muss man folgendes beachten:&lt;br /&gt;
&lt;br /&gt;
* Das Array enthält pro Element höchstens ein (key,value)-Paar&lt;br /&gt;
* Das Array muss stets mindestens ''einen'' freien Platz haben (sonst gäbe es beim Ausprobieren anderer Indizes eine Endlosschleife). Es gilt immer &amp;lt;tt&amp;gt;self.size &amp;lt; self.capacity&amp;lt;/tt&amp;gt;. Dies war bei der vorigen Hash-Implementation mit linearer Verkettung nicht notwendig (aber im Sinne schneller Zugriffszeiten trotzdem wünschenswert).&lt;br /&gt;
&lt;br /&gt;
==== Vorgehen bei Kollisionen ====&lt;br /&gt;
&lt;br /&gt;
=====Sequentielles Sondieren=====&lt;br /&gt;
&lt;br /&gt;
Probiere den nächsten Index: &amp;lt;tt&amp;gt;index = (index+1) % capacity&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Vorteil: einfach&lt;br /&gt;
* Nachteil: Clusterbildung&lt;br /&gt;
&lt;br /&gt;
Clusterbildung heißt, dass sich größere zusammenhängende Bereiche bilden die belegt sind, unterbrochen von Bereichen die komplett frei sind. Beim Versuch des Einfügens eines Elements an einen Platz, der schon belegt ist, muss jetzt das ganze Cluster sequentiell durchlaufen werden, bis ein freier Platz gefunden wird. Damit entspricht die Komplexität der Suche der mittleren Länge der belegten Bereiche, was sich entsprechend in einer langsamen Suche widerspiegelt.&lt;br /&gt;
&lt;br /&gt;
=====Doppeltes Hashing=====&lt;br /&gt;
&lt;br /&gt;
[http://de.wikipedia.org/wiki/Doppel-Hashing Wikipedia (de)] [http://en.wikipedia.org/wiki/Double_hashing Wikipedia (en)]&lt;br /&gt;
&lt;br /&gt;
Bestimme einen neuen Index (bei Kollisionen) durch eine ''2. Hashfunktion''.&lt;br /&gt;
&lt;br /&gt;
Das doppelte Hashing wird typischerweise in der Praxis angewendet und liegt auch der Python Implementierung des Datentyps [http://docs.python.org/tut/node7.html#SECTION007500000000000000000 Dictionary] (Syntax &amp;lt;tt&amp;gt;{'a':1, 'b':2, 'c':3}&amp;lt;/tt&amp;gt; zugrunde.&lt;br /&gt;
&lt;br /&gt;
Eine effiziente Implementierung dieses Datentyps ist für die Performance der Skriptsprache Python extrem wichtig, da z.B. beim Aufruf einer Funktion der auszuführunde Code in einem Dictionary unter dem Schlüssel ''Funktionsname'' nachgeschlagen wird oder die Werte lokaler Variablen innerhalb einer Funktion ebenfalls in einem Dictionary zu finden sind.&lt;br /&gt;
&lt;br /&gt;
Für die Implementierung in Python werden wieder die obigen Klassen &amp;lt;tt&amp;gt;HashNode&amp;lt;/tt&amp;gt; (das Attribut &amp;lt;tt&amp;gt;next&amp;lt;/tt&amp;gt; kann allerdings jetzt entfernt werden) und &amp;lt;tt&amp;gt;HashTable&amp;lt;/tt&amp;gt; benötigt, es folgen die angepassten Implementationen von &amp;lt;tt&amp;gt;__setitem__&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;__getitem__&amp;lt;/tt&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
  def __setitem__(self, key, value):&lt;br /&gt;
      h = hash(key)&lt;br /&gt;
      index = h % self.capacity&lt;br /&gt;
      # erste Schleife: teste, ob key schon vorhanden ist&lt;br /&gt;
      while True:&lt;br /&gt;
          if self.array[index] is None:     # freies Feld gefunden =&amp;gt; key nicht vorhanden&lt;br /&gt;
               break&lt;br /&gt;
          if self.array[index].key == key:  # key gefunden =&amp;gt; Daten aktualisieren&lt;br /&gt;
               self.array[index].data = value&lt;br /&gt;
               return&lt;br /&gt;
          # self.array[index].key ist anderer Schlüssel oder als gelöscht markiert&lt;br /&gt;
          # =&amp;gt; neuen Index durch 2. Hashfunktion berechnen&lt;br /&gt;
          index = (5*index+1+h) % self.capacity&lt;br /&gt;
          h = h &amp;gt;&amp;gt; 5&lt;br /&gt;
      # wenn wir hier landen, wurde key nicht gefunden&lt;br /&gt;
      h = hash(key)&lt;br /&gt;
      index = h % self.capacity&lt;br /&gt;
      # zweite Schleife: neues Element einfügen&lt;br /&gt;
      while True:&lt;br /&gt;
          if self.array[index] is None or self.array[index].key is None:&lt;br /&gt;
               # index ist frei (1. Bedingung) oder als gelöscht markiert (2. Bedingung)&lt;br /&gt;
               # =&amp;gt; hier gehört key hin&lt;br /&gt;
               self.array[index] = HashNode(key, value)&lt;br /&gt;
               self.size +=1&lt;br /&gt;
               ... # eventuell muss hier die Kapazität optimiert werden&lt;br /&gt;
               return&lt;br /&gt;
          # index ist schon belegt =&amp;gt; neuen Index durch 2. Hashfunktion berechnen&lt;br /&gt;
          index = (5*index+1+h) % self.capacity&lt;br /&gt;
          h = h // 5&lt;br /&gt;
&lt;br /&gt;
Wir nehmen bei dieser Implementation an, dass gelöschte Elemente dadurch markiert werden, dass &amp;lt;tt&amp;gt;self.array[index].key&amp;lt;/tt&amp;gt; auf einen Schlüssel gesetzt wird, der sonst nicht vorkommen kann (z.B. &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;). Dann wird die if-Abfrage &amp;lt;tt&amp;gt;self.array[index].key == key&amp;lt;/tt&amp;gt; niemals wahr, und es wird weitergesucht. Würde man hingegen das Element vollständig löschen, könnte die Bedingung &amp;lt;tt&amp;gt;self.array[index] is None&amp;lt;/tt&amp;gt; zu früh wahr werden, so dass die Schleife vorzeitig abgebrochen und das vorhandene Element &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; nicht erreicht würde.&lt;br /&gt;
 &lt;br /&gt;
 def __getitem__(self, key):&lt;br /&gt;
     h = hash(key)&lt;br /&gt;
     index = h % self.capacity&lt;br /&gt;
     while True:&lt;br /&gt;
         if self.array[index] is None:    # die Suchkette bricht ab =&amp;gt; key existiert nicht&lt;br /&gt;
             raise KeyError(key)&lt;br /&gt;
         if self.array[index].key == key: # key gefunden =&amp;gt; zugehörige Daten zurückgeben&lt;br /&gt;
             return self.array[index].data&lt;br /&gt;
         # index enthält nicht den passenden kay =&amp;gt; neuen Index durch 2. Hashfunktion berechnen&lt;br /&gt;
         index = (5*index+1+h) % self.capacity&lt;br /&gt;
         h = h // 5&lt;br /&gt;
&lt;br /&gt;
Die vorgestellte Implementierung orientiert sich an Pythons interner Dictionary Implementierung, der zugehörige Quellcode (mit ausführlichem Kommentar) findet sich im File [https://github.com/python/cpython/blob/master/Objects/dictobject.c dictobject.c] der Python Implementation.&lt;br /&gt;
&lt;br /&gt;
===== Beispiel für doppeltes Hashing =====&lt;br /&gt;
&lt;br /&gt;
Der Übersichtlichkeit wegen wählen wir M'=2&amp;lt;sup&amp;gt;5&amp;lt;/sup&amp;gt; (statt 2&amp;lt;sup&amp;gt;32&amp;lt;/sup&amp;gt;) und eine Kapazität von M=8.&lt;br /&gt;
&lt;br /&gt;
Roher Hashwert (für das Beispiel willkürlich gewählt):&lt;br /&gt;
  h=25&lt;br /&gt;
Erster Index:&lt;br /&gt;
  i0 = h % capacity = 25 % 8 = 1&lt;br /&gt;
Es finde eine Kollision statt. Es wird ein zweiter Index berechnet:&lt;br /&gt;
  i1 = (5*i0 + 1 + h) % 8 = (5*1 + 1 + 25) % 8 = 31 % 8 = 7&lt;br /&gt;
Der Hashwert wird aktualisiert um die höherwertigen Bits von &amp;lt;tt&amp;gt;h&amp;lt;/tt&amp;gt; ins Spiel zu bringen (hier durch &amp;lt;tt&amp;gt;h &amp;gt;&amp;gt; 2&amp;lt;/tt&amp;gt; anstelle von &amp;lt;tt&amp;gt;h &amp;gt;&amp;gt; 5&amp;lt;/tt&amp;gt; im originalen Pythoncode). Wir stellen &amp;lt;tt&amp;gt;h&amp;lt;/tt&amp;gt; als Binärzahl dar, damit der Rechtsshift besser sichtbar wird:&lt;br /&gt;
  h = h &amp;gt;&amp;gt; 2 &lt;br /&gt;
  ==&amp;gt; h = (11001 &amp;gt;&amp;gt; 2) = 00110 = 6&lt;br /&gt;
Es finde wieder eine Kollision statt, so dass ein dritter Index berechnet werden muss.&lt;br /&gt;
  i2 = (5*i1 + 1 + h) % 8 = (5*7 + 1 + 6) % 8 = 42 % 8 = 2&lt;br /&gt;
Der Hashwert wird wiederum aktualisiert:&lt;br /&gt;
  h = h &amp;gt;&amp;gt; 2&lt;br /&gt;
  ==&amp;gt; h = (00110 &amp;gt;&amp;gt; 2) = 00001 = 1&lt;br /&gt;
Es finde eine Kollision statt, und wir berechnen den vierten Index:&lt;br /&gt;
  i3 = (5*i2 + 1 + h) % 8 = (5*2 + 1 + 1) % 8 = 12 % 8 = 4&lt;br /&gt;
Der Hashwert wird nochmals aktualisiert und erreicht jetzt den Wert 0 (der sich dann nicht mehr ändert):&lt;br /&gt;
  h = h &amp;gt;&amp;gt; 2 &lt;br /&gt;
  ==&amp;gt; h = (00110 &amp;gt;&amp;gt; 2) = 0&lt;br /&gt;
Es finde eine Kollision statt. Da jetzt &amp;lt;tt&amp;gt;h = 0&amp;lt;/tt&amp;gt; gilt, und die Zahlen 5 (Multiplikator) und 8 (capacity) teilerfremd sind, werden ab jetzt systematisch alle Indizes von 0 bis 7 durchprobiert (in der durch die Modulo-Operation bestimmten Reihenfolge):&lt;br /&gt;
  i4  = (5*i3  + 1 + h) % 8 = (5*4 + 1 + 0) % 8 = 21 % 8 = 5&lt;br /&gt;
  i5  = (5*i4  + 1 + h) % 8 = (5*5 + 1 + 0) % 8 = 26 % 8 = 2&lt;br /&gt;
  i6  = (5*i5  + 1 + h) % 8 = (5*2 + 1 + 0) % 8 = 11 &amp;amp; 8 = 3&lt;br /&gt;
  i7  = (5*i6  + 1 + h) % 8 = (5*3 + 1 + 0) % 8 = 16 &amp;amp; 8 = 0&lt;br /&gt;
  i8  = (5*i7  + 1 + h) % 8 = (5*0 + 1 + 0) % 8 =  1 &amp;amp; 8 = 1&lt;br /&gt;
  i9  = (5*i8  + 1 + h) % 8 = (5*1 + 1 + 0) % 8 =  6 &amp;amp; 8 = 6&lt;br /&gt;
  i10 = (5*i9  + 1 + h) % 8 = (5*6 + 1 + 0) % 8 = 31 &amp;amp; 8 = 7&lt;br /&gt;
  i11 = (5*i10 + 1 + h) % 8 = (5*7 + 1 + 0) % 8 = 36 &amp;amp; 8 = 4&lt;br /&gt;
Allen Indizes werden also erreicht, bevor sich die Folge wiederholt. Da man &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; immer so wählt, dass mindestens ein Arrayfeld noch frei ist, wird dadurch immer ein geeigneter Platz für das einzufügende Element gefunden.&lt;br /&gt;
&lt;br /&gt;
==== Komplexität der offenen Adressierung ====&lt;br /&gt;
&lt;br /&gt;
* Annahme: uniformes Hashing, das heißt alle Indizes haben gleiche Wahrscheinlichkeit&lt;br /&gt;
* Füllstand &amp;lt;math&amp;gt;\alpha =\frac{N}{M} = \frac{\text{size}}{\text{capacity}}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* '''Erfolglose Suche''' (d.h. es wird entweder ein neues Element eingefügt oder ein &amp;lt;tt&amp;gt;KeyError&amp;lt;/tt&amp;gt; geworfen): Untere Schranke für die Komplexität ist &amp;lt;math&amp;gt;\Omega\left(\frac{1}{1-\alpha}\right)&amp;lt;/math&amp;gt; Schritte (= Anzahl der notwendigen index-Berechnungen).&lt;br /&gt;
* '''Erfolgreiche Suche''' &amp;lt;math&amp;gt;\Omega\left(\frac{1}{\alpha}\ln\left(\frac{1} {1-\alpha}\right)\right)&amp;lt;/math&amp;gt; Schritte.&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;
! &amp;lt;math&amp;gt;\alpha&amp;lt;/math&amp;gt;&lt;br /&gt;
! 0.5&lt;br /&gt;
! 0.9&lt;br /&gt;
|- &lt;br /&gt;
| erfolglos&lt;br /&gt;
| 2.0&lt;br /&gt;
| 10&lt;br /&gt;
|-&lt;br /&gt;
| erfolgreich&lt;br /&gt;
| 1.4&lt;br /&gt;
| 2.6&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== Wahl der Kapazität ====&lt;br /&gt;
Man sieht an der obigen Tabelle, dass die erfolglose Suche (und damit das Einfügen) sehr langsam wird, wenn der Füllstand hoch ist. In Python wird &amp;lt;tt&amp;gt;capacity&amp;lt;/tt&amp;gt; deshalb so gewählt, dass &amp;lt;math&amp;gt;\alpha \leq 2/3&amp;lt;/math&amp;gt;. Falls &amp;lt;math&amp;gt;\alpha&amp;lt;/math&amp;gt; größer werden sollte, verdopple die Kapazität und kopiere das alte array in das neue Array (analog zum dynamischen Array)&lt;br /&gt;
&lt;br /&gt;
In Python werden die Kapazitätsgrößen als Zweierpotenzen gewählt, also 4,8,16,32,...,&lt;br /&gt;
so dass &amp;lt;tt&amp;gt;h % self.capacity&amp;lt;/tt&amp;gt; nur die unteren Bits von &amp;lt;tt&amp;gt;h&amp;lt;/tt&amp;gt; benutzt. Die oberen Bits von &amp;lt;tt&amp;gt;h&amp;lt;/tt&amp;gt; kommen erst ins Spiel, wenn bei der Berechnung der 2. Hashfunktion die Aktualisierung &amp;lt;tt&amp;gt;h = h &amp;gt;&amp;gt; 5&amp;lt;/tt&amp;gt; erfolgt. Dies hat sich bei umfangreichen Experimenten als sehr gute Lösung erwiesen.&lt;br /&gt;
&lt;br /&gt;
== Anwendungen von Hashing ==&lt;br /&gt;
&lt;br /&gt;
* Hashtabelle, assoziatives Aray&lt;br /&gt;
* Sortieren in linearer Zeit (Übungsaufgabe 6.2)&lt;br /&gt;
* Suchen von Strings in Texten: Rabin-Karp-Algorithmus&lt;br /&gt;
* ...&lt;br /&gt;
&lt;br /&gt;
=== Rabin Karp Algorithmus ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Rabin-Karp-Algorithmus Wikipedia (de)] [http://en.wikipedia.org/wiki/Rabin-Karp_string_search_algorithm Wikipedia (en)]&lt;br /&gt;
&lt;br /&gt;
In Textverarbeitungsanwendungen ist eine häufig benutzte Funktion die ''Search &amp;amp; Replace'' Funktionalität. Die Suche sollte in O(len(text)) möglich sein, aber ein naiver Algorithmus braucht O(len(text)*len(searchstring))&lt;br /&gt;
&lt;br /&gt;
==== Naive Implementierung der Textsuche ====&lt;br /&gt;
  def search(text, s):&lt;br /&gt;
      M, N = len(text), len(s)&lt;br /&gt;
      for k in range(M-N):&lt;br /&gt;
          if s==text[k:k+N]:   # O(N), da N Zeichen verglichen werden müssen&lt;br /&gt;
              return k&lt;br /&gt;
      return -1 #nicht gefunden&lt;br /&gt;
&lt;br /&gt;
==== Idee des Rabin Karp Algorithmus ====&lt;br /&gt;
Statt Vergleichen &amp;lt;tt&amp;gt;s==text[k:k+N]&amp;lt;/tt&amp;gt;, die O(N) benötigen, weil N Vergleiche der Buchstaben durchgeführt werden müssen, vergleichen wir die Hashs von Suchstring und dem zu untersuchenden Textabschnitt: &amp;lt;tt&amp;gt;hash(s) == hash(text[k:k+N])&amp;lt;/tt&amp;gt;. Dabei muss natürlich &amp;lt;tt&amp;gt;hash(s)&amp;lt;/tt&amp;gt; nur einmal berechnet werden, wohingegen &amp;lt;tt&amp;gt;hash(text[k:k+N])&amp;lt;/tt&amp;gt; immer wieder neu berechnet werden muss. Damit der Vergleich O(1) sein kann, ist es deswegen erforderlich, eine solche Hashfunktion zu haben, die nicht alle Zeichen (das wäre O(N) ) einlesen muss, sondern die den vorhergehenden Hashwert mit einbezieht.&lt;br /&gt;
&lt;br /&gt;
Eine solche Hashfunktion heißt ''Running Hash'' und funktioniert analog zum ''Sliding Mean''.&lt;br /&gt;
&lt;br /&gt;
Die Running Hash Funktion berechnet in O(1) den hash von &amp;lt;tt&amp;gt;text[k+1:k+1+N]&amp;lt;/tt&amp;gt; ausgehend vom hash für &amp;lt;tt&amp;gt;text[k:k+N]&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Idee: Interpretiere den Text als Ziffern in einer base d Darstellung:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;h_k = \text{text}[k]\cdot d^{N-1} + \text{text}[k+1]\cdot d^{N-2} + \cdots + \text{text}[k+N-1]&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Für die Basis 10 (Dezimalsystem) ergibt sich also&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;h_k = \text{text}[k]\cdot {10}^{N-1} + \text{text}[k+1]\cdot {10}^{N-2} + \cdots + \text{text}[k+N-1]&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Daraus folgt&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;h_{k+1} = 10 \cdot h_k - \text {text}[k]\cdot {10}^{N} + \text {text}[k+N]&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Komplexität dieses Updates ist O(1), falls man &amp;lt;math&amp;gt;{10}^{N}&amp;lt;/math&amp;gt; vorberechnet hat.&lt;br /&gt;
&lt;br /&gt;
In der Realität wählt man dann d=32 und benutzt noch an einigen Stellen modulo Operationen, um die Zahlen nicht zu groß werden zu lassen (siehe die Zahl &amp;lt;tt&amp;gt;q&amp;lt;/tt&amp;gt; in der folgenden Implementation).&lt;br /&gt;
&lt;br /&gt;
==== Implementation ====&lt;br /&gt;
  def searchRabinKarp(text, s):&lt;br /&gt;
      M, N = len(text), len(s)&lt;br /&gt;
      d = 32&lt;br /&gt;
      q = 33554393  # q ist eine große Primzahl, aber so,&lt;br /&gt;
                    # dass d*q &amp;lt; 2**32 (um Überlauf bei 32-bit Integerarithmetik zu vermeiden)&lt;br /&gt;
      dN = d**N % q # Vorberechnung des Vorfaktors für das Entfernen aus dem Hash&lt;br /&gt;
      &lt;br /&gt;
      # Initialisierung      &lt;br /&gt;
      hs, ht = 0, 0&lt;br /&gt;
      for k in range(N):&lt;br /&gt;
          # ord() gibt die Zeichen-Nummer (z.B. ASCII- oder UTF-8-Code) des &lt;br /&gt;
          # übergebenen Zeichens zurück&lt;br /&gt;
          hs = (hs*d + ord( s[k] )) % q&lt;br /&gt;
          ht = (ht*d + ord(text[k])) % q&lt;br /&gt;
      # Die Variablen sind jetzt wie folgt initialisiert:&lt;br /&gt;
      # hs = hash(s)&lt;br /&gt;
      # ht = hash(text[0:N])&lt;br /&gt;
      &lt;br /&gt;
      # Hauptschleife&lt;br /&gt;
      k = 0 &lt;br /&gt;
      while k &amp;lt; M-N:&lt;br /&gt;
          if hs == ht:  # übereinstimmende Hashs =&amp;gt; prüfe, dass es nicht nur&lt;br /&gt;
                        # eine Kollision ist&lt;br /&gt;
              if s == text[k:k+N]:  # O(N)-Vergleich nur nötig, wenn Hashs übereinstimmen&lt;br /&gt;
                   return k         # search string an Position k gefunden&lt;br /&gt;
          # nicht gefunden =&amp;gt; aktualisiere Hash für den nächsten Teilabschnitt von text:&lt;br /&gt;
          ht = (d*ht + ord(text[k+N])) % q # neues Zeichen text[k+N] in Hash einfügen&lt;br /&gt;
          ht = (ht - dN*ord(text[k])) % q  # Zeichen text[k] aus Hash entfernen.&lt;br /&gt;
          k +=1&lt;br /&gt;
      return -1  # search string nicht gefunden&lt;br /&gt;
&lt;br /&gt;
[[Iteration versus Rekursion|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Effizienz&amp;diff=5700</id>
		<title>Effizienz</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Effizienz&amp;diff=5700"/>
				<updated>2020-05-26T11:14:16Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* O-Kalkül */&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 [https://en.wikipedia.org/wiki/Profiling_(computer_programming) 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 [https://www.cs.sjsu.edu/~mak/CS185C/KnuthStructuredProgrammingGoTo.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 [https://docs.python.org/3/library/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 Integer- 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 [https://en.wikipedia.org/wiki/Cache_(computing) 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;
Code aus kompilierten Sprachen wie C/C++ 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;
    min = i&lt;br /&gt;
    for j in range(i+1, len(a)):&lt;br /&gt;
      if a[j] &amp;lt; a[min]:&lt;br /&gt;
        min = j&lt;br /&gt;
    a[min], a[i] = a[i], a[min]      # 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) \in \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) \in \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 verwenden 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.next&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. &lt;br /&gt;
&lt;br /&gt;
Dies kann man sehr einfach exakt beweisen: Betrachtet man jede Stelle der Binärzahlen einzeln, erkennt man, dass sich die letzte Stelle (2&amp;lt;sup&amp;gt;0&amp;lt;/sup&amp;gt;) in jedem Schritt ändert und man jedesmal eine Einheit dafür bezahlen muss. Die vorletzte Stelle (2&amp;lt;sup&amp;gt;1&amp;lt;/sup&amp;gt;) ändert sich in jedem zweiten Schritt. Man zahlt also in jedem Schritt durchschnittlich nur 1/2 Einheit. Die drittletzte Stelle (2&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt;) ändert sich in jedem vierten Schritt und verursacht somit durchschnittliche Kosten von 1/4 Einheit usw. Die durchschnittlichen Gesamtkosten pro Schritt kann man durch die unendliche Summe&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;c = 1  + \frac{1}{2} + \frac{1}{4} + \frac{1}{8} + ...&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
berechnen. Dies ist die bekannte Summe der geometrischen Reihe mit &amp;lt;math&amp;gt;q=\frac{1}{2}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;c = \sum_{k=0}^{\infty} q^k = \frac{1}{1-q} = 2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
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 der Accounting Methode====&lt;br /&gt;
&lt;br /&gt;
Um den formalen Beweis zu führen, legen wir fast, dass &amp;lt;i&amp;gt;Kosten&amp;lt;/i&amp;gt; mit positiven Zahlen ausgedrückt werden, während &amp;lt;i&amp;gt;Guthaben&amp;lt;/i&amp;gt; als negative Werte geschrieben werden. Wir definieren also das Guthaben nach Schritt &amp;lt;i&amp;gt;i&amp;lt;/i&amp;gt; als Differenz zwischen Größe und Kapazität des Arrays:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\Phi_i = \mathrm{size}_i - \mathrm{capacity}_i&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Dies kann niemals positiv sein, weil die Anzahl der Elemente des Arrays niemals die Kapazität überschreitet, und entspricht der negierten Anzahl der freien Speicherzellen. Wir zahlen also Guthaben ein, wenn wir mehr Speicher allokieren als zur Zeit benötigt wird, und verbrauchen es, wenn wir neue Elemente in die freien Speicherzellen einfügen.&lt;br /&gt;
&lt;br /&gt;
Bei jeder Einfügung erhöht sich die Arraygröße um ein Element:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\mathrm{size}_i = \mathrm{size}_{i-1}+1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die amortisierten Kosten der Einfügeoperation &amp;lt;math&amp;gt;\hat c_i&amp;lt;/math&amp;gt; setzen sich zusammen aus den tatsächlichen Kosten &amp;lt;math&amp;gt;c_i&amp;lt;/math&amp;gt;  der Operation (der Einfügung des neuen Elements und eventuell dem Umkopieren der vorhandenen Elemente) sowie der Änderung des Guthabens:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\hat c_i = c_i + \Phi_i - \Phi_{i-1}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Durch Änderung des Guthabens können die Kosten der Einfügeoperation kompensiert werden. Wir unterscheiden zwei Fälle:&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 ist kein Umkopieren nötig, da noch Kapazität frei ist. Daher gilt&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\mathrm{capacity}_i = \mathrm{capacity}_{i-1}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die Einfügung kostet nur eine Einheit für das Kopieren des neuen Elements&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;c_i=1&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Einsetzen in die Formel für die amortisierten Kosten liefert:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\hat c_i = 1 + (\mathrm{size}_{i-1} + 1 - \mathrm{capacity}_{i-1}) - (\mathrm{size}_{i-1} - \mathrm{capacity}_{i-1}) = 2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Die amortisierten Kosten betragen somit zwei Einheiten.&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;
Das heißt, vor dem Einfügen gilt&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\mathrm{size}_{i-1} = \mathrm{capacity}_{i-1}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Jetzt muss der Speicher zunächst verdoppelt und die vorhandenen Elemente umkopiert werden. Die Kapazität ändert sich somit nach&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\mathrm{capacity}_i = 2\cdot\mathrm{capacity}_{i-1}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Zu den Kosten für das Kopieren des neuen Elements kommen jetzt die Kosten für das Umkopieren der vorhandenen Elemente (wir nehmen an, dass das Kopieren jedes einzelnen Elements stets eine Einheit kostet):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;c_i=1 + \mathrm{size}_{i-1}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Einsetzen in die Formel für die amortisierten Kosten liefert jetzt:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\hat c_i = (1 + \mathrm{size}_{i-1}) + (\mathrm{size}_{i-1} + 1 - 2\cdot\mathrm{capacity}_{i-1}) - (\mathrm{size}_{i-1} - \mathrm{capacity}_{i-1}) = 2 + \mathrm{size}_{i-1} - \mathrm{capacity}_{i-1}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wegen &amp;lt;math&amp;gt;\mathrm{size}_{i-1} = \mathrm{capacity}_{i-1}&amp;lt;/math&amp;gt; (das Array war vor der Einfügung voll) vereinfacht sich dies aber zu&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\hat c_i = 2&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Auch in diesem Fall betragen die amortisierten Kosten zwei Einheiten. &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 konstante amortisierte Komplexität 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; = 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;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;2&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;0&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;2&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;-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;
|-&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;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;2&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;-3&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;2&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;-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;2&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;-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;
|-&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;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;2&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;-7&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;2&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 amortisierten Kosten, die mit Hilfe des Guthabens berechnet werden, sind hingegen konstant 2, 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>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Container&amp;diff=5699</id>
		<title>Container</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Container&amp;diff=5699"/>
				<updated>2020-04-28T10:56:16Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Required Interfaces */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Abstrakte Datentypen==&lt;br /&gt;
&lt;br /&gt;
Bei einem abstrakten Datentyp wird die Datenstruktur definiert, indem man die Menge der erlaubten Operationen und deren Bedeutung in abstrakter Form (d.h. unabhängig von einer bestimmten Implementation) angibt. Dazu verwendet man im allgemeinen die ''algebraische Spezifikation'', die zunächst die Operationen auflistet und danach deren Eigenschaften in Form von ''Axiomen'' beschreibt, die nach der Ausführung einer Operation jeweils gelten müssen.&lt;br /&gt;
&lt;br /&gt;
Wir unterscheiden folgende Arten von Operationen:&lt;br /&gt;
# Observer: geben Informationen über den Zustand eines Objekts&lt;br /&gt;
# Modifier:&amp;lt;br/&amp;gt;beim funktionalen Programmierstil: erzeugen ein neues, verändertes Objekt&amp;lt;br/&amp;gt;beim prozeduralen und objekt-orientierten Programmierstil: verändern ein vorhandenes Objekt&lt;br /&gt;
# Konstruktoren: erzeugen ein neues Objekt (bei funktionaler Programmierung sind Konstruktoren nur ein Spezialfall der Modifier).&lt;br /&gt;
Wird ein Objekt '''a''' modifiziert, ist sein alter Wert in der Spezifikation unter dem formalen Namen '''a&amp;lt;sub&amp;gt;old&amp;lt;/sub&amp;gt;''' zugreifbar. Dies ermöglicht es, in den Axiomen den alten mit dem neuen Zustand zu vergleichen.&lt;br /&gt;
&lt;br /&gt;
Im folgenden beschreiben wir die algebraische Spezifikation am Beispiel der ''Container-Datenstrukturen''. Container dienen, wie in der Schifffahrt, zum Aufbewahren anderer Datenobjekte und sind damit grundlegend für die Programmierung, siehe auch [http://de.wikipedia.org/wiki/Datenstruktur Datenstrukturen] in der Wikipedia.&lt;br /&gt;
&lt;br /&gt;
==Array==&lt;br /&gt;
&lt;br /&gt;
In einem Array erhalten alle Elemente einen ''Index'', d.h. eine nicht-negative laufende Nummer, die bei Null (&amp;quot;zero-based indexing&amp;quot;, z.B. C++, Python) oder bei Eins (&amp;quot;one-based indexing&amp;quot;, z.B. Fortran, Matlab) startet. Auf die Elemente des Arrays wird zugegriffen, indem man den jeweiligen Index angibt. Wir definieren das Array hier mit zero-based indexing.&lt;br /&gt;
&lt;br /&gt;
Seien a ∈ Array, i ∈ &amp;lt;math&amp;gt;\mathbb{N}_0&amp;lt;/math&amp;gt; (ein nicht-negativer Index) und v ∈ Object (ein beliebiges Objekt).&lt;br /&gt;
&lt;br /&gt;
====Operationen:====&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| erzeuge ein neues Array:&lt;br /&gt;
| &amp;lt;tt&amp;gt;new_array(size ∈ &amp;lt;math&amp;gt;\mathbb{N}_0&amp;lt;/math&amp;gt;, initial ∈ Object) → Array&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|erfrage die Anzahl der Arrayelemente:&lt;br /&gt;
|&amp;lt;tt&amp;gt;len(a) → &amp;lt;math&amp;gt;\mathbb{N}_0&amp;lt;/math&amp;gt;&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|erfrage das Element beim Index i:&lt;br /&gt;
|&amp;lt;tt&amp;gt;get(a, i) → Object&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|setze das Objekt beim Index i:&lt;br /&gt;
|&amp;lt;tt&amp;gt;set(a, i, v) → Array&amp;lt;/tt&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Axiome:====&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| Ein neues Array enthält so viele Elemente, wie in &amp;lt;tt&amp;gt;size&amp;lt;/tt&amp;gt; angegeben waren.&amp;lt;br/&amp;gt;Alle Elemente haben den gegebenen Initialwert &amp;lt;tt&amp;gt;initial&amp;lt;/tt&amp;gt;.&lt;br /&gt;
| &amp;lt;tt&amp;gt;a = new_array(size, initial)&amp;lt;br/&amp;gt;assert(len(a) == size)&amp;lt;/tt&amp;gt;&amp;lt;br/&amp;gt;für alle &amp;lt;tt&amp;gt;i ∈ 0, ..., size-1&amp;lt;/tt&amp;gt; gilt:&amp;lt;br/&amp;gt; &amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;tt&amp;gt;assert(get(a, i) == initial)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
|Nach der Zuweisung von v beim Index i gilt:&amp;lt;br/&amp;gt;(i) die Größe bleibt unverändert,&amp;lt;br/&amp;gt;(ii) Index i enthält das Element v,&amp;lt;br/&amp;gt;(iii) die übrigen Elemente haben sich nicht verändert.&lt;br /&gt;
|&amp;lt;tt&amp;gt;a = set(a, i, v)&amp;lt;br/&amp;gt;assert(len(a) == len(a&amp;lt;sub&amp;gt;old&amp;lt;/sub&amp;gt;))&amp;lt;br/&amp;gt;assert(get(a, i) == v)&amp;lt;/tt&amp;gt;&amp;lt;br/&amp;gt;für alle &amp;lt;tt&amp;gt;k ≠ i&amp;lt;/tt&amp;gt; gilt:&amp;lt;br/&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;tt&amp;gt;assert(get(a, k) == get(a&amp;lt;sub&amp;gt;old&amp;lt;/sub&amp;gt;, k))&amp;lt;/tt&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====in Python:====&lt;br /&gt;
Der Array-Typ heißt &amp;lt;tt&amp;gt;list&amp;lt;/tt&amp;gt; (aus historischen Gründen, das hat nichts mit verketteten Listen zu tun):&lt;br /&gt;
* &amp;lt;tt&amp;gt;get&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;set&amp;lt;/tt&amp;gt; heißen &amp;lt;tt&amp;gt;__getitem__&amp;lt;/tt&amp;gt; bzw. &amp;lt;tt&amp;gt;__setitem__&amp;lt;/tt&amp;gt;&lt;br /&gt;
* rufe Funktionen mit Punktsyntax auf:&amp;lt;br/&amp;gt;statt &amp;lt;tt&amp;gt;get(a, i)&amp;lt;/tt&amp;gt; schreibe &amp;lt;tt&amp;gt;a.__getitem__(i)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* Indexschreibweise:&amp;lt;br/&amp;gt;&amp;lt;tt&amp;gt;v = a[i]&amp;lt;/tt&amp;gt; ist äquivalent zu &amp;lt;tt&amp;gt;v = a.__getitem__(i)&amp;lt;/tt&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;tt&amp;gt;a[i] = v&amp;lt;/tt&amp;gt; ist äquivalent zu &amp;lt;tt&amp;gt;a.__setitem__(i, v)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* Konstruktoren:&amp;lt;br/&amp;gt;&amp;lt;tt&amp;gt;a = list()&amp;lt;/tt&amp;gt; ist äquivalent zu &amp;lt;tt&amp;gt;a = []&amp;lt;/tt&amp;gt; und entspricht &amp;lt;tt&amp;gt;a = new_array(0, 0)&amp;lt;/tt&amp;gt; (erzeugt ein leeres Array)&amp;lt;br/&amp;gt;&amp;lt;tt&amp;gt;a = [initial]*size&amp;lt;/tt&amp;gt; entspricht &amp;lt;tt&amp;gt;a = new_array(size, initial)&amp;lt;/tt&amp;gt; (erzeugt ein Array der gewünschten Größe mit Initialwert)&amp;lt;br/&amp;gt;&lt;br /&gt;
&amp;lt;tt&amp;gt;a = [1, 3, 2, 0, 5]&amp;lt;/tt&amp;gt; initialisiert ein Array mit den gegebenen Elementen.&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Stack==&lt;br /&gt;
&lt;br /&gt;
Ein Stack verhält sich wie ein Stapel (z.B. von Büchern oder Bierkästen) in der realen Welt: nur das jeweils oberste Element ist einfach zugänglich (Funktion &amp;lt;tt&amp;gt;top&amp;lt;/tt&amp;gt;), die darunterliegenden sind blockiert. Ein neues Element wird immer oben auf den Stapel gelegt (Funktion &amp;lt;tt&amp;gt;push&amp;lt;/tt&amp;gt;), und das ''zuletzt'' eingefügte Element wird als ''erstes'' wieder entfernt (Funktion &amp;lt;tt&amp;gt;pop&amp;lt;/tt&amp;gt;). Dies bezeichnet man als &amp;quot;last in - first out&amp;quot;-Verhalten (LIFO).&lt;br /&gt;
&lt;br /&gt;
Seien s ∈ Stack und v ∈ Object (ein beliebiges Objekt).&lt;br /&gt;
&lt;br /&gt;
====Operationen:====&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| erzeuge einen leeren Stack:&lt;br /&gt;
| &amp;lt;tt&amp;gt;new_stack() → Stack&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|erfrage die Anzahl der Stackelemente:&lt;br /&gt;
|&amp;lt;tt&amp;gt;len(s) → &amp;lt;math&amp;gt;\mathbb{N}_0&amp;lt;/math&amp;gt;&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|hänge ein neues Element am Ende an:&lt;br /&gt;
|&amp;lt;tt&amp;gt;push(s, v) → Stack&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|erfrage das letzte Element:&lt;br /&gt;
|&amp;lt;tt&amp;gt;top(s) → Object&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|entferne das letzte Element:&lt;br /&gt;
|&amp;lt;tt&amp;gt;pop(s) → Stack&amp;lt;/tt&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Axiome:====&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| Ein neuer Stack ist leer.&lt;br /&gt;
| &amp;lt;tt&amp;gt;s = new_stack()&amp;lt;br/&amp;gt;assert(len(s) == 0)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| Nach einem &amp;lt;tt&amp;gt;push&amp;lt;/tt&amp;gt; gilt:&amp;lt;br/&amp;gt;(i) die Größe hat sich um eins erhöht,&amp;lt;br/&amp;gt;(ii) das gerade eingefügte Element ist jetzt das letzte.&lt;br /&gt;
|&amp;lt;tt&amp;gt;s = push(s, v)&amp;lt;br/&amp;gt;assert(len(s) == len(s&amp;lt;sub&amp;gt;old&amp;lt;/sub&amp;gt;)+1)&amp;lt;br/&amp;gt;assert(top(s) == v)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| &amp;lt;tt&amp;gt;push&amp;lt;/tt&amp;gt; gefolgt von &amp;lt;tt&amp;gt;pop&amp;lt;/tt&amp;gt; reproduziert den Stack vor dem &amp;lt;tt&amp;gt;push&amp;lt;/tt&amp;gt;.&lt;br /&gt;
|&amp;lt;tt&amp;gt;s = pop(push(s, v))&amp;lt;br/&amp;gt;assert(s == s&amp;lt;sub&amp;gt;old&amp;lt;/sub&amp;gt;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====in Python:====&lt;br /&gt;
Der Typ &amp;lt;tt&amp;gt;list&amp;lt;/tt&amp;gt; ist gleichzeitig ein Stack:&lt;br /&gt;
* &amp;lt;tt&amp;gt;push(s, v)&amp;lt;/tt&amp;gt; heißt &amp;lt;tt&amp;gt;s.append(v)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;pop(s)&amp;lt;/tt&amp;gt; heißt &amp;lt;tt&amp;gt;s.pop()&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;top(s)&amp;lt;/tt&amp;gt; heißt &amp;lt;tt&amp;gt;s[-1]&amp;lt;/tt&amp;gt; (Python unterstützt negative Indizes &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; und interpretiert sie als &amp;lt;tt&amp;gt;s[len(s)-abs(i)]&amp;lt;/tt&amp;gt;, hier also &amp;lt;tt&amp;gt;s[len(s)-1]&amp;lt;/tt&amp;gt;)&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Queue==&lt;br /&gt;
&lt;br /&gt;
Ein Queue realisiert das Verhalten einer Warteschlange (wie z.B. im Supermarkt): das aktuell vorderste Element ist einfach zugänglich (Funktion &amp;lt;tt&amp;gt;first&amp;lt;/tt&amp;gt;), die dahinterliegenden sind blockiert. Ein neues Element wird immer am Ende der Queue angefügt (Funktion &amp;lt;tt&amp;gt;push&amp;lt;/tt&amp;gt;), und das ''zuerst'' eingefügte Element wird als ''erstes'' wieder entfernt (Funktion &amp;lt;tt&amp;gt;popFirst&amp;lt;/tt&amp;gt;). Dies bezeichnet man als &amp;quot;first in - first out&amp;quot;-Verhalten (FIFO).&lt;br /&gt;
&lt;br /&gt;
Seien q ∈ Queue und v ∈ Object (ein beliebiges Objekt).&lt;br /&gt;
&lt;br /&gt;
====Operationen:====&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| erzeuge eine leere Queue:&lt;br /&gt;
| &amp;lt;tt&amp;gt;new_queue() → Queue&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|erfrage die Anzahl der Queueelemente:&lt;br /&gt;
|&amp;lt;tt&amp;gt;len(q) → &amp;lt;math&amp;gt;\mathbb{N}_0&amp;lt;/math&amp;gt;&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|hänge ein neues Element am Ende an:&lt;br /&gt;
|&amp;lt;tt&amp;gt;push(q, v) → Queue&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|erfrage das erste Element:&lt;br /&gt;
|&amp;lt;tt&amp;gt;first(q) → Object&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|entferne das erste Element:&lt;br /&gt;
|&amp;lt;tt&amp;gt;pop(q) → Queue&amp;lt;/tt&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Axiome:====&lt;br /&gt;
Um die Axiome der Queue zu formulieren, brauchen wir zusätzlich die Zugriffsfunktion &amp;lt;tt&amp;gt;get&amp;lt;/tt&amp;gt; des Arrays. Die Queue muss diese Funktion aber nur zum Testen und Debuggen unterstützen, beim Normalbetrieb ist sie nicht notwendig.&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| Eine neue Queue ist leer.&lt;br /&gt;
| &amp;lt;tt&amp;gt;q = new_queue()&amp;lt;br/&amp;gt;assert(len(q) == 0)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| Nach einem &amp;lt;tt&amp;gt;push&amp;lt;/tt&amp;gt; gilt:&amp;lt;br/&amp;gt;(i) die Größe hat sich um eins erhöht,&amp;lt;br/&amp;gt;(ii) das gerade eingefügte Element ist jetzt das letzte,&amp;lt;br/&amp;gt;(iii) die übrigen Elemente haben sich nicht verändert.&lt;br /&gt;
|&amp;lt;tt&amp;gt;q = push(q, v)&amp;lt;br/&amp;gt;assert(len(q) == len(q&amp;lt;sub&amp;gt;old&amp;lt;/sub&amp;gt;)+1)&amp;lt;br/&amp;gt;assert(get(q, len(q)-1) == v)&amp;lt;/tt&amp;gt;&amp;lt;br/&amp;gt;für alle &amp;lt;tt&amp;gt;k ∈ 0, ..., len(q)-2&amp;lt;/tt&amp;gt; gilt:&amp;lt;br/&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;tt&amp;gt;assert(get(q, k) == get(q&amp;lt;sub&amp;gt;old&amp;lt;/sub&amp;gt;, k))&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| Wenn die Queue nicht leer ist, hat das erste Element den Index 0.&lt;br /&gt;
| &amp;lt;tt&amp;gt;assert(len(q) &amp;gt; 0)&amp;lt;br/&amp;gt;assert(first(q) == get(q, 0))&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| Nach einem &amp;lt;tt&amp;gt;popFirst&amp;lt;/tt&amp;gt; gilt:&amp;lt;br/&amp;gt;(i) die Größe hat sich um eins verringert,&amp;lt;br/&amp;gt;(ii) alle Elemente ab dem zweiten rücken einen Index nach vorn.&lt;br /&gt;
|&amp;lt;tt&amp;gt;q = popFirst(q)&amp;lt;br/&amp;gt;assert(len(q) == len(q&amp;lt;sub&amp;gt;old&amp;lt;/sub&amp;gt;)-1)&amp;lt;/tt&amp;gt;&amp;lt;br/&amp;gt;für alle &amp;lt;tt&amp;gt;k ∈ 0, ..., len(q)-1&amp;lt;/tt&amp;gt; gilt:&amp;lt;br/&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;tt&amp;gt;assert(get(q, k) == get(q&amp;lt;sub&amp;gt;old&amp;lt;/sub&amp;gt;, k+1))&amp;lt;/tt&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====in Python:====&lt;br /&gt;
Der Typ &amp;lt;tt&amp;gt;list&amp;lt;/tt&amp;gt; ist gleichzeitig eine Queue:&lt;br /&gt;
* &amp;lt;tt&amp;gt;push(q, v)&amp;lt;/tt&amp;gt; heißt &amp;lt;tt&amp;gt;q.append(v)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;popFirst(q)&amp;lt;/tt&amp;gt; heißt &amp;lt;tt&amp;gt;q.pop(0)&amp;lt;/tt&amp;gt; oder &amp;lt;tt&amp;gt;del q[0]&amp;lt;/tt&amp;gt; (Mit beiden Funktionen kann man allgemein das Element bei einem beliebigen Index entfernen.)&lt;br /&gt;
* &amp;lt;tt&amp;gt;first(q)&amp;lt;/tt&amp;gt; heißt &amp;lt;tt&amp;gt;q[0]&amp;lt;/tt&amp;gt;&lt;br /&gt;
Allerdings ist es nicht sehr effizient, das erste Element aus einem &amp;lt;tt&amp;gt;list&amp;lt;/tt&amp;gt;-Objekt zu entfernen. Eine effizientere Implementation der Queue-Funktionalität bietet der Type &amp;lt;tt&amp;gt;deque&amp;lt;/tt&amp;gt; aus dem Modul &amp;lt;tt&amp;gt;collections&amp;lt;/tt&amp;gt;, der sowohl für das Queue-Verhalten (FIFO) als auch das Stack-Verhalten (LIFO) effizient ist (deque ist die Abkürzung für &amp;quot;double-ended queue&amp;quot;).&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Assoziatives Array==&lt;br /&gt;
&lt;br /&gt;
Ein assoziatives Array erweitert die Array-Funktionalität, indem es statt der Indizes beliebige Schlüssel unterstützt, unter denen die Elemente abgerufen werden. Typische Schlüssel sind z.B. Zahlen, die keine laufende Nummer bilden (z.B. Matrikelnummern), Strings (z.B. Namen). Auf die Elemente des assoziativen Arrays wird zugegriffen, indem man den jeweiligen Schlüsselwert angibt. Assoziative Array werden häufig auch als ''dictionaries'' bezeichnet.&lt;br /&gt;
&lt;br /&gt;
Seien d ∈ Dictionary, i ∈ Object (ein Schlüsselobjekt), v ∈ Object (ein beliebiges Objekt) und error ∈ Error (ein Objekt, das einen Fehler signalisiert).&lt;br /&gt;
&lt;br /&gt;
====Operationen:====&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| erzeuge ein neues Dictionary:&lt;br /&gt;
| &amp;lt;tt&amp;gt;new_dictionary() → Dictionary&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|erfrage die Anzahl der Elemente:&lt;br /&gt;
|&amp;lt;tt&amp;gt;len(d) → &amp;lt;math&amp;gt;\mathbb{N}_0&amp;lt;/math&amp;gt;&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|erfrage die zur Zeit gültigen Schlüssel:&lt;br /&gt;
|&amp;lt;tt&amp;gt;keys(d) → Array&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|teste, ob der angegebene Schlüssel gültig ist:&lt;br /&gt;
|&amp;lt;tt&amp;gt;has_key(d, i) → Boolean&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|erfrage das Element zum Schlüssel i:&lt;br /&gt;
|&amp;lt;tt&amp;gt;get(d, i) → Object&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|setze das Objekt zum Schlüssel i:&lt;br /&gt;
|&amp;lt;tt&amp;gt;set(d, i, v) → Dictionary&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
|lösche den Schlüssel i und das zugehörige Objekt:&lt;br /&gt;
|&amp;lt;tt&amp;gt;del_key(d, i) → Dictionary&amp;lt;/tt&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Axiome:====&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| Ein neues Dictionary ist leer, ebenso die Liste seiner Schlüssel.&lt;br /&gt;
| &amp;lt;tt&amp;gt;d = new_dictionary()&amp;lt;br/&amp;gt;assert(len(d) == 0)&amp;lt;br/&amp;gt;assert(len(keys(d)) == 0)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
| Es gilt stets: alle Elemente im Array &amp;lt;tt&amp;gt;keys&amp;lt;/tt&amp;gt; sind tatsächlich Schlüssel.&lt;br /&gt;
| &amp;lt;tt&amp;gt;k = keys(d)&amp;lt;/tt&amp;gt;&amp;lt;br/&amp;gt;für alle &amp;lt;tt&amp;gt;j ∈ k&amp;lt;/tt&amp;gt; gilt:&amp;lt;br/&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;tt&amp;gt;assert(has_key(d, j))&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
|Wenn i kein Schlüssel ist, gilt:&amp;lt;br/&amp;gt;(i) &amp;lt;tt&amp;gt;has_key&amp;lt;/tt&amp;gt; liefert &amp;lt;tt&amp;gt;false&amp;lt;/tt&amp;gt;,&amp;lt;br/&amp;gt;(ii) Zugriffe mit &amp;lt;tt&amp;gt;get&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;del_key&amp;lt;/tt&amp;gt; signalisieren einen Fehler.&lt;br /&gt;
|&amp;lt;tt&amp;gt;assert(not has_key(d, i))&amp;lt;br/&amp;gt;assert(get(d, i) == error)&amp;lt;br/&amp;gt;assert(del_key(d, i) == error)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
|Wenn i kein Schlüssel ist, gilt nach der Zuweisung von v an den Schlüssel i:&amp;lt;br/&amp;gt;(i) die Größe erhöht sich um eins,&amp;lt;br/&amp;gt;(ii) i ist jetzt Schlüssel und enthält das Element v,&amp;lt;br/&amp;gt;(iii) die übrigen Schlüssel und Elemente haben sich nicht verändert.&lt;br /&gt;
|&amp;lt;tt&amp;gt;assert(not has_key(d, i))&amp;lt;br/&amp;gt;d = set(d, i, v)&amp;lt;br/&amp;gt;assert(len(d) == len(d&amp;lt;sub&amp;gt;old&amp;lt;/sub&amp;gt;)+1)&amp;lt;br/&amp;gt;assert(has_key(d, i))&amp;lt;br/&amp;gt;assert(get(d, i) == v)&amp;lt;/tt&amp;gt;&amp;lt;br/&amp;gt;für alle &amp;lt;tt&amp;gt;j ∈ keys(d&amp;lt;sub&amp;gt;old&amp;lt;/sub&amp;gt;)&amp;lt;/tt&amp;gt; gilt:&amp;lt;br/&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;tt&amp;gt;assert(get(d, j) == get(d&amp;lt;sub&amp;gt;old&amp;lt;/sub&amp;gt;, j))&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
|Wenn i bereits Schlüssel ist, gilt nach der Zuweisung von v an den Schlüssel i:&amp;lt;br/&amp;gt;(i) die Größe bleibt unverändert,&amp;lt;br/&amp;gt;(ii) Schlüssel i enthält das Element v,&amp;lt;br/&amp;gt;(iii) die übrigen Schlüssel und Elemente haben sich nicht verändert.&lt;br /&gt;
|&amp;lt;tt&amp;gt;assert(has_key(d, i))&amp;lt;br/&amp;gt;d = set(d, i, v)&amp;lt;br/&amp;gt;assert(len(d) == len(d&amp;lt;sub&amp;gt;old&amp;lt;/sub&amp;gt;))&amp;lt;br/&amp;gt;assert(get(d, i) == v)&amp;lt;br/&amp;gt;assert(keys(d) == keys(d&amp;lt;sub&amp;gt;old&amp;lt;/sub&amp;gt;))&amp;lt;/tt&amp;gt;&amp;lt;br/&amp;gt;für alle &amp;lt;tt&amp;gt;j ∈ keys(d), j ≠ i&amp;lt;/tt&amp;gt; gilt:&amp;lt;br/&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;tt&amp;gt;assert(get(d, j) == get(d&amp;lt;sub&amp;gt;old&amp;lt;/sub&amp;gt;, j))&amp;lt;/tt&amp;gt;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
|Wenn i ein Schlüssel ist, gilt nach dem Löschen von i:&amp;lt;br/&amp;gt;(i) die Größe verringert sich um eins,&amp;lt;br/&amp;gt;(ii) der Schlüssel ist nicht mehr vorhanden,&amp;lt;br/&amp;gt;(iii) die übrigen Schlüssel und Elemente bleiben unverändert.&lt;br /&gt;
|&amp;lt;tt&amp;gt;assert(has_key(d, i)&amp;lt;br/&amp;gt;d = del_key(d, i)&amp;lt;br/&amp;gt;assert(len(d) == len(d&amp;lt;sub&amp;gt;old&amp;lt;/sub&amp;gt;)-1)&amp;lt;br/&amp;gt;assert(not has_key(d, i))&amp;lt;/tt&amp;gt;&amp;lt;br/&amp;gt;für alle &amp;lt;tt&amp;gt;j ∈ keys(d)&amp;lt;/tt&amp;gt; gilt:&amp;lt;br/&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;tt&amp;gt;assert(get(d, j) == get(d&amp;lt;sub&amp;gt;old&amp;lt;/sub&amp;gt;, j))&amp;lt;/tt&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====in Python:====&lt;br /&gt;
Der Dictionary-Typ heißt &amp;lt;tt&amp;gt;dict&amp;lt;/tt&amp;gt;:&lt;br /&gt;
* rufe Funktionen mit Punktsyntax auf:&amp;lt;br/&amp;gt;statt &amp;lt;tt&amp;gt;has_key(d, i)&amp;lt;/tt&amp;gt; schreibe &amp;lt;tt&amp;gt;d.has_key(i)&amp;lt;/tt&amp;gt; etc.&lt;br /&gt;
* &amp;lt;tt&amp;gt;get&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;set&amp;lt;/tt&amp;gt; heißen &amp;lt;tt&amp;gt;__getitem__&amp;lt;/tt&amp;gt; bzw. &amp;lt;tt&amp;gt;__setitem__&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;get&amp;lt;/tt&amp;gt; ist ebenfalls implementiert, aber es signalisiert keinen Fehler, wenn der Schlüssel unbekannt ist, sondern gibt ein Defaultobjekt zurück (siehe Dokumentation für Einzelheiten)&lt;br /&gt;
* &amp;lt;tt&amp;gt;del_key(d, i)&amp;lt;/tt&amp;gt; heißt &amp;lt;tt&amp;gt;d.pop(i)&amp;lt;/tt&amp;gt; oder &amp;lt;tt&amp;gt;del d[i]&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;d.keys()&amp;lt;/tt&amp;gt; liefert ab Python 3 kein Array, sondern ein ''iterable'', siehe Dokumentation&lt;br /&gt;
* Indexschreibweise:&amp;lt;br/&amp;gt;&amp;lt;tt&amp;gt;v = d[i]&amp;lt;/tt&amp;gt; ist äquivalent zu &amp;lt;tt&amp;gt;v = d.__getitem__(i)&amp;lt;/tt&amp;gt;&amp;lt;br/&amp;gt;&amp;lt;tt&amp;gt;d[i] = v&amp;lt;/tt&amp;gt; ist äquivalent zu &amp;lt;tt&amp;gt;d.__setitem__(i, v)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* Konstruktoren:&amp;lt;br/&amp;gt;&amp;lt;tt&amp;gt;d = dict()&amp;lt;/tt&amp;gt; ist äquivalent zu &amp;lt;tt&amp;gt;d = {}&amp;lt;/tt&amp;gt; und entspricht &amp;lt;tt&amp;gt;d = new_dictionary()&amp;lt;/tt&amp;gt;: erzeugt ein leeres Dictionary&amp;lt;br/&amp;gt;&amp;lt;tt&amp;gt;d = {'eins': 1, 'zwei': 2, 'drei': 3}&amp;lt;/tt&amp;gt; initialisiert ein Dictionary mit den angegebenen Schlüssel/Wert-Paaren (die Schlüssel sind hier Strings, die Werte Zahlen).&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Required Interfaces==&lt;br /&gt;
&lt;br /&gt;
Eine andere Möglichkeit, die Anforderungen an Container-Datenstrukturen zu definieren, ist das ''required interface''. Dabei nimmt man den Standpunkt eines Algorithmus ein, der diese Datenstrukturen benutzen will, und fragt: Welche Operationen hätte man denn gerne bei einem Container? Wie sollten die Daten organisiert sein, damit der Algorithmus effizient damit arbeiten können? &lt;br /&gt;
&lt;br /&gt;
Eine solche Anforderungsanalyse ist sehr aufwändig und kann sich über Jahre erstrecken, weil Erfahrungen gesammelt werden müssen, welche Anforderungen in vielen Algorithmen immer wieder auftreten. Wir listen im folgenden nur das Resultat, also die wichtigsten Operationen von Container-Datenstrukturen auf. Wenig überraschend kommen am Ende gerade die Datenstrukturen heraus, die wir oben bereits behandelt haben.&lt;br /&gt;
&lt;br /&gt;
Sei &amp;lt;tt&amp;gt;c&amp;lt;/tt&amp;gt; eine Container-Datenstruktur und &amp;lt;tt&amp;gt;v&amp;lt;/tt&amp;gt; ein darin gespeicherter Wert:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;br /&gt;
===Lesender Zugriff===&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
|'''0.'''&lt;br /&gt;
| &amp;lt;tt&amp;gt;c.size()&amp;lt;/tt&amp;gt;&lt;br /&gt;
|gibt die Anzahl der Elemente im Container an&lt;br /&gt;
|-&lt;br /&gt;
|'''1a.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;v = c.get(i)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|das i-te Element im Container lesen&lt;br /&gt;
|-&lt;br /&gt;
|'''1b.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;v = c.get(pos)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|das Element an Position &amp;lt;tt&amp;gt;pos&amp;lt;/tt&amp;gt; lesen (&amp;lt;tt&amp;gt;pos&amp;lt;/tt&amp;gt; ist ein geeignetes Hilfsobjekt, das in Abhängigkeit von der Art der Datenstruktur eine Position im Container referenziert. Im Falle 1a. &amp;lt;tt&amp;gt;v = c.get(i)&amp;lt;/tt&amp;gt; ist &amp;lt;tt&amp;gt;pos&amp;lt;/tt&amp;gt; eine natürliche Zahl, aber es gibt auch andere Möglichkeiten, die Position zu kodieren.)&lt;br /&gt;
|-&lt;br /&gt;
|'''1c.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;v = c.get(key)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|das Element mit dem Schlüssel &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; lesen (Beachte den Unterschied zu 1b: In 1b markiert &amp;lt;tt&amp;gt;pos&amp;lt;/tt&amp;gt; eine Position im Container, hier in 1c bezieht sich &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; auf eine Eigenschaft der Datenelemente, die von der Position im Container unabhängig ist.)&lt;br /&gt;
|-&lt;br /&gt;
|'''2a.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;v = c.first()&amp;lt;/tt&amp;gt;&lt;br /&gt;
|erstes Element lesen (äquivalent zu &amp;lt;tt&amp;gt;v = c.get(0)&amp;lt;/tt&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
|'''2b.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;v = c.last()&amp;lt;/tt&amp;gt;&lt;br /&gt;
|letztes Element lesen (äquivalent zu &amp;lt;tt&amp;gt;v = c.get(c.size()-1)&amp;lt;/tt&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
|'''3a.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;v = c.smallest()&amp;lt;/tt&amp;gt;&lt;br /&gt;
|das kleinste Element lesen (dies bezieht sich auf eine Eigenschaft der Datenelemente bzw. Schlüssel, im Unterschied zu 2a, wo es um die Position im Container geht.)&lt;br /&gt;
|-&lt;br /&gt;
|'''3b.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;v = c.largest()&amp;lt;/tt&amp;gt;&lt;br /&gt;
|das größte Element lesen (dies bezieht sich auf eine Eigenschaft der Datenelemente bzw. Schlüssel, im Unterschied zu 2b, wo es um die Position im Container geht.)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
===Schreibender Zugriff===&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;1&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;7&amp;quot;&lt;br /&gt;
|-valign=&amp;quot;top&amp;quot;&lt;br /&gt;
|'''4a.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;v.set(i, v)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|i-tes Element überschreiben (&amp;lt;tt&amp;gt;c.size()&amp;lt;/tt&amp;gt; bleibt unverändert)&lt;br /&gt;
|-&lt;br /&gt;
|'''4b.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;v.set(pos, v)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|Element an der Stelle &amp;lt;tt&amp;gt;pos&amp;lt;/tt&amp;gt; überschreiben (&amp;lt;tt&amp;gt;c.size()&amp;lt;/tt&amp;gt; bleibt unverändert. Zur Bedeutung von &amp;lt;tt&amp;gt;pos&amp;lt;/tt&amp;gt; siehe 1b.)&lt;br /&gt;
|-&lt;br /&gt;
|'''4c.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;v.set(key, v)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|Element mit dem Schlüssel &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; überschreiben (&amp;lt;tt&amp;gt;c.size()&amp;lt;/tt&amp;gt; bleibt unverändert)&lt;br /&gt;
|-&lt;br /&gt;
|'''5a.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;c.insert(i, v)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|Objekt als i-tes in den Container einfügen (Werte ab &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; werden eine Position nach hinten verschoben, &amp;lt;tt&amp;gt;c.size()&amp;lt;/tt&amp;gt; erhöht sich um 1)&lt;br /&gt;
|-&lt;br /&gt;
|'''5b.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;c.insert(pos, v)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|Objekt an Position &amp;lt;tt&amp;gt;pos&amp;lt;/tt&amp;gt; in den Container einfügen (Werte ab &amp;lt;tt&amp;gt;pos&amp;lt;/tt&amp;gt; werden eine Position nach hinten verschoben, &amp;lt;tt&amp;gt;c.size()&amp;lt;/tt&amp;gt; erhöht sich um 1)&lt;br /&gt;
|-&lt;br /&gt;
|'''5c.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;c.insert(key, v)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|Objekt unter dem Schlüssel &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; in den Container einfügen (Wenn der Schlüssel schon vergeben war, wird ein Fehler signalisiert. &amp;lt;tt&amp;gt;c.size()&amp;lt;/tt&amp;gt; erhöht sich um 1).&lt;br /&gt;
|-&lt;br /&gt;
|'''5d.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;c.insert(v)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|Objekt an beliebiger Stelle in den Container einfügen (Der Container bestimmt die optimale Position selbst. &amp;lt;tt&amp;gt;c.size()&amp;lt;/tt&amp;gt; erhöht sich um 1).&lt;br /&gt;
|-&lt;br /&gt;
|'''6a.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;c.prepend(v)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|Objekt am Anfang einfügen (äquivalent zu &amp;lt;tt&amp;gt;c.insert(0, v)&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;c.size()&amp;lt;/tt&amp;gt; erhöht sich um 1)&lt;br /&gt;
|-&lt;br /&gt;
|'''6b.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;c.append(v)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|Objekt am Ende anhängen (äquivalent zu &amp;lt;tt&amp;gt;c.insert(c.size(), v)&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;c.size()&amp;lt;/tt&amp;gt; erhöht sich um 1)&lt;br /&gt;
|-&lt;br /&gt;
|'''7a.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;c.remove(i)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|i-tes Element aus dem Container löschen (Werte ab &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; werden eine Position nach vorn verschoben, &amp;lt;tt&amp;gt;c.size()&amp;lt;/tt&amp;gt; verringert sich um 1)&lt;br /&gt;
|-&lt;br /&gt;
|'''7b.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;c.remove(pos)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|Objekt an Position &amp;lt;tt&amp;gt;pos&amp;lt;/tt&amp;gt; aus dem Container löschen  (Werte ab &amp;lt;tt&amp;gt;pos&amp;lt;/tt&amp;gt; werden eine Position nach vorn verschoben, &amp;lt;tt&amp;gt;c.size()&amp;lt;/tt&amp;gt; verringert sich um 1)&lt;br /&gt;
|-&lt;br /&gt;
|'''7c.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;c.remove(key)&amp;lt;/tt&amp;gt;&lt;br /&gt;
|Objekt unter dem Schlüssel &amp;lt;tt&amp;gt;key&amp;lt;/tt&amp;gt; aus dem Container löschen  (Wenn der Schlüssel nicht vergeben war, wird ein Fehler signalisiert. &amp;lt;tt&amp;gt;c.size()&amp;lt;/tt&amp;gt; verringert sich um 1, wenn es kein Fehler signalisiert hat.)&lt;br /&gt;
|-&lt;br /&gt;
|'''8a.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;c.removeFirst()&amp;lt;/tt&amp;gt;&lt;br /&gt;
|das erste Element aus dem Container entfernen (äquivalent zu &amp;lt;tt&amp;gt;c.remove(0)&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;c.size()&amp;lt;/tt&amp;gt; verringert sich um 1)&lt;br /&gt;
|-&lt;br /&gt;
|'''8b.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;c.removeLast()&amp;lt;/tt&amp;gt;&lt;br /&gt;
|das letzte Element aus dem Container entfernen (äquivalent zu &amp;lt;tt&amp;gt;c.remove(c.size()-1)&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;c.size()&amp;lt;/tt&amp;gt; verringert sich um 1)&lt;br /&gt;
|-&lt;br /&gt;
|'''9a.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;c.removeSmallest()&amp;lt;/tt&amp;gt;&lt;br /&gt;
|das kleinste Element aus dem Container entfernen (dies bezieht sich auf eine Eigenschaft der Datenelemente bzw. Schlüssel, im Unterschied zu 8a, wo es um die Position im Container geht. &amp;lt;tt&amp;gt;c.size()&amp;lt;/tt&amp;gt; verringert sich um 1)&lt;br /&gt;
|-&lt;br /&gt;
|'''9b.'''&lt;br /&gt;
|&amp;lt;tt&amp;gt;c.removeLargest()&amp;lt;/tt&amp;gt;&lt;br /&gt;
|das größte Element aus dem Container entfernen (dies bezieht sich auf eine Eigenschaft der Datenelemente bzw. Schlüssel, im Unterschied zu 8b, wo es um die Position im Container geht. &amp;lt;tt&amp;gt;c.size()&amp;lt;/tt&amp;gt; verringert sich um 1)&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;br/&amp;gt;&amp;lt;br/&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Facts==&lt;br /&gt;
&lt;br /&gt;
*Jede dieser Operationen kann sehr effizient implementiert werden.&lt;br /&gt;
*Keine Datenstruktur ist bekannt, die '''alle''' diese Operationen effizient implementiert.&lt;br /&gt;
&lt;br /&gt;
==Beispiele==&lt;br /&gt;
&lt;br /&gt;
Je nachdem welche Operation effizient sein soll, wird eine andere Container Datenstruktur ausgewählt. Die Operation &amp;lt;tt&amp;gt;c.size()&amp;lt;/tt&amp;gt; wird von allen Containern effizient unterstützt.&lt;br /&gt;
&lt;br /&gt;
===Arrays===&lt;br /&gt;
;'''(statisches) Array''' [http://en.wikipedia.org/wiki/Array]: Das Array ist die einfachste Datenstruktur, es kann einfach als aufeinanderfolgender Bereich von Speicherzellen implementiert werden. Jede dieser Speicherzellen nimmt ein Objekt als Datenelement auf. Die Größe ist nicht veränderbar (daher der Name ''statisch''). &amp;lt;br/&amp;gt; Das statische Array unterstützt die Operationen&lt;br /&gt;
    1a.    c.get(i)&lt;br /&gt;
    4a.    c.set(i, value)&lt;br /&gt;
;'''Dynamisches Array''' [http://en.wikipedia.org/wiki/Dynamic_array]: Die Größe ist veränderbar, aber nur durch Anfügen oder Entfernen eines Elements am ''Ende'' des Arrays. Die unterstützen Operationen sind dieselben wie die des statischen Arrays, zusätzlich unterstützt das dynamische Array die Operationen&lt;br /&gt;
    6b.    c.append(v)&lt;br /&gt;
    8b.    c.removeLast()&lt;br /&gt;
Wir beschreiben im Abschnitt [[Effizienz#dynamisches_Array|Amortisierte Komplexität]], wie man dies effizient implementieren kann. Das Anfügen neuer Elemente am Ende ist eine sehr häufige Operation, so dass das dynamische Array eine der beliebtesten Datenstrukturen ist. In Python hat das dynamische Array den Typ &amp;lt;tt&amp;gt;list&amp;lt;/tt&amp;gt;, was in diesem Fall nichts mit verketten Listen zu tun hat, sondern eher auf Listen im Sinne von Tabellen hinweist (die Namenswahl ist dennoch etwas unglücklich und kann zu Verwechslungen führen).&lt;br /&gt;
;'''assoziatives Array (Dictionary)''' [http://en.wikipedia.org/wiki/Associative_array]: Ein Dictionary verallgemeinert das dynamische Array: Während Arrays auf ihre Elemente über Indizes (= natürliche Zahlen) zugreifen, können die Schlüssel (Keys) bei einem Dictionary einen beliebigen Typ haben. Jedes Element des Dictionary besteht aus einem Schlüssel-Wert-Paar, jeder Schlüssel bekommt somit einen Wert zugewiesen. &amp;lt;br/&amp;gt;Das Dictionary unterstützt die Operationen&lt;br /&gt;
    1c.    c.get(key)&lt;br /&gt;
    4c.    c.set(key, value)&lt;br /&gt;
    5c.    c.insert(key, value)&lt;br /&gt;
    7c.    c.remove(key)&lt;br /&gt;
Wenn als Schlüssel natürliche Zahlen 0, 1, ..., N gewählt werden, sind dies im wesentlichen dieselben Operationen wie beim Array. Man wird das Dictionary also vor allem dann einsetzen, wenn die Schlüssel einen anderen Typ haben, oder wenn die Zahlen nicht aus dem zusammenhängenden Intervall 0, ..., N kommen. Das Python-Dictionary hat den Typ &amp;lt;tt&amp;gt;dict&amp;lt;/tt&amp;gt;. Wir behandeln diese Datenstruktur in den Kapiteln [[Assoziative Arrays]] und [[Hashing und Hashtabellen]].&lt;br /&gt;
&lt;br /&gt;
===verkettete Listen===&lt;br /&gt;
;'''(einfach) verkettete Liste''' [http://en.wikipedia.org/wiki/Linked_list#Singly-linked_list]: Im Gegensatz zum Array müssen die Speicherzellen nicht nacheinenander im Speicher abgelegt sein. Statt dessen enthält jedes Element der Liste ein Feld &amp;lt;tt&amp;gt;next&amp;lt;/tt&amp;gt;, das auf das nächste Element der Liste verweist. Um das i-te Element zu finden, muss man die Liste von vorn nach hinten durchlaufen. Deshalb ist die Operation &amp;lt;tt&amp;gt;c.get(i)&amp;lt;/tt&amp;gt; für verkettete Listen nicht effizient. Wenn man allerdings auf ein Element zugegriffen hat, kann man ein &amp;lt;tt&amp;gt;pos&amp;lt;/tt&amp;gt;-Objekt (in diesem Fall eine Referenz auf das Element) speichern, so dass ein erneuter Zugriff auf das selbe Element schnell geht. Das gleiche gilt für das folgende Element, weil man nur einmal &amp;lt;tt&amp;gt;pos = pos.next&amp;lt;/tt&amp;gt; aufrufen muss. Nur wenig komplizierter (und dadurch ebenfalls effizient) ist das Einfügen eines neuen Elements an der Position &amp;lt;tt&amp;gt;pos&amp;lt;/tt&amp;gt;. &amp;lt;br/&amp;gt;Die verkette Liste unterstützt somit die Operationen:&lt;br /&gt;
    1b.    c.get(pos)&lt;br /&gt;
    2a.    c.first()&lt;br /&gt;
    4b.    c.set(pos, value)&lt;br /&gt;
    5b.    c.insert(pos, value)&lt;br /&gt;
    6a.    c.prepend(value)&lt;br /&gt;
    7b.    c.remove(pos)&lt;br /&gt;
    8a.    c.removeFirst(pos)&lt;br /&gt;
Es scheint, dass die Liste eine sehr flexible Datenstruktur ist. Allerdings ist es ein gravierender Nachteil, dass &amp;lt;tt&amp;gt;pos&amp;lt;/tt&amp;gt; nur auf das jeweils nächste Element weitergesetzt werden kann. Im Gegensatz dazu können Indizes in einem Array effizient auf beliebige Positionen gesetzt werden. Man bevorzugt deshalb heute dynamische Arrays.&lt;br /&gt;
;'''Doppelt verkettete Liste''' [http://en.wikipedia.org/wiki/Linked_list#Doubly-linked_list]: Im Gegensatz zur einfach verketteten Liste enthält jedes Element nicht nur einen Zeiger auf das darauffolgende, sondern auch auf das vorherige Element in der Liste. Dadurch kann ein &amp;lt;tt&amp;gt;pos&amp;lt;/tt&amp;gt;-Objekt auch effizient um ein Element zurückgesetzt werden: &amp;lt;tt&amp;gt;pos = pos.previous&amp;lt;/tt&amp;gt;.&amp;lt;br/&amp;gt; Die doppelt verkette Liste unterstützt deshalb die selben Operationen wie die einfach verkettete, und zusätzlich&lt;br /&gt;
    2b.    c.last()&lt;br /&gt;
    6b.    c.append(value)&lt;br /&gt;
    8b.    c.removeLast()&lt;br /&gt;
&lt;br /&gt;
===Queues===&lt;br /&gt;
;'''Stack (Stapelspeicher)''' [http://en.wikipedia.org/wiki/Stack_(data_structure)]: Speichert/Stapelt die Objekte mit push in einen Speicher. Wiederrum mit pop kann das oberste (=zuletzt eingefügte) Element herausgeholt werden: LIFO (Last In First Out) &amp;lt;br /&amp;gt;Die Python-Datenstruktur &amp;lt;tt&amp;gt;List&amp;lt;/tt&amp;gt; eignet sich beispielsweise als Stack.&amp;lt;br/&amp;gt;Operationen:&lt;br /&gt;
    2b.    c.last()         # auf das oberste Element zugreifen, ohne es zu entfernen&lt;br /&gt;
    6b.    c.append(value)  # Element auf den Stapel legen (beim Stack meist c.push(value) genannt)&lt;br /&gt;
    8b.    c.removeLast()   # oberstes Element entfernen (beim Stack meist c.pop() genannt)&lt;br /&gt;
;'''Queue (Schlange)''' [http://en.wikipedia.org/wiki/Queue_(data_structure)]: Eine Queue ist wie eine Warteschlange an der Kasse im Supermarkt, bedient wird derjenige der als erster an die Kasse kommt: FIFO (First In First Out)&amp;lt;br/&amp;gt;Operationen:&lt;br /&gt;
    2a.    c.first()&lt;br /&gt;
    6b.    c.append(value)&lt;br /&gt;
    8a.    c.removeFirst()&lt;br /&gt;
;'''Deque (Double Ended Queue)''' [http://en.wikipedia.org/wiki/Deque]:  wie Stack + Queue, d.h. Objekte können am Ende eingefügt, aber sowohl vorn als auch hinten gelesen und entfernt werden.&amp;lt;br/&amp;gt;Operationen&lt;br /&gt;
    2a.    c.first()&lt;br /&gt;
    2b.    c.last()&lt;br /&gt;
    6b.    c.append(value)&lt;br /&gt;
    8a.    c.removeFirst()&lt;br /&gt;
    8b.    c.removeLast()&lt;br /&gt;
Die Deque ist Thema in [[Media:Übung-3.pdf|Übungsblatt 3]].&lt;br /&gt;
&lt;br /&gt;
===Prioritätswarteschlangen===&lt;br /&gt;
;'''MinPriorityQueue''' [http://en.wikipedia.org/wiki/Priority_queue]: Warteschlange, die das Element mit der kleinsten Priorität zuerst zurückgibt (z.B. an der Kasse im Supermarkt diejenige/derjenige, die/der die wenigsten Produkte kaufen möchte) &amp;lt;br/&amp;gt; Mögliche Operationen:&lt;br /&gt;
    3a.    c.smallest()&lt;br /&gt;
    5d.    c.insert(value)&lt;br /&gt;
    9a.    c.removeSmallest()&lt;br /&gt;
;'''MaxPriorityQueue''' [http://en.wikipedia.org/wiki/Priority_queue]: Warteschlange, die das Element mit der größten Priorität zuerst zurückgibt &amp;lt;br/&amp;gt; Unterstützte Operationen sind:&lt;br /&gt;
    3b.    c.largest()&lt;br /&gt;
    5d.    c.insert(value)&lt;br /&gt;
    9b.    c.removeLargest()&lt;br /&gt;
;'''MinMaxPriorityQueue''' [http://en.wikipedia.org/wiki/Priority_queue]: kombiniert MinPriorityQueue + MaxPriorityQueue&lt;br /&gt;
&lt;br /&gt;
Die drei letzten Datenstrukturen behandeln wir im Kapitel [[Prioritätswarteschlangen]].&lt;br /&gt;
&lt;br /&gt;
==Container in Python==&lt;br /&gt;
&lt;br /&gt;
Wir hatten die Python-Datenstrukturen &amp;lt;tt&amp;gt;list&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;dict&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;collections.deque&amp;lt;/tt&amp;gt; bereits weiter oben diskutiert. Es fehlen noch die Prioritätswarteschlangen, für die Python das Modul &amp;lt;tt&amp;gt;heapq&amp;lt;/tt&amp;gt; anbietet. Es implementiert allerdings keine eigene Datenstruktur, sondern stellt Funktionen zur Verfügung, die die notwendigen Operationen auf der Basis eines normalen Arrays von Typ &amp;lt;tt&amp;gt;list&amp;lt;/tt&amp;gt; realisieren, siehe Dokumentation für Einzelheiten.&lt;br /&gt;
&lt;br /&gt;
[[Sortieren|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Einf%C3%BChrung&amp;diff=5698</id>
		<title>Einführung</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Einf%C3%BChrung&amp;diff=5698"/>
				<updated>2020-04-28T10:49:05Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Beispiele für Datenformate */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Definition von Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
Es gibt viele Definitionen von Algorithmen. Hier sind die Ergebnisse einer Google-Suche auf  [http://www.google.de/search?hl=de&amp;amp;defl=en&amp;amp;q=define:Algorithm&amp;amp;sa=X&amp;amp;oi=glossary_definition&amp;amp;ct=title englisch] und auf&lt;br /&gt;
[http://www.google.de/search?hl=de&amp;amp;defl=de&amp;amp;q=define:Algorithmus&amp;amp;sa=X&amp;amp;oi=glossary_definition&amp;amp;ct=title deutsch]. Die Grundidee ist aber immer gleich:&lt;br /&gt;
&lt;br /&gt;
Ein '''Algorithmus''' ist eine Problemlösung durch endlich viele elementare Schritte. Die Teile der Definition bedürfen näherer Erläuterung:&lt;br /&gt;
&lt;br /&gt;
;Problemlösung: Damit ein Algorithmus ein Problem (genauer: eine Menge von gleichartigen Problemen) lösen kann, muss das Problem zunächst definiert (''spezifiziert'') werden. Die '''Spezifikation''' legt fest, ''was'' der Algorithmus erreichen soll, sagt aber nichts über das ''wie''. Die Spezifikation beschreibt somit relevante Eigenschaften des Systemzustands ''vor'' und ''nach'' der Ausführung des Algorithmus (sogenannte '''Vor-''' und '''Nachbedingungen'''), während der Algorithmus einen bestimmten ''Lösungsweg'' repräsentiert. Mit Hilfe der Spezifikation kann getestet werden, ob der Algorithmus tatsächlich eine Lösung des gestellten Problems liefert. Diese Frage untersuchen wir im Kapitel [[Korrektheit]].&lt;br /&gt;
;Endlich viele Schritte: Die Forderung nach endlich vielen Schritten unterstellt, dass jeder einzelne Schritt eine gewisse Zeit benötigt, also nicht unendlich schnell ausgeführt werden kann. Damit ist diese Forderung äquivalent zu der Forderung, dass der Algorithmus in endlicher Zeit zum Ergebnis kommen muss. Der Sinn einer solchen Forderung leuchtet aus praktischer Sicht unmittelbar ein. Interessant ist darüber hinaus die Frage, wie man mit möglichst wenigen Schritten, also möglichst schnell, zur Lösung kommt. Diese Frage untersuchen wir im Kapitel [[Effizienz]].&lt;br /&gt;
;Elementare Schritte: Im weiteren Sinne verstehen wir unter einem elementaren Schritt ein Teilproblem, für das bereits ein Algorithmus bekannt ist. Im engeren Sinne ist die Menge der elementaren Schritte durch die Hilfsmittel vorgegeben, mit denen der Algorithmus ausgeführt werden soll, also z.B. durch die Hardware oder die Programmiersprache. Wir gehen darauf im nächsten Abschnitt näher ein.&lt;br /&gt;
&lt;br /&gt;
=== Zur Frage der elementaren Schritte ===&lt;br /&gt;
&lt;br /&gt;
Welche Schritte als elementar angesehen werden können, hängt sehr stark vom Kontext der Aufgabe und den Hilfsmitteln zu ihrer Lösung ab. Ein interessantes Beispiel ist die Geometrie der alten Griechen, wo geometrische Probleme in der Ebene allein mit Zirkel und Lineal gelöst werden. In diesem Fall sind folgende elementare Operationen erlaubt:&lt;br /&gt;
* das Markieren eines Punktes (beliebig in der Ebene oder als Schnittpunkt zwischen bereits gezeichneten Linien),&lt;br /&gt;
* das Zeichnen einer Geraden durch zwei Punkte,&lt;br /&gt;
* das Zeichnen eines Kreises um einen Punkt,&lt;br /&gt;
* das Abgreifen des Abstands zwischen zwei Punkten mit dem Zirkel.&lt;br /&gt;
Auf der Basis dieser Operationen kann zum Beispiel kein Algorithmus für die Dreiteilung eines beliebigen Winkels definiert werden, während der Algorithmus für die Zweiteilung sehr einfach ist. &lt;br /&gt;
&lt;br /&gt;
Eine völlig andere Menge von elementaren Operationen ergibt sich für arithmetische Berechnungen mit Hilfe des Abacus (Rechenbrett), der seit der Römerzeit in Europa weit verbreitet war. Hier werden Zahlen durch die Positionen von Perlen auf Rillen oder Drähten dargestellt und Berechnungen durch deren Verschiebung. Eine ausführliche Beschreibung der wichtigsten Abacus-Algorithmen findet sich unter [http://abacus.etherwork.net/ The Bead Unbuffled] von Totton Heffelfinger und Gary Flom.&lt;br /&gt;
&lt;br /&gt;
Die moderne Auffassung von elementaren Operationen wird durch die Berechenbarkeitstheorie (ein Teilgebiet der theoretischen Informatik) bestimmt. Verschiedene Mathematiker (darunter die Pioniere Alan Turing, Alonso Church, Kurt Gödel, Stephen Kleene und Emil Post) haben seit den 1930er Jahren versucht, den intuitiven Begriff der Berechenbarkeit einer Funktion zu formalisieren und sind dabei zu völlig verschiedenen Lösungen gelangt (z.B. Turingmaschine, Lambda-Kalkül, μ-Rekursion und WHILE-Programm). Interessanterweise stellte sich heraus, dass diese Lösungen alle die gleiche Mächtigkeit haben: Obwohl die elementaren Operationen jeweils ganz anders definiert sind, ist die Menge der damit berechenbaren Funktionen immer gleich. Die [http://en.wikipedia.org/wiki/Church_thesis Church-Turing-These] besagt, dass es prinzipiell unmöglich ist, eine mächtigere Definition von elementaren Operationen zu finden, aber dies ist unbewiesen. Am bequemsten für die Praxis sind die  [http://de.wikipedia.org/wiki/WHILE-Programm WHILE-Programme], da sie sich direkt auf die heute gebräuchliche Hardware-Architektur abbilden lassen. Die elementaren Operationen eines WHILE-Programms lauten in erweiterter Backus-Naur Notation:&lt;br /&gt;
 P ::= x[i] = x[j] + c            # Addition einer Konstanten zur Variable x[i]&lt;br /&gt;
     | x[i] = x[j] - c            # Subtraktion einer Konstanten von x[i]&lt;br /&gt;
     | P; P                       # Nacheinanderausführung von zwei Anweisungen&lt;br /&gt;
     | WHILE x[i] != 0 DO P DONE  # Wiederholte Ausführung der Anweisung(en) P &lt;br /&gt;
                                  # (x[i] muss sich innerhalb von P ändern, um eine Endlosschleife zu vermeiden)&lt;br /&gt;
wobei &amp;lt;tt&amp;gt;c&amp;lt;/tt&amp;gt; eine beliebige ganzahlige Konstante (eine ausgeschriebene ganze Zahl) und &amp;lt;tt&amp;gt;x[i]&amp;lt;/tt&amp;gt; die Speicherzelle &amp;lt;tt&amp;gt;i&amp;lt;/tt&amp;gt; bezeichnen. Alle Speicherzellen können ganze Zahlen aufnehmen und sind anfangs mit Null belegt. Darüber hinaus wird vorausgesetzt, dass mindestens soviele Speicherzellen vorhanden sind, wie der gegebene Algorithmus benötigt, und jede Speicherzelle groß genug ist, um die größte auftretende Zahl aufzunehmen. Beide Annahmen sind in der Praxis nicht immer erfüllt.&lt;br /&gt;
&lt;br /&gt;
In einem WHILE-Programm gibt es keine elementare Funktion, um die Summe von zwei &amp;lt;i&amp;gt;Variablen&amp;lt;/i&amp;gt; zu berechnen. Diese Operation muss man bereits als Algorithmus implementieren. Der folgende Code berechnet die Summe unter der Voraussetzung, dass &amp;lt;tt&amp;gt;x[j]&amp;lt;/tt&amp;gt; nicht negativ ist, indem &amp;lt;tt&amp;gt;x[j]&amp;lt;/tt&amp;gt; solange dekrementiert (um 1 erniedrigt) wird, bis es den Wert 0 annimmt, und &amp;lt;tt&amp;gt;x[i]&amp;lt;/tt&amp;gt; entsprechend bei jedem Schritt inkrementiert (um 1 erhöht) wird. Die alten Werte der Variablen gehen bei der Berechnung verloren:&lt;br /&gt;
 Algorithmus: x[i] = x[i] + x[j] als WHILE-Programm (Vorbedingung: x[j] &amp;gt;= 0)&lt;br /&gt;
     WHILE x[j] != 0 DO&lt;br /&gt;
         x[i] = x[i] + 1;&lt;br /&gt;
         x[j] = x[j] - 1&lt;br /&gt;
     DONE&lt;br /&gt;
Man erkennt, dass tatsächlich nur die vier elementaren Operationen (Addition/Subtraktion einer Konstanten, Nacheinanderausführung von Anweisungen, WHILE-Schleife) vorkommen. Allerdings ist dieser Algorithmus sehr langsam. Außerdem ist die Zerlegung in Form eines WHILE-Programms (oder eines äquivalenten Formalismus der Berechenbarkeitstheorie) für unsere Zwecke zu feinkörnig: Sie würde bedeuten, dass alle Algorithmen auf einem extrem einfachen Prozessor in Assembler programmiert werden müssten. Bereits eine so einfache Operation wie die Summe von zwei Variablen erfordert vier Codezeilen! &lt;br /&gt;
&lt;br /&gt;
Deshalb definiert man ''höhere Programmiersprachen'', die wichtige Algorithmen wie z.B. die arithmetischen Operationen mit ganzen Zahlen und Gleitkomma-Zahlen bereits als elementare Operationen enthalten. Weitere nicht ganz so wichtige Funktionen wie die Wurzel oder der Logarithmus werden in Programmbibliotheken angeboten, die standardmäßig mitgeliefert werden. In der Praxis betrachtet man eine Operation deshalb als elementar, wenn sie von einer typischen Programmiersprache oder einer typischen Standardbibliothek unterstützt wird. In dieser Vorlesung wählen wir die Operationen und Bibliotheken der Programmiersprache [http://www.python.org Python]. Wenn ein Algorithmus Anforderungen stellt, die nicht selbstverständlich sind, müssen sie als ''Requirements'' explizit angegeben werden. Wir werden darauf im Kapitel [[Generizität]] zurückkommen.&lt;br /&gt;
&lt;br /&gt;
=== Zur Geschichte ===&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|-valign=&amp;quot;top&amp;quot; &lt;br /&gt;
| Algorithmen wurden bereits im Altertum verwendet. Besonders die alten Griechen haben Pionierarbeit geleistet, z.B. auf dem Gebiet der Arithmetik (Euklidischer Algorithmus für den größten gemeinsamen Teiler von zwei Zahlen, Sieb des Eratosthenes zur Bestimmung von Primzahlen) und der Geometrie (Teilung einer Strecke oder eines Winkels nur mit Zirkel und Lineal). Der Begriff ''Algorithmus'' ist vom Namen des arabischen Gelehrten Muhammed Al Chwarizmi (ca. 783-850) abgeleitet, der in seinem Werk „Über das Rechnen mit indischen Ziffern“ (um 825) grundlegende Verfahren für das Rechnen im dekadischen Positionssystem beschrieben hat. Im 12. Jahrhundert wurde dieses Buch ins Lateinische übersetzt, und die Einleitung begann mit den Worten „Dixit Algorismi“ (Al Chwarizmi hat gesagt). Ab etwa 1200 wurden die neuen Rechenmethoden als „Algorismus de integris“ bzw. „Algorismus vulgaris“ (Rechnen mit ganzen Zahlen, d.h. Grundrechenarten und Wurzelziehen) sowie „Algorismus de minutiis“ (Bruchrechnung) zum festen Bestandteil der mathematischen Ausbildung im Rahmen der sieben freien Künste. Dabei diente der Begriff Algorithmus ursprünglich vor allem zur Abgrenzung des schriftlichen Rechnens mit indischen/arabischen Zahlen (wie wir es noch heute in der Schule lernen) vom traditionellen mechanischen Rechnen mit Abacus und römischen Zahlen, das noch bis ca. 1500 in Europa vorherrschend blieb. &lt;br /&gt;
&lt;br /&gt;
Die allgemeinere Bedeutung des Wortes Algorithmus als systematische Rechenvorschrift war jedoch ebenfalls schon früh gebräuchlich. Dies zeigt zum Beispiel der Titel des Buches „Algorismus proportionum“ (Rechenkunst mit Proportionen, ca. 1350) von Nicole Oresme, wo erstmals die Rechenregeln für Potenzen mit rationalen Exponenten beschrieben werden. Durch die steigenden Anforderungen des kaufmännischen Rechnens und der Navigation verbreitete sich die algorithmische Denkweise ab etwa 1500 rasch. Der Buchdruck machte mit Werken wie Adam Ries' „Rechenung auff der linihen und federn“ (d.h. mit Abacus und mit indischen/arabischen Zahlen, zuerst 1522) die grundlegenden Rechenalgorithmen einem breiten Bevölkerungskreis bekannt. Umfangreiche gedruckte Tafelwerke, z.B. der „Canon“ von G.J. Rhaeticus (1551) mit bis zu siebenstelligen Tabellen der trigonometrischen Funktionen, erlaubten es, komplizierte Berechnungen auf einfache Schritte (Addition, Subtraktion sowie Nachschlagen in der Tabelle) zurückzuführen. Unsere heutige Verwendung des Begriffs geht wohl auf Alonso Church's Aufsatz „An Unsolvable Problem of Elementary Number Theory“ (1936) zurück, wo die Berechenbarkeit einer Funktion mit der Existenz eines terminierenden Berechnungsalgorithmus gleichgesetzt wird.&lt;br /&gt;
| [[Image:Al-Khwarizmi.jpg]] &amp;lt;br&amp;gt; Al Chwarizmi-Denkmal in Teheran&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Definition von Datenstrukturen ==&lt;br /&gt;
&lt;br /&gt;
=== Beispiele für Datenformate ===&lt;br /&gt;
&lt;br /&gt;
Der Speicher eines Computers enthält eine Folge von Zeichen aus einem gegebenen Alphabet. Bei fast allen heutigen Computern ist dies eine Folge von Bits aus dem Alphabet {0,1}. Ein '''Datenformat''' ordnet eine Bitfolge in Gruppen und gibt jeder Gruppe eine Bedeutung. Der Gruppierungsprozess kann dann hierarchisch fortgesetzt werden.&lt;br /&gt;
&lt;br /&gt;
Die selben Bits können somit völlig verschiedene Bedeutungen annehmen, ja nachdem in welchem Datenformat sie sich befinden. Man betrachte z.B. die Folge von 16 Bits:&lt;br /&gt;
 1101011001101100&lt;br /&gt;
Wenn wir diese Folge als eine zusammengehörende Gruppe betrachten und als positive ganze Zahl in Binärdarstellung interpretieren (unsigned integer, &amp;lt;tt&amp;gt;uint16&amp;lt;/tt&amp;gt;), ergibt sich die Dezimalzahl &lt;br /&gt;
 54892 = 1*2&amp;lt;sup&amp;gt;15&amp;lt;/sup&amp;gt; + ... + 1*2&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt; + 1*2&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; + 0*2&amp;lt;sup&amp;gt;1&amp;lt;/sup&amp;gt; + 0*2&amp;lt;sup&amp;gt;0&amp;lt;/sup&amp;gt;&lt;br /&gt;
Interpretieren wir dieselbe Gruppe als vorzeichenbehaftete ganze Zahl in [http://de.wikipedia.org/wiki/Zweierkomplement Zweierkomplement]-Darstellung (signed integer, &amp;lt;tt&amp;gt;int16&amp;lt;/tt&amp;gt;), ergibt sich eine andere Dezimalzahl: Da das linke (höchstwertige) Bit Eins ist, handelt es sich um eine negative Zahl. Das Zweierkomplement erhält man durch Negieren aller Bits und nachfolgende Addition von 1:&lt;br /&gt;
 Zweierkomplement von 1101011001101100:&lt;br /&gt;
                      0010100110010011 + 1 = 0010100110010100&lt;br /&gt;
Die resultierende Dezimalzahl ist somit&lt;br /&gt;
 -10644 = -(0*2&amp;lt;sup&amp;gt;15&amp;lt;/sup&amp;gt; + ... + 0*2&amp;lt;sup&amp;gt;3&amp;lt;/sup&amp;gt; + 1*2&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt; + 0*2&amp;lt;sup&amp;gt;1&amp;lt;/sup&amp;gt; + 0*2&amp;lt;sup&amp;gt;0&amp;lt;/sup&amp;gt;)&lt;br /&gt;
Alternativ können wir die Folge in zwei Gruppen zu 8 Bit gruppieren, und die Gruppen als Zeichencodes im Windows-Zeichensatz interpretieren. Wir erhalten die Zeichenkette &amp;quot;Öl&amp;quot;:&lt;br /&gt;
 11010110  01101100 = char[214] char[108] =&amp;gt; Öl&lt;br /&gt;
Eine weitere Interpretation ist diejenige als 16-Bit Gleitkommazahl (&amp;lt;tt&amp;gt;float16&amp;lt;/tt&amp;gt;) gemäß [http://en.wikipedia.org/wiki/IEEE_floating-point_standard IEEE Standard 754]. Dabei wird die Folge in Gruppen zu 1 Bit, 5 Bit und 10 Bit eingeteilt:&lt;br /&gt;
 1  10101  1001101100&lt;br /&gt;
Die Gruppen werden als nicht-negative Binärzahlen gelesen, wobei die erste Gruppe das Vorzeichen &amp;lt;tt&amp;gt;s&amp;lt;/tt&amp;gt; der Gleitkommazahl ist (0 bedeutet &amp;quot;+&amp;quot;, 1 bedeutet &amp;quot;-&amp;quot;), die zweite ist ihr Exponent &amp;lt;tt&amp;gt;exp&amp;lt;/tt&amp;gt; und die dritte die Mantisse &amp;lt;tt&amp;gt;m&amp;lt;/tt&amp;gt;. In unserem Beispiel gilt &amp;lt;tt&amp;gt;s = 1&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;exp = 21&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;m = 620&amp;lt;/tt&amp;gt;). Die Umrechnung in eine Gleitkommazahl erfolgt, gemäß IEEE Standard, nach folgender Formel:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;tt&amp;gt;z = (1 - 2*s) * 2&amp;lt;sup&amp;gt;exp-15&amp;lt;/sup&amp;gt; * (1 + m * 2&amp;lt;sup&amp;gt;-10&amp;lt;/sup&amp;gt;)&amp;lt;/tt&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
In Dezimaldarstellung ist dies &amp;lt;tt&amp;gt;-102.75&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Das analoge Beispiel für eine Folge von 32 Bits ist vielleicht realistischer, weil 32-bit Zahlen (integer und float) in der Praxis häufiger vorkommen. Wir betrachten die Bitfolge:&lt;br /&gt;
 11111100011000100110010101101110&lt;br /&gt;
Als positive ganze Zahl in Binärdarstellung (unsigned integer, &amp;lt;tt&amp;gt;uint32&amp;lt;/tt&amp;gt;) ergibt sich die Dezimalzahl 4234306926. Dieselben Bits als vorzeichenbehaftete ganze Zahl in Zweierkomplement-Darstellung (signed integer, &amp;lt;tt&amp;gt;int32&amp;lt;/tt&amp;gt;) ergiben die Dezimalzahl -60660370. Als Zeichenfolge (vier Gruppen zu 8 Bit) bekommen wir die Zeichenkette &amp;quot;üben&amp;quot;. Eine weitere mögliche Interpretation ist diejenige als Farbe im RGBA System (8 Bit pro Farbkanal, 8 Bit Transparenzwert), und wir erhalten ein halbtransparentes Rosa (Rot: 252, Grün: 98, Blau: 101, Alpha: 110).&amp;lt;br&amp;gt;&lt;br /&gt;
Eine 32-Bit Gleitkommazahl (&amp;lt;tt&amp;gt;float32&amp;lt;/tt&amp;gt;) ist gemäß IEEE Standard 754 definiert durch Gruppen zu 1 Bit für das Vorzeichen, 8 Bit für den Exponenten und 23 Bit für die Mantisse, d.h:&lt;br /&gt;
 1 11111000 11000100110010101101110&lt;br /&gt;
Hier gilt also &amp;lt;tt&amp;gt;s = 1&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;exp = 248&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;m = 6448494&amp;lt;/tt&amp;gt;). Die Umrechnung in eine Gleitkommazahl erfolgt jetzt nach der Formel:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;tt&amp;gt;z = (1 - 2*s) * 2&amp;lt;sup&amp;gt;exp-127&amp;lt;/sup&amp;gt; * (1 + m * 2&amp;lt;sup&amp;gt;-23&amp;lt;/sup&amp;gt;)&amp;lt;/tt&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
In Dezimaldarstellung ist dies rund &amp;lt;tt&amp;gt;-4.7020653*10&amp;lt;sup&amp;gt;36&amp;lt;/sup&amp;gt;&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Im Sinne einer hierarchischen Gruppierung können wir jetzt z.B. eine Datenstruktur &amp;quot;Farbbild&amp;quot; definieren, indem wir viele RGBA-Werte zu einem 2-dimensionalen Array zusammenfassen. Eine Datenstruktur &amp;quot;komplexe Zahl&amp;quot; wird durch ein geordnetes Paar von Gleitkommazahlen gebildet, eine &amp;quot;Meßreihe&amp;quot; als Liste von ganzen Zahlen oder Gleitkommawerten (je nach Art der Messung), usw.&lt;br /&gt;
&lt;br /&gt;
=== Varianten der Datenstrukturdefinition ===&lt;br /&gt;
&lt;br /&gt;
{| border=&amp;quot;0&amp;quot; cellspacing=&amp;quot;0&amp;quot; cellpadding=&amp;quot;5&amp;quot; &lt;br /&gt;
|-valign=&amp;quot;bottom&amp;quot; &lt;br /&gt;
| Bei den Beispielen im vorigen Abschnitt habe wir das Speicherlayout und die Bedeutung der einzelnen Bits bzw. Bit-Gruppen festgelegt. Wir bezeichnen eine auf diese Weise definierte Datenstruktur als &amp;lt;b&amp;gt;Datenformat&amp;lt;/b&amp;gt;. Datenformate werden vor allem verwendet, um Datenstrukturen auf Festplatte oder in einer Datenbank zu speichern und Daten über ein Netzwerk auszutauschen (vgl. den Eintrag [http://de.wikipedia.org/wiki/Dateitypen Dateityp] in der WikiPedia). Aus Sicht des Betriebssystems ist ein File einfach eine Folge von Bits, deren Bedeutung aus anderen Informationen geschlossen werden muss, z.B. aus der Endung des Filenames (.jpg, .png, .xml usw.) oder aus dem mit dem File assoziierten [http://de.wikipedia.org/wiki/Internet_Media_Type MIME-Type]. Viele Fileformate beginnen zudem mit bestimmten Bitfolgen (&amp;quot;[http://de.wikipedia.org/wiki/Magische_Zahl_%28Informatik%29 magischen Zahlen]&amp;quot;), die für das betreffende Fileformat charakteristisch sind. Jedes JPEG-File beginnt z.B. mit dem Bytemuster &amp;lt;tt&amp;gt;255 216 255&amp;lt;/tt&amp;gt;, jedes PNG-File mit der Folge &amp;lt;tt&amp;gt;137 80 78 71&amp;lt;/tt&amp;gt;, jedes XML-File mit dem String &amp;lt;tt&amp;gt;&amp;quot;&amp;amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot; ?&amp;amp;gt;&amp;lt;/tt&amp;gt; (wobei Versionsnummer und Zeichensatzdefinition natürlich verschieden sein können, je nach Fileinhalt). Wann immer möglich sollte man bei der Verwendung von Datenformaten auf vorhandene Standards (wie z.B. IEEE 754 für Gleitkommazahlen oder XML für hierarchisch strukturierte Dokumente) zurückgreifen, weil sonst beim Einlesen und Interpretieren der gespeicherten Bitfolgen sehr leicht Fehler passieren.&lt;br /&gt;
&lt;br /&gt;
Innerhalb einer Programmiersprache werden Datenstrukturen typischerweise nicht als Datenformate definiert, sondern durch die Verknüpfung eines Speicherlayouts mit einer &amp;lt;i&amp;gt;Menge erlaubter Operationen&amp;lt;/i&amp;gt; auf diesen Daten. Die Interpretation ergibt sich implizit aus der Definition dieser Operationen. Verwendet man beispielsweise eine Folge von 32 Bits zusammen mit den arithmetischen Operationen für natürliche Zahlen (inklusive der zugehörigen Vor- und Nachbedingungen), ist die Interpretation als &amp;lt;tt&amp;gt;uint32&amp;lt;/tt&amp;gt; dadurch gegeben. Eine Folge von Bytes mit den Operationen &amp;lt;tt&amp;gt;print&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;append&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;toLowerCase&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;toUpperCase&amp;lt;/tt&amp;gt; usw. weist auf die Interpretation &amp;quot;Zeichenkette&amp;quot; (&amp;lt;tt&amp;gt;string&amp;lt;/tt&amp;gt;). Eine solche Verknüpfung von Datenrepräsentation mit Operationen bezeichnen wir als '''(Daten-)Typ''' oder '''Klasse'''. Klassen sind für den Programmierer das wichtigste Mittel, um eigene Datenstrukturen zu definieren, und wir werden in der Vorlesung ausführlich darauf eingehen.&lt;br /&gt;
&lt;br /&gt;
Die dritte Möglichkeit ist schließlich die Kombination einer Interpretation mit einer Menge erlaubter Operationen, ohne ein bestimmtes Speicherlayout oder eine konkrete Implementation der Operationen festzulegen. In diesem Fall sprechen wir von '''Abstrakten Datentypen''' (ADTs). Diese spielen beim Entwurf von anwendungsübergreifenden Programmierschnittstellen und bei der theoretischen Analyse von Algorithmen und Datenstrukturen eine wichtige Rolle. Da von den Besonderheiten einer bestimmten Implementation und eines bestimmten Computers abstrahiert wird, sind die gewonnen Erkenntnisse auf viele Anwendungen übertragbar. Konzepte, die als abstrakte Datentypen definiert sind, können je nach Kontext immer wieder anders implementiert werden, ohne dass die übergreifenden (abstrakten) Eigenschaften verloren gehen. Viele der konkreten Datenstrukturen, die wir behandeln werden, kann man zu abstrakten Datenstrukturen verallgemeinern. Dies ist eine Schlüsselaufgabe beim Entwurf wiederverwendbarer Programmbibliotheken. Wir kommen im Kapitel [[Generizität]] auf ADTs zurück.&lt;br /&gt;
&lt;br /&gt;
Man kann sich die drei Möglichkeiten &amp;quot;Speicherlayout&amp;quot;, &amp;quot;Bedeutung&amp;quot; und &amp;quot;Menge der darauf ausführbaren Operatoren&amp;quot; als Ecken eines Dreiecks wie in der nebenstehenden Skizze vorstellen. Definiert man zwei Ecken des Dreiecks, ist auch die dritte weitgehend (oder zumindest zu einem gewissen Grade, wie bei ADTs) festgelegt. Die drei Kanten entsprechen den drei Arten der Datenstrukturen: Legt man &amp;quot;Speicherlayout&amp;quot; und &amp;quot;Bedeutung&amp;quot; fest, erhalten wir ein Datenformat, bei &amp;quot;Speicherlayout&amp;quot; plus &amp;quot;Operatoren&amp;quot; einen Klasse bzw. einen Typ, und aus &amp;quot;Operatoren&amp;quot; plus &amp;quot;Bedeutung&amp;quot; folgt ein abstrakter Datentyp.&lt;br /&gt;
| [[Image:Dt dreieck.png|400px]] &amp;lt;br&amp;gt; &amp;lt;center&amp;gt;Datenstruktur-Dreieck&amp;lt;/center&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Wichtige Begriffe ===&lt;br /&gt;
&lt;br /&gt;
Programmiersprachen, die ausgereifte Mechanismen zur Definition von Klassen bieten, werden als ''objekt-orientiert'' bezeichnet. Sprachen heißen ''streng typisiert'', wenn der Compiler bzw. Interpreter der Sprache sicherstellt, dass auf jeder Datenstruktur nur die jeweils explizit erlaubten Operationen ausgeführt werden (jeder Versuch, eine illegale Operation auszuführen, wird hier als Fehler signalisiert). Erfolgt diese Prüfung während der Compilierung (also während der Übersetzung des Quellcodes in eine Maschinensprache), spricht man von einer ''statisch typisierten Sprache''. Wird die Prüfung hingegen während der Ausführung des Programms durchgeführt, handelt es sich um eine ''dynamisch typisierte Sprache''. Python ist eine dynamisch-typisierte, objekt-orientierte Sprache. Streng typisiert ist sie allerdings nur für die vordefinierten Klassen. Bei benutzerdefinierten Klassen gibt es (wie bei den meisten anderen Programmiersprachen auch) Möglichkeiten, die erlaubten Operationen zu umgehen. Dies sollte man allerdings nur dann tun, wenn es einen wichtigen Grund gibt. Solange man sich nämlich auf die erlaubten Operationen beschränkt, ist eine große Menge von Fehlerquellen von vornherein ausgeschlossen. &lt;br /&gt;
&lt;br /&gt;
Ein bestimmter Speicherbereich, der den Anforderungen an eine Klasse genügt (wo also die Bits in entsprechender Weise gruppiert und interpretiert werden), wird als '''Objekt''' dieser Klasse oder als '''Instanz''' bezeichnet. Jede Instanz hat eine eindeutige Identität, einen ''Schlüssel''. Innerhalb eines Programms wird dafür gewöhnlich die Speicheradresse des ersten Bytes der Instanz (also der Index der ersten Speicherzelle) verwendet. Dies ist besonders effizient, weil die Speicheradresse für jedes Objekt eindeutig und leicht feststellbar ist. Ist das Objekt hingegen als Datei gespeichert, benötigt man einen expliziten Schlüssel, z.B. den Dateinamen oder die URL. &lt;br /&gt;
&lt;br /&gt;
Das Bitmuster selbst bzw. die daraus folgende Interpretation wird als '''Zustand''' oder '''Wert''' der Instanz bezeichnet. Daraus folgt, dass verschiedene Instanzen einer Klasse dennoch gleiche Werte haben können. Die Menge aller legalen Werte bilden den ''Wertebereich'' der Klasse. Werden Instanzen ausschließlich mit den explizit erlaubten Operationen ihrer Klasse manipuliert, können niemals illegale Werte entstehen. Es liegt auf der Hand, dass illegale Werte schwerwiegende Programmfehler darstellen, die man auf diese Weise vermeidet. [Computerviren tun genau das Gegenteil: Sie verwenden absichtlich verbotene Operationen, um das Programm in einen illegalen, vom Angreifer gewünschten Zustand zu bringen. Dies ist möglich, weil nicht alle verbotenen Operationen automatisch als Fehler erkannt werden, siehe oben.]&lt;br /&gt;
&lt;br /&gt;
Die meisten Programmiersprachen haben einen oder mehrere spezielle Typen für das Speichern von Objektschlüsseln. Die gebräuchlichsten Namen für diese Typen sind ''Zeiger'' (pointer), ''Referenz'' (reference) und ''Handle''. Wir verwenden das Wort '''Referenz'''. Ein Objekt der Klasse Referenz enthält also den Schlüssel eines anderen Objekts. Man sagt, dass die Referenz ''auf das andere Objekt verweist''. Diese Art der Indirektion ist uns heutzutage durch das Internet bestens vertraut: Jede WWW-Seite ist ein Objekt, und seine URL ist der dazugehörige Schlüssel. Hyperlinks und Lesezeichen (bookmarks) hingegen sind Referenzen, die mittels der URL auf andere Seiten verweisen.&lt;br /&gt;
&lt;br /&gt;
Aus der Unterscheidung von Werten und Referenzen ergibt sich die wichtige Unterscheidung von ''Wertsemantik'' und ''Referenzsemantik''. Wird nämlich ein Objekt an eine Variable zugewiesen&lt;br /&gt;
 x = anObject&lt;br /&gt;
so hängt die korrekte Verwendung der Variablen &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; davon ab, ob sie das Objekt in Form eines Wertes oder einer Referenz speichert. Im ersten Fall wird das Objekt selbst kopiert, und es entsteht ein neues Objekt mit neuer Identität, aber gleichem Zustand. Im anderen Fall wird nur der Schlüssel kopiert, und die Referenz verweist nach wie vor auf das ursprüngliche Objekt. Ist &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; ein Wert, so verändert eine Manipulation von &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; nur das neue Objekt (das ursprüngliche bleibt erhalten). Ist &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; hingegen eine Referenz, wird immer das ürsprüngliche Objekt manipuliert (denn es gibt ja keine Kopie). Ob eine Variable einen Wert oder eine Referenz enthält, wird in jeder Programmiersprache anderes festgelegt. In Python gilt&lt;br /&gt;
* Zahlen (Typen &amp;lt;tt&amp;gt;bool&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;int&amp;lt;/tt&amp;gt;, und &amp;lt;tt&amp;gt;float&amp;lt;/tt&amp;gt;) werden immer als Werte gespeichert und kopiert.&lt;br /&gt;
* Alle anderen Typen werden als Referenzen gespeichert und kopiert.&lt;br /&gt;
* Für alle Typen kann Wertsemantik mit Hilfe des Python-Moduls [http://docs.python.org/lib/module-copy.html copy] erzwungen werden.&lt;br /&gt;
Das Verständnis von Werten und Referenzen wird in der 1. Übung vertieft.&lt;br /&gt;
&lt;br /&gt;
Der Entwurf von Datentypen bzw. Klassen wird uns im Laufe der Vorlesung immer wieder beschäftigen.&lt;br /&gt;
&lt;br /&gt;
== Fundamentale Algorithmen ==&lt;br /&gt;
&lt;br /&gt;
Einige Algorithmen werden praktisch bei jeder Klasse benötigt, unabhängig vom eigentlichem Verwendungszweck der Klasse. Es ist wichtig, diese fundamentalen Algorithmen zu kennen. Außerdem eignen sie sich gut zur Einführung der Grundprinzipien der Algorithmen-Spezifikation mittels Vor- und Nachbedingungen. Diese Bedingungen beschreiben Eigenschaften, die die Variablen des Systems ''vor'' bzw. ''nach'' der Ausführung des Algorithmus haben sollen. Damit man außerdem die Veränderungen durch den Algorithmus beschreiben kann, führt man zu jeder Variablen (z.B. &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt;) eine Hilfsvariable (z.B. &amp;lt;tt&amp;gt;x&amp;lt;sub&amp;gt;o&amp;lt;/sub&amp;gt;&amp;lt;/tt&amp;gt;, sprich &amp;quot;x-old&amp;quot;) ein. In den Hilfsvariablen wird der Zustand ''vor'' der Ausführung des Algorithmus gespeichert, so dass man diesen noch abfragen kann, wenn Variablen durch den Algorithmus verändert werden. Wenn der Algorithmus beispielsweise die Variable &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; inkrementiert (um eins erhöht), gilt die Nachbedingung &amp;lt;tt&amp;gt;x == x&amp;lt;sub&amp;gt;o&amp;lt;/sub&amp;gt; + 1&amp;lt;/tt&amp;gt; (darin ist &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; der neue, und &amp;lt;tt&amp;gt;x&amp;lt;sub&amp;gt;o&amp;lt;/sub&amp;gt;&amp;lt;/tt&amp;gt; der alte Wert der Variablen). Falls &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; hingegen nicht verändert wird, gilt &amp;lt;tt&amp;gt;x == x&amp;lt;sub&amp;gt;o&amp;lt;/sub&amp;gt;&amp;lt;/tt&amp;gt;. (Man beachte, dass dies in der Literatur nicht einheitlich gehandhabt wird -- einige Autoren verwenden z.B. &amp;lt;tt&amp;gt;x&amp;lt;/tt&amp;gt; für den Zustand vor Ausführung des Algorithmus, und &amp;lt;tt&amp;gt;x'&amp;lt;/tt&amp;gt; für denjenigen danach. Diese Syntax ist jedoch mit den meisten Programmiersprachen inkompatibel.)&lt;br /&gt;
&lt;br /&gt;
Die wichtigste Gruppe von fundamentalen Funktionen sind die '''Konstruktoren''', die einen vorher unbenutzten Speicherbereich in eine Datenstruktur mit einem wohldefinierten Anfangswert transformieren. In Python haben die Konstruktoren im allgemeinen den gleichen Namen wie die dazugehörige Klasse, also z.B.&lt;br /&gt;
 i = int()   # erzeuge eine ganze Zahl mit Anfangswert 0&lt;br /&gt;
 f = float() # erzeuge eine Gleitkommazahl mit Anfangswert 0&lt;br /&gt;
 a = list()  # erzeuge ein leeres Array&lt;br /&gt;
usw. (Man beachte, dass das Python-Array den Klassennamen &amp;lt;tt&amp;gt;list&amp;lt;/tt&amp;gt; hat. Dies hat nichts mit verketteten Listen zu tun.) Konstruktoren ohne Argumente bezeichnet man als ''Standard-Konstruktoren'' (default constructors). Ja nach Typ gibt es meist noch weitere Konstruktoren, die Objekte mit anderen Anfangswerten erzeugen, z.B.&lt;br /&gt;
 i = int(2)     # erzeuge eine ganze Zahl mit Anfangswert 2&lt;br /&gt;
 i = 2          # ebenso (abgekürzte Schreibweise)&lt;br /&gt;
 f = float(1.5) # erzeuge eine Gleitkommazahl mit Anfangswert 1.5&lt;br /&gt;
 f = 1.5        # ebenso (abgekürzte Schreibweise)&lt;br /&gt;
 a = [i, f]     # erzeuge ein Array mit Kopien der Werte von i und f&lt;br /&gt;
(Das Array &amp;lt;tt&amp;gt;a&amp;lt;/tt&amp;gt; enthält Kopien der Werte, weil Zahlen immer mit Wertsemantik zugewiesen werden.) Die allgemeine Spezifikation eines Standard-Konstruktors lautet&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
\mathrm{Precondition: } &amp;amp; T \in \mathrm{Types}\\&lt;br /&gt;
\mathrm{Constructor: } &amp;amp; t = T() \\&lt;br /&gt;
\mathrm{Postcondition: } &amp;amp; t \in T&lt;br /&gt;
\end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Der Ausdruck &amp;lt;math&amp;gt;t \in T&amp;lt;/math&amp;gt; besagt, dass t nach Ausführung des Konstruktors eine legale Instanz des Typs T (oder eine Referenz auf einen solche Instanz) sein muss. In Pythonsyntax kann dies folgendermassen geschrieben werden&lt;br /&gt;
 import inspect           # wir brauchen das inspect-Modul&lt;br /&gt;
 &lt;br /&gt;
 if inspect.isclass(T):   # prüfe, dass T ein Type ist&lt;br /&gt;
      t = T()&lt;br /&gt;
 assert isinstance(t, T)&lt;br /&gt;
Natürlich funktioniert der Code nur, wenn die Klasse &amp;lt;tt&amp;gt;T&amp;lt;/tt&amp;gt; tatsächlich existiert und dafür ein Standardkonstruktor definiert wurde. Das Gegenstück zu Konstruktoren sind die '''Destruktoren''', die den Speicher der Datenstruktur wieder frei geben. Da Python automatisches Speichermanagment unterstützt, werden die Destruktoren automatisch aufgerufen. Wir können sie deshalb hier übergehen.&lt;br /&gt;
&lt;br /&gt;
Sehr wichtig sind auch die '''Vergleichsoperatoren'''. Wir müssen dabei unterscheiden, ob auf Gleichheit der Referenzen (''identity'') oder auf Gleichkeit der Werte (''equality'') geprüft werden soll. In Python werden dazu die Operatoren &amp;lt;tt&amp;gt;is&amp;lt;/tt&amp;gt; bzw. &amp;lt;tt&amp;gt;==&amp;lt;/tt&amp;gt; verwendet. Die Negation erhält man durch &amp;lt;tt&amp;gt;is not&amp;lt;/tt&amp;gt; bzw. &lt;br /&gt;
&amp;lt;tt&amp;gt;!=&amp;lt;/tt&amp;gt;&lt;br /&gt;
 a = [1, 2]&lt;br /&gt;
 b = [1, 2]&lt;br /&gt;
 &lt;br /&gt;
 a == b      # True  weil gleiche Werte&lt;br /&gt;
 a != b      # False weil Negation&lt;br /&gt;
 a is b      # False weil unterschiedliche Identität&lt;br /&gt;
 a is not b  # True  weil Negation&lt;br /&gt;
&lt;br /&gt;
(Beachte: beim Vergleich von Zahlen des gleichen Typs liefern &amp;lt;tt&amp;gt;is&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;==&amp;lt;/tt&amp;gt; immer dasselbe Ergebnis.) Natürlich impliziert die Gleichheit der Schlüssel (Identität der Objekte) die Gleichheit der Werte.&lt;br /&gt;
&lt;br /&gt;
Ebenso wichtig sind die '''Zuweisungen'''. Hier zeigt sich besonders der Unterschied zwischen Wert- und Referenzsemantik. Im Falle von Wertsemantik gilt&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
\mathrm{Preconditions: } &amp;amp; s,t \in T \\&lt;br /&gt;
                         &amp;amp; s \mathrm{\ is\ not\ } t \\&lt;br /&gt;
\mathrm{Assign\ by\ value: } &amp;amp; s = t \\&lt;br /&gt;
\mathrm{Postconditions: } &amp;amp; t \mathrm{\ is\ } t_o \\&lt;br /&gt;
                          &amp;amp; s \mathrm{\ is\ not\ } t \\&lt;br /&gt;
                          &amp;amp; s == t &lt;br /&gt;
\end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Das heisst, t darf sich nicht verändern, und s hat nach der Zuweisung den gleichen Wert wie t. Bei Referenzsemantik gilt sogar&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
\mathrm{Precondition: } &amp;amp; t \in T \\&lt;br /&gt;
\mathrm{Assign\ by\ reference: } &amp;amp; s = t \\&lt;br /&gt;
\mathrm{Postconditions: } &amp;amp; t \mathrm{\ is\ } t_o \\&lt;br /&gt;
                          &amp;amp; s \mathrm{\ is\ } t&lt;br /&gt;
\end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Dies entspricht dem Pythoncode&lt;br /&gt;
 x = y&lt;br /&gt;
 assert x is y&lt;br /&gt;
Die Wertsemantik muss man in Python explizit erzwingen&lt;br /&gt;
 import copy  # wir brauchen das copy-Modul&lt;br /&gt;
 &lt;br /&gt;
 x = copy.deepcopy(y)&lt;br /&gt;
 assert x == y&lt;br /&gt;
 assert x is not y&lt;br /&gt;
&lt;br /&gt;
Mit der Zuweisung eng verwandt ist die Funktion &amp;lt;tt&amp;gt;swap&amp;lt;/tt&amp;gt;, die den Inhalt von zwei Variablen vertauscht:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt;\begin{array}{ll}&lt;br /&gt;
\mathrm{Precondition: } &amp;amp; t \in T, s \in S \\&lt;br /&gt;
\mathrm{Algorithm\ swap: } &amp;amp; \mathrm{swap}(s, t) \\&lt;br /&gt;
\mathrm{Postconditions: } &amp;amp; t \mathrm{\ is\ } s_o \\&lt;br /&gt;
                          &amp;amp; s \mathrm{\ is\ } t_o&lt;br /&gt;
\end{array}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Diese Funktion wird sich beim Sortieren als sehr nützlich erweisen, weil dort das Vertauschen von zwei Datenelementen eine Grundoperation ist. In Python kann man dies so implementieren:&lt;br /&gt;
 t, s = s, t  # swap&lt;br /&gt;
Dabei macht man sich zunutze, dass Python mehrere Variablen in einem einzigen Statement zuweisen kann.&lt;br /&gt;
&lt;br /&gt;
[[Container|Nächstes Thema]]&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Main_Page&amp;diff=5697</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=5697"/>
				<updated>2020-04-23T19:20:51Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* 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;
apl. Prof. Dr. Ullrich Köthe, Universität Heidelberg, Sommersemester 2020&lt;br /&gt;
&lt;br /&gt;
Die Vorlesung findet '''dienstags''' um 14:15 Uhr und '''donnerstags''' um 16:15 Uhr online auf Discord und Twitch statt. Die Links haben in Müsli angemeldete Teilnehmer per Email erhalten.&lt;br /&gt;
&lt;br /&gt;
=== Klausur und Nachprüfung ===&lt;br /&gt;
&lt;br /&gt;
Der Termin der '''Abschlussklausur''' steht noch nicht fest.&lt;br /&gt;
&amp;lt;!--Die '''Klausur 1''' findet am Donnerstag, dem 20.7.2017 von 13:00 bis 14:30 Uhr im Großen Hörsaal Chemie (INF 252) statt. &amp;lt;b&amp;gt;Bitte melden Sie sich in [https://muesli.mathi.uni-heidelberg.de/lecture/view/707 MÜSLI] für die Klausur an.&amp;lt;/b&amp;gt; Zur Klausur wird zugelassen, wer mindestens 50% der Übungspunkte erreicht und eine Aufgabe in der Übungsgruppe vorgerechnet hat. (Hinweis: Sie benötigen einen Lichtbildausweis, um sich bei der Klausur zu indentifizieren!)--&amp;gt;&lt;br /&gt;
&amp;lt;!---&lt;br /&gt;
* '''[[Media:2014-Klausur-1.pdf|Ergebnis der Klausur 1 vom 29.7.2014]]''' (anonymisiert)&lt;br /&gt;
Die '''Klausur 2''' findet am Mittwoch, dem 8.10.2014 von 10:00 bis 12:00 Uhr im Seminarraum des [http://hci.iwr.uni-heidelberg.de/contact.php HCI, Speyerer Str. 6], statt. Teilnahmeberechtigt sind diejenigen, die Klausur 1 nicht bestanden haben (bitte unbedingt per Email &amp;lt;b&amp;gt;anmelden!&amp;lt;/b&amp;gt;) sowie diejenigen, die mich vorab informiert haben (Sie brauchen sich nicht nochmals anzumelden).&lt;br /&gt;
* '''[[Media:2014-Klausur-2.pdf|Ergebnis der Klausur 2 vom 8.10.2014]]''' (anonymisiert)&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;
&amp;lt;!-------&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;
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;
&amp;lt;!---&lt;br /&gt;
* Wegen der großen Nachfrage haben wir zusätzliche Tutoren/Korrektoren eingestellt und die Gruppengröße auf 32 Teilnehmer erhöht. Bitte melden Sie sich im [https://www.mathi.uni-heidelberg.de/muesli/lecture/view/355 MÜSLI] an.&lt;br /&gt;
* Termine und Räume: &lt;br /&gt;
** Mo 14:00 - 16:00 Uhr, INF 501, R-102 (Tutor und Korrektor: Niels Buwen [mailto:buwen@stud.uni-heidelberg.de buwen AT stud.uni-heidelberg.de])&lt;br /&gt;
** Mo 16:00 - 18:00 Uhr, INF 288, HS6 (Tutor: Niels Buwen, Korrektor: Katrin Honauer [mailto:katrin.honauer@iwr.uni-heidelberg.de katrin.honauer AT iwr.uni-heidelberg.de])&lt;br /&gt;
** Di  9:00 - 11:00 Uhr, &amp;lt;b&amp;gt;neuer Raum:&amp;lt;/b&amp;gt; INF 294, R-103 (Tutor und Korrektor: Marius Killinger [mailto:marius.felix.killinger@stud.uni-heidelberg.de marius.felix.killinger AT stud.uni-heidelberg.de])&lt;br /&gt;
** Di 11:00 - 13:00 Uhr, INF 294, R-102 (Tutor und Korrektor: Fynn Beuttenmüller: [mailto:f.beuttenmueller@gmx.de f.beuttenmueller AT gmx.de])&lt;br /&gt;
** Mi 14:00 - 16:00 Uhr, INF 294, R-113 (Tutor: Axel Wagner, Korrektor: Philipp Schubert [mailto:phil.jo.schubert@gmail.com phil.jo.schubert AT gmail.com])&lt;br /&gt;
** Mi 16:00 - 18:00 Uhr, INF 294, R-113 (Tutor und Korrektor: Axel Wagner: [mailto:axel.wagner@stud.uni-heidelberg.de axel.wagner AT stud.uni-heidelberg.de])&lt;br /&gt;
---&amp;gt;&lt;br /&gt;
* Die Übungsgruppen werden über &amp;lt;b&amp;gt;[https://muesli.mathi.uni-heidelberg.de/lecture/view/1171 MÜSLI]&amp;lt;/b&amp;gt; verwaltet. &lt;br /&gt;
* Übungsblätter werden auf [https://moodle.uni-heidelberg.de/course/view.php?id=2239 Moodle] veröffentlicht.&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;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 Korrektor.&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 Ullrich Köthe auf Anfrage gerne einrichtet.&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;
-------&amp;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;
(Termine werden nach und nach aktualisiert)&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Einführung]] (21. und 23.4.2020) &lt;br /&gt;
#* Definition von Algorithmen und Datenstrukturen, Geschichte&lt;br /&gt;
#* Fundamentale Algorithmen: Konstruktoren, Kopierfunktionen, swap.&lt;br /&gt;
#* Fundamentale Datenstrukturen: Zahlen, Container, Handles&lt;br /&gt;
#* Python-Grundlagen&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Container]] (28.4.2020)&lt;br /&gt;
#* Abstrakte Datentypen und algebraische Spezifikation&lt;br /&gt;
#* Grundlegende Container: Array, Stack, Queue, assoziatives Array&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Sortieren]] (27. bis 4.5.2017)&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;
#* Anzahl der benötigten Vergleiche&lt;br /&gt;
&amp;lt;!-------------&amp;gt;&lt;br /&gt;
# [[Korrektheit]] (29.4. und 6.5.2014 -- ab hier altes Datum)&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]] (8. und 13.5.2014)&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]] (15. und 20.5.2014)&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]] (22.5.2014)&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]] (27.5.2014)&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]] (3.6.2014)&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. und 10.6.2014)&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]] (12.6.2014)&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]] (17.6.2014)&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]] (24.6. bis 10.7.2014)&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 15.7.2014)&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.2014)&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]] (22.7.2014)&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;
# Wiederholung (24.7.2014)&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 verspäteter Abgabe bis zu drei Tagen werden noch 50% der erreichten Punkte angerechnet. Danach wird die Musterlösung freigeschaltet. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;i&amp;gt;Die Übungsaufgaben sind zur Zeit nicht freigeschaltet.&amp;lt;/i&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!------&lt;br /&gt;
# [[Media:Uebung-1.pdf|Übung]] (Abgabe 29.4.2014) und [[Media:muster_blatt1.pdf|Musterlösung]]&lt;br /&gt;
#* Python-Tutorial&lt;br /&gt;
#* Sieb des Eratosthenes&lt;br /&gt;
#* Wert- und Referenzsemantik&lt;br /&gt;
#* Freitag der 13.&lt;br /&gt;
#* Dynamisches Array&lt;br /&gt;
# [[Media:Uebung-2.pdf|Übung]] (Abgabe 6.5.2014) und [[Media:muster_blatt2.pdf|Musterlösung]]&lt;br /&gt;
#* Sortieren: Implementation und Geschwindigkeitsvergleich (Diagramme in Abhängigkeit von der Problemgröße)&lt;br /&gt;
#* Zelluläre Automaten&lt;br /&gt;
# [[Media:Uebung-3.pdf|Übung]] (Abgabe 13.5.2014) und [[Media:muster_blatt3.pdf|Musterlösung]]&lt;br /&gt;
#* Manuelles Debuggen&lt;br /&gt;
#* Einführung in 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 20.5.2014) 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]] (Abgabe 27.5.2014) 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 '''Donnerstag''' 5.6.2014) 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 die Files [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/die-drei-musketiere.txt die-drei-musketiere.txt] und [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/stopwords.txt stopwords.txt]. Die Zeichenkodierung in diesen Files ist Latin-1.)&lt;br /&gt;
#* BucketSort&lt;br /&gt;
# [[Media:Uebung-7.pdf|Übung]] (Abgabe 12.6.2014) und [[Media:muster_blatt7.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 19.6.2014) 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 '''Dienstag''' 1.7.2014) und Musterlösung für [[Media:muster_blatt9-1+3.pdf|Aufgaben 1 und 3]] sowie für [[Media:muster_blatt9-2.pdf|Aufgabe 2]]&lt;br /&gt;
#* Übungen zur Generizität: Sortieren mit veränderter Ordnung, Iterator für Tiefensuche&lt;br /&gt;
#* Graphenaufgaben: Weg aus einem Labyrinth, Erzeugen einer perfekten Hashfunktion (Dazu benötigen Sie den Artikel [http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.5566 &amp;lt;i&amp;gt;&amp;quot;An optimal algorithm for generating minimal perfect hash functions&amp;quot;&amp;lt;/i&amp;gt;] sowie 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-10.pdf|Übung]] (Abgabe 8.7.2014) und Musterlösung für [[Media:muster_blatt10-1.pdf|Aufgabe 1]] sowie für [[Media:muster_blatt10-2.pdf|Aufgabe 2]]&lt;br /&gt;
#* Fortgeschrittene Graphenaufgaben: Routenplaner (Dazu benötigen Sie wieder das File  [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/entfernungen.json entfernungen.json]. Die Zeichenkodierung in diesem File ist UTF-8.) und Bildverarbeitung (Dazu benötigen Sie 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-11.pdf|Übung]] (Abgabe 15.7.2014) &lt;br /&gt;
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, Seam Carving (Dazu benötigen Sie wieder die Files [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/entfernungen.json entfernungen.json] und [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/pgm.py pgm.py] aowie das Bild [http://hci.iwr.uni-heidelberg.de/Staff/ukoethe/download/coast.pgm coast.pgm].)&lt;br /&gt;
# [[Media:Uebung-12.pdf|Übung]] (Abgabe 22.7.2014)&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;
#* Bonusaufgaben: indirektes Sortieren und Prüfungswiederholung&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;
# [[Media:Bonusuebung.pdf|Übung (Bonus)]] (&amp;lt;font color=red&amp;gt;Achtung: Abgabe bereits am Dienstag, 24.7.2014&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>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Korrektheit&amp;diff=5696</id>
		<title>Korrektheit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=Korrektheit&amp;diff=5696"/>
				<updated>2019-05-02T10:04:18Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: &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&amp;amp;le;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://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.54.3892 Practical Strategy for Testing Pair-wise Coverage of Network Interfaces], [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>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5690</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5690"/>
				<updated>2017-08-03T15:29:56Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Alg. 2 (det. Alg. für k=2 mittels SZK in gerichtetem Graphen) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage Y und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei Y impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern: [[Image:Bild 11.jpg]]&lt;br /&gt;
: Jede logische Schaltung kann als SAT-Ausdruck geschrieben werden.&lt;br /&gt;
* Bsp.: Zuordnung von Heim und Auswärtsspielen beim Fußball&lt;br /&gt;
: &amp;lt;math&amp;gt;x_{it} = \begin{cases} &lt;br /&gt;
true,  &amp;amp; \mbox{Mannschaft i hat am Spieltag t Heimspiel} \\&lt;br /&gt;
false, &amp;amp; \mbox{Mannschaft i hat am Spieltag t Auswärtsspiel} &lt;br /&gt;
\end{cases}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
: 1. Nebenbedingung: spielt Mannschaft i am Spieltag t gegen Mannschaft j, muss gelten &amp;lt;math&amp;gt; x_{it} = \lnot x_{jt}&amp;lt;/math&amp;gt; &lt;br /&gt;
: 2. Nebenbedingung: Jede Mannschaft spielt gegen jede&lt;br /&gt;
: 3. Nebenbedingung: Jede Mannschaft spielt abwechselnd Heim und auswärts &amp;lt;math&amp;gt; x_{it} \neq x_{i(t+1)} &amp;lt;/math&amp;gt;&lt;br /&gt;
: Alle Bedingungen sollen gleichzeitig Erfüllt sein: &lt;br /&gt;
:: &amp;lt;math&amp;gt;y = \begin{cases} (x_{11} \neq x_{21}) \and (x_{31} \neq x_{41}) \and ... \\ &lt;br /&gt;
( x_{12} \neq x_{32} ) \and ... \\&lt;br /&gt;
( x_{11} \neq x_{12} ) \and ( x_{12} \neq x_{13} ) \and ...&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&lt;br /&gt;
: Frage: Kann man X so belegen, dass Y wahr ist? &lt;br /&gt;
: &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Nein, nur möglich wenn es nur 2 Mannschaften gibt und diese abwechselnd gegeneinander antreten. &lt;br /&gt;
&lt;br /&gt;
Normalformen für logische Ausdrücke zur Vereinfachung und Systematisierung&lt;br /&gt;
* 3-CNF (Konjunktionen-NF)&lt;br /&gt;
** jede Klausel enthält max 3 Variablen (genau 3 mit dummy Variablen)&lt;br /&gt;
** jede Klausel enthält nur &amp;lt;math&amp;gt; \or &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; /lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
** alle Klauseln sind durch &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft.&lt;br /&gt;
z.B. &amp;lt;math&amp;gt; ( x_1 \or x_2 \or \lnot x_4 ) \and ( \lnot x_2 \or x_3 \or x_4) \and (...) \and &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \Rightarrow &amp;lt;/math&amp;gt; Ausdruck ist wahr, wenn jede Klausel wahr ist. &lt;br /&gt;
: In jeder Klausel hat man 3 Chancen die Klausel wahr zu machen. &lt;br /&gt;
: Aber: Klauseln können sich widersprechen und nicht erfüllbar sein!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz:&amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; Jeder logische Ausdruck effizient (in pol. Zeit) in 3-CNF umwandelbar.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz v. Cook: &amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; 3-SAT (Erfüllbarkeitsproblem für Ausdrücke in 3-CNF) ist NP-vollständig&lt;br /&gt;
&lt;br /&gt;
zur Zeit ist kein effizienterer Algorithmus bekannt, als im schlechtesten Fall alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Belegungen von {&amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt;} auszuprobieren &lt;br /&gt;
&lt;br /&gt;
2-CNF: wie 3-CNF, nur 2 Variablen pro Klausel&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; effiziente Alg existieren, aber nicht jeder logische Ausdruck in 2-CNF transformierbar.&lt;br /&gt;
: z.B. Heim-Auswärtsproblem&lt;br /&gt;
&lt;br /&gt;
INF (Implikationen-NF):&lt;br /&gt;
* 2 Variablen pro Klausel, Operatoren &amp;lt;math&amp;gt; \implies und \lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
* Klauseln mit &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft. &lt;br /&gt;
&lt;br /&gt;
Satz: jede 2-CNF effizient in INF umwandelbat.&lt;br /&gt;
: &amp;lt;math&amp;gt; ( x_i \or x_j ) \rightsquigarrow ( \lnot x_i \implies x_j ) \and ( \lnot x_j \implies x_i )&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;  INF als gerichteter Graph schreibbar und mittels starker Zusammenhangskomponenten lösbar. &lt;br /&gt;
&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;x_1&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \or x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \implies x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_1 \implies x_2 (A)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_2 \implies x_1 (B)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;A \and B&amp;lt;/math&amp;gt;  &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 1 || 0 || 0 || 0 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden.&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== k-SAT, k=2 in pol. Zeit lösbar === &lt;br /&gt;
&lt;br /&gt;
==== Alg. 1 ====&lt;br /&gt;
(f. bei k) (nur für k=2 effizient) '''Randomisiert'''&lt;br /&gt;
* (0) initialisiere &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; beliebig&lt;br /&gt;
* (1) wiederhole &amp;lt;math&amp;gt;T_{max}&amp;lt;/math&amp;gt; - mal&lt;br /&gt;
** (a) wenn das aktuelle x den Ausdruck erfüllt: return x (x=[&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;
** (b) wähle zufällig eine Klausel, die nicht erfüllt ist&lt;br /&gt;
** (c) wähle in dieser Klausel zufällig eine der k Variablen und invertiere sie =&amp;gt; Klausel ist jetzt erfüllt&lt;br /&gt;
::: (andere können jetzt false geworden sein) &lt;br /&gt;
::: (&amp;lt;math&amp;gt;x_1 \or x_2 ) \and ( x_1 \or \lnot x_2 )&amp;lt;/math&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;math&amp;gt; x_1 = 0, x_2 = 0,&amp;lt;/math&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;math&amp;gt; x_2&amp;lt;/math&amp;gt; auf 1 =&amp;gt; 1. Klausel wahr, 2. falsch&lt;br /&gt;
* (2) return &amp;quot;keine Lösung gefunden&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Nach wie vielen Iterationen wird im Mittel eine Lösung gefunden?&lt;br /&gt;
* Ausdruck unerfüllbar =&amp;gt; Endlosschleife, Timeout nach &amp;lt;math&amp;gt;T_{max}&amp;lt;/math&amp;gt; Iterationen &lt;br /&gt;
* Ausrduck erfüllbar:&lt;br /&gt;
** falls k&amp;lt;math&amp;gt;\geq&amp;lt;/math&amp;gt;3: nach &amp;lt;math&amp;gt;O((\frac{2(k-1)}{k})^N)&amp;lt;/math&amp;gt; Iterationen wird Lösung gefunden&lt;br /&gt;
** k=3: &amp;lt;math&amp;gt;O((\frac{4}{3})^N)&amp;lt;/math&amp;gt; exponentielle Zeit, wie zu erwarten für NP-vollständiges Problem&lt;br /&gt;
** k=2: &amp;lt;math&amp;gt;O(N^2)&amp;lt;/math&amp;gt; Iterationen bis Lösung&lt;br /&gt;
&lt;br /&gt;
Beweis: Algorithmus entspricht im Wesentlichen dem '''Random Walk'''&lt;br /&gt;
: Sei &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; die korrekte Lösung und x die aktuelle Belegung&lt;br /&gt;
: RW: Stuhl i &amp;lt;math&amp;gt;\mathrel{\hat=}&amp;lt;/math&amp;gt; i Variablen zwischen &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; und x stimmen überein =&amp;gt; Ziel: erreiche Stuhl N &lt;br /&gt;
* (c):&lt;br /&gt;
** Fall 1: beide Variablen falsch =&amp;gt; egal welche wir invertieren, bewegen wir uns von Stuhl i zu i+1&lt;br /&gt;
** Fall 2: eine Variable ist falsch: &lt;br /&gt;
*** mit Wahrscheinlichkeit 1/2 wählen wir diese und gehen von i nach i+1&lt;br /&gt;
*** mit Wahrscheinlichkeit 1/2 wählen wir die andere und gehen von i nach i-1&lt;br /&gt;
&lt;br /&gt;
schlechtester Fall: Es existiert keine Lösung &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; und wir haben immer Fall 2&lt;br /&gt;
: =&amp;gt;RW braucht &amp;lt;math&amp;gt;O(N^2 - i^2)&amp;lt;/math&amp;gt; Schritte zum Stuhl N &amp;lt;math&amp;gt;\mathrel{\hat=} O(N^2)&amp;lt;/math&amp;gt; falls i anfangs zufällig ist &lt;br /&gt;
&lt;br /&gt;
==== Alg. 2 (det. Alg. für k=2 mittels SZK in gerichtetem Graphen) ====&lt;br /&gt;
: geg.: Ausdruck 2-CNF&lt;br /&gt;
* (1) wandle nach INF: ersetze jede Klausel &amp;lt;math&amp;gt;(x_i \or x_j)&amp;lt;/math&amp;gt; durch &amp;lt;math&amp;gt;(\lnot x_i \Rightarrow x_j) \and (\lnot x_j \Rightarrow x_i)&amp;lt;/math&amp;gt;&lt;br /&gt;
: (entsprechend, wenn in Originalklausel &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt; vorkommen)&lt;br /&gt;
* (2) repräsentiere den Ausdruck als Graph: &lt;br /&gt;
** (a) 2 Knoten pro Var:&amp;lt;math&amp;gt;v_i \mathrel{\hat=} x_i , v_{i+N} \mathrel{\hat=} \lnot x_i &amp;lt;/math&amp;gt;&lt;br /&gt;
** (b) Verbindung für jede Implikation durch korrespondierenden Knoten durch gerichtete Kante&lt;br /&gt;
&lt;br /&gt;
Bsp.:&lt;br /&gt;
&amp;lt;math&amp;gt;C_1 \and C_2 \Leftrightarrow (\lnot x_1 \Rightarrow x_2 ) \and (\lnot x_2 \Rightarrow x_1) \and (x_2 \Rightarrow x_3) \and (\lnot x_3 \Rightarrow \lnot x_2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* (3) Prüfe ob der Ausdruck erfüllbar ist. Bilde SZK des Graphen&lt;br /&gt;
: '''Satz''': Ausdruck erfüllbar &amp;lt;math&amp;gt;\Leftrightarrow \forall&amp;lt;/math&amp;gt;i: &amp;lt;math&amp;gt; v_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v_{i+N}&amp;lt;/math&amp;gt; sind in verschiedenen Komponenten&lt;br /&gt;
&lt;br /&gt;
Beweis: in jeder SZK gilt: &amp;lt;math&amp;gt;u,v \in SZK: \exists u \rightsquigarrow v und v \rightsquigarrow u&amp;lt;/math&amp;gt;&lt;br /&gt;
: Kanten &amp;lt;math&amp;gt;\to&amp;lt;/math&amp;gt; Implikationen, Implikationen sind transitiv&lt;br /&gt;
: &amp;lt;math&amp;gt;\Rightarrow u \rightsquigarrow v \mathrel{\hat=} u \to v &amp;lt;/math&amp;gt; &amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp; &amp;lt;math&amp;gt;\to u \leftrightarrow v&amp;lt;/math&amp;gt; &amp;amp;nbsp; bzw. &amp;amp;nbsp; u == v&lt;br /&gt;
:: &amp;lt;math&amp;gt; v \rightsquigarrow u \mathrel{\hat=} v \to u &amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt; alle Knoten in einer SZK haben den gleichen Wahrheitswert true oder false&lt;br /&gt;
: aber &amp;lt;math&amp;gt;v_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v_{i+N} \mathrel{\hat=} x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\lnot x_i&amp;lt;/math&amp;gt; haben immer verschiedene Werte&lt;br /&gt;
: &amp;lt;math&amp;gt;\Rightarrow v_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v_{i+N}&amp;lt;/math&amp;gt; dürfen nicht in selber SZK sein, andernfalls fordert der Graph &amp;lt;math&amp;gt;x_i == \lnot x_i&amp;lt;/math&amp;gt;, was unmöglich ist.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* (4) Bilde den Komponentengraphen &amp;lt;math&amp;gt;\to&amp;lt;/math&amp;gt; azyklisch (zu jedem Knoten existiert Komplementärknoten mit negierter Variable)[jede SZK in je 1 Knoten kontrahieren]&lt;br /&gt;
** (b) bestehende topologische Sortierung&lt;br /&gt;
** (c) gehe in topologischer Sortierung von hinten nach vorne &lt;br /&gt;
*** (I) wenn aktueller Knoten noch keinen Wert hat: setze ihn auf true und Komplementoren false&lt;br /&gt;
*** (II) sonst: überspringe Knoten&lt;br /&gt;
&lt;br /&gt;
Beweis, dass ein Problem aus NP auch NP-vollständig ist&lt;br /&gt;
* Möglichkeit 1: z.B. 3-SAT (Satz von Cook): mühsam, aber mindestens für ein Problem unbermeidbar (für erstes)&lt;br /&gt;
* Möglichkeit 2: zeige dass  jedes Problem vom Typ A in eines von Typ B umwandelbar (in pol. Zeit)&lt;br /&gt;
** &amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt; Problem Type B nicht einfacher als Typ A&lt;br /&gt;
** falls Typ A NP-vollständig &amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt; Typ B auch&lt;br /&gt;
&lt;br /&gt;
==== Anwendung auf TSP ====&lt;br /&gt;
3-SAT &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; Hamiltonzyklus im gerichteten Graph &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; Hamiltonzyklus im ungerichteten Graph &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; TSP im gerwichteten ungerichteten Graph&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5689</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5689"/>
				<updated>2017-08-03T15:05:17Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Alg. 2 (det. Alg. für k=2 mittels SZK in gerichtetem Graphen) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage Y und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei Y impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern: [[Image:Bild 11.jpg]]&lt;br /&gt;
: Jede logische Schaltung kann als SAT-Ausdruck geschrieben werden.&lt;br /&gt;
* Bsp.: Zuordnung von Heim und Auswärtsspielen beim Fußball&lt;br /&gt;
: &amp;lt;math&amp;gt;x_{it} = \begin{cases} &lt;br /&gt;
true,  &amp;amp; \mbox{Mannschaft i hat am Spieltag t Heimspiel} \\&lt;br /&gt;
false, &amp;amp; \mbox{Mannschaft i hat am Spieltag t Auswärtsspiel} &lt;br /&gt;
\end{cases}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
: 1. Nebenbedingung: spielt Mannschaft i am Spieltag t gegen Mannschaft j, muss gelten &amp;lt;math&amp;gt; x_{it} = \lnot x_{jt}&amp;lt;/math&amp;gt; &lt;br /&gt;
: 2. Nebenbedingung: Jede Mannschaft spielt gegen jede&lt;br /&gt;
: 3. Nebenbedingung: Jede Mannschaft spielt abwechselnd Heim und auswärts &amp;lt;math&amp;gt; x_{it} \neq x_{i(t+1)} &amp;lt;/math&amp;gt;&lt;br /&gt;
: Alle Bedingungen sollen gleichzeitig Erfüllt sein: &lt;br /&gt;
:: &amp;lt;math&amp;gt;y = \begin{cases} (x_{11} \neq x_{21}) \and (x_{31} \neq x_{41}) \and ... \\ &lt;br /&gt;
( x_{12} \neq x_{32} ) \and ... \\&lt;br /&gt;
( x_{11} \neq x_{12} ) \and ( x_{12} \neq x_{13} ) \and ...&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&lt;br /&gt;
: Frage: Kann man X so belegen, dass Y wahr ist? &lt;br /&gt;
: &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Nein, nur möglich wenn es nur 2 Mannschaften gibt und diese abwechselnd gegeneinander antreten. &lt;br /&gt;
&lt;br /&gt;
Normalformen für logische Ausdrücke zur Vereinfachung und Systematisierung&lt;br /&gt;
* 3-CNF (Konjunktionen-NF)&lt;br /&gt;
** jede Klausel enthält max 3 Variablen (genau 3 mit dummy Variablen)&lt;br /&gt;
** jede Klausel enthält nur &amp;lt;math&amp;gt; \or &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; /lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
** alle Klauseln sind durch &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft.&lt;br /&gt;
z.B. &amp;lt;math&amp;gt; ( x_1 \or x_2 \or \lnot x_4 ) \and ( \lnot x_2 \or x_3 \or x_4) \and (...) \and &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \Rightarrow &amp;lt;/math&amp;gt; Ausdruck ist wahr, wenn jede Klausel wahr ist. &lt;br /&gt;
: In jeder Klausel hat man 3 Chancen die Klausel wahr zu machen. &lt;br /&gt;
: Aber: Klauseln können sich widersprechen und nicht erfüllbar sein!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz:&amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; Jeder logische Ausdruck effizient (in pol. Zeit) in 3-CNF umwandelbar.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz v. Cook: &amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; 3-SAT (Erfüllbarkeitsproblem für Ausdrücke in 3-CNF) ist NP-vollständig&lt;br /&gt;
&lt;br /&gt;
zur Zeit ist kein effizienterer Algorithmus bekannt, als im schlechtesten Fall alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Belegungen von {&amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt;} auszuprobieren &lt;br /&gt;
&lt;br /&gt;
2-CNF: wie 3-CNF, nur 2 Variablen pro Klausel&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; effiziente Alg existieren, aber nicht jeder logische Ausdruck in 2-CNF transformierbar.&lt;br /&gt;
: z.B. Heim-Auswärtsproblem&lt;br /&gt;
&lt;br /&gt;
INF (Implikationen-NF):&lt;br /&gt;
* 2 Variablen pro Klausel, Operatoren &amp;lt;math&amp;gt; \implies und \lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
* Klauseln mit &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft. &lt;br /&gt;
&lt;br /&gt;
Satz: jede 2-CNF effizient in INF umwandelbat.&lt;br /&gt;
: &amp;lt;math&amp;gt; ( x_i \or x_j ) \rightsquigarrow ( \lnot x_i \implies x_j ) \and ( \lnot x_j \implies x_i )&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;  INF als gerichteter Graph schreibbar und mittels starker Zusammenhangskomponenten lösbar. &lt;br /&gt;
&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;x_1&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \or x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \implies x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_1 \implies x_2 (A)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_2 \implies x_1 (B)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;A \and B&amp;lt;/math&amp;gt;  &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 1 || 0 || 0 || 0 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden.&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== k-SAT, k=2 in pol. Zeit lösbar === &lt;br /&gt;
&lt;br /&gt;
==== Alg. 1 ====&lt;br /&gt;
(f. bei k) (nur für k=2 effizient) '''Randomisiert'''&lt;br /&gt;
* (0) initialisiere &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; beliebig&lt;br /&gt;
* (1) wiederhole &amp;lt;math&amp;gt;T_{max}&amp;lt;/math&amp;gt; - mal&lt;br /&gt;
** (a) wenn das aktuelle x den Ausdruck erfüllt: return x (x=[&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;
** (b) wähle zufällig eine Klausel, die nicht erfüllt ist&lt;br /&gt;
** (c) wähle in dieser Klausel zufällig eine der k Variablen und invertiere sie =&amp;gt; Klausel ist jetzt erfüllt&lt;br /&gt;
::: (andere können jetzt false geworden sein) &lt;br /&gt;
::: (&amp;lt;math&amp;gt;x_1 \or x_2 ) \and ( x_1 \or \lnot x_2 )&amp;lt;/math&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;math&amp;gt; x_1 = 0, x_2 = 0,&amp;lt;/math&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;math&amp;gt; x_2&amp;lt;/math&amp;gt; auf 1 =&amp;gt; 1. Klausel wahr, 2. falsch&lt;br /&gt;
* (2) return &amp;quot;keine Lösung gefunden&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Nach wie vielen Iterationen wird im Mittel eine Lösung gefunden?&lt;br /&gt;
* Ausdruck unerfüllbar =&amp;gt; Endlosschleife, Timeout nach &amp;lt;math&amp;gt;T_{max}&amp;lt;/math&amp;gt; Iterationen &lt;br /&gt;
* Ausrduck erfüllbar:&lt;br /&gt;
** falls k&amp;lt;math&amp;gt;\geq&amp;lt;/math&amp;gt;3: nach &amp;lt;math&amp;gt;O((\frac{2(k-1)}{k})^N)&amp;lt;/math&amp;gt; Iterationen wird Lösung gefunden&lt;br /&gt;
** k=3: &amp;lt;math&amp;gt;O((\frac{4}{3})^N)&amp;lt;/math&amp;gt; exponentielle Zeit, wie zu erwarten für NP-vollständiges Problem&lt;br /&gt;
** k=2: &amp;lt;math&amp;gt;O(N^2)&amp;lt;/math&amp;gt; Iterationen bis Lösung&lt;br /&gt;
&lt;br /&gt;
Beweis: Algorithmus entspricht im Wesentlichen dem '''Random Walk'''&lt;br /&gt;
: Sei &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; die korrekte Lösung und x die aktuelle Belegung&lt;br /&gt;
: RW: Stuhl i &amp;lt;math&amp;gt;\mathrel{\hat=}&amp;lt;/math&amp;gt; i Variablen zwischen &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; und x stimmen überein =&amp;gt; Ziel: erreiche Stuhl N &lt;br /&gt;
* (c):&lt;br /&gt;
** Fall 1: beide Variablen falsch =&amp;gt; egal welche wir invertieren, bewegen wir uns von Stuhl i zu i+1&lt;br /&gt;
** Fall 2: eine Variable ist falsch: &lt;br /&gt;
*** mit Wahrscheinlichkeit 1/2 wählen wir diese und gehen von i nach i+1&lt;br /&gt;
*** mit Wahrscheinlichkeit 1/2 wählen wir die andere und gehen von i nach i-1&lt;br /&gt;
&lt;br /&gt;
schlechtester Fall: Es existiert keine Lösung &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; und wir haben immer Fall 2&lt;br /&gt;
: =&amp;gt;RW braucht &amp;lt;math&amp;gt;O(N^2 - i^2)&amp;lt;/math&amp;gt; Schritte zum Stuhl N &amp;lt;math&amp;gt;\mathrel{\hat=} O(N^2)&amp;lt;/math&amp;gt; falls i anfangs zufällig ist &lt;br /&gt;
&lt;br /&gt;
==== Alg. 2 (det. Alg. für k=2 mittels SZK in gerichtetem Graphen) ====&lt;br /&gt;
: geg.: Ausdruck 2-CNF&lt;br /&gt;
* (1) wandle nach INF: ersetze jede Klausel &amp;lt;math&amp;gt;(x_i \or x_j)&amp;lt;/math&amp;gt; durch &amp;lt;math&amp;gt;(\lnot x_i \Rightarrow x_j) \and (\lnot x_j \Rightarrow x_i)&amp;lt;/math&amp;gt;&lt;br /&gt;
: (entsprechend, wenn in Originalklausel &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt; vorkommen)&lt;br /&gt;
* (2) repräsentiere den Ausdruck als Graph: &lt;br /&gt;
** (a) 2 Knoten pro Var:&amp;lt;math&amp;gt;v_i \mathrel{\hat=} x_i , v_{i+N} \mathrel{\hat=} \lnot x_i &amp;lt;/math&amp;gt;&lt;br /&gt;
** (b) Verbindung für jede Implikation durch korrespondierenden Knoten durch gerichtete Kante&lt;br /&gt;
&lt;br /&gt;
Bsp.:&lt;br /&gt;
&amp;lt;math&amp;gt;C_1 \and C_2 \Leftrightarrow (\lnot x_1 \Rightarrow x_2 ) \and (\lnot x_2 \Rightarrow x_1) \and (x_2 \Rightarrow x_3) \and (\lnot x_3 \Rightarrow \lnot x_2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* (3) Prüfe ob der Ausdruck erfüllbar ist. Bilde SZK des Graphen&lt;br /&gt;
: '''Satz''': Ausdruck erfüllbar &amp;lt;math&amp;gt;\Leftrightarrow \forall&amp;lt;/math&amp;gt;i: &amp;lt;math&amp;gt; v_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v_{i+N}&amp;lt;/math&amp;gt; sind in verschiedenen Komponenten&lt;br /&gt;
&lt;br /&gt;
Beweis: in jeder SZK gilt: &amp;lt;math&amp;gt;u,v \in SZK: \exists u \rightsquigarrow v und v \rightsquigarrow u&amp;lt;/math&amp;gt;&lt;br /&gt;
: Kanten &amp;lt;math&amp;gt;\to&amp;lt;/math&amp;gt; Implikationen, Implikationen sind transitiv&lt;br /&gt;
: &amp;lt;math&amp;gt;\Rightarrow u \rightsquigarrow v \mathrel{\hat=} u \to v &amp;lt;/math&amp;gt; &amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp; &amp;lt;math&amp;gt;\to u \leftrightarrow v&amp;lt;/math&amp;gt; &amp;amp;nbsp; bzw. &amp;amp;nbsp; u == v&lt;br /&gt;
:: &amp;lt;math&amp;gt; v \rightsquigarrow u \mathrel{\hat=} v \to u &amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;\Rightarrow&amp;lt;/math&amp;gt; alle Knoten in einer SZK haben den gleichen Wahrheitswert true oder false&lt;br /&gt;
: aber &amp;lt;math&amp;gt;v_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v_{i+N} \mathrel{\hat=} x_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;\lnot x_i&amp;lt;/math&amp;gt; haben immer verschiedene Werte&lt;br /&gt;
: &amp;lt;math&amp;gt;\Rightarrow v_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v_{i+N}&amp;lt;/math&amp;gt; dürfen nicht in selber SZK sein, andernfalls fordert der Graph &amp;lt;math&amp;gt;x_i == \lnot x_i&amp;lt;/math&amp;gt;, was unmöglich ist.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5688</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5688"/>
				<updated>2017-08-03T14:30:33Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Alg. 2 det. Alg. für k=2 mittels SZK in gerichtetem Graphen */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage Y und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei Y impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern: [[Image:Bild 11.jpg]]&lt;br /&gt;
: Jede logische Schaltung kann als SAT-Ausdruck geschrieben werden.&lt;br /&gt;
* Bsp.: Zuordnung von Heim und Auswärtsspielen beim Fußball&lt;br /&gt;
: &amp;lt;math&amp;gt;x_{it} = \begin{cases} &lt;br /&gt;
true,  &amp;amp; \mbox{Mannschaft i hat am Spieltag t Heimspiel} \\&lt;br /&gt;
false, &amp;amp; \mbox{Mannschaft i hat am Spieltag t Auswärtsspiel} &lt;br /&gt;
\end{cases}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
: 1. Nebenbedingung: spielt Mannschaft i am Spieltag t gegen Mannschaft j, muss gelten &amp;lt;math&amp;gt; x_{it} = \lnot x_{jt}&amp;lt;/math&amp;gt; &lt;br /&gt;
: 2. Nebenbedingung: Jede Mannschaft spielt gegen jede&lt;br /&gt;
: 3. Nebenbedingung: Jede Mannschaft spielt abwechselnd Heim und auswärts &amp;lt;math&amp;gt; x_{it} \neq x_{i(t+1)} &amp;lt;/math&amp;gt;&lt;br /&gt;
: Alle Bedingungen sollen gleichzeitig Erfüllt sein: &lt;br /&gt;
:: &amp;lt;math&amp;gt;y = \begin{cases} (x_{11} \neq x_{21}) \and (x_{31} \neq x_{41}) \and ... \\ &lt;br /&gt;
( x_{12} \neq x_{32} ) \and ... \\&lt;br /&gt;
( x_{11} \neq x_{12} ) \and ( x_{12} \neq x_{13} ) \and ...&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&lt;br /&gt;
: Frage: Kann man X so belegen, dass Y wahr ist? &lt;br /&gt;
: &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Nein, nur möglich wenn es nur 2 Mannschaften gibt und diese abwechselnd gegeneinander antreten. &lt;br /&gt;
&lt;br /&gt;
Normalformen für logische Ausdrücke zur Vereinfachung und Systematisierung&lt;br /&gt;
* 3-CNF (Konjunktionen-NF)&lt;br /&gt;
** jede Klausel enthält max 3 Variablen (genau 3 mit dummy Variablen)&lt;br /&gt;
** jede Klausel enthält nur &amp;lt;math&amp;gt; \or &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; /lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
** alle Klauseln sind durch &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft.&lt;br /&gt;
z.B. &amp;lt;math&amp;gt; ( x_1 \or x_2 \or \lnot x_4 ) \and ( \lnot x_2 \or x_3 \or x_4) \and (...) \and &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \Rightarrow &amp;lt;/math&amp;gt; Ausdruck ist wahr, wenn jede Klausel wahr ist. &lt;br /&gt;
: In jeder Klausel hat man 3 Chancen die Klausel wahr zu machen. &lt;br /&gt;
: Aber: Klauseln können sich widersprechen und nicht erfüllbar sein!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz:&amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; Jeder logische Ausdruck effizient (in pol. Zeit) in 3-CNF umwandelbar.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz v. Cook: &amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; 3-SAT (Erfüllbarkeitsproblem für Ausdrücke in 3-CNF) ist NP-vollständig&lt;br /&gt;
&lt;br /&gt;
zur Zeit ist kein effizienterer Algorithmus bekannt, als im schlechtesten Fall alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Belegungen von {&amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt;} auszuprobieren &lt;br /&gt;
&lt;br /&gt;
2-CNF: wie 3-CNF, nur 2 Variablen pro Klausel&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; effiziente Alg existieren, aber nicht jeder logische Ausdruck in 2-CNF transformierbar.&lt;br /&gt;
: z.B. Heim-Auswärtsproblem&lt;br /&gt;
&lt;br /&gt;
INF (Implikationen-NF):&lt;br /&gt;
* 2 Variablen pro Klausel, Operatoren &amp;lt;math&amp;gt; \implies und \lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
* Klauseln mit &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft. &lt;br /&gt;
&lt;br /&gt;
Satz: jede 2-CNF effizient in INF umwandelbat.&lt;br /&gt;
: &amp;lt;math&amp;gt; ( x_i \or x_j ) \rightsquigarrow ( \lnot x_i \implies x_j ) \and ( \lnot x_j \implies x_i )&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;  INF als gerichteter Graph schreibbar und mittels starker Zusammenhangskomponenten lösbar. &lt;br /&gt;
&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;x_1&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \or x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \implies x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_1 \implies x_2 (A)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_2 \implies x_1 (B)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;A \and B&amp;lt;/math&amp;gt;  &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 1 || 0 || 0 || 0 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden.&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== k-SAT, k=2 in pol. Zeit lösbar === &lt;br /&gt;
&lt;br /&gt;
==== Alg. 1 ====&lt;br /&gt;
(f. bei k) (nur für k=2 effizient) '''Randomisiert'''&lt;br /&gt;
* (0) initialisiere &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; beliebig&lt;br /&gt;
* (1) wiederhole &amp;lt;math&amp;gt;T_{max}&amp;lt;/math&amp;gt; - mal&lt;br /&gt;
** (a) wenn das aktuelle x den Ausdruck erfüllt: return x (x=[&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;
** (b) wähle zufällig eine Klausel, die nicht erfüllt ist&lt;br /&gt;
** (c) wähle in dieser Klausel zufällig eine der k Variablen und invertiere sie =&amp;gt; Klausel ist jetzt erfüllt&lt;br /&gt;
::: (andere können jetzt false geworden sein) &lt;br /&gt;
::: (&amp;lt;math&amp;gt;x_1 \or x_2 ) \and ( x_1 \or \lnot x_2 )&amp;lt;/math&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;math&amp;gt; x_1 = 0, x_2 = 0,&amp;lt;/math&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;math&amp;gt; x_2&amp;lt;/math&amp;gt; auf 1 =&amp;gt; 1. Klausel wahr, 2. falsch&lt;br /&gt;
* (2) return &amp;quot;keine Lösung gefunden&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Nach wie vielen Iterationen wird im Mittel eine Lösung gefunden?&lt;br /&gt;
* Ausdruck unerfüllbar =&amp;gt; Endlosschleife, Timeout nach &amp;lt;math&amp;gt;T_{max}&amp;lt;/math&amp;gt; Iterationen &lt;br /&gt;
* Ausrduck erfüllbar:&lt;br /&gt;
** falls k&amp;lt;math&amp;gt;\geq&amp;lt;/math&amp;gt;3: nach &amp;lt;math&amp;gt;O((\frac{2(k-1)}{k})^N)&amp;lt;/math&amp;gt; Iterationen wird Lösung gefunden&lt;br /&gt;
** k=3: &amp;lt;math&amp;gt;O((\frac{4}{3})^N)&amp;lt;/math&amp;gt; exponentielle Zeit, wie zu erwarten für NP-vollständiges Problem&lt;br /&gt;
** k=2: &amp;lt;math&amp;gt;O(N^2)&amp;lt;/math&amp;gt; Iterationen bis Lösung&lt;br /&gt;
&lt;br /&gt;
Beweis: Algorithmus entspricht im Wesentlichen dem '''Random Walk'''&lt;br /&gt;
: Sei &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; die korrekte Lösung und x die aktuelle Belegung&lt;br /&gt;
: RW: Stuhl i &amp;lt;math&amp;gt;\mathrel{\hat=}&amp;lt;/math&amp;gt; i Variablen zwischen &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; und x stimmen überein =&amp;gt; Ziel: erreiche Stuhl N &lt;br /&gt;
* (c):&lt;br /&gt;
** Fall 1: beide Variablen falsch =&amp;gt; egal welche wir invertieren, bewegen wir uns von Stuhl i zu i+1&lt;br /&gt;
** Fall 2: eine Variable ist falsch: &lt;br /&gt;
*** mit Wahrscheinlichkeit 1/2 wählen wir diese und gehen von i nach i+1&lt;br /&gt;
*** mit Wahrscheinlichkeit 1/2 wählen wir die andere und gehen von i nach i-1&lt;br /&gt;
&lt;br /&gt;
schlechtester Fall: Es existiert keine Lösung &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; und wir haben immer Fall 2&lt;br /&gt;
: =&amp;gt;RW braucht &amp;lt;math&amp;gt;O(N^2 - i^2)&amp;lt;/math&amp;gt; Schritte zum Stuhl N &amp;lt;math&amp;gt;\mathrel{\hat=} O(N^2)&amp;lt;/math&amp;gt; falls i anfangs zufällig ist &lt;br /&gt;
&lt;br /&gt;
==== Alg. 2 (det. Alg. für k=2 mittels SZK in gerichtetem Graphen) ====&lt;br /&gt;
: geg.: Ausdruck 2-CNF&lt;br /&gt;
* (1) wandle nach INF: ersetze jede Klausel &amp;lt;math&amp;gt;(x_i \or x_j)&amp;lt;/math&amp;gt; durch &amp;lt;math&amp;gt;(\lnot x_i \Rightarrow x_j) \and (\lnot x_j \Rightarrow x_i)&amp;lt;/math&amp;gt;&lt;br /&gt;
: (entsprechend, wenn in Originalklausel &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt; vorkommen)&lt;br /&gt;
* (2) repräsentiere den Ausdruck als Graph: &lt;br /&gt;
** (a) 2 Knoten pro Var:&amp;lt;math&amp;gt;v_i \mathrel{\hat=} x_i , v_{i+N} \mathrel{\hat=} \lnot x_i &amp;lt;/math&amp;gt;&lt;br /&gt;
** (b) Verbindung für jede Implikation durch korrespondierenden Knoten durch gerichtete Kante&lt;br /&gt;
&lt;br /&gt;
Bsp.:&lt;br /&gt;
&amp;lt;math&amp;gt;C_1 \and C_2 \Leftrightarrow (\lnot x_1 \Rightarrow x_2 ) \and (\lnot x_2 \Rightarrow x_1) \and (x_2 \Rightarrow x_3) \and (\lnot x_3 \Rightarrow \lnot x_2)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* (3) Prüfe ob der Ausdruck erfüllbar ist. Bilde SZK des Graphen&lt;br /&gt;
: '''Satz''': Ausdruck erfüllbar &amp;lt;math&amp;gt;\Leftrightarrow \forall&amp;lt;/math&amp;gt;i: &amp;lt;math&amp;gt; v_i&amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt;v_{i+N}&amp;lt;/math&amp;gt; sind in verschiedenen Komponenten&lt;br /&gt;
&lt;br /&gt;
Beweis: in jeder SZK gilt:&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5687</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5687"/>
				<updated>2017-08-01T14:22:21Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* k-SAT, k=2 in pol. Zeit lösbar */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage Y und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei Y impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern: [[Image:Bild 11.jpg]]&lt;br /&gt;
: Jede logische Schaltung kann als SAT-Ausdruck geschrieben werden.&lt;br /&gt;
* Bsp.: Zuordnung von Heim und Auswärtsspielen beim Fußball&lt;br /&gt;
: &amp;lt;math&amp;gt;x_{it} = \begin{cases} &lt;br /&gt;
true,  &amp;amp; \mbox{Mannschaft i hat am Spieltag t Heimspiel} \\&lt;br /&gt;
false, &amp;amp; \mbox{Mannschaft i hat am Spieltag t Auswärtsspiel} &lt;br /&gt;
\end{cases}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
: 1. Nebenbedingung: spielt Mannschaft i am Spieltag t gegen Mannschaft j, muss gelten &amp;lt;math&amp;gt; x_{it} = \lnot x_{jt}&amp;lt;/math&amp;gt; &lt;br /&gt;
: 2. Nebenbedingung: Jede Mannschaft spielt gegen jede&lt;br /&gt;
: 3. Nebenbedingung: Jede Mannschaft spielt abwechselnd Heim und auswärts &amp;lt;math&amp;gt; x_{it} \neq x_{i(t+1)} &amp;lt;/math&amp;gt;&lt;br /&gt;
: Alle Bedingungen sollen gleichzeitig Erfüllt sein: &lt;br /&gt;
:: &amp;lt;math&amp;gt;y = \begin{cases} (x_{11} \neq x_{21}) \and (x_{31} \neq x_{41}) \and ... \\ &lt;br /&gt;
( x_{12} \neq x_{32} ) \and ... \\&lt;br /&gt;
( x_{11} \neq x_{12} ) \and ( x_{12} \neq x_{13} ) \and ...&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&lt;br /&gt;
: Frage: Kann man X so belegen, dass Y wahr ist? &lt;br /&gt;
: &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Nein, nur möglich wenn es nur 2 Mannschaften gibt und diese abwechselnd gegeneinander antreten. &lt;br /&gt;
&lt;br /&gt;
Normalformen für logische Ausdrücke zur Vereinfachung und Systematisierung&lt;br /&gt;
* 3-CNF (Konjunktionen-NF)&lt;br /&gt;
** jede Klausel enthält max 3 Variablen (genau 3 mit dummy Variablen)&lt;br /&gt;
** jede Klausel enthält nur &amp;lt;math&amp;gt; \or &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; /lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
** alle Klauseln sind durch &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft.&lt;br /&gt;
z.B. &amp;lt;math&amp;gt; ( x_1 \or x_2 \or \lnot x_4 ) \and ( \lnot x_2 \or x_3 \or x_4) \and (...) \and &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \Rightarrow &amp;lt;/math&amp;gt; Ausdruck ist wahr, wenn jede Klausel wahr ist. &lt;br /&gt;
: In jeder Klausel hat man 3 Chancen die Klausel wahr zu machen. &lt;br /&gt;
: Aber: Klauseln können sich widersprechen und nicht erfüllbar sein!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz:&amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; Jeder logische Ausdruck effizient (in pol. Zeit) in 3-CNF umwandelbar.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz v. Cook: &amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; 3-SAT (Erfüllbarkeitsproblem für Ausdrücke in 3-CNF) ist NP-vollständig&lt;br /&gt;
&lt;br /&gt;
zur Zeit ist kein effizienterer Algorithmus bekannt, als im schlechtesten Fall alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Belegungen von {&amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt;} auszuprobieren &lt;br /&gt;
&lt;br /&gt;
2-CNF: wie 3-CNF, nur 2 Variablen pro Klausel&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; effiziente Alg existieren, aber nicht jeder logische Ausdruck in 2-CNF transformierbar.&lt;br /&gt;
: z.B. Heim-Auswärtsproblem&lt;br /&gt;
&lt;br /&gt;
INF (Implikationen-NF):&lt;br /&gt;
* 2 Variablen pro Klausel, Operatoren &amp;lt;math&amp;gt; \implies und \lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
* Klauseln mit &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft. &lt;br /&gt;
&lt;br /&gt;
Satz: jede 2-CNF effizient in INF umwandelbat.&lt;br /&gt;
: &amp;lt;math&amp;gt; ( x_i \or x_j ) \rightsquigarrow ( \lnot x_i \implies x_j ) \and ( \lnot x_j \implies x_i )&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;  INF als gerichteter Graph schreibbar und mittels starker Zusammenhangskomponenten lösbar. &lt;br /&gt;
&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;x_1&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \or x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \implies x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_1 \implies x_2 (A)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_2 \implies x_1 (B)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;A \and B&amp;lt;/math&amp;gt;  &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 1 || 0 || 0 || 0 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden.&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== k-SAT, k=2 in pol. Zeit lösbar === &lt;br /&gt;
&lt;br /&gt;
==== Alg. 1 ====&lt;br /&gt;
(f. bei k) (nur für k=2 effizient) '''Randomisiert'''&lt;br /&gt;
* (0) initialisiere &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; beliebig&lt;br /&gt;
* (1) wiederhole &amp;lt;math&amp;gt;T_{max}&amp;lt;/math&amp;gt; - mal&lt;br /&gt;
** (a) wenn das aktuelle x den Ausdruck erfüllt: return x (x=[&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;
** (b) wähle zufällig eine Klausel, die nicht erfüllt ist&lt;br /&gt;
** (c) wähle in dieser Klausel zufällig eine der k Variablen und invertiere sie =&amp;gt; Klausel ist jetzt erfüllt&lt;br /&gt;
::: (andere können jetzt false geworden sein) &lt;br /&gt;
::: (&amp;lt;math&amp;gt;x_1 \or x_2 ) \and ( x_1 \or \lnot x_2 )&amp;lt;/math&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;math&amp;gt; x_1 = 0, x_2 = 0,&amp;lt;/math&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;math&amp;gt; x_2&amp;lt;/math&amp;gt; auf 1 =&amp;gt; 1. Klausel wahr, 2. falsch&lt;br /&gt;
* (2) return &amp;quot;keine Lösung gefunden&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Nach wie vielen Iterationen wird im Mittel eine Lösung gefunden?&lt;br /&gt;
* Ausdruck unerfüllbar =&amp;gt; Endlosschleife, Timeout nach &amp;lt;math&amp;gt;T_{max}&amp;lt;/math&amp;gt; Iterationen &lt;br /&gt;
* Ausrduck erfüllbar:&lt;br /&gt;
** falls k&amp;lt;math&amp;gt;\geq&amp;lt;/math&amp;gt;3: nach &amp;lt;math&amp;gt;O((\frac{2(k-1)}{k})^N)&amp;lt;/math&amp;gt; Iterationen wird Lösung gefunden&lt;br /&gt;
** k=3: &amp;lt;math&amp;gt;O((\frac{4}{3})^N)&amp;lt;/math&amp;gt; exponentielle Zeit, wie zu erwarten für NP-vollständiges Problem&lt;br /&gt;
** k=2: &amp;lt;math&amp;gt;O(N^2)&amp;lt;/math&amp;gt; Iterationen bis Lösung&lt;br /&gt;
&lt;br /&gt;
Beweis: Algorithmus entspricht im Wesentlichen dem '''Random Walk'''&lt;br /&gt;
: Sei &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; die korrekte Lösung und x die aktuelle Belegung&lt;br /&gt;
: RW: Stuhl i &amp;lt;math&amp;gt;\mathrel{\hat=}&amp;lt;/math&amp;gt; i Variablen zwischen &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; und x stimmen überein =&amp;gt; Ziel: erreiche Stuhl N &lt;br /&gt;
* (c):&lt;br /&gt;
** Fall 1: beide Variablen falsch =&amp;gt; egal welche wir invertieren, bewegen wir uns von Stuhl i zu i+1&lt;br /&gt;
** Fall 2: eine Variable ist falsch: &lt;br /&gt;
*** mit Wahrscheinlichkeit 1/2 wählen wir diese und gehen von i nach i+1&lt;br /&gt;
*** mit Wahrscheinlichkeit 1/2 wählen wir die andere und gehen von i nach i-1&lt;br /&gt;
&lt;br /&gt;
schlechtester Fall: Es existiert keine Lösung &amp;lt;math&amp;gt;x^*&amp;lt;/math&amp;gt; und wir haben immer Fall 2&lt;br /&gt;
: =&amp;gt;RW braucht &amp;lt;math&amp;gt;O(N^2 - i^2)&amp;lt;/math&amp;gt; Schritte zum Stuhl N &amp;lt;math&amp;gt;\mathrel{\hat=} O(N^2)&amp;lt;/math&amp;gt; falls i anfangs zufällig ist &lt;br /&gt;
&lt;br /&gt;
==== Alg. 2 det. Alg. für k=2 mittels SZK in gerichtetem Graphen ====&lt;br /&gt;
: geg.: Ausdruck 2-CNF&lt;br /&gt;
* (1) wandle nach INF: ersetze jede Klausel &amp;lt;math&amp;gt;(x_i \or x_j)&amp;lt;/math&amp;gt; durch &amp;lt;math&amp;gt;(\lnot x_i \Rightarrow x_j) \and (\lnot x_j \Rightarrow x_i)&amp;lt;/math&amp;gt;&lt;br /&gt;
: (entsprechend, wenn in Originalklausel &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt; vorkommen)&lt;br /&gt;
* (2) repräsentiere den Ausdruck als Graph: &lt;br /&gt;
** (a) 2 Knoten pro Var:&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5686</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5686"/>
				<updated>2017-08-01T13:48:19Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Alg. 1 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage Y und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei Y impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern: [[Image:Bild 11.jpg]]&lt;br /&gt;
: Jede logische Schaltung kann als SAT-Ausdruck geschrieben werden.&lt;br /&gt;
* Bsp.: Zuordnung von Heim und Auswärtsspielen beim Fußball&lt;br /&gt;
: &amp;lt;math&amp;gt;x_{it} = \begin{cases} &lt;br /&gt;
true,  &amp;amp; \mbox{Mannschaft i hat am Spieltag t Heimspiel} \\&lt;br /&gt;
false, &amp;amp; \mbox{Mannschaft i hat am Spieltag t Auswärtsspiel} &lt;br /&gt;
\end{cases}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
: 1. Nebenbedingung: spielt Mannschaft i am Spieltag t gegen Mannschaft j, muss gelten &amp;lt;math&amp;gt; x_{it} = \lnot x_{jt}&amp;lt;/math&amp;gt; &lt;br /&gt;
: 2. Nebenbedingung: Jede Mannschaft spielt gegen jede&lt;br /&gt;
: 3. Nebenbedingung: Jede Mannschaft spielt abwechselnd Heim und auswärts &amp;lt;math&amp;gt; x_{it} \neq x_{i(t+1)} &amp;lt;/math&amp;gt;&lt;br /&gt;
: Alle Bedingungen sollen gleichzeitig Erfüllt sein: &lt;br /&gt;
:: &amp;lt;math&amp;gt;y = \begin{cases} (x_{11} \neq x_{21}) \and (x_{31} \neq x_{41}) \and ... \\ &lt;br /&gt;
( x_{12} \neq x_{32} ) \and ... \\&lt;br /&gt;
( x_{11} \neq x_{12} ) \and ( x_{12} \neq x_{13} ) \and ...&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&lt;br /&gt;
: Frage: Kann man X so belegen, dass Y wahr ist? &lt;br /&gt;
: &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Nein, nur möglich wenn es nur 2 Mannschaften gibt und diese abwechselnd gegeneinander antreten. &lt;br /&gt;
&lt;br /&gt;
Normalformen für logische Ausdrücke zur Vereinfachung und Systematisierung&lt;br /&gt;
* 3-CNF (Konjunktionen-NF)&lt;br /&gt;
** jede Klausel enthält max 3 Variablen (genau 3 mit dummy Variablen)&lt;br /&gt;
** jede Klausel enthält nur &amp;lt;math&amp;gt; \or &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; /lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
** alle Klauseln sind durch &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft.&lt;br /&gt;
z.B. &amp;lt;math&amp;gt; ( x_1 \or x_2 \or \lnot x_4 ) \and ( \lnot x_2 \or x_3 \or x_4) \and (...) \and &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \Rightarrow &amp;lt;/math&amp;gt; Ausdruck ist wahr, wenn jede Klausel wahr ist. &lt;br /&gt;
: In jeder Klausel hat man 3 Chancen die Klausel wahr zu machen. &lt;br /&gt;
: Aber: Klauseln können sich widersprechen und nicht erfüllbar sein!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz:&amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; Jeder logische Ausdruck effizient (in pol. Zeit) in 3-CNF umwandelbar.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz v. Cook: &amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; 3-SAT (Erfüllbarkeitsproblem für Ausdrücke in 3-CNF) ist NP-vollständig&lt;br /&gt;
&lt;br /&gt;
zur Zeit ist kein effizienterer Algorithmus bekannt, als im schlechtesten Fall alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Belegungen von {&amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt;} auszuprobieren &lt;br /&gt;
&lt;br /&gt;
2-CNF: wie 3-CNF, nur 2 Variablen pro Klausel&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; effiziente Alg existieren, aber nicht jeder logische Ausdruck in 2-CNF transformierbar.&lt;br /&gt;
: z.B. Heim-Auswärtsproblem&lt;br /&gt;
&lt;br /&gt;
INF (Implikationen-NF):&lt;br /&gt;
* 2 Variablen pro Klausel, Operatoren &amp;lt;math&amp;gt; \implies und \lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
* Klauseln mit &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft. &lt;br /&gt;
&lt;br /&gt;
Satz: jede 2-CNF effizient in INF umwandelbat.&lt;br /&gt;
: &amp;lt;math&amp;gt; ( x_i \or x_j ) \rightsquigarrow ( \lnot x_i \implies x_j ) \and ( \lnot x_j \implies x_i )&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;  INF als gerichteter Graph schreibbar und mittels starker Zusammenhangskomponenten lösbar. &lt;br /&gt;
&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;x_1&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \or x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \implies x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_1 \implies x_2 (A)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_2 \implies x_1 (B)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;A \and B&amp;lt;/math&amp;gt;  &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 1 || 0 || 0 || 0 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden.&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== k-SAT, k=2 in pol. Zeit lösbar === &lt;br /&gt;
&lt;br /&gt;
==== Alg. 1 ====&lt;br /&gt;
(f. bei k) (nur für k=2 effizient) '''Randomisiert'''&lt;br /&gt;
* (0) initialisiere &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; beliebig&lt;br /&gt;
* (1) wiederhole &amp;lt;math&amp;gt;T_{max}&amp;lt;/math&amp;gt; - mal&lt;br /&gt;
** (a) wenn das aktuelle x den Ausdruck erfüllt: return x (x=[&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;
** (b) wähle zufällig eine Klausel, die nicht erfüllt ist&lt;br /&gt;
** (c) wähle in dieser Klausel zufällig eine der k Variablen und invertiere sie =&amp;gt; Klausel ist jetzt erfüllt&lt;br /&gt;
::: (andere können jetzt false geworden sein) &lt;br /&gt;
::: (&amp;lt;math&amp;gt;x_1 \or x_2 ) \and ( x_1 \or \lnot x_2 )&amp;lt;/math&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;math&amp;gt; x_1 = 0, x_2 = 0,&amp;lt;/math&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;math&amp;gt; x_2&amp;lt;/math&amp;gt; auf 1 =&amp;gt; 1. Klausel wahr, 2. falsch&lt;br /&gt;
* (2) return &amp;quot;keine Lösung gefunden&amp;quot;&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5685</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5685"/>
				<updated>2017-08-01T13:47:22Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Die Problemklassen P und NP */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage Y und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei Y impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern: [[Image:Bild 11.jpg]]&lt;br /&gt;
: Jede logische Schaltung kann als SAT-Ausdruck geschrieben werden.&lt;br /&gt;
* Bsp.: Zuordnung von Heim und Auswärtsspielen beim Fußball&lt;br /&gt;
: &amp;lt;math&amp;gt;x_{it} = \begin{cases} &lt;br /&gt;
true,  &amp;amp; \mbox{Mannschaft i hat am Spieltag t Heimspiel} \\&lt;br /&gt;
false, &amp;amp; \mbox{Mannschaft i hat am Spieltag t Auswärtsspiel} &lt;br /&gt;
\end{cases}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
: 1. Nebenbedingung: spielt Mannschaft i am Spieltag t gegen Mannschaft j, muss gelten &amp;lt;math&amp;gt; x_{it} = \lnot x_{jt}&amp;lt;/math&amp;gt; &lt;br /&gt;
: 2. Nebenbedingung: Jede Mannschaft spielt gegen jede&lt;br /&gt;
: 3. Nebenbedingung: Jede Mannschaft spielt abwechselnd Heim und auswärts &amp;lt;math&amp;gt; x_{it} \neq x_{i(t+1)} &amp;lt;/math&amp;gt;&lt;br /&gt;
: Alle Bedingungen sollen gleichzeitig Erfüllt sein: &lt;br /&gt;
:: &amp;lt;math&amp;gt;y = \begin{cases} (x_{11} \neq x_{21}) \and (x_{31} \neq x_{41}) \and ... \\ &lt;br /&gt;
( x_{12} \neq x_{32} ) \and ... \\&lt;br /&gt;
( x_{11} \neq x_{12} ) \and ( x_{12} \neq x_{13} ) \and ...&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&lt;br /&gt;
: Frage: Kann man X so belegen, dass Y wahr ist? &lt;br /&gt;
: &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Nein, nur möglich wenn es nur 2 Mannschaften gibt und diese abwechselnd gegeneinander antreten. &lt;br /&gt;
&lt;br /&gt;
Normalformen für logische Ausdrücke zur Vereinfachung und Systematisierung&lt;br /&gt;
* 3-CNF (Konjunktionen-NF)&lt;br /&gt;
** jede Klausel enthält max 3 Variablen (genau 3 mit dummy Variablen)&lt;br /&gt;
** jede Klausel enthält nur &amp;lt;math&amp;gt; \or &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; /lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
** alle Klauseln sind durch &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft.&lt;br /&gt;
z.B. &amp;lt;math&amp;gt; ( x_1 \or x_2 \or \lnot x_4 ) \and ( \lnot x_2 \or x_3 \or x_4) \and (...) \and &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \Rightarrow &amp;lt;/math&amp;gt; Ausdruck ist wahr, wenn jede Klausel wahr ist. &lt;br /&gt;
: In jeder Klausel hat man 3 Chancen die Klausel wahr zu machen. &lt;br /&gt;
: Aber: Klauseln können sich widersprechen und nicht erfüllbar sein!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz:&amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; Jeder logische Ausdruck effizient (in pol. Zeit) in 3-CNF umwandelbar.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz v. Cook: &amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; 3-SAT (Erfüllbarkeitsproblem für Ausdrücke in 3-CNF) ist NP-vollständig&lt;br /&gt;
&lt;br /&gt;
zur Zeit ist kein effizienterer Algorithmus bekannt, als im schlechtesten Fall alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Belegungen von {&amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt;} auszuprobieren &lt;br /&gt;
&lt;br /&gt;
2-CNF: wie 3-CNF, nur 2 Variablen pro Klausel&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; effiziente Alg existieren, aber nicht jeder logische Ausdruck in 2-CNF transformierbar.&lt;br /&gt;
: z.B. Heim-Auswärtsproblem&lt;br /&gt;
&lt;br /&gt;
INF (Implikationen-NF):&lt;br /&gt;
* 2 Variablen pro Klausel, Operatoren &amp;lt;math&amp;gt; \implies und \lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
* Klauseln mit &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft. &lt;br /&gt;
&lt;br /&gt;
Satz: jede 2-CNF effizient in INF umwandelbat.&lt;br /&gt;
: &amp;lt;math&amp;gt; ( x_i \or x_j ) \rightsquigarrow ( \lnot x_i \implies x_j ) \and ( \lnot x_j \implies x_i )&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;  INF als gerichteter Graph schreibbar und mittels starker Zusammenhangskomponenten lösbar. &lt;br /&gt;
&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;x_1&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \or x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \implies x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_1 \implies x_2 (A)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_2 \implies x_1 (B)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;A \and B&amp;lt;/math&amp;gt;  &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 1 || 0 || 0 || 0 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden.&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== k-SAT, k=2 in pol. Zeit lösbar === &lt;br /&gt;
&lt;br /&gt;
== Alg. 1 ==&lt;br /&gt;
(f. bei k) (nur für k=2 effizient) '''Randomisiert'''&lt;br /&gt;
* (0) initialisiere &amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt; beliebig&lt;br /&gt;
* (1) wiederhole &amp;lt;math&amp;gt;T_{max}&amp;lt;/math&amp;gt; - mal&lt;br /&gt;
** (a) wenn das aktuelle x den Ausdruck erfüllt: return x (x=[&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;
** (b) wähle zufällig eine Klausel, die nicht erfüllt ist&lt;br /&gt;
** (c) wähle in dieser Klausel zufällig eine der k Variablen und invertiere sie =&amp;gt; Klausel ist jetzt erfüllt&lt;br /&gt;
::: (andere können jetzt false geworden sein) &lt;br /&gt;
::: (&amp;lt;math&amp;gt;x_1 \or x_2 ) \and ( x_1 \or \lnot x_2 )&amp;lt;/math&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;math&amp;gt; x_1 = 0, x_2 = 0,&amp;lt;/math&amp;gt;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;lt;math&amp;gt; x_2&amp;lt;/math&amp;gt; auf 1 =&amp;gt; 1. Klausel wahr, 2. falsch&lt;br /&gt;
* (2) return &amp;quot;keine Lösung gefunden&amp;quot;&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5684</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5684"/>
				<updated>2017-07-25T19:07:21Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* 3-SAT ist NP vollständig */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage Y und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei Y impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern: [[Image:Bild 11.jpg]]&lt;br /&gt;
: Jede logische Schaltung kann als SAT-Ausdruck geschrieben werden.&lt;br /&gt;
* Bsp.: Zuordnung von Heim und Auswärtsspielen beim Fußball&lt;br /&gt;
: &amp;lt;math&amp;gt;x_{it} = \begin{cases} &lt;br /&gt;
true,  &amp;amp; \mbox{Mannschaft i hat am Spieltag t Heimspiel} \\&lt;br /&gt;
false, &amp;amp; \mbox{Mannschaft i hat am Spieltag t Auswärtsspiel} &lt;br /&gt;
\end{cases}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
: 1. Nebenbedingung: spielt Mannschaft i am Spieltag t gegen Mannschaft j, muss gelten &amp;lt;math&amp;gt; x_{it} = \lnot x_{jt}&amp;lt;/math&amp;gt; &lt;br /&gt;
: 2. Nebenbedingung: Jede Mannschaft spielt gegen jede&lt;br /&gt;
: 3. Nebenbedingung: Jede Mannschaft spielt abwechselnd Heim und auswärts &amp;lt;math&amp;gt; x_{it} \neq x_{i(t+1)} &amp;lt;/math&amp;gt;&lt;br /&gt;
: Alle Bedingungen sollen gleichzeitig Erfüllt sein: &lt;br /&gt;
:: &amp;lt;math&amp;gt;y = \begin{cases} (x_{11} \neq x_{21}) \and (x_{31} \neq x_{41}) \and ... \\ &lt;br /&gt;
( x_{12} \neq x_{32} ) \and ... \\&lt;br /&gt;
( x_{11} \neq x_{12} ) \and ( x_{12} \neq x_{13} ) \and ...&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&lt;br /&gt;
: Frage: Kann man X so belegen, dass Y wahr ist? &lt;br /&gt;
: &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Nein, nur möglich wenn es nur 2 Mannschaften gibt und diese abwechselnd gegeneinander antreten. &lt;br /&gt;
&lt;br /&gt;
Normalformen für logische Ausdrücke zur Vereinfachung und Systematisierung&lt;br /&gt;
* 3-CNF (Konjunktionen-NF)&lt;br /&gt;
** jede Klausel enthält max 3 Variablen (genau 3 mit dummy Variablen)&lt;br /&gt;
** jede Klausel enthält nur &amp;lt;math&amp;gt; \or &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; /lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
** alle Klauseln sind durch &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft.&lt;br /&gt;
z.B. &amp;lt;math&amp;gt; ( x_1 \or x_2 \or \lnot x_4 ) \and ( \lnot x_2 \or x_3 \or x_4) \and (...) \and &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \Rightarrow &amp;lt;/math&amp;gt; Ausdruck ist wahr, wenn jede Klausel wahr ist. &lt;br /&gt;
: In jeder Klausel hat man 3 Chancen die Klausel wahr zu machen. &lt;br /&gt;
: Aber: Klauseln können sich widersprechen und nicht erfüllbar sein!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz:&amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; Jeder logische Ausdruck effizient (in pol. Zeit) in 3-CNF umwandelbar.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz v. Cook: &amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; 3-SAT (Erfüllbarkeitsproblem für Ausdrücke in 3-CNF) ist NP-vollständig&lt;br /&gt;
&lt;br /&gt;
zur Zeit ist kein effizienterer Algorithmus bekannt, als im schlechtesten Fall alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Belegungen von {&amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt;} auszuprobieren &lt;br /&gt;
&lt;br /&gt;
2-CNF: wie 3-CNF, nur 2 Variablen pro Klausel&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; effiziente Alg existieren, aber nicht jeder logische Ausdruck in 2-CNF transformierbar.&lt;br /&gt;
: z.B. Heim-Auswärtsproblem&lt;br /&gt;
&lt;br /&gt;
INF (Implikationen-NF):&lt;br /&gt;
* 2 Variablen pro Klausel, Operatoren &amp;lt;math&amp;gt; \implies und \lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
* Klauseln mit &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft. &lt;br /&gt;
&lt;br /&gt;
Satz: jede 2-CNF effizient in INF umwandelbat.&lt;br /&gt;
: &amp;lt;math&amp;gt; ( x_i \or x_j ) \rightsquigarrow ( \lnot x_i \implies x_j ) \and ( \lnot x_j \implies x_i )&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;  INF als gerichteter Graph schreibbar und mittels starker Zusammenhangskomponenten lösbar. &lt;br /&gt;
&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;x_1&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \or x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \implies x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_1 \implies x_2 (A)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_2 \implies x_1 (B)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;A \and B&amp;lt;/math&amp;gt;  &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 1 || 0 || 0 || 0 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden.&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5683</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5683"/>
				<updated>2017-07-25T19:07:02Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Die Problemklassen P und NP */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage Y und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei Y impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern: [[Image:Bild 11.jpg]]&lt;br /&gt;
: Jede logische Schaltung kann als SAT-Ausdruck geschrieben werden.&lt;br /&gt;
* Bsp.: Zuordnung von Heim und Auswärtsspielen beim Fußball&lt;br /&gt;
: &amp;lt;math&amp;gt;x_{it} = \begin{cases} &lt;br /&gt;
true,  &amp;amp; \mbox{Mannschaft i hat am Spieltag t Heimspiel} \\&lt;br /&gt;
false, &amp;amp; \mbox{Mannschaft i hat am Spieltag t Auswärtsspiel} &lt;br /&gt;
\end{cases}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
: 1. Nebenbedingung: spielt Mannschaft i am Spieltag t gegen Mannschaft j, muss gelten &amp;lt;math&amp;gt; x_{it} = \lnot x_{jt}&amp;lt;/math&amp;gt; &lt;br /&gt;
: 2. Nebenbedingung: Jede Mannschaft spielt gegen jede&lt;br /&gt;
: 3. Nebenbedingung: Jede Mannschaft spielt abwechselnd Heim und auswärts &amp;lt;math&amp;gt; x_{it} \neq x_{i(t+1)} &amp;lt;/math&amp;gt;&lt;br /&gt;
: Alle Bedingungen sollen gleichzeitig Erfüllt sein: &lt;br /&gt;
:: &amp;lt;math&amp;gt;y = \begin{cases} (x_{11} \neq x_{21}) \and (x_{31} \neq x_{41}) \and ... \\ &lt;br /&gt;
( x_{12} \neq x_{32} ) \and ... \\&lt;br /&gt;
( x_{11} \neq x_{12} ) \and ( x_{12} \neq x_{13} ) \and ...&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&lt;br /&gt;
: Frage: Kann man X so belegen, dass Y wahr ist? &lt;br /&gt;
: &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Nein, nur möglich wenn es nur 2 Mannschaften gibt und diese abwechselnd gegeneinander antreten. &lt;br /&gt;
&lt;br /&gt;
Normalformen für logische Ausdrücke zur Vereinfachung und Systematisierung&lt;br /&gt;
* 3-CNF (Konjunktionen-NF)&lt;br /&gt;
** jede Klausel enthält max 3 Variablen (genau 3 mit dummy Variablen)&lt;br /&gt;
** jede Klausel enthält nur &amp;lt;math&amp;gt; \or &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; /lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
** alle Klauseln sind durch &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft.&lt;br /&gt;
z.B. &amp;lt;math&amp;gt; ( x_1 \or x_2 \or \lnot x_4 ) \and ( \lnot x_2 \or x_3 \or x_4) \and (...) \and &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \Rightarrow &amp;lt;/math&amp;gt; Ausdruck ist wahr, wenn jede Klausel wahr ist. &lt;br /&gt;
: In jeder Klausel hat man 3 Chancen die Klausel wahr zu machen. &lt;br /&gt;
: Aber: Klauseln können sich widersprechen und nicht erfüllbar sein!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz:&amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; Jeder logische Ausdruck effizient (in pol. Zeit) in 3-CNF umwandelbar.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz v. Cook: &amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; 3-SAT (Erfüllbarkeitsproblem für Ausdrücke in 3-CNF) ist NP-vollständig&lt;br /&gt;
&lt;br /&gt;
zur Zeit ist kein effizienterer Algorithmus bekannt, als im schlechtesten Fall alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Belegungen von {&amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt;} auszuprobieren &lt;br /&gt;
&lt;br /&gt;
2-CNF: wie 3-CNF, nur 2 Variablen pro Klausel&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; effiziente Alg existieren, aber nicht jeder logische Ausdruck in 2-CNF transformierbar.&lt;br /&gt;
: z.B. Heim-Auswärtsproblem&lt;br /&gt;
&lt;br /&gt;
INF (Implikationen-NF):&lt;br /&gt;
* 2 Variablen pro Klausel, Operatoren &amp;lt;math&amp;gt; \implies und \lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
* Klauseln mit &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft. &lt;br /&gt;
&lt;br /&gt;
Satz: jede 2-CNF effizient in INF umwandelbat.&lt;br /&gt;
: &amp;lt;math&amp;gt; ( x_i \or x_j ) \rightsquigarrow ( \lnot x_i \implies x_j ) \and ( \lnot x_j \implies x_i )&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;  INF als gerichteter Graph schreibbar und mittels starker Zusammenhangskomponenten lösbar. &lt;br /&gt;
&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;x_1&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \or x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \implies x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_1 \implies x_2 (A)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_2 \implies x_1 (B)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;A \and B&amp;lt;/math&amp;gt;  &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 1 || 0 || 0 || 0 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden, z.B.:&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5682</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5682"/>
				<updated>2017-07-25T11:21:04Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Erfüllbarkeitsproblem */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage Y und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei Y impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern: [[Image:Bild 11.jpg]]&lt;br /&gt;
: Jede logische Schaltung kann als SAT-Ausdruck geschrieben werden.&lt;br /&gt;
* Bsp.: Zuordnung von Heim und Auswärtsspielen beim Fußball&lt;br /&gt;
: &amp;lt;math&amp;gt;x_{it} = \begin{cases} &lt;br /&gt;
true,  &amp;amp; \mbox{Mannschaft i hat am Spieltag t Heimspiel} \\&lt;br /&gt;
false, &amp;amp; \mbox{Mannschaft i hat am Spieltag t Auswärtsspiel} &lt;br /&gt;
\end{cases}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
: 1. Nebenbedingung: spielt Mannschaft i am Spieltag t gegen Mannschaft j, muss gelten &amp;lt;math&amp;gt; x_{it} = \lnot x_{jt}&amp;lt;/math&amp;gt; &lt;br /&gt;
: 2. Nebenbedingung: Jede Mannschaft spielt gegen jede&lt;br /&gt;
: 3. Nebenbedingung: Jede Mannschaft spielt abwechselnd Heim und auswärts &amp;lt;math&amp;gt; x_{it} \neq x_{i(t+1)} &amp;lt;/math&amp;gt;&lt;br /&gt;
: Alle Bedingungen sollen gleichzeitig Erfüllt sein: &lt;br /&gt;
:: &amp;lt;math&amp;gt;y = \begin{cases} (x_{11} \neq x_{21}) \and (x_{31} \neq x_{41}) \and ... \\ &lt;br /&gt;
( x_{12} \neq x_{32} ) \and ... \\&lt;br /&gt;
( x_{11} \neq x_{12} ) \and ( x_{12} \neq x_{13} ) \and ...&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&lt;br /&gt;
: Frage: Kann man X so belegen, dass Y wahr ist? &lt;br /&gt;
: &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Nein, nur möglich wenn es nur 2 Mannschaften gibt und diese abwechselnd gegeneinander antreten. &lt;br /&gt;
&lt;br /&gt;
Normalformen für logische Ausdrücke zur Vereinfachung und Systematisierung&lt;br /&gt;
* 3-CNF (Konjunktionen-NF)&lt;br /&gt;
** jede Klausel enthält max 3 Variablen (genau 3 mit dummy Variablen)&lt;br /&gt;
** jede Klausel enthält nur &amp;lt;math&amp;gt; \or &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; /lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
** alle Klauseln sind durch &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft.&lt;br /&gt;
z.B. &amp;lt;math&amp;gt; ( x_1 \or x_2 \or \lnot x_4 ) \and ( \lnot x_2 \or x_3 \or x_4) \and (...) \and &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \Rightarrow &amp;lt;/math&amp;gt; Ausdruck ist wahr, wenn jede Klausel wahr ist. &lt;br /&gt;
: In jeder Klausel hat man 3 Chancen die Klausel wahr zu machen. &lt;br /&gt;
: Aber: Klauseln können sich widersprechen und nicht erfüllbar sein!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz:&amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; Jeder logische Ausdruck effizient (in pol. Zeit) in 3-CNF umwandelbar.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz v. Cook: &amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; 3-SAT (Erfüllbarkeitsproblem für Ausdrücke in 3-CNF) ist NP-vollständig&lt;br /&gt;
&lt;br /&gt;
zur Zeit ist kein effizienterer Algorithmus bekannt, als im schlechtesten Fall alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Belegungen von {&amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt;} auszuprobieren &lt;br /&gt;
&lt;br /&gt;
2-CNF: wie 3-CNF, nur 2 Variablen pro Klausel&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; effiziente Alg existieren, aber nicht jeder logische Ausdruck in 2-CNF transformierbar.&lt;br /&gt;
: z.B. Heim-Auswärtsproblem&lt;br /&gt;
&lt;br /&gt;
INF (Implikationen-NF):&lt;br /&gt;
* 2 Variablen pro Klausel, Operatoren &amp;lt;math&amp;gt; \implies und \lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
* Klauseln mit &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft. &lt;br /&gt;
&lt;br /&gt;
Satz: jede 2-CNF effizient in INF umwandelbat.&lt;br /&gt;
: &amp;lt;math&amp;gt; ( x_i \or x_j ) \rightsquigarrow ( \lnot x_i \implies x_j ) \and ( \lnot x_j \implies x_i )&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;  INF als gerichteter Graph schreibbar und mittels starker Zusammenhangskomponenten lösbar. &lt;br /&gt;
&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;x_1&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \or x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;x_1 \implies x_2&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_1 \implies x_2 (A)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;\lnot x_2 \implies x_1 (B)&amp;lt;/math&amp;gt; &lt;br /&gt;
|width=&amp;quot;70px&amp;quot;| &amp;lt;math&amp;gt;A \and B&amp;lt;/math&amp;gt;  &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 0 || 0 || 1 || 0 || 0 || 0 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 0 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 0 || 1 || 0 || 1 || 1 || 1 &lt;br /&gt;
|- align=&amp;quot;center&amp;quot;&lt;br /&gt;
| 1 || 1 || 1 || 1 || 1 || 1 || 1 &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden, z.B.:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5681</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5681"/>
				<updated>2017-07-25T11:13:45Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Erfüllbarkeitsproblem */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage Y und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei Y impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern: [[Image:Bild 11.jpg]]&lt;br /&gt;
: Jede logische Schaltung kann als SAT-Ausdruck geschrieben werden.&lt;br /&gt;
* Bsp.: Zuordnung von Heim und Auswärtsspielen beim Fußball&lt;br /&gt;
: &amp;lt;math&amp;gt;x_{it} = \begin{cases} &lt;br /&gt;
true,  &amp;amp; \mbox{Mannschaft i hat am Spieltag t Heimspiel} \\&lt;br /&gt;
false, &amp;amp; \mbox{Mannschaft i hat am Spieltag t Auswärtsspiel} &lt;br /&gt;
\end{cases}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
: 1. Nebenbedingung: spielt Mannschaft i am Spieltag t gegen Mannschaft j, muss gelten &amp;lt;math&amp;gt; x_{it} = \lnot x_{jt}&amp;lt;/math&amp;gt; &lt;br /&gt;
: 2. Nebenbedingung: Jede Mannschaft spielt gegen jede&lt;br /&gt;
: 3. Nebenbedingung: Jede Mannschaft spielt abwechselnd Heim und auswärts &amp;lt;math&amp;gt; x_{it} \neq x_{i(t+1)} &amp;lt;/math&amp;gt;&lt;br /&gt;
: Alle Bedingungen sollen gleichzeitig Erfüllt sein: &lt;br /&gt;
:: &amp;lt;math&amp;gt;y = \begin{cases} (x_{11} \neq x_{21}) \and (x_{31} \neq x_{41}) \and ... \\ &lt;br /&gt;
( x_{12} \neq x_{32} ) \and ... \\&lt;br /&gt;
( x_{11} \neq x_{12} ) \and ( x_{12} \neq x_{13} ) \and ...&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&lt;br /&gt;
: Frage: Kann man X so belegen, dass Y wahr ist? &lt;br /&gt;
: &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Nein, nur möglich wenn es nur 2 Mannschaften gibt und diese abwechselnd gegeneinander antreten. &lt;br /&gt;
&lt;br /&gt;
Normalformen für logische Ausdrücke zur Vereinfachung und Systematisierung&lt;br /&gt;
* 3-CNF (Konjunktionen-NF)&lt;br /&gt;
** jede Klausel enthält max 3 Variablen (genau 3 mit dummy Variablen)&lt;br /&gt;
** jede Klausel enthält nur &amp;lt;math&amp;gt; \or &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; /lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
** alle Klauseln sind durch &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft.&lt;br /&gt;
z.B. &amp;lt;math&amp;gt; ( x_1 \or x_2 \or \lnot x_4 ) \and ( \lnot x_2 \or x_3 \or x_4) \and (...) \and &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \Rightarrow &amp;lt;/math&amp;gt; Ausdruck ist wahr, wenn jede Klausel wahr ist. &lt;br /&gt;
: In jeder Klausel hat man 3 Chancen die Klausel wahr zu machen. &lt;br /&gt;
: Aber: Klauseln können sich widersprechen und nicht erfüllbar sein!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz:&amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; Jeder logische Ausdruck effizient (in pol. Zeit) in 3-CNF umwandelbar.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz v. Cook: &amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; 3-SAT (Erfüllbarkeitsproblem für Ausdrücke in 3-CNF) ist NP-vollständig&lt;br /&gt;
&lt;br /&gt;
zur Zeit ist kein effizienterer Algorithmus bekannt, als im schlechtesten Fall alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Belegungen von {&amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt;} auszuprobieren &lt;br /&gt;
&lt;br /&gt;
2-CNF: wie 3-CNF, nur 2 Variablen pro Klausel&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; effiziente Alg existieren, aber nicht jeder logische Ausdruck in 2-CNF transformierbar.&lt;br /&gt;
: z.B. Heim-Auswärtsproblem&lt;br /&gt;
&lt;br /&gt;
INF (Implikationen-NF):&lt;br /&gt;
* 2 Variablen pro Klausel, Operatoren &amp;lt;math&amp;gt; \implies und \lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
* Klauseln mit &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft. &lt;br /&gt;
&lt;br /&gt;
Satz: jede 2-CNF effizient in INF umwandelbat.&lt;br /&gt;
: &amp;lt;math&amp;gt; ( x_i \or x_j ) \rightsquigarrow ( \lnot x_i \implies x_j ) \and ( \lnot x_j \implies x_i )&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
&amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;  INF als gerichteter Graph schreibbar und mittels starker Zusammenhangskomponenten lösbar. &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden, z.B.:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5680</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5680"/>
				<updated>2017-07-25T10:40:21Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Einleitung */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage Y und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei Y impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern: [[Image:Bild 11.jpg]]&lt;br /&gt;
: Jede logische Schaltung kann als SAT-Ausdruck geschrieben werden.&lt;br /&gt;
* Bsp.: Zuordnung von Heim und Auswärtsspielen beim Fußball&lt;br /&gt;
: &amp;lt;math&amp;gt;x_{it} = \begin{cases} &lt;br /&gt;
true,  &amp;amp; \mbox{Mannschaft i hat am Spieltag t Heimspiel} \\&lt;br /&gt;
false, &amp;amp; \mbox{Mannschaft i hat am Spieltag t Auswärtsspiel} &lt;br /&gt;
\end{cases}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
: 1. Nebenbedingung: spielt Mannschaft i am Spieltag t gegen Mannschaft j, muss gelten &amp;lt;math&amp;gt; x_{it} = \lnot x_{jt}&amp;lt;/math&amp;gt; &lt;br /&gt;
: 2. Nebenbedingung: Jede Mannschaft spielt gegen jede&lt;br /&gt;
: 3. Nebenbedingung: Jede Mannschaft spielt abwechselnd Heim und auswärts &amp;lt;math&amp;gt; x_{it} \neq x_{i(t+1)} &amp;lt;/math&amp;gt;&lt;br /&gt;
: Alle Bedingungen sollen gleichzeitig Erfüllt sein: &lt;br /&gt;
:: &amp;lt;math&amp;gt;y = \begin{cases} (x_{11} \neq x_{21}) \and (x_{31} \neq x_{41}) \and ... \\ &lt;br /&gt;
( x_{12} \neq x_{32} ) \and ... \\&lt;br /&gt;
( x_{11} \neq x_{12} ) \and ( x_{12} \neq x_{13} ) \and ...&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&lt;br /&gt;
: Frage: Kann man X so belegen, dass Y wahr ist? &lt;br /&gt;
: &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Nein, nur möglich wenn es nur 2 Mannschaften gibt und diese abwechselnd gegeneinander antreten. &lt;br /&gt;
&lt;br /&gt;
Normalformen für logische Ausdrücke zur Vereinfachung und Systematisierung&lt;br /&gt;
* 3-CNF (Konjunktionen-NF)&lt;br /&gt;
** jede Klausel enthält max 3 Variablen (genau 3 mit dummy Variablen)&lt;br /&gt;
** jede Klausel enthält nur &amp;lt;math&amp;gt; \or &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; /lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
** alle Klauseln sind durch &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft.&lt;br /&gt;
z.B. &amp;lt;math&amp;gt; ( x_1 \or x_2 \or \lnot x_4 ) \and ( \lnot x_2 \or x_3 \or x_4) \and (...) \and &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \Rightarrow &amp;lt;/math&amp;gt; Ausdruck ist wahr, wenn jede Klausel wahr ist. &lt;br /&gt;
: In jeder Klausel hat man 3 Chancen die Klausel wahr zu machen. &lt;br /&gt;
: Aber: Klauseln können sich widersprechen und nicht erfüllbar sein!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz:&amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; Jeder logische Ausdruck effizient (in pol. Zeit) in 3-CNF umwandelbar.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz v. Cook: &amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; 3-SAT (Erfüllbarkeitsproblem für Ausdrücke in 3-CNF) ist NP-vollständig&lt;br /&gt;
&lt;br /&gt;
zur Zeit ist kein effizienterer Algorithmus bekannt, als im schlechtesten Fall alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Belegungen von {&amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt;} auszuprobieren &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden, z.B.:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5679</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5679"/>
				<updated>2017-07-25T10:39:00Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Erfüllbarkeitsproblem */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage &amp;gt; und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei &amp;gt; impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern: [[Image:Bild 11.jpg]]&lt;br /&gt;
: Jede logische Schaltung kann als SAT-Ausdruck geschrieben werden.&lt;br /&gt;
* Bsp.: Zuordnung von Heim und Auswärtsspielen beim Fußball&lt;br /&gt;
: &amp;lt;math&amp;gt;x_{it} = \begin{cases} &lt;br /&gt;
true,  &amp;amp; \mbox{Mannschaft i hat am Spieltag t Heimspiel} \\&lt;br /&gt;
false, &amp;amp; \mbox{Mannschaft i hat am Spieltag t Auswärtsspiel} &lt;br /&gt;
\end{cases}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
: 1. Nebenbedingung: spielt Mannschaft i am Spieltag t gegen Mannschaft j, muss gelten &amp;lt;math&amp;gt; x_{it} = \lnot x_{jt}&amp;lt;/math&amp;gt; &lt;br /&gt;
: 2. Nebenbedingung: Jede Mannschaft spielt gegen jede&lt;br /&gt;
: 3. Nebenbedingung: Jede Mannschaft spielt abwechselnd Heim und auswärts &amp;lt;math&amp;gt; x_{it} \neq x_{i(t+1)} &amp;lt;/math&amp;gt;&lt;br /&gt;
: Alle Bedingungen sollen gleichzeitig Erfüllt sein: &lt;br /&gt;
:: &amp;lt;math&amp;gt;y = \begin{cases} (x_{11} \neq x_{21}) \and (x_{31} \neq x_{41}) \and ... \\ &lt;br /&gt;
( x_{12} \neq x_{32} ) \and ... \\&lt;br /&gt;
( x_{11} \neq x_{12} ) \and ( x_{12} \neq x_{13} ) \and ...&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&lt;br /&gt;
: Frage: Kann man X so belegen, dass Y wahr ist? &lt;br /&gt;
: &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Nein, nur möglich wenn es nur 2 Mannschaften gibt und diese abwechselnd gegeneinander antreten. &lt;br /&gt;
&lt;br /&gt;
Normalformen für logische Ausdrücke zur Vereinfachung und Systematisierung&lt;br /&gt;
* 3-CNF (Konjunktionen-NF)&lt;br /&gt;
** jede Klausel enthält max 3 Variablen (genau 3 mit dummy Variablen)&lt;br /&gt;
** jede Klausel enthält nur &amp;lt;math&amp;gt; \or &amp;lt;/math&amp;gt; und &amp;lt;math&amp;gt; /lnot &amp;lt;/math&amp;gt;&lt;br /&gt;
** alle Klauseln sind durch &amp;lt;math&amp;gt; \and &amp;lt;/math&amp;gt; verknüpft.&lt;br /&gt;
z.B. &amp;lt;math&amp;gt; ( x_1 \or x_2 \or \lnot x_4 ) \and ( \lnot x_2 \or x_3 \or x_4) \and (...) \and &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;math&amp;gt; \Rightarrow &amp;lt;/math&amp;gt; Ausdruck ist wahr, wenn jede Klausel wahr ist. &lt;br /&gt;
: In jeder Klausel hat man 3 Chancen die Klausel wahr zu machen. &lt;br /&gt;
: Aber: Klauseln können sich widersprechen und nicht erfüllbar sein!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz:&amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; Jeder logische Ausdruck effizient (in pol. Zeit) in 3-CNF umwandelbar.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;&amp;lt;u&amp;gt;Satz v. Cook: &amp;lt;/u&amp;gt;&amp;lt;/b&amp;gt; 3-SAT (Erfüllbarkeitsproblem für Ausdrücke in 3-CNF) ist NP-vollständig&lt;br /&gt;
&lt;br /&gt;
zur Zeit ist kein effizienterer Algorithmus bekannt, als im schlechtesten Fall alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Belegungen von {&amp;lt;math&amp;gt;x_i&amp;lt;/math&amp;gt;} auszuprobieren &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden, z.B.:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5678</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5678"/>
				<updated>2017-07-25T09:44:53Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Erfüllbarkeitsproblem */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage &amp;gt; und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei &amp;gt; impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern: [[Image:Bild 11.jpg]]&lt;br /&gt;
: Jede logische Schaltung kann als SAT-Ausdruck geschrieben werden.&lt;br /&gt;
* Bsp.: Zuordnung von Heim und Auswärtsspielen beim Fußball&lt;br /&gt;
: &amp;lt;math&amp;gt;x_{it} = \begin{cases} &lt;br /&gt;
true,  &amp;amp; \mbox{Mannschaft i hat am Spieltag t Heimspiel} \\&lt;br /&gt;
false, &amp;amp; \mbox{Mannschaft i hat am Spieltag t Auswärtsspiel} &lt;br /&gt;
\end{cases}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
: 1. Nebenbedingung: spielt Mannschaft i am Spieltag t gegen Mannschaft j, muss gelten &amp;lt;math&amp;gt; x_{it} = \lnot x_{jt}&amp;lt;/math&amp;gt; &lt;br /&gt;
: 2. Nebenbedingung: Jede Mannschaft spielt gegen jede&lt;br /&gt;
: 3. Nebenbedingung: Jede Mannschaft spielt abwechselnd Heim und auswärts &amp;lt;math&amp;gt; x_{it} \neq x_{i(t+1)} &amp;lt;/math&amp;gt;&lt;br /&gt;
: Alle Bedingungen sollen gleichzeitig Erfüllt sein: &lt;br /&gt;
:: &amp;lt;math&amp;gt;y = \begin{cases} (x_{11} \neq x_{21}) \and (x_{31} \neq x_{41}) \and ... \\ &lt;br /&gt;
( x_{12} \neq x_{32} ) \and ... \\&lt;br /&gt;
( x_{11} \neq x_{12} ) \and ( x_{12} \neq x_{13} ) \and ...&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden, z.B.:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5677</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5677"/>
				<updated>2017-07-25T09:26:14Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Erfüllbarkeitsproblem */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage &amp;gt; und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei &amp;gt; impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\lnot&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern: [[Image:Bild 11.jpg]]&lt;br /&gt;
: Jede logische Schaltung kann als SAT-Ausdruck geschrieben werden.&lt;br /&gt;
* Bsp.: Zuordnung von Heim und Auswärtsspielen beim Fußball&lt;br /&gt;
: &amp;lt;math&amp;gt;x_{it} = \begin{cases} &lt;br /&gt;
true,  &amp;amp; \mbox{Mannschaft i hat am Spieltag t Heimspiel} \\&lt;br /&gt;
false, &amp;amp; \mbox{Mannschaft i hat am Spieltag t Auswärtsspiel} &lt;br /&gt;
\end{cases}&lt;br /&gt;
&amp;lt;/math&amp;gt;&lt;br /&gt;
: 1. Nebenbedingung: spielt Mannschaft i am Spieltag t gegen Mannschaft j, muss gelten &amp;lt;math&amp;gt; x_{it} = \lnot x_{jt}&amp;lt;/math&amp;gt; &lt;br /&gt;
: 2. Nebenbedingung: Jede Mannschaft spielt gegen jede&lt;br /&gt;
: 3. Nebenbedingung: Jede Mannschaft spielt abwechselnd Heim und auswärts &amp;lt;math&amp;gt; x_{it} \neq x_{i(t+1)} &amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden, z.B.:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5676</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5676"/>
				<updated>2017-07-25T08:46:35Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Die Problemklassen P und NP */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage &amp;gt; und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei &amp;gt; impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\not&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\not&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern&lt;br /&gt;
[[Image:Bild 11.jpg]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden, z.B.:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5675</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5675"/>
				<updated>2017-07-25T08:44:35Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Einleitung */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage &amp;gt; und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei &amp;gt; impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Erfüllbarkeitsproblem === &lt;br /&gt;
(SAT-satisfyability) ist das kanonische NP-Vollständige Problem (Satz von Cook 1971)&lt;br /&gt;
*boolsche Variable x1 &amp;lt;math&amp;gt;\element&amp;lt;/math&amp;gt;{true, false}, i=1,...,N (Problemgröße N-Bits)&lt;br /&gt;
*logische Ausdrücke Y über X mit Operatoren &amp;lt;math&amp;gt;\not&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\leftrightarrow&amp;lt;/math&amp;gt;, &amp;lt;math&amp;gt;\neq&amp;lt;/math&amp;gt;, ()&lt;br /&gt;
: z.B. N= 3, Y=(x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2)&amp;lt;math&amp;gt;\and&amp;lt;/math&amp;gt;(&amp;lt;math&amp;gt;\not&amp;lt;/math&amp;gt;x1&amp;lt;math&amp;gt;\or&amp;lt;/math&amp;gt;x2) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; Z=(true[x1], true[x2], true[x3])&lt;br /&gt;
&lt;br /&gt;
* Entscheidungsfrage: Gibt es eine Belegung con X sodass Y wahr ist?&lt;br /&gt;
* Bei komplizierten Problemen ist kein besserer Algorithmus bekannt als alle &amp;lt;math&amp;gt;\2^N&amp;lt;/math&amp;gt; Möglichkeiten zu probieren. &lt;br /&gt;
*Jede CPU kann als logische Schaltung geschrieben werden (damit auch jedes while-Programm)&lt;br /&gt;
: Mit Gattern&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden, z.B.:&lt;br /&gt;
&lt;br /&gt;
[[Image:Bild 11.jpg]]&lt;br /&gt;
&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5674</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5674"/>
				<updated>2017-07-25T08:31:22Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Einleitung */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage &amp;gt; und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei &amp;gt; impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
: &amp;lt;math&amp;gt;\uparrow&amp;lt;/math&amp;gt; ineffizient, da es meist exponentiell viele Kandidaten Z gibt.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden, z.B.:&lt;br /&gt;
&lt;br /&gt;
[[Image:Bild 11.jpg]]&lt;br /&gt;
&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5673</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5673"/>
				<updated>2017-07-25T08:28:45Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Einleitung */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
: Orakel: &amp;quot;Rundreise Z ist &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt;200kM&amp;quot; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt;leicht &amp;amp; effizient zu testen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Klassische Definition von NP: Probleme die von einer nicht-deterministischen Turingmaschine gelöst werden können (N = Nicht deterministisch, P = Polynomiell).&lt;br /&gt;
: nicht deterministische Turingmaschine: formale Definition kompliziert &amp;lt;math&amp;gt;\rightarrow&amp;lt;/math&amp;gt; Theoretische Informatik&lt;br /&gt;
:: anschaulich: TM kann in kritischen Situationen das Orakel fragen und sich vorsagen lassen&lt;br /&gt;
&lt;br /&gt;
moderne Definition: &amp;quot;polynomiell Verifizierbar&amp;quot;: es gibt effizienten Algorithmus, der für Probleme X und Entscheidungsfrage &amp;gt; und Kandidatenlösung Z entscheidet, ob Z eine &amp;quot;ja-Antwort&amp;quot; bei &amp;gt; impliziert.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 1&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;ja&amp;quot; (wissen wir aber nicht): &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;z: V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; OK&lt;br /&gt;
:: Z ist Beweis (proof/witness/certificate) dafür, dass Y die Antwort &amp;quot;ja&amp;quot; hat&lt;br /&gt;
:: liefert V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch, ist Z kein Beweis und wir wissen noch nicht, ib Y mit &amp;quot;ja&amp;quot; oder &amp;quot;nein&amp;quot; zu beantworten ist.&lt;br /&gt;
: &amp;lt;u&amp;gt;Fall 2&amp;lt;/u&amp;gt;: korrekte Antwort auf Y ist &amp;quot;nein&amp;quot;: &amp;lt;math&amp;gt;\forall&amp;lt;/math&amp;gt;Z V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; falsch&lt;br /&gt;
: &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; hat man einen Überprüfungsalgorithmus V, kann man X mit Y stets duch erschöpfende Suche (&amp;quot;brute-force&amp;quot;) lösen&lt;br /&gt;
&lt;br /&gt;
: für jede mögliche Kandidatenlösung Z:&lt;br /&gt;
:: falls V(X, Y, Z) &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; ok: &lt;br /&gt;
:::return &amp;quot;ja&amp;quot;&lt;br /&gt;
:: return &amp;quot;nein&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden, z.B.:&lt;br /&gt;
&lt;br /&gt;
[[Image:Bild 11.jpg]]&lt;br /&gt;
&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5672</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5672"/>
				<updated>2017-07-25T08:06:05Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Einleitung */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
*Komplexitätsklasse NP-Schwer (NP-hard): mindestens so schwer wie NP-C, aber nicht unbedingt &amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP&lt;br /&gt;
[[Image:DiagramNP.jpg]] &amp;lt;u&amp;gt;Vereinfachung&amp;lt;/u&amp;gt;: NP enthält nur Entscheigungsprobleme: Fragen mit Ja/Nein-Antwort.&lt;br /&gt;
::::: z.B. &lt;br /&gt;
::::: TSP-Optimierungsproblem (NP-Schwer):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: gesucht: kürzeste Rundreise&lt;br /&gt;
::::: TSP-Entscheidungsproblem (NP-Vollständig):&lt;br /&gt;
:::::: gegeben: gewichteter Graph&lt;br /&gt;
:::::: &amp;lt;math&amp;gt;\exist&amp;lt;/math&amp;gt;Rundreise &amp;lt;math&amp;gt;\le&amp;lt;/math&amp;gt; 200kM, ist das wahr oder falsch?&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- * fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden, z.B.:&lt;br /&gt;
&lt;br /&gt;
[[Image:Bild 11.jpg]]&lt;br /&gt;
&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5671</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5671"/>
				<updated>2017-07-25T07:47:03Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Einleitung */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
: offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
:: -P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
:: -oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
: Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
&lt;br /&gt;
[[Image:DiagramNP.jpg]]&lt;br /&gt;
&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden, z.B.:&lt;br /&gt;
&lt;br /&gt;
[[Image:Bild 11.jpg]]&lt;br /&gt;
&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5670</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5670"/>
				<updated>2017-07-25T07:42:57Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Einleitung */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
*offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
: P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
: oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
:: &amp;lt;math&amp;gt;\downarrow&amp;lt;/math&amp;gt; &lt;br /&gt;
: Lösung (R) &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt; Lösung Q(R)&lt;br /&gt;
** Reduktion muss effizient funktionieren, d.h. O(&amp;lt;math&amp;gt;\N^D&amp;lt;/math&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
&lt;br /&gt;
[[Image:DiagramNP.jpg]]&lt;br /&gt;
&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden, z.B.:&lt;br /&gt;
&lt;br /&gt;
[[Image:Bild 11.jpg]]&lt;br /&gt;
&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5669</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5669"/>
				<updated>2017-07-24T12:57:54Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Die Problemklassen P und NP */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
: O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
: z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
: (s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
: x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
=== Einleitung ===&lt;br /&gt;
*Komplexitätsklasse P: Effiziente Lösung bekannt (sortieren, MST, Dijkstra)&lt;br /&gt;
*Komplexitätsklasse NP: Existiert ein effizienter Algorithmus um einen '''geratenen''' Lösungsvorschlag zu überprüfen.&lt;br /&gt;
: geraten durch &amp;quot;Orakel&amp;quot; -&amp;gt; Black Box, nicht bekannt wie!&lt;br /&gt;
*offensichtlich gilt P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (bekannter Lösungsalgorithmus kann immer als Orakel dienen). Offen ob:&lt;br /&gt;
: P&amp;lt;math&amp;gt;\subset&amp;lt;/math&amp;gt;NP (es gibt Probleme ohne effizienten Alg)&lt;br /&gt;
: oder P=NP (effizienter Algorithmus nur noch nicht entdeckt)&lt;br /&gt;
*Komplexitätsklasse NP-Vollständig (NP-C [complete]): Schwierigste Probleme in NP, wenn Q&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C kann man mit Algorithmus für Q indirekt auch jedes andere Problem in NP lösen&lt;br /&gt;
: R&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP &amp;lt;math&amp;gt;\rightsquigarrow&amp;lt;/math&amp;gt;Q(R)&amp;lt;math&amp;gt;\in&amp;lt;/math&amp;gt;NP-C (Reduktion)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
&lt;br /&gt;
[[Image:DiagramNP.jpg]]&lt;br /&gt;
&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden, z.B.:&lt;br /&gt;
&lt;br /&gt;
[[Image:Bild 11.jpg]]&lt;br /&gt;
&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5668</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5668"/>
				<updated>2017-07-19T02:23:15Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
*für viele Probleme kein effizienter Algorithmus bekannt (effizient = polynomielle Komplexität&lt;br /&gt;
O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), für ein beliebig großes festes D; nicht effizient: langsamer als polynomiell, &lt;br /&gt;
z.b. O(&amp;lt;math&amp;gt;2^N&amp;lt;/math&amp;gt;))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Bsp:&lt;br /&gt;
*Problem des Handlungsreisenden&lt;br /&gt;
*Steine Bäume verallg. MST: man darf zusätzliche Punkte hinzufügen&lt;br /&gt;
*Clique - Problem: Clique in Graph G: maximaler vollständiger Teilgraph, trivial: 2 Kinder (gibt es eine Clique mit k Mitgliedern?)&lt;br /&gt;
*Integer Linear Programming &amp;lt;math&amp;gt;\hat{x}&amp;lt;/math&amp;gt; = arg max &amp;lt;math&amp;gt;c^T&amp;lt;/math&amp;gt;x [c,x Spaltenvektoren der Länge N] &lt;br /&gt;
**(s.t. A*x &amp;lt;math&amp;gt;\leq&amp;lt;/math&amp;gt; b [A, Matrix MxN, b Spaltenvektor von M]&lt;br /&gt;
**x&amp;lt;math&amp;gt;\in \mathbb{N}^N, \mathbb{Z}^N&amp;lt;/math&amp;gt;, {0, 1}&amp;lt;math&amp;gt;^N&amp;lt;/math&amp;gt; &amp;lt;math&amp;gt;\implies&amp;lt;/math&amp;gt; nicht effizient&lt;br /&gt;
** x&amp;lt;math&amp;gt;\in \mathbb{R}^N \implies&amp;lt;/math&amp;gt; effizient)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
&lt;br /&gt;
[[Image:DiagramNP.jpg]]&lt;br /&gt;
&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden, z.B.:&lt;br /&gt;
&lt;br /&gt;
[[Image:Bild 11.jpg]]&lt;br /&gt;
&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5667</id>
		<title>NP-Vollständigkeit</title>
		<link rel="alternate" type="text/html" href="https://alda.iwr.uni-heidelberg.de/index.php?title=NP-Vollst%C3%A4ndigkeit&amp;diff=5667"/>
				<updated>2017-07-19T01:38:24Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Systematisches Erzeugen aller Permutationen */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Das Problem des Handlungsreisenden ==&lt;br /&gt;
'''(engl.: Traveling Salesman Problem; abgekürzt: TSP)'''&amp;lt;br\&amp;gt;&lt;br /&gt;
[http://de.wikipedia.org/wiki/Problem_des_Handlungsreisenden Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Prim%27s_algorithm (en)]&lt;br /&gt;
[[Image:TSP_Deutschland_3.PNG|thumb|200px|right|Optimaler Reiseweg eines Handlungsreisenden([http://de.wikipedia.org/w/index.php?title=Bild:TSP_Deutschland_3.PNG&amp;amp;filetimestamp=20070110124506 Quelle])]]&lt;br /&gt;
&lt;br /&gt;
*Eine der wohl bekanntesten Aufgabenstellungen im Bereich der Graphentheorie ist das Problem des Handlungsreisenden. &lt;br /&gt;
*Hierbei soll ein Handlungsreisender nacheinander ''n'' Städte besuchen und am Ende wieder an seinem Ausgangspunkt ankommen. Dabei soll jede Stadt nur einmal besucht werden und der Weg mit den minimalen Kosten gewählt werden. &lt;br /&gt;
*Alternativ kann auch ein Weg ermittelt werden, dessen Kosten unter einer vorgegebenen Schranke liegen.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
:&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zusammenhängender, gewichteter Graph (oft vollständiger Graph)&lt;br /&gt;
:&amp;lt;u&amp;gt;''gesucht''&amp;lt;/u&amp;gt;: kürzester Weg, der alle Knoten genau einmal (falls ein solcher Pfad vorhanden) besucht (und zum Ausgangsknoten zurückkehrt)&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
:auch genannt: kürzester Hamiltonkreis &lt;br /&gt;
::- durch psychologische Experimente wurde herausgefunden, dass Menschen (in 2D) ungefähr proportionale Zeit zur Anzahl der Knoten brauchen, um einen guten Pfad zu finden, der typischerweise nur &amp;lt;math&amp;gt;\lesssim 5%&amp;lt;/math&amp;gt; länger als der optimale Pfad ist&amp;lt;br\&amp;gt;&lt;br /&gt;
:&amp;lt;u&amp;gt;''vorgegeben''&amp;lt;/u&amp;gt;: Startknoten (kann willkürlich gewählt werden), vollständiger Graph&lt;br /&gt;
&lt;br /&gt;
::::: =&amp;gt; v-1 Möglichkeiten für den ersten Nachfolgerknoten =&amp;gt; je v-2 Möglichkeiten für dessen Nachfolger...&lt;br /&gt;
:::::also &amp;lt;math&amp;gt;\frac{(v-1)!}{2}&amp;lt;/math&amp;gt; mögliche Wege in einem vollständigen Graphen&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
*Ein naiver Ansatz zur Lösung des TSP Problems ist das erschöpfende Durchsuchen des Graphen, auch &amp;quot;brute force&amp;quot; Algorithmus (&amp;quot;mit roher Gewalt&amp;quot;), indem alle möglichen Rundreisen betrachtet werden und schließlich die mit den geringsten Kosten ausgewählt wird. &lt;br /&gt;
*Dieses Verfahren versagt allerdings bei größeren Graphen, aufgrund der hohen Komplexität.&lt;br /&gt;
&lt;br /&gt;
=== Approximationsalgorithmus === &lt;br /&gt;
&lt;br /&gt;
Für viele Probleme in der Praxis sind keine effizienten Algorithmen bekannt&lt;br /&gt;
(NP-schwer). Diese (z.B. TSP) werden mit Approximationsalgorithmen berechnet,&lt;br /&gt;
die effizient berechenbar sind, aber nicht unbedingt die optimale&lt;br /&gt;
Lösung liefern. Beispielsweise ist es relativ einfach, eine Tour zu finden, die höchstens um den Faktor zwei länger ist als die optimale Tour. Die Methode beruht darauf, dass einfach der minimale Spannbaum ermittelt wird. &lt;br /&gt;
&lt;br /&gt;
'''Approximationsalgorithmus für TSP'''&amp;lt;br\&amp;gt;&lt;br /&gt;
* TSP für ''n'' Knoten sei durch Abstandsmatrix D = &amp;lt;math&amp;gt;(d_{ij}) 1 \le i, j \le n&amp;lt;/math&amp;gt; &lt;br /&gt;
:gegeben (vollständiger Graph mit ''n'' Knoten, &amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Kosten der Kante (i,j)) &amp;lt;br\&amp;gt;&lt;br /&gt;
:''gesucht:'' Rundreise mit minimalen Kosten. Dies ist NP-schwer!&amp;lt;br\&amp;gt;&lt;br /&gt;
* D erfüllt die Dreiecksungleichung  &amp;lt;math&amp;gt; \Leftrightarrow d_{ij} + d_{jk} \geq d_{ik} \text{ fuer } \forall{i, j, k} \in \lbrace 1, ..., n  \rbrace&amp;lt;/math&amp;gt; &amp;lt;br\&amp;gt; &lt;br /&gt;
* Dies ist insbesondere dann erfüllt, wenn D die Abstände bezüglich einer Metrik darstellt oder D Abschluss einer beliebigen Abstandsmatrix C ist, d.h. :&amp;lt;math&amp;gt;d_{ij}&amp;lt;/math&amp;gt; = Länge des kürzesten Weges (bzgl. C) von i nach j.&lt;br /&gt;
&lt;br /&gt;
*Die ”Qualität”der Lösung mit einem Approximationsalgorithmus ist höchstens um einen konstanten Faktor schlechter ist als die des Optimums.&lt;br /&gt;
&lt;br /&gt;
=== Systematisches Erzeugen aller Permutationen === &lt;br /&gt;
*Allgemeines Verfahren, wie man von einer gegebenen Menge verschiedene Schlüssel - in diesem Fall: Knotennummern - sämtliche Permutationen systematisch erzeugen kann. &amp;lt;br\&amp;gt;&lt;br /&gt;
*'''Trick''': interpretiere jede Permutation als Wort und betrachte dann deren lexikographische (&amp;quot;wie im Lexikon&amp;quot;) Ordnung.&amp;lt;br\&amp;gt;&lt;br /&gt;
*Der erste unterschiedliche Buchstabe unterscheidet. Wenn die Buchstaben gleich sind, dann kommt das kürzere Wort zuerst. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;u&amp;gt;''gegeben''&amp;lt;/u&amp;gt;: zwei Wörter a, b der Länge n=len(a) bzw. m=len(b). Sei k = min(n,m) (im Spezialfall des Vergleichs von Permutationen gilt k = n = m)&amp;lt;br\&amp;gt;&lt;br /&gt;
Mathematische Definition, wie die Wörter im Wörterbuch sortiert sind: &amp;lt;br\&amp;gt;&lt;br /&gt;
:::&amp;lt;math&amp;gt;a&amp;lt;b \Leftrightarrow &lt;br /&gt;
\begin{cases}&lt;br /&gt;
n &amp;lt; m &amp;amp; \text{ falls fuer } 0 \le i \le k-1 \text{ gilt: } a[i] = b[i] \\&lt;br /&gt;
a[j] &amp;lt; b[j] &amp;amp; \text{ falls fuer } 0 \le i \le j-1 \text{ gilt: } a[i] = b[i], \text{ aber fuer ein } j&amp;lt;k: a[j] \ne b[j]&lt;br /&gt;
\end{cases}&amp;lt;/math&amp;gt;&amp;lt;br\&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Algorithmus zur Erzeuguung aller Permutationen:&lt;br /&gt;
# beginne mit dem kleinsten Wort bezüglich der lexikographischen Ordnung =&amp;gt; das ist das Wort, wo a aufsteigend sortiert ist&lt;br /&gt;
# definiere Funktion &amp;quot;next_permutation&amp;quot;, die den Nachfolger in lexikographischer Ordnung erzeugt&lt;br /&gt;
&lt;br /&gt;
Beispiel: Die folgenden Permutationen der Zahlen 1,2,3 sind lexikographisch geordnet&lt;br /&gt;
&lt;br /&gt;
 1 2 3    6 Permutationen, da 3! = 6&lt;br /&gt;
 1 3 2&lt;br /&gt;
 2 1 3&lt;br /&gt;
 2 3 1&lt;br /&gt;
 3 1 2&lt;br /&gt;
 3 2 1&lt;br /&gt;
 -----&lt;br /&gt;
 0 1 2 Position&lt;br /&gt;
&lt;br /&gt;
Die lexikographische Ordnung wird deutlicher, wenn wir statt dessen die Buchstaben a,b,c verwenden:&lt;br /&gt;
&lt;br /&gt;
 abc&lt;br /&gt;
 acb&lt;br /&gt;
 bac&lt;br /&gt;
 bca&lt;br /&gt;
 cab&lt;br /&gt;
 cba&lt;br /&gt;
&lt;br /&gt;
Eine Funktion, die aus einer gegebenen Permutation die in lexikographischer Ordnung nächst folgende erzeugt, kann wie folgt implementiert werden:&lt;br /&gt;
&lt;br /&gt;
 def next_permutation(a):&lt;br /&gt;
 	i = len(a) -1  #letztes Element; man arbeitet sich von hinten nach vorne durch&lt;br /&gt;
 	while True:  # keine Endlosschleife, da i dekrementiert wird und damit irgendwann 0 wird&lt;br /&gt;
 		if i &amp;lt;= 0: return False  # a ist letzte Permutation&lt;br /&gt;
 		i -= 1&lt;br /&gt;
 		if a[i]&amp;lt;a[i+1]: break&lt;br /&gt;
 	#lexikogr. Nachfolger hat größeres a[i]&lt;br /&gt;
 	j = len(a)&lt;br /&gt;
 	while True:&lt;br /&gt;
 		j -= 1&lt;br /&gt;
 		if a[i] &amp;lt; a[j]: break&lt;br /&gt;
 	a[i], a[j] = a[j], a[i] #swap a[i], a[j]&lt;br /&gt;
 	#sortiere aufsteigend zwischen a[i] und Ende&lt;br /&gt;
 	#zur Zeit absteigend sortiert =&amp;gt; invertieren&lt;br /&gt;
 	i += 1&lt;br /&gt;
 	j = len(a) -1&lt;br /&gt;
 	while i &amp;lt; j:&lt;br /&gt;
 		a[i], a[j] = a[j], a[i]&lt;br /&gt;
 		i += 1&lt;br /&gt;
 		j-= 1&lt;br /&gt;
 	return True  # eine weitere Permutation gefunden&lt;br /&gt;
  	&lt;br /&gt;
  def naiveTSP(graph):&lt;br /&gt;
 	start = 0&lt;br /&gt;
 	result = range(len(graph))+[start]&lt;br /&gt;
 	rest = range(1,len(graph))&lt;br /&gt;
 	c = pathCost(result, graph)&lt;br /&gt;
 	while next_permutation(rest):&lt;br /&gt;
 		r = [start]+rest+[start]&lt;br /&gt;
 		cc = pathCost(r, graph)&lt;br /&gt;
 		if cc &amp;lt; c:&lt;br /&gt;
 			c = cc&lt;br /&gt;
 			result = r&lt;br /&gt;
 		return c, result&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
'''Komplexität''': &amp;lt;math&amp;gt;(v-1)!&amp;lt;/math&amp;gt; Schleifendurchläufe (=Anzahl der Permutationen, da die Schleife abgebrochen wird, sobald es keine weiteren Permutationen mehr gibt), also &lt;br /&gt;
	&amp;lt;math&amp;gt;O(v!) = O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
;Beispiel:&lt;br /&gt;
{| &lt;br /&gt;
|- &lt;br /&gt;
| | i = 0 || |  |||  ||| j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
|| &amp;amp;darr; || || || &amp;amp;darr; ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 || #input für next_permutation&lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  || i = 2 || ||  j = 3 ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || &amp;amp;darr;|| || &amp;amp;darr; ||&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 1|| # vertauschen der beiden Elemente &lt;br /&gt;
|-&lt;br /&gt;
|-&lt;br /&gt;
&lt;br /&gt;
||  ||  ||i = 2 ||   ||&lt;br /&gt;
|-&lt;br /&gt;
||  ||  ||j = 2 ||   ||&lt;br /&gt;
&lt;br /&gt;
|-&lt;br /&gt;
||  || || &amp;amp;darr;|| ||&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|- &lt;br /&gt;
| style=&amp;quot;background:silver; color:white&amp;quot; | 1 ||style=&amp;quot;background:silver; color:white&amp;quot; | 2 ||style=&amp;quot;background:silver; color:white&amp;quot;| 3 ||style=&amp;quot;background:silver; color:white&amp;quot; | 4|| #absteigend sortiert&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Stirling'sche Formel ===&lt;br /&gt;
[http://de.wikipedia.org/wiki/Stirling-Formel Wikipedia (de)]&lt;br /&gt;
[http://en.wikipedia.org/wiki/Stirling%27s_approximation (en)]&lt;br /&gt;
&lt;br /&gt;
Die Stirling-Formel ist eine mathematische Formel, mit der man für große Fakultäten Näherungswerte berechnen kann. Die Stirling-Formel findet überall dort Verwendung, wo die exakten Werte einer Fakultät nicht von Bedeutung sind. Damit lassen sich durch die Stirling'sche Formel z.T. starke Vereinfachungen erzielen. &lt;br /&gt;
&amp;lt;math&amp;gt;v! \approx \sqrt{2 \pi v} \left(\frac{v}{e}\right)^v&amp;lt;/math&amp;gt;&lt;br /&gt;
: &amp;lt;math&amp;gt;O(v!) = O\left(\sqrt{v}\left(\frac{v}{e}\right)^v\right) \approx O(v^v)&amp;lt;/math&amp;gt;&lt;br /&gt;
&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;
&lt;br /&gt;
&lt;br /&gt;
== Die Problemklassen P und NP ==&lt;br /&gt;
&lt;br /&gt;
* fundamentale Unterscheidung:&lt;br /&gt;
** Komplexität O(&amp;lt;math&amp;gt;n^p&amp;lt;/math&amp;gt;), p &amp;lt; ∞ (n = Problemgröße), &amp;amp;rArr; ist eventuell effizient&lt;br /&gt;
**exponentielle Komplexität O(&amp;lt;math&amp;gt;2^n&amp;lt;/math&amp;gt;), O(&amp;lt;math&amp;gt;2^{\sqrt{n}}&amp;lt;/math&amp;gt;), &amp;amp;rArr; prinzipiell nicht effizient &lt;br /&gt;
* Vereinfachung:&lt;br /&gt;
** betrachte nur Entscheidungsprobleme, d.h. Algorithmen, die True/False liefern&lt;br /&gt;
** z.B. BP: „Gibt es einen Pfad der Länge ≤ L?“&lt;br /&gt;
* Klasse P: alle Algorithmen, die in polynomieller Zeit eine Lösung finden,  &lt;br /&gt;
: Klasse NP: Alle Algorithmen, wo man eine gegebene Lösung in polynomieller Zeit überprüfen kann&lt;br /&gt;
* Ungelöstes Problem: Sind alle Probleme in NP auch in P? („P = NP?“)&lt;br /&gt;
* Welches sind die schwierigsten Probleme in NP?&lt;br /&gt;
: =&amp;gt; die, sodass man alle anderen NP-Probleme in diese umwandeln kann: „NP vollständig“, „NP complete“&lt;br /&gt;
&lt;br /&gt;
[[Image:DiagramNP.jpg]]&lt;br /&gt;
&lt;br /&gt;
* umwandeln:&lt;br /&gt;
** Problem wird auf ein anderes reduziert&lt;br /&gt;
** Reduktion darf nur polynomielle Zeit erfordern (d.h. alle Zwischenschritte müssen polynomiell sein)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 3-SAT ist NP vollständig ===&lt;br /&gt;
Skizze des Beweises:&lt;br /&gt;
# Unsere Algorithmen können auf einer Turingmaschine ausgeführt werden (äquivalent zur Turingmaschine: λ-Kalkül, while-Programm usw.)&lt;br /&gt;
# Die Turingmaschine und ein gegebenes (festes) Programm können als logische Schaltung (Schaltnetz) implementiert werden, „Algorithmus in Hardware gegossen“&lt;br /&gt;
# Jedes Schaltnetzwerk kann als logische Formel geschrieben werden, z.B.:&lt;br /&gt;
&lt;br /&gt;
[[Image:Bild 11.jpg]]&lt;br /&gt;
&lt;br /&gt;
: 4.   Jede logische Formel kann in 3-CNF umgewandelt werden&lt;br /&gt;
&lt;br /&gt;
:=&amp;gt; Jedes algorithmische Entscheidungsproblem kann als 3-SAT-Problem geschrieben werden.&lt;/div&gt;</summary>
		<author><name>Alda</name></author>	</entry>

	<entry>
		<id>https://alda.iwr.uni-heidelberg.de/index.php?title=Graphen_und_Graphenalgorithmen&amp;diff=5666</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=5666"/>
				<updated>2017-07-19T00:02:16Z</updated>
		
		<summary type="html">&lt;p&gt;Alda: /* Anwendung: Das Erfüllbarkeitsproblem in Implikationengraphen */&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 auf demselben Weg zurück (sogenanntes &amp;lt;i&amp;gt;back tracking&amp;lt;/i&amp;gt;), bis man einen Knoten findet, der noch einen unbesuchten 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;
Die 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 &amp;lt;i&amp;gt;property map&amp;lt;/i&amp;gt; genannt. (Die Verwendung von property maps hat sich gegenüber der alternativen Idee durchgesetzt, solche Eigenschaften in speziellen Knotenklassen zu speichern. Im letzteren Fall braucht man nämlich für jede Anwendung eine angepasste Knotenklasse mit den jeweils gewünschten Attributen und damit auch angepasste Implementationen der Graphenfunktionen, was sich als sehr aufwändig erwiesen hat.) &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 Nachbarpixeln 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, Informationen über den Verlauf der Suche zu sammeln und am Ende zurückzugeben, so dass andere Algorithmen diese Information nutzen können. Typische Beispiele dafür sind eine Reihenfolge der Knoten (in discovery oder finishing order) oder die Vorgänger jedes Knotens im Tiefensuchbaum (also  von welchem Knoten aus man den jeweiligen Knoten zuerst erreicht hat). Wir führen dafür drei neue Arrays ein. &lt;br /&gt;
&lt;br /&gt;
 def dfs(graph, startnode):&lt;br /&gt;
     visited = [False]*len(graph)    # wurde ein Knoten bereits besucht?&lt;br /&gt;
     parents = [None]*len(graph)     # registriere für jeden Knoten den Vorgänger im Tiefensuchbaum&lt;br /&gt;
     discovery_order = []            # enthält am Ende die pre-order Sortierung&lt;br /&gt;
     finishing_order = []            # enthält am Ende die post-order Sortierung&lt;br /&gt;
     &lt;br /&gt;
     def visit(node, parent):        # rekursive Hilfsfunktion&lt;br /&gt;
         if not visited[node]:       # besuche 'node', wenn noch nicht besucht wurde&lt;br /&gt;
             visited[node] = True           # markiere 'node' als besucht&lt;br /&gt;
             parents[node] = parent         # speichere den Vorgänger von 'node'&lt;br /&gt;
             discovery_order.append(node)   # registriere, dass 'node' jetzt entdeckt wurde&lt;br /&gt;
             for neighbor in graph[node]:   # besuche rekursiv die Nachbarn ...&lt;br /&gt;
                 visit(neighbor, node)      #  ... wobei 'node' zu deren Vorgänger wird&lt;br /&gt;
             finishing_order.append(node)   # registriere, dass 'node' jetzt fertiggestellt wurde&lt;br /&gt;
     &lt;br /&gt;
     visit(startnode, None)          # beginne bei 'startnode', der keinen Vorgänger hat&lt;br /&gt;
     &lt;br /&gt;
     return parents, discovery_order, finishing_order # gib die zusätzliche Informationen zurück&lt;br /&gt;
&lt;br /&gt;
Beginnt man die Suche bei Knoten 1, entsprechen die Inhalte der Arrays &amp;lt;tt&amp;gt;discovery_order&amp;lt;/tt&amp;gt; und &amp;lt;tt&amp;gt;finishing_order&amp;lt;/tt&amp;gt; für den obigen Beispielgraphen gerade den vorher angeführten &amp;lt;tt&amp;gt;print&amp;lt;/tt&amp;gt;-Ausgaben. Die Vorgänger im Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; lauten: &lt;br /&gt;
  Knotennummer  |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7&lt;br /&gt;
  --------------+-----+-----+-----+-----+-----+-----+-----+-----&lt;br /&gt;
  Vorgänger     | None| None|  1  |  4  |  2  |  2  |  3  |  3&lt;br /&gt;
&lt;br /&gt;
Die Knotennummern dienen hier als Array-Indizes, und die dazugehörigen Arrayeinträge verweisen auf die Vorgänger. Man kann mit diesen Informationen den Weg von jedem Knoten zur Wurzel zurückverfolgen und damit den Tiefensuchbaum von unten nach oben rekonstruieren. Man beachte, dass &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; den Eintrag &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; für die Knoten 0 umd 1 enthält, weil Knoten 0 in diesem Graphen nicht existiert und Knoten 1 als Wurzel der Suche keinen Vorgänger hat.&lt;br /&gt;
&lt;br /&gt;
Wird das Array &amp;lt;tt&amp;gt;parents&amp;lt;/tt&amp;gt; verwendet, kann man den Code vereinfachen, indem man das Array &amp;lt;tt&amp;gt;visited&amp;lt;/tt&amp;gt; einspart: Sobald ein Knoten erstmals besucht wurde, ist sein Vorgänger bekannt und damit ungleich &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt;. Die Abfrage &amp;lt;tt&amp;gt;if parents[node] is None:&amp;lt;/tt&amp;gt; liefert damit das gleiche Resultat wie die Abfrage &amp;lt;tt&amp;gt;if not visited[node]:&amp;lt;/tt&amp;gt;. Einzige Ausnahme ist der Startknoten der Suche, dessen Vorgänger bisher &amp;lt;tt&amp;gt;None&amp;lt;/tt&amp;gt; war. Dieses Problem löst man leicht mit der Konvention, dass man den Startknoten zu seinem eigenen Vorgänger erklärt. Man startet die Suche also mit &amp;lt;tt&amp;gt;visit(startnode, startnode)&amp;lt;/tt&amp;gt; statt mit &amp;lt;tt&amp;gt;visit(startnode, None)&amp;lt;/tt&amp;gt;.&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 Nachbarknoten 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;
     parents = [None]*len(graph)            # speichere für jeden Knoten den Vorgänger im Breitensuchbaum&lt;br /&gt;
     parents[startnode] = startnode         # Konvention: der Startknoten hat sich selbst als Vorgänger &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 noch Knoten zu bearbeiten sind&lt;br /&gt;
         node = q.popleft()                 # Knoten aus der Queue nehmen (first in - first out)&lt;br /&gt;
         print(node)                        # den Knoten bearbeiten (hier: Knotennummer drucken)&lt;br /&gt;
         for neighbor in graph[node]:       # die Nachbarn expandieren&lt;br /&gt;
             if parents[neighbor] is None:  # Nachbar wurde noch nicht besucht&lt;br /&gt;
                 parents[neighbor] = node   # =&amp;gt; Vorgänger merken, Knoten dadurch als &amp;quot;besucht&amp;quot; markieren&lt;br /&gt;
                 q.append(neighbor)         #    und in die Queue aufnehmen&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 range(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 = list(range(len(graph)))  # Initialisierung der property map: jeder Knoten ist sein eigener Anker&lt;br /&gt;
     &lt;br /&gt;
     for node in range(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 range(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 zunächst eine wichtige Eigenschaft des Algorithmus: Die Priorität (=Pfadlänge) des Knotens an der Spitze des Heaps wächst im Laufe des Algorithmus monoton an (aber nicht notwendigerweise streng monoton). Mit anderen Worten: liefert &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; in der i-ten Iteration der &amp;lt;tt&amp;gt;while&amp;lt;/tt&amp;gt;-Schleife den Knoten u mit der Pfadlänge l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt;, und in der (i+1)-ten Iteration den Knoten v mit der Pfadlänge l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt;, so gilt stets l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; &amp;amp;ge; l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt;. Wir zeigen dies mit der Technik des indirekten Beweises, d.h. wir nehmen das Gegenteil an und führen diese Annahme zum Widerspruch. Wäre also l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; &amp;lt; l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt;, gäbe es zwei Möglichkeiten:&lt;br /&gt;
&amp;lt;ol&amp;gt;&lt;br /&gt;
&amp;lt;li&amp;gt;Der Weg nach v mit der Länge l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; war in der i-ten Iteration schon bekannt und somit bereits im Heap enthalten. Dann hätte &amp;lt;tt&amp;gt;heappop&amp;lt;/tt&amp;gt; in dieser Iteration aber v zurückgegeben, im Widerspruch zur Annahme, dass u zurückgegeben wurde.&amp;lt;/li&amp;gt;&lt;br /&gt;
&amp;lt;li&amp;gt;Der Weg wurde erst bei der Expansion von u in der i-ten Iteration gefunden. Dann muss v ein Nachbar von u sein, und seine Weglänge berechnet sich als l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; = l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt; + w&amp;lt;sub&amp;gt;u,v&amp;lt;/sub&amp;gt;. Da für die Kantengewichte aber w&amp;lt;sub&amp;gt;u,v&amp;lt;/sub&amp;gt; &amp;amp;ge; 0 gefordert ist, kann l&amp;lt;sub&amp;gt;v&amp;lt;/sub&amp;gt; &amp;lt; l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt; nicht gelten.&amp;lt;/li&amp;gt;&lt;br /&gt;
&amp;lt;/ol&amp;gt;&lt;br /&gt;
Diese Monotonieeigenschaft hat eine interessante Konsequenz: Beträgt der Abstand vom Start zum Zielknoten l&amp;lt;sub&amp;gt;z&amp;lt;/sub&amp;gt;, so findet Dijsktra's Algorithmus als Nebenprodukt auch die kürzesten Wege zu allen näher gelegenen Knoten, also zu allen Knoten u, für deren Abstand l&amp;lt;sub&amp;gt;u&amp;lt;/sub&amp;gt; &amp;lt; l&amp;lt;sub&amp;gt;z&amp;lt;/sub&amp;gt; gilt. Dies trifft auch dann zu, wenn diese Wege für den Benutzer gar nicht von Interesse sind. Der A*-Algorithmus, der weiter unten erklärt wird, versucht dem abzuhelfen.&lt;br /&gt;
&lt;br /&gt;
Wir können nun mittels vollständiger Induktion die folgende Schleifen-Invariante beweisen: 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 wieder 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 dem Momemnt in den Heap eingefügt werden, wo der kürzeste Weg für &amp;lt;tt&amp;gt;predecessor&amp;lt;/tt&amp;gt; gefunden wurde. Man beachte auch, dass wegen der Monotonieeigenschaft 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 die Knoten in &amp;lt;math&amp;gt;S&amp;lt;/math&amp;gt;. &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 im Heap 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;: Wegen der Monotonieeigenschaft muss jetzt &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) &amp;gt; Kosten(node &amp;amp;rarr; predecessor &amp;amp;rarr; startnode)&amp;lt;/tt&amp;gt; gelten. 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 aber als &amp;lt;tt&amp;gt;Kosten(x &amp;amp;rarr; startnode) + weight[(x, node)]&amp;lt;/tt&amp;gt;, und deshalb kann dieser Weg keinesfalls kürzer sein.&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 range(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 range(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 range(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:     # neuer Knoten gefunden:&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;
         else:&lt;br /&gt;
             return False                     # node war bereits 'finished' =&amp;gt; kein Zyklus&lt;br /&gt;
     &lt;br /&gt;
     for node in range(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 range(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;
== 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>Alda</name></author>	</entry>

	</feed>