线段树上二分

线段树上二分

费劲学的,得单领出来说说。网上有没有多少详细文章,有例题。
线段树的奇幻科技——线段树上二分 - Mercury_City - 博客园 (cnblogs.com) 这篇博客讲的好,但仍不详细。
不是二分+线段树,是直接利用线段树去二分查找。从而把 \(O(\log^2 n)\) 变为 \(O(\log n)\)

基本原理是直接利用当前的左子树和右子树,去判断目的解是在左还是右,然后直接进入有解的子树。而对于限制区间,只需要按正常查询的思维,只去与目标区间相交(包括在之内的情况)的区间。因为如果这个区间不完全包含在目标区间内,那么提供的信息就不准确,所以要到完全包含的区间内提供信息。看了代码应该就理解这句话了。

我研究了得一天,下面给出我测试所用的代码包,和这个优化的效果。

一个基本的例题是查找区间 \([1,n]\) 内第一个大于 \(x\) 的数的下标。按照意思可以写出以下代码:

int query(int u, int x)
{
    if (tr[u].l == tr[u].r) return tr[u].l;
    else 
    {
        int mid = l + r >> 1;
        if (tr[u << 1].maxv > x) return query(u << 1, l, r, x);  // 如果左边有就直接去左边
        else return query(u << 1 | 1, l, r, x); // 否则一定在右边
    }
}

但是这会导致一个问题,即如果不存在这样的数,我们没法表示出,所以要表示无解情况,一般以 -1 作为无解情况。即:

int query(int u, int x)
{
    if (tr[u].l == tr[u].r)
    {
        if (tr[u].maxv <= x) return -1;
        return tr[u].l;
    }
    else 
    {
        int mid = l + r >> 1; // 多余了
        if (tr[u << 1].maxv > x) return query(u << 1, l, r, x); 
        else return query(u << 1 | 1, l, r, x);
    }
}

每次要么去左边要么去右边,所以时间复杂度一定 \(O(\log n)\)

如果限制区间为 \([l, r]\) 呢?按照上面说的,如果有限制区间,那么当前区间和限制区间的关系就分为相交,被包含和不相交。对于不相交,我们就不进入子树,而对于相交,只要相交就进入对应子区间,因为要利用相交的那部分的信息。实际上这部分和正常 query 一样。

对于包含则进行上面那样的二分。如果到了对应节点就返回信息。实际上对于不相交的情况,我们不用管他,只进行相交的进入子树即可。

于是可以写出以下代码

int query(int u, int l, int r, int x)
{
    if (tr[u].l == tr[u].r) // 找到一个解
    {
        if (tr[u].maxv <= x) return -1; // 去除无效解
        return tr[u].l;
    }
    else if (l <= tr[u].l && tr[u].r <= r) // 可以提供信息
    {
        if (tr[u << 1].maxv > x) return query(u << 1, l, r, x); 
        else return query(u << 1 | 1, l, r, x);
    }
    else // 仅相交,要继续划分区间,直到被包含
    {
        int mid = tr[u].l + tr[u].r >> 1;
        int res = -1;
        if (l <= mid) res = query(u << 1, l, r, x); // 去含有的区间内, 和正常query一样
        if (res == -1 && r > mid) res = query(u << 1 | 1, l, r, x); 
    
        return res;
    }
}

整体思路就是,正常 query 进限制区间,在区间内二分,直到有解返回。

二分是可以封边界的,上面代码就封左边界,即找尽量靠左的符合条件的点。对于封左/右边界,你只需要让它趋于进入左/右子树即可。给出对应封右的代码。

int query(int u, int l, int r, int x)
{
    if (tr[u].l == tr[u].r)
    {
        if (tr[u].maxv <= x) return -1; 
        return tr[u].l;
    }
    else if (l <= tr[u].l && tr[u].r <= r) 
    {
        if (tr[u << 1 | 1].maxv > x) return query(u << 1 | 1, l, r, x); // 尽量先进右子树
        else return query(u << 1, l, r, x);
    }
    else 
    {
        int mid = tr[u].l + tr[u].r >> 1;
        int res = -1;
        if (r > mid) res = query(u << 1 | 1, l, r, x);  // 同理
        if (res == -1 && l <= mid) res = query(u << 1, l, r, x); 
    
        return res;
    }
}

测试文件: https://pan.axianyu.cn/f/3vqFn/线段树二分.zip

例题

来做做例题把,我费劲自己搞得 U502676 线段树上二分模版 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
还有一个例题 P11217 【MX-S4-T1】「yyOI R2」youyou 的垃圾桶 - blind5883 - 博客园 (cnblogs.com)

posted @ 2024-11-23 22:05  blind5883  阅读(1)  评论(0编辑  收藏  举报