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 ::1:: - Easy
Topics: Array, Depth First Search, Breadth First Search, Matrix
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
There
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [DFS] DFS Recursive Late Pruning Track All Visited Land In Current Island - Graph/something
def islandPerimeter(self, grid: List[List[int]]) -> int:
# Problem:
# Each land cell contributes 4 edges.
#
# But:
# If two land cells are adjacent,
# they share one edge.
# That shared edge removes 2 perimeter edges
# (one from each cell).
#
# Therefore:
# For each land cell:
# Start with 4 sides
# Subtract 1 for each adjacent land neighbor
#
# Since each shared edge is counted twice
# (once per cell), subtracting per neighbor
# naturally handles correct perimeter.
rows = len(grid)
cols = len(grid[0])
perimeter = 0
# Iterate through entire grid
# tc: O(rows * cols)
for r in range(rows):
for c in range(cols):
# Only process land cells
if grid[r][c] == 1:
# Each land cell starts with 4 sides
perimeter += 4
# Check top neighbor
if r > 0 and grid[r - 1][c] == 1:
perimeter -= 1
# Check bottom neighbor
if r < rows - 1 and grid[r + 1][c] == 1:
perimeter -= 1
# Check left neighbor
if c > 0 and grid[r][c - 1] == 1:
perimeter -= 1
# Check right neighbor
if c < cols - 1 and grid[r][c + 1] == 1:
perimeter -= 1
# overall: tc O(rows * cols)
# overall: sc O(1)
return perimeter| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [BFS] BFS Iterative Track All Visited Land In Current Island - Graph/something
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).
# Directions and grid representation
rows = len(grid)
cols = len(grid[0])
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
# Find any starting land cell to initialize the BFS
start = None
for r in range(rows):
for c in range(cols):
# Found 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 next land cell to process
r, c = queue.popleft()
# Examine all 4 neighbors
for dr, dc in directions:
# Shifting Coordinates:
nr, nc = r + dr, c + dc
# Perimeter Counting:
# If a neighbor is out of bounds of the grid, is water,
# or faces outward, then it counts towards the perimeter
if nr < 0 or nr >= rows or nc < 0 or nc >= cols or grid[nr][nc] == 0:
perimeter += 1
continue
# Neighbor Exploring:
# If neighbor is unvisited, enqueue it to continue
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 perimeter| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
133. Clone Graph ::2:: - Medium
Topics: Hash Table, Depth First Search, Breadth First Search, Graph
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 matrix, return a deep copy.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [DFS] Recursive DFS - Graph/something
def cloneGraph(self, node: Optional['Node']) -> Optional['Node']:
# Note:
# 1. Use DFS to traverse the graph
# 2. Use a hashmap to store already cloned nodes
# 3. For each node, recursively clone its neighbors
# 4. Return the cloned node corresponding to the input
# Empty Check: no islands, grid is empty
# tc: O(1)
if not node:
return None
# Deep copy for new list
# sc: O(n)
cloned = {}
# tc O(V + E) visit each node and edge once
# sc O(V), for hashmap + recursion stack
def dfs(n) -> Node:
# Process Root:
# clone current node
copy = Node(n.val)
# Add clone to hashmap
cloned[n] = copy
# Process Candidates:
# recursively clone all neighbors
for neighbor in n.neighbors:
# Early Prune: only recurse if neighbor not yet cloned
if neighbor not in cloned:
copy.neighbors.append(dfs(neighbor))
# else just grab neighbor from clone hashmap
else:
copy.neighbors.append(cloned[neighbor])
# Return:
# pass back the cloned node
return copy
# Initial Call:
# return new root of cloned graph
cloneRoot = dfs(node)
# overall: tc O(V + E)
# overall: sc O(V)
return cloneRootSolution 2: [BFS] Iterative BFS - Graph/something
def cloneGraph(self, node: Optional['Node']) -> Optional['Node']:
# Note:
# 1. Iterative BFS traversal to clone graph nodes
# 2. Use hashmap to track original -> cloned node
# 3. Process Root -> start with input node in queue
# 4. Process Candidates -> clone neighbors iteratively
# 5. Early Prune -> skip already cloned neighbors
# 6. Backtrack -> queue ensures all reachable nodes are processed
# Empty Check: no islands, grid is empty
# tc: O(1)
if not node:
return None
# Deep copy of root for new list
# sc: O(n)
cloned = {node: Node(node.val)}
# Iterative queue
# sc: O(V)
queue = deque()
# Append Original Root Node
# tc: O(1)
queue.append(node)
# While we still have nodes in original graph
# tc O(V + E), each node and edge visited once
# sc O(V), for hashmap + queue
while queue:
# Get Bottom Of Queue Node (original root):
# tc O(1)
current = queue.popleft()
# Iterate over node's neighbors
for neighbor in current.neighbors:
# Early Prune: only recurse if neighbor not yet cloned
if neighbor not in cloned:
cloned[neighbor] = Node(neighbor.val)
queue.append(neighbor)
# Grab neighbor from clone hashmap
cloned[current].neighbors.append(cloned[neighbor])
cloneRoot = cloned[node]
# overall: tc O(V + E)
# overall: sc O(V)
return cloneRoot417. Pacific Atlantic Water Flow ::2:: - Medium
Topics: Array, Depth First Search, Breadth First Search, Matrix
Intro
There is an m x n rectangular island that borders both the Pacific Ocean and Atlantic Ocean. The Pacific Ocean touches the island's left and top edges, and the Atlantic Ocean touches the island's right and bottom edges. The island is partitioned into a grid of square cells. You are given an m x n integer matrix heights where heights[r][c] represents the height above sea level of the cell at coordinate (r, c). The island receives a lot of rain, and the rain water can flow to neighboring cells directly north, south, east, and west if the neighboring cell's height is less than or equal to the current cell's height. Water can flow from any cell adjacent to an ocean into the ocean. Return a 2D list of grid coordinates result where result[i] = [ri, ci] denotes that rain water can flow from cell (ri, ci) to both the Pacific and Atlantic oceans.
| Example Input | Output |
|---|---|
| grid height (see LeetCode) | res grid |
| grid height (see LeetCode) | res grid |
Constraints:
m == heights.length
n == heights[r].length
1 ≤ m, n ≤ 200
0 ≤ heights[r][c] ≤ 105
Abstraction
Given a grid of heights, return which cells can flow to the ocean.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [DFS] DFS Recursive Reverse Flow - Graph/something
def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:
# Note:
# 1. Instead of simulating water flow forward (downhill), reverse the process,
# start from the oceans and "climb uphill" to neighbors with height >= current.
# 2. DFS is used for traversal; recursion handles the exploration
# 3. Each ocean has its own visited set
# 4. Cells visited in both traversals can reach both oceans
# 5. Intersection of visited sets gives the answer
# Empty check
if not heights:
return []
# boundaries
m, n = len(heights), len(heights[0])
# seen
pacific = set()
atlantic = set()
def dfs_TravelUphill(r: int, c: int, ocean_visited: set, prev_height: int) -> None:
# Late Prune:
# ignore cells if they are out of bounds or visited
# but specifically if they are downhill, as this means they cannot reach the island
if (r < 0 or r >= m or
c < 0 or c >= n or
(r, c) in ocean_visited or
heights[r][c] < prev_height):
return
# Process Root:
# mark cell as ocean_visited
ocean_visited.add((r, c))
# Process Candidates:
# recursively explore neighbors explore 4 directions
dfs_TravelUphill(r + 1, c, ocean_visited, heights[r][c])
dfs_TravelUphill(r - 1, c, ocean_visited, heights[r][c])
dfs_TravelUphill(r, c + 1, ocean_visited, heights[r][c])
dfs_TravelUphill(r, c - 1, ocean_visited, heights[r][c])
# Process Roots:
# start from Pacific and Atlantic edges
# These are the edge cells adjacent to each ocean from which water can "flow uphill"
# just collecting outermost edge calls
for i in range(m):
# left column
dfs_TravelUphill(i, 0, pacific, heights[i][0])
# right column
dfs_TravelUphill(i, n - 1, atlantic, heights[i][n - 1])
for j in range(n):
# top row
dfs_TravelUphill(0, j, pacific, heights[0][j])
# bottom row
dfs_TravelUphill(m - 1, j, atlantic, heights[m - 1][j])
# Cells reachable to both oceans
# tc: O(V)
intersection = list(pacific & atlantic)
# overall: tc O(m*n)
# overall: sc O(m*n)
return intersectionSolution 2: [BFS] Iterative BFS Reverse Flow - Graph/something
def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:
# Note:
# 1. Same reverse flow idea, but BFS is used instead of DFS.
# 2. BFS avoids recursion depth issues and may be easier to reason about.
# 3. Initialize queues with Pacific and Atlantic edges separately.
# 4. Traverse "uphill" from oceans, track visited cells for each.
# 5. Answer = intersection of both visited sets.
# Empty check
if not heights:
return []
# boundaries
m, n = len(heights), len(heights[0])
#
def bfs(starts: List[Tuple[int,int]]) -> set:
# Visited set for BFS traversal
ocean_visited = set(starts)
# Iterative queue
queue = deque(starts)
while queue:
# Process Root:
# Grab leftmost cell
r, c = queue.popleft()
# Process Choices:
# Recursively explore neighbors
for dr, dc in [(1,0), (-1,0), (0,1), (0,-1)]:
nr, nc = r + dr, c + dc
# Early Prune:
# skip if cell is out of bounds or visited
# but most importantly uphill as that means
if (0 <= nr < m and
0 <= nc < n and
(nr, nc) not in ocean_visited and
heights[nr][nc] >= heights[r][c]):
# mark as visited
ocean_visited.add((nr, nc))
# add neighbor for processing
queue.append((nr, nc))
# Return all visited cells for this ocean
return ocean_visited
# Process Roots -> edge cells for each ocean
# These are the edge cells adjacent to each ocean from which water can "flow uphill"
# just collecting outermost edge calls
# pacific_starts = all cells on the top row (0, j) and all cells on the left column (i, 0)
pacific_starts = []
# Top row (first row) for Pacific
for j in range(n):
pacific_starts.append((0, j))
# Left column (first column) for Pacific
for i in range(m):
pacific_starts.append((i, 0))
# Atlantic Ocean edge cells
atlantic_starts = []
# Bottom row (last row) for Atlantic
for j in range(n):
atlantic_starts.append((m - 1, j))
# Right column (last column) for Atlantic
for i in range(m):
atlantic_starts.append((i, n - 1))
pacific = bfs(pacific_starts)
atlantic = bfs(atlantic_starts)
# Cells reachable to both oceans
# tc: O(V)
intersection = list(pacific & atlantic)
# overall: tc O(m*n)
# overall: sc O(m*n)
return intersection934. Shortest Bridge ::2:: - Medium
Topics: Array, Depth First Search, Breadth First Search, Matrix
Intro
You are given an n x n binary matrix grid where 1 represents land and 0 represents water. An island is a 4-directionally connected group of 1's not connected to any other 1's. There are exactly two islands in grid. You may change 0's to 1's to connect the two islands to form one island. Return the smallest number of 0's you must flip to connect the two islands.
| Example Input | Output |
|---|---|
| grid = [[0,1],[1,0]] | 1 |
| grid = [[0,1,0],[0,0,0],[0,0,1]] | 2 |
| grid = [[1,1,1,1,1],[1,0,0,0,1],[1,0,1,0,1],[1,0,0,0,1],[1,1,1,1,1]] | 1 |
Constraints:
n == grid.length == grid[i].length
2 ≤ n ≤ 100
grid[i][j] is either 0 or 1
There are exactly two islands in grid
Abstraction
Given a list of undirected edges, determine if there are redundant connections.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [DFS + BFS] Using DFS To Mark Islands And BFS To Expand Layer By Layer To Reach Second Island - Graph/something
def shortestBridge(self, grid: List[List[int]]) -> int:
# Dimensions
n = len(grid)
# Directions for 4-way movement
directions = [(1,0), (-1,0), (0,1), (0,-1)]
# Step 1: DFS to mark the first island
def dfs(x, y, q):
grid[x][y] = 2 # mark as visited
q.append((x, y)) # add to BFS queue
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < n and 0 <= ny < n and grid[nx][ny] == 1:
dfs(nx, ny, q)
# Step 1a: Find the first island and mark it
queue = deque()
found = False
for i in range(n):
if found:
break
for j in range(n):
if grid[i][j] == 1:
dfs(i, j, queue)
found = True
break
# Step 2: BFS to expand from first island to reach second island
steps = 0
while queue:
for _ in range(len(queue)):
x, y = queue.popleft()
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < n and 0 <= ny < n:
if grid[nx][ny] == 1:
# Reached second island
return steps
elif grid[nx][ny] == 0:
# Expand water cell
grid[nx][ny] = 2
queue.append((nx, ny))
steps += 1
return -1 # just in case, though problem guarantees two islands