0% found this document useful (0 votes)
38 views105 pages

A2SV DFS Lecture - No Code

The document provides a comprehensive overview of Depth First Search (DFS), including its definition, implementation, and various applications such as pathfinding and cycle detection in graphs. It outlines prerequisites, common pitfalls, and offers practice problems to enhance understanding. Additionally, it discusses both recursive and iterative approaches to DFS, along with time and space complexity considerations.

Uploaded by

remidan37
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
38 views105 pages

A2SV DFS Lecture - No Code

The document provides a comprehensive overview of Depth First Search (DFS), including its definition, implementation, and various applications such as pathfinding and cycle detection in graphs. It outlines prerequisites, common pitfalls, and offers practice problems to enhance understanding. Additionally, it discusses both recursive and iterative approaches to DFS, along with time and space complexity considerations.

Uploaded by

remidan37
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 105

DFS

(Depth First Search)


Lecture Flow
1) Pre-requisites
2) Problem definitions and uses
3) Different approaches
4) Applications of DFS
5) Pair Programming
6) Things to pay attention(common pitfalls)
7) Practice questions
8) Resources
9) Quote of the day

2
Prerequisites
- Recursion
- Graph
- Stack

3
Objectives
- Learn about DFS graph traversal

- Learn different operations we can do on graphs using DFS

- Learn about applications of DFS

4
Definition

- DFS is a graph traversal algorithm.

- It aims to visit all nodes or vertices of a graph in a systematic way.

5
Definition

● The algorithm starts at a particular node, known as the source or starting node, and
explores as far as possible along each branch before backtracking.

SOMETHIN
G

6
Implementation`

def dfs(vertex, visited):

# base case

visited.add(vertex)

for neighbour in graph[vertex]:

if neighbour not in visited:

dfs(neighbour, visited)

7
When to Use

● Finding Connected Components

● Detecting Cycles

● Path Finding

● Maze Solving

● Solving Puzzles

● Generating Permutations

● Topological Sorting

8
Recursive and Iterative Approach of Implementing
DFS

9
Find if Path
Exists in Graph

10
Recursive Approach

11
Recursive Implementation
● Since it’s a recursive
implementation we have to
obey the 3 rules of recursive
functions.

● What are those 3 rules?

12
Rule 1: State
● We need to know what node def dfs(node, visited):
we are on.

● We need to keep track of the


visited nodes.

13
Rule 2: Base case
● For the base case, when if node == destination:
should we stop the recursion return True
when the current node is our
target.

● Alternatively, we can also


finish when we have traversed
over all the nodes.

14
Rule 3: Recurrence relation
● We want to traverse all the for neighbour in graph[node]:
adjacent nodes. found = dfs(neighbour)

if found:
return True

15
Build Graph

def validPath(self, n, edges,


source, destination):

graph = defaultdict(list)

for node1, node2 in edges:


graph[node1].append(node2)
graph[node2].append(node1)

return dfs(source)

16
Graph traversal

def dfs(node):
if node == destination:
return True

for neighbour in graph[node]:


found = dfs(neighbour)

if found:
return True
return False

17
What’s wrong with the above code?

18
Build Graph

def validPath(n, edges,


source, destination)

graph = defaultdict(list)

for node1, node2 in edges:


graph[node1].append(node2)
graph[node2].append(node1)

visited = set()
return dfs(source, visited)

19
Graph traversal

def dfs(node, visited):


if node == destination:
return True

visited.add(node)

for neighbour in graph[node]:


if neighbour not in visited:
found = dfs(neighbour, visited)
if found:
return True
return False
20
Visualization

21
Visualization

22
Visualization

23
Visualization

24
Visualization

25
Visualization

26
Visualization

27
Visualization

28
Visualization

29
Visualization

30
Time Complexity Space Complexity

● We have V nodes and E edges ● We have V nodes and E edges

● We will only visit a node once and ● We will store the nodes
edge once
Space Complexity = O (V)
Time Complexity = O (V + E)
- Why not O(V + E)?
- Note: if the graph is a complete - Does it matter if the graph is
graph, the time complexity is complete or not?

= O(V + (V * (V - 1)))

= O(V2)

31
Iterative Approach

32
“The Iterative implementation is just the recursive implementation done
iteratively.”

- Mahatma Gandhi

As such it obeys the 3 rules as well.

33
Rule 1: State
stack = [source]
● Since we don’t have access
to the call stack we need our visited = set([source])
own stack to keep track of
the current state.

● For the state, we only need to


keep track of the node,
because the visited set will be
kept track of separately.

34
Rule 2: Base case
● The base case is similar to the if node == destination:
recursive implementation.
return True

● We only need to know if the


current node is the target
node.

● Alternatively, we can also


finish when we have visited all
the nodes.

35
Rule 3: Iteration relation

for neighbour in graph[node]:


● We visit any adjacent vertices
if neighbour not in visited:
that have not yet been
stack.append(neighbour)
visited.
visited.add(neighbour)

● For each unvisited adjacent


vertex, we mark it as visited
and push it onto the stack.

36
We will continue to run the loop until we reach the base case or until we
visit all the nodes

we can visit by starting from the source node.

37
Implementation
Class Solution:
def validPath(self, n: int, edges: List[List[int]], source: int, destination: int) -> bool:
graph = defaultdict(list)

for node1, node2 in edges:


graph[node1].append(node2)
graph[node2].append(node1)

stack = [source]
visited = set([source])

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

for neighbour in graph[node]:


if neighbour not in visited:
stack.append(neighbour)
visited.add(neighbour)

38
return False
DFS on grid

39
DFS on grid
● Grid vertices are cells, and edges connect adjacent ones.

● DFS on a grid starts at a cell, visits its neighbors, and repeats.

● The process continues until all cells are visited.

40
Direction vectors

41
Code
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]

visited = [[False for i in range(len(grid[0]))] for j in range(len(grid))]

def inbound(row, col):


return (0 <= row < len(grid) and 0 <= col < len(grid[0]))

def dfs(grid, visited, row, col):


# base case

visited[row][col] = True

for row_change, col_change in directions:

new_row = row + row_change

new_col = col + col_change

if inbound(new_row, new_col) and not visited[new_row][new_col]:

dfs(grid, visited, new_row, new_col)


42
Practice Problem

43
DFS Applications

44
Path Finding

● DFS is a graph traversal algorithm that can be used for pathfinding.

● DFS works by starting at a vertex and exploring as far as possible along


each branch before backtracking.

45
Path Finding

● It is possible that DFS will find a longer path before finding the shortest
one.

● While DFS can be used for pathfinding, it may not always be the most
efficient or accurate method.

46
Pair Programming

47
Check if There is a
Valid Path in a Grid

48
Implementation
def hasValidPath(self, grid): def dfs(row, col):
destination = (len(grid)-1, len(grid[0]) - if (row, col) == destination:
1) return True
directions =
{1: [(0,-1),(0,1)], for row_change, col_change in directions[grid[row][col]]:
2: [(-1,0),(1,0)],
new_row= row + row_change
3: [(0,-1),(1,0)],
new_col = col + col_change
4: [(0,1),(1,0)],
5: [(0,-1),(-1,0)],
if (inbound(new_row, new_col) and
6: [(0,1),(-1,0)]}
(new_row, new_col) not in visited and

def inbound(row, col): (-row_change, -col_change) in

return 0 <= row < len(grid) directions[grid[new_row][new_col]]):

and 0 <= col < len(grid[0])


visited.add((new_row, new_col))
visited = set([(0, 0)]) found = dfs(new_row, new_col)
return dfs(0, 0) if found:
return True

return False 49
Determine if a graph is bipartite or not?

50
Bipartite Graph
● Bipartite graphs have
two sets of nodes.

● Each edge connects


nodes from different sets.

51
Is this graph Bipartite?

52
Is this graph bipartite?

53
How do we identify if a graph is bipartite or not using
dfs?

54
We use DFS Coloring

55
DFS Coloring

● Assign a color to each vertex of a graph in such a way that no two adjacent
vertices have the same color.

56
Is a graph bipartite?

57
Practice Problem

58
Implementation

def isBipartite(self, graph, n):


color = [-1 for _ len(n)]
result = True
for node in range(n):
if color[node] == -1:
color[node] = 0
result = result and dfs(node, graph)

return result

59
def dfs(node, graph):
for neighbour in graph[node]:
temp = True
if color[neighbour] == -1:
if color[node] == 0:
color[neighbour] = 1
else:
color[neighbour] = 0
temp = temp and dfs(neighbour, graph)
else:
return color[node] != color[neighbour]

return temp

60
Connected components
The connected parts of a graph are called its components

61
Finding Connected Components

A connected component of a graph is a subset of vertices in the graph such that


there is a path between any two vertices in the subset.

62
Brainstorm on how to find connected components.

63
Number of Islands

64
Here's how we can use DFS to find the number of islands

65
1. Initialize all vertices as unvisited.

66
2. For each unvisited vertex, perform a DFS starting from that vertex

67
3. Mark all visited vertices as part of the same connected component as the
starting vertex.

68
4. Repeat steps 2-3 for any remaining unvisited vertices until all vertices have
been visited.

69
After this process, the set of marked vertices for each DFS traversal will give you the
connected components of the graph.

70
Visualization

71
Implementation
def numIslands(grid):
rows = len(grid)
cols = len(grid[0])
islands = 0
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]

def dfs(row, col):


if row < 0 or row >= rows or col < 0 or col >= cols or grid[row][col] == '0':
return
grid[row][col] = '0'
for dr, dc in directions:
new_row, new_col = row + dr, col + dc
dfs(new_row, new_col)

for i in range(rows):
for j in range(cols):
if grid[i][j] == '1':
islands += 1
dfs(i, j)

72
return islands
How can we detect cycles in directed
graph using dfs?

73
Cycle detection
● We will run a series of DFS in the graph.

● Initially all vertices are colored white (0)

● From each unvisited (white) vertex, start the DFS, mark it gray (1) while
entering and mark it black (2) on exit

● If DFS moves to a gray vertex, then we have found a cycle

74
DFS Algorithm

● During traversal:
○ Only traverse to white nodes.
○ If a black node is found, skip it - it has been processed.
○ If a grey node is found, this means there is a cycle. Why?
Visualization

94
Cycle detection

95
Practice problem

96
Common Pitfalls
Infinite loops: DFS can get stuck in an infinite loop if it encounters a cycle in the
graph. To avoid this, it is important to keep track of visited nodes and avoid revisiting
them.

97
Common Pitfalls
Stack overflow & Exceeding Maximum Recursion Depth
- DFS uses a stack to keep track of nodes to visit. If the graph is very deep or has a
large number of branches, the stack can become very large and cause a stack
overflow.

- If you are using Python you are aware that the maximum recursion depth is
around 1000, in some cases we might have more than 1000 nodes in our call
stack in these cases we might be faced by maximum recursion depth
exceeded error

98
Common Pitfalls
● To fix the maximum recursion import threading
from sys import stdin,stdout,setrecursionlimit
depth exceeded error we can
from collections import defaultdict
manually increase the recursion
limit.
setrecursionlimit(1 << 30)
threading.stack_size(1 << 27)

● To fix the stack overflow error we def main():

can manually increase the stack # Enter your code here

size for our python program. pass

Taken together it will look like the


main_thread = threading.Thread(target=main)
image on the right.
main_thread.start()
main_thread.join()
99
Common Pitfalls
Choosing the wrong starting node: The output of DFS can depend on the starting
node. If the starting node is not chosen carefully, it may not be possible to reach
some nodes in the graph. It is important to consider the structure of the graph and
the problem at hand when choosing the starting node.

100
Recap

101
Recap Points
● DFS Definition and Algorithm
● Visual: summary of DFS algorithm on a graph
● DFS Applications

102
Resources
GeeksForGeeks

Visualization

103
Practice Problems
Employee-importance ✔ Surrounded-regions ✔

Number-of-provinces ✔ Minesweeper ✔

Sum-of-nodes-with-even-valued-grandparent ✔ Lowest-common-ancestor-of-deepest-leaves ✔

Max-area-of-island ✔ Recover-binary-search-tree ✔

Evaluate-division ✔

Sum-root-to-leaf-numbers ✔

Detonate-the-maximum-bombs ✔

104
Quote of the day

"Turn your face to the sun and the shadows fall


behind you"

- Maori Proverb

105

You might also like