DP chữ số
Trang này sẽ giới thiệu ngắn gọn về Quy hoạch động trên số (Số vị DP, Digit DP).
Dẫn nhập
"Số vị" nghĩa là tách một số ra thành từng chữ số theo từng hàng đơn vị, chục, trăm, nghìn,... và quan tâm đến giá trị của từng chữ số đó. Nếu tách theo hệ thập phân, mỗi chữ số sẽ nằm trong khoảng 0~9, các hệ cơ số khác cũng tương tự.
Số vị DP (Digit DP): là kỹ thuật dùng để giải quyết một lớp bài toán đặc biệt, thường có các đặc điểm sau:
-
Yêu cầu đếm số lượng các số thỏa mãn điều kiện nào đó (tức là mục tiêu cuối cùng là đếm số lượng);
-
Các điều kiện này sau khi chuyển đổi có thể sử dụng tư duy "số vị" để kiểm tra và đánh giá;
-
Đầu vào thường cho một đoạn số (đôi khi chỉ cho giới hạn trên);
-
Giới hạn trên rất lớn (ví dụ \(10^{18}\)), vét cạn sẽ bị TLE.
Nguyên lý cơ bản của số vị DP:
Xét cách con người đếm số, cách đơn giản nhất là tăng dần từng số một. Nhưng với các số nhiều chữ số, quá trình này có nhiều phần lặp lại. Ví dụ, đếm từ 7000 đến 7999, từ 8000 đến 8999, từ 9000 đến 9999, ta thấy ba đoạn này đều có ba chữ số cuối chạy từ 000 đến 999, chỉ khác nhau ở chữ số hàng nghìn. Ta có thể gom các trường hợp này lại, lưu kết quả đếm vào một mảng chung. Mảng này (thường gọi là mảng trạng thái) sẽ được thiết kế tùy theo yêu cầu bài toán, và dùng quy hoạch động hoặc truy hồi để chuyển trạng thái.
Trong số vị DP, ta thường dùng các kỹ thuật đếm thông thường, ví dụ tách đoạn \([l, r]\) thành hai phần rồi lấy hiệu: \(\mathit{ans}_{[l, r]} = \mathit{ans}_{[0, r]}-\mathit{ans}_{[0, l - 1]}\).
Sau khi có mảng trạng thái chung, việc còn lại là thống kê đáp án. Có thể dùng truy hồi có nhớ (memoization) hoặc lặp lại (iterative DP). Để không bị trùng hoặc thiếu, ta sẽ duyệt từng chữ số từ cao xuống thấp, xét mỗi chữ số có thể điền giá trị nào, rồi dùng mảng trạng thái để cộng dồn đáp án.
Tiếp theo, ta sẽ xem xét một số bài toán cụ thể.
Ví dụ 1
Ví dụ 1 Luogu P2602 Đếm chữ số
Đề bài: Cho hai số nguyên dương \(a, b\), hãy đếm trong đoạn \([a, b]\) mỗi chữ số (digit) xuất hiện bao nhiêu lần.
Cách 1
Giải thích
Nhận thấy với các số có đúng \(\mathit{i}\) chữ số, mỗi chữ số xuất hiện số lần như nhau. Đặt mảng \(\mathit{dp}_i\) là số lần mỗi chữ số xuất hiện trong tất cả các số có \(i\) chữ số (chưa xét số 0 ở đầu). Khi đó, có công thức: \(\mathit{dp}_i=10 \times \mathit{dp}_{i−1}+10^{i−1}\), trong đó phần đầu là đóng góp từ các chữ số trước, phần sau là đóng góp từ chữ số hiện tại.
Sau khi có mảng \(\mathit{dp}\), ta sẽ tách giới hạn trên thành từng chữ số, duyệt từ cao xuống thấp. Nếu không "bám sát" giới hạn trên, các chữ số phía sau có thể điền tự do. Nếu bám sát, chỉ được điền từ 0 đến giá trị của giới hạn trên ở vị trí đó. Cuối cùng, cần xử lý trường hợp số 0 ở đầu: khi chữ số thứ \(i\) là 0 ở đầu, tức là các chữ số trước cũng đều là 0, ta đã đếm thừa các trường hợp này, cần trừ đi.
Cài đặt
Code tham khảo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | |
Cách 2
Giải thích
Bài này cũng có thể dùng truy hồi có nhớ (memoization). \(\mathit{dp}_i\) là đáp án với số có \(i\) chữ số, không bám sát giới hạn trên và không có số 0 ở đầu.
Chi tiết xem trong chú thích code.
Quy trình
Code tham khảo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | |
Ví dụ 2
Ví dụ 2 HDU 2089 Không có 62
Đề bài: Đếm số lượng số trong một đoạn mà không có chữ số 4 và không có cặp liên tiếp 62.
Giải thích
Không có số 4 thì khi duyệt chỉ cần bỏ qua 4 là đảm bảo hợp lệ, không cần nhớ trạng thái. Nhưng với cặp 62, liên quan đến hai chữ số liên tiếp, nên trạng thái phải ghi nhớ chữ số trước có phải là 6 hay không. Đặt \(\mathit{dp}_{\mathit{pos},\mathit{sta}}\) với \(\mathit{sta}\) chỉ cần 0 hoặc 1 (không phải 6 hoặc là 6).
Cài đặt
Code tham khảo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | |
Ví dụ 3
Ví dụ 3 SCOI2009 windy số
Đề bài: Cho đoạn \([l,r]\), đếm số lượng số không có số 0 đầu và hai chữ số liên tiếp chênh lệch ít nhất 2.
Giải thích
Chuyển bài toán về dạng: đặt \(\mathit{ans}_i\) là số lượng số thỏa mãn trong \([1,i]\), đáp án là \(\mathit{ans}_r-\mathit{ans}_{l-1}\).
Với một số nhỏ hơn \(n\), khi duyệt từ cao xuống thấp, sẽ có một vị trí mà chữ số ở đó nhỏ hơn chữ số tương ứng của \(n\), các vị trí trước đó đều giống \(n\).
Ta định nghĩa \(f(i,st,op)\) là số lượng số khi đang ở vị trí \(i\) (từ cao xuống), trạng thái trước là \(st\), và \(op\) là quan hệ với giới hạn trên (\(op=1\) là bằng, \(op=0\) là nhỏ hơn). Ở bài này, trạng thái là giá trị chữ số trước. Ở các bài khác, trạng thái có thể là tổng chữ số, gcd, phần dư,... hoặc kết hợp nhiều thứ.
Công thức chuyển: $$ f(i,st,op)=\sum_{k=1}^{\mathit{maxx}} f(i+1,k,op=1~ \operatorname{and}~ k=\mathit{maxx} )\quad (|\mathit{st}-k|\ge 2) $$
Ở đây \(k\) là chữ số hiện tại, \(\mathit{maxx}\) là giá trị lớn nhất có thể điền ở vị trí đó (nếu bám sát giới hạn trên thì là chữ số tương ứng của \(n\), ngược lại là 9).
Có thể dùng truy hồi có nhớ để tránh tính trùng.
Cài đặt
Code tham khảo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | |
Ví dụ 4
Ví dụ 4.SPOJMYQ10
Đề bài: Nếu viết tất cả các số trong đoạn \([n,m]\), có bao nhiêu số nhìn trong gương vẫn giống hệt như ban đầu? (\(n,m<10^{44}, T<10^5\))
Giải thích
Chú ý: chỉ có \(0,1,8\) là đối xứng gương với chính nó. Vậy "giống hệt" ở đây là số chỉ gồm \(0,1,8\) và là số đối xứng (palindrome).
Trong quá trình số vị DP, chỉ được chọn \(0,1,8\).
Vì số rất lớn, không thể dùng \([1,m]-[1,n-1]\) như thường lệ (so sánh số lớn phức tạp), mà phải kiểm tra riêng \(n\) có hợp lệ không: \([n,m]=[1,m]-[1,n]+\mathrm{check}(n)\).
Để kiểm tra đối xứng, cần lưu lại các chữ số đã chọn. Khi chưa đi quá nửa, chỉ cần không vượt giới hạn trên; khi đã đi quá nửa, phải kiểm tra đối xứng với vị trí tương ứng.
Lưu ý: phần memoization không dùng memset để tránh TLE.
Cài đặt
Code tham khảo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | |
Ví dụ 5
Ví dụ 5.P3311 Đếm số
Đề bài: Gọi một số nguyên dương \(x\) là số may mắn nếu trong biểu diễn thập phân của nó không chứa bất kỳ chuỗi nào trong tập \(S\) dưới dạng chuỗi con. Ví dụ \(S = \{22, 333, 0233\}\) thì \(233233\) là số may mắn, còn \(23332333\), \(2023320233\), \(32233223\) không phải. Cho \(n\) và \(S\), đếm số lượng số may mắn không vượt quá \(n\). Kết quả lấy mod \(10^9 + 7\).
\(1 \leq n<10^{1201}, 1 \leq m \leq 100, 1 \leq \sum_{i = 1}^m |s_i| \leq 1500, \min_{i = 1}^m |s_i| \geq 1\). \(n\) không có số 0 đầu, nhưng \(s_i\) có thể có.
Giải thích
Nhận thấy nếu coi số là chuỗi, đây là bài toán nhiều mẫu (multi-pattern matching), nên dùng tự động hóa AC (AC Automaton).
Trong số vị DP thông thường, ta duyệt từng chữ số từ cao xuống thấp, mỗi lần xét điền chữ số nào. Ở bài này, trạng thái còn bao gồm vị trí hiện tại trên cây AC. Đặt \(f(i,j,0/1)\) là số cách đã điền \(i\) chữ số, đang ở nút \(j\) trên AC, và có bám sát giới hạn trên không.
Điều kiện "không chứa" chỉ cần đánh dấu các nút kết thúc mẫu trên AC, nếu gặp thì bỏ qua.
Chuyển trạng thái khá trực tiếp, xem chi tiết trong code.
Cài đặt
Code tham khảo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | |
Bài này giúp hiểu sâu hơn về nguyên lý số vị DP.
Bài tập
Ahoi2009 self Phân phối đồng loại
Last updated on this page:, Update history
Found an error? Want to help improve? Edit this page on GitHub!
Contributors to this page:OI-wiki
All content on this page is provided under the terms of the CC BY-SA 4.0 and SATA license, additional terms may apply