瑞瑞的体力回收路径

【题目来源】

瑞学堂:瑞瑞的体力回收路径

【题目描述】

瑞瑞所在的城市有 \(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

【核心思想】

  1. 问题分析:给定 \(n\) 个路口、\(m\) 条有向道路,每条道路有通行时间 \(t_i\) 和能耗系数 \(k_i\)(正为恢复、负为消耗)。初始体力为 \(S\),体力上限为 \(H\),体力不能为负、超过 \(H\) 则截断。求从路口 \(1\) 到路口 \(n\) 的最短通行时间;若时间相同,选到达时体力最大的路径。无法到达输出 \(-1\)。这是一个状态扩展 Dijkstra问题,关键在于将"体力"作为状态维度,用二维最短路求解。

  2. 算法选择

    • Dijkstra 算法(状态扩展):状态定义为 \((u, s)\),表示到达路口 \(u\) 且当前体力为 \(s\) 的最短通行时间
    • 小根堆优化:按通行时间升序的优先队列,保证每次取出当前最优状态
    • 体力约束处理:转移时检查 \(ns = s + k_i\),若 \(ns < 0\) 则不可行,若 \(ns > H\) 则截断为 \(H\)
  3. 关键步骤

    • 初始化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\) 最大
  4. 时间/空间复杂度

    • 时间复杂度:\(O(m \cdot H \log(nH))\),每个状态 \((u, s)\) 最多被更新一次,每次堆操作 \(O(\log(nH))\)
    • 空间复杂度:\(O(nH + m)\)dist 数组、邻接表、优先队列
  5. 状态扩展 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
posted @ 2026-08-07 11:44  团爸讲算法  阅读(3)  评论(0)    收藏  举报