博客园同步
你首先要学会的:
我们把 RMQ \text{RMQ} RMQ 和分块 所解决的问题搬出来:
显然我们已经有了一个 O ( n log n ) − O ( 1 ) \mathcal{O}(n \log n) - \mathcal{O}(1) O(nlogn)−O(1) 的算法。
现在做一个 简单 的加强, n , T ≤ 2 × 1 0 7 n,T \leq 2 \times 10^7 n,T≤2×107.
你会发现预处理的时间不够了。
很显然,我们考虑朴素的分块怎么做。
将数组分为若干个长度为 b b b 的块,对于每个块,处理块内答案,前缀块的答案,后缀块的答案。
当 b = n b = \sqrt{n} b=n 时 取到最平均的答案,可以 O ( n ) − O ( n ) \mathcal{O}(n) - \mathcal{O}(\sqrt{n}) O(n)−O(n),但显然不行。
那考虑 RMQ \text{RMQ} RMQ 呢?用 f i , j f_{i,j} fi,j 表示 [ i , i + 2 j − 1 ] [i , i + 2^j-1] [i,i+2j−1] 区间的答案,倍增处理即可。显然是 O ( n log n ) − O ( 1 ) \mathcal{O}(n \log n) - \mathcal{O}(1) O(nlogn)−O(1).
这两个算法都无法通过 2 × 1 0 7 2 \times 10^7 2×107 如此庞大的数据。我们思考一个新的算法。
首先考虑分块基础上的优化。同样 处理块内答案,前缀块的答案,后缀块的答案,这里是 O ( n ) \mathcal{O}(n) O(n) 的,考虑如何优化询问。
询问分两种情况:
显然,第一种情况我们可以把 横跨的整块 进行前缀计算,然后转为 对两侧不完整快的计算,即剩余两个部分各自包含在一个完整的块内。这样做是 O ( 1 ) \mathcal{O}(1) O(1) 的,但如何计算第二种方案?
考虑如何处理一个块内的方案,如果按照 分块的暴力思想,那么 O ( n ) \mathcal{O}(\sqrt{n}) O(n) 就奠定了。显然这不是我们想要的。
现在的问题转为,在长度为 n \sqrt{n} n 的数组上, T T T 组询问 [ l , r ] [l,r] [l,r] 区间的 和(差,异或等有结合律的算术)。
咦?这不是分块吗?
人见人爱的 分块套分块 又重出江湖了?
我们可以设法将 n \sqrt{n} n 长的块进行再分块,分为 n \sqrt{\sqrt{n}} n 长的块,然后再不断往下分 ⋯ ⋯ \cdots \cdots ⋯⋯ 直到块长为 1 1 1 为止。
首先,似乎这样的时间复杂度很稳, O ( n + n + n + ⋯ ⋯ ) = O ( n ) \mathcal{O}(n + \sqrt{n} + \sqrt{\sqrt{n}} + \cdots \cdots) = \mathcal{O}(n) O(n+n+n+⋯⋯)=O(n),但空间上无法承受,需要处理的区间太多。
既然分块失败了,我们考虑在分块的基础上,思考新的算法。
俗语云:“智商不够,数据结构来凑”。
现在我们想的一种 基于递归,搜索 的算法,能否有数据结构与其接轨呢?
显然,我们可以用 树 来实现。
对长度为 k k k 的区间,将其分裂为 k \sqrt{k} k 个长 k \sqrt{k} k 的子区间,即有 k \sqrt{k} k 个儿子。叶子节点的长度为 1 1 1 或 2 2 2.
第一层的节点维护 [ 1 , n ] [1,n] [1,n] 的答案。
假想一下:如果这棵树只建立 2 2 2 层,可以认为和 朴素分块 没有任何区别。
显然我们建完这棵树后,应当考虑复杂度怎样。
整棵树的高度是 O ( log log n ) \mathcal{O(\log \log n)} O(loglogn) 的,每层区间总长为 n n n,建树的时间复杂度为 O ( n log log n ) \mathcal{O(n \log \log n)} O(nloglogn),可以接受。而空间上也可以接受。
那么问题来了:在这样类似于 线段树 的数据结构上,如何询问呢?
在树高为 O ( log log n ) \mathcal{O}(\log \log n) O(loglogn) 的树上,按照线段树的思想,显然单次查询是 O ( log log n ) \mathcal{O}(\log \log n) O(loglogn) 的。这样并不优。
如何优化?
其实瓶颈在于如何快速确定树高。树高很好确定,直接二分就行。那么这样就变成了 O ( log log log n ) \mathcal{O}(\log \log \log n) O(logloglogn).
对于 n = 2 × 1 0 7 n = 2 \times 10^7 n=2×107,这个数已经变成了近似 2 2 2,几乎 可以视为常数。
我们考虑如何把这个数彻底地降为 O ( 1 ) \mathcal{O}(1) O(1),以免夜长梦多!
精髓来了。
上面我们已经做到了 O ( n log log n ) − O ( log log log n ) \mathcal{O}(n \log \log n) - \mathcal{O}(\log \log \log n) O(nloglogn)−O(logloglogn) 的算法,我们试图做到一个 O ( n log log n ) − O ( 1 ) \mathcal{O}(n \log \log n) - \mathcal{O}(1) O(nloglogn)−O(1).
思考一下: RMQ \text{RMQ} RMQ 的预处理,如果你在询问的时候暴力倍增,不也是 O ( log n ) \mathcal{O}(\log n) O(logn) 吗?最后我们用 二进制 的特效完成了从 O ( log n ) \mathcal{O}(\log n) O(logn) 到 O ( 1 ) \mathcal{O}(1) O(1) 的跳跃,让 RMQ \text{RMQ} RMQ 彻彻底底的战胜了 线段树(只是在两者都能解决的领域)。
现在我们仍要运用这个黑科技,二进制的运算从来不庸俗。
所以,按照 OI Wiki \text{OI Wiki} OI Wiki 的说法,
非常形象。如果你觉得这些太枯燥,我们来简单解释一下。
首先假设所有区间的长度都可以表示为 2 t ( t 为 自 然 数 ) 2^t (t为自然数) 2t(t为自然数) 的形式,对于不满足的区间我们可以在后面添上一些 不影响运算 的数值,在 + + + 中可以是 0 0 0. 由于区间的增大最多 × 2 \times 2 ×2 不到,因此不影响复杂度。
这样我们可以来用二进制了。首先我们把端点写成二进制的形式,由于每个块长度一样,端点呈 等差 形式,因此 每个块的两个端点的二进制有且仅有后 k k k 位不同。所以显然的,我们预处理的时候可以求出 k k k,并可以 O ( 1 ) \mathcal{O}(1) O(1) 计算 当前区间两个端点的二进制值。
如何判断当前区间是否涵盖在一个块里?显然这个问题就变成,当前取件两个端点的二进制是否只有后 k k k 位不同。这个问题不难解决。我们只需要将区间两端进行 异或操作,判断是否 ≤ 2 k − 1 \leq 2^k-1 ≤2k−1 即可。
那么如何快速找到树高呢(即对应再第几层)?对当前 [ l , r ] [l,r] [l,r] 计算最高位上的 1 1 1 的位置,并处理 i ∈ [ 1 , n ] i \in [1,n] i∈[1,n] 中 i i i 的最高位上 1 1 1 的位置。这样可以 O ( 1 ) \mathcal{O}(1) O(1) 计算树高, O ( 1 ) \mathcal{O}(1) O(1) 查询啦!
这就是 Sqrt tree \text{Sqrt tree} Sqrt tree,可以实现 O ( n log log n ) − O ( 1 ) \mathcal{O}(n \log \log n) - \mathcal{O}(1) O(nloglogn)−O(1).
可能你会说, O ( n log log n ) \mathcal{O}(n \log \log n) O(nloglogn) 对于 n = 2 × 1 0 7 n = 2 \times 10^7 n=2×107 会跑到 1 0 8 10^8 108 左右,不稳。
O ( n log n ) − O ( 1 ) \mathcal{O}(n \log n) - \mathcal{O}(1) O(nlogn)−O(1) 就稳了?
O ( n ) − O ( n ) \mathcal{O(n) - \mathcal{O}(\sqrt{n})} O(n)−O(n) 就稳了?
显然 Sqrt tree \text{Sqrt tree} Sqrt tree 已经是此类问题最优的算法,没有之一!
现在出题人意外地发现你用 不带修改 的简单 Sqrt tree \text{Sqrt tree} Sqrt tree 模板切题了!
出题人非常生气,毫不客气地加上了这样一句话:
那么如何修改呢?
直接在 Sqrt tree \text{Sqrt tree} Sqrt tree 上暴力?
首先考虑单点如何修改 a x = v a l a_x = val ax=val。
暴力的话,我们需要更改树上含 x x x 的区间,显然我们需要重新计算的区间为:
O ( n + n + n + ⋯ ⋯ ) = O ( n ) \mathcal{O}(n + \sqrt{n} + \sqrt{\sqrt{n}} + \cdots \cdots) = \mathcal{O}(n) O(n+n+n+⋯⋯)=O(n)
但是你觉得这样很满足吗?那出题人给你个修改都要 O ( n ) \mathcal{O}(n) O(n),岂不是要让 O ( n T ) \mathcal{O}(nT) O(nT) 的暴力通过了?
类似于线段树吧,可以打 lazy_tag \text{lazy\_tag} lazy_tag,不必完全暴力的。
这里引进 Index \text{Index} Index 的概念,记录每个区间被修改的块编号。
那么 O ( n ) \mathcal{O}(\sqrt{n}) O(n) 很稳。但是这样询问也增加了复杂度。
考虑如何将 [ l , r ] [l,r] [l,r] 的所有数都改成 x x x.
我们已经有了以下的算法(修改 - 查询):
O ( n log log n ) − O ( 1 ) \mathcal{O}(\sqrt{n} \log \log n) - \mathcal{O}(1) O(nloglogn)−O(1)
O ( n ) − O ( log log n ) \mathcal{O}(\sqrt{n}) - \mathcal{O}(\log \log n) O(n)−O(loglogn)
下面会一一解释这两种实现。
第一种实现中,把第一层 被 [ l , r ] [l,r] [l,r] 完全覆盖的区间 打上标记。
两侧的块修改一部分,没有办法,只能暴力, O ( n log log n ) \mathcal{O}(\sqrt{n} \log \log n) O(nloglogn).
查询的话,直接去 Index \text{Index} Index 里查询, O ( 1 ) \mathcal{O}(1) O(1).
每个节点都会被打上标记,那么每次询问要把祖先的标记更新一遍,就会形成 O ( n ) − O ( log log n ) \mathcal{O}(\sqrt{n}) - \mathcal{O}(\log \log n) O(n)−O(loglogn) 的复杂度。
Sqrt tree \text{Sqrt tree} Sqrt tree 在不带修改的情况下效率比 线段树 要高很多,但是实现的功能不如 线段树 多(比方说线段树可以维护 LIS \text{LIS} LIS);
带修改时, RMQ \text{RMQ} RMQ 无疑最优,其次是 线段树,然后是分块和 Sqrt tree \text{Sqrt tree} Sqrt tree.
OI \text{OI} OI 中不常考,但希望大家掌握。
Sqrt Tree - OI Wiki \text{Sqrt Tree - OI Wiki} Sqrt Tree - OI Wiki
洛谷 P3793 \text{P3793} P3793 由乃救爷爷