cv.cm

코딩 · 알고리즘

백트래킹 쉽게 이해하기: 동작 원리, 코드, 예제

7분 분량

핵심 답변

백트래킹은 후보 해를 한 번에 하나의 선택씩 만들어 가다가, 더 이상 유효한 해로 이어질 수 없다고 판단되는 즉시 마지막 선택을 되돌리는 방식으로 모든 가능한 해를 탐색하는 기법입니다. 부분 해로 이루어진 트리에 대한 깊이 우선 탐색이며 선택(choose), 탐색(explore), 선택 취소(unchoose)의 패턴을 따릅니다. N-Queens, 스도쿠, 부분집합, 순열 같은 퍼즐과 조합 문제를 풀 수 있습니다. 최악의 경우는 지수 시간이므로 유효하지 않은 가지를 일찍 잘라 내는 가지치기가 실용성의 열쇠입니다.

정규식 테스트 열기

백트래킹이 해결하는 문제

규칙을 만족하는 모든 배치, 또는 그런 배치 하나를 요구하는 문제가 있습니다. 서로 공격하지 않도록 퀸 8개 놓기, 스도쿠 채우기, 합이 목표값이 되는 집합의 모든 부분집합 나열하기, 문자열을 사전의 단어들로 나누는 모든 방법 찾기가 그 예입니다. 영리한 공식은 없고 탐색해야 합니다. 완성된 조합을 무작정 전부 시도하면 너무 느리지만, 대부분의 부분 조합은 몇 번의 선택만으로도 이미 유효하지 않습니다.

백트래킹은 선택 하나하나가 끝날 때마다 규칙을 확인합니다. 부분 해가 규칙을 어기면 탐색 트리의 그 가지 전체를 즉시 포기하고 돌아가 다음 선택지를 시도합니다.

핵심 아이디어

  1. 현재 부분 해가 완성되었다면 기록합니다.
  2. 그렇지 않다면 가능한 다음 선택마다, 그 선택이 허용될 경우 선택을 수행합니다(choose).
  3. 재귀 호출로 해를 더 확장합니다(explore).
  4. 선택을 취소하여 다음 선택지가 같은 상태에서 시작하도록 합니다(unchoose).

탐색 공간은 트리입니다. 루트는 빈 해이고, 각 레벨은 선택을 하나씩 더하며, 리프는 완성된 후보입니다. 백트래킹은 이 트리에 대한 깊이 우선 탐색이고, 가지치기는 해를 포함할 수 없는 서브트리를 잘라 내는 것입니다.

예제: 4-Queens

4 × 4 보드에 퀸 4개를 행마다 하나씩, 어떤 두 퀸도 같은 열이나 대각선에 놓이지 않게 배치합니다. 열은 0부터 3까지 번호를 붙입니다.

단계동작지금까지의 퀸 (행별 열)
10행: 0열에 퀸을 놓음0
21행: 0열과 1열은 공격받음. 2열 시도0, 2
32행: 모든 열이 공격받음. 백트랙: 1행을 3열로 이동0, 3
42행: 1열이 안전함0, 3, 1
53행: 모든 열이 공격받음. 2행으로 백트랙: 다른 열도 안 됨. 1행으로 백트랙: 남은 열 없음. 0행으로 백트랙: 1열로 이동1
61행: 3열이 안전함1, 3
72행: 0열이 안전함1, 3, 0
83행: 2열이 안전함. 해를 찾음1, 3, 0, 2

탐색을 계속하면 유일한 다른 해인 2, 0, 3, 1을 찾으며, 이는 첫 번째 해의 거울상입니다.

파이썬 구현

사용 중인 열과 대각선의 집합을 유지하면 안전 검사를 O(1)에 할 수 있습니다. 한 대각선에서는 row - col이 일정하고 다른 대각선에서는 row + col이 일정합니다.

def solve_n_queens(n):
    solutions, cols = [], []
    used_cols, diag1, diag2 = set(), set(), set()

    def place(row):
        if row == n:
            solutions.append(cols.copy())   # copy, not the live list
            return
        for col in range(n):
            if col in used_cols or row - col in diag1 or row + col in diag2:
                continue                    # prune: square is attacked
            cols.append(col)                # choose
            used_cols.add(col); diag1.add(row - col); diag2.add(row + col)
            place(row + 1)                  # explore
            cols.pop()                      # unchoose
            used_cols.remove(col); diag1.remove(row - col); diag2.remove(row + col)

    place(0)
    return solutions

print(solve_n_queens(4))       # [[1, 3, 0, 2], [2, 0, 3, 1]]
print(len(solve_n_queens(8)))  # 92

같은 템플릿으로 모든 부분집합과 모든 순열을 생성할 수 있습니다.

def subsets(items):
    result, current = [], []

    def go(i):
        if i == len(items):
            result.append(current.copy())
            return
        current.append(items[i]); go(i + 1); current.pop()   # take items[i]
        go(i + 1)                                            # skip items[i]

    go(0)
    return result

def permutations(items):
    result, current, used = [], [], [False] * len(items)

    def go():
        if len(current) == len(items):
            result.append(current.copy())
            return
        for i, x in enumerate(items):
            if not used[i]:
                used[i] = True; current.append(x)
                go()
                used[i] = False; current.pop()

    go()
    return result

print(subsets([1, 2, 3]))
# [[1, 2, 3], [1, 2], [1, 3], [1], [2, 3], [2], [3], []]
print(permutations([1, 2, 3]))
# [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]

단순한 부분집합과 순열은 실제 코드에서 itertools.combinations와 itertools.permutations가 더 빠릅니다. 문제 고유의 규칙으로 가지치기해야 할 때 직접 백트래킹을 작성하세요.

시간 복잡도와 공간 복잡도

문제결과 수시간추가 공간
n개 항목의 모든 부분집합2ⁿO(n × 2ⁿ)O(n) 재귀 깊이와 출력
n개 항목의 모든 순열n!O(n × n!)O(n)과 출력
N-Queens경우에 따라 다름 (n = 8이면 92)최대 O(n!), 가지치기하면 훨씬 적음O(n)
스도쿠 (9 × 9)올바른 퍼즐이면 1최악의 경우 빈 칸 수에 대해 지수적O(칸 수)

백트래킹은 출력 크기보다 빠를 수 없습니다. n!개의 순열을 나열해야 한다면 최소 n!번의 단계가 필요합니다. 가지치기는 아무것도 만들지 못하는 가지를 건너뛸 때만 도움이 됩니다.

변형과 흔한 실수

  • 되돌리기를 잊음: 재귀 호출 뒤에 상태를 복원하지 않으면 이후의 가지가 오래된 선택을 보게 됩니다.
  • 살아 있는 리스트를 그대로 추가함: result.append(current)는 나중에 바뀌는 참조를 저장합니다. current.copy()를 추가하세요.
  • 입력의 중복: 항목을 정렬하고 같은 깊이에서 이전 항목과 같은 항목을 건너뛰면 중복된 부분집합이나 순열을 피할 수 있습니다.
  • 첫 해에서 멈추기: 해 하나면 충분할 때는 스도쿠 풀이기처럼 재귀에서 True를 반환하고 반복문을 멈춥니다.
  • 더 나은 순서: 가장 제약이 많은 선택을 먼저 시도합니다(스도쿠라면 후보가 가장 적은 빈 칸). 트리가 극적으로 줄어드는 경우가 많습니다.

언제 쓰는지와 실제 활용

  • 제약 충족 문제: 스도쿠, 십자말풀이, 지도 색칠, 시간표 짜기, 시험 일정 짜기.
  • SAT 솔버와 Prolog 방식의 논리 엔진은 강력한 가지치기를 갖춘 백트래킹 탐색을 기반으로 만들어집니다.
  • 파이썬의 re를 포함해 여러 언어의 정규 표현식 엔진은 백트래킹을 사용하며, 잘못 작성한 패턴은 지수적으로 백트래킹할 수 있습니다("치명적 백트래킹").
  • 코딩 인터뷰 문제: 부분집합, 순열, 조합의 합, N-Queens, 격자에서 단어 찾기, 팰린드롬 분할, 괄호 생성, 전화번호 문자 조합.

자주 묻는 질문

백트래킹과 완전 탐색의 차이는 무엇인가요?

완전 탐색은 완성된 후보를 모두 만들어 검사합니다. 백트래킹은 선택할 때마다 제약을 확인하고 부분 후보가 실패하면 즉시 포기하므로 후보의 무리 전체를 건너뜁니다.

백트래킹은 DFS와 같은 건가요?

백트래킹은 부분 해의 트리에 대한 깊이 우선 탐색이며, 각 선택을 취소하고 유효하지 않은 가지를 쳐내는 규칙이 추가됩니다. 그래프에서의 DFS는 이미 존재하는 노드와 간선에 같은 아이디어를 적용한 것입니다.

백트래킹의 시간 복잡도는 얼마인가요?

최악의 경우 보통 지수 또는 팩토리얼 시간입니다. 예를 들어 부분집합은 O(2ⁿ), 순열은 O(n!)입니다. 좋은 가지치기는 실제 작업량을 줄이지만 최악의 경우 한계는 줄이지 못합니다.

8-Queens 퍼즐의 해는 몇 개인가요?

해는 92개이며, 서로 회전이나 반사로 일치하는 해를 하나로 세면 12개입니다.

코딩 튜토리얼 전체