文章

【GESP】C++五级练习题 luogu-P2249 【深基13.例1】查找

GESP C++ 五级练习题,二分查找考点应用,重点理解二分查找和STL二分查找库的应用。五级考生可以练习。题目难度⭐⭐☆☆☆,洛谷难度等级普及−

luogu-P2249 【深基13.例1】查找

题目要求

题目描述

输入 $n$ 个不超过 $10^9$ 的单调不减的(就是后面的数字不小于前面的数字)非负整数 $a_1,a_2,\dots,a_{n}$,然后进行 $m$ 次询问。对于每次询问,给出一个整数 $q$,要求输出这个数字在序列中第一次出现的编号,如果没有找到的话输出 $-1$ 。

输入格式

第一行 $2$ 个整数 $n$ 和 $m$,表示数字个数和询问次数。

第二行 $n$ 个整数,表示这些待查询的数字。

第三行 $m$ 个整数,表示询问这些数字的编号,从 $1$ 开始编号。

输出格式

输出一行,$m$ 个整数,以空格隔开,表示答案。

输入输出样例 #1

输入 #1
1
2
3
11 3
1 3 3 3 5 7 9 11 13 15 15
1 3 6
输出 #1
1
1 2 -1 

说明/提示

数据保证,$1 \leq n \leq 10^6$,$0 \leq a_i,q \leq 10^9$,$1 \leq m \leq 10^5$

本题输入输出量较大,请使用较快的 IO 方式。


题目分析

1. 问题分析

本题的核心任务是在一个单调不减(即有序)的序列中,查找给定数字 $q$ 第一次出现的位置(编号)。如果找不到,输出 -1。

  • 数据规模:序列长度 $n \le 10^6$,询问次数 $m \le 10^5$。
  • 朴素做法:如果对于每次询问都从头遍历序列查找(线性查找),单次查询时间复杂度为 $O(n)$,总时间复杂度为 $O(n \times m) \approx 10^{11}$。这一计算量远超通常 1 秒(约 $10^8$ 次运算)的时间限制,会导致超时(TLE)。
  • 优化算法:利用序列有序这一特性,我们可以使用二分查找算法。二分查找的单次查询时间复杂度为 $O(\log n)$。总时间复杂度为 $O(m \log n) \approx 10^5 \times 20 \approx 2 \times 10^6$,这是一个非常高效的算法,完全可以在时限内通过。

2. 解题思路

我们需要找到序列中第一个等于 $q$ 的元素的位置。这与普通的二分查找(只要找到一个相等甚至找 mid)略有不同,需要处理“第一个”这个要求。

方法一:手写二分查找

我们可以维护一个查找区间 $[l, r]$,并使用此时的中间位置 $mid$ 进行判断:

  1. 如果 nums[mid] < q:说明 $q$ 一定在 $mid$ 的右侧,更新 $l = mid + 1$。
  2. 如果 nums[mid] >= q
    • 此时 $nums[mid]$ 可能是 $q$,也可能比 $q$ 大。
    • 即使 nums[mid] == q,由于我们要找的是第一个出现的位置,可能左边还有等于 $q$ 的数。
    • 因此,我们记录当前位置为可能的答案(ans = mid,仅当相等时),并继续尝试向左侧区间收缩查找,更新 $r = mid - 1$。
  3. 循环结束后的 ans 即为结果。初始化 ans = -1,如果从未找到等于 $q$ 的数,则输出 -1。
方法二:使用 STL 的 lower_bound

我们在文章【GESP/CSP】编程武器库-5, 二分查找标准库(lower_bound/upper_bound)中刚刚介绍了C++ 标准库 <algorithm> 中提供了 std::lower_bound 函数,非常适合解决此类问题。

  • 判断
    • 通过 lower_bound 找到位置 p
    • 首先检查 p 是否越界(即是否等于 last),如果越界说明所有数都比 $q$ 小,没找到。
    • 其次检查 *p 是否等于 q。因为 lower_bound 返回的是 $\ge q$ 的第一个数,可能是 $q$ 也可能是比 $q$ 大的数。只有当 *p == q 时,才是找到了。
  • 计算下标:利用指针减法 p - nums 即可得到对应的下标(注意数组的起始位置)。


示例代码

方法一、手写二分查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>

int nums[1000005];
int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int n, m;
    std::cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        std::cin >> nums[i];
    }
    // 二分查找:寻找第一个等于 q 的位置
    while (m--) {
        int q;
        std::cin >> q;
        int ans = -1;
        int l = 1, r = n;  // 初始化左右边界
        while (l <= r) {
            int mid = l + (r - l) / 2;  // 防止溢出的中间位置计算
            if (nums[mid] < q) {
                // 中间值小于目标值,说明目标在右半部分,且不包括 mid
                l = mid + 1;
            } else {
                // nums[mid] >= q
                if (nums[mid] == q) {
                    // 找到了一个等于 q 的值,记录位置
                    // 但我们要找的是 *第一个*,所以继续向左找
                    ans = mid;
                }
                // 即使找到了,也要向左收缩 r,尝试寻找更靠左的 q
                // 或者 nums[mid] > q,也需要向左收缩
                r = mid - 1;
            }
        }
        std::cout << ans << " ";
    }

    return 0;
}

方法二、STL 二分查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <algorithm>
#include <iostream>
#include <vector>

// P2249 【深基13.例1】查找 - STL版本实现
// 使用 std::lower_bound 快速查找第一个大于等于目标值的元素位置

int nums[1000005];

int main() {
    // 优化输入输出效率
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int n, m;
    if (!(std::cin >> n >> m)) return 0;

    for (int i = 1; i <= n; i++) {
        std::cin >> nums[i];
    }

    while (m--) {
        int q;
        std::cin >> q;

        // std::lower_bound 返回指向第一个 >= q 的元素的指针(或迭代器)
        // 范围是 [nums + 1, nums + n + 1)
        int* p = std::lower_bound(nums + 1, nums + n + 1, q);

        // 检查是否找到:
        // 1. p != nums + n + 1: 确保没有越界(即数组中存在 >= q 的数)
        // 2. *p == q: 确保找到的数确实是 q(因为 lower_bound 也可能返回大于 q
        // 的数)
        if (p != nums + n + 1 && *p == q) {
            // 计算下标:指针相减
            std::cout << (p - nums) << " ";
        } else {
            // 没找到或者找到的是大于 q 的数
            std::cout << -1 << " ";
        }
    }
    std::cout << "\n";

    return 0;
}

所有代码已上传至Github:https://github.com/lihongzheshuai/yummy-code

GESP 学习专题站:GESP WIKI

luogu-”系列题目可在洛谷题库进行在线评测。

bcqm-”系列题目可在编程启蒙题库进行在线评测。

欢迎加入Java、C++、Python技术交流QQ群(982860385),大佬免费带队,有问必答

欢迎加入C++ GESP/CSP认证学习QQ频道,考试资源总结汇总

欢迎加入C++ GESP/CSP学习交流QQ群(688906745),考试认证学员交流,互帮互助

GESP/CSP 认证学习微信公众号
GESP/CSP 认证学习微信公众号
本文由作者按照 CC BY-NC-SA 4.0 进行授权