AlgoAtlas

Graphs

Phase 1 28 solutions

Graphs

mindmap
  root((Graphs))
    Grid BFS/DFS
      Island Perimeter LC463
      Number of Islands LC200
      Max Area of Island LC695
      Surrounded Regions LC130
      Clone Graph LC133
    Topological Sort
      Course Schedule LC207
      Course Schedule II LC210
      Course Schedule IV LC1462
      Minimum Height Trees LC310
      Verifying Alien Dictionary LC953
    Union-Find
      Graph Valid Tree LC261
      Number of Connected Components LC323
      Redundant Connection LC684
      Accounts Merge LC721
    Multi-Source BFS
      Walls and Gates LC286
      Rotting Oranges LC994
      Pacific Atlantic Water Flow LC417
    Implicit Graph
      Open the Lock LC752
      Word Ladder LC127
      Word Ladder II LC126
      Evaluate Division LC399
      Find Town Judge LC997
    Union-Find
      Number of Provinces LC547
    Bipartite
      Bipartite Graph LC785
    Grid BFS/DFS
      Flood Fill LC733
      Number of Enclaves LC1020
      Distance of Nearest Cell LC542
    Topological Sort
      Find Eventual Safe States LC802

Problem List

# Problem LC # Difficulty Key Technique
1 Island Perimeter 463 Easy Grid traversal
2 Verifying Alien Dictionary 953 Easy Order mapping
3 Find Town Judge 997 Easy In/out degree
4 Number of Islands 200 Medium DFS/BFS/Union-Find
5 Max Area of Island 695 Medium DFS
6 Clone Graph 133 Medium DFS + hashmap
7 Walls and Gates 286 Medium Multi-source BFS
8 Rotting Oranges 994 Medium Multi-source BFS
9 Pacific Atlantic Water Flow 417 Medium Multi-source BFS
10 Surrounded Regions 130 Medium DFS from border
11 Open the Lock 752 Medium BFS shortest path
12 Course Schedule 207 Medium Topological sort / cycle detection
13 Course Schedule II 210 Medium Topological sort (Kahn's)
14 Graph Valid Tree 261 Medium Union-Find / DFS
15 Course Schedule IV 1462 Medium Topological sort + reachability
16 Number of Connected Components 323 Medium Union-Find
17 Redundant Connection 684 Medium Union-Find
18 Accounts Merge 721 Medium Union-Find
19 Evaluate Division 399 Medium Weighted DFS/BFS
20 Minimum Height Trees 310 Medium Topological sort (leaf trimming)
21 Word Ladder 127 Hard BFS shortest path
22 Number of Provinces 547 Medium Union-Find / DFS
23 Flood Fill 733 Easy Grid DFS
24 Bipartite Graph 785 Medium BFS/DFS coloring
25 Number of Enclaves 1020 Medium Grid DFS from border
26 Distance of Nearest Cell (01 Matrix) 542 Medium Multi-source BFS
27 Word Ladder II 126 Hard BFS + backtrack
28 Find Eventual Safe States 802 Medium Topological sort / reverse DFS

Patterns & Diagrams

1. Graph Traversal Decision Tree

flowchart TD
    A[Graph Problem] --> B{Need shortest path?}
    B -->|Yes, unweighted| C[BFS]
    B -->|No| D{"Need connected components<br/>or cycle detection?"}
    D -->|Yes, dynamic union ops| E[Union-Find]
    D -->|No| F{"Directed graph<br/>with ordering?"}
    F -->|Yes| G[Topological Sort]
    F -->|No| H{Grid problem?}
    H -->|Yes| I{Explore all reachable?}
    I -->|Yes| J[DFS - simpler stack]
    I -->|No, level-by-level| K[BFS]
    H -->|No, adjacency list| L{Explore all paths?}
    L -->|Yes| M[DFS + backtrack]
    L -->|No| N[BFS]

2. Grid BFS/DFS Pattern

flowchart TD
    A[Grid Problem] --> B["Define directions<br/>4-dir or 8-dir"]
    B --> C{Multi-source start?}
    C -->|Yes - Walls&Gates, Rotting| D["Enqueue ALL sources at once<br/>before BFS loop"]
    C -->|No - single source| E["Enqueue/push single cell"]
    D --> F["BFS/DFS loop"]
    E --> F
    F --> G["For each neighbor<br/>check bounds + visited"]
    G --> H{Valid & unvisited?}
    H -->|Yes| I["Mark visited<br/>Add to queue/stack"]
    H -->|No| J[Skip]
    I --> F
    J --> F
    F --> K[Done]

3. Topological Sort — Kahn's BFS

flowchart TD
    A["Build adjacency list + in-degree map"] --> B["Enqueue all nodes<br/>with in-degree = 0"]
    B --> C{Queue empty?}
    C -->|No| D["Dequeue node u<br/>Append to result"]
    D --> E["For each neighbor v<br/>of u"]
    E --> F["in-degree[v] -= 1"]
    F --> G{"in-degree[v] == 0?"}
    G -->|Yes| H[Enqueue v]
    G -->|No| I[Continue]
    H --> C
    I --> C
    C -->|Yes| J{len result == n?}
    J -->|Yes| K["Valid order / no cycle"]
    J -->|No| L[Cycle detected]

4. Union-Find Operations

flowchart TD
    A["Union-Find Init: parent[i]=i, rank[i]=0"] --> B[Operation?]
    B -->|find x| C{"parent[x] == x?"}
    C -->|Yes| D[Return x]
    C -->|No| E["Path compression: parent[x] = find(parent[x])"]
    E --> D
    B -->|union x,y| F["rx = find x<br/>ry = find y"]
    F --> G{rx == ry?}
    G -->|Yes| H["Already connected<br/>return False"]
    G -->|No| I{"rank[rx] vs rank[ry]"}
    I -->|rx higher| J["parent[ry] = rx"]
    I -->|ry higher| K["parent[rx] = ry"]
    I -->|equal| L["parent[ry] = rx, rank[rx] += 1"]
    J --> M[Return True]
    K --> M
    L --> M

5. Multi-Source BFS Pattern

flowchart TD
    A[Identify ALL source nodes] --> B["Add all sources to queue<br/>Mark all as visited/distance=0"]
    B --> C[BFS level by level]
    C --> D[Process current level]
    D --> E[Expand to neighbors]
    E --> F{Unvisited neighbor?}
    F -->|Yes| G["dist[neighbor] = dist[cur]+1, Enqueue"]
    F -->|No| H[Skip]
    G --> C
    H --> C
    C --> I[All cells processed]

    style A fill:#f96,color:#000
    style B fill:#f96,color:#000

Complexity Summary

Problem Time Space Pattern
island_perimeter O(m×n) O(1) Grid traversal
verifying_alien_dictionary O(n·k) O(1) Order mapping
find_the_town_judge O(V+E) O(V) In/out degree
number_of_islands O(m×n) O(m×n) Grid DFS/BFS
max_area_of_island O(m×n) O(m×n) Grid DFS
clone_graph O(V+E) O(V) DFS + hashmap
walls_and_gates O(m×n) O(m×n) Multi-source BFS
rotting_oranges O(m×n) O(m×n) Multi-source BFS
pacific_atlantic_water_flow O(m×n) O(m×n) Multi-source BFS
surrounded_regions O(m×n) O(m×n) DFS from border
open_the_lock O(10⁴) O(10⁴) BFS shortest path
course_schedule O(V+E) O(V+E) Topological sort / cycle detection
course_schedule_ii O(V+E) O(V+E) Topological sort (Kahn's)
graph_valid_tree O(V+E) O(V) Union-Find / DFS
course_schedule_iv O(V+E) O(V²) Topological sort + reachability
number_of_connected_components O(V·α(V)) O(V) Union-Find
redundant_connection O(V·α(V)) O(V) Union-Find
accounts_merge O(n·k·α(n)) O(n·k) Union-Find
evaluate_division O(Q·(V+E)) O(V+E) Weighted DFS/BFS
minimum_height_trees O(V+E) O(V+E) Topological sort (leaf trimming)
word_ladder O(M²·N) O(M²·N) BFS shortest path
number_of_provinces O(V²·α(V)) O(V) Union-Find / DFS
flood_fill O(m×n) O(m×n) Grid DFS
bipartite_graph O(V+E) O(V) BFS/DFS coloring
number_of_enclaves O(m×n) O(m×n) Grid DFS from border
distance_of_nearest_cell O(m×n) O(m×n) Multi-source BFS
word_ladder_ii O(M²·N) O(M²·N) BFS + backtrack
find_eventual_safe_states O(V+E) O(V+E) Topological sort / reverse DFS

Study Order

  1. Warm-up — 463, 953, 997 (no traversal needed)
  2. Grid DFS — 200, 695, 130 (flood fill pattern)
  3. Grid BFS / Multi-source — 286, 994, 417 (multi-source BFS)
  4. General graph BFS/DFS — 133, 752, 127 (adjacency list)
  5. Topological Sort — 207, 210, 1462, 310 (Kahn's algorithm)
  6. Union-Find — 261, 323, 684, 721 (union-find template)
  7. Weighted/Advanced — 399 (weighted BFS/DFS)

Graphs — Pattern Notes

Core Intuition

Graphs are about connectivity and traversal. The key questions: Is it a grid (implicit graph)? Do edges have direction? Are there cycles? Do you need shortest path or just reachability?

  • BFS = shortest path in unweighted graphs, level-by-level exploration.
  • DFS = connectivity, cycle detection, topological sort.
  • Union-Find = dynamic connectivity, cycle detection in undirected graphs.
  • Topological Sort = ordering with dependencies (DAG only).
  • Multi-source BFS = start from multiple nodes simultaneously (e.g., rotting oranges).

Decision Flowchart

flowchart TD
    A[Graph Problem] --> B{"Grid / implicit graph?"}
    B -- Yes --> C{"Shortest path / min steps?"}
    C -- Yes --> D[BFS from source]
    C -- No --> E["DFS / flood fill"]
    B -- No --> F{Directed graph?}
    F -- Yes --> G{Cycle detection or ordering?}
    G -- Yes --> H["Topological Sort<br/>Kahn's BFS or DFS"]
    G -- No --> I[DFS with visited set]
    F -- No --> J{Dynamic connectivity?}
    J -- Yes --> K[Union-Find]
    J -- No --> L{Multiple sources?}
    L -- Yes --> M[Multi-source BFS]
    L -- No --> N["BFS / DFS from single source"]

Per-Problem Notes

1. Island Perimeter — LC 463 | Easy

  • Key insight: For each land cell, contribute 4 edges. Subtract 2 for each shared edge with a neighbor land cell.
  • Tricky: No BFS/DFS needed — pure math on the grid.
  • Common mistakes: Trying to use DFS when a simple O(n·m) scan works.
  • Time: O(n·m) | Space: O(1)

2. Verifying Alien Dictionary — LC 953 | Easy

  • Key insight: Build order map from alien alphabet. For each adjacent word pair, find first differing char and check order.
  • Tricky: If word1 is a prefix of word2 but longer → invalid (e.g., "apple" before "app").
  • Common mistakes: Not handling the prefix case.
  • Time: O(n·m) | Space: O(1) (26 chars)

3. Find the Town Judge — LC 997 | Easy

  • Key insight: Judge is trusted by everyone (in-degree = n-1) and trusts nobody (out-degree = 0). Track both with arrays.
  • Tricky: A person can't be the judge if they trust anyone.
  • Common mistakes: Only checking in-degree, forgetting out-degree check.
  • Time: O(n + e) | Space: O(n)

4. Number of Islands — LC 200 | Medium

  • Key insight: DFS/BFS flood fill. Mark visited cells as '0' (or use visited set). Count connected components.
  • Tricky: Modify grid in-place vs. using a visited set — in-place is O(1) extra space.
  • Common mistakes: Not marking cells visited before adding to queue (BFS) → duplicates.
  • Time: O(n·m) | Space: O(n·m)

5. Max Area of Island — LC 695 | Medium

  • Key insight: Same flood fill as #200, but return size of each component and track max.
  • Tricky: Return count from DFS recursion.
  • Common mistakes: Forgetting to mark visited before recursing → infinite loop.
  • Time: O(n·m) | Space: O(n·m)

6. Clone Graph — LC 133 | Medium

  • Key insight: DFS/BFS with a hashmap {original: clone}. If node already cloned, return clone.
  • Tricky: The hashmap serves as both visited set and clone registry.
  • Common mistakes: Not using the hashmap as visited → infinite loop on cycles.
  • Time: O(n + e) | Space: O(n)

7. Walls and Gates — LC 286 | Medium

  • Key insight: Multi-source BFS from all gates (0s) simultaneously. Fill INF cells with distance.
  • Tricky: Start BFS from all gates at once — don't BFS from each gate separately (O(n·m·g)).
  • Common mistakes: BFS from each gate individually → TLE. Not skipping walls (-1).
  • Time: O(n·m) | Space: O(n·m)

8. Rotting Oranges — LC 994 | Medium

  • Key insight: Multi-source BFS from all rotten oranges. Count fresh oranges; decrement as they rot.
  • Tricky: If fresh > 0 after BFS, return -1 (unreachable fresh oranges).
  • Common mistakes: Not counting initial fresh oranges. Returning time when queue is empty instead of when fresh == 0.
  • Time: O(n·m) | Space: O(n·m)

9. Pacific Atlantic Water Flow — LC 417 | Medium

  • Key insight: Reverse thinking — BFS/DFS from ocean borders inward. Find cells reachable from both oceans.
  • Tricky: Going "uphill" from ocean (water flows downhill, so reverse: mark cells that can reach ocean).
  • Common mistakes: Forward DFS from each cell → O(n²·m²). Must reverse the direction.
  • Time: O(n·m) | Space: O(n·m)

10. Surrounded Regions — LC 130 | Medium

  • Key insight: Mark 'O's connected to border as safe (DFS from borders). Flip remaining 'O' to 'X'.
  • Tricky: Only 'O's NOT connected to border get flipped.
  • Common mistakes: Flipping all 'O's. Not doing the border-connected DFS first.
  • Time: O(n·m) | Space: O(n·m)

11. Open the Lock — LC 752 | Medium

  • Key insight: BFS on state space. Each state is a 4-digit string. 8 neighbors per state (each wheel ±1).
  • Tricky: Start state "0000" might be in deadends. Handle this upfront.
  • Common mistakes: Not adding initial state to visited before BFS. Forgetting wrap-around (9→0, 0→9).
  • Time: O(10⁴) | Space: O(10⁴)

12. Course Schedule — LC 207 | Medium

  • Key insight: Detect cycle in directed graph. DFS with 3 states: unvisited(0), visiting(1), visited(2). Cycle if we hit a "visiting" node.
  • Tricky: Must distinguish "currently in stack" (visiting) from "fully processed" (visited).
  • Common mistakes: Using only a boolean visited — can't detect back edges properly.
  • Time: O(V + E) | Space: O(V + E)

13. Course Schedule II — LC 210 | Medium

  • Key insight: Topological sort. DFS postorder (add to result after all descendants processed) or Kahn's BFS (in-degree).
  • Tricky: If cycle exists, return []. Kahn's: if result size < numCourses → cycle.
  • Common mistakes: Not reversing DFS postorder result. Not detecting cycles.
  • Time: O(V + E) | Space: O(V + E)

14. Graph Valid Tree — LC 261 | Medium

  • Key insight: Valid tree = connected + no cycles = exactly n-1 edges + all nodes connected.
  • Tricky: Check edge count first: if edges != n-1, return False immediately.
  • Common mistakes: Not checking edge count. Using Union-Find but forgetting connectivity check.
  • Time: O(n + e) | Space: O(n)

15. Course Schedule IV — LC 1462 | Medium

  • Key insight: Transitive closure. BFS/DFS from each node, or Floyd-Warshall for dense graphs. Store reachability matrix.
  • Tricky: Precompute all reachability, then answer queries in O(1).
  • Common mistakes: Running BFS per query → TLE.
  • Time: O(V·(V+E)) | Space: O(V²)

16. Number of Connected Components — LC 323 | Medium

  • Key insight: Union-Find or DFS. Count components = n - number of successful unions.
  • Tricky: Union-Find with path compression + union by rank is cleanest.
  • Common mistakes: Forgetting to initialize each node as its own parent.
  • Time: O(n + e · α(n)) | Space: O(n)

17. Redundant Connection — LC 684 | Medium

  • Key insight: Union-Find. Process edges in order; the first edge that connects two already-connected nodes is redundant.
  • Tricky: Return the last such edge if multiple exist (problem guarantees one answer).
  • Common mistakes: Using DFS cycle detection — works but Union-Find is cleaner and O(α(n)) per operation.
  • Time: O(n · α(n)) | Space: O(n)

18. Accounts Merge — LC 721 | Medium

  • Key insight: Union-Find on emails. Union all emails in same account. Group by root, sort, prepend name.
  • Tricky: Map email → account name for the root email. Sort emails within each group.
  • Common mistakes: Not sorting the final email lists. Forgetting to map root email to account name.
  • Time: O(n·m · α(n·m)) | Space: O(n·m)

19. Evaluate Division — LC 399 | Medium

  • Key insight: Build weighted directed graph. BFS/DFS to find path product between query nodes.
  • Tricky: Add both directions: a/b = x means add edge a→b (x) and b→a (1/x).
  • Common mistakes: Not adding reverse edges. Not handling queries where nodes don't exist in graph.
  • Time: O((V+E) · Q) | Space: O(V + E)

20. Minimum Height Trees — LC 310 | Medium

  • Key insight: Topological sort from leaves inward. Iteratively remove leaf nodes. Last 1-2 nodes are roots.
  • Tricky: Answer is always 1 or 2 nodes. Stop when ≤ 2 nodes remain.
  • Common mistakes: BFS from all nodes to find min height — O(n²). Must use leaf-trimming approach.
  • Time: O(n) | Space: O(n)

21. Word Ladder — LC 127 | Hard

  • Key insight: BFS on word state space. Each word's neighbors = all words differing by 1 char. Use word set for O(1) lookup.
  • Tricky: Generate neighbors by replacing each char with a-z (26·L neighbors per word). Remove from set when visited.
  • Common mistakes: Using edit distance between all pairs → O(n²·L). Not removing visited words from set → TLE.
  • Time: O(n·L·26) | Space: O(n·L)

Edge Cases to Watch For

  • Disconnected graph: Always check if all nodes are visited after traversal.
  • Self-loops: Handle in cycle detection and Union-Find.
  • Empty graph / single node: Return appropriate base case.
  • Grid boundaries: Always check 0 <= r < rows and 0 <= c < cols before accessing.
  • Already visited in BFS: Add to visited when enqueuing, not when dequeuing — prevents duplicates.
  • Directed vs undirected: Cycle detection differs. Undirected: parent tracking. Directed: 3-color DFS.
  • Topological sort on cyclic graph: Must detect cycle and return empty/invalid.
  • Multi-source BFS initial state: Add ALL sources to queue and visited set before starting BFS.
# Problem LC # Difficulty Key Technique
1 Island Perimeter 463 Easy Grid traversal
2 Verifying Alien Dictionary 953 Easy Order mapping
3 Find Town Judge 997 Easy In/out degree
4 Number of Islands 200 Medium DFS/BFS/Union-Find
5 Max Area of Island 695 Medium DFS
6 Clone Graph 133 Medium DFS + hashmap
7 Walls and Gates 286 Medium Multi-source BFS
8 Rotting Oranges 994 Medium Multi-source BFS
9 Pacific Atlantic Water Flow 417 Medium Multi-source BFS
10 Surrounded Regions 130 Medium DFS from border
11 Open the Lock 752 Medium BFS shortest path
12 Course Schedule 207 Medium Topological sort / cycle detection
13 Course Schedule II 210 Medium Topological sort (Kahn's)
14 Graph Valid Tree 261 Medium Union-Find / DFS
15 Course Schedule IV 1462 Medium Topological sort + reachability
16 Number of Connected Components 323 Medium Union-Find
17 Redundant Connection 684 Medium Union-Find
18 Accounts Merge 721 Medium Union-Find
19 Evaluate Division 399 Medium Weighted DFS/BFS
20 Minimum Height Trees 310 Medium Topological sort (leaf trimming)
21 Word Ladder 127 Hard BFS shortest path
22 Number of Provinces 547 Medium Union-Find / DFS
23 Flood Fill 733 Easy Grid DFS
24 Bipartite Graph 785 Medium BFS/DFS coloring
25 Number of Enclaves 1020 Medium Grid DFS from border
26 Distance of Nearest Cell (01 Matrix) 542 Medium Multi-source BFS
27 Word Ladder II 126 Hard BFS + backtrack
28 Find Eventual Safe States 802 Medium Topological sort / reverse DFS
Accounts Merge ▼ expand
from typing import List
from collections import defaultdict


class Solution:
    """
    ## 721. Accounts Merge

    Given a list of `accounts` where each element `accounts[i]` is a list of strings,
    where `accounts[i][0]` is a name and the rest are emails belonging to that account.

    Merge accounts that share at least one common email. Return the merged accounts in
    any order. Within each account, return emails in sorted order, with the name first.

    ### Examples

    ```text
    Input:
      accounts = [
        ["John","johnsmith@mail.com","john_newyork@mail.com"],
        ["John","johnsmith@mail.com","john00@mail.com"],
        ["Mary","mary@mail.com"],
        ["John","johnnybravo@mail.com"]
      ]
    Output:
      [
        ["John","john00@mail.com","john_newyork@mail.com","johnsmith@mail.com"],
        ["Mary","mary@mail.com"],
        ["John","johnnybravo@mail.com"]
      ]

    Input:
      accounts = [["John","j1@","j2@"],["John","j1@","j3@"]]
    Output:
      [["John","j1@","j2@","j3@"]]
    ```

    ### Constraints

    - `1 <= accounts.length <= 1000`
    - `2 <= accounts[i].length <= 10`
    - `1 <= accounts[i][j].length <= 30`
    - `accounts[i][0]` consists of English letters.
    - `accounts[i][j]` (for `j > 0`) is a valid email.
    """

    def accountsMerge_dfs(self, accounts: List[List[str]]) -> List[List[str]]:
        # O(n*k*log(n*k)) time, O(n*k) space
        email_to_accounts = defaultdict(list)
        for i, account in enumerate(accounts):
            for email in account[1:]:
                email_to_accounts[email].append(i)

        visited_accounts = set()
        result = []

        def dfs(idx: int, emails: set) -> None:
            if idx in visited_accounts:
                return
            visited_accounts.add(idx)
            for email in accounts[idx][1:]:
                emails.add(email)
                for neighbor in email_to_accounts[email]:
                    dfs(neighbor, emails)

        for i in range(len(accounts)):
            if i not in visited_accounts:
                emails: set = set()
                dfs(i, emails)
                result.append([accounts[i][0]] + sorted(emails))
        return result

    def accountsMerge(self, accounts: List[List[str]]) -> List[List[str]]:
        # Union-Find: union all emails within the same account
        # Group emails by their root representative; attach account name
        # O(n*k*alpha(n*k)) time, O(n*k) space — Union-Find
        parent: dict = {}
        rank: dict = {}

        def find(x: str) -> str:
            if parent[x] != x:
                parent[x] = find(parent[x])
            return parent[x]

        def union(x: str, y: str) -> None:
            px, py = find(x), find(y)
            if px == py:
                return
            if rank.get(px, 0) < rank.get(py, 0):
                px, py = py, px
            parent[py] = px
            if rank.get(px, 0) == rank.get(py, 0):
                rank[px] = rank.get(px, 0) + 1

        email_to_name: dict = {}
        for account in accounts:
            name = account[0]
            for email in account[1:]:
                if email not in parent:
                    parent[email] = email
                email_to_name[email] = name
                union(account[1], email)

        groups: dict = defaultdict(list)
        for email in parent:
            groups[find(email)].append(email)

        return [[email_to_name[root]] + sorted(emails) for root, emails in groups.items()]


if __name__ == '__main__':
    s = Solution()

    accounts1 = [
        ["John", "johnsmith@mail.com", "john_newyork@mail.com"],
        ["John", "johnsmith@mail.com", "john00@mail.com"],
        ["Mary", "mary@mail.com"],
        ["John", "johnnybravo@mail.com"],
    ]
    res = s.accountsMerge(accounts1)
    assert len(res) == 3

    accounts2 = [["John", "j1@", "j2@"], ["John", "j1@", "j3@"]]
    res2 = s.accountsMerge(accounts2)
    assert len(res2) == 1 and sorted(res2[0][1:]) == ["j1@", "j2@", "j3@"]

    res3 = s.accountsMerge_dfs(accounts2)
    assert len(res3) == 1 and sorted(res3[0][1:]) == ["j1@", "j2@", "j3@"]
    print("All tests passed.")
Bipartite Graph ▼ expand
"""
# LC 785: Is Graph Bipartite?

A graph is bipartite if its nodes can be split into two groups such that
every edge connects nodes from different groups (i.e., it is 2-colorable and
contains no odd-length cycles). Return whether the given adjacency-list graph
is bipartite.

## Examples
```text
Input: graph = [[1,3],[0,2],[1,3],[0,2]]
Output: True

Input: graph = [[1,2,3],[0,2],[0,1,3],[0,2]]
Output: False
```

## Constraints
- Disconnected components must each be checked independently.
- A self-loop makes a graph non-bipartite.
- An empty graph or a single node is bipartite.
"""
from typing import List
from collections import deque

class Solution:
    # Approach 1: BFS coloring - try to 2-color the graph
    def isBipartite(self, graph: List[List[int]]) -> bool:
        n = len(graph)
        color = [-1] * n  # -1 = unvisited, 0 and 1 are the two colors

        for start in range(n):  # handle disconnected components
            if color[start] != -1:
                continue
            queue = deque([start])
            color[start] = 0
            while queue:
                node = queue.popleft()
                for neighbor in graph[node]:
                    if color[neighbor] == -1:
                        color[neighbor] = 1 - color[node]  # assign opposite color
                        queue.append(neighbor)
                    elif color[neighbor] == color[node]:
                        return False  # same color on both ends = odd cycle
        return True

    # Approach 2: DFS coloring
    def isBipartite_dfs(self, graph: List[List[int]]) -> bool:
        n = len(graph)
        color = [-1] * n

        def dfs(node, c):
            color[node] = c
            for neighbor in graph[node]:
                if color[neighbor] == -1:
                    if not dfs(neighbor, 1 - c):
                        return False
                elif color[neighbor] == c:
                    return False  # conflict
            return True

        for i in range(n):
            if color[i] == -1:
                if not dfs(i, 0):
                    return False
        return True


if __name__ == "__main__":
    s = Solution()
    assert s.isBipartite([[1,3],[0,2],[1,3],[0,2]]) is True
    assert s.isBipartite([[1,2,3],[0,2],[0,1,3],[0,2]]) is False
    assert s.isBipartite_dfs([[1,3],[0,2],[1,3],[0,2]]) is True
    assert s.isBipartite_dfs([[1,2,3],[0,2],[0,1,3],[0,2]]) is False
    print("All tests passed.")
Clone Graph ▼ expand
"""
# 133. Clone Graph

## Problem
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 of its neighbors.

```text
class Node {
    public int val;
    public List<Node> neighbors;
}
```

## Examples
```text
Input:  adjList = [[2,4],[1,3],[2,4],[1,3]]
Output: [[2,4],[1,3],[2,4],[1,3]]
Explanation: 4 nodes, node 1 connects to 2 and 4, node 2 connects to 1 and 3, etc.
```

```text
Input:  adjList = [[]]
Output: [[]]
Explanation: Single node with no neighbors.
```

```text
Input:  adjList = []
Output: []
Explanation: Empty graph.
```

## 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
- The graph is connected (all nodes can be visited starting from the given node)
"""

from typing import Optional
from collections import deque


class Node:
    def __init__(self, val=0, neighbors=None):
        self.val = val
        self.neighbors = neighbors if neighbors is not None else []


class Solution:
    def cloneGraph_dfs(self, node: Optional['Node']) -> Optional['Node']:
        # O(V+E) time, O(V) space — DFS with visited map
        if not node:
            return None
        visited = {}

        def dfs(n):
            if n in visited:
                return visited[n]
            clone = Node(n.val)
            visited[n] = clone
            for nb in n.neighbors:
                clone.neighbors.append(dfs(nb))
            return clone

        return dfs(node)

    def cloneGraph(self, node: Optional['Node']) -> Optional['Node']:
        # BFS/DFS with a visited map: old_node -> new_node clone
        # For each node, clone it if not seen, then clone all its neighbors
        # O(V+E) time, O(V) space — BFS with visited map
        if not node:
            return None
        visited = {node: Node(node.val)}
        q = deque([node])
        while q:
            curr = q.popleft()
            for nb in curr.neighbors:
                if nb not in visited:
                    visited[nb] = Node(nb.val)
                    q.append(nb)
                visited[curr].neighbors.append(visited[nb])
        return visited[node]


def build_graph(adj: list) -> Optional[Node]:
    if not adj:
        return None
    nodes = [Node(i+1) for i in range(len(adj))]
    for i, neighbors in enumerate(adj):
        nodes[i].neighbors = [nodes[j-1] for j in neighbors]
    return nodes[0]

def graph_to_adj(node: Optional[Node]) -> list:
    if not node:
        return []
    visited, result = {}, {}
    q = deque([node])
    visited[node.val] = node
    while q:
        curr = q.popleft()
        result[curr.val] = sorted(nb.val for nb in curr.neighbors)
        for nb in curr.neighbors:
            if nb.val not in visited:
                visited[nb.val] = nb
                q.append(nb)
    return [result[i] for i in sorted(result)]


if __name__ == '__main__':
    s = Solution()
    adj = [[2,4],[1,3],[2,4],[1,3]]
    g = build_graph(adj)
    assert graph_to_adj(s.cloneGraph_dfs(g)) == [[2,4],[1,3],[2,4],[1,3]]
    assert graph_to_adj(s.cloneGraph(build_graph(adj))) == [[2,4],[1,3],[2,4],[1,3]]
    assert s.cloneGraph(None) is None
    print("All tests passed.")
Course Schedule ▼ expand
"""
# 207. Course Schedule

## Problem
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] = [a, b]`
indicates that you must take course `b` first if you want to take course `a`.

Return `true` if you can finish all courses. Otherwise, return `false`.

## Examples
```text
Input:  numCourses = 2, prerequisites = [[1,0]]
Output: true
Explanation: Take course 0 first, then course 1.
```

```text
Input:  numCourses = 2, prerequisites = [[1,0],[0,1]]
Output: false
Explanation: Courses 0 and 1 are mutually dependent — impossible.
```

```text
Input:  numCourses = 5, prerequisites = [[1,0],[2,1],[3,2],[4,3]]
Output: true
```

## Constraints
- `1 <= numCourses <= 2000`
- `0 <= prerequisites.length <= 5000`
- `prerequisites[i].length == 2`
- `0 <= a, b < numCourses`
- All the pairs `prerequisites[i]` are unique
"""

from typing import List
from collections import deque, defaultdict


class Solution:
    def canFinish_dfs(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        # O(V+E) time, O(V+E) space — DFS cycle detection with 3-color marking
        graph = defaultdict(list)
        for a, b in prerequisites:
            graph[b].append(a)
        # 0=unvisited, 1=visiting, 2=done
        state = [0] * numCourses

        def dfs(node):
            if state[node] == 1:
                return False   # back edge → cycle
            if state[node] == 2:
                return True
            state[node] = 1
            for nb in graph[node]:
                if not dfs(nb):
                    return False
            state[node] = 2
            return True

        return all(dfs(i) for i in range(numCourses))

    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        # Build adjacency list; detect cycle using DFS with 3-state coloring
        # States: 0=unvisited, 1=in-progress (cycle if revisited), 2=done
        # O(V+E) time, O(V+E) space — BFS topological sort (Kahn's algorithm)
        graph = defaultdict(list)
        indegree = [0] * numCourses
        for a, b in prerequisites:
            graph[b].append(a)
            indegree[a] += 1
        q = deque(i for i in range(numCourses) if indegree[i] == 0)
        processed = 0
        while q:
            node = q.popleft()
            processed += 1
            for nb in graph[node]:
                indegree[nb] -= 1
                if indegree[nb] == 0:
                    q.append(nb)
        return processed == numCourses


if __name__ == '__main__':
    s = Solution()
    assert s.canFinish_dfs(2, [[1,0]]) == True
    assert s.canFinish_dfs(2, [[1,0],[0,1]]) == False
    assert s.canFinish_dfs(5, [[1,0],[2,1],[3,2],[4,3]]) == True
    assert s.canFinish(2, [[1,0]]) == True
    assert s.canFinish(2, [[1,0],[0,1]]) == False
    assert s.canFinish(1, []) == True
    print("All tests passed.")
Course Schedule Ii ▼ expand
from typing import List
from collections import deque


class Solution:
    """
    ## 210. Course Schedule II

    There are `numCourses` courses labeled `0` to `numCourses - 1`. You are given
    an array `prerequisites` where `prerequisites[i] = [ai, bi]` indicates you must
    take course `bi` first if you want to take course `ai`.

    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.

    ### Examples

    ```text
    Input:  numCourses = 2, prerequisites = [[1,0]]
    Output: [0, 1]

    Input:  numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
    Output: [0,2,1,3]

    Input:  numCourses = 1, prerequisites = []
    Output: [0]
    ```

    ### Constraints

    - `1 <= numCourses <= 2000`
    - `0 <= prerequisites.length <= numCourses * (numCourses - 1)`
    - `prerequisites[i].length == 2`
    - `0 <= ai, bi < numCourses`
    - `ai != bi`
    - All pairs `[ai, bi]` are distinct.
    """

    def findOrder_dfs(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
        # O(V+E) time, O(V+E) space
        graph = [[] for _ in range(numCourses)]
        for a, b in prerequisites:
            graph[b].append(a)

        # 0=unvisited, 1=visiting, 2=visited
        state = [0] * numCourses
        order = []

        def dfs(node: int) -> bool:
            if state[node] == 1:
                return False  # cycle
            if state[node] == 2:
                return True
            state[node] = 1
            for nei in graph[node]:
                if not dfs(nei):
                    return False
            state[node] = 2
            order.append(node)
            return True

        for i in range(numCourses):
            if not dfs(i):
                return []
        return order[::-1]

    def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
        # Topological sort via DFS post-order; cycle detection aborts
        # Reverse post-order gives valid course order
        # O(V+E) time, O(V+E) space — Kahn's BFS
        graph = [[] for _ in range(numCourses)]
        indegree = [0] * numCourses
        for a, b in prerequisites:
            graph[b].append(a)
            indegree[a] += 1

        queue = deque(i for i in range(numCourses) if indegree[i] == 0)
        order = []
        while queue:
            node = queue.popleft()
            order.append(node)
            for nei in graph[node]:
                indegree[nei] -= 1
                if indegree[nei] == 0:
                    queue.append(nei)
        return order if len(order) == numCourses else []


if __name__ == '__main__':
    s = Solution()
    assert s.findOrder(2, [[1, 0]]) == [0, 1]
    result = s.findOrder(4, [[1, 0], [2, 0], [3, 1], [3, 2]])
    assert result[0] == 0 and result[-1] == 3
    assert s.findOrder(2, [[1, 0], [0, 1]]) == []
    assert s.findOrder(1, []) == [0]

    assert s.findOrder_dfs(2, [[1, 0]]) == [0, 1]
    assert s.findOrder_dfs(2, [[1, 0], [0, 1]]) == []
    print("All tests passed.")
Course Schedule Iv ▼ expand
from typing import List
from collections import deque


class Solution:
    """
    ## 1462. Course Schedule IV

    There are `numCourses` courses labeled `0` to `numCourses - 1`. You are given
    an array `prerequisites` where `prerequisites[i] = [ai, bi]` means course `ai`
    must be taken before course `bi`.

    Given a list of `queries` where `queries[j] = [uj, vj]`, answer whether course
    `uj` is a prerequisite of course `vj` (directly or indirectly).

    ### Examples

    ```text
    Input:  numCourses = 2, prerequisites = [[1,0]], queries = [[0,1],[1,0]]
    Output: [false, true]

    Input:  numCourses = 2, prerequisites = [], queries = [[1,0],[0,1]]
    Output: [false, false]

    Input:  numCourses = 3, prerequisites = [[1,2],[1,0],[2,0]], queries = [[1,0],[1,2]]
    Output: [true, true]
    ```

    ### Constraints

    - `2 <= numCourses <= 100`
    - `0 <= prerequisites.length <= (numCourses * (numCourses - 1) / 2)`
    - `prerequisites[i].length == 2`
    - `0 <= ai, bi < numCourses`
    - `ai != bi`
    - All prerequisite pairs are distinct.
    - `1 <= queries.length <= 10^4`
    """

    def checkIfPrerequisite_bfs(self, numCourses: int, prerequisites: List[List[int]], queries: List[List[int]]) -> List[bool]:
        # O(V*(V+E)) time, O(V^2) space
        graph = [[] for _ in range(numCourses)]
        for a, b in prerequisites:
            graph[a].append(b)

        def can_reach(src: int, dst: int) -> bool:
            visited = set()
            queue = deque([src])
            while queue:
                node = queue.popleft()
                if node == dst:
                    return True
                for nei in graph[node]:
                    if nei not in visited:
                        visited.add(nei)
                        queue.append(nei)
            return False

        return [can_reach(u, v) for u, v in queries]

    def checkIfPrerequisite(self, numCourses: int, prerequisites: List[List[int]], queries: List[List[int]]) -> List[bool]:
        # Build reachability matrix via DFS/BFS or Floyd-Warshall
        # reachable[u][v] = True if u can reach v through prerequisites
        # O(V^3) time, O(V^2) space — Floyd-Warshall transitive closure
        reach = [[False] * numCourses for _ in range(numCourses)]
        for a, b in prerequisites:
            reach[a][b] = True
        for k in range(numCourses):
            for i in range(numCourses):
                for j in range(numCourses):
                    if reach[i][k] and reach[k][j]:
                        reach[i][j] = True
        return [reach[u][v] for u, v in queries]


if __name__ == '__main__':
    s = Solution()
    assert s.checkIfPrerequisite(2, [[1, 0]], [[0, 1], [1, 0]]) == [False, True]
    assert s.checkIfPrerequisite(2, [], [[1, 0], [0, 1]]) == [False, False]
    assert s.checkIfPrerequisite(3, [[1, 2], [1, 0], [2, 0]], [[1, 0], [1, 2]]) == [True, True]

    assert s.checkIfPrerequisite_bfs(2, [[1, 0]], [[0, 1], [1, 0]]) == [False, True]
    assert s.checkIfPrerequisite_bfs(3, [[1, 2], [1, 0], [2, 0]], [[1, 0], [1, 2]]) == [True, True]
    print("All tests passed.")
Distance Of Nearest Cell ▼ expand
"""
# LC 542: 01 Matrix (Distance of Nearest 0)

For each cell in a binary matrix, find the distance to the nearest `0`, where
distance is the number of 4-directional steps.

## Examples
```text
Input: mat = [[0,0,0],[0,1,0],[1,1,1]]
Output: [[0,0,0],[0,1,0],[1,2,1]]

Input: mat = [[0,0,0],[0,1,0],[0,0,0]]
Output: [[0,0,0],[0,1,0],[0,0,0]]
```

## Constraints
- A cell that is already `0` has distance 0.
- Use multi-source BFS from all `0` cells simultaneously.
- At least one `0` is present in the matrix.
"""
from typing import List
from collections import deque

class Solution:
    # Multi-source BFS: start from ALL 0s simultaneously, expand outward
    def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]:
        rows, cols = len(mat), len(mat[0])
        dist = [[float('inf')] * cols for _ in range(rows)]
        queue = deque()

        # initialize BFS with all 0-cells at distance 0
        for r in range(rows):
            for c in range(cols):
                if mat[r][c] == 0:
                    dist[r][c] = 0
                    queue.append((r, c))

        # BFS expands layer by layer - first time we reach a 1-cell is shortest distance
        while queue:
            r, c = queue.popleft()
            for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)]:
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols:
                    if dist[nr][nc] > dist[r][c] + 1:
                        dist[nr][nc] = dist[r][c] + 1
                        queue.append((nr, nc))

        return dist


if __name__ == "__main__":
    s = Solution()
    assert s.updateMatrix([[0,0,0],[0,1,0],[1,1,1]]) == [[0,0,0],[0,1,0],[1,2,1]]
    assert s.updateMatrix([[0,0,0],[0,1,0],[0,0,0]]) == [[0,0,0],[0,1,0],[0,0,0]]
    print("All tests passed.")
Evaluate Division ▼ expand
from typing import List
from collections import defaultdict, deque


class Solution:
    """
    ## 399. Evaluate Division

    You are given an array of variable pairs `equations` and an array of real numbers
    `values`, where `equations[i] = [Ai, Bi]` and `values[i]` represent the equation
    `Ai / Bi = values[i]`.

    Given some `queries`, where `queries[j] = [Cj, Dj]`, return the answers to all
    queries. If the answer does not exist, return `-1.0`.

    ### Examples

    ```text
    Input:
      equations = [["a","b"],["b","c"]], values = [2.0,3.0]
      queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]]
    Output: [6.0, 0.5, -1.0, 1.0, -1.0]

    Input:
      equations = [["a","b"],["b","c"]], values = [2.0,3.0]
      queries = [["a","c"]]
    Output: [6.0]
    ```

    ### Constraints

    - `1 <= equations.length <= 20`
    - `equations[i].length == 2`
    - `1 <= Ai.length, Bi.length <= 5`
    - `values[i] > 0.0`
    - `1 <= queries.length <= 20`
    """

    def calcEquation_bfs(self, equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]:
        # O(Q*(V+E)) time, O(V+E) space
        graph: dict = defaultdict(dict)
        for (a, b), val in zip(equations, values):
            graph[a][b] = val
            graph[b][a] = 1.0 / val

        def bfs(src: str, dst: str) -> float:
            if src not in graph or dst not in graph:
                return -1.0
            if src == dst:
                return 1.0
            visited = set()
            queue = deque([(src, 1.0)])
            visited.add(src)
            while queue:
                node, product = queue.popleft()
                if node == dst:
                    return product
                for nei, weight in graph[node].items():
                    if nei not in visited:
                        visited.add(nei)
                        queue.append((nei, product * weight))
            return -1.0

        return [bfs(c, d) for c, d in queries]

    def calcEquation(self, equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]:
        # Build weighted graph: a/b=k means edge a->b with weight k, b->a with 1/k
        # BFS/DFS for each query to find the product of weights along the path
        # O(E*alpha(V) + Q) time, O(V) space — Union-Find with weights
        parent: dict = {}
        weight: dict = {}  # weight[x] = x / find(x)

        def find(x: str):
            if x not in parent:
                parent[x] = x
                weight[x] = 1.0
            if parent[x] != x:
                root, w = find(parent[x])
                parent[x] = root
                weight[x] *= w
            return parent[x], weight[x]

        def union(x: str, y: str, val: float) -> None:
            rx, wx = find(x)
            ry, wy = find(y)
            if rx != ry:
                parent[rx] = ry
                weight[rx] = val * wy / wx

        for (a, b), val in zip(equations, values):
            union(a, b, val)

        results = []
        for c, d in queries:
            if c not in parent or d not in parent:
                results.append(-1.0)
            else:
                rc, wc = find(c)
                rd, wd = find(d)
                results.append(wc / wd if rc == rd else -1.0)
        return results


if __name__ == '__main__':
    s = Solution()
    eq = [["a", "b"], ["b", "c"]]
    vals = [2.0, 3.0]
    queries = [["a", "c"], ["b", "a"], ["a", "e"], ["a", "a"], ["x", "x"]]
    expected = [6.0, 0.5, -1.0, 1.0, -1.0]

    res = s.calcEquation_bfs(eq, vals, queries)
    assert res == expected, res

    res2 = s.calcEquation(eq, vals, [["a", "c"]])
    assert abs(res2[0] - 6.0) < 1e-9
    print("All tests passed.")
Find Eventual Safe States ▼ expand
"""
# LC 802: Find Eventual Safe States

Return, in ascending order, all nodes of a directed graph from which every
path eventually leads to a terminal node (no cycle is reachable).

## Examples
```text
Input: graph = [[1,2],[2,3],[5],[0],[5],[],[]]
Output: [2,4,5,6]

Input: graph = [[1,2,3,4],[1,2],[3,4],[0,4],[]]
Output: [4]
```

## Constraints
- A node with no outgoing edges is always safe (terminal).
- If all nodes form one cycle, return `[]`.
- Handle disconnected components.
"""
from typing import List
from collections import deque

class Solution:
    # Approach 1: DFS with 3-color marking (0=unvisited, 1=in-stack, 2=safe)
    def eventualSafeNodes(self, graph: List[List[int]]) -> List[int]:
        n = len(graph)
        state = [0] * n  # 0=unvisited, 1=visiting(in cycle path), 2=safe

        def dfs(node) -> bool:
            if state[node] == 1: return False  # back edge = cycle
            if state[node] == 2: return True   # already confirmed safe
            state[node] = 1  # mark as being visited
            for neighbor in graph[node]:
                if not dfs(neighbor):
                    return False  # neighbor leads to cycle
            state[node] = 2  # all neighbors safe, mark safe
            return True

        return [i for i in range(n) if dfs(i)]

    # Approach 2: Reverse graph + topological sort (Kahn's)
    def eventualSafeNodes_topo(self, graph: List[List[int]]) -> List[int]:
        n = len(graph)
        rev = [[] for _ in range(n)]
        out_degree = [0] * n

        for u in range(n):
            for v in graph[u]:
                rev[v].append(u)  # reverse edge
            out_degree[u] = len(graph[u])

        # nodes with out_degree 0 are terminal (safe)
        queue = deque(i for i in range(n) if out_degree[i] == 0)
        safe = set()

        while queue:
            node = queue.popleft()
            safe.add(node)
            for parent in rev[node]:
                out_degree[parent] -= 1
                if out_degree[parent] == 0:
                    queue.append(parent)

        return sorted(safe)


if __name__ == "__main__":
    s = Solution()
    assert s.eventualSafeNodes([[1,2],[2,3],[5],[0],[5],[],[]]) == [2,4,5,6]
    assert s.eventualSafeNodes([[1,2,3,4],[1,2],[3,4],[0,4],[]]) == [4]
    assert s.eventualSafeNodes_topo([[1,2],[2,3],[5],[0],[5],[],[]]) == [2,4,5,6]
    assert s.eventualSafeNodes_topo([[1,2,3,4],[1,2],[3,4],[0,4],[]]) == [4]
    print("All tests passed.")
Find The Town Judge ▼ expand
"""
# 997. Find the Town Judge

## Problem
In a town, there are `n` people labeled from `1` to `n`. There is a rumor that one of
these people is secretly the town judge. If the town judge exists, then:
1. The town judge trusts nobody.
2. Everybody (except the town judge) trusts the town judge.
3. There is exactly one person that satisfies properties 1 and 2.

Given an array `trust` where `trust[i] = [a, b]` means person `a` trusts person `b`,
return the label of the town judge if the town judge exists, otherwise return `-1`.

## Examples
```text
Input:  n = 2, trust = [[1,2]]
Output: 2
```

```text
Input:  n = 3, trust = [[1,3],[2,3]]
Output: 3
```

```text
Input:  n = 3, trust = [[1,3],[2,3],[3,1]]
Output: -1
```

## Constraints
- `1 <= n <= 1000`
- `0 <= trust.length <= 10^4`
- `trust[i].length == 2`
- All the pairs in `trust` are unique
- `a != b`
"""

from typing import List


class Solution:
    def findJudge(self, n: int, trust: List[List[int]]) -> int:
        # Judge trusts nobody (out-degree 0) and is trusted by everyone else (in-degree n-1)
        # Track trust counts: net_trust[i] = in_degree[i] - out_degree[i]; judge has n-1
        # O(n+e) time, O(n) space — net trust score: +1 for being trusted, -1 for trusting
        score = [0] * (n + 1)
        for a, b in trust:
            score[a] -= 1
            score[b] += 1
        for person in range(1, n + 1):
            if score[person] == n - 1:
                return person
        return -1


if __name__ == '__main__':
    s = Solution()
    assert s.findJudge(2, [[1,2]]) == 2
    assert s.findJudge(3, [[1,3],[2,3]]) == 3
    assert s.findJudge(3, [[1,3],[2,3],[3,1]]) == -1
    assert s.findJudge(1, []) == 1
    print("All tests passed.")
Flood Fill ▼ expand
"""
# LC 733: Flood Fill

Starting from pixel `(sr, sc)`, change all 4-directionally connected pixels
that share the same original color to `color`, and return the image.

## Examples
```text
Input: image = [[1,1,1],[1,1,0],[1,0,1]], sr = 1, sc = 1, color = 2
Output: [[2,2,2],[2,2,0],[2,0,1]]

Input: image = [[0,0,0],[0,0,0]], sr = 0, sc = 0, color = 0
Output: [[0,0,0],[0,0,0]]
```

## Constraints
- If the starting pixel already has the target color, return as-is (no-op).
- A single-pixel image just changes that pixel.
- Only pixels connected to the start with the original color are repainted.
"""
from typing import List

class Solution:
    # DFS flood fill - change all connected same-color pixels to new color
    def floodFill(self, image: List[List[int]], sr: int, sc: int, color: int) -> List[List[int]]:
        original = image[sr][sc]
        if original == color:
            return image  # edge case: already the target color, avoid infinite loop

        rows, cols = len(image), len(image[0])

        def dfs(r, c):
            if r < 0 or r >= rows or c < 0 or c >= cols:
                return
            if image[r][c] != original:
                return
            image[r][c] = color  # paint in-place
            dfs(r + 1, c)
            dfs(r - 1, c)
            dfs(r, c + 1)
            dfs(r, c - 1)

        dfs(sr, sc)
        return image


if __name__ == "__main__":
    s = Solution()
    assert s.floodFill([[1,1,1],[1,1,0],[1,0,1]], 1, 1, 2) == [[2,2,2],[2,2,0],[2,0,1]]
    assert s.floodFill([[0,0,0],[0,0,0]], 0, 0, 0) == [[0,0,0],[0,0,0]]
    print("All tests passed.")
Graph Valid Tree ▼ expand
from typing import List
from collections import deque


class Solution:
    """
    ## 261. Graph Valid Tree

    You have `n` nodes labeled from `0` to `n - 1` and a list of undirected edges.
    Write a function to check whether these edges make up a valid tree.

    A valid tree must be connected and contain no cycles.

    ### Examples

    ```text
    Input:  n = 5, edges = [[0,1],[0,2],[0,3],[1,4]]
    Output: true

    Input:  n = 5, edges = [[0,1],[1,2],[2,3],[1,3],[1,4]]
    Output: false
    ```

    ### Constraints

    - `1 <= n <= 2000`
    - `0 <= edges.length <= 5000`
    - `edges[i].length == 2`
    - `0 <= ai, bi < n`
    - `ai != bi`
    - No self-loops or repeated edges.
    """

    def validTree_bfs(self, n: int, edges: List[List[int]]) -> bool:
        # O(V+E) time, O(V+E) space
        if len(edges) != n - 1:
            return False
        graph = [[] for _ in range(n)]
        for a, b in edges:
            graph[a].append(b)
            graph[b].append(a)

        visited = set()
        queue = deque([0])
        visited.add(0)
        while queue:
            node = queue.popleft()
            for nei in graph[node]:
                if nei not in visited:
                    visited.add(nei)
                    queue.append(nei)
        return len(visited) == n

    def validTree(self, n: int, edges: List[List[int]]) -> bool:
        # A valid tree has exactly n-1 edges and is fully connected (no cycles)
        # Union-Find: if any edge connects already-connected nodes, it's a cycle
        # O(V+E) time, O(V) space — Union-Find
        if len(edges) != n - 1:
            return False
        parent = list(range(n))
        rank = [0] * n

        def find(x: int) -> int:
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x

        def union(x: int, y: int) -> bool:
            px, py = find(x), find(y)
            if px == py:
                return False
            if rank[px] < rank[py]:
                px, py = py, px
            parent[py] = px
            if rank[px] == rank[py]:
                rank[px] += 1
            return True

        return all(union(a, b) for a, b in edges)


if __name__ == '__main__':
    s = Solution()
    assert s.validTree(5, [[0, 1], [0, 2], [0, 3], [1, 4]]) is True
    assert s.validTree(5, [[0, 1], [1, 2], [2, 3], [1, 3], [1, 4]]) is False
    assert s.validTree(1, []) is True
    assert s.validTree(4, [[0, 1], [2, 3]]) is False

    assert s.validTree_bfs(5, [[0, 1], [0, 2], [0, 3], [1, 4]]) is True
    assert s.validTree_bfs(5, [[0, 1], [1, 2], [2, 3], [1, 3], [1, 4]]) is False
    print("All tests passed.")
Island Perimeter ▼ expand
"""
# 463. Island Perimeter

## Problem
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.
There is exactly one island (no lakes). Return the perimeter of the island.

## Examples
```text
Input:  grid = [[0,1,0,0],
                [1,1,1,0],
                [0,1,0,0],
                [1,1,0,0]]
Output: 16
```

```text
Input:  grid = [[1]]
Output: 4
```

```text
Input:  grid = [[1,0]]
Output: 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`
"""

from typing import List
from collections import deque


class Solution:
    def islandPerimeter_counting(self, grid: List[List[int]]) -> int:
        # O(m*n) time, O(1) space
        rows, cols = len(grid), len(grid[0])
        perimeter = 0
        for r in range(rows):
            for c in range(cols):
                if grid[r][c] == 1:
                    perimeter += 4
                    if r > 0 and grid[r-1][c] == 1:
                        perimeter -= 2
                    if c > 0 and grid[r][c-1] == 1:
                        perimeter -= 2
        return perimeter

    def islandPerimeter(self, grid: List[List[int]]) -> int:
        # For each land cell, add 4 sides; subtract 2 for each shared edge with neighbor
        # O(m*n) time, O(m*n) space — DFS
        rows, cols = len(grid), len(grid[0])
        visited = set()

        def dfs(r, c):
            if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] == 0:
                return 1
            if (r, c) in visited:
                return 0
            visited.add((r, c))
            return dfs(r+1, c) + dfs(r-1, c) + dfs(r, c+1) + dfs(r, c-1)

        for r in range(rows):
            for c in range(cols):
                if grid[r][c] == 1:
                    return dfs(r, c)
        return 0


if __name__ == '__main__':
    s = Solution()
    assert s.islandPerimeter([[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]]) == 16
    assert s.islandPerimeter([[1]]) == 4
    assert s.islandPerimeter([[1,0]]) == 4
    assert s.islandPerimeter_counting([[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]]) == 16
    print("All tests passed.")
Max Area Of Island ▼ expand
"""
# 695. Max Area of Island

## Problem
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). The area of an island is the
number of cells with value `1` in the island. Return the maximum area of an island in
`grid`. If there is no island, return `0`.

## Examples
```text
Input:  grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],
                [0,0,0,0,0,0,0,1,1,1,0,0,0],
                [0,1,1,0,1,0,0,0,0,0,0,0,0],
                [0,1,0,0,1,1,0,0,1,0,1,0,0],
                [0,1,0,0,1,1,0,0,1,1,1,0,0],
                [0,0,0,0,0,0,0,0,0,0,1,0,0],
                [0,0,0,0,0,0,0,1,1,1,0,0,0],
                [0,0,0,0,0,0,0,1,1,0,0,0,0]]
Output: 6
```

```text
Input:  grid = [[0,0,0,0,0,0,0,0]]
Output: 0
```

## Constraints
- `m == grid.length`
- `n == grid[i].length`
- `1 <= m, n <= 50`
- `grid[i][j]` is `0` or `1`
"""

from typing import List
from collections import deque


class Solution:
    def maxAreaOfIsland_dfs(self, grid: List[List[int]]) -> int:
        # O(m*n) time, O(m*n) space — DFS flood fill tracking area
        rows, cols = len(grid), len(grid[0])

        def dfs(r, c):
            if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] != 1:
                return 0
            grid[r][c] = 0
            return 1 + dfs(r+1,c) + dfs(r-1,c) + dfs(r,c+1) + dfs(r,c-1)

        return max(dfs(r, c) for r in range(rows) for c in range(cols) if grid[r][c] == 1) or 0

    def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
        # DFS flood fill: return size of each island; track global maximum
        # O(m*n) time, O(m*n) space — BFS flood fill tracking area
        rows, cols = len(grid), len(grid[0])
        best = 0
        for r in range(rows):
            for c in range(cols):
                if grid[r][c] == 1:
                    grid[r][c] = 0
                    area, q = 1, deque([(r, c)])
                    while q:
                        cr, cc = q.popleft()
                        for dr, dc in [(1,0),(-1,0),(0,1),(0,-1)]:
                            nr, nc = cr+dr, cc+dc
                            if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
                                grid[nr][nc] = 0
                                area += 1
                                q.append((nr, nc))
                    best = max(best, area)
        return best


if __name__ == '__main__':
    import copy
    s = Solution()
    g = [[0,0,1,0,0,0,0,1,0,0,0,0,0],
         [0,0,0,0,0,0,0,1,1,1,0,0,0],
         [0,1,1,0,1,0,0,0,0,0,0,0,0],
         [0,1,0,0,1,1,0,0,1,0,1,0,0],
         [0,1,0,0,1,1,0,0,1,1,1,0,0],
         [0,0,0,0,0,0,0,0,0,0,1,0,0],
         [0,0,0,0,0,0,0,1,1,1,0,0,0],
         [0,0,0,0,0,0,0,1,1,0,0,0,0]]
    assert s.maxAreaOfIsland_dfs(copy.deepcopy(g)) == 6
    assert s.maxAreaOfIsland(copy.deepcopy(g)) == 6
    assert s.maxAreaOfIsland([[0,0,0,0,0,0,0,0]]) == 0
    print("All tests passed.")
Minimum Height Trees ▼ expand
from typing import List
from collections import deque


class Solution:
    """
    ## 310. Minimum Height Trees

    A tree is an undirected graph in which any two vertices are connected by exactly
    one path. Given a tree of `n` nodes labeled from `0` to `n - 1`, and an array of
    `n - 1` edges, find all the nodes that can be the root of a Minimum Height Tree (MHT).

    Return a list of all MHT root labels. You may return the answer in any order.

    ### Examples

    ```text
    Input:  n = 4, edges = [[1,0],[1,2],[1,3]]
    Output: [1]

    Input:  n = 6, edges = [[3,0],[3,1],[3,2],[3,4],[5,4]]
    Output: [3,4]

    Input:  n = 1, edges = []
    Output: [0]
    ```

    ### Constraints

    - `1 <= n <= 2 * 10^4`
    - `edges.length == n - 1`
    - `0 <= ai, bi < n`
    - `ai != bi`
    - All pairs `(ai, bi)` are distinct.
    - The given input is guaranteed to be a tree.
    """

    def findMinHeightTrees_brute(self, n: int, edges: List[List[int]]) -> List[int]:
        # O(V^2) time, O(V+E) space — BFS from each node
        if n == 1:
            return [0]
        graph = [[] for _ in range(n)]
        for a, b in edges:
            graph[a].append(b)
            graph[b].append(a)

        def bfs_height(root: int) -> int:
            visited = {root}
            queue = deque([root])
            height = 0
            while queue:
                for _ in range(len(queue)):
                    node = queue.popleft()
                    for nei in graph[node]:
                        if nei not in visited:
                            visited.add(nei)
                            queue.append(nei)
                height += 1
            return height

        min_h = float('inf')
        heights = []
        for i in range(n):
            h = bfs_height(i)
            heights.append(h)
            min_h = min(min_h, h)
        return [i for i, h in enumerate(heights) if h == min_h]

    def findMinHeightTrees(self, n: int, edges: List[List[int]]) -> List[int]:
        # Iteratively remove leaf nodes (degree 1) layer by layer
        # The remaining 1 or 2 nodes are the roots of minimum height trees
        # O(V+E) time, O(V+E) space — iterative leaf removal
        if n == 1:
            return [0]
        graph = [set() for _ in range(n)]
        for a, b in edges:
            graph[a].add(b)
            graph[b].add(a)

        leaves = deque(i for i in range(n) if len(graph[i]) == 1)
        remaining = n
        while remaining > 2:
            remaining -= len(leaves)
            new_leaves = deque()
            for leaf in leaves:
                nei = next(iter(graph[leaf]))
                graph[nei].remove(leaf)
                if len(graph[nei]) == 1:
                    new_leaves.append(nei)
            leaves = new_leaves
        return list(leaves)


if __name__ == '__main__':
    s = Solution()
    assert s.findMinHeightTrees(4, [[1, 0], [1, 2], [1, 3]]) == [1]
    assert sorted(s.findMinHeightTrees(6, [[3, 0], [3, 1], [3, 2], [3, 4], [5, 4]])) == [3, 4]
    assert s.findMinHeightTrees(1, []) == [0]

    assert s.findMinHeightTrees_brute(4, [[1, 0], [1, 2], [1, 3]]) == [1]
    assert sorted(s.findMinHeightTrees_brute(6, [[3, 0], [3, 1], [3, 2], [3, 4], [5, 4]])) == [3, 4]
    print("All tests passed.")
Number Of Connected Components ▼ expand
from typing import List


class Solution:
    """
    ## 323. Number of Connected Components in an Undirected Graph

    You have `n` nodes labeled from `0` to `n - 1` and a list of undirected edges.
    Write a function to find the number of connected components in the graph.

    ### Examples

    ```text
    Input:  n = 5, edges = [[0,1],[1,2],[3,4]]
    Output: 2

    Input:  n = 5, edges = [[0,1],[1,2],[2,3],[3,4]]
    Output: 1
    ```

    ### Constraints

    - `1 <= n <= 2000`
    - `1 <= edges.length <= 5000`
    - `edges[i].length == 2`
    - `0 <= ai, bi < n`
    - `ai != bi`
    - No repeated edges.
    """

    def countComponents_dfs(self, n: int, edges: List[List[int]]) -> int:
        # O(V+E) time, O(V+E) space
        graph = [[] for _ in range(n)]
        for a, b in edges:
            graph[a].append(b)
            graph[b].append(a)

        visited = [False] * n

        def dfs(node: int) -> None:
            visited[node] = True
            for nei in graph[node]:
                if not visited[nei]:
                    dfs(nei)

        count = 0
        for i in range(n):
            if not visited[i]:
                dfs(i)
                count += 1
        return count

    def countComponents(self, n: int, edges: List[List[int]]) -> int:
        # Union-Find or DFS: count distinct connected components
        # O(V+E) time, O(V) space — Union-Find
        parent = list(range(n))
        rank = [0] * n

        def find(x: int) -> int:
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x

        def union(x: int, y: int) -> bool:
            px, py = find(x), find(y)
            if px == py:
                return False
            if rank[px] < rank[py]:
                px, py = py, px
            parent[py] = px
            if rank[px] == rank[py]:
                rank[px] += 1
            return True

        components = n
        for a, b in edges:
            if union(a, b):
                components -= 1
        return components


if __name__ == '__main__':
    s = Solution()
    assert s.countComponents(5, [[0, 1], [1, 2], [3, 4]]) == 2
    assert s.countComponents(5, [[0, 1], [1, 2], [2, 3], [3, 4]]) == 1
    assert s.countComponents(3, []) == 3

    assert s.countComponents_dfs(5, [[0, 1], [1, 2], [3, 4]]) == 2
    assert s.countComponents_dfs(5, [[0, 1], [1, 2], [2, 3], [3, 4]]) == 1
    print("All tests passed.")
Number Of Enclaves ▼ expand
"""
# LC 1020: Number of Enclaves

Count land cells (`1`s) from which you cannot walk off the boundary of the
grid by moving 4-directionally.

## Examples
```text
Input: grid = [[0,0,0,0],[1,0,1,0],[0,1,1,0],[0,0,0,0]]
Output: 3

Input: grid = [[0,1,1,0],[0,0,1,0],[0,0,1,0],[0,0,0,0]]
Output: 0
```

## Constraints
- If no land touches the boundary, all land cells are enclaves.
- If all land connects to the boundary, the answer is 0.
- A grid with no land yields 0.
"""
from typing import List
from collections import deque

class Solution:
    # BFS from all boundary land cells, mark reachable land, count unmarked land
    def numEnclaves(self, grid: List[List[int]]) -> int:
        rows, cols = len(grid), len(grid[0])
        queue = deque()

        # seed BFS with all boundary land cells
        for r in range(rows):
            for c in range(cols):
                if (r == 0 or r == rows - 1 or c == 0 or c == cols - 1) and grid[r][c] == 1:
                    queue.append((r, c))
                    grid[r][c] = 0  # mark visited by sinking to 0

        # flood-fill from boundary - sink all connected land
        while queue:
            r, c = queue.popleft()
            for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)]:
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
                    grid[nr][nc] = 0
                    queue.append((nr, nc))

        # remaining 1s are enclaves (not reachable from boundary)
        return sum(grid[r][c] for r in range(rows) for c in range(cols))


if __name__ == "__main__":
    s = Solution()
    assert s.numEnclaves([[0,0,0,0],[1,0,1,0],[0,1,1,0],[0,0,0,0]]) == 3
    assert s.numEnclaves([[0,1,1,0],[0,0,1,0],[0,0,1,0],[0,0,0,0]]) == 0
    print("All tests passed.")
Number Of Islands ▼ expand
"""
# 200. Number of Islands

## Problem
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.

## Examples
```text
Input:  grid = [["1","1","1","1","0"],
                ["1","1","0","1","0"],
                ["1","1","0","0","0"],
                ["0","0","0","0","0"]]
Output: 1
```

```text
Input:  grid = [["1","1","0","0","0"],
                ["1","1","0","0","0"],
                ["0","0","1","0","0"],
                ["0","0","0","1","1"]]
Output: 3
```

## Constraints
- `m == grid.length`
- `n == grid[i].length`
- `1 <= m, n <= 300`
- `grid[i][j]` is `'0'` or `'1'`
"""

from typing import List
from collections import deque


class Solution:
    def numIslands_dfs(self, grid: List[List[str]]) -> int:
        # DFS flood fill: mark visited cells as '0' to avoid revisiting
        # Each DFS call from an unvisited '1' cell counts as one island
        # O(m*n) time, O(m*n) space — DFS flood fill
        rows, cols = len(grid), len(grid[0])

        def dfs(r, c):
            if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] != '1':
                return
            grid[r][c] = '0'
            dfs(r+1, c); dfs(r-1, c); dfs(r, c+1); dfs(r, c-1)

        count = 0
        for r in range(rows):
            for c in range(cols):
                if grid[r][c] == '1':
                    dfs(r, c)
                    count += 1
        return count

    def numIslands_bfs(self, grid: List[List[str]]) -> int:
        # BFS flood fill: same idea but uses a queue instead of recursion
        # O(m*n) time, O(min(m,n)) space — BFS flood fill
        rows, cols = len(grid), len(grid[0])
        count = 0
        for r in range(rows):
            for c in range(cols):
                if grid[r][c] == '1':
                    count += 1
                    grid[r][c] = '0'
                    q = deque([(r, c)])
                    while q:
                        cr, cc = q.popleft()
                        for dr, dc in [(1,0),(-1,0),(0,1),(0,-1)]:
                            nr, nc = cr+dr, cc+dc
                            if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == '1':
                                grid[nr][nc] = '0'
                                q.append((nr, nc))
        return count

    def numIslands(self, grid: List[List[str]]) -> int:
        # Union-Find: union adjacent land cells; count distinct components
        # O(m*n) time, O(m*n) space — Union-Find
        rows, cols = len(grid), len(grid[0])
        parent = list(range(rows * cols))
        rank = [0] * (rows * cols)

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

        def union(x, y):
            px, py = find(x), find(y)
            if px == py:
                return False
            if rank[px] < rank[py]:
                px, py = py, px
            parent[py] = px
            if rank[px] == rank[py]:
                rank[px] += 1
            return True

        count = sum(grid[r][c] == '1' for r in range(rows) for c in range(cols))
        for r in range(rows):
            for c in range(cols):
                if grid[r][c] == '1':
                    for dr, dc in [(1,0),(0,1)]:
                        nr, nc = r+dr, c+dc
                        if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == '1':
                            if union(r*cols+c, nr*cols+nc):
                                count -= 1
        return count


if __name__ == '__main__':
    import copy
    s = Solution()
    g1 = [["1","1","0","0","0"],["1","1","0","0","0"],["0","0","1","0","0"],["0","0","0","1","1"]]
    g2 = [["1","1","1","1","0"],["1","1","0","1","0"],["1","1","0","0","0"],["0","0","0","0","0"]]
    assert s.numIslands(copy.deepcopy(g1)) == 3
    assert s.numIslands(copy.deepcopy(g2)) == 1
    assert s.numIslands_dfs(copy.deepcopy(g1)) == 3
    assert s.numIslands_bfs(copy.deepcopy(g1)) == 3
    print("All tests passed.")
Number Of Provinces ▼ expand
"""
# LC 547: Number of Provinces

Count connected components in an adjacency matrix where `isConnected[i][j] = 1`
means city `i` and city `j` are directly connected.

## Examples
```text
Input: isConnected = [[1,1,0],[1,1,0],[0,0,1]]
Output: 2

Input: isConnected = [[1,0,0],[0,1,0],[0,0,1]]
Output: 3
```

## Constraints
- A single city yields 1 province.
- If all cities are connected, the answer is 1.
- The identity matrix (no connections) yields `n`.
"""
from typing import List

class Solution:
    # Approach 1: DFS - treat adjacency matrix as graph, count connected components
    def findCircleNum(self, isConnected: List[List[int]]) -> int:
        n = len(isConnected)
        visited = [False] * n

        def dfs(node):
            for neighbor in range(n):
                if isConnected[node][neighbor] == 1 and not visited[neighbor]:
                    visited[neighbor] = True
                    dfs(neighbor)

        provinces = 0
        for i in range(n):
            if not visited[i]:
                visited[i] = True
                dfs(i)
                provinces += 1  # each unvisited node starts a new component
        return provinces

    # Approach 2: Union-Find
    def findCircleNum_uf(self, isConnected: List[List[int]]) -> int:
        n = len(isConnected)
        parent = list(range(n))
        rank = [0] * n

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

        def union(x, y):
            px, py = find(x), find(y)
            if px == py:
                return False
            # union by rank
            if rank[px] < rank[py]:
                px, py = py, px
            parent[py] = px
            if rank[px] == rank[py]:
                rank[px] += 1
            return True

        components = n
        for i in range(n):
            for j in range(i + 1, n):
                if isConnected[i][j] == 1:
                    if union(i, j):
                        components -= 1  # merging two components
        return components


if __name__ == "__main__":
    s = Solution()
    assert s.findCircleNum([[1,1,0],[1,1,0],[0,0,1]]) == 2
    assert s.findCircleNum([[1,0,0],[0,1,0],[0,0,1]]) == 3
    assert s.findCircleNum_uf([[1,1,0],[1,1,0],[0,0,1]]) == 2
    assert s.findCircleNum_uf([[1,0,0],[0,1,0],[0,0,1]]) == 3
    print("All tests passed.")
Open The Lock ▼ expand
"""
# 752. Open the Lock

## Problem
You have a lock in front of you with 4 circular wheels. Each wheel has 10 slots:
`'0', '1', '2', ..., '9'`. The wheels can rotate freely and wrap around (e.g., `'9'`
goes to `'0'` and vice versa). The lock initially starts at `'0000'`.

You are given a list of `deadends` — if the lock displays any of these codes, the wheels
will stop turning. Given a `target` representing the value of the wheels that will unlock
the lock, return the minimum total number of turns required to open the lock, or `-1` if
it is impossible.

## Examples
```text
Input:  deadends = ["0201","0101","0102","1212","2002"], target = "0202"
Output: 6
Explanation: A sequence of valid moves: 0000 -> 1000 -> 1100 -> 1200 -> 1201 -> 1202 -> 0202
```

```text
Input:  deadends = ["8888"], target = "0009"
Output: 1
```

```text
Input:  deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"
Output: -1
```

## Constraints
- `1 <= deadends.length <= 500`
- `deadends[i].length == 4`
- `target.length == 4`
- `target` will not be in the range of `deadends`
- `target != "0000"`
"""

from typing import List
from collections import deque


class Solution:
    def openLock_bfs(self, deadends: List[str], target: str) -> int:
        # O(10^4 * 4) time, O(10^4) space — standard BFS
        dead = set(deadends)
        if '0000' in dead:
            return -1
        visited = {'0000'}
        q = deque([('0000', 0)])
        while q:
            state, turns = q.popleft()
            if state == target:
                return turns
            for i in range(4):
                d = int(state[i])
                for delta in (1, -1):
                    nxt = state[:i] + str((d + delta) % 10) + state[i+1:]
                    if nxt not in visited and nxt not in dead:
                        visited.add(nxt)
                        q.append((nxt, turns + 1))
        return -1

    def openLock(self, deadends: List[str], target: str) -> int:
        # BFS from '0000'; each step turns one wheel up or down
        # Skip deadends; return number of steps when target is reached
        # O(10^4) time, O(10^4) space — bidirectional BFS
        dead = set(deadends)
        if '0000' in dead:
            return -1
        if target == '0000':
            return 0

        def neighbors(state):
            for i in range(4):
                d = int(state[i])
                for delta in (1, -1):
                    yield state[:i] + str((d + delta) % 10) + state[i+1:]

        front, back = {'0000'}, {target}
        visited = {'0000', target}
        turns = 0
        while front and back:
            # always expand the smaller frontier
            if len(front) > len(back):
                front, back = back, front
            turns += 1
            nxt = set()
            for state in front:
                for nb in neighbors(state):
                    if nb in dead:
                        continue
                    if nb in back:
                        return turns
                    if nb not in visited:
                        visited.add(nb)
                        nxt.add(nb)
            front = nxt
        return -1


if __name__ == '__main__':
    s = Solution()
    assert s.openLock_bfs(["0201","0101","0102","1212","2002"], "0202") == 6
    assert s.openLock_bfs(["8888"], "0009") == 1
    assert s.openLock_bfs(["8887","8889","8878","8898","8788","8988","7888","9888"], "8888") == -1
    assert s.openLock(["0201","0101","0102","1212","2002"], "0202") == 6
    print("All tests passed.")
Pacific Atlantic Water Flow ▼ expand
"""
# 417. Pacific Atlantic Water Flow

## Problem
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.

Water can flow to a neighboring cell (north, south, east, 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 that ocean.

Return a list of grid coordinates where water can flow to both the Pacific and Atlantic oceans.

## Examples
```text
Input:  heights = [[1,2,2,3,5],
                   [3,2,3,4,4],
                   [2,4,5,3,1],
                   [6,7,1,4,5],
                   [5,1,1,2,4]]
Output: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]]
```

```text
Input:  heights = [[1]]
Output: [[0,0]]
```

## Constraints
- `m == heights.length`
- `n == heights[i].length`
- `1 <= m, n <= 200`
- `0 <= heights[i][j] <= 10^5`
"""

from typing import List
from collections import deque


class Solution:
    def pacificAtlantic_brute(self, heights: List[List[int]]) -> List[List[int]]:
        # O((m*n)^2) time, O(m*n) space — check each cell independently
        rows, cols = len(heights), len(heights[0])

        def can_reach(r, c, ocean):
            visited = set()
            q = deque([(r, c)])
            visited.add((r, c))
            while q:
                cr, cc = q.popleft()
                if ocean == 'P' and (cr == 0 or cc == 0):
                    return True
                if ocean == 'A' and (cr == rows-1 or cc == cols-1):
                    return True
                for dr, dc in [(1,0),(-1,0),(0,1),(0,-1)]:
                    nr, nc = cr+dr, cc+dc
                    if 0 <= nr < rows and 0 <= nc < cols and (nr,nc) not in visited and heights[nr][nc] <= heights[cr][cc]:
                        visited.add((nr, nc))
                        q.append((nr, nc))
            return False

        return [[r, c] for r in range(rows) for c in range(cols)
                if can_reach(r, c, 'P') and can_reach(r, c, 'A')]

    def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:
        # BFS/DFS from Pacific border cells and Atlantic border cells separately
        # Cells reachable from both oceans are the answer
        # Flow is reversed: water flows from ocean inward (uphill is ok)
        # O(m*n) time, O(m*n) space — reverse BFS from ocean borders inward
        rows, cols = len(heights), len(heights[0])

        def bfs(starts):
            visited = set(starts)
            q = deque(starts)
            while q:
                r, c = q.popleft()
                for dr, dc in [(1,0),(-1,0),(0,1),(0,-1)]:
                    nr, nc = r+dr, c+dc
                    if 0 <= nr < rows and 0 <= nc < cols and (nr,nc) not in visited and heights[nr][nc] >= heights[r][c]:
                        visited.add((nr, nc))
                        q.append((nr, nc))
            return visited

        pacific  = bfs([(r, 0) for r in range(rows)] + [(0, c) for c in range(cols)])
        atlantic = bfs([(r, cols-1) for r in range(rows)] + [(rows-1, c) for c in range(cols)])
        return sorted([r, c] for r, c in pacific & atlantic)


if __name__ == '__main__':
    s = Solution()
    h = [[1,2,2,3,5],[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5],[5,1,1,2,4]]
    expected = [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]]
    assert s.pacificAtlantic(h) == expected
    assert s.pacificAtlantic([[1]]) == [[0,0]]
    print("All tests passed.")
Redundant Connection ▼ expand
from typing import List


class Solution:
    """
    ## 684. Redundant Connection

    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.

    Return an edge that can be removed so that the resulting graph is a tree. If there are
    multiple answers, return the answer that occurs last in the input.

    ### Examples

    ```text
    Input:  edges = [[1,2],[1,3],[2,3]]
    Output: [2,3]

    Input:  edges = [[1,2],[2,3],[3,4],[1,4],[1,5]]
    Output: [1,4]
    ```

    ### Constraints

    - `n == edges.length`
    - `3 <= n <= 1000`
    - `edges[i].length == 2`
    - `1 <= ai, bi <= n`
    - `ai != bi`
    - No repeated edges.
    - The given graph is connected.
    """

    def findRedundantConnection_dfs(self, edges: List[List[int]]) -> List[int]:
        # O(V*E) time, O(V+E) space
        n = len(edges)
        graph = [[] for _ in range(n + 1)]

        def has_path(src: int, dst: int, visited: set) -> bool:
            if src == dst:
                return True
            visited.add(src)
            for nei in graph[src]:
                if nei not in visited and has_path(nei, dst, visited):
                    return True
            return False

        for a, b in edges:
            if has_path(a, b, set()):
                return [a, b]
            graph[a].append(b)
            graph[b].append(a)
        return []

    def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
        # Union-Find: process edges in order; the first edge that connects already-connected nodes is redundant
        # O(V*alpha(V)) ≈ O(V) time, O(V) space — Union-Find
        n = len(edges)
        parent = list(range(n + 1))
        rank = [0] * (n + 1)

        def find(x: int) -> int:
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x

        def union(x: int, y: int) -> bool:
            px, py = find(x), find(y)
            if px == py:
                return False
            if rank[px] < rank[py]:
                px, py = py, px
            parent[py] = px
            if rank[px] == rank[py]:
                rank[px] += 1
            return True

        for a, b in edges:
            if not union(a, b):
                return [a, b]
        return []


if __name__ == '__main__':
    s = Solution()
    assert s.findRedundantConnection([[1, 2], [1, 3], [2, 3]]) == [2, 3]
    assert s.findRedundantConnection([[1, 2], [2, 3], [3, 4], [1, 4], [1, 5]]) == [1, 4]

    assert s.findRedundantConnection_dfs([[1, 2], [1, 3], [2, 3]]) == [2, 3]
    assert s.findRedundantConnection_dfs([[1, 2], [2, 3], [3, 4], [1, 4], [1, 5]]) == [1, 4]
    print("All tests passed.")
Rotting Oranges ▼ expand
from typing import List
# from collections import deque

class Solution:
    """
    **LC 994: Rotting Oranges**

    Given an `m x n` grid where each cell has one of three values:
    * `0` — empty cell
    * `1` — fresh orange
    * `2` — rotten orange

    Every minute, any fresh orange that is **4-directionally adjacent** to a rotten
    orange becomes rotten.

    Return the minimum number of minutes until no cell has a fresh orange.
    Return `-1` if it is impossible.

    Example 1:
    ```text
    Input:  grid = [[2,1,1],[1,1,0],[0,1,1]]
    Output: 4
    ```

    Example 2:
    ```text
    Input:  grid = [[2,1,1],[0,1,1],[1,0,1]]
    Output: -1
    Explanation: The orange at (2,0) is never reachable — rotting only happens 4-directionally.
    ```

    Example 3:
    ```text
    Input:  grid = [[0,2]]
    Output: 0
    Explanation: No fresh oranges exist at minute 0.
    ```

    **Constraints:**
    * `m == grid.length`
    * `n == grid[i].length`
    * `1 <= m, n <= 10`
    * `grid[i][j]` is `0`, `1`, or `2`
    """

    def orangesRotting(self, grid: List[List[int]]) -> int:
        # Multi-source BFS from all initially rotten oranges simultaneously
        # Each BFS level = 1 minute; count fresh oranges; return -1 if any remain
        # your code here
        pass


if __name__ == "__main__":
    sol = Solution()

    # Example 1: expected 4
    print(sol.orangesRotting([[2, 1, 1], [1, 1, 0], [0, 1, 1]]))

    # Example 2: expected -1
    print(sol.orangesRotting([[2, 1, 1], [0, 1, 1], [1, 0, 1]]))

    # Example 3: expected 0
    print(sol.orangesRotting([[0, 2]]))
Surrounded Regions ▼ expand
"""
# 130. Surrounded Regions

## Problem
Given an `m x n` matrix `board` containing `'X'` and `'O'`, capture all regions that are
4-directionally surrounded by `'X'`. A region is captured by flipping all `'O'`s into
`'X'`s in that surrounded region.

An `'O'` is not captured if it is on the border or connected to a border `'O'`.

## Examples
```text
Input:  board = [["X","X","X","X"],
                 ["X","O","O","X"],
                 ["X","X","O","X"],
                 ["X","O","X","X"]]
Output:         [["X","X","X","X"],
                 ["X","X","X","X"],
                 ["X","X","X","X"],
                 ["X","O","X","X"]]
Explanation: The bottom 'O' is on the border and not captured.
```

```text
Input:  board = [["X"]]
Output: [["X"]]
```

## Constraints
- `m == board.length`
- `n == board[i].length`
- `1 <= m, n <= 200`
- `board[i][j]` is `'X'` or `'O'`
"""

from typing import List
from collections import deque


class Solution:
    def solve_dfs(self, board: List[List[str]]) -> None:
        # O(m*n) time, O(m*n) space — DFS mark border-connected O's, then flip rest
        rows, cols = len(board), len(board[0])

        def dfs(r, c):
            if r < 0 or r >= rows or c < 0 or c >= cols or board[r][c] != 'O':
                return
            board[r][c] = 'S'
            dfs(r+1,c); dfs(r-1,c); dfs(r,c+1); dfs(r,c-1)

        for r in range(rows):
            for c in range(cols):
                if (r in (0, rows-1) or c in (0, cols-1)) and board[r][c] == 'O':
                    dfs(r, c)

        for r in range(rows):
            for c in range(cols):
                if board[r][c] == 'O':
                    board[r][c] = 'X'
                elif board[r][c] == 'S':
                    board[r][c] = 'O'

    def solve(self, board: List[List[str]]) -> None:
        # Mark all 'O' cells connected to the border as safe (DFS/BFS from borders)
        # Flip remaining 'O' cells to 'X'; restore safe cells back to 'O'
        # O(m*n) time, O(m*n) space — BFS mark border-connected O's, then flip rest
        rows, cols = len(board), len(board[0])
        q = deque()
        for r in range(rows):
            for c in range(cols):
                if (r in (0, rows-1) or c in (0, cols-1)) and board[r][c] == 'O':
                    board[r][c] = 'S'
                    q.append((r, c))
        while q:
            r, c = q.popleft()
            for dr, dc in [(1,0),(-1,0),(0,1),(0,-1)]:
                nr, nc = r+dr, c+dc
                if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] == 'O':
                    board[nr][nc] = 'S'
                    q.append((nr, nc))
        for r in range(rows):
            for c in range(cols):
                board[r][c] = 'O' if board[r][c] == 'S' else 'X'


if __name__ == '__main__':
    import copy
    s = Solution()
    board = [["X","X","X","X"],["X","O","O","X"],["X","X","O","X"],["X","O","X","X"]]
    expected = [["X","X","X","X"],["X","X","X","X"],["X","X","X","X"],["X","O","X","X"]]

    b1 = copy.deepcopy(board)
    s.solve_dfs(b1)
    assert b1 == expected

    b2 = copy.deepcopy(board)
    s.solve(b2)
    assert b2 == expected

    print("All tests passed.")
Verifying An Alien Dictionary ▼ expand
"""
# 953. Verifying an Alien Dictionary

## Problem
In an alien language, the alphabet uses the same letters as English but in a possibly
different order. Given a sequence of `words` written in the alien language and the `order`
of the alphabet, return `true` if and only if the given `words` are sorted lexicographically
in this alien language.

## Examples
```text
Input:  words = ["hello","leetcode"], order = "hlabcdefgijkmnopqrstuvwxyz"
Output: true
Explanation: 'h' comes before 'l' in the alien alphabet.
```

```text
Input:  words = ["word","world","row"], order = "worldabcefghijkmnpqstuvxyz"
Output: false
Explanation: 'd' comes after 'l' in the alien alphabet, so "word" > "world".
```

```text
Input:  words = ["apple","app"], order = "abcdefghijklmnopqrstuvwxyz"
Output: false
Explanation: "apple" > "app" because the first word is longer.
```

## Constraints
- `1 <= words.length <= 100`
- `1 <= words[i].length <= 20`
- `order.length == 26`
- All characters in `words[i]` and `order` are English lowercase letters
"""

from typing import List


class Solution:
    def isAlienSorted(self, words: List[str], order: str) -> bool:
        # Build order map from alien alphabet; compare adjacent words character by character
        # If a word is a prefix of the next word but longer, it's invalid
        # O(n*m) time, O(1) space — compare each adjacent pair character by character
        rank = {c: i for i, c in enumerate(order)}
        for i in range(len(words) - 1):
            w1, w2 = words[i], words[i+1]
            for j in range(len(w1)):
                if j == len(w2):          # w2 is prefix of w1 → not sorted
                    return False
                if w1[j] != w2[j]:
                    if rank[w1[j]] > rank[w2[j]]:
                        return False
                    break
        return True


if __name__ == '__main__':
    s = Solution()
    assert s.isAlienSorted(["hello","leetcode"], "hlabcdefgijkmnopqrstuvwxyz") == True
    assert s.isAlienSorted(["word","world","row"], "worldabcefghijkmnpqstuvxyz") == False
    assert s.isAlienSorted(["apple","app"], "abcdefghijklmnopqrstuvwxyz") == False
    assert s.isAlienSorted(["z","z"], "abcdefghijklmnopqrstuvwxyz") == True
    print("All tests passed.")
Walls And Gates ▼ expand
"""
# 286. Walls and Gates

## Problem
You are given an `m x n` grid `rooms` initialized with these three possible values:
- `-1` — A wall or an obstacle.
- `0` — A gate.
- `INF` — Infinity means an empty room. We use `2147483647` to represent `INF`.

Fill each empty room with the distance to its nearest gate. If it is impossible to reach
a gate, leave it as `INF`. Modify `rooms` in-place.

## Examples
```text
Input:
  INF  -1   0  INF
  INF INF INF  -1
  INF  -1 INF  -1
    0  -1 INF INF

Output:
    3  -1   0   1
    2   2   1  -1
    1  -1   2  -1
    0  -1   3   4
```

```text
Input:  rooms = [[-1]]
Output: [[-1]]
```

## Constraints
- `m == rooms.length`
- `n == rooms[i].length`
- `1 <= m, n <= 250`
- `rooms[i][j]` is `-1`, `0`, or `2^31 - 1`
"""

from typing import List
from collections import deque

INF = 2147483647


class Solution:
    def wallsAndGates_brute(self, rooms: List[List[int]]) -> None:
        # O((m*n)^2) time, O(m*n) space — BFS from each empty room
        rows, cols = len(rooms), len(rooms[0])

        def bfs(sr, sc):
            q = deque([(sr, sc, 0)])
            visited = {(sr, sc)}
            while q:
                r, c, dist = q.popleft()
                if rooms[r][c] == 0:
                    return dist
                for dr, dc in [(1,0),(-1,0),(0,1),(0,-1)]:
                    nr, nc = r+dr, c+dc
                    if 0 <= nr < rows and 0 <= nc < cols and (nr,nc) not in visited and rooms[nr][nc] != -1:
                        visited.add((nr, nc))
                        q.append((nr, nc, dist+1))
            return INF

        for r in range(rows):
            for c in range(cols):
                if rooms[r][c] == INF:
                    rooms[r][c] = bfs(r, c)

    def wallsAndGates(self, rooms: List[List[int]]) -> None:
        # Multi-source BFS from all gates (0 cells) simultaneously
        # Each BFS level increments distance; INF cells get updated with shortest distance
        # O(m*n) time, O(m*n) space — multi-source BFS from all gates simultaneously
        rows, cols = len(rooms), len(rooms[0])
        q = deque((r, c) for r in range(rows) for c in range(cols) if rooms[r][c] == 0)
        while q:
            r, c = q.popleft()
            for dr, dc in [(1,0),(-1,0),(0,1),(0,-1)]:
                nr, nc = r+dr, c+dc
                if 0 <= nr < rows and 0 <= nc < cols and rooms[nr][nc] == INF:
                    rooms[nr][nc] = rooms[r][c] + 1
                    q.append((nr, nc))


if __name__ == '__main__':
    import copy
    s = Solution()
    rooms = [[INF,-1,0,INF],[INF,INF,INF,-1],[INF,-1,INF,-1],[0,-1,INF,INF]]
    expected = [[3,-1,0,1],[2,2,1,-1],[1,-1,2,-1],[0,-1,3,4]]

    r1 = copy.deepcopy(rooms)
    s.wallsAndGates(r1)
    assert r1 == expected

    r2 = copy.deepcopy(rooms)
    s.wallsAndGates_brute(r2)
    assert r2 == expected

    print("All tests passed.")
Word Ladder ▼ expand
from typing import List
from collections import deque, defaultdict


class Solution:
    """
    ## 127. Word Ladder

    A transformation sequence from word `beginWord` to word `endWord` using a dictionary
    `wordList` is a sequence `beginWord -> s1 -> s2 -> ... -> sk` such that:

    - Every adjacent pair of words differs by exactly one letter.
    - Every `si` for `1 <= i <= k` is in `wordList`.
    - `sk == endWord`

    Given `beginWord`, `endWord`, and `wordList`, return the number of words in the
    shortest transformation sequence, or `0` if no such sequence exists.

    ### Examples

    ```text
    Input:  beginWord = "hit", endWord = "cog",
            wordList = ["hot","dot","dog","lot","log","cog"]
    Output: 5
    Explanation: "hit" -> "hot" -> "dot" -> "dog" -> "cog"

    Input:  beginWord = "hit", endWord = "cog",
            wordList = ["hot","dot","dog","lot","log"]
    Output: 0
    ```

    ### Constraints

    - `1 <= beginWord.length <= 10`
    - `endWord.length == beginWord.length`
    - `1 <= wordList.length <= 5000`
    - `wordList[i].length == beginWord.length`
    - All words consist of lowercase English letters.
    - `beginWord != endWord`
    - All words in `wordList` are unique.
    """

    def ladderLength_bfs(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
        # O(m^2 * n) time, O(m^2 * n) space — standard BFS
        word_set = set(wordList)
        if endWord not in word_set:
            return 0

        queue = deque([(beginWord, 1)])
        visited = {beginWord}
        while queue:
            word, length = queue.popleft()
            for i in range(len(word)):
                for c in 'abcdefghijklmnopqrstuvwxyz':
                    next_word = word[:i] + c + word[i+1:]
                    if next_word == endWord:
                        return length + 1
                    if next_word in word_set and next_word not in visited:
                        visited.add(next_word)
                        queue.append((next_word, length + 1))
        return 0

    def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
        # BFS from beginWord; each level = one transformation step
        # For each word, try changing each character to find valid neighbors in wordSet
        # O(m^2 * n) time, O(m^2 * n) space — bidirectional BFS
        word_set = set(wordList)
        if endWord not in word_set:
            return 0

        front, back = {beginWord}, {endWord}
        visited = {beginWord, endWord}
        length = 1

        while front:
            length += 1
            next_front = set()
            for word in front:
                for i in range(len(word)):
                    for c in 'abcdefghijklmnopqrstuvwxyz':
                        next_word = word[:i] + c + word[i+1:]
                        if next_word in back:
                            return length
                        if next_word in word_set and next_word not in visited:
                            visited.add(next_word)
                            next_front.add(next_word)
            front = next_front
            if len(front) > len(back):
                front, back = back, front
        return 0


if __name__ == '__main__':
    s = Solution()
    wl = ["hot", "dot", "dog", "lot", "log", "cog"]
    assert s.ladderLength_bfs("hit", "cog", wl) == 5
    assert s.ladderLength_bfs("hit", "cog", ["hot", "dot", "dog", "lot", "log"]) == 0

    assert s.ladderLength("hit", "cog", wl) == 5
    assert s.ladderLength("hit", "cog", ["hot", "dot", "dog", "lot", "log"]) == 0
    print("All tests passed.")
Word Ladder Ii ▼ expand
"""
# LC 126: Word Ladder II

Find all shortest transformation sequences from `beginWord` to `endWord`,
changing one letter at a time, where each intermediate word must exist in
`wordList`.

## Examples
```text
Input: beginWord = "hit", endWord = "cog",
       wordList = ["hot","dot","dog","lot","log","cog"]
Output: [["hit","hot","dot","dog","cog"],["hit","hot","lot","log","cog"]]

Input: beginWord = "hit", endWord = "cog",
       wordList = ["hot","dot","dog","lot","log"]
Output: []
```

## Constraints
- If `endWord` is not in `wordList`, return `[]`.
- If no transformation path exists, return `[]`.
- The order of returned paths is not significant.
"""
from typing import List
from collections import defaultdict, deque

class Solution:
    # BFS to find shortest path length + build parent map, then DFS to collect all paths
    def findLadders(self, beginWord: str, endWord: str, wordList: List[str]) -> List[List[str]]:
        word_set = set(wordList)
        if endWord not in word_set:
            return []

        # BFS: build layer-by-layer parent map (child -> set of parents)
        parents = defaultdict(set)
        current_layer = {beginWord}
        found = False

        while current_layer and not found:
            word_set -= current_layer  # remove current layer to prevent revisiting
            next_layer = set()
            for word in current_layer:
                for i in range(len(word)):
                    for c in 'abcdefghijklmnopqrstuvwxyz':
                        nw = word[:i] + c + word[i+1:]
                        if nw in word_set:
                            next_layer.add(nw)
                            parents[nw].add(word)  # record how we reached nw
                            if nw == endWord:
                                found = True
            current_layer = next_layer

        if not found:
            return []

        # DFS backtrack from endWord to beginWord using parent map
        result = []
        def dfs(word, path):
            if word == beginWord:
                result.append(path[::-1])
                return
            for parent in parents[word]:
                path.append(parent)
                dfs(parent, path)
                path.pop()

        dfs(endWord, [endWord])
        return result


if __name__ == "__main__":
    s = Solution()
    result = s.findLadders("hit", "cog", ["hot","dot","dog","lot","log","cog"])
    expected = [["hit","hot","dot","dog","cog"], ["hit","hot","lot","log","cog"]]
    # path order is non-deterministic; compare as sorted lists of paths
    assert sorted(result) == sorted(expected)
    assert s.findLadders("hit", "cog", ["hot","dot","dog","lot","log"]) == []
    print("All tests passed.")

Graph Traversal

BFS, DFS, and Dijkstra's shortest path

Queue — expands level by level
Level 0 Level 1 Level 2 Level 3number = visit order within level
Click a cell to set the start point.