출시·고도화 중
백트래킹 안내서 · 2/6
이 장에서는 작은 예제 두 개를 손으로 따라가며 백트래킹이 실제로 어떻게 움직이는지 봅니다. 먼저 가지치기가 없는 조합 생성을 추적하고, 다음으로 4-Queens에서 제약 검사와 되돌아가기가 어떻게 맞물리는지 확인합니다. 마지막으로 같은 원리가 스도쿠에서 어떻게 쓰이는지 살펴봅니다.
[1, 2, 3, 4]에서 2개를 고르는 조합을 만듭니다. 순서만 다른 중복을 피하려고 다음 선택은 항상 지금 고른 수보다 뒤에서 시작합니다. 이것만으로도 [2, 1] 같은 가지가 처음부터 생기지 않습니다.
def combine(n, k):
result, path = [], []
def dfs(start):
if len(path) == k:
result.append(path[:])
return
for x in range(start, n + 1):
path.append(x) # 선택
dfs(x + 1) # 탐색
path.pop() # 되돌리기
dfs(1)
return result
print(combine(4, 2))
# [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]호출 순서를 표로 따라가 보면 path가 늘었다 줄었다 하는 모습이 보입니다.
| 단계 | 동작 | path | 결과 |
|---|---|---|---|
| 1 | 1 선택 | [1] | |
| 2 | 2 선택 | [1, 2] | [1, 2] 저장 |
| 3 | 2 되돌리고 3 선택 | [1, 3] | [1, 3] 저장 |
| 4 | 3 되돌리고 4 선택 | [1, 4] | [1, 4] 저장 |
| 5 | 4, 1 되돌리고 2 선택 | [2] | |
| 6 | 3 선택 | [2, 3] | [2, 3] 저장 |
| 7 | ... | [3, 4] | 마지막 조합 |
여기에 가지치기를 하나 더할 수 있습니다. 남은 수가 채워야 할 개수보다 적으면 더 볼 필요가 없으므로, 반복 범위를 range(start, n - (k - len(path)) + 2)로 줄이면 [4]처럼 끝까지 가도 2개를 채울 수 없는 가지를 아예 만들지 않습니다.
4×4 판에 퀸 4개를 서로 공격하지 않게 놓습니다. 한 행에 퀸을 하나씩 놓는다고 정하면 행 충돌은 저절로 사라지고, 열과 두 대각선만 검사하면 됩니다. 같은 대각선 위의 칸은 row - col 값이 같고, 반대 방향 대각선 위의 칸은 row + col 값이 같습니다.
def is_safe(queens, row, col):
# queens[r] = r행에 놓은 퀸의 열
for r, c in enumerate(queens):
if c == col or r - c == row - col or r + c == row + col:
return False
return True열 번호로 첫 번째 해를 찾기까지의 과정입니다.
| 상태 | 시도 | 판정 |
|---|---|---|
| [] | 0행 0열 | 놓음 |
| [0] | 1행 0열, 1열 | 열 충돌, 대각선 충돌 |
| [0] | 1행 2열 | 놓음 |
| [0, 2] | 2행 0~3열 | 모두 충돌, 되돌아감 |
| [0] | 1행 3열 | 놓음 |
| [0, 3] | 2행 1열 | 놓음 |
| [0, 3, 1] | 3행 0~3열 | 모두 충돌, 되돌아감 |
| [0, 3] | 2행 2열, 3열 | 충돌, 0행까지 되돌아감 |
| [] | 0행 1열 | 놓음 |
| [1] | 1행 3열 | 놓음 |
| [1, 3] | 2행 0열 | 놓음 |
| [1, 3, 0] | 3행 2열 | 놓음, 해 [1, 3, 0, 2] |
0행 0열에서 시작한 가지 전체에서 해가 없다는 것을 확인하는 데 몇 걸음이면 충분했습니다. 4^4 = 256가지 배치를 모두 만드는 대신, 실제로 방문한 노드는 루트를 포함해 17개뿐입니다(두 해를 모두 찾을 때까지).
def solve(n):
solutions = []
def place(queens):
row = len(queens)
if row == n:
solutions.append(queens[:])
return
for col in range(n):
if is_safe(queens, row, col):
queens.append(col)
place(queens)
queens.pop()
place([])
return solutions
print(solve(4)) # [[1, 3, 0, 2], [2, 0, 3, 1]]스도쿠도 같은 틀입니다. 빈 칸 하나를 골라 1부터 9까지 넣어 보고, 같은 행, 같은 열, 같은 3×3 상자에 이미 있는 숫자는 건너뜁니다. 넣을 수 있는 숫자가 하나도 없으면 앞 칸으로 돌아가 다른 숫자를 시도합니다.
def can_place(board, r, c, d):
if any(board[r][j] == d for j in range(9)):
return False
if any(board[i][c] == d for i in range(9)):
return False
br, bc = 3 * (r // 3), 3 * (c // 3)
return all(board[i][j] != d
for i in range(br, br + 3)
for j in range(bc, bc + 3))실제 풀이기는 후보가 가장 적은 칸부터 채우는 방식(MRV, minimum remaining values)을 더해 탐색량을 크게 줄입니다. 후보가 하나뿐인 칸을 먼저 채우면 잘못된 가지가 훨씬 일찍 드러나기 때문입니다.
path를 하나 두고 선택과 되돌리기를 짝지어 깊이 우선으로 내려갑니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.