二分法——学习记录

二分查找

在一个严格递增的序列中找出给定的数target

bool BinarySearch(int arr[],int n,int target){
    int left=0,right=n-1,mid;
    while(left<=right){  // 对于[left, right]来说,left>right 或 arr[mid]==target意味着找到答案;前者意味着序列不存在target,后者意味着序列中存在target
        mid=left+(right-left)/2;   // 避免left+right超过int范围而发生的溢出现象
        if(arr[mid]==target) return true;
        if(arr[mid]<target) left = mid+1;
        else right = mid-1;
    }
    return false;
}

        若序列是递减的,只需把第6行代码 arr[mid]<target 改为 arr[mid]>target

在一个非递减序列中找出第一个大于等于x的元素的位置L

        假设当前的二分区间为左闭右闭区间[left, right],那么可以根据mid位置处的元素与待查询元素x的大小来判断应当往哪个子区间继续查找:

  1. 若arr[mid]>=x,说明第一个大于等于x的元素的位置一定在mid处或者mid的左侧,应往左子区间[left, mid]继续查询,即令right=mid。
  2. 若arr[mid]<x,说明第一个大于等于x的元素的位置一定在mid的右侧,应往右子区间[mid+1, right]继续查询,即令left=mid+1。
int LowerBound(int arr[],int n,int target){
    int left=0,right=n,mid;
    while(left<right){  // 对于[left, right]来说,left==right意味着找到唯一位置
        mid=left+(right-left)/2;
        if(arr[mid]>=target) right = mid;
        else left = mid+1;
    }
    return left;
}

代码中有几个需要注意的地方:

  1. 循环条件为left<right而非之前的left<=right,这是由问题本身决定的。在上一个问题中,需要当元素不存在时返回-1,这样当left>right时[left, right]就不再是闭区间,可以作为元素不存在的判定原则,因此left<=right满足时循环应当一直执行;但是如果想要返回第一个大于等于x的元素的位置,就不需要判断元素x本身是否还存在,因为就算它不存在,返回的也是“假设它存在,它应该在的位置”,于是当left==right时,[left, right]刚好能夹出唯一的位置,就是需要的结果,因此只需当left<right时让循环一直执行即可。
  2. 由于当left==right时while循环停止,因此最后的返回值既可以是left,也可以是right。
  3. 二分的初始区间应当能覆盖到所有可能返回的结果。首先,二分下界是0是显然的,但是二分上界是n-1还是n呢?考虑到欲查询元素有可能比序列中的所有元素都要打,此时应当返回n(即假设它存在,它应该在的位置),因此二分上界是n,故二分的初始区间为[left, right] = [0, n]

在一个非递减序列中找出第一个大于x的元素的位置R

        假设当前的二分区间为左闭右闭区间[left, right],那么可以根据mid位置处的元素与待查询元素x的大小来判断应当往哪个子区间继续查找:

  1. 若arr[mid]>x,说明第一个大于x的元素的位置一定在mid处或者mid的左侧,应往左子区间[left, mid]继续查询,即令right=mid。
  2. 若arr[mid]<=x,说明第一个大于x的元素的位置一定在mid的右侧,应往右子区间[mid+1, right]继续查询,即令left=mid+1。
int UpperBound(int arr[],int n,int target){
    int left=0,right=n,mid;
    while(left<right){  // 对于[left, right]来说,left==right意味着找到唯一位置
        mid=left+(right-left)/2;
        if(arr[mid]>target) right = mid;
        else left = mid+1;
    }
    return left;   // 返回夹出来的位置
}

总结

        稍加观察,不难发现,和LowerBound函数的代码相比,UpperBound函数只是把代码中的arr[mid]>=target改成了arr[mid]>target,其他完全相同,这启发我们去寻找它们的共同点。

        这两个函数都在解决这样一个问题:寻找有序序列中第一个满足某条件的元素的位置。对于LowerBound函数来说,它寻找的就是第一个满足条件“值大于等于target”的元素的位置;对于UpperBound函数来说,它寻找的就是第一个满足条件“值大于target”的元素的位置。显然,所谓的“某条件”在序列中一定是从左到右先不满足,然后满足的(否则把该条件取反即可)。

// 解决“寻找有序序列第一个满足某条件的元素的位置”问题的固定模板
// 二分区间为左闭右闭的[left, right],初值必须能覆盖解的所有可能取值
int Solve(int left, int right){
    int mid;
    while(left<right){  // 对于[left, right]来说,left==right意味着找到唯一位置
        mid=left+(right-left)/2;
        if(条件成立) right = mid;
        else left = mid+1;
    }
    return left;   // 返回夹出来的位置
}

        另外,如果想要寻找最后一个满足“条件C”的元素的位置,则可以先求第一个满足“条件!C”的元素的位置,然后将该位置减1即可。

        寻找有序序列中是否存在满足某条件的元素

// 寻找有序序列中是否存在满足某条件的元素
bool SolveExist(int arr[],int n,int target){
    int left=0,right=n-1,mid;
    while(left<=right){  // 对于[left, right]来说,left>right 或 arr[mid]==target意味着找到答案;前者意味着序列不存在target,后者意味着序列中存在target
        mid=left+(right-left)/2;
        if(条件满足) return true;
        if(右子区间可能有条件满足的情况) left = mid+1;
        else right = mid-1;
    }
    return false;
}

二分法拓展

计算近似值

        给定一个定义在[L, R]上的单调函数f(x),求方程f(x)=target的根

解题思路

        假设精度要求为eps=10^-5,函数f(x)在[L, R]上递增,并令left与right的初值分别为L、R,然后就可以根据left与right的中点mid的函数值f(mid)与target的大小关系来判断应当往哪个子区间继续逼近f(x)=target的根 

  1. 若f(mid)>target,说明f(x)=target的根在mid左侧,应往左子区间[left, mid]继续逼近,即令right=mid。
  2. 若f(mid)<target,说明f(x)=target的根在mid右侧,应往右子区间[mid, right]继续逼近,即令left=mid。

当right-left<10^{-5}时表明达到精度要求,结束算法,所返回的当前mid值即为f(x)=target的根

const double eps = 1e-5;  // 精度为10^-5
double f(double x){   // 计算f(x)
    return ......;   // 此处可以替换为任意单调函数
}
double Calculate(double left, double right, double target){
    double mid;
    while(right-left>eps){
        mid = left + (right-left)/2;
        if((f(mid)>target) right = mid;   // 若f是单调递减函数,则此处的判断需改为(f(mid)<target
        else left = mid;
    }
    return mid;  // 所返回的当前mid值即为f(x)=target的根
}

木棒切割问题

凸多边形外接圆最大半径问题

问题描述:

        给出N个线段的长度,试将它们头尾相接(顺序任意)地组合成一个凸多边形,使得该凸多边形的外接圆(即能使凸多边形的所有顶点都在圆周上的圆)的半径最大,求该最大半径。其中N不超过10^5,线段长度均不超过100,要求算法中不涉及坐标计算。

解题分析:

        这题要从二分法的角度去解而不是数学坐标计算的角度。先考虑如何组成一个有外接圆的凸多边形,然后求其半径即可,不要误入歧途在“最大”这两个字上绞尽脑汁。

  1. 设凸多边形周长为C,最大边长为A,总边数为N,边Li与两条半径为R的边围成的等腰三角形的顶角为ai
  2. 首先判断所给边长是否能组成多边形。当C-A<A时,无法围成多边形
  3. 然后,判断凸多边形的外接圆圆心是否在凸多边形最长边上。以最长边为直径求圆心角之和,若为2*π则圆心在最长边上,解题结束;反之则不是。
  4. 然后,判断凸多边形的外接圆圆心在凸多边形内部还是外部。初始化圆的半径范围[A, MAX_INT],在这个范围进行二分查找(left = A/2, right = MAX_INT;当right-left <= eps = 1e-5时,认为误差范围内,mid= (left+right)/2 即所求半径)。如果除去最大圆心角,其他圆心角之和小于π,说明圆心在多边形外部;反之,则在内部。
  5. 二分查找中,圆心可能在多边形外部的情况下,更新left、right的策略。(两角之和为360度时,它们叫“互周角”)若 其他圆心角之和 + 最大圆心角的互周角 < 2*π,半径需要增大,left=mid;反之,半径需要减小,right=mid。
  6. 二分查找中,圆心可能在多边形内部的情况下,更新left、right的策略。若 所有圆心角之和 > 2*π,半径需要增大,left=mid;反之,半径需要减小,right=mid。

        具体实现的代码参考一篇博文,它给我提供了很多的解题思路,非常感谢它。

(不过我暂时没有找到这个题目对应的在线测试链接,所以代码的实际效果也不好夸大,此处做个参考,毕竟解题思路还是可以借鉴的)

#include<cstdio>
#include<cmath>
const double PI=acos(-1.0);
const double eps=1e-5;//比较精度
//求圆心角之和
double totalCornerAngles(double edges[],int n,double r) {
    double sum = 0.0;
    for(int i =0; i<n; i++)
        sum+=asin(edges[i]/(2/r))*2;
    return sum;
}
//二分法求半径
int main() {
    int N;//边数
    scanf("%d",&N);//输入边数
    double edges[N];//边长数组
    double sum;//圆心角之和
    double maxAngle=0.0;//最长边对应的圆心角
    double maxEdge=0.0;//最长边
    //初始化edges
    for(int i=0; i<N; i++) {
        scanf("%lf",&edges[i]);
        if(edges[i]>maxEdge)
            maxEdge = edges[i];//保存最大边
    }
    //以最长边为直径求圆心角之和,若为2π则直接返回
    sum = totalCornerAngles(edges,N,maxEdge/2);
    if(fabs(sum-PI*2)<eps) {
        printf("外接圆的最大半径是最大边的一半:%.2f",maxEdge/2);
        return 0 ;
    }
    //半径大于最大边的一半(即斜边大于直角边)
    double left =maxEdge/2,right=10000000,mid;
    double other=0;
    //在误差范围内循环求解
    while(right -left >eps) {
        mid = (right + left) /2;
        maxAngle=asin(maxEdge/2/mid)*2;//求出最大边对应的圆心角
        sum = totalCornerAngles(edges,N,mid);
        other=sum-maxAngle;
        //如果除去最大圆心角的其他圆心角之和小于π,说明圆心在多边形外面
        if(other<PI) {
            sum=other+2*PI-maxAngle;
            if( sum<2*PI)
                left = mid;
            else
                right = mid;
        }
        //圆心在多边形里面
        else {
            if( sum>2*PI)
                left = mid;
            else
                right = mid;
        }
    }
    printf("外接圆的最大半径是:%.2f",mid);
    return 0;
}

快速幂

参考 这篇博文

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值