- 二分的基础用法是在单调序列(函数)中进行查找
- 当问题的答案具有单调性时,可以通过二分把求解转为判定,而判定的难度小于求解
- 可以扩展到三分法去解决单峰函数的极值
整数集合上的二分
- 从严格单增序列中找出给定的元素x
- 复杂度:\(O(logn)\)
- 因为每次查询\(n\)的规模减半,\(k\)次查询时,数据规模为\(n/2^{k-1}\) ,当数据规模为1时,则一定能找到,此时有\(n/2^{k-1}=1\) ,可得\(k=logn+1\) ,故复杂度为\(O(logn)\)
- 问题归纳为:序列中是否存在满足某条件的元素
- 复杂度:\(O(logn)\)
int binarysort(int a[], int l, int r, int x){
while(l<=r){ // l>r时不构成序列
int mid = (l+r)>>1; //避免溢出的话 l+(l-r)>>1
if(a[mid]==x) return mid;
else if(a[mid] > x) r = mid-1;
else l = mid+1;
}
return -1;
}
- 单增序列中第一个大于等于x的元素位置L,第一个大于x的元素位置R
- eg. \(\{1,3,3,3,6\}\) 查询3,返回L=1,R=4; 查询5,返回L=4,R=4;查询6,返回L=4,R=5;查询8,返回L=5,R=5;
- 假设序列中不存在x,则L=R=x应当在的位置
- 单增序列中第一个大于等于x的元素位置L
- 返回位置是x或者x的后继
- 循环条件是\(l<r\)
- 因为最后查找不到的话,停下来的位置就是\(l==r\) ,此时是x应当在的位置
- 二分的初始区间为\([0,n]\)
- 二分下界为0,不可能是-1,因为如果不存在且小于所有元素的话,那么第一个大于x的位置应该是0
- 二分上界为\(n\), 因为如果不存在且大于所有元素的话,则第一个大于x的位置应该是\(n\)
- 如果位置为\(n\) 说明元素不存在
- 如果\(a[mid]\ge{x}\), 说明第一个大于等于的位置在mid左边,且包含mid,故\(r=mid\)
- 否则\(a[mid]<x\),说明第一个大于等于的位置在mid右边,不包括mid,故\(l=mid+1\)
int binarysort(int a[], int l, int r, int x){
while(l<r){
int mid = (l+r)>>1;
if(a[mid]>=x) r = mid;
else l = mid+1;
}
}
- 单增序列中第一个大于x的元素位置R
- 循环条件\(l<r\)
- 二分区间\([0,n]\)
- 如果\(a[mid]>x\) ,说明第一个大于x的位置,在mid左边,且包含mid,\(r=mid\)
- 否则\(a[mid]\le{x}\) ,说明第一个大于x的位置,在mid右边,不包含mid,\(l=mid+1\)
int binarysort(int a[], int l, int r, int x){
while(l<r){
int mid = (l+r)>>1;
if(a[mid]>x) r = mid;
else l = mid+1;
}
}
- 问题归纳为:有序序列中寻找第一个满足某条件的元素位置(一定是先不满足,然后满足)
int solve(int l, int r){ //[l,r]一定要满足所有可能取值
int mid;
while(l<r){ //l==r找到唯一位置
mid = l + ((l-r)>>1);
if(条件成立){ //第一个满足某条件的位置<=mid
r = mid;
}else{ //第一个满足某条件的位置在mid右边(>mid)
l = mid + 1;
}
}
}
- 扩展:寻找最后一个满足条件C的元素的位置,可以先求第一个满足条件!C的位置,然后位置减1
实数域上的二分
- 计算\(\sqrt{2}\)
- \(f(x)=x^2\) ,在区间\([1,2]\) 上是单增的,可以用二分。
- \(\sqrt{2}\) 精度设置为\(10^{-5}\) ,注意精度是\(x\)的精度
- 令浮点数初值\(l\),\(r\) ,然后通过两者中点\(mid\) 与2比较来选择子区间,不断逼近
- 当\(r-l<10^{-5}\) 时,达到精度要求,\(mid\) 即为所求值!
- 归纳为:给定一个定义在\([l,r]\) 上的单调函数,求方程\(f(x)=0\)的根
- 一般保留k位小数的话,eps取\(10^{-(k+2)}\)
//假设fx单减,精度10^-5
double f(double x){ //首先定义好这个函数,注意求解方程fx=2的话,令gx = fx-2
return -x*x;
}
double eps = 1e-5;
double solve(double l, double r){
double mid;
while(r-l>eps){
mid = (l+r)>>1;
if(f(mid)>0){ //单减的话,应该在右边找
l = mid;
}else{
r = mid;
}
}
return mid;
}
三分求单峰函数极值
当函数不是单调函数,而是先增后减或者先减后增的单峰/谷函数时,可以用三分法求极值。
只要函数在区间内只有唯一的极值,且在极值点两侧都是严格单调的,即没有存在值相同的一段,那么可以用三分法求极值。
以单峰函数为例,即存在唯一极大值。
在定义域\([l,r]\) 上任取两个点\(lmid,rmid\) (\(lmid<rmid\)),把函数分为三段
- \(f(lmid)<f(rmid)\)
- \(lmid,rmid\) 在极值点两侧,或者同在左侧(单调上升函数段)
- 无论哪种情况,极大值点都在\(lmid\) 右侧,故可令\(l=lmid\)
- \(f(lmid)>f(rmid)\)
- 在两侧或者同在右侧(单调下降函数段)
- 极大值点一定在\(rmid\) 左侧,令\(r=rmid\)
比较好的取法是:取\(lmid,rmid\) 为三等分点
- \(lmid\) 在区间三分之一处,\(rmid\) 在区间三分之二处
lmid = l + (r - l)/3;
rmid = r - (r - l)/3
- 代码
double solve(double l, double r){
double lmid = l + (r-l)/3;
double rmid = r - (r-l)/3;
double eps = 1e-5;
while(l+eps<r){ //r-l>eps,在等号处跳出循环
if(f(lmid) > f(rmid)) r = lmid;
else l = rmid;
}
return f(l); //返回极值,如果返回极值点就是l
}
二分答案转化为判定
一个宏观的最优化问题抽象为函数
- 定义域:该问题的所有可行方案
- 值域:评估可行方案的数值
假设最优方案的评分为\(S\),那么在数轴上,任意的\(x\le{S}\) ,存在合法方案的评分等于\(x\),即值为1。任意的\(x>S\) ,不存在任何合法方案,即值为0。抽象为一个分段函数。可以通过二分去找分界点\(S\)
最大值最小
有\(N\) 本书排成一排,已知第\(i\)本的厚度是\(A_i\) ,把它们分成连续的\(M\)组,使\(T\) 最小化。\(T\) 表示厚度之和最大的一组的厚度
- 最大值最小是答案具有单调性,用二分转为判定的最典型特征
- 定义域:分成\(M\)组的所有方案
- 值域:厚度之和最大的一组的厚度
- 最优化:评分最小最好
假设评分为\(x\),最优值为\(S\),因为是评分越小越好,那么任意的\(x\) 小于\(S\),分段函数值为0,否则为1。得到的分段函数是单调递增的,分界点在\(S\)。二分可以找到这一点。
int l=0,r=sum_of_ai;
while(l<r){
int mid = (l+r)>>1;
if(valid(mid)) r = mid;
else l = mid + 1;
}
return l;
关键在与这个valid函数怎么写,也就是如何判断方案可行。\(mid\) 是方案的评分,即厚度之和最大的一组的厚度。
方案可行的标准:组数小于m
//把n本书分成m组,每组厚度之和<=size, 是否可行
//模拟分组过程
bool valid(int size){
int group = 1, rest = size;
for(int i=1;i<=n;i++){
if(rest >= a[i]) rest -= a[i];
else group++, rest = size-a[i]; //新的一组肯定要装一个元素
}
return group <= m;
}
有\(N\)条绳子,长度为\(L_i\),从中切割出\(K\)条长度相同的绳子,这个\(K\)条绳子最长能有多长
input:\(N=4,K=11,L=\{8.02,7.43,4.57,5.39\}\)
output:2.00
- 定义域:所有切割方案
- 值域:绳子的长度
- 最优值:越大越好
设绳子长度为\(x\), 最优值为\(S\), 小于\(S\)的方案都可行,大于\(S\)的都不可行。是一个减函数。
可行与否的标准:切割出的绳子数 >= k
bool valid(double x){
int num = 0;
for(int i=0;i<n;i++){
num += (int)(L[i]/x);
}
return num >= k;
}
然后二分判定
void solve(){
double l = 0, r = INF; //因为是浮点数,所以切割可以无限小
for(int i=0;i<100;i++){ //浮点数,不使用eps的另一种写法
double mid = (l+r)>>1;
if(valid(mid)) l = mid;
else r = mid; //浮点数,取mid
}
printf("%.2f\n", floor(r*100)/100); //这个写法有点厉害
}
针对浮点数使用二分搜索时,可以用\(l+eps<r\) ,但是eps太小的话,可能因为浮点数精度问题陷入死循环。
可以用循环固定次数的二分方法,从而达到更好的精度,100次可以达到\(10^{-30}\) 精度。
for(int i=0;i<100;i++){
...
}
N个屋子,位置为\(x_i\),放\(M\)个牛,使得每头牛放在离其他牛尽可能远的牛舍。即使得最大化最近两头牛的距离
- 定义域:放置牛的方案
- 值域:最近的两头牛的距离
- 最优值:最大值
令\(x\) 为最近两头牛的距离,\(S\) 为最优值,是一个单减函数。关键判定方案是否可行
可行的标准:放置\(M\) 个牛能不能放下在\(N\) 个屋子
bool valid(int d){
int k = 0, cnt = 1;//cnt=1!!!
for(int i=1;i<n;i++){
if(a[i]-a[k]>=d){
cnt++;
k = i;
}
}
return cnt>=c;
}
void solve(){
sort(a,a+n); //注意排序!!!!!
int l = 1, r = a[n-1]-a[0];
while(l<r){
int mid = (l+r)>>1;
if(!valid(mid)) r = mid;
else l = mid + 1;
}
printf("%d\n",l-1);
}
最大化平均值
有\(n\) 个物品重量和价值为\(w_i\),\(v_i\) 选出\(k\)个物品使得单位重量的价值最大
- 一般想法是贪心:求出每个物品的平均价值,然后从大到小排序,其实是错的!!!
- 定义域:选择k个物品的所有方案
- 值域:k个物品的单位重量的价值
- 最优:最大
令\(x\)为\(k\)个物品单位重量的价值,\(S\)是最大值,所以是单减函数。使用二分去找这个边界S
方案可行的标准:
假设选了一个方案,物品集合为\(S\),可行的话则要:\({\sum\limits_{i\in{s}}{v_i}}/{\sum\limits_{i\in{s}}{w_i}}\ge{x}\)
变形得到\(\sum\limits_{i\in{s}}(v_i-w_ix)\ge{0}\) 。从而可以贪心选择前\(k\)个\((v_i-w_ix)\) 值大的物品来判断是否满足大于等于0
注意:单位重量的价值是浮点数,所以进行实数域二分就好
二分查找
有放回地从\(n\)个数字中抽取4个数字,是否存在和为\(m\)的方案
\(O(n^4)\)
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
for(int k=0;k<n;k++){
for(int l=0;l<n;l++){
if(a[i]+a[j]+a[k]+a[l] == m) flag = true;
}
}
}
}
\(O(n^3logn)\)
\(是否存在l,使得a[l] == m-a[i]-a[j]-a[k]\)
二分查找的是\(l\) ,故对数组\(a\) 排序后,在三重循环内进行二分查找
bool binarysort(int x){
int l = 0, r = n-1;
while(l<=r){
int mid = (l+r)>>1;
if(a[mid] == x) return true;
else if(a[mid] > x) r = mid - 1;
else l = mid + 1;
}
return false;
}
void solve(){
sort(a,a+n);
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
for(int k=0;k<n;k++){
if(binarysort(m-a[j]-a[i]-a[k])) flag = true;
}
}
}
}
\(O(n^2logn)\)
- \(a[i]+a[j] == m-a[k]-a[l]\)
- 预处理两者的和,再二分查找新数组
bool binarysort(int x){
int l = 0, r = 2*n-1;
while(l<=r){
int mid = (l+r)>>1;
if(b[mid] == x) return true;
else if(b[mid] > x) r = mid - 1;
else l = mid + 1;
}
return false;
}
void solve(){
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
//b.push_back(a[i]+a[j]);
b[i*n+j] = a[i]+a[j]; //自动去除了重复值
}
}
sort(b.begin(),b.end());
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(binarysort(m-a[i]-a[j])) flag = true;
}
}
}
总结
整数集上二分
- 有序序列上是否存在\(x\) (查找某个值时使用)
//单减函数求x // [l,r] = [0,n-1] while(l<=r){ int mid = (l+r)>>1; if(a[mid] == x) return mid; else if(a[mid] > x) l = mid+1; else r = mid-1; } return -1; //没找到
- 有序序列上第一个满足某条件的\(x\)
- 这个也可以用来查找值:对减函数就是第一个小于等于x,对增函数就是第一个大于等于x
- 即使\(x\) 不存在,返回的位置也是\(x\) 应当存在的位置
- 二分区间要满足所有取值,要仔细分析边界
while(l<r){ int mid = (l+r)>>1; if(第一个满足某条件的x的位置<=mid){ r = mid; }else{ l = mid + 1; } }
- 有序序列上最后一个满足某条件的\(x\)
- 比如单减序列中的判定问题
- 转化为“第一个不满足某条件的x的位置<=mid” 则 r=mid
- 最后结果减1即可
溢出:
l+(l-r)>>1
死循环:
(l+r+1)>>1
实数域上二分
- 当精度要求很高时,不能用eps,要用for循环,100次for循环精度可以达到\(10^{-30}\)
- 一般保留k位小数的话,eps取\(10^{-(k+2)}\)
- 注意把方程的根变成函数求零点问题
while(l+eps<r){ int mid = (l+r)>>1; if(){ l = mid; }else{ r = mid; } }
- 三分法求极值
- 在区间内只存在唯一的极值
- 极值点两侧是严格单调,不能用值相等的一段
double solve(double l, double r){
double lmid = l + (r-l)/3;
double rmid = r - (r-l)/3;
double eps = 1e-5;
while(l+eps<r){ //r-l>eps,在等号处跳出循环
//lmid,rmid 在极值点两侧,或者同在左侧(单调上升函数段),无论哪种情况,极大值点都在lmid 右侧,故可令l=lmid
if(f(lmid) > f(rmid)) r = lmid;
else l = rmid;
}
return f(l); //返回极值,如果返回极值点就是l
}
- 二分答案转换为判定问题!!!
- 定义域:xxx方案
- 值域:让你求的那个值,也算方案评分,比如厚度之和最大的一组的厚度
- 最优化:求最大还是最小
- 设最优值为\(S\), \(x\) 为让你求的那个值,判断是单增,单减的分段函数(画图就知道了)
- 设计方案是否可行的函数(难点!!!,一般是数目和题目所给条件比较,需要模拟过程)
- 特征:最大值最小,最大化平均值