0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1967,日期见页头。两边不一致时信原站。
题目背景:NOIP2013 提高组 D1T3
题目描述
A 国有 n 座城市,编号从 1 到 n,城市之间有 m 条双向道路。
每一条道路对车辆都有重量限制,简称限重。
现在有 q 辆货车在运输货物,司机们想知道每辆车在不超过车辆限重的情况下,最多能运多重的货物。
输入格式
第一行有两个用一个空格隔开的整数 n, m,表示 A 国有 n 座城市和 m 条道路。
接下来 m 行每行三个整数 x, y, z,表示从 x 号城市到 y 号城市有一条限重为 z 的道路。
注意:x ≠ y,两座城市之间可能有多条道路。
接下来一行有一个整数 q,表示有 q 辆货车需要运货。
接下来 q 行,每行两个整数 x, y,表示一辆货车需要从 x 城市运输货物到 y 城市,保证 x ≠ y。
输出格式
共有 q 行,每行一个整数,表示对于每一辆货车,它的最大载重是多少。
如果货车不能到达目的地,输出 −1。
数据范围
- 对于 30% 的数据,
1 ≤ n < 1000,1 ≤ m < 10000,1 ≤ q < 1000; - 对于 60% 的数据,
1 ≤ n < 1000,1 ≤ m < 5×10⁴,1 ≤ q < 1000; - 对于 100% 的数据,
1 ≤ n < 10⁴,1 ≤ m < 5×10⁴,1 ≤ q < 3×10⁴,0 ≤ z ≤ 10⁵。
时限 1 秒,内存 131072 KB(128 MiB)。
输入输出样例
输入
4 3 1 2 4 2 3 3 3 1 1 3 1 3 1 4 1 3
输出
3 -1 3
第一辆车 1 → 3:走 1-2-3 的瓶颈是 min(4,3) = 3,走 1-3 的瓶颈是 1 ⇒ 取大的 3。
第二辆车 1 → 4:4 号城市根本没有路(n = 4 但只有三条边,全在 1~3 之间)⇒ −1。
1★★ 这道题立在一句需要证明的话上
题目问的是:从 x 到 y 的所有路径里,让「这条路上最小的那条边」尽量大。
而正解只在一棵树上找路 —— 凭什么?
把边按权值从大到小加(Kruskal 求最大生成树),
x和y第一次连通的那一刻, 用到的那条边就是答案。· 比它更大的边全都已经加过了,而那时
x、y还没连通 ⇒ 不可能有更好的路; · 比它更小的边加不加都改不了「已经连通」⇒ 不会更差。
⇒ 而这句话这一页真去验了(第 ③ 步):拿一条一行代码都不共享的路 (每个询问跑一次「最大瓶颈路」)在五种顶格数据 × 3×10⁴ 个询问上逐字节对了一遍。
★ 于是这道题就是第 34 章(最小生成树)和 第 51 章第 13 步「同一张表,换个东西记」的合体。
// P1967 货车运输 —— 正解:**最大生成树 + 倍增维护路径最小边权**//// ★ 这道题是[第 34 章(最小生成树)](/ch/34-mst/)和[第 51 章第 13 步](/ch/51-lca/)的合体,// 而它立在一句需要证明的话上://// > **在最大生成树上,x 到 y 那条路径的最小边权,就是原图上所有 x→y 路径里// > 「最小边权」能取到的最大值。**//// 一句话的道理:把边按权值**从大到小**加(Kruskal 求最大生成树),// x 和 y 第一次连通的那一刻,用到的最后一条边就是答案 ——// 再小的边加进来也改不了「已经连通」这件事,而更早(更大)的边不足以让它们连通。// ⇒ 解析页第 ③ 步用一条**完全无关的路**(每询问跑一次「最大瓶颈路」)把它验了一遍。//// ⚠⚠ 三处题面写着的坑:// ① **图不保证连通** ⇒ 最大生成**森林**,两点不在同一棵树里就输出 −1;// ② **`0 ≤ z`** ⇒ 答案可以是 **0**,而 0 和 −1 是两件事(别拿 0 当「不通」的标志);// ③ **两座城市之间可能有多条道路**(重边)—— Kruskal 天然不怕,可暴力那边要小心。
#include <bits/stdc++.h>using namespace std;
const int MAXN = 10005;const int MAXM = 50005;const int LOG = 15; // 2^14 = 16384 > 10⁴const int INF = 0x3f3f3f3f;
struct Edge { int u, v, w; };Edge es[MAXM];int fa[MAXN];
int head_[MAXN], nxt_[MAXN * 2], to_[MAXN * 2], wt_[MAXN * 2], ecnt; // ★ 只存生成森林的边 ⇒ 至多 2(n−1) 条int up[MAXN][LOG]; // 祖先int mn[MAXN][LOG]; // ★ 同一张表,换个东西记:到 2^k 级祖先那一段的**最小边权**int dep[MAXN], comp[MAXN]; // comp = 属于哪一棵树(森林)int n, m, q;
int find_(int x) { while (fa[x] != x) x = fa[x] = fa[fa[x]]; return x; }inline void addEdge(int u, int v, int w) { to_[++ecnt] = v; wt_[ecnt] = w; nxt_[ecnt] = head_[u]; head_[u] = ecnt; }
int main() { if (scanf("%d %d", &n, &m) != 2) return 0; for (int i = 0; i < m; i++) if (scanf("%d %d %d", &es[i].u, &es[i].v, &es[i].w) != 3) return 0;
/* ① 最大生成森林:按权值**从大到小**排(⚠ 不是从小到大) */ sort(es, es + m, [](const Edge& a, const Edge& b) { return a.w > b.w; }); for (int i = 1; i <= n; i++) fa[i] = i; for (int i = 0; i < m; i++) { int a = find_(es[i].u), b = find_(es[i].v); if (a == b) continue; // 已经连通(重边、成环的边都在这儿被丢掉) fa[a] = b; addEdge(es[i].u, es[i].v, es[i].w); addEdge(es[i].v, es[i].u, es[i].w); }
/* ② 每棵树各 BFS 一趟(⚠ ① 是森林,不是一棵树) */ { vector<char> vis(n + 1, 0); vector<int> que; que.reserve(n); for (int r = 1; r <= n; r++) { if (vis[r]) continue; vis[r] = 1; dep[r] = 0; comp[r] = r; up[r][0] = r; mn[r][0] = INF; // 根往上没有边 ⇒ 那一段的最小边权是「无穷大」 que.clear(); que.push_back(r); for (size_t i = 0; i < que.size(); i++) { int u = que[i]; for (int e = head_[u]; e; e = nxt_[e]) { int v = to_[e]; if (vis[v]) continue; vis[v] = 1; dep[v] = dep[u] + 1; comp[v] = r; up[v][0] = u; mn[v][0] = wt_[e]; que.push_back(v); } } } } for (int k = 1; k < LOG; k++) for (int v = 1; v <= n; v++) { up[v][k] = up[up[v][k - 1]][k - 1]; mn[v][k] = min(mn[v][k - 1], mn[up[v][k - 1]][k - 1]); // ★ 两段拼起来取 min }
if (scanf("%d", &q) != 1) return 0; while (q--) { int x, y; if (scanf("%d %d", &x, &y) != 2) return 0; if (comp[x] != comp[y]) { printf("-1\n"); continue; } // ⚠ ① 不连通 int res = INF; if (dep[x] < dep[y]) swap(x, y); int d = dep[x] - dep[y]; for (int k = 0; k < LOG; k++) if ((d >> k) & 1) { res = min(res, mn[x][k]); x = up[x][k]; } if (x != y) { for (int k = LOG - 1; k >= 0; k--) if (up[x][k] != up[y][k]) { res = min(res, min(mn[x][k], mn[y][k])); x = up[x][k]; y = up[y][k]; } res = min(res, min(mn[x][0], mn[y][0])); // ⚠ 最后各再上一步 } printf("%d\n", res); } return 0;}点「运行 ▶」看结果
| 题面原话 | 后果 |
|---|---|
| 「如果货车不能到达目的地,输出 −1」 | ★★ 图不保证连通 ⇒ 是最大生成森林,不是一棵树 |
0 ≤ z |
答案可以是 0,而 0 和 −1 是两件事 —— 别拿 0 当「不通」的标志 |
| 「两座城市之间可能有多条道路」 | Kruskal 天然不怕(第二条自动被丢),可暴力那边要存两遍 |
⚠ 而「森林」这一条会连累两处:BFS 要对每个连通块各跑一趟(不能只从 1 号点出发),
comp[] 也要记下「谁属于哪棵树」。
2第一版:每个询问跑一次「最大瓶颈路」
「不超过限重最多运多重」= 「让路上最小的那条边尽量大」,写成递推就是
best[v] = max over 边(u,v) of min( best[u], w(u,v) )—— 把第 32 章 Dijkstra 那套骨架里的「加法 + 取小」换成「取小 + 取大」, 堆也从小根换成大根。不排序、不建生成树、不用倍增。 ⇒ 所以它既是对拍的标准答案,也是第 ① 步那句话的独立验证者。
⚠ 顺带一个自己踩的:它的边表要按 m 开(2m = 10⁵),
第一版照着正解写成了 MAXN*2 = 2×10⁴ —— 顶格上直接写飞,
而小数据(m < 10⁴)上一点事都没有。
3★★★ 顶格实测:正解快 1724 倍,而两条路的答案一个字节都不差
// P1967 的两笔账:这份数据长什么样,以及两条路各做多少次基本动作(机器无关)//// 用法:./p1967Count <csv|table> < 一份输入
#include <bits/stdc++.h>using namespace std;
const int MAXN = 10005;const int MAXM = 50005;const int LOG = 15;const int INF = 0x3f3f3f3f;
struct Edge { int u, v, w; };static Edge es[MAXM];static int fa[MAXN];static int head_[MAXN], nxt_[MAXN * 2], to_[MAXN * 2], wt_[MAXN * 2], ecnt;static int up[MAXN][LOG], mn[MAXN][LOG], dep[MAXN], comp[MAXN];static int gh[MAXN], gn[MAXM * 2], gt[MAXM * 2], gw[MAXM * 2], gc;static int n, m, q;
static int find_(int x) { while (fa[x] != x) x = fa[x] = fa[fa[x]]; return x; }
int main(int argc, char** argv) { string mode = argc > 1 ? argv[1] : "table"; if (scanf("%d %d", &n, &m) != 2) return 0; for (int i = 0; i < m; i++) { if (scanf("%d %d %d", &es[i].u, &es[i].v, &es[i].w) != 3) return 0; gt[++gc] = es[i].v; gw[gc] = es[i].w; gn[gc] = gh[es[i].u]; gh[es[i].u] = gc; gt[++gc] = es[i].u; gw[gc] = es[i].w; gn[gc] = gh[es[i].v]; gh[es[i].v] = gc; } sort(es, es + m, [](const Edge& a, const Edge& b) { return a.w > b.w; }); for (int i = 1; i <= n; i++) fa[i] = i; int used = 0; for (int i = 0; i < m; i++) { int a = find_(es[i].u), b = find_(es[i].v); if (a == b) continue; fa[a] = b; used++; to_[++ecnt] = es[i].v; wt_[ecnt] = es[i].w; nxt_[ecnt] = head_[es[i].u]; head_[es[i].u] = ecnt; to_[++ecnt] = es[i].u; wt_[ecnt] = es[i].w; nxt_[ecnt] = head_[es[i].v]; head_[es[i].v] = ecnt; } int comps = n - used, maxDep = 0; { vector<char> vis(n + 1, 0); vector<int> que; que.reserve(n); for (int r = 1; r <= n; r++) { if (vis[r]) continue; vis[r] = 1; dep[r] = 0; comp[r] = r; up[r][0] = r; mn[r][0] = INF; que.clear(); que.push_back(r); for (size_t i = 0; i < que.size(); i++) { int u = que[i]; for (int e = head_[u]; e; e = nxt_[e]) { int v = to_[e]; if (vis[v]) continue; vis[v] = 1; dep[v] = dep[u] + 1; comp[v] = r; up[v][0] = u; mn[v][0] = wt_[e]; que.push_back(v); } } } for (int i = 1; i <= n; i++) maxDep = max(maxDep, dep[i]); } for (int k = 1; k < LOG; k++) for (int v = 1; v <= n; v++) { up[v][k] = up[up[v][k - 1]][k - 1]; mn[v][k] = min(mn[v][k - 1], mn[up[v][k - 1]][k - 1]); }
if (scanf("%d", &q) != 1) return 0; long long jumps = 0, relax = 0; int neg = 0, zero = 0; vector<int> best(n + 1); for (int t = 0; t < q; t++) { int x, y; if (scanf("%d %d", &x, &y) != 2) return 0; /* ① 倍增:数「跳了多少次」 */ int ans; if (comp[x] != comp[y]) ans = -1; else { int a = x, b = y, res = INF; if (dep[a] < dep[b]) swap(a, b); int d = dep[a] - dep[b]; for (int k = 0; k < LOG; k++) if ((d >> k) & 1) { res = min(res, mn[a][k]); a = up[a][k]; jumps++; } if (a != b) { for (int k = LOG - 1; k >= 0; k--) if (up[a][k] != up[b][k]) { res = min(res, min(mn[a][k], mn[b][k])); a = up[a][k]; b = up[b][k]; jumps += 2; } res = min(res, min(mn[a][0], mn[b][0])); } ans = res; } if (ans < 0) neg++; else if (ans == 0) zero++; /* ② 暴力:数「松弛了多少条边」 */ for (int i = 1; i <= n; i++) best[i] = -1; best[x] = INT_MAX; priority_queue<pair<int, int> > pq; pq.push(make_pair(best[x], x)); while (!pq.empty()) { pair<int, int> tt = pq.top(); pq.pop(); int u = tt.second; if (tt.first != best[u]) continue; if (u == y) break; for (int e = gh[u]; e; e = gn[e]) { relax++; int v = gt[e], cand = min(best[u], gw[e]); if (cand > best[v]) { best[v] = cand; pq.push(make_pair(cand, v)); } } } }
vector<pair<string, string> > out; out.push_back(make_pair("n", to_string(n))); out.push_back(make_pair("m", to_string(m))); out.push_back(make_pair("q", to_string(q))); out.push_back(make_pair("comps", to_string(comps))); out.push_back(make_pair("maxdep", to_string(maxDep))); out.push_back(make_pair("ans_neg", to_string(neg))); out.push_back(make_pair("ans_zero", to_string(zero))); out.push_back(make_pair("jumps", to_string(jumps))); out.push_back(make_pair("relax", to_string(relax))); char buf[64]; snprintf(buf, sizeof(buf), "%.0f", relax / (double)max(1LL, jumps)); out.push_back(make_pair("ratio", string(buf)));
if (mode == "csv") for (size_t i = 0; i < out.size(); i++) printf("%s,%s\n", out[i].first.c_str(), out[i].second.c_str()); else for (size_t i = 0; i < out.size(); i++) printf(" %-10s %s\n", out[i].first.c_str(), out[i].second.c_str()); return 0;}点「运行 ▶」看结果
| 数据 | n / m / q |
连通块 | 生成森林最深 | ✗ 暴力松弛边数 | ★ 倍增跳数 | ✗ 暴力 | ★ 正解 |
|---|---|---|---|---|---|---|---|
| 30% 档 | 999 / 9999 / 999 | 1 | 51 | 10 439 997 | 5 240 | 0.13 秒 | 0.00 |
| 60% 档 | 999 / 5×10⁴ / 999 | 1 | 32 | 50 384 928 | 5 575 | 0.41 秒 | 0.00 |
full(100%) |
10⁴ / 5×10⁴ / 3×10⁴ | 1 | 106 | 1 552 698 807 | 223 660 | ⚠ 34.9 秒 | 0.015 |
chain(生成树是一条链) |
同上 | 1 | 9 999 | 1 501 734 704 | 184 186 | 25.1 秒 | 0.015 |
forest(只给 n/3 条边) |
10⁴ / 3333 / 3×10⁴ | 6 667 | 13 | 121 888 | 7 | 0.01 秒 | 0.00 |
⇒ ★★★ 五种顶格数据、每种 3×10⁴ 个询问,暴力和正解逐字节相同 —— 这就是第 ① 步那句话的验证,而验它的那条路和正解一行代码都不共享。
★ 次数上正解快 6942 倍,秒表上快 2300 倍(34.9 / 0.015 —— ⚠ 正解那一格 单跑一次分辨率不够,是跑 20 次算的平均)。 ★★ 而题面那两个部分分档正好是「暴力值多少分」的答案: 30% 档 0.13 秒、60% 档 0.41 秒 ⇒ 暴力稳拿 60 分。
只给 n/3 条边 ⇒ 6667 个连通块 ⇒ 随便挑两个点,几乎一定不在同一棵树里。
⇒ 那一档「一律输出 −1」的试金石能拿 29 994 / 30 000,
而倍增在那一档一共只跳了 7 次。
⇒ 「一致有两种:都算对了,和都没算」的顶格版: 这一档测的是「你会不会判不连通」,测不出任何和倍增有关的东西。
4⚠ 四个错法:两个在「建哪棵树」上,两个在「怎么合并 min」上
| 它其实在算什么 | 白送的推论 | |
|---|---|---|
| ①Min | 最小生成树上的路径瓶颈 —— 题目要的反义词 | 所有边权都一样时一分不扣 |
| ②Conn | 两个点各自到自己那棵树的根这一路的最小边权 | 图恰好连通时一分不扣 |
| ③Last | 路径上除了 LCA 正下方那两条边以外的最小边权 | ⇒ 答案只会偏大;祖先对上一分不扣 |
| ④Half | mn[v][k] 恒等于 mn[v][1](往上翻只看最靠下那两条边) |
⇒ 同样只会偏大;路径短时一分不扣 |
★★ 而 ③ 和 ④ 都只会让答案偏大 —— 于是「拿两个错法互相对拍」是查不出它们的 (两边一起偏大时可能撞成同一个数)。必须和一条独立的路比。
5★ 对拍:四个错法 + 一份「一律 −1」
| 档位 | ①Min | ②Conn | ③Last | ④Half | 试金石 |
|---|---|---|---|---|---|
| 0 顺手写法(先造一棵树保证连通 + 几条额外边) | 275 | ★ 0 | 277 | 243 | 300 |
| 1 稀疏随机边(不保证连通) | 198 | 258 | 172 | 181 | 290 |
| 2 边权只有 0 和 1 + 重边 | 274 | ★ 0 | 252 | 113 | 300 |
| 3 最终档 = 1 + 2 + 点多一些 | 190 | 281 | 154 | 140 | 288 |
★★★ ②Conn 在档 0 和档 2 上是结构性的精确的 0 —— 那两档的生成器
先造一棵树保证连通,而这几乎是所有人写图论题生成器的本能
(第 34 章 P3366 那次一模一样:那道题「不连通输出 orz」也是被同一个习惯藏住的)。
⚠ 而档 2 把 ④Half 从 243 压到 113:边权只有 0 和 1 ⇒ 路径上的最小值很容易就是 0, 而 0 一旦出现在最靠下那两条边里,「只看前半段」和「两段取 min」就一样了。 ⇒ ★ 同一个旋钮(值域)把 ①Min 和 ④Half 推向了相反方向(274 vs 113)。
6★ 哪一版就已经能过了
| 版本 | 能过吗 | 数字 |
|---|---|---|
| ✗ 每询问跑一次最大瓶颈路 | 60 分 | 60% 档 0.41 秒;100% 档 34.9 秒 / 时限 1 秒 |
| ★ 正解 | ✓ | 0.015 秒、6.3 MiB / 128 MiB —— 余量 60 倍以上 |
⇒ 这道题的正解一点都不卡常,功课全在三件事上:
① 那句需要证明的话(最大生成树);
② 「同一张表,换个东西记」(倍增维护路径 min);
③ 题面写着的三个坑(森林 / z 可以是 0 / 重边)。
★ 而这三件里,只有第 ② 件是这一章教的 —— 另外两件一件来自第 34 章, 一件只来自把题面读完。
倍增在这道题里不是主角,是配角 —— 主角是那句「最大生成树上的路径瓶颈就是答案」,而倍增只是把这棵树上的路径 min 问快一点。 ⇒ 于是这一页最值钱的动作不是写倍增,是用一条完全无关的路(最大瓶颈路) 在五种顶格数据上把那句话验了一遍。