学会二分法,有这一篇就够啦!

发布时间:2025-09-19 20:42

'生活哲学短篇:学会独处,也是一种能力。' #生活乐趣# #生活分享# #生活哲学感悟# #生活哲学短篇#

简介: 本文由blue撰写于2024年9月,深入讲解二分法这一基础但不简单的算法。文章从二分法的两大经典应用场景——二分查找与二分答案出发,详细解析其原理与实现。通过实例代码(如LeetCode第704题)和竞赛题目,探讨了不同区间定义(左闭右闭、左闭右开)下的实现方式,并延伸到寻找目标值首次/最后出现位置及二分答案的实际应用。适合初学者系统掌握二分法的核心思想与技巧。

学会二分法,有这一篇就够啦!

作者:blue

时间:2024.9

二分法是大家耳熟能详的基础算法,但是二分法,可没有你想像的那么简单,本篇将从二分法的两个经典利用场景——二分查找和二分答案,来讲懂二分法。

二分法应用在一串有序的序列中,查找一个我们所需要的值,注意一定是有序的序列。

比如说两个人玩猜数字游戏,1-10000,A心中想一个数,B来猜,B每次猜一个数,A都会回答他大了还是小了,这样B每次都可以折半的缩小查找的区间,这样就比直接把数从1数到10000要快很多。

1.二分查找

很多教学都让大家背二分法的模板,我不这样认为,因为二分法是一个非常好理解的算法,所以用不着背模板,而是要理解算法的过程知道代码是怎么写的,这样才能熟练的掌握二分法。

首先:二分法代码不能乱来,一定要根据你的区间来敲

我推荐的写法是根据左闭右闭区间的方法来写,下面是二分法的左闭右闭区间的代码

while(left<=right) //这里是你的循环条件,left<=right,这就意味着left和rigth都是包含在我们可取的一个范围之内 { int mid=(left+right)/2; //那我们这里算出mid,如果mid的值不符合条件,那么mid就不应该包含在我们接下来我们 if(target<nums[mid]) right=mid-1;//循环的区间中,所以应该是mid-1或mid+1,这样才能将mid排除在区间之外 else if(target>nums[mid]) left=mid+1;//我们的区间才符合我们设置的左闭右闭区间的规则 else return mid; }

AI 代码解读

再次强调:你进入while循环的条件一定要符合你的区间,也就是说你更新的区间一定要与你一开始查找的区间类型一致。

你一开始是[left,right](左闭右闭),你更新完还得是左闭右闭;!!!

有了固定好区间的思想,你在写二分法的时候,才不会搞不懂,到底是应该left<=right,还是left<right,其实都是可以的,只是后者是左闭右开区间,对应的区间变化,也应该符合左闭右开区间的规则。

接下来我们来看例题:

704. 二分查找 - 力扣(LeetCode)

这是二分查找的模板题,大家可以先点击链接自己做一遍再回来看我的代码。

我在这里给出了左闭右闭,和左闭右开区间的两种写法,如果对此前讲解有不理解的同学可以结合以下代码再体会以下。

左闭右闭:

class Solution { public: int search(vector<int>& nums, int target) { int left=0,right=nums.size()-1;//左闭右闭 while(left<=right) { int mid=(left+right)/2; if(target<nums[mid]) right=mid-1; else if(target>nums[mid]) left=mid+1; else return mid; } return -1; } };

AI 代码解读

左闭右开:

class Solution { public: int search(vector<int>& nums, int target) { int left=0,right=nums.size();//左闭右开,为什么这里是左闭右开,因为nums.size()是取不到的下标 while(left<right) { int mid=(left+right)/2; if(target<nums[mid]) right=mid; //由于是左闭右开,所以mid可以包含在开区间中,不用mid-1 else if(target>nums[mid]) left=mid+1; else return mid; } return -1; } };

AI 代码解读

如果你成功的用两种不同的区间,写出了这道题,那么恭喜你,搞明白了区间与while循环判断条件的关系,接下来就要看一些比较特殊的二分查找:

1.二分查找元素第一次出现的位置

由于我们查找的有序序列元素可能还是重复的,那我们应该如何去找第一个符合要求元素出现的位置呢?请看如下解释:

代码中用ans来表示查找到元素的位置,与原来二分查找不同的是,这里找到目标值后并不直接退出,而是你要找在这个已经找到的目标值的位置的左边还有没有target,所以用先ans记住这个位置,然后r=mid-1查找左边还有没有目标值,这样就可以找到元素第一次出现的值了。

int ans=-1; int l=1,r=n;//左闭右闭 while(l<=r) { int mid=(r+l)/2; if(a[mid]<target) l=mid+1; else if(a[mid]>target) r=mid-1; else if(a[mid]==target){ ans=mid; r=mid-1; } } cout<<ans<<" ";

AI 代码解读

2.二分查找元素最后一次出现的位置

查找元素最后一次出现的位置也很简单,那就是先用ans记住当前位置,然后l=mid+1 查找右边还有没有目标值,这样就可以找到元素最后一次出现的位置。

int ans=-1; scanf("%lld",&target); int l=1,r=n;//左闭右闭 while(l<=r) { int mid=(r+l)/2; if(a[mid]<target) l=mid+1; else if(a[mid]>target) r=mid-1; else if(a[mid]==target){ ans=mid; l=mid+1; } } cout<<ans<<" ";

AI 代码解读

以上的两个类型在后面有比较大的应用,请读者细细品味,掌握了再往下看

2.二分答案

其实二分答案和二分查找中找元素第一次出现的位置,和最后一次出现的位置的思想非常的像!

它具有这种典型的特征:求...最大值的最小 、 求...最小值的最大

只要你搜索的答案是一个单调有序的区间

我们直接通过例题来讲

P1873 [COCI 2011/2012 #5] EKO / 砍树 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题目大意:让我们找到一个尽可能高的高度(因为这样砍的树少),然后得到至少等于M的木材

这题我们对答案(也就是砍树的高度)进行一个二分搜索(只要这个高度下所得到木材>=M,那他就是一个合法的高度),题目的要求是你找到一个符合条件的答案,并不是直接输出这个答案,而是让这个值尽量大,所以你找到在这道题中 ans>=m 都是符合条件的,所以你每找到一个符合条件的,就先把他记录下来,然后再往他的右边找,那就是 l=mid+1;

#include <iostream> using namespace std; using ll=long long; const int N=1e7+10; ll a[N]; int main() { ll n,m; cin>>n>>m;//M是目标木材数 ll ans=-1; ll l=0x3f3f3f3f,r=0; for(int i=1;i<=n;i++) //边输入边找左右区间,显然最小的高度是最矮的树,最大的高度是最高的树 { cin>>a[i]; l=min(l,a[i]); r=max(r,a[i]); } //左闭右闭 while(l<=r) { ll res=0; int mid=l+(r-l)/2;//防止爆int for(int i=1;i<=n;i++){ //算出这个高度下能获得的木材数 if(a[i]>mid) res+=(a[i]-mid); } if(res>=m){ //尽量高,所以要找符合的最后出现的位置 ,所以先用ans记录合法位置,再往右找看看能不能 ans=mid; //有更高的位置 l=mid+1; } else if(res<m){ //说明太高了,不够,那只能降低高度 r=mid-1; } } cout<<ans; return 0; }

AI 代码解读

再来一道题:

P2440 木材加工 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题目大意:把n根木头,切成k段,长为l的木头,使这个l尽量大

我们可以二分l的长度,看看合法的l中最长的是哪一个,详细看代码中的注释

#include <iostream> #define int long long using namespace std; const int N=1e5+10; int a[N]; signed main() { int n,k; int ans=0; int l=1,r=0;//左区间l=1,题目中描述如果连 1cm 长的小段都切不出来,输出 0。 cin>>n>>k; //所以最后查找不到的话,就输出0即可 for(int i=1;i<=n;i++) { cin>>a[i]; r=max(a[i],r);//最长的段自然是只可能是最长的木材 } while(l<=r)//左闭右闭 { int res=0; int mid=l+(r-l)/2;//防止爆int,mid就是我们设置的切的段长 for(int i=1;i<=n;i++){ res+=a[i]/mid; //看看每根木头能切几段这样的木材 } if(res>=k){ //合法,看看能不能更长 ans=mid; l=mid+1; } else r=mid-1; //不合法,太长了,要短一点 } cout<<ans; return 0; }

AI 代码解读

恭喜你已经做到这里了,我们趁热打铁来看看两道竞赛真题,这两题都有非二分的做法,但我个人认为二分的做法更易于理解

0冶炼金属 - 蓝桥云课 (lanqiao.cn)

题目大意:炉子有一个称作转换率的属性 V,V 是一个正整数,这意味着消耗 V 个普通金属 O 恰好可以冶炼出一个特殊金属 X

现有N 条冶炼记录

每条记录中包含两个整数 A 和 B,这表示本次投入了 A 个普通金属 O,最终冶炼出了 B 个特殊金属 X。

(每条记录都是独立的,这意味着上一次没消耗完的普通金属 O 不会累加到下一次的冶炼当中。)

根据这 N 条冶炼记录,请你推测出转换率 V 的最小值和最大值

#include <iostream> #define int long long using namespace std; const int N=1e4+10; int A[N],B[N]; signed main() { int n; int ans_min,ans_max; int MIN=0x3f3f3f3f;//上界 cin>>n; for(int i=1;i<=n;i++) { cin>>A[i]>>B[i]; MIN=min(MIN,A[i]/B[i]);//为什么我们要找一个最小的转换率来当右边界 } //因为如果你找的是一个最大的转换率,那么其他记录A个金属O,可能炼制不出B个金属X //但你找最小的,至少可以保证每条记录都是合法的 int l=1,r=MIN;//左闭右闭,找最小值,即符合要求后往左边找 while(l<=r) { int flag=1; int mid=l+(r-l)/2; for(int i=1;i<=n;i++)//检测mid这个转换率是否合格 { if((A[i]/mid)>B[i]) { flag=2;break;} //转换率太小当向右 else if((A[i]/mid)<B[i]) { flag=3;break;}//转换率太大当向左 } if(flag==1){ ans_min=mid;r=mid-1;} else if(flag==2) { l=mid+1;} else if(flag==3) { r=mid-1;} } cout<<ans_min<<" "; //其实接下来这段代码可以不写,为什么,因为我们在一开始找右边界的时候,其实就已经找到了一个最大的,能让所有记录都合法的边界值了。 /*l=1;r=MIN; while(l<=r)//同理,找最大值 { int flag=1; int mid=l+(r-l)/2; for(int i=1;i<=n;i++) { if((A[i]/mid)>B[i]) {flag=2;break;} else if((A[i]/mid)<B[i]) {flag=3;break;} } if(flag==1){ans_max=mid;l=mid+1;} else if(flag==2) {l=mid+1;} else if(flag==3) {r=mid-1;} } cout<<ans_max<<" ";*/ cout<<MIN; return 0; }

AI 代码解读

0卡片 - 蓝桥云课 (lanqiao.cn)

小蓝有 k 种卡片, 一个班有 n 位同学, 小蓝给每位同学发了两张卡片, 一 位同学的两张卡片可能是同一种, 也可能是不同种, 两张卡片没有顺序。没有 两位同学的卡片都是一样的。

给定 n, 请问小蓝的卡片至少有多少种?

根据题目,我们不难看出牌的种数可以组成的不同的方案为、

(1,1) (1,2) (1,3) (1,4) (1,5) (2,2) (2,3) (2,4) (2,5) (3,3) (3,4) (3,5) (4,4) (4,5) (5,5) 牌的种类,可以构成的牌组合的总数,其实是一个等差数列 比如说有3种牌 那就可以构成 [(1+3)*3]/2=6,6种不同的组合,就可以分给6位同学。 所以题目给定同学的数量,我们就找最少几种牌,构成的组合总数,可以容纳这些同学, 这样就把问题演变成了,查找最少种类牌,那就可以用到二分答案

AI 代码解读

#include <iostream> #define int long long using namespace std; signed main() { int n; int ans; cin>>n; int l=1,r=1e9; while(l<=r) { int mid=(l+r)/2; int s=(mid+1)*mid/2; if(s>=n) { ans=mid;r=mid-1; } else l=mid+1; } cout<<ans; return 0; }

AI 代码解读

网址:学会二分法,有这一篇就够啦! https://klqsh.com/news/view/255877

相关内容

小红书运营怎么做?码住这篇就够了!
小红书运营深度分析, 看这一篇就够了!
心理疏导怎么做?看完这篇就能学会!
中考化学 “酸碱盐”傻傻分不清,看这一篇文章就够了!
解码SPAC,看这一篇就够了
如何做好一场读书会,这一篇就够了(建议收藏)
有耕耘,无收获,想改变命运吗?一本书、一个笔记方法就够了。
晨会励志小故事分享(精选20篇)
普通人零基础做Vlog,看这一篇就够了
美食分享会作文11篇

随便看看