二分
枚举是蛮力法的应用,所谓蛮力,并非指用人脑的智力或体力,而是利用计算机的强大特性进行暴力计算并验证的方式。
最朴素的枚举法是线性枚举,线性枚举优化方案有二分枚举、三分枚举等。
二分
二分是分治法的一种思想,可分为整数二分、实数二分等。二分的应用条件线性函数,必须为严格的单调有序序列,若无序则必须先排序再二分。常见应用为二分查找和二分答案。
注:对于区间 [ l , r ] [l,r] [l,r]和 [ l , r ) [l,r) [l,r),其二分写法的边界处理不同,需要特别注意。
整数二分
整数二分的中值计算
实现 | 原理 | 适用情形 | 潜在问题 |
---|---|---|---|
m = ( l + r ) / 2 m=(l+r)/2 m=(l+r)/2 | 求区间边界平均数,即为区间中值 | l ≥ 0 , r ≥ 0 , l + r l\ge0,r\ge0,l+r l≥0,r≥0,l+r无溢出 | l + r l+r l+r可能溢出,负数情形下向0取整 |
m = l + ( r − l ) / 2 m=l+(r-l)/2 m=l+(r−l)/2 | 将 1 2 \frac{1}{2} 21区间长度加在区间左边界上,即为区间中值 | l − r l-r l−r无溢出 | r , l r,l r,l一正一负, r − l r-l r−l可能溢出 |
m = l + r > > 1 m=l+r>>1 m=l+r>>1 | 与 m = ( l + r ) / 2 m=(l+r)/2 m=(l+r)/2原理相同,通过位运算实现 | l + r l+r l+r无溢出 | l + r l+r l+r可能溢出 |
二分边界问题
确定边界的更新核心是确定二分区间为 [ l , r ] [l,r] [l,r]或 [ l , r ) [l,r) [l,r)情形,需确保左右边界始终为当前情形不变。
- [ l , r ] [l,r] [l,r]情形:1. 允许 l = r l=r l=r,即 w h i l e ( l < = r ) while(l<=r) while(l<=r) 2. 因左右边界必须都是合法值,因此更新边界时候均需排除不合法的 m m m,即为 l = m + 1 l=m+1 l=m+1, r = m − 1 r=m-1 r=m−1
extern int l=0,r=MAX-1,m;
extern bool check(int x);
bool bs(){
while(l<=r){
m=l+r>>1;
if(check(m)) //l=m+1或r=m-1
else //r=m-1或l=m+1
}
}
- [ l , r ) [l,r) [l,r)情形:1. 不允许 l = r l=r l=r,即 w h i l e ( l < r ) while(l<r) while(l<r) 2.因左边界合法,右边界不合法,因此左边界更新需排除不合法的 m m m,右边界更新无需排除不合法的 m m m
extern int l=0,r=MAX,m;
extern bool check(int x);
bool bs(){
while(l<r){
m=l+r>>1;
if(check(m)) //l=m+1或r=m
else //r=m或l=m+1
}
}
实数二分
实数二分与整数二分的区别:
- 浮点数不支持位运算,故实数二分中值mid的计算应采用 l + ( r − l ) / 2 l+(r-l)/2 l+(r−l)/2。
- 应控制精度:在计算机科学中,由于浮点数存储机制问题,2个浮点数只能互相无限趋近,不存在2个严格相等的浮点数,因此需考虑精度问题。常用数学中表示趋向无穷小的正数 ϵ \epsilon ϵ作为可接受误差范围以控制精度, ϵ \epsilon ϵ设计需合理,过小的 ϵ \epsilon ϵ精度高但会超时,过大的 ϵ \epsilon ϵ会导致精度低不符合要求。
- 边界更新不同:实数二分的边界一般为左开右开问题,因此更边界为中值即可,无需 ± \pm ± ϵ \epsilon ϵ。
extern double l,r,m;
extern bool check(double x);
const double eps=1e-7;//控制精度
bool bs(){
while(r-l>eps){
mid=l+(r-l)/2;
if(check(m)) //... r=m;
else //... l=m;
}
}
二分查找
在 [ l , r ] [l,r] [l,r]区间内查找某个数据是否存在,最直接的方法是采用枚举法,线性枚举的复杂度为 O ( n ) O(n) O(n),超时。最常见的优化是进行二分枚举,复杂度为 O ( log 2 ( n ) ) O(\log_2(n)) O(log2(n)),通过二分枚举进行查找数据的过程称为二分查找。二分查找可看做在所给区间范围内进行二分答案。下面以整数二分查找为例进行说明。
二分查找单个元素: O ( log 2 ( n ) O(\log_2(n) O(log2(n)
extern int a[MAX],key,l=0,r=MAX-1,m;
bool bs(){
while(l<=r){
m=l+r>>1;
if(a[m]==key) return 1;
else if(a[m]>key) r=m-1;//注意整数二分由于取整问题,mid需要+1或-1
else if(a[m]<key) l=m+1;
}
return 0;
}
二分查找边界
- 先二分查找,再线性查找:整体复杂度仍为 O ( n ) O(n) O(n),时间性能退化为线性阶
extern int a[MAX],key,l=0,r=MAX-1,m;
bool bs(){
while(l<=r){
m=l+r>>1;
if(a[m]==key){
int nl=m,nr=m;//左右边界
while((nl-1)!=0&&a[nl-1]==key) nl--;//线性查找左边界
while((nr+1)!=MAX-1&&a[nr+1]==key) nr++;//线性查找右边界
return 1;
}else if(a[m]<key) l=m+1;
else if(a[m]>key) r=m-1;
}
return 0;
}
- 持续二分查找
- 查找单侧边界情形
extern int a[MAX],key,l,r,m,ans;
bool bs(){
while(l<=r){
m=l+r>>1;
if(a[m]<key) l=m+1;
else if(a[m]>key) r=m-1;
else r=m-1;//查找左边界 l=m+1则为查找右边界
}
}
- 查找双边界情形:巧妙之处在于构造一个 ≥ \ge ≥当前元素的值,并进行二分查找,无需关心该值是否存在,若不存在则会指向首个大于key的元素。右边界则为构造值-1。
C二分查找函数——bsearch(<stdlib.h>)
参数:key地址、数组名、元素个数、元素大小、自定义比较规则
返回值:若查找成功返回指向该元素的指针,否则返回NULL
C++二分查找函数(<algorithm>)
-
binary_search:查找首个** = = =**给定值的元素 返回值:找到返回1,否则返回0
-
lower_bound:查找首个 ≥ \geq ≥给定值的元素
-
upper_bound:查找首个 > > >给定值的元素
注:upper_bound是找严格>,因此在数组中位置相对较高,为upper;lower_bound找的是**>=,在数组中位置相对较低,为lower。
返回值:
1.指针法使用:若找到则返回指向满足指定条件元素的指针**,否则返回NULL
2.迭代器法使用:若找到则返回指向满足指定条件元素的迭代器,否则返回end()
二分答案
枚举法:若可通过某种映射关系,推断出答案具有严格单调性,并且可确定所在的合法区间的上下界,则可通过对边界进行不断更新,从而缩小合法区间,不断试探答案。枚举法的核心在于合法边界的确定以及check函数的设计。
线性枚举:暴力枚举。不断更新合法区间下界,当合法区间下界不符合要求时,则上一次枚举出的下界即为正确答案。复杂度为 O ( n ) O(n) O(n),易超时。
extern int l,r;//l:答案下界 r:答案上界
bool check(int x){
int count;//记录满足条件的实例数
for(){//验证给出所有实例在x的条件下是否都能符合要求(广度)
if() count++;
}
if() return 1;//若当前x在所有实例都满足条件情形下,此x为一个可行解
else return 0;//否则此答案不可行,若为线性枚举答案即为上一次枚举出的x,二分枚举则继续缩小答案区间
}
int verify(){
for(int i=l;i<=r;i++){
if(check(i)) ans=i;//该答案允许,但可能并非满足要求的最优解,继续向答案上界方向进行线性枚举
else break;//答案不符,说明上一次找到的答案已为最优解
return ans;
}
二分枚举:不断更新合法区间上下界,复杂度为 O ( log 2 ( n ) ) O(\log_2(n)) O(log2(n))。通过二分枚举合法区间、试探答案的过程,称为二分答案。二分答案最大优点是将复杂度由线性阶( O ( n ) O(n) O(n))优化至对数阶( O ( l o g 2 ( n ) ) O(log_2(n)) O(log2(n)))。下面以整数二分为例说明二分答案。
extern int l,r,ans,mid;//l:答案下界 r:答案上界
extern bool check(int x);//检查答案正确性,check函数同线性枚举
int verify(){
while(l<r){
m=l+r>>1;
if(check(m)) ans=m; //... 答案允许,但可能并不是满足要求的最优解。移动r或l,更新答案上下界确定最优解
else //... 答案不符,移动l或r
}
return ans;
}
二分答案应用实例:最值中的最值问题(最大值最小化/最小值最大化问题)
三分
三分法是二分法的扩展,三分适用于求解单峰/单谷函数的最值问题。三分法要求极值点左右两侧具有严格的单调性,即有且仅存在一个极值点。
三分法用两个三等分点 m i d 1 mid1 mid1和 m i d 2 mid2 mid2将答案区间分为三份,复杂度为 O ( log 3 ( n ) ) O(\log_3(n)) O(log3(n))。计算方法:
double k=(r-l)/3.0;
double mid1=l+k,mid2=r-k;
以单峰函数为例,讨论三分法的情形:
- 若 f ( m i d 1 ) < f ( m i d 2 ) f(mid1)<f(mid2) f(mid1)<f(mid2),则 m i d 1 mid1 mid1和 m i d 2 mid2 mid2均位于答案左侧,或 m i d 1 mid1 mid1位于答案左侧, m i d 2 mid2 mid2位于答案右侧。之后令 l = m i d 1 l=mid1 l=mid1
- 若 f ( m i d 1 ) > f ( m i d 2 ) f(mid1)>f(mid2) f(mid1)>f(mid2),则 m i d 1 mid1 mid1和 m i d 2 mid2 mid2均位于答案右侧,或 m i d 1 mid1 mid1位于答案左侧, m i d 2 mid2 mid2位于答案右侧。之后令 r = m i d 2 r=mid2 r=mid2
实数三分
extern double l,r,mid1,mid2;
extern bool check(double x);
const double eps=1e-7;//控制精度
bool ts(){
while(r-l>eps){
double t=(l+r)/3.0;
mid1=l+t,mid2=r-t;
if(check(mid1)>check(mid2)) //... r=mid2;
else //... l=mid1;
}
}
整数三分
extern int l,r,mid1,mid2;
extern bool check(int x){
while(r-l>2){//2或其他数 绝不能写成r>l
mid1=l+(r-l)/3,mid2=r-(r-l)/3;
if(check(mid1)>check(mid2)) //... 移动r
else //... 移动l
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具