문제 설명
외판원 순회 문제는 영어로 Traveling Salesman problem (TSP) 라고 불리는 문제로 computer science 분야에서 가장 중요하게 취급되는 문제 중 하나이다. 여러 가지 변종 문제가 있으나, 여기서는 가장 일반적인 형태의 문제를 살펴보자.
1번부터 N번까지 번호가 매겨져 있는 도시들이 있고, 도시들 사이에는 길이 있다. (길이 없을 수도 있다) 이제 한 외판원이 어느 한 도시에서 출발해 N개의 도시를 모두 거쳐 다시 원래의 도시로 돌아오는 순회 여행 경로를 계획하려고 한다. 단, 한 번 갔던 도시로는 다시 갈 수 없다. (맨 마지막에 여행을 출발했던 도시로 돌아오는 것은 예외) 이런 여행 경로는 여러 가지가 있을 수 있는데, 가장 적은 비용을 들이는 여행 계획을 세우고자 한다.
각 도시간에 이동하는데 드는 비용은 행렬 W[i][j]형태로 주어진다. W[i][j]는 도시 i에서 도시 j로 가기 위한 비용을 나타낸다. 비용은 대칭적이지 않다. 즉, W[i][j] 는 W[j][i]와 다를 수 있다. 모든 도시간의 비용은 양의 정수이다. W[i][i]는 항상 0이다. 경우에 따라서 도시 i에서 도시 j로 갈 수 없는 경우도 있으며 이럴 경우 W[i][j]=0이라고 하자.
N과 비용 행렬이 주어졌을 때, 가장 적은 비용을 들이는 외판원의 순회 여행 경로를 구하는 프로그램을 작성하시오.
제한 사항
풀이
문제를 요약하면, 시작 지점부터 모든 노드를 정확히 한 번만 방문한 뒤, 시작 지점을 돌아오는 경로의 최솟값을 구하는 것이다.
해당 문제는 유명한 유형의 문제이다.
문제에서 요구하는 경로를 해밀턴 회로라고 한다.
해밀턴 회로는 dfs를 통해 $O(n!)$ 시간에 구해낼 수 있다.
하지만, 동적계획법을 통해 $O(2^nn^2)$ 시간에 구하는 방법이 있다.
동적계획법은 각 노드를 비트로 표현하여 $v_i$노드로 부터 방문한 노드들의 집합의 최솟값을 기록한다.
예를 들어, dp[1][$110_2$]이라면 1번 노드에서 {2, 3} 노드들을 방문한 뒤 돌아오는 최솟값을 의미한다.
비트로 노드의 집합을 나타낸다면 모든 노드를 나타내는 비트는 $111...$이 된다.
이때, 1의 개수는 노드의 개수이다.
따라서, 다음과 같이 표현할 수 있다.
Max = (1 << N) - 1;
그렇다면 모든 노드를 방문할 때까지 dfs를 진행하며 dp를 갱신하면 된다.
이때, dp자체로 방문 여부를 체크할 수 있다.
dp의 값들을 모두 -1로 초기화한 뒤, 방문하여 값이 갱신한다.
만약 dfs를 진행 중, dp의 값이 -1이 아니라면 이미 방문한 경우이기 때문에 바로 값을 반환한다.
그렇지 않고 최초로 방문하는 경우라면 dp를 갱신해야 한다.
0번 노드는 시작 노드이기 때문에 1번 노드부터 시작하여 N-1번 노드까지 방문해 본다.
이때 방문 노드 역시 비트로 표현해야 한다.
해당 분기에 방문 노드에 이미 포함되어 있거나 갈 수 있는 길이 없다면 다음 노드를 탐색한다.
만약, 모든 조건에 만족하여 방문할 수 있다면 방문 노드 집합에 포함하여 dfs를 진행한다.
dfs를 통해 반환된 값에 현재 노드에서 다음 노드로의 가중치를 더해 dp를 갱신한다.
int dfs(int cur, int mask)
{
if (mask == Max) // 방문 완료
{
return graph[cur][0] ? graph[cur][0] : INF;
}
if (dp[cur][mask] != -1) // 방문 체크
{
return dp[cur][mask];
}
dp[cur][mask] = INF;
for (int next = 1; next < N; next++)
{
int target = 1 << next;
if ((mask & target) || graph[cur][next] == 0) // 이미 방문했거나 길이 없는 경우
{
continue;
}
dp[cur][mask] = min(dp[cur][mask], dfs(next, mask | target) + graph[cur][next]);
}
return dp[cur][mask];
}
전체 코드
#include <stdio.h>
#include <cstring>
#include <string>
#include <vector>
#include <algorithm>
#include <iostream>
#include <sstream>
#include <cmath>
#include <climits>
#include <queue>
#include <map>
#include <unordered_map>
#include <set>
using namespace std;
vector<vector<int>> graph, dp;
int N, Max;
const int INF = 987'654'321;
int dfs(int cur, int mask)
{
if (mask == Max) // 방문 완료
{
return graph[cur][0] ? graph[cur][0] : INF;
}
if (dp[cur][mask] != -1) // 방문 체크
{
return dp[cur][mask];
}
dp[cur][mask] = INF;
for (int next = 1; next < N; next++)
{
int target = 1 << next;
if ((mask & target) || graph[cur][next] == 0) // 이미 방문했거나 길이 없는 경우
{
continue;
}
dp[cur][mask] = min(dp[cur][mask], dfs(next, mask | target) + graph[cur][next]);
}
return dp[cur][mask];
}
int main()
{
ios_base::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> N;
Max = (1 << N) - 1;
dp.assign(N, vector<int>(Max + 1, -1));
graph.assign(N, vector<int>(N));
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
cin >> graph[i][j];
}
}
cout << dfs(0, 1);
return 0;
}