整体二分


对于一个询问,我们往往可以通过二分答案的方法求出解:在答案的可能范围中选择一个值 \(mid\) ,然后判断正确答案是比 \(mid\) 大还是小,以此来缩小可能的值域范围。如果判断的过程可以 \(O(n)\) 解决,那么往往可以在 \(n \log w\) 的复杂度之后解出答案。

比如求区间第 \(k\) 。我们可以先假设一个 \(mid\) 是第 \(k\) 小,然后统计实际上比 \(mid\) 小的数有几个,以此来确定 \(mid\) 真正的排名是多少。如果比 \(k\) 大,说明真正的第 \(k\) 小小于 \(mid\) ,反之则大于 \(mid\) 。以此可以完成二分。

但是当我们要多次求不同的区间第 \(k\) 小时,如果我们对每一个询问都进行一次二分操作,复杂度将会难以接受。怎么办呢?

我们考虑延续二分的思路。把所有的询问都读进来之后,我们不妨假设每一个询问的答案都是 \(mid\) ,然后判断所有询问中,有哪些询问的真实答案比 \(mid\) 大,有哪些询问的真实答案比 \(mid\) 小,然后分别递归求解。

但这么做有两个问题:

  1. 如何判断?按照上面的方法,对于每一个询问都要花 \(O(n)\) 的时间去判断,这显然难以接受。
  2. 二分能有 \(\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):

根据上面的分析,其实我们已经可以发现保持整体二分的复杂度的要点了:对于序列中的每一个元素,我们应该做到只对当前值域中的那些元素进行数据结构操作。这样,如果一次数据结构操作的时间为 \(T\) ,总时间复杂度就是 \(O(Tn \log n)\)

最后就是如何进行修改的问题。其实非常简单,把它和询问一起作为操作参与递归。只不过在递归过程中要注意以下三点:

  1. 注意保持时间顺序。始终保证修改和查询的相对位置不变。
  2. 修改后的元素如果超过了值域之外,不应该写入数据结构中。
  3. 递归的结尾别忘了把修改的内容还原。

带修区间第 \(k\) 小参考代码(引自 OI-wiki):

最后上例题和代码:

[POI2011 R3 Day2] 流星 Meteors

AC 代码: