将费用作为状态维度拆点,运用 Dijkstra 算法求出每种费用下的最小时间,再单调扫描提取 Pareto 最优前沿。
OJ: roj
题目 ID: 1505
难度:提高
标签:最短路Dijkstra动态规划
创建: 2026-09-30 15:05
更新: 2026-10-07 11:01
形式化题目
给定一个包含
对于从
若一条路径
请求出从
思路
这是一道典型的**双目标最短路(Bi-objective Shortest Path)**问题,要求统计 Pareto 前沿(非支配解集合)的大小。
1. 朴素方法与瓶颈
朴素思路是通过 DFS 搜索从起点
2. 状态维度转化与上界估计
注意到两点关键性质:
- 边的权值非负:边费用
,边时间 。如果一条路径包含环路,走环只会让费用或时间增加或不变,绝不会比不走环更优。因此,所有的最小路径都可以规约为不含环的简单路径。 - 费用有明确上界:简单路径包含的边数最多为
条。每条边费用最大为 ,因此任何有效简单路径的总费用必不超过:
既然费用总和的上界较小,我们可以将费用作为状态的一个维度,将原问题转化为:
对于每个可能的总费用
,求从起点 到各节点 的最小时间。
设 dist[u][c] 表示到达节点
3. 拆点图上的 Dijkstra
由于每条边的时间
这等价于在一个有
- 堆中元素为三元组
(time, cost, u)。 - 每次取出当前时间最小的状态进行松弛,更新
dist[v][c + edge_c]并重新入堆。
4. 统计 Pareto 最优解
在 Dijkstra 结束后,我们获得了所有到达终点 dist[e][c](即费用为
- 随着费用
逐渐增大,其对应的时间必须严格小于之前出现过的所有时间最小值(即 )。 - 否则,若
,则说明之前已经存在一个费用更低( )且时间更优或相等( )的方案,当前状态被它支配,必须舍弃。
因此,只需维护一个变量 min_time 记录历史最小时间。每当遇到更小的时间,就找到了一组新的 Pareto 最优解,计数器加 1,并更新 min_time。
图示过程
以样例为例:
graph LR
1((1)) -- c=2, t=1 --- 2((2))
1((1)) -- c=1, t=4 --- 3((3))
2((2)) -- c=1, t=2 --- 3((3))
2((2)) -- c=2, t=4 --- 4((4))
3((3)) -- c=3, t=1 --- 4((4))
所有简单路径产生的
| 路径 | 总费用 |
总时间 |
是否为最小路径 | 原因说明 |
|---|---|---|---|---|
| 是 | 费用为 4 时时间最短 | |||
| 是 | 与上一条费用和时间相同,算作同一种 | |||
| 是 | 费用虽高,但时间更快( |
|||
| 否 | 被 |
按费用
时,时间为 , ,计入答案,当前 min_time = 5;时,时间为 , ,计入答案,当前 min_time = 4;- 最终不同种类的最小路径数为
。
代码
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-05 21:47
# update_at: 2026-07-05 21:47
import heapq
import sys
INF = 10**9
MAX_COST = 10000
def solve() -> None:
data = sys.stdin.read().split()
if not data:
return
it = iter(map(int, data))
n, m, s, e = next(it), next(it), next(it), next(it)
adj: list[list[tuple[int, int, int]]] = [[] for _ in range(n + 1)]
for _ in range(m):
u, v, c, t = next(it), next(it), next(it), next(it)
adj[u].append((v, c, t))
adj[v].append((u, c, t))
# dist[u][cost]: 到达节点 u 恰好花费 cost 时的最小时间
dist = [[INF] * (MAX_COST + 1) for _ in range(n + 1)]
dist[s][0] = 0
pq: list[tuple[int, int, int]] = [(0, 0, s)] # (time, cost, u)
while pq:
t, c, u = heapq.heappop(pq)
if t > dist[u][c]:
continue
for v, edge_c, edge_t in adj[u]:
nc, nt = c + edge_c, t + edge_t
if nc <= MAX_COST and nt < dist[v][nc]:
dist[v][nc] = nt
heapq.heappush(pq, (nt, nc, v))
# 统计帕累托最优解:按 cost 从小到大扫描,只有 time 严格小于当前历史最小 time 才是非支配解
min_time = INF
ans = 0
for c in range(MAX_COST + 1):
t = dist[e][c]
if t < min_time:
min_time = t
ans += 1
print(ans)
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* 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-05 05:18
* update_at: 2026-10-05 05:18
*/
#include <iostream>
#include <queue>
using namespace std;
const int MAXN = 105;
const int MAXM = 305;
const int MAX_COST = 10000; // 简单路径边数不超过 n-1,总费用最多 99*100,取 10000 作上界
const int INF = 1000000000;
typedef long long ll;
ll n, m, s, e;
// 链式前向星存无向图,每条无向边拆成两条有向边
int head[MAXN];
int nxt[2 * MAXM];
int to[2 * MAXM];
int edge_cost[2 * MAXM]; // 边的费用
int edge_time[2 * MAXM]; // 边的时间
int edge_cnt;
// dist[u][c]:到达节点 u 且累计费用恰好为 c 时的最小时间
int dist[MAXN][MAX_COST + 1];
struct State {
int t; // 已累计的时间
int c; // 已累计的费用
int u; // 当前节点
bool operator<(const State &other) const {
return t > other.t; // 时间小的优先,配合 priority_queue 实现小根堆
}
};
// 加入一条 u -> v 的有向边,携带费用 c 和时间 t。
void add_edge(int u, int v, int c, int t) {
edge_cnt++;
to[edge_cnt] = v;
edge_cost[edge_cnt] = c;
edge_time[edge_cnt] = t;
nxt[edge_cnt] = head[u];
head[u] = edge_cnt;
}
// 以费用为状态维度拆点,在 (节点, 费用) 状态图上跑 Dijkstra。
void dijkstra() {
for (int u = 1; u <= n; u++) {
for (int c = 0; c <= MAX_COST; c++) {
dist[u][c] = INF;
}
}
dist[s][0] = 0;
priority_queue<State> pq;
State start;
start.t = 0;
start.c = 0;
start.u = s;
pq.push(start);
while (!pq.empty()) {
State cur = pq.top();
pq.pop();
if (cur.t > dist[cur.u][cur.c]) {
continue; // 过期状态,跳过
}
for (int i = head[cur.u]; i != 0; i = nxt[i]) {
int v = to[i];
int nc = cur.c + edge_cost[i];
int nt = cur.t + edge_time[i];
if (nc > MAX_COST) {
continue;
}
if (nt < dist[v][nc]) {
dist[v][nc] = nt;
State nxt_state;
nxt_state.t = nt;
nxt_state.c = nc;
nxt_state.u = v;
pq.push(nxt_state);
}
}
}
}
// 按费用升序扫描终点状态,只有时间严格小于历史最小值时才是 Pareto 最优解。
int count_pareto() {
int min_time = INF;
int ans = 0;
for (int c = 0; c <= MAX_COST; c++) {
int t = dist[e][c];
if (t < min_time) {
min_time = t;
ans++;
}
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> s >> e;
for (ll i = 0; i < m; i++) {
int p, r, c, t; // 端点编号不超过 100,费用与时间不超过 100,用 int 即可
cin >> p >> r >> c >> t;
add_edge(p, r, c, t);
add_edge(r, p, c, t);
}
dijkstra();
cout << count_pareto() << "\n";
return 0;
}复杂度分析
- 时间复杂度:状态总数为
。边数 。使用二叉堆优化 Dijkstra 算法的时间复杂度为 。实际可达状态远少于理论上限,在 Python 下运行效率高。 - 空间复杂度:存储距离数组
dist的空间为,约需几兆字节内存,远低于评测系统 128 MB 的限制。
总结
面对双目标权值优化问题,当其中一个维度的取值范围较小(或受简单路径长度限制)时,将该维度化作状态维度进行拆点,并用 Dijkstra 算法求解另一个维度的最优值,是一种非常高效的标准解法。最后通过一次单调扫描即可去重并滤出所有 Pareto 最优解。
