[USACO06NOV] Roadblocks G

GitHub跳转原题关系图返回列表

Dijkstra 同时维护每点严格不同的最短与次短距离。

OJ: luogu

题目 ID: P2865

难度:普及+/提高-

标签:次短路Dijkstra最短路python

创建: 2026-07-17 03:00

更新: 2026-10-20 19:50

题意

求从 1 到 n 严格长于最短路的最小路径长度,路径允许回退。

思路

先破除一个直觉

“用 Dijkstra 往外走,到达 n 的第一条路径是最短路,第二条就是次短路”——这个想法是错的。

题面明确说次短路可以和最短路共用道路、可以回头。所以对链 1-2-3(边权皆 1),最短路是 2,次短路是 1-2-1-2-3 = 4,它比最短路长,而且走了一遍回头路。

这说明次短路可能不是简单路径,不能在简单路径空间里枚举。“第二条到达”和“长度第二小”是两件事。

状态扩成两个

对每个点维护两个严格不同的距离:

变量 含义
dist1[v] 到 v 的最小距离
dist2[v] 到 v 的严格大于 dist1[v] 的最小距离

用堆按长度升序弹出候选,每条候选都是一条真实路径。松弛 u → v 得到候选 nd 时分两种情形。

情形一:nd < dist1[v]——旧最短路要降级,不能丢。

这是本题唯一一个不写就会错的细节。dist1[v] 的旧值确实是一条到 v 的真实路径,只是不再是最短的。它现在是次短的有力候选,所以不能丢弃:

python
candidate, shortest[neighbor] = shortest[neighbor], candidate

交换之后 candidate 自动变成旧的最短路,直接进入第二个判断去竞争次短。

实测后果:对 1-2 权 4、1-3 权 8、2-3 权 2,不降级的版本会输出 10,而正确答案是 8(1-3 直达)。

情形二:dist1[v] < nd < dist2[v]——直接更新次短。

两个不等号都不能改:

  • 严格大于 dist1[v]:长度等于最短路的另一条路径不算次短路。对“两条长度 2 的最短路 + 一条长度 10 直连”的菱形图,改成 >= 会输出 2(错误),正确是 10 之外的某个值。
  • 严格小于 dist2[v]:一旦有更好的次短候选,旧的就可以永久丢弃,这与 Dijkstra 的定型逻辑一致。

这里为什么不需要 visited

main.cpp 里没有 visited[] 数组,却依然正确——因为状态扩成两个后,一个点要合法地被处理两次(一次作为最短路候选,一次作为次短路候选),不能简单地“处理过就跳过”。

定型判断没有消失,而是从“按点”变成“按记录”:

python
if current > second[node]:
    continue

这条记录既不是最短也非次短,早已被更优的值取代,展开它不会产生更优答案。于是堆里每条记录最多被有效展开一次——和 visited 达到的效果一样。

也就是说,Dijkstra 依赖的从来不是那个布尔数组,而是“堆弹出的当前最小值不会再被改进”这条单调性;visited 只是它的一个廉价实现。状态扩展后,只需把定型的粒度从“点”换成“记录”。

Python 知识

  • candidate, shortest[neighbor] = shortest[neighbor], candidate 用多重赋值一次完成交换,等价于 C++ 里先存 old_shortest 再交换。
  • 一个最小堆同时处理最短路和次短路两种记录,靠 current > second[node] 筛掉过期项。
  • shortest[neighbor] < candidate < second[neighbor] 链式比较,语义与两个 and 完全一致但更易读。
  • 整数无穷大 10**30,Python int 任意精度,不必像 C++ 那样担心 INF + w 溢出。

代码

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-07-17 01:40
# update_at: 2026-10-20 17:50
#
# main.py:次短路。求 1 到 n 严格大于最短路的路径长度中的最小值。
# 题面允许路径共用道路、允许回头,所以次短路可能不是简单路径,
# 例如链 1-2-3(边权皆 1)的次短路是 1-2-1-2-3 = 4。
# 因此把每个点的状态从一个距离扩成两个严格不同的距离:
#   shortest[v] 到 v 的最小值;second[v] 严格大于 shortest[v] 的最小值。
# 用堆按长度升序弹出候选,每条候选都是一条真实路径。
# 一个点的最短路被推翻时,旧值不能丢——它仍是真实路径,应降级为次短候选,
# 这就是下面那个多重赋值交换的意义,去掉它结果会错。
# 这里不需要 visited:状态扩成两个后一个点要被处理两次,定型判断从"按点"
# 变成"按记录",用 current > second[node] 筛掉过期记录即可。

import sys
from heapq import heappop, heappush

INF = 10**30  # 整数哨兵,比任何合法路径都大;Python int 任意精度,不会溢出


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n, r = next(data), next(data)

    graph: list[list[tuple[int, int]]] = [[] for _ in range(n + 1)]
    for _ in range(r):
        u, v, weight = next(data), next(data), next(data)
        graph[u].append((v, weight))
        graph[v].append((u, weight))  # 道路双向

    shortest = [INF] * (n + 1)  # 到 i 的最短距离
    second = [INF] * (n + 1)    # 到 i 的次短距离,要求严格大于 shortest[i]
    shortest[1] = 0
    heap: list[tuple[int, int]] = [(0, 1)]  # (距离, 点编号),距离在前才能按它比大小

    while heap:
        current, node = heappop(heap)

        # 这条记录既不是最短路也不是次短路,已被更优的值取代,展开它无益
        if current > second[node]:
            continue

        for neighbor, weight in graph[node]:
            candidate = current + weight

            if candidate < shortest[neighbor]:
                # 找到更短的最短路:旧值是真实路径不能丢,
                # 交换后 candidate 变成旧的最短路,正好进入下面去竞争次短
                candidate, shortest[neighbor] = shortest[neighbor], candidate
                heappush(heap, (shortest[neighbor], neighbor))

            # 只接受严格大于最短路、且能改善次短的候选;
            # 长度等于最短路的另一条路径不算次短路,所以这里是 < 而非 <=
            if shortest[neighbor] < candidate < second[neighbor]:
                second[neighbor] = candidate
                heappush(heap, (candidate, neighbor))

    print(second[n])


if __name__ == "__main__":
    solve()

原有 C++ 版本仍保留:

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-17 01:40
 * update_at: 2026-10-20 20:05
 */
// 次短路:求 1 到 n 严格大于最短路的路径长度中的最小值。
// 题面允许路径共用道路、允许回头,所以次短路可能不是简单路径,
// 例如链 1-2-3(边权皆 1)的次短路是 1-2-1-2-3 = 4,比最短路长且走了一遍回头路。
// 因此不能在简单路径空间里枚举,而是把每个点的状态从一个距离扩成两个严格不同的距离:
//   dist1[v] 到 v 的最小值;dist2[v] 严格大于 dist1[v] 的最小值。
// 用堆按长度升序弹出候选,每条候选都是一条真实路径;一个点的最短路被推翻时,
// 旧值不能丢——它仍是真实路径,应降级为次短候选,这正是不写出那个交换就会错的地方。
// 这里不需要 visited 数组:状态扩成两个后一个点要被处理两次,定型判断从"按点"
// 变成"按记录",用 d > dist2[u] 筛掉过期记录即可。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 5000 + 5; // 点数上界 N <= 5000
const long long INF = (1LL << 60);

struct Edge {
    int to; // 边指向的点
    int w;  // 边的长度
};

struct HeapNode {
    int u;
    long long dist;

    // 小根堆:距离大的排后面
    bool operator<(const HeapNode &other) const {
        return dist > other.dist;
    }
};

int n, r;
vector<Edge> g[MAXN];  // g[u]:从 u 出发的全部边
long long dist1[MAXN]; // 到 i 的最短距离
long long dist2[MAXN]; // 到 i 的次短距离,要求严格大于 dist1[i]

void add_edge(int u, int v, int len) {
    g[u].push_back({v, len});
}

void dijkstra() {
    for (int i = 1; i <= n; i++) {
        dist1[i] = INF;
        dist2[i] = INF;
    }

    priority_queue<HeapNode> pq;
    dist1[1] = 0;
    pq.push({1, 0});

    while (!pq.empty()) {
        HeapNode cur = pq.top();
        pq.pop();

        int u = cur.u;
        long long d = cur.dist;

        // 这条记录既不是最短路也不是次短路,早已被更优的值取代,展开它无益
        if (d > dist2[u]) continue;

        for (const Edge &e : g[u]) {
            int v = e.to;
            long long nd = d + e.w;

            // 找到了更短的最短路:旧的最短路是真实路径,不能丢,
            // 通过交换把它降级成次短候选(交换后 nd 就是旧的最短路值)
            if (nd < dist1[v]) {
                std::swap(nd, dist1[v]);
                pq.push({v, dist1[v]});
            }

            // 只接受严格大于最短路、且能改善次短的候选。
            // 长度等于最短路的另一条路径不算次短路,所以这里是 < 而不是 <=
            if (dist1[v] < nd && nd < dist2[v]) {
                dist2[v] = nd;
                pq.push({v, dist2[v]});
            }
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> r;

    for (int i = 1; i <= r; i++) {
        int u, v, len;
        cin >> u >> v >> len;
        add_edge(u, v, len);
        add_edge(v, u, len); // 道路双向
    }

    dijkstra();

    cout << dist2[n] << '\n';
    return 0;
}

复杂度

时间 O((n+m) log n),空间 O(n+m)。每个点最多被有效处理两次,不影响阶。

前提是边权非负:整套做法靠“堆弹出的当前最小值已定型”这条单调性,边权为负时立即失效。

总结

次短路不是“第二条被找到的路径”,而是严格大于最短值的最小距离。它可能不是简单路径,所以要把每个点的状态从一个距离扩成两个。

关键有三点:旧最短路被推翻时要降级为次短候选而不是丢弃;两个不等号都必须是严格的;定型判断从“按点”变成“按记录”,于是不写 visited 也对。