Quy Hoạch Động Chữ Số (Digit DP)
Nội dung bài học
1. Khái niệm & bản chất của Quy hoạch động chữ số
Quy hoạch động chữ số (Digit DP) là phương pháp chuyên dùng để giải quyết các bài toán: Đếm số lượng số nguyên trong đoạn $[L, R]$ thỏa mãn một tính chất chữ số đặc biệt (ví dụ: tổng chữ số bằng $K$, không chứa chữ số 4 và 7, các chữ số tăng dần, số nguyên tố, số chia hết cho $D$).
Với $L, R \le 10^{18}$, duyệt trâu từng số mất $10^{18}$ phép tính $\implies$ TLE.
Digit DP giải quyết bài toán bằng cách:
- Chuyển đổi bài toán đoạn: $\text{Count}([L, R]) = f(R) - f(L - 1)$ với $f(X)$ là số lượng số thỏa mãn trong $[0, X]$.
- Biểu diễn số $X$ thành mảng các chữ số $D_0, D_1, \dots, D_{M-1}$ ($M \le 19$).
- Xây dựng số từ trái sang phải qua hàm đệ quy có nhớ
memo[index][tight][leading_zero][state].
2. Các tham số trạng thái
index(Vị trí chữ số hiện tại): Duyệt từ chữ số đầu tiên (cao nhất) $0$ đến chữ số cuối cùng $M - 1$.tight(Cờ giới hạn cận trên): -tight = true: Các chữ số phía trước đều đã chọn trùng khít với các chữ số của $X$. Chữ số hiện tại chỉ được chọn từ $0$ đến $D_{index}$. -tight = false: Đã có ít nhất một chữ số phía trước chọn nhỏ hơn $D$, số hiện tại được tự do chọn từ $0$ đến $9$.leading_zero(Cờ số 0 vô nghĩa ở đầu): Xác định xem ta đã bắt đầu viết số thực tế chưa hay vẫn đang là các số 0 vô nghĩa (ảnh hưởng đến việc đếm chữ số 0).state(Trạng thái đặc thù của bài toán): Ví dụ tổng các chữ số đã chọn, số dư khi chia cho $K$, mặt nạ bit của các chữ số đã xuất hiện.
3. Mẫu cài đặt chuẩn thi đấu: Đếm số có tổng chữ số bằng $S$ trong đoạn $[L, R]$
#include <bits/stdc++.h>
using namespace std;
string num_str;
long long dp[20][2][200]; // dp[index][tight][sum]
int target_sum;
long long digit_dp(int idx, bool tight, int current_sum) {
if (idx == num_str.size()) {
return (current_sum == target_sum ? 1 : 0);
}
if (dp[idx][tight][current_sum] != -1) {
return dp[idx][tight][current_sum];
}
int limit = (tight ? (num_str[idx] - '0') : 9);
long long total = 0;
for (int digit = 0; digit <= limit; ++digit) {
bool next_tight = tight && (digit == limit);
total += digit_dp(idx + 1, next_tight, current_sum + digit);
}
return dp[idx][tight][current_sum] = total;
}
long long count_valid(long long x) {
if (x < 0) return 0;
num_str = to_string(x);
memset(dp, -1, sizeof(dp));
return digit_dp(0, true, 0);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
long long L, R;
if (!(cin >> L >> R >> target_sum)) return 0;
cout << count_valid(R) - count_valid(L - 1) << "\n";
return 0;
}
4. Ranh giới áp dụng
| Dạng Bài | Cận Biên $R$ | Kỹ Thuật Tối Ưu | Độ Phức Tạp |
|---|---|---|---|
| Đếm số theo tính chất chữ số | $R \le 10^{18}$ | Digit DP | $\mathcal{O}(\text{Length}(R) \times \text{States} \times 10) \approx 19 \times 200 \times 10 < 10^5$ |
| Đếm số theo tính chất đại số lớn | $R \le 10^9$ | Sàng / Toán học / Bù trừ PIE | $\mathcal{O}(\sqrt{R})$ hoặc $\mathcal{O}(1)$ |