[백준 9376번] 탈옥
- 알고리즘/문제풀이
- 2024. 5. 23. 02:51
문제 링크
https://www.acmicpc.net/problem/9376
문제 요약
두 죄수 A, B가 최소한의 문을 열어 감옥에서 탈출하려고 한다. 한 죄수가 문을 열면, 그 문은 계속 열려 있어서 다른 죄수도 그 문을 이용할 수 있다.
(원래 문제에서는 감옥 밖에 있는 상근이가 특별한 기술을 이용해 문을 열어준다고 되어 있지만, 문제를 더 쉽게 이해할 수 있게 내용을 살짝 바꿨다.)
문제 풀이
가장 먼저 떠오르는 생각은 두 죄수의 상태를 동시에 관리하는 것이다. 하지만 죄수가 있을 수 있는 장소는 1만 곳이고, 이를 두 죄수의 상태로 표현하면 1억 가지의 경우의 수가 생긴다. 여기에 탈출을 위해 이용한 문의 개수까지 고려한다면 이 방법은 문제를 제한 시간 내에 풀 수 있는 방법이 아니다.
경우의 수 나누기
이 문제를 풀기 위해서는 두 죄수가 어떤 전략을 가지고 움직여야 최적해를 찾을 수 있을지 경우의 수를 나눠야 한다. 죄수 A와 B는 최적해를 위해 아래 두 가지 전략 중에 하나를 선택할 수 있다.
- 죄수 A와 B가 따로 탈출한다.
- 죄수 A와 B는 어느 한 점에 만나 함께 탈출한다.
1번 전략을 선택했을 경우, 최적해가 될 수 있다는 것은 직관적으로 이해할 수 있다. 하지만 2번 전략의 경우, 반드시 한 지점에서 만나서 같이 움직여야 최적해가 된다는 점이 와닿지 않을 수 있다. 그러면 A와 B가 만난 뒤 따로 움직이는 경우가 최적해라고 가정을 해보자. A와 B가 만난 다음, A가 사용한 문들을 B가 사용하지 않고 따로 움직였을 때 최적해가 된다면, 그냥 A가 B를 따라가면 더 좋은 최적해가 되지 않을까? 즉 이 가설 자체가 모순이기 때문에 A와 B가 만나면 항상 같이 다니는 것이 최적해라는 것을 알 수 있다.
최적해는 두 가지 경우 중 하나에서 나온다는 것을 알았으니, 이제 각각의 경우에서 최적해를 구한 다음에 그중 더 작은 값을 출력하면 문제를 풀 수 있다.
그래프 모델링 하기
최적해를 구하기 전에 문제에서 주어진 감옥을 그래프로 모델링 해보자. 2차원 행렬을 그래프로 모델링할 때 1차원으로 압축시키면 문제를 더 쉽게 풀 수 있다.
int getIdx(int i, int j) {
return i * w + j;
}
그다음 각 노드들 간의 관계를 가중치가 있는 그래프로 표현할 수 있다. 빈칸에서 문으로 이동할 때는 가중치를 1로 두고, 빈칸에서 빈칸, 문에서 빈칸으로 이동할 때는 가중치를 0으로 모델링하면 탈출할 때 최소한의 문을 사용하는 조건을 최단거리를 구하는 문제로 바꿔 풀 수 있게 된다.
그런데 A와 B가 탈출했다는 것을 어떻게 탐지할 수 있을까? 벽이 아닌 모서리에 도착했을 때 A와 B가 탈출했다고 판단을 할 수 있다. 그러나 탈출 지점을 의미하는 가상의 노드를 하나 만들어 벽이 아닌 모서리들과 연결하면, 하나의 노드만 보고도 A와 B가 탈출했다는 것을 알 수 있게 된다.
다익스트라를 이용해 답을 구하기
다익스트라 알고리즘을 이용해 최단거리를 구한 다음 각각의 경우에 수에 대해 최적해를 계산해 준다. 먼저 A와 B가 따로 탈출하는 경우를 계산해 보자.
int case1_ans = dijkstra(A_IDX, EXIT_IDX) + dijkstra(B_IDX, EXIT_IDX);
그다음은 A와 B가 한 지점에 만나서 같이 탈출하는 경우의 최적해를 구해야 한다. A와 B가 어느 곳에서 만나야 최적의 경우인지 모르니 만날 수 있는 모든 곳에 대해서 최적해를 계산해 준다. 만약 A와 B가 만나는 곳이 문이라면 한 명만 문을 열면 되니 1을 빼준다.
int case2_ans = INF;
for (int i = 0 ; i < h ; ++i) {
for (int j = 0 ; j < w ; ++j) {
if (A[i][j] == '*') {
continue;
}
int MEETING_IDX = getIdx(i, j);
int dist = dijkstra(A_IDX, MEETING_IDX) + dijkstra(B_IDX, MEETING_IDX) + dijkstra(MEETING_IDX, EXIT_IDX);
if (A[i][j] == '#') {
dist -= 1;
}
case2_ans = min(case2_ans, dist);
}
}
여기서 끝나면 좋겠지만, 이대로 제출하면 시간초과가 발생한다. 만날 수 있는 지점이 1만 개나 되기 때문에 다익스트라를 1만 번이나 돌려야 하며, 거기다 테스트 케이스까지 있으니 시간초과가 나는 것은 당연한 일이다.
다익스트라 결과 배열을 이용해 답을 구하기
A와 B가 만날 수 있는 지점마다 다익스트라를 매번 새롭게 돌려야 해서 시간초과가 발생한다. A와 B가 만날 수 있는 모든 지점들에 대해서 탈출 지점까지 가는 최단 거리를 빠르게 구할 수 있는 방법이 없을까? 우리가 모델링한 그래프가 무향 그래프이기 때문에 출발 지점과 도착 지점을 반대로 생각해도 최단거리가 똑같다는 점을 이용할 수 있다. 탈출 지점에서 다익스트라를 한 번 돌리면 탈출 지점에서 시작해 도달할 수 있는 모든 지점들에 대한 최단거리를 구할 수 있고, 결국 이 값들이 각 지점에서 탈출 지점으로 가는 최단 거리가 된다.
vector<int> startFromA = dijkstra2(A_IDX);
vector<int> startFromB = dijkstra2(B_IDX);
vector<int> startFromExit = dijkstra2(EXIT_IDX);
int ans = startFromA[n - 1] + startFromB[n - 1];
for (int i = 0 ; i < h ; ++i) {
for (int j = 0 ; j < w ; ++j) {
if (A[i][j] == '*') {
continue;
}
int MEETING_IDX = getIdx(i, j);
int dist = startFromA[MEETING_IDX] + startFromB[MEETING_IDX] + startFromExit[MEETING_IDX];
if (A[i][j] == '#') {
dist -= 2;
}
ans = min(ans, dist);
}
}
printf("%d\n", ans);
한 가지 주의할 점은 A와 B가 한 지점에서 만나서 같이 움직일 때의 최적해를 구할 때, 만나는 곳이 문이라면 1이 아니라 2를 빼줘야 한다는 것이다. 왜냐면 이 전과 다르게 탈출 지점에서 만나는 곳까지 오는 것으로 계산했기 때문이다.
어렵다고 생각할 수 있는 지점을 최대한 쉽게 설명하려고 하다보니 생각했던 것보다 글이 너무 길어졌다. 설명이 길어진 만큼 누군가에게는 쉽게 전달이 됐으면 좋겠다.
소스 코드
#include <stdio.h>
#include <vector>
#include <queue>
using namespace std;
const int INF = 10000000;
const int dx[] = {-1, 0, 1, 0};
const int dy[] = {0, -1, 0, 1};
int t, h, w, n;
char A[110][110];
vector<vector<pair<int, int>>> nodes;
int getIdx(int i, int j) {
return i * w + j;
}
void buildGraph() {
nodes.assign(n, vector<pair<int, int>>());
for (int i = 0 ; i < h ; ++i) {
for (int j = 0 ; j < w ; ++j) {
if (A[i][j] == '*') {
continue;
}
int here = getIdx(i, j);
for (int k = 0 ; k < 4 ; ++k) {
int ni = i + dx[k], nj = j + dy[k];
if (ni == -1 || ni == h || nj == -1 || nj == w) {
nodes[here].push_back({ n - 1, 0 });
nodes[n - 1].push_back({ here, A[i][j] == '#' ? 1 : 0 });
} else if (A[ni][nj] != '*') {
int there = getIdx(ni, nj);
nodes[here].push_back({ there, A[ni][nj] == '#' ? 1 : 0 });
}
}
}
}
}
vector<int> dijkstra2(int src) {
vector<int> dist(n, INF);
priority_queue<pair<int, int>> pq;
dist[src] = 0;
pq.push({ 0, src });
while (!pq.empty()) {
int cost = -pq.top().first;
int here = pq.top().second;
pq.pop();
if (dist[here] < cost) {
continue;
}
for (const auto& node: nodes[here]) {
int there = node.first;
int nextDist = cost + node.second;
if (dist[there] > nextDist) {
dist[there] = nextDist;
pq.push({ -nextDist, there });
}
}
}
return dist;
}
int main() {
freopen("input.txt", "r", stdin);
scanf("%d", &t);
while (t--) {
scanf("%d%d", &h, &w);
n = h * w + 1;
int A_IDX = -1, B_IDX = -1, EXIT_IDX = n - 1;
for (int i = 0 ; i < h ; ++i) {
scanf("%s", A[i]);
for (int j = 0 ; j < w ; ++j) {
if (A[i][j] == '$') {
if (A_IDX == -1) {
A_IDX = getIdx(i, j);
} else {
B_IDX = getIdx(i, j);
}
}
}
}
buildGraph();
vector<int> startFromA = dijkstra2(A_IDX);
vector<int> startFromB = dijkstra2(B_IDX);
vector<int> startFromExit = dijkstra2(EXIT_IDX);
int ans = startFromA[n - 1] + startFromB[n - 1];
for (int i = 0 ; i < h ; ++i) {
for (int j = 0 ; j < w ; ++j) {
if (A[i][j] == '*') {
continue;
}
int MEETING_IDX = getIdx(i, j);
int dist = startFromA[MEETING_IDX] + startFromB[MEETING_IDX] + startFromExit[MEETING_IDX];
if (A[i][j] == '#') {
dist -= 2;
}
ans = min(ans, dist);
}
}
printf("%d\n", ans);
}
}
'알고리즘 > 문제풀이' 카테고리의 다른 글
[백준 15989번] 1, 2, 3, 더하기 4 (0) | 2024.05.28 |
---|---|
[백준 17086번] 아기 상어 2 (0) | 2024.05.26 |
[백준 10021번] Watering the Fields (0) | 2024.05.20 |
[백준 17404번] RGB거리 2 (0) | 2024.05.20 |
[백준 1022번] 소용돌이 예쁘게 출력하기 (0) | 2024.05.18 |