AlgoAtlas

Advanced Graphs

Phase 1 20 solutions

Advanced Graphs

mindmap
  root((Advanced Graphs))
    Shortest Path Dijkstra/Bellman-Ford
      Network Delay Time LC743
      Path with Minimum Effort LC1631
      Swim in Rising Water LC778
      Cheapest Flights Within K Stops LC787
    MST Prim/Kruskal
      Min Cost to Connect All Points LC1584
      Find Critical/Pseudo-Critical Edges LC1489
    Euler Path
      Reconstruct Itinerary LC332
    Topological Sort
      Alien Dictionary LC269
      Build Matrix with Conditions LC2392
    Union-Find
      Greatest Common Divisor Traversal LC2709
      Find Critical/Pseudo-Critical Edges LC1489
      Number of Operations to Connect Network LC1319
      Most Stones Removed LC947
      Number of Islands II LC305
      Making a Large Island LC827
    Graph Theory
      Bridges in Graph LC1192
      Articulation Points LC1568
      Kosaraju's Algorithm SCC
    Shortest Path Dijkstra/Bellman-Ford
      Shortest Path in Binary Maze LC1091
      Number of Ways to Arrive LC1976
      Find City with Smallest Neighbors LC1334

Problem List

# Problem LC # Difficulty Key Technique
1 Path with Minimum Effort 1631 Medium Dijkstra / Binary search
2 Network Delay Time 743 Medium Dijkstra
3 Reconstruct Itinerary 332 Hard Hierholzer's (Euler path)
4 Min Cost to Connect All Points 1584 Medium Prim's / Kruskal's MST
5 Swim in Rising Water 778 Hard Dijkstra / Binary search
6 Alien Dictionary 269 Hard Topological sort
7 Cheapest Flights Within K Stops 787 Medium Bellman-Ford (K relaxations)
8 Find Critical/Pseudo-Critical Edges 1489 Hard Kruskal's + bridge finding
9 Build Matrix with Conditions 2392 Hard Topological sort (2 passes)
10 Greatest Common Divisor Traversal 2709 Hard Union-Find on prime factors
11 Shortest Path in Binary Maze 1091 Medium BFS
12 Number of Ways to Arrive at Destination 1976 Medium Dijkstra + count paths
13 Find City with Smallest Number of Neighbors 1334 Medium Floyd-Warshall / Dijkstra
14 Number of Operations to Connect Network 1319 Medium Union-Find
15 Most Stones Removed with Same Row/Column 947 Medium Union-Find
16 Number of Islands II 305 Hard Online Union-Find
17 Making a Large Island 827 Hard Union-Find + BFS
18 Bridges in Graph 1192 Hard Tarjan's DFS
19 Articulation Points 1568 Hard Tarjan's DFS
20 Kosaraju's Algorithm (SCC) Hard Two-pass DFS

Patterns & Diagrams

1. Shortest Path Algorithm Decision Tree

flowchart TD
    A[Shortest Path Problem] --> B{Weighted edges?}
    B -->|No| C[BFS — O V+E]
    B -->|Yes| D{Negative weights?}
    D -->|Yes| E{K-step constraint?}
    E -->|Yes| F["Bellman-Ford<br/>K relaxation rounds"]
    E -->|No| G["Bellman-Ford<br/>full V-1 rounds"]
    D -->|No| H{All weights equal?}
    H -->|Yes| C
    H -->|No| I{Single source?}
    I -->|Yes| J[Dijkstra — O E log V]
    I -->|No| K[Floyd-Warshall — O V³]

2. MST Algorithm Decision

flowchart TD
    A[Minimum Spanning Tree] --> B{Dense or sparse graph?}
    B -->|Dense — E ≈ V²| C["Prim's with min-heap<br/>O E log V"]
    B -->|Sparse — E ≈ V| D["Kruskal's with Union-Find<br/>O E log E"]
    C --> E["Start from any node<br/>Greedily pick min edge<br/>to unvisited node"]
    D --> F["Sort all edges by weight<br/>Add edge if no cycle<br/>Union-Find for cycle check"]
    E --> G["MST complete when<br/>all V nodes visited"]
    F --> G

3. Dijkstra's Algorithm Flow

flowchart TD
    A["Init dist[src]=0, All others=∞, Min-heap: src,0"] --> B{Heap empty?}
    B -->|No| C[Pop u, d with min dist]
    C --> D{"d > dist[u]?"}
    D -->|Yes — stale| B
    D -->|No| E[For each neighbor v, w]
    E --> F{"dist[u]+w < dist[v]?"}
    F -->|Yes| G["dist[v"] = dist[u]+w<br/>Push v, dist[v] to heap]
    F -->|No| H[Skip]
    G --> B
    H --> B
    B -->|Empty| I["dist["] = shortest paths]

4. Bellman-Ford with K Stops

flowchart TD
    A["Init dist[src]=0, All others=∞"] --> B[For round = 1 to K+1]
    B --> C["Copy dist as prev_dist<br/>to avoid using same-round updates"]
    C --> D[For each edge u→v, w]
    D --> E{"prev_dist[u] + w < dist[v]?"}
    E -->|Yes| F["dist[v"] = prev_dist[u] + w]
    E -->|No| G[Skip]
    F --> D
    G --> D
    D --> B
    B --> H["dist[dst] = answer or ∞ if unreachable"]

    style C fill:#f90,color:#000

5. Euler Path — Hierholzer's Algorithm

flowchart TD
    A["Build adjacency list<br/>sort neighbors for lex order"] --> B[Start DFS from source]
    B --> C{"Current node has<br/>unvisited edges?"}
    C -->|Yes| D["Take next edge<br/>Move to neighbor"]
    D --> C
    C -->|No — dead end| E[Prepend node to result]
    E --> F[Backtrack to previous node]
    F --> C
    F --> G{"Back at start<br/>with no edges left?"}
    G -->|Yes| H["Result is Euler path<br/>reversed = itinerary"]
    G -->|No| F

Complexity Summary

Problem Time Space Pattern
path_with_minimum_effort O(m·n·log(m·n)) O(m·n) Dijkstra / Binary search
network_delay_time O(E log V) O(V+E) Dijkstra
reconstruct_itinerary O(E log E) O(E) Hierholzer's (Euler path)
min_cost_to_connect_all_points O(V² log V) O(V) Prim's / Kruskal's MST
swim_in_rising_water O(n² log n) O(n²) Dijkstra / Binary search
alien_dictionary O(V+E) O(V+E) Topological sort
cheapest_flights_within_k_stops O(K·E) O(V) Bellman-Ford (K relaxations)
find_critical_and_pseudo_critical_edges O(E² · α(V)) O(V) Kruskal's + bridge finding
build_a_matrix_with_conditions O(k²) O(k²) Topological sort (2 passes)
greatest_common_divisor_traversal O(n·√M) O(n+P) Union-Find on prime factors
shortest_path_in_binary_maze O(n²) O(n²) BFS
number_of_ways_to_arrive O(E log V) O(V+E) Dijkstra + count paths
find_city_with_smallest_neighbors O(V³) O(V²) Floyd-Warshall / Dijkstra
number_of_operations_to_connect_network O(E·α(V)) O(V) Union-Find
most_stones_removed O(n·α(n)) O(n) Union-Find
number_of_islands_ii O(k·α(m·n)) O(m·n) Online Union-Find
making_a_large_island O(n²) O(n²) Union-Find + BFS
bridges_in_graph O(V+E) O(V+E) Tarjan's DFS
articulation_points O(V+E) O(V+E) Tarjan's DFS
kosarajus_algorithm O(V+E) O(V+E) Two-pass DFS (SCC)

Study Order

  1. Dijkstra foundation — 743 (Network Delay Time)
  2. Dijkstra variants — 1631, 778 (min-max path cost)
  3. MST — 1584 (Prim's or Kruskal's)
  4. Bellman-Ford — 787 (K stops constraint)
  5. Topological sort — 269, 2392 (alien dict, matrix conditions)
  6. Euler path — 332 (Hierholzer's)
  7. Hard combinatorics — 1489, 2709 (critical edges, GCD traversal)

Advanced Graphs — Pattern Notes

Core Intuition

Advanced graph problems go beyond plain BFS/DFS. The key is recognizing which algorithm fits the constraint:

  • Need shortest path with weights → Dijkstra (non-negative) or Bellman-Ford (negative edges / K-hops)
  • Need minimum spanning tree → Prim's (dense) or Kruskal's (sparse, union-find)
  • Need to traverse every edge exactly once → Euler path (Hierholzer's)
  • Need topological order from implicit constraints → build graph from rules, then topo sort

Decision Diagram

flowchart TD
    A[Advanced Graph Problem] --> B{What is the goal?}

    B --> C[Shortest path with weights]
    B --> D[Minimum spanning tree]
    B --> E[Visit every edge once]
    B --> F[Order from constraints]
    B --> G[Detect critical structure]

    C --> C1{Negative edges or K-hop limit?}
    C1 -->|No| C2[Dijkstra: min-heap, relax neighbors]
    C1 -->|Yes| C3[Bellman-Ford: relax all edges V-1 times]

    D --> D1{Graph density?}
    D1 -->|Dense| D2[Prim's: min-heap from any node]
    D1 -->|Sparse| D3[Kruskal's: sort edges, union-find]

    E --> E1[Hierholzer's: DFS, post-order push to result]
    F --> F1[Build graph from rules, Kahn's topo sort or DFS]
    G --> G1["Tarjan's / modified Dijkstra for bridges"]

Per-Problem Notes

# Problem LC Difficulty What to Learn Common Mistakes Time / Space
1 Min Effort Path 1631 Medium Dijkstra on grid; cost = max effort so far on path; min-heap by effort Using BFS (unweighted); not updating effort correctly as max O(mn log mn) / O(mn)
2 Network Delay Time 743 Medium Classic Dijkstra from source k; answer = max dist in dist array; -1 if any unreachable 1-indexed nodes — adjust array size; not checking all nodes reached O((V+E) log V) / O(V+E)
3 Reconstruct Itinerary 332 Medium Euler path: sort adjacency lists lexicographically; Hierholzer's DFS; post-order build result Not sorting neighbors; reversing result at end O(E log E) / O(E)
4 Min Cost Connect Points 1584 Medium MST with Prim's or Kruskal's; edge weight = Manhattan distance Building all O(n²) edges explicitly for Kruskal's (fine here); Prim's avoids this O(n² log n) / O(n²)
5 Swim in Rising Water 778 Medium Dijkstra where cost to reach cell = max(current_cost, grid[r][c]); or binary search + BFS Using plain BFS (ignores water levels); not taking max with current cost O(n² log n) / O(n²)
6 Alien Dictionary 269 Hard Build graph from adjacent word pairs (first differing char); topo sort; cycle → invalid Not detecting cycles (return ""); comparing words where prefix is longer (invalid input) O(C) where C = total chars / O(1) (26 chars)
7 Cheapest Flights K Stops 787 Medium Bellman-Ford for exactly K+1 hops; copy dist array each iteration to prevent chaining in same round Using Dijkstra (can't limit hops easily); updating dist in-place within a round O(K·E) / O(V)
8 Critical Connections 1489 Hard Tarjan's bridge-finding algorithm; track discovery time and low values; bridge when low[v] > disc[u] Confusing bridge condition with articulation point condition O(V+E) / O(V+E)
9 Build Matrix (2392) 2392 Hard Two topo sorts: row constraints graph + col constraints graph; detect cycles in both Not checking both graphs for cycles independently O(k² + k) / O(k²)
10 GCD Traversal 2709 Hard Build graph: connect nodes sharing a common factor > 1; use factor-based grouping (sieve-like); check connectivity Building O(n²) edges naively — TLE; must group by prime factors O(n√M) / O(n+M)

Edge Cases to Watch

  • Dijkstra: node unreachable → dist remains infinity; check before returning answer
  • Bellman-Ford: negative cycle detection (extra relaxation round still changes dist)
  • Euler path: graph must have 0 or 2 nodes with odd degree; start from odd-degree node if 2 exist
  • Alien Dictionary: word A is prefix of word B but appears after it → return ""
  • Prim's/Kruskal's on disconnected graph → MST impossible, return -1
  • Cheapest Flights: src == dst → cost is 0
  • Critical Connections: parallel edges (multiple edges between same pair) — handle carefully in Tarjan's
  • GCD Traversal: n=1 → always traversable (trivially connected)
# Problem LC # Difficulty Key Technique
1 Path with Minimum Effort 1631 Medium Dijkstra / Binary search
2 Network Delay Time 743 Medium Dijkstra
3 Reconstruct Itinerary 332 Hard Hierholzer's (Euler path)
4 Min Cost to Connect All Points 1584 Medium Prim's / Kruskal's MST
5 Swim in Rising Water 778 Hard Dijkstra / Binary search
6 Alien Dictionary 269 Hard Topological sort
7 Cheapest Flights Within K Stops 787 Medium Bellman-Ford (K relaxations)
8 Find Critical/Pseudo-Critical Edges 1489 Hard Kruskal's + bridge finding
9 Build Matrix with Conditions 2392 Hard Topological sort (2 passes)
10 Greatest Common Divisor Traversal 2709 Hard Union-Find on prime factors
11 Shortest Path in Binary Maze 1091 Medium BFS
12 Number of Ways to Arrive at Destination 1976 Medium Dijkstra + count paths
13 Find City with Smallest Number of Neighbors 1334 Medium Floyd-Warshall / Dijkstra
14 Number of Operations to Connect Network 1319 Medium Union-Find
15 Most Stones Removed with Same Row/Column 947 Medium Union-Find
16 Number of Islands II 305 Hard Online Union-Find
17 Making a Large Island 827 Hard Union-Find + BFS
18 Bridges in Graph 1192 Hard Tarjan's DFS
19 Articulation Points 1568 Hard Tarjan's DFS
20 Kosaraju's Algorithm (SCC) Hard Two-pass DFS
Alien Dictionary ▼ expand
from typing import List
from collections import defaultdict, deque


class Solution:
    """
    ## 269. Alien Dictionary

    There is a new alien language that uses the English alphabet. However, the order among the
    letters is unknown to you.

    You are given a list of strings `words` from the alien language's dictionary, where the
    strings in `words` are **sorted lexicographically** by the rules of this new language.

    Return a string of the unique letters in the new alien language sorted in the order they
    appear in the alien language. If there is no solution, return `""`. If there are multiple
    solutions, return **any of them**.

    ---

    ### Examples

    ```text
    Input:  words = ["wrt","wrf","er","ett","rftt"]
    Output: "wertf"

    Input:  words = ["z","x"]
    Output: "zx"

    Input:  words = ["z","x","z"]
    Output: ""
    Explanation: The order is invalid, so return "".
    ```

    ---

    ### Constraints

    - `1 <= words.length <= 100`
    - `1 <= words[i].length <= 100`
    - `words[i]` consists of only lowercase English letters.
    """

    def alienOrder(self, words: List[str]) -> str:
        # Compare adjacent words to extract character ordering constraints
        # Build directed graph of char dependencies; topological sort gives the order
        # Return '' if cycle detected (contradictory ordering)
        # O(C) — Kahn's BFS topological sort, C = total characters across all words
        adj = defaultdict(set)
        in_degree = {c: 0 for word in words for c in word}

        for i in range(len(words) - 1):
            w1, w2 = words[i], words[i + 1]
            min_len = min(len(w1), len(w2))
            if len(w1) > len(w2) and w1[:min_len] == w2[:min_len]:
                return ""
            for j in range(min_len):
                if w1[j] != w2[j]:
                    if w2[j] not in adj[w1[j]]:
                        adj[w1[j]].add(w2[j])
                        in_degree[w2[j]] += 1
                    break

        q = deque([c for c in in_degree if in_degree[c] == 0])
        result = []
        while q:
            c = q.popleft()
            result.append(c)
            for nb in adj[c]:
                in_degree[nb] -= 1
                if in_degree[nb] == 0:
                    q.append(nb)

        return "".join(result) if len(result) == len(in_degree) else ""

    def alienOrder_dfs(self, words: List[str]) -> str:
        # DFS topological sort with 3-state coloring: unvisited/in-progress/done
        # Cycle = revisiting an in-progress node; append to result in post-order
        # O(C) — DFS topological sort with cycle detection
        adj = defaultdict(set)
        chars = {c for word in words for c in word}

        for i in range(len(words) - 1):
            w1, w2 = words[i], words[i + 1]
            min_len = min(len(w1), len(w2))
            if len(w1) > len(w2) and w1[:min_len] == w2[:min_len]:
                return ""
            for j in range(min_len):
                if w1[j] != w2[j]:
                    adj[w1[j]].add(w2[j])
                    break

        # 0=unvisited, 1=visiting, 2=visited
        state = {c: 0 for c in chars}
        result = []

        def dfs(c: str) -> bool:
            if state[c] == 1:
                return False  # cycle
            if state[c] == 2:
                return True
            state[c] = 1
            for nb in adj[c]:
                if not dfs(nb):
                    return False
            state[c] = 2
            result.append(c)
            return True

        for c in chars:
            if state[c] == 0:
                if not dfs(c):
                    return ""

        return "".join(reversed(result))


if __name__ == '__main__':
    s = Solution()
    # BFS
    assert s.alienOrder(["wrt", "wrf", "er", "ett", "rftt"]) == "wertf"
    assert s.alienOrder(["z", "x"]) == "zx"
    assert s.alienOrder(["z", "x", "z"]) == ""

    # DFS (multiple valid orderings possible; check length and no cycle)
    res = s.alienOrder_dfs(["wrt", "wrf", "er", "ett", "rftt"])
    assert len(res) == 5
    assert s.alienOrder_dfs(["z", "x", "z"]) == ""

    print("All tests passed.")
Articulation Points ▼ expand
"""
# Articulation Points (Tarjan's Algorithm)

Find all vertices whose removal increases the number of connected components
of an undirected graph.

## Examples
```text
Input: n=5, edges=[[0,1],[1,2],[2,0],[1,3],[3,4]]
Output: [1,3]
# Removing node 1 disconnects {0,2} from {3,4}; removing node 3 disconnects {4}.
```

## Constraints
- Non-root node u is an articulation point if some child v has low[v] >= disc[u]
- Root node is an articulation point if it has 2+ children in the DFS tree
- Graph may be disconnected; run DFS from every unvisited node
"""
from typing import List

class Solution:
    # Tarjan's articulation points:
    # node u is AP if: (1) root with 2+ children, OR (2) non-root with low[v] >= disc[u]
    def articulationPoints(self, n: int, edges: List[List[int]]) -> List[int]:
        graph = [[] for _ in range(n)]
        for u, v in edges:
            graph[u].append(v)
            graph[v].append(u)

        disc = [-1] * n
        low = [0] * n
        timer = [0]
        is_ap = [False] * n

        def dfs(node, parent):
            disc[node] = low[node] = timer[0]
            timer[0] += 1
            children = 0
            for neighbor in graph[node]:
                if disc[neighbor] == -1:
                    children += 1
                    dfs(neighbor, node)
                    low[node] = min(low[node], low[neighbor])
                    # non-root AP condition
                    if parent != -1 and low[neighbor] >= disc[node]:
                        is_ap[node] = True
                    # root AP condition: root is AP if it has 2+ children in DFS tree
                    if parent == -1 and children > 1:
                        is_ap[node] = True
                elif neighbor != parent:
                    low[node] = min(low[node], disc[neighbor])

        for i in range(n):
            if disc[i] == -1:
                dfs(i, -1)

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


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

    # Example from README: node 1 and node 3 are articulation points
    assert sol.articulationPoints(5, [[0, 1], [1, 2], [2, 0], [1, 3], [3, 4]]) == [1, 3]

    # Simple bridge: 0-1-2 in a line, both endpoints' middle is AP
    assert sol.articulationPoints(3, [[0, 1], [1, 2]]) == [1]

    # Triangle (cycle) → no articulation points
    assert sol.articulationPoints(3, [[0, 1], [1, 2], [2, 0]]) == []

    # Single node, no edges → no articulation points
    assert sol.articulationPoints(1, []) == []

    print("All tests passed.")
Bridges In Graph ▼ expand
"""
# LC 1192: Critical Connections in a Network (Bridges)

Find all edges whose removal increases the number of connected components
(bridges) of an undirected graph.

## Examples
```text
Input: n=4, connections=[[0,1],[1,2],[2,0],[1,3]]
Output: [[1,3]]
# 0-1-2 form a cycle (no bridge among them); edge (1,3) is a bridge.
```

## Constraints
- Edge (u,v) is a bridge if low[v] > disc[u]
- Pass parent to avoid treating the tree edge back to parent as a back edge
- Graph may be disconnected; run DFS from every unvisited node
"""
from typing import List

class Solution:
    # Tarjan's bridge finding: edge (u,v) is bridge if low[v] > disc[u]
    # low[v] = min discovery time reachable from subtree of v (excluding parent edge)
    def criticalConnections(self, n: int, connections: List[List[int]]) -> List[List[int]]:
        graph = [[] for _ in range(n)]
        for u, v in connections:
            graph[u].append(v)
            graph[v].append(u)

        disc = [-1] * n   # discovery time
        low = [0] * n     # lowest disc reachable
        timer = [0]
        bridges = []

        def dfs(node, parent):
            disc[node] = low[node] = timer[0]
            timer[0] += 1
            for neighbor in graph[node]:
                if disc[neighbor] == -1:  # unvisited
                    dfs(neighbor, node)
                    low[node] = min(low[node], low[neighbor])
                    if low[neighbor] > disc[node]:
                        bridges.append([node, neighbor])  # bridge found
                elif neighbor != parent:  # back edge (not to direct parent)
                    low[node] = min(low[node], disc[neighbor])

        for i in range(n):
            if disc[i] == -1:
                dfs(i, -1)

        return bridges


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

    def norm(bridges):
        # normalize: sort each edge, then sort the list (order not deterministic)
        return sorted(sorted(edge) for edge in bridges)

    # Example from README: only edge (1,3) is a bridge
    assert norm(sol.criticalConnections(4, [[0, 1], [1, 2], [2, 0], [1, 3]])) == [[1, 3]]

    # A path 0-1-2-3: every edge is a bridge
    assert norm(sol.criticalConnections(4, [[0, 1], [1, 2], [2, 3]])) == [[0, 1], [1, 2], [2, 3]]

    # Triangle cycle: no bridges
    assert norm(sol.criticalConnections(3, [[0, 1], [1, 2], [2, 0]])) == []

    print("All tests passed.")
Build A Matrix With Conditions ▼ expand
from typing import List
from collections import defaultdict, deque


class Solution:
    """
    ## 2392. Build a Matrix With Conditions

    You are given a positive integer `k`. You are also given:
    - a 2D integer array `rowConditions` of size `n` where `rowConditions[i] = [above_i, below_i]`,
      meaning row of `above_i` must appear **above** row of `below_i`.
    - a 2D integer array `colConditions` of size `m` where `colConditions[i] = [left_i, right_i]`,
      meaning column of `left_i` must appear to the **left** of column of `right_i`.

    The matrix contains each of the integers from `1` to `k` exactly once. Return **any** matrix
    of size `k x k` satisfying the conditions. If no answer exists, return an empty matrix.

    ---

    ### Examples

    ```text
    Input:  k=3, rowConditions=[[1,2],[3,2]], colConditions=[[2,1],[3,2]]
    Output: [[3,0,0],[0,0,1],[0,2,0]]

    Input:  k=3, rowConditions=[[1,2],[2,3],[3,1]], colConditions=[[1,2]]
    Output: []
    Explanation: Row conditions form a cycle.
    ```

    ---

    ### Constraints

    - `2 <= k <= 400`
    - `1 <= rowConditions.length, colConditions.length <= 10^4`
    - `rowConditions[i].length == colConditions[i].length == 2`
    - `1 <= above_i, below_i, left_i, right_i <= k`
    - `above_i != below_i`
    - `left_i != right_i`
    """

    def buildMatrix(self, k: int, rowConditions: List[List[int]], colConditions: List[List[int]]) -> List[List[int]]:
        # Topological sort row conditions to get row order, col conditions for col order
        # Place each number at (row_pos[num], col_pos[num]) in the k×k matrix
        # Return empty if either topological sort detects a cycle
        # O(k + conditions) — Kahn's BFS topological sort for rows and columns independently
        def topo_sort(conditions: List[List[int]]) -> List[int]:
            adj = defaultdict(list)
            in_deg = defaultdict(int)
            nodes = set(range(1, k + 1))
            for u, v in conditions:
                adj[u].append(v)
                in_deg[v] += 1
                nodes.add(u)
                nodes.add(v)
            q = deque([node for node in range(1, k + 1) if in_deg[node] == 0])
            order = []
            while q:
                node = q.popleft()
                order.append(node)
                for nb in adj[node]:
                    in_deg[nb] -= 1
                    if in_deg[nb] == 0:
                        q.append(nb)
            return order if len(order) == k else []

        row_order = topo_sort(rowConditions)
        col_order = topo_sort(colConditions)
        if not row_order or not col_order:
            return []

        row_pos = {val: i for i, val in enumerate(row_order)}
        col_pos = {val: i for i, val in enumerate(col_order)}

        matrix = [[0] * k for _ in range(k)]
        for val in range(1, k + 1):
            matrix[row_pos[val]][col_pos[val]] = val
        return matrix


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

    res = s.buildMatrix(3, [[1, 2], [3, 2]], [[2, 1], [3, 2]])
    assert len(res) == 3 and len(res[0]) == 3
    # Verify each number 1-3 appears exactly once
    flat = [x for row in res for x in row]
    assert sorted(x for x in flat if x != 0) == [1, 2, 3]

    # Cycle in row conditions -> empty
    assert s.buildMatrix(3, [[1, 2], [2, 3], [3, 1]], [[1, 2]]) == []

    print("All tests passed.")
Cheapest Flights Within K Stops ▼ expand
from typing import List
import heapq
from collections import defaultdict


class Solution:
    """
    ## 787. Cheapest Flights Within K Stops

    There are `n` cities connected by some number of flights. You are given an array `flights`
    where `flights[i] = [from_i, to_i, price_i]` indicates that there is a flight from city
    `from_i` to city `to_i` with cost `price_i`.

    Given three integers `src`, `dst`, and `k`, return the **cheapest price** from `src` to `dst`
    with at most `k` stops. If there is no such route, return `-1`.

    ---

    ### Examples

    ```text
    Input:  n=4, flights=[[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src=0, dst=3, k=1
    Output: 700
    Explanation: 0 -> 1 -> 3 costs 700.

    Input:  n=3, flights=[[0,1,100],[1,2,100],[0,2,500]], src=0, dst=2, k=1
    Output: 200

    Input:  n=3, flights=[[0,1,100],[1,2,100],[0,2,500]], src=0, dst=2, k=0
    Output: 500
    ```

    ---

    ### Constraints

    - `1 <= n <= 100`
    - `0 <= flights.length <= n * (n - 1) / 2`
    - `flights[i].length == 3`
    - `0 <= from_i, to_i < n`
    - `from_i != to_i`
    - `1 <= price_i <= 10^4`
    - There will not be any multiple flights between two cities.
    - `0 <= k <= n`
    """

    def findCheapestPrice(self, n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int:
        # Bellman-Ford with k+1 relaxation rounds (k stops = k+1 edges)
        # Use a copy of prices each round to prevent using more than one edge per round
        # O(k*E) — Bellman-Ford limited to k+1 relaxation rounds
        prices = [float('inf')] * n
        prices[src] = 0

        for _ in range(k + 1):
            tmp = prices[:]
            for u, v, w in flights:
                if prices[u] + w < tmp[v]:
                    tmp[v] = prices[u] + w
            prices = tmp

        return prices[dst] if prices[dst] < float('inf') else -1

    def findCheapestPrice_dijkstra(self, n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int:
        # O(V*E) — modified Dijkstra tracking stops; heap entry: (cost, node, stops_used)
        graph = defaultdict(list)
        for u, v, w in flights:
            graph[u].append((v, w))

        # dist[node][stops] = min cost
        dist = [[float('inf')] * (k + 2) for _ in range(n)]
        dist[src][0] = 0
        heap = [(0, src, 0)]  # (cost, node, stops)

        while heap:
            cost, u, stops = heapq.heappop(heap)
            if u == dst:
                return cost
            if stops > k:
                continue
            for v, w in graph[u]:
                new_cost = cost + w
                if new_cost < dist[v][stops + 1]:
                    dist[v][stops + 1] = new_cost
                    heapq.heappush(heap, (new_cost, v, stops + 1))

        return -1


if __name__ == '__main__':
    s = Solution()
    assert s.findCheapestPrice(4, [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], 0, 3, 1) == 700
    assert s.findCheapestPrice(3, [[0,1,100],[1,2,100],[0,2,500]], 0, 2, 1) == 200
    assert s.findCheapestPrice(3, [[0,1,100],[1,2,100],[0,2,500]], 0, 2, 0) == 500

    assert s.findCheapestPrice_dijkstra(4, [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], 0, 3, 1) == 700
    assert s.findCheapestPrice_dijkstra(3, [[0,1,100],[1,2,100],[0,2,500]], 0, 2, 1) == 200
    assert s.findCheapestPrice_dijkstra(3, [[0,1,100],[1,2,100],[0,2,500]], 0, 2, 0) == 500

    print("All tests passed.")
Find City With Smallest Neighbors ▼ expand
"""
# LC 1334: Find the City With the Smallest Number of Neighbors at a Threshold Distance

Find the city that has the fewest other cities reachable within
`distanceThreshold`. On a tie, return the city with the largest index.

## Examples
```text
Input: n=4, edges=[[0,1,3],[1,2,1],[1,3,4],[2,3,1]], distanceThreshold=4
Output: 3
# City 3 reaches {1,2} (2 cities), the smallest count.
```

## Constraints
- Weighted undirected graph; use Floyd-Warshall all-pairs shortest paths — O(n^3)
- Tie-break toward the larger index (use <= when comparing counts)
- Unreachable pairs remain at infinity
"""
from typing import List

class Solution:
    # Floyd-Warshall: compute all-pairs shortest paths, then count reachable cities
    def findTheCity(self, n: int, edges: List[List[int]], distanceThreshold: int) -> int:
        INF = float('inf')
        dist = [[INF] * n for _ in range(n)]

        for i in range(n):
            dist[i][i] = 0  # distance to self is 0

        for u, v, w in edges:
            dist[u][v] = w
            dist[v][u] = w

        # relax all pairs through intermediate node k
        for k in range(n):
            for i in range(n):
                for j in range(n):
                    if dist[i][k] + dist[k][j] < dist[i][j]:
                        dist[i][j] = dist[i][k] + dist[k][j]

        # find city with fewest reachable neighbors within threshold
        # tie-break: pick the city with larger index
        best_city, min_count = -1, INF
        for i in range(n):
            count = sum(1 for j in range(n) if i != j and dist[i][j] <= distanceThreshold)
            if count <= min_count:  # <= ensures we pick larger index on tie
                min_count = count
                best_city = i

        return best_city


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

    # Example from README
    assert sol.findTheCity(4, [[0, 1, 3], [1, 2, 1], [1, 3, 4], [2, 3, 1]], 4) == 3

    # LC classic example: threshold 2 → city 0 has fewest, tie broken by larger index
    assert sol.findTheCity(5, [[0, 1, 2], [0, 4, 8], [1, 2, 3], [1, 4, 2], [2, 3, 1], [3, 4, 1]], 2) == 0

    # Two isolated cities, threshold 0 → each reaches nobody, pick larger index
    assert sol.findTheCity(2, [], 0) == 1

    print("All tests passed.")
Find Critical And Pseudo Critical Edges ▼ expand
from typing import List


class Solution:
    """
    ## 1489. Find Critical and Pseudo-Critical Edges in Minimum Spanning Tree

    Given a weighted undirected connected graph with `n` vertices numbered from `0` to `n - 1`,
    and an array `edges` where `edges[i] = [ai, bi, weighti]` represents a bidirectional and
    weighted edge between nodes `ai` and `bi`.

    A minimum spanning tree (MST) is a subset of the graph's edges that connects all vertices
    without cycles and with the minimum possible total edge weight.

    Find all the **critical** and **pseudo-critical** edges in the given graph's MST.

    - An MST edge whose deletion from the graph would cause the MST weight to increase is called
      a **critical edge**.
    - A **pseudo-critical edge** is one that can appear in some MSTs but not all.

    Return a list of two lists: the first list contains the indices of all critical edges, and
    the second list contains the indices of all pseudo-critical edges.

    ---

    ### Examples

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

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

    ---

    ### Constraints

    - `2 <= n <= 100`
    - `1 <= edges.length <= min(200, n * (n - 1) / 2)`
    - `edges[i].length == 3`
    - `0 <= ai < bi < n`
    - `1 <= weighti <= 1000`
    - All pairs `(ai, bi)` are distinct.
    """

    def findCriticalAndPseudoCriticalEdges(self, n: int, edges: List[List[int]]) -> List[List[int]]:
        # Critical edge: removing it increases MST weight
        # Pseudo-critical: forcing it in doesn't increase MST weight
        # Use Kruskal's MST as baseline; test each edge by exclusion/inclusion
        # O(E^2 * alpha(n)) — Kruskal's with per-edge inclusion/exclusion to find base MST weight
        indexed = [(w, u, v, i) for i, (u, v, w) in enumerate(edges)]
        indexed.sort()

        def make_uf():
            parent = list(range(n))
            rank = [0] * n
            return parent, rank

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

        def union(parent, rank, x, y):
            px, py = find(parent, x), find(parent, 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

        def kruskal(skip_idx=-1, force_idx=-1):
            parent, rank = make_uf()
            weight = 0
            cnt = 0
            if force_idx != -1:
                w, u, v, _ = indexed[force_idx]
                union(parent, rank, u, v)
                weight += w
                cnt += 1
            for i, (w, u, v, orig) in enumerate(indexed):
                if i == skip_idx:
                    continue
                if union(parent, rank, u, v):
                    weight += w
                    cnt += 1
            return weight if cnt == n - 1 else float('inf')

        base = kruskal()
        critical, pseudo = [], []

        for i in range(len(indexed)):
            # Critical: removing raises MST weight
            if kruskal(skip_idx=i) > base:
                critical.append(indexed[i][3])
            # Pseudo-critical: forcing inclusion keeps same MST weight
            elif kruskal(force_idx=i) == base:
                pseudo.append(indexed[i][3])

        return [sorted(critical), sorted(pseudo)]


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

    res2 = s.findCriticalAndPseudoCriticalEdges(4, [[0,1,1],[1,2,1],[0,2,1],[2,3,1]])
    assert res2 == [[3], [0, 1, 2]], res2

    print("All tests passed.")
Greatest Common Divisor Traversal ▼ expand
from typing import List
import math


class Solution:
    """
    ## 2709. Greatest Common Divisor Traversal

    You are given a **0-indexed** integer array `nums`. There is an edge between two indices `i`
    and `j` if and only if `gcd(nums[i], nums[j]) > 1`.

    Return `true` if all the indices can be traversed (i.e., the graph is connected), or `false`
    otherwise.

    Note that a graph is connected if there is a path between every pair of vertices.

    ---

    ### Examples

    ```text
    Input:  nums = [2,3,6]
    Output: true
    Explanation: 2 and 6 share factor 2; 3 and 6 share factor 3. All connected.

    Input:  nums = [3,9,5]
    Output: false
    Explanation: 5 shares no factor with 3 or 9.

    Input:  nums = [4,3,12,8]
    Output: true
    ```

    ---

    ### Constraints

    - `1 <= nums.length <= 10^5`
    - `1 <= nums[i] <= 10^5`
    """

    def canTraverseAllPairs(self, nums: List[int]) -> bool:
        # Union-Find: connect indices that share a common prime factor
        # Factorize each number; union it with any previous number sharing a prime
        # All indices reachable iff they all belong to the same component
        # O(n * sqrt(max_val)) — Union-Find connecting indices via shared prime factors
        if len(nums) == 1:
            return True

        parent = list(range(len(nums) + 100001))  # indices 0..n-1, primes mapped to n+prime
        rank = [0] * (len(nums) + 100001)
        n = len(nums)

        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) -> None:
            px, py = find(x), find(y)
            if px == py:
                return
            if rank[px] < rank[py]:
                px, py = py, px
            parent[py] = px
            if rank[px] == rank[py]:
                rank[px] += 1

        prime_to_idx = {}  # first index that had this prime factor

        for i, num in enumerate(nums):
            val = num
            d = 2
            while d * d <= val:
                if val % d == 0:
                    if d in prime_to_idx:
                        union(i, prime_to_idx[d])
                    else:
                        prime_to_idx[d] = i
                    while val % d == 0:
                        val //= d
                d += 1
            if val > 1:
                if val in prime_to_idx:
                    union(i, prime_to_idx[val])
                else:
                    prime_to_idx[val] = i

        root = find(0)
        return all(find(i) == root for i in range(n))


if __name__ == '__main__':
    s = Solution()
    assert s.canTraverseAllPairs([2, 3, 6]) == True
    assert s.canTraverseAllPairs([3, 9, 5]) == False
    assert s.canTraverseAllPairs([4, 3, 12, 8]) == True
    assert s.canTraverseAllPairs([1]) == True
    assert s.canTraverseAllPairs([2, 2]) == True

    print("All tests passed.")
Kosarajus Algorithm ▼ expand
"""
# Kosaraju's Algorithm — Strongly Connected Components

Find all Strongly Connected Components (SCCs) in a directed graph. An SCC is a
maximal set of nodes where every node is reachable from every other node.

## Examples
```text
Input: n=5, edges=[[0,1],[1,2],[2,0],[1,3],[3,4]]
Output: [[0,1,2],[3],[4]]
# 0->1->2->0 form a cycle (one SCC); 3 and 4 are singletons.
```

## Constraints
- Two-pass DFS — O(V+E)
- Pass 1 records finish order on the original graph
- Pass 2 runs DFS on the transposed graph in reverse finish order
"""
from typing import List
from collections import defaultdict

class Solution:
    # Kosaraju's: 1) DFS on original, push to stack in finish order
    #             2) Transpose graph  3) DFS in reverse finish order on transposed
    def findSCCs(self, n: int, edges: List[List[int]]) -> List[List[int]]:
        graph = defaultdict(list)
        rev_graph = defaultdict(list)
        for u, v in edges:
            graph[u].append(v)
            rev_graph[v].append(u)

        visited = [False] * n
        stack = []

        def dfs1(node):
            visited[node] = True
            for neighbor in graph[node]:
                if not visited[neighbor]:
                    dfs1(neighbor)
            stack.append(node)  # push after all descendants processed

        def dfs2(node, component):
            visited[node] = True
            component.append(node)
            for neighbor in rev_graph[node]:
                if not visited[neighbor]:
                    dfs2(neighbor, component)

        # Pass 1: fill stack with finish order
        for i in range(n):
            if not visited[i]:
                dfs1(i)

        # Pass 2: process in reverse finish order on transposed graph
        visited = [False] * n
        sccs = []
        while stack:
            node = stack.pop()
            if not visited[node]:
                component = []
                dfs2(node, component)
                sccs.append(sorted(component))

        return sccs


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

    # SCC order in the outer list is not deterministic → compare sorted list of components
    assert sorted(sol.findSCCs(5, [[0, 1], [1, 2], [2, 0], [1, 3], [3, 4]])) == [[0, 1, 2], [3], [4]]

    # Single cycle over all nodes → one SCC
    assert sorted(sol.findSCCs(3, [[0, 1], [1, 2], [2, 0]])) == [[0, 1, 2]]

    # DAG → every node is its own SCC
    assert sorted(sol.findSCCs(3, [[0, 1], [1, 2]])) == [[0], [1], [2]]

    print("All tests passed.")
Making A Large Island ▼ expand
"""
# LC 827: Making A Large Island

Flip at most one `0` to `1` in an n x n binary grid. Return the size of the
largest island (4-directionally connected group of 1s) after the flip.

## Examples
```text
Input: grid=[[1,0],[0,1]]
Output: 3
# Flipping the 0 at (0,1) (or (1,0)) bridges the two size-1 islands → size 3.
```

## Constraints
- Label + lookup — O(n^2)
- Label each island with a unique id (starting at 2) and record its size
- For each 0 cell, sum sizes of distinct neighboring islands + 1
- If the grid is all 1s (no 0 to flip), the answer is n^2
"""
from typing import List
from collections import defaultdict

class Solution:
    # Label each island with unique id, store sizes. For each 0, sum unique neighbor island sizes + 1.
    def largestIsland(self, grid: List[List[int]]) -> int:
        n = len(grid)
        island_id = 2  # start from 2 (0 and 1 are used)
        island_size = defaultdict(int)

        def dfs(r, c, iid):
            if r < 0 or r >= n or c < 0 or c >= n or grid[r][c] != 1:
                return 0
            grid[r][c] = iid  # label with island id
            return 1 + dfs(r+1,c,iid) + dfs(r-1,c,iid) + dfs(r,c+1,iid) + dfs(r,c-1,iid)

        # label all islands and record their sizes
        for r in range(n):
            for c in range(n):
                if grid[r][c] == 1:
                    island_size[island_id] = dfs(r, c, island_id)
                    island_id += 1

        # best without flipping (all 1s case)
        result = max(island_size.values(), default=0)

        # try flipping each 0
        for r in range(n):
            for c in range(n):
                if grid[r][c] == 0:
                    seen = set()
                    size = 1  # the flipped cell itself
                    for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)]:
                        nr, nc = r + dr, c + dc
                        if 0 <= nr < n and 0 <= nc < n and grid[nr][nc] > 1:
                            iid = grid[nr][nc]
                            if iid not in seen:
                                size += island_size[iid]
                                seen.add(iid)
                    result = max(result, size)

        return result


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

    # Example from README (solution mutates grid, so pass a fresh copy each time)
    assert sol.largestIsland([[1, 0], [0, 1]]) == 3

    # Flipping the single 0 joins two halves → size 4
    assert sol.largestIsland([[1, 1], [1, 0]]) == 4

    # All ones, no 0 to flip → answer is n^2
    assert sol.largestIsland([[1, 1], [1, 1]]) == 4

    # All zeros → flip any one cell → island of size 1
    assert sol.largestIsland([[0, 0], [0, 0]]) == 1

    print("All tests passed.")
Min Cost To Connect All Points ▼ expand
from typing import List
import heapq


class Solution:
    """
    ## 1584. Min Cost to Connect All Points

    You are given an array `points` representing integer coordinates of some points on a 2D plane,
    where `points[i] = [xi, yi]`.

    The cost of connecting two points `[xi, yi]` and `[xj, yj]` is the **Manhattan distance**
    between them: `|xi - xj| + |yi - yj|`.

    Return the minimum cost to make all points connected. All points are connected if there is
    exactly one simple path between any two points.

    ---

    ### Examples

    ```text
    Input:  points = [[0,0],[2,2],[3,10],[5,2],[7,0]]
    Output: 20

    Input:  points = [[3,12],[-2,5],[-4,1]]
    Output: 18
    ```

    ---

    ### Constraints

    - `1 <= points.length <= 1000`
    - `-10^6 <= xi, yi <= 10^6`
    - All pairs `(xi, yi)` are distinct.
    """

    def minCostConnectPoints(self, points: List[List[int]]) -> int:
        # Prim's MST: start from any node, greedily add cheapest edge to unvisited node
        # Manhattan distance is the edge weight between two points
        # O(n^2) — Prim's algorithm with visited set
        n = len(points)
        visited = set()
        heap = [(0, 0)]  # (cost, index)
        total = 0

        while len(visited) < n:
            cost, i = heapq.heappop(heap)
            if i in visited:
                continue
            visited.add(i)
            total += cost
            for j in range(n):
                if j not in visited:
                    dist = abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1])
                    heapq.heappush(heap, (dist, j))

        return total

    def minCostConnectPoints_kruskal(self, points: List[List[int]]) -> int:
        # O(n^2 log n) — Kruskal's with Union-Find on all edges
        n = len(points)
        edges = []
        for i in range(n):
            for j in range(i + 1, n):
                dist = abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1])
                edges.append((dist, i, j))
        edges.sort()

        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

        total, edges_used = 0, 0
        for dist, i, j in edges:
            if union(i, j):
                total += dist
                edges_used += 1
                if edges_used == n - 1:
                    break
        return total


if __name__ == '__main__':
    s = Solution()
    assert s.minCostConnectPoints([[0, 0], [2, 2], [3, 10], [5, 2], [7, 0]]) == 20
    assert s.minCostConnectPoints([[3, 12], [-2, 5], [-4, 1]]) == 18
    assert s.minCostConnectPoints([[0, 0]]) == 0

    assert s.minCostConnectPoints_kruskal([[0, 0], [2, 2], [3, 10], [5, 2], [7, 0]]) == 20
    assert s.minCostConnectPoints_kruskal([[3, 12], [-2, 5], [-4, 1]]) == 18

    print("All tests passed.")
Most Stones Removed ▼ expand
"""
# LC 947: Most Stones Removed with Same Row or Column

A stone can be removed if it shares a row or column with another remaining
stone. Return the maximum number of stones that can be removed.

## Examples
```text
Input: stones=[[0,0],[0,1],[1,0],[1,2],[2,1],[2,2]]
Output: 5
# All 6 stones form one connected component → remove 6 - 1 = 5.
```

## Constraints
- Union-Find grouping stones that share a row or column — O(n)
- Offset column indices to keep row and column keys in a shared namespace
- Answer = total_stones - number_of_components
"""
from typing import List

class Solution:
    # Union-Find: group stones sharing row or col. Each group can be reduced to 1 stone.
    # Answer = total_stones - number_of_groups
    def removeStones(self, stones: List[List[int]]) -> int:
        parent = {}

        def find(x):
            if x not in parent:
                parent[x] = x
            if parent[x] != x:
                parent[x] = find(parent[x])
            return parent[x]

        def union(x, y):
            parent[find(x)] = find(y)

        for r, c in stones:
            # offset col by 10001 to avoid collision with row indices
            union(r, c + 10001)

        # count distinct components among stones
        components = len({find(r) for r, c in stones})
        return len(stones) - components


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

    # Example from README: one big component → remove all but one
    assert sol.removeStones([[0, 0], [0, 1], [1, 0], [1, 2], [2, 1], [2, 2]]) == 5

    # LC example 2: two components → remove 5 - 2 = 3
    assert sol.removeStones([[0, 0], [0, 2], [1, 1], [2, 0], [2, 2]]) == 3

    # Single stone → nothing to remove
    assert sol.removeStones([[0, 0]]) == 0

    print("All tests passed.")
Network Delay Time ▼ expand
from typing import List
import heapq
from collections import defaultdict


class Solution:
    """
    ## 743. Network Delay Time

    You are given a network of `n` nodes, labeled from `1` to `n`. You are also given `times`,
    a list of travel times as directed edges `times[i] = (ui, vi, wi)`, where `ui` is the source
    node, `vi` is the target node, and `wi` is the time it takes for a signal to travel from
    source to target.

    We will send a signal from a given node `k`. Return the **minimum time** it takes for all
    the `n` nodes to receive the signal. If it is impossible for all the `n` nodes to receive
    the signal, return `-1`.

    ---

    ### Examples

    ```text
    Input:  times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2
    Output: 2

    Input:  times = [[1,2,1]], n = 2, k = 1
    Output: 1

    Input:  times = [[1,2,1]], n = 2, k = 2
    Output: -1
    ```

    ---

    ### Constraints

    - `1 <= k <= n <= 100`
    - `1 <= times.length <= 6000`
    - `times[i].length == 3`
    - `1 <= ui, vi <= n`
    - `ui != vi`
    - `0 <= wi <= 100`
    - All pairs `(ui, vi)` are unique.
    """

    def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
        # Dijkstra from source k; shortest path to each node = signal arrival time
        # Answer is max of all shortest paths; -1 if any node unreachable
        # O((V+E)logV) — Dijkstra's single-source shortest path
        graph = defaultdict(list)
        for u, v, w in times:
            graph[u].append((v, w))

        dist = {i: float('inf') for i in range(1, n + 1)}
        dist[k] = 0
        heap = [(0, k)]

        while heap:
            d, u = heapq.heappop(heap)
            if d > dist[u]:
                continue
            for v, w in graph[u]:
                if dist[u] + w < dist[v]:
                    dist[v] = dist[u] + w
                    heapq.heappush(heap, (dist[v], v))

        ans = max(dist.values())
        return ans if ans < float('inf') else -1

    def networkDelayTime_bellman_ford(self, times: List[List[int]], n: int, k: int) -> int:
        # O(V*E) — Bellman-Ford relaxing all edges V-1 times
        dist = {i: float('inf') for i in range(1, n + 1)}
        dist[k] = 0

        for _ in range(n - 1):
            for u, v, w in times:
                if dist[u] + w < dist[v]:
                    dist[v] = dist[u] + w

        ans = max(dist.values())
        return ans if ans < float('inf') else -1


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

    assert s.networkDelayTime_bellman_ford([[2, 1, 1], [2, 3, 1], [3, 4, 1]], 4, 2) == 2
    assert s.networkDelayTime_bellman_ford([[1, 2, 1]], 2, 1) == 1
    assert s.networkDelayTime_bellman_ford([[1, 2, 1]], 2, 2) == -1

    print("All tests passed.")
Number Of Islands Ii ▼ expand
"""
# LC 305: Number of Islands II

Given an m x n grid, add land cells one at a time from `positions`. After each
addition, return the current number of islands (4-directionally connected).

## Examples
```text
Input: m=3, n=3, positions=[[0,0],[0,1],[1,2],[2,1]]
Output: [1,1,2,3]
# (0,1) merges with (0,0); the last two additions are isolated.
```

## Constraints
- Online Union-Find with lazy initialization — O(k * alpha(mn))
- Encode a cell (r,c) as r*n + c
- Duplicate positions leave the island count unchanged
"""
from typing import List

class Solution:
    # Union-Find with lazy initialization - add land cells one by one
    def numIslands2(self, m: int, n: int, positions: List[List[int]]) -> List[int]:
        parent = {}
        rank = {}
        count = 0

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

        def union(x, y):
            nonlocal count
            px, py = find(x), find(y)
            if px == py:
                return
            if rank[px] < rank[py]:
                px, py = py, px
            parent[py] = px
            if rank[px] == rank[py]:
                rank[px] += 1
            count -= 1  # merging two islands

        result = []
        for r, c in positions:
            cell = r * n + c
            if cell in parent:  # duplicate position
                result.append(count)
                continue

            parent[cell] = cell  # new island
            rank[cell] = 0
            count += 1

            for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)]:
                nr, nc = r + dr, c + dc
                neighbor = nr * n + nc
                if 0 <= nr < m and 0 <= nc < n and neighbor in parent:
                    union(cell, neighbor)  # merge with adjacent land

            result.append(count)
        return result


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

    # Example from README
    assert sol.numIslands2(3, 3, [[0, 0], [0, 1], [1, 2], [2, 1]]) == [1, 1, 2, 3]

    # LC classic example
    assert sol.numIslands2(3, 3, [[0, 0], [0, 1], [1, 2], [2, 1]]) == [1, 1, 2, 3]

    # Duplicate position leaves count unchanged
    assert sol.numIslands2(2, 2, [[0, 0], [0, 0], [1, 1]]) == [1, 1, 2]

    # Chain that eventually merges into one island
    assert sol.numIslands2(1, 3, [[0, 0], [0, 2], [0, 1]]) == [1, 2, 1]

    print("All tests passed.")
Number Of Operations To Connect Network ▼ expand
"""
# LC 1319: Number of Operations to Make Network Connected

Find the minimum number of cable moves needed to connect all `n` computers. A
cable can be reused from any redundant connection. Return -1 if impossible.

## Examples
```text
Input: n=4, connections=[[0,1],[0,2],[1,2]]
Output: 1
# Edge (1,2) is redundant; move it to connect the isolated computer 3.
```

## Constraints
- Union-Find — O(n + e)
- If len(connections) < n-1 → return -1 (not enough cables)
- Answer = number_of_components - 1
"""
from typing import List

class Solution:
    # Union-Find: count components and redundant edges
    # Need (components-1) moves; possible only if redundant_edges >= components-1
    def makeConnected(self, n: int, connections: List[List[int]]) -> int:
        if len(connections) < n - 1:
            return -1  # not enough edges to connect n nodes

        parent = list(range(n))
        rank = [0] * n

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

        def union(x, y):
            px, py = find(x), find(y)
            if px == py:
                return False  # redundant edge
            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 u, v in connections:
            if union(u, v):
                components -= 1  # merged two components

        return components - 1  # need this many moves to connect remaining components


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

    # Example from README: one redundant edge, one isolated node
    assert sol.makeConnected(4, [[0, 1], [0, 2], [1, 2]]) == 1

    # LC example 2: two redundant edges cover two extra components
    assert sol.makeConnected(6, [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3]]) == 2

    # Not enough cables to ever connect n nodes
    assert sol.makeConnected(6, [[0, 1], [0, 2], [0, 3], [1, 2]]) == -1

    # Already fully connected → no moves needed
    assert sol.makeConnected(1, []) == 0

    print("All tests passed.")
Number Of Ways To Arrive ▼ expand
"""
# LC 1976: Number of Ways to Arrive at Destination

Count the number of shortest paths from node `0` to node `n-1` in a weighted
undirected graph. Return the count modulo 10^9 + 7.

## Examples
```text
Input: n=7, roads=[[0,6,7],[0,1,2],[1,2,3],[1,3,3],[6,3,3],
                    [3,5,1],[6,5,1],[2,5,1],[0,4,5],[4,6,2]]
Output: 4
# Four distinct paths achieve the minimum travel time.
```

## Constraints
- Dijkstra + path count — O((V+E) log V)
- Shorter path found → ways[v] = ways[u]; equal path → ways[v] += ways[u]
- Apply the modulo at every addition
"""
from typing import List
import heapq

class Solution:
    # Dijkstra + count: when we find a shorter path, reset count; equal path, add count
    def countPaths(self, n: int, roads: List[List[int]]) -> int:
        MOD = 10**9 + 7
        graph = [[] for _ in range(n)]
        for u, v, w in roads:
            graph[u].append((v, w))
            graph[v].append((u, w))

        dist = [float('inf')] * n
        ways = [0] * n
        dist[0] = 0
        ways[0] = 1  # one way to reach source

        heap = [(0, 0)]  # (distance, node)
        while heap:
            d, u = heapq.heappop(heap)
            if d > dist[u]:
                continue  # stale entry
            for v, w in graph[u]:
                if dist[u] + w < dist[v]:
                    dist[v] = dist[u] + w
                    ways[v] = ways[u]  # new shortest path found, inherit count
                    heapq.heappush(heap, (dist[v], v))
                elif dist[u] + w == dist[v]:
                    ways[v] = (ways[v] + ways[u]) % MOD  # another equally short path

        return ways[n - 1]


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

    # LC classic example: 4 shortest paths from 0 to 6
    assert sol.countPaths(7, [
        [0, 6, 7], [0, 1, 2], [1, 2, 3], [1, 3, 3], [6, 3, 3],
        [3, 5, 1], [6, 5, 1], [2, 5, 1], [0, 4, 5], [4, 6, 2],
    ]) == 4

    # Single edge → exactly one shortest path
    assert sol.countPaths(2, [[0, 1, 1]]) == 1

    # Two equal-length parallel routes 0->1->3 and 0->2->3
    assert sol.countPaths(4, [[0, 1, 1], [1, 3, 1], [0, 2, 1], [2, 3, 1]]) == 2

    print("All tests passed.")
Path With Minimum Effort ▼ expand
from typing import List
from collections import deque
import heapq


class Solution:
    """
    ## 1631. Path With Minimum Effort

    You are a hiker preparing for an upcoming hike. You are given `heights`, a 2D array of size
    `rows x columns`, where `heights[row][col]` represents the height of cell `(row, col)`.
    You are situated in the top-left cell, `(0, 0)`, and you hope to travel to the bottom-right
    cell, `(rows-1, columns-1)`. You can move **up**, **down**, **left**, or **right**, and you
    wish to find the route that requires the minimum **effort**.

    The **effort** of a route is the maximum absolute difference in heights between two consecutive
    cells of the route.

    Return the minimum effort required to travel from the top-left cell to the bottom-right cell.

    ---

    ### Examples

    ```text
    Input:  heights = [[1,2,2],[3,8,2],[5,3,5]]
    Output: 2
    Explanation: The route [1,3,5,3,5] has a maximum absolute difference of 2.

    Input:  heights = [[1,2,3],[3,8,4],[5,3,5]]
    Output: 1
    Explanation: The route [1,2,3,4,5] has a maximum absolute difference of 1.

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

    ---

    ### Constraints

    - `rows == heights.length`
    - `cols == heights[i].length`
    - `1 <= rows, cols <= 100`
    - `1 <= heights[i][j] <= 10^6`
    """

    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        # Dijkstra: minimize the maximum absolute difference along the path
        # Priority queue stores (max_effort_so_far, row, col)
        # O(m*n*log(10^6)) — binary search on effort, BFS to validate
        rows, cols = len(heights), len(heights[0])
        dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)]

        def can_reach(effort: int) -> bool:
            visited = [[False] * cols for _ in range(rows)]
            q = deque([(0, 0)])
            visited[0][0] = True
            while q:
                r, c = q.popleft()
                if r == rows - 1 and c == cols - 1:
                    return True
                for dr, dc in dirs:
                    nr, nc = r + dr, c + dc
                    if 0 <= nr < rows and 0 <= nc < cols and not visited[nr][nc]:
                        if abs(heights[nr][nc] - heights[r][c]) <= effort:
                            visited[nr][nc] = True
                            q.append((nr, nc))
            return False

        lo, hi = 0, 10**6 - 1
        while lo < hi:
            mid = (lo + hi) // 2
            if can_reach(mid):
                hi = mid
            else:
                lo = mid + 1
        return lo

    def minimumEffortPath_dijkstra(self, heights: List[List[int]]) -> int:
        # O(m*n*log(m*n)) — Dijkstra treating effort as edge weight
        rows, cols = len(heights), len(heights[0])
        dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)]
        effort = [[float('inf')] * cols for _ in range(rows)]
        effort[0][0] = 0
        heap = [(0, 0, 0)]  # (effort, row, col)

        while heap:
            e, r, c = heapq.heappop(heap)
            if r == rows - 1 and c == cols - 1:
                return e
            if e > effort[r][c]:
                continue
            for dr, dc in dirs:
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols:
                    new_e = max(e, abs(heights[nr][nc] - heights[r][c]))
                    if new_e < effort[nr][nc]:
                        effort[nr][nc] = new_e
                        heapq.heappush(heap, (new_e, nr, nc))
        return effort[rows - 1][cols - 1]


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

    assert s.minimumEffortPath_dijkstra([[1, 2, 2], [3, 8, 2], [5, 3, 5]]) == 2
    assert s.minimumEffortPath_dijkstra([[1, 2, 3], [3, 8, 4], [5, 3, 5]]) == 1

    print("All tests passed.")
Reconstruct Itinerary ▼ expand
from typing import List
from collections import defaultdict


class Solution:
    """
    ## 332. Reconstruct Itinerary

    You are given a list of airline `tickets` represented by pairs of departure and arrival
    airports `[from, to]`. Reconstruct the itinerary in order. All of the tickets must be used
    once and only once.

    The itinerary must begin with `"JFK"`. If there are multiple valid itineraries, return the
    itinerary that has the **smallest lexical order** when read as a single string.

    You may assume all tickets form at least one valid itinerary. You must use all the tickets.

    ---

    ### Examples

    ```text
    Input:  tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]]
    Output: ["JFK","MUC","LHR","SFO","SJC"]

    Input:  tickets = [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
    Output: ["JFK","ATL","JFK","SFO","ATL","SFO"]
    ```

    ---

    ### Constraints

    - `1 <= tickets.length <= 300`
    - `tickets[i].length == 2`
    - `from_i` and `to_i` consist of uppercase English letters.
    - `from_i != to_i`
    """

    def findItinerary(self, tickets: List[List[str]]) -> List[str]:
        # Hierholzer's algorithm for Eulerian path: DFS, add to result on backtrack
        # Sort destinations so lexicographically smallest path is found first
        # O(E^2) — DFS with backtracking on sorted adjacency lists
        graph = defaultdict(list)
        for src, dst in sorted(tickets, reverse=True):
            graph[src].append(dst)
        # Sort descending so we can pop from end (cheapest pop) in lex order
        for k in graph:
            graph[k].sort(reverse=True)

        result = []

        def dfs(airport: str) -> None:
            while graph[airport]:
                dfs(graph[airport].pop())
            result.append(airport)

        dfs("JFK")
        return result[::-1]

    def findItinerary_hierholzer(self, tickets: List[List[str]]) -> List[str]:
        # O(E log E) — Hierholzer's iterative Eulerian path
        graph = defaultdict(list)
        for src, dst in tickets:
            graph[src].append(dst)
        for k in graph:
            graph[k].sort(reverse=True)

        stack = ["JFK"]
        result = []
        while stack:
            while graph[stack[-1]]:
                stack.append(graph[stack[-1]].pop())
            result.append(stack.pop())
        return result[::-1]


if __name__ == '__main__':
    s = Solution()
    t1 = [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]]
    assert s.findItinerary(t1) == ["JFK", "MUC", "LHR", "SFO", "SJC"]

    t2 = [["JFK", "SFO"], ["JFK", "ATL"], ["SFO", "ATL"], ["ATL", "JFK"], ["ATL", "SFO"]]
    assert s.findItinerary(t2) == ["JFK", "ATL", "JFK", "SFO", "ATL", "SFO"]

    t3 = [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]]
    assert s.findItinerary_hierholzer(t3) == ["JFK", "MUC", "LHR", "SFO", "SJC"]

    print("All tests passed.")
Shortest Path In Binary Maze ▼ expand
"""
# LC 1091: Shortest Path in Binary Matrix

Find the length of the shortest clear path (cells with value `0`) from `(0,0)`
to `(n-1,n-1)` moving in 8 directions. Return -1 if no such path exists.

## Examples
```text
Input: grid=[[0,1],[1,0]]
Output: 2
# Move diagonally (0,0) -> (1,1); both cells are clear.
```

## Constraints
- BFS on an unweighted grid — O(n^2)
- 8-directional movement (includes diagonals)
- Return -1 if start or end cell is blocked (value 1)
"""
from typing import List
from collections import deque

class Solution:
    # BFS in 8 directions - first time we reach (n-1,n-1) is shortest path
    def shortestPathBinaryMatrix(self, grid: List[List[int]]) -> int:
        n = len(grid)
        if grid[0][0] == 1 or grid[n-1][n-1] == 1:
            return -1  # start or end is blocked

        if n == 1:
            return 1  # single cell grid

        queue = deque([(0, 0, 1)])  # (row, col, path_length)
        grid[0][0] = 1  # mark visited by setting to 1

        while queue:
            r, c, dist = queue.popleft()
            for dr in [-1, 0, 1]:
                for dc in [-1, 0, 1]:
                    if dr == 0 and dc == 0:
                        continue
                    nr, nc = r + dr, c + dc
                    if 0 <= nr < n and 0 <= nc < n and grid[nr][nc] == 0:
                        if nr == n - 1 and nc == n - 1:
                            return dist + 1  # reached destination
                        grid[nr][nc] = 1  # mark visited
                        queue.append((nr, nc, dist + 1))

        return -1  # no path found


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

    # Example from README (solution mutates grid, so pass a fresh copy each time)
    assert sol.shortestPathBinaryMatrix([[0, 1], [1, 0]]) == 2

    # Single clear cell → path length 1
    assert sol.shortestPathBinaryMatrix([[0]]) == 1

    # LC classic example: 3x3 shortest clear path
    assert sol.shortestPathBinaryMatrix([[0, 0, 0], [1, 1, 0], [1, 1, 0]]) == 4

    # Blocked start → no path
    assert sol.shortestPathBinaryMatrix([[1, 0], [0, 0]]) == -1

    print("All tests passed.")
Swim In Rising Water ▼ expand
from typing import List
from collections import deque
import heapq


class Solution:
    """
    ## 778. Swim in Rising Water

    You are given an `n x n` integer matrix `grid` where each value `grid[i][j]` represents the
    elevation at that point `(i, j)`.

    The rain starts to fall. At time `t`, the depth of the water everywhere is `t`. You can swim
    from a square to another 4-directionally adjacent square if and only if the elevation of both
    squares individually are at most `t`. You can swim infinite distances in zero time. Of course,
    you must stay within the boundaries of the grid during your swim.

    Return the least time until you can reach the bottom-right square `(n-1, n-1)` if you start
    at the top-left square `(0, 0)`.

    ---

    ### Examples

    ```text
    Input:  grid = [[0,2],[1,3]]
    Output: 3
    Explanation: At time 3, we can swim from (0,0) -> (0,1) -> (1,1).

    Input:  grid = [[0,1,2,3,4],[24,23,22,21,5],[12,13,14,15,16],[11,17,18,19,20],[10,9,8,7,6]]
    Output: 16
    ```

    ---

    ### Constraints

    - `n == grid.length == grid[i].length`
    - `1 <= n <= 50`
    - `0 <= grid[i][j] < n^2`
    - Each value `grid[i][j]` is unique.
    """

    def swimInWater(self, grid: List[List[int]]) -> int:
        # Dijkstra variant: minimize the maximum elevation on the path to (n-1, n-1)
        # Priority queue stores (max_elevation_so_far, row, col)
        # O(n^2 log n) — binary search on time t, BFS to check reachability
        n = len(grid)
        dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)]

        def can_reach(t: int) -> bool:
            if grid[0][0] > t:
                return False
            visited = [[False] * n for _ in range(n)]
            visited[0][0] = True
            q = deque([(0, 0)])
            while q:
                r, c = q.popleft()
                if r == n - 1 and c == n - 1:
                    return True
                for dr, dc in dirs:
                    nr, nc = r + dr, c + dc
                    if 0 <= nr < n and 0 <= nc < n and not visited[nr][nc] and grid[nr][nc] <= t:
                        visited[nr][nc] = True
                        q.append((nr, nc))
            return False

        lo, hi = grid[0][0], n * n - 1
        while lo < hi:
            mid = (lo + hi) // 2
            if can_reach(mid):
                hi = mid
            else:
                lo = mid + 1
        return lo

    def swimInWater_dijkstra(self, grid: List[List[int]]) -> int:
        # O(n^2 log n) — min-heap treating max elevation on path as cost
        n = len(grid)
        dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)]
        visited = [[False] * n for _ in range(n)]
        heap = [(grid[0][0], 0, 0)]

        while heap:
            t, r, c = heapq.heappop(heap)
            if visited[r][c]:
                continue
            visited[r][c] = True
            if r == n - 1 and c == n - 1:
                return t
            for dr, dc in dirs:
                nr, nc = r + dr, c + dc
                if 0 <= nr < n and 0 <= nc < n and not visited[nr][nc]:
                    heapq.heappush(heap, (max(t, grid[nr][nc]), nr, nc))
        return -1


if __name__ == '__main__':
    s = Solution()
    assert s.swimInWater([[0, 2], [1, 3]]) == 3
    assert s.swimInWater([[0, 1, 2, 3, 4], [24, 23, 22, 21, 5],
                          [12, 13, 14, 15, 16], [11, 17, 18, 19, 20], [10, 9, 8, 7, 6]]) == 16

    assert s.swimInWater_dijkstra([[0, 2], [1, 3]]) == 3
    assert s.swimInWater_dijkstra([[0, 1, 2, 3, 4], [24, 23, 22, 21, 5],
                                   [12, 13, 14, 15, 16], [11, 17, 18, 19, 20], [10, 9, 8, 7, 6]]) == 16

    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.