用正反两次可达性筛出既能从 1 到达、又能走到 n 的可行城市,再按价格 1→100 分层多源传播求出每个卖点的最低买价,取卖价减最低买价的最大值。
OJ: roj
题目 ID: 3574
难度:普及+/提高-
标签:图论可达性多源BFSpython
创建: 2026-10-02 08:28
更新: 2026-10-10 11:30
形式化题目
城市
求一条从 1 出发、到
样例(
flowchart LR
c1["城市 1<br/>价格 4"] --> c2["城市 2<br/>价格 3"]
c1 --> c4["城市 4<br/>价格 6"]
c2 --> c3["城市 3<br/>价格 5"]
c3 --> c2
c3 --> c5["城市 5<br/>价格 1"]
c4 --> c5
c5 --> c4
图中
正解
思路
第一步:朴素做法,瓶颈在可达性。 枚举买点
第二步:刻画可行的买卖对。 记
- 存在合法行走
,当且仅当 、 且 。必要性直接从行走上读:行走从 1 出发,故 ;从 沿行走到 再到 ,故 、 。充分性把三段拼起来即可。 - 顺带得到一个关键结论:这样的行走上任何中间点
都满足 ( 在 之后)与 ( 在 之前),即买点到卖点的路全程落在 内——不存在"必须先绕出可行区才能抄到低价"的暗坑。
第三步:二维枚举压成一维查询。 固定卖点
取到最小值的买点
第四步:floor 是环上的不动点。 对
第五步:按价格分层的多源传播。 floor 问的是"能到达
两个关键论证,缺一不可:
- 被更低价覆盖的城市不再当源点,不会丢解。 若价格
的 城市 已被 层覆盖,说明存在价格 的源点 ;于是 能到达的任何城市, 也能到达—— 作买点处处被 的更低价支配,跳过它不会改变任何 floor。 - 传播不会越出
, from1只需在选源点时检查。 源点都在,而 对出边封闭:能从 1 到达的点,它的后继仍能从 1 到达。所以扩散途中遇到的点天然属于 ,途中不必再做可达性判断。
低层先跑,高层只把仍是
样例上的传播过程(
| 价格层 |
桶内城市 | 处理结果 |
|---|---|---|
| 1 | 5 | 5 未被覆盖,当源点;沿出边覆盖 5, 4, 1, 2, 3,floor 全记 1 |
| 3 | 2 | floor[2] = 1 已被更低层覆盖,跳过 |
| 4 | 1 | floor[1] = 1 已被覆盖,跳过 |
| 5 | 3 | floor[3] = 1 已被覆盖,跳过 |
| 6 | 4 | floor[4] = 1 已被覆盖,跳过 |
表格第一列是值域桶的处理顺序,第三列是该层结束后的覆盖增量。要观察两件事:① 价格 1 的 5 号城一次扩散就覆盖了全图(
最后一步:收尾查询。 to_n[s] and floor[s] < INF:floor[s] < INF 恰好等价于 to_n[s] 是
代码
/**
* 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 int INF = 1000000000;
int n, m;
int price[100005];
vector<int> out[100005], rev[100005];
bool from1[100005], to_n[100005];
int floor_[100005];
// 沿邻接表标出从 start 出发能到达的全部城市
void walk(int start, vector<int> g[], bool seen[]) {
seen[start] = true;
vector<int> stack;
stack.push_back(start);
while (!stack.empty()) {
int u = stack.back(); stack.pop_back();
for (size_t i = 0; i < g[u].size(); i++) {
int v = g[u][i];
if (!seen[v]) { seen[v] = true; stack.push_back(v); }
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> price[i];
for (int i = 0; i < m; i++) {
int x, y, z;
cin >> x >> y >> z;
out[x].push_back(y);
rev[y].push_back(x);
if (z == 2) {
out[y].push_back(x);
rev[x].push_back(y);
}
}
for (int i = 1; i <= n; i++) { from1[i] = false; to_n[i] = false; floor_[i] = INF; }
walk(1, out, from1);
walk(n, rev, to_n);
// 按价格 1..100 分层多源传播:floor_[u] = 能到达 u 的可买入城市最低价
vector<vector<int> > by_price(101);
for (int u = 1; u <= n; u++) by_price[price[u]].push_back(u);
for (int cost_ = 1; cost_ <= 100; cost_++) {
for (size_t i = 0; i < by_price[cost_].size(); i++) {
int src = by_price[cost_][i];
if (!from1[src] || floor_[src] <= cost_) continue;
floor_[src] = cost_;
vector<int> stack;
stack.push_back(src);
while (!stack.empty()) {
int u = stack.back(); stack.pop_back();
for (size_t t = 0; t < out[u].size(); t++) {
int v = out[u][t];
if (floor_[v] > cost_) {
floor_[v] = cost_;
stack.push_back(v);
}
}
}
}
}
int ans = 0;
for (int s = 1; s <= n; s++)
if (to_n[s] && floor_[s] < INF)
ans = max(ans, price[s] - floor_[s]);
cout << ans << "\n";
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 08:28
# update_at: 2026-10-02 08:28
import sys
INF = 10**9 # 没有任何买点能到达时的哨兵(价格上界只有 100)
def walk(start: int, adj: list[list[int]]) -> list[bool]:
"""沿邻接表标出从 start 出发能到达的全部城市(迭代遍历,避免深递归)。"""
seen = [False] * len(adj)
seen[start] = True
stack = [start]
while stack:
u = stack.pop()
for v in adj[u]:
if not seen[v]:
seen[v] = True
stack.append(v)
return seen
def buy_floor(price: list[int], out: list[list[int]], from1: list[bool]) -> list[int]:
"""buy_floor[u] = 能到达 u 的可买入城市中的最低价;无买点到达则保持 INF。
按价格 1→100 逐层做多源传播:本层中"属于 A(from1)且尚未被更低价覆盖"
的城市成为买点,沿出边把 floor 填成本层价格。低层先跑,所以高层只填 INF,
每个城市至多入栈一次、每条出边至多扫描一次。
"""
by_price: list[list[int]] = [[] for _ in range(101)] # 价格 1~100,按价分桶
for u in range(1, len(price)):
by_price[price[u]].append(u)
floor = [INF] * len(price)
for cost in range(1, 101):
for src in by_price[cost]:
if not from1[src] or floor[src] <= cost: # 买不到的城市 / 已被更便宜买点覆盖
continue
floor[src] = cost
stack = [src]
while stack:
u = stack.pop()
for v in out[u]:
if floor[v] > cost: # 还没有任何买点到达
floor[v] = cost
stack.append(v)
return floor
def solve() -> None:
readline = sys.stdin.buffer.readline # 逐行读入,不一次性 split 全文(省一份 token 列表的内存)
n, m = map(int, readline().split())
price = [0] + list(map(int, readline().split()))
out: list[list[int]] = [[] for _ in range(n + 1)] # 正向出边
rev: list[list[int]] = [[] for _ in range(n + 1)] # 反向出边(原道路的入边)
for _ in range(m):
x, y, z = map(int, readline().split())
out[x].append(y)
rev[y].append(x)
if z == 2: # 双向道路拆成两条单向边
out[y].append(x)
rev[x].append(y)
from1 = walk(1, out) # 买入城市必须能从 1 出发到达
to_n = walk(n, rev) # 卖出城市必须还能走到 n
floor = buy_floor(price, out, from1)
# 卖点 s 合法 ⇔ s 能走到 n 且有买点能到达 s;此时 floor[s] <= price[s],答案天然非负
ans = max(price[s] - floor[s] for s in range(1, n + 1) if to_n[s] and floor[s] < INF)
print(ans)
if __name__ == "__main__":
solve()代码与推导一一对应:walk 被调用两次,分别沿正向边和反向边求出 from1)与 to_n);buy_floor 的 for cost in range(1, 101) 就是值域分层,not from1[src] or floor[src] <= cost 同时落实"源点必须属于 floor[v] > cost 是"该点尚无买点到达"的覆盖判定,保证每城每边只处理一次;末行 to_n[s] and floor[s] < INF 即
复杂度
- 时间
:建两张邻接表 ;两次遍历各 ;按价分桶 ;分层传播中每城入栈、每条出边扫描各至多一次, ;取答案 。与 Tarjan SCC + DAG DP 的标准正解同阶——价格上界只贡献分桶的 ,不进入内层循环。实测数据仓 10 组真实数据全部 PASS,最慢单点 0.075s(OJ 限时 1000ms)。 - 空间
:两张 list[list[int]]邻接表与若干数组。实测峰值 RSS 约 37MB,略高于题面 32MB 的内存限制——超出部分主要是 CPython 解释器基线(约 15MB)加 Python 对象开销,本仓库允许 Python 侧 TLE/MLE;同一算法换成 C++ 前向星只需几 MB,若必须压进 32MB,把邻接表换成 array('i')的 CSR 即可,算法与结构不变。
总结
整条路线是三次化归:先用正、反各一次可达性把"能不能买、能不能卖"筛成
图示解析
下图汇总 main.py 的数据流:两张邻接表各自服务哪一步、两个可达性集合在哪里合流。
flowchart LR
inp["读入价格与道路"] --> adj["拆双向边<br/>建正向/反向邻接表"]
adj --> fa["walk(1, out)<br/>A:能从 1 到达"]
adj --> fb["walk(n, rev)<br/>B:能走到 n"]
fa --> prop["buy_floor<br/>价格 1→100 分层传播"]
fb --> ans["max(p_s − floor[s])<br/>s ∈ A ∩ B"]
prop --> ans
正向邻接表承担两件事——从 1 出发的可达性、以及传播时的扩散方向;反向邻接表只回答"能否走到 n"。两个集合在最后一步合流,只有 walk / buy_floor / 末行查询的边界。
