백트래킹 가이드 시리즈 (3/3)

이 시리즈는 백트래킹의 원리를 이해하고 코딩테스트에서 활용하는 감각을 기르기 위한 가이드다.

1. 백트래킹 기본 개념

2. 가지치기와 최적화

3. 실전 패턴과 면접 대비 ← 현재 문서

패턴을 알면 풀이가 보인다

백트래킹 문제는 겉모습이 다양하지만, 내부 구조는 몇 가지 패턴으로 수렴한다. 패턴을 인식하면 "이 문제를 어떻게 백트래킹으로 모델링할까"에 대한 답이 빠르게 나온다.

패턴 1 — 순열

N개 원소를 나열하는 모든 순서를 구하는 유형이다.

  • 선택 단위 : 각 위치에 어떤 원소를 놓을 것인가
  • 제약 : 이미 사용한 원소는 다시 사용할 수 없다
  • 상태 관리 : boolean[] visited 배열
void permutation(int depth) {
    if (depth == N) {
        결과 처리;
        return;
    }

    for (int i = 0; i < N; i++) {
        if (visited[i]) continue;

        visited[i] = true;
        result[depth] = arr[i];
        permutation(depth + 1);
        visited[i] = false;
    }
}
  • visited 배열이 "이미 선택했는가"를 추적한다.
  • 시간복잡도 : O(N!)
순열 vs 조합 구분법

"순서가 의미 있으면" 순열, "순서가 의미 없으면" 조합이다. {1, 2}와 {2, 1}이 다른 답이면 순열, 같은 답이면 조합.

패턴 2 — 조합과 부분집합

N개 원소에서 일부를 뽑는 유형이다. 조합은 정확히 K개를 뽑는 것이고, 부분집합은 0~N개 중 아무 개수나 뽑는 것이다.

  • 선택 단위 : 각 원소를 포함할 것인가, 말 것인가
  • 제약 : 중복 방지를 위해 이전 인덱스 이후만 탐색
  • 핵심 : start 파라미터

조합 (N개 중 K개)

void combination(int start, int depth) {
    if (depth == K) {
        결과 처리;
        return;
    }

    for (int i = start; i < N; i++) {
        result[depth] = arr[i];
        combination(i + 1, depth + 1);
    }
}
  • start가 매 재귀마다 증가하므로 이전에 선택한 것보다 뒤의 원소만 고른다.
  • 되돌리기가 명시적으로 안 보이지만, result[depth]가 다음 반복에서 덮어쓰이므로 자연스럽게 복구된다.

부분집합 (포함/미포함)

void subset(int index) {
    if (index == N) {
        결과 처리;
        return;
    }

    selected[index] = true;
    subset(index + 1);
    selected[index] = false;
    subset(index + 1);
}
  • 각 원소에 대해 "포함한다 / 안 한다" 두 갈래로 나뉜다.
  • 트리의 모양이 이진 트리가 된다. 시간복잡도 : O(2^N)

패턴 3 — 2D 격자 배치

2차원 격자 위에 무언가를 배치하거나 덮는 유형이다. N-Queens, 색종이 붙이기 등이 여기에 해당한다.

  • 선택 단위 : 격자의 특정 위치에 어떤 것을 놓을 것인가
  • 상태 관리 : 2D 배열 자체가 상태
  • 되돌리기 : 배열의 영역을 원래 값으로 복구
sequenceDiagram participant 탐색기 participant 격자 participant 재귀 탐색기->>격자: 다음 처리할 칸 탐색 Note over 격자: (r, c) 발견 loop 가능한 크기/모양 각각 탐색기->>격자: 해당 영역을 0으로 변경 Note over 격자: 상태 변경 탐색기->>재귀: 재귀 호출 (다음 칸부터) 재귀-->>탐색기: 반환 탐색기->>격자: 해당 영역을 1로 복구 Note over 격자: 상태 복구 end

이 패턴에서 자주 실수하는 부분이 영역 전체를 변경/복구하는 것이다. 이전에 분석한 BOJ17136 실패 코드에서 테두리만 지우고 내부를 안 지운 것이 바로 이 실수다.

// 올바른 영역 변경 — 이중 for문으로 전체 순회
for (int i = r; i < r + size; i++) {
    for (int j = c; j < c + size; j++) {
        grid[i][j] = value;
    }
}

패턴 4 — 그래프 탐색 + 조건

그래프 위에서 특정 조건을 만족하는 경로나 부분 그래프를 찾는 유형이다. 해밀턴 경로, 외판원 순회 등이 대표적이다.

  • 선택 단위 : 다음에 방문할 노드
  • 제약 : 이미 방문한 노드 재방문 금지, 거리/비용 제한 등
  • 상태 관리 : visited 배열 + 현재 비용/거리

이 유형은 보통 N이 작을 때(20 이하) 백트래킹으로 풀 수 있고, N이 크면 DP + 비트마스킹 등 다른 기법이 필요하다.

문제를 만났을 때 접근 순서

새로운 문제를 만났을 때, 아래 순서로 생각하면 빠르게 구조를 잡을 수 있다.

graph TD A["문제 읽기"] --> B{"모든 경우를
따져야 하는가?"} B -->|"아니오"| C["그리디/DP 고려"] B -->|"예"| D{"N이 충분히
작은가?"} D -->|"N > 20"| E["DP, 비트마스킹 등
다른 기법 고려"] D -->|"N ≤ 20"| F["백트래킹 결정"] F --> G{"어떤 패턴인가?"} G --> H["순열/조합/부분집합"] G --> I["2D 격자 배치"] G --> J["그래프 탐색"] H --> K["상태 설계 → 구현"] I --> K J --> K style A fill:#fff3e0,stroke:#e65100,stroke-width:2px style F fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style K fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
  1. 완전탐색이 필요한지 — "모든 경우" "최소" "최대" "가능한지" 같은 키워드가 있으면 완전탐색 계열을 의심
  2. N의 크기 확인 — 백트래킹은 N이 작을 때만 유효
  3. 패턴 매칭 — 순열/조합/격자/그래프 중 어디에 가까운지 판단
  4. 상태 설계 — 선택 단위, 유망 조건, 되돌리기 대상을 결정

면접 Q&A

Q. 백트래킹과 DFS의 차이는?

DFS는 그래프/트리를 깊이 우선으로 순회하는 탐색 방법이고, 백트래킹은 DFS를 하되 유망하지 않은 노드에서 되돌아오는 문제 해결 전략이다. 백트래킹은 DFS를 도구로 사용하지만, 모든 DFS가 백트래킹인 것은 아니다.

[!QUESTION] Q. 백트래킹과 완전탐색의 차이는?

완전탐색은 상태 공간의 모든 노드를 방문한다. 백트래킹은 유망하지 않은 가지를 잘라내서 일부만 방문한다. 가지치기가 없는 백트래킹은 곧 완전탐색이다.

[!QUESTION] Q. 백트래킹의 시간복잡도를 어떻게 분석하는가?

최악의 경우는 가지치기가 전혀 안 될 때로, 완전탐색과 같다 (순열이면 O(N!), 부분집합이면 O(2^N)). 하지만 실제 실행 시간은 가지치기 효과에 의해 훨씬 줄어든다. 정확한 분석은 어렵고, 보통 최악 기준으로 N의 범위를 보고 "통과 가능한가"를 판단한다.

[!QUESTION] Q. (함정) 백트래킹으로 항상 최적해를 구할 수 있는가?

백트래킹은 모든 유망한 경우를 탐색하므로 정확한 최적해를 보장한다. 단, 시간이 오래 걸릴 수 있다. 근사해를 빠르게 구하는 휴리스틱과는 다르다. 가지치기가 답을 잘못 잘라내지 않는 한, 최적해를 놓치지 않는다.

[!QUESTION] Q. (함정) 그리디로 풀 수 있는 문제를 굳이 백트래킹으로 풀어야 하나?

그리디로 풀 수 있다면 당연히 그리디가 더 효율적이다. 하지만 그리디가 최적해를 보장하려면 탐욕 선택 속성최적 부분 구조가 성립해야 한다. 이 두 조건을 증명할 수 없으면 그리디는 오답을 낼 수 있고, 그때 백트래킹이 안전한 대안이 된다.

[!QUESTION] Q. 백트래킹에서 "되돌리기"를 안 하면 어떻게 되는가?

이전 선택의 흔적이 남아서 이후 탐색이 전부 오염된다. 예를 들어 visited[i] = true를 한 뒤 복구하지 않으면, 다른 경로를 탐색할 때도 i번이 방문된 것으로 처리되어 탐색 공간이 부당하게 줄어든다. 결과적으로 정답을 놓치게 된다.

정리

패턴선택 단위핵심 상태시간복잡도
순열각 위치에 어떤 원소visited[]O(N!)
조합/부분집합각 원소를 포함 여부start 파라미터O(2^N)
2D 격자 배치특정 위치에 무엇을 놓을지2D 배열문제마다 다름
그래프 탐색다음 방문 노드visited[] + 비용O(N!) 이하

백트래킹은 결국 세 가지만 잘하면 된다. 상태를 잘 설계하고, 가지를 잘 치고, 되돌리기를 빠뜨리지 않는 것.