Buổi 10 — Advanced DP

Navigation

09-Dynamic-Programming | → 11-Segment-Tree Giai đoạn: FAANG Mastery | Độ khó: ⭐⭐⭐⭐⭐


1. Analogy & Context

Bitmask DP — Task Delegation Analogy

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 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?

TechniqueState SpaceDifficultyWhen to use
Standard DP or MediumSingle sequence, grid
Bitmask DPHardSmall N (≤ 20), all-pairs
Tree DP nodesHardTree structure, subtree answers
Digit DPHardCount numbers in range
Interval DPHardMerge/split intervals
State Machine DPMedium-HardMultiple 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 state
 
function 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 INCLUDED
 
interface 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 + rightDepth
 
interface 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 position
function 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);
}

4.6 Interval DP: Burst Balloons (#312) — Complete Solution

// #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

TechniqueTimeSpaceN Constraint
Bitmask DP
Tree DP — tree heightAny
Digit DP = digits, = state space
Interval DP
State Machine DP = number of states

Why for Bitmask DP?

  • possible masks (subsets)
  • Với mỗi mask, xét possible “last element” → transitions
  • Total: states × per transition

Why for Interval DP?

  • pairs
  • Với mỗi pair, try split points
  • Total:

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 . Với : 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 problems
const 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”
  • “Optimal way to fully parenthesize”
  • Keywords: “burst balloons”, “matrix chain multiplication”, “strange printer”

State Machine DP — Dấu hiệu:

  • Có multiple modes/states tại mỗi position
  • Transition giữa states có rule rõ ràng
  • Stock problems, regex matching, parser DP

8. Top LeetCode Problems

#ProblemDifficultyTechniqueKey Insight
#464Can I WinMediumBitmask DP + Game Theorymask = used numbers, memoize by mask
#847Shortest Path Visiting All NodesHardBitmask BFSMulti-source BFS, state=(node, mask)
#943Find the Shortest SuperstringHardBitmask DP + DPTSP on strings
#124Binary Tree Max Path SumHardTree DPMax path through each node as root
#543Diameter of Binary TreeEasyTree DPleftDepth + rightDepth at each node
#968Binary Tree CamerasHardTree DP3 states: covered, not covered, has camera
#309Best Time Stock with CooldownMediumState Machine DP3 states: holding, sold, cooldown
#312Burst BalloonsHardInterval DPLast burst insight — fixed boundaries
#357Count Numbers Unique DigitsMediumDigit DPCount choices at each position

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]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ể

Next Steps

11-Segment-Tree — Range Query Data Structure Prerequisite: vững DP fundamentals (09-Dynamic-Programming) trước khi sang Segment Tree.