线段树上二分
线段树上二分
费劲学的,得单领出来说说。网上有没有多少详细文章,有例题。
线段树的奇幻科技——线段树上二分 - 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)