Wiki Thuật toán Quy hoạch động (DP)

Quy hoạch động (DP)

huunguyen huunguyen Updated Tháng sáu 1, 2026

Quy hoạch động (Dynamic Programming — DP) giải bài toán bằng cách chia thành các bài toán con chồng lặp và lưu lại kết quả để tránh tính lại.


Hai tính chất cần có

  1. Optimal substructure: Đáp án tối ưu của bài toán lớn được xây dựng từ đáp án tối ưu của bài toán con.
  2. Overlapping subproblems: Các bài toán con bị lặp lại nhiều lần.

Nếu thiếu một trong hai, DP không áp dụng được (dùng Greedy hoặc Divide & Conquer thay thế).


Hai cách cài đặt DP

Top-down (Memoization — đệ quy + ghi nhớ)
map<int, long long> memo;
long long dp(int n) {
    if (n <= 1) return n;
    if (memo.count(n)) return memo[n];
    return memo[n] = dp(n-1) + dp(n-2);
}

Ưu điểm: Chỉ tính các trạng thái thực sự cần thiết, dễ cài khi transition phức tạp.

Bottom-up (Tabulation — lặp)
vector<long long> dp(n+1);
dp[0] = 0; dp[1] = 1;
for (int i = 2; i <= n; i++)
    dp[i] = dp[i-1] + dp[i-2];

Ưu điểm: Không có overhead đệ quy, dễ tối ưu bộ nhớ (rolling array).


Quy trình thiết kế DP

  1. Xác định trạng tháidp[i] có nghĩa là gì?
  2. Xác định công thức chuyểndp[i] tính từ dp[j] nào?
  3. Xác định base case — giá trị khởi tạo.
  4. Xác định thứ tự tính — tính dp[i] trước dp[j] hay sau?
  5. Xác định kết quả — lấy dp[n], max(dp), hay tổng hợp gì?

Ví dụ 1: Dãy con tăng dài nhất (LIS)

Tìm dãy con tăng dần dài nhất trong mảng a.

O(N2):

vector<int> dp(n, 1);
for (int i = 1; i < n; i++)
    for (int j = 0; j < i; j++)
        if (a[j] < a[i]) dp[i] = max(dp[i], dp[j]+1);
cout << *max_element(dp.begin(), dp.end());

O(NlogN) — dùng binary search:

vector<int> lis;
for (int x : a) {
    auto it = lower_bound(lis.begin(), lis.end(), x);
    if (it == lis.end()) lis.push_back(x);
    else *it = x;
}
cout << lis.size();

Ví dụ 2: Bài toán cái túi 0/1 (0/1 Knapsack)

Chọn các vật có trọng lượng wi và giá trị vi sao cho tổng trọng lượng W và tổng giá trị lớn nhất.

vector<int> dp(W+1, 0);
for (int i = 0; i < n; i++)
    for (int j = W; j >= w[i]; j--)
        dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
cout << dp[W];

Unbounded Knapsack (dùng mỗi vật không giới hạn lần):

for (int i = 0; i < n; i++)
    for (int j = w[i]; j <= W; j++)
        dp[j] = max(dp[j], dp[j-w[i]] + v[i]);

Ví dụ 3: Dãy con chung dài nhất (LCS)

int n = s.size(), m = t.size();
vector<vector<int>> dp(n+1, vector<int>(m+1, 0));
for (int i = 1; i <= n; i++)
    for (int j = 1; j <= m; j++)
        if (s[i-1] == t[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
        else dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
cout << dp[n][m];

Ví dụ 4: Edit Distance

vector<vector<int>> dp(n+1, vector<int>(m+1));
for (int i = 0; i <= n; i++) dp[i][0] = i;
for (int j = 0; j <= m; j++) dp[0][j] = j;
for (int i = 1; i <= n; i++)
    for (int j = 1; j <= m; j++)
        if (s[i-1] == t[j-1]) dp[i][j] = dp[i-1][j-1];
        else dp[i][j] = 1 + min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]});
cout << dp[n][m];

Các dạng DP — trang chi tiết

Dạng Mô tả Trang
DP khoảng dp[l][r] trên đoạn [l,r] DP khoảng
DP bitmask Trạng thái là tập con bitmask DP bitmask
SOS DP Tổng trên mọi tập con (Sum over Subsets) SOS DP
DP broken profile Lát lưới theo từng ô với bitmask DP broken profile
DP trên cây Trạng thái là nút cây, DFS DP trên cây
Knapsack trên cây Chọn tập đỉnh trên cây với ràng buộc cấu trúc Knapsack trên cây
DP trên đồ thị DAG, SCC, Dijkstra + DP DP trên đồ thị
DP chữ số Xây số theo từng chữ số DP chữ số
DP xác suất Trạng thái là xác suất / kỳ vọng DP xác suất
DP nhân ma trận Truy hồi tuyến tính với N1018 DP nhân ma trận
DP tối ưu hóa CHT, D&C DP, Knuth DP tối ưu hóa
Slope Trick DP với hàm lồi từng đoạn tuyến tính Slope Trick

Tối ưu bộ nhớ: Rolling Array

Khi dp[i] chỉ phụ thuộc dp[i-1], dùng 2 mảng thay vì N mảng:

vector<vector<int>> dp(2, vector<int>(m+1, 0));
for (int i = 1; i <= n; i++) {
    int cur = i & 1, prev = 1 - cur;
    for (int j = 1; j <= m; j++)
        dp[cur][j] = ...dp[prev][...];
}

Phân biệt DP với các kỹ thuật khác

Kỹ thuật Khi nào dùng
DP Bài toán con chồng lặp, cần tối ưu hoặc đếm
Greedy Chọn tham lam tại mỗi bước luôn dẫn đến tối ưu toàn cục
Divide & Conquer Bài toán con độc lập (không chồng lặp)
Backtracking Cần liệt kê tất cả nghiệm, không gian tìm kiếm nhỏ
gnatmake 12.2.0 a68g 3.1.2 nasm 2.16.1 as_x64 2.46 awk 1.3.4 gcc 16.1.0 csc 6.12.0.200 g++ 16.1.0 g++-themis 16.1.0 g++17 16.1.0 g++20 16.1.0 g++23 16.1.0 clang++ 22.1.6 dmd 2.112.0 dart 3.12.1 gforth 0.7.3 gfortran 12.2.0 go 1.26.3 groovyc 5.0.6 javac 25.0.3 node 26.2.0 kotlinc 2.3.21 sbcl 2.2.9 lua 5.4.8 nim 2.2.10 fpc 3.2.2 fpc-themis 3.2.2 perl 5.36.0 php 8.5.6 pike 8.0 pypy3 7.3.23 python3 3.14.5 racket 8.7 ruby 4.0.5 rustc 1.96.0 csc 5.3.0 ctoj-scratch 0.0.1 sed 4.9 tclsh 8.6 bun 1.3.14 deno 2.8.1 v 0.5.1 zig 0.16.0