优雅の二分-整体二分

整体二分

标题是乱起的

萌萌题

有一个集合,初始为空。现在有 \(q\) 次操作:

  • 1 x:将 \(x\) 插入集合。
  • 2 k:查询集合中的第 \(k\) 大数。

平衡树

二分答案。第 \(k\) 大就相当于大于等于它的数恰有 \(k\) 个。

先假设答案是 \(x\),然后从头到尾执行一遍操作序列。记录一个 \(c\) 表示当前序列中 \(\ge x\) 的数的个数,对于一个询问,如果 \(c> k\) 说明取大了,否则说明取小了。

按照得到的「取大了还是取小了」将操作序列分为两个部分 \(S_1,S_2\),一部分表示确定了答案在 \([1,x)\) 内的操作与询问,一部分表示确定了答案在 \([x,V]\) 内的操作与询问,递归下去处理就行了。

形式化地,我们考虑一个过程 \(\text{solve}(l,r,S)\)

  • \(m=\dfrac{l+r}{2}\)。首先扫一遍操作序列 \(S\),并用一个计数器 \(c\) 表示当前集合中 \(\ge m\) 的数的个数。
  • 如果遇到插入操作就更新计数器 \(c\),并且根据插入的数与 \(m\) 的大小关系将其放入 \(S_1\)\(S_2\) 中。
  • 如果遇到查询操作,比较当前计数器 \(c\) 与查询的 \(k\) 的大小。若 \(c\ge k\) 就将其放入小的那部分 \(S_1\) 中,否则将其放入大的那部分 \(S_2\) 中。
  • 递归求解 \(\text{solve}(l,m,S_1),\text{solve}(m+1,r,S_2)\)
  • 计算 \([m+1,r]\) 中数对 \(S_2\) 中询问的影响:直接在将询问插入 \(S_2\) 时令 k-=c 即可。

递归的边界是 \(l=r\),此时序列中所有插入操作插入的数必然都是 \(l\),所有询问操作的答案也都是 \(l\)

时间复杂度 \(O(n\log V)\),其中 \(V\) 为值域。

一道经典题

给定长为 \(n\) 的序列 \(a\),有 \(q\) 次询问,每次给出 \(l,r,k\) ,你需要求出区间 \([l,r]\) 中所有数的第 \(k\) 大值是多少。

\(1\le n,q\le 10^5,1\le a_i\le 10^9\)

感觉上题理解了之后这题应该很好做......不会也没关系,我们接着往下看。

如果询问只有一个?

你或许知道一车数据结构能解决这个题,不过我们今天的做法不需要数据结构!

二分答案!

我们二分一个答案 \(x\),如果这个区间内有 \(p\) 个数 \(\ge x\) ,那么若 \(p\ge k\) 说明 \(x\) 取小了,反之说明取大了。

每次找这里 \(\ge x\) 的数的个数是 \(O(n)\),加上二分的复杂度就是 \(O(n\log V)\),其中 \(V=10^9\) 为值域。

在这一次二分中我们其实得到了很多对其他询问也有用的信息,

多组询问怎么做?

你或许知道一个经典分块题就是 \(O(\sqrt{n\log n})\) 得到区间 \(\ge x\) 的数的个数,不过我们今天并不打算用这个。

我们仍然考虑二分答案。先假设一个答案就是 \(x\),从头到尾扫一遍询问。用类似上题的做法就行了。

要判断一个询问的答案比 \(x\) 大还是小,我们只需要知道 \([l,r]\) 内有多少 \(\ge x\) 的数。

这个东西很好维护,把 \(\ge x\) 的数看做 \(1\)\(< x\) 的数看做 \(0\),那么要求的就是一个区间和。

貌似直接 \(O(n)\) 扫一遍序列做前缀和就行了?并不是!

注意整体二分的过程中我们每一次的复杂度必须只能和当前操作序列 \(S\) 的长度相关,而不能和整个序列相关。

这就像普通分治的时候不能直接 memset 而是要一个一个清空一样。

其实真要求前缀和其实也可以!

注意到至多只有 \(O(|S|)\) 个点是 \(1\),因此可以考虑把这些点桶排一下,查询单点前缀和时双指针就行了。

虽然这样是做总复杂度是 1log,不过这个算法不支持动态修改,而且没有我们下面讲的算法简洁=w=

其实有一个简单的方法就是,将初始的 \(n\) 个数看做 \(n\) 次赋值操作,然后直接用类似上题的方法,如果赋的值 \(\ge m\) 就扔进 \(S_2\),否则扔进 \(S_1\)。使用树状数组维护单点加区间和即可。这样一来复杂度就是 \(O(|S|\log n)\) 了。

不难发现这个问题同样支持了动态修改。

复杂度 \(O((n+q)\log n\log V)\)。离散化后可以做到 \(O((n+q)\log ^2n)\)AC Code


简单总结两句?

区间第 \(k\) 大类型的题目,如果可以离线,那么一个很好的方法就是整体二分。

二分答案转为求一个区间内 \(\ge k\) 的元素个数(并且每次询问的 \(k\) 是不变的!),往往就能够使问题迎刃而解。

注意二分过程中对于操作序列 \(S\) 的处理复杂度只能和 \(|S|\) 有关。

当然整体二分还有别的应用(比如「保序回归问题」),不过貌似不太常考qaq


题目选讲

LuoguP3332 ZJOI2013 K 大数查询

想到整体二分之后这题就非常 simple 了吧。

整体二分!设二分答案为 \(x\)

对于一个插入操作,如果插入的 \(c> x\),那么就相当于区间 \([l,r]\) 集合内 \(>x\) 的数都多了一个,也就是区间加一。

询问只需要查询出区间 \(>x\) 的数的个数,那么就是一个区间和。

使用线段树维护区间加区间求和就行了。复杂度 \(O((n+m)\log n\log V)\)AC Code

LuoguP3527 POI2011 MET-Meteor

貌似并不是查询第 \(k\) 大,咋办?

既然我都写在这里了那肯定是用整体二分对不对hhh

首先考虑如果只算一个国家要多少次才能达到指标:这个可以二分,二分答案 \(x\),算出前 \(x\) 个区间加对这个国家会产生多少贡献,如果达成指标了说明 \(x\) 取大了,否则说明取小了。

那么多个国家一起算其实也一样:直接当成 \(n\) 个查询,每次根据二分结果分成两半就完事了。

时间复杂度 \(O(n\log ^2n)\),注意线段树常数巨大所以要用树状数组做区间加单点查询。AC Code

习题

posted @ 2022-03-09 22:31  云浅知处  阅读(100)  评论(0)    收藏  举报