题解:洛谷 P2226 遥控赛车比赛

【题目来源】

洛谷:P2226 [HNOI2001] 遥控赛车比赛

【题目描述】

全国遥控赛车大赛近日在星沙举行。竞赛选用一块大小为 \(N\times M\) 的场地作为竞赛场地,要求选手的赛车在最短的时间内从起点移动到终点。虽然赛场地形高低有少许的起伏,但并不存在无法到达的地点。但是在赛场上增加了许多无法穿越的障碍物,若赛车在到达终点前撞上障碍物,就视为任务失败。

在赛车的马力和灵活性等性能相差较小的情况下,要控制速度极快的赛车绕开障碍物移动到终点,关键是提高选手的反应灵敏度,即两次改变赛车运动方向所间隔的最短时间,也可称为选手的反应时间。使自己能够更快地控制赛车改变前进的方向。

当然,由于选手反应灵敏度的不同,可选择的路径就会大不相同。如图 \(1\) 和图 \(2\) 所示,对于同一个赛场,两位选手的反应时间分别为 \(2\) 秒和 \(1\) 秒,而其到达终点所需的时间分别为 \(18\) 秒和 \(16\) 秒(赛车每秒可沿当前方向移动一格,从起点出发时算改变一次方向)。

由图 \(1\) 和图 \(2\) 可知,赛车的最短路线长度是由选手的反应灵敏度所决定的,当选手的反应很慢时,可能就不会存在可行的路径。你的任务是:在能够完成赛程(即存在从起点到终点的路径)的条件下,求出选手每个可能的反应时间所对应的最短路线长度。

img/Pasted image 20260809135546.png

【输入】

第一行包含两个整数 \(N\)\(M\),表示赛场的行数和列数。\(1≤N, M≤100\)

第二行有四个整数 \(x1\)\(y1\)\(x2\)\(y2\),表示赛程的起点和终点的位置分别为 \((x1, y1)\)\((x2,y2)\)

接下来 \(N\) 行是一个 \(N×M\) 的 0-1 矩阵,矩阵中第 \(i\) 行第 \(j\) 列元素 \(A_{i,j}=1\) 表示赛场中位置 \((i,j)\) 为赛道,\(A_{i,j}=0\) 表示赛场中位置 \((i,j)\) 有障碍物。

【输出】

输出文件可能有多行,其中每行输出一个可能的选手反应时间及该时间所对应的最短路线长度。输出文件中必须包含所有符合题目要求的选手反应时间。注意:由于选手的反应时间不可能很慢,因而不必考虑选手反应时间大于 \(10\) 秒的情况,只需输出 \(10\) 以内(包含 \(10\))的解。

【输入样例】

10 10
1 4 10 7
0 0 0 1 0 0 0 0 0 0
0 0 0 1 1 1 0 0 0 0
0 0 0 1 0 1 0 0 0 0
1 1 1 1 0 1 1 1 1 0
1 0 0 0 0 0 0 0 1 0
1 0 1 1 1 0 1 1 1 0
1 1 1 0 1 1 1 0 1 0
0 0 1 0 0 0 0 0 1 0
0 0 1 1 1 1 1 1 1 0
0 0 0 0 0 0 1 0 0 0

【输出样例】

1 16
2 18

【核心思想】

  1. 问题分析:给定 \(N \times M\) 的网格地图,赛车每秒移动一格,从起点 \((sx, sy)\) 出发到终点 \((ex, ey)\)。选手反应时间 \(t\) 定义为两次改变方向之间的最小间隔(即连续直走至少 \(t\) 格后才能转弯)。求每个可行反应时间 \(t \in [1, 10]\) 对应的最短路线长度。这是一个分层图最短路问题,关键在于状态需记录当前位置和连续直走长度以约束转弯条件。

  2. 算法选择

    • SPFA + 状态扩展:将"连续直走长度"纳入状态维度,构建分层图后用 SPFA 求最短路
    • 枚举反应时间:对每个 \(t\)\(1\)\(10\) 分别运行最短路,若某 \(t\) 不可达则更大 \(t\) 也不可达,提前终止
  3. 关键步骤

    • 初始化:读取 \(N, M\)、起点 \((sx, sy)\)、终点 \((ex, ey)\)、赛道地图 \(g[N][M]\)
    • 枚举反应时间\(t\)\(1\)\(10\)):
      • 状态定义\(dist[x][y][dir][len]\) 表示到达 \((x, y)\),方向为 \(dir\)\(0\) 上/\(1\) 下/\(2\) 左/\(3\) 右),当前已连续直走 \(len\) 格的最短距离
      • 初始化\(dist\) 全设为 \(\infty\),从起点向四个方向走第一步(算第一次改变方向),\(dist[nx][ny][i][1] = 1\)
      • SPFA 松弛
        • 取出队首状态 \((x, y, k, l)\)
        • 向四个方向扩展 \((nx, ny)\),检查边界和障碍物
        • 继续直走\(i == k\)):\(nl = \min(10, l+1)\),更新 \(dist[nx][ny][k][nl]\)
        • 转弯\(i \neq k\)):仅当 \(l \geq t\) 时允许,\(nl = 1, nk = i\),更新 \(dist[nx][ny][nk][nl]\)
      • 统计答案\(ans = \min_{dir, len} dist[ex][ey][dir][len]\),若 \(ans = \infty\) 则终止枚举,否则输出 \(t\)\(ans\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(10 \times N \times M \times 4 \times 10 \times 4)\),枚举 \(10\)\(t\) 值,每个 SPFA 状态数约 \(N \times M \times 4 \times 10\)
    • 空间复杂度:\(O(N \times M \times 4 \times 10)\),四维距离数组
  5. 分层图最短路的核心思想

    • 状态升维:将题目约束(连续直走长度)作为状态的一维,使原本无法直接建模的转弯限制转化为状态转移条件
    • 转弯约束转化\(l \geq t\) 时才允许改变方向,将反应时间约束融入状态转移,而非修改图结构
    • 连续直走上限\(len\) 上限设为 \(10\)(因只需考虑 \(t \leq 10\)),避免状态无限膨胀
    • 起点特殊处理:从起点出发向四个方向的第一步均视为"第一次改变方向",统一状态初始化
    • 适用于带有过程约束(如转弯间隔、加速限制、连续操作次数限制)的最短路径问题

【算法标签】

普及+ #SPFA

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 105; // 最大网格尺寸
int n, m, ans; // n:行数, m:列数, ans:当前反应时间下的最短路线长度
int sx, sy, ex, ey; // 起点和终点坐标
int g[N][N]; // 赛道地图:1表示赛道,0表示障碍物
// dist[x][y][dir][len]:到达(x,y)时方向为dir,当前直走长度为len的最短距离
// dir: 0上/1下/2左/3右, len: 当前连续直走的步数(限制在10以内)
int dist[N][N][4][15];
int dx[4] = {-1, 1, 0, 0}; // 四个方向的行偏移
int dy[4] = {0, 0, -1, 1}; // 四个方向的列偏移
struct Node
{
    int i, j, k, l; // i:行, j:列, k:方向, l:当前直走长度
};

void spfa(int t) // t:当前测试的选手反应时间
{
    queue<Node> q; // SPFA队列
    ans = 0x3f3f3f3f; // 初始化为无穷大
    memset(dist, 0x3f, sizeof(dist)); // 距离数组初始化为无穷大
    // 从起点出发,向四个方向走第一步(算第一次改变方向)
    for (int i=0; i<4; i++)
    {
        int nx = sx + dx[i];
        int ny = sy + dy[i];
        if (nx<1||nx>n||ny<1||ny>m) continue; // 边界检查
        if (!g[nx][ny]) continue; // 障碍物检查
        dist[nx][ny][i][1] = 1; // 第一步距离为1,直走长度为1
        q.push({nx, ny, i, 1}); // 入队
    }
    while (!q.empty())
    {
        auto tmp = q.front(); q.pop(); // 取出队首状态
        int x = tmp.i, y = tmp.j, k = tmp.k, l = tmp.l; // 当前位置和状态
        for (int i=0; i<4; i++) // 尝试向四个方向扩展
        {
            int nx = x + dx[i];
            int ny = y + dy[i];
            if (nx<1||nx>n||ny<1||ny>m) continue; // 边界检查
            if (!g[nx][ny]) continue; // 障碍物检查
            if (k==i)  // 继续直走(方向不变)
            {
                int nl = min(10, l+1); // 直走长度加1,但不超过10
                // 松弛:如果新距离更短则更新
                if (dist[nx][ny][k][nl]>dist[x][y][k][l]+1)
                {
                    dist[nx][ny][k][nl]=dist[x][y][k][l]+1;
                    q.push({nx, ny, k, nl});
                }

            }
            else // 尝试拐弯(改变方向)
            {
                // 只有当前直走长度l大于等于反应时间t时才能拐弯
                if (l<t) continue;
                int nl = 1; // 拐弯后新的直走长度重置为1
                int nk = i; // 新的方向
                // 松弛操作
                if (dist[nx][ny][nk][nl]>dist[x][y][k][l]+1)
                {
                    dist[nx][ny][nk][nl]=dist[x][y][tmp.k][tmp.l]+1;
                    q.push({nx, ny, nk, nl});
                }
            }
        }
    }
    // 在所有到达终点的状态中取最短距离
    for (int i=0; i<4; i++)
        for (int j=0; j<=10; j++)
            ans = min(ans, dist[ex][ey][i][j]);

}
int main()
{
    cin >> n >> m; // 读入网格尺寸
    cin >> sx >> sy >> ex >> ey; // 读入起点和终点坐标
    for (int i=1; i<=n; i++) // 读入赛道地图
        for (int j=1; j<=m; j++)
            cin >> g[i][j];

    // 枚举所有可能的反应时间(1到10秒)
    for (int t=1; t<=10; t++)
    {
        spfa(t); // 对当前反应时间运行最短路算法
        if (ans==0x3f3f3f3f) // 如果终点不可达,说明更大的反应时间也不可行
            break;
        else
            cout << t << " " << ans << endl; // 输出反应时间和对应的最短路线长度
    }
    return 0;
}

【运行结果】

10 10
1 4 10 7
0 0 0 1 0 0 0 0 0 0
0 0 0 1 1 1 0 0 0 0
0 0 0 1 0 1 0 0 0 0
1 1 1 1 0 1 1 1 1 0
1 0 0 0 0 0 0 0 1 0
1 0 1 1 1 0 1 1 1 0
1 1 1 0 1 1 1 0 1 0
0 0 1 0 0 0 0 0 1 0
0 0 1 1 1 1 1 1 1 0
0 0 0 0 0 0 1 0 0 0
1 16
2 18
posted @ 2026-08-10 12:27  团爸讲算法  阅读(12)  评论(0)    收藏  举报