LeetCode: Graphs II Kahn Topological Sort BFS

Table Of Contents
261. Graph Valid Tree ::3:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: [DFS] Cycle Detection Tracking Set For Undirected Graph - Graph/something
- Solution 2: [Topological] BFS Cycle Detection + Connectivity Check - Graph/something
- Solution 3: [Union Find] Single Component Edge Comparison Cycle Detection Tracking For Undirected Graph - Graph/something
329. Longest Increasing Path in a Matrix ::3:: - Hard
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: DFS + Memoization - 2D Dynamic Programming/2D Dynamic Programming
- Solution 2: 0 to i Iterative Bottom Up Array - 2D Dynamic Programming/2D Dynamic Programming
- Solution 3: [Topological] 0 to i Iterative Bottom Up Array - 2D Dynamic Programming/2D Dynamic Programming
Kahn's BFS Topological Sort Algorithm Intro
Intro
Topological Sort is a linear ordering of vertices in a Directed Acyclic Graph (DAG) such that for every directed edge (u -> v), where vertex u comes before vertex v in the ordering.
Commonly used to model dependencies or task scheduling
Not defined for graphs with cycles
Graph Requirements
- Directed Acyclic Graph (DAG)
- Represented Using:
- Adjacency List
- Adjacency Matrix
Output
A list of vertices in topologically sorted order Can have multiple valid orders if multiple vertices have no dependencies
Video Animation
Kahn: https://www.youtube.com/watch?v=7J3GadLzydI
Pseudo Code
def topo_sort_dfs(graph):
visited = set()
stack = []
def dfs(node):
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs(neighbor)
stack.append(node)
for node in graph:
if node not in visited:
dfs(node)
return stack[::-1] # reverse to get correct topological orderTime Complexity
Space Complexity
IRL Use Case
DFS Topological Sort Algorithm Intro
Intro
Graph Requirements
Output
Video Animation
DFS Topological Sort: https://www.youtube.com/watch?v=_1TDxihjtoE
Pseudo Code
Time Complexity
Space Complexity
IRL Use Case
207. Course Schedule ::2:: - Medium
Topics: Depth First Search, Breadth First Search, Graph, Topological Sort, Directed Graph
Intro
There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai. For example, the pair [0, 1], indicates that to take course 0 you have to first take course 1. Return true if you can finish all courses. Otherwise, return false.
| Example Input | Output |
|---|---|
| numCourses = 2, prerequisites = [[1,0]] | true |
| numCourses = 2, prerequisites = [[1,0],[0,1]] | false |
Constraints:
1 ≤ numCourses ≤ 2000
0 ≤ prerequisites.length ≤ 5000
prerequisites[i].length == 2
0 ≤ ai, bi < numCourses
All the pairs prerequisites[i] are unique.
Abstraction
Given a number of courses and prerequisites, determine if you can finish all courses.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [DFS] 3 State Recursive Stack Cycle Tracking For Directed Graphs - Graph/something
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
# DFS Cycle Detection in Directed Graph
# We need the 3 state tracking for directed graphs
# Graph Representation:
# - unique class = node in a directed graph
# - directed edge (c1 -> c2):
# means c1 class must be taken BEFORE c2 in course order
# Problem Abstraction:
# - Find a valid ordering of classes
# - if a cycle exists between classes, no valid ordering exists
# 3 State Tracking:
# During DFS, nodes can be in one of three states:
# 0 = UNVISITED => node has not been explored yet
# 1 = VISITING => node is currently in the recursion path
# active on the recursive call stack
# 2 = PROCESSED => node and all descendants have been fully processed
# and confirmed to contain no cycles
# Cycle Detection Insight:
# If we reach a node already marked VISITING (state = 1),
# it means we returned to a node already in our current DFS path,
# which forms a cycle.
# Safety Insight:
# Once a node becomes VISITED (state = 2),
# we know all paths below it are cycle-free,
# so we can safely skip reprocessing it.
# Initialize Graph + In Degree:
# tc: O(total_classes)
# sc: O(C), C = number of unique classes
# graph[c] = set of classes that come after c
graph = defaultdict(list)
# State array
# 0 = unvisited, 1 = visiting, 2 = visited
visited = [0] * numCourses
# Build Graph From Adjacent Classes
# C = number of classes
# L = average number of pre requisites
# tc: O(C * L)
for course, prereq in prerequisites:
graph[prereq].append(course)
def dfs(course):
# Early Prune:
# We are visiting a node currently in MY OWN recursion path,
# so cycle exists, course is not possible
if visited[course] == 1:
return False
# Early Return:
# Already marked as safe, we can skip
if visited[course] == 2:
return True
# Mark node as currently exploring
visited[course] = 1
# Explore:
# Recursively explore all neighbors we are pointing to
for nei in graph[course]:
# Neighbor was not processed correctly, thus current node is not safe
if not dfs(nei):
return False
# All nodes after current node have been processed safely,
# thus current node is also safe
visited[course] = 2
# return safely processed
return True
# Directed Graph May Be Disconnected:
# We start DFS from every node to ensure we check all Components:
# tc: O(V)
for c in range(numCourses):
# If class was not safely processed
if not dfs(c):
return False
# All courses processed, no cycles
# overall: tc O(V + E)
# overall: sc O(V + E)
return TrueSolution 2: [Topological] Iterative BFS Topological Sort [TC Opt] - Graph/something
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
# Topological Sort (BFS / Kahn's Algorithm)
# Graph Representation:
# - unique class = node in a directed graph
# - directed edge (c1 -> c2):
# means c1 class must be taken BEFORE c2 in course order
# Problem Abstraction:
# - Find a valid ordering of classes
# - if a cycle exists between classes, no valid ordering exists
# Initialize Graph + In Degree:
# tc: O(total_classes)
# sc: O(C), C = number of unique classes
# graph[c] = set of classes that come after c
graph = defaultdict(set)
# in_degree[c] = number of pre requisite classes for c
indegree = [0] * numCourses
# Build Graph From Adjacent Classes
# C = number of classes
# L = average number of pre requisites
# tc: O(C * L)
for course, prereq in prerequisites:
# Add pre requisite to classes In Degree
graph[prereq].append(course)
# Increase In Degree for class
indegree[course] += 1
# BFS Topological Sort (Kahn's Algorithm):
# Add all nodes with an In degree == 0 to the queue
# Process:
# 1. Pop node from queue
# 2. Add to ordering
# 3. Remove outgoing edges
# 4. Push new zero in-degree nodes
# sc: O(V)
queue = deque()
# tc: O(V)
for i in range(numCourses):
# if node has 0 out degree
if indegree[i] == 0:
queue.append(numCourses[i])
# BFS Topological Sort
# Tracking classes we have processed, to check for cycles at the end
count = 0
# C = nodes (characters)
# E = edges (ordering constraints)
# tc: O(C + E)
while queue:
# Remove leftmost node with an In Degree of 0
curr = queue.popleft()
# Mark node as processed
count += 1
# For all directed neighbors of current node,
# decrease their In Degree by 1
for nei in graph[curr]:
# decreasing neighbors In Degree
indegree[nei] -= 1
# If neighbor now has an In Degree of 0,
# append to stack for processing
if indegree[nei] == 0:
queue.append(nei)
# If not all nodes were not processed, a cycle exists
if count != numCourses:
return False
# overall: tc O(1)
# overall: sc O(1)
return True210. Course Schedule II ::3:: - Medium
Topics: Depth First Search, Breadth First Search, Graph, Topological Sort, Directed Graph
Intro
There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai. For example, the pair [0, 1], indicates that to take course 0 you have to first take course 1. Return the ordering of courses you should take to finish all courses. If there are many valid answers, return any of them. If it is impossible to finish all courses, return an empty array.
| Example Input | Output |
|---|---|
| numCourses = 2, prerequisites = [[1,0]] | [0,1] |
| numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]] | [0,2,1,3] |
| numCourses = 1, prerequisites = [] | [0] |
Constraints:
1 ≤ numCourses ≤ 2000
0 ≤ prerequisites.length ≤ numCourses * (numCourses-1)
prerequisites[i].length == 2
0 ≤ ai, bi < numCourses
ai != bi
All the pairs prerequisites[i] are distinct.
Abstraction
Given a number of courses and prerequisites, determine if you can finish all courses, and return the order to finish all courses.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [DFS] Build Course List Bottom Up Then Reverse With 3 State Recursive Stack Cycle Tracking For Directed Graphs - Graph/something
def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
# DFS Topological Sort in Directed Graph
# We need the 3 state tracking for directed graphs
# Graph Representation:
# - unique class = node in a directed graph
# - directed edge (c1 -> c2):
# means c1 class must be taken BEFORE c2 in course order
#
# Problem Abstraction:
# - Find a valid ordering of classes
# - if a cycle exists between classes, no valid ordering exists
# 3 State Tracking:
# During DFS, nodes can be in one of three states:
# 0 = UNVISITED => node has not been explored yet
# 1 = VISITING => node is currently in the recursion path
# active on the recursive call stack
# 2 = PROCESSED => node and all descendants have been fully processed
# and confirmed to contain no cycles
# Cycle Detection Insight:
# If we reach a node already marked VISITING (state = 1),
# it means we returned to a node already in our current DFS path,
# which forms a cycle.
# Topological Sort Insight:
# A node is added to the result AFTER all neighbors
# are processed (postorder traversal).
# Reversing postorder gives a valid course ordering.
# Initialize Graph:
# tc: O(total_classes)
# sc: O(C), C = number of unique classes
# graph[c] = classes that come after c
graph = defaultdict(list)
# Build Graph From Adjacent Classes
# C = number of classes
# L = average number of pre requisites
# tc: O(C * L)
for course, prereq in prerequisites:
graph[prereq].append(course)
# State array
# 0 = unvisited, 1 = visiting, 2 = visited
visited = [0] * numCourses
# Stores reverse topological order
res = []
# Tracks if graph is valid (no cycles)
is_possible = True
def dfs(course):
nonlocal is_possible
# Early Prune:
# We are visiting a node currently in MY OWN recursion path,
# so cycle exists and ordering is not possible
if visited[course] == 1:
is_possible = False
return
# Early Return:
# Already safely processed
if visited[course] == 2:
return
# Mark node as currently exploring
visited[course] = 1
# Explore:
# Recursively explore all neighbors
for nei in graph[course]:
dfs(nei)
# Stop early if cycle found
if not is_possible:
return
# All neighbors processed safely,
# current node is also safe
visited[course] = 2
# Postorder append
# (reverse topo order)
res.append(course)
# Directed Graph May Be Disconnected:
# We start DFS from every node to ensure we check all Components:
# tc: O(V)
for c in range(numCourses):
# Start DFS from unvisited nodes
if visited[c] == 0:
dfs(c)
# Cycle detected
if not is_possible:
return []
# Reverse postorder to get correct topological order
res.reverse()
# overall: tc O(V + E)
# overall: sc O(V + E)
return resSolution 2: [Topological] Iterative BFS Topological Sort [TC Opt] - Graph/something
def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
# Topological Sort (BFS / Kahn's Algorithm)
# Graph Representation:
# - unique class = node in a directed graph
# - directed edge (c1 -> c2):
# means c1 class must be taken BEFORE c2 in course order
# Problem Abstraction:
# - Find a valid ordering of classes
# - if a cycle exists between classes, no valid ordering exists
# Initialize Graph + In Degree:
# tc: O(total_classes)
# sc: O(C), C = number of unique classes
# graph[c] = set of classes that come after c
graph = defaultdict(set)
# in_degree[c] = number of pre requisite classes for c
indegree = [0] * numCourses
# Build Graph From Adjacent Classes
# C = number of classes
# L = average number of pre requisites
# tc: O(C * L)
for course, prereq in prerequisites:
# Add pre requisite to classes In Degree
graph[prereq].append(course)
# Increase In Degree for class
indegree[course] += 1
# BFS Topological Sort (Kahn's Algorithm):
# Add all nodes with an In degree == 0 to the queue
# Process:
# 1. Pop node from queue
# 2. Add to ordering
# 3. Remove outgoing edges
# 4. Push new zero in-degree nodes
# sc: O(V)
queue = []
# tc: O(V)
for i in range(numCourses):
# if node has 0 out degree
if indegree[i] == 0:
queue.append(numCourses[i])
# BFS Topological Sort
# Tracking classes we have processed, to check for cycles at the end
res = []
# C = nodes (characters)
# E = edges (ordering constraints)
# tc: O(C + E)
while queue:
# Remove leftmost node with an In Degree of 0
curr = queue.popleft()
# Mark node as processed
res.append(curr)
# For all directed neighbors of current node,
# decrease their In Degree by 1
for nei in graph[curr]:
# decreasing neighbors In Degree
indegree[nei] -= 1
# If neighbor now has an In Degree of 0, append to stack for processing
if indegree[nei] == 0:
queue.append(nei)
# If not all nodes were not processed, a cycle exists
if len(res) != numCourses:
return []
# overall: tc O(1)
# overall: sc O(1)
return res261. Graph Valid Tree ::3:: - Medium
Topics: Depth First Search, Breadth First Search, Graph, Union Find, Undirected Graph
Intro
Given n nodes labeled from 0 to n - 1 and a list of undirected edges (each edge is a pair of nodes), write a function to check whether these edges make up a valid tree. You can assume that no duplicate edges will appear in edges. Since all edges are undirected, [0, 1] is the same as [1, 0] and thus will not appear together in edges.
| Example Input | Output |
|---|---|
| n = 5 edges = [[0, 1], [0, 2], [0, 3], [1, 4]] | true |
| n = 5 edges = [[0, 1], [1, 2], [2, 3], [1, 3], [1, 4]] | false |
Constraints:
1 ≤ n ≤ 100
0 ≤ edges.length ≤ n * n(-1) / 2
Abstraction
Given a list of undirected edges, check if valid tree is made.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [DFS] Cycle Detection Tracking Set For Undirected Graph - Graph/something
def validTree(self, n: int, edges: List[List[int]]) -> bool:
# DFS Cycle Detection with Connectivity Check (Undirected Graph)
# We don't need the 3 state tracking for undirected graphs
# Tree Validation:
# 1. Must have exactly n-1 edges
# 2. Must be connected (all nodes reachable from any node)
# 3. Must not contain cycles
# Edge Case:
# If edges != n-1, cannot be a tree
if len(edges) != n - 1:
return False
# Initialize Graph:
# undirected adjacency list
# tc: O(V + E)
graph = defaultdict(list)\
# sc: O(V + E)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
# Track visited nodes
# tc: O(V)
visited = set()
#
def dfs(node, parent):
# Cycle Detection:
# If we visit a node already seen in this traversal, cycle exists
if node in visited:
return False
# Mark node as visited
visited.add(node)
# Explore neighbors
for nei in graph[node]:
# Skip back edge to parent
if nei == parent:
continue
# If neighbor is not safe, current node is not safe
if not dfs(nei, node):
return False
# All neighbors processed safely
return True
# Start DFS from node 0
if not dfs(0, -1):
return False
# If not all nodes were not processed, a cycle exists
if len(visited) != n:
return False
# overall: tc O(V + E)
# overall: sc O(V + E)
return True
Solution 2: [Topological] BFS Cycle Detection + Connectivity Check - Graph/something
def validTree(self, n: int, edges: List[List[int]]) -> bool:
# Topological Sort (BFS / Kahn's Algorithm)
# Graph Representation:
# - node n = node in a undirected graph
# - undirected edge (n1 <-> n2)
# Problem Abstraction:
# - validate (n-1) edges
# - validate all nodes are connected in tree
# - validate no cycles
# Edge Case:
# A valid tree for n nodes, it must have(n - 1) edges
if len(edges) != n - 1:
return False
# Initialize Graph + In Degree:
# tc: O(total_classes)
# sc: O(C), C = number of unique classes
# graph[c] = node connected to c
graph = defaultdict(set)
# Mark nodes as connected
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
# Tracking classes we have processed, to check for cycles at the end
visited = set()
# Iterative Queue
# sc: O(E)
queue = deque()
# Build Graph from any node (undirected so order does not matter)
queue.append((0, -1))
# While we still have nodes
# tc: O(V)
while queue:
# Remove leftmost node
node, parent = queue.popleft()
# Early Exit:
# node has already been processed,
# cycle has been detected
if node in visited:
return False
# Mark node as processed
visited.add(node)
# For all directed neighbors of current node,
# append them for processing
for nei in graph[node]:
# ignore if node is pointing to self
if nei != parent:
# append to stack for processing
queue.append((nei, node))
# If not all nodes were not processed, a cycle exists
if len(visited) != n:
return False
# overall: tc O(1)
# overall: sc O(1)
return TrueSolution 3: [Union Find] Single Component Edge Comparison Cycle Detection Tracking For Undirected Graph - Graph/something
def validTree(self, n: int, edges: List[List[int]]) -> bool:
# Union-Find (Disjoint Set) for Cycle Detection in Undirected Graph
# Union Find can be used for cycle detection in Undirected Graphs,
# since direction does not matter
# Graph Representation:
# - Each node = element in a disjoint set
# - Each edge connects two nodes
# Problem Abstraction:
# - A valid tree must satisfy two conditions:
# 1. No cycles exist
# 2. Graph is connected
# - Union-Find detects cycles efficiently and ensures connectivity
# Early Prune -> a valid tree must have exactly n-1 edges
if len(edges) != n - 1:
return False
# Initialize parent array and rank for Union-Find
# tc: O(V)
# sc: O(V)
# parent[i] = representative of the set containing node i
parent = [i for i in range(n)]
# rank[i] = size or depth heuristic for union by rank
rank = [1] * n
# Find + Path Compression:
# Returns root of the set containing x
# Compresses path along the way for efficiency
# tc: O(α(V))
def find(x):
# if parent of node is not self
while x != parent[x]:
# path compression, flatten the tree
parent[x] = parent[parent[x]]
# set parent, to parent of parent
x = parent[x]
# return originals parent, after path compression
return x
# Union by Rank:
# Merges sets containing x and y
# Returns False if x and y are already in the same set (cycle detected)
# tc: O(α(N))
def union(x, y):
rootX, rootY = find(x), find(y)
# Early Prune -> cycle detected
if rootX == rootY:
return False
# Merge smaller tree into larger tree to keep tree shallow
if rank[rootX] > rank[rootY]:
parent[rootY] = rootX
elif rank[rootX] < rank[rootY]:
parent[rootX] = rootY
else:
parent[rootY] = rootX
rank[rootX] += 1
return True
# Process all edges
# tc: O(E)
for u, v in edges:
# If union failed, that means we tried to join two nodes
# that already belong to the same component,
# which implies we ran into a cycle
if not union(u, v):
return False
# All edges processed without cycle -> valid tree
# Connectivity is guaranteed by n-1 edges rule
# overall: tc O(E * α(N))
# overall: sc O(N)
return True269. Alien Dictionary ::1:: - Hard
Topics: Breadth First Search, Graph, Topological Sort, Directed Graph
Intro
There is a foreign language which uses the latin alphabet, but the order among letters is not "a", "b", "c" ... "z" as in English. You receive a list of non-empty strings words from the dictionary, where the words are sorted lexicographically based on the rules of this new language. Derive the order of letters in this language. If the order is invalid, return an empty string. If there are multiple valid order of letters, return any of them. A string a is lexicographically smaller than a string b if either of the following is true: The first letter where they differ is smaller in a than in b. a is a prefix of b and a.length < b.length.
| Example Input | Output |
|---|---|
| ["z","o"] | "zo" |
| ["hrn","hrf","er","enn","rfnn"] | "hernf" |
Constraints:
The input words will contain characters only from lowercase 'a' to 'z'.
1 ≤ nums.length ≤ 100
1 ≤ words[i].length ≤ 100
Abstraction
Given a foreign language, derive the order of letters.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Iterative BFS Topological Sort - Advanced Graphs/Advanced Graphs
def foreignDictionary(self, words: List[str]) -> str:
# Topological Sort (BFS / Kahn's Algorithm)
# Graph Representation:
# - unique character = node in a directed graph
# - directed edge (c1 -> c2):
# means c1 must appear BEFORE c2 in the alien dictionary order
# Problem Abstraction:
# - Find a valid ordering of characters
# - if a cycle exists between character, no valid ordering exists
# Initialize Graph + In Degree:
# tc: O(total_chars)
# sc: O(C), C = number of unique characters
# graph[c] = set of characters that come after c
graph = defaultdict(set)
# in_degree[c] = number of incoming edges for c
in_degree = {c: 0 for word in words for c in word}
# Build Graph From Adjacent Words
# W = number of words
# L = average word length
# tc: O(W * L)
for i in range(len(words) - 1):
#
word1, word2 = words[i], words[i+1]
min_len = min(len(word1), len(word2))
found_diff = False
for j in range(min_len):
c1, c2 = word1[j], word2[j]
if c1 != c2:
# Add edge once (avoid duplicate in-degree increments)
if c2 not in graph[c1]:
graph[c1].add(c2)
in_degree[c2] += 1
found_diff = True
break
# Edge case: prefix situation invalid, e.g., "abc" before "ab"
if not found_diff and len(word1) > len(word2):
return ""
# BFS Topological Sort (Kahn's Algorithm):
# Add all nodes with an In degree == 0 to the queue
# Process:
# 1. Pop node from queue
# 2. Add to ordering
# 3. Remove outgoing edges
# 4. Push new zero in-degree nodes
# sc: O(V)
queue = deque()
# tc: O(V)
for i in range(n):
# if node has 0 out degree
if indegree[i] == 0:
queue.append(i)
# BFS Topological Sort
# Tracking nodes we have processed, to check for cycles at the end
processed = []
# C = nodes (characters)
# E = edges (ordering constraints)
# tc: O(C + E)
while queue:
# Remove leftmost node with an In Degree of 0
c = queue.popleft()
# Mark node as processed
processed.append(c)
# For all directed neighbors of current node,
# decrease their In Degree by 1
for nei in graph[c]:
# decreasing neighbors In Degree
in_degree[nei] -= 1
# If neighbor now has an In Degree of 0, append to stack for processing
if in_degree[nei] == 0:
queue.append(nei)
# If not all nodes were not processed, a cycle exists
if len(processed) != len(in_degree):
return ""
# Return Valid Topological Ordering (one of many orderings)
res = "".join(processed)
# C = number of unique characters, W = number of words, L = average word length
# overall: tc O(C + W*L)
# overall: sc O(C + W*L)
return res310. Minimum Height Trees ::1:: - Hard
Topics: Depth First Search, Breadth First Search, Graph Theory, Topological Sort, Undirected Graph
Intro
A tree is an undirected graph in which any two vertices are connected by exactly one path. In other words, any connected graph without simple cycles is a tree. Given a tree of n nodes labelled from 0 to n - 1, and an array of n - 1 edges where edges[i] = [ai, bi] indicates that there is an undirected edge between the two nodes ai and bi in the tree, you can choose any node of the tree as the root. When you select a node x as the root, the result tree has height h. Among all possible rooted trees, those with minimum height (i.e. min(h)) are called minimum height trees (MHTs). Return a list of all MHTs' root labels. You can return the answer in any order. The height of a rooted tree is the number of edges on the longest downward path between the root and a leaf.
| Example Input | Output |
|---|---|
| n = 4, edges = [[1,0],[1,2],[1,3]] | [1] |
| n = 6, edges = [[3,0],[3,1],[3,2],[3,4],[5,4]] | [3,4] |
Constraints:
1 ≤ n ≤ 2 * 10^4
edges.length == n - 1
0 ≤ ai, bi < n
ai != bi
All the pairs (ai, bi) are distinct.
The given input is guaranteed to be a tree and there will be no repeated edges.
Abstraction
Given a foreign language, derive the order of letters.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Leaf Trimming - Advanced Graphs/Advanced Graphs
def findMinHeightTrees(self, n: int, edges: List[List[int]]) -> List[int]:
# Topological Leaf Trimming:
# In a tree, the roots that produce minimum height
# are the CENTROIDS of the tree
# Idea:
# Instead of computing the height from every node O(V^2)
# we iteratively remove leaf nodes (nodes with only one neighbor)
# layer by layer.
# Each layer we remove brings us closer to the center of the tree,
# the last 1 or 2 remaining nodes are the roots that produce Minimum Height Trees
# Tree Centroids:
# A centroid of a tree is a node which, if chosen as the root,
# minimizes the height of the tree
# - There can be 1 or 2 centroids in a tree
# - For a tree with N nodes, removing leaves layer by layer
# eventually exposes the centroid(s)
# - The centroid(s) are exactly the roots of the Minimum Height Trees
# Edge case: single node tree
if n == 1:
return [0]
# Build Undirected Graph:
# sc: O(n)
# tc: O(n)
# graph[u] = neighbors of u
graph = defaultdict(set)
for u, v in edges:
graph[u].add(v)
graph[v].add(u)
# Initialize leaves:
# Nodes with exactly one neighbor, the outermost layer of the tre
queue = deque([i for i in range(n) if len(graph[i]) == 1])
remaining_nodes = n
# Trim Leaves Layer By Layer (Topological Sort BFS):
# Remove current leaves simultaneously, which reveal new layer of leaves
# tc: O(n)
while remaining_nodes > 2:
# Current number of leaves
leaves_count = len(queue)
# Track remaining nodes
remaining_nodes -= leaves_count
# To build next layer of leaves
new_leaves = deque()
# Remove each leaf in current layer
for leaf in queue:
# Grab leaf neighbor (leaf only has one neighbor)
neighbor = graph[leaf].pop()
# Remove leaf from neighbor, and from tree
graph[neighbor].remove(leaf)
# If neighbor has Degree == 1, neighbor has become new leaf
if len(graph[neighbor]) == 1:
# append neighbor to queue for processing
new_leaves.append(neighbor)
# Process next layer of leaves
queue = new_leaves
# <= 2 nodes remain:
# The final 1 or 2 nodes are tree centers
# and therefore roots of the minimum height trees
# tc: O(1)
res = list(queue)
# overall: tc O(V)
# overall: sc O(V)
return res329. Longest Increasing Path in a Matrix ::3:: - Hard
Topics: Array, Dynamic Programming, Depth First Search, Breadth First Search, Graph, Topological Sort, Memoization, Matrix
Intro
Given an m x n integers matrix, return the length of the longest increasing path in matrix. From each cell, you can either move in four directions: left, right, up, or down. You may not move diagonally or move outside the boundary (i.e., wrap-around is not allowed).
| Example Input | Output |
|---|---|
| matrix = [[9,9,4],[6,6,8],[2,1,1]] | 4 |
| matrix = [[3,4,5],[3,2,6],[2,2,1]] | 4 |
| matrix = [[1]] | 1 |
Constraints:
m == matrix.length
n == matrix[i].length
1 ≤ m, n ≤ 200
0 ≤ matrix[i][j] ≤ 231 - 1
Abstraction
Given a matrix, find the length of the longest increasing path.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: DFS + Memoization - 2D Dynamic Programming/2D Dynamic Programming
def longestIncreasingPath(self, matrix: List[List[int]]) -> int:
# Notes:
# - Use DFS with memoization to avoid recomputation.
# - Each cell (i, j) stores the length of the longest increasing path starting there.
# - Explore 4 directions: up, down, left, right if the next cell is larger.
#
# Result: max value across all cells in memo
if not matrix or not matrix[0]:
return 0
m, n = len(matrix), len(matrix[0])
memo = [[0] * n for _ in range(m)] # memo[i][j] = longest path starting from (i, j)
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
def dfs(i, j):
# Return cached result
if memo[i][j] != 0:
return memo[i][j]
# At least the cell itself
best = 1
for dx, dy in directions:
x, y = i + dx, j + dy
if 0 <= x < m and 0 <= y < n and matrix[x][y] > matrix[i][j]:
best = max(best, 1 + dfs(x, y))
memo[i][j] = best
return best
res = 0
for i in range(m):
for j in range(n):
res = max(res, dfs(i, j))
return resSolution 2: 0 to i Iterative Bottom Up Array - 2D Dynamic Programming/2D Dynamic Programming
def longestIncreasingPath(self, matrix: List[List[int]]) -> int:
# Notes:
# - Treat each cell as a node in a DAG.
# - Edges go from smaller value -> larger value neighbor.
# - Topological sorting via Kahn's algorithm counts layers = path length.
#
# Result: number of BFS layers processed.
if not matrix or not matrix[0]:
return 0
m, n = len(matrix), len(matrix[0])
indegree = [[0] * n for _ in range(m)]
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
# Compute indegree for each cell
for i in range(m):
for j in range(n):
for dx, dy in directions:
x, y = i + dx, j + dy
if 0 <= x < m and 0 <= y < n and matrix[x][y] > matrix[i][j]:
indegree[x][y] += 1
# Collect nodes with indegree 0
q = deque()
for i in range(m):
for j in range(n):
if indegree[i][j] == 0:
q.append((i, j))
res = 0
# BFS layer by layer
while q:
res += 1
for _ in range(len(q)):
i, j = q.popleft()
for dx, dy in directions:
x, y = i + dx, j + dy
if 0 <= x < m and 0 <= y < n and matrix[x][y] > matrix[i][j]:
indegree[x][y] -= 1
if indegree[x][y] == 0:
q.append((x, y))
return resSolution 3: [Topological] 0 to i Iterative Bottom Up Array - 2D Dynamic Programming/2D Dynamic Programming
def longestIncreasingPath(self, matrix: List[List[int]]) -> int:
# Topological Sort (BFS / Kahn's Algorithm) for Longest Increasing Path
# Graph Representation:
# - Each cell (i, j) is a node in a Directed Acyclic Graph (DAG)
# - Directed edge exists from cell A -> cell B if matrix[A] < matrix[B], so increasing
# Problem Abstraction:
# - With our two representations, that ensures that the topological path
# will be the longest increasing path in the matrix
# Empty Check: no graph, return 0
if not matrix or not matrix[0]:
return 0
# Boundaries
m, n = len(matrix), len(matrix[0])
# Constants for direction calculation
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
# Initialize Graph + In Degree:
# indegree[i][j] = number of incoming edges to cell (i, j)
indegree = [[0] * n for _ in range(m)]
# Build indegree array
# tc: O(V*4) = O(V), V = m*n
for i in range(m):
for j in range(n):
for dx, dy in directions:
x, y = i + dx, j + dy
if (0 <= x < m and
0 <= y < n and
matrix[x][y] > matrix[i][j]):
indegree[x][y] += 1
# BFS Topological Sort:
# Start with cells that have indegree 0
q = deque()
for i in range(m):
for j in range(n):
if indegree[i][j] == 0:
q.append((i, j))
# Track number of layers = length of longest increasing path
longest_path = 0
# C = number of nodes (cells)
# E = number of edges (neighboring increasing cells)
# tc: O(C + E)
while q:
# Increase longest path
longest_path += 1
# For all elements in queue
for _ in range(len(q)):
# Pop farthest left node? (what does this mean in this context)
i, j = q.popleft()
# Explore:
# check all neighbors
for dx, dy in directions:
# Generate coordinates for neighbors in 4 directions
x, y = i + dx, j + dy
# Check if valid in boundary
if (0 <= x < m and
0 <= y < n and
matrix[x][y] > matrix[i][j]):
# decrease In Degree of neighbor
indegree[x][y] -= 1
# If neighbor now has an In Degree == 0, append to queue for processing
if indegree[x][y] == 0:
q.append((x, y))
# overall: tc O(m*n) - each cell processed at most once
# overall: sc O(m*n) - indegree array + queue
return longest_path444. Sequence Reconstruction ::3:: - Hard
Topics: Array, Dynamic Programming, Depth First Search, Breadth First Search, Graph, Topological Sort, Memoization, Matrix
Intro
You are given an integer array nums of length n where nums is a permutation of the integers in the range [1, n]. You are also given a 2D integer array sequences where sequences[i] is a subsequence of nums. Check if nums is the shortest possible and the only supersequence. The shortest supersequence is a sequence with the shortest length and has all sequences[i] as subsequences. There could be multiple valid supersequences for the given array sequences. For example, for sequences = [[1,2],[1,3]], there are two shortest supersequences, [1,2,3] and [1,3,2]. While for sequences = [[1,2],[1,3],[1,2,3]], the only shortest supersequence possible is [1,2,3]. [1,2,3,4] is a possible supersequence but not the shortest. Return true if nums is the only shortest supersequence for sequences, or false otherwise. A subsequence is a sequence that can be derived from another sequence by deleting some or no elements without changing the order of the remaining elements.
| Example Input | Output |
|---|---|
| nums = [1,2,3], sequences = [[1,2],[1,3]] | false |
| nums = [1,2,3], sequences = [[1,2]] | false |
| nums = [1,2,3], sequences = [[1,2],[1,3],[2,3]] | true |
Constraints:
n == nums.length
1 ≤ n ≤ 10^4
nums is a permutation of all the integers in the range [1, n]
1 ≤ sequences.length ≤ 10^4
1 ≤ sequences[i].length ≤ 10^4
1 ≤ sum(sequences[i].length) ≤ 10^5
1 ≤ sequences[i][j] ≤ N All the arrays of sequences are unique
Sequences[i] is a subsequence of nums
Abstraction
Given a matrix, find the length of the longest increasing path.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Unique Order Check - 2D Dynamic Programming/2D Dynamic Programming
def sequenceReconstruction(self, nums: List[int], sequences: List[List[int]]) -> bool:
# Topological Sort (BFS / Kahn's Algorithm)
# Unique Topological Ordering:
# - A graph has a unique topological ordering,
# if at each step, there is exactly one node with an In Degree of 0.
# So the queue must at most only have 1 node at a time
# If the topological order matches nums order exactly, return True.
# Otherwise, return False.
# Graph Representation:
# - Each unique number = node in a directed graph
# - A sequence [u, v] = directed edge u -> v
# means u must come before v in any valid order
# Problem Abstraction:
# - Determine if there is a unique topological order
# that matches the given nums array exactly
# - A unique topological order exists if at each step
# there is only one node with indegree 0
n = len(nums)
# Initialize Graph + In Degree:
# tc: O(sum(len(seq))) <= 1e5
# sc: O(n + sum(len(seq))) <= 1e5
# graph[node] = set of neighbors that come after node
graph = defaultdict(set)
# in_degree[c] = number of pre requisite classes for c, nodes are 1-indexed
in_degree = [0] * (n + 1)
# Build Graph from sequences
# tc: O(sum(len(seq)))
for seq in sequences:
# For num i, check [0, i]
for i in range(len(seq) - 1):
# Build edge i -> i+1
u, v = seq[i], seq[i + 1]
# If edge has not already been added
if v not in graph[u]:
# Add In Degree
graph[u].add(v)
# Increase In Degree
in_degree[v] += 1
# BFS Topological Sort (Kahn's Algorithm):
# Add all nodes with an In degree == 0 to the queue
# Initialize queue with nodes having indegree 0
# Unique ordering check: queue must have exactly one node at each step
queue = deque([i for i in range(1, n + 1) if in_degree[i] == 0])
# Tracking nodes we have processed, to check for cycles at the end
topo_order = []
# BFS Topological Sort
while queue:
# Unique Topological Ordering Early Exit
# For unique ordering, at any point the queue must only have one node
# If multiple nodes have 0 In Degreee, multiple sequences are possible
if len(queue) > 1:
# Multiple nodes have 0 In Degree, multiple sequences possible, unique is not possible
return False
# Remove leftmost node with an In Degree of 0
node = queue.popleft()
# Mark node as processed
topo_order.append(node)
# For all directed neighbors of current node,
# decrease their In Degree by 1
for nei in graph[node]:
# decreasing neighbors In Degree
in_degree[nei] -= 1
# If neighbor now has an In Degree of 0,
# append to stack for processing
if in_degree[nei] == 0:
queue.append(nei)
# If not all nodes were not processed, a cycle exists
# tc: O(1)
if len(topo_order) != len(nums):
return False
# Check if reconstructed order matches nums
# tc: O(V)
if topo_order != n:
return False
# Complexity Analysis
# Time Complexity:
# O(sum(len(seq))) to build graph
# + O(n + E) for BFS topo sort
# = O(sum(len(seq))) overall
#
# Space Complexity:
# O(n + E) for graph and in-degree array
# overall: tc O(sum(len(seq)))
# overall: sc O(V + sum(len(seq)))
return True802. Find Eventual Safe States ::1:: - Medium
Topics: Depth First Search, Breadth First Search, Graph Theory, Topological Sort
Intro
There is a directed graph of n nodes with each node labeled from 0 to n - 1. The graph is represented by a 0-indexed 2D integer array graph where graph[i] is an integer array of nodes adjacent to node i, meaning there is an edge from node i to each node in graph[i]. A node is a terminal node if there are no outgoing edges. A node is a safe node if every possible path starting from that node leads to a terminal node (or another safe node). Return an array containing all the safe nodes of the graph. The answer should be sorted in ascending order.
| Example Input | Output |
|---|---|
| graph = [[1,2],[2,3],[5],[0],[5],[],[]] | [2,4,5,6] |
| graph = [[1,2,3,4],[1,2],[3,4],[0,4],[]] | [4] |
Constraints:
n == graph.length
1 ≤ n ≤ 10^4
0 ≤ graph[i].length ≤ n
0 ≤ graph[i][j] ≤ n-1
graph[i] is sorted in a strictly increasing order
The graph may contain self loops.
The number of edges in the graph will be in the range [1, 4 * 104].
Abstraction
Something?
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Reverse Topological Elimination - Advanced Graphs/Advanced Graphs
def eventualSafeNodes(self, graph: List[List[int]]) -> List[int]:
# Topological Sort (BFS / Kahn's Algorithm):
# Graph Representation:
# - unique node = node in a directed graph
# - edge u -> v = u can go to v
# Problem Abstraction:
# - A node is SAFE if every path from it eventually reaches a terminal node.
# - Terminal nodes = nodes with no outgoing edges
# - Instead of detecting cycles directly, reverse the graph:
# - terminal nodes (outDegree = 0) are inherently safe
# - nodes pointing to safe nodes eventually become safe
# - Use topological BFS to remove edges layer by layer starting from terminal nodes
# Idea:
# Instead of detecting cycles directly, reverse the graph.
# Since we know that terminal nodes (outdegree = 0) are always safe,
# we know that nodes that point to that terminal node are also safe.
# This then becomes a topological style BFS where we remove
# edges starting from terminal nodes.
# So instead of queuing nodes with In Degree == 0,
# we instead queue nodes with Out Degree == 0
# Original graph:
# u -> v (u depends on v)
# Reversed graph:
# v -> u
# Build A Reverse Graph + Out Degree Count
# tc: O(V + E)
# sc: O(V + E)
n = len(graph)
# reverse_graph[v] contains all nodes pointing to v
reverse_graph = [[] for _ in range(n)]
# outDegree[u] = number of outgoing edges in original graph
outDegree = [0] * n
# Build Reverse Graph + Count Outgoing Edges
# outDegree[u] stores the number of outgoing edges for node u in the original graph.
# Nodes with outDegree == 0 are terminal nodes and are inherently safe.
for u in range(n):
#
for v in graph[u]:
# Add u as a predecessor of v in the reversed graph
reverse_graph[v].append(u)
# Count outgoing edges for u in original graph
outDegree[u] = len(graph[u])
# BFS Topological Sort (Kahn's Algorithm):
# Terminal nodes have out degree = 0, these are guaranteed safe.
# So we add all terminal nodes with Out Degree == 0, to the queue
# Process:
# 1. Pop node from queue
# 2. Add to ordering
# 3. Remove outgoing edges
# 4. Push new zero in-degree nodes
# sc: O(V)
queue = deque()
# tc: O(V)
for i in range(n):
# if node has 0 out degree
if outDegree[i] == 0:
queue.append(i)
# BFS Topological Sort
# Tracking nodes we have marked as safe
safe = [False] * n
# C = nodes (characters)
# E = edges (?)
# tc: O(C + E)
while queue:
# remove leftmost node with an Out Degree of 0
node = queue.popleft()
# Mark node as processed
safe[node] = True
# Reduce outgoing edge count for predecessors in reversed graph
for prev in reverse_graph[node]:
# decreasing neighbors Out Degree
outDegree[prev] -= 1
# If neighbor now has an Out Degree of 0, append to stack for processing
if outDegree[prev] == 0:
queue.append(prev)
# Collect all safe nodes:
# ensure ascending order
res = [i for i in range(n) if safe[i]]
# V = n
# E = total edges
# overall: tc O(V + E)
# overall: sc O(V + E)
return sorted(res)851. Loud and Rich ::1:: - Medium
Topics: Array, Depth First Search, Graph Theory, Topological Sort
Intro
There is a group of n people labeled from 0 to n - 1 where each person has a different amount of money and a different level of quietness. You are given an array richer where richer[i] = [ai, bi] indicates that ai has more money than bi and an integer array quiet where quiet[i] is the quietness of the ith person. All the given data in richer are logically correct (i.e., the data will not lead you to a situation where x is richer than y and y is richer than x at the same time). Return an integer array answer where answer[x] = y if y is the least quiet person (that is, the person y with the smallest value of quiet[y]) among all people who definitely have equal to or more money than the person x.
| Example Input | Output |
|---|---|
| richer = [[1,0],[2,1],[3,1],[3,7],[4,3],[5,3],[6,3]], quiet = [3,2,5,4,6,1,7,0] | [5,5,2,5,4,5,6,7] |
| richer = [], quiet = [0] | [0] |
Constraints:
n == quiet.length
1 ≤ n ≤ 500
0 ≤ quiet[i] < n
All the values of quiet are unique
0 ≤ richer.length ≤ n * (n - 1) / 2
0 ≤ ai, bi < n
ai != bi
All the pairs of richer are unique
The observations in richer are all logically consistent
Abstraction
Something else?
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Topological DP (Value Propagation) - Advanced Graphs/Advanced Graphs
def loudAndRich(self, richer: List[List[int]], quiet: List[int]) -> List[int]:
# Topological Sort (BFS Kahn's Algorithm)
# For each person v: find the quietest person among
# { all people richer than v } + { v }
# Idea:
# Richness defines a directed acyclic order
# If we process people from richer -> poorer (topological order)
# we can propagate the quietest known person downward
# This turns the problem into:
# - Topological BFS where each node passes its best answer to its poorer neighbors
n = len(quiet)
# Build graph:
# tc: O(E)
# sc: O(V + E)
# graph[u] = people poorer than u
graph = [[] for _ in range(n)]
# indegree[v] = number of richer people pointing to v
indegree = [0] * n
# Edge u -> v means u is richer than v
for u, v in richer:
graph[u].append(v) # u richer than v
indegree[v] += 1
# result array
# sc: O(V)
answer = [i for i in range(n)]
# Initialize answer array:
# Start with people that have no richer individuals (In Degree == 0)
# tc: O(V)
queue = deque([i for i in range(n) if indegree[i] == 0])
# BFS Topological Sort
while queue:
# Pop node with 0 In Degree
u = queue.popleft()
# Check people poorer
for v in graph[u]:
# Propagate better (quieter) answer
if quiet[answer[u]] < quiet[answer[v]]:
answer[v] = answer[u]
# Standard topological reduction
indegree[v] -= 1
if indegree[v] == 0:
queue.append(v)
# V = number of people
# E = number of richer relationships
# overall: tc O(V + E)
# overall: sc O(V + E)
return answer913. Cat and Mouse ::1:: - Hard
Topics: Math, Dynamic Programming, Graph Theory, Topological Sort, Memoization, Game Theory
Intro
A game on an undirected graph is played by two players, Mouse and Cat, who alternate turns. The graph is given as follows: graph[a] is a list of all nodes b such that ab is an edge of the graph. The mouse starts at node 1 and goes first, the cat starts at node 2 and goes second, and there is a hole at node 0. During each player's turn, they must travel along one edge of the graph that meets where they are. For example, if the Mouse is at node 1, it must travel to any node in graph[1]. Additionally, it is not allowed for the Cat to travel to the Hole (node 0). Then, the game can end in three ways: If ever the Cat occupies the same node as the Mouse, the Cat wins. If ever the Mouse reaches the Hole, the Mouse wins. If ever a position is repeated (i.e., the players are in the same position as a previous turn, and it is the same player's turn to move), the game is a draw. Given a graph, and assuming both players play optimally, return 1 if the mouse wins the game, 2 if the cat wins the game, or 0 if the game is a draw.
| Example Input | Output |
|---|---|
| graph = [[2,5],[3],[0,4,5],[1,4,5],[2,3],[0,2,3]] | 0 |
| graph = [[1,3],[0],[3],[0,2]] | 1 |
Constraints:
3 ≤ graph.length ≤ 50
1 ≤ graph[i].length < graph.length
0 ≤ graph[i][j] < graph.length
graph[i][j] != i
graph[i] is unique
The mouse and the cat can always move
Abstraction
Given a foreign language, derive the order of letters.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Topological MiniMax Propagation - Advanced Graphs/Advanced Graphs
def catMouseGame(self, graph: List[List[int]]) -> int:
# Reverse Topological BFS on Game States
# Instead of simulating the game forward, we solve states backwards from known outcomes
# A game state is defined as:
# (mouse_position, cat_position, turn)
# turn = 0 => mouse moves next
# turn = 1 => cat moves next
# Each state has one of three results:
# DRAW = 0
# MOUSE = 1 (mouse wins)
# CAT = 2 (cat wins)
# Idea:
# Treat all states as nodes in a state graph
# Terminal states are known immediately:
# - mouse reaches hole (node 0) => mouse wins
# - cat catches mouse => cat wins
# From these known states, propagate results backward
# to parent states using reverse BFS (topological reasoning)
# Essentially:
# Topological elimination on a game state graph
n = len(graph)
# Constants For Game State
DRAW, MOUSE, CAT = 0, 1, 2
# State Arrays:
# sc: O(n^3)
# color[m][c][t] = result of state
color = [[[DRAW]*2 for _ in range(n)] for _ in range(n)]
# degree[m][c][t] = number of available moves
degree = [[[0]*2 for _ in range(n)] for _ in range(n)]
# Initialize Move Counts
for m in range(n):
for c in range(n):
# Mouse Moves
degree[m][c][0] = len(graph[m])
# Cat cannot enter hole
degree[m][c][1] = len(graph[c]) - (0 in graph[c])
# Initialize Terminal States:
# Known outcomes:
# 1. Mouse at hole -> MOUSE wins
# 2. Cat catches mouse -> CAT wins
# These become starting points for reverse BFS
# tc: O(n)
# Queue of known results (terminal states)
queue = deque()
# Terminal states
for i in range(n):
for t in range(2):
# Mouse at hole -> mouse wins
color[0][i][t] = MOUSE
queue.append((0, i, t, MOUSE))
# Cat catches mouse -> cat wins
if i != 0:
color[i][i][t] = CAT
queue.append((i, i, t, CAT))
# Reverse BFS on State Graph:
# Process known states nad update their parent states
# Parent logic:
# - If current result is winning for the player whose turn it was previously,
# parent becomes winning
#
# - Otherwise, reduce degree
#
# BFS / Topological sort on states
while queue:
# Current Resolve State
mouse, cat, turn, result = queue.popleft()
# Find Parent States (determine states that can reach the current state):
# mouse just moved:
if turn == 0:
# previous turn was cat
prev_turn = 1
# Determine where cat could have come from:
for prev_cat in graph[cat]:
# Cat cannot move to hole
if prev_cat == 0:
continue
# Skip already resolved states:
if color[mouse][prev_cat][prev_turn] != DRAW:
continue
# If current state is Cat win,
# can can choose this move and force win
if result == CAT:
color[mouse][prev_cat][prev_turn] = CAT
queue.append((mouse, prev_cat, prev_turn, CAT))
else:
# Otherwise this move is bad for cat
# Remove this option, remove one available option
degree[mouse][prev_cat][prev_turn] -= 1
# If all moves are bad: mouse wins
if degree[mouse][prev_cat][prev_turn] == 0:
color[mouse][prev_cat][prev_turn] = MOUSE
queue.append((mouse, prev_cat, prev_turn, MOUSE))
# cat just moved:
else:
# previous turn was cat
prev_turn = 0
# Determine where mouse could have come from
for prev_mouse in graph[mouse]:
# Skip already resolved states
if color[prev_mouse][cat][prev_turn] != DRAW:
continue
# If current state is MOUSE win,
# mouse can choose this move and force win
if result == MOUSE:
color[prev_mouse][cat][prev_turn] = MOUSE
queue.append((prev_mouse, cat, prev_turn, MOUSE))
else:
# Other this move is bad for mouse
# Remove this option, remove one available option
degree[prev_mouse][cat][prev_turn] -= 1
# If all moves lose: cat wins
if degree[prev_mouse][cat][prev_turn] == 0:
color[prev_mouse][cat][prev_turn] = CAT
queue.append((prev_mouse, cat, prev_turn, CAT))
# Final Result:
# Initial State
# mouse => 1
# cat => 2
# mouse moves first
# Result Starting From:
# mouse = 1
# cat=2
# mouse turn
res = color[1][2][0]
# overall: tc O(n^3) since there are O(n^2 * 2) states and each state checks neighbors
# overall: sc O(n^3) for color and degree arrays
return res1976. Number of Ways to Arrive at Destination ::1:: - Medium
Topics: Dynamic Programming, Graph Theory, Topological Sort, Shortest Path
Intro
You are in a city that consists of n intersections numbered from 0 to n - 1 with bi-directional roads between some intersections. The inputs are generated such that you can reach any intersection from any other intersection and that there is at most one road between any two intersections. You are given an integer n and a 2D integer array roads where roads[i] = [ui, vi, timei] means that there is a road between intersections ui and vi that takes timei minutes to travel. You want to know in how many ways you can travel from intersection 0 to intersection n - 1 in the shortest amount of time. Return the number of ways you can arrive at your destination in the shortest amount of time. Since the answer may be large, return it modulo 10^9 + 7.
| Example Input | Output |
|---|---|
| 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]] | 4 |
| n = 2, roads = [[1,0,10]] | 1 |
Constraints:
1 ≤ n ≤ 200
n - 1 ≤ roads.length ≤ n * (n - 1) / 2
roads[i].length == 3
0 ≤ ui, vi ≤ n - 1
1 ≤ timei ≤ 10^9
ui != vi
There is at most one road connecting any two intersections
You can reach any intersection from any other intersection
Abstraction
Huh?
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: Topological Sort using BFS - Advanced Graphs/Advanced Graphs
def countPaths(self, n: int, roads: List[List[int]]) -> int:
MOD = 10**9 + 7
# Dijkstras + Topological DP on Shortest Path
# Step 1: Compute shortest path distances from node 0
# Step 2: Treat nodes as DAG ordered by distance
# Step 3: Propagate number of ways along edges consistent with shortest paths
# Key Insight:
# After Dijkstra, any edge u->v such that dist[u] + t == dist[v]
# can only be used along a shortest path.
# Topological DP:
# - process nodes in increasing distance (topological order)
# - propagate counts along shortest paths edges
# Build adjacency list with travel times:
# graph[u] = [(neighbor, travel_time), ...]
# sc: O(V + E)
graph = defaultdict(list)
# tc: O(E)
for u, v, t in roads:
graph[u].append((v, t))
graph[v].append((u, t))
# Dijkstra to find shortest distances from node 0
# dist[u] = shortest distance from node 0 to u
# sc: O(V)
dist = [float('inf')] * n
dist[0] = 0
heap = [(0, 0)] # (distance, node)
# tc: O(E log V)
while heap:
#
d, u = heapq.heappop(heap)
if d > dist[u]:
continue
for v, t in graph[u]:
if dist[u] + t < dist[v]:
dist[v] = dist[u] + t
heapq.heappush(heap, (dist[v], v))
# Sort nodes by distance (topological order by shortest path)
# Nodes can now be treated as DAG ordered by dist:
# edges along shortest paths only go from smaller to larger distance
# tc: O(V log V)
# sc: O(V)
nodes_by_dist = sorted(range(n), key=lambda x: dist[x])
# DP to count ways along shortest paths
# ways[u] = number of shortest paths from 0 to u
ways = [0] * n
# Start from node 0
ways[0] = 1
# Propagate along edges where dist[u] + t == dist[v]
# tc: O(E)
# sc: O(V)
for u in nodes_by_dist:
for v, t in graph[u]:
# Only propagate along shortest paths
if dist[u] + t == dist[v]:
ways[v] = (ways[v] + ways[u]) % MOD
# Number of ways to reach destination n-1
res = ways[n - 1]
# Let V = n nodes, E = len(roads) edges
# tc:
# O(E log V) → Dijkstra
# + O(V log V) → sorting nodes
# + O(E) → DP propagation
# = O(E log V)
# sc:
# O(V + E) → adjacency list, distance array, ways array
# overall: tc (E log V)
# overall sc O(V + E)
return res1203. Sort Items by Groups Respecting Dependencies ::1:: - Hard
Topics: Depth First Search, Breadth First Search, Graph Theory, Topological Sort
Intro
There are n items each belonging to zero or one of m groups where group[i] is the group that the i-th item belongs to and it's equal to -1 if the i-th item belongs to no group. The items and the groups are zero indexed. A group can have no item belonging to it. Return a sorted list of the items such that: The items that belong to the same group are next to each other in the sorted list. There are some relations between these items where beforeItems[i] is a list containing all the items that should come before the i-th item in the sorted array (to the left of the i-th item). Return any solution if there is more than one solution and return an empty list if there is no solution.
| Example Input | Output |
|---|---|
| n = 8, m = 2, group = [-1,-1,1,0,0,1,0,-1], beforeItems = [[],[6],[5],[6],[3,6],[],[],[]] | [6,3,4,1,5,2,0,7] |
| n = 8, m = 2, group = [-1,-1,1,0,0,1,0,-1], beforeItems = [[],[6],[5],[6],[3],[],[4],[]] | [] |
Constraints:
1 ≤ m ≤ n ≤ 3 * 10^3
group.length == beforeItems.length == n
-1 ≤ group[i] ≤ m - 1
0 ≤ beforeItems[i].length ≤ n-1
0 ≤ beforeItems[i][j] ≤ n-1
i != beforeItems[i][j]
beforeItems[i] does not contains duplicate elements
Abstraction
Huh?
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Topological Sort using BFS - Advanced Graphs/Advanced Graphs
def sortItems(self, n: int, m: int, group: List[int], beforeItems: List[List[int]]) -> List[int]:
# Topological Sort (BFS / Kahn's Algorithm)
#
# Graph Representation:
# - item = node in item graph
# - group = node in group graph
#
# Problem Abstraction:
# - Items must respect dependency ordering
# - Items in same group must appear together
#
# Key Idea:
# - Perform TWO topological sorts:
# 1. group-level ordering
# 2. item-level ordering
#
# Group ordering determines group blocks.
# Item ordering determines order inside blocks.
# Assign unique groups to ungrouped items:
for i in range(n):
# group[i] == -1 means item has no group.
if group[i] == -1:
# Treat each as its own unique group
group[i] = m
m += 1
# Graphs + In Degrees
item_graph = defaultdict(list)
group_graph = defaultdict(list)
# item_graph: dependency between items
item_indegree = [0] * n
# group_graph: dependency between groups
group_indegree = [0] * m
# group => list of items
group_items = defaultdict(list)
for i in range(n):
group_items[group[i]].append(i)
# Build Item Graph + Group Graph:
# - If two dependent items belong to different groups:
# create edge between groups.
# - Otherwise:
# create edge between items.
for curr in range(n):
for prev in beforeItems[curr]:
# item dependency
item_graph[prev].append(curr)
item_indegree[curr] += 1
# cross-group dependency
if group[prev] != group[curr]:
group_graph[group[prev]].append(group[curr])
group_indegree[group[curr]] += 1
# Topological Sort Helper (BFS)
def topo_sort(nodes, graph, indegree):
queue = deque([x for x in nodes if indegree[x] == 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)
# cycle detection
if len(order) != len(nodes):
return []
return order
# Topological Sort Groups:
group_order = topo_sort(range(m), group_graph, group_indegree)
if not group_order:
return []
# Topological Sort Items:
item_order = topo_sort(range(n), item_graph, item_indegree)
if not item_order:
return []
# Organize Items by Group Order
# item_order already respects dependencies.
# We now place items together by group.
ordered_group_items = defaultdict(list)
for item in item_order:
ordered_group_items[group[item]].append(item)
result = []
for g in group_order:
result.extend(ordered_group_items[g])
# overall: tc O(n + total dependencies)
# overall: sc O(n + total dependencies)
return result1462. Course Schedule IV ::1:: - Medium
Topics: Depth First Search, Breadth First Search, Graph Theory, Topological Sort
Intro
There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course ai first if you want to take course bi. For example, the pair [0, 1] indicates that you have to take course 0 before you can take course 1. Prerequisites can also be indirect. If course a is a prerequisite of course b, and course b is a prerequisite of course c, then course a is a prerequisite of course c. You are also given an array queries where queries[j] = [uj, vj]. For the jth query, you should answer whether course uj is a prerequisite of course vj or not. Return a boolean array answer, where answer[j] is the answer to the jth query.
| Example Input | Output |
|---|---|
| numCourses = 2, prerequisites = [[1,0]], queries = [[0,1],[1,0]] | [false,true] |
| numCourses = 2, prerequisites = [], queries = [[1,0],[0,1]] | [false,false] |
| numCourses = 3, prerequisites = [[1,2],[1,0],[2,0]], queries = [[1,0],[1,2]] | [true,true] |
Constraints:
2 ≤ numCourses ≤ 100
0 ≤ prerequisites.length ≤ (numCourses * (numCourses - 1) / 2)
prerequisites[i].length == 2
0 ≤ ai, bi ≤ numCourses - 1
ai != bi
All the pairs [ai, bi] are unique
The prerequisites graph has no cycles
1 ≤ queries.length ≤ 10^4
0 ≤ ui, vi ≤ numCourses - 1
ui != vi
Abstraction
Huh?
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Topological Sort using BFS - Advanced Graphs/Advanced Graphs
def checkIfPrerequisite(self, numCourses: int, prerequisites: List[List[int]], queries: List[List[int]]) -> List[bool]:
# Topological Sort (BFS / Kahn's Algorithm)
#
# Graph Representation:
# - unique class = node in a directed graph
# - directed edge (a -> b):
# means a must be taken BEFORE b
#
# Problem Abstraction:
# - Determine if course u is a prerequisite of course v
# - Includes DIRECT and INDIRECT prerequisites
#
# Key Idea:
# - Perform BFS topological sort
# - While processing nodes,
# propagate prerequisite sets forward.
#
# If:
# a -> b
# then:
# prereqs[b] includes prereqs[a] + {a}
# --------------------------------------------------
# Initialize Graph + In Degree
# --------------------------------------------------
graph = defaultdict(list)
indegree = [0] * numCourses
for a, b in prerequisites:
graph[a].append(b)
indegree[b] += 1
# --------------------------------------------------
# prereqs[i] = all prerequisites of course i
# --------------------------------------------------
prereqs = [set() for _ in range(numCourses)]
# --------------------------------------------------
# BFS Topological Sort (Kahn's Algorithm)
# --------------------------------------------------
queue = deque()
for i in range(numCourses):
if indegree[i] == 0:
queue.append(i)
# --------------------------------------------------
# Process:
# 1. Pop node
# 2. Propagate prereqs to neighbors
# 3. Reduce indegree
# 4. Push new zero indegree nodes
# --------------------------------------------------
while queue:
curr = queue.popleft()
for nei in graph[curr]:
# curr itself is prerequisite of neighbor
prereqs[nei].add(curr)
# all prereqs of curr propagate forward
prereqs[nei].update(prereqs[curr])
indegree[nei] -= 1
if indegree[nei] == 0:
queue.append(nei)
# --------------------------------------------------
# Answer Queries
# --------------------------------------------------
res = []
for u, v in queries:
res.append(u in prereqs[v])
# overall: tc O(V^2 + Q)
# V <= 100 so set propagation is safe
#
# overall: sc O(V^2)
return res1591. Strange Printer II ::1:: - Hard
Topics: Array, Graph Theory, Topological Sort, Matrix
Intro
There is a strange printer with the following two special requirements: On each turn, the printer will print a solid rectangular pattern of a single color on the grid. This will cover up the existing colors in the rectangle. Once the printer has used a color for the above operation, the same color cannot be used again. You are given a m x n matrix targetGrid, where targetGrid[row][col] is the color in the position (row, col) of the grid. Return true if it is possible to print the matrix targetGrid, otherwise, return false.
| Example Input | Output |
|---|---|
| targetGrid = [[1,1,1,1],[1,2,2,1],[1,2,2,1],[1,1,1,1]] | true |
| targetGrid = [[1,1,1,1],[1,1,3,3],[1,1,3,4],[5,5,1,4]] | true |
| targetGrid = [[1,2,1],[2,1,2],[1,2,1]] | false |
Constraints:
m == targetGrid.length
n == targetGrid[i].length
1 ≤ m, n ≤ 60
1 ≤ targetGrid[row][col] ≤ 60
Abstraction
Huh?
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Topological Sort using BFS - Advanced Graphs/Advanced Graphs
def isPrintable(self, targetGrid: List[List[int]]) -> bool:
# Topological Sort (BFS / Kahn's Algorithm)
#
# Graph Representation:
# - unique color = node in directed graph
# - edge (c1 -> c2):
# means color c1 must be printed BEFORE c2
#
# Problem Abstraction:
# - Each color must be printed exactly once as a rectangle.
# - Any other color appearing inside that rectangle
# must be printed AFTER this color.
#
# Therefore:
# - Build dependency graph between colors
# - Check if a valid topological ordering exists.
#
# If cycle exists -> impossible printing.
m, n = len(targetGrid), len(targetGrid[0])
MAX_COLOR = 61 # constraints: colors 1..60
# --------------------------------------------------
# Step 1: Find Bounding Rectangle For Each Color
# --------------------------------------------------
#
# For each color:
# min_row, max_row, min_col, max_col
min_r = [float("inf")] * MAX_COLOR
min_c = [float("inf")] * MAX_COLOR
max_r = [-1] * MAX_COLOR
max_c = [-1] * MAX_COLOR
exists = [False] * MAX_COLOR
for r in range(m):
for c in range(n):
color = targetGrid[r][c]
exists[color] = True
min_r[color] = min(min_r[color], r)
max_r[color] = max(max_r[color], r)
min_c[color] = min(min_c[color], c)
max_c[color] = max(max_c[color], c)
# --------------------------------------------------
# Step 2: Build Dependency Graph
# --------------------------------------------------
#
# If another color appears inside color C's rectangle:
#
# C must be printed BEFORE that color
#
# edge: C -> other
graph = defaultdict(set)
indegree = [0] * MAX_COLOR
for color in range(1, MAX_COLOR):
if not exists[color]:
continue
for r in range(min_r[color], max_r[color] + 1):
for c in range(min_c[color], max_c[color] + 1):
other = targetGrid[r][c]
if other != color and other not in graph[color]:
graph[color].add(other)
indegree[other] += 1
# --------------------------------------------------
# Step 3: BFS Topological Sort (Kahn's Algorithm)
# --------------------------------------------------
queue = deque()
total_colors = 0
for color in range(1, MAX_COLOR):
if exists[color]:
total_colors += 1
if indegree[color] == 0:
queue.append(color)
processed = 0
while queue:
curr = queue.popleft()
processed += 1
for nei in graph[curr]:
indegree[nei] -= 1
if indegree[nei] == 0:
queue.append(nei)
# --------------------------------------------------
# Step 4: Cycle Detection
# --------------------------------------------------
#
# If not all colors processed -> cycle exists
# overall: tc O(C * m * n) (C <= 60)
# overall: sc O(C^2)
return processed == total_colors1632. Rank Transform of a Matrix ::1:: - Hard
Topics: Array, Union Find, Graph Theory, Topological Sort, Sorting, Matrix
Intro
Given an m x n matrix, return a new matrix answer where answer[row][col] is the rank of matrix[row][col]. The rank is an integer that represents how large an element is compared to other elements. It is calculated using the following rules: The rank is an integer starting from 1. If two elements p and q are in the same row or column, then: If p < q then rank(p) < rank(q) If p == q then rank(p) == rank(q) If p > q then rank(p) > rank(q) The rank should be as small as possible. The test cases are generated so that answer is unique under the given rules.
| Example Input | Output |
|---|---|
| matrix = [[1,2],[3,4]] | [[1,2],[2,3]] |
| matrix = [[7,7],[7,7]] | [[1,1],[1,1]] |
| matrix = [[20,-21,14],[-19,4,19],[22,-47,24],[-19,4,19]] | [[4,2,3],[1,3,4],[5,1,6],[1,3,4]] |
Constraints:
m == matrix.length
n == matrix[i].length
1 ≤ m, n ≤ 500
-10^9 ≤ matrix[row][col] ≤ 10^9
Abstraction
Huh?
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Topological Sort using BFS - Advanced Graphs/Advanced Graphs
def matrixRankTransform(self, matrix: List[List[int]]) -> List[List[int]]:
# Topological Sort (BFS / Kahn's Algorithm)
#
# Graph Representation:
# - nodes = connected components of equal values
# - edge (A -> B):
# value in A must have smaller rank than value in B
#
# Problem Abstraction:
# - Equal values in same row/column must share rank
# - Smaller values must get smaller ranks
#
# Key Idea:
# 1. Union equal values in rows/columns
# 2. Build DAG between components
# 3. Topological BFS propagates ranks
m, n = len(matrix), len(matrix[0])
# --------------------------------------------------
# Union Find
# --------------------------------------------------
parent = {}
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
def union(a, b):
pa, pb = find(a), find(b)
if pa != pb:
parent[pb] = pa
# --------------------------------------------------
# Step 1: Create nodes for all cells
# --------------------------------------------------
for r in range(m):
for c in range(n):
parent[(r, c)] = (r, c)
# --------------------------------------------------
# Step 2: Union equal values (same row / column)
# --------------------------------------------------
for r in range(m):
values = defaultdict(list)
for c in range(n):
values[matrix[r][c]].append((r, c))
for cells in values.values():
for i in range(1, len(cells)):
union(cells[0], cells[i])
for c in range(n):
values = defaultdict(list)
for r in range(m):
values[matrix[r][c]].append((r, c))
for cells in values.values():
for i in range(1, len(cells)):
union(cells[0], cells[i])
# --------------------------------------------------
# Step 3: Build Component DAG
# --------------------------------------------------
graph = defaultdict(set)
indegree = defaultdict(int)
# helper to build ordering constraints
def build_edges(cells):
cells.sort(key=lambda x: x[0])
for i in range(1, len(cells)):
val1, node1 = cells[i - 1]
val2, node2 = cells[i]
if val1 == val2:
continue
p1 = find(node1)
p2 = find(node2)
if p1 != p2 and p2 not in graph[p1]:
graph[p1].add(p2)
indegree[p2] += 1
# row constraints
for r in range(m):
arr = [(matrix[r][c], (r, c)) for c in range(n)]
build_edges(arr)
# column constraints
for c in range(n):
arr = [(matrix[r][c], (r, c)) for r in range(m)]
build_edges(arr)
# --------------------------------------------------
# Step 4: BFS Topological Sort
# --------------------------------------------------
rank = defaultdict(lambda: 1)
components = set(find((r, c)) for r in range(m) for c in range(n))
queue = deque([x for x in components if indegree[x] == 0])
while queue:
curr = queue.popleft()
for nei in graph[curr]:
rank[nei] = max(rank[nei], rank[curr] + 1)
indegree[nei] -= 1
if indegree[nei] == 0:
queue.append(nei)
# --------------------------------------------------
# Step 5: Build Result Matrix
# --------------------------------------------------
res = [[0] * n for _ in range(m)]
for r in range(m):
for c in range(n):
res[r][c] = rank[find((r, c))]
# overall: tc O(m*n log(m*n))
# overall: sc O(m*n)
return res2127. Maximum Employees to Be Invited to a Meeting ::1:: - Hard
Topics: Depth First Search, Graph Theory, Topological Sort
Intro
A company is organizing a meeting and has a list of n employees, waiting to be invited. They have arranged for a large circular table, capable of seating any number of employees. The employees are numbered from 0 to n - 1. Each employee has a favorite person and they will attend the meeting only if they can sit next to their favorite person at the table. The favorite person of an employee is not themself. Given a 0-indexed integer array favorite, where favorite[i] denotes the favorite person of the ith employee, return the maximum number of employees that can be invited to the meeting.
| Example Input | Output |
|---|---|
| favorite = [2,2,1,2] | 3 |
| favorite = [1,2,0] | 3 |
| favorite = [3,0,1,4,1] | 4 |
Constraints:
n == favorite.length
2 ≤ n ≤ 10^5
0 ≤ favorite[i] ≤ n-1
favorite[i] != i
Abstraction
Huh?
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Topological Sort using BFS - Advanced Graphs/Advanced Graphs
def maximumInvitations(self, favorite: List[int]) -> int:
# Topological Sort (BFS / Kahn's Algorithm)
#
# Graph Representation:
# - each employee = node
# - directed edge i -> favorite[i]
#
# Problem Abstraction:
# - Each node has exactly one outgoing edge.
# - Graph structure becomes:
# cycles + chains feeding into cycles.
#
# Key Insight:
# 1. Use topological BFS to trim all chains.
# 2. Remaining nodes belong to cycles.
#
# Two valid seating cases:
# A) largest cycle (length >= 3)
# B) mutual pairs (2-cycles) + longest chains feeding them
n = len(favorite)
# --------------------------------------------------
# Step 1: Build In-Degree
# --------------------------------------------------
indegree = [0] * n
for u in range(n):
indegree[favorite[u]] += 1
# --------------------------------------------------
# Step 2: Topological Leaf Trimming
# --------------------------------------------------
#
# Remove nodes with indegree == 0.
# These are chain nodes not inside cycles.
#
# dist[i] = longest chain ending at i
queue = deque()
dist = [1] * n
for i in range(n):
if indegree[i] == 0:
queue.append(i)
while queue:
curr = queue.popleft()
nxt = favorite[curr]
# propagate longest chain length
dist[nxt] = max(dist[nxt], dist[curr] + 1)
indegree[nxt] -= 1
if indegree[nxt] == 0:
queue.append(nxt)
# --------------------------------------------------
# Step 3: Process Cycles
# --------------------------------------------------
visited = [False] * n
max_cycle = 0
pair_sum = 0
for i in range(n):
# nodes with indegree > 0 are in cycles
if indegree[i] > 0 and not visited[i]:
cycle_len = 0
curr = i
# traverse cycle
while not visited[curr]:
visited[curr] = True
curr = favorite[curr]
cycle_len += 1
# case 1: mutual pair (cycle length = 2)
if cycle_len == 2:
a = i
b = favorite[i]
pair_sum += dist[a]
pair_sum += dist[b]
else:
# case 2: normal cycle
max_cycle = max(max_cycle, cycle_len)
# --------------------------------------------------
# Step 4: Result
# --------------------------------------------------
#
# answer is max between:
# - largest cycle
# - sum of all mutual-pair components
# overall: tc O(V)
# overall: sc O(V)
return max(max_cycle, pair_sum)2192. All Ancestors of a Node in a Directed Acyclic Graph ::1:: - Medium
Topics: Depth First Search, Breadth First Search, Graph Theory, Topological Sort
Intro
You are given a positive integer n representing the number of nodes of a Directed Acyclic Graph (DAG). The nodes are numbered from 0 to n - 1 (inclusive). You are also given a 2D integer array edges, where edges[i] = [fromi, toi] denotes that there is a unidirectional edge from fromi to toi in the graph. Return a list answer, where answer[i] is the list of ancestors of the ith node, sorted in ascending order. A node u is an ancestor of another node v if u can reach v via a set of edges.
| Example Input | Output |
|---|---|
| n = 8, edgeList = [[0,3],[0,4],[1,3],[2,4],[2,7],[3,5],[3,6],[3,7],[4,6]] | [[],[],[],[0,1],[0,2],[0,1,3],[0,1,2,3,4],[0,1,2,3]] |
| n = 5, edgeList = [[0,1],[0,2],[0,3],[0,4],[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]] | [[],[0],[0,1],[0,1,2],[0,1,2,3]] |
Constraints:
1 ≤ n ≤ 1000
0 ≤ edges.length ≤ min(2000, n * (n-1) / 2)
edges[i].length == 2
0 ≤ fromi, toi ≤ n-1
fromi != toi
There are no duplicate edges
The graph is directed and acyclic
Abstraction
Huh?
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Topological Sort using BFS - Advanced Graphs/Advanced Graphs
def getAncestors(self, n: int, edges: List[List[int]]) -> List[List[int]]:
# Topological Sort (BFS / Kahn's Algorithm)
#
# Graph Representation:
# - unique node = node in directed graph
# - directed edge (u -> v):
# means u comes BEFORE v
#
# Problem Abstraction:
# - A node's ancestors are all nodes that can reach it.
# - Since graph is a DAG, we can process nodes
# in topological order and propagate ancestors forward.
#
# Key Idea:
# - During BFS topological sort:
# ancestors[v] += ancestors[u] + {u}
# --------------------------------------------------
# Initialize Graph + In Degree
# --------------------------------------------------
graph = defaultdict(list)
indegree = [0] * n
for u, v in edges:
graph[u].append(v)
indegree[v] += 1
# --------------------------------------------------
# ancestors[i] = set of all ancestors of node i
# --------------------------------------------------
ancestors = [set() for _ in range(n)]
# --------------------------------------------------
# BFS Topological Sort (Kahn's Algorithm)
# --------------------------------------------------
queue = deque()
for i in range(n):
if indegree[i] == 0:
queue.append(i)
# Process:
# 1. Pop node
# 2. Propagate ancestor sets
# 3. Reduce indegree
# 4. Push new zero indegree nodes
while queue:
curr = queue.popleft()
for nei in graph[curr]:
# curr is direct ancestor
ancestors[nei].add(curr)
# propagate all ancestors of curr
ancestors[nei].update(ancestors[curr])
indegree[nei] -= 1
if indegree[nei] == 0:
queue.append(nei)
# --------------------------------------------------
# Convert to sorted lists
# --------------------------------------------------
res = []
for i in range(n):
res.append(sorted(ancestors[i]))
# overall: tc O(V + E + V^2)
# (set propagation dominates)
#
# overall: sc O(V^2)
return res2050. Parallel Courses III ::1:: - Hard
Topics: Array, Dynamic Programming, Graph Theory, Topological Sort
Intro
You are given an integer n, which indicates that there are n courses labeled from 1 to n. You are also given a 2D integer array relations where relations[j] = [prevCoursej, nextCoursej] denotes that course prevCoursej has to be completed before course nextCoursej (prerequisite relationship). Furthermore, you are given a 0-indexed integer array time where time[i] denotes how many months it takes to complete the (i+1)th course. You must find the minimum number of months needed to complete all the courses following these rules: You may start taking a course at any time if the prerequisites are met. Any number of courses can be taken at the same time. Return the minimum number of months needed to complete all the courses. Note: The test cases are generated such that it is possible to complete every course (i.e., the graph is a directed acyclic graph).
| Example Input | Output |
|---|---|
| n = 3, relations = [[1,3],[2,3]], time = [3,2,5] | 8 |
| n = 5, relations = [[1,5],[2,5],[3,5],[3,4],[4,5]], time = [1,2,3,4,5] | 12 |
Constraints:
1 ≤ n ≤ 5 * 10^4
0 ≤ relations.length ≤ min(n * (n - 1) / 2, 5 * 10^4)
relations[j].length == 2
1 ≤ prevCoursej, nextCoursej ≤ n
prevCoursej != nextCoursej
All the pairs [prevCoursej, nextCoursej] are unique
time.length == n
1 ≤ time[i] ≤ 10^4
The given graph is a direct acyclic graph
Abstraction
Huh?
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Topological Sort using BFS - Advanced Graphs/Advanced Graphs
def minimumTime(self, n: int, relations: List[List[int]], time: List[int]) -> int:
# Topological Sort (BFS / Kahn's Algorithm)
#
# Graph Representation:
# - unique course = node in directed graph
# - directed edge (u -> v):
# course u must be completed BEFORE course v
#
# Problem Abstraction:
# - Each course has a completion time.
# - Multiple courses can run in parallel.
# - A course can only start after ALL prerequisites finish.
#
# Key Idea:
# - Perform topological BFS.
# - Propagate earliest completion times forward.
#
# If:
# u -> v
#
# then:
# finish[v] = max(finish[v],
# finish[u] + time[v])
# --------------------------------------------------
# Initialize Graph + In Degree
# --------------------------------------------------
graph = defaultdict(list)
indegree = [0] * (n + 1)
for u, v in relations:
graph[u].append(v)
indegree[v] += 1
# --------------------------------------------------
# finish[i] = earliest time course i can complete
# --------------------------------------------------
finish = [0] * (n + 1)
# --------------------------------------------------
# BFS Topological Sort (Kahn's Algorithm)
# --------------------------------------------------
queue = deque()
# start with courses having no prerequisites
for i in range(1, n + 1):
if indegree[i] == 0:
queue.append(i)
finish[i] = time[i - 1]
# Process:
# 1. Pop course
# 2. Propagate finish time to neighbors
# 3. Reduce indegree
# 4. Push new zero indegree nodes
while queue:
curr = queue.popleft()
for nei in graph[curr]:
# propagate longest prerequisite chain
finish[nei] = max(
finish[nei],
finish[curr] + time[nei - 1]
)
indegree[nei] -= 1
if indegree[nei] == 0:
queue.append(nei)
# --------------------------------------------------
# Result
# --------------------------------------------------
#
# total time = max completion time among all courses
# overall: tc O(V + E)
# overall: sc O(V + E)
return max(finish)2115. Find All Possible Recipes from Given Supplies ::1:: - Medium
Topics: Array, Hash Table, String, Graph Theory, Topological Sort
Intro
You have information about n different recipes. You are given a string array recipes and a 2D string array ingredients. The ith recipe has the name recipes[i], and you can create it if you have all the needed ingredients from ingredients[i]. A recipe can also be an ingredient for other recipes, i.e., ingredients[i] may contain a string that is in recipes. You are also given a string array supplies containing all the ingredients that you initially have, and you have an infinite supply of all of them. Return a list of all the recipes that you can create. You may return the answer in any order. Note that two recipes may contain each other in their ingredients.
| Example Input | Output |
|---|---|
| recipes = ["bread"], ingredients = [["yeast","flour"]], supplies = ["yeast","flour","corn"] | ["bread"] |
| recipes = ["bread","sandwich"], ingredients = [["yeast","flour"],["bread","meat"]], supplies = ["yeast","flour","meat"] | ["bread","sandwich"] |
| recipes = ["bread","sandwich","burger"], ingredients = [["yeast","flour"],["bread","meat"],["sandwich","meat","bread"]], supplies = ["yeast","flour","meat"] | ["bread","sandwich","burger"] |
Constraints:
n == recipes.length == ingredients.length
1 ≤ n ≤ 100
1 ≤ ingredients[i].length, supplies,length ≤ 100
1 ≤ recipes[i].length, ingredients[i][j].length, supplies[k].length ≤ 10
recipes[i], ingredients[i][j], and supplies[j] consist only of lowercase English letter
All the values of recipes and supplies combined are unique
Each ingredients[i] does not contain any duplicate values
Abstraction
Huh?
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Topological Sort using BFS - Advanced Graphs/Advanced Graphs
def findAllRecipes(self, recipes: List[str], ingredients: List[List[str]], supplies: List[str]) -> List[str]:
# Topological Sort (BFS / Kahn's Algorithm)
#
# Graph Representation:
# - recipe = node in directed graph
# - ingredient -> recipe edge:
# ingredient must exist BEFORE recipe can be made
#
# Problem Abstraction:
# - Each recipe depends on its ingredients.
# - Supplies are already available nodes.
# - A recipe becomes available once all dependencies are met.
#
# Key Idea:
# - Treat supplies as starting nodes.
# - Use indegree to count missing ingredients.
# - When indegree reaches 0, recipe becomes a new supply.
# --------------------------------------------------
# Initialize Graph + In Degree
# --------------------------------------------------
graph = defaultdict(list)
# indegree[recipe] = number of required ingredients
indegree = {}
for i, recipe in enumerate(recipes):
indegree[recipe] = len(ingredients[i])
# ingredient -> recipe edge
for ing in ingredients[i]:
graph[ing].append(recipe)
# --------------------------------------------------
# BFS Topological Sort
# --------------------------------------------------
#
# Start from available supplies
queue = deque(supplies)
result = []
# Process:
# 1. Pop available ingredient/recipe
# 2. Reduce indegree of dependent recipes
# 3. If indegree == 0 → recipe can be created
# 4. Add recipe as new supply
while queue:
curr = queue.popleft()
for nei in graph[curr]:
indegree[nei] -= 1
# recipe becomes available
if indegree[nei] == 0:
queue.append(nei)
result.append(nei)
# overall: tc O(V + E)
# overall: sc O(V + E)
return result2246. Longest Path With Different Adjacent Characters ::1:: - Hard
Topics: Array, String, Tree, Depth First Search, Graph Theory, Topological Sort
Intro
You are given a tree (i.e. a connected, undirected graph that has no cycles) rooted at node 0 consisting of n nodes numbered from 0 to n - 1 The tree is represented by a 0-indexed array parent of size n, where parent[i] is the parent of node i. Since node 0 is the root, parent[0] == -1. You are also given a string s of length n, where s[i] is the character assigned to node i. Return the length of the longest path in the tree such that no pair of adjacent nodes on the path have the same character assigned to them.
| Example Input | Output |
|---|---|
| parent = [-1,0,0,1,1,2], s = "abacbe" | 3 |
| parent = [-1,0,0,0], s = "aabc" | 3 |
Constraints:
n == parent.length == s.length
1 ≤ n ≤ 10^5
0 ≤ parent[i] ≤ n-1 for all 1 ≤ i
parent[0] == -1
parent represents a valid tree
s consists of only lowercase English letters
Abstraction
Huh?
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Topological Sort using BFS - Advanced Graphs/Advanced Graphs
def longestPath(self, parent: List[int], s: str) -> int:
# Topological Sort (BFS / Leaf Trimming)
#
# Graph Representation:
# - tree node = node in graph
# - edge parent -> child
#
# Problem Abstraction:
# - We want longest path where adjacent characters differ.
# - A valid path through a node may combine TWO child chains.
#
# Key Idea:
# - Process tree bottom-up using topological BFS.
# - Leaves are processed first.
# - Each node collects best chains from children.
#
# This is reverse topological order:
# children -> parent.
n = len(parent)
# --------------------------------------------------
# Step 1: Build children list + outdegree
# --------------------------------------------------
children = [[] for _ in range(n)]
outdegree = [0] * n
for i in range(1, n):
p = parent[i]
children[p].append(i)
outdegree[p] += 1
# --------------------------------------------------
# Step 2: Initialize queue with leaves
# --------------------------------------------------
#
# Leaves have no children (outdegree = 0)
queue = deque()
for i in range(n):
if outdegree[i] == 0:
queue.append(i)
# --------------------------------------------------
# longest[i] = best downward path starting at i
# --------------------------------------------------
longest = [1] * n
ans = 1
# --------------------------------------------------
# Step 3: BFS Reverse Topological Processing
# --------------------------------------------------
while queue:
node = queue.popleft()
p = parent[node]
# root has no parent
if p == -1:
continue
# only combine if characters differ
if s[node] != s[p]:
# update global answer:
# path through parent combines two chains
ans = max(ans, longest[p] + longest[node])
# update parent's best downward chain
longest[p] = max(longest[p], longest[node] + 1)
# remove processed child
outdegree[p] -= 1
# parent becomes leaf in reversed processing
if outdegree[p] == 0:
queue.append(p)
# overall: tc O(V)
# overall: sc O(V)
return ans2360. Longest Cycle in a Graph ::1:: - Hard
Topics: Depth First Search, Breadth First Search, Graph Theory, Topological Sort
Intro
You are given a directed graph of n nodes numbered from 0 to n - 1, where each node has at most one outgoing edge. The graph is represented with a given 0-indexed array edges of size n, indicating that there is a directed edge from node i to node edges[i]. If there is no outgoing edge from node i, then edges[i] == -1. Return the length of the longest cycle in the graph. If no cycle exists, return -1. A cycle is a path that starts and ends at the same node.
| Example Input | Output |
|---|---|
| edges = [3,3,4,2,3] | 3 |
| edges = [2,-1,3,1] | -1 |
Constraints:
n == edges.length
2 ≤ n ≤ 10^5
-1 ≤ edges[i] < n
edges[i] != i
Abstraction
Huh?
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Topological Sort using BFS - Advanced Graphs/Advanced Graphs
def longestCycle(self, edges: List[int]) -> int:
# Topological Sort (BFS / Leaf Trimming)
#
# Graph Representation:
# - Node i has at most one outgoing edge edges[i] -> j
# - If edges[i] == -1, no outgoing edge
#
# Problem Abstraction:
# - Remove all nodes that cannot be part of a cycle
# - Remaining nodes form cycles, measure lengths
n = len(edges)
# Step 1: Compute indegree for each node
indegree = [0] * n
for u in range(n):
v = edges[u]
if v != -1:
indegree[v] += 1
# Step 2: Initialize queue with leaves (nodes with indegree 0)
queue = deque([i for i in range(n) if indegree[i] == 0])
# Step 3: BFS leaf trimming
while queue:
node = queue.popleft()
nei = edges[node]
if nei != -1:
indegree[nei] -= 1
if indegree[nei] == 0:
queue.append(nei)
# Step 4: Remaining nodes have indegree > 0 → part of cycles
visited = [False] * n
max_cycle = -1
for i in range(n):
if indegree[i] > 0 and not visited[i]:
# Traverse cycle
current = i
cycle_len = 0
while not visited[current]:
visited[current] = True
cycle_len += 1
current = edges[current]
max_cycle = max(max_cycle, cycle_len)
# overall: tc O(n) - each node processed at most once
# overall: sc O(n) - indegree + visited
return max_cycle2924. Find Champion II ::1:: - Medium
Topics: Graph Theory, Topological Sort
Intro
There are n teams numbered from 0 to n - 1 in a tournament; each team is also a node in a DAG. You are given the integer n and a 0-indexed 2D integer array edges of length m representing the DAG, where edges[i] = [ui, vi] indicates that there is a directed edge from team ui to team vi in the graph. A directed edge from a to b in the graph means that team a is stronger than team b and team b is weaker than team a. Team a will be the champion of the tournament if there is no team b that is stronger than team a. Return the team that will be the champion of the tournament if there is a unique champion, otherwise, return -1. Notes: A cycle is a series of nodes a1, a2, ..., an, an+1 such that node a1 is the same node as node an+1, the nodes a1, a2, ..., an are distinct, and there is a directed edge from the node ai to node ai+1 for every i in the range [1, n]. A DAG is a directed graph that does not have any cycle.
| Example Input | Output |
|---|---|
| n = 3, edges = [[0,1],[1,2]] | 0 |
| n = 4, edges = [[0,2],[1,3],[1,2]] | -1 |
Constraints:
1 ≤ n ≤ 100
m == edges.length
0 ≤ m ≤ n * (n-1) / 2
edges[i].length == 2
0 ≤ edge[i][j] ≤ n-1
edges[i][0] != edges[i][1]
The input is generatred such that if team a is stronger than team b, team b is not stronger than team a
The input is generated such that if team a is stronger than team b, and team b is stronger than team c, then team a is stronger than team c
Abstraction
Huh?
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Topological Sort using BFS - Advanced Graphs/Advanced Graphs
def findChampion(self, n: int, edges: List[List[int]]) -> int:
# Build graph and indegree array
indegree = [0] * n
for u, v in edges:
indegree[v] += 1
# Collect nodes with indegree 0 (potential champions)
sources = [i for i in range(n) if indegree[i] == 0]
# If there is exactly one source, that is the unique champion
if len(sources) == 1:
return sources[0]
# Otherwise, no unique champion
return -12328. Number of Increasing Paths in a Grid ::1:: - Hard
Topics: Array, Dynamic Programming, Depth First Search, Breadth First Search, Graph Theory, Topological Sort, Memoization, Matrix
Intro
You are given an m x n integer matrix grid, where you can move from a cell to any adjacent cell in all 4 directions. Return the number of strictly increasing paths in the grid such that you can start from any cell and end at any cell. Since the answer may be very large, return it modulo 10^9 + 7. Two paths are considered different if they do not have exactly the same sequence of visited cells.
| Example Input | Output |
|---|---|
| grid = [[1,1],[3,4]] | 8 |
| grid = [[1],[2]] | 3 |
Constraints:
m == grid.length
n == grid[i].length
1 ≤ m, n ≤ 1000
1 ≤ m * n ≤ 10^5
1 ≤ grid[i][j] ≤ 10^5
Abstraction
Huh?
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Topological Sort using BFS - Advanced Graphs/Advanced Graphs
def countPaths(self, grid: List[List[int]]) -> int:
MOD = 10**9 + 7
# Grid boundaries
m, n = len(grid), len(grid[0])
directions = [(1,0), (-1,0), (0,1), (0,-1)]
# Step 1: Build indegree for DAG
indegree = [[0]*n for _ in range(m)]
for i in range(m):
for j in range(n):
for dx, dy in directions:
x, y = i+dx, j+dy
if 0 <= x < m and 0 <= y < n and grid[x][y] > grid[i][j]:
indegree[x][y] += 1 # incoming edge to larger neighbor
# Step 2: Initialize queue with cells that have indegree 0
q = deque()
for i in range(m):
for j in range(n):
if indegree[i][j] == 0:
q.append((i,j))
# Step 3: Dynamic programming array to count paths ending at each cell
# Initially, each cell itself is a path
dp = [[1]*n for _ in range(m)]
# Step 4: BFS Topological Sort
while q:
i, j = q.popleft()
# Propagate paths to neighbors that are strictly larger
for dx, dy in directions:
x, y = i+dx, j+dy
if 0 <= x < m and 0 <= y < n and grid[x][y] > grid[i][j]:
dp[x][y] = (dp[x][y] + dp[i][j]) % MOD
indegree[x][y] -= 1
if indegree[x][y] == 0:
q.append((x,y))
# Step 5: Total number of increasing paths = sum over all cells
total_paths = sum(dp[i][j] for i in range(m) for j in range(n)) % MOD
return total_paths2392. Build a Matrix With Conditions ::1:: - Hard
Topics: Array, Graph Theory, Topological Sort, Matrix
Intro
You are given a positive integer k. You are also given: a 2D integer array rowConditions of size n where rowConditions[i] = [abovei, belowi], and a 2D integer array colConditions of size m where colConditions[i] = [lefti, righti]. The two arrays contain integers from 1 to k. You have to build a k x k matrix that contains each of the numbers from 1 to k exactly once. The remaining cells should have the value 0. The matrix should also satisfy the following conditions: The number abovei should appear in a row that is strictly above the row at which the number belowi appears for all i from 0 to n - 1. The number lefti should appear in a column that is strictly left of the column at which the number righti appears for all i from 0 to m - 1. Return any matrix that satisfies the conditions. If no answer exists, return an empty matrix.
| Example Input | Output |
|---|---|
| k = 3, rowConditions = [[1,2],[3,2]], colConditions = [[2,1],[3,2]] | [[3,0,0],[0,0,1],[0,2,0]] |
| k = 3, rowConditions = [[1,2],[2,3],[3,1],[2,3]], colConditions = [[2,1]] | [] |
Constraints:
2 ≤ k ≤ 400
1 ≤ rowConditions.length, colConditions.length ≤ 10^4
rowConditions[i].length == colConditions[i].length == 2
1 ≤ abovei, belowi, lefti, righti ≤ k
abovei != belowi
lefti != righti
Abstraction
Huh?
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Topological] Topological Sort using BFS - Advanced Graphs/Advanced Graphs
def buildMatrix(self, k: int, rowConditions: List[List[int]], colConditions: List[List[int]]) -> List[List[int]]:
# Helper function: BFS Topological Sort
def topo_sort(conditions: List[List[int]]) -> List[int]:
graph = defaultdict(list)
indegree = [0] * (k + 1) # 1-indexed
# Build graph + indegree
for u, v in conditions:
graph[u].append(v)
indegree[v] += 1
# Queue nodes with indegree 0
q = deque([i for i in range(1, k+1) if indegree[i] == 0])
order = []
while q:
node = q.popleft()
order.append(node)
for nei in graph[node]:
indegree[nei] -= 1
if indegree[nei] == 0:
q.append(nei)
# If not all nodes included, cycle exists
if len(order) != k:
return []
return order
# Step 1: Topological order for rows
row_order = topo_sort(rowConditions)
if not row_order:
return [] # Impossible due to cycle
# Step 2: Topological order for columns
col_order = topo_sort(colConditions)
if not col_order:
return [] # Impossible due to cycle
# Step 3: Map number -> row index / column index
row_idx = {num: i for i, num in enumerate(row_order)}
col_idx = {num: i for i, num in enumerate(col_order)}
# Step 4: Build matrix
matrix = [[0]*k for _ in range(k)]
for num in range(1, k+1):
r, c = row_idx[num], col_idx[num]
matrix[r][c] = num
return matrix