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