赞
踩
求解非线性方程的一种简单高效的方法:二分法
非线性方程是指 f (x)中含有三角函数、指数函数或其他超越函数(如对数函数,反三角函数,指数函数等)。这些方程很难求得精确解,不过在实际应用中,只要满足一定精度要求的近似解就可以了。此时需要考虑以下两个问题:
(1)根的存在性。定理:函数 f (x) 在闭区间 [a,b] 上连续,且 f (a) · f (b) < 0 ,则 f(x)存在根。
(2)求根。一般有两种方法,搜索法和二分法,这边只介绍二分法。
二分法:如果确定 f (x) 在闭区间 [a,b] 上连续,且 f (a) · f (b) < 0(也就是根存在的情况下),把 [a,b] 逐次分半,检查每次分半后区间两端点函数值符号的变化,确定有根的区间。
注意:使用二分法,需要函数满足两个条件:上下界 [a,b] 确定;且函数在 [a,b] 内单调。
算法竞赛中有两种二分题型:整数二分和实数二分。整数二分要注意边界问题,避免漏掉答案或者陷入死循环;实数二分要注意精度问题。
通过两个问题来给出整数二分的基本代码:在单调递增序列中找x或x的后继;在单调递增序列中找x或x的前驱
分析:题目中要求我们寻找一个单调递增数列中的x的起始位置和终止位置。我们可以分为两种情况讨论:
寻找x的起始位置,即找第一个x所在位置,也就是在单调递增序列中找x或x的后继;寻找x的终止位置,即找最后一个x所在位置,也就是在单调递增序列中找x或x的前驱;
下面给出“在单调递增序列中找x或x的后继”的过程模拟:
代码如下:
- //a表示有n个元素的单调递增数组,x为将要查找的目标元素
- int find(int a,int n,int x){
- int l=0,r=n-1; //定义左右边界
- while(l<r){
- int mid=l+r>>1;
- if(a[mid]>=x)r=mid; //说明目标元素不在mid位置的右边,搜索区间更新为[l,mid]
- else l=mid+1; //说明目标元素不在mid位置以及mid位置的左边,搜索区间更新为[mid+1,r]
- } //终止于l=r
- return l;
- }
在单调递增序列中找x或x的后继中,注意:
(1)对mid的计算
处理 | 使用场合 | 可能出现的问题 |
mid=(l+r)/2 | l,r>=0;且 l+r 无溢出 | l+r 可能溢出;负数情况下有向0取整问题 |
mid=l+(r-l)/2 | l-r 无溢出 | 若 l,r 一正一负,l-r 可能溢出 |
mid=l+r>>1 | l+r 无溢出 | l+r 可能溢出 |
(2)对mid的处理
整数二分中存在取整的问题。例如当 l=2,r=3 时,mid=l+r>>1=2,若取 l=mid=2,那么更新后的 l、r 的值仍然不变,while()陷入死循环。所以不能写成 l=mid。
不能。例如当 l=2,r=3 时,mid=l+r+1>>1=3,若取 r=mid=3,那么更新后的 l、r 的值仍然不变,while()陷入死循环。
结合以上两条,我们可以发现,当 l =mid时,mid不能向 l 取整,而必须向 r 取整;当 r =mid时,mid不能向 r 取整,而必须向 l 取整。否则将有可能出现死循环。
若(l+r)是正数,那么mid向下(向 l)取整;若(l+r)是负数,那么mid向上(向 r)取整。所以能在正区间正确运行的代码,在负区间可能出现死循环。
(3)在二分之前需要进行排序(虽然本题不用进行排序)
下面给出“在单调递增序列中找x或x的前驱”的代码:
- int find(int a,int n,int x){
- int l=0,r=n-1;
- while(l<r){
- int mid=l+r+1>>1; //注意:由于l=mid,所以mid必须向上取整
- if(a[mid]<=x)l=mid;
- else r=mid-1;
- }
- return r;
- }
结合在单调递增序列中找x或x的后继,以及在单调递增序列中找x或x的前驱的代码,本题代码如下:
- #include<bits/stdc++.h>
- using namespace std;
- const int N=100010;
- int a[N];
- int main(){
- int n,q;
- cin>>n>>q;
- for(int i=0;i<n;i++)cin>>a[i];
- while(q--){
- int k;
- cin>>k;
-
- //查找x与x的后继
- int l=0,r=n-1;
- while(l<r){
- int mid=l+r>>1;
- if(a[mid]>=k)r=mid;
- else l=mid+1;
- }
- if(a[l]!=k){
- cout<<"-1 -1"<<endl;
- continue;
- }else cout<<l<<' ';
-
- //查找x与x的前驱
- l=0,r=n-1;
- while(l<r){
- int mid=l+r+1>>1;
- if(a[mid]<=k)l=mid;
- else r=mid-1;
- }
- cout<<r<<endl;
- }
- return 0;
- }
分析:
C已知,要找到A-B=C
那么问题可以转化为:对于数列中的每一个元素A,是否存在一个数B,使得A-B=C。若存在,求出B的个数。
显然,我们的目标是,从数列中查找一个数,那么可以考虑用二分法
代码如下:
- #include<iostream>
- #include<algorithm>
- using namespace std;
- const int N=200010;
- int a[N]; //存储正整数数列
- int n,c;
- long long res; //注意,长度为n的正整数列中,数对最多有n^2/4个,res>2e9,超出int数据范围
-
- int main(){
- cin>>n>>c;
- for(int i=0;i<n;i++)cin>>a[i];
- sort(a,a+n); //先对数列进行排序
-
- //遍历A的值
- for(int i=0;i<n;i++){
- int b=a[i]-c; //需要查找的目标元素
- int st; //储存目标元素的起始位置
-
- int l=0,r=n-1;
- //由于c为正整数,所以b一定小于a[i],又因为a数列由小到大排序
- //所以目标元素b一定在 [0,i)区间内,所以可以取 l=0,r=i
- while(l<r){
- int mid=l+r>>1;
- if(a[mid]>=b)r=mid;
- else l=mid+1;
- }
- if(a[l]!=b)continue; //如果找不到目标元素,则进入下一个循环
- st=l;
-
- l=0,r=n-1;
- //由于起始位置st已知,所以目标元素b一定在(st,i)区间内,所以可以取 l=st,r=i
- while(l<r){
- int mid=l+r+1>>1;
- if(a[mid]<=b)l=mid;
- else r=mid-1;
- }
- res+=r-st+1; //注意要+1
- }
- cout<<res;
- return 0;
- }
代码如下:
- #include<iostream>
- #include<algorithm>
- using namespace std;
- const int N=100010;
- int a[N],b[N]; //a[]为学校预计分数线;b[]为学生估分
- int n,m;
- long long res;
-
- int main(){
- cin>>m>>n; //m为学校数,n为学生数
- for(int i=0;i<m;i++)cin>>a[i];
- for(int i=0;i<n;i++)cin>>b[i];
- sort(a,a+m); //对学校分数线进行排序
-
- for(int i=0;i<n;i++){ //遍历每个学生的估分
-
- int l=0,r=m-1;
- while(l<r){
- int mid=l+r>>1;
- if(a[mid]>=b[i])r=mid;
- else l=mid+1;
- }
-
- //当b[i]小于所有学校分数线时,l=r=0,此时l-1越界,所以要特判
- if(b[i]<=a[0])res+=a[0]-b[i];
- else res+=min(abs(a[l]-b[i]),abs(a[l-1]-b[i]));
- }
- cout<<res;
- return 0;
- }
特征1:整数二分法的两个经典模型:最大值最小化(最大值尽量小),最小值最大化(最小值尽量大)
特征2:答案在一个区间内
分析:
求最大值的最小化,考虑整数二分答案的方法来解题。
该题涉及到求区间的和,考虑使用前缀和数组(前缀和数组的下标最好从1开始)
1.输入数列的同时,初始化前缀和数组,找到前缀和数组中的最大值sum_max,
2.定义二分答案所在的区间 [l,r],其中 r=sum_max
3.取mid=l+r>>1
4.对于每次二分出的mid,判断mid是否满足条件(check函数判断,若数组每一段的和都<=mid,那么一共需要分成多少段,若分成的段数<=m,说明mid满足条件,反之则不满足)
过程模拟:
- #include<iostream>
- using namespace std;
- const int N=100010;
- int a[N];
- long long sum[N];
- int n,m,a_max,sum_max; //a_max存储数列a中的最大值
-
- bool check(int x){
- int k=0;
- if(x<a_max)return false;
- for(int i=1,j=0;i<=n;i++){
- if(sum[i]-sum[j]>x){
- j=i-1;
- k++;
- }
- }
- if(k<m)return true;
- else return false;
- }
-
- int main(){
- cin>>n>>m;
- for(int i=1;i<=n;i++){
- cin>>a[i];
- sum[i]=a[i]+sum[i-1]; //初始化前缀和数组
- if(a[i]>a_max)a_max=a[i]; //找到数列中的最大值
- if(sum[i]>sum_max)sum_max=sum[i]; //找到数列中的最大值
- }
-
- int l=0,r=sum_max;
- while(l<r){
- int mid=l+r>>1;
- if(check(mid))r=mid;
- else l=mid+1;
- }
- cout<<l;
- return 0;
- }
其他例题:[TJOI2007] 路标设置 - 洛谷
分析:
题目要求我们求出 l 的最大值,且 l 在一个区间内,于是可以考虑用二分答案的方法来求解:从区间中找到一个最优答案。
对于样例:n=3 , k=7 , a[ ]={232,124,456}
代码如下:
- #include<iostream>
- #include<algorithm>
- using namespace std;
- const int N=100010;
- int a[N];
- int n,k;
-
- bool check(int x){
- int res=0; //记录把木头切割为长度均为x的小段木头,可以切出res段
- for(int i=0;i<n;i++){
- res+=a[i]/x;
- }
- if(res>=k)return true;
- else return false;
- }
-
-
- int main(){
- cin>>n>>k;
- for(int i=0;i<n;i++)cin>>a[i];
- sort(a,a+n);
-
- int l=0,r=a[n-1];
- while(l<r){
- int mid=l+r+1>>1;
- if(check(mid))l=mid;
- else r=mid-1;
- }
- cout<<l<<endl;
- return 0;
- }
代码如下:
- #include<iostream>
- using namespace std;
- const int N=50010;
- int a[N]; //岩石的位置
- int d,n,m;//d表示起点到终点的距离
-
- bool check(int x){
- int j=0,k=0; //j表示目前所在位置,k表示目前已经移走的岩石数量
- for(int i=1;i<=n+1;i++){ //易错点!!i<=n+1
- //i表示将要跳的岩石位置,要考虑终点。若j位置无法跳到终点,则把j位置的岩石移走
- if(a[i]-a[j]<x)k++; //如果由j位置无法跳到i位置,那么把i位置的岩石移走
- else j=i; //如果由j位置能够跳到i位置,那么更新当前位置j
- }
- if(k<=m)return true; //如果移走的岩石总数<=m,说明该跳跃距离满足条件
- return false;
- }
-
- int main(){
- cin>>d>>n>>m;
- for(int i=1;i<=n;i++)cin>>a[i];
- a[n+1]=d; //a[n+1]表示位于终点的石头
-
- int l=1,r=d; //最短跳跃距离,即最优解所在区间
- while(l<r){
- int mid=l+r+1>>1;
- if(check(mid))l=mid; //因为要求最大值,所以尽可能往右走
- else r=mid-1;
- }
- cout<<l;
- return 0;
- }
其他例题:进击的奶牛 - 洛谷
代码如下:
- #include<iostream>
- using namespace std;
- int main(){
- double n;
- cin>>n; //输入一个浮点数
- double l=-10000,r=10000; //定义左右边界
-
- //题目要求保留n-2位小数时,写1e-n(记得多2)
- //比如说这道题目中,要求保留六位小数,那么写成1e-8
- while(r-l>=1e-8){ //也可以直接循环100次
- double mid=(l+r)/2;
- if(mid*mid*mid>n)r=mid; //若mid的三次方大于n,说明n的三次方根在mid的左侧,所以令r=mid
- else l=mid;
- }
- printf("%.6lf",l);
- return 0;
- }
假设有n个元素,那么二分后每次查找的区间大小为 n , n/2 ,… n/2^k。其中k是循环的次数。
最坏的情况是k次二分之后,每个区间的大小为1时,找到目标元素。令n/2^k=1,得到k=logn
所以二分查找的时间复杂度为O(logn)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。