Quay lui — Backtracking
Quay lui thử mọi khả năng một cách có hệ thống: chọn một bước, đi sâu, và khi đụng ràng buộc thì quay lại bỏ lựa chọn vừa rồi để thử phương án khác.
Đây là cách chuẩn để liệt kê toàn bộ nghiệm (tập con, hoán vị, tổ hợp) hoặc tìm một cấu hình thoả mãn (N-Queens, Sudoku). Chìa khoá để không bị chậm là cắt nhánh sớm — loại bỏ ngay những lựa chọn không thể dẫn tới nghiệm.
Như đường về quê — đi nhầm ngõ thì quay lại đầu ngõ, thử ngõ khác.
Thử mọi khả năng có hệ thống, cắt nhánh sớm khi đụng ràng buộc: nếu ngõ này đã rõ ràng sai thì không cần chui tiếp, quay luôn.
Dấu hiệu nhận biết
Gặp những từ khoá này trong đề → nghĩ ngay đến Quay lui
| Từ khoá trong đề | Tại sao gợi đến chủ đề này |
|---|---|
| liệt kê tổ hợp | Subset / permutation / combination |
| tìm một cách thoả | N-Queens, Sudoku, Word Search |
| cây quyết định | Mỗi bước có nhiều lựa chọn |
| cắt nhánh sớm | Kiểm tra ràng buộc trước khi đệ quy |
Khi nào dùng
3 tình huống điển hình cần nghĩ đến chủ đề này
Cần liệt kê mọi tổ hợp, hoán vị, tập con thoả ràng buộc
Bài toán "thử tất cả khả năng": N-Queens, Sudoku, Word Search trên lưới
Khi không gian tìm kiếm có thể thu hẹp bằng ràng buộc sớm (pruning)
Khi nào KHÔNG dùng
3 tình huống chủ đề này không phù hợp — biết để tránh overuse
Bài có công thức closed-form hoặc DP polynomial: dùng đúng cách rẻ hơn nhiều
Chỉ cần TÌM 1 nghiệm hợp lệ (không phải liệt kê tất cả): greedy / heuristic có thể đủ
Search space quá lớn không prune được: backtracking sẽ TLE — cần đổi approach (DP / greedy)
Hình minh hoạ
Sơ đồ thể hiện cách giải của chủ đề này.
Mỗi node là 1 trạng thái. Đi xuống = chọn / bỏ một phần tử. Tới lá → ghi kết quả → quay lại thử nhánh khác.
Code mẫu
Hầu hết bài cùng chủ đề dùng chung mẫu này — copy rồi điền phần logic riêng của bài.
function backtrack(state: State, choices: Choice[], result: R[]): void {
// Base case: đạt mục tiêu → ghi nhận
if (isGoal(state)) {
result.push(clone(state))
return
}
for (const choice of choices) {
// Prune: bỏ choice không hợp lệ sớm
if (!isValid(choice, state)) continue
// Choose: áp dụng choice vào state
apply(state, choice)
// Recurse: đi sâu với state mới
backtrack(state, nextChoices(state), result)
// Unchoose: rollback state (quan trọng!)
undo(state, choice)
}
}Recap
Tổng hợp nhanh — đọc lại trước phỏng vấn
Điểm chính
Template ba bước: chọn — đệ quy — huỷ chọn (choose, recurse, unchoose)
Base case: đạt đến kết quả hợp lệ thì ghi nhận rồi trả về
Pruning: kiểm tra ràng buộc trước khi đệ quy → cắt nhánh xấu sớm
Quay lui chính là DFS có pruning — chỗ khác nhau là điểm cắt nhánh
Lỗi thường gặp
Quên huỷ chọn (unchoose) sau khi đệ quy → state bị "ô nhiễm" giữa các nhánh
Pruning sai điều kiện → bỏ qua kết quả hợp lệ hoặc không cắt được nhánh xấu
Quên copy khi lưu kết quả:
result.push(current)thay vìresult.push([...current])→ mọi phần tử trỏ chung 1 mảng, cuối cùng đều rỗng
Độ phức tạp
Time O(b^d) — b là branching factor, d là độ sâu; pruning giảm đáng kể trong thực tếBài luyện theo chủ đề này
Phân theo độ khó — luyện tuần tự cho chắc tay