cards-deck: 100-199_university::111-120_theoretic_cs::111_algorithms_datastructures

| DFS | Depth first search |

Wir möchten uns jetzt eine Form anschauen, die zuerst in die Tiefe sucht, bevor sie die restlichen Knoten betrachten.

Konzept von DFS:

[!Important] Konzeption eines DFS Wir schauen uns einen Startknoten an. Von diesem aus werden wir anschließend einen weiteren, beliebigen Knoten anschauen. Innerhalb diesem werden wir das Prinzip wiederholen und wieder einen beliebigen Knoten anschauen, der erreichbar ist. Dies machen wir bis wir das Ende erreichen. Danach haben wir quasi einen Pfad abgeschlossen und werden jetzt zurückspringen und ab dem vorherigen Knoten den nächsten Nachbar besuchen etc.

Das heißt jetzt:

Von Startknoten immer Nachbar betrachten, das bis zum Ende dessen (keine Nachbarn) und dann mit backtracking immer zurück, um entsprechend einen Pfad zu finden.

[!Error] Wichtig wie oft besuchen wir Knoten mit DFS? #card Wir möchten jeden Knoten maximal 1 besuchen! Würden wir das erlauben, würden wir einen Loop erzeugen, weil wir so nach und nach uns in den gleichen Knoten wiederinden werden und sich alles wiederholt. ^1704708844168

Wir möchten das Ganze noch als PseudoCode betrachten:

PseudoCode:

[!Definition] Iterativer PseudoCode DFS wie bauen wir ihn auf? #card

for all u in V 
  u.color = white // indicates, that it wasnt visited yet 
forall u in V // traversing through all nodes available!
  if u.color = white // we have not used it yet!
  	DFS-visit(G,u) # traversing to next neighbor, if it wasnt visited before 
  	
DFS-Visit(G,u) // function that is actually traversing through graph 
u.color = grey # grey signals "in process"
for all v in Adj(u) ## going trough all neighbors
  if v.color == white 
  	v.pre = u # remember where we came from >> only for analysis
  	DFS-VIIST(G,v) # visiting this neighbor, because it was not visited yet 
u.color = black # once traversed trough all vertices, we can mark this node as visited

^1704708844177

PseudoCode Kaufmann:

[!Definition] Rekursive Beschreibung von DFS wie bauen wir das auf? #card Wir setzen hier als impliziten calling-stack der Rekursion auf ( also er gibt an, was wir wann aufrufen bzw wie wir die Graphen durchlaufen) Weiterhin werden dfsnum und compnum die Aufrufs und Abschlussreihenfolge angeben ( also wann wir es aufgerufen / abgeschlossen haben) -> diese Information kann helfen, weil wir somit nachvollziehen können, wann welcher Knoten durchlaufen / aufgerufen wurde. ( also etwa, dass er innerhalb einer anderen aufgerufen wurde und deswegen Nummer ist)

T = tree-edges // also alle die, mit dem wir einen Baum aufbauen!
F = forward-edges // alle die, wo wir von einem Zwei in einen anderen kommen
B = backwards-edges // alle die, die von einem Zweig zu einem vorherigen Knoten zurückspringen 
C = cross-edges // alle die, die außerhalb oder zu einem Punkt springen 

def DFS(v): Unit = 
  for all ( ( v,w ) in E)
  	if w not in S 
  		S = S mit w // adding w as node that we now have traversed ( because we have to track the ones we have visited already!)
  		dfsnum(w) = count1  // indicating that we have called node _w_ at which step 
  		// this could be at the beginning or later within another recursion --> this helps to keep track when we traverse a node!
  		count1 ++ //
  		DFS(w) // calling recursively to explore within the available neighbors of this node!
  		
  		// once we are done, returning to this state
  		compnum(w) = count2  // indicates when this node was completed / explored completely 
  		count ++ 
  		add (v,w) to T
  	else if ( v --> w ) // also ein Pfad von v nach w existiert ( wir ihn schon kennen):
  		füge (v,w) zu F hinzu
  	else if ( w --> v) // es gibt einen Pfad zurück zum Startpunkt 
  		füge (v,w) zu B 
  	else: 
  		füge  (v,w) zu C 

^1704708844183

| T,F,B,C Partitionen im Graph |

[!Example] Betrachtung von T,F,B,C Kanten in Graph: Consider the following graph: ![[Pasted image 20231125175624.png]] Welche Kanten beschreiben wir hier mit folgenden Termen? T(schwarz) = tree-edges // also alle die, mit dem wir einen Baum aufbauen! F(blau) = forward-edges // alle die, wo wir von einem Zwei in einen anderen kommen B(grün) = backwards-edges // alle die, die von einem Zweig zu einem vorherigen Knoten zurückspringen C(rot) = cross-edges // alle die, die außerhalb oder zu einem Punkt springen
#card ![[Pasted image 20231125175741.png]] ^1704708844190

[!Example] Anwendung des DFS:

![[Pasted image 20221104113634.png]] Unter Betrachtung des obigen Graphen: Wende den DFS an! #card

^1704708844196

Kantenpartitionen mit DFS

Wir haben zuvor schon die vier möglichen Parts betrachten, in die wir Kanten einordnen können, wenn wir DFS laufen lassen. Welche sind das ? #card T = tree-edges // also alle die, mit dem wir einen Baum aufbauen! F = forward-edges // alle die, wo wir von einem Zwei in einen anderen kommen B = backwards-edges // alle die, die von einem Zweig zu einem vorherigen Knoten zurückspringen C = cross-edges // alle die, die außerhalb oder zu einem ^1704708844202

[!Tip] Partition T entspricht dem Aufruf-baum was bedeutet das? #card Mit Partition T meinen wir die tree-edges, also alle Knoten, die bei der Suche einen Baum ergeben haben. Wir können hieraus jetzt ziehen, dass dieser Baum auch dem Ablauf/Aufbau-Baum der Rekursion entspricht ^1704708844208

[!Important] Aussage wenn , wobei beide tree-edges (T) sind was können wir folgern? #card Existiert dieser Pfad dann muss hier folglich dfsnum(v) kleiner als dfsnum(w) sein. Also v wurde früher aufgerufen als w. ^1704708844214

Es gelten jetzt für alle Kanten folgende Eigenschaften:

[!Definition] 1. #card 1. dfsnum(v) dfsnum(w) // kommt also vor w! ^1704708844221

[!Definition] 2. #card mit B meinen wir die backwards-edges 2. dfsnum(v) dfsnum(w) compnum(w) compnum(v) // also v wird später erst gefunden und wurde früher abgeschlossen ( weil wir quasi bei w sind und danach zu v springen) ^1704708844227

[!Definition] 3. Mit C meinen wir die Querkanten/ cross-edges 3. dfsnum(v) dfsnum(w) compnum(w) compnum(v) Also wird v zuerst besucht, jedoch beenden wir v viel später als w ( weil wir von zu einem viel komplexeren / tieferen Bereich gesprungen sind!)

Folgerungen:

Wir können aus dieser Betrachtung jetzt zwei wichtige Punkte ableiten:

[!Important] Einteilung in die Edge-partitions wie können wir das realisieren? #card Wir können die Abhängigkeiten zwischen dfsnum und compnum nutzen, um entsprechend einteilen zu können ^1704708844235

[!Important] Folgerung Bei Anwendung von DFS auf DAG? #card Wenden wir einen DFS auf einen DAG an, dann gibt es da keine Backwards-edges. Hierbei gilt dann auch für alle Kanten:

Das heißt: –> bildet eine topologische Ordnung ^1704708844242

[!Error] DFS dient als alternativer Algorithmus zu Top-Sort

Betrachtung der Laufzeit:

[!Important] Faktoren zur Laufzeitberechnung welche gibt es? #card Die Laufzeit baut sich hier aus zwei wichtigen Faktoren auf:

  • wir traversieren höchstens 1x durch jeden Knoten
  • wir schauen uns jede Kante maximal 1x an!

Aus dieser Betrachtung heraus werden wir wohl wahrscheinlich folgende Laufzeit erhalten:

^1704708844248

Geschwindigkeit mit [[111.13_Graphen_basics#Darstellen als Adjazenzliste|Adjazenzliste]]

Wir möchten die Geschwindigkeit / Laufzeit mit einer Adjazenzliste näher betrachten. Dafür müssen wir diverse Falle betrachten: welche ? #card

  • time spent in DFS(G) itself without subroutines is –> we are visiting each node after all
  • within each node we look at its edges, which is denoted with weight of each of given vertex
  • which is also equal to amount of edges and thus results with
    • Furthermore all visits together are represented by :
    • Each Edge is only visited once ^1704708844254

[!Definition] Laufzeit DFS mit Adjazenzliste: womit resultieren wir? #card Wir erhalten eine Laufzeit von ^1704708844261

Geschwindigkeit mit [[111.13_Graphen_basics#Darstellen als Matrix|Adjazenzmatrix]]

while the procedure remains the same as for adjacency lists, we have an important change of time required, due to matrix: which? #card Finding neighbors of a vertex requires us to traverse through a row in the matrix, thus taking Because of this we running time for DFS-visit is now . ^1704708844267

Mit dieser wichtigen Modifikation ändert sich die Laufzeit dieser Struktur immens:

[!Definition] Laufzeit DFS mit Adjazenzmatrix: womit resultieren wir jetzt? #card Wir erhalten eine neue Laufzeit von ^1704708844274


Using DFS > Connected components

undirected graphs | scanning for connected components with DFS

  • in undirected graph, a connected component is a maximal subset A of vertices such that there exists a path between each tow vertices that means that each subset discovered by the DFS-Visit is connected to the preceeding node and thus part of the connected component within the graph.

Observation :: ^1704708844282 DFS-Visit starts in vertex u, then the discovered tree by DFS- Visit (G,u) contains all vertices in the connected component of u.

![[Pasted image 20221107143423.png]]

:: start in one vertices and gather all the other vertices within the component of connected graphs and combine them to be a a component.

directed graph - scanning for connected components with DFS:

component Graph : Recap definition? #card in a directed graph, a strongly connected component is maximal subset S of vertices such that for each two vertices of S theres a directed path from one to the other and vice versa

graph TD
point1 --> point2;
point2 --> point1;
point3 --> point2;
point2 --> point3;

Definition of a component graph of directed graph

  • vertices of correspond to components of G
  • there exists an edge between vertices A and B in if there exists vertices u and v in the connected components represented by A and B such that there is an edge from u to v ![[Pasted image 20221107165934.png]] also to be represented in shorter notation :: ^1704708844311 ![[Pasted image 20221107165956.png]]

Proposition:cted acyclic graph, meaning that there is no loop present, there is no circle

Proof: If G(SCC) would contain a cycle, then all corresponding vertices would be part of a larger SCC, which contradicts the definition of the SCC_{s} itself.

Sink and Source components of a directed Graph :

sink component :: if the corresponding vertex in a does not have an out-edge >> only directed towards, it does not help with loops. (example would be B and D) ^1704708844323

source component :: if the corresponding does not have an in-edge, only edges pointing outward (example is A and C) ^1704708844330

![[Pasted image 20221107144356.png]] A component can either be a sink, source or none of them. They cannot be sink and source at the saem time either, unless they are “isolated” - there are no in and out-going edges from the component. A DAG - directed acyclic graph always has at least one source and at least one sink.

running DFS starting in a sink component ::

^1704708844336 :: requirement : sink component B If DFS started on G in vertex then the tree constructed with DFS-Visit(G,u) covers whole component B > because it cant leave the component, but only traverse within. However starting the DFS-Visit in in another component, we are able to construct a tree that is greater than the component choosen >>> because it can possibly enter B

![[Pasted image 20221107195110.png]]

Resulting Idea:: ^1704708844342 To discover a SCC we start a DFS within a sink component and then work backwards along . However we ough to figure out how to find sink holes efficiently first :: ^1704708844347

Finding a sink component within a graph ::

^1704708844353 requirement :: default DFS on directed graph ^1704708844359

defining the discovery time d(u) and finishing time f(u) of a vertex as: discovery time : time when DFS algorithm first visited the node - and marked it grey for in process - f(A) := max_{u\in A}f(u)$

Proposition :: finding sink hole based on time passed on node ::

^1704708844365 Let A and B be two SCCs of a graph G and assume that B is a descendent of A in $G^{SCC}$. Then f(B) < f(A) > B will always be faster than A, because in order to finish A, the DFS ought to finish B first -> traverses into b, taking all depth checks and once done its returning to A. ![[Pasted image 20221107200303.png]]

Proof ::

^1704708844370 Case d(A) < d(B) :: Denote by u the first vertex we visit in A. Then the DFS builds a tree starting from u, and the tree will cover all of B before it is done with u. ^1704708844376 ![[Pasted image 20221107200424.png]] Thus all of B is processed, before A is able to finish its execution

Case d(A) > d(B) :: Denote by u the first vertex we visit in B. ^1704708844382 Then the DFS will first visit all of B before moving ![[Pasted image 20221107200633.png]]

However this result does not conclude, that the vertex with the smallest finishing time is necessarily a sin component.

Attention Statement of Proposition onlys talk about finishing times of components, not individual vertices.

Proposition 12 :: finding a source component within a graph ::

^1704708844388

Assuming that we run a DFS on G ( with any starting vertex v) and recording all finishing times of each vertices. Then the verte with the largest finishing time is in a source component.

Proof :: ^1704708844393

Consequence of previous propositon - finding source component - that the component with the largest finishing time f(A) ought to be a source component.

Converting sources to sinks >> sources can be found easily::

In order to map strongly connected components, we can apply a trick that helps finding the sink components, although we are only able to spot and located source locations so far: Inverting the graph We consider the graph $G^{t}G^{t}G^{t}$, as they are equal to the sink components in G. Graph before converting : ![[Pasted image 20221107210128.png]] Graph after converting : ![[Pasted image 20221107210147.png]]

Final Algorithm SCC ::

^1704708844399

General Idea for the algorithm :: ^1704708844405

  • run a first DFS on G, with any arbitrary starting vertex - not important yet. The verte u* with the largest finishing time f(u) is in the source of the sought $G^{SCC}(G^{t})^{SCC}G^{t}G^{t},u^{*}$) is the first strongly connected component of our Graph.
  • Continue with a DFS on the vertex V = v* > that has the highest f(u) among the remaining vertices.
  • this continues until done.
SCC(G)
Call DFS(G) to compute finishing times f(u)
Compute reverse graph Gt
Call DFS(Gt) where the vertices in the main loop are considered in order of decreasing f(u) > start with the largest execution time, because it is a sink component now - by our proposition
Output the subsets that have been discovered by the individual calls of DFS-Visit 

Starting with the reversed graph and then running the second DFS on the original graph is also possible, because both graphs share the same subsets

SCC running time ::

Running time when the graph is an adjacency list - faster than matrix as seen [[111.13_Graphen_basics#Runtime analysis of DFS ::|DFS runtime]] ^1704708844411 execution of DFS twice :: $\mathcal{O}(|V| + |E|)\mathcal{O}(|E|)\mathcal{O}(|V|)\mathcal{O}(|V|+|E|)$ ^1704708844423

is there a more efficient solution / could there be one ?

:: probably not because with this running time we are constant -> scaling with size of graph of course <- already. And we have to traverse trough each edge and vertices at least once so thats the minimum effort we have for the whole graph

Proposition - DFS on directed Graphs ::

^1704708844429

A directed graph has a cycle if and only if its DFS reveals a back edge - an edge that goes from the current vertex to a previously visited vertex.

Proof $\Longleftarrowv_{1},\ldots,v_{k}v_{1},\ldots,v_{k}->u$

Proof $\Longrightarrowv_{i}v_{j}v_{i}v_{i-1}(v_{i-1},v_{i}$ which is a back edge >> thus proof.

Proposition - RUN DFS on a DAG ::

^1704708844435 If we run DFS on a DAG, all graph edges go from larger to smaller finishing times.

DFS on a DAG does not have any back edges Then we always finish the descendants first before finishing their ancestors :: ^1704708844442 ![[Pasted image 20221107212747.png]]

Algorithm for Topological sort ::

^1704708844449

  • Run DFS with arbitrary starting vertex
  • If DFS reveals back-edge > no topological sort, abort
  • If not >> sort vertices by decreasing finishing times.s