Back to blog

Dynamic Programming & Greedy Algorithms: Hướng Dẫn Toàn Diện

algorithmsdynamic-programminggreedyinterview-prepdsaoptimization
Dynamic Programming & Greedy Algorithms: Hướng Dẫn Toàn Diện

Giới Thiệu

Dynamic Programming và Greedy Algorithms là hai paradigm chính để giải các bài toán tối ưu — tìm solution tốt nhất, rẻ nhất, dài nhất, ngắn nhất, hay hiệu quả nhất. Chúng cũng nằm trong số những chủ đề đáng sợ nhất trong coding interviews.

Nhưng tin vui là: cả hai paradigm đều tuân theo pattern. DP chia bài toán thành overlapping subproblems và lưu kết quả để tránh tính toán thừa. Greedy đưa ra lựa chọn tối ưu cục bộ ở mỗi bước, tin rằng local optimality dẫn đến global optimality. Khi bạn nắm vững những pattern này, những bài toán tưởng như impossible bắt đầu clicking.

Trong coding interviews, các bài DP xuất hiện trong khoảng 25-30% các buổi phỏng vấn kỹ thuật. Chúng được coi là category khó nhất — nhưng cũng là có công thức nhất. Bài viết này cho bạn framework có hệ thống để tackle bất kỳ bài DP nào, cộng thêm hiểu biết rõ ràng về khi nào greedy là approach đúng (và đơn giản hơn).

Đây là bài viết cuối cùng trong DSA series. Mọi thứ bạn đã học — arrays, linked lists, trees, graphs, heaps, sorting — hội tụ ở đây. DP và greedy algorithms xây dựng trên tất cả những data structures đó.

Bạn Sẽ Học Được Gì

✅ Hiểu khi nào dùng DP vs greedy vs brute force
✅ Nắm vững framework DP 5 bước cho mọi bài toán
✅ Cài đặt memoization (top-down) và tabulation (bottom-up)
✅ Giải các bài DP 1D và 2D có hệ thống
✅ Nhận diện các DP pattern phổ biến (knapsack, subsequence, grid, string)
✅ Hiểu greedy choice property và khi nào greedy hoạt động
✅ Giải 10 bài DP + 4 bài greedy kinh điển từng bước

Yêu Cầu Trước


1. Nền Tảng Dynamic Programming

1.1 Dynamic Programming Là Gì?

Dynamic Programming (DP) là kỹ thuật tối ưu giải các bài toán phức tạp bằng cách chia thành overlapping subproblems, giải mỗi subproblem một lần, và lưu kết quả để dùng lại.

Hãy nghĩ nó như recursion + caching. Không có DP, recursive solution tính lại cùng subproblems theo cấp số mũ. Với DP, mỗi subproblem được giải một lần rồi tái sử dụng.

Trong cây trên, fib(3) và fib(2) được tính nhiều lần. DP loại bỏ sự dư thừa này.

1.2 Hai Điều Kiện Cho DP

Bài toán có thể giải bằng DP nếu có:

  1. Overlapping subproblems: Cùng bài toán nhỏ hơn được giải lặp đi lặp lại
  2. Optimal substructure: Solution tối ưu có thể được xây dựng từ các solution tối ưu của subproblems
Điều kiệnÝ nghĩaVí dụ
Overlapping subproblemsCùng phép tính được lặp lạiFibonacci: fib(3) tính nhiều lần
Optimal substructureĐáp án tối ưu dùng các đáp án con tối ưuĐường đi ngắn nhất: A→C dùng A→B + B→C ngắn nhất

1.3 Memoization vs Tabulation

ApproachHướngCài đặtƯu điểmNhược điểm
MemoizationTop-downRecursion + cache (hashmap/array)Trực quan, chỉ giải subproblems cần thiếtCó thể stack overflow, overhead gọi hàm
TabulationBottom-upIterative, điền bảng từ base casesKhông overhead recursion, dễ tối ưu spacePhải giải tất cả subproblems, thứ tự quan trọng

Nguyên tắc chung: Bắt đầu với memoization (dễ suy nghĩ hơn), rồi chuyển sang tabulation nếu cần tối ưu.

1.4 Framework DP 5 Bước

Mọi bài DP đều theo approach có hệ thống này:

  1. Định nghĩa state — dp[i] (hoặc dp[i][j]) biểu diễn gì?
  2. Định nghĩa hệ thức truy hồi — Tính dp[i] từ các states đã tính trước đó thế nào?
  3. Xác định base cases — Subproblems nhỏ nhất có thể giải tầm thường?
  4. Quyết định thứ tự tính — Top-down (memoization) hay bottom-up (tabulation)?
  5. Tối ưu space — Có thể giảm từ 2D xuống 1D? Từ array xuống hai biến?

2. Dynamic Programming 1D

Bài 1: Leo Cầu Thang

Bạn leo cầu thang có n bậc. Mỗi lần bạn có thể leo 1 hoặc 2 bậc. Có bao nhiêu cách khác nhau để lên đỉnh?

Framework 5 bước:

  1. State: dp[i] = số cách để đến bậc i
  2. Truy hồi: dp[i] = dp[i-1] + dp[i-2] (đến từ 1 bậc trước hoặc 2 bậc trước)
  3. Base cases: dp[0] = 1, dp[1] = 1
  4. Thứ tự: Bottom-up (0 → n)
  5. Tối ưu space: Chỉ cần hai giá trị trước → O(1) space
// TypeScript - Climbing Stairs
// Memoization (top-down)
function climbStairsMemo(n: number): number {
  const memo = new Map<number, number>();
 
  function dp(i: number): number {
    if (i <= 1) return 1;
    if (memo.has(i)) return memo.get(i)!;
 
    const result = dp(i - 1) + dp(i - 2);
    memo.set(i, result);
    return result;
  }
 
  return dp(n);
}
 
// Tabulation (bottom-up)
function climbStairsTab(n: number): number {
  if (n <= 1) return 1;
  const dp = new Array(n + 1);
  dp[0] = 1;
  dp[1] = 1;
 
  for (let i = 2; i <= n; i++) {
    dp[i] = dp[i - 1] + dp[i - 2];
  }
 
  return dp[n];
}
 
// Tối ưu space
function climbStairs(n: number): number {
  if (n <= 1) return 1;
  let prev2 = 1, prev1 = 1;
 
  for (let i = 2; i <= n; i++) {
    const curr = prev1 + prev2;
    prev2 = prev1;
    prev1 = curr;
  }
 
  return prev1;
}
 
console.log(climbStairs(5)); // 8
# Python - Climbing Stairs
from functools import lru_cache
 
# Memoization (top-down)
def climb_stairs_memo(n: int) -> int:
    @lru_cache(maxsize=None)
    def dp(i: int) -> int:
        if i <= 1:
            return 1
        return dp(i - 1) + dp(i - 2)
    return dp(n)
 
# Tabulation (bottom-up)
def climb_stairs_tab(n: int) -> int:
    if n <= 1:
        return 1
    dp = [0] * (n + 1)
    dp[0] = dp[1] = 1
    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    return dp[n]
 
# Tối ưu space
def climb_stairs(n: int) -> int:
    if n <= 1:
        return 1
    prev2, prev1 = 1, 1
    for _ in range(2, n + 1):
        prev2, prev1 = prev1, prev1 + prev2
    return prev1
 
print(climb_stairs(5))  # 8

Time: O(n). Space: O(1) khi tối ưu.


Bài 2: Cướp Nhà

Bạn là tên cướp lên kế hoạch cướp các nhà dọc phố. Mỗi nhà có tiền. Bạn không thể cướp hai nhà liền kề. Tìm số tiền tối đa có thể cướp?

Framework 5 bước:

  1. State: dp[i] = tiền tối đa cướp từ nhà 0..i
  2. Truy hồi: dp[i] = max(dp[i-1], dp[i-2] + nums[i]) — bỏ qua nhà i, hoặc cướp nó (+ bỏ qua i-1)
  3. Base cases: dp[0] = nums[0], dp[1] = max(nums[0], nums[1])
  4. Thứ tự: Bottom-up (0 → n)
  5. Space: O(1) — chỉ cần hai giá trị trước
// TypeScript - House Robber
function rob(nums: number[]): number {
  if (nums.length === 0) return 0;
  if (nums.length === 1) return nums[0];
 
  let prev2 = nums[0];
  let prev1 = Math.max(nums[0], nums[1]);
 
  for (let i = 2; i < nums.length; i++) {
    const curr = Math.max(prev1, prev2 + nums[i]);
    prev2 = prev1;
    prev1 = curr;
  }
 
  return prev1;
}
 
console.log(rob([2, 7, 9, 3, 1])); // 12 (cướp nhà 0, 2, 4)
# Python - House Robber
def rob(nums: list[int]) -> int:
    if not nums:
        return 0
    if len(nums) == 1:
        return nums[0]
 
    prev2, prev1 = nums[0], max(nums[0], nums[1])
 
    for i in range(2, len(nums)):
        prev2, prev1 = prev1, max(prev1, prev2 + nums[i])
 
    return prev1
 
print(rob([2, 7, 9, 3, 1]))  # 12

Time: O(n). Space: O(1).


Bài 3: Đổi Tiền Xu

Cho các đồng xu với mệnh giá khác nhau và tổng số tiền, tìm số đồng xu ít nhất cần để tạo ra số tiền đó. Trả về -1 nếu không thể.

Framework 5 bước:

  1. State: dp[amount] = số xu tối thiểu để tạo amount
  2. Truy hồi: dp[i] = min(dp[i - coin] + 1) cho mỗi loại xu
  3. Base cases: dp[0] = 0
  4. Thứ tự: Bottom-up (0 → amount)
  5. Space: O(amount)
// TypeScript - Coin Change
function coinChange(coins: number[], amount: number): number {
  const dp = new Array(amount + 1).fill(Infinity);
  dp[0] = 0;
 
  for (let i = 1; i <= amount; i++) {
    for (const coin of coins) {
      if (coin <= i && dp[i - coin] !== Infinity) {
        dp[i] = Math.min(dp[i], dp[i - coin] + 1);
      }
    }
  }
 
  return dp[amount] === Infinity ? -1 : dp[amount];
}
 
console.log(coinChange([1, 5, 10, 25], 30)); // 2 (25 + 5)
console.log(coinChange([2], 3));              // -1
# Python - Coin Change
def coin_change(coins: list[int], amount: int) -> int:
    dp = [float("inf")] * (amount + 1)
    dp[0] = 0
 
    for i in range(1, amount + 1):
        for coin in coins:
            if coin <= i and dp[i - coin] != float("inf"):
                dp[i] = min(dp[i], dp[i - coin] + 1)
 
    return dp[amount] if dp[amount] != float("inf") else -1
 
print(coin_change([1, 5, 10, 25], 30))  # 2
print(coin_change([2], 3))               # -1

Time: O(amount × coins). Space: O(amount).


Bài 4: Dãy Con Tăng Dài Nhất (LIS)

Cho mảng số nguyên, tìm độ dài dãy con tăng nghiêm ngặt dài nhất.

Framework 5 bước:

  1. State: dp[i] = độ dài LIS kết thúc tại index i
  2. Truy hồi: dp[i] = max(dp[j] + 1) cho mọi j < i mà nums[j] < nums[i]
  3. Base cases: dp[i] = 1 cho mọi i (mỗi phần tử tự nó là subsequence)
  4. Thứ tự: Bottom-up (0 → n)
  5. Đáp án: max(dp) — LIS có thể kết thúc ở index bất kỳ
// TypeScript - Longest Increasing Subsequence
// Solution DP O(n²)
function lengthOfLIS(nums: number[]): number {
  const n = nums.length;
  const dp = new Array(n).fill(1);
 
  for (let i = 1; i < n; i++) {
    for (let j = 0; j < i; j++) {
      if (nums[j] < nums[i]) {
        dp[i] = Math.max(dp[i], dp[j] + 1);
      }
    }
  }
 
  return Math.max(...dp);
}
 
// Tối ưu O(n log n) với binary search
function lengthOfLISOptimal(nums: number[]): number {
  const tails: number[] = []; // tail nhỏ nhất cho mỗi độ dài LIS
 
  for (const num of nums) {
    let lo = 0, hi = tails.length;
    while (lo < hi) {
      const mid = Math.floor((lo + hi) / 2);
      if (tails[mid] < num) lo = mid + 1;
      else hi = mid;
    }
    tails[lo] = num;
  }
 
  return tails.length;
}
 
console.log(lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18])); // 4 → [2,3,7,101]
# Python - Longest Increasing Subsequence
# Solution DP O(n²)
def length_of_lis(nums: list[int]) -> int:
    n = len(nums)
    dp = [1] * n
 
    for i in range(1, n):
        for j in range(i):
            if nums[j] < nums[i]:
                dp[i] = max(dp[i], dp[j] + 1)
 
    return max(dp)
 
# Tối ưu O(n log n) với binary search
import bisect
 
def length_of_lis_optimal(nums: list[int]) -> int:
    tails = []
    for num in nums:
        pos = bisect.bisect_left(tails, num)
        if pos == len(tails):
            tails.append(num)
        else:
            tails[pos] = num
    return len(tails)
 
print(length_of_lis([10, 9, 2, 5, 3, 7, 101, 18]))  # 4

Time: O(n²) cơ bản, O(n log n) tối ưu. Space: O(n).


3. Dynamic Programming 2D

Bài 5: Đường Đi Duy Nhất

Robot bắt đầu ở góc trên-trái của lưới m × n. Nó chỉ có thể đi phải hoặc xuống. Có bao nhiêu đường đi duy nhất để đến góc dưới-phải?

Framework 5 bước:

  1. State: dp[i][j] = số đường đi đến ô (i, j)
  2. Truy hồi: dp[i][j] = dp[i-1][j] + dp[i][j-1] (từ trên + từ trái)
  3. Base cases: dp[0][j] = 1, dp[i][0] = 1 (chỉ có một cách dọc theo cạnh)
  4. Thứ tự: Hàng theo hàng, trái sang phải
  5. Space: O(n) — chỉ cần hàng hiện tại
// TypeScript - Unique Paths
function uniquePaths(m: number, n: number): number {
  const dp = new Array(n).fill(1);
 
  for (let i = 1; i < m; i++) {
    for (let j = 1; j < n; j++) {
      dp[j] += dp[j - 1]; // dp[j] đã có giá trị từ hàng trên
    }
  }
 
  return dp[n - 1];
}
 
console.log(uniquePaths(3, 7)); // 28
# Python - Unique Paths
def unique_paths(m: int, n: int) -> int:
    dp = [1] * n
 
    for i in range(1, m):
        for j in range(1, n):
            dp[j] += dp[j - 1]
 
    return dp[n - 1]
 
print(unique_paths(3, 7))  # 28

Time: O(m × n). Space: O(n).


Bài 6: Dãy Con Chung Dài Nhất (LCS)

Cho hai chuỗi, tìm độ dài dãy con chung dài nhất — chuỗi ký tự dài nhất xuất hiện trong cả hai chuỗi theo cùng thứ tự (không nhất thiết liên tiếp).

Framework 5 bước:

  1. State: dp[i][j] = độ dài LCS của text1[0..i-1] và text2[0..j-1]
  2. Truy hồi:
    • Nếu text1[i-1] == text2[j-1]: dp[i][j] = dp[i-1][j-1] + 1
    • Ngược lại: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
  3. Base cases: dp[0][j] = 0, dp[i][0] = 0
  4. Thứ tự: Hàng theo hàng
  5. Space: O(n) với rolling array
// TypeScript - Longest Common Subsequence
function longestCommonSubsequence(text1: string, text2: string): number {
  const m = text1.length, n = text2.length;
  const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
 
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (text1[i - 1] === text2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1;
      } else {
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
  }
 
  return dp[m][n];
}
 
console.log(longestCommonSubsequence("abcde", "ace")); // 3 → "ace"
# Python - Longest Common Subsequence
def longest_common_subsequence(text1: str, text2: str) -> int:
    m, n = len(text1), len(text2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
 
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if text1[i - 1] == text2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
 
    return dp[m][n]
 
print(longest_common_subsequence("abcde", "ace"))  # 3

Time: O(m × n). Space: O(m × n), có thể tối ưu xuống O(n).


Bài 7: Khoảng Cách Chỉnh Sửa

Cho hai chuỗi word1 và word2, trả về số thao tác tối thiểu (chèn, xóa, thay thế) để chuyển word1 thành word2.

Framework 5 bước:

  1. State: dp[i][j] = số thao tác tối thiểu chuyển word1[0..i-1] thành word2[0..j-1]
  2. Truy hồi:
    • Nếu word1[i-1] == word2[j-1]: dp[i][j] = dp[i-1][j-1] (không cần thao tác)
    • Ngược lại: dp[i][j] = 1 + min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) (thay, xóa, chèn)
  3. Base cases: dp[i][0] = i (xóa hết), dp[0][j] = j (chèn hết)
  4. Thứ tự: Hàng theo hàng
// TypeScript - Edit Distance
function minDistance(word1: string, word2: string): number {
  const m = word1.length, n = word2.length;
  const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
 
  for (let i = 0; i <= m; i++) dp[i][0] = i;
  for (let j = 0; j <= n; j++) dp[0][j] = j;
 
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (word1[i - 1] === word2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1];
      } else {
        dp[i][j] = 1 + Math.min(
          dp[i - 1][j - 1], // thay thế
          dp[i - 1][j],     // xóa
          dp[i][j - 1]      // chèn
        );
      }
    }
  }
 
  return dp[m][n];
}
 
console.log(minDistance("horse", "ros"));     // 3
console.log(minDistance("intention", "execution")); // 5
# Python - Edit Distance
def min_distance(word1: str, word2: str) -> int:
    m, n = len(word1), len(word2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
 
    for i in range(m + 1):
        dp[i][0] = i
    for j in range(n + 1):
        dp[0][j] = j
 
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if word1[i - 1] == word2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1]
            else:
                dp[i][j] = 1 + min(
                    dp[i - 1][j - 1],  # thay thế
                    dp[i - 1][j],      # xóa
                    dp[i][j - 1],      # chèn
                )
 
    return dp[m][n]
 
print(min_distance("horse", "ros"))      # 3
print(min_distance("intention", "execution"))  # 5

Time: O(m × n). Space: O(m × n).


Bài 8: Bài Toán Ba Lô 0/1

Cho n vật phẩm với trọng lượng và giá trị, và ba lô có sức chứa W, tìm giá trị tối đa có thể mang mà không vượt quá giới hạn trọng lượng. Mỗi vật phẩm chỉ lấy tối đa một lần.

Framework 5 bước:

  1. State: dp[i][w] = giá trị tối đa dùng vật phẩm 0..i-1 với sức chứa w
  2. Truy hồi: dp[i][w] = max(dp[i-1][w], dp[i-1][w - weight[i]] + value[i])
    • Bỏ qua vật phẩm i, hoặc lấy nó (nếu vừa)
  3. Base cases: dp[0][w] = 0 cho mọi w
  4. Thứ tự: Từng vật phẩm, sức chứa 0 → W
  5. Space: O(W) với tối ưu 1D (duyệt sức chứa ngược)
// TypeScript - 0/1 Knapsack
function knapsack(weights: number[], values: number[], capacity: number): number {
  const n = weights.length;
  // DP 1D tối ưu space
  const dp = new Array(capacity + 1).fill(0);
 
  for (let i = 0; i < n; i++) {
    // Duyệt ngược để tránh dùng vật phẩm i hai lần
    for (let w = capacity; w >= weights[i]; w--) {
      dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
    }
  }
 
  return dp[capacity];
}
 
console.log(knapsack([2, 3, 4, 5], [3, 4, 5, 6], 8)); // 10
# Python - 0/1 Knapsack
def knapsack(weights: list[int], values: list[int], capacity: int) -> int:
    n = len(weights)
    dp = [0] * (capacity + 1)
 
    for i in range(n):
        # Duyệt ngược để đảm bảo mỗi vật phẩm dùng tối đa một lần
        for w in range(capacity, weights[i] - 1, -1):
            dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
 
    return dp[capacity]
 
print(knapsack([2, 3, 4, 5], [3, 4, 5, 6], 8))  # 10

Time: O(n × W). Space: O(W).


Bài 9: Chia Tập Con Bằng Tổng

Cho mảng số nguyên, xác định xem có thể chia thành hai tập con có tổng bằng nhau không.

Approach: Đây là biến thể 0/1 knapsack. Target = totalSum / 2. Liệu có tìm được tập con tổng bằng target?

// TypeScript - Partition Equal Subset Sum
function canPartition(nums: number[]): boolean {
  const total = nums.reduce((a, b) => a + b, 0);
  if (total % 2 !== 0) return false;
 
  const target = total / 2;
  const dp = new Array(target + 1).fill(false);
  dp[0] = true;
 
  for (const num of nums) {
    for (let j = target; j >= num; j--) {
      dp[j] = dp[j] || dp[j - num];
    }
  }
 
  return dp[target];
}
 
console.log(canPartition([1, 5, 11, 5])); // true → [1,5,5] và [11]
console.log(canPartition([1, 2, 3, 5]));  // false
# Python - Partition Equal Subset Sum
def can_partition(nums: list[int]) -> bool:
    total = sum(nums)
    if total % 2 != 0:
        return False
 
    target = total // 2
    dp = [False] * (target + 1)
    dp[0] = True
 
    for num in nums:
        for j in range(target, num - 1, -1):
            dp[j] = dp[j] or dp[j - num]
 
    return dp[target]
 
print(can_partition([1, 5, 11, 5]))  # True
print(can_partition([1, 2, 3, 5]))   # False

Time: O(n × target). Space: O(target).


Bài 10: Tách Từ

Cho chuỗi s và từ điển, trả về true nếu s có thể được tách thành các từ trong từ điển.

Framework 5 bước:

  1. State: dp[i] = s[0..i-1] có thể tách được không?
  2. Truy hồi: dp[i] = true nếu tồn tại j < i mà dp[j] là true và s[j..i-1] nằm trong từ điển
  3. Base case: dp[0] = true (chuỗi rỗng)
// TypeScript - Word Break
function wordBreak(s: string, wordDict: string[]): boolean {
  const words = new Set(wordDict);
  const n = s.length;
  const dp = new Array(n + 1).fill(false);
  dp[0] = true;
 
  for (let i = 1; i <= n; i++) {
    for (let j = 0; j < i; j++) {
      if (dp[j] && words.has(s.slice(j, i))) {
        dp[i] = true;
        break;
      }
    }
  }
 
  return dp[n];
}
 
console.log(wordBreak("leetcode", ["leet", "code"]));  // true
console.log(wordBreak("applepenapple", ["apple", "pen"])); // true
# Python - Word Break
def word_break(s: str, word_dict: list[str]) -> bool:
    words = set(word_dict)
    n = len(s)
    dp = [False] * (n + 1)
    dp[0] = True
 
    for i in range(1, n + 1):
        for j in range(i):
            if dp[j] and s[j:i] in words:
                dp[i] = True
                break
 
    return dp[n]
 
print(word_break("leetcode", ["leet", "code"]))   # True
print(word_break("applepenapple", ["apple", "pen"]))  # True

Time: O(n² × k) với k = độ dài từ trung bình. Space: O(n).


4. Các Pattern DP Phổ Biến

PatternĐịnh nghĩa StateVí dụ
Linear (1D)dp[i] = đáp án cho i phần tử đầuLeo cầu thang, Cướp nhà, Đổi xu
Hai chuỗi (2D)dp[i][j] = đáp án cho s1[0..i] và s2[0..j]LCS, Edit Distance
Lưới (2D)dp[i][j] = đáp án cho ô (i,j)Unique Paths, Min Path Sum
Knapsackdp[i][w] = đáp án dùng vật 0..i với sức chứa w0/1 Knapsack, Subset Sum
Intervaldp[i][j] = đáp án cho đoạn [i..j]Burst Balloons, Matrix Chain
Subsequencedp[i] = đáp án cho subsequence kết thúc tại iLIS, Maximum Subarray
State machinedp[i][state] = đáp án tại vị trí i ở state cho trướcBest Time to Buy/Sell Stock

Mẹo Nhận Diện Pattern

  • "Tối thiểu / tối đa / số cách" → khả năng cao là DP
  • "Có thể làm được không?" (có/không) → DP với boolean states
  • "Chia thành tập con" → Biến thể Knapsack
  • "Subsequence / substring" → DP 1D hoặc 2D
  • "So sánh hai chuỗi" → DP 2D (pattern LCS / Edit Distance)
  • "Duyệt lưới có ràng buộc" → DP 2D

5. Greedy Algorithms

5.1 Greedy Algorithm Là Gì?

Greedy algorithm đưa ra lựa chọn tối ưu cục bộ ở mỗi bước, hy vọng tìm được tối ưu toàn cục. Khác với DP, nó không bao giờ xem xét lại các lựa chọn trước.

5.2 Greedy vs DP

Tính chấtGreedyDynamic Programming
ApproachLocal optimal → global optimalGiải tất cả subproblems → kết hợp
Xem xét lại?Không bao giờSo sánh tất cả options
Cần chứng minh?Phải chứng minh greedy hoạt độngOptimal substructure đủ
ComplexityThường O(n log n) hoặc O(n)Thường O(n²) hoặc O(n × W)
Ví dụFractional knapsack0/1 knapsack

Insight chính: Greedy đơn giản và nhanh hơn — nhưng chỉ hoạt động khi bạn có thể chứng minh rằng lựa chọn tối ưu cục bộ dẫn đến solution tối ưu toàn cục. Khi không chắc, dùng DP.

5.3 Khi Nào Greedy Hoạt Động

Greedy hoạt động khi bài toán có:

  1. Greedy choice property: Lựa chọn tối ưu cục bộ dẫn đến solution tối ưu toàn cục
  2. Optimal substructure: Solution tối ưu chứa các solution tối ưu của subproblems

6. Các Bài Toán Greedy Kinh Điển

Bài 11: Jump Game

Cho mảng mà mỗi phần tử biểu diễn độ dài nhảy tối đa từ vị trí đó, xác định xem bạn có thể đến index cuối không.

Greedy insight: Theo dõi index xa nhất có thể đến. Nếu không đến được index hiện tại, trả về false.

// TypeScript - Jump Game
function canJump(nums: number[]): boolean {
  let farthest = 0;
 
  for (let i = 0; i < nums.length; i++) {
    if (i > farthest) return false;
    farthest = Math.max(farthest, i + nums[i]);
  }
 
  return true;
}
 
console.log(canJump([2, 3, 1, 1, 4])); // true
console.log(canJump([3, 2, 1, 0, 4])); // false
# Python - Jump Game
def can_jump(nums: list[int]) -> bool:
    farthest = 0
    for i in range(len(nums)):
        if i > farthest:
            return False
        farthest = max(farthest, i + nums[i])
    return True
 
print(can_jump([2, 3, 1, 1, 4]))  # True
print(can_jump([3, 2, 1, 0, 4]))  # False

Time: O(n). Space: O(1).


Bài 12: Jump Game II

Cùng setup, tìm số lần nhảy tối thiểu để đến index cuối.

Greedy insight: Dùng levels giống BFS. Ở mỗi "level", tìm điểm xa nhất có thể đến. Mỗi level = một lần nhảy.

// TypeScript - Jump Game II
function jump(nums: number[]): number {
  let jumps = 0;
  let currentEnd = 0;
  let farthest = 0;
 
  for (let i = 0; i < nums.length - 1; i++) {
    farthest = Math.max(farthest, i + nums[i]);
 
    if (i === currentEnd) {
      jumps++;
      currentEnd = farthest;
    }
  }
 
  return jumps;
}
 
console.log(jump([2, 3, 1, 1, 4])); // 2
# Python - Jump Game II
def jump(nums: list[int]) -> int:
    jumps = 0
    current_end = 0
    farthest = 0
 
    for i in range(len(nums) - 1):
        farthest = max(farthest, i + nums[i])
        if i == current_end:
            jumps += 1
            current_end = farthest
 
    return jumps
 
print(jump([2, 3, 1, 1, 4]))  # 2

Time: O(n). Space: O(1).


Bài 13: Chọn Hoạt Động / Khoảng Không Chồng Lấn

Cho các khoảng [start, end], tìm số lượng khoảng không chồng lấn tối đa.

Greedy insight: Sắp xếp theo end time. Luôn chọn khoảng kết thúc sớm nhất — điều này để lại nhiều chỗ nhất cho các khoảng sau.

// TypeScript - Non-overlapping Intervals (trả về số khoảng cần xóa tối thiểu)
function eraseOverlapIntervals(intervals: number[][]): number {
  intervals.sort((a, b) => a[1] - b[1]); // Sắp xếp theo end time
 
  let count = 0;
  let prevEnd = -Infinity;
 
  for (const [start, end] of intervals) {
    if (start >= prevEnd) {
      prevEnd = end; // Giữ khoảng này
    } else {
      count++; // Xóa khoảng này (bị chồng)
    }
  }
 
  return count;
}
 
console.log(eraseOverlapIntervals([[1,2],[2,3],[3,4],[1,3]])); // 1
# Python - Non-overlapping Intervals
def erase_overlap_intervals(intervals: list[list[int]]) -> int:
    intervals.sort(key=lambda x: x[1])  # Sắp xếp theo end time
    count = 0
    prev_end = float("-inf")
 
    for start, end in intervals:
        if start >= prev_end:
            prev_end = end
        else:
            count += 1
 
    return count
 
print(erase_overlap_intervals([[1,2],[2,3],[3,4],[1,3]]))  # 1

Time: O(n log n). Space: O(1).


Bài 14: Trạm Xăng

Có n trạm xăng xếp thành vòng tròn. gas[i] là lượng xăng tại trạm i, cost[i] là xăng cần để đi đến trạm i+1. Tìm trạm bắt đầu để đi hết vòng, hoặc -1 nếu không thể.

Greedy insight: Nếu tổng xăng ≥ tổng chi phí, solution tồn tại. Bắt đầu từ trạm mà running tank lần đầu âm — trạm tiếp theo là đáp án.

// TypeScript - Gas Station
function canCompleteCircuit(gas: number[], cost: number[]): number {
  let totalTank = 0;
  let currentTank = 0;
  let start = 0;
 
  for (let i = 0; i < gas.length; i++) {
    const net = gas[i] - cost[i];
    totalTank += net;
    currentTank += net;
 
    if (currentTank < 0) {
      start = i + 1;
      currentTank = 0;
    }
  }
 
  return totalTank >= 0 ? start : -1;
}
 
console.log(canCompleteCircuit([1,2,3,4,5], [3,4,5,1,2])); // 3
# Python - Gas Station
def can_complete_circuit(gas: list[int], cost: list[int]) -> int:
    total_tank = 0
    current_tank = 0
    start = 0
 
    for i in range(len(gas)):
        net = gas[i] - cost[i]
        total_tank += net
        current_tank += net
 
        if current_tank < 0:
            start = i + 1
            current_tank = 0
 
    return start if total_tank >= 0 else -1
 
print(can_complete_circuit([1,2,3,4,5], [3,4,5,1,2]))  # 3

Time: O(n). Space: O(1).


7. Hướng Dẫn Chọn DP vs Greedy

Loại bài toánDùng GreedyDùng DP
Fractional knapsack✅ Lấy theo tỷ lệ value/weight—
0/1 knapsack❌ Greedy sai✅
Jump Game (đến được?)✅ Theo dõi farthest—
Coin Change (ít xu nhất)❌ Greedy sai với xu tùy ý✅
Activity Selection✅ Sort theo end time—
Longest Increasing Subsequence—✅
Interval scheduling✅ Sort theo end time—
Edit Distance—✅

8. Tham Khảo Complexity

Các Bài DP

Bài toánTimeSpaceSpace Tối ưu
Leo Cầu ThangO(n)O(n)O(1)
Cướp NhàO(n)O(n)O(1)
Đổi XuO(n × amount)O(amount)—
LISO(n²) / O(n log n)O(n)—
Unique PathsO(m × n)O(m × n)O(n)
LCSO(m × n)O(m × n)O(n)
Edit DistanceO(m × n)O(m × n)O(n)
0/1 KnapsackO(n × W)O(n × W)O(W)
Chia Tập ConO(n × sum)O(sum)—
Tách TừO(n²)O(n)—

Các Bài Greedy

Bài toánTimeSpace
Jump GameO(n)O(1)
Jump Game IIO(n)O(1)
Non-overlapping IntervalsO(n log n)O(1)
Trạm XăngO(n)O(1)

9. Bài Tập Thực Hành

Easy (Xây Dựng Nền Tảng)

#Bài toánPatternApproach
1Climbing Stairs1D DPdp[i] = dp[i-1] + dp[i-2]
2Min Cost Climbing Stairs1D DPdp[i] = cost[i] + min(dp[i-1], dp[i-2])
3Maximum SubarrayKadane's / DPTrack max ending here
4Best Time to Buy and Sell StockGreedyTrack min price, max profit
5Is SubsequenceGreedy / Two pointerMatch characters greedily

Medium (Xây Dựng Tự Tin)

#Bài toánPatternApproach
6House Robber1D DPmax(skip, rob + dp[i-2])
7Coin ChangeKnapsackmin coins cho mỗi amount
8Longest Increasing SubsequenceSubsequence DPdp[i] = max(dp[j]+1) for j < i
9Unique PathsGrid DPdp[i][j] = dp[i-1][j] + dp[i][j-1]
10Longest Common SubsequenceTwo-string DPMatch hoặc max(skip)
11Word Break1D DPdp[i] = any dp[j] && s[j:i] in dict
12Partition Equal Subset SumKnapsackSubset sum = total/2
13Jump GameGreedyTrack farthest reachable
14Jump Game IIGreedy (BFS)Level-by-level jumps
15Non-overlapping IntervalsGreedySort by end, count kept
16Gas StationGreedyTrack tank and start

Hard (Master Level)

#Bài toánPatternApproach
17Edit DistanceTwo-string DPinsert/delete/replace min
18Regular Expression MatchingTwo-string DPHandle '.' và '*' cases
19Burst BalloonsInterval DPdp[i][j] = max(burst last)
20Longest Valid Parentheses1D DP / StackTrack valid ranges

Tóm Tắt

Dynamic Programming và Greedy Algorithms là hai trụ cột của tối ưu trong khoa học máy tính — và là boss cuối cùng của coding interviews.

Framework DP (5 bước):

  1. Định nghĩa state
  2. Viết hệ thức truy hồi
  3. Xác định base cases
  4. Chọn thứ tự tính (top-down hoặc bottom-up)
  5. Tối ưu space

Các DP pattern phổ biến:

  • 1D: Leo cầu thang, Cướp nhà, Đổi xu, Tách từ
  • Lưới 2D: Unique Paths, Min Path Sum
  • Hai chuỗi: LCS, Edit Distance
  • Knapsack: 0/1 Knapsack, Subset Sum, Coin Change

Khi nào dùng greedy:

  • Bạn có thể chứng minh local optimal = global optimal
  • Sort + duyệt một lượt thường đủ
  • Ví dụ: Jump Game, Activity Selection, Gas Station

Khi nào dùng DP:

  • Có overlapping subproblems
  • Cần xem xét tất cả khả năng
  • Greedy không hoạt động (vd: 0/1 knapsack, coin change với mệnh giá tùy ý)

Đây là bài cuối trong DSA series! Bạn giờ đã có bộ công cụ đầy đủ — từ arrays và strings qua trees, graphs, heaps, sorting, và bây giờ DP và greedy. Chìa khóa để master là practice: chọn bài từ các bảng trên, áp dụng framework, và xây dựng pattern recognition thông qua lặp lại.

Bài trước: Heaps & Priority Queues
Tổng quan series: Data Structures & Algorithms Roadmap

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.