Introduction
对于有分界性的数组,需要寻找某个条件的分界处,可以使用二分法
例如,给定一个单调数组和一个数 x
,可以找到 x
第一次出现的位置
当然,二分法不止适合于单调性,只要是有边界性,都可以使用二分法寻找
Question
$$ \begin{align} &给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。\\ &对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。\\ &如果数组中不存在该元素,则返回 `-1 -1`。\\ &\textbf{输入格式}\\ &第一行包含整数 n 和 q,表示数组长度和询问个数。\\ &第二行包含 n 个整数(均在 1∼10000 范围内),表示完整数组。\\ &接下来 q 行,每行包含一个整数 k,表示一个询问元素。\\ &\textbf{输出格式}\\ &共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。\\ &如果数组中不存在该元素,则返回 `-1 -1`。\\ &\textbf{数据范围}\\ &1≤n≤100000\\ &1≤q≤10000\\ &1≤k≤10000\\ \end{align} $$
输入样例:
6 3
1 2 2 3 3 4
3
4
5
输出样例
3 4
5 5
-1 -1
思路
由于数字出现的次数可能不止一次,所以本题可以用二分法寻找边界。左右边界即该数出现的位置区间端点
二分法的一般形式:
- 设置搜索范围,头尾分别为
l
和r
- 设置循环条件:
l < r
,跳出循环时即找到了所求分界点 - 初始化
mid
,由l
和r
计算而来 - 判断此时
a[mid]
与寻求数值的相对关系,更新l
或者r
- 跳出循环时,就找到了目标
// 这里展示的是寻找左侧分界点,即数k首次出现的位置
int l = 0, r = n - 1;
while (l < r) {
int mid = l + r >> 1;
// 初始化、更新mid
if (a[mid] >= k) r = mid;
// 给定数组是有序的。因为要找最左侧的出现,所以进行判断:
/*
1. 如果a[mid]大于等于k,说明目标位置在mid的左侧或和mid重合,我们让边界点r变成mid
2. 如果a[mid]小于k,说明目标位置在mid右侧,最最最接近也是在mid+1的位置,我们让边界点l变成mid+1
*/
else l = mid + 1;
}
另一种理解:
- 当
a[mid] >= k
时,则答案比mid
一定小/相等,在mid
的左侧,或者和mid
相等。
mid
位置的右侧 (不包括 mid
),一定不会是答案,所以可以排除 mid
右侧的所有部分,但是不能排除 mid
。所以我们让 r
端点变成 mid
- 当
a[mid] < k
时,则答案一定比mid
大,在mid
的右侧,不能和mid
相等
mid
位置的左侧 (包括 mid
),一定不会是答案,所以可以排除 mid
及其左侧的部分。我们让 l
端点变成 mid + 1
mid
的更新是 (l + r) / 2
。整除,也就是向下取整。为什么不能向上取整来更新 mid
?考虑这种情况:
l + 1 = r。即 l
和 r
相邻。并且!!!——a[l] = a[r] = k
,即两者都等于查找的数 k
,显然 l
是需要找到的数。在 (l + r) / 2
处理时,mid
= l
,会向左靠,所以可以获取到正确位置。
但是,如果使用 (l + r + 1) / 2
的更新方式,会出现死循环的大问题:r = mid
之后,更新 mid
时,给 mid
赋值仍为 r
!!!会陷入死循环,永远达不到 l > r
不满足。
代码实现
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int main () {
int n, q;
scanf("%d%d", &n, &q);
for (int i = 0; i < n; i ++) scanf("%d", &a[i]);
while (q --) {
int l = 0, r = n - 1;
int k;
scanf("%d", &k);
while (l < r) {
int mid = (l + r) >> 1;
if (a[mid] >= k) r = mid;
else l = mid + 1;
}
if (a[l] != k) cout << "-1 -1" << endl;
else {
cout << l << ' ';
l = 0, r = n - 1;
while (l < r) {
int mid = (l + r + 1) >> 1;
if (a[mid] <= k) l = mid;
else r = mid - 1;
}
cout << l << endl;
}
}
return 0;
}