So, guten Morgen. Willkommen zur Algorithmik.
Wir beschäftigen uns heute nochmal mit der amortisierten Analyse.
Ich habe in der letzten Vorlesung angefangen, ein Beispiel einzuführen,
an dem wir die amortisierte Analyse nochmal betrachten wollen.
Ein ganz wichtiges Beispiel, wo es um Race gibt,
die dynamisch im Laufe eines Algorithmus vergrößert oder verkleinert werden sollen,
um die Speicherauslastung zu optimieren.
Das werden wir uns noch anschauen. Dafür brauchen wir wahrscheinlich
noch so eine halbe Stunde, schätze ich mal,
und haben damit das Analysekapitel abgeschlossen
Und dann haben wir sozusagen alles in der Tasche,
was wir so brauchen, an Techniken,
um uns dann den Algorithmen zuzuwenden.
Und wir werden dann heute noch in das größte Kapitel der Vorlesung einsteigen.
Das ist die Graf-Algorithmik. Das wird uns eine sehr lange Zeit beschäftigen.
Und heute wird es aber dann erstmal ein ganz sanfter Einstieg,
machen wir eine Menge Wiederholungen aus dem Grundstudium.
Einfach mal eine grundlegende Terminologie nochmal anschauen und dann werden wir
mit der Tiefensuche beginnen
Auch die sollten sie eigentlich schon mal gesehen haben.
Ich zeige ihnen hier nochmal so die wichtigsten Eigenschaften der Tiefensuche.
Und ja, bis dahin werden wir wahrscheinlich ungefähr kommen.
Und dann werden wir in der nächsten Vorlesung ein paar
etwas komplexere Anwendungen in der Tiefensuche uns dann anschauen.
Gut. Okay, dynamische Arrays, das Problem ist das folgende.
Wir haben Algorithmen, die einen zusammenhängenden Speicherbedarf benötigen,
weil sie ihre Datenstrukturen entsprechend über die Indizes in einem Array organisieren,
wie wir das zum Beispiel kennen, von Stapeln Warteschlangen.
Wenn wir denn vorher nicht wissen, wie groß diese Warteschlange werden,
wird es im Laufe der Ausführung des Algorithmus müssen wir dynamisch diesen Speicherbereich anpassen.
Wir versuchen dabei immer, eine optimale Belegung zu erreichen.
Unser Belegungsfaktor ist Alpha, das ist der Faktor,
der sich ergibt, wenn wir die tatsächliche Belegung,
die Anzahl der Elemente, die wir nutzen in dem Array,
dividieren durch die Größe des Arrays, was wir vorher reserviert haben.
Wir versuchen, dass dieser Belegungsfaktor irgendwie so bei eineinhalb liegt,
dann können wir immer noch Elemente hinzufügen ohne dass quasi wir zu viel Speicher verschwenden müssen.
Wenn der Belegungsfaktor gegen eins geht, dann wird unsere Datenstruktur
irgendwann in die Situation kommen, dass das Array nicht mehr ausreicht.
Wir werden das vergrößern müssen. Dass wir Zeit kosten,
weil wir in dem Augenblick die Informationen oder die Daten,
die in dem Array schon gespeichert sind,
von der alten Position auf die neue Position kopieren müssen.
Und wenn der Belegungsfaktor kleiner wird als ein Viertel,
ist es so, dass die Speicherauslastung zu ungünstig wird und
wir für diesen Fall einen Teil des Arrays halt freigeben wollen.
Wir gehen mal davon aus, dass das Betriebssystem uns potenziell
einen anderen Speicherbereich zuweist und wir dann verbleibenden Datenelemente noch kopieren müssen.
Gut, bei einem solchen Verhalten ist die Situation erst mal so,
dass die Operation zum Einfügen und Löschen in diese Tabelle,
in den allermeisten Fällen konstante Zeit benötigen,
so wie wir das auch erwarten würden.
Also wenn wir irgendwo was in dem Array speichern,
kostet uns konstante Zeit. Allerdings gibt es dann diese Situation,
wo wir über diese,
an die Belegungsgrenze gehen und dann entscheiden,
jetzt müssen wir das Array verdoppeln und in dem Augenblick brauchen
wir linear viel Zeit,
weil wir alle Informationen in dem Array kopieren müssen
Amortisiert, besteht dann so die Hoffnung,
dass wir trotzdem eigentlich einen konstanten Zeitbedarf haben, weil diese Situation,
dass wir die Daten kopieren müssen, nur relativ selten eintritt.
Okay, um jetzt zu analysieren, dass das tatsächlich
der Fall ist, benötigen wir entsprechend eine Potenzialfunktion.
Wir werden da sich Potenzialmethode verwenden und dann müssen wir zeigen,
dass diese Potenzialfunktion tatsächlich die Eigenschaft hat,
dass sie dazu führt,
dass die amortisierten Kosten zum Einführung und Löschen in ein Array
konstant sind
Gut, den Algorithmus habe ich Ihnen letztes Mal schon vorgestellt,
auch so ein bisschen die Randbedingungen, die gelten muss,
damit die Analyse am Ende korrekt ist.
Da müssen wir, glaube ich, jetzt nicht nochmal drüber gehen.
Und wir können uns jetzt also der Potenzialfunktion zuwenden.
Hier sehen Sie die Potenzialfunktion. Das ist die Funktion selber und hier ist sie einmal grafisch dargestellt,
weil das Wichtigste bei dieser Analyse ist,
dass man diese Funktion irgendwie versteht und motiviert,
weil wenn sie diese Analysemethode einsetzen wollen, später mal,
dann ist eigentlich der einzige kritische Schritt,
dass sie sich eine gute Potenzialfunktion ausdenken.
Der Rest ist nachher nur noch nachrechnen,
wie sie gleich auf den folgenden Folien sehen werden
Okay, also wie sieht unsere Potenzialfunktion aus? Sie ist
hier oben dargestellt, also viel von Tee. Tees,
also unsere Array-Tabelle, die hat natürlich im Prinzip noch eine zeitliche Komponente.
Also wir haben diese Tabelle zu jedem Zeitpunkt I nach jeder
Einfüge- oder Lesh-Operation. Und was wir dann machen ist,
wir machen das Potenzial abhängig von der Größe der Tabelle und
von der Anzahl der Elemente, die wir gespeichert haben.
Und wir haben eine Fallunterscheidung,
wenn der Belegungsfaktor größer gleich einhalb ist,
dann verwenden wir die Funktion zweimal Tee von N-T von G,
Punkt G. Also, das bedeutet zweimal die Anzahl der tatsächlich gespeicherten Elemente oder der belegten Array-Position minus die Größe der Tabelle.
Und wenn der Belegungsfaktor kleiner ist als eineinhalb,
dann verwenden wir die Größe der Tabelle,
t.g halbe minus die Anzahl der tatsächlich gespeicherten Elemente T von N.
Wenn man nur diese Funktion so betrachtet,
dann fällt die jetzt mehr oder weniger von der Decke und man fragt sich irgendwie,
ja, und warum ist die das jetzt?
Und da hilft es dann immer,
sich das vielleicht mal aufzumalen und zu überlegen,
wie dieses Potenzial sich verheilt und was wir eigentlich erreichen müssen
Ich habe das versucht, mal hier zu skizzieren. Hier unten,
das ist sozusagen, ja, wenn sie so wollen,
Array-Positionen, also das ist die Größe des Aris oder der Speicher.
Und hier sehen sie das Potenzial, ja. Und wir gehen jetzt
mal davon aus, dass wir aktuell eine Array-Größe von 16 haben.
Das sind hier 16 Kässchen, also bis hier,
da steht eine Eins, das hängt mit dem Alpha zusammen,
weil sie unten die grünen Zahlen zeigen, das Alpha.
Also hier hätten wir dann eins, dass Arrays,
also 16 Elemente groß. Und je nachdem,
wie viele Elemente des Aris wir verwenden,
haben wir in unterschiedlichen Belegungsfaktoren
Die ersten vier Elemente verwenden, hätten wir also einen Belegungsfaktor, ein Viertel.
Dann hier, wenn wir die ersten acht haben, haben wir einen Belegungsfaktor von eineinhalb.
Und hier nähern wir uns dann den Belegungsfaktor 1. Wir wissen,
dass mit jeder Operation, die wir ausführen,
uns in diesem Array, also bezüglich dieses Füllstandes,
immer nur um ein Feld bewegen.
Also wenn wir eine Insert-Operation aufrufen, dann fügen wir ein Element hinzu.
Und dann geht die nächste Zelle des Arrays hier belegt.
Wenn wir eine Dilite-Operation ausführen, dann wird eine Zelle hier
entsprechend frei
Das ist jetzt natürlich nur bildlich gesprochen. Die Anwendung kann natürlich
die Array-Position völlig willkürliche belegen. Es geht hier nur darum
zu zeigen, wie viele Array-Elemente benutzt sind oder nicht.
So, im Idealzustand befinden wir uns irgendwo bei eineinhalb,
weil wenn wir bei eineinhalb sind, sind wir relativ weit entfernt von der Situation,
dass wir unser Array kopieren müssen, weil es voll ist.
Und wir sind auch noch relativ weit davon entfernt,
dass wir das Array kopieren müssen, weil wir den Array-Bereich, den Speicherbereich verkleinern wollen.
Also bei eineinhalb könnten wir sagen, gut,
Wir brauchen eigentlich keine Zeit auf unserem Konto, ja,
auf unserem Zeitkonto, in unserem Potenzial, weil diese sehr teuren Operationen,
die finden ja bei einem Viertel und bei allen statt und die sind noch relativ weit entfernt.
Also es finden noch viele Operationen statt, bevor wir mal hier angekommen sind. Das Zweite,
was man sich überlegt, ist, dass die Expansion,
also wenn wir sozusagen immer mehr einfügen und dann müssen wir das Array verdoppeln,
die ist tendenziell teurer, weil wir sehr viele Elemente des Arrays dann kopieren müssen.
Wenn wir die Kontraktion haben, also da sind wir bei ein Viertel,
dann müssen wir nur dieses Viertel der Elemente kopieren.
Also wir überlegen uns einfach mal, wenn wir davon ausgehen,
dass wir auch diese Operation in konstanter Zeit durchführen wollen,
wie viel Zeiteinheiten müssen wir uns eigentlich ansparen, damit wir uns das leisten können.
Und dann können wir sagen, ja, das ist eigentlich ganz klar,
weil wenn wir hier bei einem Viertel sind,
dann müssten wir noch bei den 16er Array hier,
also vier Datenelemente kopieren und dann brauchen wir also hier vier Zeiteinheiten,
um dieses Kopier noch durchführen zu können,
ohne quasi extra Zeit dafür in Anspruch nehmen zu müssen,
also um weitere, um die,
da möglicherweise noch die konstante Laufzeit realisieren können.
Und am oberen Ende hier, da ist das dann ähnlich,
da haben wir dann, also das ist Array voll,
16 Elemente haben wir gespeichert und wir werden 16 Zeiteinheiten brauchen,
um die dann zu kopieren.
So, damit haben wir eigentlich drei Punkte in der Potenzialfunktion festgelegt. Wir haben entschieden,
dass wenn wir eine Array-Belegung von einem Viertel haben,
dann brauchen wir ein Potenzial von vier,
also genau genommen viermal die Größe des Arrays,
das ist ja hier nur ein Beispiel,
wenn wir die Belege im Belegungsfaktor von einem von eins haben,
dann brauchen wir ein Potenzial in der Höhe von
der Größe des Arrays
Ja, und wir wollen natürlich, wir müssen natürlich dafür sorgen,
dass das Potenzial nicht unnötig hoch ist und wir sagen dann,
ja, irgendwo in der Mitte,
wenn wir relativ weit entfernt sind von diesen Positionen,
brauchen wir eigentlich gar kein Potenzial,
weil da haben wir relativ viele Operationen, um das Potenzial noch aufzubauen.
Dabei entsteht jetzt dieser Knick hier und das,
was Sie oben gesehen haben, als Funktion ist nichts weiter,
als diese zwei linearen Funktionen,
um das Potenzial hier für die beiden Fälle,
dass das Alpha kleiner ist als ein halb oder größer
als ein halb zu beschreiben.
Das ist das
Okay, so, jetzt könnte man sich natürlich noch fragen,
ja, okay, aber warum muss das hier bei eineinhalb sein?
Es könnte ja zum Beispiel auch weiter hier nach rechts gehen oder weiter nach links.
Der Grund, warum man die Situation ist,
dass das Potenzial ein Null ist, bei ein Halb liegt,
ist ein ganz einfacher.
Wenn wir nämlich jetzt einen Schritt weiter denken,
wir überlegen uns, wir führen diese Expansion durch oder die Kontraktion,
dann ist danach ja das Potenzial aufgebraucht.
Wir haben ja die Zeiteinheiten verwendet, um das Array zu kopieren.
Und danach ist der Alpha-Wert hier wieder ein Halb
Das heißt, wenn wir also hier beispielsweise zu einem Viertel kommen,
jetzt machen wir die Kontraktion,
dann ist unser Array dann nachher nur noch acht Elemente groß.
Ja, und wir haben unsere Zeit verbraucht.
Das heißt, dann wissen wir an dieser Position,
ist das Potenzial wieder null, ja? Okay, das heißt,
nach einer Kontraktion wäre dann an der Position acht das Array voll.
Wir haben ja nur noch acht Elemente. Hier wäre dann
der Bewegungsfaktor eins. Das vorherige Viertel wird so eineinhalb,
das heißt, hier haben wir den Bewegungsfaktor eineinhalb und ja,
die neue untere Grenze ist dann wieder ein Viertel,
das ist diese Position.
Ja, und jetzt können wir quasi nach dieser Kontraktion wieder Array-Elemente einfügen oder löschen
Und wenn wir wieder an die Eric-Grenzen stoßen,
führen wir wieder eine Expansion oder eine Kontraktion durch.
Okay, das ist quasi die Motivation dafür, dass man genau diese Potenzialfunktion verwendet.
Und das ist bezüglich der Analyse eigentlich der einzig kreative Schritt,
dass man sich überlegt, wie sollte diese Funktion aussehen,
damit sie optimal dieses Laufzeitverhalten zwischen den verschiedenen Funktionsaufrufen dann austarieren kann.
Gut, wenn wir die einmal gefunden haben,
müssen wir jetzt überprüfen,
dass diese Potenzialfunktion
das Laufzeitverhalten korrekt beschreibt
Und dazu machen wir jetzt Folgendes. Wir müssen erstmal zeigen,
dass das Potenzial immer größer gleich null ist.
Das ist wichtig, weil wenn das kleiner wäre als null,
dann hätten wir ja mehr Laufzeit, Real verbraucht,
als wir in der für die amortisierten Kosten veranschlagt hätten.
Und dann wäre die Worst Case-Analyse natürlich falsch und es wäre nicht mehr gegeben,
dass wir eine Arschschätzung kriegen für die Worst Case-Laufzeit.
Also das Potenzial sollte immer größer gleich null sein zu jedem Zeitpunkt.
Und das haben wir im Prinzip schon, kann man schon
an dem Bild hier sehen, dass das so ist.
Wir sehen ja hier die Potenzialfunktion, die haben sie ja aufgemalt,
Umsichtlich ist die immer größer, gleich null.
Und das können wir natürlich auch noch nachrechnen.
Wir müssen jetzt die zwei Fälle unterscheiden, einfach nur,
weil die Funktion diese Fallunterscheidung hat, das Alpha hier,
das ist entweder größer, gleich eineinhalb oder kleiner als einhalb.
Und Alpha ist ja nichts weiter als das Verhältnis von den
benutzten Zellen zur Größe des Airrace. Das heißt,
wenn der das Alpha größer gleich eineinhalb ist, heißt das,
dass T.N, also die Anzahl der belegten Zellen, größer,
gleich eineinhalb, malt.ges, also die Anzahl der zur Verfügung stehenden Zellen.
So, und dann können wir unsere Potenzialfunktion nehmen,
die ist für diesen Fall zweimal t.n-t.g.
Das können wir
hier einsetzen
Und ich habe jetzt einfach mal das T.N ersetzt können.
Das ist natürlich auch andersrum machen. Dann steht hier zweimal,
eineinhalb T.G.-T.G. Und das ist genau null. Ja? Das
heißt, das ist Potenzial ist immer größer gleich null.
Und der andere ist halt, den müssen wir auch anschauen.
Das heißt, wenn das Alpha-T kleiner ist als ein Halb,
da haben wir einen anderen Funktionsverlauf,
SST von in kleiner als ein Halb-T.G. Und ja,
das können wir dann auch wieder einsetzen.
Die Funktion für den Fall ist t.g halbe-T von n. Und
jetzt können wir für das
Ein halb t.g einsetzen. Setzen wir da ein,
kommt auch wieder null raus.
Und das ist genau das, was wir erwartet haben.
Das heißt, das Potenzial ist immer größer oder gleich null.
Das haben wir aber auch an dem Funktionsverlauf ja eigentlich schon gesehen.
Gut, was gilt zu Anfang? Zu Anfang gehen wir
davon aus, dass Array ist mehr oder weniger leer.
Und ja, unser Werte-T.G. und T.N sind null.
Also wir haben noch keinen Array verfügbar. Und die Belegung ist entsprechend,
also die Anzahl der benutzten Zellen ist natürlich dann auch null.
Und der Belegungsfaktor ist per Definition
dann eins
Das Potenzial wäre an dieser Stelle null. Das ist eine sehr artifizielle Anfangsbedingung,
also in keinem realen Programm würde man so beginnen. Also in realer,
in einer realen Anwendung fängt man mit einer schon sinnvollen Array-Größe an,
die irgendwie wahrscheinlich konservativ nach unten abschätzt, wie groß das Array werden muss.
Na gut, aber das ist jetzt sozusagen für die Analyse mal die Staatssituation.
So, jetzt müssen wir die Inside und die Lead-Operation analysieren.
Und das einzige, was wir jetzt noch machen müssen,
ist für diese Operation immer überlegen, Was sind in den verschiedenen Szenarien,
die real auftretenden Kosten, also wie viele Operationen brauche ich?
Das ist ja unterschiedlich, ob ich dann nur ein Element
einfüge oder ob ich eine Expansion oder eine Kontraktion durchfüllen muss.
Das müssen wir uns überlegen. Und dann müssen wir die
Potenzialfunktion einsetzen und müssen das Ganze nur ein bisschen zusammenfassen und auflösen.
Und dann kriegen wir als Resultat die amortisierten Kosten für die Operation heraus.
Okay, im Folgenden, damit das ein bisschen leichter aufzuschreiben ist,
benutze ich jetzt also folgende Variablen.
Wir haben ja quasi einen Zeitstrahl, also Tabelle,
die füllt sich ja über die Zeit oder lehrt sich auch wieder über die Zeit.
Das bezeichnen wir wieder mit I. Ja, also das I,
das läuft jetzt also von irgendwie eins bis immer so weiter,
ja, und beschreibt sozusagen die Anzahl der Operationen, die ausgeführt werden.
Und dann haben wir natürlich zu jedem Zeitpunkt eine Array-Größe.
Das ist das GI dann, das entspricht also T.G,
zu dem Zeitpunkt I. Und wir haben eine Anzahl von belegten Feldern,
also der genutzte Speicherbereich in dem Array.
Das ist dann das NI und das entspricht das T.N zum Zeitpunkt I. Ja,
und dann der Belegungsfaktor, den gibt es dann natürlich auch,
das ist das AlphaI,
das wäre dann nichts weiter als hier entsprechend wieder dass N durchgeht.
Annie durch G.I. und alle die Operationen gelten nach der,
diese Werte gelten nach der Eaten-Operation.
Und dann haben wir natürlich noch die realen Kosten,
die beschreiben wir mit C.I. und die amortisierten Kosten mit C.I.-Strich.
Okay, jetzt gucken wir uns die Operation an und überlegen uns immer,
wenn wir so eine Operation durchführen,
was passiert am Übergang vom Zeitpunkt I minus 1?
Ja, und jetzt kommt die IT-Operation,
das ist ein Insight oder ein Delete.
Und was passiert nun bis zum Zeitpunkt I?
Also wie verändert sich das Potenzial und was kostet uns dann
diese Operation
Gut, aufgrund dieser Fallunterscheidung in der Funktion und aufgrund der Tatsache,
dass wir im Algorithmus eine Fallunterscheidung haben,
nämlich ob wir Expansionen betreiben oder nicht,
müssen wir halt jetzt tatsächlich vier Fälle unterscheiden.
Und die geben wir jetzt einfach der Reihe nach mal durch.
Wir werden jetzt gleich sehen,
es ist eigentlich immer wieder das Gleiche, was hier passiert.
Die stehen jetzt hier der Vollständigkeit, halber mal alle aufgelistet.
Okay, der erste Fall ist unser Belegungsfaktor zum Zeitpunkt I.
Also wir gucken uns Inside-Operationen an. Das ist schon mal
das Erste. Jetzt gucken wir uns Inside-Operationen an.
Das bedeutet ja als erstes mal, Anzahl der Elemente,
die wir speichern in dem Array, sich um eins vergrößert hat.
Das heißt, NI ist NI minus eins plus eins.
Das ist ein Element in das Array hereingekommen. Okay,
erster Fall ist der Belegungsfaktor, ist kleiner als eineinhalb.
Ja, also der ist relativ niedrig. Wir haben also
viel Platz in dem Array. Und dann passiert Folgendes.
Wir brauchen auf keinen Fall eine Expansion. Das heißt,
die Array-Größe verändert sich nicht. Das GI ist das GI
minus eins. Und weil wir auch keine Expansion brauchen,
sind die Kosten für das Speichern auch konstant.
Und wir können für die realen Kosten eine Zeiteinheit hier einplanen
Gut, dann können wir die amortisierten Kosten ausrechnen. Wir wissen,
weil das der Belegungsfaktor hier kleiner ist als ein Halb,
welchen Teil der Potenzialfunktion wir verwenden müssen.
Und dann können wir hier alles einsetzen, also das CI ist eins,
die Potenzialfunktion zum Zeitpunkt I ist GI-halbe-N-N-I und die Potenzialfunktion zum
Zeitpunkt i minus eins ist der E-Mails 1, halbe-N-I-minus 1.
Um das jetzt zusammenzufassen, können wir, ja,
wir können das GI GI minus eins überführen oder andersrum,
die sind ja identisch, die Array-Größe hat sich nicht verändert.
Und wir kennen auch das Verhältnis von NI und NI minus eins
und können also zum Beispiel das NI minus eins durch NI ersetzen,
wenn wir denn so wollen.
Kann man sich auch andersrum machen, ist völlig egal,
weil das hebt sich dann nämlich alles auf.
Und was sie sehen, ist, sie haben dann hier eins plus,
ja, das hatten wir schon. Und der zweite Teil,
hier ersetzen wir das GI minus eins,
das Annie minus eins durch GI und das NI minus eins durch NI minus eins,
ja, aufgrund dieser Beziehung hier oben, wir haben ein Element eingefügt.
Okay, und wenn sie das jetzt alles zusammenfassen, dann kommt da null raus.
Das heißt, die amortisierten Kosten in diesem Fall sind null.
Also das heißt, sie haben zwar reale Kosten,
aber sie haben ja das Potenzial und sie würden in diesem Fall
die anfallenden Kosten der Operation komplett aus dem Potenzial bezahlen und könnten davon ausgehen,
dass sie amortisiert keine Kosten haben, um diese Operation durchzuführen.
So, der zweite Fall ist, wir sind irgendwie in dem Intervall
so rund um eineinhalb. Das bedeutet, Der Belegungsfaktor,
bevor wir das Element eingefügt haben, ist noch kleiner als einhalb,
aber wenn wir das Element eingefügt haben, haben wir eins mehr,
dann steigt ja der Belegungsfaktor, dann ist das größer gleich einhalb.
Wir sind also genau jetzt an dem Punkt unten in diesem Knick der Potenzialfunktion. Auch hier wissen wir wieder,
wir haben ein Element eingefügt, das heißt,
NI ist NI minus eins, plus eins und wir wissen auch,
die Array-Größe hat sich nicht verändert, das heißt,
GI ist gleich GI minus eins, wir brauchen keine Expansion,
die realen Kosten sind damit eine Kosteneinheit,
eine Zeiteinheit und auch das
können wir wieder einfach
alles einsetzen
Einzige Schwierigkeit hier ist,
dass wir zum Zeitpunkt I bereits den aufsteigenden Teil der Potenzialfunktion haben,
also den für den Fall größer gleich eineinhalb und für den
Zeitpunkt I minus eins haben wir noch den anderen Fall,
der Potenzialfunktion, ja, also diesen hier, das heißt,
wir müssen einmal für den Zeitpunkt E diesen Funktionsteil einsetzen für den Zeitpunkt I minus eins,
müssen wir diesen unteren Funktionsteil einsetzen.
Okay, das setzen wir da ein,
dann kommt der Reis eins plus zweimal inny-gi minus und
dann kommt dieses Potenzial zum Zeitpunkt I minus eins,
das ist GI minus eins, halbe, minus eins.
Und auch hier können wir jetzt wieder entweder das NI durch
NI-1 ersetzen oder das GI durch GI-minus eins oder andersrum.
Ich mache das jetzt so rum. Ich setze jetzt für
das NI-minus für das NI den Parameter any minus eins plus eins
ein, also so wie das hier steht.
Ja, und das GI entspricht dem GI-minus eins,
also G-minus einzeln schreiben. Und dann kriegen sie diese Formel,
das können sie jetzt zusammenfassen. Und dann steht da,
kommt da sowas raus wie drei plus dreimal in ihm
minus eins,
minus dreihalbe
Ja, das ist jetzt erst mal komisch, das ist ja jetzt noch nicht konstant,
das wollen wir eigentlich erreichen. Aber glücklicherweise wissen wir ja,
wie das Verhältnis zwischen N-I-minus 1 und G-I-minus 1 ist,
weil das ist ja gerade durch das Alpha beschrieben und das Alpha ist ja hier,
wir wissen,
wie groß das Alpha ist,
das ist ja kleiner als ein Halb,
das heißt N-I-minus 1 durch G-I-minus 1 ist kleiner als ein
Halb und wenn wir diese Ungleichung hier wieder einsetzen,
dann steht hier 3 plus 3 mal 1
halb G-I-minus 1 minus 3 halb G-I-minus 1
und das ist
Also das heißt, über den Belegungsfaktor kennen wir das Verhältnis zwischen
der tatsächlichen Belegung und der Array-Größe und können dann hier entsprechend
diese Parameter noch gegeneinander aufwiegen und kriegen dann auch wieder unsere konstante,
amortisierte Laufzeit.
Dritter Fall ist, wir sind jetzt in diesem aufsteigenden Ast der Potenzialfunktion,
unser Belegungsfaktor zum Zeitpunkt minus eins ist irgendwo zwischen ein halb und eins.
Er ist aber noch nicht eins. Also wir können noch was einfügen.
Ja, das ist ein ganz einfacher Fall. Ich glaube,
das muss ich jetzt noch gar nicht mehr Teile für Zeile durchgehen.
Also die Anzahl der Elemente erhöht sich um eins,
die Array-Größe bleibt gleich,
die realen Kosten sind eins
Und für die tatsächlichen,
also für die Potenzialfunktion müssen wir jeweils die Funktion,
den Funktionszeit 2 annie-g-i einsetzen,
beziehungsweise hier 2 n-i-minus 1 minus-g minus 1. Ja,
das können wir einfach wieder einsetzen, umformen und dann kommen Kosten 3 raus.
Das ist eigentlich ein ganz interessanter Fall, da kann man mal nochmal gucken,
wie das Ganze funktioniert. Was ja hier passiert ist,
also wir sind ungefähr hier, ja, auf diesen,
auf diesem Ast hier und wir sehen diese,
dieser Teil der Potenzialfunktion hat ja die Steigung 2,
ne,
also wenn wir weitere Elemente hinzufügen,
haben wir immer
die Steigung
So, wieso kommt da die Steigerung 2 eigentlich raus?
Also kann man das am Algorithmus eigentlich festmachen, dass da
zwei rauskommen muss? Das kann man relativ gut sehen,
weil wenn wir hier bei der Belegung von einem Halb sind,
haben wir kein Potenzial aufgebaut. Aber die Hälfte des Arrays
ist ja schon belegt. Wenn wir jetzt ein Element hinzufügen,
dann haben wir erstmal die Kosten, um das Element hinzuzufügen.
Das ist eine Zeiteinheit. Irgendwann müssen wir das Array expandieren,
wenn das immer so weitergeht und wir müssen dafür etwas Zeit ansparen.
Und um dieses Element, was wir gerade eingefügt haben,
dann nochmal kopieren zu können, müssen wir eine zweite Zeiteinheit
quasi in die Potenzialfunktion einzahlen, wenn sie so wollen.
Das sind zwei. So, und dann ist es aber so,
dass wenn wir an diesem Punkt angekommen ist,
ist ja die Hälfte des Array schon belegt.
Das heißt, das Element, für das wir hier einfügen,
zahlt noch eine Zeiteinheit ein und zwar für ein Element,
was vorher schon in der Tabelle drin stand.
Und deswegen kommt er drei raus. Also drei Zeiteinheiten zum Einfügen
. Also eine, um es tatsächlich einzufügen, eine,
um dieses Element später mal kopieren zu können und eine weitere,
um ein Element, was wir vorher irgendwann mal eingefügt haben,
auch mitkopieren zu können
Gut. Der letzte Fall ist jetzt der, wo wir mehr Zeit benötigen.
Das ist der Expansionsschritt. Jetzt ist es also so,
dass die Belegung vor dem Einfügen 1 war und jetzt kommt
das Element rein. Und wenn das Element jetzt hineinkommt,
passt es ja nicht mehr, dann wird das Array verdoppelt.
Das heißt, G-I wird zweimal G-I-minus 1. Array ist jetzt doppelt so groß.
Und die realen Kosten, die wir haben,
die entsprechen N-I, weil wir alle Elemente,
die wir haben, also das eine neue Element müssen wir einfügen.
Und vorher hatten wir N-minus 1 Elemente und die müssen wir
ja alle kopieren.
Macht also
reale Kosten
Gut, können wir auch einsetzen,
dann kriegen wir also hier die realen Kosten, sind NI,
Potenzialfunktion für diesen Fall, sind es jeweils zweimal n-g,
also dann zweimal annie-gi und zum Zeitpunkt minus eins,
zweimal in minus eins, minus Gi minus eins.
Und jetzt können wir wieder das NI durch das N-I minus eins ersetzen,
weil wir wissen, es ist ein Element dazu gekommen und
das GI können wir auch ersetzen.
Allerdings ist es jetzt doppelt so groß,
also das GI ist doppelt so groß, das GI minus eins.
Wir haben das Array ja vergrößert, also steht hier zweimal GI minus eins.
Und wir haben diese Formel hier. Wie können wir vereinfachen zu
dreimal 3 plus NE minus 1, minus GI minus eins.
Und da unser Belegungsfaktor zum Zeitpunkt i-1, Jahr 1 war,
das war der Expansionsschritt, wissen wir,
dass NE genauso groß war wie GI.
Das Array war voll. Ja,
und wenn Annie genauso groß ist wie GI,
können wir die beiden hier,
heben sich die beiden gegeneinander auf und wir haben wieder konstante Kosten.
Obwohl, wie gesagt, die realen Kosten dieser Operation
sehr viel höher waren
Ja, das war der Inside-Fall.
Jetzt muss man für den Delete-Fall das Gleiche nochmal machen.
Und die Schritte sind exakt die gleichen. Nur,
dass man jetzt natürlich auf diesen Fall zuarbeitest, die Kontraktion irgendwann mal kommt.
Sie sind also jetzt einfach nur anders angeordnet.
Erster Fall ist, ach so, erster Unterschied ist natürlich,
dass die Anzahl der Elemente um eins abnimmt.
Das heißt, NI ist gleich NI minus eins,
minus eins. Und dann haben wir drei Fälle,
wo das Array gleich groß bleibt, die wir unterscheiden müssen,
weil die Potenzialfunktion, wie gesagt, die verschiedenen Äste hat.
Und der erste Fall ist, dass das AlphaI größer gleich einhalb ist.
Wir sind also auf dem rechten, steigenden Ast.
Wir haben noch eine relativ volle Tabelle.
Der zweite Fall ist, dass wir irgendwo uns zwischen eineinhalb,
also dass wir uns genau in einem Knickpunkt befinden.
Das heißt, vorher ist das Potenz, ist die Belegung noch größer gleicheinhalb.
Und nachdem wir das Element entfernt haben, ist die Belegung
dann kleiner als einhalb. Der dritte Fall ist jetzt,
dass wir irgendwo zwischen eineinhalb und ein Viertel sind.
Also jetzt wird das Array immer leerer. Und der vierte
Fall ist dann, dass wir bei einem Viertel angekommen sind
und die Kontraktion haben.
So, okay
Hier sind, wie gesagt, die Herleitung.
Das funktioniert ganz genauso wie eben auch. Wir überlegen uns
, was passiert mit der Array-Größe? Die bleibt gleich.
Was sind die realen Kosten? Die sind in diesem Fall
hier konstant eine Zeiteinheit. Wir setzen das Potenzial,
die Potenzialfunktion ein, fassen zusammen und kriegen dann Kosten.
Hier kommen sogar interessanterweise amortisierte Kosten raus, die negativ sind.
Wir verschwenden sogar ein bisschen was.
Also wir geben ein bisschen was von unserem Potenzial her,
was wir eigentlich gar nicht brauchen, um diese Funktion auszuführen.
Auch das kann man nachvollziehen, dass das so ist,
weil, wenn wir mal nochmal gucken, Potenzialfunktionen. Wenn Sie sich vorstellen,
wir bewegen uns jetzt hier in diesem Bereich,
wo wir weder Expansion noch Kontraktionen machen,
dann betrachten wir ja den ungünstigsten Fall,
dass wir direkt von eineinhalb so viel, also von Belegungsfaktoren hat,
immer wieder einfügen, einfügen, einfügen,
bis das Array voll ist.
Das wird natürlich in der Realität wahrscheinlich nicht so sein.
Also wir werden einfügen und löschen und dann pendeln wir hier
so ein bisschen immer hin und her.
Und gegebenenfalls stoßen wir hier an den Rand und müssen dann das Array vergrößern.
Und dieses Hin und Her pendeln, das sorgt dafür,
dass wir immer Potenzial aufbauen, was wir aber streng genommen ja gar nicht brauchen.
Also wenn ich hier dreimal was einfüge und es dreimal wieder lösche und danach wieder dreimal einfüge,
habe ich die ganze Zeit immer Zeiteinheiten zur Seite gelegt,
weil es könnte ja sein, dass ich kopieren muss,
ohne dass ich sie real brauche.
Und das sieht man da in der Analyse,
dadurch werden die realen Kosten tatsächlich negativ an dieser einen Stelle mal.
Die Entschuldigung, die amortisierten Kosten werden negativ an dieser einen Stelle hier.
Okay, gut, ja, der zweite Fall,
ich begehe da, glaube ich, nicht mehr durch.
Sie setzen die Potenzialfunktion ein, lösen das auf,
benutzen hier wieder den Belegungsfaktor und kriegen dann entsprechend raus,
dass da Konstante rauskommt
Der dritte Fall. Genauso, der vierte Fall ist jetzt
der mit der Kontraktion. Bei der Kontraktion ist die Situation
jetzt die, dass sich die Eric-Größe verändert.
Die Eric-Größe halbiert sich. Das GI ist dann GI-minus eins,
halbe. Und dass die Kosten, die wir jetzt haben,
sind NI plus eins. Also wir müssen alle Elemente,
die wir vorher hatten, entsprechend kopieren. Nein,
wir müssen alle Elemente, die wir zum Zeitpunkt I haben,
kopieren und dann müssen wir das eine Element ja auch noch löschen.
Deswegen haben wir NI plus eins reale Kosten.
Und ja, auch das können wir
wieder einsetzen
Zusammenfassen. Am Ende haben wir sowas stehen wie eins plus NI minus eins minus geimius
eins Viertel und dann können wir wieder den Belegungsfaktor nutzen,
von dem wir wissen,
dass er zum Zeitpunkt ihm minus eins gleich ein Viertel war,
um das Annie und das GI gegeneinander aufzuwiegen und dann kommt da gerade eins raus.
Ja, okay und damit haben wir die amortisierten Kosten berechnet und wissen,
dass die tatsächlich für beide Operationen in allen Fällen,
die auftreten können, konstant sind.
Schauen Sie nochmal auf diese Stelle hier,
also für die Kontraktion haben wir angenommen,
Dass die realen Kosten NI plus 1 ist.
Das bedeutet, dass Array ist ja jetzt nur noch ein Viertel gefüllt.
Und diese Daten müssen wir kopieren und die Kosten dafür haben wir
abgeschätzt mit der Anzahl der Datenobjekte, die wir kopieren müssen.
Stellen Sie sich mal die Situation vor,
wir benutzen so eine Tabelle für eine Hash-Funktion.
Also für ein Hashing. Wir haben mal so ein Hashing mit
offener Adressierung zum Beispiel. Da brauchen wir so ein Array
und dann rechnen wir heute im Index immer eine Position aus,
wo wir ein Objekt speichern
Wenn wir in diesem Fall das Array verkleinern würden,
dann brauchen wir eine neue Hash-Funktion.
Denn die neue Hash-Funktion bildet ja jetzt in das kleinere Array ab.
Das bedeutet, alle Elemente, die wir noch gespeichert haben,
müssen wir über die Hash-Funktion einer neuen Position dem Array zuordnen.
Das ist noch nicht so schlimm, was aber schlimmer ist,
ist, dass wir zu dem Zeitpunkt nicht wissen,
wo in unserem Array die Elemente stehen, die noch gespeichert sind.
Wir wissen also nicht, welche Array-Positionen sind belegt und welche sind frei. Also,
wir wissen das natürlich, wenn wir die Array-Position anschauen,
aber wir wissen es nicht in der Form,
dass wir einfach die belegten Positionen aufzählen können
Um die zu finden, müssen wir jetzt eigentlich durch das Array gehen.
Wir müssen also alle Array-Positionen durchgehen und sagen,
steht denn da jetzt ein Element drin oder nicht?
Wenn wir das machen, dann sind die realen Kosten hier nicht NI,
sondern dann sind die GI. Also die Größe des Arrays,
weil wir jede Position ja angucken müssen. Und wenn da GI steht,
wenn wir uns das mal vorstellen,
dann würde ja dieses NI minus eins,
das würde ja jetzt zu einem GI, dann sehen wir,
dass die Analyse hier nicht hinhaut,
dann würde hier nämlich so was stehen wie eins plus drei Viertel GI.
Und das ist die Array-Größe
Wenn die Kosten so sind, dann würden wir,
zumindest mit dieser Potenzialfunktion, keine konstante Laufzeit erhalten.
Das heißt, diese Potenzialfunktion setzt voraus,
dass wir wirklich auf die belegten Zellen zugreifen können,
und zwar proportional zur Anzahl der belegten Zellen.
Nicht zur Größe des ARAs. Und das sieht man genau an dieser Stelle,
da haben wir das benutzt, diese Eigenschaft,
die in der Praxis eben manchmal nicht gegeben ist.
Ja, okay. So, das war die amortisierte Analyse.
Drei Techniken gesehen und wir haben drei Beispiele gesehen,
wie man das einsetzen kann
Und daraus kann man erst mal was für die Praxis lernen.
Das bezieht sich insbesondere auf das letzte Beispiel,
weil diese dynamischen Tabellen oder Airrays werden viel verwendet,
auch wenn sie sie nicht mehr explizit programmieren,
wenn sie jetzt irgendwie Software schreiben, aber sie verwenden sie in den Datenstrukturklassen,
ja, in Java und C Plus Plus etc.
Und das Erste, was man da sieht, ist,
dass wir das Speichermanagement für solche dynamischen Arrays bei einer
beliebigen Sequenz von N-Operation in amortisierter Zeit O von N,
Gesamtzeit,
also konstante Zeit für Inside und Delete
amortisiert durchführen können
Und dabei erreichen wir einen Belebungsfaktor,
der zu jedem Zeitpunkt größer ist als ein Viertel des Speicherbedarfs.
Das ist erst mal gut, weil wir viele Algorithmen haben,
wo wir quasi ein Array voraussetzen. Also wem sie so darstellen,
wenn sie einen Hieb zum Beispiel nehmen und über den Hieb realisieren sie eine Prioritätswarteschlange,
da rechnen sie die ganze Zeit mit den Index-Positionen,
da brauchen sie einen Array.
Und wenn sie vorher nicht wissen, wie viele Elemente
da drin sind, dann haben sie ein Problem.
Dann müssen sie ja das Array vergrößern und verkleinern. Und in dem Augenblick sehen sie,
dass es dann kein Problem war mit dieser Analyse,
haben wir gezeigt, dass uns das amortisiert,
keine zusätzlichen Kosten keine zusätzlichen Kosten erzeugt.
Bei dem Instagram-Telete haben wir ein paar Annahmen gemacht,
und zwar das asymptotische Laufzeitverhalten von Inside Intellied, ist erstmal gleich.
Also es kostet gleich viel Zeit, ein Element einzufügen und zu löschen.
Warum ist das wichtig? Das ist deswegen wichtig,
weil, oder an welcher Stelle haben wir das verwendet,
diese Eigenschaft, die haben wir verwendet,
weil wir haben ja nur eine Potenzialfunktion, wo wir unser Guthaben speichern.
Und sowohl die Inside-Operation zahlt auf diese Funktion, zahlt ein,
als auch die Lied, Und zwar immer mit einer Zeiteinheit.
Wenn das eine jetzt viel länger dauern würde als das andere,
dann könnten wir die Laufzeit von einem Insert,
nicht durch ein Delite kompensieren oder andersherum.
Das heißt, wenn sie eine Asymmetrie haben,
dann muss man nochmal in die Analyse schauen.
Das heißt nicht, dass es da nicht geht,
aber dann muss man in die Analyse schauen.
Okay, und dann der zweite Aspekt war,
der Zugriff auf eine belegte Felder eines Areas,
der größte G ist in O von Endzeit möglich.
Und das ist eine relativ starke Einschränkung,
wo man auf der Datenstrukturseite eine Menge machen muss und das
in allen Fällen auch wirklich zu realisieren
Gut, nochmal zur amortisierten Analyse. Drei Techniken, Aggregatanalyse haben wir gesehen,
Account-Methode, Potenzialmethode. Die Aggregatanalyse ist einfach das schnelle Argument,
was wir in allen einfachen Fällen einfach so einsetzen,
wo wir einfach sagen, ja,
also in dem Fall können wir sofort sehen,
die Endoperation werden garantiert ausgeführt und im Mittel wird das so und so lange dauern.
Und sie haben, das werden wir gleich sogar noch sehen,
sie haben die, sie haben die auch schon benutzt,
ja, bei der einfachen Analyse von Algorithmen,
die sie so im Grundstudium, also in der Datenstrukturvorlesungen gesehen haben.
Ihnen war das vielleicht nicht so klar,
aber sie haben das da schon, das Argument schon benutzt.
Dann haben wir die Account-Methode genommen, wo wir gesagt haben,
wir weisen den Objektenkosten zu und konnten, um die Kosten zu verwalten.
Das ist ganz intuitiv, damit kann man gut arbeiten.
Großer Nachteil ist, dass sie nicht zwischen verschiedenen Operationen kompensieren können.
Also sie können nicht sagen, dass die eine Operation oder zwischen verschiedenen Objekten,
muss man sagen, kompensieren können, also zwischen Operationen schon,
aber nicht zwischen Objekten. Das ist so ein bisschen dadurch
ein bisschen sperrig und die bessere Methode ist eigentlich die Potenzialmethode,
wo sie sich einmal überlegen, wollen sie Laufzeit quasi ansparen,
um an anderen Stellen wieder zu kompensieren.
Sie geben eine Funktion dafür an und die verwendet man.
Und dann kann man ganz gleich zeigen, dass ein spezielles,
amortisiertes Laufzeitverhalten resultiert. Gut, okay,
amortisierte Analyse werden wir an vielen Stellen auch noch wieder sehen in der Vorlesung,
wenn es dann immer in der einen oder anderen Variante verwenden.
Okay, so, das war das und jetzt steigen wir
in die Grafalgorithmen ein
Okay. So, das ist der Aufbau des Kapitels.
Das ist alles, was wir machen werden in den nächsten Wochen.
Ich erzähle Ihnen gleich ein bisschen was zu grafen.
Da können Sie sich relaxed zurücklehnen. Dann fangen wir an
mit der tiefen Suche. Die kennen Sie vermutlich auch alle
. Wir werden uns hier Algorithmen anschauen,
gerade im Kontext von Zusammenhang,
also Zusammenhangsberechnung
Der einfache Zusammenhang in Grafen, das Ganze einfach zu berechnen,
in der tiefen Suche. Das ist easy,
aber man kann mit der Tiefensuche eben auch den Beat-Zusammenhang berechnen,
also den zweifachen Zusammenhang. Das werden wir uns genau anschauen,
weil das ein Problem war, wo man sehr lange nicht wusste,
wie man das lösen kann und man echt erstaunt war,
dass es jemand dann mit einer Tiefensuche hingekriegt hat.
Und da werden wir uns den Algorithmus genau anschauen.
Das ist also eine Anwendung der Tiefensuche. Auch den starken
Zusammenhang werden wir uns nochmal angucken. Da ist ein Algorithmus
typischerweise auch schon Algorithmen und Datenstrukturen da. Das ist,
vielleicht erinnert sich der eine oder andere, der Algorithmus,
wo man in der Mitte einfach mal alle gerichteten Kanten umdrehen
muss und lässt dann zweimal in der tiefen Suche laufen.
Der ist so ein bisschen unschön, der Algorithmus,
weil sie den ganzen Graf verändern müssen.
Und wenn sie jetzt einen Zusammenhang berechnen wollen,
also einen starken Zusammenhang auf einem implizit gegebenen Grafen,
zum Beispiel nehmen sie die Querverweise in Wikipedia-Dokumenten.
Ja, da ist so eine Operation, drehen sie mal alle Kanten um.
Ein echtes Problem, ja, wie wollen sie das machen?
Ja, das sind ja alles irgendwie Links,
die müssen sie ja
dann rückverfolgen
Ich könnte es noch nicht mal im Speicher halten,
also zumindest nicht mal so eben. Deswegen werden wir
da noch einen anderen Algorithmus sehen, der das vermeidet.
Okay, das wären so die etwas komplexeren Algorithmen auf der Tiefensuche sein.
Dann gucken wir uns noch mal kurz Breitensuche an und den deichsten Algorithmus,
dann machen wir eigentlich, das machen wir ganz schnell.
Also da, was wir da tun werden, ist,
wir werden noch so einen Schritt in die Verallgemeinerung gehen,
wo wir Wegeprobleme verallgemeinern und dabei dann so Dinge berechnen,
wie so einen transitiven Abschluss in den Grafen
Etc. und werden das noch anschauen. Gut, dann fangen wir an mit Flüssen.
Ja, wir werden maximale Flüsse berechnen und minimale Schnitte,
das gehört sehr eng zusammen, diese beiden Themen. Themen,
maximale Flüsse sind etwas, eine ganz wichtige Klasse von Graph-Algorithmen,
die auch unheimlich viele Anwendungen haben, ja, wenn sie also,
ja, Warenflüsse berechnen, wenn sie optimieren wollen,
wie Logistik funktioniert in einem Netzwerk von verteilten Zentren.
Die Bierinformatiker brauchen Flüsse, wenn sie metabbolische Netze analysieren.
Also es gibt ganz viele Anwendungen,
wo man sowas macht und parallel dazu eben die Schnitte,
also zu versuchen, einen Graf zu teilen in zwei Teile,
wo sie möglichst wenig Kanten durchschneiden müssen.
Dafür gibt es auch ganz viele Anwendungen. Also zum Beispiel in der Elektrotechnik,
wenn sie Chips designen und müssen dann ihre Komponenten auf verschiedene Baugruppen aufteilen und sagen,
naja, idealerweise sollte man möglichst wenig Leitungen haben,
die von dem einen zu dem anderen Teil laufen.
Solche Fragestellungen, da braucht man Schnitte. Das ist dann
das vierte Kapitel und das fünfte Kapitel sind Matching-Verfahren,
Matching-Verfahren kann man grob unterscheiden,
in B-Partitis Matching
Bibatitis Matching ist das klassische Problem, wo sie zwei Gruppen haben.
Und sie müssen alle Objekte der ersten Gruppe auf ein Objekt
der zweiten Gruppe zuordnen oder zuweisen, unter Berücksichtigung von Kosten.
Das ist ein V-Partietes Matching-Problem. Und dann gibt es das allgemeine Matching-Problem,
wo sie einen beliebigen Graphen haben und in diesem Graphen Paare von Knoten bilden wollen,
so dass jeder Knoten sozusagen einen entsprechenden Partner hat,
wo der also die drunterliegende Struktur nicht bipativ ist,
sondern ein normaler Gras
Ja, das werden wir auf der Seite der Graf-Algorithmen so machen in den nächsten Wochen.
Und wir beginnen mit dem ganz sanften Einstieg und wir wiederholen einfach mal die wesentlichsten Begriffe.
Ja? Gut, also Grafen haben immer Knoten und Kanten,
Knoten sind halt die Objekte, die über die Kanten miteinander verbunden sind.
Wir werden mit N immer die Anzahl der Knoten und mit M
immer die Anzahl der Erkannten bezeichnen. Wir haben ungerichtete und gerichtete Grafen,
ungerichtete Grafen haben halt zwei Elementegemengen als Kanten und gerichtete Grafen haben Tupel,
also mit einem XM Y, als Kanten,
ein Tupel hat ja eine explizite Position,
also das ist ein erstes,
es gibt dann eher so ein zweites Element und dadurch hat eine Kante eine Richtung,
die geht dann von Knoten X zu Knoten.
Y, wir gehen davon aus, dass ungerichtete Graphen nie Schleifen haben,
die Elemente, diese Teilmengen sind immer zwei Elemente,
die gerichteten, aber durchaus Schleifen haben können.
Gut, von der Nomenklatur her muss man nochmal sagen,
eigentlich sind das hier Mengen und man müsste so eine Schreibweise machen,
wie diese hier, dass man also geschweifte Klammern als Mengenklammern verwenden.
Im Kormen werden sie das nicht finden,
also im Kormen werden auch für ungerichtete Grafen immer runde Klammern verwendet,
um die Kanten zu beschreiben.
Das ist einfach eine Vereinfachung. Man hat dann einfach
eine einheitliche Norminklatur für Kanten und man sagt,
vorher einfach mal, ob der Graf gerichtet oder umgerichtet ist.
Ja, das ist der Hintergrund. Also diese geschweiften Klammern,
die sehen sie hier, formal sind das eben zwei elementige Mengen,
aber die Norminklatur im Folgenden wird dann so sein,
dass da einfach mal runde Klammern stehen.
Gut, es gibt sogenannte Multigrafen und Hypergrafen.
Beide werden in dieser Vorlesung keine Rolle spielen,
aber in der Praxis gibt es die halt an vielen Stellen,
ein Multigraf ist ein ungerichteter Graf mit multiplen Kanten.
Das heißt, es kann mehrere Kanten geben, die zwischen zwei Knoten verlaufen.
Der hat dann auch Schleifen, so ein Multigraph.
Und ein Hypergraph verallgemeinert das Graph-Konzept insoweit,
dass eine Kante nicht nur zwei Knoten miteinander verbinden kann,
sondern eine beliebige Teilmenge vom Knoten.
Diese beiden Konzepte sind in der häufig wichtig,
weil man Anwendung so modelliert. Allerdings, ja,
es gibt da natürlich auch spezielle Graph-Algorithmik dann für diese Fälle.
Die werden wir einfach hier aus Zeitgründen gar nicht betrachten können.
Wir gehen einfach von normalen Graphen aus, aber sie sollten wissen,
die gibt es und die sollten mal wissen,
wie die heißen,
wenn sie selber mal danach suchen
Dann gibt's den Begriff der Addizenz und Inzidenz,
also zwei Knoten sind Adjazent oder benachbart,
wenn es eine Kante zwischen den beiden gibt und sie sind inzident,
wenn, also eine Kante ist inzident, mit einem Knoten,
wenn dieser Knoten an diese Kanteanschlüsse oder die Kante an den Knoten anstößt.
Bei gerichteten Grafen gibt's ja jetzt zwei Fälle,
also entweder der Knoten ist am Anfang der Kante,
da wo die losgeht,
also am Pfeilanfang oder an der Pfeilspitze und dann ist die Kante inzident von V,
ja, wenn dort die Kante losgeht oder sie ist inzident nach V,
wenn die Kante dann dahin zeigt.
Ja, das ist die Nominklatur. Das sind Beispiele für Grafen
. Der Grad eines Knotens ist die Anzahl der Kanten,
die Inzidenz sind zu diesem Knoten.
Im gerichteten Fall müssen wir wieder den Eingangsgrad und den Ausgangsgrad
unterscheiden. Also wir zählen einmal die Anfall der Pfeile,
der Pfeilanfänge und der Pfeilspitzen, wenn sie so wollen.
Ja, der Indigree beschreibt also die Anzahl der Kanten für
einen Knoten V, der der Art XV.
Ja, also in für die Knoten,
die Kanten zeigen in den Knoten hinein und der Outingree bezeichnet,
die Anzahl der Kanten, die diesen Knoten entsprechend verlassen.
Der Knotengrad ist dann für einen gerichteten Fall Indigree plus Outigreen.
Pfade sind Listen von Knoten mit der Eigenschaft,
dass je zwei benachbarte Knoten in dieser Liste durch eine Kante verbunden sind.
Wir sagen, dass der Pfad einfach ist,
wenn kein Knoten doppelt vorkommt, jede Teilmenge hiervon bezeichnen wir als einen Teilpfad.
Wenn der erste und der letzte Knoten identisch ist,
reden wir von einem Kreis- oder Zyklus
Und wenn ein Graf keine Kreise hat oder es
keine Zyklen hat, dann bezeichnen wir ihn als ad-zyklisch.
Ich möchte Sie motivieren dazu, möglichst den Begriff des Zyklus zu verwenden.
Ich sage ab und zu auch immer Kreis, das rutscht
mir dann immer so raus. Meiner Meinung nach ist,
also das, was wirklich gemeint ist,
ist ein Zyklus, also Zyklus bedeutet ja,
man geht von einem Zustand in den nächsten,
in den nächsten, in den nächsten und kommt wieder an einem festen vorherigen Zustand an.
Das ist ein Zyklus. Ein Kreis ist eigentlich
etwas Geometrisches. Und aus irgendeinem Grund, Hypothese ist,
dass es im Englischen einfach diese extreme Nähe zwischen Circle und Cycle gibt,
spricht man auch in vielen Büchern oder auch,
man sieht es immer wieder, dass auch von Kreisen gesprochen wird.
Aber eigentlich hat das ja nichts mit einem geometrischen Kreis zu tun.
Deswegen lieber Zyklus. Gut, ja, vielleicht noch,
also zu Faden sagt man ab und zu auch Wege,
dass es auch dann wird auch relativ sinn und verwendet.
Erreichbarkeit, wir sagen, V ist erreichbar von W,
genau dann, wenn es einen Pfad gibt von W nach V
und in einem ungerichteten Grafen sprechen wir von Verbundenheit oder im Englischen connected.
Genau dann, wenn für alle Knoten VW ein Pfad existiert,
der von V nach W führt. Im gerichteten Grafen verwenden wir den Begriff verbunden nicht,
sofort, sondern wir haben den Begriff der starken Verbundenheit und meinen damit,
dass für alle Knotenpaare VW ein Pfad von V nach W existiert,
der Vollständigkeit halber steht hier auch und damit auch von W nach V,
weil es ist eigentlich, fände ich, die Definition schöner, wenn es explizit dabei steht.
Es ist nur so, es ist dann doppelt gemoppelt,
weil sie hier alle beliebigen Kombinationen von Knoten nehmen.
Also es reicht eigentlich zu verlangen für alle Knotenpaare,
existiert ein Pfad von V nach W fürs bessere Verständnis,
heißt es nur für alle Knoten V nach
Knotenpaare V und W, gibt es einen Pfad von V
nach W und zurück von W nach V. Frage.
Das ist das gleiche wie die Stadt von Stoffe zusammen dann?
Das war gerade mit dem,
was ist... Ist das das Gleiche wie Sprachmannschaften zusammen?
Genau, also das ist erstmal die Definition für Knotenpaare.
Ja, also hier gucken wir uns zwei Paare an,
ein paar an. Das ist die Erreichbarkeit.
Und diese Verbundenheit ist tatsächlich genau das Gleiche wie Zusammenhang.
Das kommt, glaube ich, auch auf der Zusammenhang.
Okay, gut
Also Subgrafen, wir reden von Subgrafen,
wenn wir einen Grafen, V-Strich-E-Strich haben, der,
wo die Knoten und die kannten jeweils eine Teilmenge von V und E sind.
Und die wichtigste Art von Subgrafen, die meistens vorkommen,
sind sogenannte induzierte Subgrafen, induzierte Subgrafen,
da beschreibt man nur die Knotenmenge, V-Strich,
also man wählt eine Menge von Knoten aus.
Und die Kanten sind implizit dadurch beschrieben,
dass man sagt, alle kannten zwischen diesen ausgewählten Knoten,
die gehören jetzt zu dem Graf dazu.
Also E-Strich ist dann die Menge allerkannten UV aus dem ursprünglichen Grafen,
wo U und V zu dem Knotenmenge V-Strich gehören.
Das sind induzierte Subgrafen
Jetzt kommt der Zusammenhang. Also die, wie gesagt,
das ist das eh die gleiche wie Verbundenheit.
Zusammenhang. Wir reden von einer Zusammenhangskomponente oder einer Connected Component
. Das ist der maximal zusammenhängende Subkraft eines ungerichteten Grafen.
Also sie nehmen alle Knoten zusammen,
die sie paarweise jeweils durch Pfade erreichen oder miteinander verbinden können.
Und bei gerichteten Grafen gibt es in Analogie dazu den starken Zusammenhang,
die Strongly Connected Component. Die beschreibt die Menge aller Knoten von,
sodass sie für jedes Paar ein Pfad von U nach V und von V nach U haben. Nochmal,
habe ich das jetzt richtig verstanden, dass sie also nie von schwachem Zusammenhang sprechen,
weil es gibt ja Bücher, die sagen,
das sind Gerichte der Grafen, dann schwach zusammenhängt,
ist, wenn der Graf Geer spricht, den ich erhalte,
wenn ich ihn quasi ungerichtet mache, dass wenn der zusammenhängt,
ist, dann ist der richtige... Genau.
Stimmt, das ist, den Begriff gibt es,
dann redet man von schwachem Zusammenhang, ja,
das ist wahr, das hätte ich hier noch mit auflisten können,
wir werden das im Folgenden einfach nicht brauchen,
aber das, was wichtig ist, ist,
dass man eben beim gerichteten Grafen nicht einfach nur Zusammenhang sagt.
Also, weil dann ist nicht klar, was gemeint ist.
Also, wenn man die gerichteten Grafen hat, ist es
entweder starker oder schwacher Zusammenhang, dann muss man das unterscheiden.
Gut, dann fragt man sich natürlich bei Grafenbann sind Grafen
gleich und dafür hat man den Isomorphie-Begriff. Das heißt,
wir betrachten Grafen dann als gleich,
wenn wir eine bijektive Abbildung zwischen den Knoten haben, sodass immer dann,
wenn in dem einen Grafen eine Kante zwischen den beiden Knoten ist,
auch eine Kante zwischen den Bildern in dem anderen Grafen ist
Das ist ganz wichtig, hier,
dass die Gleichheit von Graphen nichts damit zu tun hat,
wie die Knoten heißen oder in welcher Reihenfolge sie irgendwie angegeben sind,
sondern es geht immer nur darum,
ob die Topologie gleich ist und um die Topologie zu vergleichen,
brauche ich so eine Funktion, die mir erstmal sagt,
welcher Knoten soll denn mit welchem übereinstimmen.
Das macht also diese Abbildung F und darüber
kann ich dann prüfen, ob zwei Graphen gleich sind.
Und hier sind zwei Beispiele. Es gibt natürlich ein paar Kriterien,
mit denen man ganz schnell feststellen kann,
dass Graphen nicht Isomorph sind oder gleich sind
Das erste Kriterium ist die Anzahl der Knoten, ist unterschiedlich.
Das zweite, was man dann prüft, ist,
sie gucken sich an, welche Knoten haben welchen Knotengrad.
Und offensichtlich müssen die Knoten gerade ja übereinstimmen in der Abbildung.
Und das ist das zweite Kriterium, mit dem sie ganz schnell
feststellen können, dass zwei Grafen potenziell nicht isomorph sind.
Wenn die beiden Kriterien erfüllt sind,
also die Anzahl der Knoten ist gleich,
und die Liste der Knoten gerade, die auftreten,
ist auch gleich, dann wird es schwierig.
Da müssen sie tatsächlich diese bieaktive Abbildung finden.
Und die Erkennung der Isomorphie zwischen Grafen ist ein schwieriges Problem,
wo sie einen hohen Laufzeitbedarf haben.
Man weiß nicht ganz genau, wie komplex die Fragestellung ist.
Es ist irgendwie, es geistert so in der Welt zwischen
P und NP, aber es ist erstmal ein aufwendiges Problem.
Ja, hier sind zwei Beispiele. A ist ein Beispiel,
wo sie Isomorphie haben und B,
wo sie keine Isomorphie haben und das können sie auch relativ schnell sicherlich nachvollziehen,
indem sie sich überlegen, was hier auch was abgebildet werden musste,
dass immer, wenn hier eine Kante ist,
hier auch eine Kante ist
Was sie auch an dem Bild hier sehen, ist,
dass Grafen natürlich dargestellt werden. Wir malen Grafen gerne.
Ja, meistens haben wir eine bildliche Vorstellung von einem Grafen.
Aber dass diese bildliche Darstellung wirklich eine andere Ebene, Betrachtungsebene ist.
Und die Art und Weise, wie wir Grafen darstellen,
zum Beispiel nichts damit zu tun hat, ob sie Isomorph sind oder nicht.
Und die allermeisten Graf-Eigenschaften, die wir betrachten,
sind komplett unabhängig von der Art und Weise, wie wir sie darstellen.
Ja, es gibt viele Grafen, die einen Namen haben.
Und hier sind einfach nur mal die absolut Zeiten, zentralsten,
wichtigsten Begriffe für Grafen, Bäume sind ungerichtete, zusammenhängende Grafen ohne Zyklen.
Sowas zum Beispiel, sein Baum kann gewurzelt sein,
dann hat er einen ausgezeichneten Knoten, das ist die Wurzel,
gewurzelte Bäume werden immer so dargestellt,
dass die Wurzel oben steht und die dann folgenden Knoten darunter.
Und das kennen sie zum Beispiel aus Suchbäumen,
das sind gewurzelte Bäume als Grafen. Dann gibt es Wälder,
Wälder haben die Eigenschaft, dass sie aus Bäumen bestehen.
Das bedeutet, dass wir ungerichtete Grafen haben ohne Zyklen,
die nicht notwendigerweise zusammenhängend sind. Ja, das ist die Eigenschaft,
die fehlt und weil der Zusammenhang nicht da ist,
zerfällt der Graf in mehrere Bäume, wenn er keine Zyklen hat,
und wird dann zum Wald.
Und im gerichteten Fall haben wir den Deck.
Der Deck ist der Directed Ascycling Graph,
also der gerichtete unzyklische Graf oder Azyklische Graf. Und ja,
wie der Name schon sagt, also ein Graf,
der keine Zyklen enthält, wie zum Beispiel dieser hier.
Also hier haben sie keine Möglichkeit,
einen Zyklus zu bilden und von einem Knoten über eine Liste
von Kanten zu dem Knoten zurückzugelernen
Decks sind deswegen wichtig, weil fast allen Grafproblemen sich eine
algorithmische Vereinfachung ergibt, wenn der Graf ein Deck ist.
Deswegen ist es unheimlich wichtig, über die Decks nachzudenken und immer,
wenn sie ein Grafproblem mit gerichteten Grafen haben,
sollten sie eine Minute darauf verwenden,
ob in ihrer Anwendung, einfach aus der Anwendung heraus,
ist nicht ein beliebiger gerichteter Grafis,
sondern es ein Deck ist.
Und wir werden dann Beispiele sehen, wo das einfach
viel effizienter wird. Kürzeste Wege zum Beispiel. Können sie
auf Decks viel effizienter berechnen als auf beliebigen ungerichteten Grafen
Ja, ein vollständiger Graf ist ein Graf,
wo zwischen allen Knoten, die da sind,
eine Kante existiert, die die mathematisch ein bisschen vorbelasteter sind,
kennen da wahrscheinlich auch noch einen anderen Begriff.
Das ist hier der K5, so nennt man die.
Das ist also der vollständige Graf mit fünf Knoten,
der K5 spielt eine gewisse Rolle,
und zwar ist der K5 der kleinste vollständige Graf,
den sie nicht mehr in der Ebene überschneidungsfrei darstellen können.
Das ist der, nee, das ist der Ja,
genau, es ist der. Ich bin gerade irritiert von
meinem eigenen Bild hier. Und der K33,
das ist der hier, das ist der vollständige Bi-Partite-Graf,
mit den drei Knoten ist der kleinste vollständige Bi-Partite-Graf,
den sie nicht planal darstellen können.
Bi-Partite-Grafen sind Grafen, wo die Knoten in zwei Mengen zerfallen.
Die Menge U und W in meinem Fall und alle Kanten
verlaufen nur Zwischenknoten der Menge U und W.
Also innerhalb dieser Menge dürfen dann keine Kanten auftreten
Ja, Planarität habe ich gerade schon erwähnt,
was bezeichnet man als Planar? Ein Graf ist dann Planar,
wenn er sich in der Ebene ohne Kantenüberschneidungen zeichnen lässt.
Und diese Eigenschaft hat die Graf-Theoretiker, das sind dann teilweise
Mathematiker und auch Informatiker. Lange Zeit verwirrt,
weil die ist natürlich super wichtig, wenn wir zum Beispiel,
wir haben so viele Grafen im täglichen Leben und viele davon wollen wir zeichnen,
wir wollen unseren U-Bahn-Plan zeichnen und Stadtpläne zeichnen,
das ist alles eigentlich Grafen.
Und dann fragt man sich natürlich, wie gehe ich mit diesen Überschneidungen
um und gibt es die Überschneidung, gibt es sie nicht?
Und das hat, Also wenn man das Problem betrachtet,
hat man das Gefühl, es ist erstmal ein Problem,
was mit Geometrie zu tun hat,
weil wir wollen das ja in der Ebene zeichnen und nicht zum Beispiel dreidimensionalen Raum oder so, ja, überschneidungsfrei.
Und es hat eine gewisse Zeit gedauert, bis man herausgefunden hat,
dass das diese überschneidungsfreie Zeichenbarkeit in der Ebene ein topologisches Kriterium ist.
Das heißt, man kann an der Topologie des Grafen erkennen,
ob er überschneidungsfrei in der Ebene zeichenbar ist oder nicht.
Und das ist ein ganz bemerkenswertes Resultat der Graftheorie, dass man das machen kann.
Es geht bei der Planarität auch nicht darum, ob die Darstellung,
die sie sehen, überschneidungsfrei ist oder nicht, sondern nur,
ob der Graf selber überschneidungsfrei zeichenbar ist oder nicht.
Also dieser Graf hier zum Beispiel, den Sie hier sehen,
der ist Planar, obwohl die Darstellung natürlich Überschneidung hat.
Sie sehen hier die Kanten kreuzen sich und die Tatsache,
dass er Planar ist, sieht man daran,
dass es eine Darstellungsform gibt.
Es muss eine existieren, die überschneidungsfrei ist.
Zum Beispiel diese hier. Hier ist eine Anlauf dann dargestellt.
Gut, wie repräsentieren wir Grafen? Zum einen können wir
eine Addizenzmatrix wählen
Eine Addizenzmatrix ist eine N-Mail-N-Matrix. Wir haben also
eine Zeile, eine Spalte für jeden Knoten.
Und in dieser Matrix schreiben wir Eisen oder Nullen rein.
Eine Eins steht an der Position IJ, wenn der Knoten-I
mit dem Knoten-J verbunden ist und eine Null sonst.
Manchmal stehen da auch Gewichte drin, je nach Anwendung,
können das auch sein. Der Speicherbedarf von so einer Matrix ist N-Quadrat,
ist also unabhängig davon, wie viele kannten wir in dem Grafen haben.
Wenn wir einen ungerichteten Grafen haben, dann ist die Matrix symmetrisch,
weil dann A-I-J bedeutet,
es gibt eine Kante von I nach J,
dann gibt es auch eine von J
Und im gerichteten Grafen ist es so,
dass die Matrix nicht symmetrisch sein muss. Und da können
wir diese Matrix sogar transponieren und erhalten dann den Graf,
der entsteht, wenn wir alle kannten, umkehren.
Also alle Pfeile in die umgekehrte Richtung zeigen. Addizenzmatrizen verwendet man dann,
wenn man einen möglichst effizienten Test auf das Vorhandensein einer Kante benötigt.
Addizenzmatrizen sind aber speicherintensiv und für viele Probleme gar nicht realisierbar.
Also weil wir gar nicht so eine große Matrix bilden können
Ja. Die Alternative dazu sind Addizenzlisten, das heißt,
pro Knoten wird eine Liste erstellt und in der Liste stehen die Nummern der Adjazentenknoten,
also der benachbarten Knoten, der Vorteil einer Adjazenzlistendarstellung ist,
dass sie einen Speicherbedarf von Tetter von N plus M haben.
Ja, und wenn ihr Graf Dünn ist, wenn er also
wenige Kanten hat, ist das wesentlich effizienter als die Matrix-Form.
Das Zetter hier kann man sich noch genauer anschauen,
im ungerichteten Fall ist das genau zwei M, das heißt,
jede ungerichtete Kante kommt in zwei Listen im gerichteten Fall ist es genau M.
Also wir haben genau M-Kanten, die wir darstellen müssen.
Es sind M-Listen-Elemente, die dargestellt werden.
Für sie mag das eine relative, starke Trivialität sein.
Das sage ich nochmal schnell im Bild. Also hier,
das ist ein ungerichteter Graf, eine Attenzenzlisten-Darstellung und eine Matrix-Darstellung.
Hier haben wir einen gerichteten Fall mit Attenzenzlisten und einer Matrix-Darstellung.
Also für Sie zum heutigen Zeitpunkt der Entwicklung in der Informatik
mag das eine relativ triviale Geschichte sein,
ob man jetzt an Grafen so oder so darstellt,
die Vorteile sehen sie auch wahrscheinlich sofort,
warum man manchmal das nimmt und manchmal vielleicht auch das nimmt.
In der Graf-Algorithmik war der Schritt, also früher hat man
nur das gemacht, da hat man nur mal Tritzen gehabt.
Das liegt daran, die Grafen haben ihren Ursprung in
der Mathematik und da hat man immer mit Matrizen gearbeitet.
In der Graf-Algorithmik ist dieser Wechsel von der Matrix zu der Addizenzlisten-Darstellung ein Riesenschritt gewesen.
Das hat man also ganz lange Zeit gar nicht gemacht und dann kam einer,
der hat gesagt, oh,
ich mache das jetzt mal mit diesen Listen und durch
den Wechsel von der Matrix zu den Listen
konnte man die Laufzeit von ganz vielen Algorithmen beschleunigen
Weil man eben nicht mehr,
wenn man alle Nachbarn wissen will, zu einem Knoten,
envile Knoten, also Positionen in dem Array angucken muss,
sondern eben nur noch so viele, wie dieser Graf Nachbarn hat.
Und eine Tiefensuche zum Beispiel ändert damit die Laufzeit von Endquadrat auf N plus M.
Und häufig ist eben die Anzahl der Kanten doch wesentlich geringer als Endquadrat.
Also insofern ist das ein ganz wichtiger Punkt. Ja,
dann haben wir noch viele Anwendungen,
wo Kanten und Knoten Gewichte tragen,
ein Gewichteter Graf bedeutet immer, dass die Kantengewichte tragen,
das ist der häufigere Fall,
wenn ausnahmsweise mal die Knotengewichte tragen,
das kann auch mal sein,
haben wir halt
einen Knotengewichteten Graf
Gut, so,
das war sozusagen der Schnelldurchlauf durch die Grundlagen der Begriffe,
wenn ihnen das ein oder andere jetzt dann nicht mehr so geläufig war,
schauen sie es einfach nochmal nach,
weil ich werde jetzt mit den Begriffen einfach arbeiten,
weil ich mal davon ausgehe,
dass sie die eh auch alle schon häufig vor dieser Vorlesung gehört haben.
Ja, das allererste, was man mit Grafen eigentlich so tut,
wenn man sie algorithmisch betrachtet, ist die Struktur von Grafen zu analysieren.
Und da gibt es also eine ganze Reihe von Fragestellungen,
die man haben kann.
Wenn sie zum ersten Mal mit so einem Graf konfrontiert sind,
dann können sie sich fragen, Ist der überhaupt zusammenhängend oder nicht?
Sie können fragen, gehört der in eine spezielle Klasse?
Ist es ein Baum? Ist es ein Wald oder nicht?
Der Graf, den sie haben, ist der vielleicht sogar bipathiet oder nicht?
Dann können Sie fragen, ist der vielleicht zweifach?
Wir nennen das auch ein Beat zusammenhängt.
Und enthält er dann sogenannte Gelenknoten. So zweifacher Zusammenhang,
den werden wir ja noch genauer sehen.
Bedeutet, dass sie mindestens zwei Kanten mal entfernen müssen,
um den Graf in zwei Teile
zu zerlegen
Ja, Planarität kann man sich anschauen. Ist ja eben ein Planar.
Das ist hier ausgegraut, nicht weil das unwichtig ist,
sondern weil wir es hier nicht machen werden.
Das ist, ja, da kann man ein paar Vorlesungen drauf verwenden.
Also ein paar Vorlesungsdoppelstunden, bis man das durch hat, das Thema.
Deswegen machen wir das hier nicht. Zyklen kann man natürlich analysieren,
das ist der Graf A zyklisch oder enthält der Zyklen.
Und der gerichtete Graf kann natürlich auch noch stark zusammenhängend sein oder nicht.
Und einige dieser Fragen hängen dann auch damit zusammen,
dass man natürlich dann auch wissen will, ja, okay,
wenn der nicht zusammenhängend ist, wie ist dann die Zusammenhangskomponentenstruktur etc.
Das sind die wichtigsten Strukturfragen,
die so auftreten
Der Zusammenhang ist deswegen so zentral. Man fragt sich jetzt
, warum ist das jetzt so wichtig? Weil,
wenn wir den Zusammenhang analysieren, finden wir häufig auch raus,
wie wir Grafen zerlegen können. Und wenn wir Grafen zerlegen können,
können wir die Komplexität von Algorithmen in der Regel reduzieren.
Wenn sie Graf, also wenn sie Wegeprobleme lösen,
dann lohnt es sich vorher mal den Zusammenhang zu analysieren,
weil sie dann ja wissen,
wenn zwei Knoten in einer unterschiedlichen Zusammenhangskomponente ist,
wird es keinen kürzesten Pfad geben.
Ja,
deswegen brauchen wir Zusammenhang so viel in der Praxis.
Die Methode der Wahl, mit der man
die allermeisten dieser Fragen beantworten kann, ist die Tiefensuche.
Die Tiefensuche ist nichts weiter als eine spezielle Strategie,
die Knoten eines Grafens zu besuchen.
Und die Strategie funktioniert wie folgt. Wir beginnen mit einem beliebigen Knoten
. Wir versuchen als erstes in die Tiefe zu gehen,
das heißt von jedem Knoten, den wir sehen,
werden wir zunächst einmal weiter eingucken,
ob wir noch einen weiteren Knoten binden,
bevor wir dann weitere auslaufende Kanten betrachten werden.
Deswegen hast du die Tiefensuche auch abgekürzt,
DFS oder Depth First Search.
Also man geht zuerst in die Tiefe
Um das zu realisieren, verwendet man einen Stack oder auf neu in deutschen Stapel.
Und zwar liegen auf diesem Stapel immer alle Knoten,
die man schon einmal gesehen hat,
aber von denen man noch nicht alle Kanten betrachtet hat.
Die Knoten kriegen eine Farbe, wir verwenden die Farben Weißgrau und Schwarz.
Das sind sehr unschöne Farben, hat aber den Vorteil,
dass auch die Farbblinden mitkommen. Und es hat den Vorteil,
dass das dann auch in einem schwarz-weiß ausgedruckten Skript oder Buch entsprechend gut nachvollziehen vollziehbar ist.
Ja, also im Kommen sind das eben weiß-grau und schwarze
Knoten und deswegen habe ich das hier auch so übernommen.
Weiße Knoten sind Knoten, die noch nicht besucht wurden.
Die ist also, das kann man sich super gut merken,
dass sie die weißen Flecken auf der Landkarte,
die Grauen, die hat man schon mal gesehen,
aber man hat noch nicht alle Nachbarn gesehen.
Also man hat noch nicht die Addizenzliste abgearbeitet. Die Schwarzen sind,
die die vollständig bearbeitet sind. Während der Tiefensuche läuft,
verteilen wir Zeitstempel. Wir haben also eine diskrete Uhr,
die tickt so vor sich hin und wir messen die Zeit,
wo wir den Knoten das erste Mal sehen,
das ist die D-Zeit oder die Discovery-Zeit,
das ist der Zeitpunkt, wo wir einen Knoten erstmalig betrachten.
Und da wechselt er die Farbe von Weiß nach Grau.
Und wir haben die F-Zeit, das ist die Finishing-Zeit.
Das ist der Zeitpunkt, wo wir den Knoten vollständig abgearbeitet haben.
Da wechseln die Farbe von Grau nach Schwarz.
Offensichtlich ist die Discovery-Zeit immer vor der Finishing-Zeit,
weil erst betrachten wir den Knoten das erste Mal und dann ist er irgendwann mal komplett abgearbeitet.
Während der DFS-Algorithmus läuft, erzeugt er einen Wald.
Das ist der sogenannte DFS-Wald oder Tiefensuchwald. Das ist ein Teilgraf
des ursprünglichen Grafen, in dem eine ausgewählte Menge von Knoten sind.
Und das sind die Knoten, oder das sind die Kanten,
Hast du jetzt gerade Knoten da?
Also es ist in der eine ausgewählte Menge von Kanten ist.
Und das sind die Kanten, die durchlaufen werden,
wenn wir in der rekusiven Struktur durch den Algorithmus durchgehen.
Und immer dann, wenn er einen Knoten erstmalig besucht,
ist die Kante, die wir da durchlaufen haben,
eine Kante dieses DFS-Waldes. Um den zu speichern,
merken wir uns nicht, einen zusätzlichen Graf,
sondern wir speichern uns von jedem Knoten einfach den Vorgänger.
Ja, und der es wird in der Regel
mit Pi von U
Gut, das ist die Tiefensuche im Pseudo-Code. Wir haben
eine Vorschleife, wir markieren erstmal alle Knoten als weiß und
der Nachfolger ist unbekannt, das wird auf Nil gesetzt.
Die Zeit unserer Uhr, die wird auf Null gesetzt und
von da aus wird die jetzt in diskreten Schritten laufen.
Dann beginnen wir, betrachten jeden Knoten dieses Graphen,
wenn die Farbe des Knoten weiß ist,
rufen wir unsere rekrusive Tiefensuchfunktion auf, die heißt DFS Visit.
Hier steht die DFS Visit-Funktion und die färbt den Knoten als
allererstes mal grau
Dann wird die Uhr um einen Schritt weiter gedreht und
die Discovery-Zeit für diesen Knoten wird festgehalten in dem RAD.
Dann haben wir eine Vorschleife, die alle Knoten betrachtet.
Und also alle Knoten betrachtet in der Adjazzenzliste des Knotens U,
dem wir gerade bei uns befinden.
Wenn dieser Knoten-V noch weiß ist,
dann haben wir den vorher noch nicht betrachtet,
dann machen wir den rekursiven Abstieg.
Dann wird der Vorgänger auf U gesetzt. Und wir rufen
die rekursiv unsere Besuchsfunktion,
DFS-Vizet für diesen KnotenV
Und am Ende, wenn wir alle Knoten betrachtet haben,
dann wird dieser Knoten-U schwarz gefärbt.
Das heißt, der ist vollständig abgearbeitet. Wir erhöhen wieder
unseren Diskrete, unsere diskrete Uhr hier und setzen die Finishing-Zeit.
Das ist der DFS-Algorithmus in seiner Grundstruktur.
Die Laufzeit des Algorithmus ist O von N plus M,
also Anzahl von Kanten plus Anzahl von Knoten,
die wir in dem Grafen haben.
Und die Tatsache, dass da diese Laufzeit rauskommt,
basiert auf einer amortisierten Analyse
Da muss man vielleicht jetzt eine Sekunde drüber nachdenken,
warum hat man hier eine amortisierte Analyse?
Weil wenn man jetzt ganz strikt analysieren würde,
also ganz einfach nach dem Worst-Case-Schema, dann würde man sich über,
müsste man sich überlegen, wie lange dauert diese Vorschleife hier?
Die müsste man abschätzen in der Anzahl der Schleifendurchläufe.
Und im Worst-Case-Szenario hat diese Vorschleife N-Durchläufe, weil jeder Knoten,
also ein Knoten kann ja zu allen anderen Knoten in dem Graf benachbart sein,
dann hätte diese Vorschleife hier N-Durchläufe oder N-Minus-1-Durchläufe, wenn sie so abschätzen,
dann wäre die Laufzeit der DFS Visit-Routine in O von N.
Sie wissen, dass die DFS Visit für jeden Knoten aufrufen
müssen und sie kommen zu einer Laufzeitschranke von O von N-Quadrat.
Steht da aber nicht, da steht N plus M.
Warum? Wenn man amortisiert draufschaut, dann sieht man,
dass man hier natürlich zwar im Worst Case tatsächlich alle so Endschritte hat,
weil man eine Kante zu jedem anderen Knoten hat,
dass man aber amortisiert, wenn man alle Durchläufe gemeinsam betrachtet, Aggregatenmethode.
Also das Argument. Natürlich jede Kante nur maximal zweimal betrachtet.
Einmal von U aus, einmal von V, aus dem ungerichteten Fall.
Jede Kante guckt man nur zweimal an. Das heißt,
das amortisiert diese Laufzeit hier gerade E durch V,
also die durchschnittliche Anzahl von Kanten pro Knoten hat und die Gesamtlaufzeit dann zu N plus M,
also E plus V wird an dieser Stelle.
Also das ist zum Beispiel eine Stelle,
wo wir traditionell Schon, also ich nehme an,
dass es in dem dritten Semester schon gesehen haben,
eigentlich schon amortisiert, analysiert haben.
Weil wir eben nicht einfach nur den Worst Case angenommen haben,
dass diese Schleifwörter einen mal durchlaufen wird.
Ja, hier ist ein schönes Beispiel. Ich glaube,
Sie sind da wahrscheinlich hinreichend fit drin, um das auch so hinzukriegen.
Ich gehe da mal nicht jeden Schritt durch. Also in diesen Kästen,
das ist jetzt mein Graf hier, Sie sehen verschiedene Zeitschritte.
Diese rot unterlegten Kanten sind die Baumkanten. Oder die DFS-Waldkanten,
wenn Sie so wollen, hier innen drin,
sehen Sie die Zeiten.
Vorne steht immer die Discovery-Zeit,
hinten steht die Finishing-Zeit
Am Anfang suchen wir uns einen beliebigen Startknoten aus,
das ist EU, der kriegt dann die Zeit 1,
dann laufen wir zu Nachbarn,
der kriegt dann die Zeit 2, zu einem Nachbarn,
zu einem Nachbarn.
Und wenn wir keine weiteren Nachbarn mehr haben,
wird der Knoten hier schwarz, wird komplett abgearbeitet.
Wir gehen dann zu einem Vorgänger zurück. Und so weiter,
und das machen wir immer, immer weiter.
Und dadurch werden dann langsam, aber sicher immer mehr Knoten schwarz,
kriegen alle ihre Discovery in Finishing-Zeiten.
Und am Ende ist der Algorithmus abgeschlossen,
die rot unterlegten Kanten hier sind die entsprechenden Bekannten des DFS-Waldes
Sie sehen hier, es ist kein Baum,
es ist ein Wald, ja, es gibt,
wenn Sie die roten Knachten, Kanten betrachten, zwei Zusammenhangskomponenten.
Auch ist nicht wie jede Kante eine Baumkante.
Es gibt natürlich noch weitere Kanten und die werden entsprechend klassifiziert,
je nachdem, wie die in dem Baum verlaufen.
Okay, so, das ist sozusagen erstmal der Algorithmus.
Ich gehe mal davon aus, Sie erinnern sich da
im Großen und Ganzen noch dran, wie das funktioniert.
Ganz wichtig für das Arbeit mit dem Algorithmus sind zwei Struktureigenschaften für sich ergeben.
Diese beiden Struktureigenschaften bezeichnen wir als dass Klammerungstheorien und das Theorien der weißen Pfade.
Gut, was sagt das Klammerungstheorien, das Klammerungstheorien sagt,
dass nach Durchführung des BFS-Algorithmus gelten für die Entdeckungs- und Endzeiten, D&F.
Für je zwei Knoten UNV, das ist ein von drei Fällen gibt.
Der erste Fall ist, die beiden haben eine leere Schnittmenge.
Also ich betrachte jetzt die Zeiten als Zeitintervalle von DU bis
F von U, also von der Discovery bis zur Finishing-Zeit,
das sind Zeiten dabei
Und dieses Intervall hat einen leeren Schnitt. Und wenn die einen leeren Schnitt haben,
dann sind U und V im Wald nicht benachbart.
Das heißt, sie sind weder, also sie stehen nicht
in Relation zueinander, sind weder Vorfahren noch Nachfahre voneinander.
Wenn das U-Intervall in dem V-Intervall enthalten ist,
dann ist U ein Nachfahre von V.
Und wenn das V-Intervall im U-Intervall enthalten ist oder das U-Intervall
eine Obermenge vom V-Intervall ist,
dann ist U
ein Vorfahre von
Das Wichtigste im Klammerungstheorien befindet sich in dem vierten Fall,
der hier fehlt,
und zwar, wenn sie sich zwei Intervalle angucken,
dann gibt es ja vier Szenarien, ja,
also sie haben das U-Intervall und sie haben das V Intervall und die verschiedenen Szenarien,
die es ja gibt, ist,
V ist vollständig in den U-In-U enthalten oder U ist vollständig in V enthalten.
Das ist das zweite mögliche Szenario. Das dritte Szenario ist
U und V haben nichts miteinander zu tun.
Diese drei Fälle sehen wir hier in dem Theorie,
den vierten Fall, den es immer gibt,
bei zwei Intervallen ist natürlich natürlich, dass sie partiell überlappen.
Das ist der Fall 4, den es gibt.
Und das Wichtige an dem Klammerungstheorien ist, ist,
dass Fall 4 nicht auftritt. Es verbirgt sich so ein bisschen hier,
einfach weil gerade das, was nicht da steht,
das Entscheidende ist, deswegen sage ich es nochmal explizit,
das kann nie auftreten in einer Tiefensuche.
So, ist ein Theorie, brauchen wir natürlich einen Beweis,
um zu zeigen, dass das korrekt ist.
Dazu muss man sich jetzt überlegen, wie der Algorithmus abläuft.
Wir haben Zwei Knoten, UV. Und ja,
das sind ja hier beliebige Knoten in dem Theorien.
Mir steht ja nur zwei Knoten, UNV.
Und wir können einfach mal diese Annahme machen,
dass das D von U auf alle Fälle kleiner ist als
das D von V. Sonst vertauschen wir einfach UNV.
So, das bedeutet jetzt,
ich habe jetzt irgendwie meinen Graf und der Tiefensuchalgorithmus,
der läuft hier irgendwie so durch den Grafen.
Und irgendwann bin ich an diesem Knoten U.
Und das passiert, bevor ich an dem Knoten V bin.
So, das ist erstmal die erste Annahme,
die ich hier mache
Jetzt kommen die Fälle, der erste Fall, der auftreten kann,
ist, dass die Discovery-Zeit von V, also die,
also was man hier parallel dazu natürlich noch malen kann,
ist so unser Zeitstrahl.
Also hier ist irgendwie die Zeit, die abläuft. Und das,
was wir jetzt gesehen haben, ist,
irgendwo hier ist die Discovery-Zeit von U,
wir erkennen den Knoten U und irgendwann später mal,
hier vielleicht kommt die Discovery-Zeit von dem Knoten
Also EU ist erkennen wir zuerst und dann erkennen wir Frau. So, und jetzt,
wenn wir an unsere Intervalle denken, dann bedeutet das,
also an dieser Stelle hier beginnt das U-Intervall und es läuft dann nach hier.
Und an dieser Stelle hier, ja,
hier beginnt jetzt das V-Intervall und das geht da nach hier.
Und irgendwo hören die jetzt noch hier auf, das haben wir noch nicht gesehen, wo. Okay,
um das jetzt weiter zu analysieren,
machen wir eine Fallunterscheidung und der erste Fall, den wir mal betrachten,
ist das D von V kleiner ist als F von U. Das heißt,
DVNV ist kleiner als F&U, das heißt,
was wir jetzt sagen in Fall 1, jetzt kommt Fall 1,
ist, dass diese Zeit mindestens bis hier geht,
dass es also über diesen Punkt hinaus läuft.
Okay, ja. So, was bedeutet denn das im Algorithmus, wenn das hier darüber hinausläuft?
Also das Intervall ist nicht abgeschlossen und wir wissen ja,
dass wenn wir das Zeitintervall betrachten, Ja,
und wenn wir das Zeit,
also wenn wir unseren Zeitstrahl nochmal haben, hier ist die Zeit,
ja, und dann haben wir hier,
ist die Discovery-Zeit und hier ist die Finishing-Zeit von so einem Knoten,
ja, dann wissen wir ja, was mit den Farben passiert.
Also hier ist der Knoten weiß, ja, genau,
bis zu diesem Zeitpunkt und dann schaltet der Algorithmus um,
dann wird der Knoten das erste Mal besucht,
in dieser Phase hier ist er grau,
ja,
und dann kommt die Finishing-Zeit und hier
ist er dann schwarz
So verhält sich ja der Algorithmus. Und deswegen wissen wir,
wenn wir hier drauf gucken, wo der Knoten weiß,
grau und schwarz ist. Also der U-Knoten,
der ist bis hierhin weiß, dann wird der grau.
Und dann hier, wenn das irgendwann kommt, die Finishing-Zeit,
dann wird der schwarz. Und jetzt, wir wissen,
dass zu diesem Zeitpunkt hier ist er auf alle Fälle grau.
Der U-Knoten ist, hier ist er noch auf alle Fälle grau.
Okay, Farbe ist grau. Wir wissen, U ist
ein Vorfahre von V. Denn wir haben U gesehen,
U ist noch nicht abgearbeitet. Und das bedeutet,
wir haben hier den nächsten Knoten besucht und dann haben wir
wieder einen Knoten besucht und dann wieder
Und irgendwann sind wir bei. V angekommen, ja.
Und deswegen ist, uh, das sind dann die Barmkannen,
eine U ist dann ein Vorfahrer von V, das wissen wir.
Wir wissen auch, dass alle Kanten von V sondiert,
also betrachtet werden in dieser Vorschleife, bevor U abgeweitet wird.
Weil das ist ja ein Stack, ja,
die liegen jetzt alle auf dem Stapel und irgendwo ganz unten auf dem Stapel,
ja, oder oben, je nachdem,
in welcher Richtung der wächst, also wenn das unser Stack ist,
den wir noch haben,
ja,
und der wächst jetzt mal in diese Richtung hier so runter,
dann haben wir hier irgendwie das U und dann geht das
irgendwie weiter und hier ist das V,
Und weil das ein Steak ist, wird ja V
zu Ende bearbeitet, bevor wir wieder U sehen.
Ja, also wir wandern hier runter, dann sehen wir
V und irgendwann bearbeiten eine V zu Ende und dann kommen wir wieder zur U.
Ja. Das bedeutet, alle Kanten von V werden sondiert,
also betrachtet, die Vorschleife wird komplett abgearbeitet, bevor U abgearbeitet wird.
Und deswegen ist die Finishing-Zeit von V kleiner als die Finishing-Zeit von U. Also
die haben ihr beide ihre Finishing-Zeiten hier irgendwo, ja.
Und wir wissen,
dass die von V kleiner ist als sie von U. Das heißt,
hier könnte zum Beispiel F von V sein und hier könnt ihr irgendwie F von U sein.
So, und wenn das so ist, dann sehen wir,
dass das Intervall läuft bis hier und dieses läuft bis hier
und dann sehen wir, dass das U-Intervall das Größere ist.
Dass das U-Intervall, das Intervall von V vollständig enthält.
Und das ist Fall 3 des Theoriens. Okay,
der zweite Fall ist jetzt, wir sagen,
dass die Zeit D von V größer ist als F&U.
Wir müssen jetzt ein neues Bild malen, damit sie das...
So, wir malen wieder hier unseren Zeitstrahl hin.
Das ist jetzt Fall 2. Wir haben wieder D von
Und wir wissen, irgendwann später kommt hier B von V.
Und eben war die Annahme, dass die Finishing-Zeit von U später ist,
als sie von V. Jetzt gehen wir davon aus,
die Finishing-Zeit ist kleiner.
Also wir haben hier das F von U. Das heißt,
das Intervall läuft also nur von hier bis vier.
Und das UV-Intervall, das läuft hier los. Das ist
der zweite Fall. Einen anderen Fall gibt es ja nicht.
Also entweder wir liegen davor oder wir liegen dahinter.
Okay, wenn wir davor liegen, dann gilt,
dass U bereits abgearbeitet ist.
Also dann gilt erstmal, dass die Farbe schwarz ist
Wir wissen ja noch, hier, Knoten V,
bis hier ist er. Weiß, grau, schwarz.
Das heißt, zu diesem Zeitpunkt hier ist unser Knoten bereits schwarz.
Ja, also wir wissen, der Knoten ist schwarz.
U ist abgearbeitet, also in der Stack. Wir liefen hier runtergelaufen,
sind wieder hochgelaufen. Wir haben U vom Stack runtergenommen,
haben alle Knoten betrachtet, sonst wäre er ja nicht schwarz.
Also U ist komplett abgearbeitet. Und zu diesem Zeitpunkt
finden wir V das erste Mal. Deswegen können die keine
Vorfahre oder Nachfahre-Relation zueinander haben. Weil, naja,
der eine ist ja schon komplett abgearbeitet,
bevor wir den anderen betrachten.
Ja, und da jetzt das Dee von vorne kleinlich kleiner ist,
als das F&V, das heißt,
das muss hier irgendwo hier mal kommen, irgendwann hier hinten,
kommt dann das F und V, sehen wir auch direkt,
dass diese beiden Intervalle überlappfrei sind und weil die überlappfrei sind,
haben wir Fall 1 unseres Theoriens.
Das müsste ich hier nur noch,
wenn sie das für den Fall,
dass sie es abgeschrieben haben,
schreiben sie besser Fall A und Fall B hin,
dann bezieht sich das hier auf den Beweis und dann kommen sie nicht durcheinander mit denen da.
Okay, und eine andere Situation kann ja nicht auftreten,
weil wir haben ja die beiden Fälle unterschieden,
mehr gibt es eigentlich nicht
Gut, aus dem Klammerungstheorien ergeben sich dann eine Struktur und zwar genau diese hier.
Wir können den Zeitstrahl aufmalen und für jeden Knoten ergibt sich
ein Zeitintervall und die Intervalle sind entweder ineinander enthalten wie hier
oder sie sind komplett voneinander getrennt.
Und das ist eine Eigenschaft, die die Tiefensuche an ganz vielen Stellen nutzt.
Das macht sie so mächtig und so weit einsetzbar,
dass diese Klammerstruktur der Knoten entsteht.
Gut, so, nächste Woche an dieser Stelle weiter.
Das war's für heute