《浅谈利用分散层叠算法对经典分块问题的优化》 - 学习笔记

一个 niubi 技巧,看起来只能解决一个没什么用的问题,但一通操作之后竟然也可以达到比较广的适用范围。

但是写起来好像还是很要死。

1 分散层叠

1.2 举例说明

假设你有 \(k\) 个有序序列 \(L_i\) ,共 \(n\) 个数。有 \(q\) 次询问,每次给出一个数 \(x\) ,你要对于 \(k\) 个序列分别找到 \(x\) 的后继。

最无脑的方法当然是对每个序列都二分一次,最坏复杂度单次 \(O(k\log {n\over k})\)

如果 \(q\) 太大,可以采用空间换时间的方法,把 \(k\) 个序列归并到一起,然后对于每个元素求出它在 \(k\) 个序列中的位置。一次询问只需要二分一次即可得到所有信息。单次复杂度 \(O(k+\log n)\) ,但空间复杂度 \(O(nk)\)

然后就是神奇分散层叠算法。

构造 \(k\) 个序列 \(M_i\) ,其中 \(M_k=L_k\)\(M_{k-1}\)\(L_{k-1}\) 和从 \(M_k\) 中每两个选一个得到的子序列 \(M'_k\) 归并而来,前面的以此类推。对于 \(M_i\) 的每一个元素,记录它在 \(L_i\) 中的位置和在 \(M_{i+1}\) 中的位置。询问时,先在 \(M_1\) 里面二分,即可得到在 \(L_1\) 中的位置和在 \(M_2\) 中的大致位置。这里的大致位置的意思是距离真正位置最多只差一个,所以 \(O(1)\) 即可找到在 \(M_2\) 中的位置,然后继续往后推。单次询问时间复杂度 \(O(k+\log n)\) ,空间复杂度 \(O(n)\)

1.3 普遍情况

在一个 DAG 上,每个点的度数不超过 \(d\) ,每个点上有一个有序序列。每次询问一条链和一个数,求出这个数在这条链上的每个点的后继。

直接从每个点的后继的 \(M\) 里面拿 \({1\over d}\) 过来和自己归并即可。

2 经典分块题

区间加,区间 \(\text{rank}\)

\(O(n\sqrt{n\log n})\) 非常简单,考虑优化二分的时间复杂度。

2.3 离线算法

对于单个块,在重构之前,它的结构基本不变。所以可以离线下来,尝试用线性时间把两次重构之间的询问全部处理掉。

显然在给询问的数减去 add tag 之后就变成在原数列上找后继,只要能把询问也变得有序,就可以 \(O(B+Q)\) 归并。

当值域为 \(2^{O(\log n)}\) 时可以以 \(B\) 为底进行基数排序,经过 \(O(1)\) 轮就会停下。

3 基于分散层叠的算法

一次操作对序列的影响这么大,怎么看也不能用分散层叠算法吧

3.1 \(O(n\sqrt{n\log \log n})\) 做法

造一个阈值 \(D\) ,每块中选取 \({1\over D}\) 个元素出来,每连续 \(D\) 个块建立分散层叠,共 \(O({n\over BD})\) 组。

一次修改操作只会对零散的两组分散层叠造成大的影响,而中间的分散层叠可以打标记来解决。直接重构这两个分散层叠,复杂度 \(O(B)\)

询问时,对每组分散层叠进行查询。每组时间为 \(O(D+\log B)\) ,共 \(O({n\over BD})\) 组。另外由于每块只选了 \({1\over D}\) 个元素,所以还需要分别进行一次 \(O(\log D)\) 的二分。没有完全覆盖的块还要暴力,复杂度 \(O(B)\) 。所以最终复杂度单次 \(O({n\over B}\log D+{n\over BD}\log B+B)\)

\(B=\sqrt{n\log \log n},D=\log n\) ,得到单次复杂度 \(O(\sqrt{n\log\log n})\)

分块算法由于需要对多个块进行二分查找,所以和分散层叠算法的适用范围比较贴合。

3.2 \(O(n\sqrt n)\) 做法

在分块上面建立线段树,共 \({n\over B}\) 个叶节点。在叶节点上存放块中的有序序列,其他点的序列由子节点的序列选取一部分归并得到。

一次修改时,只有 \(O(\log {n\over B})\) 个节点需要重构。如果只从子节点的序列中选取 \({1\over 3}\) ,那么总长度就是 \(O(B\sum _i ({2\over 3})^i)=O(B)\)

对于查询,直接对根进行二分,然后一路往下推到叶子。还要对零散块暴力。复杂度 \(O(\log n+{n\over B}+B)\)

平均一下,总复杂度 \(O(n\sqrt n)\)

可以看出,分散层叠算法也比较适合线段树,因为分析一下复杂度竟然不带 \(\log\)

3.4 扩展

3.4.1 题目 1

区间加,区间求有多少个子区间满足所有元素 \(\le x\)

肯定先分块。暴力是把所有 \(> x\) 的元素找出来,然后相邻两个之间的区间的所有子区间都合法。

考虑一个块的贡献。在这个块的形态不改变时,我可以处理出对于它有序序列的每个后缀,这个块的答案 & 左右两个 \(>x\) 的元素的位置。

如果没有修改操作,那么处理出这东西之后一次询问就变成在每个块里面二分。直接套 3.2 的分散层叠算法即可。

加上修改操作仍然可以用 3.2 的做法,但要考虑怎么重构一个块。其实非常简单,从小到大做,用链表维护当前哪些元素还没有被删即可。

复杂度 \(O(\sqrt n)\)

3.4.2 题目 2

区间加,区间最大子段和。

先分块。最大子段和的信息也可以通过合并多个块来得到,但给一个块打上 tag 之后这个块的信息会发生改变,比较麻烦。

给每一个块建立线段树,线段树的每个节点维护该区间整体加上一个值之后得到的各种信息的分段函数。用某种方式证明这个函数只有 \(O(len)\) 段,所以可以 \(O(B\log B)\) 建出一个块的线段树。

然后是两个问题:一是 \(O(B\log B)\) 重构太慢,二是询问的时候需要对每个块的分段函数二分,也太慢。

对于第一个问题,可以考虑重构一个块时造成的影响仅仅是区间加,即把某一些节点的分段函数整体平移,可以使用懒标记维护,上面那些需要重构的节点每层只有 \(O(1)\) 个,所以总复杂度 \(O(B)\)

对于第二个问题有两种解法。一个是使用 2.3 的方法,离线之后对每一个块分别做,也要用到基数排序。另一种方法就是继续套 3.2 的线段树。

(这是人能写的吗?)

posted @ 2021-02-07 20:44  p_b_p_b  阅读(802)  评论(0编辑  收藏  举报