
专栏导读
本专栏收录于《华为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 2≤n≤100)
- m m m:路线数量( 1 ≤ m ≤ 1000 1 \le m \le 1000 1≤m≤1000)
- w w w:最大预算( 1 ≤ w ≤ 10 5 1 \le w \le 10^5 1≤w≤105)
接下来是一个二维数组,记录了所有的路线信息。每条路线有 3 个整数 u , v , c o s t u, v, cost u,v,cost:
- u u u:起点城市( 1 ≤ u ≤ n 1 \le u \le n 1≤u≤n)
- v v v:终点城市( 1 ≤ v ≤ n 1 \le v \le n 1≤v≤n)
- c o s t cost cost:所需费用( 1 ≤ c o s t ≤ 10 5 1 \le cost \le 10^5 1≤cost≤105)
三、输出描述
- 若存在满足预算的最优路线,返回最小花费;
- 若不存在,则返回 − 1 -1 −1。
补充说明:
- 出发城市编号为 1,目的地城市编号为 n n n 所代表的值。
- 每条路线的起点和终点都不相同( u ≠ v u \ne v u=v),且只能单方向通行( u → v u \to v u→v,不能 v → u v \to u v→u)。
- 从城市 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算法的适用场景,发现新题目,随时更新。


被折叠的 条评论
为什么被折叠?



