🧠 Quay lui, cắt nhánh và CSP
Quay lui thử từng lựa chọn, kiểm tra ràng buộc, đi sâu và hoàn tác. Cắt nhánh giúp bỏ sớm những trạng thái chắc chắn vô ích. CSP mở rộng tư duy này bằng biến, miền giá trị và ràng buộc, cùng các chiến lược như MRV, forward checking và symmetry breaking.
🎯 Mục tiêu bài học
Cần hiểu
- Mẫu
choose → check → recurse → undo. - Khác nhau giữa quay lui thuần và quay lui có cắt nhánh.
- Branch ordering và symmetry breaking.
- CSP gồm biến, miền giá trị và ràng buộc.
- Forward checking và MRV.
Cần làm được
- Viết quay lui sinh cấu hình hợp lệ.
- Cắt nhánh theo ràng buộc và cận.
- Sắp thứ tự thử để tìm nghiệm sớm.
- Mô hình hóa bài toán thành CSP.
- Giải N-Queens bằng mảng đánh dấu.
1. Đặt vấn đề
Đặt n quân hậu lên bàn cờ n×n sao cho không có hai quân hậu cùng hàng, cùng cột hoặc cùng đường chéo.
Nếu thử mọi cách chọn n ô trong n² ô, số trường hợp rất lớn. Ta khai thác ràng buộc:
- Mỗi hàng chỉ đặt đúng một quân hậu.
- Khi đang đặt ở hàng r, chỉ thử các cột chưa bị chiếm.
- Nếu cột hoặc đường chéo xung đột, bỏ nhánh ngay.
2. Mẫu quay lui chuẩn
chọn giá trị
kiểm tra hợp lệ
đi tới biến tiếp
hoàn tác
void backtrack(int position) {
if (position == n) {
recordAnswer();
return;
}
for (Value value : candidates(position)) {
choose(value);
if (valid()) {
backtrack(position + 1);
}
undo(value);
}
}
Ý nghĩa
- Choose: thay đổi trạng thái hiện tại.
- Check: loại trạng thái vi phạm.
- Recurse: giải phần còn lại.
- Undo: trả trạng thái về trước lựa chọn.
3. Mô phỏng cây tìm kiếm
4. Cắt nhánh – Pruning
Vi phạm ràng buộc
Nếu tổng hiện tại đã vượt S, không cần đi sâu.
Cận tối ưu
Nếu ngay cả trường hợp tốt nhất còn lại cũng không thắng đáp án hiện tại, bỏ nhánh.
Miền rỗng
Nếu một biến chưa gán không còn giá trị hợp lệ, quay lui ngay.
5. Branch ordering và symmetry breaking
Branch ordering
Thử lựa chọn hứa hẹn trước để tìm nghiệm tốt sớm. Trong bài tối ưu, nghiệm tốt sớm tạo cận mạnh hơn.
- Thử giá trị lớn trước trong bài tối đa hóa.
- Thử ô có ít lựa chọn trước.
- Ưu tiên nước đi gây nhiều ràng buộc.
Symmetry breaking
Loại các nghiệm tương đương do đối xứng để tránh duyệt lặp.
- N-Queens: có thể cố định hậu hàng đầu ở nửa bàn cờ.
- Phân nhóm giống nhau: ép thứ tự nhóm.
- Hoán vị có phần tử trùng: bỏ lựa chọn trùng ở cùng mức.
6. CSP – Constraint Satisfaction Problem
| Thành phần | Ý nghĩa | Ví dụ Sudoku | Ví dụ N-Queens |
|---|---|---|---|
| Variables | Các biến cần gán | Các ô trống | Hàng 0..n−1 |
| Domains | Giá trị có thể gán | 1..9 còn hợp lệ | Các cột chưa xung đột |
| Constraints | Điều kiện phải thỏa | Không trùng hàng/cột/khối | Không cùng cột/chéo |
| Assignment | Trạng thái gán hiện tại | Một số ô đã điền | Một số hàng đã đặt hậu |
7. MRV và Forward Checking
8. Mô phỏng N-Queens
9. Phân tích thuật toán
| Kỹ thuật | Không gian tìm kiếm | Tác dụng |
|---|---|---|
| Quay lui thuần | Có thể gần b^d | Duyệt mọi lựa chọn hợp lệ theo từng mức. |
| Pruning theo ràng buộc | Giảm mạnh tùy bài | Không mở rộng trạng thái đã sai. |
| Branch ordering | Không đổi worst-case | Tìm nghiệm tốt sớm hơn. |
| Symmetry breaking | Giảm trạng thái tương đương | Tránh nghiệm đối xứng/trùng lặp. |
| MRV | Không đổi tập nghiệm | Phát hiện thất bại sớm. |
| Forward checking | Tốn thêm cập nhật miền | Cắt nhánh khi miền biến trở thành rỗng. |
10. Code mẫu
#include <bits/stdc++.h>
using namespace std;
vector<int> current;
void backtrack(int position, int n) {
if (position == n) {
for (int value : current) {
cout << value;
}
cout << '\n';
return;
}
for (int value : {0, 1}) {
current.push_back(value);
backtrack(position + 1, n);
current.pop_back();
}
}vector<int> a;
int limitValue;
vector<int> chosen;
void generate(int index, int currentSum) {
if (currentSum > limitValue) {
return; // Pruning.
}
if (index == (int)a.size()) {
// Ghi nhận cấu hình hợp lệ.
return;
}
// Không chọn a[index].
generate(index + 1, currentSum);
// Chọn a[index].
chosen.push_back(a[index]);
generate(
index + 1,
currentSum + a[index]
);
chosen.pop_back();
}#include <bits/stdc++.h>
using namespace std;
int n;
vector<int> queenColumn;
vector<bool> usedColumn;
vector<bool> usedDiagonal1;
vector<bool> usedDiagonal2;
bool solve(int row) {
if (row == n) {
return true;
}
for (int column = 0;
column < n;
column++) {
int diagonal1 = row - column + n - 1;
int diagonal2 = row + column;
if (usedColumn[column]
|| usedDiagonal1[diagonal1]
|| usedDiagonal2[diagonal2]) {
continue;
}
queenColumn[row] = column;
usedColumn[column] = true;
usedDiagonal1[diagonal1] = true;
usedDiagonal2[diagonal2] = true;
if (solve(row + 1)) {
return true;
}
usedColumn[column] = false;
usedDiagonal1[diagonal1] = false;
usedDiagonal2[diagonal2] = false;
}
return false;
}sort(a.begin(), a.end());
for (int i = 0; i < (int)a.size(); i++) {
if (used[i]) continue;
if (i > 0
&& a[i] == a[i - 1]
&& !used[i - 1]) {
continue;
}
used[i] = true;
current.push_back(a[i]);
backtrack();
current.pop_back();
used[i] = false;
}bool solveSudoku() {
int bestRow = -1;
int bestColumn = -1;
vector<int> bestCandidates;
for (int row = 0; row < 9; row++) {
for (int column = 0; column < 9; column++) {
if (board[row][column] != 0) continue;
vector<int> candidates
= getCandidates(row, column);
if (candidates.empty()) {
return false;
}
if (bestRow == -1
|| candidates.size()
< bestCandidates.size()) {
bestRow = row;
bestColumn = column;
bestCandidates = candidates;
}
}
}
if (bestRow == -1) {
return true;
}
for (int value : bestCandidates) {
board[bestRow][bestColumn] = value;
if (solveSudoku()) {
return true;
}
board[bestRow][bestColumn] = 0;
}
return false;
}11. Giải thích code
11.1. Vì sao kiểm tra ràng buộc trước khi gọi sâu?
Một trạng thái đã sai thì mọi trạng thái mở rộng từ nó cũng sai. Dừng sớm tránh sinh cả cây con vô ích.
11.2. Vì sao N-Queens dùng ba mảng đánh dấu?
usedColumn kiểm tra cột; row-column+n-1 xác định đường chéo chính; row+column xác định đường chéo phụ. Mỗi kiểm tra chỉ O(1).
11.3. Vì sao solve trả về bool?
Khi chỉ cần một nghiệm, trả về true giúp dừng toàn bộ quá trình ngay khi tìm thấy.
11.4. MRV giúp gì cho Sudoku?
Ô có ít ứng viên nhất được thử trước. Nếu ô đó không có ứng viên, nhánh bị loại ngay; nếu chỉ có một ứng viên, lựa chọn gần như bắt buộc.
11.5. Forward checking khác kiểm tra hợp lệ thông thường thế nào?
Kiểm tra thông thường chỉ xác nhận gán hiện tại không vi phạm. Forward checking còn cập nhật miền của các biến chưa gán và phát hiện miền rỗng sớm.
11.6. Branch ordering có làm đổi đáp án không?
Không, nếu vẫn thử đủ các nhánh cần thiết. Nó chỉ thay đổi thứ tự tìm kiếm và thời điểm tìm thấy nghiệm.
12. Lỗi thường gặp
- Quên hoàn tác trạng thái sau lời gọi đệ quy.
- Đánh dấu ràng buộc sai chỉ số đường chéo.
- Cắt nhánh bằng điều kiện chưa chắc chắn, làm mất nghiệm.
- Không kiểm tra miền rỗng sau forward checking.
- Dùng MRV nhưng tính miền ứng viên sai.
- Không xử lý phần tử trùng khi sinh hoán vị.
- Nhầm branch ordering với pruning.
- Không dừng sớm khi chỉ cần một nghiệm.
- Sao chép toàn bộ trạng thái ở mỗi bước gây chậm và tốn bộ nhớ.
- Không ước lượng kích thước cây tìm kiếm.
13. Quiz và bài tập
📘 Cơ bản
- Sinh xâu nhị phân không có hai số 1 liên tiếp.
- Chọn tập con có tổng không vượt S.
- Liệt kê hoán vị không trùng.
📗 Trung bình
- N-Queens.
- Chia nhóm với ràng buộc.
- Tô màu đồ thị nhỏ.
📙 Nâng cao
- Sudoku với MRV.
- Branch and Bound cho balô.
- Hamilton path trên đồ thị nhỏ.
🐉 HSG
- Exact Cover định hướng.
- Constraint propagation.
- Backtracking + memoization.
💳 Quét mã ủng hộ tuỳ tâm nhé!