题解:洛谷 P5195 Knights of Ni

【题目来源】

洛谷:P5195 [USACO05DEC] Knights of Ni S - 洛谷

【题目描述】

贝茜遇到了一件很麻烦的事:她无意中闯入了森林里的一座城堡,如果她想回家,就必须穿过这片由骑士们守护着的森林。为了能安全地离开,贝茜不得不按照骑士们的要求,在森林寻找一种特殊的灌木并带一棵给他们。

当然,贝茜想早点离开这可怕的森林,于是她必须尽快完成骑士们给的任务,贝茜随身带着这片森林的地图,地图上的森林被放入了直角坐标系,并按 \(x,y\) 轴上的单位长度划分成了 \(W \times H\ ( 1 \leq W,H \leq 1000 )\) 块,贝茜在地图上查出了她自己以及骑士们所在的位置,当然地图上也标注了她所需要的灌木生长的区域。某些区域是不能通过的(比如说沼泽地,悬崖,以及食人兔的聚居地)。在没有找到灌木之前,贝茜不能通过骑士们所在的那个区域,为了确保她自己不会迷路,贝茜只向正北、正东、正南、正西四个方向移动(注意,她不会走对角线)。她要走整整一天,才能从某块区域走到与它相邻的那块区域。

贝茜希望你能帮她计算一下,她最少需要多少天才可脱离这可怕的地方?输入数据保证贝茜一定能完成骑士的任务。

【输入】

第一行输入 \(2\) 个用空格隔开的整数,即题目中提到的 \(W,H\)

接下来输入贝茜持有的地图,每一行用若干个数字代表地图上对应行的地形。第一行描述了地图最北的那一排土地;最后一行描述的则是最南面的。相邻的数字所对应的区域是相邻的。如果地图的宽小于或等于 \(40\),那每一行数字恰好对应了地图上的一排土地。如果地图的宽大于 \(40\),那每行只会给出 \(40\) 个数字,并且保证除了最后一行的每一行都包含恰好 \(40\) 个数字。没有哪一行描述的区域分布在两个不同的行里。

地图上的数字所对应的地形:

  • \(0\):贝茜可以通过的空地;
  • \(1\):由于各种原因而不可通行的区域;
  • \(2\):贝茜现在所在的位置;
  • \(3\):骑士们的位置;
  • \(4\):长着贝茜需要的灌木的土地。

【输出】

输出一个正整数,即贝茜最少要花多少天才能完成骑士们给的任务。

【输入样例】

8 4
4 1 0 0 0 0 1 0
0 0 0 1 0 1 0 0
0 2 1 1 3 0 4 0
0 0 0 4 1 1 1 0

【输出样例】

11

【核心思想】

  1. 问题分析:贝茜需要从起点出发,先到达灌木位置,再到达骑士位置,求最短路径。这是一个两次BFS问题,关键在于分阶段搜索:第一阶段从起点搜索到灌木(不能经过骑士),第二阶段从骑士搜索到灌木(可以经过任何可通行区域)。

  2. 算法选择

    • 两次BFS(广度优先搜索):通过两次BFS分别计算起点到所有位置的最短距离、骑士到所有位置的最短距离
    • 多源BFS:第二次BFS以所有骑士位置为起点同时进行搜索
    • 距离数组:使用 dist1 存储起点到各位置的距离,dist2 存储骑士到各位置的距离
  3. 关键步骤

    • 初始化:读取 \(W \times H\) 网格,记录起点位置、骑士位置(可能有多个)、灌木位置(可能有多个)
    • 第一次BFS(从起点)
      • 起点:贝茜的起始位置
      • 限制:不能经过骑士所在位置(值为3)
      • 目标:计算 dist1[r][c],即起点到每个位置的最短距离
    • 第二次BFS(从骑士)
      • 起点:所有骑士位置(多源BFS)
      • 限制:可以经过任何可通行区域(除了墙)
      • 目标:计算 dist2[r][c],即骑士到每个位置的最短距离
    • 计算答案
      • 遍历所有灌木位置 \((r, c)\)
      • 计算 dist1[r][c] + dist2[r][c]
      • 取最小值作为答案
  4. 时间/空间复杂度

    • 时间复杂度:\(O(W \times H)\),两次BFS各遍历整个网格一次
    • 空间复杂度:\(O(W \times H)\),需要存储网格、两个距离数组、访问标记数组
  5. 两次BFS的核心思想

    • 分阶段搜索:将复杂路径问题拆分为两个简单子问题,分别计算后再组合
    • 多源BFS:以多个起点同时进行BFS,适用于求多个起点到终点的最短距离
    • 距离叠加dist1[灌木] + dist2[灌木] 表示从起点到灌木再到骑士的最短路径
    • 障碍物处理:第一次BFS将骑士视为障碍物,第二次BFS正常通行
    • 适用于需要经过中间点的最短路径问题

【算法标签】

普及+ #BFS-二维

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 1005;  // 定义网格最大尺寸
int h, w, ans=1e9;  // 网格高度h,宽度w,答案ans(初始化为极大值)
int a[N][N], dist1[N][N], dist2[N][N];  // 网格数组a,起点距离dist1,钥匙距离dist2
struct Node
{
    int r, c;  // 结构体存储坐标
}st, ks[N * N], fs[N*N];  // 起点st,钥匙数组ks,食物数组fs
int cur1, cur2;  // 钥匙数量cur1,食物数量cur2
bool vis[N][N];  // 访问标记数组
int dx[4] = {-1, 1, 0, 0};  // 四个方向的x偏移
int dy[4] = {0, 0, -1, 1};  // 四个方向的y偏移

void bfs1()  // 从起点开始的第一轮广度优先搜索
{
    memset(vis, 0, sizeof(vis));  // 初始化访问标记为未访问
    memset(dist1, 0x3f, sizeof(dist1));  // 初始化起点距离为无穷大
    queue<Node> q;  // 广度优先搜索队列
    q.push(st);  // 起点入队
    dist1[st.r][st.c] = 0;  // 起点到自己的距离为0
    vis[st.r][st.c] = 1;  // 标记起点为已访问
    while (!q.empty())  // 当队列不为空时循环
    {
        int x = q.front().r, y = q.front().c;  // 取出队首坐标
        q.pop();  // 出队
        for (int i=0; i<4; i++)  // 遍历四个方向
        {
            int nx = x + dx[i], ny = y + dy[i];  // 计算相邻位置
            if (nx<1 || nx>h || ny<1 || ny>w || vis[nx][ny] || a[nx][ny]==1 || a[nx][ny]==3) continue;  // 检查是否越界、已访问、是墙或是钥匙
            dist1[nx][ny] = dist1[x][y] + 1;  // 更新相邻位置的距离
            vis[nx][ny] = 1;  // 标记相邻位置为已访问
            q.push({nx, ny});  // 相邻位置入队
        }
    }
}

void bfs2()  // 从所有钥匙开始的第二轮广度优先搜索
{
    memset(vis, 0, sizeof(vis));  // 初始化访问标记为未访问
    memset(dist2, 0x3f, sizeof(dist2));  // 初始化钥匙距离为无穷大
    queue<Node> q;  // 广度优先搜索队列
    for (int i=1; i<=cur1; i++)  // 将所有钥匙加入队列
    {
        q.push(ks[i]);  // 钥匙入队
        vis[ks[i].r][ks[i].c] = 1;  // 标记钥匙位置为已访问
        dist2[ks[i].r][ks[i].c] = 0;  // 钥匙到自己的距离为0
    }
    while (!q.empty())  // 当队列不为空时循环
    {
        int x = q.front().r, y = q.front().c;  // 取出队首坐标
        q.pop();  // 出队
        for (int i=0; i<4; i++)  // 遍历四个方向
        {
            int nx = x + dx[i], ny = y + dy[i];  // 计算相邻位置
            if (nx<1 || nx>h || ny<1 || ny>w || vis[nx][ny] || a[nx][ny]==1) continue;  // 检查是否越界、已访问、是墙
            dist2[nx][ny] = dist2[x][y] + 1;  // 更新相邻位置的距离
            vis[nx][ny] = 1;  // 标记相邻位置为已访问
            q.push({nx, ny});  // 相邻位置入队
        }
    }
}

int main()  // 主函数
{
    cin >> w >> h;  // 输入网格宽度w和高度h
    for (int i=1; i<=h; i++)  // 遍历网格的每一行
    {
        for (int j=1; j<=w; j++)  // 遍历网格的每一列
        {
            cin >> a[i][j];  // 输入网格单元格的值
            if (a[i][j] == 2)  // 如果当前单元格是起点
            {
                st={i, j};  // 记录起点坐标
            }
            if (a[i][j] == 3)  // 如果当前单元格是钥匙
            {
                ks[++cur1] = {i, j};  // 记录钥匙坐标,钥匙数量加1
            }
            if (a[i][j] == 4)  // 如果当前单元格是食物
            {
                fs[++cur2] = {i, j};  // 记录食物坐标,食物数量加1
            }
        }
    }
    bfs1();  // 执行从起点开始的广度优先搜索
    bfs2();  // 执行从所有钥匙开始的广度优先搜索
    for (int i=1; i<=cur2; i++)  // 遍历所有食物位置
    {
        Node t = fs[i];  // 获取当前食物的坐标
        ans = min(ans, dist1[t.r][t.c] + dist2[t.r][t.c]);  // 更新最短路径长度为起点到食物距离加上钥匙到食物距离的最小值
    }
    cout << ans << endl;  // 输出最短路径长度
    return 0;  // 程序正常结束
}

【运行结果】

8 4
4 1 0 0 0 0 1 0
0 0 0 1 0 1 0 0
0 2 1 1 3 0 4 0
0 0 0 4 1 1 1 0
11
posted @ 2026-06-25 09:15  团爸讲算法  阅读(13)  评论(0)    收藏  举报