백트래킹 | 모든 경우의 수 탐색 알고리즘 완벽 정리

백트래킹 | 모든 경우의 수 탐색 알고리즘 완벽 정리

이 글의 핵심

백트래킹에 대한 실전 가이드입니다. 모든 경우의 수 탐색 알고리즘 완벽 정리 등을 예제와 함께 상세히 설명합니다.

들어가며

”모든 경우를 시도하되, 불가능하면 되돌아가기”

백트래킹은 모든 경우의 수를 탐색하되, 조건 불만족 시 즉시 포기하는 알고리즘입니다. DFS와 유사하지만 가지치기(Pruning)를 통해 불필요한 탐색을 건너뜁니다.

이 글을 읽으면

  • 백트래킹의 핵심 패턴을 이해합니다
  • 순열, 조합, 부분집합을 구현할 수 있습니다
  • N-Queen, 스도쿠 같은 제약 만족 문제를 풉니다
  • 가지치기로 성능을 최적화합니다

목차

  1. 사전 지식
  2. 백트래킹 기본
  3. 순열과 조합
  4. N-Queen 문제
  5. 고급 활용
  6. 실무 사례
  7. 트러블슈팅
  8. 마무리

사전 지식 (초보자를 위한 기초)

1. 재귀(Recursion) 복습

백트래킹은 재귀를 기반으로 합니다. 재귀가 낯설다면 먼저 이해하고 오세요.

# 재귀 예시: 1부터 n까지 출력
def print_numbers(n):
    if n == 0:  # 기저 조건 (멈추는 조건)
        return
    
    print_numbers(n - 1)  # 재귀 호출 (자기 자신 호출)
    print(n)

print_numbers(3)
# 출력:
# 1
# 2
# 3

# 실행 과정:
# print_numbers(3)
#   → print_numbers(2)
#     → print_numbers(1)
#       → print_numbers(0) (멈춤)
#       → print(1)
#     → print(2)
#   → print(3)

2. 트리 구조 이해

백트래킹은 결정 트리(Decision Tree)를 탐색합니다.

예시: [1, 2, 3]에서 2개 선택하기

                    []
          /          |          \
        [1]         [2]         [3]
       /   \         |
    [1,2] [1,3]   [2,3]

각 노드: 현재까지의 선택
각 가지: 다음 선택지
리프 노드: 최종 결과

3. 경우의 수란?

경우의 수는 가능한 모든 경우를 세는 것입니다.

# 예시: [1, 2, 3]을 나열하는 모든 방법

# 순열 (순서 중요, 3! = 6가지)
[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 1, 2]
[3, 2, 1]

# 조합 (순서 무관, 2개 선택 = 3가지)
[1, 2]
[1, 3]
[2, 3]

# 부분집합 (2³ = 8가지)
[]
[1]
[2]
[3]
[1, 2]
[1, 3]
[2, 3]
[1, 2, 3]

4. 백트래킹의 핵심 아이디어

“미로 탈출”을 생각해보세요:

미로 탈출 전략:

1. 한 방향으로 계속 진행
2. 막다른 길이면 → 되돌아가기 (Backtrack)
3. 다른 방향 시도
4. 출구 찾을 때까지 반복

백트래킹도 똑같습니다:

1. 하나의 선택을 함
2. 조건을 만족하지 않으면 → 되돌아가기
3. 다른 선택 시도
4. 해를 찾을 때까지 반복

코드로 표현:

def backtrack(선택들):
    # 1. 해를 찾았으면 저장
    if 조건_만족(선택들):
        결과_저장(선택들)
        return
    
    # 2. 모든 선택지 시도
    for 선택 in 가능한_선택들:
        if 유효한_선택(선택):  # 가지치기!
            선택_추가(선택)      # 선택
            backtrack(선택들)    # 다음 단계로
            선택_제거(선택)      # 되돌리기 (Backtrack)

5. 가지치기(Pruning)란?

가지치기불가능한 경우를 미리 제거하는 것입니다.

# 예시: 합이 10인 3개 숫자 조합 찾기 (1~9 사용)

# ❌ 가지치기 없음 (모든 경우 탐색)
def no_pruning(nums, target):
    # [1,2,3], [1,2,4], ..., [7,8,9]
    # 총 84가지 모두 확인 (느림!)
    pass

# ✅ 가지치기 있음 (불가능한 경우 제외)
def with_pruning(nums, current_sum, target):
    # 현재 합이 이미 10을 넘으면 → 더 이상 탐색 안함!
    if current_sum > target:
        return  # 가지치기!
    
    # 예: [1, 2]까지 선택했고 합이 3
    # 다음에 8을 선택하면 합이 11 → 10 초과
    # 8, 9는 탐색할 필요 없음!

가지치기의 효과:

가지치기 없음:
              []
       /      |      \
     [1]    [2]    [3]
    / | \   / | \   / | \
  ...  ...  ...  ...  ...
  (모든 경우 탐색 - 느림)

가지치기 있음:
              []
       /      |      \
     [1]    [2]    [3]
    / | \    |      X (합 초과, 가지치기!)
  ...  X     X
  (불가능한 경우 제외 - 빠름!)

6. 백트래킹 vs 완전 탐색

완전 탐색 (Brute Force):

  • 모든 경우를 다 확인
  • 느리지만 확실함

백트래킹:

  • 조건을 만족하는 경우만 확인
  • 가지치기로 빠름
# 완전 탐색: 모든 3자리 비밀번호 시도 (000~999)
for i in range(1000):
    if try_password(i):
        return i
# 최악: 1000번 시도

# 백트래킹: 조건 활용 (첫 자리가 5라는 힌트)
for i in range(500, 600):  # 500~599만 시도
    if try_password(i):
        return i
# 최악: 100번 시도 (10배 빠름!)

1. 백트래킹 기본

DFS vs 백트래킹

DFS (깊이 우선 탐색):

  • 연결된 모든 노드를 방문
  • 방문 여부만 체크

백트래킹:

  • 조건을 만족하는 해만 탐색
  • 조건 불만족 시 즉시 되돌아감 (가지치기)
# DFS: 모든 노드 방문
def dfs(node, visited):
    visited.add(node)
    for child in node.children:
        if child not in visited:
            dfs(child, visited)

# 백트래킹: 조건 체크 + 가지치기
def backtrack(state):
    if is_solution(state):
        add_solution(state)
        return
    
    for choice in get_choices(state):
        if is_valid(choice):  # 가지치기!
            make_choice(choice)
            backtrack(state)
            undo_choice(choice)  # 되돌리기

백트래킹 템플릿

def backtrack_template(state, choices, result):
    """
    백트래킹 범용 템플릿
    """
    # 1. 종료 조건
    if is_complete(state):
        result.append(state.copy())
        return
    
    # 2. 가지치기 (조기 종료)
    if not is_valid(state):
        return
    
    # 3. 선택 시도
    for choice in choices:
        # 선택
        state.add(choice)
        
        # 재귀
        backtrack_template(state, choices, result)
        
        # 되돌리기
        state.remove(choice)

2. 순열과 조합

순열 (Permutation)

정의: n개 중 r개를 순서를 고려하여 선택

예시: [1, 2, 3]에서 2개 선택 → [1,2], [1,3], [2,1], [2,3], [3,1], [3,2]

Python 구현

def permutations(arr, n):
    """
    순열: nPr
    - 순서 O, 중복 X
    - 시간: O(n!)
    """
    result = []
    
    def backtrack(path, remaining):
        if len(path) == n:
            result.append(path[:])
            return
        
        for i in range(len(remaining)):
            path.append(remaining[i])
            backtrack(path, remaining[:i] + remaining[i+1:])
            path.pop()
    
    backtrack([], arr)
    return result

# 테스트
print(permutations([1, 2, 3], 2))
# [[1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]]

C++ 구현

#include <vector>
#include <iostream>

using namespace std;

void permutationsHelper(
    vector<int>& arr, 
    int n, 
    vector<int>& path, 
    vector<bool>& used, 
    vector<vector<int>>& result
) {
    if (path.size() == n) {
        result.push_back(path);
        return;
    }
    
    for (int i = 0; i < arr.size(); ++i) {
        if (!used[i]) {
            used[i] = true;
            path.push_back(arr[i]);
            
            permutationsHelper(arr, n, path, used, result);
            
            path.pop_back();
            used[i] = false;
        }
    }
}

vector<vector<int>> permutations(vector<int>& arr, int n) {
    vector<vector<int>> result;
    vector<int> path;
    vector<bool> used(arr.size(), false);
    
    permutationsHelper(arr, n, path, used, result);
    return result;
}

int main() {
    vector<int> arr = {1, 2, 3};
    auto result = permutations(arr, 2);
    
    for (const auto& perm : result) {
        for (int num : perm) {
            cout << num << " ";
        }
        cout << "\n";
    }
    
    return 0;
}

시간 복잡도: O(n!)
공간 복잡도: O(n)

조합 (Combination)

정의: n개 중 r개를 순서 무관하게 선택

예시: [1, 2, 3]에서 2개 선택 → [1,2], [1,3], [2,3]

Python 구현

def combinations(arr, n):
    """
    조합: nCr
    - 순서 X, 중복 X
    - 시간: O(2ⁿ)
    """
    result = []
    
    def backtrack(start, path):
        if len(path) == n:
            result.append(path[:])
            return
        
        for i in range(start, len(arr)):
            path.append(arr[i])
            backtrack(i + 1, path)
            path.pop()
    
    backtrack(0, [])
    return result

# 테스트
print(combinations([1, 2, 3], 2))
# [[1, 2], [1, 3], [2, 3]]

C++ 구현

#include <vector>
#include <iostream>

using namespace std;

void combinationsHelper(
    const vector<int>& arr, 
    int n, 
    int start, 
    vector<int>& path, 
    vector<vector<int>>& result
) {
    if (path.size() == n) {
        result.push_back(path);
        return;
    }
    
    for (int i = start; i < arr.size(); ++i) {
        path.push_back(arr[i]);
        combinationsHelper(arr, n, i + 1, path, result);
        path.pop_back();
    }
}

vector<vector<int>> combinations(const vector<int>& arr, int n) {
    vector<vector<int>> result;
    vector<int> path;
    
    combinationsHelper(arr, n, 0, path, result);
    return result;
}

int main() {
    vector<int> arr = {1, 2, 3};
    auto result = combinations(arr, 2);
    
    for (const auto& comb : result) {
        for (int num : comb) {
            cout << num << " ";
        }
        cout << "\n";
    }
    
    return 0;
}

시간 복잡도: O(2ⁿ)
공간 복잡도: O(n)

부분집합 (Subset)

정의: 모든 가능한 부분집합 생성

Python 구현

def subsets(arr):
    """
    모든 부분집합
    - 시간: O(2ⁿ)
    """
    result = []
    
    def backtrack(start, path):
        result.append(path[:])
        
        for i in range(start, len(arr)):
            path.append(arr[i])
            backtrack(i + 1, path)
            path.pop()
    
    backtrack(0, [])
    return result

# 테스트
print(subsets([1, 2, 3]))
# [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]

3. N-Queen 문제

문제 설명

N×N 체스판에 N개의 퀸을 배치하되, 서로 공격할 수 없도록 배치합니다.

제약:

  • 같은 행에 퀸 없음
  • 같은 열에 퀸 없음
  • 같은 대각선에 퀸 없음

Python 구현

def solve_n_queens(n):
    """
    N-Queen 문제
    - 백트래킹으로 모든 해 찾기
    """
    result = []
    board = [['.'] * n for _ in range(n)]
    
    def is_valid(row, col):
        # 같은 열 체크
        for i in range(row):
            if board[i][col] == 'Q':
                return False
        
        # 왼쪽 위 대각선
        i, j = row - 1, col - 1
        while i >= 0 and j >= 0:
            if board[i][j] == 'Q':
                return False
            i -= 1
            j -= 1
        
        # 오른쪽 위 대각선
        i, j = row - 1, col + 1
        while i >= 0 and j < n:
            if board[i][j] == 'Q':
                return False
            i -= 1
            j += 1
        
        return True
    
    def backtrack(row):
        if row == n:
            result.append([''.join(row) for row in board])
            return
        
        for col in range(n):
            if is_valid(row, col):
                board[row][col] = 'Q'
                backtrack(row + 1)
                board[row][col] = '.'
    
    backtrack(0)
    return result

# 테스트
solutions = solve_n_queens(4)
print(f"{len(solutions)}개 해")

for i, sol in enumerate(solutions):
    print(f"\n{i + 1}:")
    for row in sol:
        print(row)

# 출력:
# 2개 해
# 
# 해 1:
# .Q..
# ...Q
# Q...
# ..Q.
# 
# 해 2:
# ..Q.
# Q...
# ...Q
# .Q..

최적화 버전 (Set 사용)

def solve_n_queens_optimized(n):
    """
    N-Queen 최적화
    - Set으로 O(1) 충돌 체크
    """
    result = []
    board = [['.'] * n for _ in range(n)]
    
    cols = set()
    diag1 = set()  # row - col
    diag2 = set()  # row + col
    
    def backtrack(row):
        if row == n:
            result.append([''.join(row) for row in board])
            return
        
        for col in range(n):
            if col in cols or (row - col) in diag1 or (row + col) in diag2:
                continue
            
            board[row][col] = 'Q'
            cols.add(col)
            diag1.add(row - col)
            diag2.add(row + col)
            
            backtrack(row + 1)
            
            board[row][col] = '.'
            cols.remove(col)
            diag1.remove(row - col)
            diag2.remove(row + col)
    
    backtrack(0)
    return result

# 테스트
solutions = solve_n_queens_optimized(8)
print(f"8-Queen: {len(solutions)}개 해")  # 92개

시간 복잡도: O(n!) — 가지치기로 실제는 훨씬 빠름


4. 고급 활용

1) 부분집합 합 (Subset Sum)

문제: 합이 target인 부분집합 찾기

Python 구현

def subset_sum(arr, target):
    """
    부분집합 합 문제
    - 가지치기: current_sum > target
    """
    result = []
    
    def backtrack(start, path, current_sum):
        if current_sum == target:
            result.append(path[:])
            return
        
        if current_sum > target:
            return
        
        for i in range(start, len(arr)):
            path.append(arr[i])
            backtrack(i + 1, path, current_sum + arr[i])
            path.pop()
    
    backtrack(0, [], 0)
    return result

# 테스트
arr = [1, 2, 3, 4, 5]
target = 5
print(subset_sum(arr, target))
# [[1, 4], [2, 3], [5]]

2) 조합 합 (Combination Sum)

문제: LeetCode 39 - Combination Sum

특징: 같은 숫자를 여러 번 사용 가능

Python 구현

class Solution:
    def combinationSum(self, candidates: list[int], target: int) -> list[list[int]]:
        result = []
        
        def backtrack(start, path, current_sum):
            if current_sum == target:
                result.append(path[:])
                return
            
            if current_sum > target:
                return
            
            for i in range(start, len(candidates)):
                path.append(candidates[i])
                backtrack(i, path, current_sum + candidates[i])
                path.pop()
        
        backtrack(0, [], 0)
        return result

# 테스트
sol = Solution()
print(sol.combinationSum([2, 3, 6, 7], 7))
# [[2, 2, 3], [7]]

3) 스도쿠 (Sudoku)

문제: LeetCode 37 - Sudoku Solver

Python 구현

def solve_sudoku(board):
    """
    9x9 스도쿠 풀기
    """
    def is_valid(row, col, num):
        # 행 체크
        if num in board[row]:
            return False
        
        # 열 체크
        if num in [board[i][col] for i in range(9)]:
            return False
        
        # 3x3 박스 체크
        box_row, box_col = 3 * (row // 3), 3 * (col // 3)
        for i in range(box_row, box_row + 3):
            for j in range(box_col, box_col + 3):
                if board[i][j] == num:
                    return False
        
        return True
    
    def backtrack():
        for i in range(9):
            for j in range(9):
                if board[i][j] == '.':
                    for num in '123456789':
                        if is_valid(i, j, num):
                            board[i][j] = num
                            
                            if backtrack():
                                return True
                            
                            board[i][j] = '.'
                    
                    return False
        return True
    
    backtrack()
    return board

# 테스트
board = [
    ["5","3",".",".","7",".",".",".","."],
    ["6",".",".","1","9","5",".",".","."],
    [".","9","8",".",".",".",".","6","."],
    ["8",".",".",".","6",".",".",".","3"],
    ["4",".",".","8",".","3",".",".","1"],
    ["7",".",".",".","2",".",".",".","6"],
    [".","6",".",".",".",".","2","8","."],
    [".",".",".","4","1","9",".",".","5"],
    [".",".",".",".","8",".",".","7","9"]
]

solve_sudoku(board)
for row in board:
    print(' '.join(row))

문제: LeetCode 79 - Word Search

Python 구현

class Solution:
    def exist(self, board: list[list[str]], word: str) -> bool:
        rows, cols = len(board), len(board[0])
        
        def backtrack(r, c, idx):
            if idx == len(word):
                return True
            
            if (r < 0 or r >= rows or c < 0 or c >= cols or 
                board[r][c] != word[idx]):
                return False
            
            temp = board[r][c]
            board[r][c] = '#'
            
            found = (backtrack(r + 1, c, idx + 1) or
                    backtrack(r - 1, c, idx + 1) or
                    backtrack(r, c + 1, idx + 1) or
                    backtrack(r, c - 1, idx + 1))
            
            board[r][c] = temp
            return found
        
        for i in range(rows):
            for j in range(cols):
                if backtrack(i, j, 0):
                    return True
        
        return False

# 테스트
sol = Solution()
board = [
    ['A','B','C','E'],
    ['S','F','C','S'],
    ['A','D','E','E']
]
print(sol.exist(board, "ABCCED"))  # True
print(sol.exist(board, "SEE"))     # True
print(sol.exist(board, "ABCB"))    # False

5. 실무 사례

사례 1: 작업 스케줄링 - 제약 만족

시나리오: 작업 시간과 의존성 제약을 만족하는 스케줄 찾기

Python 구현

from dataclasses import dataclass

@dataclass
class Task:
    id: int
    duration: int
    dependencies: list[int]

def schedule_tasks(tasks, max_time):
    """
    제약 만족 스케줄링
    """
    n = len(tasks)
    result = []
    
    def is_valid(schedule, task_idx):
        task = tasks[task_idx]
        
        for dep_id in task.dependencies:
            if dep_id not in schedule:
                return False
        
        current_time = sum(tasks[i].duration for i in schedule)
        if current_time + task.duration > max_time:
            return False
        
        return True
    
    def backtrack(schedule):
        if len(schedule) == n:
            result.append(schedule[:])
            return
        
        for i in range(n):
            if i not in schedule and is_valid(schedule, i):
                schedule.append(i)
                backtrack(schedule)
                schedule.pop()
    
    backtrack([])
    return result

# 테스트
tasks = [
    Task(0, 2, []),
    Task(1, 3, [0]),
    Task(2, 1, [0]),
    Task(3, 2, [1, 2]),
]

schedules = schedule_tasks(tasks, 10)
print(f"{len(schedules)}개 가능한 스케줄")
for schedule in schedules:
    print([tasks[i].id for i in schedule])

사례 2: 조합 최적화 - 배낭 문제

시나리오: 무게 제한 내에서 가치 최대화

Python 구현

def knapsack_backtrack(items, capacity):
    """
    0-1 배낭 문제 (백트래킹)
    - items: [(weight, value), ...]
    """
    best_value = [0]
    best_items = []
    
    def backtrack(idx, current_weight, current_value, path):
        if current_value > best_value[0]:
            best_value[0] = current_value
            best_items.clear()
            best_items.extend(path)
        
        if idx == len(items):
            return
        
        weight, value = items[idx]
        
        if current_weight + weight <= capacity:
            path.append(idx)
            backtrack(idx + 1, current_weight + weight, current_value + value, path)
            path.pop()
        
        backtrack(idx + 1, current_weight, current_value, path)
    
    backtrack(0, 0, 0, [])
    return best_value[0], best_items

# 테스트
items = [(2, 3), (3, 4), (4, 5), (5, 6)]
capacity = 8

max_value, selected = knapsack_backtrack(items, capacity)
print(f"최대 가치: {max_value}")
print(f"선택한 아이템: {selected}")
# 최대 가치: 10
# 선택한 아이템: [0, 1, 2] (무게 2+3+4=9 X, 2+3=5로 재계산)

사례 3: 경로 탐색 - 미로 탈출

시나리오: 미로에서 모든 가능한 경로 찾기

Python 구현

def find_all_paths(maze, start, end):
    """
    미로 모든 경로 찾기
    - 0: 통행 가능, 1: 벽
    """
    rows, cols = len(maze), len(maze[0])
    directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
    result = []
    
    def backtrack(r, c, path, visited):
        if (r, c) == end:
            result.append(path[:])
            return
        
        for dr, dc in directions:
            nr, nc = r + dr, c + dc
            
            if (0 <= nr < rows and 0 <= nc < cols and 
                maze[nr][nc] == 0 and (nr, nc) not in visited):
                
                visited.add((nr, nc))
                path.append((nr, nc))
                
                backtrack(nr, nc, path, visited)
                
                path.pop()
                visited.remove((nr, nc))
    
    visited = {start}
    backtrack(start[0], start[1], [start], visited)
    return result

# 테스트
maze = [
    [0, 0, 1],
    [0, 0, 0],
    [1, 0, 0]
]
start = (0, 0)
end = (2, 2)

paths = find_all_paths(maze, start, end)
print(f"{len(paths)}개 경로")
for path in paths:
    print(path)

6. 트러블슈팅

문제 1: 되돌리기 누락

증상: 상태가 복구되지 않아 잘못된 결과

# 잘못된 예
def backtrack_wrong(path):
    if len(path) == n:
        result.append(path)  # path 참조 저장
        return
    
    for choice in choices:
        path.append(choice)
        backtrack_wrong(path)
        # path.pop() 누락!

해결:

def backtrack_correct(path):
    if len(path) == n:
        result.append(path[:])  # 복사본 저장
        return
    
    for choice in choices:
        path.append(choice)
        backtrack_correct(path)
        path.pop()  # 되돌리기

문제 2: 가지치기 누락

증상: 불필요한 탐색으로 시간 초과

# 가지치기 없음
def backtrack_slow(current_sum, path):
    if len(path) == n:
        if current_sum == target:
            result.append(path[:])
        return
    
    for num in arr:
        path.append(num)
        backtrack_slow(current_sum + num, path)
        path.pop()

해결: 조기 종료 추가

def backtrack_fast(start, current_sum, path):
    if current_sum == target:
        result.append(path[:])
        return
    
    if current_sum > target:  # 가지치기!
        return
    
    for i in range(start, len(arr)):
        path.append(arr[i])
        backtrack_fast(i + 1, current_sum + arr[i], path)
        path.pop()

문제 3: 중복 결과

증상: 같은 조합이 여러 번 나옴

# 잘못된 예
def combinations_wrong(arr, n):
    result = []
    
    def backtrack(path):
        if len(path) == n:
            result.append(path[:])
            return
        
        for num in arr:  # start 없음
            path.append(num)
            backtrack(path)
            path.pop()
    
    backtrack([])
    return result

# [1,2,3], n=2 → [[1,2], [1,3], [2,1], [2,3], [3,1], [3,2]]
# 중복: [1,2]와 [2,1]

해결: start 인덱스 추가

def combinations_correct(arr, n):
    result = []
    
    def backtrack(start, path):
        if len(path) == n:
            result.append(path[:])
            return
        
        for i in range(start, len(arr)):
            path.append(arr[i])
            backtrack(i + 1, path)
            path.pop()
    
    backtrack(0, [])
    return result

문제 4: 시간 초과 (TLE)

증상: n이 커지면 지수 시간 복잡도로 시간 초과

해결 1: 가지치기 강화

# 정렬 후 조기 종료
arr.sort()

def backtrack(start, current_sum, path):
    if current_sum == target:
        result.append(path[:])
        return
    
    for i in range(start, len(arr)):
        if current_sum + arr[i] > target:
            break  # 정렬되어 있으므로 이후는 불필요
        
        path.append(arr[i])
        backtrack(i + 1, current_sum + arr[i], path)
        path.pop()

해결 2: DP로 전환

# 부분집합 합은 DP로도 해결 가능
def subset_sum_dp(arr, target):
    dp = [False] * (target + 1)
    dp[0] = True
    
    for num in arr:
        for i in range(target, num - 1, -1):
            if dp[i - num]:
                dp[i] = True
    
    return dp[target]

마무리

백트래킹모든 경우의 수를 탐색하되, 가지치기로 불필요한 탐색을 건너뛰는 알고리즘입니다.

핵심 패턴

def backtrack(state):
    # 1. 종료 조건
    if is_complete(state):
        save_solution(state)
        return
    
    # 2. 가지치기
    if not is_valid(state):
        return
    
    # 3. 선택 → 재귀 → 되돌리기
    for choice in get_choices():
        make_choice(choice)
        backtrack(new_state)
        undo_choice(choice)

시간 복잡도

문제시간 복잡도가지치기 효과
순열O(n!)낮음
조합O(2ⁿ)중간
부분집합 합O(2ⁿ)높음 (정렬 시)
N-QueenO(n!)매우 높음
스도쿠O(9^m)매우 높음 (m은 빈 칸)

선택 가이드

문제 유형알고리즘
모든 순열백트래킹
모든 조합백트래킹
최적 부분집합DP (가능하면)
제약 만족백트래킹

추천 문제

백준:

LeetCode:

다음 단계

  • 동적 계획법: DP 완벽 정리
  • BFS/DFS: BFS와 DFS 완벽 정리
  • 그래프: 그래프 자료구조 완벽 정리

백트래킹은 가지치기가 핵심입니다. 조건을 빨리 판단할수록 성능이 좋아집니다.