【基础算法】二分算法深度剖析:从模板到实战,二分查找与二分答案一网打尽

【基础算法】二分算法深度剖析:从模板到实战,二分查找与二分答案一网打尽
在这里插入图片描述
🔭 个人主页:散峰而望

《C语言:从基础到进阶》《编程工具的下载和使用》《C语言刷题》《算法竞赛从入门到获奖》《人工智能》《AI Agent》

愿为出海月,不做归山云


🎬博主简介

在这里插入图片描述
请添加图片描述

【基础算法】二分算法深度剖析:从模板到实战,二分查找与二分答案一网打尽


前言

前言

在算法的世界里,二分算法如同一把精准的钥匙,能快速开启有序数据的大门。无论是经典的有序序列查找,还是复杂的“最小值最大”类问题,二分思想都以惊人的效率(O(log n))化繁为简,成为程序员必须掌握的核心技能。然而,许多初学者在运用二分时常常陷入边界不清、死循环或答案错误的窘境。本文将从二分的本质出发,深入剖析其原理与易错点,提供清晰可靠的模板,并带你走进 STL 中的二分利器。随后,我们将通过“二分查找”与“二分答案”两大实战板块,精选六道经典题目,从“牛可乐和魔法封印”的区间查询,到“跳石头”的最优化决策,一步步拆解题意、推导思路、实现代码。无论你是刚接触二分的新手,还是希望查漏补缺的进阶者,本文都将助你彻底吃透二分算法,让二分成为你解题时的本能反应。

1. 二分算法

1.1 二分算法的相关概念

二分算法是一种基于分治策略的高效搜索算法。它利用数据的有序性,每轮缩小一半搜索范围,直至找到目标元素或搜索区间为空为止。

二分算法的原理以及模板其实是很简单的,主要的难点在于问题中的各种各样的细节问题。因此,大多数情况下,只是背会二分模板并不能解决题目,还要去处理各种乱七八糟的边界问题。

二分算法的核心:根据中点位置的值,判断最终解集是在左边区域还是在右边区域。如果确定一半区域,可以舍弃另一半区域,在有所求答案的区域内找。

1.2 二分算法的探讨

在排序数组中查找元素的第一个和最后一个位置

在这里插入图片描述

算法原理:

  1. 暴力解法:从前往后扫描数组,时间复杂度是 O(N) ,慢就慢在没有利用数组的有序性。
在这里插入图片描述

我们发现,假设要查找的数为 6 ,我们可以将这一串数分为两部分,计算中点为 12 ,则将 12 的左边分为小于 12 的区域,右边分为大于 12 的区域。因为 6 比 12 小,故 6 肯定在 12 的左侧区域,这样的话 12 右侧的区域就不用查找了。之后再在 12 的左侧按照以上的方式继续查找。此时我们发现解集中存在二段性,就可以用到二分算法了。

在这里插入图片描述
  1. 二分算法:

二分算法中需要解决两个位置,一个是起始位置,另一个是终止位置

首先需要更新左右指针的位置:

细节问题:

(1) 查找起始位置:

在这里插入图片描述
  • while 循环判断需要如何写:
    两种写法:while(left < right)while(left <= right)
    应该选 while(left < right) ,另外一种写法会造成死循环。
  • 求中点的方式:
    同样有两种写法:(left + right) / 2(left + right + 1) / 2
    应该选 (left + right) / 2 ,另外一种写法会造成死循环。

(2) 查找终止位置:

在这里插入图片描述
  • while 循环判断需要如何写:
    两种写法:while(left < right)while(left <= right)
    应该选 while(left < right) ,另外一种写法会造成死循环。
  • 求中点的方式:
    同样有两种写法:(left + right) / 2(left + right + 1) / 2
    应该选 (left + right + 1) / 2 ,另外一种写法会造成死循环。
classSolution{public: vector<int>searchRange(vector<int>& nums,int target){int n = nums.size();//处理边界情况if(n ==0)return{-1,-1};//求起始位置int left =0, right = n -1;while(left < right){int mid =(left + right)/2;if(nums[mid]>= target) right = mid;else left = mid +1;}//left 或 right 所指位置可能是最终结果if(nums[left]!= target)return{-1,-1};int releft = left;//求终止位置 left =0, right = n -1;while(left < right){int mid =(left + right +1)/2;if(nums[mid]<= target) left = mid;else right = mid -1;}return{releft, left};}};

1.3 二分算法模板

这里我们可以整理出来两个二分模板,方便解决前面所提的细节问题,便于以后使用:

// 二分查找区间左端点 int l =1, r = n;while(l < r){int mid =(l + r)/2;if(check(mid)) r = mid;else l = mid +1;}// 二分结束之后可能需要判断是否存在结果 
// 二分查找区间右端点 int l =1, r = n;while(l < r){int mid =(l + r +1)/2;if(check(mid)) l = mid;else r = mid -1;}// 二分结束之后可能需要判断是否存在结果
check() 是查找函数。

为了防止溢出,求中点时可以下面的方式:mid = l + (r - l) / 2

时间复杂度:

每次二分都会去掉一半的查找区域,因此时间复杂度为 O(logN)

1.4 STL 中的二分

头文件:<algorithm>

  1. lower_bound:大于等于 x 的最小元素,返回的是迭代器;时间复杂度:O(logN)
  2. upper_bound:大于 x 的最小元素,返回的是迭代器。时间复杂度:O(logN)

二者均采用二分实现。但是 STL 中的二分查找只能适用于"在有序的数组中查找",如果是二分答案就不能使用。因此还是需要记忆二分模板。

2. 二分查找

2.1 牛可乐和魔法封印

牛可乐和魔法封印

在这里插入图片描述

算法原理:

  1. 暴力解法:从前往后扫描一遍,时间复杂度 O(n * q)
  2. 二分查找算法模版题,直接上手模版即可。
    但是需要注意,有可能并没有这个区间,需要在二分结束之后判断一下。

参考代码:

#include<iostream>usingnamespace std;constint N =1e5+10;int n;int a[N];intbinary_search(int x,int y){//大于等于 x 的最小元素int left =1, right = n;while(left < right){int mid =(left + right)/2;if(a[mid]>= x) right = mid;else left = mid +1;}if(a[left]< x)return0;int tmp = left;//小于等于 y 的最大元素 left =1, right = n;while(left < right){int mid =(left + right +1)/2;if(a[mid]<= y) left = mid;else right = mid -1;}if(a[left]> y)return0;return left - tmp +1;}intmain(){ cin >> n;for(int i =1; i <= n; i++) cin >> a[i];int q; cin >> q;while(q--){int x, y; cin >> x >> y; cout <<binary_search(x, y)<< endl;}return0;}

2.2 A-B 数对

A-B 数对

在这里插入图片描述

算法原理:

由于顺序不影响最终结果,所以可以先把整个数组排序。

  1. 暴力解法:两层 for 循环,将 A 和 B 选出来,会超时。

由 A - B = C 得:B = A - C ,由于 C 是已知的数,我们可以从前往后枚举所有的 A,然后去前面找有多少个符合要求的 B ,然后找 B 起始终止位置,可以用二分查找。即时间复杂度为 O(nlogn)

这里我们因为已经有序,所以可以使用 STL 中的二分。

  1. 二分查找:
    (1) lower_bound:传入要查询区间的左右迭代器(注意是左闭右开的区间,如果是数组就是左右指针)以及要查询的值 k ,然后返回该数组中 >= k 的第一个位置;

(2) upper_bound:传入要查询区间的左右迭代器(注意是左闭右开的区间,如果是数组就是左右指针)以及要查询的值 k ,然后返回该数组中 >k 的第一个位置

比如:a=[10,20,20,20,30,40] ,设下标从 1 开始计数,在整个数组中查询 20:

  • lower_bound(a+1,a+1+6,20) ,返回 a+2 位置的指针;
  • upper_bound(a+1,a+1+6,20),返回 a+5 位置的指针;
  • 然后两个指针相减,就是包含 20 这个数区间的长度。
lower_bound(a+1,a+1+6,20)||| 指针1 指针2 查找数 
在这里插入图片描述
【注意】虽然 STL 用起来很爽,但是不要依赖它。因为后续学习「二分答案」的时候,STL 就无法使用了;同时 STL 的使用范围很局限,查询有序序列的时候才有用,数组无序的时候就无法使用。但是我们的二分模板也能在数组无序的时候使用,只要有二段性即可。

参考代码:

#include<iostream>#include<algorithm>usingnamespace std;typedeflonglong LL;constint N =2e5+10;int n, c; LL a[N];intmain(){ cin >> n >> c;for(int i =1; i <= n; i++) cin >> a[i];sort(a +1, a +1+ n);//排序 LL ret =0;//统计多少对 for(int i =2; i <= n; i++){ LL b = a[i]- c; ret +=upper_bound(a +1, a + i, b)-lower_bound(a +1, a + i, b);} cout << ret << endl;return0;}

2.3 烦恼的高考志愿

烦恼的高考志愿

在这里插入图片描述

算法原理:

  1. 利用 set 来解决。
  2. 排序 + 二分:找出大于等于估分的最小元素的位置和小于等于估分的最大元素

先把学校的录取分数「排序」,然后针对每一个学生的成绩 b ,在「录取分数」中二分出 >= b 的「第一个」位置 pos ,那么差值最小的结果要么在 pos - 1 位置。
abs(a[pos]−b)abs(a[pos−1]−b) 两者的最小值。

细节问题:

  • 如果所有元素都大于 b 的时候,pos−1 会在 0 下标的位置,有可能结果出错;
  • 如果所有元素都小于 b 的时候, pos 会在 n 的位置,此时结果倒不会出错,但是我们要想到这个细节问题,这道题不出错不代表下一道题不出错。

需要设定一下边界,处理越界访问。

参考代码:

#include<iostream>#include<algorithm>usingnamespace std;typedeflonglong LL;constint N =1e5+10;int m, n; LL a[N];intfind(LL x){int left =1, right = m;while(left < right){int mid =(left + right)/2;if(a[mid]>= x) right = mid;else left = mid +1;}return left;}intmain(){ cin >> m >> n;for(int i =1; i <= m; i++) cin >> a[i];sort(a +1, a +1+ m);//处理边界情况 a[0]=-1e7+10; LL ret =0;for(int i =1; i <= n; i++){ LL b; cin >> b;int pos =find(b); ret +=min(abs(a[pos]- b),abs(a[pos -1]- b));} cout << ret << endl;return0;}

3. 二分答案

准确来说,应该叫做二分答案 + 判断

二分答案可以处理大部分「最大值最小」以及「最小值最大」的问题。如果解空间在从小到大的变化过程中,判断答案的结果出现二段性,此时我们就可以「二分」这个解空间,通过「判断」,找出最优解。

3.1 木材加工

木材加工

在这里插入图片描述

算法原理:

设要切成的长度为 x ,能切成的段数为 c 。根据题意,我们可以发现如下性质:

  • 当 x 增大的时候,c 在减小。也就是最终要切成的长度越大,能切的段数越少;
  • 当 x 减小的时候,c 在增大。也就是最终要切成的长度越小,能切的段数越多。
  1. 暴力解法:

枚举所有的切割长度 x ,求出在 x 的情况下,能切出多少段 c 。又由上面的性质我们可以优化从 c >= k(设定段数) 开始枚举,但会超时。

  1. 二分答案:

设最终的要切的长度结果是 ret,则:

  • 当 x <= ret 时,c >= k 。也就是要切的长度小于等于最优长度时,最终切出来的段数大于等于 k ;
  • 当 x > ret 时,c < k 。也就是要切的长度大于最优长度时,最终切出来的段数小于 k 。

然后用 c = a[i] / x 算出能切的段数。

参考代码:

#include<iostream>usingnamespace std;typedeflonglong LL;constint N =1e5+10; LL n, k; LL a[N];// 当切割⻓度为 x 的时候,最多能切出来多少段intcalc(LL x){ LL cnt =0;for(int i =1; i <= n; i++){ cnt += a[i]/ x;}return cnt;}intmain(){ cin >> n >> k;for(int i =1; i <= n; i++) cin >> a[i]; LL left =0, right =1e8;while(left < right){ LL mid =(left + right +1)/2;if(calc(mid)>= k) left = mid;else right = mid -1;} cout << left << endl;return0;}

3.2 砍树

砍树

在这里插入图片描述

算法原理:

设伐木机的长度为 H ,能得到的木材为 c 。根据题意,我们可以发现如下性质:

  • 当 H 增大的时候,c 在减小;
  • 当 H 减小的时候,c 在增大。

设最终的要切的高度结果是 ret,则:

  • 当 H <= ret 时,c >= M 。也就是伐木机的高度大于等于最优长度时,最终能得到的木材小于等于 M ;
  • 当 H > ret 时,c < M 。也就是伐木机的高度小于最优长度时,最终能得到的木材大于 M 。

同时记得算出木材需要用到 c = a[i] - H

参考代码:

#include<iostream>usingnamespace std;typedeflonglong LL;constint N =1e6+10; LL n, m; LL a[N];//当伐木机的高度为 x 时,所能获得的木材 LL calc(LL x){ LL ret =0;for(int i =1; i <= n; i++){if(a[i]> x) ret += a[i]- x;}return ret;}intmain(){ cin >> n >> m;for(int i =1; i <= n; i++) cin >> a[i]; LL left =1, right =2e9;while(left < right){ LL mid =(left + right +1)/2;if(calc(mid)>= m) left = mid;else right = mid -1;} cout << left << endl;return0;}

3.3 跳石头

跳石头

在这里插入图片描述

算法原理:

设每次跳的最短距离为 x ,移走的石头块数为 c 。根据题意,我们可以发现如下性质:

  • 当 x 增大的时候,c 在减小;
  • 当 x 减小的时候,c 在增大。

设最终的结果是 ret,则:

  • 当 x <= ret 时,c >= M 。也就是要切的长度小于等于最优长度时,最终切出来的段数小于等于 M ;
  • 当 x > ret 时,c < M 。也就是要切的长度大于最优长度时,最终切出来的段数大于 M 。

当我们每次二分一个最短距离 x 时,如何算出移走的石头块数 c ?

  • 定义前后两个指针 i,j 遍历整个数组,设 i≤j ,每次 j 从 i 的位置开始向后移动;
  • 当第一次发现 a[j]−a[i]≥x 时,说明 [i+1,j−1] 之间的石头都可以移走;
  • 然后将 i 更新到 j 的位置,继续重复上面两步。

参考代码:

#include<iostream>usingnamespace std;typedeflonglong LL;constint N =5e4+10; LL l, n, m; LL a[N]; LL calc(LL x){ LL ret =0;for(int i =0; i <= n; i++){int j = i +1;while(j <= n && a[j]- a[i]< x) j++; ret += j - i -1; i = j -1;}return ret;}intmain(){ cin >> l >> n >> m;for(int i =1; i <= n; i++) cin >> a[i]; a[n +1]= l; n++; LL left =1, right = l;while(left < right){ LL mid =(left + right +1)/2;if(calc(mid)<= m) left = mid;else right = mid -1;} cout << left << endl;return0;}

结语

结语

二分算法,看似简单,实则精妙。从有序序列中的快速定位,到单调场景下的最优解搜索,它用 O(log n) 的效率诠释了“分而治之”的智慧。本文从基础概念出发,剖析了二分的本质与易错点,给出了清晰通用的模板,并借助 STL 工具提升编码效率。在实战环节,我们通过二分查找解决统计类问题(如牛可乐和魔法封印、A-B 数对、高考志愿),又用二分答案攻克了最优化问题的求解(如木材加工、砍树、跳石头)。这些题目不仅巩固了模板的使用,更让读者体会到二分思想在不同场景下的灵活运用。

二分算法是算法竞赛和日常开发中的必备利器,掌握它,你便能在海量数据面前从容不迫,在复杂问题中快速找到答案。但纸上得来终觉浅,绝知此事要躬行——希望你在理解模板和例题后,能主动找更多习题加以练习,让二分成为你解决算法问题的本能反应。愿你在算法的道路上,每一次二分,都离正确答案更近一步。

愿诸君能一起共渡重重浪,终见缛彩遥分地,繁光远缀天



Read more

OpenClaw 多机器人多 Agent 模式:打造你的 AI 助手团队

OpenClaw 多机器人多 Agent 模式:打造你的 AI 助手团队

OpenClaw 多机器人多 Agent 模式:打造你的 AI 助手团队 完整教程:https://awesome.tryopenclaw.asia/docs/04-practical-cases/15-solo-entrepreneur-cases.html 16.1 为什么需要多 Agent? 作为超级个体创业者,你可能需要不同类型的 AI 助手来处理不同的工作: * 主助理:使用最强大的模型(Claude Opus)处理复杂任务 * 内容创作助手:专注于文章写作、文案创作 * 技术开发助手:处理代码开发、技术问题 * AI 资讯助手:快速获取和整理 AI 行业动态 传统的单 Agent 模式需要频繁切换模型和上下文,效率低下。多 Agent 模式让你可以同时拥有多个专业助手,各司其职。

By Ne0inhk

AI测试大模型测试(十)spring集成大模型(SpringAI)

目录 1.1 SpringAI简介 1.2  需要环境 1.3  示例-对话&流式调用 步骤一: 去deepseek平台对应大模型平台申请app key   步骤二: 添加pom 步骤三: 配置文件,在 application.properties文件中添加 DeepSeek 的配置信息 步骤四:【示例】流式响应 & 流式响应 1.4  ChatClient接口 更多示例(预设角色) 流式输出 1.5 ChatModel接口 1.6 示例-函数调用 1.7 提示词 1.8 Spring AI官方文档 参考 1.

By Ne0inhk
你还在手动画ER图吗?让SQL自动生成ER图,轻松解决作业难题!

你还在手动画ER图吗?让SQL自动生成ER图,轻松解决作业难题!

你还在手动画ER图吗?让SQL自动生成ER图,轻松解决作业难题! 项目介绍 每到数据库课程或者毕业设计阶段,大家是否总会遇到一个让人头疼的问题——手绘ER图。不论是老师要求的数据库设计,还是毕业设计中的系统建模,ER图似乎成了不可绕过的一道坎。但你有没有想过,其实ER图是和数据库中的表结构一一对应的,难道我们非得一个个表、字段、关系地画下来吗?完全不需要! 为了帮助大家更高效地完成作业和项目设计,我们开发了一款在线SQL转ER图工具。通过这款工具,你只需要将SQL语句输入工具,它就能自动解析你的数据库表结构,并生成精准的ER图。无论是创建表、外键约束,还是其他数据库结构,工具都能一键转化成专业的ER图。 我们知道,学校的数据库课程通常会要求你根据某个需求设计数据库并绘制ER图,很多同学都会为绘制ER图而烦恼,手动画图不仅费时费力,还容易出错。在线SQL转ER图工具的出现,就是为了解决这一难题。它不仅能帮助你快速、准确地生成ER图,还能让你专注于数据库设计的核心,而不必花费过多的时间在画图上。 不管你是刚接触数据库的同学,还是已经有了一定基础的学生,这款工具都能大大提高你的工

By Ne0inhk
Java 中间件:RabbitMQ 消费端限流(basicQos 配置)

Java 中间件:RabbitMQ 消费端限流(basicQos 配置)

👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕Java中间件这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获! 文章目录 * Java 中间件:RabbitMQ 消费端限流(basicQos 配置) 🐇 * RabbitMQ 消息传递模式基础 📡 * 推模式(Push Mode)vs 拉模式(Pull Mode) * 自动确认 vs 手动确认 * 预取计数(Prefetch Count)概念 * basicQos 原理深度解析 🔍 * basicQos 方法签名 * 参数详解 * prefetchCount(预取计数) * global(全局标志) * prefetchSize(预取大小) * 作用范围层次 * 工作流程图解 * Java 客户端配置实战 💻 * 环

By Ne0inhk