跳转至

拯救大兵瑞恩

原题链接

题目描述

1944 年,特种兵麦克接到国防部的命令,要求立即赶赴太平洋上的一个孤岛,营救被敌军俘虏的大兵瑞恩。

瑞恩被关押在一个迷宫里,迷宫地形复杂,但幸好麦克得到了迷宫的地形图。

迷宫的外形是一个长方形,其南北方向被划分为 N 行,东西方向被划分为 M 列, 于是整个迷宫被划分为 N×M 个单元。

每一个单元的位置可用一个有序数对 (单元的行号, 单元的列号) 来表示。

南北或东西方向相邻的 2 个单元之间可能互通,也可能有一扇锁着的门,或者是一堵不可逾越的墙。

注意: 门可以从两个方向穿过,即可以看成一条无向边。

迷宫中有一些单元存放着钥匙,同一个单元可能存放 多把钥匙,并且所有的门被分成 P 类,打开同一类的门的钥匙相同,不同类门的钥匙不同。

大兵瑞恩被关押在迷宫的东南角,即 (N,M) 单元里,并已经昏迷。

迷宫只有一个入口,在西北角。

也就是说,麦克可以直接进入 (1,1) 单元。

另外,麦克从一个单元移动到另一个相邻单元的时间为 1,拿取所在单元的钥匙的时间以及用钥匙开门的时间可忽略不计。

试设计一个算法,帮助麦克以最快的方式到达瑞恩所在单元,营救大兵瑞恩。


思路 状态压缩+双端队列BFS

动态规划可以看成特殊的最短路问题。因为动态规划所求的路径,在最短路内是拓扑序的。因此在做最短路的问题时,可以先用动态规划的思想,然后转化为最短路求解。

状态压缩DP

关于建图

对于 \([x,y]\) 向其他点位连边是很麻烦的。可以将 \([x,y]\) 压缩成一维状态,用一维数字代表点位。即 \([x,y]=x+(y-1)*x\)

存储钥匙

对于每个点,用key数组存储所包含的钥匙。其中下标是一维状态,即 关于建图 内的方法。

存储钥匙,可以用二进制压缩,二进制的每一位代表第 \(i\) 类的钥匙是否存在。

双端队列BFS

根据状态压缩DP的状态计算

  • 如果存在钥匙,那么 \(dist[ver][state | key] = \min(dist[ver][state|key],dist[ver][state])\)。 即当前状态到上一个状态的边权为 \(0\)
  • 向上下左右四个方向走。

    1. 没有门和墙。
    2. 有门,但是有匹配的钥匙。

    \(dist[next][state]=\min(dist[next][state], dist[ver][state] + 1)\)。即当前状态大于上一个状态时,边权为 \(1\)

使用双端队列BFS可以达到 \(O(n)\) 的时间复杂度。

代码

  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
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#include <bits/stdc++.h>
using namespace std;

typedef pair<int, int> PII;

const int N = 11, M = 400, P = 1 << N;

int n, m, p, k;
int h[N * N], e[M], w[M], ne[M], idx;
int dist[N * N][P], key[N * N], g[N][N];
bool st[N * N][P];
set<PII> edges;

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

void build() {
    int dx[] = {1, -1, 0, 0};
    int dy[]  ={0, 0, 1, -1};

    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            for (int u = 0; u < 4; u++) {
                int x = i + dx[u], y = j + dy[u];
                if (x < 1 || x > n || y < 1 || y > m) continue;
                int a = g[i][j], b = g[x][y];
                // 如果存在 a->b 的边,continue。否则连边
                if (edges.count({a, b})) continue;
                add(a, b, 0);
            }
}

int bfs() {
    memset(dist, 0x3f, sizeof dist);
    dist[1][0] = 0;

    deque<PII> q;
    q.push_back({1, 0});

    while (!q.empty()) {
        auto [ver, state] = q.front();
        q.pop_front();

        if (st[ver][state]) continue;
        st[ver][state] = true;

        // 第一次到终点,返回答案,因为bfs保证找到的第一次到达的是最短路
        if (ver == n * m) return dist[ver][state];

        // 当前格子存在钥匙
        if (key[ver]) {
            int new_state = state | key[ver];
            if (dist[ver][new_state] > dist[ver][state]) {
                dist[ver][new_state] = dist[ver][state];
                q.push_front({ver, new_state});
            }
        }

        // 向周围点转移
        for (int i = h[ver]; i != -1; i = ne[i]) {
            int j = e[i];
            if (w[i] && !(state >> w[i] - 1 & 1)) continue; // 有门,无匹配钥匙
            if (dist[j][state] > dist[ver][state] + 1) {
                dist[j][state] = dist[ver][state] + 1;
                q.push_back({j, state});
            }
        }
    }

    return -1; // 找不到答案
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);

    memset(h, -1, sizeof h);

    cin >> n >> m >> p >> k;

    // 将二维的点映射到一维状态
    for (int i = 1, t = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            g[i][j] = t++;

    // door and wall
    while (k--) {
        int x1, y1, x2, y2, z;
        cin >> x1 >> y1 >> x2 >> y2 >> z;
        int a = g[x1][y1], b = g[x2][y2];
        edges.insert({a, b});
        edges.insert({b, a});
        if (z) {    // 有门,连边
            add(a, b, z);
            add(b, a, z);
        }
    }

    build(); // 没有门和墙之间的格子连边

    // key
    cin >> k;
    while (k--) {
        int x, y, z;
        cin >> x >> y >> z;
        int t = g[x][y];
        key[t] |= 1 << z - 1; // 可能存在多个钥匙在一个格子内
    }

    cout << bfs() << endl;

    return 0;
}
回到页面顶部