[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\),然后接下来走一步会有两种选择:

  1. 向上跳到祖先 \(p\),那么限制会变为 \(lim=\min(lim,v_p)\),容易发现一定会是 \(lim=v_p\)
  2. 向下走到一个儿子 \(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,会遇到两种点。

  1. 如果碰到了已经求过 DP 值的节点,那么这个节点的限制一定比 \(x\) 严格,直接用这个节点的 DP 值更新 \(f(x)\)
  2. 若 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\) 的含义与原题中的含义不同,在题解一开始的时候讲过):

\[f(x)=f(nxt_x)+\sum_{x<y<nxt_x}\sum_{l_y\leq i\leq \min(r_y,v_x)}f(i) \]

其中 \(\sum f(i)\) 可以用前缀和表示,记 \(s(x)\)\(f\) 的前缀和,那么

\[f(x)=f(nxt_x)+\sum_{x<y<nxt_x}s(\min(r_y,v_x))-s(l_y-1) \]

其中 \(-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)\)

向上跳肯定会改变限制,那么考虑朝子树里走的情况:

  1. 朝子树里走到一个改变限制的点,需要求出这些点的 \(f\) 之和。
  2. 走到一个不改变限制的点,然后向上跳。

第二种情况是跳到一条祖先后代链,假如说找到了这些点就可以用前缀和解决。

那么我们来找这些点。我们求出一个点的 \(f\) 就对其子树里的所有点打上一个删除标记。记 \(u(x)\) 表示 \(x\) 被删除了几次。那么求 \(f(x)\) 时,所有 \(u\) 值等于 \(u(x)\) 的那些点都是第二种情况里的点。通过 DFS 序将子树变成区间,用线段树维护 pair 的方法维护区间内 \(u\) 最小值的信息即可。

第一种情况就是打删除标记时不将根打标记即可。复杂度 \(O(n\log n)\)

代码只拍了照片,而且写的比较丑陋,如果某天我想写了就重写一份吧(

代码照片

posted @ 2024-08-19 22:16  tevenqwq  阅读(201)  评论(0编辑  收藏  举报