Chương 5.3 • Backtracking & CSP

🧠 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.

ChooseChọn một giá trị
CheckKiểm tra ràng buộc
PruneCắt nhánh vô ích
UndoHoàn tác trạng thái

🎯 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 đề

Bài toán mở đầu.
Đặ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.
Không sinh mọi bàn cờ → chỉ sinh các trạng thái còn có khả năng thành nghiệm
Ý tưởng: xây lời giải từng bước; phát hiện sai càng sớm thì càng tiết kiệm nhiều nhánh phía dưới.

2. Mẫu quay lui chuẩn

1. Choose
chọn giá trị
2. Check
kiểm tra hợp lệ
3. Recurse
đi tới biến tiếp
4. Undo
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.
Quên undo làm trạng thái của nhánh trước “rò rỉ” sang nhánh sau và cho kết quả sai.

3. Mô phỏng cây tìm kiếm

Bài toán minh họa: chọn các số trong {1,2,3,4} sao cho tổng không vượt quá S.

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.

Pruning đúng phải loại nhánh chắc chắn không chứa nghiệm cần tìm
Cắt nhánh càng mạnh không phải lúc nào càng tốt. Điều kiện cắt phải đúng và chi phí kiểm tra cắt nhánh không nên quá lớn.

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ĩaVí dụ SudokuVí dụ N-Queens
VariablesCác biến cần gánCác ô trốngHàng 0..n−1
DomainsGiá trị có thể gán1..9 còn hợp lệCác cột chưa xung đột
ConstraintsĐiều kiện phải thỏaKhông trùng hàng/cột/khốiKhông cùng cột/chéo
AssignmentTrạng thái gán hiện tạiMột số ô đã điềnMột số hàng đã đặt hậu
Tư duy CSP: chọn biến nào trước, thử giá trị nào trước, và cập nhật miền các biến còn lại.

7. MRV và Forward Checking

MRV: chọn biến có số giá trị hợp lệ ít nhất để phát hiện bế tắc sớm.
Forward checking: sau khi gán, loại giá trị xung đột khỏi miền các biến chưa gán.

8. Mô phỏng N-Queens

for (int col=0; col<n; col++) {
if (usedCol[col] || usedDiag1[...] || usedDiag2[...]) continue;
place queen; mark constraints;
backtrack(row+1);
remove queen; unmark constraints;

9. Phân tích thuật toán

Kỹ thuậtKhông gian tìm kiếmTác dụng
Quay lui thuầnCó thể gần b^dDuyệt mọi lựa chọn hợp lệ theo từng mức.
Pruning theo ràng buộcGiảm mạnh tùy bàiKhông mở rộng trạng thái đã sai.
Branch orderingKhông đổi worst-caseTìm nghiệm tốt sớm hơn.
Symmetry breakingGiảm trạng thái tương đươngTránh nghiệm đối xứng/trùng lặp.
MRVKhông đổi tập nghiệmPhát hiện thất bại sớm.
Forward checkingTốn thêm cập nhật miềnCắt nhánh khi miền biến trở thành rỗng.
Worst-case vẫn có thể là hàm mũ. Các heuristic thường cải thiện rất lớn trên dữ liệu thực tế nhưng không luôn thay đổi giới hạn xấu nhất.

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âu 1. Trình tự cốt lõi của quay lui?
Câu 2. MRV chọn biến nào?
Câu 3. Forward checking làm gì?
Câu 4. Branch ordering có nhất thiết giảm worst-case không?

📘 Cơ bản

  1. Sinh xâu nhị phân không có hai số 1 liên tiếp.
  2. Chọn tập con có tổng không vượt S.
  3. Liệt kê hoán vị không trùng.

📗 Trung bình

  1. N-Queens.
  2. Chia nhóm với ràng buộc.
  3. Tô màu đồ thị nhỏ.

📙 Nâng cao

  1. Sudoku với MRV.
  2. Branch and Bound cho balô.
  3. Hamilton path trên đồ thị nhỏ.

🐉 HSG

  1. Exact Cover định hướng.
  2. Constraint propagation.
  3. Backtracking + memoization.