Ô Vuông
Đề bài
Cho các số nguyên dương $K, N$ với $K < N$ và bảng ô vuông $2 \times N$ như hình bên dưới.
Tính số cách chọn $K$ ô vuông trong bảng sao cho không có hai ô vuông nào chung đỉnh. Do kết quả có thể rất lớn, in ra kết quả sau khi đã chia lấy dư cho $10^9 + 7$.
Biết rằng, hai ô có chung đỉnh là hai ô có chung ít nhất một trong bốn đỉnh. Bên dưới là 2 ví dụ về hai ô có chung đỉnh:
Giới hạn chung: $1 \le K < N \le 10^5$.
Ràng buộc:
- Ràng buộc 1 (40% số điểm): $1 \le K < N \le 10$.
- Ràng buộc 2 (20% số điểm): $10 < K < N \le 10^3$.
Đầu vào: một dòng duy nhất chứa hai số nguyên $K$ và $N$.
Đầu ra: một số duy nhất là đáp án của yêu cầu đề bài. Lưu ý, đáp án là kết quả sau khi đã chia lấy dư cho $10^9 + 7$.
2 202 100019940041 10002000Lưu ý nhỏ: đề ghi $K < N$ nhưng ví dụ 1 lại có $K = N = 2$; trong bộ test chính thức thì luôn có $K < N$. Các lời giải bên dưới vẫn đúng khi $K = N$.
Giải thích ví dụ 3: $K = 1$ thì chọn ô nào cũng được, bảng $2 \times 1000$ có $2000$ ô. Ví dụ 1: bảng $2 \times 2$ thì 4 ô đôi một chung đỉnh, không thể chọn 2 ô → $0$ cách.
Lời mở đầu
Đây là bài thứ 4 của đề Tin học trẻ bảng C2 Cần Thơ 2024 – một bài quy hoạch động đếm rất "sạch". Mình sẽ đi theo đúng lộ trình của chương: vét cạn mọi cách chọn để ăn 40% điểm, rồi tìm ra quan sát quan trọng để viết công thức quy hoạch động $f[i][j]$, và cuối cùng biến công thức đó thành một tổ hợp $O(N)$ đúng với mọi $K$.
Bấm vào nút bên dưới để nộp bài và được hệ thống CLUE OJ chấm tự động.
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ì?
| Test | $K$ | $N$ | Số test đáp án 0 |
|---|---|---|---|
| 1–40 | 1 … 9 | 3 … 10 | 16 |
| 41–60 | 13 … 47 | 109 … 1.000 | 0 |
| 61–100 | 11 … 50 | 10.000 … 100.000 | 0 |
Bộ test chia đúng theo ràng buộc của đề: 40 test đầu có $N \le 10$ (đủ mọi cặp $1 \le K < N \le 10$), 20 test kế có $N \le 1000$, 40 test cuối có $N$ = $10^4$ hoặc $10^5$. Có 20 test đáp án bằng 0 (ví dụ $K = 3, N = 4$) – sau khi hiểu ý tưởng các bạn sẽ thấy ngay vì sao. Một điểm đáng chú ý: tuy đề cho $K$ tới gần $10^5$, nhưng trong mọi test $K \le 50$.
| Phần | Điều kiện | Test | Cách 1 (vét cạn) | Cách 2 (QHĐ bảng) | Cách 3 (tổ hợp) |
|---|---|---|---|---|---|
| Ràng buộc 1 | $N \le 10$ | 1–40 | qua | qua | qua |
| Ràng buộc 2 | $N \le 10^3$ | 41–60 | không chạy nổi | qua | qua |
| Còn lại | $N \le 10^5$ | 61–100 | không chạy nổi | qua (nhờ $K \le 50$) | qua |
Cách 1Vét cạn mọi tập ô (ràng buộc 1)
Bảng có $2N$ ô. Đánh số ô hàng trên cột $c$ là bit $c$, ô hàng dưới cột $c$ là bit $N + c$ (cột đánh số từ 0). Mỗi số mask từ $0$ đến $2^{2N} - 1$ là một tập ô. Ta chỉ giữ các mask có đúng $K$ bit 1, rồi kiểm tra điều kiện "không chung đỉnh". Nhìn kỹ hình đề bài thì hai ô chung đỉnh chỉ có thể là:
- hai ô cùng một cột (chung cạnh ngang), hoặc
- hai ô ở hai cột kề nhau – kể cả khác hàng, vì khi đó chúng chạm nhau ở góc.
Hai ô cách nhau từ 2 cột trở lên thì không bao giờ chạm nhau. Vậy chỉ cần kiểm tra từng cột và từng cặp cột kề nhau.
Kết quả chạy thật: bản C++ đúng 40/100 test – trọn ràng buộc 1, chậm nhất 0,009 giây. Từ test 41 trở đi $N \ge 109$ thì $2^{2N}$ là con số khổng lồ (và cũng không còn vừa trong kiểu int), cách này không chạy nổi. Bản Python cũng qua cả 40 test đầu, chậm nhất 0,35 giây.
Ý tưởng QHĐMỗi cột chọn tối đa 1 ô, 2 cột kề nhau không cùng chọn
Từ nhận xét ở cách 1, bài toán trở thành: chọn ra $K$ cột trong $N$ cột sao cho không có 2 cột kề nhau, rồi ở mỗi cột được chọn quyết định lấy ô trên hay ô dưới (2 cách). Giờ ta đếm bằng quy hoạch động theo từng cột từ trái sang phải.
Gọi $f[i][j]$ = số cách chọn $j$ ô trong $i$ cột đầu tiên thoả điều kiện. Xét cột cuối cùng – cột $i$:
- Cột $i$ để trống: $j$ ô đều nằm trong $i - 1$ cột đầu → $f[i-1][j]$ cách.
- Cột $i$ có 1 ô (trên hoặc dưới – 2 cách): khi đó cột $i - 1$ bắt buộc trống, $j - 1$ ô còn lại nằm trong $i - 2$ cột đầu → $2 \cdot f[i-2][j-1]$ cách.
Em nhập $K$, $N$ rồi bấm Tiếp để xem bảng $f$ được điền từng ô. Ô vàng đang tính, hai ô xanh tím là $f[i-1][j]$ và $f[i-2][j-1]$; hàng dưới vẽ bảng $2 \times N$ để thấy cột $i$ (vàng) và cột $i-1$ phải để trống (gạch):
Cách 2Quy hoạch động bảng $f[i][j]$ – $O(N \cdot K)$
Cài đặt đúng công thức trên. Thêm một nhận xét để thoát sớm: $K$ cột không kề nhau thì cần ít nhất $2K - 1$ cột (chọn – trống – chọn – … – chọn), nên nếu $2K - 1 > N$ đáp án chắc chắn là $0$. Đây chính là 20 test có đáp án 0 trong bộ test.
- Mọi phép cộng đều lấy
% MODngay: $f[i-1][j]$ $+ 2 f[i-2][j-1] < 3 \cdot 10^9$ $+ 21$ vẫn vừalong long. - Bảng có $(N + 1)(K + 1)$ ô. Với bộ test thật ($K \le 50$) bảng chỉ khoảng $10^5 \times 51$ số
long long≈ 41 MB và $5 \cdot 10^6$ phép tính – rất nhẹ. - Nhưng nếu đúng như giới hạn đề ($K$ tới khoảng $5 \cdot 10^4$ mà đáp án vẫn khác 0) thì bảng có tới $5 \cdot 10^9$ ô – không đủ bộ nhớ và thời gian. Ta cần làm tốt hơn: cách 3.
Kết quả chạy thật: bản C++ đúng 100/100 test, chậm nhất 0,054 giây (vì bộ test chỉ có $K \le 50$). Bản Python cùng thuật toán được 91/100 test: 9 test có $N = 10^5$ và $K$ khoảng 35–50 cần tới $5 \cdot 10^6$ phép tính, Python chạy 1,0–1,5 giây nên quá thời gian – các bạn học Python dùng cách 3 nhé.
Cách 3Đếm trực tiếp bằng tổ hợp – $O(N)$ với mọi $K$
Bài toán "chọn $K$ cột đôi một không kề nhau trong $N$ cột" có lời giải đẹp: giữa 2 cột được chọn liên tiếp phải có ít nhất 1 cột trống. Ta "rút" bớt 1 cột trống ngay sau mỗi cột được chọn (trừ cột cuối) – tổng cộng rút $K - 1$ cột. Khi đó ta còn $N - K + 1$ cột và chỉ cần chọn $K$ cột bất kỳ trong đó; ngược lại, từ một cách chọn $K$ cột bất kỳ trong $N - K + 1$ cột, chèn lại 1 cột trống sau mỗi cột được chọn (trừ cột cuối) là được một cách hợp lệ. Hai bên tương ứng 1–1, nên:
Muốn tính $C_n^k = \dfrac{n!}{k!\,(n-k)!}$ theo modulo số nguyên tố $p = 10^9 + 7$, ta không chia trực tiếp được mà nhân với nghịch đảo modulo: theo định lý Fermat nhỏ $a^{-1} \equiv a^{p-2} \pmod p$, tính bằng lũy thừa nhanh. Mình tính sẵn mảng giai thừa fact, nghịch đảo inv[N] = fact[N]^{p-2}, rồi lùi dần inv[i-1] = inv[i] * i.
while của lũy thừa nhanh thành 1 bước để khung chạy tay không quá dài.Kết quả chạy thật: bản C++ đúng 100/100 test, chậm nhất 0,006 giây; bản Python (chỉ cần nhân $K$ số) cũng 100/100, chậm nhất 0,021 giây.
- Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
- Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
- Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
- Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
- Dễ nản giữa chừng khi không có ai theo sát.
- Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
- Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
- Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
- Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.
Nhận xét
Đăng nhận xét