Hướng dẫn giải của Tổng các số từ L đến R
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.
Lời giải này đang bị ẩn cho đến khi bạn chọn mở ra.
Chúng tôi khuyên bạn nên tự thử giải bài trước. Việc mở lời giải có thể làm lộ mất ý tưởng chính trước khi bạn có cơ hội tự giải.
Bạn phải đăng nhập để mở lời giải này.
Đăng nhậpTác giả: , , ,
Editorial for dpd1: Tổng các số từ L đến R
Hiểu bài toán
Bài toán yêu cầu tính tổng các số nguyên dương trong đoạn [L, R] (1 ≤ L ≤ R ≤ 10^18) mà số lượng chữ số phân biệt của chúng không vượt quá k (k ≤ 10). Kết quả cần được in ra dưới dạng lấy dư cho 998244353.
Ví dụ: L=10, R=50, k=2. Các số từ 10 đến 50 có số chữ số phân biệt ≤ 2:
- 10, 11, ..., 19 (chữ số {1,0} hoặc {1})
- 20, ..., 29 (chữ số {2,0})
- ...
- 50 (chữ số {5,0})
- Các số như 100, 101... không nằm trong đoạn [10, 50]. Tổng các số này là 1230.
Bài toán này có thể được giải quyết bằng kỹ thuật Quy hoạch động có giới hạn (Digit DP).
Các cách tiếp cận
Cách Quy hoạch động theo Digit (Digit DP) - Gộp đếm và tính tổng
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MOD = 998244353;
ll L, R, k;
ll arr[22]; // Lưu các chữ số của số đang xét
ll pow10[22]; // Mảng lũy thừa 10
// F[id][mask][leadingZero]: pair<count, sum>
// id: vị trí chữ số đang xét (từ cao xuống thấp)
// mask: bitset thể hiện các chữ số đã xuất hiện
// leadingZero: boolean, true nếu các chữ số trước đều là 0
pair<ll, ll> f[22][1 << 10][2];
bool exist[22][1 << 10][2];
pair<ll, ll> Calc(int id, int mask, bool tight, bool leadingZero) {
if (id == 0) { // Đã xét hết các chữ số
// Nếu không phải toàn bộ số 0 và số lượng chữ số phân biệt <= k
if (!leadingZero && __builtin_popcount(mask) <= k) {
return {1, 0}; // 1 cách, tổng là 0 (vì đây là chữ số cuối)
}
return {0, 0};
}
// Nếu không bị giới hạn (tight) và đã tính rồi thì trả về kết quả nhớ
if (!tight && exist[id][mask][leadingZero]) {
return f[id][mask][leadingZero];
}
pair<ll, ll> res = {0, 0};
int maxDigit = tight ? arr[id] : 9;
for (int digit = 0; digit <= maxDigit; digit++) {
bool newTight = tight && (digit == maxDigit);
bool newLz = leadingZero && (digit == 0);
int newMask = mask;
// Nếu không phải là số 0 đứng đầu thì cập nhật mask
if (!newLz) {
newMask |= (1 << digit);
}
pair<ll, ll> cur = Calc(id - 1, newTight, newMask, newLz);
// Cập nhật số lượng
res.first = (res.first + cur.first) % MOD;
// Cập nhật tổng
// cur.second là tổng các số tạo thành từ các chữ số sau
// cur.first là số lượng các số đó
// digit * pow10[id-1] là giá trị đóng góp của chữ số hiện tại
res.second = (res.second + cur.second + (ll)digit * pow10[id - 1] % MOD * cur.first % MOD) % MOD;
}
if (!tight) {
exist[id][mask][leadingZero] = true;
f[id][mask][leadingZero] = res;
}
return res;
}
ll solve(ll n) {
if (n == 0) return 0;
// Phân tích n thành các chữ số
int len = 0;
while (n > 0) {
arr[++len] = n % 10;
n /= 10;
}
// Đảo mảng để arr[1] là chữ số cao nhất
for (int i = 1; i <= len / 2; i++) swap(arr[i], arr[len - i + 1]);
// Reset DP state (hoặc chỉ cần thay đổi cách đánh dấu)
// Ở đây ta dùng mảng exist, cần clear trước
memset(exist, 0, sizeof(exist));
return Calc(len, 0, true, true).second;
}
int main() {
// Tính lũy thừa 10
pow10[0] = 1;
for (int i = 1; i <= 19; i++) pow10[i] = (pow10[i-1] * 10) % MOD;
cin >> L >> R >> k;
ll ansR = solve(R);
ll ansL = solve(L - 1);
ll ans = (ansR - ansL + MOD) % MOD;
cout << ans << endl;
return 0;
}
- Time Complexity: O(19 * 1024 * 2 * 10) ~ 400,000 phép tính cho mỗi truy vấn (R hoặc L-1)
- Space Complexity: O(19 * 1024 * 2) ~ 40KB
Đây là phương pháp chuẩn để giải các bài toán đếm hoặc tính tổng trên một đoạn số với điều kiện phức tạp.
- Chuyển đổi bài toán: Tính tổng các số thỏa mãn trong đoạn [1, R] trừ đi tổng các số trong đoạn [1, L-1].
- Quy hoạch động: Hàm
Calc(id, mask, tight, leadingZero)trả về một cặp (số lượng, tổng) các số có thể tạo ra từ vị tríidtrở về sau.mask: Dùng bitset để lưu các chữ số đã xuất hiện (ví dụ bit 0 bật nếu chữ số 0 đã xuất hiện).tight: Biến giới hạn, nếu true thì chữ số hiện tại không được vượt quá chữ số tương ứng củan.leadingZero: Xử lý các số 0 đứng đầu (ví dụ số 5 là 00...05).
- Công thức chuyển trạng thái: Với mỗi chữ số
dcó thể chọn:- Gọi đệ quy để lấy (count, sum) của phần đuôi.
- Tổng mới = sum + d * 10^(id-1) * count + sum của đuôi.
- Độ phức tạp: Do
ktối đa là 10, nên số lượng các tổ hợp chữ số phân biệt là tổng tổ hợp của 10 chữ số, nhưng thực tếmaskcó 2^10 = 1024 trạng thái. Độ dài số là 19. Nên độ phức tạp chấp nhận được.
Cách Quy hoạch động tách biệt (Đếm và Tính tổng riêng)
#include <iostream>
#include <string>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int MOD = 998244353;
string S;
int K;
ll power_of_10[20];
// dp[pos][mask][tight][is_leading_zero]
// Lưu số lượng số hợp lệ
ll dp_cnt[20][1 << 10][2][2];
// Lưu tổng của các số hợp lệ
ll dp_sum[20][1 << 10][2][2];
bool visited_cnt[20][1 << 10][2][2];
bool visited_sum[20][1 << 10][2][2];
ll count_valid(int pos, int mask, bool tight, bool is_leading_zero) {
if (pos == S.size()) {
if (!is_leading_zero && __builtin_popcount(mask) <= K) return 1;
return 0;
}
if (visited_cnt[pos][mask][tight][is_leading_zero]) return dp_cnt[pos][mask][tight][is_leading_zero];
ll res = 0;
int upper = tight ? (S[pos] - '0') : 9;
for (int d = 0; d <= upper; ++d) {
bool new_tight = tight && (d == upper);
bool new_leading = is_leading_zero && (d == 0);
int new_mask = new_leading ? mask : (mask | (1 << d));
res = (res + count_valid(pos + 1, new_mask, new_tight, new_leading)) % MOD;
}
visited_cnt[pos][mask][tight][is_leading_zero] = true;
dp_cnt[pos][mask][tight][is_leading_zero] = res;
return res;
}
ll sum_valid(int pos, int mask, bool tight, bool is_leading_zero) {
if (pos == S.size()) {
return 0; // Base case sum is 0 (no digit added)
}
if (visited_sum[pos][mask][tight][is_leading_zero]) return dp_sum[pos][mask][tight][is_leading_zero];
ll res = 0;
int upper = tight ? (S[pos] - '0') : 9;
for (int d = 0; d <= upper; ++d) {
bool new_tight = tight && (d == upper);
bool new_leading = is_leading_zero && (d == 0);
int new_mask = new_leading ? mask : (mask | (1 << d));
// Nếu chọn d tại đây, d sẽ là chữ số có trọng số là 10^(len - 1 - pos)
// Ví dụ: 123, pos=0 (chữ số 1), tổng trọng số là 100. Pos=2 (chữ số 3), tổng trọng số là 1.
ll weight = power_of_10[S.size() - 1 - pos];
ll contribution = (d * weight) % MOD;
// Số lượng các số hợp lệ tạo thành từ phần đuôi
ll count = count_valid(pos + 1, new_mask, new_tight, new_leading);
// Tổng các số tạo thành từ phần đuôi (chữ số đầu là 0 trong phần đuôi)
ll sub_sum = sum_valid(pos + 1, new_mask, new_tight, new_leading);
// Tổng tại bước này = (d * weight * count) + sub_sum
ll term = (contribution * count) % MOD;
term = (term + sub_sum) % MOD;
res = (res + term) % MOD;
}
visited_sum[pos][mask][tight][is_leading_zero] = true;
dp_sum[pos][mask][tight][is_leading_zero] = res;
return res;
}
ll solve(ll n) {
if (n == 0) return 0;
S = to_string(n);
memset(visited_cnt, 0, sizeof(visited_cnt));
memset(visited_sum, 0, sizeof(visited_sum));
return sum_valid(0, 0, true, true);
}
int main() {
power_of_10[0] = 1;
for (int i = 1; i < 20; ++i) power_of_10[i] = (power_of_10[i - 1] * 10) % MOD;
ll L, R;
cin >> L >> R >> K;
ll ans = (solve(R) - solve(L - 1) + MOD) % MOD;
cout << ans << endl;
return 0;
}
- Time Complexity: Tương tự cách 1, nhưng do tách DP nên số phép tính tăng lên một chút nhưng vẫn nằm trong giới hạn.
- Space Complexity: Tương tự cách 1.
Phương pháp này tách biệt logic tính số lượng (count_valid) và logic tính tổng (sum_valid).
count_validchỉ đếm số lượng cách tạo ra số.sum_validsử dụng kết quả củacount_validđể tính tổng.
Công thức tính tổng:
Khi chọn chữ số d tại vị trí pos (có trọng số là 10^p):
Tổng = d * 10^p * (số lượng số hợp lệ ở phần đuôi) + (tổng các số hợp lệ ở phần đuôi).
Cách này giúp tách rõ ràng logic, nhưng thường dễ mắc lỗi hơn trong việc truyền trạng thái mask (phải đảm bảo không update mask nếu là leading zero) và việc gọi chéo nhau giữa 2 hàm DP có thể gây overhead.
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(19 * 1024 * 2 * 10) ~ 400,000 phép tính cho mỗi truy vấn (R hoặc L-1) | O(19 * 1024 * 2) ~ 40KB | Quy hoạch động theo Digit (Digit DP) - Gộp đếm và tính tổng |
| 2 | Tương tự cách 1, nhưng do tách DP nên số phép tính tăng lên một chút nhưng vẫn nằm trong giới hạn. | Tương tự cách 1. | Quy hoạch động tách biệt (Đếm và Tính tổng riêng) |
Bài học kinh nghiệm
- Bài toán có thể quy về bài toán đếm và tính tổng các số có đặc điểm nào đó trong đoạn [1, X] bằng cách sử dụng công thức: Kết quả = f(R) - f(L-1).
- Biến
mask(thường dùng bitmask 10 bit) là chìa khóa để kiểm soát điều kiện 'không quá k chữ số phân biệt'. - Kỹ thuật Digit DP yêu cầu xử lý các trường hợp đặc biệt của số 0 (số 0 tự nhiên, số 0 đứng đầu).
Lỗi thường gặp
- Sai sót khi chuyển đổi từ số nguyên sang chuỗi hoặc mảng chữ số.
- Quên xử lý trường hợp L=1 (tức là tính từ f(R) - f(0)).
- Tràn số nguyên khi tính tổng các số lớn (cần lấy dư liên tục trong quá trình tính toán).
Bình luận