十二重铲雪法(上)

本文首发于洛谷平台,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. 序列铲雪

P1969 [NOIP 2013 提高组] 积木大赛

给定序列 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. 树上铲雪

P7246 手势密码

给定一棵树,点权为 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;
}
相关推荐
愚者游世1 小时前
long long各版本异同
开发语言·c++·程序人生·职场和发展
ccLianLian2 小时前
计算机基础·cs336·RLHF
深度学习·算法
上海合宙LuatOS2 小时前
LuatOS核心库API——【hmeta 】硬件元数据
单片机·嵌入式硬件·物联网·算法·音视频·硬件工程·哈希算法
滴滴答滴答答3 小时前
LeetCode Hot100 之 17 合并区间
算法·leetcode·职场和发展
你怎么知道我是队长3 小时前
C语言---排序算法8---递归快速排序法
c语言·算法·排序算法
007张三丰3 小时前
软件测试专栏(5/20):自动化测试入门指南:从零开始构建你的第一个测试框架
自动化测试·python·算法·压力测试·测试框架
Zachery Pole3 小时前
根据高等代数与数分三计算线性回归中的w
算法·回归·线性回归
得一录3 小时前
星图·全参数调试qwen3.1-B
深度学习·算法·aigc
yyjtx3 小时前
DHU上机打卡D22
算法