登山 NOI2024
登山 NOI2024
WZH[NOI2024] 登山
这是第一个我自己写出来的树形 DP 黑题。
题目链接: [NOI2024] 登山 。
题目描述:
“为什么要攀登?因为山就在那里。”
慕士塔格山上有 处点位,点从 到 编号, 号点位为山顶。这 个点位构成一棵有根树的结构,其中 号点位为根,对于 , 号点位的父亲结点为 号点位。
记 为 号点位到山顶所需经过的边数。形式化地说,,对于 ,。
定义一条登山路径为从 号点位中的某一个开始,经过若干次移动后到达山顶的方案。
定义一次从 号点位出发的移动为以下两种方式之一:
- 冲刺:在给定的冲刺范围 内,选择一个正整数 满足 ,向山顶移动 步,即移动至 号点位在有根树上的 级父亲处。保证 。
- 休息:由于慕士塔格山地形陡峭,休息时会滑落到某一个儿子结点处。形式化地说,选择一个满足 的 ,移动至到 号点位。特别地,若 号点位为有根树的叶子结点,则不存在满足 的 ,因此此时不能选择休息。
定义一条登山路径对应的登山序列为初始点位以及每次移动到的点位所构成的序列。形式化地说,一条从 号点位开始的登山路径对应的登山序列是一个点序列 满足对于 , 是 的 级祖先或 。
为了保证每次冲刺都能更接近山顶,一条合法的登山路径需要满足:对于初始点位或某次移动到的点位 ,以后冲刺到的点位 都必须满足 ,其中 是一个给定的参数,保证 。形式化地说,一条合法的登山路径对应的登山序列 需要满足:对于所有 ,若 ,则 。
对于 号所有点位,求从这些点位开始的合法的登山路径条数。两条登山路径不同当且仅当其对应的登山序列不同。由于答案可能较大,你只需要求出答案对 取模后的结果。
对于所有测试数据保证:,。
对于任意的 ,保证:,,。
测试点编号 是否有 是否有 是否有 否 否 否 ^ ^ ^ ^ ^ ^ 是 是 是 ^ ^ ^ 否 ^ ^ 否 是 ^ ^ ^ 否 ^ 否 是 是 ^ ^ ^ 否 ^ ^ 否 是 ^ ^ ^ 否
观察题目,可以发现这是一个计数题目,并且这题目还在树上,那么我们就只有树形 DP 这一个解决方案了。
:暴力树形 DP
有些题目,像这题,即使是最朴素的暴力 DP 也很难想。
首先,这个题目的决策点是树上的每个点,而决策是选择休息或冲刺。
那么自然可以得出第一个状态设计: 表示 冲向 的方案数。
那么按照决策,分两种情况:
- 冲刺。这种情况下需要冲刺到 级祖先。方案数为 。
- 休息。这种情况需要累加所有儿子的方案数,即 。但是由于有 的限制,下一次冲刺必须冲刺到 及以上的位置,此时无法转移。
所以我们要加一维。定义 表示从节点 出发,冲向 且下一次冲刺到深度 的方案数。
设 的深度为 的祖先为 。
接着按照决策,分三种情况:
- 不合法,即 。这种情况下 。
- 在 合法时,冲刺。此时需要满足 。这种情况方案数为 。
- 在 合法时,休息。此时可以滑落至任意一个儿子。这种情况的方案数为 。
然后又会发现,这样转移似乎有后效性,因为一个点会从他的祖先和儿子同时转移而来。
但仔细想后就会发现,其实每个点只会从他的儿子转移,于是从下到上合并儿子转移就行。这是因为对于祖先的转移是 ,因为 无法冲刺到它本身。
于是我们对着 这一维顺序树形 DP 就行。
时间复杂度 。
代码:
1 |
|
:优化版暴力树形 DP
可以发现,在上面的代码对着 转移时,所有可以冲刺到的点带来的贡献都是 , 是某个深度为 的点。
也就是说,对于 的祖先,他们的 DP 值都是 ,而对于 子树内的点,他们的深度为 的祖先都是 ,于是就可以把 级祖先的 去掉,复杂度变成 。
如果再想想就会发现,对于固定的 , 的转移只会用到 和 ,于是可以把 这一维压掉,空间由 优化至 。
空间 代码:
1 |
|
空间 代码:
1 |
|
:暴力线段树合并优化树形 DP
暴力部分结束了,接下来是优化 DP 。
首先,优化 DP 一般有两种:一种是改变状态优化,一种是利用 DP 的结构性质优化。
这个 DP 显然状态设计上无法优化,于是只能通过观察 DP 的结构性质。
一般的一个观察是抛弃 数组,直接利用 DP 结构进行优化,但很可惜这是无法优化的(至少我没想到)。
错误的思路:
考虑刻画 DP 结构。容易发现这个 DP 的结构本质上是对于每个 ,每个 ,若 对于 合法且 可以冲刺到 ,则找到最小深度的祖先 满足 对于 到 路径上的点合法,将路径上的点的 累加上 深度为 的祖先 的权值 。
可惜到这一步为止了,无法进一步优化。
刻画 DP 结构本质是不可能了,只能找到一些可以优化的地方进行优化。
我们发现 转移时,会将子节点 的 累加,这很像线段树合并。
进一步的,对于叶子节点,他们不能休息,只能冲刺。那么对于叶子 , 在一段区间里面有值。那么对于任意节点 , 里面最多只有 个区间(可重叠)有值,这正好对应线段树区间加和区间合并。
这引导我们去线段树合并。
但是这样做又有一个问题:直接合并就不能按 的顺序转移了,又有后效性了。
此时又需要一个极为 tricky 的想法( How have I thought out this tricky method? ):系数。
我们发现,对于 ,设他的深度 的祖先为 ,那么对于任意一个 子树内的节点 , 必然由 转移而来,同时 和 都有 转移而来,即, 。
当我们不按照 得顺序转移时,虽然我不能直接转移 ,但是前面的那个系数 与 无关,可以不按照 的顺序转移。那么我们就可以成功的线段树合并转移出系数。
对于每个点 :
- 先累加 自己的贡献,也就是 的位置,每个位置上的数 。
- 然后区间合并儿子,具体就是只合并区间 的位置。
这里面还有一个小细节,就是线段树合并不好处理区间加,这里可以用差分代替。但是注意,由于 有值的地方仅限于 ,也就是说,最后需要将区间 的和在 减去,不然会影响到 的父节点的合并。
这样就完成了系数 的合并。
接下来就是还原 的值。这一点可以通过自上而下还原。因为 一定永远是 的祖先,在转移 时, 必定被转移完了,所以无后效性。而且这里只需要维护和就行了,正好答案也求的是和,十分匹配。
具体来说就是遍历每个节点的线段树,同时累加前缀和。每到一个叶子节点,用前缀和加上叶子权值得到的结果与对应 相乘后返回。每到一个空节点,用前缀和乘上 的对应区间和就行。因为我们不关心具体值,只在意和。
然后就完成了。于是你自认为 AC 了此题。
分析一下复杂度:线段树合并是 的,遍历是 的(不要像我一样想当然的认为是 的),常数比普通的大许多,但是实际前 个点表现优秀。
代码:
1 |
|
:优化线段树合并优化树形 DP
其实做出上一步已经很难了(我是怎么在半小时内想出上面的部分的?),如果上面的做出来了,这一步也不难。
容易发现复杂度瓶颈在 relax 函数的遍历。但是理论上线段树合并的遍历和这个遍历,或许是差不多的。
但是实际上一个 一个 。
线段树合并的过程,为什么这个复杂度是 ?原因是线段树合并过程中出现了节点公用,即一个节点同时存在于多棵动态开点线段树,由 if(!p||!q)return p+q; 这里看出。
那么这个也意味着,如果 relax 函数中,一个节点每次被访问的贡献一样,那么我就可以记录下他的贡献,下次遇到是直接返回,这样复杂度就也是 了。
至于每次进来时的贡献,容易发现这个节点本身的贡献是平凡的,这里指这个线段子树内的贡献,不包含这个数以前的前缀和贡献(即 mx )。这是因为这个点线段子树内的权值一样,而会共用节点的线段树必然在树上构成祖孙关系,也就是说,属于同一个位置的数的那个 s[l]-s[l-1] 是一样的。
然后就是那个前缀和 mx 的贡献了。容易发现这个 mx 的贡献永远是 mx 所代表和的右侧的数的对应的 s[l]-s[l-1] 的贡献,使用乘法分配律得到 mx 贡献为 mx*(s[r]-s[l-1]) ,因此这一部分也是平凡的。
于是我们就解决了这道 NOI2024 的黑题( D2T2 ),还是挺不错的一道题目。
代码:
1 |
|

