P2249 【深基13.例1】查找

记录112

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int a[N];
int n,m,q;
int f_first(int x){//找到第一个下标(二分查找修改版) 
	int l=1,r=n;//左右边界 
	int pos=-1;//返回下标位置 
	while(l<=r){//满足查找 
		int mid=(l+r)/2;//中间值 
		if(a[mid]==x){//找到了 
			pos=mid;//记录下标 
			r=mid-1;//缩小右边界(找第一个下标) 
		}else if(a[mid]>x) r=mid-1;//缩小有边界 
		else l=mid+1;//缩小左边界 
	}
	return pos;//返回下标 
} 
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=m;i++){
		cin>>q;
		cout<<f_first(q)<<" ";
	}
	return 0;//结束程序 
}

前言

我是一名专注信奥赛(CSP-J/S、NOIP)的教练。

  • 如果你觉得这篇题解对你有帮助,欢迎点击关注我的CSDN账号,我会持续更新高质量算法解析。
  • 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
  • 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。

题目传送门https://www.luogu.com.cn/problem/P2249


突破口

输入 n 个不超过 10的9次方 的单调不减的(就是后面的数字不小于前面的数字)非负整数 a1​,a2​,...,an​,然后进行 m 次询问。对于每次询问,给出一个整数 q,要求输出这个数字在序列中第一次出现的编号,如果没有找到的话输出 −1 。


🔍 一、题目本质与核心要求

🎯 问题重述

  • 给定一个 长度为 n 的非负整数序列 a[1..n]
  • 序列满足:单调不减 (即 a[i] ≤ a[i+1]
  • 进行 m 次查询,每次给一个整数 q
  • 要求:输出 q 在序列中第一次出现的下标(从 1 开始)
  • 若未出现,输出 -1

✅ 关键词提炼:

  • 有序数组 → 可用二分查找
  • 第一次出现 → 需要"左边界"二分
  • 多次查询 → 每次 O(log n) 比 O(n) 快得多
  • 数据量大(n ≤ 1e6, m ≤ 1e5)→ 必须用高效算法

📌 样例解析(输入 #1)

cpp 复制代码
n=11, m=3
a = [1, 3, 3, 3, 5, 7, 9, 11, 13, 15, 15]
queries: q = 1, 3, 6
  • q=1:首次出现在位置 1
  • q=3:首次出现在位置 2(不是 3 或 4!)
  • q=6:不存在 → -1

输出:1 2 -1

🧠 二、核心知识点拆解

1. 为什么能用二分?

  • 因为数组 单调不减(sorted non-decreasing)
  • 二分查找的前提是 有序性

2. 普通二分 vs "找第一个出现位置"

  • 普通二分:找到任意一个等于 x 的位置即可
  • 本题要求:最左边的等于 x 的位置
  • 解法:在找到 a[mid] == x 后,不立即返回,而是继续向左搜索

💡 这称为 "lower_bound"(下界)问题

3. 时间复杂度分析

  • 每次查询:O(log n)
  • 总时间:O(m log n) ≈ 1e5 × log₂(1e6) ≈ 1e5 × 20 = 2e6 操作 ✅ 可接受
  • 若暴力扫描:O(m × n) ≈ 1e11 ❌ 超时

4. 边界处理

  • 数组下标从 1 开始(题目要求输出从 1 编号)
  • 二分初始:l = 1, r = n
  • 未找到时返回 -1

代码分析

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int a[N]; // 存储序列,下标从1开始
int n, m, q;
  • N = 1e6 + 10:略大于最大 n(1e6),防越界
  • 全局变量自动初始化为 0
cpp 复制代码
int f_first(int x) { // 找 x 第一次出现的位置(1-based)
    int l = 1, r = n;   // 搜索区间 [1, n]
    int pos = -1;       // 默认未找到
cpp 复制代码
    while (l <= r) {
        int mid = (l + r) / 2; // 中点(注意:此处无溢出风险,因 n ≤ 1e6)
  • 标准二分循环条件:l <= r
cpp 复制代码
        if (a[mid] == x) {
            pos = mid;     // 记录当前位置
            r = mid - 1;   // 继续向左找更早的出现位置!
        }

🔥 这是本题最关键的逻辑!

  • 即使找到了 x,也不能停
  • 必须尝试在 [l, mid-1] 中找更靠左的 x
cpp 复制代码
        else if (a[mid] > x) 
            r = mid - 1;   // x 在左半部分
        else 
            l = mid + 1;   // a[mid] < x,x 在右半部分
    }
    return pos; // 若从未找到,pos 保持 -1
}
cpp 复制代码
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) 
        cin >> a[i]; // 读入序列,1-based
cpp 复制代码
    for (int i = 1; i <= m; i++) {
        cin >> q;
        cout << f_first(q) << " ";
    }
    return 0;
}

潜在优化与注意事项

问题 说明
IO 速度 题目提示"输入输出量较大",建议加 ios::sync_with_stdio(false); cin.tie(0); 但原代码未加,可能在极限数据下 TLE
mid 计算 (l + r) / 2 在 n ≤ 1e6 时安全(l+r ≤ 2e6 < 2³¹)
返回值 正确处理了"未找到"情况(pos 初始为 -1)
下标一致性 全程使用 1-based,与题目要求一致

✅ 建议优化 IO(虽原代码没写,但实际竞赛应加上):

cpp 复制代码
ios::sync_with_stdio(false);
cin.tie(nullptr);

与其他方法对比

方法 时间复杂度 是否可行
暴力线性扫描 O(m × n) ≈ 1e11 ❌ 超时
哈希表预处理 O(n + m),但无法保证"第一次出现"顺序 ❌ 错误(哈希存的是任意位置)
二分查找(左边界) O(m log n) ≈ 2e6 ✅ 正确高效
相关推荐
烤麻辣烫2 小时前
算法--二分搜索
java·开发语言·学习·算法·intellij-idea
苏宸啊2 小时前
C++智能指针
c++
山甫aa2 小时前
二叉树算法-----从零开始的算法
数据结构·算法
OYangxf2 小时前
基于epoll的单线程Reactor:Tinyredis的网络层实现
c++·redis
睡觉就不困鸭3 小时前
第十七天 翻转字符串里的单词
数据结构·算法·哈希算法·散列表
ulias2123 小时前
leetcode热题 - 4
算法·leetcode·职场和发展
yinbinggang3 小时前
vmware安装虚拟机
c++
学术阿凡提3 小时前
Spring Boot 优雅实现异步调用:从入门到自定义线程池与异常处理
java·数据库·算法
MicroTech20253 小时前
微算法科技(NASDAQ :MLGO)量子化边缘检测技术:重塑图像处理的新范式
图像处理·科技·算法