[SNOI2017]炸弹
首先这个题没有刻意卡错解,所以有一种解法就是,设一次性可以炸 l~r,那么用 l~r 内的 f 来更新 f[i],其中 (pair<int,int>)f[i] 表示维护的 i 的能炸区间。从左到右来一次,从右到左来一次,再重复一次,就 AC 了;当然,如果是 CF 肯定过不了。
正解有两种,一种是标准的【线段树优化建图】做法 \(O(n\log n)\),第二种是创新型做法,使用了隐式单调栈,\(O(n)\)。
法1. 线段树优化建图
一个自然的想法是对每个点 \(i\) 向 \(j,|x_i-x_j|\le r_i\) 连有向边,然后缩点在 DAG 上 DP。但是这样时空复杂度都是 \(O(n^2)\) 的,无法接受。
由于每次是向一个连续的 \(j\) 的区间连边,而线段树是将区间分解为 \(O(\log n)\) 个定区间,我们借用这一思路,先建一棵线段树,那么每个节点代表一个区间,对线段树的每个叶子 \(i\) 向对应的 \(j\) 的区间分解出的若干段区间所对应的节点分别连有向边,然后缩点在 DAG 上 DP。这样的时空复杂度都是 \(O(n\log n)\) 的,可以接受。
至于怎么 DP,你可以对每个缩点后的点设一个 \(f_u\) 表示从 \(u\) 出发到达的区间是 \([f_u.first,f_u.second]\),初始时每个点的 \(f_u\) 为强连通分量 \(u\) 中的最小的l和最大的r。
法2. 隐式单调栈
考虑维护最开始引爆炸弹 \(i\) 后,所有最后被引爆的炸弹区间 \([l_i, r_i]\)。
先把所有 \([l_i, r_i]\) 初始化为 \([i, i]\),然后令 \(i\) 从 \(2\) 开始,到 \(n\) 结束,执行以下操作:
- 判断自己能否引爆炸弹 $l_i - 1$,如果不能或者 $l_i = 1$,终止操作
- 比较自己的爆炸右边界 $(x_i + range_i)$ 与 $l_i - 1$ 的爆炸右边界 $(x_{l_i - 1} + range_{l_i - 1})$,如果后者更大那么用后者减去 $x_i$ 的值来更新 $range_i$
- 将 $l_i$ 更新为 $l_{l_i - 1}$,并返回第一个操作
这样一轮算下来之后我们就可以计算出只往左爆炸能够到达的边界 $l_i$,时间复杂度是线性的
运用同样的方法倒着来一遍,求出 $r_i$,即得答案。简单证一下复杂度,外层循环显然是线性的;内层循环中,会存在某一个炸弹 $p$ 它往前炸到了很多以前并不关联的炸弹的情况。看起来这个循环的极限复杂度很高,但是一旦这些炸弹纳入了当前炸弹 $p$ 的影响范围内后,后续炸弹与这段炸弹的关联无非就是都能炸到,或者都不能炸到。不可能只炸一部分,因为这段炸弹的最右侧是 $p$,炸到一部分就炸到了 $p$,炸到了 $p$ 就能炸完这一整段。所以这段炸弹以后的复杂度贡献就几乎没有了。更何况如果后续有炸弹能炸完这一段炸弹,那这段炸弹就被划进一个更大的炸弹爆炸段了。所以这两个循环的总复杂度是 $O(n)$。
-----Extracted from Krystallos's Blog

浙公网安备 33010602011771号