문제
크기가 1x1인 정사각형으로 나누어진 WxH 크기의 지도가 있다. 지도의 각 칸은 빈 칸이거나 벽이며, 두 칸은 'C'로 표시되어 있는 칸이다.
'C'로 표시되어 있는 두 칸을 레이저로 통신하기 위해서 설치해야 하는 거울 개수의 최솟값을 구하는 프로그램을 작성하시오. 레이저로 통신한다는 것은 두 칸을 레이저로 연결할 수 있음을 의미한다.
레이저는 C에서만 발사할 수 있고, 빈 칸에 거울('/', '')을 설치해서 방향을 90도 회전시킬 수 있다.
아래 그림은 H = 8, W = 7인 경우이고, 빈 칸은 '.', 벽은 '*'로 나타냈다. 왼쪽은 초기 상태, 오른쪽은 최소 개수의 거울을 사용해서 두 'C'를 연결한 것이다.
7 . . . . . . . 7 . . . . . . . 6 . . . . . . C 6 . . . . . /-C 5 . . . . . . * 5 . . . . . | * 4 * * * * * . * 4 * * * * * | * 3 . . . . * . . 3 . . . . * | . 2 . . . . * . . 2 . . . . * | . 1 . C . . * . . 1 . C . . * | . 0 . . . . . . . 0 . -------/ . 0 1 2 3 4 5 6 0 1 2 3 4 5 6
입력
첫째 줄에 W와 H가 주어진다. (1 <= W, H <= 100)
둘째 줄부터 H개의 줄에 지도가 주어진다. 지도의 각 문자가 의미하는 것은 다음과 같다.
- .: 빈 칸
- *: 벽
- C: 레이저로 연결해야 하는 칸
'C'는 항상 두 개이고, 레이저로 연결할 수 있는 입력만 주어진다.
출력
첫째 줄에 C를 연결하기 위해 설치해야 하는 거울 개수의 최솟값을 출력한다.
풀이
이 문제는 거울 설치와 거의 같은 유형이다. 핵심은 한 칸에 도착했는지만 보는 것이 아니라 어떤 방향으로 그 칸에 도착했는지도 함께 상태로 관리해야 한다는 점이다.
하지만 차이점은 모든 칸에서 거울 설치를 고려해야 한다는 점이다. 그래서 상태를 (x, y, direction)으로 두고 각 상태까지 도달할 때 사용한 거울 수의 최솟값을 기록해야 한다. 직진은 비용이 그대로이고 좌회전이나 우회전은 거울 하나를 설치한 것과 같으므로 비용이 1 증가한다.
현재 코드에서는 dist[x][y][d]에 해당 방향으로 그 칸에 도착했을 때의 최소 거울 수를 저장하고 queue를 사용해 상태를 전파한다. 현재 방향으로 한 칸 전진한 뒤, 그대로 가는 경우는 비용 증가 없이 넣고 좌우로 꺾는 경우는 비용을 1 늘려서 함께 갱신한다.
참고로 PriorityQueue를 사용해서도 제출해볼 수 있지만 오히려 메모리를 더 먹을 수 있다. 이 문제에서는 방향별 최소 거울 개수만 잘 관리하면 일반 큐 기반 전개로도 충분했고, 핵심은 “모든 칸에서 꺾을 수 있다”는 점을 상태 전이로 빠짐없이 반영하는 것이다.
코드
#include <iostream>
#include <queue>
#include <algorithm>
using namespace std;
int W, H;
char graph[100][100];
int dist[100][100][4];
pair<int, int> s, e;
const int INF = 1e9;
int dx[] = { -1, 0, 1, 0 };
int dy[] = { 0, 1, 0, -1 };
struct Node {
int x, y, d, c;
};
int solve() {
queue<Node> q;
for (int i = 0; i < 4; i++) {
q.push({ s.first, s.second, i, 0 });
dist[s.first][s.second][i] = 0;
}
int ans = INF;
while (!q.empty()) {
Node cur = q.front();
q.pop();
if (cur.x == e.first && cur.y == e.second) {
ans = min(ans, cur.c);
continue;
}
if (dist[cur.x][cur.y][cur.d] < cur.c) continue;
int nx = cur.x + dx[cur.d];
int ny = cur.y + dy[cur.d];
if (nx >= 0 && ny >= 0 && nx < H && ny < W && graph[nx][ny] != '*') {
if (dist[nx][ny][cur.d] > cur.c) {
dist[nx][ny][cur.d] = cur.c;
q.push({ nx, ny, cur.d, cur.c });
}
int nd1 = (cur.d + 1) % 4;
int nd2 = (cur.d + 3) % 4;
int nc = cur.c + 1;
if (dist[nx][ny][nd1] > nc) {
dist[nx][ny][nd1] = nc;
q.push({ nx, ny, nd1, nc });
}
if (dist[nx][ny][nd2] > nc) {
dist[nx][ny][nd2] = nc;
q.push({ nx, ny, nd2, nc });
}
}
}
return ans;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
cin >> W >> H;
bool flag = false;
for (int i = 0; i < H; i++) {
for (int j = 0; j < W; j++) {
cin >> graph[i][j];
if (graph[i][j] == 'C') {
if (!flag) {
s = make_pair(i, j);
flag = true;
}
else {
e = make_pair(i, j);
}
}
for (int d = 0; d < 4; d++) {
dist[i][j][d] = INF;
}
}
}
cout << solve() << '\n';
return 0;
}복잡도
- 시간 복잡도:
- 공간 복잡도:
마무리
겉보기에는 단순한 BFS처럼 보여도 실제로는 방향 상태를 함께 갖고 가야 하는 문제다. 거울 설치와 같은 감각으로 접근하되 모든 칸에서 꺾는 선택지를 열어두고 비용을 갱신한다는 점만 정확히 잡으면 된다.
