Quy hoạch động 1 chiều — 1D Dynamic Programming
Quy hoạch động giải bài tối ưu hoặc đếm bằng cách chia thành các bài toán con gối nhau và lưu lại kết quả để khỏi tính lại. "Một chiều" nghĩa là trạng thái chỉ cần một chỉ số i, lưu trong một mảng dp.
Hai điều kiện để áp dụng: bài toán con lặp lại nhiều lần, và lời giải lớn ghép được từ lời giải con. Tìm đúng công thức chuyển trạng thái là phần khó nhất; nhiều bài chỉ cần giữ vài giá trị gần nhất nên tối ưu được bộ nhớ về O(1).
Như sổ chi tiêu — đã tính rồi thì ghi lại, lần sau chỉ tra.
- Tránh tính lại bài toán con đã giải: lưu kết quả của bài toán con nhỏ để ghép thành lời giải bài toán lớn.
- Mỗi ô trong bảng dp là một "câu trả lời đã biết".
Dấu hiệu nhận biết
Gặp những từ khoá này trong đề → nghĩ ngay đến Quy hoạch động 1 chiều
| Từ khoá trong đề | Tại sao gợi đến chủ đề này |
|---|---|
| số cách thực hiện | Đếm cách = DP đếm |
| min / max tại bước i | Optimization DP cổ điển |
| leo bậc / chia hộp | Cấu trúc đệ quy có lựa chọn |
| TLE với đệ quy thô | Cần memo hoặc tabulation |
Khi nào dùng
3 tình huống điển hình cần nghĩ đến chủ đề này
Bài toán có cấu trúc đệ quy và các bài toán con giao nhau (overlapping subproblems)
Cần tối ưu max / min hoặc đếm số lượng cách thực hiện
Bài toán có "lựa chọn" tại mỗi bước: lấy hoặc bỏ, leo bậc hoặc không
Khi nào KHÔNG dùng
3 tình huống chủ đề này không phù hợp — biết để tránh overuse
State có nhiều chiều phụ thuộc (vị trí + balance + remaining): cần DP 2D/3D, không phải 1D
Bài chỉ cần greedy chọn local optimal là đủ: DP overkill, greedy ngắn gọn hơn
Subproblem không lặp lại (không overlap): không phải DP — đây là divide & conquer / recursion thuần
Hình minh hoạ
Sơ đồ thể hiện cách giải của chủ đề này.
Mỗi ô lưu kết quả 1 bài con. Tính từ trái sang phải — ô sau ghép từ ô trước, không tính lại.
Code mẫu
Hầu hết bài cùng chủ đề dùng chung mẫu này — copy rồi điền phần logic riêng của bài.
function dp1d(n: number, /* params */): R {
const dp = new Array(n + 1).fill(initValue)
dp[0] = baseCase0 // base case
// dp[1] = baseCase1 // nếu cần
for (let i = 1; i <= n; i++) {
dp[i] = transition(dp[i - 1], dp[i - 2], /* ... */)
}
return dp[n]
}
// Tối ưu space O(1) nếu chỉ phụ thuộc 2 giá trị trước:
function dp1dOptimized(n: number): R {
let prev2 = baseCase0
let prev1 = baseCase1
for (let i = 2; i <= n; i++) {
const curr = transition(prev1, prev2)
prev2 = prev1
prev1 = curr
}
return prev1
}Recap
Tổng hợp nhanh — đọc lại trước phỏng vấn
Điểm chính
Xác định bài toán con: dp[i] là gì? Ý nghĩa của chỉ số i?
Viết công thức chuyển tiếp (recurrence) từ bài toán con nhỏ sang lớn
Tabulation (bottom-up) tiết kiệm stack hơn Memoization (top-down)
Nhiều bài DP 1D tối ưu được Space từ O(n) xuống O(1) bằng cách chỉ giữ 1–2 giá trị trước
Lỗi thường gặp
Khởi tạo sai base case (dp[0], dp[1]) → kết quả sai từ đầu
Viết sai chiều lặp (forward hay backward?) khi tối ưu space
Nhầm bài DP 1 chiều với bài cần DP 2 chiều (ma trận, substring)
Độ phức tạp
Time O(n), Space O(n) hoặc O(1) tuỳ bàiBài luyện theo chủ đề này
Phân theo độ khó — luyện tuần tự cho chắc tay