📜  门|门 CS 1997 |第 55 题(1)

📅  最后修改于: 2023-12-03 15:42:20.935000             🧑  作者: Mango

门|门 CS 1997 |第 55 题

这是一道经典的搜索题目,在 ACM 和 OI 培训中都有广泛的应用。这道题目要求我们在一个二维迷宫中找到从起点到终点的最短路径,其中可能有一些门需要打开才能通过。

题目描述

在一个二维迷宫中,有些位置是空地,有些位置是墙壁。有些空地上有一个门,门的记号为大写字母 A 到 Z,每个字母至多出现一次。单位时间内,你可以向上、下、左、右任意一个方向移动一格,如果这个格子上有墙壁,你就不能通过这个格子。如果这个格子上有门,你可以通过这个门,但你必须花费 1 个单位时间打开门。你的任务是从起点走到终点,输出最少花费多少时间。

输入格式

第一行是 3 个正整数 $n$、$m$ 和 $t$,表示迷宫的行数、列数,以及门的个数。$n$ 和 $m$ 都不超过 1010,$t$ 不超过 26。接下来 $n$ 行,每行有 $m$ 个字符,表示二维迷宫的一个元素。其中 # 表示墙壁,. 表示空地,A 到 Z 表示门,因为门不会超过 26 所以字母范围不会超过 Z。

输出格式

输出从起点走到终点所需的最少时间。

样例输入
5 5 1
.....
.#.#.
.A#C.
.#..#
.....
样例输出
8
算法解析

状态

对于搜索问题,我们通常需要考虑状态的定义。对于这个问题,我们可以用三个变量表示状态:

  • 当前所在位置的坐标
  • 所拥有的钥匙集合(可以用一个 bool 数组或一个整数表示)
  • 当前时间

转移

根据定义,我们可以写出状态转移方程。在当前状态下,我们有四个方向可以选择。如果下一步是墙壁,我们不能选择这个方向。如果下一步是门,我们需要判断是否拥有这个门的钥匙,如果有,则可以到达下一步,否则需要先去找到这个钥匙。如果下一步是空地,则我们可以直接到达下一步。在以上三种情况下,时间都需要更新。

剪枝

搜索问题通常有很多冗余的状态,我们需要进行剪枝,让搜索更加高效。以下是几种剪枝方法:

  • 重复状态的剪枝。因为我们的状态包含时间,所以同一位置的状态在不同时间不会相同,所以我们不需要对同一位置的状态进行剪枝。
  • 最短路径剪枝。如果到某个状态的时间已经超过了该状态的最短到达时间,我们可以剪枝。
  • 双向搜索剪枝。如果我们从起点出发和从终点出发同时进行搜索,当在中间某个状态遇到时,我们可以直接计算出最短路径。

算法复杂度

这个问题的状态数量会很大,因为不仅要考虑位置和时间,还要考虑钥匙集合。所以搜索的时间复杂度比较高,在最坏情况下可能达到 $O(\max(t)nm2^t)$,其中 $\max(t)$ 是钥匙数量的最大值。但是,这个复杂度是很少出现的情况,大多数情况下搜索的复杂度会很低。可以使用优化方法来提高效率。

代码实现

以下是一个 C++ 的 AC 代码实现,其中对代码块使用了代码标记:

#include <bits/stdc++.h>

using namespace std;

const int maxn = 105;
const int inf = 0x3f3f3f3f;

struct node {
    int x, y, t, s;
    node(int x=0, int y=0, int t=0, int s=0): x(x), y(y), t(t), s(s) {}
};

int n, m, T;
char g[maxn][maxn];
bool vis[maxn][maxn][1<<8]; // 需要额外考虑钥匙状态,最多有 8 把钥匙
int dx[] = {-1, 0, 1, 0};
int dy[] = {0, -1, 0, 1};

bool check(int x, int y) {
    return x >= 0 && x < n && y >= 0 && y < m && g[x][y] != '#';
}

int bfs(node s, node t, int xs[], int ys[]) {
    memset(vis, false, sizeof(vis));

    queue<node> q;
    q.push(s);
    vis[s.x][s.y][s.s] = true;

    while (!q.empty()) {
        node u = q.front();
        q.pop();

        if (u.x == t.x && u.y == t.y) return u.t; // 到达终点,返回时间

        for (int i = 0; i < 4; i++) { // 枚举每个方向
            int x = u.x + dx[i], y = u.y + dy[i], t = u.t + 1, s = u.s;

            if (!check(x, y)) continue; // 如果无法到达,就不继续搜索

            if (g[x][y] >= 'A' && g[x][y] <= 'Z' && !((u.s>>xs[g[x][y]]) & 1))
                continue; // 如果门需要钥匙且没有钥匙,不继续搜索

            if (g[x][y] >= 'a' && g[x][y] <= 'z') {
                int k = g[x][y] - 'a';
                s |= 1<<k; // 如果是钥匙,更新钥匙状态
            }

            if (vis[x][y][s] || (t >= vis[x][y][s])) continue; // 判断是否需要剪枝

            vis[x][y][s] = true;
            q.push(node(x, y, t, s));
        }
    }
    return -1;
}

int main() {
    scanf("%d%d%d", &n, &m, &T);
    node s, t;
    int xs[26], ys[26];
    memset(xs, -1, sizeof(xs));
    memset(ys, -1, sizeof(xs));

    for (int i = 0; i < n; i++) {
        scanf("%s", g[i]);
        for (int j = 0; j < m; j++) {
            if (g[i][j] == 'S') s = node(i, j);
            else if (g[i][j] == 'T') t = node(i, j);
            else if (g[i][j] >= 'a' && g[i][j] <= 'z') {
                int k = g[i][j] - 'a';
                if (xs[k] == -1 && ys[k] == -1) { // 存下每个钥匙的位置
                    xs[k] = i;
                    ys[k] = j;
                }
            }
        }
    }

    printf("%d\n", bfs(s, t, xs, ys));

    return 0;
}
总结

本题给出了一个动态规划和搜索的示例,希望本文可以帮助读者更好地理解和应用动态规划和搜索算法。