외판원 문제: 모든 방문 순서를 비교하고 출발점으로 돌아오기

·설명 약 4분·작성: 해비·블로그 소개

외판원 문제는 모든 도시를 방문하는 어떤 경로를 찾나요?

외판원 문제는 한 출발점에서 모든 도시를 한 번씩 방문하고 돌아오는 경로 중 총비용이 가장 작은 것을 찾습니다. 한 목적지까지 가는 최단 경로와 목표가 다릅니다.

적용 조건·범위: 분석 조건: 1≤n≤9, 각 비용은 null 또는 0~1,000,000의 정수이며 방향 비용을 따로 봅니다.

본문 근거: Competitive Programmer’s Handbook

읽기 전에 알아두면 좋아요

아래 개념이 익숙하면 본문을 바로 읽으세요. 낯선 개념만 링크에서 복습하면 됩니다.

개념과 예제는 실행하지 않아도 읽을 수 있습니다. 내 컴퓨터·터미널에서 직접 실행하는 과정은 선택 실습입니다.

과정 위치와 선택 복습 안내

과정 위치: 유량·상태 압축과 계산 복잡도 (선택) · 알고리즘 개념

과정 위치는 이 글이 속한 묶음입니다. 이 단계의 모든 개념을 먼저 읽을 필요는 없습니다. 위에 연결한 개념 중 낯선 내용만 복습하세요. 문제의 조건·상태·갱신 순서는 익숙한 언어로 설명하면 됩니다.

JavaScript로 문법·실행 복습하기 (선택)

개념 글의 코드는 JavaScript 예시입니다. 문제 화면에서는 JavaScript·Python·Java·C++·C# 중 익숙한 언어의 함수를 작성하세요. 본문의 Node.js 입출력 예시는 로컬 실행을 위한 별도 안내입니다.

외판원 문제: 모든 방문 순서를 비교하고 출발점으로 돌아오기 주제를 표현한 밝은 일러스트와 작은 곰 표지

외판원 문제는 한 출발점에서 모든 도시를 한 번씩 방문하고 돌아오는 경로 중 총비용이 가장 작은 것을 찾습니다. 한 목적지까지 가는 최단 경로와 목표가 다릅니다.

선수 지식: 재귀·백트래킹의 되돌리기, 배열과 가중 그래프의 뜻을 알아야 합니다.

  • 방문할 수 있는 다음 도시만 선택합니다.
  • 마지막 복귀 간선까지 포함한 순환 비용을 비교합니다.

마지막 도시를 방문해도 아직 여행이 끝난 것은 아닙니다

도시 0, 1, 2가 있고 0→1이 2, 1→2가 3이라도 2→0이 없으면 이 순서는 완성된 순환이 아닙니다. 모든 도시를 방문했다는 조건과 출발점으로 돌아갈 수 있다는 조건을 함께 만족해야 합니다. 이 글은 null을 “간선 없음”으로, 숫자 0을 실제 무료 이동으로 사용합니다. 두 뜻을 같은 0에 넣지 않습니다.

도시 0에서 1과 2를 방문했지만 2에서 0으로 돌아갈 간선이 없어 순환이 되지 않는 그림
0→1 비용 2, 1→2 비용 3을 지나도 마지막 복귀 간선이 없으면 순환 후보가 아닙니다. null과 실제 비용 0의 의미도 구분합니다. 원본 보기 (새 탭)
작은 입력에서 달라지는 상태
상태 확인할 것 다음 행동
현재 경로 [0] 방문하지 않은 이웃 1 또는 2로 분기
현재 경로 [0,1] 1에서 2로 가는 간선 있으면 비용을 더해 진행
현재 경로 [0,1,2] 2에서 0으로 복귀 가능 여부 가능할 때만 최소 비용 후보

예제 비용 행렬의 행은 출발 도시, 열은 도착 도시입니다. 반대 방향의 비용이 같을 필요는 없습니다. 무방향 여행을 다루려면 입력을 대칭으로 준비해야 합니다.

한 선택을 끝내면 방문 표시와 경로를 되돌립니다

next로 이동하기 직전에 visited[next]를 켜고 path에 추가합니다. 그 분기의 탐색이 끝나면 path에서 꺼내고 visited[next]를 끕니다. 다음 분기는 방금 선택하기 전과 같은 상태에서 시작해야 합니다. 방문 기록을 복구하지 않으면 다른 순서에서 사용할 도시까지 이미 방문한 것으로 남아 정답 후보가 사라집니다.

도시 1 분기의 탐색 전후에 경로와 방문 표시를 복구하는 세 상태
1을 선택할 때 path에 넣고 visited[1]을 켭니다. 탐색이 끝나면 꺼내고 표시를 꺼서 다음 분기가 같은 상태에서 시작하게 합니다. 원본 보기 (새 탭)

누적 비용은 함수 인자로 전달해 각 경로가 자기 값을 갖게 합니다. 완성된 순환의 최솟값은 호출 전체에서 공유해도 되지만, 아직 끝나지 않은 하위 문제의 최솟값을 전역 변수 하나로 돌려주면 서로 다른 상태의 답이 섞일 수 있습니다. 이 구현은 완성된 순환만 best와 비교하고 경로도 복사해 보관합니다.

작은 도시 수에서 정확성 기준으로 사용합니다

n개 도시에서 출발점을 고정해도 나머지 방문 순서가 빠르게 늘어납니다. 이 학습용 완전탐색은 n을 1~9로 제한합니다. 이는 9개까지 항상 어떤 기기에서 몇 초 안에 끝난다는 보장이 아니라 예제를 무리하게 확장하지 않기 위한 입력 계약입니다. 도시 하나만 있으면 이동 없이 비용 0으로 처리합니다.

도시 하나·순환 없음·학습 입력 제한의 세 계약
도시 하나는 이동 없이 비용 0입니다. 순환이 없으면 Infinity와 빈 경로로 구분하며, n≤9는 학습용 제한입니다. 원본 보기 (새 탭)

순환이 없으면 cost는 Infinity, route는 빈 배열입니다. Infinity를 JSON으로 직렬화하면 null로 보일 수 있으므로 표시할 때 “경로 없음”으로 구분해야 합니다. 표준 예제의 출력은 실제 순환이 있는 경우이며, 테스트는 마지막 복귀 간선이 없는 경우도 별도로 확인합니다.

전체 코드와 직접 실행

개념을 읽었다면 아래 예제를 직접 실행하며 상태 변화를 확인할 수 있습니다.

전체 JavaScript 코드와 실행 방법 펼치기

별도 패키지는 필요하지 않습니다. Node.js가 처음이라면 브라우저와 Node.js 시작 안내부터 확인하세요.

tsp-backtracking.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 tspBrute(costs, start = 0) {
  checkCosts(costs, start, 9);
  const n = costs.length;
  if (n === 1) return { cost: 0, route: [start, start] };
  const visited = Array(n).fill(false);
  const path = [start];
  visited[start] = true;
  let best = Infinity;
  let bestRoute = [];
  function search(city, accumulated) {
    if (path.length === n) {
      const back = costs[city][start];
      if (back !== null && accumulated + back < best) {
        best = accumulated + back;
        bestRoute = [...path, start];
      }
      return;
    }
    for (let next = 0; next < n; next++) {
      if (visited[next] || costs[city][next] === null) continue;
      visited[next] = true; path.push(next);
      search(next, accumulated + costs[city][next]);
      path.pop(); visited[next] = false;
    }
  }
  search(start, 0);
  return { cost: best, route: bestRoute };
}

const costs = [[null, 2, 9, null], [1, null, 6, 4], [null, 7, null, 8], [6, 3, null, null]];
console.log(JSON.stringify(tspBrute(costs)));

파일이 있는 폴더의 터미널에서 실행합니다.

node tsp-backtracking.mjs

기대 출력

{"cost":21,"route":[0,2,3,1,0]}

비용과 적용 범위

시간: 출발점 고정 후 순열을 탐색합니다. 보수적으로 O(n·(n−1)!)로 상한을 설명합니다.

출발점을 고정한 뒤 남은 도시의 순열을 탐색하는 시간 상한과 방문·경로·스택 공간
남은 n−1개 도시의 순서를 비교합니다. 이 글은 시간 상한을 O(n·(n−1)!)로 설명하고, 입력 행렬을 제외한 방문 표시·경로·재귀 스택을 O(n)으로 셉니다. 원본 보기 (새 탭)

공간: 입력 행렬을 제외하고 방문·경로·재귀 스택 O(n)입니다.

분석 조건: 1≤n≤9, 각 비용은 null 또는 0~1,000,000의 정수이며 방향 비용을 따로 봅니다.

직접 확인해 보기

[[null,1],[null,null]]에서 출발점을 0으로 두면 비용 1의 순환이 있나요?

0에서 1로 가는 비용 1의 방향 간선과 원본 비용 행렬로 순환 여부를 묻는 연습
비용 행렬의 행과 열을 방향에 맞게 읽고, 모든 도시를 방문한 뒤 출발점으로 돌아올 수 있는지 직접 확인합니다. 원본 보기 (새 탭)
답과 이유 확인

없습니다. 0→1로 갈 수 있지만 1→0이 없으므로 완성된 순환이 아닙니다. Infinity와 빈 경로를 반환합니다.

참고 자료

  • Competitive Programmer’s Handbook — 동적 계획법의 상태·전이, 부분집합과 순회 최적화의 일반 원리를 대조합니다. 예제와 설명은 이 원고의 독립 구현입니다.

이 글이 도움이 되었나요?

조회 중

이어서 공부하기

체크포인트: 예제의 상태 변화를 손으로 추적하고 아래 문제에 적용하세요. 값이 없는 경우와 원소 하나인 경우도 설명해 보세요. 작은 기준 풀이와 비교하고 경로 복원·역방향 취소·입력 크기에 따른 비용을 설명하세요.

다음 추천 · 알고리즘 개념: 외판원 DP: 현재 도시와 방문 집합을 함께 저장하기

같은 주제에서 다음 개념과 예제로 이해를 넓힙니다.

권장 순서는 학습 가이드와 같습니다. 다른 순서는 접힌 참고 목록에서 확인할 수 있습니다. 읽음 표시는 숙달이나 채점 결과가 아닙니다.

원래 과정 목록 순서 (선택)

읽음 기록

읽음 — / 전체 30개 · 필수 0개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기