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

思路

由于数字出现的次数可能不止一次,所以本题可以用二分法寻找边界。左右边界即该数出现的位置区间端点

二分法的一般形式:

  1. 设置搜索范围,头尾分别为 lr
  2. 设置循环条件:l < r,跳出循环时即找到了所求分界点
  3. 初始化 mid,由 lr计算而来
  4. 判断此时 a[mid]与寻求数值的相对关系,更新 l或者 r
  5. 跳出循环时,就找到了目标

// 这里展示的是寻找左侧分界点,即数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;
}

另一种理解

  1. a[mid] >= k时,则答案比 mid一定小/相等,在 mid的左侧,或者和 mid相等。

mid位置的右侧 (不包括 mid,一定不会是答案,所以可以排除 mid右侧的所有部分,但是不能排除 mid。所以我们让 r端点变成 mid

  1. a[mid] < k时,则答案一定比 mid大,在 mid的右侧,不能和 mid相等

mid位置的左侧 (包括 mid,一定不会是答案,所以可以排除 mid及其左侧的部分。我们让 l端点变成 mid + 1

这里有一个很重要的区分点!!!——mid的更新

mid的更新是 (l + r) / 2。整除,也就是向下取整。为什么不能向上取整来更新 mid考虑这种情况

l + 1 = r。即 lr相邻。并且!!!——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;
}
最后修改:2025 年 03 月 21 日
如果觉得我的文章对你有用,请随意赞赏