瑞瑞的体力回收路径
【题目来源】
瑞学堂:瑞瑞的体力回收路径
【题目描述】
瑞瑞所在的城市有 \(n\) 个路口和 \(m\) 条有向道路。每条道路连接两个路口,并有一个非负的通行时间 \(t_i\)。瑞瑞每天骑自行车从 \(1\) 号路口出发,前往 \(n\) 号路口的学校上学。
他的自行车有一个特殊的能量回收系统。每条道路都有一个能耗系数 \(k_i\)。当瑞瑞经过能耗系数为 \(k_i\) 的道路时,他的体力值会相应地变化 \(k_i\)(正数为恢复体力,负数为消耗体力)。
瑞瑞的初始体力为 \(S\),且体力值在任何时刻都不允许为负数,所以他无法通过会导致体力变为负数的道路。
同时,体力值拥有上限 \(H\),如果经过某条道路后体力值将要超过 \(H\),则体力值将维持在上限 \(H\),多出的部分将丢失。
瑞瑞想知道,从家到学校的最短通行时间是多少。如果有多条路径的通行时间相同,他希望能选择到达学校时体力值最大的那条路径。
【输入】
第一行包含四个整数 \(n,m,S,H\),分别表示路口数、道路条数、初始体力值和体力上限。
接下来的 \(m\) 行,每行四个整数 \(u_i,v_i,t_i,k_i\),表示第 \(i\) 条从 \(u_i\) 到 \(v_i\) 的有向道路,其通行时间为 \(t_i\),回收系数为 \(k_i\),且 \(u_i\) 必定不等于 \(v_i\)。
【输出】
输出一行。如果无法从 \(1\) 号路口到达 \(n\) 号路口,输出 \(−1\);否则输出用空格分隔的两个整数:最短的总通行时间和在该时间下到达 \(n\) 号路口时的最大体力值。
【输入样例】
2 2 5 10
1 2 10 3
1 2 20 8
【输出样例】
10 8
【核心思想】
-
问题分析:给定 \(n\) 个路口、\(m\) 条有向道路,每条道路有通行时间 \(t_i\) 和能耗系数 \(k_i\)(正为恢复、负为消耗)。初始体力为 \(S\),体力上限为 \(H\),体力不能为负、超过 \(H\) 则截断。求从路口 \(1\) 到路口 \(n\) 的最短通行时间;若时间相同,选到达时体力最大的路径。无法到达输出 \(-1\)。这是一个状态扩展 Dijkstra问题,关键在于将"体力"作为状态维度,用二维最短路求解。
-
算法选择:
- Dijkstra 算法(状态扩展):状态定义为 \((u, s)\),表示到达路口 \(u\) 且当前体力为 \(s\) 的最短通行时间
- 小根堆优化:按通行时间升序的优先队列,保证每次取出当前最优状态
- 体力约束处理:转移时检查 \(ns = s + k_i\),若 \(ns < 0\) 则不可行,若 \(ns > H\) 则截断为 \(H\)
-
关键步骤:
- 初始化:
dist[u][s]表示到达路口 \(u\) 且体力为 \(s\) 的最短通行时间,初始化为 \(\infty\) - 起点入队:
dist[1][S] = 0,将 \((0, 1, S)\) 入堆 - Dijkstra 扩展循环:
- 取出堆顶 \((t, u, s)\),若 \(t > dist[u][s]\) 则跳过
- 遍历 \(u\) 的所有出边 \((v, t_i, k_i)\):
- 计算新体力 \(ns = s + k_i\)
- 若 \(ns < 0\),跳过(体力不能为负)
- 若 \(ns > H\),令 \(ns = H\)(截断到上限)
- 新时间 \(nt = t + t_i\)
- 若 \(nt < dist[v][ns]\),更新并入堆
- 统计答案:遍历 \(s\) 从 \(0\) 到 \(H\),找
dist[n][s]最小值;若相同则取 \(s\) 最大
- 初始化:
-
时间/空间复杂度:
- 时间复杂度:\(O(m \cdot H \log(nH))\),每个状态 \((u, s)\) 最多被更新一次,每次堆操作 \(O(\log(nH))\)
- 空间复杂度:\(O(nH + m)\),
dist数组、邻接表、优先队列
-
状态扩展 Dijkstra 的核心思想:
- 体力作为状态维度:将一维最短路扩展为二维状态 \((u, s)\),因为体力变化影响后续可行路径,必须纳入状态
- 截断简化状态空间:体力上限 \(H\) 将无限状态空间压缩为有限空间 \(s \in [0, H]\),保证算法可终止
- 体力非负约束剪枝:\(ns < 0\) 时直接跳过,避免无效状态入队,减少搜索空间
- 双目标优化:Dijkstra 保证通行时间最短;时间相同时通过最后遍历体力维度取最大,实现次优目标最大化
- 适用于"路径代价 + 资源约束"类问题,核心在于将资源量化为状态维度,用多维 Dijkstra 求解带约束的最短路
【算法标签】
Dijkstra
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 20005, INF = 1e9; // N为路口最大数量,INF为极大值
int n, m, S, H; // n为路口数,m为道路条数,S为初始体力,H为体力上限
struct Edge
{
int v, t, k; // v为目标路口,t为通行时间,k为能耗系数
};
vector<Edge> g[N]; // g[u]存储从路口u出发的所有道路
int dist[N][205]; // dist[u][s]表示到达路口u且体力为s时的最短通行时间
// 优先队列节点:存储当前通行时间、当前路口、当前体力
struct Node
{
int t, u, S; // t为累计通行时间,u为当前路口,S为当前体力
};
// 重载大于运算符:使priority_queue成为小根堆(按通行时间升序)
bool operator>(Node x, Node y)
{
return x.t > y.t; // 通行时间短的优先
}
// Dijkstra算法:状态为(路口, 体力),求最短通行时间
void dijkstra()
{
memset(dist, 0x3f, sizeof(dist)); // 将所有距离初始化为极大值
priority_queue<Node, vector<Node>, greater<Node>> pq; // 小根堆
dist[1][S] = 0; // 起点:路口1,初始体力S,通行时间为0
pq.push({0, 1, S}); // 将初始状态入队
while (!pq.empty()) // 当队列不为空时继续
{
auto x = pq.top(); pq.pop(); // 取出通行时间最小的状态
int t = x.t, u = x.u, s = x.S; // 取出当前通行时间、路口、体力
if (t > dist[u][s]) // 如果该状态已被更优路径访问过,跳过
continue;
for (auto x : g[u]) // 遍历从当前路口u出发的所有道路
{
int ns = s + x.k; // 计算经过该道路后的新体力值
if (ns < 0) // 如果体力变为负数,无法通过该道路
continue;
if (ns > H) // 如果体力超过上限H
ns = H; // 体力维持在上限H,多余部分丢失
int nt = t + x.t; // 计算新的累计通行时间
// 如果找到到达目标路口v、体力为ns的更短路径
if (nt < dist[x.v][ns])
{
dist[x.v][ns] = nt; // 更新最短通行时间
pq.push({nt, x.v, ns}); // 将新状态入队
}
}
}
}
int main()
{
cin >> n >> m >> S >> H; // 读入路口数、道路条数、初始体力、体力上限
while (m--) // 读入m条有向道路
{
int u, v, k, t;
cin >> u >> v >> t >> k;
g[u].push_back({v, t, k}); // 添加从u到v的道路
}
dijkstra(); // 执行Dijkstra算法
// 在所有到达路口n的状态中,找最短通行时间;时间相同时选体力最大的
int minn = INF, maxn = -1; // minn记录最短通行时间,maxn记录对应的最大体力
for (int s = 0; s <= H; s++) // 遍历所有可能的体力值
{
if (dist[n][s] < minn) // 如果找到更短的通行时间
{
minn = dist[n][s]; // 更新最短通行时间
maxn = s; // 记录对应的体力值
}
else if (dist[n][s] == minn && s > maxn) // 如果时间相同但体力更大
{
maxn = s; // 更新最大体力
}
}
if (minn == INF) // 如果无法到达路口n
cout << -1 << endl; // 输出-1
else
cout << minn << " " << maxn << endl; // 输出最短通行时间和最大体力值
return 0;
}
【运行结果】
2 2 5 10
1 2 10 3
1 2 20 8
10 8
浙公网安备 33010602011771号