LeetCode: Graphs I DFS BFS

Graphs Intro
LeetCode problems with graph based solutions.
What is a Graph?
A graph is a data structure used to represent relationships between entities.
463. Island Perimeter ::2:: - Easy
Topics: Connected Component, Array, Depth First Search, Breadth First Search, Matrix, Grid
Intro
You are given row x col grid representing a map where grid[i][j] = 1 represents land and grid[i][j] = 0 represents water. Grid cells are connected horizontally/vertically (not diagonally). The grid is completely surrounded by water, and there is exactly one island (i.e., one or more connected land cells). The island doesn't have "lakes", meaning the water inside isn't connected to the water around the island. One cell is a square with side length 1. The grid is rectangular, width and height don't exceed 100. Determine the perimeter of the island.
| Example Input | Output |
|---|---|
| grid = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]] | 16 |
| grid = [[1]] | 4 |
| grid = [[1,0]] | 4 |
Constraints:
row == grid.length
col == grid[i].length
1 ≤ row, col ≤ 100
grid[i][j] is 0 or 1
There is exactly one island in grid
Abstraction
For every node in the single guaranteed connected component, count how many of its 4 sides touch water or the grid boundary. DFS and BFS can operate directly on the grid representation.
Pseudocode
Sol 1: DFS Cell Adjacent Counting
1. (rows, cols = width, height)
2. (perimeter = 0)
3. for (r, c) in range(rows, cols):
a. if grid[r][c] == 1:
perimeter += 4
For neighbor if neighbor is == 1
perimeter -= 1
4. return perimeter
Sol 2: BFS Iterative Track All Visited Land In Current Island
1. (rows, cols = width, height)
3. (start = any land cell (r, c))
4. (perimeter = 0)
5. (visited = {start})
6. (queue = deque([start]))
7. while queue:
a. (r, c) = queue.popleft()
b. For neighbor if neighbor == 0
perimeter += 1
continue
c. else neighbor (r, c) not in visited:
visited.add(neighbor (r, c))
queue.append(neighbor (r, c))
8. return perimeterSolution 1: [DFS] DFS Cell Adjacent Counting - Graph/DFS Recursive Grid Exploration
def islandPerimeter(self, grid: List[List[int]]) -> int:
# Note:
# Each land cell contributes 4 edges,
# but if two land cells are adjacent they share one edge,
# and that shared edge removes 2 perimeter edges,
# one from each cell.
# Thus, for each land cell start with 4 sides
# and subtract 1 for each adjacent land neighbor.
# Since each shared edge is counted twice, once per cell),
# and subtracting per neighbor naturally handles correct perimeter.
# Grid Traversal
rows = len(grid)
cols = len(grid[0])
# Total Perimeter
perimeter = 0
# Iterate through entire grid
# tc: O(r*c)
for r in range(rows):
for c in range(cols):
# Only process land cells, skip water cells
if grid[r][c] == 1:
# Starts each land cell with a value of 4,
# we subtract 1 per land neighbor due to their shared edge
perimeter += 4
# 4 Neighbors:
# - top, bottom, left, right
if r-1 >= 0 and grid[r-1][c] == 1:
perimeter -= 1
if r+1 <= rows-1 and grid[r + 1][c] == 1:
perimeter -= 1
if c-1 >= 0 and grid[r][c-1] == 1:
perimeter -= 1
if c-1 <= cols-1 and grid[r][c+1] == 1:
perimeter -= 1
# overall: tc O(r*c)
# overall: sc O(1)
return perimeterSolution 2: [BFS] BFS Iterative Track All Visited Land In Current Island - Graph/BFS Iterative Grid Exploration Skip If Seen Already
def islandPerimeter(self, grid: List[List[int]]) -> int:
# Note:
# Each land cell contributes 4 edges to the perimeter.
# If a land cell's neighbor is also land, that shared edge
# is internal to the island and does not belong to the perimeter.
# For each land cell, count only the sides that face "outward",
# either off the grid entirely, or into a water cell (0).
# Grid Traversal
rows = len(grid)
cols = len(grid[0])
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
# Pick any land cell to initialize the BFS
start = None
for r in range(rows):
for c in range(cols):
# Found some land, save coordinates
if grid[r][c] == 1:
start = (r, c)
break
if start:
break
# Total perimeter count
perimeter = 0
# Track land we have already visited to avoid
# sc: O(n)
visited = {start}
# BFS Iterative Queue:
# holds land cells we have queued to expand
queue = deque([start])
while queue:
# Pop land cell
r, c = queue.popleft()
# Examine all 4 neighbors
for dr, dc in directions:
# New Neighbor Coordinates
nr, nc = r + dr, c + dc
# Valid perimeter neighbor has either neighbor of:
# - out of bounds of the grid
# - water cell
# - out of bounds and water
if nr < 0 or nr >= rows or
nc < 0 or nc >= cols or
grid[nr][nc] == 0:
perimeter += 1
# Early Pruning:
# no need to explore a perimeter neighbor
continue
# Neighbor was inbounds and land,
# if has not been visited, enqueue it to explore:
if (nr, nc) not in visited:
visited.add((nr, nc))
queue.append((nr, nc))
# overall: tc O(r*c)
# overall: sc O(r*c)
return perimeter733. Flood Fill ::2:: - Easy
Topics: Connected Component, Array, Depth First Search, Breadth First Search, Matrix, Grid
Intro
You are given an image represented by an m x n grid of integers image, where image[i][j] represents the pixel value of the image. You are also given three integers sr, sc, and color. Your task is to perform a flood fill on the image starting from the pixel image[sr][sc]. To perform a flood fill: Begin with the starting pixel and change its color to color. Perform the same process for each pixel that is directly adjacent (pixels that share a side with the original pixel, either horizontally or vertically) and shares the same color as the starting pixel. Keep repeating this process by checking neighboring pixels of the updated pixels and modifying their color if it matches the original color of the starting pixel. The process stops when there are no more adjacent pixels of the original color to update. Return the modified image after performing the flood fill.
| Example Input | Output |
|---|---|
| image = [[1,1,1],[1,1,0],[1,0,1]], sr = 1, sc = 1, color = 2 | [[2,2,2],[2,2,0],[2,0,1]] |
| image = [[0,0,0],[0,0,0]], sr = 0, sc = 0, color = 0 | [[0,0,0],[0,0,0]] |
Constraints:
m == image.length
n == image[i].length
1 ≤ m, n ≤ 50
0 ≤ image[i][j], color < 2^16
0 ≤ sr < m
0 ≤ sc < n
Abstraction
Change every node in the connected component containing the starting pixel (sr, sc)to the target color. Given a grid representation of a graph. DFS and BFS can operate directly on the grid representation.
Pseudocode
Sol 1: DFS Recursive Same Color Neighbor Fill
1. (rows, cols = width, height)
2. (from_color = image[start_row][start_column])
3. if from_color == to_color:
return image
4. dfs(r, c):
a. if out of bounds:
return
b. if image[r][c] != start_color:
return
c. image[r][c] = from_color
for neighbor:
dfs(neighbor (r, c))
5. dfs(, sc)
6. return image
Sol 2: BFS Iterative Track All Repainted Pixels In Current Region
1. (rows, cols = width, height)
2. (from_color = image[start_row][start_column])
4. if from_color == to_color:
return image
5. queue = deque([(start_row, start_column)])
6. while queue:
a. (r, c) = queue.popleft()
b image[r][c] = color
c. for each neighbor (nr, nc):
if out of bounds:
continue
if image[nr][nc] != start_color:
continue
queue.append((nr, nc))
8. return imageSolution 1: [DFS] DFS Recursive Same Color Neighbor Fill - Graph/DFS Recursive Grid Exploration
def floodFill(self, image: List[List[int]], sr: int, sc: int, color: int) -> List[List[int]]:
# Note:
# Starting from (sr, sc), repaint every pixel that is
# connected to it (up/down/left/right) and shares the
# same original color. Recursion naturally stops once
# we hit a pixel that doesn't match the original color,
# including pixels we've already repainted (since after
# repainting they no longer match the old color, unless
# old color == new color, which we guard against).
# Grid Traversal
rows = len(image)
cols = len(image[0])
# Original color we are replacing
start_color = image[sr][sc]
# Edge case: if the new color is the same as the old color,
# repainting would recurse forever (never "changes" color),
# so just return the image unchanged.
if start_color == color:
return image
def dfs(r, c):
# Out of bounds, stop
if r < 0 or r >= rows or c < 0 or c >= cols:
return
# Not the color we're flooding, stop
if image[r][c] != start_color:
return
# Repaint current pixel
image[r][c] = color
# 4 Neighbors:
# - top, bottom, left, right
dfs(r - 1, c)
dfs(r + 1, c)
dfs(r, c - 1)
dfs(r, c + 1)
# Kick off recursion from the starting pixel
# tc: O(r*c)
dfs(sr, sc)
# overall: tc O(r*c)
# overall: sc O(r*c) -- recursion stack in the worst case (all one color)
return imageSolution 2: [BFS] BFS Iterative Track All Repainted Pixels In Current Region - Graph/BFS Iterative Grid Exploration Skip If Seen Already
def floodFill(self, image: List[List[int]], sr: int, sc: int, color: int) -> List[List[int]]:
# Note:
# Starting from (sr, sc), repaint every pixel connected to it
# (up/down/left/right) that shares the original color.
# Use a queue to expand outward layer by layer, only enqueueing
# pixels that still match the original color and haven't been
# repainted yet.
# Grid Traversal
rows = len(image)
cols = len(image[0])
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
# Original color we are replacing
start_color = image[sr][sc]
# Edge case: if the new color is the same as the old color,
# there is nothing to do, and checking image[r][c] != color
# below would never be true, so short-circuit here.
if start_color == color:
return image
# BFS Iterative Queue:
# holds pixels we have queued to expand
queue = deque([(sr, sc)])
# Repaint the starting pixel immediately so we never
# re-enqueue it
image[sr][sc] = color
while queue:
# Pop pixel
r, c = queue.popleft()
# Examine all 4 neighbors
for dr, dc in directions:
# New Neighbor Coordinates
nr, nc = r + dr, c + dc
# Valid neighbor to repaint has all of:
# - in bounds of the grid
# - still matches the original color
if nr < 0 or nr >= rows or nc < 0 or nc >= cols:
continue
if image[nr][nc] != start_color:
continue
# Repaint and enqueue to expand from later
image[nr][nc] = color
queue.append((nr, nc))
# overall: tc O(r*c)
# overall: sc O(r*c)
return image133. Clone Graph ::2:: - Medium
Topics: Deep Copy, Connected Component, Hash Table, Depth First Search, Breadth First Search, Graph Theory, Adjacency List
Intro
Given a reference of a node in a connected undirected graph. Return a deep copy (clone) of the graph. Each node in the graph contains a value (int) and a list
(List[Node]) of its neighbors. class Node ( public int val; public List[Node] neighbors; ) Test case format: For simplicity, each node's value is the same as the node's index (1-indexed). For example, the first node with val == 1, the second node with val == 2, and so on. The graph is represented in the test case using an adjacency list. An adjacency list is a collection of unordered lists used to represent a finite graph. Each list describes the set of neighbors of a node in the graph. The given node will always be the first node with val = 1. You must return the copy of the given node as a reference to the cloned graph.
| Example Input | Output |
|---|---|
| adjList = [[2,4],[1,3],[2,4],[1,3]] | [[2,4],[1,3],[2,4],[1,3]] |
| adjList = [[]] | [[]] |
| adjList = [] | [] |
Constraints:
The number of nodes in the graph is in the range [0, 100].
1 ≤ Node.val ≤ 100
Node.val is unique for each node.
There are no repeated edges and no self-loops in the graph.
The Graph is connected and all nodes can be visited starting from the given node.
Abstraction
Given a graph represented by an adjacency list, return a deep copy.
Pseudocode
Sol 1: Recursive DFS
1. if not node: return None
2. clonedSeen = {}
3. dfs(n):
a. copy = Node(n.val)
b. clonedSeen[n] = copy
c. for each neighbor in n.neighbors:
if neighbor not in clonedSeen:
copy.neighbors.append(dfs(neighbor))
else:
copy.neighbors.append(clonedSeen[neighbor])
d. return copy
4. return dfs(node)
Sol 2: Iterative BFS
1. if not node: return None
2. clonedSeen = {node: Node(node.val)}
3. queue = deque([node])
4. while queue:
a. current = queue.popleft()
b. for each neighbor in current.neighbors:
if neighbor not in clonedSeen:
clonedSeen[neighbor] = Node(neighbor.val)
queue.append(neighbor)
clonedSeen[current].neighbors.append(clonedSeen[neighbor])
5. return clonedSeen[node]Solution 1: [DFS] Recursive DFS - Graph/DFS Recursive Grid Exploration Skip If Seen Already
def cloneGraph(self, node: Optional['Node']) -> Optional['Node']:
# Note:
# 1. Iterate over all nodes
# 2. For each new node, clone and add to hashmap,
# if neighbors are not in seen set, explore
# 3. HashMap() will act as clone hash set and tracking seen set
# Empty Check:
# nothing to copy
if not node:
return None
# Deep copy for new list
# sc: O(n)
clonedSeen = {}
# tc: O(V + E) visit each node and edge once
# sc: O(V) for hashmap + recursion stack
def dfs(n) -> Node:
# New Node Found:
# clone and add to hashmap
copy = Node(n.val)
clonedSeen[n] = copy
# If neighbors are not in seen set, explore
for neighbor in n.neighbors:
# Not Yet Seen:
if neighbor not in clonedSeen:
copy.neighbors.append(dfs(neighbor))
# Already seen, grab clone from hashmap:
else:
copy.neighbors.append(clonedSeen[neighbor])
# Pass back the cloned node
return copy
# Grab root of cloned graph
cloneRoot = dfs(node)
# overall: tc O(V + E)
# overall: sc O(V)
return cloneRootSolution 2: [BFS] Iterative BFS - Graph/BFS Iterative Grid Exploration Skip If Seen
def cloneGraph(self, node: Optional['Node']) -> Optional['Node']:
# Note:
# BFS Iterative Level By Level:
# 1. Start at any node
# 2. For each new node, make a clone, and
# check if neighbors are not in seen set, if so explore
# 3. HashMap will serve as clone hashmap and seen set
# Empty Check:
# graph has no nodes, nothing to clone
if not node:
return None
# Deep copy for new list
# sc: O(n)
clonedSeen = {}
clonedSeen[node] = Node(node.val)
# BFS Iterative Queue:
# sc: O(V)
queue = deque()
queue.append(node)
# tc: O(V + E), each node and edge visited once
# sc: O(V), for hashmap + queue
while queue:
# Pop node
current = queue.popleft()
# Explore neighbors
for neighbor in current.neighbors:
# Not yet cloned:
if neighbor not in clonedSeen:
clonedSeen[neighbor] = Node(neighbor.val)
queue.append(neighbor)
# Grab clone from hashmap:
clonedSeen[current].neighbors.append(clonedSeen[neighbor])
# Grab root of cloned graph
cloneRoot = clonedSeen[node]
# overall: tc O(V + E)
# overall: sc O(V)
return cloneRoot329. Longest Increasing Path in a Matrix ::2:: - Hard
Topics: Connected Component, Array, Dynamic Programming, Depth First Search, Breadth First Search, Rule Based Graph, Graph Theory, Topological Sort, Memoization, Matrix, Directed Acyclic Graph
Intro
Given an m x n integers matrix, return the length of the longest increasing path in matrix. From each cell, you can either move in four directions: left, right, up, or down. You may not move diagonally or move outside the boundary (i.e., wrap-around is not allowed).
| Example Input | Output |
|---|---|
| matrix = [[9,9,4],[6,6,8],[2,1,1]] | 4 |
Constraints:
m == matrix.length
n == matrix[i].length
1 ≤ m, n ≤ 200
0 ≤ matrix[i][j] ≤ 2^31 - 1
Abstraction
Given a tree represented as a matrix, find the longest increasing path where nodes are representing by cells in the graph and connections between nodes are represented by up, down, right, left neighbor connections.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [DFS] DFS - Tree/DFS Post Order Traversal
Solution 2: [BFS] BFS Topological Sort - Tree/BFS Reverse Level Order Traversal