文章目录
二分查找
基本概念
- 二分查找,也称为折半查找(Binary Search),是一种在有序数组中查找特定元素的搜索算法。
基本思想是 将目标值与数组中间的元素进行比较,如果目标值小于中间元素,则在数组的左半部分继续查找,否则在右半部分查找
优点
- 可以将最朴素的做法 (从头到尾的遍历数组)时间复杂度从 O(n)优化到O(logn)
- 比较次数少,速度快,效率高
查找过程:

二分查找:在一个已知的有序数据集上进行二分地查找
二分答案:答案有一个区间,在这个区间中二分,直到找到最优答案
二分查找写法
while(l<r)
下标边界二分
向左找目标
cpp
//在a数组中寻找第一个>=x的数,并返回其下标
int find(ll l, ll r)
{
while (l < r)
{
ll mid = l + r >> 1;//或(l+r)/2
if (a[mid]>=x)
{
r = mid;
}
else
{
l = mid + 1;
}
}
return l;//因为while循环当l==r时终止,return r也可以
}
向右找目标
cpp
//在a数组中寻找最后一个<=x的数,并返回其下标
int find(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;//(l+r+1)/2
if (a[mid]<=x)
{
l = mid;
}
else
{
r = mid - 1;
}
}
return l;
}
cpp
// while(l<r) 找第一个 > x
int find(ll l, ll r)
{
while (l < r)
{
ll mid = l + (r - l) / 2;
if (a[mid] > x)
{
r = mid;
}
else
{
l = mid + 1;
}
}
return l;
}
while(l<=r)
下标二分
cpp
//在a数组中寻找第一个>=x的数,并返回其下标
//可以先将res设置为一个很大的数,如果while循环终止后res没变,便没找到
int find(int l, int r)
{
int res=0x3f3f3f3f;
while (l <= r)
{
int mid = l + r >> 1;//或写成(l+r)/2
if (a[mid]>=x)
{
r = mid-1;
res=mid;
}
else
{
l = mid + 1;
}
}
return res;
}
cpp
//在a数组中寻找最后一个<=x的数,并返回其下标
int find(int l, int r)
{
int res=-1;
while (l <= r)
{
int mid = l + r >> 1;//或(l+r)/2
if (a[mid]<=x)
{
l = mid+1;
res=mid;
}
else
{
r = mid - 1;
}
}
return res;
}
cpp
// while(l<=r)+res 找第一个 > x
int find(int l, int r)
{
int res = 0x3f3f3f3f;
while (l <= r)
{
int mid = l + (r - l) / 2;
if (a[mid] > x)
{
res = mid;
r = mid - 1;
}
else
{
l = mid + 1;
}
}
return res;
}
二分答案
二分答案信号
「最大值最小化」
「最小值最大化」
「至少需要多少个」
「最多能分成多少段」
「能否在 x 时间内完成」
「求最小 / 最大的满足条件的值」
最大化最小值
cpp
ll l=1,r=0x3f3f3f3f;
ll ans=0;
while(l<=r)
{
ll mid=(l+r)/2;
if(check(mid))
{
ans=max(mid,ans); // mid是可行答案,记录下来
l=mid+1; // 尝试往更大答案搜索
}
else
{
r=mid-1; // mid不可行,要变小
}
}
cout<<ans;
最小化最大值
cpp
//最小化最大值模板
ll l=1,r=1e9;
ll ans=0;
while(l<=r)
{
ll mid=l+(r-l)/2;
if(check(mid))
{
ans=mid;
r=mid-1; //可行,尝试找更小答案
}
else
{
l=mid+1; //不可行,放大
}
}
cout<<ans;
区分
- 下标二分:答案一定是数组中已经存在的某个元素,目标求元素的位置。
- 二分答案:答案不一定是输入数组里面的值;可以写一个check(x)函数判断x这个候选答案是否可行;题目特征:最大化最小值、最小化最大值。
| 项目 | while(l < r)边界二分 (lower_bound) |
while(l <= r)传统值查找 |
|---|---|---|
| 用途 | 找第一个≥x、第一个 > x(边界) | 查找是否存在某确切数值 |
| 循环结束 | l == r,下标重合 |
l > r,区间为空 |
| 边界更新 | r=mid / l=mid+1,mid 不排除 |
r=mid‑1 / l=mid+1,mid 直接排除 |
| 数组重复元素 | 支持,拿到首次出现位置 | 找到任意一个,不一定是第一次 |
| 返回之后 | 必须额外判断 a l==x | 找到就直接 return,不用二次判断 |
记忆口诀 :
while(l<=r)时,mid直接排除,靠l=mid+1或r=mid-1收缩;用res记录最近一次可行答案,循环结束后res即为结果。
快速识别二分答案题:
- 问题具备单调性:存在阈值,一侧全部不可行,另一侧全部可行。
- 可以实现O(n)的
check(x),快速验证候选x是否可行。 - 题目要求:最大化最小值 / 最小化最大值。
例题
模板题
cpp
#include<bits/stdc++.h>
#define ll long long
#define endl '\n'
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define ull unsigned long long
#define fi first
#define se second
#define PLL pair<string, ll>
#define YES cout<<"YES"<<endl;
#define NO cout<<"NO"<<endl;
using namespace std;
const ll MAXN=1005;
const ll mod=1e9+7;
using namespace std;
ll n,m;
ll b;
ll find(ll b,vector<ll>&a)
{
ll l=0;
ll r=n-1;
while(l<r)
{
ll mid=(r+l)/2;
if(a[mid]>=b)
{
r=mid;
}
else
{
l=mid+1;
}
}
return l;
}
int main()
{
IOS
cin>>n>>m;
vector<ll>a(n);
for(ll i=0;i<n;i++)
{
cin>>a[i];
}
while(m--)
{
cin>>b;
ll q=find(b,a);
if(a[q]==b)
{
cout<<q+1<<' ';
}
else
{
cout<<-1<<' ';
}
}
// cout<<fixed<<setprecision(x)<< ;
return 0;
}
进击的奶牛(整型二分)
P1824 [USACO05FEB 进击的奶牛 Aggressive Cows G - 洛谷](https://www.luogu.com.cn/problem/P1824)

解题过程
- 二分距离,判断函数check(x):间距至少 x 时最多能放下多少头牛。排序坐标;
- 二分区间l=1,r=最大坐标;check(mid)满足(能放下≥m 头牛),说明距离 mid 可行,尝试更大值:ans=mid,l=mid+1;不满足则缩小距离:r=mid-1
代码实现
cpp
//5303
#include<bits/stdc++.h>
#define ll long long
#define endl '\n'
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define ull unsigned long long
#define fi first
#define se second
using namespace std;
ll n,m;
vector<ll>a;
bool check(ll x)
{
ll cnt=1;
ll head=a[0];
for(ll i=1;i<n;i++)
{
if(a[i]-head>=x)
{
cnt++;
head=a[i];
}
}
return cnt>=m;
}
int main()
{
IOS
cin>>n>>m;
for(ll i=0;i<n;i++)
{
ll A;
cin>>A;
a.push_back(A);
}
sort(a.begin(),a.end());
ll l=1,r=0x3f3f3f3f3f3f3f3f;//longlong 型
ll ans=0;
while(l<=r)
{
ll mid=(l+r)/2;
if(check(mid))
{
ans=max(mid,ans);
l=mid+1;
}
else
{
r=mid-1;
}
}
cout<<ans<<endl;
// cout<<fixed<<setprecision(x)<< ;
return 0;
}
切绳子(浮点型二分)

解题过程
- 二分单段绳子长度,check(x) 统计能切出多少段长度≥x 的绳子
- 二分循环固定迭代 100 次(替代整数边界判断,保证精度);
- 满足段数≥k,记录答案并向右二分找更长长度:ans=mid,l=mid;
- 不满足则向左缩小长度:r=mid;
最后向下取整保留两位小数输出。
代码实现
cpp
//1577
#include<bits/stdc++.h>
#define ll long long
#define endl '\n'
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define ull unsigned long long
#define fi first
#define se second
using namespace std;
ll n,k;
vector<double>a;
bool check(double x)
{
ll cnt=0;
for(ll i=0;i<n;i++)
{
cnt+=(ll)(a[i]/x);
}
return cnt>=k;
}
int main()
{
IOS
cin>>n>>k;
double r=0;
for(ll i=0;i<n;i++)
{
double A;
cin>>A;
a.push_back(A);
r=max(r,A);
}
// sort(a.begin(),a.end());
double l=0;
double ans=0;
for(ll i=1;i<=100;i++)
{
double mid=(l+r)/2;
if(check(mid))
{
ans=mid;
l=mid;
}
else
{
r=mid;
}
}
ans=floor(ans*100)/100;//向下取整保留两位小数
cout<<fixed<<setprecision(2)<<ans<<endl ;
return 0;
}
不能直接 cout << fixed << setprecision(2) << ans;
setprecision(2) 默认是四舍五入。比如真实答案是 2.009,直接输出会变成 2.01,题目要求向下取整,必须手动 floor(ans*100)/100
整型二分 vs 浮点二分
- 整型二分
区间边界是整数,可以用 l <= r 循环,靠 mid±1 收缩边界
cpp
ll l=1, r=1e9;
while(l <= r)
{
ll mid = (l + r) / 2;
if(check(mid))
{
ans=mid;
l=mid+1; // 可行,往右找更大
}
else
r=mid-1; // 不可行,往左缩小
}
原理:每次直接排除mid,不会死循环。
- 浮点二分
实数有无穷多个,不能用 l<=r,无法精准边界,会陷入循环;
标准写法:固定循环 100 次(100 次二分后精度远超题目要求的 1e-6/1e-2)。
cpp
double l=0, r=max_len;
for(int i=1;i<=100;i++)
{
double mid = (l + r) / 2;
if(check(mid))
{
ans=mid;
l=mid; // 可行,右边界移到mid
}
else
r=mid; // 不可行,左边界移到mid
}
}
原理:实数区间不能 mid±1,只能不断缩小区间范围,靠迭代保证精度。
浮点:存在浮点数精度丢失,不能直接输出;
如切绳子需要向下取整保留两位小数:ans=floor(ans*100)/100,配合setprecision(2)输出。
奶牛晒衣服

解题过程
- 二分答案
二分总晾晒时间s,check(s)判断s时间内能否晒干所有衣服:
每件衣服自然风干s*a,剩余水分需要烘干机;
统计所有衣服烘干总次数,若总次数≤s 则时间 s 可行,尝试更小时间:ans=mid,r=mid-1;
不可行则增大时间:l=mid+1。 - 优先队列暴力
每次取出含水量最大的衣服烘干 b 水分,循环直到所有衣服自然风干即可
代码实现
cpp
// 二分
#include<bits/stdc++.h>
#define ll long long
#define endl '\n'
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define ull unsigned long long
#define fi first
#define se second
using namespace std;
ll n,aa,b;
//ll s;
ll mx,ti;
vector<ll>a;
bool check(ll s)
{
ll sum=0;
for(ll i=0;i<n;i++)
{
if(a[i]<=s*aa)
continue;
ll re=a[i]-s*aa;
sum+=(re+b-1)/b;
if(sum>s)
return false;
}
return sum<=s;
}
int main()
{
IOS
cin>>n>>aa>>b;
for(ll i=0;i<n;i++)
{
ll x;
cin>>x;
a.push_back(x);
}
sort(a.begin(),a.end());
ll l=0,r=a[n-1];
ll ans=0;
while(l<=r)
{
ll mid=(l+r)/2;
if(check(mid))
{
ans=mid;
r=mid-1;
}
else
{
l=mid+1;
}
}
cout<<ans<<endl;
return 0;
}
cpp
//优先队列
#include<bits/stdc++.h>
#define ll long long
#define endl '\n'
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define ull unsigned long long
#define fi first
#define se second
using namespace std;
ll n,a,b;
ll mx,ti;
int main()
{
IOS
priority_queue<ll>q;
cin>>n>>a>>b;
for(ll i=0;i<n;i++)
{
ll x;
cin>>x;
q.push(x);
}
mx=q.top();
q.pop();
while(mx>ti*a)
{
ti++;
mx-=b;
q.push(mx);
mx=q.top();
q.pop();
}
cout<<ti<<endl;
return 0;
}
逛画展
说明:滑动窗口

解题过程
-
用两个指针
l(窗口左边界)、r(窗口右边界)维护一个动态区间:-
右指针
r不断向右扩张窗口,把新画作纳入窗口,统计窗口内画作种类数量kind; -
当窗口集齐全部 m种画(kind == m)说明当前区间合法;
持续收缩左指针
l,尽可能缩短区间长度;每次收缩前更新最短区间答案;
-
左指针移出某类画,且该画在窗口内数量变为 0 时,种类数
kind减一,停止收缩,继续右移r。
-
代码实现
cpp
//1843
#include<bits/stdc++.h>
#define ll long long
#define endl '\n'
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define ull unsigned long long
#define fi first
#define se second
using namespace std;
ll n,m;
int main()
{
IOS
cin>>n>>m;
vector<ll>a(n+2);
vector<ll>cnt(m+2,0);
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
ll l=1;
ll kind=0;
ll ansx=0,ansy=0;
ll mincur=LLONG_MAX;
for(ll r=1;r<=n;r++)
{
if(cnt[a[r]]==0)
{
kind++;
}
cnt[a[r]]++;
while(kind==m)
{
ll cur=r-l+1;
if(cur<mincur||(cur==mincur&&l<ansx))
{
mincur=cur;
ansx=l;
ansy=r;
}
cnt[a[l]]--;
if(cnt[a[l]]==0)
kind--;
l++;
}
}
cout<<ansx<<" "<<ansy<<endl;
return 0;
}
STL内置二分函数
头文件 <algorithm>,数组必须升序
返回迭代器,
it-a.begin()得到下标
lower_bound(a.begin(),a.end(),x):第一个 ≥ xupper_bound(a.begin(),a.end(),x):第一个 > xequal_range(a.begin(),a.end(),x)返回pair,等于x的区间[L,R)binary_search(a.begin(),a.end(),x)返回bool,只判断是否存在
最后一个 <=x
cpp
auto it = upper_bound(a.begin(),a.end(),x);
if(it != a.begin())
--it;
逻辑:
upper_bound 返回第一个 > x的位置。
- 它前一个元素,就是最后一个 ≤ x。
it == a.begin():代表全部元素都大于 x,不存在<=x,不能--it(越界)。
示例:a={1,3,5,7,9},x=6
- upper_bound 找第一个 > 6 -> 指向 7(下标 3)
--it指向 5,5 就是数组最后一个 ≤6。
最后一个 < x
cpp
auto it = lower_bound(a.begin(),a.end(),x);
if(it != a.begin())
--it;
逻辑:
lower_bound 返回第一个 >= x的位置。
- 它前一个元素,就是最后一个 < x。
it == a.begin():全部元素都≥x,不存在<x,禁止--it。
示例:a={1,3,5,7,9},x=5
- lower_bound 找第一个≥5 →指向 5(下标 2)
--it指向 3,3 就是数组最后一个 <5。
set/map使用成员函数
.lower_bound(x),不要用全局函数,防止退化O(n)
cpp
auto it = s.lower_bound(x); // 正确
// auto it = lower_bound(s.begin(),s.end(),x); 错误
7.24的库存