Backtracking adalah teknik rekursif untuk mengeksplorasi semua kemungkinan solusi secara sistematis. Bila sebuah pilihan tidak mengarah ke solusi, algoritma mundur (backtrack) dan mencoba pilihan lain. Bayangkan Sudoku: kita menebak angka, lanjut mengisi, bila buntu mundur dan ganti angka sebelumnya. Backtracking adalah jantung dari banyak algoritma kombinatorial: N-Queens, Sudoku solver, subset generation, maze solving. Pada Chapter ini kita akan mempelajari kerangka umum backtracking, menerapkannya pada N-Queens, dan melihat sekilas struktur Sudoku solver.
Kerangka Backtracking: Choose, Explore, Unchoose #
Setiap algoritma backtracking memiliki tiga langkah wajib per pemanggilan rekursif:
- Choose: pilih satu opsi dari kandidat yang tersedia.
- Explore: panggil rekursif dengan opsi tersebut dipasang.
- Unchoose: batalkan pilihan (undo) untuk mencoba opsi lain.
Pola pseudocode:
def backtrack(state):
if is_solution(state):
record(state)
return
for option in candidates(state):
choose(option) # modify state
backtrack(state) # recurse
unchoose(option) # restore stateKunci sukses backtracking: state harus dapat dimodifikasi dan dipulihkan dengan murah. Untuk vector, ini berarti push_back/pop_back. Untuk array, tetapkan nilai dan reset.
Aturan N-Queens: Solitaire Klasik #
Aturan #
Tempatkan n ratu pada papan catur n × n sehingga tidak ada dua ratu yang saling serang. Dua ratu saling serang bila berada di baris, kolom, atau diagonal yang sama.
Mekanisme: Backtrack per Baris #
Tempatkan ratu satu per satu per baris dari atas ke bawah. Untuk setiap baris, coba setiap kolom:
- Choose: letakkan ratu di
(row, col). - Explore: rekursi ke baris berikutnya.
- Unchoose: hapus ratu dari
(row, col)sebelum mencoba kolom lain.
Bila row == n, semua ratu terpasang → catat solusi.
Kode: N-Queens Solver #
#include <iostream>
#include <vector>
#include <string>
int g_solutions = 0;
bool aman(const std::vector<int>& pos, int row, int col) {
for (int r = 0; r < row; ++r) {
int c = pos[r];
if (c == col) return false; // kolom sama
int dr = row - r;
int dc = c - col;
if (dc < 0) dc = -dc;
if (dr == dc) return false; // diagonal
}
return true;
}
void solve_nqueens(std::vector<int>& pos, int row, int n) {
if (row == n) {
++g_solutions;
std::cout << "solusi " << g_solutions << ": ";
for (int c : pos) std::cout << c << ' ';
std::cout << '\n';
return;
}
for (int col = 0; col < n; ++col) {
if (aman(pos, row, col)) {
pos[row] = col; // choose
solve_nqueens(pos, row + 1, n); // explore
// unchoose: pos[row] akan ditimpa di iterasi berikutnya
}
}
}
int main() {
int n = 4;
std::vector<int> pos(n, -1);
std::cout << "N-Queens untuk n=" << n << ":\n";
solve_nqueens(pos, 0, n);
std::cout << "total solusi: " << g_solutions << '\n';
return 0;
}Output yang Dihasilkan:
N-Queens untuk n=4:
solusi 1: 1 3 0 2
solusi 2: 2 0 3 1
total solusi: 2std::vector<int> pos(n, -1):pos[row] = colmenyimpan posisi ratu per baris.-1berarti belum ada ratu.- Fungsi
aman: cek apakah ratu di(row, col)aman dari semua ratu di baris0..row-1. Cek kolom sama (c == col) dan diagonal (|dr| == |dc|). - Baris
pos[row] = col;: choose — letakkan ratu di kolomcolbarisrow. - Baris
solve_nqueens(pos, row + 1, n);: explore — rekursi ke baris berikutnya. - Tidak ada unchoose eksplisit:
pos[row]akan ditimpa di iterasi berikutnya dari loopfor. Bila tidak ada iterasi lagi,pos[row]tidak relevan karena pemanggil akan menimpa di level atas. - Hasil 2 solusi: untuk
n = 4, ada 2 solusi. Solusi[1, 3, 0, 2]artinya ratu di (0,1), (1,3), (2,0), (3,2).
Sketsa Sudoku Solver #
N-Queens adalah kasus khusus dengan struktur sederhana. Sudoku lebih kompleks: ada constraint awal (angka yang sudah terisi), dan pemilihan sel berikutnya tidak trivial.
Kode: Sketsa Sudoku Solver #
#include <iostream>
#include <vector>
bool valid(const std::vector<std::vector<int>>& grid, int r, int c, int num) {
// Cek baris dan kolom
for (int i = 0; i < 9; ++i) {
if (grid[r][i] == num) return false;
if (grid[i][c] == num) return false;
}
// Cek kotak 3x3
int br = (r / 3) * 3, bc = (c / 3) * 3;
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
if (grid[br + i][bc + j] == num) return false;
return true;
}
bool solve_sudoku(std::vector<std::vector<int>>& grid) {
for (int r = 0; r < 9; ++r) {
for (int c = 0; c < 9; ++c) {
if (grid[r][c] == 0) { // sel kosong
for (int num = 1; num <= 9; ++num) {
if (valid(grid, r, c, num)) {
grid[r][c] = num; // choose
if (solve_sudoku(grid)) // explore
return true;
grid[r][c] = 0; // unchoose
}
}
return false; // tidak ada angka valid → backtrack
}
}
}
return true; // semua sel terisi → solusi
}
int main() {
std::vector<std::vector<int>> grid = {
{5, 3, 0, 0, 7, 0, 0, 0, 0},
{6, 0, 0, 1, 9, 5, 0, 0, 0},
{0, 9, 8, 0, 0, 0, 0, 6, 0},
{8, 0, 0, 0, 6, 0, 0, 0, 3},
{4, 0, 0, 8, 0, 3, 0, 0, 1},
{7, 0, 0, 0, 2, 0, 0, 0, 6},
{0, 6, 0, 0, 0, 0, 2, 8, 0},
{0, 0, 0, 4, 1, 9, 0, 0, 5},
{0, 0, 0, 0, 8, 0, 0, 7, 9}
};
if (solve_sudoku(grid)) {
std::cout << "solusi:\n";
for (const auto& row : grid) {
for (int x : row) std::cout << x << ' ';
std::cout << '\n';
}
} else {
std::cout << "tidak ada solusi\n";
}
return 0;
}Output yang Dihasilkan:
solusi:
5 3 4 6 7 8 9 1 2
6 7 2 1 9 5 3 4 8
1 9 8 3 4 2 5 6 7
8 5 9 7 6 1 4 2 3
4 2 6 8 5 3 7 9 1
7 1 3 9 2 4 8 5 6
9 6 1 5 3 7 2 8 4
2 8 7 4 1 9 6 3 5
3 4 5 2 8 6 1 7 9 - Fungsi
valid: cek apakah angkanumdapat ditempatkan di(r, c)tanpa melanggar aturan Sudoku (baris, kolom, kotak 3×3). - Loop dua tingkat: cari sel kosong (
grid[r][c] == 0). Bila tidak ada sel kosong, semua terisi → return true. - Baris
grid[r][c] = num;: choose — isi dengan angka kandidat. - Baris
if (solve_sudoku(grid)) return true;: explore — rekursi untuk mengisi sel berikutnya. - Baris
grid[r][c] = 0;: unchoose — kosongkan kembali sebelum mencoba angka lain. - Baris
return false;: bila tidak ada angka 1-9 yang valid untuk sel saat ini, backtrack ke pemanggil. - Hasil: Sudoku terisi lengkap dan valid.
State-Space Tree #
Backtracking mengeksplorasi state-space tree: pohon di mana setiap node adalah state parsial, anak-anaknya adalah pilihan yang mungkin, dan daun adalah solusi atau dead-end.
Kunci: algoritma memangkas cabang yang tidak menjanjikan lebih awal. Tanpa pruning, backtracking menjadi exhaustive search — eksponensial penuh.
Pola Umum Backtracking #
Berikut pola umum untuk berbagai soal backtracking:
void backtrack(state, params...) {
if (is_solution(state)) {
record_solution(state);
return;
}
for (auto& choice : candidates(state)) {
if (is_valid(choice, state)) {
apply(choice, state); // choose
backtrack(state, params...); // explore
undo(choice, state); // unchoose
}
}
}Empat komponen kunci:
is_solution(state): kondisi terminasi. Bila state adalah solusi lengkap, catat.candidates(state): daftar pilihan yang mungkin pada state saat ini.is_valid(choice, state): filter pilihan yang melanggar constraint.apply/undo: pasang dan pulihkan state. Harus berpasangan simetris.
Ringkasan #
- Backtracking mengeksplorasi pohon kemungkinan dengan pola choose-explore-unchoose.
- Tiga langkah wajib: pilih opsi, rekursi, batalkan pilihan.
- N-Queens: tempatkan ratu per baris, cek konflik kolom/diagonal sebelum rekursi.
- Sudoku: isi sel kosong dengan angka 1-9, cek aturan baris/kolom/kotak.
- State-space tree: visualisasi pohon keputusan. Backtracking memangkas cabang tidak valid lebih awal.
- Tanpa pruning, backtracking = exhaustive search. Pruning efektif mengecilkan state space.