Introduction
Every serious software engineer eventually confronts a class of problems that feels unlike anything else: navigating a maze, finding connected regions in a grid, scheduling tasks without circular dependencies, generating all valid configurations of a puzzle. These problems share a common skeleton - they require exploring a space of possibilities in a structured, exhaustive way. Depth-First Search (DFS) is the algorithm at the heart of all of them.
DFS is deceptively simple at its surface: go as deep as you can down one path, then backtrack and try another. But this simple idea unfolds into one of the richest algorithmic patterns in computer science. It underlies topological sorting, cycle detection, strongly connected components, backtracking combinatorics, and even certain dynamic programming optimizations. If BFS is about exploring layer by layer, DFS is about commitment - it dives into a single direction with full conviction and only reconsiders when forced to.
This guide is structured as a progressive curriculum. We begin with the raw mechanics of DFS, build mental models for trees and graphs, then move through increasingly sophisticated applications - backtracking, directed graph analysis, and advanced hybrid techniques involving memoization and bitmask state. Every level includes working code in Python and TypeScript, annotated to emphasize engineering reasoning, not just syntax.
Whether you are preparing for a system design interview, deepening your algorithms knowledge, or just trying to become the kind of engineer who can derive solutions rather than memorize them, this guide is for you.
Why DFS Is Worth Mastering Deeply
It is tempting to treat DFS as a rote tool - something you learn once, recognize in interview problems, and move on from. This would be a mistake. The engineers who truly internalize DFS develop a kind of algorithmic intuition that makes an entire class of hard problems tractable on sight.
Consider the scope of what DFS touches. Tree traversal (pre-order, in-order, post-order) is DFS. The flood-fill algorithm behind the paint bucket tool in image editors is DFS. Dependency resolution in build systems like Gradle or npm - when you need to install packages in the right order - relies on topological sorting, which is implemented with DFS. Tarjan's algorithm for finding strongly connected components, used in compilers for control flow analysis, is DFS with a stack and a discovery timer. Even certain decision procedures in constraint satisfaction systems reduce to DFS with pruning.
The reason DFS appears in so many places is structural. Most problems that require exploring a state space - where each decision leads to a new set of decisions - naturally map onto a tree or graph. DFS is the canonical tool for traversing such structures. Understanding it deeply means understanding recursion, the call stack, state management, and backtracking. These are not peripheral skills. They are foundational to the way professional engineers think about recursive systems, from parser combinators to game tree search.
There is also a practical argument. DFS problems are heavily represented in technical interviews at companies ranging from early-stage startups to the largest technology firms. Problems tagged "DFS" on LeetCode span from medium to hard difficulty and frequently appear in final interview rounds. Mastering DFS is one of the highest-return investments a software engineer can make in terms of interview preparation.
Level 1 - The Core Mechanics
Recursive and Iterative Implementations
DFS has two canonical implementations, and understanding both is important. The recursive version is elegant and closely mirrors the mathematical definition of a depth-first traversal, but it is limited by the call stack depth. On most systems, Python's default recursion limit is 1,000 frames; for very deep graphs, this will raise a RecursionError. The iterative version uses an explicit stack data structure and avoids this limitation entirely.
Here is the recursive form on a graph represented as an adjacency list:
def dfs(graph: dict[str, list[str]], node: str, visited: set | None = None) -> set:
if visited is None:
visited = set()
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs(graph, neighbor, visited)
return visited
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [], 'E': [], 'F': []
}
dfs(graph, 'A') # Visits: A, B, D, E, C, F
Notice the default argument pattern visited=None. This is a Python best practice - using a mutable default argument like visited=set() directly would cause the set to be shared across all calls, producing incorrect results. Always initialize mutable defaults inside the function body.
The iterative version in TypeScript makes the stack explicit and handles the visited check at pop time rather than push time, which is a subtle but important distinction:
function dfs(graph: Record<string, string[]>, start: string): string[] {
const visited = new Set<string>();
const stack: string[] = [start];
const order: string[] = [];
while (stack.length > 0) {
const node = stack.pop()!;
if (visited.has(node)) continue; // skip if already processed
visited.add(node);
order.push(node);
// push neighbors in reverse order to preserve left-to-right traversal
for (const neighbor of [...graph[node]].reverse()) {
if (!visited.has(neighbor)) stack.push(neighbor);
}
}
return order;
}
One important note about the iterative form: because a stack is LIFO (last in, first out), pushing neighbors in their natural order will traverse them in reverse. If traversal order matters - for example, when reconstructing paths - push neighbors in reverse to match the recursive behavior.
Exercises to Build the Foundation
Before moving on, these three exercises will solidify the basics. The goal is not to just get them working, but to be able to write them fluently from memory.
- Binary tree traversal. Implement pre-order (node -> left -> right), in-order (left -> node -> right), and post-order (left -> right -> node) DFS on a binary tree, both recursively and iteratively.
- Node count. Given a tree, count the total number of nodes using DFS.
- Path existence. Given an undirected graph, determine whether a path exists between two given nodes.
Level 2 - Trees and Classic Graph Problems
Computing Properties Over Subtrees
Once you understand the traversal mechanics, the next step is using DFS to compute things during the traversal - not just visit nodes. The key insight is that in a post-order traversal, by the time you process a node, you already have the results from both its subtrees. This makes DFS a natural fit for any problem where a node's answer depends on its children.
Two canonical examples are tree depth and path sum:
class TreeNode:
def __init__(self, val: int = 0, left: 'TreeNode | None' = None, right: 'TreeNode | None' = None):
self.val = val
self.left = left
self.right = right
def max_depth(root: TreeNode | None) -> int:
if root is None:
return 0
left_depth = max_depth(root.left)
right_depth = max_depth(root.right)
return 1 + max(left_depth, right_depth)
def has_path_sum(root: TreeNode | None, target: int) -> bool:
if root is None:
return False
# At a leaf, check if the remaining target equals the leaf's value
if root.left is None and root.right is None:
return root.val == target
remaining = target - root.val
return has_path_sum(root.left, remaining) or has_path_sum(root.right, remaining)
The has_path_sum function demonstrates a pattern that recurs constantly in DFS: carry state downward by modifying a parameter on each recursive call. Here, we reduce the target by the current node's value as we descend. This eliminates the need for a running accumulator or any global state.
Grid DFS and the Island Problem
The "Number of Islands" problem (LeetCode #200) is one of the most important exercises in the entire DFS curriculum. It generalizes to dozens of variants and is the prototypical example of DFS on a 2D grid. The grid is an implicit graph: each cell is a node, and its neighbors are the four adjacent cells.
def num_islands(grid: list[list[str]]) -> int:
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
count = 0
def dfs(r: int, c: int) -> None:
# Boundary check and water/visited check
if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] != '1':
return
grid[r][c] = '#' # Mark as visited in-place
dfs(r + 1, c)
dfs(r - 1, c)
dfs(r, c + 1)
dfs(r, c - 1)
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
dfs(r, c)
count += 1
return count
This solution uses an in-place mutation strategy - replacing '1' with '#' - to avoid maintaining a separate visited set. This is a common optimization in grid DFS problems. Note, however, that it mutates the input; if you need to preserve the original grid, make a deep copy first or maintain an explicit visited set.
The exercises at this level include finding the lowest common ancestor of two nodes in a binary tree, checking whether a tree is symmetric, and finding all root-to-leaf paths. Each of these reinforces a distinct pattern: LCA involves returning information from both subtrees and comparing; symmetry requires mirroring traversal; path collection requires maintaining a mutable list and snapshotting it at leaves.
Level 3 - Backtracking: DFS with State Undo
The Core Pattern
Backtracking is the most powerful application of DFS for combinatorial problems. It extends the basic DFS loop with one critical step: after exploring a branch, undo the change you made before trying the next branch. This allows you to reuse a single mutable data structure across the entire search tree, which is far more efficient than creating a new copy of state at each level.
The canonical template is:
choose -> explore -> un-choose
Here is the pattern applied to generating all subsets of a list:
def subsets(nums: list[int]) -> list[list[int]]:
result: list[list[int]] = []
def dfs(index: int, current: list[int]) -> None:
# Snapshot the current state - every intermediate state is valid
result.append(list(current))
for i in range(index, len(nums)):
current.append(nums[i]) # choose
dfs(i + 1, current) # explore
current.pop() # un-choose (backtrack)
dfs(0, [])
return result
The list(current) snapshot is critical. Without it, all entries in result would point to the same list object, which will be empty by the time the function returns. Always snapshot mutable state when you append it to your results.
N-Queens: Backtracking with Validation
The N-Queens problem is the textbook backtracking problem, and implementing it from scratch is one of the best exercises you can do. It requires placing N queens on an N*N chessboard such that no two queens attack each other. The solution space is enormous but pruning via isValid eliminates most branches early:
function solveNQueens(n: number): string[][] {
const results: string[][] = [];
const board: string[][] = Array.from({ length: n }, () => Array(n).fill('.'));
function isValid(row: number, col: number): boolean {
// Check column above
for (let r = 0; r < row; r++) {
if (board[r][col] === 'Q') return false;
}
// Check upper-left diagonal
for (let r = row - 1, c = col - 1; r >= 0 && c >= 0; r--, c--) {
if (board[r][c] === 'Q') return false;
}
// Check upper-right diagonal
for (let r = row - 1, c = col + 1; r >= 0 && c < n; r--, c++) {
if (board[r][c] === 'Q') return false;
}
return true;
}
function dfs(row: number): void {
if (row === n) {
results.push(board.map(r => r.join('')));
return;
}
for (let col = 0; col < n; col++) {
if (!isValid(row, col)) continue;
board[row][col] = 'Q'; // choose
dfs(row + 1); // explore
board[row][col] = '.'; // un-choose
}
}
dfs(0);
return results;
}
Notice that we only check above the current row in isValid - not below - because we place queens row by row and the rows below are still empty. This is a subtle but important optimization that eliminates redundant checks.
The exercises at this level - valid parentheses, Sudoku solver, word search on a grid, and combination sum - each stress a different aspect of backtracking: early termination, constraint propagation, 2D state management, and unbounded selection respectively. Solving all four will give you a complete fluency with the pattern.
Level 4 - Directed Graphs: Cycles, Ordering, and Components
Cycle Detection with Three-Color Marking
When DFS runs on a directed graph, the structure of the traversal reveals deep properties of the graph. The most important of these for practical engineering - dependency resolution, deadlock detection, build system validation - is cycle detection. A directed graph has a cycle if and only if there exists a back edge in the DFS tree: an edge that points from a node currently on the recursion stack back to an ancestor.
The three-color marking scheme (WHITE / GRAY / BLACK) elegantly captures this:
def has_cycle(graph: dict[str, list[str]]) -> bool:
WHITE, GRAY, BLACK = 0, 1, 2
color: dict[str, int] = {node: WHITE for node in graph}
def dfs(node: str) -> bool:
color[node] = GRAY # mark as "currently being explored"
for neighbor in graph[node]:
if color[neighbor] == GRAY:
return True # back edge found - cycle exists
if color[neighbor] == WHITE and dfs(neighbor):
return True
color[node] = BLACK # mark as "fully explored"
return False
return any(dfs(node) for node in graph if color[node] == WHITE)
WHITE means unvisited, GRAY means currently on the active recursion path (in-stack), and BLACK means fully processed. Encountering a GRAY node during traversal is the definitive signal of a cycle: you have found an edge leading back into your own call stack.
Topological Sorting
Topological sort produces a linear ordering of nodes in a directed acyclic graph (DAG) such that for every directed edge u -> v, node u appears before node v in the ordering. This is the foundation of dependency resolution in package managers and build systems. The DFS-based approach derives the order from post-order traversal: a node is appended to the result only after all its descendants have been fully explored. Reversing this list gives a valid topological order.
def topo_sort(graph: dict[str, list[str]]) -> list[str]:
visited: set[str] = set()
order: list[str] = []
def dfs(node: str) -> None:
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs(neighbor)
order.append(node) # post-order append
for node in graph:
if node not in visited:
dfs(node)
return order[::-1] # reverse for topological order
The reason post-order gives a reverse topological sort is intuitive: a node is only appended after all nodes it depends on have already been appended. Reversing places dependencies before dependents.
The exercises here - LeetCode's Course Schedule series (#207, #210), finding connected components, and Tarjan's strongly connected components - represent the most practically applicable graph algorithms in the DFS family. The Course Schedule problems in particular are frequently asked in interviews and map directly to real engineering problems like detecting circular imports or circular service dependencies in a microservices graph.
Level 5 - Advanced DFS: Memoization, Implicit Graphs, and Bitmask State
DFS with Memoization
When a DFS traversal visits the same subproblem multiple times - the same node with the same remaining state - pure recursion leads to exponential redundancy. Memoization (caching results by state) transforms such problems into polynomial time. The canonical example is the longest increasing path in a matrix (LeetCode #329):
from functools import lru_cache
def longest_increasing_path(matrix: list[list[int]]) -> int:
if not matrix:
return 0
rows, cols = len(matrix), len(matrix[0])
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
@lru_cache(maxsize=None)
def dfs(r: int, c: int) -> int:
best = 1
for dr, dc in directions:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] > matrix[r][c]:
best = max(best, 1 + dfs(nr, nc))
return best
return max(dfs(r, c) for r in range(rows) for c in range(cols))
Using @lru_cache on the DFS function is idiomatic in Python for this pattern. The key insight is that the result for cell (r, c) is deterministic - it does not depend on how we arrived at (r, c), only on the local values. This makes memoization safe. If the result depended on the path taken (e.g., the set of visited nodes), we could not memoize without encoding that set in the cache key.
DFS on Implicit Graphs
Some graphs are never explicitly constructed. Instead, the nodes are states and the edges are valid transitions between states. Word Ladder is the classic example: each word is a node, and two words are connected if they differ by exactly one character. The graph has potentially enormous size, but DFS (or BFS - preferably BFS for shortest path) explores it lazily.
from functools import lru_cache
def ladder_length(begin_word: str, end_word: str, word_list: list[str]) -> int:
word_set = frozenset(word_list)
# Note: for shortest path, BFS is strictly better. This is DFS + memo for pedagogical purposes.
@lru_cache(maxsize=None)
def dfs(word: str, visited: frozenset) -> int:
if word == end_word:
return 0
best = float('inf')
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
candidate = word[:i] + c + word[i+1:]
if candidate in word_set and candidate not in visited:
result = dfs(candidate, visited | {candidate})
if result != float('inf'):
best = min(best, 1 + result)
return best
if end_word not in word_set:
return 0
result = dfs(begin_word, frozenset({begin_word}))
return result + 1 if result != float('inf') else 0
Note the important caveat in the code comment: for the shortest path variant of Word Ladder (finding the minimum number of steps), BFS guarantees optimality and is the correct algorithm. DFS explores paths in depth-first order and may not find the shortest path without exploring the entire space. The version above uses DFS with memoization purely for illustrative purposes. Choosing the right traversal strategy - DFS for exhaustive exploration, BFS for shortest unweighted path - is a key engineering decision.
Bitmask DFS for State Compression
In problems where you need to track which elements have been visited as part of the state, a bitmask is a compact and efficient representation. This technique appears in problems like the Traveling Salesman Problem (TSP) and "shortest path visiting all nodes" (LeetCode #847).
from functools import lru_cache
def shortest_path_length(graph: list[list[int]]) -> int:
n = len(graph)
full_mask = (1 << n) - 1 # all nodes visited
@lru_cache(maxsize=None)
def dfs(node: int, visited_mask: int) -> int:
if visited_mask == full_mask:
return 0
best = float('inf')
for neighbor in graph[node]:
new_mask = visited_mask | (1 << neighbor)
if new_mask != visited_mask: # only recurse if state changes
best = min(best, 1 + dfs(neighbor, new_mask))
return best
# Try starting from each node
return min(dfs(start, 1 << start) for start in range(n))
Here the cache key is (node, visited_mask) - the current position and the set of all previously visited nodes encoded as an integer. This is the defining feature of bitmask DP: a set of N boolean values is compressed into a single N-bit integer, making it a valid dictionary key and enabling efficient bitwise operations.
Common Pitfalls and Trade-offs
Recursion Depth and Stack Overflow
The most practical pitfall with recursive DFS is hitting Python's recursion limit on large inputs. A binary tree with 10,000 nodes can trigger a RecursionError if it is highly unbalanced (essentially a linked list). The fix is either to increase the limit with sys.setrecursionlimit() - use this cautiously and only when you understand the maximum depth - or to convert to an iterative implementation using an explicit stack.
In TypeScript and JavaScript, the call stack limit varies by engine and environment but is typically between 10,000 and 15,000 frames. For competitive programming contexts, this is usually not an issue. For production systems handling arbitrarily deep inputs, always prefer iterative DFS or tail-recursive patterns where available.
Forgetting to Snapshot Mutable State in Backtracking
This is the single most common backtracking bug: appending a reference to a mutable list rather than a copy of it. By the time the recursion completes, all appended references will point to the same (now-empty) list. The fix is always result.append(list(current)) in Python or results.push([...current]) in TypeScript - creating a shallow copy at the moment of collection.
DFS vs. BFS: Choosing the Right Tool
DFS and BFS are not interchangeable. DFS is ideal for: exhaustive exploration (finding all solutions, cycle detection, topological sort, connected components), problems where the solution is deep in the search tree, and memory-constrained scenarios (DFS only keeps a single path in memory at a time). BFS is ideal for: finding the shortest path in an unweighted graph, level-order traversal, and problems where the solution is close to the root. Using DFS when BFS is required - for example, finding the minimum word ladder length - will either produce incorrect results or require exploring the entire search space unnecessarily.
Mutating Input vs. Separate Visited Set
In grid DFS, it is tempting to mutate the input grid as a visited marker. This is efficient (O(1) extra space) but has two costs: it modifies the caller's data, and it makes the code harder to reason about. For interview settings, discuss the trade-off explicitly. For production code, prefer an explicit visited set unless memory is genuinely constrained.
Best Practices for Engineering-Quality DFS Code
Writing DFS that is correct in one test case is easy. Writing DFS that is correct, readable, maintainable, and efficient requires deliberate practice. These principles will elevate your implementations from "it works" to "it's well-engineered."
Separate the recursion from the setup. Use a public outer function to handle initialization (building the visited set, validating inputs, handling edge cases) and a private inner dfs function or helper that handles the recursive logic. This separation makes code easier to test and read. Python's nested function pattern and TypeScript's closure pattern both support this cleanly.
Be explicit about what you are tracking and why. Every piece of state in a DFS - the visited set, the current path, the color map - should have a clear purpose. If you cannot articulate why a variable exists, you probably do not need it, or you do not yet fully understand the problem.
Name your states. In cycle detection, using named constants WHITE = 0, GRAY = 1, BLACK = 2 is vastly more readable than bare integers. In bitmask DFS, add a comment explaining what the full mask represents. Algorithmic code has a reputation for being inscrutable; clear naming is how you fight that reputation.
Validate your base cases first. Every DFS function should start with its termination conditions: what causes the recursion to stop? Make these explicit, clear, and exhaustive. Missing a base case is a common source of infinite recursion bugs.
Test with small, hand-traceable inputs. Before running large inputs, trace your DFS manually on a 3-4 node graph or a 3*3 grid. Draw the recursion tree on paper. This habit catches more bugs than any debugger.
Know your time and space complexity. For a graph with V vertices and E edges, DFS runs in O(V + E) time. For backtracking problems, the complexity depends on the size of the search space - O(2^N) for subsets, O(N!) for permutations. Understanding these numbers tells you whether your solution will scale.
80/20 Insight: The Concepts That Unlock Everything Else
If you master only a handful of ideas from this guide, these five will give you the most leverage across the widest range of problems.
1. Post-order is for computing. Whenever a node's answer depends on its children - depth, size, balance, LCA - use post-order DFS. Process children first, then compute the parent's result. This single pattern solves a large fraction of tree problems.
2. Backtracking = choose -> explore -> un-choose. Internalize this three-step loop and you can solve any combinatorial enumeration problem: subsets, permutations, combinations, Sudoku, N-Queens, word search. The variation is always in the constraint check and the snapshot logic.
3. GRAY node = cycle. In directed graph DFS, a GRAY node is one currently on the recursion stack. Reaching a GRAY node means you have found a back edge, which means a cycle exists. This insight is the foundation of cycle detection and, by extension, topological sort (which requires a DAG).
4. Post-order append + reverse = topological sort. A single DFS pass with post-order collection, reversed, gives a valid topological ordering of a DAG. This is simpler and more elegant than maintaining in-degrees (Kahn's algorithm), though both are O(V + E).
5. State = (position, mask/path). For advanced DFS + memoization problems, the key is identifying what constitutes the complete state of a subproblem. Usually it is the current position plus some compact representation of history (a frozenset, a bitmask, a tuple). If two recursive calls share the same state, their results are identical - memoize them.
The 5-Week Study Roadmap
This roadmap is designed for engineers who can commit three to five focused hours per week. The goal is progressive mastery, not speed.
| Week | Focus | Core LeetCode Problems |
|---|---|---|
| 1 | Tree DFS basics | #104 (Max Depth), #112 (Path Sum), #257 (Root-to-Leaf Paths), #543 (Diameter) |
| 2 | Grid DFS / islands | #200 (Number of Islands), #130 (Surrounded Regions), #417 (Pacific Atlantic), #695 (Max Area of Island) |
| 3 | Backtracking | #46 (Permutations), #78 (Subsets), #79 (Word Search), #51 (N-Queens), #37 (Sudoku Solver) |
| 4 | Graph DFS (directed) | #207 (Course Schedule), #210 (Course Schedule II), #802 (Find Eventual Safe States) |
| 5 | Advanced / hard | #332 (Reconstruct Itinerary), #685 (Redundant Connection II), #1192 (Critical Connections), #329 (Longest Increasing Path) |
For each problem, aim for three iterations: write a brute force solution, then optimize it, then write it again from memory two days later. The third pass - writing from memory - is where real retention happens.
Analogies and Mental Models
DFS is like exploring a cave system. You pick a tunnel and walk as far as you can, leaving chalk marks on the walls as you go. When you hit a dead end, you backtrack to the last junction and try a different tunnel. The chalk marks are your visited set. The junction is where you undo your last choice in backtracking.
Backtracking is like playing chess. You make a move (choose), think through what follows (explore), then pick up the piece and try somewhere else (un-choose). The board is always in a consistent state. You never duplicate the board - you just move pieces.
GRAY nodes are "currently on your plate." WHITE is untouched food, GRAY is food you've started eating, BLACK is a clean plate. If someone tries to serve you food you've already started eating (GRAY), that's a cycle - the kitchen is sending food back around.
Post-order is like washing dishes. You only wash a dish (process a node) after everyone at the table (all children) has finished eating. You work from the leaves inward.
Conclusion
Depth-First Search is not a single algorithm so much as a pattern - a way of thinking about exploration, choice, and commitment that appears across an enormous range of computational problems. From the simple mechanics of tree traversal to the sophisticated state compression of bitmask memoization, every concept in this guide is a variation on the same underlying idea: go deep, track your state, and backtrack when you must.
The engineers who are best at DFS problems are not the ones who have memorized the most solutions. They are the ones who have internalized the patterns deeply enough to derive solutions. They see a new problem, recognize the structure - a tree, a grid, a DAG, a combinatorial space - and know which DFS pattern to reach for. That fluency comes only from deliberate practice: solving problems, tracing through execution by hand, making mistakes, and building the intuition that no amount of reading can substitute for.
Use the roadmap in this guide, work through the exercises at each level, and resist the urge to look at solutions before genuinely attempting each problem. The friction is the point. Every time you work through why your base case was wrong, or why your backtracking forgot to un-choose, you are building the kind of deep understanding that makes hard problems feel approachable.
DFS is one of the highest-leverage skills in algorithmic problem solving. Master it, and a large portion of the hard problem landscape opens up to you.
Key Takeaways
- Learn both recursive and iterative DFS. The recursive form is more readable; the iterative form is more robust for deep inputs. Know when to use each.
- Backtracking always follows choose -> explore -> un-choose. Master this template and you can solve any combinatorial enumeration problem.
- Use three-color marking (WHITE/GRAY/BLACK) for cycle detection in directed graphs. A GRAY node encountered during traversal is a cycle.
- Post-order DFS reversed gives topological sort. This is the most elegant implementation of topo sort and is worth knowing cold.
- Memoize on (position, state). For advanced DFS problems, identify the complete state of each subproblem, encode it compactly (frozenset, bitmask, tuple), and cache results. This transforms exponential time complexity into polynomial.
References
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms (3rd ed.). MIT Press. - Chapters 22-24 cover graph algorithms including DFS, topological sort, and SCC.
- Skiena, S. S. (2008). The Algorithm Design Manual (2nd ed.). Springer. - Chapter 7 covers DFS and backtracking with extensive practical commentary.
- Sedgewick, R., & Wayne, K. (2011). Algorithms (4th ed.). Addison-Wesley. - Part 4 covers graph processing, including DFS-based algorithms.
- LeetCode. (2024). Problems tagged "Depth-First Search." https://leetcode.com/tag/depth-first-search/
- Python Software Foundation. (2024). sys.setrecursionlimit documentation. https://docs.python.org/3/library/sys.html#sys.setrecursionlimit
- Python Software Foundation. (2024). functools.lru_cache documentation. https://docs.python.org/3/library/functools.html#functools.lru_cache
- Tarjan, R. E. (1972). Depth-first search and linear graph algorithms. SIAM Journal on Computing, 1(2), 146-160. - The original paper introducing DFS-based SCC and articulation point algorithms.
- TypeScript Handbook. (2024). TypeScript language reference. https://www.typescriptlang.org/docs/