二分

  • 二分的基础用法是在单调序列(函数)中进行查找
  • 当问题的答案具有单调性时,可以通过二分把求解转为判定,而判定的难度小于求解
  • 可以扩展到三分法去解决单峰函数的极值

整数集合上的二分

  • 从严格单增序列中找出给定的元素x
    • 复杂度:\(O(logn)\)
      • 因为每次查询\(n\)的规模减半,\(k\)次查询时,数据规模为\(n/2^{k-1}\) ,当数据规模为1时,则一定能找到,此时有\(n/2^{k-1}=1\) ,可得\(k=logn+1\) ,故复杂度为\(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\) 为让你求的那个值,判断是单增,单减的分段函数(画图就知道了)
    • 设计方案是否可行的函数(难点!!!,一般是数目和题目所给条件比较,需要模拟过程)
    • 特征:最大值最小,最大化平均值

转载于:https://www.cnblogs.com/doragd/p/11301817.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值