DEV Community

M.T.Ramkrushna
M.T.Ramkrushna

Posted on

Topic 12: Graphs πŸ•ΈοΈ

Graphs are one of the most important DSA topics for interviews and exams.

A graph is a collection of:

  • Vertices / Nodes
  • Edges / Connections

Example:

    1
   / \
  2   3
  |   |
  4---5
Enter fullscreen mode Exit fullscreen mode

Here:

Nodes = {1, 2, 3, 4, 5}
Edges = {(1,2), (1,3), (2,4), (3,5), (4,5)}
Enter fullscreen mode Exit fullscreen mode

1. Types of Graphs

Undirected Graph

Connection works both ways.

1 ---- 2
Enter fullscreen mode Exit fullscreen mode

If 1 is connected to 2, then 2 is connected to 1.


Directed Graph

Edges have a direction.

1 ---> 2
Enter fullscreen mode Exit fullscreen mode

This means:

1 β†’ 2
Enter fullscreen mode Exit fullscreen mode

but not necessarily:

2 β†’ 1
Enter fullscreen mode Exit fullscreen mode

Weighted Graph

Edges have costs/weights.

1 --5--> 2
Enter fullscreen mode Exit fullscreen mode

The cost of travelling from 1 to 2 is 5.


Unweighted Graph

All edges are treated equally.

1 ---- 2
Enter fullscreen mode Exit fullscreen mode

2. How to Represent a Graph

There are three common representations.


A. Adjacency Matrix

For:

1 ---- 2
|      |
|      |
3 ---- 4
Enter fullscreen mode Exit fullscreen mode

We can use:

graph = [
    [0, 1, 1, 0],
    [1, 0, 0, 1],
    [1, 0, 0, 1],
    [0, 1, 1, 0]
]
Enter fullscreen mode Exit fullscreen mode

graph[i][j] == 1 means an edge exists.

Complexity

Space:

O(VΒ²)
Enter fullscreen mode Exit fullscreen mode

Useful when the graph has many edges.


3. Adjacency List

This is usually the most useful representation in interviews.

For:

1 ---- 2
|      |
|      |
3 ---- 4
Enter fullscreen mode Exit fullscreen mode

We can write:

graph = {
    1: [2, 3],
    2: [1, 4],
    3: [1, 4],
    4: [2, 3]
}
Enter fullscreen mode Exit fullscreen mode

Or more commonly with defaultdict:

from collections import defaultdict

graph = defaultdict(list)

graph[1].append(2)
graph[2].append(1)
Enter fullscreen mode Exit fullscreen mode

Complexity

Space:

O(V + E)
Enter fullscreen mode Exit fullscreen mode

This is usually preferred for sparse graphs.


4. Building an Undirected Graph

Suppose edges are:

edges = [
    [0, 1],
    [0, 2],
    [1, 3],
    [2, 3]
]
Enter fullscreen mode Exit fullscreen mode

Build the adjacency list:

from collections import defaultdict

graph = defaultdict(list)

for u, v in edges:
    graph[u].append(v)
    graph[v].append(u)
Enter fullscreen mode Exit fullscreen mode

Result:

0 β†’ [1, 2]
1 β†’ [0, 3]
2 β†’ [0, 3]
3 β†’ [1, 2]
Enter fullscreen mode Exit fullscreen mode

Important

For an undirected graph:

graph[u].append(v)
graph[v].append(u)
Enter fullscreen mode Exit fullscreen mode

For a directed graph:

graph[u].append(v)
Enter fullscreen mode Exit fullscreen mode

Only one direction is added.


5. Graph Traversal

The two fundamental graph traversal techniques are:

  1. DFS β€” Depth First Search
  2. BFS β€” Breadth First Search

You already learned BFS when studying trees. The same idea applies to graphs, but graphs require extra care because they can contain cycles.


6. DFS β€” Depth First Search

DFS goes as deep as possible before backtracking.

Example:

    1
   / \
  2   3
 / \
4   5
Enter fullscreen mode Exit fullscreen mode

Possible DFS:

1 β†’ 2 β†’ 4 β†’ 5 β†’ 3
Enter fullscreen mode Exit fullscreen mode

Recursive DFS

def dfs(graph, node, visited):
    if node in visited:
        return

    visited.add(node)

    for neighbor in graph[node]:
        dfs(graph, neighbor, visited)
Enter fullscreen mode Exit fullscreen mode

Usage:

visited = set()
dfs(graph, 0, visited)
Enter fullscreen mode Exit fullscreen mode

7. Why Do We Need visited?

Consider:

1 ---- 2
Enter fullscreen mode Exit fullscreen mode

Since the graph is undirected:

1 β†’ 2
2 β†’ 1
Enter fullscreen mode Exit fullscreen mode

Without visited, DFS could repeatedly do:

1 β†’ 2 β†’ 1 β†’ 2 β†’ 1 β†’ ...
Enter fullscreen mode Exit fullscreen mode

So:

visited = set()
Enter fullscreen mode Exit fullscreen mode

is one of the most important graph patterns.


8. DFS Iteratively

Instead of recursion, use a stack.

def dfs_iterative(graph, start):
    visited = set()
    stack = [start]

    while stack:
        node = stack.pop()

        if node in visited:
            continue

        visited.add(node)

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

    return visited
Enter fullscreen mode Exit fullscreen mode

Remember:

DFS β†’ Stack
BFS β†’ Queue
Enter fullscreen mode Exit fullscreen mode

9. BFS in a Graph

BFS explores nodes level by level.

Use:

from collections import deque
Enter fullscreen mode Exit fullscreen mode

Template:

def bfs(graph, start):
    visited = set([start])
    queue = deque([start])

    while queue:
        node = queue.popleft()

        for neighbor in graph[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)
Enter fullscreen mode Exit fullscreen mode

Important interview detail

Mark a node visited when you add it to the queue, not when you remove it.

Good:

if neighbor not in visited:
    visited.add(neighbor)
    queue.append(neighbor)
Enter fullscreen mode Exit fullscreen mode

This prevents duplicate queue entries.


10. Number of Connected Components

Suppose:

1 --- 2      4 --- 5

3
Enter fullscreen mode Exit fullscreen mode

There are:

3 connected components
Enter fullscreen mode Exit fullscreen mode

Solution

Run DFS from every unvisited node.

def count_components(n, edges):
    graph = [[] for _ in range(n)]

    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)

    visited = set()
    count = 0

    def dfs(node):
        visited.add(node)

        for neighbor in graph[node]:
            if neighbor not in visited:
                dfs(neighbor)

    for node in range(n):
        if node not in visited:
            count += 1
            dfs(node)

    return count
Enter fullscreen mode Exit fullscreen mode

Pattern

For every unvisited node:
    start DFS/BFS
    count one component
Enter fullscreen mode Exit fullscreen mode

11. Number of Islands 🌊

This is one of the most famous graph/grid interview questions.

Given:

1 1 0 0
1 0 0 1
0 0 1 1
Enter fullscreen mode Exit fullscreen mode

Each group of connected 1s is an island.

Answer:

3
Enter fullscreen mode Exit fullscreen mode

DFS solution

def num_islands(grid):
    rows = len(grid)
    cols = len(grid[0])
    count = 0

    def dfs(r, c):
        if (
            r < 0 or
            r >= rows or
            c < 0 or
            c >= cols or
            grid[r][c] != "1"
        ):
            return

        grid[r][c] = "0"

        dfs(r + 1, c)
        dfs(r - 1, c)
        dfs(r, c + 1)
        dfs(r, c - 1)

    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == "1":
                count += 1
                dfs(r, c)

    return count
Enter fullscreen mode Exit fullscreen mode

Key pattern

Whenever you see:

connected cells / regions / islands

Think:

DFS or BFS
Enter fullscreen mode Exit fullscreen mode

12. Graph Cycle Detection β€” Undirected Graph

Example:

1
| \
|  \
2---3
Enter fullscreen mode Exit fullscreen mode

This contains a cycle.

For an undirected graph, while doing DFS, keep track of the parent.

def has_cycle(graph, n):
    visited = set()

    def dfs(node, parent):
        visited.add(node)

        for neighbor in graph[node]:
            if neighbor not in visited:
                if dfs(neighbor, node):
                    return True

            elif neighbor != parent:
                return True

        return False

    for node in range(n):
        if node not in visited:
            if dfs(node, -1):
                return True

    return False
Enter fullscreen mode Exit fullscreen mode

Why neighbor != parent?

Suppose:

1 ---- 2
Enter fullscreen mode Exit fullscreen mode

From 1, we visit 2.

From 2, we see 1 again.

That's not a cycle β€” it's simply the edge we came from.


13. Cycle Detection β€” Directed Graph

Directed graphs require a different technique.

We maintain:

visited
current recursion path
Enter fullscreen mode Exit fullscreen mode

Example:

1 β†’ 2 β†’ 3
    ↑   |
    β””β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

There is a directed cycle.

def has_cycle_directed(graph, n):
    visited = set()
    path = set()

    def dfs(node):
        if node in path:
            return True

        if node in visited:
            return False

        visited.add(node)
        path.add(node)

        for neighbor in graph[node]:
            if dfs(neighbor):
                return True

        path.remove(node)
        return False

    for node in range(n):
        if dfs(node):
            return True

    return False
Enter fullscreen mode Exit fullscreen mode

The important distinction:

Undirected β†’ parent tracking
Directed   β†’ recursion path / 3-state DFS
Enter fullscreen mode Exit fullscreen mode

14. Shortest Path in an Unweighted Graph

This is a very important BFS application.

Suppose:

0 -- 1 -- 2 -- 3
     |
     4
Enter fullscreen mode Exit fullscreen mode

To find the shortest number of edges from 0 to 3, use BFS.

from collections import deque

def shortest_path(graph, start, target):
    queue = deque([(start, 0)])
    visited = {start}

    while queue:
        node, distance = queue.popleft()

        if node == target:
            return distance

        for neighbor in graph[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append((neighbor, distance + 1))

    return -1
Enter fullscreen mode Exit fullscreen mode

Why BFS?

Because BFS explores:

distance 0
distance 1
distance 2
distance 3
...
Enter fullscreen mode Exit fullscreen mode

So the first time you reach the target, you've found a shortest path in an unweighted graph.


15. Shortest Path in a Grid

Suppose:

0 0 0
1 1 0
0 0 0
Enter fullscreen mode Exit fullscreen mode

You can move:

↑ ↓ ← β†’
Enter fullscreen mode Exit fullscreen mode

This is also a graph.

Each cell is effectively a node.

Neighbors are:

directions = [
    (1, 0),
    (-1, 0),
    (0, 1),
    (0, -1)
]
Enter fullscreen mode Exit fullscreen mode

This pattern appears constantly in interviews.


16. Multi-Source BFS

Suppose several starting nodes spread simultaneously.

Example:

2 1 1
1 1 0
0 1 1
Enter fullscreen mode Exit fullscreen mode

2 represents an already-processed source.

If multiple 2s exist, put all of them into the queue initially.

queue = deque()

for r in range(rows):
    for c in range(cols):
        if grid[r][c] == 2:
            queue.append((r, c))
Enter fullscreen mode Exit fullscreen mode

Then perform normal BFS.

This is called:

Multi-source BFS

Classic example:

Rotting Oranges


17. Topological Sort

Topological sorting applies to a directed acyclic graph (DAG).

Suppose:

A β†’ B
A β†’ C
B β†’ D
C β†’ D
Enter fullscreen mode Exit fullscreen mode

A valid ordering is:

A β†’ B β†’ C β†’ D
Enter fullscreen mode Exit fullscreen mode

The key requirement:

Every prerequisite must appear before the thing that depends on it.

This is useful for:

  • Course prerequisites
  • Task dependencies
  • Build systems
  • Scheduling

18. Topological Sort Using Kahn's Algorithm

Calculate the indegree of every node.

indegree = number of incoming edges
Enter fullscreen mode Exit fullscreen mode

Nodes with:

indegree == 0
Enter fullscreen mode Exit fullscreen mode

can be processed first.

from collections import deque

def topological_sort(n, edges):
    graph = [[] for _ in range(n)]
    indegree = [0] * n

    for u, v in edges:
        graph[u].append(v)
        indegree[v] += 1

    queue = deque()

    for i in range(n):
        if indegree[i] == 0:
            queue.append(i)

    order = []

    while queue:
        node = queue.popleft()
        order.append(node)

        for neighbor in graph[node]:
            indegree[neighbor] -= 1

            if indegree[neighbor] == 0:
                queue.append(neighbor)

    if len(order) != n:
        return []

    return order
Enter fullscreen mode Exit fullscreen mode

Important

If:

len(order) != n
Enter fullscreen mode Exit fullscreen mode

then there is a cycle.


19. DFS vs BFS

DFS BFS
Stack / recursion Queue
Goes deep first Goes level by level
Connected components Shortest unweighted path
Cycle detection Level distances
Backtracking Multi-source BFS
Topological sort Topological sort

20. Graph Complexity

With adjacency lists:

DFS

O(V + E)
Enter fullscreen mode Exit fullscreen mode

BFS

O(V + E)
Enter fullscreen mode Exit fullscreen mode

Why?

Every vertex and edge is processed a limited number of times.

Space:

O(V + E)
Enter fullscreen mode Exit fullscreen mode

for the graph plus traversal structures.


21. Graph Problem Recognition 🧠

This is one of the most valuable things to memorize.

Problem wording Think
Visit all connected nodes DFS/BFS
Connected components DFS/BFS
Number of islands DFS/BFS
Shortest unweighted path BFS
Minimum number of moves BFS
Spread simultaneously Multi-source BFS
Prerequisites Topological sort
Dependency ordering Topological sort
Detect undirected cycle DFS + parent
Detect directed cycle DFS + recursion path
Weighted shortest path Dijkstra
Negative edge weights Bellman-Ford
Connect all nodes cheaply MST
Minimum spanning tree Kruskal / Prim

22. The Most Important Graph Template

For an undirected graph:

from collections import defaultdict

graph = defaultdict(list)

for u, v in edges:
    graph[u].append(v)
    graph[v].append(u)

visited = set()

def dfs(node):
    if node in visited:
        return

    visited.add(node)

    for neighbor in graph[node]:
        dfs(neighbor)
Enter fullscreen mode Exit fullscreen mode

Memorize this structure.

You can adapt it to many problems.


23. Grid DFS Template

Also memorize this:

def dfs(r, c):
    if (
        r < 0 or
        r >= rows or
        c < 0 or
        c >= cols
    ):
        return

    if grid[r][c] == 0:
        return

    grid[r][c] = 0

    for dr, dc in directions:
        dfs(r + dr, c + dc)
Enter fullscreen mode Exit fullscreen mode

Then:

directions = [
    (1, 0),
    (-1, 0),
    (0, 1),
    (0, -1)
]
Enter fullscreen mode Exit fullscreen mode

This solves a huge family of grid problems.


24. What You Should Know for Exams

Make sure you can implement these without looking at notes:

Level 1

  • Build adjacency list
  • DFS
  • BFS
  • Detect connected components
  • Number of islands

Level 2

  • Shortest path using BFS
  • Cycle detection
  • Flood Fill
  • Rotting Oranges
  • Course Schedule

Level 3

  • Topological Sort
  • Dijkstra's Algorithm
  • Union-Find / DSU
  • Minimum Spanning Tree
  • Kruskal's Algorithm
  • Prim's Algorithm

⭐ Most Important Interview Questions

If you're prioritizing practice, focus on:

1. Number of Islands
2. Clone Graph
3. Rotting Oranges
4. Course Schedule
5. Course Schedule II
6. Pacific Atlantic Water Flow
7. Graph Valid Tree
8. Number of Connected Components
9. Word Ladder
10. Network Delay Time
Enter fullscreen mode Exit fullscreen mode

The next major graph topic is Shortest Path Algorithms, where we'll learn Dijkstra's algorithm, 0-1 BFS, Bellman-Ford, and when to use each one.

Top comments (0)