Dynamic Programming: Recursive Numbers

Đây là phần 1 trong bộ 5 notes về Dynamic Programming của tôi

1 recursive number là 1 số mà có thể được tạo ra từ 1 phương trình sử dụng đệ quy để liên kết các mệnh đề trong chương trình đó.

Lấy ví dụ cho dễ hiểu, dãy fibonacci, ae chắc hẳn quá quen rồi

0 1 1 2 3 5 8 13 21

Trong dãy fibonacci, số i được tạo bởi tổng của số i-1 với số i-2.

Bắt đầu từ số 21 nhé (top-down)

21 = (13 + 8)
   = (8 + 5) + (5 + 3)
   = (5 + 3) + (3 + 2) + (3 + 2) + (2 + 1)
   = (3 + 2) + (2 + 1) + (2 + 1) + (1 + 1) + (2 + 1) + (1 + 1) + (1 + 1) + (1 + 0)

Thấy đệ quy chưa? Ta thử biến đổi block trên thành các hàm f(x) nhé

f(21) = f(13) + f(8)
      = f(8) + f(5) + f(5) + f(3)
      = f(5) + f(3) + f(3) + f(2) + f(3) + f(2) + f(2) + f(1)

Vậy là ta được đệ quy vét cạn.

int fibonacci(int n) {
  if (n == 0 || n == 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

Nhận thấy ở trên, các hàm f(8), f(5), f(3), ... lặp đi lặp lại nhiều lần, vậy là ta có thể áp dụng đc DP cho đệ quy đc rồi.

vector<int> dp (n);
int fibonacci(int n) {
  if (n == 0 || n == 1) return n;
  if (dp[n] != 0) return dp[n] // Returns if we solve the subproblem before
  dp[n] = fibonacci(n - 1) + fibonacci(n - 2);
  return dp[n];
}

Top-down DP rồi, giờ ta thử hướng bottom-up xem. Bắt đầu từ 0, nhận thấy:

0 + 1 = 1
1 + 1 = 2
1 + 2 = 3
2 + 3 = 5
3 + 5 = 8
5 + 8 = 13
8 + 13 = 21

Chuyển hóa thành dãy số, chương trình của ta có thể chạy như sau:

   [0 1]
-> 0 [1 1]
-> 0 1 [1 2]
-> 0 1 1 [2 3]
-> 0 1 1 2 [3 5]
-> 0 1 1 2 3 [5 8]
-> 0 1 1 2 3 5 [8 13]
-> 0 1 1 2 3 5 8 [13 21]

Như cách chạy trên, ta cần lưu lại các số và dần dần hình thành 1 dãy aka mảng 1 chiều. Cuối cùng...

int fibonacci(int n) {
  vector<int> dp (n+1);
  dp[0] = 0;
  dp[1] = 1;
  for (int i = 2; i < n+1; i++) {
    dp[i] = dp[i-1] + dp[i-2];
  }
  return dp.back();
}

Vậy là ta chỉ tốn O(n) time và O(n) space cho bài fibonacci rồi.

Ngoài ra ta vẫn có thể tối ưu hơn nữa, vì cố định ở 2 số i-1 và i-2, nên ta hoàn toàn có thể sử dụng 2 var để lưu 2 số này cho tính toán, giúp giảm space complexity từ O(n) xuống còn O(1).

int fibonacci(int n) {
  int i1 = 0, i2 =1;
  int ans = 0;
  for (int i = 2; i < n+1; i++) {
    ans = i1 + i2;
    i2 = ans;
    i1 = i2;
  }
  return ans;
}

Sample questions