题单 · 习题解析

洛谷 P1073 [NOIP 2009 提高组] 最优贸易

★ 状态 = 「在哪儿 + 贸易做到哪一步」⇒ 三层图;★★★ 而让它落进 Dijkstra 射程的那一步是把买入记 `p`、卖出记 `100 − p` ⇒ 总权 = 100 − 利润、**全非负** —— 那个 100 来自题面最后一行;★★★ 「正反两遍」是对的,可**用 Dijkstra 的贪心去做它就错**(最小反例 4 个点),⚠ 而 minBuy 错推不出答案错(DAG 档 300 轮一次没错);⚠ 「50% 的数据无环」正是给拓扑 DP 留的分;★★★ 「存一遍还是两遍」演到第四次 —— 这次同一张输入里 z 说了算

原题:洛谷 P1073出自 第 32 章 最短路一:Dijkstra 的题单题面本地存档:2026-08-30
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P1073,日期见页头。两边不一致时信原站。

题目背景

本题原题数据极弱,Subtask 0 中的测试点为原题测试点,Subtask 1 中的测试点为 Hack 数据。

题目描述

C 国有 n 个大城市和 m 条道路,每条道路连接这 n 个城市中的某两个城市。 任意两个城市之间最多只有一条道路直接相连。m 条道路中有一部分为单向通行的道路,一部分为双向通行的道路, 双向通行的道路在统计条数时也计为 1 条。

C 国幅员辽阔,各地的资源分布情况各不相同,这就导致了同一种商品在不同城市的价格不一定相同。 但是,同一种商品在同一个城市的买入价和卖出价始终是相同的。

商人阿龙来到 C 国旅游。当他得知同一种商品在不同城市的价格可能会不同这一信息之后, 便决定在旅游的同时,利用商品在不同城市中的差价赚回一点旅费。 设 C 国 n 个城市的标号从 1 ~ n,阿龙决定从 1 号城市出发,并最终在 n 号城市结束自己的旅行。 在旅游的过程中,任何城市可以重复经过多次,但不要求经过所有 n 个城市。 阿龙通过这样的贸易方式赚取旅费:他会选择一个经过的城市买入他最喜欢的商品――水晶球, 并在之后经过的另一个城市卖出这个水晶球,用赚取的差价当做旅费。 由于阿龙主要是来 C 国旅游,他决定这个贸易只进行最多一次, 当然,在赚不到差价的情况下他就无需进行贸易。

假设 C 国有 5 个大城市,城市的编号和道路连接情况如下图, 单向箭头表示这条道路为单向通行,双向箭头表示这条道路为双向通行。

P1073 样例的那张 5 个城市的图

假设 1 ~ n 号城市的水晶球价格分别为 4, 3, 5, 6, 1

阿龙可以选择如下一条线路:1 → 2 → 3 → 5,并在 2 号城市以 3 的价格买入水晶球, 在 3 号城市以 5 的价格卖出水晶球,赚取的旅费数为 2。

阿龙也可以选择如下一条线路:1 → 4 → 5 → 4 → 5,并在第 1 次到达 5 号城市时以 1 的价格买入水晶球, 在第 2 次到达 4 号城市时以 6 的价格卖出水晶球,赚取的旅费数为 5。

现在给出 n 个城市的水晶球价格,m 条道路的信息。请你告诉阿龙,他最多能赚取多少旅费。

输入格式

第一行包含 2 个正整数 nm,分别表示城市的数目和道路的数目。

第二行 n 个正整数,按标号顺序分别表示这 n 个城市的商品价格。

接下来 m 行,每行有 3 个正整数 x, y, z。 如果 z = 1,表示这条道路是城市 x 到城市 y单向道路; 如果 z = 2,表示这条道路为城市 x 和城市 y 之间的双向道路。

输出格式

一个整数,表示最多能赚取的旅费。如果没有进行贸易,则输出 0。

说明/提示

【数据范围】

输入数据保证 1 号城市可以到达 n 号城市

对于 10% 的数据,1 ≤ n ≤ 6。 对于 30% 的数据,1 ≤ n ≤ 100对于 50% 的数据,不存在一条旅游路线,可以从一个城市出发,再回到这个城市。 对于 100% 的数据,1 ≤ n ≤ 1000001 ≤ m ≤ 5000001 ≤ x, y ≤ n1 ≤ z ≤ 2

水晶球价格 ≤ 100。

NOIP 2009 提高组 第三题。时限 1 秒,内存 128 MB。

输入输出样例

输入

5 5
4 3 5 6 1
1 2 1
1 4 1
2 3 2
3 5 1
4 5 2

输出

5

最优是 1 → 4 → 5 → 4 → 5:在 5 号以 1 买、回到 4 号以 6 卖,赚 5。 ⚠ 注意这条线路把 4、5 号各走了两遍 —— 这道题的图不是一条路,是可以来回绕的。

1★ 关键的一步(上半):状态不是「在哪儿」,是「在哪儿 + 贸易做到哪一步」

★ 把图复制成三层
含义
第 0 层 还没买
第 1 层 买了,还没卖
第 2 层 已经卖了
  • 层内:原图的边,边权 0(走路不花钱);
  • 第 0 层的 i → 第 1 层的 i在 i 买入
  • 第 1 层的 i → 第 2 层的 i在 i 卖出

答案就是「从 (1, 第 0 层) 走到 (n, 第 2 层)」的最优值。 ★ 「可以重复经过城市」这件事一点额外代价都没有 —— 分层图上它就是普通的走边。

2★★★ 关键的一步(下半):怎么把「求最大利润」变成非负权最短路

顺手的记法是「买入 −p、卖出 +p,求最长路」—— 那就不是 Dijkstra 了(有负权,本章第 9 步整节都在讲这件事)。

★★★ 换一个记法,两条边权同时变成非负
买入那条边的权 =  p[i]          (0 ≤ p ≤ 100)
卖出那条边的权 =  100 − p[i]    (0 ≤ … ≤ 100)

一条完整路线的总权 = 买价 + (100 − 卖价) = 100 − 利润

最小化它就是最大化利润,而所有边权都 ≥ 0 —— 本章的 Dijkstra 原样能用。

★ 那个 100 是从哪儿来的?题面最后一行:「水晶球价格 ≤ 100」。 ⇒ 又一次「题面上那几行数字,每一行都是一件工具」—— 这一行不是背景,它是让这道题落进 Dijkstra 射程的那把钥匙。

p1073.cpp★ 这一版就能 AC(分层图 + Dijkstra)
// P1073 [NOIP 2009 提高组] 最优贸易 —— ★ 这一版就能 AC(分层图 + 本章的 Dijkstra)
//
// 题目要的是:从 1 走到 n(点可以重复经过),中途选一个城市买、之后某个城市卖,
// 让「卖价 − 买价」最大。
//
// ★ 关键的一步分两半。
//
// 【一】**状态是「走到哪儿」+「贸易做到哪一步」** ⇒ 把图复制成三层:
// 第 0 层 = 还没买 第 1 层 = 买了还没卖 第 2 层 = 已经卖了
// · 层内:原图的边,边权 0(走路不花钱);
// · 第 0 层的 i → 第 1 层的 i:在 i 买入;
// · 第 1 层的 i → 第 2 层的 i:在 i 卖出。
// 答案就是「从 (1, 第 0 层) 走到 (n, 第 2 层)」的最优值。
//
// 【二】★★★ 而这道题真正的机关是:**怎么把「求最大利润」变成非负权最短路。**
// 顺手的做法是买入 −p、卖出 +p 再求最长路 —— 那就**不是 Dijkstra 了**(有负权)。
// 换个记法:
// 买入那条边的权 = p[i] (0 ≤ p ≤ 100)
// 卖出那条边的权 = 100 − p[i] (0 ≤ … ≤ 100)★ 也非负
// 于是一条完整路线的总权 = 买价 + (100 − 卖价) = **100 − 利润**。
// ⇒ **最小化它就是最大化利润**,而所有边权都 ≥ 0 —— 本章的 Dijkstra 原样能用。
//
// ★ 那个 100 是从哪儿来的?题面最后一行:「**水晶球价格 ≤ 100**」。
// ⇒ 又一次「题面上那几行数字,每一行都是一件工具」——
// 这一行不是背景,它是**让这道题落进 Dijkstra 射程**的那把钥匙。
//
// ⚠ 「不做贸易」也是允许的:在同一个城市买了立刻卖,利润 0;
// 分层图里就是 i 的三层顺着走一遍,权 = p + (100 − p) = 100 ⇒ 利润 0。所以答案天然 ≥ 0。
//
// 复杂度 O(3m log(3n)):顶格 n = 10⁵、m ≈ 5×10⁵ ⇒ 本机 0.28 秒(时限 1 秒;
// 而正反两遍那版 0.14 秒 —— 分层图的代价是常数 3,换来的是「状态」这件事说得清)。
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int INF = 0x3f3f3f3f;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
vector<int> p(n + 1);
for (int i = 1; i <= n; i++) cin >> p[i];
int N = 3 * n; // 三层,(层, 点) 编号成 层*n + 点
auto id = [&](int layer, int v) { return layer * n + v; };
vector<vector<PII>> g(N + 1);
for (int i = 0; i < m; i++) {
int x, y, z;
cin >> x >> y >> z;
for (int L = 0; L < 3; L++) { // 三层里各连一遍,边权 0
g[id(L, x)].push_back({id(L, y), 0});
if (z == 2) g[id(L, y)].push_back({id(L, x), 0});
}
}
for (int v = 1; v <= n; v++) {
g[id(0, v)].push_back({id(1, v), p[v]}); // ★ 买入:花 p[v]
g[id(1, v)].push_back({id(2, v), 100 - p[v]}); // ★ 卖出:花 100 − p[v]
}
vector<int> dist(N + 1, INF);
priority_queue<PII, vector<PII>, greater<PII>> q;
int s = id(0, 1);
dist[s] = 0;
q.push({0, s});
while (!q.empty()) {
auto [d, u] = q.top();
q.pop();
if (d > dist[u]) continue;
for (auto [v, w] : g[u])
if (d + w < dist[v]) { dist[v] = d + w; q.push({dist[v], v}); }
}
int t = dist[id(2, n)];
cout << (t >= INF ? 0 : 100 - t) << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

⚠ 「不做贸易」也是允许的:在同一个城市买了立刻卖,权 = p + (100 − p) = 100 ⇒ 利润 0。 所以答案天然 ≥ 0,不用另写一句 max(0, …)

本机实测(A 机 · WSL2 · 6.18.33.2-microsoft-standard-WSL2 · 8 线程 / 7 GB · 2026-08-30 · 独占;./p1073Gen 1 4 100000 500000,顶格 n = 10⁵m ≈ 5 × 10⁵): 分层图 0.28 秒,正反两遍那版 0.14 秒(时限 1 秒)。 ⇒ 分层图的代价是常数 3;换来的是 「状态」这件事说得清 这一点。

3★★★ 第一个错法:题单说的「正反两遍」是对的 —— 可用 Dijkstra 的贪心去做它就错了

题单给的另一条路是「正反两遍最短路」:

minBuy[v]  = 从 1 走到 v 的所有路线上,见过的最低价
maxSell[v] = 从 v 走到 n 的所有路线上,见过的最高价
答案 = max over v (maxSell[v] − minBuy[v])

这条路是对的(本页留了一份,300 轮和分层图逐字节相同)。 可它不能用 Dijkstra 的骨架去实现。

★★★ Dijkstra 成立靠的那句话,在这儿正好反过来了

本章第 5 步那三句反证,落脚点是一句话:

「那条路走到 x 之后还要往前走,而往前走只会更长(边长非负)。」

⇒ 所以「当前最小的那个」不可能再被刷小,可以当场定死

而这道题的 minBuy 是「路上的最低价」—— 往前多走一步只会让它更小。 那个单调性正好反过来 ⇒ 「弹出即定死」的理由整个不成立。

p1073DijMin.cpp✗ 正反两遍,但用 Dijkstra 的贪心(样例照样打 5)
// ✗ 错法①:正反两遍是对的,可**用 Dijkstra 的贪心去做它**(弹出即定死)
//
// ★★★ 这是这一页最值钱的一条:**Dijkstra 的骨架不是随便能套的。**
//
// 本章第 5 步那三句反证,靠的是一句话:
// 「那条路走到 x 之后还要往前走,而**往前走只会更长**(边长非负)。」
// ⇒ 所以「当前最小的那个」不可能再被刷小,可以当场定死。
//
// 而这道题的 minBuy 是「路上的**最低价**」—— **往前多走一步只会让它更小**。
// 那个单调性**正好反过来了** ⇒ 弹出即定死的理由整个不成立。
//
// ⚠ 最小反例只要 4 个点(度量程序第 ③ 段真跑了一遍):
// 价格 10 8 1 7,边 1 → 2、2 → 3、3 → 4、1 → 4
// · 真正的 minBuy[4] = 1(走 1 → 2 → 3 → 4)
// · 贪心:弹出 1(10)⇒ 4 的暂定值 min(10, 7) = 7、2 的暂定值 8
// ⇒ 堆顶是 (7, 4),**4 号被带着 7 定死了**,而 3 号(价 1)还没轮到。
//
// ⚠⚠ 而「minBuy 算错了」推不出「**答案**一定错」—— 这两件事这一页分开量了(正文第 ③ 步)。
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
int main(int argc, char** argv) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
bool showMin = (argc > 1 && string(argv[1]) == "minbuy"); // 只打 minBuy[],给反例用
int n, m;
if (!(cin >> n >> m)) return 0;
vector<int> p(n + 1);
for (int i = 1; i <= n; i++) cin >> p[i];
vector<vector<int>> g(n + 1), rg(n + 1);
for (int i = 0; i < m; i++) {
int x, y, z;
cin >> x >> y >> z;
g[x].push_back(y);
rg[y].push_back(x);
if (z == 2) { g[y].push_back(x); rg[x].push_back(y); }
}
const int INF = 0x3f3f3f3f;
/* ⚠ 小根堆 + 「弹出即定死」—— 照搬 Dijkstra 的骨架 */
vector<int> mn(n + 1, INF), done(n + 1, 0);
priority_queue<PII, vector<PII>, greater<PII>> q;
mn[1] = p[1];
q.push({mn[1], 1});
while (!q.empty()) {
auto [d, u] = q.top();
q.pop();
if (done[u]) continue;
done[u] = 1; // ⚠ 就是这一句:定死了就不许再改
for (int v : g[u]) {
int cand = min(d, p[v]);
if (!done[v] && cand < mn[v]) { mn[v] = cand; q.push({cand, v}); }
}
}
if (showMin) {
for (int i = 1; i <= n; i++) printf("%d%c", mn[i] >= INF ? -1 : mn[i], " \n"[i == n]);
return 0;
}
/* 反着那一遍同样用大根堆 + 弹出即定死 */
vector<int> mx(n + 1, -1), done2(n + 1, 0);
priority_queue<PII> q2;
mx[n] = p[n];
q2.push({mx[n], n});
while (!q2.empty()) {
auto [d, u] = q2.top();
q2.pop();
if (done2[u]) continue;
done2[u] = 1;
for (int v : rg[u]) {
int cand = max(d, p[v]);
if (!done2[v] && cand > mx[v]) { mx[v] = cand; q2.push({cand, v}); }
}
}
int ans = 0;
for (int v = 1; v <= n; v++)
if (mn[v] < INF && mx[v] >= 0) ans = max(ans, mx[v] - mn[v]);
cout << ans << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

最小反例只要 4 个点(价格 10 8 1 7,边 1→22→33→41→4):

minBuy[1] [2] [3] [4]
真值(反复松弛) 10 8 1 1
Dijkstra 的贪心 10 8 1 7

弹出 1 号(10)之后,4 号的暂定值是 min(10, 7) = 7、2 号是 8 ⇒ 堆顶是 (7, 4),4 号就带着 7 被定死了,而价格只有 1 的 3 号还没轮到。

300 轮 默认档 全单向档 多环档 DAG 档
「用 Dijkstra 的贪心」被抓 38 32 25 0
⚠ 而「minBuy 算错了」推不出「答案一定错」—— 这两件事得分开量

上面那个 4 点反例是一张 DAG,它把 minBuy[4] 算错了 —— 可那一轮的最终答案仍然是对的(另一个点上的 maxSell − minBuy 补了回来)。 实测 DAG 档 300 轮,它的答案一次都没错

⇒ ★★ 这正是第 28 章 P3959 那条: 「它算的是另一个量」推不出「它一定和正解不同」 —— 两个不同的量会在某些输入上取到同一个值,而那个「某些」只能数出来。

p1073Spfa.cpp★ 同一条路,改成反复松弛到不动 —— 这一版是对的

4⚠ 第二个错法:假设图无环,按拓扑序 DP —— 而题面替它留了 50 分

p1073Dag.cpp✗ 拓扑序 DP(官方样例当场打 0)
// ✗ 错法②:假设这张图**无环**,按拓扑序做一遍 DP
//
// 想法很自然:从 1 出发的路线是「一步一步往前走」,那就按拓扑序推
// minBuy[v] = min(p[v], min over u→v of minBuy[u])
// 一遍扫完,`O(n + m)`,比什么都快。
//
// ⚠ 而题面里那一行就是冲着它写的:
// 「**对于 50% 的数据,不存在一条旅游路线,可以从一个城市出发,再回到这个城市。**」
// —— 出题人明说了:**有一半的数据是 DAG**。这一版稳拿 50 分。
// ⇒ 又一次「[题面的数据范围分档是出题人递过来的工具](/sol/p1141/)」:
// 那一档不是提示,是**给这个写法留的分**。
//
// ⚠ 而题面样例里就有环(`2 3 2` 和 `4 5 2` 都是双向的,双向边本身就是一个二元环)
// ⇒ 环上的点进不了拓扑序,这一版直接把它们当成走不到。
//
// ★ 方向可判:它**少算**了一批路线 ⇒ 利润只会更小 ⇒ **答案恒 ≤ 正解**。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
vector<int> p(n + 1);
for (int i = 1; i <= n; i++) cin >> p[i];
vector<vector<int>> g(n + 1), rg(n + 1);
vector<int> deg(n + 1, 0), rdeg(n + 1, 0);
for (int i = 0; i < m; i++) {
int x, y, z;
cin >> x >> y >> z;
g[x].push_back(y); rg[y].push_back(x); deg[y]++; rdeg[x]++;
if (z == 2) { g[y].push_back(x); rg[x].push_back(y); deg[x]++; rdeg[y]++; }
}
const int INF = 0x3f3f3f3f;
vector<int> mn(n + 1, INF), mx(n + 1, -1);
mn[1] = p[1];
{ // ⚠ 正着一遍拓扑序
queue<int> q;
for (int i = 1; i <= n; i++) if (!deg[i]) q.push(i);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : g[u]) {
if (mn[u] < INF) mn[v] = min(mn[v], min(mn[u], p[v]));
if (--deg[v] == 0) q.push(v);
}
}
}
mx[n] = p[n];
{ // ⚠ 反着一遍拓扑序
queue<int> q;
for (int i = 1; i <= n; i++) if (!rdeg[i]) q.push(i);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : rg[u]) {
if (mx[u] >= 0) mx[v] = max(mx[v], max(mx[u], p[v]));
if (--rdeg[v] == 0) q.push(v);
}
}
}
int ans = 0;
for (int v = 1; v <= n; v++)
if (mn[v] < INF && mx[v] >= 0) ans = max(ans, mx[v] - mn[v]);
cout << ans << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 题面那一行数据范围就是给它的

对于 50% 的数据,不存在一条旅游路线,可以从一个城市出发,再回到这个城市。

—— 出题人明说了:有一半的数据是 DAG。这一版稳拿 50 分。

⇒ 又一次「题面的数据范围分档是出题人递过来的工具」: 那一档不是提示,是给这个写法留的分。 ★ 而读的时候要问一句「这一档的主语是谁」—— 别的四档说的是 n, 唯独这一档说的是图的形状

300 轮 默认档 多环档 ★ DAG 档
「拓扑序 DP」被抓 270 242 精确的 0

★ 方向可判:它少算了一批路线 ⇒ 利润只会更小 ⇒ 答案恒 ≤ 正解(300 轮比正解大的是 0)。 ⚠ 而官方样例当场就挡住它(打 0)—— 因为样例里的双向边本身就是一个二元环。

5★★★ 第三个错法:把 z = 2 的双向道路也当成单向

p1073Dir.cpp✗ z 读进来了但没人用它(样例打 2)
// ✗ 错法③:`z = 2` 那些**双向**道路,只当成 x → y 一个方向
//
// 题面:「如果 `z = 1`,表示这条道路是城市 x 到城市 y 的单向道路;
// 如果 `z = 2`,表示这条道路为城市 x 和城市 y 之间的**双向**道路。」
//
// ⇒ ★★★ 「存一遍还是两遍」在这一章的题单里出现了**第四次**,而且这次是**同一张输入里两种都有**:
// [P1339](/sol/p1339/) 全无向 —— 必须存两遍;
// [P1629](/sol/p1629/) 全单向 —— 存两遍就错;
// [P1462](/sol/p1462/) 全双向 —— 又必须存两遍;
// P1073(这道题)—— ★ **同一行输入里,`z` 说了算。**
//
// ★ 方向可判:边少了 ⇒ 路线只会更少 ⇒ **答案恒 ≤ 正解**。
// ⚠ 而它在「全是单向边」那一档上是**结构性的 0**(那一档根本没有 z = 2)。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
vector<int> p(n + 1);
for (int i = 1; i <= n; i++) cin >> p[i];
vector<vector<int>> g(n + 1), rg(n + 1);
for (int i = 0; i < m; i++) {
int x, y, z;
cin >> x >> y >> z;
g[x].push_back(y);
rg[y].push_back(x);
(void)z; // ⚠ z 读进来了,但没人用它
}
const int INF = 0x3f3f3f3f;
vector<int> mn(n + 1, INF), mx(n + 1, -1);
vector<char> inq(n + 1, 0);
deque<int> q;
mn[1] = p[1]; q.push_back(1); inq[1] = 1;
while (!q.empty()) {
int u = q.front(); q.pop_front(); inq[u] = 0;
for (int v : g[u]) {
int cand = min(mn[u], p[v]);
if (cand < mn[v]) { mn[v] = cand; if (!inq[v]) { inq[v] = 1; q.push_back(v); } }
}
}
fill(inq.begin(), inq.end(), 0);
mx[n] = p[n]; q.push_back(n); inq[n] = 1;
while (!q.empty()) {
int u = q.front(); q.pop_front(); inq[u] = 0;
for (int v : rg[u]) {
int cand = max(mx[u], p[v]);
if (cand > mx[v]) { mx[v] = cand; if (!inq[v]) { inq[v] = 1; q.push_back(v); } }
}
}
int ans = 0;
for (int v = 1; v <= n; v++)
if (mn[v] < INF && mx[v] >= 0) ans = max(ans, mx[v] - mn[v]);
cout << ans << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 「存一遍还是两遍」在这一张题单里出现了第四次 —— 而这次同一张输入里两种都有
题面怎么说 g[v].push_back(...) 那一行
P1339 无向图 必须写
P1629 「所有的道路都是单行的」 写了当场就错
P1462 m双向的公路」 又必须写
P1073(这道题) z = 1 单向、z = 2 双向 ★★ 同一张输入里,z 说了算

它从来不是一个能背的习惯,是每道题(这道题是每条边)读一次题面的事。 本书那条「上一章的正确写法就是这一章的 bug」, 在同一张题单里演到了第四次,而这一次连「一道题一个答案」都不成立了。

300 轮 默认档 ★ 全是单向边那一档
「双向当单向」被抓 80 精确的 0

★ 方向同样可判:边少了 ⇒ 路线只会更少 ⇒ 答案恒 ≤ 正解(比正解大的是 0)。 ★ 而那个 0 是结构性的:那一档里根本没有 z = 2,两版做的是同一件事。

6★ 对拍这一页

参照物照题面的定义硬算:传递闭包算出「谁能走到谁」, 再枚举买入城市 u、卖出城市 v,只要 1 → u → v → n 都通就是一对合法买卖。

p1073Brute.cpp参照物:传递闭包 + 枚举买卖两点(300 轮不一致 0 轮)
300 轮(n 随机 5~9,照题面保证 1 能到 n、且无重边) 默认档 换一档
分层图 ≡ 暴力 不一致 0 轮 ——
正反两遍(反复松弛)≡ 暴力 不一致 0 轮 ——
正反两遍但用 Dijkstra 的贪心 38 DAG 档 ★ 0
拓扑序 DP 270(★ 恒 ≤ 正解) DAG 档 ★ 0
双向当单向 80(★ 恒 ≤ 正解) 全单向档 ★ 0
⚠ 这一档答案本来就是 0 的轮数 3 ——
⚠ 生成器要亲手做到题面那两句保证

题面保证了两件事,随手写的生成器两件都不会自动成立

  • 输入数据保证 1 号城市可以到达 n 号城市」⇒ 先按一个随机排列拉一条 1 → … → n 的单向路径;
  • 任意两个城市之间最多只有一条道路直接相连」⇒ 用一个 set 挡掉重边。

★ 这是这一章里第三次干同一件事(P1339 保证连通、P1629 造单向环 保证强连通):「题面保证了什么」得由生成器亲手做到, 否则对拍比的是两个都没定义的东西。

7度量程序和生成器

p1073Count.cpp度量程序(本页所有数字都出自它)
p1073Gen.cpp(五个档位)数据生成器

8一页纸

★ 状态 「在哪儿 + 贸易做到哪一步」⇒ 三层图;「城市可以重复经过」在分层图上零代价
★★★ 让它落进 Dijkstra 射程的那一步 买入记 p、卖出记 100 − p ⇒ 总权 = 100 − 利润所有边权非负;那个 100 来自题面最后一行「价格 ≤ 100」
★★★ 用 Dijkstra 的贪心做「正反两遍」 错的 —— 往前走只会让 minBuy 更小,「弹出即定死」的理由反过来了;最小反例 4 个点(minBuy[4] 该是 1,它给 7)
⚠ 而 minBuy 错 ≠ 答案错 DAG 档 300 轮它的答案一次没错第 28 章 P3959 那条)
⚠ 拓扑序 DP 被抓 270 / 300,恒 ≤ 正解;⚠ 而题面那句「50% 的数据无环」正是给它留的分,DAG 档精确的 0
★★★ 双向边 「存一遍还是两遍」在这张题单里演到第四次,而这次同一张输入里 z 说了算