외판원 DP는 왜 현재 도시와 방문 집합을 함께 상태로 저장하나요?
외판원 DP의 상태는 “현재 도시, 지금까지 방문한 집합”입니다. 이 두 값이 같다면 앞으로 방문할 도시와 가능한 이동도 같아 남은 최솟값을 재사용할 수 있습니다.
적용 조건·범위: 분석 조건: 1≤n≤16의 학습용 비음수 정수 비용 행렬. 상태 수가 지수적으로 늘어나는 제한은 남습니다.
본문 근거: Competitive Programmer’s Handbook · MDN — Bitwise AND
읽기 전에 알아두면 좋아요
아래 개념이 익숙하면 본문을 바로 읽으세요. 낯선 개념만 링크에서 복습하면 됩니다.
- 외판원 문제: 모든 방문 순서를 비교하고 출발점으로 돌아오기방문할 도시를 하나씩 고르고 되돌아오며 가능한 순회 경로를 비교하는 방법을 복습합니다.
- DP 메모이제이션: 피보나치의 중복 호출 줄이기동적 계획법은 같은 작은 문제의 결과를 저장해 다시 계산하는 일을 줄이는 방법입니다.
- 비트마스킹 입문: 선택한 항목을 숫자에 기록하기비트마스크는 이진수의 각 자리를 원소 하나에 대응시켜 선택한 집합을 나타냅니다.
개념과 예제는 실행하지 않아도 읽을 수 있습니다. 내 컴퓨터·터미널에서 직접 실행하는 과정은 선택 실습입니다.
과정 위치와 선택 복습 안내
과정 위치: 유량·상태 압축과 계산 복잡도 (선택) · 알고리즘 개념
과정 위치는 이 글이 속한 묶음입니다. 이 단계의 모든 개념을 먼저 읽을 필요는 없습니다. 위에 연결한 개념 중 낯선 내용만 복습하세요. 문제의 조건·상태·갱신 순서는 익숙한 언어로 설명하면 됩니다.
JavaScript로 문법·실행 복습하기 (선택)
개념 글의 코드는 JavaScript 예시입니다. 문제 화면에서는 JavaScript·Python·Java·C++·C# 중 익숙한 언어의 함수를 작성하세요. 본문의 Node.js 입출력 예시는 로컬 실행을 위한 별도 안내입니다.

외판원 DP의 상태는 “현재 도시, 지금까지 방문한 집합”입니다. 이 두 값이 같다면 앞으로 방문할 도시와 가능한 이동도 같아 남은 최솟값을 재사용할 수 있습니다.
선수 지식: 외판원 완전탐색, 메모이제이션, 비트마스크의 추가·확인을 이해해야 합니다.
- 현재 도시와 방문 집합 두 값으로 상태를 정의합니다.
- 없던 간선을 건너뛰며 남은 비용과 선택 경로를 저장합니다.
같은 도시에 도착했어도 남은 도시는 다를 수 있습니다
도시 2에 있다는 사실만 저장하면 0과 1을 이미 방문한 상태와 0과 3을 방문한 상태를 구별하지 못합니다. 다음에 가야 할 도시가 다르므로 같은 문제라고 볼 수 없습니다. city와 mask를 함께 키로 쓰면 이 두 요청이 다른 칸에 들어갑니다. 반대로 방문한 순서가 달라도 현재 도시와 집합이 같으면 이후에 필요한 최소 비용은 같습니다.

| 현재 도시 | 방문 집합 | 남은 일 |
|---|---|---|
| 2 | {0,1,2} | 3을 방문한 뒤 0으로 복귀 |
| 2 | {0,2,3} | 1을 방문한 뒤 0으로 복귀 |
| 2 | {0,1,2,3} | 2→0 비용 확인 |
누적한 앞부분의 비용은 상태의 답에 넣지 않습니다. 함수는 현재 상태에서 여행을 완성하는 데 추가로 필요한 최소 비용만 반환합니다. 그래야 서로 다른 앞 경로에서도 같은 남은 답을 재사용할 수 있습니다.
한 비트를 추가하고 다음 상태의 남은 비용을 더합니다
방문하지 않은 next와 실제 간선이 있는 경우에만 이동합니다. 후보는 costs[city][next] + remaining(next, 새 mask)입니다. 현재 상태의 best를 Infinity로 시작해 후보 중 작은 값을 고릅니다. 모든 도시를 방문했다면 출발점으로 돌아가는 비용을 반환하고, 그 간선이 없으면 Infinity를 반환합니다.

memo.has(key)를 사용하므로 답이 Infinity인 불가능 상태도 다시 풀지 않습니다. choice에는 현재 상태에서 고른 다음 도시를 따로 저장합니다. 최솟값을 구한 뒤 이 선택만 따라가면 경로를 복원할 수 있습니다. 전체 부분집합별로 경로 배열까지 복사해 넣지 않아 저장량을 불필요하게 늘리지 않습니다.
지수 상태를 없앤 것이 아니라 같은 상태의 재계산을 줄였습니다
현재 도시는 n가지, 방문 집합은 최대 2^n가지이고 각 상태에서 다음 도시 n개를 살펴봅니다. 따라서 이 방식도 큰 n을 위한 만능 해결책은 아닙니다. 예제는 Number 비트 연산과 메모리 사용을 고려해 n≤16으로 제한하며, 실제 비교 테스트는 더 작은 입력에서 완전탐색과 결과를 맞춥니다.

출발점을 바꾸거나 함수를 다시 호출할 때 memo와 choice를 새로 만들어 상태를 섞지 않습니다. 같은 최소 비용의 경로가 여러 개이면 도시 번호가 작은 후보를 먼저 고른 결과 하나를 반환합니다. 비용뿐 아니라 반환한 경로가 각 도시를 한 번 방문하고 실제 간선으로 닫히는지도 확인해야 합니다.
전체 코드와 직접 실행
개념을 읽었다면 아래 예제를 직접 실행하며 상태 변화를 확인할 수 있습니다.
전체 JavaScript 코드와 실행 방법 펼치기
별도 패키지는 필요하지 않습니다. Node.js가 처음이라면 브라우저와 Node.js 시작 안내부터 확인하세요.
tsp-bitmask-dp.mjs 파일을 만들고 아래 전체 코드를 저장합니다.
function checkCosts(costs, start, limit) {
if (!Array.isArray(costs) || costs.length < 1 || costs.length > limit) {
throw new RangeError('허용 범위 안의 정사각 비용 행렬이 필요합니다.');
}
const n = costs.length;
if (!Number.isInteger(start) || start < 0 || start >= n) throw new RangeError('출발점 범위 오류');
for (const row of costs) {
if (!Array.isArray(row) || row.length !== n) throw new RangeError('정사각 행렬이 아닙니다.');
for (const value of row) {
if (value !== null && (!Number.isInteger(value) || value < 0 || value > 1000000)) {
throw new RangeError('비용은 null 또는 0 이상 1000000 이하의 정수입니다.');
}
}
}
}
function tspDP(costs, start = 0) {
checkCosts(costs, start, 16);
const n = costs.length;
if (n === 1) return { cost: 0, route: [start, start] };
const full = (1 << n) - 1;
const memo = new Map();
const choice = new Map();
function remaining(city, mask) {
if (mask === full) return costs[city][start] ?? Infinity;
const key = mask * n + city;
if (memo.has(key)) return memo.get(key);
let best = Infinity;
for (let next = 0; next < n; next++) {
if ((mask & (1 << next)) !== 0 || costs[city][next] === null) continue;
const candidate = costs[city][next] + remaining(next, mask | (1 << next));
if (candidate < best) { best = candidate; choice.set(key, next); }
}
memo.set(key, best);
return best;
}
let mask = 1 << start;
let city = start;
const cost = remaining(city, mask);
if (!Number.isFinite(cost)) return { cost: Infinity, route: [] };
const route = [start];
while (mask !== full) {
const next = choice.get(mask * n + city);
route.push(next); mask |= 1 << next; city = next;
}
route.push(start);
return { cost, route };
}
const costs = [[null, 2, 9, null], [1, null, 6, 4], [null, 7, null, 8], [6, 3, null, null]];
console.log(JSON.stringify(tspDP(costs)));
파일이 있는 폴더의 터미널에서 실행합니다.
node tsp-bitmask-dp.mjs
기대 출력
{"cost":21,"route":[0,2,3,1,0]}
비용과 적용 범위
시간: 최대 O(n²·2^n)번의 상태 전이 확인입니다.

공간: 입력 외 메모·다음 선택은 O(n·2^n), 재귀 깊이는 O(n)입니다.
분석 조건: 1≤n≤16의 학습용 비음수 정수 비용 행렬. 상태 수가 지수적으로 늘어나는 제한은 남습니다.
직접 확인해 보기
출발점이 0이고 방문 집합이 0101이면 2번 도시는 다시 방문할 후보인가요?

답과 이유 확인
아닙니다. 0101의 2번 비트가 이미 켜져 있으므로 0번과 2번을 방문한 상태입니다. 남은 1번과 3번 중 이동 가능한 도시를 고릅니다.
함께 읽을 글
- Map·Set으로 값 조회와 중복 제거 — 숫자 키별 메모와 저장 여부 확인을 이해합니다.
- 다익스트라의 원리 — 경로 문제라도 최적화하려는 목표와 상태가 다름을 확인합니다.
참고 자료
- Competitive Programmer’s Handbook — 동적 계획법의 상태·전이, 부분집합과 순회 최적화의 일반 원리를 대조합니다. 예제와 설명은 이 원고의 독립 구현입니다.
- MDN — Bitwise AND — Number 비트 연산의 32비트 변환을 확인합니다. 예제는 부호 비트를 피하는 20개 위치만 허용합니다.
이 글이 도움이 되었나요?
이어서 공부하기
체크포인트: 예제의 상태 변화를 손으로 추적하고 아래 문제에 적용하세요. 값이 없는 경우와 원소 하나인 경우도 설명해 보세요. 작은 기준 풀이와 비교하고 경로 복원·역방향 취소·입력 크기에 따른 비용을 설명하세요.
다음 추천 · 알고리즘 개념: P와 NP: 답을 찾는 일과 확인하는 일의 차이
권장 순서는 학습 가이드와 같습니다. 다른 순서는 접힌 참고 목록에서 확인할 수 있습니다. 읽음 표시는 숙달이나 채점 결과가 아닙니다.
원래 과정 목록 순서 (선택)
읽음 기록
읽음 — / 전체 30개 · 필수 0개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.