ABC457F Second Gap
f---second-gap">F - Second Gap
题意
给定一个长度为 的序列 ,我们需要构造一个 的排列 ,使得对于每一个 ,在后缀 中,最大值和次大值所在下标的差的绝对值恰好等于 。求满足条件的排列总数。
思路
正着推显然是非常困难的,注意到题目条件是基于后缀定义的,不妨试试倒推,从右往左考虑,每次相当于向序列前端添加一个元素 。
假设在后缀 中,最大值的下标为 ,次大值的下标为 ,已知 。现在加入 ,考虑 的最大值和次大值:
- 成为 的最大值,此时次大值必然是原本 的最大值 ( 即 ),。
- 成为 的次大值,此时最大值依然是原本 的最大值 ( 即 ),。
- 既不是最大也不是次大,此时 的最大次大与 一致。也就是 ,那么 可以在剩余的 个相对位置中任意选择。
既然明确是是个类递推问题,考虑动态规划 ( 其实动态规划这个名字很奇怪,既不动态也不规划 )。
状态定义:
由于次大值的位置在某些情况 (1, 2) 是由前一状态的最大值决定的,所以我们只需要记录最大值的位置。设 表示:在后缀 中,最大值位于下标 的方案数。
状态转移:
对于当前位置 与上一状态的最大值位置 :
-
若 :
- 作为最大值,转移到 ,增加 。
- 作为次大值,转移到 ,增加 。
-
若 :
- 作为平方值:转移到 ,增加 。
暴力
直接根据状态转移方程构造就可以得到这个,时空复杂度均为 的 DP。注意到 每次转移 都只依赖于上一层 ,所以空间上可以用 滚动数组 的技巧来压掉,就得到了下面这种写法:
1 | |
观察内部转移逻辑,我们发现每一轮循环只做了三件事:
- 单点查询:查询 这一点的方案数。
- 全局更新:当 时,将所有状态全体乘以 ,当 的时候,将除了目标值以外的所有状态清零。
- 单点更新:把目标值累加到产生的新状态上。
这三个操作都是典型的数据结构应用场景,我们可以针对性优化:
线段树优化
既然操作包含 "全局乘法" 和 "单点更新 / 查询",自然会想到用 "线段树" 来维护这个 DP 转移。
- 用 的时间单点查询
target = i + D[i]的值val。 - 若 ,对根节点下发乘法标记
(N - i _ 1);否则下发乘法标记0,耗时 。 - 若
val > 0,用 的时间在位置target和i分别加上val。
1 | |
全局懒标记
再深入思考线段树的做法,我们会发现:所有的乘法操作都是 全局 的。既然没有“局部区间操作”,杀鸡焉用牛刀 ( 线段树 )?我们完全可以用几个变量和数组把复杂度降到 。
-
优化全局乘法(乘法逆元):
维护一个全局乘法因子
mult(初始为 1),以及一个真实值数组A。全局乘以 时,只需mult = (mult * K) % mod。当我们要往数组里单点增加一个值
val时,为了抵消未来全局mult的影响,我们实际存入的是val * inv(mult)(即乘以mult的逆元)。查询时,真实值就是A[p] * mult。 -
优化全局清空:
当需要全局清空时,如果我们遍历数组赋 0,复杂度又退化回了 。但注意到,每轮循环最多只会 新增 2 个 非零状态。
因此,我们维护一个
active动态数组,专门记录当前值不为 0 的下标。需要清空时,只遍历active里的这几个点将其设为 0,然后清空active。整个推导过程中,加入active的总人次不超过 ,所以清空操作的总均摊复杂度严格为 。
1 | |