Luận bàn về Dynamic Programming (5 parts)

Dynamic Programming, Quy hoạch động, DP, ... Bao nhiêu ông tạch vì topic này rồi 💀

Đây là cái gì?

Đại khái thì đây là 1 cách tối ưu giải thuật bằng cách lưu lại giá trị cần thiết trong 1 state/subproblem và tái sử dụng nó khi phải lặp lại 1 state/problem giống hệt.

Chẳng hạn, ta có 1 state gọi là f("Tuslipid"), ta giải nó tốn 69 bước, đc giá trị khoaxuantu, ta lưu nó lại trong bộ nhớ

Memory table
keyvalue
Tuslipidkhoaxuantu

Chương trình của ta sau khi tính đc state đó, chạy tiếp vài bước, thì lại bắt gặp 1 state y hệt f("Tuslipid"), bình thường nếu muốn giải quyết tiếp bước này, chương trình ta phải chạy tới 69 bước nữa.

Nhưng ta đã có bộ nhớ trên, vậy nên ta chỉ cần truy cập bộ nhớ đó, lấy ra giá trị theo khóa Tuslipid, vậy là chỉ tốn 1 bước, ta đã cắt giảm đc 68 bước rồi ~

Khi nào thì ta dùng DP?

Có rất nhiều bài toán bắt ta phải giải bằng phương pháp đệ quy áp dụng cách tiếp cận chia để trị (divide & conquer).

Theo concept chia để trị, ta chia nhỏ 1 problem lớn thành các subproblem nhỏ hơn, kết hợp kết quả có đc từ các subproblems để được kết quả cuối cùng của problem lớn. Với đệ quy, từng subproblem ta lại chia ra các subproblem nhỏ hơn, 1 main -> 2 sub, 2 sub -> 4 smaller sub, 4 smaller sub -> 8 smaller smaller sub,... đến khi ko thể chia tiếp đc nữa, ta đến base case.

Divide & conquer có hạn chế đó là độ phức tạp lớn khủng khiếp:

Như vậy, để tối ưu được vấn đề trên, ta đã có DP, giải trước 1 hàm, lưu kết quả lại, sau đó nếu gặp lại hàm này thì ta chỉ lấy kết quả đã lưu ra thôi. Liên hệ tới cache, tại đó ta lưu các thông tin cần thiết để có thể tái sử dụng mà ko phải tính toán lấy kết quả từ đầu nữa.

Về cách sử dụng DP, có cả thảy 2 cách:

Về mẫu giải, ta có thể suy ra pseudocode chung cho 2 cách tiếp cận như sau:

Memoization table -> dp[]
function f(state):
  check base case, return value
  if state is solved in dp, return dp[state]

  dp[state] = f(next_state) with f(another_next_state) with ...
  return dp[state]
function f(input):
  tabular -> dp[]

  preset some base cases, dp[] at index 0 or 1 or something like this...

  traverse dp[], repeat:
    dp[state] = dp[prev_state] with dp[another_prev_state] with ...
    state -> next_state
  end repeat

  return dp[input]

Fact: Khi hỏi về các câu hỏi DP, interviewer họ đánh giá rất cao nếu ae diễn giải cho họ lần lượt 3 hướng trên (thứ tự brute force -> memoization -> tabular).

Họ tìm kiếm ứng viên có tư duy suy nghĩ, chứ ko tìm kiếm 1 máy giải thuật toán. Nếu ae nhảy vô cách tối ưu nhất là bottom-up khử đệ quy thì chưa chắc họ lại ấn tượng bằng giải lần lượt đâu.

Đây là 1 phần khó, nhìn chung muốn giải đc dạng này thì chỉ có cách luyện nhiều, ko có cách nào khác đâu 💀

Tham khảo thêm về cách giải dynamic programming

Patterns

  1. Recursive numbers

  2. 0/1 knapsack

  3. Unbounded knapsack

  4. Longest common substring

  5. Longest palindromic subsequence