[NOI]2024 登山 题解
好像在洛谷题解区里还没人和我做法一样,,?
考场做法,只用到了倍增和线段树,感觉挺好写。考场上从开题到过题只用了 2h。
最底下有省流版(?)。
以下是我考场里比较详细的思路,所以比较长。
先考虑如何 \(O(n^2)\) 做,然后再想优化。
容易先想到一个状态数是 \(O(n^2)\) 的 DP,即记录起点,并将向上跳多少的限制记在状态里。这个 DP 的转移顺序看起来比较的混乱。那么我们再观察一下这个转移的过程。
记一个节点 \(x\) 的限制为至少跳到深度为 \(v_x\) 的祖先,\(v_x=d_x-(h_x+1)\)。我们改 \(l_x\) 与 \(r_x\) 的定义为能跳到的祖先深度区间。
假如说我现在以节点 \(x\) 作为起点,首先有个限制 \(lim=v_x\),然后接下来走一步会有两种选择:
- 向上跳到祖先 \(p\),那么限制会变为 \(lim=\min(lim,v_p)\),容易发现一定会是 \(lim=v_p\)。
- 向下走到一个儿子 \(y\),那么限制会变为 \(lim=\min(lim,v_y)\)。
在第一种情况里,限制只跟 \(v_p\) 有关。因此可以看作以节点 \(p\) 作为起点。那么此时就变成了一个相同的子问题。
在第二种情况里,若 \(lim\) 不变,那么限制还是 \(v_x\);若 \(lim\) 发生了改变,那么限制就只跟 \(v_y\) 有关了。假如说 \(lim\) 发生了改变,那么就又可以将 \(y\) 作为一个新的起点。
那么此时我们就有了一个状态数 \(O(n)\) 的 DP:\(f(x)\) 表示以 \(x\) 作为起点的方案数。
由上述分析我们容易知道转移的顺序:因为 \(lim\) 是越来越小的,所以我们按 \(v_x\) 从小到大求出每个 \(f(x)\) 即可。
那么我们也容易得知求 \(f(x)\) 的一个暴力转移:
从 \(x\) 开始向子树内 BFS,会遇到两种点。
- 如果碰到了已经求过 DP 值的节点,那么这个节点的限制一定比 \(x\) 严格,直接用这个节点的 DP 值更新 \(f(x)\)。
- 若 BFS 到的当前节点还没有求出 DP 值,那么计算从这个节点向上跳的方案数(即将其可以向上跳到的节点与 \(x\) 的限制求交得到的所有节点的 DP 值之和),然后继续向下 BFS。
这个 DP 暴力转移是 \(O(n^2)\) 的,瓶颈在于上述 BFS 的过程。
对于 1 类点来说,我们希望找到从 \(x\) 向下 BFS 第一次碰到的所有限制严于 \(x\) 的所有节点的 DP 值。
对于 2 类点来说,我们希望把 \(x\) 子树中比 \(x\) 限制更严的点的子树删掉,剩下的点就是 2. 中的情况。
此时我不知道从何处下手了,接着想到了一个不知道能不能继续做的做法:求了根号次 DP 值就重构?但是这个对我来说太难往下继续思考了。
于是我先看了部分分:树为一条链。
这种情况下,对于 \(x\) 来说,向它子树内 BFS 时碰到的 1 类点只有一个,2 类点形成了一段区间。我们记 \(nxt_x\) 代表 \(x\) 子树内的 1 类点(即 \([x+1,n]\) 中第一个限制严于 \(x\) 的点)。
那么 \(f(x)\) 就很好求了(注意 \(l_x,r_x\) 的含义与原题中的含义不同,在题解一开始的时候讲过):
其中 \(\sum f(i)\) 可以用前缀和表示,记 \(s(x)\) 为 \(f\) 的前缀和,那么
其中 \(-s(l_y-1)\) 与 \(x\) 无关,可以直接预处理,\(s(\min(r_y,v_x))\) 也可以根据 \(r_y\) 与 \(v_x\) 的大小关系来分类讨论,从而去掉 \(\min\)。
总结一下,我们得出了树为一条链的做法:从前往后扫描,碰到 \(l_y-1\) 的时候将 \(-s(l_y-1)\) 的贡献放在 \(y\) 上,碰到 \(r_y\) 的时候将 \(s(r_y)\) 的贡献放在 \(y\) 上。碰到 \(v_x\) 的时候求 \(f(x)\) 的值,我们需要求出 \((x,nxt_x)\) 中的贡献之和,以及有多少个 \(y\) 假了 \(-s(l_y-1)\) 的贡献但是没有加 \(s(r_y)\) 的贡献:这些 \(y\) 都对 \(x\) 产生了 \(s(v_x)\) 的贡献。
这个统计 2 类点对 \(x\) 造成贡献的方法可以很容易地拓展到树上。那么我们又回到了一个问题上:我们如何删掉一个子树?
此时我想到了一个做法:用 \(u(x)=0/1\) 代表 \(x\) 有没有被删除。求出一个点 \(x\) 的 DP 值时,将 \(x\) 子树里的点的 \(u\) 值全都改为 \(1\)。
这显然是个错的做法:我们 DFS 到 \(x\) 子树里的时候可是要用到这个子树里的点啊,怎么能删掉呢?可是我们 DFS 到 \(x\) 子树时,直接遍历 \(x\) 子树将 \(u\) 值全都变为 \(0\) 也是不对的,因为 \(x\) 子树里可能已经有要删掉的子树了。
那么我们可以更改 \(u(x)\) 的定义,将有没有被删除改成被删除了几次。这时候豁然开朗了:我惊奇地发现,\(x\) 子树里的 \(u\) 值,它们都 \(\ge u(x)\),并且 \(x\) 子树里的 2 类点,它们的 \(u\) 值都等于 \(u(x)\)!
(这个东西证明很简单,但是我考场上没有思考正确性我就不写了)
那么我们利用 DFS 序将子树变成区间,记录一个 pair,表示区间中 \(u\) 的最小值,以及 \(u\) 等于最小值的那些点的信息就能求出所有 2 类点的贡献了!
1 类点的贡献也迎刃而解了:在打删除标记的时候不将子树的根打标记,然后维护 \(u\) 最小值的 DP 值之和即可。(2 类点的 DP 值都为 \(0\) 所以 2 类点不会造成影响)
省流
记 \(f(x)\) 为从 \(x\) 开始走的答案。假如走到某个点,限制突然改变了那么就可以直接用这个点的 \(f\) 值用来更新 \(f(x)\)。
向上跳肯定会改变限制,那么考虑朝子树里走的情况:
- 朝子树里走到一个改变限制的点,需要求出这些点的 \(f\) 之和。
- 走到一个不改变限制的点,然后向上跳。
第二种情况是跳到一条祖先后代链,假如说找到了这些点就可以用前缀和解决。
那么我们来找这些点。我们求出一个点的 \(f\) 就对其子树里的所有点打上一个删除标记。记 \(u(x)\) 表示 \(x\) 被删除了几次。那么求 \(f(x)\) 时,所有 \(u\) 值等于 \(u(x)\) 的那些点都是第二种情况里的点。通过 DFS 序将子树变成区间,用线段树维护 pair 的方法维护区间内 \(u\) 最小值的信息即可。
第一种情况就是打删除标记时不将根打标记即可。复杂度 \(O(n\log n)\)。
代码只拍了照片,而且写的比较丑陋,如果某天我想写了就重写一份吧(