Xóa chữ số
Đề bài
XOACHUSO – Xóa chữ số (Kỳ thi tuyển sinh 10 THPT chuyên TP. Hồ Chí Minh, năm học 2017 – 2018, môn Tin học)
Cho trước một số nguyên dương $N$. Gọi $L$ là số chữ số của $N$.
Yêu cầu: Xóa đi $K$ chữ số của $N$ để nhận được phần còn lại là số lớn nhất. Thứ tự các chữ số vẫn giữ nguyên.
Input:
- Dòng thứ nhất ghi số nguyên dương $N$ $(L \le 255)$.
- Dòng thứ hai ghi số nguyên dương $K$ $(K < L)$.
Output: số lớn nhất tìm được sau khi xóa $K$ chữ số của $N$.
Giới hạn thời gian: 1 giây, bộ nhớ: 256 MB.
58673
2873XOACHUSO.INP/XOACHUSO.OUT thì cứ kệ nó. Ví dụ: xoá 2 chữ số của $58673$ – xoá chữ số $5$ và $6$ còn lại $873$, đó là số lớn nhất.Lời mở đầu
$N$ có tới 255 chữ số nên không thể lưu trong long long – ta phải xử lý $N$ như một xâu ký tự. Bài này là ví dụ đẹp cho kỹ thuật tham lam: chữ số càng đứng trước thì càng "đáng giá", nên ta cố gắng làm cho các chữ số đầu tiên lớn nhất có thể. Mình đi từ cách vét cạn, tới tham lam "xoá từng chữ số", rồi tới cài đặt gọn bằng ngăn xếp.
Link này chỉ học viên đã tham gia tổ chức mới vào được. Học viên nên ưu tiên nộp ở đây vì sẽ được chấm nhanh hơn.
Nộp bài trên CLUE OJLộ trình lời giải, bấm vào từng phần để nhảy tới:
Bộ testBộ test chính thức có gì?
Bộ test có 100 test, đề không chia subtask nhưng độ dài $N$ chia làm 3 mức rõ ràng:
| Test | Số chữ số L của N | K (số chữ số xoá) | Độ dài kết quả |
|---|---|---|---|
| 1–10 | 9 | 1 … 8 | 1 … 8 |
| 11–20 | 18 | 3 … 16 | 2 … 15 |
| 21–100 | 255 | 1 … 254 | 1 … 254 |
- 80 test (21–100) có $N$ dài đúng 255 chữ số – không kiểu số nguyên nào chứa nổi, bắt buộc dùng
string. - $K$ trải từ 1 tới $L - 1$; có test chỉ giữ lại đúng 1 chữ số (kết quả là chữ số lớn nhất của $N$).
- Kết quả trong bộ test không bao giờ bắt đầu bằng chữ số 0, nên không phải lo chuyện bỏ số 0 ở đầu.
Kết quả chạy thật (giới hạn 1 giây):
| Mức | Điều kiện | Test | Cách 1 (vét cạn) | Cách 2 (xoá K lần) | Cách 3 (ngăn xếp) |
|---|---|---|---|---|---|
| Mức 1 | $L = 9$ (vừa int) | 1–10 | 10/10 | 10/10 | 10/10 |
| Mức 2 | $L = 18$ (vừa long long) | 11–20 | 10/10 | 10/10 | 10/10 |
| Mức 3 | $L = 255$ | 21–100 | 2/80 | 80/80 | 80/80 |
Ý tưởngTham lam: chữ số đứng trước càng lớn càng tốt
Sau khi xoá, mọi kết quả đều có cùng $L - K$ chữ số. Hai số cùng độ dài thì so sánh từ trái sang phải: số nào có chữ số đầu tiên khác nhau lớn hơn thì lớn hơn. Vì vậy chữ số đầu quan trọng hơn tất cả các chữ số sau cộng lại.
Cài đặt nhanh hơn bằng ngăn xếp: đọc lần lượt từng chữ số $c$; chừng nào còn lượt xoá và chữ số ở đỉnh ngăn xếp nhỏ hơn $c$ thì xoá đỉnh (vì để $c$ lên trước sẽ tốt hơn); sau đó đẩy $c$ vào. Cuối cùng nếu còn lượt xoá thì cắt bớt ở cuối. Ngăn xếp luôn là một dãy không tăng – các chữ số lớn được đẩy lên đầu.
Em nhập $N$, $K$ rồi bấm Tiếp để xem ngăn xếp hoạt động (ô vàng là chữ số đang xét, ô đỏ là chữ số bị xoá):
Cách 1Vét cạn mọi cách giữ/xoá – $L$ nhỏ
Với mỗi chữ số ta có 2 lựa chọn: giữ hoặc xoá. Dùng đệ quy (quay lui) thử hết các cách xoá đúng $K$ chữ số, mỗi cách cho một xâu kết quả dài $L - K$; lấy xâu lớn nhất. Vì các xâu cùng độ dài nên so sánh xâu cur > best cũng chính là so sánh số.
- Hàm
thu(i, xoa): đang xét chữ số $N_i$, đã xoáxoachữ số;curchứa các chữ số đã giữ. - Nhánh "giữ":
push_backrồi gọi đệ quy, quay về thìpop_backđể trả lại trạng thái (quay lui). - Nhánh "xoá" chỉ được đi khi
xoa < K. Khi xét hết $L$ chữ số, chỉ nhận cách nào xoá đủ $K$ chữ số.
Số cách chọn là $\binom{L}{K}$: với $L = 18$ tối đa $48\,620$ cách – rất nhanh; với $L = 255$ thì là con số khổng lồ. Bản Python mình dùng itertools.combinations cho gọn.
Kết quả chạy thật: Bản C++ đúng 22/100 test, quá thời gian ở test 21–38, 40–56, 58–100; test đúng chạy lâu nhất 0.05s. Bản Python đúng 22/100 test, quá thời gian ở test 21–56, 58–99; test đúng chạy lâu nhất 0.06s. Cách này ăn trọn mức 1 và 2 (test 1–20), và còn "ăn may" thêm test 39, 57 vì ở đó $K \le 2$ nên số cách chọn chỉ cỡ $\binom{255}{2} = 32\,385$.
Cách 2Tham lam: xoá lần lượt K chữ số – $O(K \cdot L)$
Làm đúng nhận xét tham lam: lặp $K$ lần, mỗi lần tìm chữ số đầu tiên nhỏ hơn chữ số đứng sau và xoá nó (không có thì xoá chữ số cuối).
- Vòng
whiledừng ở vị tríiđầu tiên cóN[i] < N[i + 1]; nếu đi tới chữ số cuối thìilà chữ số cuối. N.erase(i, 1)xoá 1 ký tự ở vị tríi.- Vì sao xoá như vậy là tối ưu? Xét đoạn đầu không tăng $N_0 \ge N_1 \ge \ldots \ge N_i < N_{i+1}$: xoá chữ số nào trước $i$ đều làm chữ số ở vị trí đó nhỏ đi hoặc giữ nguyên, còn xoá $N_i$ làm vị trí $i$ tăng lên – nên đây là lựa chọn tốt nhất cho 1 lần xoá, và lặp lại $K$ lần vẫn đúng.
Với $L = 255$ chỉ cỡ $255^2 \approx 65\,000$ phép so sánh – quá nhẹ.
Kết quả chạy thật: Bản C++ đúng 100/100 test; test đúng chạy lâu nhất 0.01s. Bản Python đúng 100/100 test; test đúng chạy lâu nhất 0.04s.
Cách 3Tham lam + ngăn xếp – $O(L)$
Cách 2 mỗi lần xoá lại đi từ đầu xâu. Dùng ngăn xếp thì chỉ cần đi một lượt: chữ số nào bị "đè" bởi chữ số lớn hơn đứng sau thì bị xoá ngay.
stlà mộtstringdùng như ngăn xếp:back()là đỉnh,pop_back()lấy ra,push_back()đẩy vào.- Vòng
whilexoá đỉnh khi còn lượt (K > 0) và đỉnh nhỏ hơnc– đúng là "xoá chữ số nhỏ hơn chữ số đứng sau nó". - Duyệt xong mà vẫn còn $K$ lượt xoá thì ngăn xếp là dãy không tăng, ta bỏ $K$ chữ số cuối bằng
resize. - Mỗi chữ số được đẩy vào 1 lần, lấy ra tối đa 1 lần nên tổng thời gian $O(L)$.
Kết quả chạy thật: Bản C++ đúng 100/100 test; test đúng chạy lâu nhất 0.01s. Bản Python đúng 100/100 test; test đúng chạy lâu nhất 0.03s.
Tổng kếtKết quả chạy thật trên bộ test
| Cách | Ngôn ngữ | Đúng | Lâu nhất | Ghi chú |
|---|---|---|---|---|
| 1. Vét cạn mọi cách giữ/xoá | C++ | 22/100 | > 1s | Quá thời gian test 21–38, 40–56, 58–100 |
| Python | 22/100 | > 1s | Quá thời gian test 21–56, 58–99 | |
| 2. Tham lam: xoá K lần | C++ | 100/100 | 0.01s | Full điểm |
| Python | 100/100 | 0.04s | Full điểm | |
| 3. Tham lam + ngăn xếp | C++ | 100/100 | 0.01s | Full điểm |
| Python | 100/100 | 0.03s | Full điểm |
Nhận xét
Đăng nhận xét