本文首发于洛谷平台,CSDN 版本为转载。
前言

众所不周知,一篇文章应该有一张头图。
2025 年 12 月,笔者发表了《再谈铲雪》。这篇文章写的依托,因此选择重构。
本文主要更新如下:
- 解决了基环树铲雪;
- 增加了使用笛卡尔树解决序列铲雪的方法;
- 增加了利用线性规划解决一般铲雪问题的方法;
- 补充了若干铲雪相关例题;
- 对全文结构进行了重新梳理;
- 新增配套题单。
如果写的还是很烂,欢迎在评论区开骂。
约定
- 在转移式中出现 f 0 / 1 , i f_{0/1,i} f0/1,i 这样的表达式时,实际表示 max ( f 0 , i , f 1 , i ) \max(f_{0,i},f_{1,i}) max(f0,i,f1,i)。
- 参考代码中默认使用
#define int long long。
铲雪模型
首先给出铲雪问题的一个基础形式:
给定无向图 G G G,每个点具有非负整数值点权 a i a_i ai。每次操作可将某个特定结构上的所有点权减 1 1 1,且操作过程中点权不能为负。
求使所有点权变为 0 0 0 的最小操作次数。
下文将分别针对 G G G 为链、环、树、基环树的情形给出多种解法,并附上一些相似类型的题目。
1. 前置知识
1.1 差分
相信大家都会。
1.2 笛卡尔树
模板题:P5854。
笛卡尔树是一棵二叉树,每个节点有权值二元组 ( i , a i ) (i,a_i) (i,ai),要求下标 i i i 满足二叉搜索树性质,权值 a i a_i ai 满足堆性质。
笛卡尔树可在 O ( n ) O(n) O(n) 时间内构建。顺序插入每个 a i a_i ai,可知新节点一定位于当前根的右链上。自底向上比较右链节点的权值,若满足偏序关系,则将新节点挂为右子,原右子树变为其左子树。可用单调栈维护右链。参考代码:
cpp
int n, a[N], l[N], r[N];
void build() {
stack<int> st;
for (int i = 1; i <= n; i++) {
while (!st.empty() && a[st.top()] > a[i]) l[i] = st.top(), st.pop();
if (!st.empty()) r[st.top()] = i;
st.emplace(i);
}
}
笛卡尔树可视为一种分治结构:找到当前区间最大值,然后从最大值两段劈开继续递归。
在铲雪问题中,我们利用的正是这种基于最小值的分治特性,并可在笛卡尔树上进行 DP 来实现 O ( n ) O(n) O(n) 复杂度。
1.3 对偶原理
1.3.1 线性规划与对偶
我们称一个线性规划问题的标准形式形如
min ∑ i = 1 n c i x i subject to ∑ j = 1 n a i , j x j ≤ b i , i = 1 , ⋯ , m x i ≥ 0 , i = 1 , ⋯ , n . \begin{aligned} \min & \sum {i=1}^n c_i x_i\\ \text{subject to } & \sum{j=1}^n a_{i,j} x_j \le b_i, i=1,\cdots,m\\ & x_i \ge 0, i = 1, \cdots, n. \end{aligned} minsubject to i=1∑ncixij=1∑nai,jxj≤bi,i=1,⋯,mxi≥0,i=1,⋯,n.
用自然语言描述,就是找到 n n n 个非负实数 x i x_i xi,同时给出 m m m 条形如 ∑ j = 1 n a i , j x j ≤ b i \sum \limits_{j=1}^n a_{i,j} x_j \le b_i j=1∑nai,jxj≤bi 的约束,要求最小化 ∑ i = 1 n c i x i \sum \limits_{i=1}^n c_i x_i i=1∑ncixi。
称以上问题的对偶问题为
max ∑ i = 1 m b i y i subject to ∑ j = 1 m a j , i x j ≥ c i , i = 1 , ⋯ , n y i ≥ 0 , i = 1 , ⋯ , m . \begin{aligned} \max & \sum {i=1}^m b_i y_i\\ \text{subject to } & \sum{j=1}^m a_{j,i} x_j \ge c_i, i=1,\cdots,n\\ & y_i \ge 0, i = 1, \cdots, m. \end{aligned} maxsubject to i=1∑mbiyij=1∑maj,ixj≥ci,i=1,⋯,nyi≥0,i=1,⋯,m.
也就是 b i , c i b_i,c_i bi,ci 互换,系数矩阵转置,然后小于号变成大于号。
对于一般的线性规划,有强对偶定理:线性规划问题的解与其对偶问题的解相等,只要原问题或对偶问题之一可行。
1.3.2 铲雪问题的对偶
对于普通铲雪,我们总是能得到这样一个线性规划:设集簇 I \mathcal I I 表示所有合法路径,对每条路径 S S S 给出一个非负整数 x S x_S xS 表示 S S S 被经过的次数,原问题即:
min ∑ S ∈ I x S subject to ∑ u ∈ S x S = a u , x S ≥ 0. \begin{aligned} \min & \sum {S \in \mathcal{I}} x_S\\ \text{subject to } & \sum{u \in S} x_S=a_u, \\ & x_S \ge 0. \end{aligned} minsubject to S∈I∑xSu∈S∑xS=au,xS≥0.
拆成不等式以得到标准形式:
min ∑ S ∈ I x S subject to ∑ u ∈ S x S ≥ a u , ∑ u ∈ S − x S ≥ − a u , x S ≥ 0. \begin{aligned} \min & \sum {S \in \mathcal{I}} x_S\\ \text{subject to } & \sum{u \in S} x_S \ge a_u, \\ & \sum_{u \in S} -x_S \ge -a_u, \\ & x_S \ge 0. \end{aligned} minsubject to S∈I∑xSu∈S∑xS≥au,u∈S∑−xS≥−au,xS≥0.
得到对偶问题
max ∑ u a u ( p u − q u ) subject to ∑ u ∈ S ( p u − q u ) ≤ 1 , p u , q u ≥ 0. \begin{aligned} \max & \sum {u} a_u(p_u-q_u)\\ \text{subject to } & \sum{u \in S} (p_u-q_u) \le 1, \\ & p_u,q_u \ge 0. \end{aligned} maxsubject to u∑au(pu−qu)u∈S∑(pu−qu)≤1,pu,qu≥0.
换元,令 w u = p u − q u w_u=p_u-q_u wu=pu−qu,问题变为
max ∑ u a u w u subject to ∑ u ∈ S w u ≤ 1. \begin{aligned} \max & \sum {u} a_uw_u\\ \text{subject to } & \sum{u \in S} w_u \le 1. \end{aligned} maxsubject to u∑auwuu∈S∑wu≤1.
用自然语言表述这个问题,相当于找到一组整数 w u w_u wu,对所有满足条件的路径, w u w_u wu 之和不大于 1 1 1,最大化 a u w u a_uw_u auwu 的和。
这个问题有一个比较通用的 DP 做法。
2. 序列铲雪
给定序列 a i a_i ai,每次操作可将一个区间内的所有元素减 1 1 1,且操作过程中元素值不能为负。
求使所有元素变为 0 0 0 的最小操作次数。
请读者重视这道题目。下面给出的四种方法都是解决铲雪题的常用思路。
2.1 差分法
用差分刻画操作。约定 a 0 = a n + 1 = 0 a_0=a_{n+1}=0 a0=an+1=0,令 d i = a i − a i − 1 d_i=a_i-a_{i-1} di=ai−ai−1,则:
- 对区间 [ l , r ] [l,r] [l,r] 的操作等价于 d l ← d l + 1 , d r + 1 ← d r + 1 − 1 d_l \gets d_l+1,\ d_{r+1} \gets d_{r+1}-1 dl←dl+1, dr+1←dr+1−1。
- 目标状态为 ∀ 1 ≤ i ≤ n , d i = 0 \forall 1 \le i \le n,\ d_i=0 ∀1≤i≤n, di=0。
注意到 ∑ i = 1 n + 1 d i = 0 \sum \limits_{i=1}^{n+1} d_i = 0 i=1∑n+1di=0,因此操作总是可行的。进一步,每个正差分必然与某个负差分配对,因此答案至少为所有正差分之和。
若将所有正差分消为 0 0 0 后仍不合法,则与可行性矛盾。故答案为:
∑ i = 1 n max ( 0 , d i ) = 1 2 ∑ i = 1 n + 1 ∣ d i ∣ \sum_{i=1}^n \max(0,d_i)=\dfrac{1}{2} \sum_{i=1}^{n+1} |d_i| i=1∑nmax(0,di)=21i=1∑n+1∣di∣
这是一个经典结论。等号两侧的式子是等价的,两种形式在后续问题中均有用到。
cpp
int n, a[N];
void _main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
int res = 0;
for (int i = 1; i <= n; i++) res += max(0LL, a[i] - a[i - 1]);
cout << res;
}
2.2 插头法
插头是铲雪题里一个重要的概念。在本题中,我们定义一个插头为一条可以向左右延伸的操作区间。
假设已处理完左侧,当前位于位置 i i i,左侧传来的右插头数量为 k k k,则:
- 若 k ≥ a i k \ge a_i k≥ai,则全部使用这些插头,并生成 a i a_i ai 个新的右插头;
- 若 k < a i k < a_i k<ai,则使用全部插头后还需额外进行 a i − k a_i-k ai−k 次操作,并生成 a i a_i ai 个新的右插头。
不难发现这个过程和差分法得到的结论是等价的。
cpp
int n, a[N];
void _main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
int res = 0, k = 0;
for (int i = 1; i <= n; i++) {
if (k < a[i]) res += a[i] - k;
k = a[i];
} cout << res;
}
2.3 笛卡尔树法
这个方法基于一个观察:每次操作应尽量选择更长的区间。
考虑区间 [ l , r ] [l,r] [l,r],我们希望每次操作都能覆盖整个区间。这样的操作次数有一个上界,即区间最小值 a x a_x ax。
设区间最小值为 a x a_x ax,可将 [ l , r ] [l,r] [l,r] 中每个数减去 a x a_x ax,此时 a x a_x ax 变为 0 0 0,不能再参与操作。之后递归处理 [ l , x − 1 ] [l,x-1] [l,x−1] 与 [ x + 1 , r ] [x+1,r] [x+1,r]。
形式化地,设 f ( l , r , v ) f(l,r,v) f(l,r,v) 表示区间 [ l , r ] [l,r] [l,r] 在已减去 v v v 的基础上还需的最小操作次数,则有:
f ( l , r , v ) = f ( l , x − 1 , v + a x ) + a x − v + f ( x + 1 , r , v + a x ) f(l,r,v)=f(l,x-1,v+a_x)+a_x-v+f(x+1,r,v+a_x) f(l,r,v)=f(l,x−1,v+ax)+ax−v+f(x+1,r,v+ax)
使用普通的 RMQ 可以做到 O ( n log n ) O(n \log n) O(nlogn)。注意到这是一个"按照最小值分裂区间"的分治结构,小根笛卡尔树上 DP 即可做到 O ( n ) O(n) O(n)。
cpp
int n, a[N], rt, l[N], r[N], ans;
void build() {
stack<int> st;
for (int i = 1; i <= n; i++) {
while (!st.empty() && a[st.top()] > a[i]) l[i] = st.top(), st.pop();
if (!st.empty()) r[st.top()] = i;
else rt = i;
st.emplace(i);
}
}
void dfs(int u) {
if (l[u]) ans += a[l[u]] - a[u], dfs(l[u]);
if (r[u]) ans += a[r[u]] - a[u], dfs(r[u]);
}
void _main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
build();
ans += a[rt], dfs(rt), cout << ans;
}
2.4 线性规划法
考虑对偶问题:找到一组整数 w i w_i wi,对所有区间 w i w_i wi 之和不大于 1 1 1,最大化 a i w i a_iw_i aiwi 的和。
显然 w i ≤ 1 w_i \le 1 wi≤1。同时 w i ≤ − 2 w_i \le -2 wi≤−2 是无意义的,因为它并不能抵消掉 w i = 1 w_i=1 wi=1 的影响。因此 w u ∈ { − 1 , 0 , 1 } w_u \in \{-1,0,1\} wu∈{−1,0,1}。
借鉴 2.2 的插头思想,定义一个插头为延伸到当前位置的最大 w i w_i wi 和。注意这里的插头概念不同,请注意区分。
考虑顺序扫描 DP。对于 i i i 位置的决策,有如下四种情况:
- 选择 w i = − 1 w_i=-1 wi=−1,右插头为 0 0 0。
- 选择 w i = 0 w_i=0 wi=0,右插头为 0 0 0。
- 选择 w i = 0 w_i=0 wi=0,右插头为 1 1 1。
- 选择 w i = 1 w_i=1 wi=1,右插头为 1 1 1。
设计 DP 状态为 f 0 / 1 / 2 / 3 , i f_{0/1/2/3,i} f0/1/2/3,i 表示 i i i 位置的决策是第 0 / 1 / 2 / 3 0/1/2/3 0/1/2/3 种情况时,前缀 [ 1 , i ] [1,i] [1,i] 的最大得分。则容易写出转移:
f 0 , i = − a i + f 0 / 1 / 2 / 3 , i − 1 f 1 , i = f 0 / 1 , i − 1 f 2 , i = f 2 / 3 , i − 1 f 3 , i = a i + f 0 / 1 , i − 1 \begin{aligned} f_{0,i}&=-a_i+f_{0/1/2/3,i-1}\\ f_{1,i}&=f_{0/1,i-1}\\ f_{2,i}&=f_{2/3,i-1}\\ f_{3,i}&=a_i+f_{0/1,i-1} \end{aligned} f0,if1,if2,if3,i=−ai+f0/1/2/3,i−1=f0/1,i−1=f2/3,i−1=ai+f0/1,i−1
答案为 f 0 / 1 / 2 / 3 , n f_{0/1/2/3,n} f0/1/2/3,n。
cpp
int n, a[N], f[4][N];
void _main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) {
f[0][i] = -a[i] + max({f[0][i - 1], f[1][i - 1], f[2][i - 1], f[3][i - 1]});
f[1][i] = max(f[0][i - 1], f[1][i - 1]);
f[2][i] = max(f[2][i - 1], f[3][i - 1]);
f[3][i] = a[i] + max(f[0][i - 1], f[1][i - 1]);
} cout << max({f[0][n], f[1][n], f[2][n], f[3][n]});
}
事实上,对于序列铲雪,我们有更强的结论:存在一个最优解,使得 w i w_i wi 除 0 0 0 外 1 , − 1 1,-1 1,−1 交替。但是上面给出的 DP 是通用的方法,请读者务必理解。
3. 环上铲雪
AT_arc136_c [ARC136C] Circular Addition。
给定环 a i a_i ai,每次操作可将一个区间(或整个环)上的所有元素减 1 1 1,且操作过程中元素值不能为负。
求使所有元素变为 0 0 0 的最小操作次数。
3.1 差分法
用环上差分刻画操作。设 d i = a i − a i − 1 d_i=a_i-a_{i-1} di=ai−ai−1,特别地 d 1 = a 1 − a n d_1=a_1-a_n d1=a1−an。
注意到序列铲雪的结论 D = 1 2 ∑ i = 1 n ∣ d i ∣ D=\dfrac{1}{2} \sum \limits_{i=1}^n |d_i| D=21i=1∑n∣di∣,这是答案的一个下界,因为一次操作至多让 D D D 减去 1 1 1,而目标是 D ← 0 D \gets 0 D←0。
但是由于环的特殊结构,只考虑 D D D 会导致存在 a i > 0 a_i >0 ai>0,如样例 #3。注意到另外一个下界: M = max i = 1 n a i M=\max \limits_{i=1}^n a_i M=i=1maxnai。
给出一个引理:当环中存在 0 0 0 时,必有 M < D M<D M<D。
证明:因为 D \\ge \\max \\limits_{i=1}\^n a_i-\\min \\limits_{i=1}\^n a_i ,后一项为 0 0 0 时 D ≥ M D \ge M D≥M,矛盾。
下面的充分性证明可能比较抽象,建议自己举一些例子来理解。
- 当 M = D M=D M=D 时,环中不存在 0 0 0,选择一个最小的包含了所有最大值的区间即可令 M ← M − 1 , D ← D − 1 M \gets M-1,D \gets D-1 M←M−1,D←D−1。
- 当 M > D M > D M>D 时,环中不存在 0 0 0,对整环操作即可令 M ← M − 1 M \gets M-1 M←M−1, D D D 至多减去 1 1 1。可以规约到 M = D M=D M=D 的情况。
- 当 M < D M < D M<D 时,选择一个全为最大值的极长区间,则 D ← D − 1 D \gets D-1 D←D−1, M M M 至多减去 1 1 1。同样规约到 M = D M=D M=D 的情况。
因此,我们证明了答案为 max ( M , D ) \max(M,D) max(M,D) 的充分性。
3.2 插头法
根据 2.2 的"尽量使用已有插头"思想,我们找一个位置断环为链以后,顺序扫描序列,初始时认为有 a 1 a_1 a1 个插头。
根据贪心思想,我们应该从最小值处断环为链。
本题的特殊性为存在"整环"这一类操作,而 2.2 的做法无法区分链头的插头属于环还是区间。
为此,我们记录 A A A 为从 1 1 1 开始的插头数目, B B B 为有潜力延伸到 1 1 1 的插头数目, C C C 为其它类型的插头数目。如下决策:
- 若 a i < a i + 1 a_i<a_{i+1} ai<ai+1,此时新产生 d = a i + 1 − a i d=a_{i+1}-a_i d=ai+1−ai 个插头,我们希望其尽量归入 B B B。
- 对于 B B B 的变化量 Δ B \Delta B ΔB,显然有 Δ B ≤ d \Delta B \le d ΔB≤d。
- 新的插头和已有的 B B B 类插头数之和不能超过用过的 A A A 类插头数目。即 Δ B ≤ a 1 − A − B \Delta B \le a_1-A-B ΔB≤a1−A−B。
- 两个上界取 min \min min,剩余插头归入 C C C 类。
- 令 B ← B + Δ B B \gets B + \Delta B B←B+ΔB, C ← C + d − Δ B C \gets C+d-\Delta B C←C+d−ΔB,同时支付 d d d 的代价。
- 若 a i > a i + 1 a_i>a_{i+1} ai>ai+1,则可以用 d = a i − a i + 1 d=a_i-a_{i+1} d=ai−ai+1 个插头。此时希望尽量保护 B B B 类插头,故按 A , C , B A,C,B A,C,B 的顺序使用插头。
最终答案加上 a 1 a_1 a1,再减去 B B B 个与 a 1 a_1 a1 形成整环的操作。
cpp
#define int long long
const int N = 2e5 + 5;
int n, a[N];
void S(int& x, int& y) {
int d = min(x, y);
x -= d, y -= d;
}
void _main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
rotate(a + 1, min_element(a + 1, a + n + 1), a + n + 1);
int A = a[1], B = 0, C = 0, res = 0;
for (int i = 1; i < n; i++) {
if (a[i] < a[i + 1]) {
int d = a[i + 1] - a[i];
int nB = min(d, max(0LL, a[1] - A - B));
B += nB, C += d - nB, res += d;
} else if (a[i] > a[i + 1]) {
int d = a[i] - a[i + 1];
S(A, d), S(C, d), S(B, d);
}
} cout << res + a[1] - B;
}
3.3 线性规划法
对偶问题的约束在环上更强:起点与终点的插头不能同时为 1 1 1。
任意找一个点断环为链,做一次 2.4 的线性 DP。我们只需钦定终点的右插头为 0 0 0,即可防止起终点的插头同时为 1 1 1 的情况。而如果钦定终点的插头为 1 1 1,则需要起点插头为 0 0 0,只需要倒着 DP 一遍。
考虑一个特殊情况:存在唯一 i i i 使得的 w i = 1 w_i=1 wi=1,其余全选 0 0 0,此时起终点的插头同时为 1 1 1 但合法,这种情况的最大得分就是 max i = 1 n a i \max \limits_{i=1}^n a_i i=1maxnai。需要加上这个特判。
cpp
int n, a[N], f[4][N];
void _main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
int res = *max_element(a + 1, a + n + 1);
auto work = [&]() -> void {
for (int i = 1; i <= n; i++) {
f[0][i] = -a[i] + max({f[0][i - 1], f[1][i - 1], f[2][i - 1], f[3][i - 1]});
f[1][i] = max(f[0][i - 1], f[1][i - 1]);
f[2][i] = max(f[2][i - 1], f[3][i - 1]);
f[3][i] = a[i] + max(f[0][i - 1], f[1][i - 1]);
}
res = max({res, f[0][n], f[1][n]});
};
work(), reverse(a + 1, a + n + 1), work();
cout << res;
}
4. 树上铲雪
给定一棵树,点权为 a i a_i ai,每次操作可将一条树链上的所有点权减 1 1 1,且操作过程中点权不能为负。
求使所有点权变为 0 0 0 的最小操作次数。
4.1 插头法
定义一个插头为一条可以向祖先 / 兄弟延伸的操作 。对于节点 u u u,要求其所有儿子已处理完毕且点权清零。
设 f u f_u fu 为 u u u 节点的插头数目,记 s u = ∑ v ∈ son(u) f v s_u = \sum \limits_{v \in \operatorname{son(u)}} f_v su=v∈son(u)∑fv。
来自不同儿子、延伸至同一节点的两个插头有两种处理方式:在 u u u 处合并,或继续向上延伸。设 c u c_u cu 为 u u u 节点处线头合并次数,则它对答案的贡献为 max ( 0 , a u − s u + c u ) − c u \max(0,a_u-s_u+c_u)-c_u max(0,au−su+cu)−cu。
我们希望尽可能合并插头以减少操作次数,这与 2.2 中"尽量使用已有插头"的思想一致。
可以通过三个简单的讨论卡到 c u c_u cu 的上界:
- 显然 c u ≤ a u c_u \le a_u cu≤au。
- 插头合并是两两配对,故 c u ≤ ⌊ s u 2 ⌋ c_u \le \left\lfloor \dfrac{s_u}{2} \right \rfloor cu≤⌊2su⌋。
- 如果 f v f_v fv 中存在一个很大的值,将其他插头都与此处的插头合并后,此处的插头仍然无法配对,因此 c u ≤ s u − max v ∈ son ( u ) f v c_u \le s_u - \max \limits_{v \in \operatorname{son}(u)} f_v cu≤su−v∈son(u)maxfv。
直接取交集是不对的。
仔细分析一下,设 S S S 为 u u u 节点未合并的插头数目。如果 u u u 已经是根节点,我们只希望最小化代价,那么代价 W W W 的一个上界为:将所有子树独立处理后加上 a u a_u au。下面的讨论中,我们将 W W W 取到这个上界再向下调整。
- 先做完所有插头合并操作,则 f u = max ( a u , S ) f_u=\max(a_u,S) fu=max(au,S)。
- 一次插头合并中, a u ← a u − 1 a_u \gets a_u-1 au←au−1, S ← S − 2 S \gets S-2 S←S−2, W ← W − 2 W \gets W-2 W←W−2。
- 一次插头延伸中, a u ← a u − 1 a_u \gets a_u-1 au←au−1, S S S 不变, W ← W − 1 W \gets W-1 W←W−1。
容易发现,存在一个时刻,使得在这个时刻之前,插头合并一定不劣于插头延伸,而之后插头延伸更优。具体地:
- 当 S ≤ a u S \le a_u S≤au 时,一次插头合并使得 f u ← f u − 2 f_u \gets f_u-2 fu←fu−2,一次插头合并等效于两次插头延伸,因此延伸更优;
- 当 S > a u S > a_u S>au 时, f u f_u fu 至多减少 1 1 1,一次插头合并等效于一次插头延伸,而 W W W 更小,故合并更优。
综上,得到 c u c_u cu 的第四个上界: c u ≤ s u − a u c_u \le s_u-a_u cu≤su−au。自底向上有递推式:
f u = a u − c u c u = max ( 0 , min ( a u , ⌊ s u 2 ⌋ , s u − max v ∈ son ( u ) f v , s u − a u ) ) \begin{aligned} f_u&=a_u-c_u\\ c_u&=\max\left(0,\min\left( a_u, \left\lfloor \dfrac{s_u}{2} \right \rfloor, s_u - \max \limits_{v \in \operatorname{son}(u)} f_v, s_u-a_u \right)\right) \end{aligned} fucu=au−cu=max(0,min(au,⌊2su⌋,su−v∈son(u)maxfv,su−au))
进一步,当 u u u 不是根节点时,存在让 W W W 较大而增加 f u f_u fu 的策略。考虑调整法说明该策略不优:
- 当 s u ≤ a u s_u \le a_u su≤au 时, f u ← f u + 1 f_u \gets f_u+1 fu←fu+1 使得 W ← W + 2 W \gets W+2 W←W+2,显然劣于原策略。
- 当 s u > a u s_u > a_u su>au 时,从 c u c_u cu 的递推式可以看出 c u = 0 c_u=0 cu=0,即不存在线头合并操作,自然也就无法调整了。
cpp
int f[N], s[N], c[N], ans;
void dfs(int u, int fa) {
int mx = 0;
for (int v : e[u]) {
if (v == fa) continue;
dfs(v, u), s[u] += f[v], mx = max(mx, f[v]);
}
c[u] = max(0LL, min({a[u], s[u] - a[u], s[u] / 2, s[u] - mx}));
ans += max(0LL, a[u] - s[u] + c[u]) - c[u];
f[u] = a[u] - c[u];
}
4.2 线性规划法
依旧解决对偶问题。
照搬 2.4 的做法,定义一个插头为延伸到当前位置的最大 w i w_i wi 和 ,设 f 0 / 1 / 2 / 3 , u f_{0/1/2/3,u} f0/1/2/3,u 为已经完成 u u u 子树内的决策, u u u 节点的决策是第 0 / 1 / 2 / 3 0/1/2/3 0/1/2/3 种情况时的最大得分。
容易写出四种情况的转移:
f 0 , u = − a u + ∑ v ∈ son ( u ) f 0 / 1 / 2 / 3 , v f 1 , u = ∑ v ∈ son ( u ) f 0 / 1 , v f 2 , u = g ( u ) + ∑ v ∈ son ( u ) f 0 / 1 , v f 3 , u = a u + ∑ v ∈ son ( u ) f 0 / 1 , v \begin{aligned} f_{0,u}&=-a_u+\sum {v \in \operatorname{son}(u)} f{0/1/2/3,v}\\ f_{1,u}&=\sum {v \in \operatorname{son}(u)} f{0/1,v}\\ f_{2,u}&=g(u)+\sum {v \in \operatorname{son}(u)} f{0/1,v}\\ f_{3,u}&=a_u+\sum {v \in \operatorname{son}(u)} f{0/1,v} \end{aligned} f0,uf1,uf2,uf3,u=−au+v∈son(u)∑f0/1/2/3,v=v∈son(u)∑f0/1,v=g(u)+v∈son(u)∑f0/1,v=au+v∈son(u)∑f0/1,v
这里 g ( u ) g(u) g(u) 的意思是,对于 f 2 , u f_{2,u} f2,u,我们需要从 u u u 的儿子中选择一个,钦定其插头为 1 1 1,其余儿子插头为 0 0 0。可以按得分差值选择,即
g ( u ) = max v ∈ son ( u ) f 2 / 3 , v − f 0 / 1 , v g(u)=\max_{v \in \operatorname{son}(u)} f_{2/3,v}-f_{0/1,v} g(u)=v∈son(u)maxf2/3,v−f0/1,v
cpp
int n, a[N], f[4][N];
void dfs(int u, int fa) {
f[0][u] = -a[u], f[3][u] = a[u];
int mx = 0;
for (int v : e[u]) {
if (v == fa) continue;
dfs(v, u);
f[0][u] += max({f[0][v], f[1][v], f[2][v], f[3][v]});
f[1][u] += max(f[0][v], f[1][v]);
f[2][u] += max(f[0][v], f[1][v]);
mx = max(mx, max(f[2][v], f[3][v]) - max(f[0][v], f[1][v]));
f[3][u] += max(f[0][v], f[1][v]);
}
f[2][u] += mx;
}
5. 基环树铲雪
给定一棵基环树,点权为 a i a_i ai,每次操作可将一条树链(或整个环)上的所有点权减 1 1 1,且操作过程中点权不能为负。
求使所有点权变为 0 0 0 的最小操作次数。
5.1 线性规划法
解决对偶问题。
首先找出环,对环上挂的每棵树跑一遍 4.2 的 DP,结果记作 f 0 / 1 / 2 / 3 , u f_{0/1/2/3,u} f0/1/2/3,u。
将环上的点从 1 1 1 开始重编号,然后仿照 3.3 进行环上 DP。先任找一个点断环为链,然后设 g 0 / 1 / 2 / 3 , u g_{0/1/2/3,u} g0/1/2/3,u 为第 u u u 个点的决策为 0 / 1 / 2 / 3 0/1/2/3 0/1/2/3,已经完成 [ 1 , u ] [1,u] [1,u] 中决策后的最大得分。
转移如下:
g 0 , u = f 0 , u + g 0 / 1 / 2 / 3 , u − 1 g 1 , u = f 1 , u + g 0 / 1 , u − 1 g 2 , u = max ( f 0 / 1 , u + g 2 / 3 , u − 1 , f 2 , u + g 0 / 1 , u − 1 ) g 3 , u = f 3 , u + g 0 / 1 , u − 1 \begin{aligned} g_{0,u}&=f_{0,u}+g_{0/1/2/3,u-1}\\ g_{1,u}&=f_{1,u}+g_{0/1,u-1}\\ g_{2,u}&=\max(f_{0/1,u}+g_{2/3,u-1},f_{2,u}+g_{0/1,u-1})\\ g_{3,u}&=f_{3,u}+g_{0/1,u-1} \end{aligned} g0,ug1,ug2,ug3,u=f0,u+g0/1/2/3,u−1=f1,u+g0/1,u−1=max(f0/1,u+g2/3,u−1,f2,u+g0/1,u−1)=f3,u+g0/1,u−1
我们仍然可以使用 3.3 的做法,钦定终点插头为 0 0 0,然后倒序再跑一次 DP。
同时,3.3 中的特判仍然需要,即环上有且仅有一个 w i = 1 w_i=1 wi=1。此时的选法类似 4.2 中的 g ( u ) g(u) g(u),选出唯一的点插头为 1 1 1,其余插头为 0 0 0,按得分差值选择。
使用拓扑排序找环,可以做到严格线性。
cpp
int n, a[N], deg[N], f[4][N], g[4][N];
bool on[N], tag[N];
vector<int> path, e[N];
void bfs() {
fill(on + 1, on + n + 1, true);
queue<int> q;
for (int i = 1; i <= n; i++) {
if (deg[i] == 1) q.emplace(i), on[i] = false;
}
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : e[u]) {
if (--deg[v] == 1) q.emplace(v), on[v] = false;
}
}
int s = find(on + 1, on + n + 1, true) - on, u = s;
while (true) {
path.emplace_back(u), tag[u] = true;
bool flag = true;
for (int v : e[u]) {
if (on[v] && !tag[v]) u = v, flag = false;
}
if (flag) break;
}
}
void dfs(int u, int fa) {
f[0][u] = -a[u], f[3][u] = a[u];
int mx = 0;
for (int v : e[u]) {
if (v == fa || on[v]) continue;
dfs(v, u);
f[0][u] += max({f[0][v], f[1][v], f[2][v], f[3][v]});
f[1][u] += max(f[0][v], f[1][v]);
f[2][u] += max(f[0][v], f[1][v]);
mx = max(mx, max(f[2][v], f[3][v]) - max(f[0][v], f[1][v]));
f[3][u] += max(f[0][v], f[1][v]);
}
f[2][u] += mx;
}
void _main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1, u, v; i <= n; i++) {
cin >> u >> v;
e[u].emplace_back(v), e[v].emplace_back(u);
deg[u]++, deg[v]++;
}
bfs();
for (int i : path) dfs(i, -1);
int m = path.size(), res = 0, mx = 0;
for (int i : path) res += max(f[0][i], f[1][i]), mx = max(mx, max(f[2][i], f[3][i]) - max(f[0][i], f[1][i]));
res += mx;
auto work = [&]() -> void {
for (int i = 1; i <= m; i++) {
int u = path[i - 1];
g[0][i] = f[0][u] + max({g[0][i - 1], g[1][i - 1], g[2][i - 1], g[3][i - 1]});
g[1][i] = f[1][u] + max(g[0][i - 1], g[1][i - 1]);
g[2][i] = max(
max(f[0][u], f[1][u]) + max(g[2][i - 1], g[3][i - 1]),
f[2][u] + max(g[0][i - 1], g[1][i - 1])
);
g[3][i] = f[3][u] + max(g[0][i - 1], g[1][i - 1]);
}
res = max({res, g[0][m], g[1][m]});
};
work(), reverse(path.begin(), path.end()), work();
cout << res;
}