华为OD机试笔试2024年C卷D卷 - 路口最短时间问题 (java/c++/python)

华为OD机试(C卷+D卷)2024真题目录(Java & c++ & python)

题目描述

假定街道是棋盘型的,每格距离相等,车辆通过每格街道需要时间均为 timePerRoad;

街道的街口(交叉点)有交通灯,灯的周期 T(=lights[row][col])各不相同;

车辆可直行、左转和右转,其中直行和左转需要等相应 T 时间的交通灯才可通行,右转无需等待。

现给出 n * m 个街口的交通灯周期,以及起止街口的坐标,计算车辆经过两个街口的最短时间。

其中:

起点和终点的交通灯不计入时间,且可以在任意方向经过街口
不可超出 n * m 个街口,不可跳跃,但边线也是道路(即:lights[0][0] -> lights[0][1] 是有效路径)
入口函数定义:

/**

  • @param lights:n*m个街口每个交通灯的周期,值范围[0, 120],n和m的范围为[1,9]
  • @param timePreRoad:相邻两个街口之间街道的通行时间,范围为[0,600]
  • @param rowStart:起点的行号
  • @param colStart:起点的列号
  • @param rowEnd:终点的行号
  • @param colEnd:终点的列号
  • @return lights[rowStart][colStart] 与 lights[rowEnd][colEnd] 两个街口之间的最短通行时间
    */
    int calcTime(int[][] lights, int timePreRoad, int rowStart, int colStart, int rowEnd, int colEnd)

输入描述

第一行输入 n 和 m,以空格分隔

之后 n 行输入 lights矩阵,矩阵每行m个整数,以空格分隔

之后一行输入 timePerRoad

之后一行输入 rowStart colStart,以空格分隔

最后一行输入 rowEnd colEnd,以空格分隔

输出描述

lights[rowStart][colStart] 与 lights[rowEnd][colEnd] 两个街口之间的最短通行时间

用例

输入

3 3
1 2 3
4 5 6
7 8 9
60
0 0
2 2

输出

245

说明 行走路线为 (0,0) -> (0,1) -> (1,1) -> (1,2) -> (2,2) 走了4格路,2个右转,1个左转,共耗时 60+0+60+5+60+0+60=245

示例解析:
在这里插入图片描述
(0,0) -> (0,1) 起点的交通灯不计入时间,因此通过该路口耗时60
(0,1) -> (1,1) 右转,右转不等待,因此通过该路口耗时60
(1,1) -> (1,2) 左转,左转需要等待lights[1][1]=5,因此通过该路口耗时60+5
(1,2) -> (2,2) 右转,右转不等待,因此通过该路口耗时60

解题思路

本题虽然地图矩阵数量级很小,n和m的范围为[1,9],但是本题会存在回路的情况,比如用例:

4 3
68 61 39
39 56 42
104 30 88
82 58 4
0
3 0
1 0

这个用例的答案是60,路径如下图所示:
在这里插入图片描述
因此本题不能使用暴力搜索找所有路径,因为肯定会超时。

如果将本题的地图矩阵看成是有权图的话,则我们会发现,每两个位置(当前位置、下一个位置)的连线(边)的权重是不固定,会随着第三个位置(上一个位置)的改变而改变。

因此,我们无法使用常规的最短路问题思路来解决此题。

本题地图矩阵的每个位置,都可能被来源于四个方向的路径探索到,即:

  • 从当前位置上方来的路径
  • 从当前位置下方来的路径
  • 从当前位置左方来的路径
  • 从当前位置右方来的路径

比如下图所示:
在这里插入图片描述
因此对于每一个位置而言,其实有四个方向维度,我们需要为每一个位置定义一个长度为4的数组,用于记录,不同来源方向的最短路径权重。

假设地图矩阵 n 行,m列,那么我们应该定义一个最短路表 dist = new int[n][m][4]

dist[i][j][k]:表示以来源方向k到达位置(i, j)的最短路权重

这样的话,我们就可以解决动态边权问题了,因为所谓的动态边权,其实就是四个来源方向对应的四种边权,并非真正的动态。

之后,我们就可以按照正常的最短路问题来解决此题,这里可以使用Dijkstra算法求解两点之间的最短路。

Dijkstra算法相关知识
在这里插入图片描述
在这里插入图片描述

本题还有一个难点就是判断拐弯方向,这里建议采用向量叉积的知识来判断三点形成的拐弯方向。

本题还有一点需要注意的是:

本题只允许直行、左拐、右拐,不能够调头,即下面路径是不允许的
在这里插入图片描述

C++、Java、Python代码如下:

C++参考代码

#include <bits/stdc++.h>
using namespace std;

class Solution {
public:
    int calcTime(int **lights, int lightsRowLen, int *lightsColLen, int timePerRoad, int rowStart, int colStart, int rowEnd, int colEnd) {
        int n = lightsRowLen;
        int m = *lightsColLen;

        vector<vector<vector<int>>> dist(n, vector<vector<int>>(m, vector<int>(4, INT_MAX)));

        auto cmp = [](const vector<int> &a, const vector<int> &b) {
            return a[4] > b[4];
        };
        priority_queue<vector<int>, vector<vector<int>>, decltype(cmp)> pq(cmp);

        for (int k = 0; k < 4; k++) {
            dist[rowStart][colStart][k] = 0;
            pq.push({-1, -1, rowStart, colStart, 0});
        }

        int offsets[4][2] = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};

        while (!pq.empty()) {
            auto tmp = pq.top();
            pq.pop();

            int preX = tmp[0];
            int preY = tmp[1];
            int curX = tmp[2];
            int curY = tmp[3];
            int cost = tmp[4];

            for (int k = 0; k < 4; k++) {
                int newX = curX + offsets[k][0];
                int newY = curY + offsets[k][1];

                if (newX < 0 || newX >= n || newY < 0 || newY >= m) continue;
                if (newX == preX && newY == preY) continue;

                int newCost = cost + timePerRoad;
                if (preX != -1 && preY != -1 && getDirection(preX, preY, curX, curY, newX, newY) >= 0) {
                    newCost += lights[curX][curY];
                }

                if (newCost >= dist[newX][newY][k]) continue;

                dist[newX][newY][k] = newCost;
                pq.push({curX, curY, newX, newY, newCost});
            }
        }

        int ans = INT_MAX;
        for (int k = 0; k < 4; k++) {
            ans = min(ans, dist[rowEnd][colEnd][k]);
        }

        return ans;
    }

    int getDirection(int preX, int preY, int curX, int curY, int nextX, int nextY) {
        int dx1 = curX - preX;
        int dy1 = curY - preY;
        int dx2 = nextX - curX;
        int dy2 = nextY - curY;

        return dx1 * dy2 - dx2 * dy1;
    }
};


Java参考代码

import java.util.PriorityQueue;
import java.util.Scanner;

// 本题实际考试为核心代码模式,即只需要写出下面Solution类实现即可
class Solution {
    // 定义四个方向的移动向量,分别对应上、右、下、左
    static int[][] offsets = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};

    /**
     * 计算从起点到终点的最短时间
     * @param lights 每个格子处的红绿灯等待时间
     * @param timePerRoad 每走一步的固定时间
     * @param rowStart 起点行坐标
     * @param colStart 起点列坐标
     * @param rowEnd 终点行坐标
     * @param colEnd 终点列坐标
     * @return 最短时间
     */
    int calcTime(int[][] lights, int timePerRoad, int rowStart, int colStart, int rowEnd, int colEnd) {
        int n = lights.length; // 行数
        int m = lights[0].length; // 列数

        // dist[i][j][k] 表示从方向 k 到达位置 (i,j) 的最短时间
        int[][][] dist = new int[n][m][4];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                // 初始化为最大值,表示尚未到达
                for (int k = 0; k < 4; k++) {
                    dist[i][j][k] = Integer.MAX_VALUE;
                }
            }
        }

        // 小顶堆,用于存储和优先处理路径,按照到达时间升序排序
        PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[4] - b[4]);

        // 初始化起点的四个方向,时间为 0
        for (int k = 0; k < 4; k++) {
            dist[rowStart][colStart][k] = 0;
            // 起点没有前一个位置,前一个位置设为 (-1, -1)
            pq.add(new int[]{-1, -1, rowStart, colStart, 0});
        }

        // 进行路径搜索
        while (pq.size() > 0) {
            int[] tmp = pq.poll(); // 取出时间最短的路径

            int preX = tmp[0];
            int preY = tmp[1];
            int curX = tmp[2];
            int curY = tmp[3];
            int cost = tmp[4];

            // 向四个方向探索
            for (int k = 0; k < 4; k++) {
                int newX = curX + offsets[k][0];
                int newY = curY + offsets[k][1];

                // 越界检查
                if (newX < 0 || newX >= n || newY < 0 || newY >= m) continue;

                // 检查是否为掉头行为(即新位置与前一个位置相同)
                if (newX == preX && newY == preY) continue;

                // 计算新的时间,包含行走时间
                int newCost = cost + timePerRoad;

                // 检查是否需要等待红绿灯
                if (preX != -1 && preY != -1 && getDirection(preX, preY, curX, curY, newX, newY) >= 0) {
                    newCost += lights[curX][curY];
                }

                // 如果新的路径时间不优,则跳过
                if (newCost >= dist[newX][newY][k]) continue;

                // 否则更新更优路径时间
                dist[newX][newY][k] = newCost;
                pq.add(new int[]{curX, curY, newX, newY, newCost});
            }
        }

        // 从终点的四个方向选取最小时间作为结果
        int ans = Integer.MAX_VALUE;
        for (int k = 0; k < 4; k++) {
            ans = Math.min(ans, dist[rowEnd][colEnd][k]);
        }

        return ans;
    }

    /**
     * 根据三个点的坐标计算转弯方向
     * @param preX 前一个点的横坐标
     * @param preY 前一个点的纵坐标
     * @param curX 当前点的横坐标
     * @param curY 当前点的纵坐标
     * @param nextX 下一个点的横坐标
     * @param nextY 下一个点的纵坐标
     * @return cur到next的转弯方向, >0 表示向左拐, ==0 表示直行(含掉头), <0 表示向右拐
     */
    public static int getDirection(int preX, int preY, int curX, int curY, int nextX, int nextY) {
        // 计算向量 pre->cur
        int dx1 = curX - preX;
        int dy1 = curY - preY;

        // 计算向量 cur->next
        int dx2 = nextX - curX;
        int dy2 = nextY - curY;

        // 两向量的叉积 >0 表示左转,==0 表示直行,<0 表示右转
        return dx1 * dy2 - dx2 * dy1;
    }
}

// 输入处理类,实际考试为核心代码模式,输入处理通常不需要实现
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        // 读取输入的网格大小
        int n = sc.nextInt();
        int m = sc.nextInt();

        // 读取每个格子的红绿灯等待时间
        int[][] lights = new int[n][m];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                lights[i][j] = sc.nextInt();
            }
        }

        // 读取每走一步的固定时间
        int timePerRoad = sc.nextInt();

        // 读取起点和终点的坐标
        int rowStart = sc.nextInt();
        int colStart = sc.nextInt();
        int rowEnd = sc.nextInt();
        int colEnd = sc.nextInt();

        // 输出计算出的最短时间
        System.out.println(
            new Solution().calcTime(lights, timePerRoad, rowStart, colStart, rowEnd, colEnd));
    }
}


Python参考代码

import sys

# 根据三点坐标,确定拐弯方向
def getDirection(preX, preY, curX, curY, nextX, nextY):
    """
    :param preX: 前一个点横坐标
    :param preY: 前一个点纵坐标
    :param curX: 当前点横坐标
    :param curY: 当前点纵坐标
    :param nextX: 下一个点横坐标
    :param nextY: 下一个点纵坐标
    :return: cur到next的拐弯方向, >0 表示向左拐, ==0 表示直行(含调头), <0 表示向右拐
    """
    # 向量 pre->cur
    dx1 = curX - preX
    dy1 = curY - preY

    # 向量 cur->next
    dx2 = nextX - curX
    dy2 = nextY - curY

    #  两个向量的叉积 >0 表示向左拐, ==0 表示直行(含调头), <0 表示向右拐
    return dx1 * dy2 - dx2 * dy1

class Solution:
    def calcTime(self, lights, timePerRoad, rowStart, colStart, rowEnd, colEnd):
        n = len(lights)
        m = len(lights[0])

        # 到达位置(i,j)的路径有四个来源方向
        # dist[i][j][k] 表示从来源方向k到达位置(i,j)所需要的时间,初始化为最大整数值
        dist = [[[sys.maxsize] * 4 for _ in range(m)] for _ in range(n)]

        # 小顶堆,堆中元素是数组 [前一个位置行号,前一个位置列号,当前位置行号,当前位置列号,到达当前位置需要的时间]
        # 到达当前位置的时间越小,优先级越高
        pq = []

        # 四个来源方向到达出发点位置 (rowStart, colStart) 所需时间均为 0
        for k in range(4):
            dist[rowStart][colStart][k] = 0
            # 出发点位置没有前一个位置,因此前一个位置设为(-1,-1)
            pq.append((-1, -1, rowStart, colStart, 0))

        # 四个方向的位移 (上, 下, 左, 右)
        offsets = ((-1, 0), (1, 0), (0, -1), (0, 1))

        # 每次取出最短路
        while len(pq) > 0:
            # 将堆中的元素按时间升序排序
            pq.sort(key=lambda x: -x[4])
            preX, preY, curX, curY, cost = pq.pop()

            # 向四个方向探索
            for k in range(4):
                # 新位置
                newX = curX + offsets[k][0]
                newY = curY + offsets[k][1]

                # 新位置越界,则不可进入
                if newX < 0 or newX >= n or newY < 0 or newY >= m:
                    continue

                # 本题不允许掉头,因此新位置处于掉头位置的话,不可进入
                if newX == preX and newY == preY:
                    continue

                # 每走一步都要花费 timePerRoad 单位时间
                newCost = cost + timePerRoad

                # 出发的第一步,或者右拐,不需要等待红绿灯,其他情况需要等待红绿灯 lights[curX][curY] 单位时间
                if preX != -1 and preY != -1 and getDirection(preX, preY, curX, curY, newX, newY) >= 0:
                    newCost += lights[curX][curY]

                # 如果以来源方向k到达位置(newX, newY)花费的时间 newCost 并非更优,则终止对应路径探索
                if newCost >= dist[newX][newY][k]:
                    continue

                # 否则更新为更优时间
                dist[newX][newY][k] = newCost
                # 并继续探索该路径
                pq.append((curX, curY, newX, newY, newCost))

        # 最终取(rowEnd, colEnd)终点位置的四个来源方向路径中最短时间的作为题解
        return min(dist[rowEnd][colEnd])

# 实际考试时,本题为核心代码模式,即无需我们解析输入输出,因此只需要写出上面代码即可
if __name__ == '__main__':
    n, m = map(int, input().split())
    lights = [list(map(int, input().split())) for _ in range(n)]
    timePerRoad = int(input())
    rowStart, colStart = map(int, input().split())
    rowEnd, colEnd = map(int, input().split())

    print(Solution().calcTime(lights, timePerRoad, rowStart, colStart, rowEnd, colEnd))


  • 11
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

算法之旅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值