【OD机试题笔记】智能驾驶

题目描述

有一辆汽车需要从 m * n 的地图左上角(起点)开往地图的右下角(终点),去往每一个地区都需要消耗一定的油量,加油站可进行加油。

请你计算汽车确保从从起点到达终点时所需的最少初始油量。

说明:

智能汽车可以上下左右四个方向移动
地图上的数字取值是 0 或 -1 或 正整数:
-1 :表示加油站,可以加满油,汽车的油箱容量最大为100;
0 :表示这个地区是障碍物,汽车不能通过
正整数:表示汽车走过这个地区的耗油量
如果汽车无论如何都无法到达终点,则返回 -1
输入描述
第一行为两个数字,M,N,表示地图的大小为 M * N

0 < M,N ≤ 200
后面一个 M * N 的矩阵,其中的值是 0 或 -1 或正整数,加油站的总数不超过 200 个

输出描述
如果汽车无论如何都无法到达终点,则返回 -1

如果汽车可以到达终点,则返回最少的初始油量

示例1
输入
2,2
10,20
30,40

输出
70

说明
行走的路线为:右→下

示例2
输入
4,4
10,30,30,20
30,30,-1,10
0,20,20,40
10,-1,30,40

输出
70

说明
行走的路线为:右→右→下→下→下→右

示例3
输入
4,5
10,0,30,-1,10
30,0,20,0,20
10,0,10,0,30
10,-1,30,0,10

输出
60

说明
行走的路线为:下→下→下→右→右→上→上→上→右→右→下→下→下

示例4
输入
4,4
10,30,30,20
30,30,20,10
10,20,10,40
10,20,30,40

输出
-1

说明
无论如何都无法到达终点

思路

核心逻辑

采用逆向Dijkstra算法(优先级队列+BFS+动态规划) 求解:

  1. 逆向视角:将“起点到终点的最小初始油量”转化为“终点到起点的最小油耗”,规避正向遍历中初始油量未知、加油状态复杂的问题;
  2. 动态规划定义:dp[x][y] 表示从(x,y)到终点的最小油耗(包含自身消耗),初始时终点dp值为自身耗油量(加油站则为0),其余为无穷大;
  3. 优先级队列(贪心):按油耗从小到大取出节点,确保每次处理的是当前最优路径,遍历四个方向的邻居节点;
  4. 状态更新:
    • 邻居为加油站(-1):油耗重置为0(正向加油后油量满100,逆向出发油耗为0);
    • 邻居为普通格子:油耗为自身耗油量 + 当前节点到终点的油耗,超过油箱容量100则跳过;
    • 若新油耗更小,更新dp并将节点加入队列;
  5. 终止条件:到达起点时返回油耗(≤100),队列遍历完仍未到起点则返回-1。

时空复杂度

  • 时间复杂度:O(M×N×log(M×N)),其中M、N为地图行列数;每个节点最多入队一次,优先级队列排序耗时为O(log(M×N)),整体遍历所有可达节点为O(M×N);
  • 空间复杂度:O(M×N),主要用于存储dp数组和优先级队列(最多存储所有可达节点)。

关键优化

  • 逆向遍历简化加油逻辑:加油站直接重置油耗为0,无需处理正向的油量加满状态;
  • 优先级队列保证最优性:每次处理油耗最小的节点,避免无效的高油耗路径遍历;
  • 剪枝策略:油耗超过油箱容量100时直接跳过,减少无效计算。

代码

function calculateMinInitialOil(input) {
    const lines = input.trim().split('\n');
    const [m, n] = lines[0].split(',').map(Number);
    const grid = [];
    for (let i = 1; i <= m; i++) {
        grid.push(lines[i].split(',').map(Number));
    }

    // 终点坐标
    const endX = m - 1;
    const endY = n - 1;

    // 检查终点是否为障碍物
    if (grid[endX][endY] === 0) return -1;

    // 方向数组:上下左右
    const dirs = [[-1, 0], [1, 0], [0, -1], [0, 1]];

    // dp[x][y]:从(x,y)到终点的最小油耗(包含(x,y)自身的消耗)
    const dp = Array.from({ length: m }, () => Array(n).fill(Infinity));
    
    // 初始化终点:自身油耗(加油站的话消耗0,因为加油不消耗且重置)
    dp[endX][endY] = grid[endX][endY] === -1 ? 0 : grid[endX][endY];

    // 优先队列:[油耗, x, y],按油耗从小到大排序
    const pq = [[dp[endX][endY], endX, endY]];

    while (pq.length > 0) {
        // 取出油耗最小的节点
        pq.sort((a, b) => b[0] - a[0]);
        const [currentCost, x, y] = pq.pop();

        // 若当前油耗已大于记录的最优值,跳过
        if (currentCost > dp[x][y]) continue;

        // 到达起点,返回结果(需≤100)
        if (x === 0 && y === 0) {
            return currentCost <= 100 ? currentCost : -1;
        }

        // 探索四个方向的邻居(反向移动,即正向中邻居→当前节点)
        for (const [dx, dy] of dirs) {
            const nx = x + dx;
            const ny = y + dy;
            // 边界检查
            if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
            // 障碍物检查
            if (grid[nx][ny] === 0) continue;

            let newCost;
            if (grid[nx][ny] === -1) {
                // 加油站:自身消耗0(加油不消耗),且到终点的油耗为0(因为加满后从这里出发)
                newCost = 0;
            } else {
                // 普通格子:自身油耗 + 从当前节点(x,y)到终点的油耗
                newCost = grid[nx][ny] + currentCost;
                // 超过油箱容量100,无法通过
                if (newCost > 100) continue;
            }

            // 更新最优油耗
            if (newCost < dp[nx][ny]) {
                dp[nx][ny] = newCost;
                pq.push([newCost, nx, ny]);
            }
        }
    }

    // 起点不可达
    return -1;
}

// 测试用例验证
const testCases = [
    {
        input: `2,2
10,20
30,40`,
        expected: 70
    },
    {
        input: `4,4
10,30,30,20
30,30,-1,10
0,20,20,40
10,-1,30,40`,
        expected: 70
    },
    {
        input: `4,5
10,0,30,-1,10
30,0,20,0,20
10,0,10,0,30
10,-1,30,0,10`,
        expected: 60
    },
    {
        input: `4,4
10,30,30,20
30,30,20,10
10,20,10,40
10,20,30,40`,
        expected: -1
    }
];

// 执行测试
testCases.forEach((test, index) => {
    console.log(`=== 测试用例 ${index + 1} ===`);
    const result = calculateMinInitialOil(test.input);
    console.log(`输入:\n${test.input}`);
    console.log(`预期输出: ${test.expected}`);
    console.log(`实际输出: ${result}`);
    console.log(result === test.expected ? '测试通过' : '测试失败');
    console.log('\n');
});

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值