N quân hậu
Đề bài
Đếm số lượng cách đặt $n$ quân hậu trên bàn cờ kích cỡ $n \times n$ sao cho không có 2 quân hậu nào tấn công được nhau.
Ví dụ một cách đặt 5 quân hậu (x là quân hậu):
x....
...x.
.x...
....x
..x..| ♛ | ||||
| ♛ | ||||
| ♛ | ||||
| ♛ | ||||
| ♛ |
Input: một số nguyên $n$.
Output: một số nguyên duy nhất là số lượng cách đặt $n$ quân hậu.
Điều kiện: $1 \le n \le 10$.
Nhắc lại luật cờ vua: quân hậu tấn công mọi ô cùng hàng, cùng cột và cùng đường chéo (cả 2 hướng chéo) với nó.
510Lời mở đầu
Bài toán 8 quân hậu là "bài kinh điển số 1" khi học quay lui. Ở bài này mình sẽ đi từ cách ngây thơ nhất (thử mọi hoán vị) tới cách quay lui chuẩn có đánh dấu cột và đường chéo – cách mà các bạn sẽ dùng lại ở rất nhiều bài sau trong chương. Mỗi khối code đều có khung chạy tay để em thấy rõ ngăn xếp lời gọi đệ quy lớn lên rồi co lại như thế nào.
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ì?
| Số test | Giá trị n | Test có đáp án 0 |
|---|---|---|
| 10 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 (mỗi giá trị 1 test) | 2 test |
Đề không chia subtask: bộ test có đúng mỗi giá trị $n$ từ 1 đến 10 một test. Có những $n$ không có cách đặt nào (đáp án 0) – em thử nghĩ xem đó là $n$ nào nhé (gợi ý: thử bằng widget bên dưới). Kết quả chạy thật của các cách (giới hạn 1 giây):
| Mức | Điều kiện | Test | Cách 1 (hoán vị) | Cách 2 (quay lui) |
|---|---|---|---|---|
| $n \le 9$ | $1 \le n \le 9$ | 1–9 | 9/9 | 9/9 |
| $n = 10$ | $n = 10$ | 10 | 1/1 | 1/1 |
(Cả 2 cách C++ đều qua hết; khác biệt chỉ thấy rõ ở bản Python – xem phần tổng kết.)
Ý tưởngMỗi hàng đúng một quân hậu
Hai quân hậu cùng hàng thì ăn nhau, mà có $n$ quân trên $n$ hàng, nên mỗi hàng có đúng một quân hậu. Tương tự mỗi cột cũng có đúng một quân. Vậy một cách đặt chính là: hàng $i$ đặt hậu ở cột $p_i$, với $p_0, p_1, \dots, p_{n-1}$ là một hoán vị của $0, 1, \dots, n-1$.
Còn lại điều kiện đường chéo. Hai ô $(i_1, j_1)$ và $(i_2, j_2)$ cùng đường chéo khi và chỉ khi:
- cùng chéo "↘": $i_1 - j_1 = i_2 - j_2$;
- cùng chéo "↙": $i_1 + j_1 = i_2 + j_2$.
Gộp lại: cùng đường chéo $\Leftrightarrow |j_1 - j_2| = |i_1 - i_2|$.
Điểm mạnh của quay lui: hễ một hàng không còn ô nào đặt được thì cả nhánh bị cắt bỏ, không phải đi hết mọi hoán vị. Em chọn $n$ rồi bấm Tiếp để xem bàn cờ và cây quay lui lớn dần (nút xanh = một cách đặt, nút đỏ = ngõ cụt):
Cách 1Thử mọi hoán vị – O(n!·n²)
Cách "không cần nghĩ": dùng next_permutation sinh lần lượt mọi hoán vị $p$ của $0..n-1$ (đã đảm bảo khác hàng, khác cột), với mỗi hoán vị kiểm tra mọi cặp hàng $i < j$ xem có cùng đường chéo không.
for trong ok() được gộp vào bước ở dòng if (mỗi cặp i, j một bước) cho gọn.abs(p[i] - p[j]) == j - i: khoảng cách cột bằng khoảng cách hàng → cùng đường chéo → hoán vị hỏng,return falsengay.- Vòng
do { ... } while(next_permutation(p, p + n));bắt đầu từ hoán vị tăng dần $0, 1, \dots, n-1$ và đi qua đủ $n!$ hoán vị.
Với $n = 10$: $10! = 3\,628\,800$ hoán vị, mỗi hoán vị thường bị loại sau vài phép so sánh, nên vẫn đủ nhanh.
Kết quả chạy thật: Bản C++ đúng 10/10 test; test đúng chạy lâu nhất 0.04s. Còn bản Python đúng 9/10 test, quá thời gian ở test 10; test đúng chạy lâu nhất 0.36s (dùng itertools.permutations).
Cách 2Quay lui + đánh dấu cột, đường chéo
Thay vì sinh trọn hoán vị rồi mới kiểm tra, ta kiểm tra ngay khi đặt từng quân. Để kiểm tra trong $O(1)$, dùng 3 mảng đánh dấu:
cot[j]: cột $j$ đã có hậu chưa;c1[i + j]: đường chéo "↙" có tổng $i + j$ (từ $0$ đến $2n - 2$) đã có hậu chưa;c2[i - j + n]: đường chéo "↘" có hiệu $i - j$ (cộng thêm $n$ cho khỏi âm) đã có hậu chưa.
if(i == n): đã đặt xong các hàng $0..n-1$ → đếm thêm 1 cách rồireturn.- Đặt hậu = bật 3 dấu, gọi
Try(i + 1), rồi tắt 3 dấu – đây là bước "quay lui" trả bàn cờ về như cũ cho lần thử cột kế tiếp. - Ngăn xếp lời gọi sâu tối đa $n + 1$ tầng (
Try(0)…Try(n)), nên không lo tràn ngăn xếp.
Số nút thật sự được thăm nhỏ hơn $n!$ rất nhiều (với $n = 10$ chỉ khoảng 35 nghìn lần gọi Try) nhờ cắt nhánh sớm.
Kết quả chạy thật: Bản C++ đúng 10/10 test; test đúng chạy lâu nhất 0.01s. Còn bản Python đúng 10/10 test; test đúng chạy lâu nhất 0.04s.
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. Thử mọi hoán vị | C++ | 10/10 | 0.04s | Full |
| Python | 9/10 | > 1s | Quá thời gian test 10 (n = 10) | |
| 2. Quay lui + đánh dấu | C++ | 10/10 | 0.01s | Full |
| Python | 10/10 | 0.04s | Full |
- 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