题单 · 习题解析

洛谷 P3258 [JLOI2014] 松鼠的新家

★★★ 它和 [P3128](/sol/p3128/) 只差一句话,而那句话就是全部难点:维尼是**连着走**的 ⇒ 每个接头处的房间被两段路各算了一次,而他只停了一次;再加上题面末尾那句「餐厅不拿糖」——★★ **两件事能写成同一句 `for`**(对 `i = 1…n−1` 做 `ans[a[i+1]]--`);★★★ 而这一页最值钱的是**那一刀落在哪一行**:把它写进差分数组里(`d[t]--`)减掉的不是「t 那一格」,是**根到 t 的一整条路** —— 官方样例上正解 `1 2 1 2 1`、那一版打出 `-3 -1 1 1 1`;★★★ 五个错法里有两个的触发条件是**一句话**,而它们的「触发 ≡ 抓获」**六档一个不差**(③减在起点上 ⟺ `a₁ ≠ aₙ`:283/300/0/0/0/300;④写进差分数组 ⟺ 存在接头不是根:300/300/300/0/300/300);⚠⚠ 而档 3(`a` 全都指着根)是**一整档在验零** —— 正确答案恒等于全 0,五个待测版本里三个满分、连试金石也满分;★★ 顶格那张表是这一轮第二次撞见「顶格随机在骗人」:`star`/`binary`/`rand` 三档上**暴力比正解还快**(0.09/0.16/0.17 vs 0.17/0.21/0.22 秒),换成一条链就是 **412.09 秒**(走进 299.6 亿个房间,比随机树多 **4440 倍**);★ 外带一条:拿 `n = 8×10⁴` 按「步数 × 单步耗时」外推到顶格得 184 秒、实测 412 秒 ⇒ **单步耗时本身在随规模变大**;⚠ 而递归建表那一版**答案永远对**、对拍六档全 0,只在顶格链上段错误(门槛 10.47 万层 / 每层 80 字节,顶格是它的 2.87 倍)

⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

松鼠的新家是一棵树,前几天刚刚装修了新家,新家有 n 个房间,并且有 n−1 根树枝连接, 每个房间都可以相互到达,且两个房间之间的路线都是唯一的。天哪,他居然真的住在“树”上。

松鼠想邀请小熊维尼前来参观,并且还指定一份参观指南,他希望维尼能够按照他的指南顺序, 先去 a₁,再去 a₂,……,最后到 aₙ,去参观新家。 可是这样会导致重复走很多房间,懒惰的维尼不停地推辞。可是松鼠告诉他, 每走到一个房间,他就可以从房间拿一块糖果吃。

维尼是个馋家伙,立马就答应了。现在松鼠希望知道为了保证维尼有糖果吃, 他需要在每一个房间各放至少多少个糖果。

因为松鼠参观指南上的最后一个房间 aₙ 是餐厅,餐厅里他准备了丰盛的大餐, 所以当维尼在参观的最后到达餐厅时就不需要再拿糖果吃了。

输入格式

第一行一个正整数 n,表示房间个数。第二行 n 个正整数,依次描述 a₁, a₂, …, aₙ

接下来 n−1 行,每行两个正整数 x, y,表示标号 xy 的两个房间之间有树枝相连。

输出格式

一共 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−22 底下挂着 344 底下挂着 5。 维尼按 1 → 4 → 5 → 3 → 2 走,而最后那个 2 是餐厅。

1★★ 和上一道题只差一句话,而那句话就是全部难点

★★★ 「n−1 条路径各 +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.cpp★ 正解:倍增求 LCA + 点差分 + 一趟子树求和 + 一句「接头 −1」
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠⚠ 那一句「接头 −1」必须落在还原之后 —— 这是本页最值钱的一处

第一版我自己就写错在这儿:把 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第一版:照着题面一步一步走

p3258Brute.cpp✗ 第一版(也是对拍的标准答案)
p3258GenBig.cpp★ 顶格生成器:rand / chain / star / binary
★★★ 顶格 n = 3×10⁵:暴力在三种形状上都比正解还快,只有链上要 412 秒
形状 最大深度 ✗ 暴力一共走进多少个房间 ✗ 暴力 ★ 正解
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 个房间。

★ 顺带一条:按小规模线性外推会低估 2.2 倍

同一份暴力、同一种形状(链),只拧 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」上,一个在「怎么还原」上

p3258NoJoint.cpp✗ 错法①:完全不减接头 —— 也就是照抄 P3128
p3258NoEnd.cpp✗ 错法②:接头减了,餐厅那一次忘了
p3258Start.cpp✗ 错法③:减在每一段的起点上
p3258InDiff.cpp✗ 错法④:「接头 −1」写进了差分数组里
p3258Order.cpp✗ 错法⑤:子树求和顺着 BFS 序走
★★★ 每个错法都能写成一句精确的等式 —— 于是抓获率是白送的推论
它其实在算什么 什么时候才会露馅
①NoJoint 正确答案 + 这个房间在 a₂…aₙ 里出现了几次 每一组都错n ≥ 2 就一定有接头)
②NoEnd 正确答案,但 aₙ 那一格多 1 每一组都错(那一格总要打出来)
③Start 减的是 a₁ … aₙ₋₁,正解减的是 a₂ … aₙ a₁ ≠ aₙ —— 中间那 n−2一模一样,差别只在两头
④InDiff 正确答案 − 这个房间的子树里出现过多少个接头 存在某个接头不是根(接头是根时,「减一整条路」和「减一格」是同一件事)
⑤Order 深一层的贡献永远传不上去 树有两层就抓得到

⇒ ★★★ ③和④的触发条件都是一句话,而下面那张表里它们的「触发 ≡ 抓获」六档一个不差

4★ 对拍:六个档位,而其中一整档在验零

p3258Zero.cpp★ 试金石:什么都不算,一律输出 0
p3258Gen.cpp★ 生成器:六个档位
p3258Count.cpp★ 数「a₁ 和 aₙ 是不是同一个房间」+「有几个接头不是根」
// 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;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★★★ 300 轮 × 六档(本机实测)
档位 ①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⚠ 第六个错法:对拍一万轮也抓不到

p3258Rec.cpp✗ 错法⑥:把 BFS 换成递归 DFS —— 答案永远对
★★ 「能不能写递归」是一道三十秒的算术题:最深多少层 × 每层多少字节

本机 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]), 以及它写在哪一行(还原之后,不是之前)。 两处各错一次,就是这一页的错法③和错法④。