整体二分学习笔记
P3332 [ZJOI2013]K 大数查询
树套树 ×
整体二分 √
虽然这道题看上去在每个节点上维护一段序列,很像树套树(极其直观),但同时发现题目要求的值是可以二分得到的,而且树套树极有可能得到一页的 Wrong Answer 且心态调炸,所以我们讲一下 整体二分 这个离线做法
从二分到整体二分
我们先来把问题简化:
1.在一个静态区间中,查询第 k 小,有多少中做法?
不能写罢?……我教给你,记着!这些字应该记着。将来做掌柜的时候,写账要用
当然可以直接排序。如果用二分法呢?可以用数据结构记录每个大小范围内有多少个数,然后用二分法猜测,利用数据结构检验。
2.在一个静态区间中,多次查询第 k 小的数?
这个问题既可以重复上面的步骤解决,也可以引出我们今天的主题。
先考虑二分的本质:我认为二分算法的有一个巧妙的特征:他并不像\(1 + 1 = ?\) 一样能直接算出来答案是 2,而是在 所有可能的值域中猜测出一个数,然后 进行检验 并调整上下界,是“猜出来”的
假设要猜一个 \([l,r]\) 之间的数,猜测之后会知道是猜大了,猜小了还是刚好。当然可以从 \(l\) 枚举到 \(r\),但更优秀的方法是二分
(为什么是 “ 二 ”分?有什么优点?能换成其他数吗?有什么区别?)
我们猜测答案是 \(m = \lfloor\frac{l + r}{2}\rfloor\),然后去验证 \(m\) 的正确性,再调整边界。这样做每次询问的复杂度为 \(O(\log n)\),若询问次数为 \(q\),则时间复杂度为 \(O(q\log n)\)。
回过头来,对于当前的所有询问,可以去猜测所有询问的答案都是 mid,然后去依次验证每个询问的答案应该是小于等于 mid 的还是大于 mid 的,并将询问分为两个部分(不大于/大于),对于每个部分继续二分。注意:如果一个询问的答案是大于 mid 的,则在将其划至右侧前需更新它的 k,即,如果当前数列中小于等于 mid 的数有 t 个,则将询问划分后实际是在右区间询问第 \(k - t\) 小数。如果一个部分的 \(l = r\) 了,则结束这个部分的二分。
整体二分的时间复杂度为 \(O(T\log n)\) (若对应的查询的时间复杂度为 \(O(T)\))。
3.在一个静态区间中,多次查询区间第 k 小的数?
再按之前的方法进行朴素 check ,时间复杂度将爆炸。仍然考虑询问与值域中点 m 的关系:若询问区间内小于等于 m 的数有 t 个,询问的是区间内的 k 小数,则当 $k \leq t $时,答案应小于等于 m;否则,答案应大于 m。(注意边界问题)此处需记录一个区间小于等于指定数的数的数量,即单点加,求区间和,可用树状数组快速处理。为提高效率,只对数列中值在值域区间 \([l,r]\) 的数进行统计,即,在进一步递归之前,不仅将询问划分,将当前处理的数按值域范围划为两半。
实战:主席树
4.在一个动态区间中,多次查询区间第 k 小的数?
为方便起见,将询问和修改统称为「操作」。因后面的操作会依附于之前的操作,不能如题 3 一样将统计和处理询问分开,故可将所有操作存于一个数组,用标识区分类型,依次处理每个操作。为便于处理树状数组,修改操作可分拆为擦除操作和插入操作。
- 注意到每次对于操作进行分类时,只会更改操作顺序,故可直接使用大的全局数组 q,二分时记录信息变为 L, R,即当前处理的操作是全局数组上的哪个区间。利用临时数组记录当前的分类情况,进一步递归前将临时数组信息写回原数组。
- 树状数组每次清空会导致时间复杂度爆炸,可采用每次使用树状数组时记录当前修改位置(这已由 1 中提到的临时数组实现),本次操作结束后在原位置加 -1 的方法快速清零。
- 一开始对于数列的初始化操作可简化为插入操作。
回到问题
(模板题中最难了
至于如何进行分类呢?
将所有的修改和查询操作离线存下来。每次二分所有修改和询问操作,分成两部分,这两部分中一个部分的答案 ⩽mid,另一部分>mid,而 C⩽mid 的修改放在第一个部分,C>mid 的修改放在另一部分,因为 C>mid 的修改对答案 ⩽mid 的部分毫无影响。
我们发现他需要用到 区间加、区间和,我们既然说了不用树套树,干脆线段树也不写,从前几题可以看到树状数组足够满足我们的需求,它维护每段区间中有多少个数>mid,每次遇到一个 C>mid 的操作就把[l,r]这段区间+1,每次遇到一个询问就计算一下[l,r]有多少个数>mid,存在一个辅助数组 cur 里。然后将将上述的操作在重做一遍 -1 清空。
我们判断对于一个询问,若 cur[i]⩾K,则答案>mid,否则 ⩽mid,之后递归处理即可。
番外
如何用树状数组实现区间修改+区间查询呢?我们看一个数是怎么求出来的:
sum[n]
= a[1] + a[2] +…+ a[n]
= (d[1]) + (d[1]+d[2]) +…+ (d[1]+d[2]+…+d[n])
= n * d[1] + (n-1) * d[2] +…+ d[n]
= n * (d[1] + d[2] + … + d[n]) - (0 * d[1] + 1 * d[2] + … + (n-1) * d[n])
(a[ ]为原数组,d[ ]为 a[ ]的差分数组)
所以可以开两个树状数组维护,一个维护 d[i],一个维护 \((i-1)\times d[i]\) 。
如果我们要实现对原数组区间\([l,r]\)内的元素+k,借助差分数组,只需使 \(tr[l]+=k,tr[r+1] -=k\) 即可。
namespace BIT
{
int diff[N], ex[N];
inline int lowbit(int x) { return x & -x; }
void add(int *tr, int p, int x)
{
for (; p <= n; p += lowbit(p))
tr[p] += x;
}
int presum(const int *tr, int p)
{
int res = 0;
for (; p; p -= lowbit(p))
res += tr[p];
return res;
}
void add(int l, int r, int x)
{
add(diff, l, x), add(diff, r + 1, -x);
add(ex, l, x * (l - 1)), add(ex, r + 1, -x * r);
}
int presum(int l, int r)
{
return (r * presum(diff, r) - presum(ex, r)) -
((l - 1) * presum(diff, l - 1) - presum(ex, l - 1));
}
}