Bootstrap

算法_确定数的范围(二分法)

目录

 一、问题介绍

二、代码模板

三、算法解释

问题分析

算法流程

总结

四、边界问题

 一、问题介绍

给定升序排序的长度为n的数组,查询m次(自定义次数)

每次查询,返回一个元素的起始位置和终止位置(从0开始)

如果数组中不存在该元素,则返回-1 -1

二、代码模板

AcWing 789. 数的范围

#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int n, m;
int q[N];

int main() {
	scanf_s("%d%d", &n, &m);
	for (int i = 0; i < n; i++) scanf_s("%d", &q[i]);
	int l = 0, r = n - 1;
	while (m--) {
		int x;
		scanf_s("%d", &x);

		int l = 0, r = n - 1;
		while (l < r) {
			int mid = l + r >> 1;
			if (q[mid] >= x) r = mid;
			else l = mid + 1;
		}
		if (q[l] != x) printf("-1 -1\n");
		else {
			printf("%d ", l);
			int l = 0, r = n - 1;
			while (l < r) {
				int mid = l + r + 1 >> 1;
				if (q[mid] <= x) l = mid;
				else r = mid - 1;
			}
			printf("%d\n", r);
		}
	}
	return 0;
}

三、算法解释

问题分析

解决该问题实际上是确定需要查询的元素在序列中的左右边界问题,比如1,2,2,2,3,3,4这段序列,需要查询2时就是要确定2对应的边界位置为1和3,即元素2的左边界和右边界。所以问题可以拆分为确定元素的左边界和右边界。

算法流程

算法的流程是先确定左边界,然后确定右边界。

如何确定左边界和右边界?利用2分法循环缩小l~r的范围直到l == r,来分别确定左边界和右边界。

后面确定左边界和右边界会用到的两个条件:

1.左边界左边(前面)的元素都小于x,位置在左边界或在左边界右边(后面)的元素都大于等于x

2.右边界右边(后面)的元素都大于x,位置在右边界或在右边界左边(前面)的元素都小于等于x

确定左边界:

设置初始边界l和r为0和n-1,进行二分,取mid = l + r >> 1,(后面会解释为什么不是mid = l + r + 1 >> 1),然后将去q[mid]与我们所要查询的元素进行比较,如果q[mid] >= x,则说明mid在左边界的右边(q[mid] > x)或就是右边界(q[mid] == x),接下来我们可以将l~r的范围缩小,因为mid在左边界右边或就是有边界,所以令r = mid(为什么等于mid而不是mid-1呢,因为mid可能就是左边界);如果q[mid] < x,则说明mid在左边界的左边(q[mid] < x),接下来将l~r的范围缩小,因为mid在左边界左边,所以令l = mid + 1(为什么加1呢?因为mid是在左边界左边肯定不是左边界)。然后不断循环上面的步骤来缩小确定边界的范围直到l == r时,确定了边界的位置为l(r也一样),然后再判断这个位置是不是边界(注意这里的边界只是我们通过缩小l和r的范围得到的可能作为左边界的位置,但这个位置可能不一定是元素x的左边界,所以我们还要判断这个位置对应的元素等不等于x,来最终确定是不是左边界)。如果这个位置不是左边界,则输出-1 -1,如果是左边界,则输出l(输出r也行),然后接下来要确定右边界。

确定右边界(和确定左边界比较相似了):

右边界是我们在已经确定了左边界以后进行了,所以右边界一定存在(至少也会出现在左边界的位置)。

设置初始边界l和r为0和n-1,进行二分,取mid = l + r + 1 >> 1(这里后面也会解释为什么不是mid = l + r >> 1),然后将去q[mid]与我们所要查询的元素进行比较吗,如果q[mid] <= x,则说明mid在右边界的左边(q[mid] < x)或就是右边界(q[mid] == x),接下来缩小l~r的范围,因为mid在右边界左边或就是右边界,所以令l = mid(为什么等于mid而不是mid+1呢?因为mid可能就是右边界);如果q[mid] > x,则说明mid在右边界的右边(q[mid] > x),接下来将l~r的范围缩小,因为mid在右边界右边所以令r = mid - 1(为什么要减1呢?因为mid是在右边界右边肯定不是右边界)。然后不断循环上面的步骤来缩小确定边界的范围直到l == r时,确定了边界的位置l(r也一样),然后不需要判断该位置是否就是右边界,因为既然确定了左边界就一定有右边界(就算只有一个元素,右边界的位置也可以就是左边界的位置)。然后输出l(输出r也行)

总结

实际上算法的流程就是1.用2分法缩小范围确定左边界位置 2.判断确定的位置对应元素是否等于x来确定是否是左边界 3.用2分法缩小范围确定右边界位置(该位置对应元素一定等于x)

四、边界问题

有两个前文落下未解释的边界问题:

1.为什么确定左边界时是mid = l + r >> 1而不是mid = l + r + 1 >> 1?

因为如果用mid = l + r + 1 >> 1,则当循环到最后只剩两个数时,mid选取的是第二个元素的位置即r,如果q[mid] >= x,则r = mid即r = r,范围不变,会进入死循环。e.g. x取1,最后只剩两个元素1,2,mid为1,q[mid] >= x,所以循环的范围不变,进入死循环。而使用mid = l + r >> 1,则会取前一个元素位置为mid即l,q[mid] >= x时,r = mid,因为此时l == r所以会退出循环;q[mid] < x时,l = mid + 1也会出现l == r退出循环。所以这样设置mid = l + r >> 1来确定左边界不会出现边界问题。

2.为什么确定右边界时是mid = l + r + 1 >> 1而不是mid = l + r >> 1?(和前面一个问题类似)

因为如果用mid = l + r >> 1,则当循环到最后只剩两个数时,mid选取的是第一个元素的位置即l,如果q[mid] <= x,则l = mid即l = l,范围不变,会进入死循环。而使用mid = l + r + 1 >> 1,则会取后面一个元素位置为mid即r,q[mid] <= x是,l = mid,因为此时l == r所以会退出循环;q[mid] > x时,r = mid - 1也会出现l == r退出循环。所以这样设置mid = l + r + 1 >> 1来确定右边界不会出现边界问题。

;