0Pricing
DSA Interview Prep · 강의

백트래킹 템플릿: 선택, 탐색, 선택 취소

세 단계 백트래킹 뼈대를 구현하고, 작은 예제에 적용해 추적하면서 가지치기 조건이 어디에 들어가는지 확인합니다.

백트래킹 템플릿: 선택, 탐색, 선택 취소은(는) CoddyKit의 무료 DSA Interview Prep 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 DSA Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

백트래킹이란 무엇입니까

백트래킹은 모든 후보를 점진적으로 탐색하면서, 해당 분기가 유효한 solution을 만들 수 없다는 사실이 확인되는 즉시 그 분기를 포기하는(가지치기하는) 체계적인 방법입니다. 이는 스도쿠를 풀고, 순열을 생성하고, 유효한 조합을 모두 찾는 데 사용되는 알고리즘입니다. 백트래킹을 결정 트리에서 수행하는 깊이 우선 탐색이라고 생각해 보십시오.

# Mental model: backtracking explores a decision tree
# At each node you make a choice, go deeper, then undo it
#
# Tree for generating subsets of [1,2,3]:
#        []
#      /    \
#    [1]   []
#   / \    / \
# [1,2][1][2] []
# ...

# Every leaf is a potential solution
# Pruning cuts branches early based on constraints
print('Backtracking = DFS on decision tree with pruning')

세 단계 템플릿

모든 백트래킹 함수는 세 단계를 따릅니다. 선택 — 사용 가능한 선택지에서 다음 후보를 고릅니다. 탐색 — 해당 선택을 사용해 재귀 호출을 수행하고, 결정 트리에서 한 단계 더 깊이 들어갑니다. 선택 취소 — 재귀 호출에서 돌아온 뒤 선택을 되돌려 다음 후보를 위해 상태를 복원합니다. 이 패턴은 상황에 따라 추가/재귀/제거 또는 표시/재귀/표시 해제라고도 합니다.

def backtrack(current_state, choices, results):
    # Base case: is current_state a complete solution?
    if is_complete(current_state):
        results.append(list(current_state))  # record solution
        return
    
    for choice in choices:
        if is_valid(choice, current_state):    # pruning condition
            # 1. CHOOSE
            current_state.append(choice)
            # 2. EXPLORE
            backtrack(current_state, choices, results)
            # 3. UNCHOOSE (backtrack)
            current_state.pop()

# Placeholder functions — filled per problem
def is_complete(state): return True
def is_valid(choice, state): return True

가장 간단한 예: 모든 부분집합

[1, 2, 3]의 모든 부분집합을 생성해 보겠습니다. 각 인덱스에서 원소를 포함할지 제외할지 선택합니다. 각 호출 뒤에 시작 인덱스가 증가하므로 이전 원소를 다시 방문하지 않습니다. 제약 조건 확인은 필요하지 않습니다. 모든 부분 상태가 유효하기 때문입니다. 이렇게 하면 2ⁿ개의 부분집합이 생성됩니다. 선택 취소 단계는 재귀 호출 뒤의 path.pop()입니다.

def subsets(nums):
    result = []
    def backtrack(start, path):
        result.append(list(path))  # every state is a valid subset
        for i in range(start, len(nums)):
            path.append(nums[i])    # CHOOSE
            backtrack(i + 1, path)  # EXPLORE
            path.pop()              # UNCHOOSE
    backtrack(0, [])
    return result

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

가지치기 조건 식별

완전 탐색보다 백트래킹이 강력한 이유는 가지치기에 있습니다. 부분 경로가 유효한 solution으로 이어질 수 없다는 사실을 일찍 알아내는 것입니다. 조합 합 문제(목표 합과 한도가 있는 경우)에서는 현재 합이 목표를 초과하는 순간 더 깊은 분기의 값은 계속 커지기만 하므로 즉시 반환하여 가지치기합니다. N-퀸 문제에서는 퀸이 기존 퀸을 공격하면 해당 열을 건너뜁니다. 가지치기를 사용하면 지수적인 트리를 감당할 수 있는 탐색으로 바꿀 수 있습니다.

def combination_sum(candidates, target):
    result = []
    candidates.sort()  # sort enables early termination
    def backtrack(start, path, remaining):
        if remaining == 0:
            result.append(list(path))
            return
        for i in range(start, len(candidates)):
            c = candidates[i]
            if c > remaining: break   # PRUNE: sorted, so rest are bigger too
            path.append(c)            # CHOOSE
            backtrack(i, path, remaining - c)   # EXPLORE (reuse allowed)
            path.pop()                # UNCHOOSE
    backtrack(0, [], target)
    return result

print(combination_sum([2, 3, 6, 7], 7))  # [[2,2,3],[7]]

상태 복원은 중요합니다

백트래킹에서 흔히 발생하는 버그는 다음 반복 전에 상태를 완전히 복원하지 않는 것입니다. 변경 가능한 자료 구조(목록, 집합, 격자)를 사용한다면 선택 단계에서 수행한 모든 변경을 선택 취소 단계에서 되돌려야 합니다. 예를 들어 격자(스도쿠나 단어 검색 등)를 수정할 때는 재귀 호출 뒤에 해당 셀을 빈 상태로 설정해야 합니다. 이를 잊으면 형제 분기를 탐색할 때 상태가 손상된 채로 남습니다.

# Bug: forgetting to unmark in word search
# Correct pattern for grid backtracking:
def word_search(board, word):
    m, n = len(board), len(board[0])
    def dfs(r, c, k):
        if k == len(word): return True
        if not (0<=r<m and 0<=c<n): return False
        if board[r][c] != word[k]: return False
        temp, board[r][c] = board[r][c], '#'  # CHOOSE (mark visited)
        found = any(dfs(r+dr, c+dc, k+1)
                    for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)])
        board[r][c] = temp  # UNCHOOSE (restore cell)
        return found
    return any(dfs(r, c, 0) for r in range(m) for c in range(n))

board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']]
print(word_search([row[:] for row in board], 'ABCCED'))  # True

결정 트리 추적

[2, 3, 6, 7]과 목표 7을 사용하는 조합 합 문제의 트리를 추적해 보겠습니다. 루트에서 2를 시도합니다. 2에서 다시 2를 시도합니다(남은 값=3). 2+2에서 다시 2를 시도합니다(남은 값=1). 2>1이므로 가지치기합니다. 3을 시도하지만 3>1이므로 가지치기합니다. 백트래킹합니다. 2+2에서 3을 시도합니다(남은 값=3). 3이 남은 값과 일치하므로 [2,2,3]을 기록합니다. 백트래킹한 뒤 계속 진행합니다. 이 추적은 잘못된 결과를 만들기 전에 가지치기가 분기를 제거하는 방식을 보여 줍니다.

def combination_sum_trace(candidates, target):
    result = []
    candidates.sort()
    def backtrack(start, path, remaining, depth):
        indent = '  ' * depth
        print(f'{indent}explore({path}, remaining={remaining})')
        if remaining == 0:
            result.append(list(path))
            print(f'{indent}FOUND: {path}')
            return
        for i in range(start, len(candidates)):
            c = candidates[i]
            if c > remaining:
                print(f'{indent}PRUNE at {c}')
                break
            path.append(c)
            backtrack(i, path, remaining - c, depth + 1)
            path.pop()
    backtrack(0, [], target, 0)
    return result

combination_sum_trace([2, 3, 6, 7], 7)

백트래킹과 완전 탐색 비교

완전 탐색은 가능한 모든 완성된 solution을 시도한 다음 각각을 검증합니다. 백트래킹은 구성하는 중간에 가지치기하므로 잘못된 경로를 끝까지 완성하지 않습니다. N=8인 N-퀸 문제에서 완전 탐색은 8^8 = 1,600만 개의 배치를 확인합니다. 백트래킹은 이를 약 2,057회의 재귀 호출로 줄입니다. N이 커지면 차이는 극적으로 증가합니다. N=12에서는 완전 탐색이 89억 개의 배치를 시도하지만 백트래킹은 트리의 일부만 탐색합니다.

# Compare call counts: brute force vs backtracking for permutations
import sys
calls_brute = [0]
calls_back = [0]

def brute_force_perms(nums):
    from itertools import permutations
    return list(permutations(nums))

def backtrack_perms(nums):
    result = []
    used = [False] * len(nums)
    def bt(path):
        calls_back[0] += 1
        if len(path) == len(nums):
            result.append(list(path))
            return
        for i, n in enumerate(nums):
            if not used[i]:
                used[i] = True
                path.append(n)
                bt(path)
                path.pop()
                used[i] = False
    bt([])
    return result

backtrack_perms([1,2,3,4])
print(f'Backtrack calls for 4 items: {calls_back[0]}')

모두 수집하기와 조기 반환

백트래킹 문제는 두 가지 범주로 나뉩니다. 모든 solution 열거하기(완성된 모든 경로 수집)와 하나의 solution 찾기(경로가 성공하는 즉시 True 반환)입니다. 열거할 때는 항상 결과 목록에 추가합니다. 하나를 찾는 경우에는 재귀 호출에서 즉시 True를 반환하고 그 값을 상위 호출로 전달합니다. any(backtrack(...))를 반환하거나 if backtrack(...): return True를 사용하면 단락 평가 동작이 구현됩니다.

# Enumerate all: collect in results list
def all_solutions(candidates):
    results = []
    def bt(path, remaining):
        if remaining == 0:
            results.append(list(path))
            return
        for c in candidates:
            if c <= remaining:
                path.append(c); bt(path, remaining - c); path.pop()
    bt([], 5)
    return results

# Find any one: return True on first success
def any_solution(candidates, target):
    def bt(path, remaining):
        if remaining == 0: return True
        for c in candidates:
            if c <= remaining:
                path.append(c)
                if bt(path, remaining - c): return True  # short-circuit
                path.pop()
        return False
    path = []
    return bt(path, target), path

백트래킹과 메모이제이션

순수 백트래킹은 캐싱 없이 모든 경로를 탐색하므로 모든 solution이 필요한 경우에는 적합합니다. 그러나 일부 백트래킹 문제에는 겹치는 하위 문제가 있습니다. 예를 들어 단어 분할 II는 백트래킹과 메모이제이션을 함께 사용해 해결할 수 있습니다. 각 시작 인덱스에서 만들 수 있는 문장 목록을 캐시하는 방식입니다. 이렇게 하면 최악의 경우 지수 시간이 걸리는 백트래킹을 다항 시간 알고리즘으로 바꿀 수 있습니다. 하위 문제가 반복되는지 파악하면 이 혼합 방식을 적용하십시오.

from functools import lru_cache

def word_break_all(s, wordDict):
    words = set(wordDict)
    
    @lru_cache(maxsize=None)
    def bt(start):
        if start == len(s): return ['']  # empty suffix
        result = []
        for end in range(start + 1, len(s) + 1):
            word = s[start:end]
            if word in words:
                for rest in bt(end):
                    result.append(word if not rest else word + ' ' + rest)
        return result
    
    return bt(0)

print(word_break_all('catsanddog', ['cat','cats','and','sand','dog']))
# ['cat sand dog', 'cats and dog']

백트래킹의 시간 복잡도

백트래킹의 시간 복잡도는 결정 트리의 리프 수와 각 노드에서 수행하는 작업의 곱에 따라 결정됩니다. 부분집합의 경우: O(n × 2ⁿ)입니다. 순열의 경우: O(n × n!)입니다. 조합 합의 경우 최악의 시간 복잡도는 O(target/min_candidate ^ n)입니다. 가지치기는 상수항을 줄이지만 점근적 상한은 줄이지 않습니다. 면접에서 복잡도를 질문받으면 최악의 트리 크기를 제시하고, 가지치기 덕분에 실제로는 일반적으로 훨씬 빠르다는 점을 언급하십시오.

# Complexity quick reference:
# Subsets of n elements:     O(n * 2^n)  - 2^n subsets, each copied in O(n)
# Permutations of n:          O(n * n!)   - n! perms, each copied in O(n)
# Combination sum (target T): O(T^n / n!) worst case without pruning
# N-Queens:                   O(n!)       - prune reduces practical count

# For n=10 permutations: 10! = 3,628,800 paths
import math
n = 10
print(f'n={n}: n!={math.factorial(n):,} paths')
print(f'n={n}: 2^n={2**n:,} subsets')

백트래킹 문제 식별

문제에 백트래킹이 필요하다는 신호는 다음과 같습니다. (1) 조합, 순열 또는 부분집합을 모두 찾거나 모두 생성해야 합니다. (2) N-퀸이나 스도쿠처럼 제약 조건에 따라 항목이나 사람을 배치해야 합니다. (3) solution 공간은 지수적이지만 제약 조건이 대부분의 분기를 일찍 제거합니다. (4) 상태를 다시 방문할 수도 있는 그래프나 격자에서 경로를 탐색해야 합니다. 이러한 신호가 보이면 선택-탐색-선택 취소 템플릿을 사용하십시오.

# Common backtracking problem types:
# 1. Subsets / Power set
# 2. Permutations (with/without duplicates)
# 3. Combinations (k from n, combination sum)
# 4. Grid path finding (word search, unique paths with visited tracking)
# 5. Constraint satisfaction (N-queens, Sudoku solver)
# 6. String partitioning (palindrome partition, word break all)

# Template reminder:
def backtrack(start, path):
    # base case: add to results or return True
    for choice in get_choices(start):
        if is_valid(choice, path):   # prune
            path.append(choice)      # choose
            backtrack(start+1, path) # explore
            path.pop()               # unchoose

def get_choices(start): return []
def is_valid(c, p): return True

간단 확인

이번 학습에서 다룬 자료 구조 및 알고리즘 — 코딩 면접 준비 개념을 제대로 이해했는지 테스트해 보십시오.

학습 내용 요약

이번 학습에서 다음을 배웠습니다. 백트래킹 템플릿은 선택, 탐색, 선택 취소라는 세 단계로 구성되며, 이는 선택 추가, 재귀 호출, 선택 제거에 해당합니다. 가지치기 조건은 분기를 일찍 제거하며 백트래킹을 완전 탐색보다 실용적으로 만드는 핵심입니다. 또한 형제 분기의 상태가 손상되지 않도록 각 재귀 호출 뒤에 상태를 완전히 복원해야 합니다. 다음에는 이 템플릿을 적용해 모든 부분집합과 멱집합을 생성합니다.

자주 묻는 질문

“백트래킹 템플릿: 선택, 탐색, 선택 취소” 강의는 무료인가요?

네 — “백트래킹 템플릿: 선택, 탐색, 선택 취소” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 DSA Interview Prep 강의 전체를 잠금 해제할 수 있습니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

“백트래킹 템플릿: 선택, 탐색, 선택 취소”에서 뭘 배우나요?

세 단계 백트래킹 뼈대를 구현하고, 작은 예제에 적용해 추적하면서 가지치기 조건이 어디에 들어가는지 확인합니다. 브라우저에서 직접 실행하는 실습 코드로 DSA Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

DSA Interview Prep을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 DSA Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.

“백트래킹 템플릿: 선택, 탐색, 선택 취소” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 DSA Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 DSA Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 백트래킹 템플릿: 선택, 탐색, 선택 취소
  2. 부분집합과 멱집합
  3. 순열과 조합
  4. N-퀸과 제약 전파
← DSA Interview Prep(으)로 돌아가기