Hãy tưởng tượng bạn là team lead cần phân công N nhiệm vụ cho N thành viên.
State = bitmask 2^N — mỗi bit cho biết thành viên đó đã được assign hay chưa.
Ví dụ: N=4 nhân viên, mask=0110 = nhân viên 1 và 2 đã có việc, 0 và 3 chưa.
Giải quyết “Traveling Salesman Problem”: bitmask = set các thành phố đã thăm.
Digit DP — Number Building Analogy
Đếm các số trong đoạn [L, R] thoả mãn một điều kiện nào đó.
Xây dựng số từng chữ số một, từ trái sang phải.
Tại mỗi bước, track: “Tôi có đang bị ràng buộc bởi giới hạn trên không?” (tight constraint).
Giống như tô màu từng ô trong grid — một lần nhầm là ra ngoài biên.
Tree DP — Bottom-Up Company Reporting
Mỗi employee (node) report số liệu lên manager (parent).
Manager tổng hợp từ tất cả direct reports → report lên director.
dp[node][0] = optimal answer nếu không include node này.
dp[node][1] = optimal answer nếu có include node này.
Kết quả cho subtree = combine kết quả của tất cả children.
Tại sao Advanced DP khó hơn?
Technique
State Space
Difficulty
When to use
Standard DP
O(N) or O(N2)
Medium
Single sequence, grid
Bitmask DP
O(2N⋅N)
Hard
Small N (≤ 20), all-pairs
Tree DP
O(N) nodes
Hard
Tree structure, subtree answers
Digit DP
O(digits⋅states)
Hard
Count numbers in range
Interval DP
O(N3)
Hard
Merge/split intervals
State Machine DP
O(N⋅states)
Medium-Hard
Multiple states per position
2. The FAANG Standard
Advanced DP ở FAANG
Bitmask DP: xuất hiện trong competitive programming rounds và L6+ interviews tại Google.
Tree DP: thường gặp ở senior/staff interviews — yêu cầu hiểu recursive decomposition.
Interval DP: Burst Balloons (#312) là bài Hard kinh điển được Google hỏi nhiều lần.
State Machine DP: Stock problems series (#121, #122, #123, #188, #309) — Meta favorites.
Kỳ vọng: biết nhận diện pattern, giải thích approach, code bitmask DP với N ≤ 20.
L6/Staff Level Expectations:
Nhận ra khi nào bitmask DP áp dụng được (N nhỏ, cần track subset của elements).
Viết Tree DP với include/exclude pattern.
Explain Digit DP approach mà không cần code hoàn chỉnh (quá phức tạp cho 45 phút).
Burst Balloons — phải biết insight “last balloon” và tại sao đúng.
3. Visual Thinking (Mermaid)
3.1 Bitmask DP — TSP State Transition (N=3 cities)
graph LR
S000["mask=000<br/>start=0"] -->|visit city 1| S010["mask=010<br/>at city 1"]
S000 -->|visit city 2| S100["mask=100<br/>at city 2"]
S010 -->|visit city 2| S110["mask=110<br/>at city 2"]
S100 -->|visit city 1| S110
S110 -->|visit city 0| S111["mask=111<br/>ALL VISITED ✓"]
style S111 fill:#90EE90
style S111 color:#000
3.2 Tree DP — Include/Exclude Pattern
graph TD
R["root<br/>dp[0]=max_excl<br/>dp[1]=max_incl"] --> A["child A<br/>dp[0], dp[1]"]
R --> B["child B<br/>dp[0], dp[1]"]
A --> C["leaf C<br/>dp[0]=0, dp[1]=val"]
A --> D["leaf D<br/>dp[0]=0, dp[1]=val"]
note["If include root:<br/> children must be excluded<br/>If exclude root:<br/> children can be include OR exclude"]
3.3 Interval DP — Burst Balloons Key Insight
graph LR
P["Problem: dp[l][r]<br/>= max coins for balloons l..r"] --> K["Try each k in l..r<br/>as LAST balloon burst"]
K --> L["dp[l][k-1]<br/>(burst all left first)"]
K --> R["dp[k+1][r]<br/>(burst all right first)"]
K --> C["Cost when k is last:<br/>nums[l-1]*nums[k]*nums[r+1]"]
4. TypeScript Template
4.1 Bitmask DP: Minimum Cost to Visit All Nodes (TSP Variant)
// #847 Shortest Path Visiting All Nodes// State: dp[mask][node] = min steps to visit all nodes in `mask`, currently at `node`// mask = bitmask of visited nodes// Use BFS (for unweighted) with bitmask statefunction shortestPathLength(graph: number[][]): number { const n = graph.length; const fullMask = (1 << n) - 1; // all nodes visited // BFS: state = [node, visitedMask] const queue: [number, number, number][] = []; // [node, mask, steps] const visited = new Set<string>(); // Start from every node simultaneously (multi-source BFS) for (let i = 0; i < n; i++) { const mask = 1 << i; queue.push([i, mask, 0]); visited.add(`${i},${mask}`); } let head = 0; while (head < queue.length) { const [node, mask, steps] = queue[head++]; if (mask === fullMask) return steps; // visited all nodes for (const neighbor of graph[node]) { const newMask = mask | (1 << neighbor); const key = `${neighbor},${newMask}`; if (!visited.has(key)) { visited.add(key); queue.push([neighbor, newMask, steps + 1]); } } } return -1;}// Time: O(2^N * N) | Space: O(2^N * N)// N ≤ 12 so 2^12 * 12 = 49,152 states — manageable
4.2 Bitmask DP: Can I Win (#464)
// #464 Can I Win// State: which numbers have been chosen (bitmask), current accumulated sum// dp[mask] = can the current player win given that numbers in mask are already used?function canIWin(maxChoosableInteger: number, desiredTotal: number): boolean { // Edge cases if (desiredTotal <= 0) return true; const total = (maxChoosableInteger * (maxChoosableInteger + 1)) / 2; if (total < desiredTotal) return false; // impossible even with all numbers const memo = new Map<number, boolean>(); // Returns true if current player can win function canWin(mask: number, currentSum: number): boolean { if (memo.has(mask)) return memo.get(mask)!; for (let i = 1; i <= maxChoosableInteger; i++) { const bit = 1 << i; if (mask & bit) continue; // already used // If picking i makes sum >= desired, current player wins if (currentSum + i >= desiredTotal) { memo.set(mask, true); return true; } // If opponent cannot win after we pick i, we win if (!canWin(mask | bit, currentSum + i)) { memo.set(mask, true); return true; } } memo.set(mask, false); return false; } return canWin(0, 0);}// Time: O(2^N * N) | Space: O(2^N)
4.3 Tree DP: Max Weight Independent Set
// Max Weight Independent Set on Tree// No two adjacent nodes (parent-child) can both be included// dp[node][0] = max weight in subtree if node is EXCLUDED// dp[node][1] = max weight in subtree if node is INCLUDEDinterface TreeNode { val: number; children: TreeNode[];}function maxWeightIndependentSet(root: TreeNode | null): number { if (!root) return 0; function dfs(node: TreeNode): [number, number] { // Returns [exclude_val, include_val] let excludeSum = 0; // sum when this node is excluded let includeSum = node.val; // sum when this node is included for (const child of node.children) { const [childExclude, childInclude] = dfs(child); // If we EXCLUDE this node: children can be either included or excluded excludeSum += Math.max(childExclude, childInclude); // If we INCLUDE this node: children must be excluded includeSum += childExclude; } return [excludeSum, includeSum]; } const [excl, incl] = dfs(root); return Math.max(excl, incl);}// Time: O(N) — visit each node once | Space: O(H) — recursion stack
4.4 Tree DP: Diameter of Binary Tree (#543)
// Diameter = longest path between any two nodes// Key: path goes THROUGH some node as the "top" of the arch// dp[node] = max depth of subtree rooted at node// At each node: candidate diameter = leftDepth + rightDepthinterface BinaryTreeNode { val: number; left: BinaryTreeNode | null; right: BinaryTreeNode | null;}function diameterOfBinaryTree(root: BinaryTreeNode | null): number { let maxDiameter = 0; function depth(node: BinaryTreeNode | null): number { if (!node) return 0; const leftDepth = depth(node.left); const rightDepth = depth(node.right); // Update diameter: path through this node maxDiameter = Math.max(maxDiameter, leftDepth + rightDepth); // Return depth of this subtree for parent's use return 1 + Math.max(leftDepth, rightDepth); } depth(root); return maxDiameter;}// Tree DP insight: each node contributes (leftDepth + rightDepth) as potential answer// Return value = depth (used by parent), side effect = update global max
4.5 Digit DP: Count Numbers With Unique Digits (#357)
// #357 Count Numbers with Unique Digits// For n=2: count numbers in [0, 99] with all unique digits// dp approach: count choices at each digit positionfunction countNumbersWithUniqueDigits(n: number): number { if (n === 0) return 1; // only 0 let result = 10; // n=1: 0-9 all unique let availableDigits = 9; // 9 choices for second digit (not same as first, not 0) let currentCount = 9; // for n=1 (excluding leading 0): 9 non-zero first digits // For each additional digit position for (let i = 2; i <= n; i++) { currentCount *= availableDigits; result += currentCount; availableDigits--; if (availableDigits < 0) break; // no more unique options } return result;}// Explanation:// n=1: 10 numbers (0-9)// n=2: 10 + 9*9 = 91 (first digit: 9 choices [1-9], second: 9 choices [0-9 except first])// n=3: 91 + 9*9*8 = 739// General Digit DP Template (memoized)function digitDP(n: number): number { const digits = String(n).split('').map(Number); const len = digits.length; // memo[pos][tight][...other states] const memo: Map<string, number> = new Map(); function dp(pos: number, tight: boolean, ...states: number[]): number { if (pos === len) return 1; // valid number formed const key = `${pos},${tight},${states.join(',')}`; if (memo.has(key)) return memo.get(key)!; const limit = tight ? digits[pos] : 9; let count = 0; for (let d = 0; d <= limit; d++) { const newTight = tight && d === limit; // Add your constraint check here // Example: no two adjacent digits same count += dp(pos + 1, newTight /* update states */); } memo.set(key, count); return count; } return dp(0, true);}
// #312 Burst Balloons// WRONG intuition: which balloon do I burst FIRST?// CORRECT intuition: which balloon do I burst LAST in interval [l, r]?//// Why last? When balloon k is last in [l,r]:// - All balloons in [l,k-1] and [k+1,r] are ALREADY gone// - The coins from bursting k = nums[l-1] * nums[k] * nums[r+1]// - nums[l-1] and nums[r+1] are the "boundary" balloons (not part of [l,r])//// dp[l][r] = max coins from bursting all balloons in range [l, r]// dp[l][r] = max over k in [l,r] of: dp[l][k-1] + nums[l-1]*nums[k]*nums[r+1] + dp[k+1][r]function maxCoins(nums: number[]): number { // Add boundary balloons with value 1 const arr = [1, ...nums, 1]; const n = arr.length; // dp[l][r] = max coins for balloons in original range [l..r] of arr const dp: number[][] = Array.from({ length: n }, () => new Array(n).fill(0)); // Fill by increasing length of interval for (let len = 1; len <= nums.length; len++) { for (let l = 1; l <= nums.length - len + 1; l++) { const r = l + len - 1; for (let k = l; k <= r; k++) { // k is the LAST balloon to burst in [l, r] const coins = arr[l - 1] * arr[k] * arr[r + 1]; dp[l][r] = Math.max(dp[l][r], dp[l][k - 1] + coins + dp[k + 1][r]); } } } return dp[1][nums.length];}// Time: O(N³) | Space: O(N²)// Key insight: "last burst" → boundaries are well-defined → no ambiguity
4.7 State Machine DP: Best Time to Buy and Sell Stock with Cooldown (#309)
// #309 Stock with Cooldown// States:// HOLDING: currently hold a stock// SOLD: just sold (must cooldown next day)// COOLDOWN: resting (can buy next day)//// Transitions:// HOLDING[i] = max(HOLDING[i-1], COOLDOWN[i-1] - price[i]) (keep or buy)// SOLD[i] = HOLDING[i-1] + price[i] (sell)// COOLDOWN[i]= max(COOLDOWN[i-1], SOLD[i-1]) (rest or come from sold)function maxProfit(prices: number[]): number { let holding = -Infinity; // haven't bought yet let sold = 0; let cooldown = 0; for (const price of prices) { const prevHolding = holding; const prevSold = sold; const prevCooldown = cooldown; holding = Math.max(prevHolding, prevCooldown - price); // keep holding or buy sold = prevHolding + price; // sell what we hold cooldown = Math.max(prevCooldown, prevSold); // rest or recover from sold // Note: use prev values to avoid using updated state in same iteration } return Math.max(sold, cooldown); // can't end while holding (no final sell implied)}// Time: O(N) | Space: O(1)// State machine makes transitions explicit and bug-free
5. Complexity Deep-dive
Complexity Summary
Technique
Time
Space
N Constraint
Bitmask DP
O(2N⋅N)
O(2N⋅N)
N≤20
Tree DP
O(N)
O(H) — tree height
Any
Digit DP
O(D⋅S)
O(D⋅S)
D = digits, S = state space
Interval DP
O(N3)
O(N2)
N≤500
State Machine DP
O(N⋅K)
O(K)
K = number of states
Why O(2N⋅N) for Bitmask DP?
Có 2N possible masks (subsets)
Với mỗi mask, xét N possible “last element” → N transitions
Total: 2N⋅N states × O(1) per transition
Why O(N3) for Interval DP?
O(N2) pairs (l,r)
Với mỗi pair, try O(N) split points k
Total: O(N3)
Bitmask Constraint:
N=20: 2^20 * 20 = ~20M operations — OK
N=25: 2^25 * 25 = ~800M operations — TLE
N=30: 2^30 * 30 = ~32B operations — definitely TLE
N > 20 với bitmask DP → cần tìm approach khác.
6. Edge Cases & Pitfalls
Bitmask Pitfall 1: Integer Overflow
JavaScript number: safe integers up to 253.
Với N>30: 1 << 30 trong JS = -1073741824 (signed 32-bit overflow!).
// BUG for N > 30:const bit = 1 << 31; // = -2147483648 in JS (negative!)// FIX: use BigInt or ensure N ≤ 20 for interview problemsconst bit = BigInt(1) << BigInt(31); // works correctly
Bitmask Pitfall 2: Bit Check vs Bit Set
// Check if bit i is set:const isSet = (mask >> i) & 1; // = 1 if set, 0 if not// Set bit i:const newMask = mask | (1 << i); // add i to set// Unset bit i:const newMask = mask & ~(1 << i); // remove i from set// Check if all N bits set:const allVisited = mask === (1 << n) - 1;
Tree DP Pitfall: Null Children
function dfs(node: TreeNode | null): [number, number] { if (!node) return [0, 0]; // base case — không phải [0, -Infinity] // ...}
Leaf nodes: left và right children đều null → [0, 0] là base case đúng.
Digit DP Pitfall: Tight Constraint
tight = true khi số đang xây dựng vẫn đang bằng giới hạn trên.
Nếu tight=true và ta chọn digit d < digits[pos] → newTight = false (thoải mái rồi).
Nếu tight=true và ta chọn digit d = digits[pos] → newTight = true (vẫn bị ràng buộc).
Quên cập nhật tight → count sai (count cả số vượt giới hạn).
Interval DP Pitfall: Loop Order
// WRONG: loop by l from 0 to n — dp[l+1][k-1] chưa được tính!for (let l = 0; l < n; l++) for (let r = l; r < n; r++) ...// CORRECT: loop by LENGTH (ensures smaller intervals computed first)for (let len = 1; len <= n; len++) for (let l = 0; l + len - 1 < n; l++) { const r = l + len - 1; ... }
Burst Balloons Specific Pitfall
The “last burst” insight is non-obvious. Common wrong approach:
Try which balloon to burst FIRST → boundaries change as you burst → state explodes.
Key: framing as “last balloon” makes boundaries FIXED → clean transition.
7. Pattern Recognition
Nhận Diện Advanced DP Pattern
Bitmask DP — Dấu hiệu:
N rất nhỏ (N ≤ 20, thường N ≤ 15)
Cần “visit all” hoặc “assign all”
State cần nhớ exactly WHICH elements đã được dùng (không chỉ HOW MANY)
Keywords: “minimum cost to visit all nodes/cities”, “assign tasks to workers optimally”
Tree DP — Dấu hiệu:
Input là tree/graph không có cycle
Cần optimal answer trên subtrees
“Maximum independent set”, “max path sum in tree”, “minimum vertex cover”
Include/exclude decision at each node
Digit DP — Dấu hiệu:
“Count numbers from 1 to N satisfying [property]”
“Count integers in [L, R] where…”
Property là về digits của số (không phải value): unique digits, digit sum < K, no two adjacent same digits
Interval DP — Dấu hiệu:
“Merge intervals for max/min value”
“Burst/remove elements, gain coins based on neighbors”
Complete Solution: #847 Shortest Path Visiting All Nodes
function shortestPathLength(graph: number[][]): number { const n = graph.length; if (n === 1) return 0; const fullMask = (1 << n) - 1; // Multi-source BFS: start from all nodes simultaneously const queue: [number, number, number][] = []; // [node, mask, dist] const visited = new Set<number>(); // encode as node * (1<<n) + mask for (let i = 0; i < n; i++) { const mask = 1 << i; queue.push([i, mask, 0]); visited.add(i * (1 << n) + mask); } let head = 0; while (head < queue.length) { const [node, mask, dist] = queue[head++]; for (const next of graph[node]) { const newMask = mask | (1 << next); if (newMask === fullMask) return dist + 1; // done! const stateKey = next * (1 << n) + newMask; if (!visited.has(stateKey)) { visited.add(stateKey); queue.push([next, newMask, dist + 1]); } } } return -1; // unreachable}// Time: O(2^N * N) | Space: O(2^N * N)
Complete Solution: #312 Burst Balloons
function maxCoins(nums: number[]): number { // Pad with virtual 1s at boundaries const arr = [1, ...nums, 1]; const n = arr.length; // n = original length + 2 const origLen = nums.length; // dp[l][r] = max coins from bursting ALL balloons in arr[l..r] // (where l and r are 1-indexed in arr, representing original balloon indices) const dp: number[][] = Array.from({ length: n }, () => new Array(n).fill(0)); // len = number of original balloons in the subproblem for (let len = 1; len <= origLen; len++) { for (let l = 1; l <= origLen - len + 1; l++) { const r = l + len - 1; // Try each k as the LAST balloon to burst in arr[l..r] for (let k = l; k <= r; k++) { // When k is burst last, arr[l-1] and arr[r+1] are still present const coins = arr[l - 1] * arr[k] * arr[r + 1]; const total = dp[l][k - 1] + coins + dp[k + 1][r]; dp[l][r] = Math.max(dp[l][r], total); } } } // Answer: burst all original balloons (indices 1..origLen) return dp[1][origLen];}// Time: O(N³) | Space: O(N²)//// Why "last burst" works:// If k is the LAST balloon in [l,r] to be burst:// - All balloons between l and k-1 are already gone → k's left neighbor = arr[l-1]// - All balloons between k+1 and r are already gone → k's right neighbor = arr[r+1]// - The cost of bursting k is FIXED: arr[l-1] * arr[k] * arr[r+1]// - Sub-problems dp[l][k-1] and dp[k+1][r] are INDEPENDENT!
9. Self-Assessment Checklist
Checklist — Advanced DP
Bitmask DP:
Giải thích bitmask DP state dp[mask][node] cho TSP
Viết bit operations: check bit, set bit, unset bit, check all-set
Biết N ≤ 20 là giới hạn thực tế cho bitmask DP
Implement Can I Win với memoization trên mask
Tree DP:
Giải thích include/exclude pattern với dp[node][0] và dp[node][1]
Implement max weight independent set trên tree
Implement diameter of binary tree dùng Tree DP
Handle null children đúng (base case = [0, 0])
Interval DP:
Giải thích tại sao “last burst” insight đúng trong Burst Balloons
Viết loop order đúng (by length, không phải by l)
Implement Burst Balloons từ đầu trong < 30 phút
State Machine DP:
Vẽ state machine diagram cho Stock with Cooldown (3 states)
Implement với biến thay vì mảng (O(1) space)
FAANG Interview Readiness
Giải #847 Bitmask BFS trong < 30 phút
Giải #312 Burst Balloons trong < 35 phút
Giải #124 Binary Tree Max Path Sum trong < 20 phút
Giải #309 Stock Cooldown trong < 20 phút
Explain Digit DP approach (không cần code) với ví dụ cụ thể