Jc-alt logo
jc
data structures and algorithms

LeetCode: Graphs I DFS BFS

LeetCode: Graphs I DFS BFS
17 min read
#data structures and algorithms

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 InputOutput
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

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace 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
AspectTime ComplexitySpace ComplexityTime RemarksSpace 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
AspectTime ComplexitySpace ComplexityTime RemarksSpace 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 InputOutput
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

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace 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 cloneRoot

Solution 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 cloneRoot

417. 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 InputOutput
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

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace 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 intersection

Solution 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 intersection

934. 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 InputOutput
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

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace 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