整体二分
对于一个询问,我们往往可以通过二分答案的方法求出解:在答案的可能范围中选择一个值 \(mid\) ,然后判断正确答案是比 \(mid\) 大还是小,以此来缩小可能的值域范围。如果判断的过程可以 \(O(n)\) 解决,那么往往可以在 \(n \log w\) 的复杂度之后解出答案。
比如求区间第 \(k\) 小 。我们可以先假设一个 \(mid\) 是第 \(k\) 小,然后统计实际上比 \(mid\) 小的数有几个,以此来确定 \(mid\) 真正的排名是多少。如果比 \(k\) 大,说明真正的第 \(k\) 小小于 \(mid\) ,反之则大于 \(mid\) 。以此可以完成二分。
但是当我们要多次求不同的区间第 \(k\) 小时,如果我们对每一个询问都进行一次二分操作,复杂度将会难以接受。怎么办呢?
我们考虑延续二分的思路。把所有的询问都读进来之后,我们不妨假设每一个询问的答案都是 \(mid\) ,然后判断所有询问中,有哪些询问的真实答案比 \(mid\) 大,有哪些询问的真实答案比 \(mid\) 小,然后分别递归求解。
但这么做有两个问题:
- 如何判断?按照上面的方法,对于每一个询问都要花 \(O(n)\) 的时间去判断,这显然难以接受。
- 二分能有 \(\log\) 的复杂度,是因为在 \([1, mid],(mid,w]\) 两个值域中只有一个会进入下一轮,而现在我们对两个值域都要递归求解,时间复杂度如何保证?
对于第1个问题,我们可以先对数据进行预处理。我们可以用树状数组维护出区间 \([1,i]\) 中有几个数小于 \(mid\)(就是小于 \(mid\) 的看成1,大于 \(mid\) 的看成0,然后用树状数组维护前缀和),之后在判断,只要拿 \([1,r]\) 的值减去 \([1,l)\) 的值就好了。这样就把 \(O(n)\) 判断降低到了 \(O(\log n)\) 。不过预处理需要 \(O(n \log n)\) 的时间。
对于第2个问题,我们使用一些技巧来解决:在1中的树状数组中,我们不再把所有的数都加入树状数组,而是只把当前答案值域的数放进树状数组。那么对于每个数,它最多会进入 $w $ 次递归,每一次递归都会进一次树状数组,总共的树状数组操作就有 \(O(n \log w)\) 次。同样的道理,对于每一个询问,它最多只会进入 \(\log w\) 次递归,每一次递归都会查询一次树状数组,总共的查询数也是 \(O(n \log w)\) 。由于树状数组单次操作复杂度为 \(O(\log n)\) ,总时间复杂度就是 \(O(n \log n \log w)\) 。我们还可以对值域进行离散化,将 \(w\) 缩小为 \(n\) 。复杂度就变成了 \(n \log^2 n\) 。
不过,由于我们只是把值域在 \([l,r]\) 中的数放进了树状数组,故在递归进入 \((mid,r]\) 时,需要把所有待递归的询问的 \(k\) 减掉 \([l,mid]\) 的贡献。因为接下来的递归, \([l,mid]\) 中的数将不再进入树状数组。
代码如下(引自 OI-wiki):
1 | struct Num { |
根据上面的分析,其实我们已经可以发现保持整体二分的复杂度的要点了:对于序列中的每一个元素,我们应该做到只对当前值域中的那些元素进行数据结构操作。这样,如果一次数据结构操作的时间为 \(T\) ,总时间复杂度就是 \(O(Tn \log n)\) 。
最后就是如何进行修改的问题。其实非常简单,把它和询问一起作为操作参与递归。只不过在递归过程中要注意以下三点:
- 注意保持时间顺序。始终保证修改和查询的相对位置不变。
- 修改后的元素如果超过了值域之外,不应该写入数据结构中。
- 递归的结尾别忘了把修改的内容还原。
带修区间第 \(k\) 小参考代码(引自 OI-wiki):
1 | struct Opt { |
最后上例题和代码:
AC 代码:
1 |
|