Graph
Notes
Breadth-First Traversal (BFS)
- BFS naturally finds the shortest path to a node
from collections import deque def bfs(graph, root): visited = set([root]) queue = deque([root]) result = [] while queue: curr = popleft() result.append(curr) for neighbor in graph[curr]: if neighbor not in visited: visited.add(neighbor) queue.append(neighbor) return result
Depth-First Search (DFS)
def dfsRecursive(graph, root, visited): visited.add(root) print(root.val) for neighbor in graph[root]: if root not in visited: dfsRecursive(graph, neighbor, visited) def dfsIterative(graph, root): visited = set() stack = [root] while stack: curr = stack.pop() print(curr.val) visited.add(curr) for neighbor in graph[curr]: if neighbor not in visited: stack.append(neighbor)
Topological Sort (Kahn's Algorithm)
from collections import deque, defaultdict def topologicalSort(nodes, edges): # Which nodes have current node as a prerequisite? # { prereqNode: [nodes] } neighbors = defaultdict(list) # How many prerequisites does the current node have? # { node: prereqCount } indegree = {} # Add all nodes. Edges may not include all nodes for node in nodes: indegree[node] = 0 for prereq, node in edges: neighbors[prereq].append(node) indegree[node] += 1 # breadth first traversal queue = deque([node for node in nodes if indegree[node] == 0]) result = [] while queue: node = queue.popleft() result.append(node) for neighbor in neighbors[node]: indegree[neighbor] -= 1 if indegree[neighbor] == 0: queue.append(neighbor) # Cycle exists if unequal lengths return result if len(result) == len(nodes) else []
Number of Islands (Medium)
Given an m x n 2D binary grid grid which represents a map of '1's (land) and '0's (water), return the number of islands.
An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.
def numIslands(grid: List[List[str]]) -> int: directions = [(-1, 0), (0, 1), (1, 0), (0, -1)] m, n, count = len(grid), len(grid[0]), 0 for row in range(m): for col in range(n): if grid[row][col] == "1": count += 1 stack = [(row, col)] # Iterative depth-first traversal, mark entire island as visited while stack: r, c = stack.pop() for d in directions: newR, newC = r + d[0], c + d[1] if 0 <= newR < m and 0 <= newC < n and grid[newR][newC] == "1": grid[newR][newC] = "2" stack.append((newR, newC)) return count
Time: O(m * n)
Space: O(m * n)
Alternative solution:
def numIslands(grid: List[List[str]]) -> int: def dfs(row, col): if not 0 <= row < len(grid) or not 0 <= col < len(grid[0]) or grid[row][col] != "1": return grid[row][col] = "2" dfs(row, col - 1) dfs(row - 1, col) dfs(row, col + 1) dfs(row + 1, col) count = 0 for row in range(len(grid)): for col in range(len(grid[0])): if grid[row][col] == "1": count += 1 dfs(row, col) return count
Time: O(m * n)
Space: O(m * n)
Surrounded Regions (Medium)
You are given an m x n matrix board containing letters 'X' and 'O', capture regions that are surrounded:
- Connect: A cell is connected to adjacent cells horizontally or vertically.
- Region: To form a region connect every
'O'cell. - Surround: The region is surrounded with
'X'cells if you can connect the region with'X'cells and none of the region cells are on the edge of theboard.
A surrounded region is captured by replacing all 'O's with 'X's in the input matrix board.
def solve(board: List[List[str]]) -> None: def dfs(row, col): if not 0 <= row < len(board) or not 0 <= col < len(board[0]) or board[row][col] != "O": return board[row][col] = "L" dfs(row, col - 1) dfs(row - 1, col) dfs(row, col + 1) dfs(row + 1, col) # Regions on the border will not be surrounded. Mark them as safe for row in (0, len(board) - 1): for col in range(len(board[0])): if board[row][col] == "O": dfs(row, col) for col in (0, len(board[0]) - 1): for row in range(len(board)): if board[row][col] == "O": dfs(row, col) # Revert marked regions and capture everything else for row in range(len(board)): for col in range(len(board[0])): if board[row][col] == "O": board[row][col] = "X" elif board[row][col] == "L": board[row][col] = "O"
Time: O(m * n)
Space: O(m * n)
Clone Graph (Medium)
Given a reference of a node in a connected undirected graph, return a deep copy (clone) of the graph.
Each node in the graph contains a value (int) and a list (List[Node]) of its neighbors.
class Node: def __init__(self, val = 0, neighbors = None): self.val = val self.neighbors = neighbors if neighbors is not None else []
from collections import deque def cloneGraph(node: Optional['Node']) -> Optional['Node']: if not node: return None # Key: old node; Value: new node visited, d = { node: Node(node.val)}, deque([node]) # Iterative breadth first traversal while d: curr = d.popleft() for neighbor in curr.neighbors: if neighbor not in visited: d.append(neighbor) visited[neighbor] = Node(neighbor.val) visited[curr].neighbors.append(visited[neighbor]) return visited[node]
Time: O(V + E), V = vertices, E = edges
Space: O(V) excluding output
Alternative solution:
def cloneGraph(node: Optional['Node']) -> Optional['Node']: if not node: return None # Key: old node; Value: new node visited = {} # Recursive depth first traversal def dfs(node): if node not in visited: visited[node] = Node(node.val) for neighbor in node.neighbors: visited[node].neighbors.append(dfs(neighbor)) return visited[node] return dfs(node)
Time: O(V + E), V = vertices, E = edges
Space: O(V) excluding output
Evaluate Division (Medium)
You are given an array of variable pairs equations and an array of real numbers values, where equations[i] = [Ai, Bi] and values[i] represent the equation Ai / Bi = values[i]. Each Ai or Bi is a string that represents a single variable.
You are also given some queries, where queries[j] = [Cj, Dj] represents the jth query where you must find the answer for Cj / Dj = ?.
Return the answers to all queries. If a single answer cannot be determined, return -1.0.
Assume the input is always valid. Evaluating the queries will not result in division by zero and there is no contradiction.
Variables that do not occur in the list of equations are undefined, so the answer cannot be determined for them.
def calcEquation(equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]: # Build weighted graph using nested map # { val: { neighbor: weight } } m = {} for i in range(len(equations)): x, y = equations[i] if x not in m: m[x] = { y: values[i] } else: m[x][y] = values[i] # Include reciprocal as weight in opposite direction if y not in m: m[y] = { x: 1 / values[i] } else: m[y][x] = 1 / values[i] # Iterative depth first search def findPath(start, end): if start not in m or end not in m: return -1 # Stack stores (value, product of current path) # If (a / b = x) and (b / c = y) then (a / c = x * y) visited, stack = set(), [(start, 1)] while stack: curr, product = stack.pop() if curr == end: return product visited.add(curr) for neighbor, weight in m[curr].items(): if neighbor not in visited: stack.append((neighbor, weight * product)) return -1 return [findPath(x, y) for x, y in queries]
Time: O(Q * (V + E)), Q = queries, V = vertices, E = edges
Space: O(V + E)
Course Schedule (Medium)
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.
from collections import deque def canFinish(numCourses: int, prerequisites: List[List[int]]) -> bool: # { prereq: [courses with prereq] } neighbors = defaultdict(list) # Track how many prereqs a course has indegree = {i: 0 for i in range(numCourses)} for node, prereq in prerequisites: indegree[node] += 1 neighbors[prereq].append(node) # Breadth first topological sort queue = deque([c for c in range(numCourses) if indegree[c] == 0]) completed = 0 while queue: course = queue.popleft() completed += 1 for neighbor in neighbors[course]: indegree[neighbor] -= 1 if indegree[neighbor] == 0: queue.append(neighbor) return completed == numCourses
Time: O(V + E), V = nodes, E = edges
Space: O(V + E)
Course Schedule II (Medium)
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.
def findOrder(numCourses: int, prerequisites: List[List[int]]) -> List[int]: # Breadth first topological sort neighbors = defaultdict(list) indegree = {i: 0 for i in range(numCourses)} for course, prereq in prerequisites: indegree[course] += 1 neighbors[prereq].append(course) queue = deque([c for c in range(numCourses) if indegree[c] == 0]) result = [] while queue: prereq = queue.popleft() result.append(prereq) for course in neighbors[prereq]: indegree[course] -= 1 if indegree[course] == 0: queue.append(course) return result if len(result) == numCourses else []
Time: O(V + E), V = nodes, E = edges
Space: O(V + E)
Snakes and Ladders (Medium)
You are given an n x n integer matrix board where the cells are labeled from 1 to n^2 in a Boustrophedon style starting from the bottom left of the board (i.e. board[n - 1][0]) and alternating direction each row.
You start on square 1 of the board. In each move, starting from square curr, do the following:
- Choose a destination square
nextwith a label in the range[curr + 1, min(curr + 6, n^2)].- This choice simulates the result of a standard 6-sided die roll: i.e., there are always at most 6 destinations, regardless of the size of the board.
- If
nexthas a snake or ladder, you must move to the destination of that snake or ladder. Otherwise, you move tonext. - The game ends when you reach the square
n^2.
A board square on row r and column c has a snake or ladder if board[r][c] != -1. The destination of that snake or ladder is board[r][c]. Squares 1 and n^2 do not have a snake or ladder.
Note that you only take a snake or ladder at most once per move. If the destination to a snake or ladder is the start of another snake or ladder, you do not follow the subsequent snake or ladder.
Return the least number of moves required to reach the square n^2. If it is not possible to reach the square, return -1.
from collections import deque def snakesAndLadders(board: List[List[int]]) -> int: end = len(board) ** 2 # Calculate coordinates now to avoid repeated work later coords = [None] * (end + 1) for square in range(1, end + 1): i = square - 1 row = len(board) - (i // len(board)) - 1 col = i % len(board) if (len(board) - row - 1) % 2 != 0: col = len(board) - col - 1 coords[square] = (row, col) visited = [False] * (end + 1) visited[1] = True queue = deque([1]) # Track number of rolls i.e. BFS level moves = 0 while queue: for _ in range(len(queue)): curr = queue.popleft() for i in range(1, 7): if curr + i > end: continue row, col = coords[curr + i] newPos = board[row][col] if board[row][col] != -1 else curr + i # First time reaching square is guaranteed minimum moves if newPos == end: return moves + 1 if not visited[newPos]: queue.append(newPos) visited[newPos] = True # Increment after looping because value applies to # nodes in queue before popping moves += 1 return -1
Time: O(n2)
Space: O(n2)
Minimum Genetic Mutation (Medium)
A gene string can be represented by an 8-character long string, with choices from 'A', 'C', 'G', and 'T'.
Suppose we need to investigate a mutation from a gene string startGene to a gene string endGene where one mutation is defined as one single character changed in the gene string.
For example, "AACCGGTT" --> "AACCGGTA" is one mutation.
There is also a gene bank bank that records all the valid gene mutations. A gene must be in bank to make it a valid gene string.
Given the two gene strings startGene and endGene and the gene bank bank, return the minimum number of mutations needed to mutate from startGene to endGene. If there is no such a mutation, return -1.
Note that the starting point is assumed to be valid, so it might not be included in the bank.
from collections import deque def minMutation(startGene: str, endGene: str, bank: List[str]) -> int: bank, visited, choices = set(bank), set(), ["A", "C", "G", "T"] queue = deque([startGene]) mutations = 0 # Breadth first search while queue: for _ in range(len(queue)): curr = queue.popleft() for i in range(8): for choice in choices: newGene = curr[:i] + choice + curr[i + 1:] if newGene not in bank: continue if newGene == endGene: return mutations + 1 if newGene not in visited: queue.append(newGene) visited.add(newGene) mutations += 1 return -1
Time: O(N * L2) or O(N), N = bank size, L = string length = 8
Space: O(NL) or O(N)
Implement Trie (Prefix Tree) (Medium)
A trie (pronounced as "try") or prefix tree is a tree data structure used to efficiently store and retrieve keys in a dataset of strings. There are various applications of this data structure, such as autocomplete and spellchecker.
Implement the Trie class:
Trie()Initializes the trie object.void insert(String word)Inserts the stringwordinto the trie.boolean search(String word)Returnstrueif the stringwordis in the trie (i.e., was inserted before), andfalseotherwise.boolean startsWith(String prefix)Returnstrueif there is a previously inserted stringwordthat has the prefixprefix, andfalseotherwise.
class Trie: def __init__(self): # Simulate graph nodes with nested maps. Use "#" to end word # "apple" -> {a: {p: {p: {l: {e: {"#": True}}}}}} self.head = {} def insert(self, word: str) -> None: node = self.head for char in word: if char not in node: node[char] = {} node = node[char] node["#"] = True def search(self, word: str) -> bool: node = self.head for char in word: if char not in node: return False node = node[char] return "#" in node def startsWith(self, prefix: str) -> bool: node = self.head for char in prefix: if char not in node: return False node = node[char] return True
Design Add and Search Words Data Structure (Medium)
Design a data structure that supports adding new words and finding if a string matches any previously added string.
Implement the WordDictionary class:
WordDictionary()Initializes the object.void addWord(word)Addswordto the data structure, it can be matched later.bool search(word)Returnstrueif there is any string in the data structure that matcheswordorfalseotherwise.wordmay contain dots'.'where dots can be matched with any letter.
class WordDictionary: def __init__(self): # Simulate graph nodes with nested maps. Use "#" to end word # "apple" -> {a: {p: {p: {l: {e: {"#": True}}}}}} self.head = {} def addWord(self, word: str) -> None: node = self.head for char in word: if char not in node: node[char] = {} node = node[char] node["#"] = {} def search(self, word: str) -> bool: def dfs(i, charMap): if i >= len(word): return "#" in charMap char = word[i] if char != ".": if char not in charMap: return False return dfs(i + 1, charMap[char]) else: for key in charMap: if dfs(i + 1, charMap[key]): return True return False return dfs(0, self.head)