形式化题目

给定有向带权图 G=(V,E)G=(V,E),∣V∣=N⩽1000|V| = N \leqslant 1000、∣E∣=M⩽10000|E| = M \leqslant 10000,边权 L⩾1L \geqslant 1。 一个城市的线路可以有多条平行边,平行边算不同线路。给定 S≠FS \ne F,保证 SS 到 FF 有路。

记 d=dist⁡(S,F)d = \operatorname{dist}(S,F) 为 S→FS \to F 的最短路径长度。要求计数

∣{ P:P 是 S→F 的路径, ∣P∣=d }∣  +  ∣{ P:P 是 S→F 的路径, ∣P∣=d+1 }∣, \Bigl|\{\,P : P \text{ 是 } S \to F \text{ 的路径},\ |P| = d\,\}\Bigr| \;+\; \Bigl|\{\,P : P \text{ 是 } S \to F \text{ 的路径},\ |P| = d + 1\,\}\Bigr|,

其中 ∣P∣|P| 是 PP 上所有边权之和。答案 ⩽109\leqslant 10^9。

因为 L⩾1L \geqslant 1,长度为 ⩽d+1\leqslant d+1 的走法里不可能插入权 ⩾2\geqslant 2 的环 (非自环的环至少 2 条边、权至少 2,自环权 ⩾2\geqslant 2 同理), 所以只需额外考虑“绕一次权为 1 的自环”这一种非简单走法; 本题的测试数据中不存在自环,两种理解结果相同。

正解

思路

1. 朴素做法与瓶颈

最直接的做法是 DFS 枚举 SS 出发的所有路径,长度一旦超过 d+1d+1 就回溯(因为边权为正), 统计落在 dd 与 d+1d+1 的条数:

text
dfs(u, 长度):
    若 u == F: 记录长度; 返回
    枚举 u 的每条出边 (u -> v, w):
        若 长度 + w <= d + 1: dfs(v, 长度 + w)

剪枝是对的,但复杂度是路径条数而不是 NN 或 MM。考虑一条链 1→2→⋯→N1 \to 2 \to \dots \to N,把每条边都换成 cc 条平行边: 从 11 到 NN 的线路数是 cN−1c^{N-1},DFS 必须展开全部 cN−1c^{N-1} 条分支。

题面保证"答案不超过 10910^9"只是说合法路径条数有上界, DFS 在搜索途中仍然会走遍所有分支,依然是指数级。 瓶颈在于枚举法把"当前长度"当成搜索参数,同一个 (u,长度)(u, \text{长度}) 会被反复展开; 正确的方向是把长度压进状态,让每个状态只算一次。

2. 关键观察:超额只有 0 或 1

设 base[v]\mathrm{base}[v] 为 S→vS \to v 的最短路长度(base[S]=0\mathrm{base}[S] = 0), dist⁡(v,F)\operatorname{dist}(v,F) 为 v→Fv \to F 的最短路长度。 对一条从 SS 出发、走到 vv、长度为 ℓv\ell_v 的路径,把

overv  =  ℓv−base[v] \mathrm{over}_v \;=\; \ell_v - \mathrm{base}[v]

叫做它在 vv 处的超额。超额一定是非负的(base\mathrm{base} 是最短路)。

若整条路径 PP 的总长 ∣P∣⩽base[F]+1|P| \leqslant \mathrm{base}[F] + 1,把两件事拼起来:

  • PP 在 vv 之后那一段是从 vv 到 FF 的一条真实路径,所以 dist⁡(v,F)⩽∣P∣−ℓv\operatorname{dist}(v,F) \leqslant |P| - \ell_v,即 ∣P∣−1⩾ℓv+dist⁡(v,F)−1|P| - 1 \geqslant \ell_v + \operatorname{dist}(v,F) - 1;
  • 从 SS 走最短路到 vv、再走最短路到 FF 是一条真实的 S→FS \to F 路径,所以 base[v]+dist⁡(v,F)⩾base[F]\mathrm{base}[v] + \operatorname{dist}(v,F) \geqslant \mathrm{base}[F]。

两式对同一个 ∣P∣|P| 夹逼:

base[v]+dist⁡(v,F)  ⩾  base[F]  ⩾  ∣P∣−1  ⩾  ℓv+dist⁡(v,F)−1. \mathrm{base}[v] + \operatorname{dist}(v,F) \;\geqslant\; \mathrm{base}[F] \;\geqslant\; |P| - 1 \;\geqslant\; \ell_v + \operatorname{dist}(v,F) - 1 .

约掉 dist⁡(v,F)\operatorname{dist}(v,F) 得 base[v]⩾ℓv−1\mathrm{base}[v] \geqslant \ell_v - 1,即 ℓv−base[v]⩽1\ell_v - \mathrm{base}[v] \leqslant 1。于是

 overv∈{0, 1}  \boxed{\ \mathrm{over}_v \in \{0,\ 1\}\ }

也就是说:沿途每个点的超额只可能是 0 或 1,三层以上的信息一律不需要。

这个推导只用到 L⩾1L \geqslant 1,并没有用到“路径不重复经过城市”, 所以它同样覆盖“绕了一次权为 1 的自环”这种走法。也就是说无论题目把“路线”理解为简单路径 还是允许重复的走法,只要长度在 d+1d+1 以内,状态模型都完整地数到。 而本题的测试数据里没有自环,两种理解结果一致。

3. 建模:点拆成两层状态

把每个城市 vv 拆成两个状态 (v,0)(v, 0) 和 (v,1)(v, 1),kk 就是超额。 走一条边 u→wvu \xrightarrow{w} v 时,新超额由

over  =  (base[u]+k+w)−base[v] \mathrm{over} \;=\; \bigl(\mathrm{base}[u] + k + w\bigr) - \mathrm{base}[v]

决定;over>1\mathrm{over} > 1 的转移直接丢弃。

下图是样例第 1 组的状态模型:实线是「超额不增」的转移(0→00 \to 0), 虚线是「超额 0→10 \to 1」的转移,灰色的 2→32 \to 3 因超额 2 被丢弃。

flowchart LR
  s10["(1,0) 长0"] -->|3| s20["(2,0) 长3"]
  s10 -->|2| s30["(3,0) 长2"]
  s10 -.->|5| s41["(4,1) 长5"]
  s30 -->|2| s40["(4,0) 长4"]
  s30 -->|4| s50["(5,0) 长6"]
  s20 -->|3| s50
  s40 -.->|3| s51["(5,1) 长7"]
  s20 -.->|1| drop["超额2 丢弃"]
  style drop fill:#e5e7eb,stroke:#9ca3af
  linkStyle 7 stroke:#9ca3af,stroke-dasharray: 3 3

观察重点:从 (4,0)(4,0) 出发走 4→54 \to 5(w=3w=3,base[5]=6\mathrm{base}[5]=6)得到长度 7, 超额 7−6=17-6=1,进入 (5,1)(5,1);同一时刻从 (4,1)(4,1) 出发长度 8,超额 2,被丢弃—— 「只有两层」这条约束在这里体现了剪枝作用。

4. 计数:一次 Dijkstra 就够

令 ways[X]\mathrm{ways}[X] 表示状态 XX 的线路条数,初始 ways[(S,0)]=1\mathrm{ways}[(S,0)] = 1(空路径)。

关键性质:状态 (v,k)(v,k) 的总长恒为 base[v]+k\mathrm{base}[v] + k,与具体路径无关。 所以一个状态第一次被到达时,它的长度就已经定死;之后被别的路径到达只是加条数。 又因为每条边权 w⩾1w \geqslant 1,转移一定让总长严格增加,状态图是 DAG。 于是把状态按总长升序处理(堆里存 (总长, 状态) 即可), 弹出 XX 时它的全部前驱都已处理完,ways[X]\mathrm{ways}[X] 已是最终值,可以安全地分发给后继。

代码用 node = 2*v + k 把状态编码成整数;ways[nxt] == 0 表示首次到达该状态, 此时才把 (nd, nxt) 入堆,避免同一个状态重复入堆(这是常数优化,不影响正确性)。

最后回到原问题:(F,0)(F,0) 的总长是 base[F]+0=d\mathrm{base}[F] + 0 = d,(F,1)(F,1) 的总长是 d+1d+1, 而分层建模保证了长度 ⩽d+1\leqslant d+1 的路径不多不少正好落在这些状态里, 所以答案就是 ways[(F,0)]+ways[(F,1)]\mathrm{ways}[(F,0)] + \mathrm{ways}[(F,1)]。

5. 样例追踪表

下表把样例第 1 组的状态弹出过程摊开(N=5N=5,S=1S=1,F=5F=5,d=6d=6)。 列依次是:弹出顺序、状态、该状态的总长、到达它的线路数、它向后继分发的条数。

弹出顺序 状态 (v,k)(v,k) 总长 base[v]+k\mathrm{base}[v]+k 该状态线路数 转移结果
1 (1,0)(1,0) 0 1 1→21 \to 2 ⇒ (2,0)(2,0) 长 3;1→31 \to 3 ⇒ (3,0)(3,0) 长 2;1→41 \to 4 ⇒ (4,1)(4,1) 长 5
2 (3,0)(3,0) 2 1 3→43 \to 4 ⇒ (4,0)(4,0) 长 4;3→53 \to 5 ⇒ (5,0)(5,0) 长 6
3 (2,0)(2,0) 3 1 2→32 \to 3 超额 2 丢弃;2→52 \to 5 ⇒ (5,0)(5,0) 长 6
4 (4,0)(4,0) 4 1 4→54 \to 5 ⇒ (5,1)(5,1) 长 7
5 (4,1)(4,1) 5 1 4→54 \to 5 超额 2 丢弃
6 (5,0)(5,0) 6 2 —
7 (5,1)(5,1) 7 1 —

终点两条状态的条数相加:2+1=32 + 1 = 3,与样例输出一致。 其中 1→2→51 \to 2 \to 5 与 1→3→51 \to 3 \to 5 贡献两条最短路, 1→3→4→51 \to 3 \to 4 \to 5 贡献唯一一条次短路。

代码

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-10-10 11:30
 * update_at: 2026-10-10 11:30
 */
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const ll INF = 1000000000000000000LL; // 不可达哨兵

int n, m, s, f;
// 邻接表:first 是边权,second 是去向
vector<pair<ll, int> > adj[1005];
ll base[1005];      // src 到每个点的最短路长度
ll ways[2005];      // 拆点 2*v+k 的路径条数
ll dist2[2005];     // 拆点状态的总长(base[v]+k)

// 第一遍 Dijkstra:求 src 到每个点的最短路 base[]
void shortest_from(int src) {
    for (int i = 1; i <= n; i++) base[i] = INF;
    base[src] = 0;
    priority_queue<pair<ll, int>, vector<pair<ll, int> >, greater<pair<ll, int> > > pq;
    pq.push(make_pair(0LL, src));
    while (!pq.empty()) {
        ll d = pq.top().first;
        int u = pq.top().second;
        pq.pop();
        if (d > base[u]) continue;
        for (size_t i = 0; i < adj[u].size(); i++) {
            ll w = adj[u][i].first;
            int v = adj[u][i].second;
            ll nd = d + w;
            if (nd < base[v]) {
                base[v] = nd;
                pq.push(make_pair(nd, v));
            }
        }
    }
}

// 第二遍分层 Dijkstra:统计最短路与「恰好长 1」的次短路条数之和
ll count_routes(int src, int dst) {
    shortest_from(src);
    for (int i = 0; i < 2 * n + 2; i++) {
        ways[i] = 0;
        dist2[i] = INF;
    }
    int start_node = 2 * src;           // k = 0 层
    ways[start_node] = 1;
    dist2[start_node] = 0;
    priority_queue<pair<ll, int>, vector<pair<ll, int> >, greater<pair<ll, int> > > pq;
    pq.push(make_pair(0LL, start_node));
    while (!pq.empty()) {
        ll d = pq.top().first;
        int node = pq.top().second;
        pq.pop();
        int v = node >> 1;
        ll cnt = ways[node];
        for (size_t i = 0; i < adj[v].size(); i++) {
            ll w = adj[v][i].first;
            int to = adj[v][i].second;
            ll nd = d + w;
            ll over = nd - base[to];   // 相对最短路超出的单位数
            if (over > 1) continue;    // 比最短路多 1 以上,丢弃
            int nxt = 2 * to + (int)over;
            if (ways[nxt] == 0) {      // 首次到达该状态,总长已定型
                dist2[nxt] = nd;
                pq.push(make_pair(nd, nxt));
            }
            ways[nxt] += cnt;
        }
    }
    return ways[2 * dst] + ways[2 * dst + 1];
}

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

    int T;
    cin >> T;
    while (T--) {
        cin >> n >> m;
        for (int i = 1; i <= n; i++) adj[i].clear();
        for (int i = 0; i < m; i++) {
            int a, b;
            ll len;
            cin >> a >> b >> len;
            adj[a].push_back(make_pair(len, b));  // 平行边各自独立
        }
        cin >> s >> f;
        cout << count_routes(s, f) << "\n";
    }
    return 0;
}
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-10-02 01:59
# update_at: 2026-10-02 02:07

import sys
from heapq import heappop, heappush

INF = 10 ** 18  # 不可达哨兵;任何真实路径长度都不超过 10^7


def shortest_from(src: int, adj: list[list[tuple[int, int]]], n: int) -> list[int]:
    """src 到每个点的最短路长度:本题的基准值 base,不可达记 INF。"""
    dist = [INF] * (n + 1)
    dist[src] = 0
    heap = [(0, src)]
    while heap:
        d, u = heappop(heap)
        if d > dist[u]:
            continue
        for w, v in adj[u]:
            nd = d + w
            if nd < dist[v]:
                dist[v] = nd
                heappush(heap, (nd, v))
    return dist


def count_routes(src: int, dst: int, adj: list[list[tuple[int, int]]], n: int) -> int:
    """src→dst 的最短路条数 + 恰好比最短路长 1 的路径条数。

    状态编码 node = 2*v + k:k 表示走到 v 时总长相对 base[v] 已经超出几个单位,
    只可能取 0 或 1。每走一条边 (v→to, 权 w),新状态由 nd - base[to] 决定;
    超出 1 的路径直接丢弃。因为 base 是最短路,nd ≤ base[to] + 1 时必有
    nd ≥ base[to],所以 (v, k) 的总长恒等于 base[v] + k,同一状态只会在首次
    到达时入堆。
    """
    base = shortest_from(src, adj, n)
    ways = [0] * (2 * n + 2)
    ways[2 * src] = 1                 # 出发点还处在“最短”这一层
    heap = [(0, 2 * src)]
    while heap:
        d, node = heappop(heap)
        v = node >> 1
        cnt = ways[node]
        for w, to in adj[v]:
            nd = d + w
            over = nd - base[to]      # 这条边让路径相对最短路超前了多少
            if over > 1:
                continue              # 比最短路多出 1 个单位以上,不算
            nxt = 2 * to + over
            if ways[nxt] == 0:        # 首次到达该状态,它的总长 nd 已是定值
                heappush(heap, (nd, nxt))
            ways[nxt] += cnt
    return ways[2 * dst] + ways[2 * dst + 1]


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    out: list[str] = []
    T = next(data)

    for _ in range(T):
        n, m = next(data), next(data)
        adj: list[list[tuple[int, int]]] = [[] for _ in range(n + 1)]
        for _ in range(m):
            a, b, length = next(data), next(data), next(data)
            adj[a].append((length, b))  # 平行边各自独立,天然计入不同线路
        s, f = next(data), next(data)
        out.append(str(count_routes(s, f, adj, n)))

    print('\n'.join(out))


if __name__ == "__main__":
    solve()

复杂度

  • 第一遍 Dijkstra 求 base\mathrm{base}:O(Mlog⁡N)O(M \log N);
  • 第二遍分层 Dijkstra:2N2N 个状态、2M2M 条转移,O(Mlog⁡N)O(M \log N)。

每组数据 O(Mlog⁡N)O(M \log N),TT 组为 O(T⋅Mlog⁡N)O(T \cdot M \log N);空间 O(N+M)O(N + M)。 对照朴素 DFS 的 O(路径条数)O(\text{路径条数}),最坏指数级。

总结

  • 超额夹逼:关心 S→FS \to F 长度 ⩽d+1\leqslant d+1 的路径时,沿途每个点相对最短路的超额只能取 00 或 11, 因为 base[v]+dist⁡(v,F)⩾d⩾ℓv+dist⁡(v,F)−1\mathrm{base}[v] + \operatorname{dist}(v,F) \geqslant d \geqslant \ell_v + \operatorname{dist}(v,F) - 1。
  • 状态自带长度:(v,k)(v,k) 的长度恒为 base[v]+k\mathrm{base}[v] + k,所以一次按长度升序的 Dijkstra 就能定死每个状态的条数, 不需要迭代松弛。
  • 平行边即线路:邻接表原样保留重复边即可,不要用集合或矩阵去重。

图示解析

这张图串起本题从建模到得到答案的主线:

text
有向带权图 + S, F
`- 先求 base[v] = S 到 v 的最短路
   `- 观察:长度 <= d+1 的路径,每点超额 over = 长度 - base[v] 只能是 0 或 1
      `- 建模:点 v 拆成状态 (v,0) 和 (v,1)
         `- 转移:over = base[u] + k + w - base[v],over > 1 丢弃
            `- 计数:按总长升序弹出状态,ways 累加给后继
               `- 答案 = ways[(F,0)] + ways[(F,1)]

先看每个节点代表的推理结论:最短路是第一层、次短路是第二层; 再顺着分支检查"超额 > 1 就丢弃"这一条如何保证状态数只有 2N2N, 最后确认答案由两条终点状态的条数相加得到。 图中只保留正文已经证明过的关系,不替代上面的样例推演和正确性说明。