Jc-alt logo
jonathancamberos
LeetCode: Graphs
49 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.

Graph Characteristics

  1. Vertices (n): entities (e.g. nodes)
  2. Edges (m): connections between entities
  3. Direction: Edges can be directed (nodes pointing to nodes), or undirected (no one pointing)
  4. Weight: Edges can have weight (e.g. cost, time) or unweighted
  5. Unordered: unlike tree, heaps, etc, graphs allow cycles, multiple paths, and varying connectivity
  6. Representation: Graphs are stored as adjacency matrix, adjacency list, or edge lists depending on the use case

Graph Representations: Adjacency Matrix

Graph and Matrix:

        1---2
         \ /
          3


        1  2  3
    1  [0, 1, 1]
    2  [1, 0, 1]
    3  [1, 1, 0]

A[i][j] = 1 -> edge exists between i and j

A[i][j] = 0 -> no edge between i and j

Space complexity O(n2) -> better for dense graphs

Graph Representations: Adjacency List

Graph and List

        1---2
         \ /
          3

    1: [2, 3]
    2: [1, 3]
    3: [1, 2]

Each vertex points to its neighbors

Space complexity: O(n + m) -> efficient for sparse graphs

Graph Representations: Edge List

Graph and Edge List

        1---2
         \ /
          3

    Edges:
    (1, 2)
    (1, 3)
    (2, 3)

Stores all edges explicitly as (u, v) pairs

Space complexity O(m) -> useful for algorithms that only need edges

Simplest representation for algorithms that only care about edges.

Graph Application: DFS Pre Order

Use pre order DFS when you need to process a node immediately before exploring its neighbors

Ex: Sink islands or mark visited immediately in a grid

def dfs_pre(r, c, grid):
    if not (0 <= r < len(grid) and 0 <= c < len(grid[0])) or grid[r][c] == '0':
        return

    grid[r][c] = '0'  # mark visited (pre-order processing)

    for dr, dc in [(1,0), (-1,0), (0,1), (0,-1)]:
        dfs_pre(r + dr, c + dc, grid)

# Example: numIslands uses pre-order DFS to flood fill

Graph Application: DFS In Order (Binary Tree Only)

Use in order DFS mainly for binary trees where left -> right order matters

Ex: Extract sorted values from a BST

def inorder(node, res):
    if not node:
        return
    inorder(node.left, res)
    res.append(node.val)  # process node in between left/right
    inorder(node.right, res)

# Example: LeetCode 98, Validate BST or BST inorder traversal

Graph Application: DFS Post Order

Use post order DFS when you want to process a node after exploring all its neighbors.

Ex: Calculate size of connected components or backtracking cleanup

def dfs_post(r, c, grid):
    if not (0 <= r < len(grid) and 0 <= c < len(grid[0])) or grid[r][c] == '0':
        return 0

    grid[r][c] = '0'  # mark visited
    size = 1  # count current cell

    for dr, dc in [(1,0), (-1,0), (0,1), (0,-1)]:
        size += dfs_post(r + dr, c + dc, grid)

    return size  # post-order: aggregate after children

# Example: maxAreaOfIsland uses post-order DFS to sum area

Graph Application: BFS Level Order / Flood Fill

Use BFS when you need shortest path in unweighted graphs, or to expand layers level by level.

Ex: Shortest path in 2D grid:

def bfs_shortest(grid, start):
    queue = deque([(*start, 0)])  # (r, c, distance)
    visited = set([start])

    while queue:
        r, c, dist = queue.popleft()
        if grid[r][c] == 'target':
            return dist

        for dr, dc in [(1,0), (-1,0), (0,1), (0,-1)]:
            nr, nc = r + dr, c + dc
            if 0 <= nr < len(grid) and 0 <= nc < len(grid[0]) and (nr,nc) not in visited:
                visited.add((nr,nc))
                queue.append((nr,nc, dist + 1))

Graph Application: Union Find Disjoint Set

Use Union Find to efficiently track connected components in dynamic graphs.

Ex: Count islands or merge friend groups

    def numIslandsUnion(grid):
        parent = {}
        
        def find(x):
            if parent[x] != x:
                parent[x] = find(parent[x])
            return parent[x]
        
        def union(x, y):
            parent[find(x)] = find(y)
        
        for r in range(len(grid)):
            for c in range(len(grid[0])):
                if grid[r][c] == '1':
                    parent[(r,c)] = (r,c)
        
        for r in range(len(grid)):
            for c in range(len(grid[0])):
                if grid[r][c] == '1':
                    for dr, dc in [(1,0),(0,1)]:
                        nr, nc = r+dr, c+dc
                        if (nr,nc) in parent:
                            union((r,c),(nr,nc))
        
        roots = {find(x) for x in parent}
        return len(roots)

200. Number of Islands ::4:: - Medium

Topics: Array, Depth First Search, Breadth First Search, Union Find, Matrix

Intro

Given an m x n 2D binary grid grid which represents a
map of '1's (land) and '0's (water), return the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by
water.

Example InputOutput
grid (see LeetCode)1
grid (see LeetCode)3

Constraints:

m == grid.length

n == grid[i].length

1 ≤ m, n ≤ 300

grid[i][j] is '0' or '1'

Abstraction

Given a graph, return the number of islands, where an islands is a cell that is surrounded by water, and islands can connect horizontally or vertically.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: Recursive DFS with Visited Set - Graph/something

    def numIslands(self, grid: List[List[str]]) -> int:

        # Note:
        # Recursive DFS traversal: mark visited cells to avoid re-processing
        # 1. Process Root -> check if current cell is '1'
        # 2. Process Choices -> explore four neighbors (up, down, left, right)
        # 3. Backtrack -> recursion returns after marking all connected land
        # Result: increment island count per root land cell
        
        # Empty check
        if not grid:
            return 0
        
        # boundaries
        rows, cols = len(grid), len(grid[0])

        # tracking
        visited = set()

        # count
        islands = 0

        # visits any land touching (r, c)
        def dfs(r, c):

            # Late Pruning -> skip if out-of-bounds, water, or already visited
            if (r < 0 or r >= rows or 
                c < 0 or c >= cols or 
                grid[r][c] == "0" or 
                (r, c) in visited):
                return

            # Process Root -> mark current cell as visited land
            visited.add((r, c))

            # Explore -> visit all 4 neighbors
            dfs(r + 1, c)
            dfs(r - 1, c)
            dfs(r, c + 1)
            dfs(r, c - 1)

            # Backtrack -> implicit, no undo needed

        # iterate over grid
        for r in range(rows):
            for c in range(cols):

                # Process Root -> unvisited land found
                if grid[r][c] == '1' and (r, c) not in visited:
                    islands += 1
                    dfs(r, c)

        # overall: time complexity
        # overall: space complexity
        return islands

Solution 2: Recursive DFS with In Place Flood Fill - Graph/something

    def numIslands(self, grid: List[List[str]]) -> int:
        # Note:
        # Recursive DFS flood fill: mark visited land in-place
        # 1. Process Root -> check if current cell is '1'
        # 2. Process Choices -> explore four neighbors
        # 3. Backtrack -> recursion returns after marking all connected land
        # Result: increment island count per root land cell
        
        # Empty check
        if not grid:
            return 0
        
        # boundaries
        rows, cols = len(grid), len(grid[0])

        # count
        islands = 0

        # visits any land touching (r, c)
        def dfs(r, c):

            # Late Pruning -> skip out-of-bounds or water
            if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] == '0':
                return

            # Build / Mark -> sink current land
            grid[r][c] = '0'

            # Explore -> visit all 4 neighbors
            dfs(r + 1, c)
            dfs(r - 1, c)
            dfs(r, c + 1)
            dfs(r, c - 1)

            # Backtrack -> implicit, no undo needed

        for r in range(rows):
            for c in range(cols):

                # Process Root -> unvisited land found
                if grid[r][c] == '1':
                    islands += 1
                    dfs(r, c)

        # overall: time complexity O(m * n)
        # overall: space complexity O(m * n) worst-case recursion depth
        return islands

Solution 3: Iterative BFS with In Place Flood Fill - Graph/something

    def numIslands(self, grid: List[List[str]]) -> int:

        # Note:
        # Iterative BFS flood fill: explore entire island per root land
        # 1. Process Root -> enqueue starting land cell
        # 2. Process Choices -> explore neighbors iteratively
        # 3. Backtrack -> implicit by queue processing
        # Result: increment island count per root land cell

        # Empty check
        if not grid:
            return 0

        # boundaries
        rows, cols = len(grid), len(grid[0])
        islands = 0

        # visits any land touching (r, c)
        def bfs(r, c):

            # iterative queue
            queue = deque()

            # start iteration
            queue.append((r, c))

            # Build -> sink root island
            grid[r][c] = '0'

            while queue:

                # grab 
                cr, cc = queue.popleft()

                # Explore -> neighbors in 4 directions
                for dr, dc in [(1,0), (-1,0), (0,1), (0,-1)]:
                    nr, nc = cr + dr, cc + dc

                    # Late Pruning -> skip out-of-bounds or water
                    if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == '1':
                        grid[nr][nc] = '0'
                        queue.append((nr, nc))

        # iterate over grid
        for r in range(rows):
            for c in range(cols):

                # Process Root -> unvisited land found
                if grid[r][c] == '1':
                    islands += 1
                    bfs(r, c)

        # overall: time complexity O(m * n)
        # overall: space complexity O(m * n) worst-case queue
        return islands

Solution 4: Union Find - Graph/something

    def numIslands(self, grid: List[List[str]]) -> int:

        # Note:
        # Union-Find approach: treat each land cell as a node
        # 1. Process Root -> initialize parent and rank for each land
        # 2. Process Choices -> union adjacent lands (right and down)
        # 3. Backtrack -> implicit via union
        # Result: number of connected components = number of islands

        # Empty check
        if not grid:
            return 0
        
        # boundaries
        rows, cols = len(grid), len(grid[0])

        # parent and rank (size)
        parent = {}
        rank = {}

        # initialize unions -> set parent to self rank to 0
        for r in range(rows):
            for c in range(cols):
                if grid[r][c] == '1':
                    parent[(r, c)] = (r, c)
                    rank[(r, c)] = 0

        # Find Path compression
        def find(x):
            if parent[x] != x:
                parent[x] = find(parent[x])
            return parent[x]

        # Union between sets
        def union(x, y):
            rootX, rootY = find(x), find(y)
            if rootX == rootY:
                return

            # union by rank (height based)
            if rank[rootX] > rank[rootY]:
               parent[rootY] = rootX
            elif rank[rootX] < rank[rootY]:
                parent[rootX] = rootY
            else:
                # height of tree only increases by 1 on equal size trees
                # A=3, B=2 -> B will point to Root A, leading to height of 3
                parent[rootY] = rootX
                rank[rootX] += 1

            # union by size (weight based)
            # if rank[rootX] > rank[rootY]:
            #     rank[rootX] += rank[rootY]
            #     parent[rootY] = rootX
            # else rank[rootX] < rank[rootY]:
            #     rank[rootY] += rank[rootX]
            #     parent[rootX] = rootY

        # iterate over grid
        for r in range(rows):
            for c in range(cols):

                # island found
                if grid[r][c] == '1':

                    # Process Choices -> 
                    # Union only down and right to avoid double union
                    for dr, dc in [(1,0), (0,1)]:
                        nr, nc = r + dr, c + dc

                        # Early Candidate Pruning -> boundaries
                        if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == '1':
                            union((r, c), (nr, nc))

        # Unique root count -> Unique Island Counts
        roots = set(find(cell) for cell in parent)

        # overall: time complexity
        # overall: space complexity
        return len(roots)

695. Max Area of Island ::3:: - Medium

Topics: Array, Depth First Search, Breadth First Search, Union Find, Matrix

Intro

You are given an m x n binary matrix grid. An island is a group of 1's (representing land) connected 4-directionally
(horizontal or vertical.) You may assume all four edges of the grid are surrounded by water. The area of an island is the number of cells with a value 1 in the island. Return the maximum area of an island in grid. If there is no island, return 0.

Example InputOutput
grid (see LeetCode)6
grid (see LeetCode)0

Constraints:

m == grid.length

n == grid[i].length

1 ≤ m, n ≤ 50

grid[i][j] is either 0 or 1

Abstraction

Given a graph, return the largest island.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: Recursive DFS with Flood Fill - Graph/something

    def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
        # Note:
        # 1. Treat grid as a graph: 1 = land node, edges = 4-direction adjacency
        # 2. DFS flood fill sinks land in-place to mark visited cells
        # 3. Track area of each island during DFS
        # 4. Return the maximum island area

        # Empty check
        if not grid:
            return 0

        # Boundaries
        m, n = len(grid), len(grid[0])

        # max
        max_area = 0

        def dfs(r: int, c: int) -> int:
            # Late Candidate Prune -> : out of bounds or water
            if r < 0 or c < 0 or r >= m or c >= n:
                return 0

            # Late Candidate Prune -> : water
            if grid[r][c] == 0:
                return 0

            # Process Root -> sink land
            grid[r][c] = 0

            # Process Root -> count land from root
            area = 1

            # Process Choices -> explore neighbors, add to land count
            area += dfs(r + 1, c)
            area += dfs(r - 1, c)
            area += dfs(r, c + 1)
            area += dfs(r, c - 1)

            # Backtrack -> return root land count to parent
            return area
    
        # iterate over grid
        for r in range(m):
            for c in range(n):

                # Process Root -> unvisited land found
                if grid[r][c] == 1:

                    # grab area of new island, check with max
                    island_area = dfs(r, c)
                    max_area = max(max_area, island_area)

        # overall: time complexity O(m * n)
        # overall: space complexity O(m * n) worst-case (DFS recursion stack)
        return max_area

Solution 2: Iterative BFS with Flood Fill - Graph/something

    def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
        # Note:
        # 1. Treat grid as a graph: 1 = land, edges = 4-direction adjacency
        # 2. BFS explores each island iteratively using a queue
        # 3. Sink land cells in-place to mark visited
        # 4. Track area of each island, return maximum

        # Empty check
        if not grid:
            return 0

        # Boundaries
        m, n = len(grid), len(grid[0])

        # max
        max_area = 0

        def bfs(r, c):
            # start BFS for this island
            area = 0
            queue = deque([(r, c)])
            grid[r][c] = 0  # mark visited

            while queue:
                cr, cc = queue.popleft()
                area += 1

                for dr, dc in [(1,0), (-1,0), (0,1), (0,-1)]:
                    nr, nc = cr + dr, cc + dc
                    if 0 <= nr < m and 0 <= nc < n and grid[nr][nc] == 1:
                        grid[nr][nc] = 0  # mark visited
                        queue.append((nr, nc))

            return area


        # iterate over grid 
        for r in range(m):
            for c in range(n):
                if grid[r][c] == 1:
                    island_area = bfs(r, c)
                    max_area = max(max_area, island_area)


        # overall: time complexity O(m * n)
        # overall: space complexity O(m * n) worst-case (queue holds all land cells)
        return max_area

Solution 3: Union Find - Graph/something

    def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
        
        # Note:
        # Union-Find approach to track connected components by size
        # 1. Process Root -> initialize parent and size for each land cell
        # 2. Process Candidates -> union adjacent land cells (down/right)
        # 3. Early Prune -> skip out-of-bounds
        # 4. Late Prune -> skip water cells
        # Result: max size among all connected components = max island area

        # Empty check
        if not grid:
            return 0

        # Boundaries
        m, n = len(grid), len(grid[0])

        # Union find info
        parent = {}
        size = {}

        # Find Path Compression
        def find(x):
            if parent[x] != x:
                parent[x] = find(parent[x])
            return parent[x]

        # Union two sets
        def union(x, y):
            
            rootX, rootY = find(x), find(y)
            if rootX == rootY:
                return
            
            # Union by size
            if size[rootX] >= size[rootY]:
                parent[rootY] = rootX
                size[rootX] += size[rootY]
            else:
                parent[rootX] = rootY
                size[rootY] += size[rootX]

        # Init Union Find,
        # set parent to self and size to 0
        for r in range(m):
            for c in range(n):
                if grid[r][c] == 1:
                    parent[(r, c)] = (r, c)
                    size[(r, c)] = 1

        # Iterate over grid and Union adjacent 
        # bottom and right land cells (to avoid double union)
        for r in range(m):
            for c in range(n):
                if grid[r][c] == 1:

                    # Process Choices -> 
                    # Union only down and right to avoid double union
                    for dr, dc in [(1,0), (0,1)]:
                        nr, nc = r + dr, c + dc
                        if 0 <= nr < m and 0 <= nc < n and grid[nr][nc] == 1:
                            union((r, c), (nr, nc))

        # grab the largest union size
        largestIsland = max(size.values(), default=0)

        # overall: time complexity
        # overall: space complexity
        return largestIsland

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: 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
        if not node:
            return None

        # original -> deep copy
        cloned = {}

        def dfs(n) -> Node:

            # Early Prune -> node already cloned
            if n in cloned:
                return cloned[n]

            # Process Root -> clone current node
            copy = Node(n.val)
            cloned[n] = copy

            # Process Candidates -> recursively clone all neighbors
            for neighbor in n.neighbors:
                copy.neighbors.append(dfs(neighbor))

            # Backtrack -> return cloned node
            return copy

        # grab root of cloned graph
        cloneRoot = dfs(node)

        # overall: time complexity O(V + E), visit each node and edge once
        # overall: space complexity O(V), for hashmap + recursion stack
        return cloneRoot

Solution 2: 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
        if not node:
            return None

        # clone root
        cloned = {node: Node(node.val)}

        # iterative queue
        queue = deque([node])

        # iterate over all nodes
        while queue:

            # get previous roots neighbor
            current = queue.popleft()

            # iterate over neighbors
            for neighbor in current.neighbors:

                # Early Prune -> neighbor already cloned
                if neighbor not in cloned:
                    cloned[neighbor] = Node(neighbor.val)
                    queue.append(neighbor)

                # Process Candidate -> connect cloned neighbor
                cloned[current].neighbors.append(cloned[neighbor])

        # overall: time complexity O(V + E), each node and edge visited once
        # overall: space complexity O(V), for hashmap + queue
        return cloned[node]

994. Rotting Oranges ::2:: - Medium

Topics: Array, Breadth First Search, Matrix

Intro

You are given an m x n grid where each cell can have one of three values: 0 representing an empty cell, 1 representing a fresh orange, or 2 representing a rotten orange. Every minute, any fresh orange that is 4-directionally adjacent to a rotten orange becomes rotten. Return the minimum number of minutes that must elapse until no cell has a fresh orange. If this is impossible, return -1.

Example InputOutput
grid = [[2,1,1],[1,1,0],[0,1,1]]4
grid = [[2,1,1],[0,1,1],[1,0,1]]-1
grid = [[0,2]]0

Constraints:

m == grid.length

n == grid[i].length

1 ≤ m, n ≤ 10

grid[i][j] is 0, 1, or 2.

Abstraction

Given a grid with oranges, return how much time until no fresh oranges remain.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: Iterative BFS Rotten Multi Source with Global Minutes Overwrite - Graph/something

    def orangesRotting(self, grid: List[List[int]]) -> int:
        # Note:
        # 1. Start BFS from all initially rotten oranges (multi-source BFS)
        # 2. Each BFS level = 1 minute of rotting spread
        # 3. Track fresh oranges count, decrement when they rot
        # 4. Return minutes if all fresh rot, else -1

        # Empty check
        if not grid:
            return -1

        # boundaries
        m, n = len(grid), len(grid[0])

        # count
        fresh = 0

        # time
        minutes = 0

        # iterative queue
        queue = deque()

        # set up multi source BFS
        for r in range(m):
            for c in range(n):

                # Append Rotten Source to BFS
                if grid[r][c] == 2:
                    # (row, col, minute)
                    queue.append((r, c, 0))
                
                # Add to fresh count
                elif grid[r][c] == 1:
                    fresh += 1

        # iterate over rotten sources
        while queue:

            # next rotten orange
            # minutes will be overwritten by pop,
            # last pop will have the longest and final minute count
            r, c, minutes = queue.popleft()

            # Process Choices -> 
            for dr, dc in [(1,0), (-1,0), (0,1), (0,-1)]:
                nr, nc = r + dr, c + dc

                # Early Prune -> : valid boundary and fresh orange
                if 0 <= nr < m and 0 <= nc < n and grid[nr][nc] == 1:
                    grid[nr][nc] = 2
                    fresh -= 1
                    
                    # add to time and pass down
                    queue.append((nr, nc, minutes + 1))

        # total time taken if no oranges remain, else fresh orange remain so -1
        res = minutes if fresh == 0 else -1

        # overall: time complexity
        # overall: space complexity
        return res

Solution 2: Iterative BFS Rotten Multi Source with Level Processing Minutes Trigger - Graph/something

    def orangesRotting(self, grid: List[List[int]]) -> int:
        
        # Note:
        # 1. Instead of storing time in queue, process BFS by levels
        # 2. Each level of queue expansion corresponds to +1 minute
        # 3. Track number of fresh oranges, ensure all rot or return -1

        # Empty check
        if not grid:
            return -1

        # boundaries
        m, n = len(grid), len(grid[0])

        # count
        fresh = 0

        # time
        minutes = 0

        # iterative
        queue = deque()

        # set up multi source BFS
        for r in range(m):
            for c in range(n):

                # append rotten source to queue
                if grid[r][c] == 2:
                    queue.append((r, c))

                # add to fresh count
                elif grid[r][c] == 1:
                    fresh += 1

        # iterate over rotten sources
        while queue and fresh > 0:

            # Process roots -> process all sources at this level
            for _ in range(len(queue)):

                # next candidate
                r, c = queue.popleft()

                # Process Choices -> : visit neighbors
                for dr, dc in [(1,0), (-1,0), (0,1), (0,-1)]:
                    nr, nc = r + dr, c + dc

                    # Early Pruning -> valid bounds and fresh fruit
                    if 0 <= nr < m and 0 <= nc < n and grid[nr][nc] == 1:
                        grid[nr][nc] = 2
                        fresh -= 1

                        # add next rotten source to queue
                        queue.append((nr, nc))

            # iterate tick for next source level
            minutes += 1

        # total time taken if no oranges remain, else fresh orange remain so -1
        res = minutes if fresh == 0 else -1

        # overall: time complexity
        # overall: space complexity
        return res

417. Pacific Atlantic Water Flow ::2:: - Medium

Topics: Array, 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: Recursive DFS 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. Perform DFS from Pacific edges and Atlantic edges separately.
        # 3. Any cell visited in both traversals can reach both oceans.
        # 4. Collect intersection of visited sets.

        # Empty check
        if not heights:
            return []

        # boundaries
        m, n = len(heights), len(heights[0])

        # seen
        pacific = set()
        atlantic = set()

        def dfs(r: int, c: int, visited: set, prev_height: int) -> None:
            
            # Early Prune -> out of bounds, visited, or downhill
            if ((r, c) in visited or r < 0 or c < 0 or r >= m or c >= n 
                or heights[r][c] < prev_height):
                return

            # Process Root -> mark as visited
            visited.add((r, c))

            # Process Candidates -> explore 4 directions
            dfs(r + 1, c, visited, heights[r][c])
            dfs(r - 1, c, visited, heights[r][c])
            dfs(r, c + 1, visited, heights[r][c])
            dfs(r, c - 1, 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(i, 0, pacific, heights[i][0])
            # right column
            dfs(i, n - 1, atlantic, heights[i][n - 1])
        for j in range(n):
            # top row
            dfs(0, j, pacific, heights[0][j])
            # bottom row
            dfs(m - 1, j, atlantic, heights[m - 1][j])

        # Late Prune -> intersection: cells reachable to both oceans
        intersection = list(pacific & atlantic)

        # overall: time complexity
        # overall: space complexity
        return intersection

Solution 2: 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(starts)
            queue = deque(starts)

            while queue:
                # Process Root -> current cell
                r, c = queue.popleft()

                # Process Choices -> explore neighbors
                for dr, dc in [(1,0), (-1,0), (0,1), (0,-1)]:
                    nr, nc = r + dr, c + dc

                    # Early Prune -> bounds, visited, uphill condition
                    if (0 <= nr < m and 0 <= nc < n and (nr, nc) not in visited
                        and heights[nr][nc] >= heights[r][c]):
                        visited.add((nr, nc))
                        queue.append((nr, nc))
            return 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 = [(0, j) for j in range(n)] + [(i, 0) for i in range(m)]
        
        # atlantic_starts = all cells on the bottom row (m-1, j) and all cells on the right column (i, n-1)
        atlantic_starts = [(m - 1, j) for j in range(n)] + [(i, n - 1) for i in range(m)]

        pacific = bfs(pacific_starts)
        atlantic = bfs(atlantic_starts)

        # return intersection between
        intersection = list(pacific & atlantic)

        # overall: time complexity
        # overall: space complexity
        return intersection

130. Surrounded Regions ::3:: - Medium

Topics: Array, Depth First Search, Breadth First Search, Union Find, Matrix

Intro

You are given an m x n matrix board containing letters 'X' and 'O', capture regions that are surrounded: Connect: A cell is connected to adjacent cells horizontally or vertically. Region: To form a region connect every 'O' cell. Surround: The region is surrounded with 'X' cells if you can connect the region with 'X' cells and none of the region cells are on the edge of the board. To capture a surrounded region, replace all 'O's with 'X's in-place within the original board. You do not need to return anything.

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

board[i][j] is 'X' or 'O'.

Abstraction

Given a board, find all surrounded regions, capture and return them.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: Recursive DFS with Flood Fill from Borders - Graph/something

    def solve(self, board: List[List[str]]) -> None:
        
        # Note:
        # 1. Any 'O' connected to the border cannot be captured.
        # 2. Mark all border-connected 'O's with DFS (temporary marker 'T').
        # 3. After traversal:
        #    Flip all remaining 'O' to 'X' (they are surrounded).
        #    Flip all 'T' back to 'O'.
        # 4. Mutates board in-place, no return required.

        # Empty check
        if not board:
            return

        # boundaries
        m, n = len(board), len(board[0])

        def dfs(r, c):

            # Early Prune -> : out of bounds or not a 'O'
            if r < 0 or c < 0 or r >= m or c >= n or board[r][c] != 'O':
                return

            # Process Root -> mark as safe
            board[r][c] = 'T'

            # Process Candidates -> explore neighbors
            dfs(r + 1, c)
            dfs(r - 1, c)
            dfs(r, c + 1)
            dfs(r, c - 1)

        # Process Roots -> start DFS from border cells
        for i in range(m):
            # left column
            dfs(i, 0)
            # right column
            dfs(i, n - 1)
        for j in range(n):
            # top row
            dfs(0, j)
            # bottom row
            dfs(m - 1, j)

        # Late Prune -> Flip surrounded 'O' -> 'X', safe 'T' -> 'O'
        for i in range(m):
            for j in range(n):
                if board[i][j] == 'O':
                    board[i][j] = 'X'
                elif board[i][j] == 'T':
                    board[i][j] = 'O'

        
        # overall: time complexity
        # overall: space complexity

Solution 2: Iterative BFS with Flood Fill from Borders - Graph/something

    def solve(self, board: List[List[str]]) -> None:
        
        # Note:
        # 1. Same idea as DFS, but BFS is used to avoid recursion depth issues.
        # 2. Traverse from border 'O's, mark as 'T'.
        # 3. After BFS, flip surrounded 'O' -> 'X' and 'T' -> 'O'.

        # Empty Check
        if not board:
            return

        # Boundaries
        m, n = len(board), len(board[0])

        def bfs(r, c):

            # Process Root -> initialize BFS from this cell
            queue = deque([(r, c)])
            board[r][c] = 'T'
            
            while queue:
                cr, cc = queue.popleft()
                # Process Candidates -> explore neighbors
                for dr, dc in [(1,0), (-1,0), (0,1), (0,-1)]:
                    nr, nc = cr + dr, cc + dc

                    # Early Prune -> valid bounds and 'O'
                    if 0 <= nr < m and 0 <= nc < n and board[nr][nc] == 'O':
                        board[nr][nc] = 'T'
                        queue.append((nr, nc))

        # Process Roots -> start BFS from border 'O's
        for i in range(m):
            if board[i][0] == 'O':
                bfs(i, 0)
            if board[i][n - 1] == 'O':
                bfs(i, n - 1)
        for j in range(n):
            if board[0][j] == 'O':
                bfs(0, j)
            if board[m - 1][j] == 'O':
                bfs(m - 1, j)

        # Late Prune -> Flip surrounded 'O' -> 'X', safe 'T' -> 'O'
        for i in range(m):
            for j in range(n):
                if board[i][j] == 'O':
                    board[i][j] = 'X'
                elif board[i][j] == 'T':
                    board[i][j] = 'O'

Solution 3: Union Find Disjoint Set Union - Graph/something

    def solve(self, board: List[List[str]]) -> None:
        # Note:
        # 1. Use Union Find (Disjoint Set Union).
        # 2. Create a dummy node representing the border.
        # 3. Union all 'O's on the border with the dummy node.
        # 4. Union all adjacent 'O's with each other.
        # 5. After unions, any 'O' connected to dummy is safe.
        # 6. Flip all other 'O' to 'X'.

        # Empty check
        if not board:
            return

        # Boundaries
        m, n = len(board), len(board[0])

        # 
        parent = {}

        # Find Path Compression
        def find(x: int) -> int:
            parent.setdefault(x, x)
            if parent[x] != x:
                parent[x] = find(parent[x])
            return parent[x]

        # Union two Sets
        def union(x: int, y: int) -> None:
            parent[find(x)] = find(y)

        # Special node for border
        dummy = m * n 

        # Process Roots and Candidates -> Union Border 'O's and neighbors
        for i in range(m):
            for j in range(n):
                if board[i][j] == 'O':
                    idx = i * n + j

                    # Union border cells with dummy
                    if i in (0, m - 1) or j in (0, n - 1):
                        union(idx, dummy)

                    # Union neighbors
                    for di, dj in [(1,0), (-1,0), (0,1), (0,-1)]:
                        ni, nj = i + di, j + dj
                        if 0 <= ni < m and 0 <= nj < n and board[ni][nj] == 'O':
                            union(idx, ni * n + nj)

        # Late Prune -> flip 'O' not connected to dummy to 'X'
        for i in range(m):
            for j in range(n):
                if board[i][j] == 'O' and find(i * n + j) != find(dummy):
                    board[i][j] = 'X'

        # overall: time complexity
        # overall: space complexity

207. Course Schedule ::2:: - Medium

Topics: Depth First Search, Breadth First Search, Graph, Topological Sort

Intro

There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai. For example, the pair [0, 1], indicates that to take course 0 you have to first take course 1. Return true if you can finish all courses. Otherwise, return false.

Example InputOutput
numCourses = 2, prerequisites = [[1,0]]true
numCourses = 2, prerequisites = [[1,0],[0,1]]false

Constraints:

1 ≤ numCourses ≤ 2000

0 ≤ prerequisites.length ≤ 5000

prerequisites[i].length == 2

0 ≤ ai, bi < numCourses

All the pairs prerequisites[i] are unique.

Abstraction

Given a number of courses and prerequisites, determine if you can finish all courses.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: Recursive DFS Cycle Detection - Graph/something

    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        
        # Note:
        # 1. Build a graph from prerequisites (course -> list of courses depending on it).
        # 2. Use DFS to detect cycles.
        #    visited[course] = 0: unvisited
        #    visited[course] = 1: visiting (currently in recursion stack)
        #    visited[course] = 2: visited (safe, no cycles in this path)
        # 3. If a cycle is found, return False (cannot finish all courses).
        # 4. Otherwise, all courses can be completed.

        # Build graph
        graph = {i: [] for i in range(numCourses)}
        for course, prereq in prerequisites:
            graph[prereq].append(course)

        # State array
        # 0 = unvisited, 1 = visiting, 2 = visited
        visited = [0] * numCourses

        def dfs(course):
            # Early Prune -> cycle detected
            if visited[course] == 1:
                return False
            # Late Prune -> already processed
            if visited[course] == 2:
                return True

            # Process Root -> mark as visiting
            visited[course] = 1

            # Process Candidates -> traverse neighbors
            for nei in graph[course]:
                if not dfs(nei):
                    return False
            
            # LAte Prune -> mark as visited
            visited[course] = 2
            return True

        # Process Roots -> traverse all courses
        for c in range(numCourses):
            if not dfs(c):
                return False

        # All courses processed, no cycles

        # overall: time complexity
        # overall: space complexity
        return True

Solution 2: Iterative BFS Topological Sort - Graph/something

    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        
        # Note:
        # 1. Build graph (course -> list of dependent courses) and indegree array.
        # 2. BFS from courses with indegree 0 (no prerequisites).
        # 3. Reduce indegree of neighbors; add to queue when indegree becomes 0.
        # 4. Count processed courses; if count == numCourses, return True.

        # Init graph and indegree
        indegree = [0] * numCourses
        graph = {i: [] for i in range(numCourses)}
        for course, prereq in prerequisites:
            graph[prereq].append(course)
            indegree[course] += 1

        # Process Roots -> start with courses without prerequisites
        queue = deque([i for i in range(numCourses) if indegree[i] == 0])
        count = 0

        # Process Candidates -> BFS topological sort
        while queue:
            curr = queue.popleft()
            count += 1
            for nei in graph[curr]:
                indegree[nei] -= 1
                # Early Prune -> neighbor ready to process
                if indegree[nei] == 0:
                    queue.append(nei)

        # Late Prune -> if all courses processed, no cycles
        return count == numCourses

210. Course Schedule II ::3:: - Medium

Topics: Depth First Search, Breadth First Search, Graph, Topological Sort

Intro

There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai. For example, the pair [0, 1], indicates that to take course 0 you have to first take course 1. Return the ordering of courses you should take to finish all courses. If there are many valid answers, return any of them. If it is impossible to finish all courses, return an empty array.

Example InputOutput
numCourses = 2, prerequisites = [[1,0]][0,1]
numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]][0,2,1,3]
numCourses = 1, prerequisites = [][0]

Constraints:

1 ≤ numCourses ≤ 2000

0 ≤ prerequisites.length ≤ numCourses * (numCourses-1)

prerequisites[i].length == 2

0 ≤ ai, bi < numCourses

ai != bi

All the pairs prerequisites[i] are distinct.

Abstraction

Given a number of courses and prerequisites, determine if you can finish all courses, and return the order to finish all courses.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: DFS Topological Sort with Cycle Detection - Graph/something

    def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
        graph = {i: [] for i in range(numCourses)}
        for course, prereq in prerequisites:
            graph[prereq].append(course)

        visited = [0] * numCourses  # 0 = unvisited, 1 = visiting, 2 = visited
        res = []
        self.is_possible = True

        def dfs(course):
            if visited[course] == 1:  # cycle detected
                self.is_possible = False
                return
            if visited[course] == 2:  # already processed
                return

            visited[course] = 1
            for nei in graph[course]:
                dfs(nei)
                if not self.is_possible:
                    return
            visited[course] = 2
            res.append(course)

        for c in range(numCourses):
            if visited[c] == 0:
                dfs(c)
                if not self.is_possible:
                    return []
        return res[::-1]  # reverse postorder

Solution 2: DFS Topological Sort with Cycle Detection - Graph/something

    def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
        indegree = [0] * numCourses
        graph = {i: [] for i in range(numCourses)}

        for course, prereq in prerequisites:
            graph[prereq].append(course)
            indegree[course] += 1

        queue = deque([i for i in range(numCourses) if indegree[i] == 0])
        res = []

        while queue:
            curr = queue.popleft()
            res.append(curr)
            for nei in graph[curr]:
                indegree[nei] -= 1
                if indegree[nei] == 0:
                    queue.append(nei)

        return res if len(res) == numCourses else []

261. Graph Valid Tree ::3:: - Medium

Topics: Depth First Search, Breadth First Search, Graph, Union Find

Intro

Given n nodes labeled from 0 to n - 1 and a list of undirected edges (each edge is a pair of nodes), write a function to check whether these edges make up a valid tree. You can assume that no duplicate edges will appear in edges. Since all edges are undirected, [0, 1] is the same as [1, 0] and thus will not appear together in edges.

Example InputOutput
n = 5 edges = [[0, 1], [0, 2], [0, 3], [1, 4]]true
n = 5 edges = [[0, 1], [1, 2], [2, 3], [1, 3], [1, 4]]false

Constraints:

1 ≤ n ≤ 100

0 ≤ edges.length ≤ n * n(-1) / 2

Abstraction

Given a list of undirected edges, check if valid tree is made.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: DFS Cycle Detection + Connectivity Check - Graph/something

    def validTree(self, n: int, edges: List[List[int]]) -> bool:
        if len(edges) != n - 1:  # a tree must have exactly n-1 edges
            return False

        graph = {i: [] for i in range(n)}
        for u, v in edges:
            graph[u].append(v)
            graph[v].append(u)

        visited = set()

        def dfs(node, parent):
            if node in visited:
                return False
            visited.add(node)
            for nei in graph[node]:
                if nei == parent:
                    continue
                if not dfs(nei, node):
                    return False
            return True

        return dfs(0, -1) and len(visited) == n

Solution 2: BFS Cycle Detection + Connectivity Check - Graph/something

    def validTree(self, n: int, edges: List[List[int]]) -> bool:
        if len(edges) != n - 1:
            return False

        graph = {i: [] for i in range(n)}
        for u, v in edges:
            graph[u].append(v)
            graph[v].append(u)

        visited = set()
        queue = deque([(0, -1)])

        while queue:
            node, parent = queue.popleft()
            if node in visited:
                return False
            visited.add(node)
            for nei in graph[node]:
                if nei != parent:
                    queue.append((nei, node))

        return len(visited) == n

Solution 3: Union Find Disjoint Set Union - Graph/something

    def validTree(self, n: int, edges: List[List[int]]) -> bool:
        if len(edges) != n - 1:
            return False

        parent = [i for i in range(n)]
        rank = [1] * n

        def find(x):
            while x != parent[x]:
                parent[x] = parent[parent[x]]  # path compression
                x = parent[x]
            return x

        def union(x, y):
            rootX, rootY = find(x), find(y)
            if rootX == rootY:
                return False  # cycle detected
            if rank[rootX] > rank[rootY]:
                parent[rootY] = rootX
            elif rank[rootX] < rank[rootY]:
                parent[rootX] = rootY
            else:
                parent[rootY] = rootX
                rank[rootX] += 1
            return True

        for u, v in edges:
            if not union(u, v):
                return False

        return True

323. Number of Connected Components in an Undirected Graph ::3:: - Medium

Topics: Depth First Search, Breadth First Search, Graph, Union Find

Intro

There is an undirected graph with n nodes. There is also an edges array, where edges[i] = [a, b] means that there is an edge between node a and node b in the graph. The nodes are numbered from 0 to n - 1. Return the total number of connected components in that graph.

Example InputOutput
n=3 edges=[[0,1], [0,2]]1
n=6 edges=[[0,1], [1,2], [2,3], [4,5]]2

Constraints:

1 ≤ n ≤ 100

0 ≤ edges.length ≤ n * (n-1) / 2

Abstraction

Given a list of undirected edges, return number of connected components.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: DFS - Graph/something

    def countComponents(self, n: int, edges: List[List[int]]) -> int:
        # build adjacency list
        graph = defaultdict(list)
        for u, v in edges:
            graph[u].append(v)
            graph[v].append(u)
        
        visited = set()
        
        def dfs(node):
            for nei in graph[node]:
                if nei not in visited:
                    visited.add(nei)
                    dfs(nei)
        
        components = 0
        for i in range(n):
            if i not in visited:
                visited.add(i)
                dfs(i)
                components += 1
        return components

Solution 2: BFS - Graph/something

    def countComponents(self, n: int, edges: List[List[int]]) -> int:
        graph = defaultdict(list)
        for u, v in edges:
            graph[u].append(v)
            graph[v].append(u)
        
        visited = set()
        
        def bfs(start):
            queue = deque([start])
            while queue:
                node = queue.popleft()
                for nei in graph[node]:
                    if nei not in visited:
                        visited.add(nei)
                        queue.append(nei)
        
        components = 0
        for i in range(n):
            if i not in visited:
                visited.add(i)
                bfs(i)
                components += 1
        return components

Solution 3: BFS - Graph/something

    def countComponents(self, n: int, edges: List[List[int]]) -> int:
        parent = [i for i in range(n)]
        rank = [1] * n
        
        def find(x):
            if parent[x] != x:
                parent[x] = find(parent[x])   # path compression
            return parent[x]
        
        def union(x, y):
            rootX, rootY = find(x), find(y)
            if rootX == rootY:
                return 0  # no merge
            if rank[rootX] > rank[rootY]:
                parent[rootY] = rootX
            elif rank[rootX] < rank[rootY]:
                parent[rootX] = rootY
            else:
                parent[rootY] = rootX
                rank[rootX] += 1
            return 1  # successful merge
        
        components = n
        for u, v in edges:
            if union(u, v):
                components -= 1
        return components

684. Redundant Connection ::3:: - Medium

Topics: Depth First Search, Breadth First Search, Union Find, Graph

Intro

In this problem, a tree is an undirected graph that is connected and has no cycles. You are given a graph that started as a tree with n nodes labeled from 1 to n, with one additional edge added. The added edge has two different vertices chosen from 1 to n, and was not an edge that already existed. The graph is represented as an array edges of length n where edges[i] = [ai, bi] indicates that there is an edge between nodes ai and bi in the graph. Return an edge that can be removed so that the resulting graph is a tree of n nodes. If there are multiple answers, return the answer that occurs last in the input.

Example InputOutput
edges = [[1,2],[1,3],[2,3]][2,3]
edges = [[1,2],[2,3],[3,4],[1,4],[1,5]][1,4]

Constraints:

n == edges.length

3 ≤ n ≤ 1000

edges[i].length == 2

1 ≤ ai, < bi ≤ edges.length

ai != bi

There are no repeated edges.

The given graph is connected.

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 - Graph/something

    def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
        graph = defaultdict(list)
        
        def dfs(u, target, visited):
            if u == target:
                return True
            visited.add(u)
            for v in graph[u]:
                if v not in visited and dfs(v, target, visited):
                    return True
            return False
        
        for u, v in edges:
            visited = set()
            if u in graph and v in graph and dfs(u, v, visited):
                return [u, v]
            graph[u].append(v)
            graph[v].append(u)

Solution 2: BFS - Graph/something

    def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
        graph = defaultdict(list)
        
        def bfs(u, target):
            visited = set([u])
            queue = deque([u])
            while queue:
                node = queue.popleft()
                if node == target:
                    return True
                for nei in graph[node]:
                    if nei not in visited:
                        visited.add(nei)
                        queue.append(nei)
            return False
        
        for u, v in edges:
            if u in graph and v in graph and bfs(u, v):
                return [u, v]
            graph[u].append(v)
            graph[v].append(u)

Solution 3: Union Find Disjoint Set Union - Graph/something

    def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
        n = len(edges)
        parent = [i for i in range(n + 1)]
        rank = [1] * (n + 1)
        
        def find(x):
            if parent[x] != x:
                parent[x] = find(parent[x])
            return parent[x]
        
        def union(x, y):
            rootX, rootY = find(x), find(y)
            if rootX == rootY:
                return False
            if rank[rootX] > rank[rootY]:
                parent[rootY] = rootX
            elif rank[rootX] < rank[rootY]:
                parent[rootX] = rootY
            else:
                parent[rootY] = rootX
                rank[rootX] += 1
            return True
        
        for u, v in edges:
            if not union(u, v):
                return [u, v]

127. Word Ladder ::3:: - Hard

Topics: Hash Table, String, Breadth First Search

Intro

A transformation sequence from word beginWord to word endWord using a dictionary wordList is a sequence of words beginWord -> s1 -> s2 -> ... -> sk such that: Every adjacent pair of words differs by a single letter. Every si for 1 ≤ i ≤ k is in wordList. Note that beginWord does not need to be in wordList sk == endWord Given two words, beginWord and endWord, and a dictionary wordList, return the number of words in the shortest transformation sequence from beginWord to endWord, or 0 if no such sequence exists.

Example InputOutput
beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]5
beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"]0

Constraints:

1 ≤ beginWord.length ≤ 10

endWord.length == beginWord.length

1 ≤ wordList.length ≤ 5000

wordList[i].length == beginWord.length

beginWord, endWord, and wordList[i] consist of lowercase English letters.

beginWord != endWord

All the words in wordList are unique.

Abstraction

Given a begin word, end word, and transformation dictionary, determine shortest transformation sequence from begin to end.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: BFS - Graph/something

    def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
        wordSet = set(wordList)
        if endWord not in wordSet:
            return 0

        L = len(beginWord)
        all_combo_dict = defaultdict(list)
        for word in wordSet:
            for i in range(L):
                pattern = word[:i] + "*" + word[i+1:]
                all_combo_dict[pattern].append(word)

        queue = deque([(beginWord, 1)])
        visited = set([beginWord])

        while queue:
            current_word, level = queue.popleft()
            for i in range(L):
                pattern = current_word[:i] + "*" + current_word[i+1:]
                for neighbor in all_combo_dict[pattern]:
                    if neighbor == endWord:
                        return level + 1
                    if neighbor not in visited:
                        visited.add(neighbor)
                        queue.append((neighbor, level + 1))
                all_combo_dict[pattern] = []  # mark pattern visited
        return 0

Solution 2: Bidirectional BFS - Graph/something

    def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
        wordSet = set(wordList)
        if endWord not in wordSet:
            return 0

        L = len(beginWord)
        all_combo_dict = defaultdict(list)
        for word in wordSet:
            for i in range(L):
                pattern = word[:i] + "*" + word[i+1:]
                all_combo_dict[pattern].append(word)

        begin_set = {beginWord}
        end_set = {endWord}
        visited = set([beginWord, endWord])
        level = 1

        while begin_set and end_set:
            if len(begin_set) > len(end_set):
                begin_set, end_set = end_set, begin_set  # expand smaller frontier

            temp = set()
            for word in begin_set:
                for i in range(L):
                    pattern = word[:i] + "*" + word[i+1:]
                    for neighbor in all_combo_dict[pattern]:
                        if neighbor in end_set:
                            return level + 1
                        if neighbor not in visited:
                            visited.add(neighbor)
                            temp.add(neighbor)
            begin_set = temp
            level += 1

        return 0