华为OD机试 - 毕业旅行 - 有向图的最短路径问题(Python/JS/C/C++ 新系统 200分)

在这里插入图片描述

华为OD机试 新系统 统一考试题库清单(持续收录中)以及考点说明(Python/JS/C/C++)

专栏导读

本专栏收录于《华为OD机试真题(Python/JS/C/C++)》

刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新。

一、题目描述

四年校园时光即将结束,小明和室友计划一次难忘的毕业旅行。他们准备从 A 城市出发,前往他们已经久仰的 B 城市。两座城市之间有多种出行方案,既可直达,也可途经其他城市中转,每条路线的费用不相同。

请帮助他们在预算 w 内,找到花费最少的路线。

二、输入描述

前有三个整数 n , m , w n, m, w n,m,w

  • n n n:城市数量( 2 ≤ n ≤ 100 2 \le n \le 100 2n100
  • m m m:路线数量( 1 ≤ m ≤ 1000 1 \le m \le 1000 1m1000
  • w w w:最大预算( 1 ≤ w ≤ 10 5 1 \le w \le 10^5 1w105

接下来是一个二维数组,记录了所有的路线信息。每条路线有 3 个整数 u , v , c o s t u, v, cost u,v,cost

  • u u u:起点城市( 1 ≤ u ≤ n 1 \le u \le n 1un
  • v v v:终点城市( 1 ≤ v ≤ n 1 \le v \le n 1vn
  • c o s t cost cost:所需费用( 1 ≤ c o s t ≤ 10 5 1 \le cost \le 10^5 1cost105

三、输出描述

  • 若存在满足预算的最优路线,返回最小花费;
  • 若不存在,则返回 − 1 -1 1

补充说明:

  1. 出发城市编号为 1,目的地城市编号为 n n n 所代表的值。
  2. 每条路线的起点和终点都不相同( u ≠ v u \ne v u=v),且只能单方向通行( u → v u \to v uv,不能 v → u v \to u vu)。
  3. 从城市 u u u 到城市 v v v,不存在多条不同费用的路线。

四、测试用例

测试用例1:

1、输入

3 3 10
1 2 5
2 3 5
1 3 8

2、输出

8

3、说明

1 -> 2 -> 3:5 + 5 = 10
1 -> 3:8

最小费用为 8,并且 8 <= 10。

测试用例2:

1、输入

4 4 20
1 2 5
2 3 5
3 4 5
1 4 25

2、输出

15

3、说明

直接:
1 -> 4 = 25
超过预算 20

中转:
1 -> 2 -> 3 -> 4
= 5 + 5 + 5
= 15

因此输出 15。

五、解题思路

这道题本质上是一个有向图的最短路径问题。

城市是节点,每条路线 u -> v 是一条有向边,cost 是边权。要求从城市 1 到城市 n 的最小花费,同时这个花费不能超过预算 w。

这里有一个关键点:预算和我们要求最小化的指标都是“费用”。因此不需要额外做背包或二维状态,只要求出从 1 到 n 的最短路:

最短路 <= w:输出最短路;

最短路 > w 或根本无法到达:输出 -1。

由于所有路线费用满足 cost >= 1,不存在负权边,所以非常适合使用 Dijkstra 算法。

数据结构采用:

邻接表保存有向图,空间复杂度 O(n + m);

小根堆 / 优先队列维护当前累计费用最小的城市;

dist[i] 保存城市 1 到城市 i 当前已知的最小费用。

同时可以做一个预算剪枝:如果某条路径累计费用已经超过 w,由于后面的边权都为正数,继续走只会更贵,因此可以直接丢弃。

六、Python算法源码

import sys
import heapq


def dijkstra(graph, n, budget):
    INF = 10 ** 30

    # dist[i] 表示从城市 1 到城市 i 的当前最小费用。
    dist = [INF] * (n + 1)
    dist[1] = 0

    /*
    Python 的 heapq 是小根堆。
    堆中的元素格式为:
        (累计费用, 城市编号)

    每次取出的都是当前费用最小的状态,
    因此可以实现 Dijkstra 算法。
    */
    pq = [(0, 1)]

    while pq:
        cur_cost, city = heapq.heappop(pq)

        # 如果该状态已经不是当前最优状态,
        # 说明它是之前遗留下来的旧记录,直接跳过。
        if cur_cost != dist[city]:
            continue

        # 当前最小费用已经超过预算,
        # 后续状态只会更贵,因此直接返回 -1。
        if cur_cost > budget:
            return -1

        # 第一次弹出终点时,就已经得到了全局最小费用。
        if city == n:
            return cur_cost

        for next_city, edge_cost in graph[city]:
            next_cost = cur_cost + edge_cost

            # 所有路线费用都为正数,
            # 因此超过预算的状态以后也不可能重新回到预算范围内。
            if next_cost <= budget and next_cost < dist[next_city]:
                dist[next_city] = next_cost
                heapq.heappush(
                    pq,
                    (next_cost, next_city)
                )

    return -1


def main():
    data = list(
        map(int, sys.stdin.buffer.read().split())
    )

    if not data:
        return

    n = data[0]
    m = data[1]
    w = data[2]

    graph = [
        [] for _ in range(n + 1)
    ]

    index = 3

    for _ in range(m):
        u = data[index]
        v = data[index + 1]
        cost = data[index + 2]

        index += 3

        # 题目是有向图,只加入 u -> v。
        graph[u].append(
            (v, cost)
        )

    print(
        dijkstra(graph, n, w)
    )


if __name__ == "__main__":
    main()

七、JavaScript算法源码

const fs = require('fs');

const input = fs.readFileSync(0, 'utf8')
    .trim()
    .split(/\s+/)
    .map(Number);

if (input.length === 0) {
    process.exit(0);
}

let index = 0;

const n = input[index++];
const m = input[index++];
const budget = input[index++];

const graph = Array.from(
    { length: n + 1 },
    () => []
);

for (let i = 0; i < m; i++) {
    const u = input[index++];
    const v = input[index++];
    const cost = input[index++];

    // 题目中的路线是单方向的,因此只保存 u -> v。
    graph[u].push([v, cost]);
}


// 手写最小堆。
// 每个元素格式为:
// [累计费用, 城市编号]
class MinHeap {
    constructor() {
        this.heap = [];
    }

    isEmpty() {
        return this.heap.length === 0;
    }

    push(item) {
        this.heap.push(item);

        let i = this.heap.length - 1;

        // 新元素不断向上调整,
        // 直到满足父节点费用 <= 子节点费用。
        while (i > 0) {
            const parent = Math.floor(
                (i - 1) / 2
            );

            if (
                this.heap[parent][0]
                <= this.heap[i][0]
            ) {
                break;
            }

            [
                this.heap[parent],
                this.heap[i]
            ] = [
                this.heap[i],
                this.heap[parent]
            ];

            i = parent;
        }
    }

    pop() {
        const top = this.heap[0];

        const last = this.heap.pop();

        if (this.heap.length > 0) {
            this.heap[0] = last;

            let i = 0;

            // 根节点向下调整,
            // 每次与费用更小的子节点进行交换。
            while (true) {
                let smallest = i;

                const left = i * 2 + 1;
                const right = i * 2 + 2;

                if (
                    left < this.heap.length
                    && this.heap[left][0]
                    < this.heap[smallest][0]
                ) {
                    smallest = left;
                }

                if (
                    right < this.heap.length
                    && this.heap[right][0]
                    < this.heap[smallest][0]
                ) {
                    smallest = right;
                }

                if (smallest === i) {
                    break;
                }

                [
                    this.heap[i],
                    this.heap[smallest]
                ] = [
                    this.heap[smallest],
                    this.heap[i]
                ];

                i = smallest;
            }
        }

        return top;
    }
}


function dijkstra() {
    const INF = Number.MAX_SAFE_INTEGER;

    const dist = Array(n + 1).fill(INF);

    dist[1] = 0;

    const pq = new MinHeap();

    pq.push([0, 1]);

    while (!pq.isEmpty()) {

        const [curCost, city] = pq.pop();

        /*
         * 一个城市可能被重复加入堆。
         * 如果当前费用已经不是 dist[city],
         * 则说明这个状态已经过期。
         */
        if (curCost !== dist[city]) {
            continue;
        }

        // 当前最小费用已经超过预算,
        // 后续状态一定更加昂贵。
        if (curCost > budget) {
            return -1;
        }

        // 第一次弹出终点就是最短路径。
        if (city === n) {
            return curCost;
        }

        for (
            const [nextCity, edgeCost]
            of graph[city]
        ) {

            const nextCost =
                curCost + edgeCost;

            /*
             * 所有边费用均为正数。
             * 因此累计费用超过预算后,
             * 后续不可能重新降回预算范围。
             */
            if (
                nextCost <= budget
                && nextCost < dist[nextCity]
            ) {

                dist[nextCity] = nextCost;

                pq.push(
                    [nextCost, nextCity]
                );
            }
        }
    }

    return -1;
}


console.log(
    dijkstra()
);

八、C算法源码

#include <stdio.h>
#include <limits.h>

#define MAX_N 105
#define MAX_M 1005
#define MAX_HEAP 5005


typedef struct {
    int to;
    int cost;
    int next;
} Edge;


typedef struct {
    int city;
    long long cost;
} HeapNode;


Edge edges[MAX_M];

int head[MAX_N];

int edgeCount = 0;


HeapNode heap[MAX_HEAP];

int heapSize = 0;


/*
 * 使用链式前向星形式保存邻接表。
 *
 * head[u] 保存城市 u 的第一条边编号,
 * next 保存同一起点的下一条边编号。
 */
void addEdge(
    int u,
    int v,
    int cost
) {
    edges[edgeCount].to = v;
    edges[edgeCount].cost = cost;

    edges[edgeCount].next = head[u];

    head[u] = edgeCount;

    edgeCount++;
}


/*
 * 最小堆插入操作。
 *
 * 堆按照累计费用 cost 排序,
 * 保证根节点始终是当前累计费用最小的状态。
 */
void heapPush(HeapNode node) {

    int i = ++heapSize;

    heap[i] = node;

    while (i > 1) {

        int parent = i / 2;

        if (
            heap[parent].cost
            <= heap[i].cost
        ) {
            break;
        }

        HeapNode temp = heap[parent];

        heap[parent] = heap[i];

        heap[i] = temp;

        i = parent;
    }
}


/*
 * 删除并返回最小堆的根节点。
 */
HeapNode heapPop(void) {

    HeapNode top = heap[1];

    heap[1] = heap[heapSize];

    heapSize--;

    int i = 1;

    while (1) {

        int smallest = i;

        int left = i * 2;

        int right = left + 1;

        if (
            left <= heapSize
            && heap[left].cost
            < heap[smallest].cost
        ) {
            smallest = left;
        }

        if (
            right <= heapSize
            && heap[right].cost
            < heap[smallest].cost
        ) {
            smallest = right;
        }

        if (smallest == i) {
            break;
        }

        HeapNode temp = heap[i];

        heap[i] = heap[smallest];

        heap[smallest] = temp;

        i = smallest;
    }

    return top;
}


long long dijkstra(
    int n,
    long long budget
) {
    long long dist[MAX_N];

    for (int i = 1; i <= n; i++) {
        dist[i] = LLONG_MAX;
    }

    dist[1] = 0;

    heapSize = 0;

    heapPush(
        (HeapNode){1, 0}
    );

    while (heapSize > 0) {

        HeapNode current =
            heapPop();

        int city =
            current.city;

        long long currentCost =
            current.cost;

        /*
         * 同一个城市可能多次进入最小堆。
         *
         * 如果当前弹出的费用已经不是
         * dist[city],说明这是一个旧状态,
         * 无需再次处理。
         */
        if (
            currentCost
            != dist[city]
        ) {
            continue;
        }

        /*
         * 堆顶已经超过预算,
         * 后续状态只会更加昂贵。
         */
        if (
            currentCost
            > budget
        ) {
            return -1;
        }

        /*
         * 第一次以最短费用弹出终点,
         * 当前费用就是最终答案。
         */
        if (city == n) {
            return currentCost;
        }

        /*
         * 遍历 city 的所有出边。
         */
        for (
            int e = head[city];
            e != -1;
            e = edges[e].next
        ) {
            int nextCity =
                edges[e].to;

            long long nextCost =
                currentCost
                + edges[e].cost;

            /*
             * 路线费用全部大于 0,
             * 因此超过预算的状态可以直接剪枝。
             */
            if (
                nextCost <= budget
                && nextCost < dist[nextCity]
            ) {

                dist[nextCity] =
                    nextCost;

                heapPush(
                    (HeapNode){
                        nextCity,
                        nextCost
                    }
                );
            }
        }
    }

    return -1;
}


int main(void) {

    int n;
    int m;

    long long w;

    if (
        scanf(
            "%d %d %lld",
            &n,
            &m,
            &w
        ) != 3
    ) {
        return 0;
    }

    // 初始化邻接表头节点。
    for (
        int i = 0;
        i <= n;
        i++
    ) {
        head[i] = -1;
    }

    for (
        int i = 0;
        i < m;
        i++
    ) {
        int u;
        int v;
        int cost;

        scanf(
            "%d %d %d",
            &u,
            &v,
            &cost
        );

        // 有向路线,只添加 u -> v。
        addEdge(
            u,
            v,
            cost
        );
    }

    printf(
        "%lld\n",
        dijkstra(n, w)
    );

    return 0;
}

九、C++算法源码

#include <iostream>
#include <vector>
#include <queue>
#include <limits>

using namespace std;


struct Edge {
    int to;
    int cost;
};


long long dijkstra(
    const vector<vector<Edge>>& graph,
    int n,
    long long budget
) {
    const long long INF =
        numeric_limits<long long>::max();

    /*
     * dist[i] 表示:
     * 从城市 1 到城市 i 当前已知的最小费用。
     */
    vector<long long> dist(
        n + 1,
        INF
    );

    dist[1] = 0;

    /*
     * pair 的 first 是累计费用,
     * second 是城市编号。
     *
     * greater 使 priority_queue
     * 从默认的大根堆变成小根堆。
     */
    priority_queue<
        pair<long long, int>,
        vector<pair<long long, int>>,
        greater<pair<long long, int>>
    > pq;

    pq.push(
        {0, 1}
    );

    while (!pq.empty()) {

        auto [currentCost, city] =
            pq.top();

        pq.pop();

        /*
         * 一个城市可能被多次加入优先队列。
         *
         * 如果当前状态已经不是最优状态,
         * 说明它是旧记录,直接跳过。
         */
        if (
            currentCost
            != dist[city]
        ) {
            continue;
        }

        /*
         * 当前最小费用已经超过预算时,
         * 后面的状态一定只会更加昂贵。
         */
        if (
            currentCost
            > budget
        ) {
            return -1;
        }

        /*
         * Dijkstra 中第一次弹出终点,
         * 当前累计费用就是最短路径费用。
         */
        if (city == n) {
            return currentCost;
        }

        for (
            const Edge& edge :
            graph[city]
        ) {
            long long nextCost =
                currentCost
                + edge.cost;

            /*
             * 所有路线费用都是正数。
             *
             * 如果累计费用已经超过预算,
             * 后续不可能重新降低,
             * 所以直接剪枝。
             */
            if (
                nextCost <= budget
                && nextCost < dist[edge.to]
            ) {
                dist[edge.to] =
                    nextCost;

                pq.push(
                    {
                        nextCost,
                        edge.to
                    }
                );
            }
        }
    }

    return -1;
}


int main() {

    ios::sync_with_stdio(false);

    cin.tie(nullptr);

    int n;
    int m;

    long long w;

    cin >> n >> m >> w;

    vector<vector<Edge>> graph(
        n + 1
    );

    for (
        int i = 0;
        i < m;
        i++
    ) {
        int u;
        int v;
        int cost;

        cin >> u >> v >> cost;

        // 题目为有向图,只加入 u -> v。
        graph[u].push_back(
            {v, cost}
        );
    }

    cout
        << dijkstra(
               graph,
               n,
               w
           )
        << '\n';

    return 0;
}


🏆下一篇:华为OD机试真题 - 简易内存池(Python/JS/C/C++ 新系统 200分)

🏆本文收录于,华为OD机试真题(Python/JS/C/C++)

刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新。

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

哪 吒

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

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

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

打赏作者

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

抵扣说明:

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

余额充值