DP tối ưu hóa (Optimization DP)
Nhiều bài DP có công thức chuyển dạng:
Duyệt trâu tất cả cho mỗi cho độ phức tạp . Các kỹ thuật dưới đây giúp tối ưu xuống hoặc khi cost có cấu trúc đặc biệt.
1. Convex Hull Trick (CHT)
Khi nào áp dụng?
Công thức chuyển có dạng:
Mỗi trạng thái tương ứng với một đường thẳng .
Bài toán trở thành: với mỗi , tìm đường thẳng cho giá trị nhỏ nhất tại .
Ý tưởng
Tập các đường thẳng tối ưu tạo thành một bao lồi dưới (lower convex hull). Chỉ cần giữ các đường thẳng nằm trên bao lồi này.
Cài đặt — Stack đơn điệu (khi và đơn điệu)
#include <bits/stdc++.h>
using namespace std;
struct Line {
long long m, b;
long long eval(long long x) { return m * x + b; }
};
// Kiểm tra đường thẳng mid có cần thiết không
bool bad(Line l1, Line l2, Line l3) {
// l2 bị l1, l3 "che" hoàn toàn
return (__int128)(l3.b - l1.b) * (l1.m - l2.m)
<= (__int128)(l2.b - l1.b) * (l1.m - l3.m);
}
struct CHT {
vector<Line> hull;
int ptr = 0;
void addLine(long long m, long long b) {
Line l = {m, b};
while (hull.size() >= 2 && bad(hull[hull.size()-2], hull[hull.size()-1], l))
hull.pop_back();
hull.push_back(l);
}
// Query khi x tăng dần (monotone queries)
long long query(long long x) {
while (ptr + 1 < (int)hull.size() && hull[ptr+1].eval(x) <= hull[ptr].eval(x))
ptr++;
return hull[ptr].eval(x);
}
};
Độ phức tạp: khi cả (slope) và (query) đều đơn điệu.
Cài đặt — Li Chao Tree (query bất kỳ)
struct LiChaoTree {
static const int MAXV = 1e6 + 5;
struct Line { long long m, b; long long eval(long long x) { return m*x+b; } };
Line tree[4 * MAXV];
bool has[4 * MAXV];
void addLine(Line newLine, int node, int lo, int hi) {
if (lo == hi) {
if (!has[node] || newLine.eval(lo) < tree[node].eval(lo)) {
tree[node] = newLine; has[node] = true;
}
return;
}
int mid = (lo + hi) / 2;
bool leftBetter = newLine.eval(lo) < tree[node].eval(lo);
bool midBetter = newLine.eval(mid) < tree[node].eval(mid);
if (midBetter) swap(tree[node], newLine), has[node] = true;
if (lo == hi) return;
if (leftBetter != midBetter) addLine(newLine, 2*node, lo, mid);
else addLine(newLine, 2*node+1, mid+1, hi);
}
long long query(int x, int node, int lo, int hi) {
long long res = has[node] ? tree[node].eval(x) : LLONG_MAX;
if (lo == hi) return res;
int mid = (lo + hi) / 2;
if (x <= mid) return min(res, query(x, 2*node, lo, mid));
else return min(res, query(x, 2*node+1, mid+1, hi));
}
};
Độ phức tạp: với là khoảng giá trị query.
Ví dụ: Bài toán chia nhóm
học sinh có điểm . Chi phí tạo một nhóm từ đến là . Chia tất cả học sinh thành các nhóm liên tiếp với tổng chi phí nhỏ nhất.// dp[i] = chi phí nhỏ nhất chia i học sinh đầu
// dp[i] = min_{j<i} (dp[j] + (a[i]-a[j+1])^2 + C)
// Khai triển: dp[j] + a[i]^2 - 2*a[i]*a[j+1] + a[j+1]^2 + C
// => đường thẳng: slope = -2*a[j+1], intercept = dp[j] + a[j+1]^2 + C
CHT cht;
dp[0] = 0;
cht.addLine(-2 * a[1], dp[0] + a[1]*a[1] + C);
for (int i = 1; i <= n; i++) {
dp[i] = cht.query(a[i]) + a[i]*a[i];
cht.addLine(-2 * a[i+1], dp[i] + a[i+1]*a[i+1] + C);
}
Với điều kiện cost thỏa mãn bất đẳng thức tứ giác (quadrangle inequality), khi đó (vị trí tối ưu cho ) đơn điệu: .
Thuật toán
Dùng chia để trị: tính trước, sau đó giới hạn tìm kiếm cho nửa trái và nửa phải.
// dp[cur][j] = min cost, layer cur, position j
// opt[j] = vị trí k tối ưu cho j
long long dp[2][MAXN];
// cost(l, r): chi phí chuyển từ l sang r (cần tính trước hoặc O(1))
void solve(int layer, int lo, int hi, int optLo, int optHi) {
if (lo > hi) return;
int mid = (lo + hi) / 2;
long long best = LLONG_MAX;
int opt = optLo;
for (int k = optLo; k <= min(mid, optHi); k++) {
long long val = dp[layer ^ 1][k] + cost(k, mid);
if (val < best) {
best = val;
opt = k;
}
}
dp[layer][mid] = best;
solve(layer, lo, mid - 1, optLo, opt);
solve(layer, mid + 1, hi, opt, optHi);
}
// Gọi cho mỗi layer i:
// solve(i & 1, 0, n-1, 0, n-1);
Độ phức tạp: mỗi layer, tổng ( layer).
3. Knuth's Optimization
Khi nào áp dụng?
DP khoảng dạng:
Với thỏa mãn:
- Monotone: với .
- Quadrangle inequality: với .
Khi đó (điểm chia tối ưu) thỏa: .
Cài đặt
int opt[MAXN][MAXN];
long long dp[MAXN][MAXN];
// Khởi tạo
for (int i = 0; i < n; i++) {
dp[i][i] = 0;
opt[i][i] = i;
}
for (int len = 2; len <= n; len++) {
for (int l = 0; l + len - 1 < n; l++) {
int r = l + len - 1;
dp[l][r] = LLONG_MAX;
// Chỉ tìm k trong [opt[l][r-1], opt[l+1][r]]
int lo = opt[l][r-1];
int hi = (l + 1 <= r) ? opt[l+1][r] : r - 1;
for (int k = lo; k <= min(hi, r - 1); k++) {
long long val = dp[l][k] + dp[k+1][r] + w[l][r];
if (val < dp[l][r]) {
dp[l][r] = val;
opt[l][r] = k;
}
}
}
}
Độ phức tạp: thay vì .
Ví dụ áp dụng: Optimal BST, Stone merging (gộp đá), Hu-Shing algorithm.
Tổng kết
| Kỹ thuật | Điều kiện | Từ | Xuống |
|---|---|---|---|
| Convex Hull Trick | cost(j,i) = b[j]·a[i] |
hoặc | |
| D&C DP | opt[j] đơn điệu |
||
| Knuth's Opt | thỏa QI |
Quy tắc nhận dạng nhanh:
- Có nhân ? → CHT
- DP layer và opt đơn điệu? → D&C DP
- DP khoảng với thỏa QI? → Knuth
Bình luận