二分与前缀和
有一个已经按升序排好序的数组,求某个数在数组中出现的下标区间。即,若这个数在数组中出现一次或多次,就给出这个范围。
#include <iostream>
using namespace std;
const int maxn = 100005;
int n, q, x, a[maxn];
int main() {
scanf("%d%d", &n, &q);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
while (q--) {
scanf("%d", &x);
int l = 0, r = n - 1;
while (l < r) {
int mid = l + r >> 1;
if (a[mid] < x) l = mid + 1;
else r = mid;
}
if (a[l] != x) {
printf("-1 -1\n");
continue;
}
int l1 = l, r1 = n;
while (l1 + 1 < r1) {
int mid = l1 + r1 >> 1;
if (a[mid] <= x) l1 = mid;
else r1 = mid;
}
printf("%d %d\n", l, l1);
}
return 0;
}
设区间为 [left,right]。
首先用二分法求区间 left。对应代码:
int l = 0, r = n - 1;
while (l < r) {
int mid = l + r >> 1;
if (a[mid] < x) l = mid + 1;
else r = mid;
}
初始时 l 和 r 处都有值。mid 的值靠近 l。
- 如果 a[mid] 小于目标值 x,则说明 l 小了,此时调整 l 到 mid+1。这里是想求出下界,所以要保证所有小于 x 的数也一定小于 a[l]。
- 如果 a[mid] 等于 x,则说明已经探测到区间内部了,但是不知道边界在哪里。注意!这一步只是要求左边界,所以需要极力保证左边,对右边可以随意。因此,将 r 移到 mid 处,缩小了查找的范围。由于已经保证了 l 处的值一定大于数组中所有小于 x 的数,因此当 x 存在时,l 就是 x 的左边界;当 x 不存在时,l 是数组中第一个大于 x 的数所在的位置。此时 l 就不会动了,r 会不停地变小,直到等于 l,循环终止。
这里证明一下,当 x 存在时 l 一定是左边界。如果 l 是 x 出现区间中除左边界以外的某个点,由于 l 的变化规则是 mid0+1,这就说明上一步的 mid0=l-1。而 l 不是左边界(l>left),因此 mid0 一定满足 left<=mid0<l,这就说明当时的情况是 a[mid0]==x,那这怎么会让 l 变化呢……矛盾了。
- 如果 a[mid] 大于 x,显然是因为 r 大了,就要让 r 减小到 mid。这里其实也可以减小到 mid-1,但是就不能和 a[mid]==x 的情况写在一起了。
else if(a[mid]==k) r=mid; else r=mid-1;
- 循环终止条件:a[l]<=x, a[r]>=x,因此 l 和 r 有可能相等。
求右边界:
int l1 = l, r1 = n;
while (l1 + 1 < r1) {
int mid = l1 + r1 >> 1;
if (a[mid] <= x) l1 = mid;
else r1 = mid;
}
直接在刚才的基础上,用 l 作为左边界(l 本来就是左边界嘛,如果要一般化就还是 l1=0),r1 从没有值的 n 处开始(总范围是 [0,n-1])。
我们的目标是让 l1 成为右边界。同时,我们要保证 r1 永远是大于 x 的。
- 当 a[mid]==x 时,我们并不知道此时的 mid 到底是内部点还是什么,所以只敢动 l1,不能动 r1。因此 l1=mid。
- 当 a[mid]<x 时,显然还是要增大 l1,同理这里也可以是 l1=mid+1,但是不能和 a[mid]==x 的情况写在一起。
- 当 a[mid]>x 时,要减小 r1。如果 r1=mid-1,则 r1 有可能变成区间右边界了,这不行,因为我们想让 r1 大于 x。因此 r1=mid。
- 再说说循环终止条件。a[l1] 永远是 <=x 的,a[r1] 永远是 >x 的。最后的关系一定是 l1=r1-1。如果 x 不存在,则 a[l1]<x, a[r1]>x,最后也是 l1=r1-1 的关系。