문제

  • 아래 조건에 따라 주어지는 지도에서 최단 경로를 구하는 문제이다.
    • 빈 칸, 벽, 문, 열쇠, 출발지, 출구가 주어진다.
    • 문, 열쇠는 각각 A-F, a-f로 주어지고, 대응하는 열쇠가 있어야 문이 있는 위치에 들어갈 수 있다.
    • 문, 열쇠, 출구는 여러 개가 있을 수 있다.
    • 열쇠는 여러 번 사용할 수 있다.
    • 문에 대응하는 열쇠가 없을 수도 있다.
    • 지도의 크기는 최대 50 x 50이다.

풀이

  • DFS로 구현하고, BFS로 구현하면서 시간, 공간 복잡도를 줄였다.
    순서대로 설명하겠다.

BFS 풀이

  • 열쇠가 6개밖에 없기 때문에, 2^6-1의 수로 모든 상태를 표현할 수 있다.
  • 최단 경로 하나만 구하면 되기 때문에, DFS보다 BFS를 사용하여 구현하는 것이 효율적이다.
    • 현재 좌표, 가지고 있는 열쇠 정보에 따라 방문 여부를 저장해야 한다.
  • 처음 출구에 도달하는 것이 최소 경로이기 때문에, 도달하면 탐색을 즉시 종료하면 된다.
BFS 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <iostream>
#include <map>
#include <queue>
#include <string>
#include <vector>
#define v vector

using namespace std;
using vi = vector<int>;
using ll = long long;
int n, m, di[4] = {-1, 0, 1, 0}, dj[4] = {0, 1, 0, -1}, ans = 70000;
v<string> ar;
v<v<v<bool>>> vis;

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n >> m;
    int si, sj;
    ar = v<string>(n);
    vis = v<v<v<bool>>>(n, v<v<bool>>(m, v<bool>(63, false)));
    for (int i = 0; i < n; i++) {
        cin >> ar[i];
        for (int j = 0; j < m; j++) {
            if (ar[i][j] == '0')
                si = i, sj = j;
        }
    }
    queue<tuple<int, int, int, char>> q;
    vis[si][sj][0] = true;
    q.push({si, sj, 0, 0});
    while (!q.empty()) {
        auto f = q.front();
        q.pop();
        int ci = get<0>(f), cj = get<1>(f), cd = get<2>(f), ck = get<3>(f);
        if (ar[ci][cj] == '1') {
            ans = cd;
            break;
        }
        for (int i = 0; i < 4; i++) {
            int ni = ci + di[i], nj = cj + dj[i];
            if (ni < 0 || ni >= n || nj < 0 || nj >= m || ar[ni][nj] == '#' || vis[ni][nj][ck]) continue;
            if (ar[ni][nj] >= 'a' && ar[ni][nj] <= 'f') {
                int key = ar[ni][nj] - 'a';
                char tkeys = ck | (0x01 << key);
                vis[ni][nj][tkeys] = true;
                q.push({ni, nj, cd + 1, tkeys});
                continue;
            }
            if (ar[ni][nj] >= 'A' && ar[ni][nj] <= 'F') {
                int key = ar[ni][nj] - 'A';
                if ((ck >> key & 0x01) == 0) continue;
            }
            vis[ni][nj][ck] = true;
            q.push({ni, nj, cd + 1, ck});
        }
    }
    if (ans == 70000)
        cout << -1;
    else
        cout << ans;
    return 0;
}

DFS 풀이

  • 처음에는 열쇠를 주우면 vis 배열의 상태를 초기화한다고 생각했지만, 가진 열쇠 정보에 따라 나눠서 방문 여부를 저장해야 한다.
    • 열쇠를 주울 때마다 vis 배열의 상태를 초기화하면 무한 루프가 발생한다.
      따라서 좌표, 열쇠 정보를 vis 배열에 포함해야 한다.
    • 하나의 변수(si*100000+sj*1000+keys)로 vis 배열에 저장해서, vis 배열의 크기가 굉장히 컸다.
  • 사실 vis 배열의 상태 변수에 열쇠 정보를 추가하면 DFS로 구현할 필요가 없는데, 이전에 떠올린 아이디어(vis 배열 상태 초기화) 때문에 첫 AC는 DFS로 구현한 코드였다.
DFS 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <iostream>
#include <map>
#include <string>
#include <vector>
#define v vector

using namespace std;
using vi = vector<int>;
using ll = long long;

int n, m, di[4] = {-1, 0, 1, 0}, dj[4] = {0, 1, 0, -1}, ans = 70000;
v<string> ar;
vi vis;

void solve(int si, int sj, char keys) {
    int cidx = si * 100000 + sj * 1000 + keys;
    for (int i = 0; i < 4; i++) {
        int ni = si + di[i], nj = sj + dj[i], nidx;
        nidx = ni * 100000 + nj * 1000 + keys;
        if (ni < 0 || ni >= n || nj < 0 || nj >= m || ar[ni][nj] == '#') continue;
        if (ar[ni][nj] == '1') {
            if (ans > vis[cidx] + 1)
                ans = vis[cidx] + 1;
            continue;
        }
        if (ar[ni][nj] >= 'a' && ar[ni][nj] <= 'f') {
            int key = ar[ni][nj] - 'a', vidx;
            char tkeys = keys | (0x01 << key);
            vidx = ni * 100000 + nj * 1000 + tkeys;
            if (vis[vidx] > vis[cidx] + 1) {
                vis[vidx] = vis[cidx] + 1;
                solve(ni, nj, tkeys);
            }
        } else if (ar[ni][nj] >= 'A' && ar[ni][nj] <= 'F') {
            int key = ar[ni][nj] - 'A';
            if (keys >> key & 0x01 && vis[nidx] > vis[cidx] + 1) {
                vis[nidx] = vis[cidx] + 1;
                solve(ni, nj, keys);
            }
        } else if ((ar[ni][nj] == '.' || ar[ni][nj] == '0') && vis[nidx] > vis[cidx] + 1) {
            vis[nidx] = vis[cidx] + 1;
            solve(ni, nj, keys);
        }
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n >> m;
    int si, sj;
    ar = v<string>(n);
    vis = vi(10000000, 70000);
    for (int i = 0; i < n; i++) {
        cin >> ar[i];
        for (int j = 0; j < m; j++) {
            if (ar[i][j] == '0')
                si = i, sj = j;
        }
    }
    vis[si * 100000 + sj * 1000] = 0;
    solve(si, sj, 0);
    if (ans == 70000)
        cout << -1;
    else
        cout << ans;
    return 0;
}

풀고나서

  • 큐의 원소로 tuple을 사용하면서 코드의 가독성을 높힐 수 있다는 것을 알게 되었다.
  • bitwise 연산자는 논리 연산자보다 우선순위가 후순위라, 의도에 맞게 쓰려면 순위에 주의해야 한다.

Leave a comment