题解:P4118 [Ynoi2018] 末日时在做什么?有没有空?可以来拯救吗?
提供一个最大点 656ms,总时间 7.79s 的做法,目前最优解第一。
思路
和主流的做法相同。
如果没有修改,这是一个经典的线段树问题:对线段树上每个区间维护
如果是全局加,上面维护的问题出于不同区间长度的增量是不同的。注意到增量是一个一次函数,所以可以对每个节点维护一个长度作为横坐标、该长度的区间最大值作为纵坐标的凸包。每次合并时
对于查询,需要在每个线段树上的节点二分。可以将所有值离线并排序,之后就只需要双指针了。这样就解决了 P5073(这道题里还需要将询问和维护的东西放在一起递归以卡空间)。
对于原题,考虑分块。对于整块的修改就相当于全局加,因此对每个块维护上面的线段树。接下来整块的修改就只需要打标记了。但是问题出在,散块需要重构这个线段树,而它的构建是 push_down 和 push_up(当一个区间开始被分裂后两边只有一个儿子会再次递归),所以暴力修改的复杂度是
考虑查询。注意到对每个块只需要求出
因为每个块是独立的,所以可以逐块处理,以减少空间复杂度。
一些常规的卡常技巧
- 散块查询时无须
push_down,而是直接记录一个变量传下来\text{tag} 。这样它的复杂度就不是\mathcal O(B) ,而是\mathcal O(\log^2 B) 的了。 - 计算斜率的时候转成
__int128相乘比double快。 - 做闵可夫斯基和时需要维护一个数组记录每个长度的答案是多少。在对这个数组求凸包时,没有被遍历过的位置不合并。
push_down的时候如果没有标记直接返回。- 基数排序时桶的大小设为
256 。一方面是因为可以卡进 cache,另一方面是因为需要做\mathcal O(n) 次基数排序(相当于散块修改的个数),如果太大了乘上n 已经过不去了。 - 对排序的长度分治,如果很大才用基数排序。或者索性将基数排序删了,实测(与分治相比)没有任何速度变化。
- 散块修改时如果修改的左端点与块的左端点重合,可以认为是整块,右端点同理。
- 将所有
vector换成指针,放到一个大的内存池里。这是为了防止vector动态开空间产生的消耗。 - 排序的时候将
pair(一个值是标记之和,另一个值是查询的时间)压成一个数。因为题目保证了任意时刻的绝对值,所以标记之和\in [-4 \times 10^9,4 \times 10^9] ,再乘上n 的大小不会爆。 - 讨论区里看到的神秘块长
B=408 ,实测对于最后一个点可以快 100ms。
一个非常规的卡常技巧
这个技巧我之前没见过,是我自己想出来的,所以想推广一下。
考虑使用 dp 求出最佳的块的分配。发现复杂度瓶颈在散块的修改和整块的查询。对于一个块
- 对于修改
[l,r] ,代价为l \in (L,R],r \in [L,R) 成立的个数乘上R-L+1 。 - 对于查询
[l,r] ,若[L,R] \in [l,r] ,代价为k (k 为一个常数)。
普通的 dp 会超时,但是注意到贡献满足四边形不等式(可以通过简单分讨证明),因此可以使用单调栈加二分解决。计算一个块的代价时,第一类贡献可以使用前缀和解决,第二类贡献可以使用主席树解决。
但是这个 dp 是
这样 dp 出来的块有可能会很长导致空间超限,因此再设一个
我的代码中三个参数分别为:
这个技巧对于非随机数据的优化效果非常大。
提交记录