Post

Graphs - Depth First Search

In this post we will show how to transverse a graph using the depth first search algorithm. As usual we will use an example to explain it. Find the code of this post in my github repository blogging-code, in the subdirectory graphs-depth-first-search.

Consider the following graph

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#d7d7d7', "lineColor": "#5972ff"}}}%%
graph TD
    A((A)) --- B((B))
    A --- C((C))
    B --- D((D))
    B --- E((E))
    C --- F((F))
    D --- A
    D --- F
    E --- F
    F --- G((G))
    H((H)) --- B

    %% Define styles
    classDef largeNodes fill:#d7d7d7,stroke:#000000 ,stroke-width:2px,font-size:24px;
    classDef visited fill:#8fff85,stroke:#000000 ,stroke-width:2px,font-size:24px;

    %% Apply base class to all nodes
    class A,B,C,D,E,F,G,H largeNodes;

    %% Step 1: Visit A
    class A visited;

consider we start from node $A$ (marked in green) and we want to visit all the nodes.

The way it’s done in depth first search is though a LIFO (Last In First Out), or equivalently a stack data structure. Let’s see it through an example. Imagine the following graph and let’s say we start from node A.

A simple way to represent this graph in python using and adjacency list is

1
2
3
4
5
6
7
8
9
10
11
12
13
from typing import Dict, List
GraphType = Dict[str, List[str]]

graph = {
    "A": ["B", "C", "D"],
    "B": ["A", "H", "E", "D"],
    "C": ["A", "F"],
    "D": ["B", "A", "F"],
    "E": ["B", "F"],
    "F": ["C", "D", "E", "G"],
    "G": ["F"],
    "H": ["B"],
}

The summary of the steps for the depth first search of this graph is

StepCurrent NodeStack (before update)Visited NodesStack (after update)
0A[][A][D, C, B]
1B[D, C][A, B][D, C, D, E, H, A]
2H[D, C, D, E][A, B, H][D, C, D, E, B]
3E[D, C, D][A, B, H, E][D, C, D, F, B]
4F[D, C, D][A, B, H, E, F][D, C, D, G, E, D, C]
5C[D, C, D, G, E, D][A, B, H, E, F, C][D, C, D, G, E, D, F, A]
6D[D, C, D, G, E][A, B, H, E, F, C, D][D, C, D, G, E, F, A, B]
7G[D, C, D][A, B, H, E, F, C, D, G][D, C, D, F]

Let’s look at some of the first steps:

  • Step 0: We visit node $A$ and add to the stack nodes $B$, $C$ and $D$.
  • Step 1: Pull the next node in the stack $B$, mark as visited and add the nodes $A$, $H$, $E$ and $D$ from the list of nodes connecting to $B$, now we will have a stack of [$A$, $H$, $E$, $D$, $C$, $D$].
  • Step 2: We should pull $A$ from the stack but is already visited so we visit $H$. We expand the stack to [$B$, $E$, $D$, $C$, $D$].
  • Step 3: Should visit $B$ but is already visited so we go to the next node in the stack, $E$, mark it as visited and add the nodes to the stack [$B$, $F$, $D$, $C$, $D$]…

The order of the visited nodes is [$A$, $B$, $H$, $E$, $F$, $C$, $D$, $G$]. Notice why we stop at step 7 even though we still have nodes in the stack. In the potential new step we search for the stack and all nodes have been visited so we have finished trasversing the graph.

This algorithm can be coded in python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from typing import Dict, List, Optional

GraphType = Dict[str, List[str]]

def dfs_filo(graph: GraphType, start: str, end: Optional[str] = None) -> bool:
    stack = [start]
    visited = set()

    while stack:
        node = stack.pop()
        if node in visited:
            continue

        visited.add(node)
        print(node, stack)

        if node == end:
            return True
        
        for neighbor in reversed(graph.get(node, [])):  
            if neighbor not in visited:
                stack.append(neighbor)

    return False

if __name__ == "__main__":
    graph = {
        "A": ["B", "C", "D"],
        "B": ["A", "H", "E", "D"],
        "C": ["A", "F"],
        "D": ["B", "A", "F"],
        "E": ["B", "F"],
        "F": ["C", "D", "E", "G"],
        "G": ["F"],
        "H": ["B"],
    }

    print("\nIterative DFS (FILO) starting from A:")
    dfs_filo(graph, "A")

    print("\nIterative DFS (FILO) starting from A and ending at F:")
    dfs_filo(graph, "A", "F")

Checking if a graph is cyclic

With depth fist search we can detect if the graph is cyclic. In the transversal if the node we are going to visit has already been visited, then we have found a loop and instead of continuing we can exit the function and say that the graph is cyclic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def is_cyclic(graph: GraphType, start: str, end: Optional[str] = None) -> bool:
    stack = [start]
    visited = set()

    while stack:
        node = stack.pop()
        if node in visited:
            return True

        visited.add(node)
        
        for neighbor in reversed(graph.get(node, [])):  
            if neighbor not in visited:
                stack.append(neighbor)

    return False

Let’s check if the graph is cyclic:

1
2
print(f"\nIs the graph cyclic? True/False")
print(is_cyclic(graph, "A"))

which clearly is. For instance, we have the loop (A, B, D, A) or (A, C, F, D, B, A).

This post is licensed under CC BY 4.0 by the author.