整体二分学习笔记

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 的方法快速清零。
  • 一开始对于数列的初始化操作可简化为插入操作。

实战:Dynamic Rankings

回到问题

(模板题中最难了

至于如何进行分类呢?
将所有的修改和查询操作离线存下来。每次二分所有修改和询问操作,分成两部分,这两部分中一个部分的答案 ⩽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));
    }
}
posted @ 2023-08-13 20:52  bingxin-ly  阅读(6)  评论(0编辑  收藏  举报