0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P3258,日期见页头。两边不一致时信原站。
题目描述
松鼠的新家是一棵树,前几天刚刚装修了新家,新家有 n 个房间,并且有 n−1 根树枝连接,
每个房间都可以相互到达,且两个房间之间的路线都是唯一的。天哪,他居然真的住在“树”上。
松鼠想邀请小熊维尼前来参观,并且还指定一份参观指南,他希望维尼能够按照他的指南顺序,
先去 a₁,再去 a₂,……,最后到 aₙ,去参观新家。
可是这样会导致重复走很多房间,懒惰的维尼不停地推辞。可是松鼠告诉他,
每走到一个房间,他就可以从房间拿一块糖果吃。
维尼是个馋家伙,立马就答应了。现在松鼠希望知道为了保证维尼有糖果吃, 他需要在每一个房间各放至少多少个糖果。
因为松鼠参观指南上的最后一个房间 aₙ 是餐厅,餐厅里他准备了丰盛的大餐,
所以当维尼在参观的最后到达餐厅时就不需要再拿糖果吃了。
输入格式
第一行一个正整数 n,表示房间个数。第二行 n 个正整数,依次描述 a₁, a₂, …, aₙ。
接下来 n−1 行,每行两个正整数 x, y,表示标号 x 和 y 的两个房间之间有树枝相连。
输出格式
一共 n 行,第 i 行输出标号为 i 的房间至少需要放多少个糖果,才能让维尼有糖果吃。
数据范围
对于全部的数据,2 ≤ n ≤ 3×10⁵,1 ≤ aᵢ ≤ n。
时限 1 秒,内存 128000 KB(125 MiB)。
输入输出样例
输入
5 1 4 5 3 2 1 2 2 4 2 3 4 5
输出
1 2 1 2 1
★ 这棵树是 1−2,2 底下挂着 3 和 4,4 底下挂着 5。
维尼按 1 → 4 → 5 → 3 → 2 走,而最后那个 2 是餐厅。
1★★ 和上一道题只差一句话,而那句话就是全部难点
P3128 问的是:K 条路径,每条把沿途所有点 +1,最后问最大值。
答案是四次单点加:
d[s]++, d[t]++, d[lca]--, d[fa[lca]]--这道题的第一半一模一样:把 a₁ → a₂、a₂ → a₃、……、aₙ₋₁ → aₙ 这 n−1 条路径叠起来。
⚠⚠ 而第二半是这道题自己的:维尼是连着走的 ——
第 i 段的终点和第 i+1 段的起点是同一个房间 aᵢ₊₁,
可他在那儿只停了一次。上面那个叠法把它算了两次。
★★ 而题面末尾还写着「最后到达餐厅时就不需要再拿糖果吃了」—— aₙ 也要减掉一次。
★★★ 两件事能写成同一句话:
对 i = 1 … n−1: ans[a[i+1]]--i+1 ≤ n−1 时减的是「接头被算了两遍」,i+1 = n 时减的是「餐厅那块糖不拿」。
⇒ 一句 for,两个坑一起填。
// P3258 [JLOI2014] 松鼠的新家 —— 正解:倍增求 LCA + **点差分**,最后一趟子树求和//// ★★★ 这道题和 [P3128](/sol/p3128/) 只差一句话,而那一句话就是全部难点。// P3128:K 条路径,每条把沿途所有点 +1 —— 四次单点加就完了。// 这道题:维尼**连着走** a₁ → a₂ → … → aₙ,而**接头处那个房间只经过一次**。//// ⇒ 第 i 段(aᵢ → aᵢ₊₁)和第 i+1 段(aᵢ₊₁ → aᵢ₊₂)**都把 aᵢ₊₁ 算了一次**,// 可维尼在那儿只停了一次 ⇒ **每个接头都要减掉一次**。// ⇒ 而最后那个房间 aₙ 是餐厅,题面明说「不需要再拿糖果吃」⇒ **它也要减一次**。//// ★★ 而这两件事能写成**同一句话**:对 i = 1 … n−1,`ans[a[i+1]]--`。// (i+1 ≤ n−1 时减的是「接头被算了两遍」,i+1 = n 时减的是「餐厅不拿糖」。)//// ⇒ 全部动作:n−1 条路径 × 四次单点加 + n−1 次「接头 −1」+ 一趟子树求和。O((n + n) log n)。
#include <bits/stdc++.h>using namespace std;
const int MAXN = 300005;const int LOG = 19; // 2^18 = 262144 < 3×10⁵ ≤ 2^19
int head_[MAXN], nxt_[MAXN * 2], to_[MAXN * 2], ecnt;int up[MAXN][LOG], dep[MAXN], order_[MAXN], d[MAXN], a[MAXN];int n;
inline void addEdge(int u, int v) { to_[++ecnt] = v; nxt_[ecnt] = head_[u]; head_[u] = ecnt; }
int lca(int u, int v) { if (dep[u] < dep[v]) swap(u, v); int diff = dep[u] - dep[v]; for (int j = 0; j < LOG; j++) if ((diff >> j) & 1) u = up[u][j]; if (u == v) return u; for (int j = LOG - 1; j >= 0; j--) if (up[u][j] != up[v][j]) { u = up[u][j]; v = up[v][j]; } return up[u][0];}
int main() { if (scanf("%d", &n) != 1) return 0; for (int i = 1; i <= n; i++) if (scanf("%d", &a[i]) != 1) return 0; for (int i = 0; i < n - 1; i++) { int x, y; if (scanf("%d %d", &x, &y) != 2) return 0; addEdge(x, y); addEdge(y, x); // ⚠ 无向边,两边都要存 }
/* BFS 求 dep 和 up[·][0](⚠ 别用递归:顶格 3×10⁵ 排成一条链就是 30 万层)*/ { vector<char> vis(n + 1, 0); int cnt = 0; order_[cnt++] = 1; vis[1] = 1; dep[1] = 1; up[1][0] = 0; // ⚠⚠ 根的父亲是 0 号点,不是它自己 for (int i = 0; i < cnt; i++) { int u = order_[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; up[v][0] = u; order_[cnt++] = v; } } } for (int j = 1; j < LOG; j++) for (int v = 1; v <= n; v++) up[v][j] = up[up[v][j - 1]][j - 1];
for (int i = 1; i < n; i++) { int s = a[i], t = a[i + 1]; int l = lca(s, t); d[s]++; d[t]++; d[l]--; if (up[l][0]) d[up[l][0]]--; // ★ l 是根时 up[l][0] == 0,这笔账没人要收 }
/* 自底向上把子树和累起来 —— ⚠ 必须**逆着** BFS 序走 */ for (int i = n - 1; i >= 1; i--) { int u = order_[i]; d[up[u][0]] += d[u]; } /* ★★★ 接头(以及最后那个餐厅)只算一次 —— ⚠⚠ 这一刀必须落在**还原之后**: 差分数组里 d[u] 管的是「u 的整棵子树」,`d[t]--` 会把 t 头上的每一个祖先也一起减掉。*/ for (int i = 1; i < n; i++) d[a[i + 1]]--;
for (int i = 1; i <= n; i++) printf("%d\n", d[i]); return 0;}点「运行 ▶」看结果
第一版我自己就写错在这儿:把 d[t]-- 顺手塞进了打标记那个循环里。
差分数组里的 d[u] 管的不是「u 这一格」,是「u 的整棵子树」 ——
还原的时候 d[u] 会被加进它的每一个祖先。
所以 d[t]-- 减掉的不是 t 那一格,是根到 t 的一整条路。
官方样例上一测就死:正确答案是 1 2 1 2 1,那一版打出 -3 -1 1 1 1
(1 号点被减了三次 —— 因为三个接头都在它的子树里)。
⇒ ★★ 想法对、落点差一步,这是第 44 章那一整章反复出现的形状;
这一版留在下面第 ③ 步,叫 p3258InDiff.cpp。
2第一版:照着题面一步一步走
| 形状 | 最大深度 | ✗ 暴力一共走进多少个房间 | ✗ 暴力 | ★ 正解 |
|---|---|---|---|---|
star |
2 | 599 997 | ★ 0.09 秒 | 0.17 |
binary |
19 | 9 125 449 | ★ 0.16 秒 | 0.21 |
rand |
31 | 6 748 547 | ★ 0.17 秒 | 0.22 |
chain |
300 000 | ⚠ 29 964 988 596 | ⚠ 412.09 秒 | 0.27 |
(本机 · A 机 WSL2 · nproc 12 · 2026-09-12 · 独占 · 每格 3 次取中位数;链上那一格只跑了一次。)
⇒ ★★★ 同样顶格,只换树的形状,暴力走的房间数差 4440 倍 —— 而秒表差 1526 倍。
⇒ ⚠ 而在另外三种形状上,暴力比正解还快(正解得先花时间建那张 up[3×10⁵][19] 的表)。
★★ 这是这一轮的第二道题上又一次撞见第 51 章那句话: 顺手造一组「顶格随机」跑一遍,这一页什么都看不出来 —— 随机树的深度只有 31,每段路平均 22 个房间。
同一份暴力、同一种形状(链),只拧 n:
n |
走进的房间数 | 秒 | 每秒处理 |
|---|---|---|---|
| 80 000 | 2 140 117 390 | 13.17 | 1.63 亿 |
| 120 000 | 4 786 778 102 | 39.89 | 1.20 亿 |
| 300 000 | 29 964 988 596 | 412.09 | ★ 0.73 亿 |
⇒ 拿 n = 8×10⁴ 那一格按「步数 × 单步耗时」外推到顶格,得 184 秒,实测 412 秒。
⇒ ★★ 单步耗时本身在随规模变大(那个 right 数组顶格能到 10 万个 int)——
所以外推只能用来判「够不够得着」,不能当成一个数写进结论里。
3⚠ 五个错法:四个在那一句「接头 −1」上,一个在「怎么还原」上
| 它其实在算什么 | 什么时候才会露馅 | |
|---|---|---|
| ①NoJoint | 正确答案 + 这个房间在 a₂…aₙ 里出现了几次 |
每一组都错(n ≥ 2 就一定有接头) |
| ②NoEnd | 正确答案,但 aₙ 那一格多 1 |
每一组都错(那一格总要打出来) |
| ③Start | 减的是 a₁ … aₙ₋₁,正解减的是 a₂ … aₙ |
★ a₁ ≠ aₙ —— 中间那 n−2 个一模一样,差别只在两头 |
| ④InDiff | 正确答案 − 这个房间的子树里出现过多少个接头 |
★ 存在某个接头不是根(接头是根时,「减一整条路」和「减一格」是同一件事) |
| ⑤Order | 深一层的贡献永远传不上去 | 树有两层就抓得到 |
⇒ ★★★ ③和④的触发条件都是一句话,而下面那张表里它们的「触发 ≡ 抓获」六档一个不差。
4★ 对拍:六个档位,而其中一整档在验零
// P3258 的两笔账:这份数据长什么样,以及两条路各做多少次基本动作(机器无关)//// 用法:./p3258Count <csv|table> < 一份输入//// ★ 顺手数三个决定成败的东西:// ① **a₁ 和 aₙ 是不是同一个房间** —— 那正好是错法③(减在起点上)的触发条件;// ② **有几个接头不是根** —— 那是错法④(把「接头 −1」写进差分数组)的触发条件;// ③ **暴力一共要走进多少个房间** —— 顶格那张表的主角。#include <bits/stdc++.h>using namespace std;
const int MAXN = 300005;const int LOG = 19;static int head_[MAXN], nxt_[MAXN * 2], to_[MAXN * 2], ecnt;static int up[MAXN][LOG], dep[MAXN], order_[MAXN], d[MAXN], a[MAXN];static int n;
static int lca(int u, int v) { if (dep[u] < dep[v]) swap(u, v); int diff = dep[u] - dep[v]; for (int j = 0; j < LOG; j++) if ((diff >> j) & 1) u = up[u][j]; if (u == v) return u; for (int j = LOG - 1; j >= 0; j--) if (up[u][j] != up[v][j]) { u = up[u][j]; v = up[v][j]; } return up[u][0];}
int main(int argc, char** argv) { string mode = argc > 1 ? argv[1] : "table"; if (scanf("%d", &n) != 1) return 0; for (int i = 1; i <= n; i++) if (scanf("%d", &a[i]) != 1) return 0; for (int i = 0; i < n - 1; i++) { int x, y; if (scanf("%d %d", &x, &y) != 2) return 0; to_[++ecnt] = y; nxt_[ecnt] = head_[x]; head_[x] = ecnt; to_[++ecnt] = x; nxt_[ecnt] = head_[y]; head_[y] = ecnt; } { vector<char> vis(n + 1, 0); int c = 0; order_[c++] = 1; vis[1] = 1; dep[1] = 1; up[1][0] = 0; for (int i = 0; i < c; i++) { int u = order_[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; up[v][0] = u; order_[c++] = v; } } } for (int j = 1; j < LOG; j++) for (int v = 1; v <= n; v++) up[v][j] = up[up[v][j - 1]][j - 1];
int maxDep = 0; for (int i = 1; i <= n; i++) maxDep = max(maxDep, dep[i]);
long long bruteSteps = 1; // 出发那一间也算「走进去」一次 long long jointNotRoot = 0, lcaRoot = 0; for (int i = 1; i < n; i++) { int s = a[i], t = a[i + 1]; int l = lca(s, t); if (l == 1) lcaRoot++; if (t != 1) jointNotRoot++; bruteSteps += (long long)(dep[s] - dep[l]) + (dep[t] - dep[l]); // ⚠ 起点不重复计 d[s]++; d[t]++; d[l]--; if (up[l][0]) d[up[l][0]]--; } for (int i = n - 1; i >= 1; i--) d[up[order_[i]][0]] += d[order_[i]]; for (int i = 1; i < n; i++) d[a[i + 1]]--;
int mx = 0, mn = 1 << 30; long long tot = 0; for (int i = 1; i <= n; i++) { mx = max(mx, d[i]); mn = min(mn, d[i]); tot += d[i]; }
/* 差分那条路的基本动作:建表 n·(LOG−1) 次写 + 每段最多 2·LOG 次读 + 4(n−1) 次单点加 */ long long fastSteps = (long long)n * (LOG - 1) + (long long)(n - 1) * (2 * LOG + 4);
vector<pair<string, string> > out; out.push_back(make_pair("n", to_string(n))); out.push_back(make_pair("maxdep", to_string(maxDep))); out.push_back(make_pair("head_eq_tail", to_string(a[1] == a[n] ? 1 : 0))); out.push_back(make_pair("joint_not_root", to_string(jointNotRoot))); out.push_back(make_pair("lca_root", to_string(lcaRoot))); out.push_back(make_pair("ans_max", to_string(mx))); out.push_back(make_pair("ans_min", to_string(mn))); out.push_back(make_pair("ans_sum", to_string(tot))); out.push_back(make_pair("brute_steps", to_string(bruteSteps))); out.push_back(make_pair("fast_steps", to_string(fastSteps))); out.push_back(make_pair("ratio", to_string(bruteSteps / fastSteps))); 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(" %-16s %s\n", out[i].first.c_str(), out[i].second.c_str()); return 0;}点「运行 ▶」看结果
| 档位 | ①NoJoint | ②NoEnd | ③Start | ④InDiff | ⑤Order | 试金石 |
|---|---|---|---|---|---|---|
0 顺手写法(随机小树、a 随便挑) |
300 | 300 | 283 | 300 | 300 | 300 |
1 a 是排列(官方样例长这样) |
300 | 300 | 300 | 300 | 300 | 300 |
2 a₁ = aₙ |
300 | 300 | ★ 0 | 300 | 300 | 300 |
3 a 全都指着根 |
300 | 300 | ★ 0 | ★ 0 | ★ 0 | ⚠ 0 |
4 a 全都是同一个非根房间 |
300 | 300 | ★ 0 | 300 | 237 | ⚠ 0 |
| 5 最终档 | 300 | 300 | 300 | 300 | 300 | 300 |
★★★ ③和④的「触发 ≡ 抓获」六档一个不差:
③的触发(a₁ ≠ aₙ)是 283 / 300 / 0 / 0 / 0 / 300;
④的触发(有接头不是根)是 300 / 300 / 300 / 0 / 300 / 300。
⇒ 两行数字和上面那张抓获表逐格相同,而两个「一个不差」都是能证的
(③错的是 a₁ 和 aₙ 两格,各差 1,必不同)。
⚠⚠ 而档 3 是这一页最值钱的一格:那一整档在验零。
a 全都指着根 ⇒ 维尼一步都没走 ⇒ 正确答案恒等于全 0(出发那块糖被「餐厅不拿」抵掉了)
⇒ 五个待测版本里有三个满分,连「什么都不算」的试金石也满分。
⇒ 「一致有两种:都算对了,和都没算」——
看到一档掉到 0,先问一句这一档的正确答案是不是个常量。
5⚠ 第六个错法:对拍一万轮也抓不到
本机 ulimit -s = 8192 KB。二分出来的门槛:
活着的最大 n(链) |
104 687(⚠ 见下面那条:这个数对环境敏感) |
死掉的最小 n(链) |
104 843 |
| ⇒ 每层约 | 80 字节 |
顶格 n = 3×10⁵ 是门槛的 |
⚠ 2.87 倍 ⇒ 必炸(退出码 139) |
★ 和本书前几次量到的放在一起看: P5318 那份精简递归 48 字节 / 17.4 万层、 P4551 64 字节 / 13.0 万层、这一份 80 字节 / 10.5 万层。 ⇒ ★★ 门槛不是一个能背的数,它是「你在那个函数里写了什么」的属性。
⚠⚠ 而这一页还多学到一层:那个门槛连「谁 spawn 它」都算数。
上面那两个数是在 shell 里二分出来的,它们只差 156;
把它们原样写成断言,闸门(由 node 起进程)里当场翻红 ——
环境变量数组也摆在栈顶上,换一个爹,门槛就挪几十层。
⇒ 所以 check:viz 里钉的是一条带(9×10⁴ 活、1.3×10⁵ 死),不是那两个整数。
⇒ ★★ 这和第 46 章 P1469 那条
「ru_maxrss 里『进程起步』那一截取决于谁 spawn 它」是同一件事的另一面。
⚠⚠ 而这个错法答案永远是对的(DFS 序倒着走同样满足「儿子先于父亲」)—— 上面那张对拍表里它那一列六档全是 0。 ⇒ 「对拍验的是『算得对不对』,从来不验『装不装得下、跑不跑得完』」。
6★ 哪一版就已经能过了
| 版本 | 能过吗 | 数字(顶格最坏形状) |
|---|---|---|
| ✗ 一步一步走 | ✗(⚠ 而随机数据上它比正解还快) | chain 412.09 秒 / 时限 1 秒 |
| ✗ 正解 + 递归建表 | ✗(答案全对,段错误) | chain 30 万层 / 门槛 10.5 万层 |
| ★ 正解(LCA + 点差分 + 接头 −1) | ✓ | chain 0.27 秒、36.3 MiB / 125 MiB |
⇒ 这道题的三件功课: ① 那一句「接头 −1」(谁被算了两次、为什么餐厅也算在里面); ② 它必须落在还原之后(差分数组管的是子树,不是一格); ③ 顶格是一条 30 万层的链 ⇒ 建表和还原都不能递归。
★ 而「要不要 long long」这次不用想:答案最大就是 n−1 < 3×10⁵。
这道题的关卡一个都不在「差分怎么写」上 —— 差分那四次单点加,
上一道题已经原样给过了。
真正要想的是读题:维尼是连着走的,接头处那个房间只经过一次。
⇒ ★★ 而这句话落到代码上,是一行 for 加一个下标(a[i+1] 不是 a[i]),
以及它写在哪一行(还原之后,不是之前)。
两处各错一次,就是这一页的错法③和错法④。