Xếp hậu (Tin học trẻ toàn quốc 2024 – Vòng khu vực – Bảng B)
Đề bài
Có một bàn cờ $n \times n$, hàng đánh số $1..n$ từ trên xuống, cột đánh số $1..n$ từ trái sang. Ô ở hàng $i$, cột $j$ gọi là ô $(i, j)$ và có một trọng số $w_{ij}$ (số nguyên không âm).
Ta cần đặt đúng $k$ quân hậu lên bàn cờ sao cho không có hai quân nào chiếu nhau. Hai quân hậu chiếu nhau khi chúng cùng hàng, hoặc cùng cột, hoặc cùng một đường chéo (chéo xuôi hay chéo ngược đều tính).
Yêu cầu: gọi $s$ là tổng trọng số của các ô có đặt hậu. Hãy đặt sao cho $s$ lớn nhất và in ra giá trị đó.
Dữ liệu vào:
- Dòng 1: hai số nguyên $n, k$ ($1 \le k \le n \le 8$).
- $n$ dòng tiếp theo: dòng thứ $i$ gồm $n$ số nguyên không âm $w_{i1}, w_{i2}, \dots, w_{in}$ (mỗi số không quá $10^6$).
Dữ liệu đảm bảo luôn có cách đặt $k$ quân hậu không chiếu nhau.
Kết quả: một số nguyên là giá trị lớn nhất của $s$.
Subtask:
- Subtask 1 (30%): $k = 1$;
- Subtask 2 (30%): $k \le 3$;
- Subtask 3 (40%): không có ràng buộc gì thêm.
Giới hạn thời gian: 1 giây. Dữ liệu nhập từ bàn phím, in ra màn hình.
3 2
1 0 2
0 0 2
0 2 04Giải thích: đặt 2 quân hậu ở ô $(1, 3)$ và $(3, 2)$: hai ô này khác hàng, khác cột và không cùng đường chéo (hiệu hàng là 2, hiệu cột là 1). Tổng trọng số $2 + 2 = 4$. Không có cách nào được nhiều hơn: ô $(2, 3)$ cũng có trọng số 2 nhưng nó cùng cột với $(1, 3)$ và cùng đường chéo với $(3, 2)$.
Lời mở đầu
Bài toán 8 quân hậu là bài "nhập môn quay lui" kinh điển. Đề Tin học trẻ 2024 đổi đi một chút: không bắt đặt đủ $n$ quân mà chỉ đúng $k$ quân, và mỗi ô có trọng số, cần tổng lớn nhất. Vì $n \le 8$ nên bài này hoàn toàn là bài vét cạn có suy nghĩ: vét cạn "ngây thơ" thì chậm, còn vét cạn theo hàng có cắt nhánh thì chạy trong nháy mắt. Mình đi từ cách dễ nhất (chỉ ăn subtask 1) tới cách full điểm.
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 có gì?
Bộ test trên CLUE do mình tự sinh theo đúng giới hạn và tỉ lệ điểm subtask của đề: 20 test (test 1 là ví dụ của đề, thuộc subtask 2 vì $k = 2$). Mỗi bảng có đủ kiểu: trọng số ngẫu nhiên, toàn số 0, toàn số $10^6$, bàn cờ có vài ô rất lớn nằm chiếu nhau để "bẫy" lời giải tham lam.
| Nhóm | Test | Các giá trị $n$ | Các giá trị $k$ | Trọng số lớn nhất | Bàn cờ mọi ô bằng nhau | Đáp án lớn nhất |
|---|---|---|---|---|---|---|
| Subtask 1 | 2–8 | 1, 2, 5, 7, 8 | 1 | 1.000.000 | test 2, 4, 6 | 1.000.000 |
| Subtask 2 | 1, 9–12 | 3, 6, 8 | 2, 3 | 1.000.000 | không có | 2.963.462 |
| Subtask 3 | 13–20 | 4, 7, 8 | 4, 5, 6, 7, 8 | 1.000.000 | test 20 | 8.000.000 |
Kết quả chạy thật các cách bên dưới theo từng subtask (ô xanh = đúng hết các test của subtask đó):
| Subtask | Điều kiện | Test | Cách 1 chỉ k = 1 | Cách 2 vét cạn bộ ô | Cách 2 Python | Cách 3 quay lui | Cách 3 Python |
|---|---|---|---|---|---|---|---|
| Subtask 1 (30%) | $k = 1$ | 2–8 | 7/7 | 7/7 | 7/7 | 7/7 | 7/7 |
| Subtask 2 (30%) | $k \le 3$ | 1, 9–12 | 0/5 | 5/5 | 5/5 | 5/5 | 5/5 |
| Subtask 3 (40%) | không ràng buộc thêm | 13–20 | 0/8 | 4/8 | 1/8 | 8/8 | 8/8 |
Ý tưởngMỗi hàng có nhiều nhất một quân hậu
Nhận xét quan trọng nhất: hai quân hậu cùng hàng thì chiếu nhau, nên mỗi hàng có tối đa 1 quân hậu. Vậy ta đi lần lượt từng hàng $1, 2, \dots, n$, với mỗi hàng chỉ có $n + 1$ lựa chọn:
- bỏ trống hàng này, hoặc
- đặt 1 quân hậu vào một cột $j$ chưa bị chiếu (cột $j$ chưa có hậu, hai đường chéo đi qua ô đó chưa có hậu).
Để kiểm tra "bị chiếu" trong $O(1)$, ta đánh dấu 3 mảng: cot[j], cheo1[i + j] (các ô trên cùng đường chéo ngược có cùng $i + j$) và cheo2[i - j + n - 1] (cùng đường chéo xuôi có cùng $i - j$; cộng $n - 1$ để chỉ số không âm).
Khi đã đặt đủ $k$ quân thì cập nhật đáp án. Thêm một phép cắt nhánh: nếu số hàng còn lại $n - i$ ít hơn số quân còn phải đặt $k - cnt$ thì đi tiếp cũng vô ích, quay lui luôn. Em nhập bàn cờ rồi bấm Tiếp để xem quá trình quay lui (ô đỏ nhạt = đang bị chiếu, hàng viền vàng = hàng đang xét):
Cách 1k = 1: chỉ cần ô lớn nhất (Subtask 1)
Khi $k = 1$ thì chỉ có một quân hậu, không có chuyện chiếu nhau. Đáp án chính là trọng số lớn nhất của bàn cờ. Đọc tới đâu lấy max tới đó:
Đáp án không âm (trọng số không âm) nên khởi tạo best = 0 là được.
Kết quả chạy thật: Bản C++ đúng 7/20 test, sai test 1, 9–20 (các test đúng chạy lâu nhất 0.003s) → 30 điểm (subtask 1). Các test có $k \ge 2$ đều sai vì chương trình vẫn chỉ in một ô.
Cách 2Vét cạn mọi bộ k ô (Subtask 1, 2)
Cách "thật thà" nhất: đánh số các ô từ $0$ tới $n^2 - 1$ (ô số $x$ nằm ở hàng $x / n$, cột $x \% n$), sinh mọi tổ hợp $k$ ô theo thứ tự tăng dần, khi đã chọn đủ $k$ ô mới kiểm tra từng cặp có chiếu nhau không. Hai ô $(r_1, c_1)$ và $(r_2, c_2)$ chiếu nhau khi cùng hàng, cùng cột, cùng $r - c$ (chéo xuôi) hoặc cùng $r + c$ (chéo ngược).
- Hàm
chon(id, cnt): đã chọncntô, ô tiếp theo phải có số thứ tự từidtrở đi – nhờ vậy mỗi bộ ô chỉ được sinh đúng một lần. - Không cắt nhánh gì cả: kể cả khi 2 ô đầu đã chiếu nhau, chương trình vẫn sinh hết các ô còn lại rồi mới phát hiện.
Số bộ phải thử là $\binom{n^2}{k}$: với $n = 8$, $k = 3$ chỉ có $41\,664$ bộ, nhưng $k = 5$ đã là $7{,}6$ triệu, $k = 8$ là $4{,}4$ tỉ bộ.
Kết quả chạy thật: Bản C++ đúng 16/20 test, quá thời gian test 13–15, 20 (các test đúng chạy lâu nhất 0.571s). Ăn trọn subtask 1, 2; subtask 3 chỉ qua được các test có $k \le 6$ hoặc bảng nhỏ (test 13, 14, 20 có $n = k = 8$, test 15 có $n = 8, k = 7$ đều quá thời gian). Bản Python đúng 13/20 test, quá thời gian test 13–17, 19–20 (các test đúng chạy lâu nhất 0.050s) – Python chậm hơn nhiều nên rớt thêm vài test của subtask 3.
Cách 3Quay lui theo từng hàng + cắt nhánh (full)
Làm đúng như phần ý tưởng: hàm dat(i, cnt, s) xét hàng $i$ khi đã đặt cnt quân với tổng trọng số s.
if(cnt == k): đủ $k$ quân thì cập nhậtbestvà dừng nhánh này (không đặt thêm vì đề yêu cầu đúng $k$ quân).if(n - i < k - cnt) return;– cắt nhánh: còn $n - i$ hàng (hàng $i..n-1$, đếm từ 0) mà cần thêm $k - cnt$ quân thì không thể đủ. Dòng này cũng thay cho điều kiện dừngi == n.- Đặt hậu: đánh dấu 3 mảng, gọi đệ quy, rồi gỡ đánh dấu (đây chính là bước "quay lui").
- Cuối cùng thử phương án bỏ trống hàng $i$.
Vì sao nhanh? Các nhánh bị chiếu bị loại ngay khi đặt, không đợi đủ $k$ quân. Mình đếm thử với $n = 8$: số lần gọi hàm dat lớn nhất là khoảng $97\,000$ (khi $k = 5$), còn $k = 8$ chỉ $4\,022$ lần – so với $4{,}4$ tỉ bộ của cách 2.
Kết quả chạy thật: Bản C++ đúng 20/20 test, chạy lâu nhất 0.004s. Full điểm. Bản Python đúng 20/20 test, chạy lâu nhất 0.080s – bài này Python cũng thoải mái.
Tổng kếtKết quả chạy thật trên bộ test
| Cách | Ngôn ngữ | Đúng | Lâu nhất (test đúng) | Điểm theo subtask |
|---|---|---|---|---|
| 1. Chỉ lấy ô lớn nhất (k = 1) | C++ | 7/20 | 0.003s | 30% |
| Python | 7/20 | 0.018s | 30% | |
| 2. Vét cạn mọi bộ k ô | C++ | 16/20 | 0.571s | 60% |
| Python | 13/20 | 0.050s | 60% | |
| 3. Quay lui theo hàng + cắt nhánh | C++ | 20/20 | 0.004s | 100% |
| Python | 20/20 | 0.080s | 100% |
Nhận xét
Đăng nhận xét