把 (城市, 剩余油量) 当作状态建分层图,加油是正权边、开车是零权边,每 个询问跑一遍优先队列 BFS(Dial 桶队列)最短路。
OJ: roj
题目 ID: 3070
难度:提高
标签:搜索优先队列BFS最短路分层图
创建: 2026-10-01 14:01
更新: 2026-10-07 13:50
形式化题目
给定一张 impossible。
样例中 impossible。
| 问询 | C | S | E | 答案 |
|---|---|---|---|---|
| 1 | 10 | 0 | 3 | 170 |
| 2 | 20 | 1 | 4 | impossible |
正解
思路
朴素做法的困境。 最直观的想法是枚举路径:从
关键观察:把油量塞进状态。 车在一个城市"还剩多少油"决定了它接下来能走哪些边、还需要买多少油。所以真正的状态不是"城市",而是二元组
flowchart LR
A["(u, f) 在 u 城剩 f 升"] -->|"加 1 升,花费 p_u"| B["(u, f+1)"]
A -->|"开车走边 u→v,耗 d 升,花费 0"| C["(v, f−d),要求 f ≥ d"]
- 加油:
,花费 ; - 开车:
,花费 。
所有转移的权值都非负,"最少油钱"就是状态图上从
从堆到桶。 状态数 buckets[已花油钱],下标 last 记录最远的非空桶(只有加油会推远它),last 仍没碰到 impossible。下面是样例 1 的分层扩展示意(只画到第二次加油):
以样例 1(
| 已花油钱 idx | 取出的状态 (城市, 剩油) | 本层发生的转移 |
|---|---|---|
| 0 | (0, 0) | 起点;在 0 城 +1 升 → 桶 10 |
| 10, 20, … | (0, 1…8) | 连续在 0 城加到 9 升 |
| 90 | (0, 9) → 开车 | → (1, 0) 留在本桶继续扩展 |
| 100, …, 160 | (1, 1…7) | 在 1 城逐升补油 |
| 170 | (1, 8) → 开车 | → (2, 7) → (3, 0) 均留在本桶;首次取出 (3, 0),答案 170 |
这张表想说明的重点是:油钱下标单调递增、同层零权开车不换桶,这正是桶队列正确性的全部前提。
正确性要点。 弹出序列按已花油钱单调不减,所以第一次从桶里取出 dist[u][f] != idx 判过期;
代码
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-01 14:01
# update_at: 2026-10-01 14:01
import sys
from collections import defaultdict, deque
def least_cost(n: int, adj: list[list[tuple[int, int]]], price: list[int],
cap: int, start: int, target: int) -> int | None:
"""分层图最短路:问 (城市, 油量) 状态图上从 (start,0) 走到任意 target 状态的最少油钱。
用 Dial 桶队列代替二叉堆:每步转移的花费增量 ≤ 最大油价 100,
桶下标(已花油钱)单调递增,只有加油转移才产生新桶,空桶区间可用 last 剪枝。
"""
buckets: defaultdict[int, deque[tuple[int, int]]] = defaultdict(deque)
dist = [[1 << 30] * (cap + 1) for _ in range(n)] # dist[u][f]:到达 u 且剩 f 升的最少油钱
dist[start][0] = 0
buckets[0].append((start, 0))
pending = 1 # 桶里还没处理的状态总数
last = 0 # 最远的非空桶下标,越过它还没事可做就说明 target 不可达
idx = 0
while pending:
if idx > last:
return None # 没有任何更贵的加油转移在等待,target 到不了
bucket = buckets.get(idx)
if not bucket:
idx += 1
continue
while bucket: # 同层内油钱相同;开车不花钱,在本层内继续扩展
u, f = bucket.popleft()
pending -= 1
if dist[u][f] != idx: # 入桶后又出现过更便宜的记录,此条已过期
continue
if u == target:
return idx # 油钱单调不降,首次取出 target 即最优
p = price[u]
if f < cap and idx + p < dist[u][f + 1]: # 在 u 加 1 升油,钱变多、进新桶
dist[u][f + 1] = idx + p
buckets[idx + p].append((u, f + 1))
pending += 1
if idx + p > last:
last = idx + p
for v, d in adj[u]: # 油够就开车去 v,油量减少、花费不变、留在本层
nf = f - d
if nf >= 0 and idx < dist[v][nf]:
dist[v][nf] = idx
bucket.append((v, nf))
pending += 1
idx += 1
return None
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, m = next(data), next(data)
price = [next(data) for _ in range(n)]
adj: list[list[tuple[int, int]]] = [[] for _ in range(n)]
for _ in range(m):
u, v, d = next(data), next(data), next(data)
adj[u].append((v, d))
adj[v].append((u, d)) # 无向图,两条反向边
q = next(data)
out = []
for _ in range(q):
cap, start, target = next(data), next(data), next(data)
ans = least_cost(n, adj, price, cap, start, target)
out.append('impossible' if ans is None else str(ans))
print('\n'.join(out))
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-06 17:13
* update_at: 2026-10-06 17:13
*/
#include <iostream>
#include <deque>
using namespace std;
const int MAXN = 1005; // 城市数上限
const int MAXM = 20005; // 无向边两倍存储
const int MAXF = 110; // 油量维度上限(C ≤ 100,再加 1)
const int MAXW = 100; // 单条权值上限(油价 ≤ 100)
const int INF = 0x3f3f3f3f;
typedef long long ll;
int n, m, q;
int price[MAXN]; // price[u]:城市 u 的单位油价
// 链式前向星存储无向图
int head[MAXN], to[MAXM], wt[MAXM], nxt[MAXM], edge_cnt;
// 分层图最短路状态:(u, f) 表示在城市 u、油箱剩 f 升;dist[u][f] 为到达该状态的最少油钱
int dist[MAXN][MAXF];
// 当前询问的参数
int cap, start_city, target_city;
// 环形桶队列:权值只有 0 或 price ≤ 100,用 101 个桶按花费对 101 取模
deque<int> bucket[MAXW + 1];
void add_edge(int u, int v, int d) {
edge_cnt++;
to[edge_cnt] = v;
wt[edge_cnt] = d;
nxt[edge_cnt] = head[u];
head[u] = edge_cnt;
}
// 把城市与剩余油量编码成一个整数,方便放进桶里。
int encode_state(int u, int f) {
return u * (cap + 1) + f;
}
// 在状态图上跑 Dial 桶队列最短路:加油 +price[u],开车 +0。
// 返回最少油钱;不可达返回 INF。
int dial_shortest() {
for (int u = 0; u < n; u++) {
for (int f = 0; f <= cap; f++) {
dist[u][f] = INF;
}
}
for (int i = 0; i <= MAXW; i++) {
bucket[i].clear();
}
int mod = MAXW + 1;
dist[start_city][0] = 0;
bucket[0].push_back(encode_state(start_city, 0));
int pending = 1; // 桶里还没处理的状态数量
int cost = 0; // 当前正在处理的花费,单调不减
while (pending > 0) {
// 同一花费层内开车不花钱,产生的状态留在本桶继续扩展
while (!bucket[cost % mod].empty()) {
int id = bucket[cost % mod].front();
bucket[cost % mod].pop_front();
pending--;
int u = id / (cap + 1);
int f = id % (cap + 1);
if (dist[u][f] != cost) {
continue; // 入桶后又出现过更便宜的记录,此条已过期
}
if (u == target_city) {
return cost; // 花费单调不降,首次取出终点即最优
}
// 加油 1 升:花费增加 price[u],进入更贵的桶
if (f < cap && cost + price[u] < dist[u][f + 1]) {
dist[u][f + 1] = cost + price[u];
bucket[(cost + price[u]) % mod].push_back(encode_state(u, f + 1));
pending++;
}
// 开车去相邻城市:油够才走,油量减少、花费不变
for (int i = head[u]; i != 0; i = nxt[i]) {
int v = to[i];
int nf = f - wt[i];
if (nf >= 0 && cost < dist[v][nf]) {
dist[v][nf] = cost;
bucket[cost % mod].push_back(encode_state(v, nf));
pending++;
}
}
}
cost++;
}
return INF;
}
void solve() {
cin >> n >> m;
for (int u = 0; u < n; u++) {
cin >> price[u];
}
for (int i = 0; i < m; i++) {
int u, v, d;
cin >> u >> v >> d;
add_edge(u, v, d);
add_edge(v, u, d); // 无向图加两条反向边
}
cin >> q;
for (int i = 0; i < q; i++) {
cin >> cap >> start_city >> target_city;
int ans = dial_shortest();
if (ans == INF) {
cout << "impossible" << '\n';
} else {
cout << ans << '\n';
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
solve();
return 0;
}复杂度
- 每个询问:状态
、转移 ,桶队列近似线性于状态数与转移数之和。 - 总复杂度
,约 (C++ 轻松通过;Python 常数大,在 1s 限制下可能超时,属允许范围)。 - 空间
: dist表与桶。
总结
把"油量"升级成状态维度,购车钱最省问题变成标准分层图最短路;再利用"加油正权有界、开车零权"的权值结构,用 Dial 桶队列代替堆。核心一句话:状态
图示解析
状态图视角
flowchart TB
subgraph layer0["油量层 f=0"]
S0["(S,0) 花费 0"]
end
subgraph layer1["油量层 f=1"]
S1["(u,f+1) 花费 +p_u"]
end
subgraph layerk["开车不换钱,只换城市与油量"]
D["(v, f−d) 花费不变"]
end
S0 -->|"+p_u 加油"| S1
S1 -->|"+0 开车(同桶扩展)"| D
D -->|"+p_u 在便宜城市补油"| S1
- 加油边只在同一列(同一城市)内向上走一格,权值为该城油价;
- 开车边跨越城市、油量下降,权值为 0;
- "绕路买便宜油"在图上表现为:先沿便宜城市的加油边爬升油量层,再横跨开车边消耗。
这份图和正解思路中的转移表共同说明:本题难点不在图论算法本身,而在识别出资源约束可以作为图的第二维,以及利用权值结构选择更快的队列实现。