Buổi 04 — Recursion & Backtracking
Navigation
← 03-Stack-and-Queue | → 05-Binary-Search Giai đoạn: Big Tech Foundation | Độ khó: ⭐⭐⭐
1. Analogy & Context
Mental Model — Recursion
Recursion = búp bê matryoshka (búp bê lồng Nga). Mỗi búp bê chứa một búp bê nhỏ hơn bên trong, cho đến khi bạn chạm đến búp bê đặc nhất — không thể mở ra nữa. Đó là base case. Khi bạn giải quyết búp bê nhỏ nhất, bạn lắp lại từng chiếc vào nhau — đó là quá trình unwinding call stack.
Mental Model — Backtracking
Backtracking = giải mê cung. Bạn đi theo một con đường, nếu gặp ngõ cụt (invalid state), bạn quay trở lại (backtrack) điểm rẽ gần nhất và thử hướng khác. Bạn không đi ngẫu nhiên — bạn có hệ thống: thử hết mọi nhánh, prune (cắt) những nhánh chắc chắn không thể dẫn đến đích.
Mối quan hệ Recursion → Backtracking → DP:
Recursion (cơ bản)
↓ + "undo state sau mỗi recursive call"
Backtracking (enumerate all valid configurations)
↓ + memoization (cache subproblem results)
Top-down Dynamic Programming
Tại sao Backtracking là nền tảng của DP?
- Backtracking generates all valid configurations — interviewer dùng để test “can you enumerate all possibilities systematically?”
- Khi bạn thêm memoization vào backtracking, bạn có top-down DP.
- N-Queens, Sudoku Solver, Word Search — đây là những bài kinh điển FAANG.
2. The FAANG Standard
Backtracking ở FAANG Interviews
- Xuất hiện trong ~15% Hard problems và ~20% Medium problems.
- Interviewer kiểm tra: bạn có biết khi nào prune không? Pruning là dấu hiệu của kỹ sư có kinh nghiệm.
- N-Queens là bài FAANG hỏi để kiểm tra tư duy combinatorial toàn diện.
- Vẽ recursion tree khi giải thích = cách bạn communicate solution trong phỏng vấn.
Công thức chung của Backtracking:
function backtrack(state, choices):
if is_complete(state):
record(state) → add to results
return
for each choice in valid_choices(state):
make_choice(state, choice) ← Thay đổi state
backtrack(new_state)
undo_choice(state, choice) ← PHẢI undo! Đây là "backtrack"
Điểm khác biệt Recursion vs Backtracking
- Recursion thuần: không có “undo”. Bạn chỉ tính và trả kết quả lên.
- Backtracking: bắt buộc có “undo” sau mỗi đệ quy. State phải được restore về trước khi thử choice đó. Đây là lý do gọi là “backtrack”.
3. Visual Thinking
Recursion Tree cho fib(4)
graph TD F4["fib(4)"] --> F3L["fib(3)"] F4 --> F2A["fib(2)"] F3L --> F2B["fib(2)"] F3L --> F1A["fib(1)=1"] F2A --> F1B["fib(1)=1"] F2A --> F0A["fib(0)=0"] F2B --> F1C["fib(1)=1"] F2B --> F0B["fib(0)=0"] style F4 fill:#4A90D9,color:#fff style F2A fill:#E74C3C,color:#fff style F2B fill:#E74C3C,color:#fff
Overlapping Subproblems
fib(2)được tính 2 lần (màu đỏ).fib(1)vàfib(0)còn bị tính nhiều lần hơn. Đây là lý do naive recursion là — exponential. Memoization cache các kết quả này, cắt xuống .
Backtracking Decision Tree — permutations([1,2,3])
graph TD ROOT["[]"] --> A1["[1]"] ROOT --> B1["[2]"] ROOT --> C1["[3]"] A1 --> A12["[1,2]"] A1 --> A13["[1,3]"] B1 --> B21["[2,1]"] B1 --> B23["[2,3]"] C1 --> C31["[3,1]"] C1 --> C32["[3,2]"] A12 --> R123["[1,2,3] RESULT"] A13 --> R132["[1,3,2] RESULT"] B21 --> R213["[2,1,3] RESULT"] B23 --> R231["[2,3,1] RESULT"] C31 --> R312["[3,1,2] RESULT"] C32 --> R321["[3,2,1] RESULT"] style R123 fill:#27AE60,color:#fff style R132 fill:#27AE60,color:#fff style R213 fill:#27AE60,color:#fff style R231 fill:#27AE60,color:#fff style R312 fill:#27AE60,color:#fff style R321 fill:#27AE60,color:#fff
Pruning — N-Queens (n=4): Cắt Bớt Nhánh Vô Hiệu
graph TD ROOT["Row 0: đặt Queen tại cột nào?"] ROOT --> C0["Col 0 — thử"] ROOT --> C1["Col 1 — thử"] ROOT --> C2["Col 2 — PRUNED"] ROOT --> C3["Col 3 — PRUNED"] C0 --> R1C2["Row 1: Col 2 — thử"] C0 --> R1X["Row 1: Col 0,1,3 — bị tấn công"] R1C2 --> R2X["Row 2: TẤT CẢ bị tấn công"] C1 --> R1C3["Row 1: Col 3 — thử"] R1C3 --> R2C0["Row 2: Col 0 — thử"] R2C0 --> R3C2["Row 3: Col 2 → VALID: [1,3,0,2]"] style C2 fill:#CCCCCC,color:#666 style C3 fill:#CCCCCC,color:#666 style R1X fill:#FFB3B3,color:#666 style R2X fill:#FFB3B3,color:#666 style R3C2 fill:#27AE60,color:#fff
4. TypeScript Template
4.1 — Fibonacci: Naive O(2^N) → Memoized O(N) → Bottom-up O(1) space
/**
* Fibonacci — 3 approaches, từ naive đến optimal
* Minh họa quá trình tối ưu hóa recursion.
*/
// NAIVE — O(2^N) time, O(N) space (call stack depth)
// Tính lại subproblems nhiều lần → exponential blowup
function fibNaive(n: number): number {
if (n <= 1) return n; // Base case
return fibNaive(n - 1) + fibNaive(n - 2);
// fib(50) với approach này: ~2^50 = 10^15 calls → timeout
}
// MEMOIZED TOP-DOWN — O(N) time, O(N) space
// Cache kết quả đã tính → mỗi subproblem chỉ tính 1 lần
function fibMemo(n: number, memo: Map<number, number> = new Map()): number {
if (n <= 1) return n;
// Cache hit: đã tính rồi, trả ngay mà không tính lại
if (memo.has(n)) return memo.get(n)!;
// Cache miss: tính và lưu vào cache trước khi return
const result = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
memo.set(n, result);
return result;
}
// BOTTOM-UP DP — O(N) time, O(1) space (tối ưu nhất)
// Không cần recursion hay cache lớn — chỉ lưu 2 giá trị
function fibDP(n: number): number {
if (n <= 1) return n;
let prev2 = 0, prev1 = 1;
for (let i = 2; i <= n; i++) {
const curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
/*
* TRANSFORMATION PATTERN (áp dụng cho MỌI overlapping-subproblem recursion):
* 1. Nhận ra: subproblems bị tính lại nhiều lần trong recursion tree.
* 2. Thêm memo map làm parameter (hoặc closure).
* 3. Check cache trước khi tính.
* 4. Lưu kết quả vào cache trước khi return.
* → O(2^N) → O(N) chỉ với 4 dòng code thêm vào.
*/4.2 — Generate All Permutations
/**
* LeetCode #46 — Permutations
*
* APPROACH: Backtracking với used[] array
* - used[i] = true: nums[i] đã được chọn vào path hiện tại
* - Tại mỗi bước: thử mọi nums[i] chưa được chọn
* - Khi path.length === nums.length: lưu kết quả
* - SAU đệ quy: PHẢI undo — path.pop(), used[i] = false
*
* Time: O(N! * N) — N! permutations, copy mỗi cái O(N)
* Space: O(N) — call stack depth + used array
*
* @param nums - Array số nguyên phân biệt
* @returns Tất cả N! permutations
*/
function permute(nums: number[]): number[][] {
const result: number[][] = [];
const path: number[] = [];
const used: boolean[] = new Array(nums.length).fill(false);
function backtrack(): void {
// Base case: path chứa đủ N số → một permutation hoàn chỉnh
if (path.length === nums.length) {
result.push([...path]); // Clone! Không push reference
return;
}
for (let i = 0; i < nums.length; i++) {
if (used[i]) continue; // Bỏ qua số đã dùng trong path hiện tại
// CHOOSE: thêm nums[i] vào path
used[i] = true;
path.push(nums[i]);
// EXPLORE: tiếp tục với số chưa dùng
backtrack();
// UNCHOOSE (backtrack!): restore state
path.pop();
used[i] = false;
}
}
backtrack();
return result;
}4.3 — Subsets (Power Set) — Include/Exclude Pattern
/**
* LeetCode #78 — Subsets
*
* PATTERN: Include/Exclude — tại mỗi index, có 2 lựa chọn:
* - INCLUDE nums[startIndex] vào current subset
* - EXCLUDE nums[startIndex] (chuyển qua startIndex+1)
*
* Đây là pattern cơ bản nhất của backtracking.
* Mọi node của recursion tree là một valid subset.
*
* Time: O(2^N * N) — 2^N subsets, mỗi cái copy O(N)
* Space: O(N) — call stack depth tối đa N
*
* @example
* subsets([1,2,3]) → [[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]]
*/
function subsets(nums: number[]): number[][] {
const result: number[][] = [];
const current: number[] = [];
function backtrack(startIndex: number): void {
// Mỗi state là một valid subset — thêm vào result ngay (không cần base case riêng)
result.push([...current]);
for (let i = startIndex; i < nums.length; i++) {
current.push(nums[i]); // Include nums[i]
backtrack(i + 1); // Chỉ xét elements SAU i (tránh duplicate subsets)
current.pop(); // Exclude nums[i] (backtrack)
}
}
backtrack(0);
return result;
}
/*
* Recursion trace cho [1,2,3]:
* backtrack(0): result=[[]], current=[]
* push(1): current=[1]
* backtrack(1): result=[[],[1]], current=[1]
* push(2): current=[1,2]
* backtrack(2): result=[[],[1],[1,2]], current=[1,2]
* push(3): backtrack(3): result=[[],[1],[1,2],[1,2,3]]
* pop(3): current=[1,2]
* pop(2): current=[1]
* push(3): current=[1,3]
* backtrack(3): result=[...,[1,3]]
* pop(3): current=[1]
* pop(1): current=[]
* push(2), push(3) → tương tự
*/4.4 — Combination Sum với Duplicate Pruning
/**
* LeetCode #39 — Combination Sum
*
* Tìm tất cả combinations của candidates có tổng = target.
* Mỗi candidate có thể dùng nhiều lần (unlimited use).
*
* PRUNING KEY: Sort candidates trước.
* Nếu candidates[i] > remaining → break (không phải continue!).
* Vì candidates đã sort tăng dần, mọi element sau cũng > remaining → bỏ hết.
*
* Time: O(N^(T/M)) — T=target, M=min candidate
* (chiều sâu tối đa T/M, mỗi level N nhánh)
* Space: O(T/M) — call stack depth tối đa
*
* @param candidates - Array số nguyên dương
* @param target - Tổng cần đạt
*/
function combinationSum(candidates: number[], target: number): number[][] {
const result: number[][] = [];
const current: number[] = [];
candidates.sort((a, b) => a - b); // Sort để pruning hoạt động
function backtrack(startIndex: number, remaining: number): void {
if (remaining === 0) {
result.push([...current]);
return;
}
for (let i = startIndex; i < candidates.length; i++) {
// PRUNING: nếu candidate hiện tại > remaining, không cần thử tiếp
// Dùng break vì array đã sorted — mọi element sau cũng > remaining
if (candidates[i] > remaining) break;
current.push(candidates[i]);
backtrack(i, remaining - candidates[i]); // i (không phải i+1) — dùng lại được
current.pop();
}
}
backtrack(0, target);
return result;
}4.5 — N-Queens Full Solution (LeetCode #51)
/**
* LeetCode #51 — N-Queens
*
* ĐỀ BÀI: Đặt N queens trên bàn cờ N×N sao cho không queen nào tấn công nhau.
* Queens tấn công theo: hàng ngang, cột dọc, và cả hai đường chéo.
*
* APPROACH: Backtracking row-by-row
* - Đặt đúng 1 queen mỗi hàng → loại bỏ conflict theo hàng từ đầu.
* - Tại mỗi hàng: thử từng cột.
* - CHECK CONFLICT với O(1) dùng 3 sets:
* - cols: set các cột đã bị chiếm.
* - diag1: set (col - row) — hằng số trên đường chéo "/"
* - diag2: set (col + row) — hằng số trên đường chéo "\"
*
* Time: O(N!) với pruning, thực tế tốt hơn nhiều
* Space: O(N^2) — board O(N^2) + 3 sets O(N) + call stack O(N)
*
* @param n - Kích thước bàn cờ và số queens
* @returns Tất cả configurations hợp lệ dưới dạng array of strings
*
* @example
* solveNQueens(4) → [
* [".Q..","...Q","Q...","..Q."],
* ["..Q.","Q...","...Q",".Q.."]
* ]
*/
function solveNQueens(n: number): string[][] {
const result: string[][] = [];
const board: string[][] = Array.from({ length: n }, () => Array(n).fill('.'));
// 3 sets để check conflict trong O(1) — không cần scan board
const cols = new Set<number>(); // Cột đã bị chiếm
const diag1 = new Set<number>(); // col - row (đường chéo /)
const diag2 = new Set<number>(); // col + row (đường chéo \)
/** Chuyển board hiện tại thành array of strings để lưu kết quả */
const buildBoard = (): string[] => board.map(row => row.join(''));
/**
* Đặt queen cho từng hàng từ 0 đến n-1.
* @param row - Hàng hiện tại (0-indexed)
*/
function backtrack(row: number): void {
// Base case: đã đặt queen cho tất cả N hàng → valid solution
if (row === n) {
result.push(buildBoard());
return;
}
for (let col = 0; col < n; col++) {
// PRUNING: kiểm tra (row, col) có bị tấn công không
if (cols.has(col) || diag1.has(col - row) || diag2.has(col + row)) {
continue; // Bị tấn công → bỏ qua, thử cột khác
}
// PLACE queen
board[row][col] = 'Q';
cols.add(col);
diag1.add(col - row);
diag2.add(col + row);
// EXPLORE next row
backtrack(row + 1);
// REMOVE queen (backtrack — restore state)
board[row][col] = '.';
cols.delete(col);
diag1.delete(col - row);
diag2.delete(col + row);
}
}
backtrack(0);
return result;
}
/*
* TRACE THROUGH với n=4:
*
* Row 0, Col 0: Q...
* Row 1, Col 1: conflict (diag2: 0+0=0, 1+1=2 != 0... wait)
* Actually Col 1: diag1 = 1-1=0, diag1 has 0-0=0 → conflict (same diag1)
* Col 2: diag1=2-1=1, diag2=2+1=3, cols={0} → OK
* Row 1, Col 2: Q... | ..Q.
* Row 2: col 0 (diag2=0+2=2, diag2 has 2+0=2? No. cols={0,2})
* col 0: cols has 0 → skip
* col 1: diag2=1+2=3, diag2 has 2+1=3 → conflict
* col 2: cols has 2 → skip
* col 3: diag1=3-2=1, diag1 has 2-1=1 → conflict
* → No valid column for row 2 → backtrack
* Col 3: diag1=3-1=2, diag2=3+1=4, cols={0} → OK
* Row 1, Col 3: Q... | ...Q
* Row 2: Col 1: diag1=1-2=-1, diag2=1+2=3, cols={0,3} → OK
* Row 3: Col 2: diag1=2-3=-1 → conflict with row2 col1 (-1)
* Col other: all conflict
* → backtrack
* → No solution with Row 0 Col 0
*
* Row 0, Col 1: .Q..
* Row 1, Col 3: diag1=3-1=2, diag2=3+1=4, cols={1} → OK
* Row 2, Col 0: diag1=0-2=-2, diag2=0+2=2, cols={1,3} → OK
* Row 3, Col 2: diag1=2-3=-1, diag2=2+3=5, cols={1,3,0} → OK
* → SOLUTION: [".Q..","...Q","Q...","..Q."] ✓
*
* → Tổng: 2 solutions cho n=4
*/4.6 — Word Search trên Grid
/**
* LeetCode #79 — Word Search
*
* Tìm xem word có tồn tại trong grid (4-directional path) không.
* Mỗi cell chỉ được dùng 1 lần trong mỗi path.
*
* APPROACH: DFS + Backtracking từ mỗi starting cell
* - Đánh dấu cell đang visit bằng sentinel '#'.
* - Nếu không dẫn đến solution: unmark (backtrack).
*
* Time: O(M*N * 4^L) — M*N cells là starting points, L = word.length
* Space: O(L) — recursion depth tối đa L
*
* @param board - Grid ký tự
* @param word - Từ cần tìm
*/
function exist(board: string[][], word: string): boolean {
const rows = board.length;
const cols = board[0].length;
const DIRS = [[0,1],[0,-1],[1,0],[-1,0]];
function dfs(row: number, col: number, index: number): boolean {
// Base case: đã match hết tất cả ký tự
if (index === word.length) return true;
// Boundary check + character match
if (row < 0 || row >= rows || col < 0 || col >= cols) return false;
if (board[row][col] !== word[index]) return false;
// Mark as visited — dùng '#' làm sentinel để tránh dùng lại
const temp = board[row][col];
board[row][col] = '#';
// Explore 4 directions
for (const [dr, dc] of DIRS) {
if (dfs(row + dr, col + dc, index + 1)) {
board[row][col] = temp; // Restore trước khi return true
return true;
}
}
// Unmark (backtrack) — không tìm thấy từ cell này
board[row][col] = temp;
return false;
}
// Thử mọi cell làm starting point
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (dfs(r, c, 0)) return true;
}
}
return false;
}5. Complexity Deep-dive
Tổng quan Complexity
| Bài toán | Time Complexity | Space Complexity | Ghi chú |
|---|---|---|---|
| Permutations | permutations, copy mỗi cái | ||
| Subsets | subsets, copy mỗi cái | ||
| Combination Sum | =target, =min element | ||
| N-Queens | Với pruning, thực tế tốt hơn nhiều | ||
| Word Search | starts, =word length |
Memoization: → cho Fibonacci
Tại sao Memoization hiệu quả?
Naive Fibonacci: → giải ra
Số unique subproblems: chỉ giá trị cần tính:
Với Memoization: mỗi subproblem tính đúng 1 lần → total work
Tổng quát: Nếu có unique subproblems, mỗi cái cần work → Total =
Pruning Effect trên N-Queens
| Không prune (theoretical) | Với 3-set pruning | |
|---|---|---|
| 4 | ~8 nodes explored | |
| 8 | ~2,057 nodes | |
| 12 | ~856,188 nodes | |
| 15 | ~2.28B nodes |
Pruning là yếu tố then chốt
N-Queens về lý thuyết là , nhưng với 3 sets để check conflicts trong , pruning cắt đi phần lớn search space. Sự khác biệt giữa “giải được” và “timeout” với phụ thuộc vào việc bạn có dùng conflict check hay không.
6. Edge Cases & Pitfalls
Pitfall 1: Missing Base Case → Stack Overflow
// SAI — không có base case → đệ quy vô hạn → stack overflow
function factorial(n: number): number {
return n * factorial(n - 1); // Gọi mãi mãi!
}
// ĐÚNG — base case rõ ràng, phải xác định TRƯỚC khi viết recursive case
function factorial(n: number): number {
if (n <= 1) return 1; // BASE CASE — khi nào dừng?
return n * factorial(n - 1);
}Pitfall 2: Không Restore State Sau Backtrack
// SAI — push vào path nhưng quên pop → state bị "ô nhiễm"
for (let i = 0; i < nums.length; i++) {
if (used[i]) continue;
used[i] = true;
path.push(nums[i]);
backtrack(path, nums, used);
// MISSING: path.pop() và used[i] = false
// → path và used không được reset → kết quả sai hoàn toàn
}
// ĐÚNG — luôn có cặp choose / unchoose đối xứng
used[i] = true;
path.push(nums[i]);
backtrack(path, nums, used);
path.pop(); // PHẢI CÓ — restore path
used[i] = false; // PHẢI CÓ — restore usedPitfall 3: Không Clone Khi Thêm vào Result
// SAI — push reference, sau đó backtrack modify path → result cũng bị thay đổi!
result.push(path); // path là mutable reference
// ĐÚNG — clone array trước khi thêm vào result
result.push([...path]); // Spread operator — shallow copy
result.push(path.slice()); // Hoặc: Array.slice()Pitfall 4: Duplicates Trong Input → Duplicate Results
// Input [1,1,2] với Permutations → sinh ra [1,1,2] hai lần nếu không xử lý
// FIX: Sort + skip khi nums[i] === nums[i-1] và nums[i-1] chưa được dùng
nums.sort((a, b) => a - b); // Sort trước — BẮT BUỘC
for (let i = 0; i < nums.length; i++) {
if (used[i]) continue;
// Skip nếu: giá trị giống element trước VÀ element trước CHƯA được dùng
// (nghĩa là ta đã xét branch dùng nums[i-1] ở vị trí này rồi)
if (i > 0 && nums[i] === nums[i-1] && !used[i-1]) continue;
used[i] = true;
path.push(nums[i]);
backtrack();
path.pop();
used[i] = false;
}Pitfall 5: Cycle trong Grid DFS — Không Đánh Dấu Visited
// SAI — không đánh dấu visited → dfs có thể đi lại cell cũ vô hạn
function dfs(r: number, c: number, idx: number): boolean {
if (board[r][c] !== word[idx]) return false;
// Tiếp tục mà không mark → có thể quay lại (r,c) từ neighbor
return dfs(r+1, c, idx+1) || dfs(r-1, c, idx+1); // Cycle!
}
// ĐÚNG — mark trước explore, unmark sau explore
const temp = board[r][c];
board[r][c] = '#'; // Mark: không dùng lại trong path này
const found = dfs(r+1, c, idx+1) || dfs(r-1, c, idx+1);
board[r][c] = temp; // Unmark (backtrack)
return found;7. Pattern Recognition
Nhận Diện Backtracking Problems
| Từ khóa | Pattern | LeetCode |
|---|---|---|
| ”all permutations” | Backtrack với used[] array | #46, #47 |
| ”all subsets / power set” | Include/exclude với startIndex | #78, #90 |
| ”combination sum” | Backtrack + remaining + pruning | #39, #40 |
| ”all valid configurations” | N-Queens style, row-by-row | #51, #52 |
| ”word search in grid” | DFS + mark/unmark grid cell | #79, #212 |
| ”palindrome partitioning” | Backtrack + isPalindrome check | #131 |
| ”generate parentheses” | Backtrack với open/close counts | #22 |
| ”sudoku solver” | Grid backtrack + row/col/box check | #37 |
| ”letter combinations” | Backtrack với phone keypad map | #17 |
| ”restore IP addresses” | Backtrack với segment count | #93 |
Khi nào KHÔNG dùng Backtracking?
- Chỉ cần đếm số lượng (không cần list hết): → thường có DP hoặc tốt hơn.
- Decision problem với overlapping subproblems: → Memoization / DP.
- N lớn (> 20): backtracking sẽ timeout — cần approach khác.
Backtracking phù hợp nhất khi: cần enumerate all solutions và nhỏ ().
Backtracking Template Tổng quát
function backtrack(state): if is_goal(state): record(state) return for choice in get_choices(state): if is_valid(choice, state): apply(choice, state) ← Make move backtrack(new_state) undo(choice, state) ← Unmake move (KEY!)Template này áp dụng cho tất cả backtracking problems. Chỉ khác nhau ở
is_goal,get_choices,is_valid,apply, vàundo.
8. Top LeetCode Problems
| # | Tên | Độ khó | Pattern | Priority |
|---|---|---|---|---|
| 46 | Permutations | Medium | used[] backtrack | Must-do |
| 78 | Subsets | Medium | Include/exclude + startIndex | Must-do |
| 39 | Combination Sum | Medium | startIndex + remaining + pruning | Must-do |
| 22 | Generate Parentheses | Medium | Backtrack + open/close count | Must-do |
| 79 | Word Search | Medium | Grid DFS + mark/unmark | High |
| 131 | Palindrome Partitioning | Medium | Backtrack + palindrome check | High |
| 47 | Permutations II | Medium | Sort + skip duplicates | High |
| 90 | Subsets II | Medium | Sort + skip duplicates | High |
| 17 | Letter Combinations of Phone Number | Medium | Keypad map + backtrack | Medium |
| 51 | N-Queens | Hard | Row-by-row + 3 sets O(1) check | Stretch |
| 37 | Sudoku Solver | Hard | Grid backtrack + constraint check | Stretch |
Lộ trình học tối ưu
- Làm #78 (Subsets) → hiểu include/exclude và startIndex pattern.
- Làm #46 (Permutations) → hiểu used[] tracking và choose/unchoose.
- Làm #39 (Combination Sum) → hiểu pruning với sorted array.
- Làm #22 (Generate Parentheses) → backtrack với numeric constraints.
- Thử #51 (N-Queens) → tổng hợp: row-by-row, 3 sets, pruning toàn diện.
9. Self-Assessment Checklist
Checklist trước khi chuyển sang buổi tiếp theo
Kiến thức nền:
- Giải thích “base case” và “recursive case” — tại sao thiếu base case gây stack overflow?
- Vẽ recursion tree cho
fib(5)— chỉ ra overlapping subproblems. - Giải thích backtracking template: choose → explore → unchoose.
- Giải thích tại sao phải clone array khi thêm vào result.
Code từ memory (không nhìn):
- Viết được
subsets([1,2,3])với include/exclude trong < 8 phút. - Viết được
permute([1,2,3])với used[] trong < 8 phút. - Viết được
combinationSumvới startIndex và pruning trong < 10 phút. - Viết được
fibMemo(memoized) trong < 5 phút.
Hiểu sâu:
- Trace through
solveNQueens(4)bằng tay — giải thích tại sao chỉ có 2 solutions. - Giải thích 3 sets trong N-Queens — tại sao
col - rowlà constant trên đường chéo ”/“? - Giải thích sort + skip duplicate trick cho Permutations II (#47) bằng một ví dụ cụ thể.
- Mô tả từng bước của
exist(board, "ABCCED")trên grid nhỏ 3×4.
Stretch goal:
- Solve được #37 Sudoku Solver mà không nhìn solution.
- Implement Word Search II (#212) với Trie optimization để giảm từ xuống .
- Giải thích tại sao Backtracking + Memoization = Top-down DP — cho một ví dụ cụ thể.
Xem tiếp: 05-Binary-Search — Từ O(N) xuống O(log N) — tư duy “loại bỏ nửa search space mỗi bước”