Loading

几种离线分治算法

现在只有口胡. 别急.

这些算法口胡起来很舒服啊. 但是沾点离线的一般都不太好写/ng

转一手 cmd 的 blog

1. cdq 分治 / 二进制分组

修改在查询之前, 会做. 修改查询交替, 不会做!
如果修改的贡献可以分批次加入查询, 那么我们可以试试下面的方法.

我们维护一些操作段, 包括修改和查询, 并且段内修改和查询之间的贡献已经算完了.
那么我们合并两个操作段的时候, 只需要把前段的修改贡献到后段上去.

然后怎么做? 暴力的方法是, 每次加入一个操作的时候都直接合并操作段.
cdq 分治聪明一点, 离线下来之后, 按照线段树建树的方式合并. 这样代码难度也不会很大 (无需显式维护操作段).
二进制分组会更强一些. 每次加入的时候, 如果前面的段和当前段的大小一样就合并 (什么 2048), 这样可以证明时间复杂度正确. 并且通常能强制在线.

例 1: 树状数组板子, 单点加, 区间求和.

首先简单转化变成单点加 (初始值也视为该操作), 前缀求和.
对于修改在查询之前的情况, 直接差分完了前缀和.
然后套用上面 cdq 分治 / 二进制分组的做法就完事了.
需要注意的是, 为了保证复杂度仅与段长有关, 需要保证修改和查询的下标有序, 这样才能前缀和+双指针简单算贡献.
如何保证有序呢? 不需要 sort, 还是利用这个分治结构, 使用归并排序的方式进行合并即可.
时间复杂度 \(O(n\log n)\).

例 2: 偏序问题

例 3: CF710F String Set Queries

首先对插入和删除分别维护, 最后作差即可.
如果修改在查询前面, 这就是简单的 AC 自动机.
但是强制在线, cdq 寄了. 使用二进制分组即可.

2. 整体二分

每个询问二分一遍太慢了.

所以我们考虑直接一起二分掉, 换句话说我们共用了判定的 mid.

每次判定完之后, 按照结果将询问划分到两边, 继续二分下去.

例 1: 区间第 k 小.

例 2: Dynamic Rankings.

3. 线段树分治

最先学会的一个 (?

用处在于, 把删除操作转成撤销操作, 而后者是容易的.

方法是, 我们处理出来每个元素存在的时间段 (从插入到删除).

然后对着时间开线段树, 这样贡献就变成了线段的形式, 直接扔到线段树上拆成 \(O(\log n)\) 段.

然后对着线段树 dfs 一遍就完事了, 此时只有加入和撤销.

例 1: 离线动态图连通性.

处理出来每条边存在的时间段, 插到线段树里, dfs 的时候用可撤销并查集即能维护在某一时刻的连通性.

拆线段一个 \(\log\), 并查集一个 \(\log\), 总时间复杂度 \(O(n\log^2n)\).

例 2: P4585 [FJOI2015]火星商店问题

(咕咕咕)

4. 猫树分治/二区间合并

喵喵喵.

猫树的精神在于预处理从中间往两边的贡献, 然后直接合起来.

所以这个东西也可以拿来分治, 每次处理经过中点的所有区间, 剩下的下放到两边.

(实际上就是对链上做点分治)

例: CF1100F Ivan and Burgers

我们不考虑前缀线性基那种离谱东西. 假设我们只会合并线性基.

使用上面的方法我们可以轻松解决该问题. 每次预处理出来中点向左的后缀构成的线性基和中点向右的前缀构成的线性基, 对于经过中点的查询, 直接合并即可.

然后把剩下的询问分到两边, 总时间复杂度差不多是 \(O(q\log^2n)\).

posted @ 2023-08-22 00:09  pjykk  阅读(48)  评论(0编辑  收藏  举报