恰好走 N 条边的最短路:把「走一条边」看成 min-plus 矩阵乘法,用快速幂把 N 次接边压到 O(log N) 次矩阵乘法。
OJ: roj
题目 ID: 3184
难度:提高+/省选-
标签:图论矩阵乘法快速幂
创建: 2026-10-01 23:46
更新: 2026-10-10 13:00
形式化题目
给定一张无向图,边有正整数边长,可能有重边,点编号是
样例图示
下图画出样例中实际出现的 4 个点(点 5、7 没有任何边落在上面,不影响结果):
--- title: 样例:从 6 走 2 条边到 4 --- graph LR 4["4"] -- "11" --- 6["6"] 4 -- "4" --- 8["8"] 4 -- "8" --- 9["9"] 6 -- "6" --- 8 6 -- "2" --- 9 8 -- "3" --- 9 style 6 fill:#fde68a style 4 fill:#bbf7d0
答案是 10:走
正解
思路
第一步:朴素做法是什么? 最直接的想法是按边数分层递推:设
初始
第二步:把"递推一层"看成矩阵乘法。 关键观察:上面的转移本质上是一种"乘法"。把所有转移写成矩阵形式——设
它和普通矩阵乘法唯一的区别是:把普通乘法的「乘」换成了「加」,把「求和」换成了「取 min」。这种代数结构叫 min-plus 半环(也常叫广义矩阵乘法)。它为什么恰好对应"接两段路"?
在这个乘法下,两个基本事实成立:
的含义: 是走 1 条边的最短路, 是"走 1 条边 + 再走 1 条边",即走 2 条边的最短路……归纳可得 就是从 恰好走 条边到 的最短距离。 - 乘法有结合律:接路这件事天然满足"先接前三段、再接第四段"和"先接两段、再接两段"结果相同,所以 min-plus 乘法满足结合律(
充当零元: ,min 时被淘汰;对角线为 0 的单位矩阵 满足 )。
有了结合律,就能对幂次做快速幂:
第三步:矩阵要多大? 点编号虽然最大 1000,但
实现里还有一个小技巧:把"恰好走 0 条边"(单位矩阵
矩阵视角的样例演算
用样例(只看 4、6、8、9 四个点,按
| 4 | 6 | 8 | 9 | |
|---|---|---|---|---|
| 4 | 11 | 4 | 8 | |
| 6 | 11 | 6 | 2 | |
| 8 | 4 | 6 | 3 | |
| 9 | 8 | 2 | 3 |
求
| 中转点 |
合计 | |
|---|---|---|
| 4 | ||
| 6 | ||
| 8 | 10 | |
| 9 | 10 |
min 得 10,正是样例答案:
代码
/**
* 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 13:00
* update_at: 2026-10-10 13:00
*/
// 奶牛接力赛:min-plus 矩阵快速幂,求恰走 n 条边的最短路
#include <cstdio>
#include <vector>
using namespace std;
typedef long long ll;
const ll INF = 1000000000000000000LL;
int size_v;
// c = a ⊗ b(min-plus 乘法),含义是“接上两段路程”
void min_plus_mul(const vector<ll>& a, const vector<ll>& b, vector<ll>& c) {
for (int i = 0; i < size_v; i++) {
for (int j = 0; j < size_v; j++) {
ll best = INF;
for (int k = 0; k < size_v; k++) {
ll x = a[i * size_v + k], y = b[k * size_v + j];
if (x >= INF || y >= INF) {
continue;
}
if (x + y < best) {
best = x + y;
}
}
c[i * size_v + j] = best;
}
}
}
int main() {
ll n;
int t, s, e;
if (scanf("%lld %d %d %d", &n, &t, &s, &e) != 4) {
return 0;
}
vector<int> uniq; // 去重后的点编号,用线性查找离散化
vector<int> el(t), eu(t), ev(t);
for (int i = 0; i < t; i++) {
scanf("%d %d %d", &el[i], &eu[i], &ev[i]);
bool fa = false, fb = false;
for (int q = 0; q < (int)uniq.size(); q++) {
if (uniq[q] == eu[i]) fa = true;
if (uniq[q] == ev[i]) fb = true;
}
if (!fa) uniq.push_back(eu[i]);
if (!fb) uniq.push_back(ev[i]);
}
int cnt = (int)uniq.size();
int s_id = -1, e_id = -1;
for (int q = 0; q < cnt; q++) {
if (uniq[q] == s) s_id = q;
if (uniq[q] == e) e_id = q;
}
size_v = cnt;
// 邻接矩阵:同一对点可能有多条边,只保留最短的那条
vector<ll> adj(size_v * size_v, INF);
for (int i = 0; i < t; i++) {
int a = -1, b = -1;
for (int q = 0; q < cnt; q++) {
if (uniq[q] == eu[i]) a = q;
if (uniq[q] == ev[i]) b = q;
}
if (el[i] < adj[a * size_v + b]) {
adj[a * size_v + b] = adj[b * size_v + a] = el[i];
}
}
// min-plus 快速幂:恰走 n 条边
vector<ll> result(size_v * size_v, INF), base = adj, tmp(size_v * size_v);
for (int i = 0; i < size_v; i++) {
result[i * size_v + i] = 0; // 恰走 0 条边:乘法的单位元
}
ll pw = n;
while (pw) {
if (pw & 1) {
min_plus_mul(result, base, tmp);
result = tmp;
}
pw >>= 1;
if (pw) { // 最后一次乘方前不再平方,省一次 O(V^3)
min_plus_mul(base, base, tmp);
base = tmp;
}
}
printf("%lld\n", result[s_id * size_v + e_id]);
return 0;
}#!/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 00:10
# update_at: 2026-10-02 00:10
import sys
from operator import add
INF = float("inf") # 还走不到的距离哨兵:inf + inf = inf,min 时自然被淘汰
Matrix = list[list[float]]
def min_plus_mul(a: Matrix, b: Matrix) -> Matrix:
"""min-plus 乘法:c[i][j] = min_k(a[i][k] + b[k][j]),含义是“接上两段路程”。"""
b_t = list(zip(*b)) # 转置成按行取列,让内层 min(map(add, ...)) 跑在 C 级速度
return [[min(map(add, row, col)) for col in b_t] for row in a]
def min_plus_power(adj: Matrix, n: int) -> Matrix:
"""min-plus 快速幂:返回 dist[i][j] = 从 i 恰好走 n 条边到 j 的最短距离。"""
size = len(adj)
result: Matrix = [[INF] * size for _ in range(size)]
for i in range(size):
result[i][i] = 0 # 恰走 0 条边:只有自己到自己,是乘法的单位元
base = adj
while n:
if n & 1:
result = min_plus_mul(result, base)
n >>= 1
if n: # 最后一次乘方前不再平方,省一次 O(V^3)
base = min_plus_mul(base, base)
return result
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data) # 恰好经过的边数
t = next(data) # 无向边条数
s = next(data) # 起点
e = next(data) # 终点
triples: list[tuple[int, int, int]] = []
ids: dict[int, int] = {} # 点编号离散化:真正出现过的点最多 2*t 个
for _ in range(t):
length = next(data)
u, v = next(data), next(data)
triples.append((length, u, v))
ids.setdefault(u, len(ids))
ids.setdefault(v, len(ids))
# 邻接矩阵:同一对点可能有多条边,只保留最短的那条
adj: Matrix = [[INF] * len(ids) for _ in range(len(ids))]
for length, u, v in triples:
iu, iv = ids[u], ids[v]
adj[iu][iv] = adj[iv][iu] = min(adj[iu][iv], length)
dist = min_plus_power(adj, n)
print(int(dist[ids[s]][ids[e]]))
if __name__ == "__main__":
solve()复杂度
设离散化后点数为
- 时间:每次 min-plus 乘法
,快速幂做 次(仔细实现约 次,最后一次平方可省),总计 次基本操作。Python 内层用 min(map(add, row, col))把双循环交给 C 执行,本地 12 组真实数据全部在 0.05s 内跑完;若 OJ 限时严格,同样算法落地到 C++ 只需把内层换成for循环即可。 - 空间:邻接矩阵与若干中间矩阵,
。
总结
- 「恰好经过
条边」的最短路 = 按边数分层递推 ,但 高达 ,线性层数是瓶颈。 - 把层间递推抽象成 min-plus 矩阵乘法(乘→加、和→min),它有结合律,于是
可用矩阵快速幂在 次乘法内求出, 即答案。 - 两个工程要点:点编号要离散化(实际点数
,矩阵从 降到 );重边在建图时取最短。 - 这套"把递推写成矩阵乘法 → 快速幂"的框架是通用套路,凡转移满足结合律(min-plus、布尔 and-or、max-plus 等)的线性递推都适用,例如斐波那契第
项、状态压缩后的图上游走计数等。
图示解析
---
title: 算法流程:N 次接边 → log N 次矩阵乘法
---
flowchart TD
A["读入 N, T, S, E 与 T 条边"] --> B["离散化点编号<br/>建 V×V 邻接矩阵 A<br/>重边取最短"]
B --> C["result = 单位矩阵 I<br/>(恰好走 0 条边)"]
C --> D{"N 的当前二进制位是 1 ?"}
D -- "是" --> E["result = result × base<br/>(min-plus 乘法)"]
D -- "否" --> F
E --> F{"N 已移位到 0 ?"}
F -- "否" --> G["base = base × base"]
G --> D
F -- "是" --> H["输出 result[S][E]"]
流程图把普通快速幂的骨架原样搬到了 min-plus 半环上:base 逐层平方(对应 result 按 base 始终是"走恰好 result 是已确定要走的那部分边数的累积——两者相接(min-plus 乘)就是把两段边数相加、路程取最短,最终 result 恰好累积成"走