二分查找
在一个严格递增的序列中找出给定的数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的大小来判断应当往哪个子区间继续查找:
- 若arr[mid]>=x,说明第一个大于等于x的元素的位置一定在mid处或者mid的左侧,应往左子区间[left, mid]继续查询,即令right=mid。
- 若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;
}
代码中有几个需要注意的地方:
- 循环条件为left<right而非之前的left<=right,这是由问题本身决定的。在上一个问题中,需要当元素不存在时返回-1,这样当left>right时[left, right]就不再是闭区间,可以作为元素不存在的判定原则,因此left<=right满足时循环应当一直执行;但是如果想要返回第一个大于等于x的元素的位置,就不需要判断元素x本身是否还存在,因为就算它不存在,返回的也是“假设它存在,它应该在的位置”,于是当left==right时,[left, right]刚好能夹出唯一的位置,就是需要的结果,因此只需当left<right时让循环一直执行即可。
- 由于当left==right时while循环停止,因此最后的返回值既可以是left,也可以是right。
- 二分的初始区间应当能覆盖到所有可能返回的结果。首先,二分下界是0是显然的,但是二分上界是n-1还是n呢?考虑到欲查询元素有可能比序列中的所有元素都要打,此时应当返回n(即假设它存在,它应该在的位置),因此二分上界是n,故二分的初始区间为[left, right] = [0, n]。
在一个非递减序列中找出第一个大于x的元素的位置R
假设当前的二分区间为左闭右闭区间[left, right],那么可以根据mid位置处的元素与待查询元素x的大小来判断应当往哪个子区间继续查找:
- 若arr[mid]>x,说明第一个大于x的元素的位置一定在mid处或者mid的左侧,应往左子区间[left, mid]继续查询,即令right=mid。
- 若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的根
- 若f(mid)>target,说明f(x)=target的根在mid左侧,应往左子区间[left, mid]继续逼近,即令right=mid。
- 若f(mid)<target,说明f(x)=target的根在mid右侧,应往右子区间[mid, right]继续逼近,即令left=mid。
当right-left<时表明达到精度要求,结束算法,所返回的当前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,要求算法中不涉及坐标计算。
解题分析:
这题要从二分法的角度去解而不是数学坐标计算的角度。先考虑如何组成一个有外接圆的凸多边形,然后求其半径即可,不要误入歧途在“最大”这两个字上绞尽脑汁。
- 设凸多边形周长为C,最大边长为A,总边数为N,边Li与两条半径为R的边围成的等腰三角形的顶角为ai
- 首先判断所给边长是否能组成多边形。当C-A<A时,无法围成多边形。
- 然后,判断凸多边形的外接圆圆心是否在凸多边形最长边上。以最长边为直径求圆心角之和,若为2*π则圆心在最长边上,解题结束;反之则不是。
- 然后,判断凸多边形的外接圆圆心在凸多边形内部还是外部。初始化圆的半径范围[A, MAX_INT],在这个范围进行二分查找(left = A/2, right = MAX_INT;当right-left <= eps = 1e-5时,认为误差范围内,mid= (left+right)/2 即所求半径)。如果除去最大圆心角,其他圆心角之和小于π,说明圆心在多边形外部;反之,则在内部。
- 二分查找中,圆心可能在多边形外部的情况下,更新left、right的策略。(两角之和为360度时,它们叫“互周角”)若 其他圆心角之和 + 最大圆心角的互周角 < 2*π,半径需要增大,left=mid;反之,半径需要减小,right=mid。
- 二分查找中,圆心可能在多边形内部的情况下,更新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;
}
快速幂
参考 这篇博文