题单 · 习题解析

洛谷 P1122 最大子树和

★★★ 「答案初值写成 0」被抓轮数 ≡ 美丽指数全为负的轮数(默认档精确的 0、全负档 300 —— 自检就在同一张表里);★★★ 「f[u] 本身就是一个合法方案 ⇒ f[u] ≤ 答案」,这条论证模式第三次登场

原题:洛谷 P1122出自 第 27 章 树形 DP:没有上司的舞会 的题单题面本地存档:2026-08-30
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

一株奇怪的花卉,上面共连有 n 朵花,共有 n−1 条枝干将花儿连在一起, 并且未修剪时每朵花都不是孤立的。每朵花都有一个「美丽指数」,该数越大说明这朵花越漂亮, 也有「美丽指数」为负数的,说明这朵花看着都让人恶心。

所谓「修剪」,意为:去掉其中的一条枝条,这样一株花就成了两株,扔掉其中一株。 经过一系列「修剪」之后,还剩下最后一株花(也可能是一朵)

老师的任务就是:通过一系列「修剪」(也可以什么「修剪」都不进行), 使剩下的那株(那朵)花卉上所有花朵的「美丽指数」之和最大。

输入格式

第一行一个整数 n (1 ≤ n ≤ 16000),表示原始的那株花卉上共 n 朵花。

第二行有 n 个整数,第 i 个整数表示第 i 朵花的美丽指数。

接下来 n−1 行每行两个整数 a, b,表示存在一条连接第 a 朵花和第 b 朵花的枝条。

输出格式

一个数,表示一系列「修剪」之后所能得到的「美丽指数」之和的最大值。 保证绝对值不超过 2147483647

说明/提示

数据范围及约定

  • 对于 60% 的数据,有 1 ≤ n ≤ 1000
  • 对于 100% 的数据,有 1 ≤ n ≤ 16000

输入输出样例

输入

7
-1 -1 -1 1 1 1 0
1 4
2 5
3 6
4 7
5 7
6 7

输出

3

七朵花,7 号是那个把三条枝连起来的点。留下 4 5 6 71 + 1 + 1 + 0)⇒ 3

⚠ 这一组样例只挡住了本页四个错法里的两个(忘了剪枝 1、只看根 2), 而「把所有正数加起来」和「答案初值写成 0」都原样输出 3 —— 见第 ① 步和第 ④ 步。

1★ 第一版:负的都剪掉,把正的加起来

这题的第一反应几乎人人相同:美丽指数是负的那些花,剪掉不就完了? 于是把所有正数一加,交上去。

p1122AllPos.cpp✗ 第一版:所有正数之和
// ✗ P1122 第一版:把所有「美丽指数为正」的花加起来
//
// 想法很顺:负的都剪掉不就完了吗?
// ★ 它错在**连通性**:剪枝只能沿着枝条剪,剩下的必须还是**连在一起的一株**。
// 两朵好花中间隔着一朵烂花,你没法只要两头。
// ⇒ 它解的是一个**放宽了的问题**(不要求连通)⇒ **恒 ≥ 正解**。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n) || n <= 0) return 0;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i < n; i++) { int x, y; cin >> x >> y; }
long long sum = 0;
int mx = INT_MIN;
for (int i = 1; i <= n; i++) { if (a[i] > 0) sum += a[i]; mx = max(mx, a[i]); }
if (sum == 0) sum = mx; // 一朵都没有正的,那就留最好的那一朵
cout << sum << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 它错在「剪」这个动作是有形状的

剪刀只能剪枝条,剩下的必须还是连在一起的一株。 两朵好花中间隔着一朵烂花,你没有办法只要两头 —— 想要它们,就得把中间那朵一起留着。

⇒ 它解的是一个放宽了的问题(不要求连通)⇒ 恒 ≥ 正解。 这就是第 26 章 P1220 那条判据:答案偏大还是偏小,不用跑就能判。

≥ 正解 300 / 300
300 轮被抓 273
错的时候平均多报 51.72%
最多多报 280.00%

⚠ 而官方样例放过了它(那组数据的三朵正花恰好连成一片)。

2★★ 关键的一步:max(0, f[v]) —— 一个儿子亏了就剪掉

★ f[u] 的定义里有「必须」两个字
    f[u] = 「必须留下 u」时,u 这一块能凑出的最大美丽指数和
         = a[u] + Σ max(0, f[v])

max(0, f[v]) 就是全部:某个儿子那一枝算出来是负的,剪掉它(题目允许),不亏; 是正的就接上。

★★ 答案是 max over 所有 uf[u],不是 f[根] —— 留下来的那一株可以挂在树的任何地方,而 f[u] 只管「以 u 为最高点」的那些方案。 每个点都当一次最高点,就把所有方案数完了。

p1122.cpp★ 这一版就能 AC
// P1122 最大子树和 —— 正解:树形 DP,O(n)
//
// f[u] = 「必须留下 u」时,u 这一块(在以 u 为根的子树里)能凑出的最大美丽指数和
// = a[u] + Σ max(0, f[v])
//
// ★ `max(0, f[v])` 这一下就是全部:一个儿子那边算出来是负的,**剪掉它**(题目允许修剪),
// 不亏;是正的就接上。
// ★ 答案是 **max over u of f[u]** —— 不是 f[根]!留下来的那一株可以挂在树的任何地方。
// ⚠ 而它也**不能取 max(0, ...)**:题面说修剪完「还剩下最后一株花(也可能是一朵)」
// ⇒ 至少要留一朵 ⇒ 美丽指数全是负数时,答案是最大的那个负数,不是 0。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 16005;
int n, a[MAXN];
vector<int> adj[MAXN];
int f[MAXN];
int ans;
void dfs(int u, int fa) {
f[u] = a[u];
for (int v : adj[u]) {
if (v == fa) continue;
dfs(v, u);
if (f[v] > 0) f[u] += f[v]; // 负的那一枝直接剪掉
}
ans = max(ans, f[u]);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n) || n <= 0) return 0;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i < n; i++) {
int x, y;
cin >> x >> y;
adj[x].push_back(y);
adj[y].push_back(x);
}
ans = INT_MIN; // ← 不能写成 0
dfs(1, 0);
cout << ans << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

3★ 两个「收紧了的问题」:忘了剪枝、只看根

p1122NoZero.cpp✗ 忘了 max(0, ·):儿子亏了也照单全收
// ✗ P1122:忘了那个 max(0, ·),儿子那边算出来是负的也照单全收
//
// f[u] = a[u] + Σ f[v]
//
// ★ 它算了什么:它要求「留下 u 就必须把 u 的整棵子树全留下」——
// **修剪这个动作被取消了**(只剩「从哪儿把整株砍下来」这一种选择)。
// ⇒ 它解的是一个**收紧了的问题** ⇒ 恒 ≤ 正解。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 16005;
int n, a[MAXN], f[MAXN], ans;
vector<int> adj[MAXN];
void dfs(int u, int fa) {
f[u] = a[u];
for (int v : adj[u]) {
if (v == fa) continue;
dfs(v, u);
f[u] += f[v]; // ← 少了 max(0, ·)
}
ans = max(ans, f[u]);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n) || n <= 0) return 0;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i < n; i++) { int x, y; cin >> x >> y; adj[x].push_back(y); adj[y].push_back(x); }
ans = INT_MIN;
dfs(1, 0);
cout << ans << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p1122Root.cpp✗ 只输出 f[1],忘了对所有点取 max
// ✗ P1122:只输出 f[1],忘了对所有点取 max
//
// ★ 它算了什么:它要求**留下来的那一株必须含 1 号花**。
// ⇒ 又一个收紧了的问题 ⇒ 恒 ≤ 正解。
// ⚠ 而 1 号花是谁完全是随机的 —— 这就是「生成器不打乱编号,就会给某个点安排特殊身份」
// (第 27 章正文第 ⑫ 步)在这道题上的样子。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 16005;
int n, a[MAXN], f[MAXN];
vector<int> adj[MAXN];
void dfs(int u, int fa) {
f[u] = a[u];
for (int v : adj[u]) {
if (v == fa) continue;
dfs(v, u);
if (f[v] > 0) f[u] += f[v];
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n) || n <= 0) return 0;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i < n; i++) { int x, y; cin >> x >> y; adj[x].push_back(y); adj[y].push_back(x); }
dfs(1, 0);
cout << f[1] << '\n'; // ← 只看了根
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 说清楚它们各自算了什么,方向就是白送的
它其实解了哪道题 和正解的关系 300 轮被抓
忘了 max(0, ·) 「留下 u 就必须留下 u 的整棵子树」(修剪这个动作被取消了) (300/300) 259
只输出 f[1] 「留下来的那一株必须含 1 号花」 (300/300) 151

两个都是收紧了的问题(多加了一条题目没有的限制)⇒ 只会少报。 ⇒ 加上第 ① 步那个「恒 ≥」,这一页三个错法方向齐了: 一个放宽、两个收紧 —— 和第 26 章 P1220 那页一样, 一页之内就能把这条判据的两个方向都验一遍。

⚠ 「只输出 f[1]」的抓获率还挂着一个前提:生成器有没有打乱编号。 不打乱的话 1 号点永远是那棵生成树的根,这个 bug 会被系统性地压低 (第 27 章正文第 ⑫ 步那条「别给某个点安排特殊身份」)。

4★★★ 第四个错法:答案初值写成 0 —— 而题面那句话是命门

p1122Zero.cpp✗ 答案初值写成 0(以为可以「什么都不留」)
// ✗ P1122:答案的初值写成了 0(以为「什么都不留」也算一种修剪)
//
// ★ 题面那句「经过一系列修剪之后,**还剩下最后一株花(也可能是一朵)**」是**命门**:
// 至少要留一朵。所以美丽指数全是负数时,答案是**最大的那个负数**,不是 0。
// ⇒ 被抓的轮数 ≡ 全部美丽指数都是负数的轮数 —— 而顺手写的生成器(正负都有)
// 造出这种输入的概率是 2⁻ⁿ ⇒ 在默认档上它是**精确的 0**。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 16005;
int n, a[MAXN], f[MAXN], ans;
vector<int> adj[MAXN];
void dfs(int u, int fa) {
f[u] = a[u];
for (int v : adj[u]) {
if (v == fa) continue;
dfs(v, u);
if (f[v] > 0) f[u] += f[v];
}
ans = max(ans, f[u]);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n) || n <= 0) return 0;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i < n; i++) { int x, y; cin >> x >> y; adj[x].push_back(y); adj[y].push_back(x); }
ans = 0; // ← 这里
dfs(1, 0);
cout << ans << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 被抓的轮数 ≡ 美丽指数全是负数的轮数 —— 两个方向都一个不差

题面那句「经过一系列修剪之后,还剩下最后一株花(也可能是一朵)」是命门至少要留一朵。所以美丽指数全是负数时,答案是最大的那个负数,不是 0。

档位(各 300 轮) 「全是负数」的轮数 「初值写 0」被抓
默认档(−20 ~ 20 0 精确的 0
全负档−20 ~ −1 300 300

⚠⚠ 上面那个「精确的 0」是配了自检才敢写的 —— 这正是本书那条通用规矩 (P2240P1094):报「0 次 / 找不到」之前, 先拿一个已知能触发的档位证明这段代码是活的。 右边那一列 300 / 300 就是那份自检。

★ 而这个 0 是结构性的:默认档里 12 朵花全为负的概率约 2⁻¹²,加多少轮都指望不上。 ⇒ 又一次「生成器最自然的默认值,正好是某个 bug 的藏身处」。

⚠ 顺带和同题单的 P1352 对照着看:那一页的全负档让两个 bug 一起隐身 (因为正解恒为 0),这一页的全负档是唯一抓得到这个 bug 的地方同一个档位,在两道题上作用正好相反。

5★★★ 要不要开 long long —— 这次不用跑就能证

★ 「答案 ≥ 任何一个被用到的中间值」——同一条论证模式,本书第三次

题面只保证了答案的绝对值不超过 2147483647,可我们真正往数组里塞的是每个 f[u]。 它会不会先溢出?

两行就能证:f[u] 本身就是一个合法方案的美丽指数和(留下以 u 为最高点的那一块), 而答案是所有合法方案里最大的那个 ⇒ f[u] ≤ 答案,对每个 u 都成立。

300 轮里「每个 f[u] 都 ≤ 答案」 300 / 300

⇒ 这和 P1164(「答案 ≤ 2³¹−1」正好是 32 位够用的充分条件)、 P5365(乘积不截断也不会溢出)是同一个形状的论证, 本书第三次用到它。⇒ 它是一个可复用的模式,不是巧合。

6★ 参照物、规模和递归深度

参照物是 2ⁿ 枚举「留下哪些花」,逐个检查它是不是连通的一株 —— 这道题真正难的地方就是连通性,而这一点只有小数据验得干净。

300 轮:正解 vs 2ⁿ 枚举连通块 不一致 0 轮
顶格 n = 16000 排成一条链,递归到底 16000 层,没爆栈

⚠ 最后这一行值得记住:16000 层递归默认栈放得下, 而同一张题单的 P3478n ≤ 10⁶ —— 那儿就放不下了「递归会不会爆栈」是随 n 变的,每道题都要重新算一次。

p1122Brute.cpp参照物:2ⁿ 枚举连通块(300 轮不一致 0 轮)

7度量程序和生成器

p1122Count.cpp度量程序(本页所有数字都出自它)
p1122Gen.cpp数据生成器

8一页纸

★★ 关键的一步 f[u] = a[u] + Σ max(0, f[v]);★ 答案是所有 f[u] 的 max,不是 f[根]
★ 第一版 所有正数相加 ⇒ 放宽了连通性 ⇒ 恒 ≥ 正解,被抓 273/300,平均多报 51.72%
★ 两个收紧 忘了 max(0,·)(≡「整棵子树都要」,被抓 259)/ 只看 f[1](≡「必须含 1 号」,被抓 151)
★★★ 第四个错法 答案初值写成 0 ⇒ 被抓轮数 ≡ 全负轮数:默认档 0 ≡ 0、全负档 300 ≡ 300
⚠ 那个 0 配了自检 右边那 300 就是「这段代码是活的」的证明
★★★ 要不要 long long f[u] 本身就是一个合法方案 ⇒ f[u] ≤ 答案 ⇒ 题面那句保证就够(300/300)
参照物 2ⁿ 枚举连通块;300 轮不一致 0 轮
规模 n ≤ 16000 的链递归 16000 层没爆栈;⚠ 到 P347810⁶ 就会