문제
https://www.acmicpc.net/problem/2098
2098번: 외판원 순회
첫째 줄에 도시의 수 N이 주어진다. (2 ≤ N ≤ 16) 다음 N개의 줄에는 비용 행렬이 주어진다. 각 행렬의 성분은 1,000,000 이하의 양의 정수이며, 갈 수 없는 경우는 0이 주어진다. W[i][j]는 도시 i에서 j
www.acmicpc.net
1.완전 탐색
처음엔 [BOJ 10971] 외판원 순회2 문제와 동일하게 모든 도시를 각각 출발지로 하여 N번 순회를 수행했지만 시간 초과가 발생했다.
#include<iostream>
#include<vector>
using namespace std;
#define INF 987654321
int N, edge[17][17], ans = INF, start;
void init() {
cin >> N;
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= N; j++) {
cin >> edge[i][j];
if (i == j) continue;
}
}
}
void travel(int cur, int cost, int visited, int cnt) {
if (cur == start && cost != 0) {
ans = ans > cost ? cost : ans;
return;
}
for (int next = 1; next <= N; next++) {
if (!edge[cur][next]) continue;
if (cnt != N - 1 && next == start) continue;
if(next != start && (visited & (1 << next)) != 0) continue;
travel(next, cost + edge[cur][next], visited | (1 << next), cnt + 1);
}
}
int main() {
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
init();
for (int i = 1; i <= N; i++) {
start = i;
travel(i, 0, 1 << i, 0);
}
cout << ans;
return 0;
}
2.순회 한번만 수행
4
0 10 15 20
5 0 9 10
6 13 0 12
8 8 9 0
직접 위 예제를 완전 탐색으로 N번 순회를 수행해본 결과 출발 도시를 바꿔가며 N번 순회를 수행할 필요가 없다는 것을 깨달았다.
0 -> 2 -> 1 -> 3 -> 0
2 -> 1 -> 3 -> 0 -> 2
위 경우 0->2의 순서만 달라지는 것이기 때문에 두 경우는 동일한 것은 자명한 사실이었던 것이다...
이에 따라 순회를 한번만 수행하도록 코드를 수정해 주었다.
#include<iostream>
#include<vector>
using namespace std;
#define INF 987654321
int N, edge[17][17], ans = INF, start;
void init() {
cin >> N;
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= N; j++) {
cin >> edge[i][j];
if (i == j) continue;
}
}
}
void travel(int cur, int cost, int visited, int cnt) {
if (cur == 1 && cost != 0) {
ans = ans > cost ? cost : ans;
return;
}
for (int next = 1; next <= N; next++) {
if (!edge[cur][next]) continue;
if (cnt != N - 1 && next == 1) continue;
if(next != 1 && (visited & (1 << next)) != 0) continue;
travel(next, cost + edge[cur][next], visited | (1 << next), cnt + 1);
}
}
int main() {
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
init();
travel(1, 0, 1 << 1, 0);
cout << ans;
return 0;
}
외판원 순회2번 문제로 비교해본 결과 확실히 속도가 향상되었다.
하지만 본 문제의 경우 위 처럼 로직을 수정해 주어도 여전히 시간 초과가 발생했다.
외판원 순회2 문제의 경우 노드의 개수가 최대 10개이기 때문에 완전 탐색으로 해결 가능하나 본 문제의 경우 노드가 최대 16개이기 때문에 완전 탐색으로는 시간 초과가 발생했다.
따라서 본 문제의 경우 DP와 비트마스킹을 사용하여 해결해주었다.
3.DP + 비트마스킹
DP memoization을 통해 동일한 계산에 대한 반복 연산을 줄이고 비트마스킹을 사용하여 방문한 도시들의 상태를 나타내었다.
즉, dp[3][11](=dp[3][1011]) = 15은 현재 3번 도시를 0, 1번 도시를 거쳐 방문했고 아직 방문하지 않은 나머지 도시들을 모두 방문한 뒤 출발 도시로 돌아가는 최소 비용은 15라는 의미이다.
코드와 예시를 통해 어떻게 반복 연산을 방지해주는지 살펴 보자
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
// 비트마스킹 + DP
#define INF 1987654321
int N, edge[16][16];
// dp[k][state] : k번째 마을, state: 방문했던 도시들을 2진법으로 표시.
int dp[16][1 << 16];
int TSP(int cur, int visited) {
int& ret = dp[cur][visited];
if (ret != -1)
return ret;
// 모든 마을 방문했는가
if (visited == (1 << N) - 1) {
// current -> 0 이동 가능한지
if (edge[cur][0] != 0)
return edge[cur][0];
// 가능하지 않으면
return INF;
}
ret = INF;
for (int next = 0; next < N; ++next) {
if (!edge[cur][next]) continue; //cur -> next로 간선이 없으면 pass
if (visited & (1 << next)) continue; //next가 이미 방문됐으면 pass
ret = min(ret, TSP(next, visited | (1 << next)) + edge[cur][next]);
}
return ret;
}
void init() {
cin >> N;
for (int i = 0; i < N; ++i)
for (int j = 0; j < N; ++j)
cin >> edge[i][j];
memset(dp, -1, sizeof(dp));
}
int main() {
ios_base::sync_with_stdio(false); cout.tie(NULL); cin.tie(NULL);
init();
cout << TSP(0, 1) << '\n';
return 0;
}
위 예제를 통해 살펴보면
0 -> 1 -> 3 -> 2 -> 0 을 순회하면서 dp[2][1111] = 6이 저장 된다.
이는 0, 1, 3번 도시를 방문한 뒤 2번 도시를 방문했을 때 다시 0으로 돌아가는 모든 경로 중 6이 최단 거리라는 것을 의미하게 되는 것이다.
이에 따라 0 -> 3 -> 1 -> 2 -> 0 을 순회할 때 2->0으로 가는 비용은 다시 구할 필요가 없게 되는 것이다.
'Alrogithm > 백준(BOJ)' 카테고리의 다른 글
[백준(BOJ)] 18185 라면 사기(Small) (0) | 2022.11.02 |
---|