二分查找和二分答案
作者:互联网
二分查找
什么是二分查找?
举个栗子:
问:有这样的一个数组:1,3,10,19,20,25,35,45,86,95,114;如何查找出一个能比17大的数的下标呢?
是一个一个的去判断比较吗?
还是写一个hash表来进行查找?
.................
可以看出前一个时间的复杂度是很高的,虽然hash算法的时间复杂度为O(1),但是它的空间复杂度升到了O(n)。有没有一种即可以让时间复杂度降低,又可以让空间 复杂度不达到O(n)。
答案肯定是有的——那就是二分查找
我们就对上面的例子,进行讨论:
问:25比17大还是小? 大
19比17大还是小? 大
3 比17大还是小? 小
因此19就一个比17大的数。对于一个区间,一半,一半的半,一半的一半的一半的去查找,如果被查找的数要大于中间值,说明这个数在上半部分,L的值就向向上移, 否则就R向下移,就是二分查找的思想。因此在开始的时候对于L和R的取值这是和重要的。
例题:
题目描述
输入 n(n\le10^6)n(n≤106) 个不超过 10^9109 的单调不减的(就是后面的数字不小于前面的数字)非负整数 a_1,a_2,\dots,a_{n}a1,a2,…,an,然后进行 m(m\le10^5)m(m≤105) 次询问。对于每次询问,给出一个整数 q(q\le10^9)q(q≤109),要求输出这个数字在序列中的编号,如果没有找到的话输出 -1 。
输入格式
第一行 2 个整数 n 和 m,表示数字个数和询问次数。
第二行 n 个整数,表示这些待查询的数字。
第三行 m 个整数,表示询问这些数字的编号,从 1 开始编号。
输出格式
m 个整数表示答案。
输入输出样例
输入 #111 3 1 3 3 3 5 7 9 11 13 15 15 1 3 6输出 #1
1 2 -1
由此可以看出,L的取值是取数组的开端的下标,R就是数组末尾的下标。使用我们刚刚的思路进行解题。
代码如下:
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=1e6+10; ll num[N],res[N]; int main() { ll n,q; cin>>n>>q; for(int i=1;i<=n;i++) cin>>num[i]; ll L=1,R=n,x,len=0; for(int i=0;i<q;i++) { cin>>x; L=1,R=n; while(L <= R) { ll mid=(L+R)/2; if(x>num[mid]) L=mid+1; else R=mid-1; } if(num[L]==x) res[len++]=L; else res[len++]=-1; } for(int i=0;i<len;i++) printf("%lld ",res[i]); return 0; }
要注意的是L和R前面的判断条件,只有这个L和R的判断条件弄清楚了,二分查找就差不多会了。
二分答案
二分答案的重点代码和二分查找是一样的,只不过二分答案要多出一个cheak()的判断条件。(下面会介绍什么是cheak())
当我们碰到一些在最大值中找最小值,最小值中找最大值或者一些方程的求解时,就可以使用二分答案,实际上就是使用二分查找一步的一步的将答案查找出来,因此需要对于查找的两端进行判断了(下面会介绍如何对两端的进行取值)。其实对于这类的题目读懂题真重要,当你读懂题后你才知道这题其实也没有这么的难,(我这种蒟蒻,就觉得二分答案这类题的题目真的很难读懂),先声明两个点,我接下来的内容在对区间的两端,我都定义为L和R,对于中间值,称为K。
最大值中找最小值:
例题一、
题目描述
伐木工人米尔科需要砍倒M米长的木材。这是一个对米尔科来说很容易的工作,因为他有一个漂亮的新伐木机,可以像野火一样砍倒森林。不过,米尔科只被允许砍倒单行树木。
米尔科的伐木机工作过程如下:米尔科设置一个高度参数H(米),伐木机升起一个巨大的锯片到高度H,并锯掉所有的树比H高的部分(当然,树木不高于H米的部分保持不变)。米尔科就行到树木被锯下的部分。
例如,如果一行树的高度分别为20,15,10和17,米尔科把锯片升到15米的高度,切割后树木剩下的高度将是15,15,10和15,而米尔科将从第1棵树得到5米,从第4棵树得到2米,共得到7米木材。
米尔科非常关注生态保护,所以他不会砍掉过多的木材。这正是他为什么尽可能高地设定伐木机锯片的原因。帮助米尔科找到伐木机锯片的最大的整数高度H,使得他能得到木材至少为M米。换句话说,如果再升高1米,则他将得 不到M米木材。
输入格式
第1行:2个整数N和M,N表示树木的数量(1<=N<=1000000),M表示需要的木材总长度(1<=M<=2000000000)
第2行:N个整数表示每棵树的高度,值均不超过1000000000。所有木材长度之和大于M,因此必有解。
输出格式
第1行:1个整数,表示砍树的最高高度。
输入输出样例
输入 #1 5 204 42 40 26 46输出 #1
36
读完题后,有找到什么是最小值,什么是最大值吗?
这里的最小值是得到的木材和,最大值是尽可能抬高切割的高度。请问L和R的值要怎样取呢?
分析:
题目要求的是切割下的木材的和要是m,在现实生活中,要是切割下的木材的和为一固定值,是不是需要移动高度,每一移动一高度,用大于该高度的数减去高度,将所有剩下的数相加,如果相加的数大于m,我们是不是会把高度下降一点,然后再重复刚刚的步骤,如果是小于,就往上移,反复这般,是不是可以找到一个最优的高度使得正好切割下的木材的和为m。
因此在这里的L是1,R是某一颗树的最大高度,K是m,cheak()用于返回每一高度剩下的木材和。
代码如下:
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int INF=0x7f7f7f7f; const int N=1e7+10; ll num[N]; ll cheak(ll x,ll len) { ll ans=0; for(int i=1;i<=len;i++) { if(num[i]>x) { ans+=(num[i]-x); } } return ans; } int main() { std::ios::sync_with_stdio(0); cin.tie(0); ll m=-INF, n,sum; cin>>n>>sum; for(int i=1;i<=n;i++) { cin>>num[i]; m=max(m,num[i]); } ll L=1,R=m,temp; while(L<=R) { ll mid=(L+R)>>1,temp=cheak(mid,n); if(temp>=sum) L=mid+1; else R=mid-1; } printf("%lld\n",R); return 0; }
可能你觉得上一题的最大值和最小值不太清楚。这里还有一题能够很清晰的表示出二分答案。
样例二、
题目描述
对于给定的一个长度为N的正整数数列A1∼N,现要将其分成 M(M≤N)段,并要求每段连续,且每段和的最大值最小。
关于最大值最小:
例如一数列 4 2 4 5 1 要分成 3 段。
将其如下分段:
[4 2][4 5][1]
第一段和为 6,第 22 段和为 9,第 3 段和为 1,和最大值为 9。
将其如下分段:
[4][2 4][5 1]
第一段和为 4,第 2 段和为 6,第 3 段和为 6,和最大值为 6。
并且无论如何分段,最大值不会小于 6。
所以可以得到要将数列 4 2 4 5 1 要分成 3 段,每段和的最大值最小为 6。
输入格式
第 11 行包含两个正整数 N,M。
第 22 行包含 N 个空格隔开的非负整数 Ai,含义如题目所述。
输出格式
一个正整数,即每段和最大值最小为多少。
输入输出样例
输入 #15 3 4 2 4 5 1输出 #1
6
说明/提示
对于20% 的数据,N≤10。
对于 40% 的数据,N≤1000。
对于100% 的数据,1≤N≤10^5,M≤N,Ai < 10^8, 答案不超过 10^9。
读完题的你,有没有找到最大值是什么,最小值是什么?
最大值是每一次分类的连续的区间的和,最小值是这些最大值中最小的哪一个。
分析:
在此之前,不知道你们是否做过这样的一题,在一数组中找出规定的连续子序列的和为sum的次数。如果没有做过的,先去做完这题,再来看这题,你会发现这题其实没有这么难。
就如上一段所说的,使用cheak返回规定sum的次数。K就是m,如果sum的次数大于m,就说明这个sum的值太小了,要往上找,即sum的值要变大,使得每一段的和的次数变小;如果sum的次数小于m,就往下找。因此我们可以使数组的中最大的哪一个数为L,所有的数的和为R。
代码如下:
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=1e6+10; ll num[N],n,m; ll cheak(ll x) { ll cnt=0,sum=0; for(int i=0;i<n;i++) { if(sum+num[i]<=x) sum+=num[i]; else sum=num[i],cnt++; } return cnt+1; } int main() { ll L=0,R=0; scanf("%lld%lld",&n,&m); for(int i=0;i<n;i++) { scanf("%lld",&num[i]); R+=num[i]; L=max(L,num[i]); } while(L<=R) { ll mid=(L+R)>>1; ll t=cheak(mid); if(t>m) L=mid+1; else R=mid-1; } printf("%lld\n",L); return 0; }
这里有些细节问题,这里不作解释(麻麻说,要自己学会思考呢,啊哈哈哈,其实我是懒了,将就一下吧,但还是要学会自己思考的丫)
最小值中找最大值:
思路是和上面是一样的,但是求的不同,如果上面的看懂的,那么下面的就不用再看了。这里做出说明:我接下来的内容在对区间的两端,我都定义为L和R,对于中间值,称为K。
样例一:
题目描述
木材厂有一些原木,现在想把这些木头切割成一些长度相同的小段木头(木头有可能有剩余),需要得到的小段的数目是给定的。当然,我们希望得到的小段木头越长越好,你的任务是计算能够得到的小段木头的最大长度。木头长度的单位是cm。原木的长度都是正整数,我们要求切割得到的小段木头的长度也是正整数。
例如有两根原木长度分别为11和21,要求切割成到等长的6段,很明显能切割出来的小段木头长度最长为5.
输入格式
第一行是两个正整数N和K(1 ≤ N ≤ 100000,1 ≤ K ≤ 100000000),N是原木的数目,K是需要得到的小段的数目。
接下来的N行,每行有一个1到100000000之间的正整数,表示一根原木的长度。
输出格式
能够切割得到的小段的最大长度。如果连1cm长的小段都切不出来,输出”0”。
输入输出样例
输入 #13 7 232 124 456输出 #1
114
看懂题目的你们,是否有找到最大值是什么?最小值是什么?
最大值是每一段的最大值,最小值是题目给出的数目k
分析:
L为1,既当每一段是1的情况,R为这些小木块的最长的长度,K为题目给出的K,cheak()返回当每一段为这个数X是的次数。如果返回的数小于K,说明数x太大了,要小一些,既要往下找;如果是大于K,说明这个数x太小了,要往上找。
代码如下:
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=1e6+10; ll num[N],n,k; ll cheak(ll x) { ll a=0,b=0; for(int i=0;i<n;i++) a+=(num[i]/x); return a; } int main() { ll R=0,L=1; scanf("%lld%lld",&n,&k); for(int i=0;i<n;i++) { scanf("%lld",&num[i]); R=max(R,num[i]); } while(L<=R) { ll mid=(L+R)>>1,t=cheak(mid); if(t<k) R=mid-1; else L=mid+1; } printf("%lld\n",R); return 0; }
可能你们觉得上面那一的最小值和最大值不太好看出,那么这题句非常的看得出要使用二分答案了。
样例二、
题目背景
B市和T市之间有一条长长的高速公路,这条公路的某些地方设有路标,但是大家都感觉路标设得太少了,相邻两个路标之间往往隔着相当长的一段距离。为了便于研究这个问题,我们把公路上相邻路标的最大距离定义为该公路的“空旷指数”。
题目描述
现在政府决定在公路上增设一些路标,使得公路的“空旷指数”最小。他们请求你设计一个程序计算能达到的最小值是多少。请注意,公路的起点和终点保证已设有路标,公路的长度为整数,并且原有路标和新设路标都必须距起点整数个单位距离。
输入格式
第1行包括三个数L、N、K,分别表示公路的长度,原有路标的数量,以及最多可增设的路标数量。
第2行包括递增排列的N个整数,分别表示原有的N个路标的位置。路标的位置用距起点的距离表示,且一定位于区间[0,L]内。
输出格式
输出1行,包含一个整数,表示增设路标后能达到的最小“空旷指数”值。
输入输出样例
输入 #1
101 2 1 0 101输出 #1
51
说明/提示
公路原来只在起点和终点处有两个路标,现在允许新增一个路标,应该把新路标设在距起点50或51个单位距离处,这样能达到最小的空旷指数51。
50%的数据中,2 ≤ N ≤100,0 ≤K ≤100
100%的数据中,2 ≤N ≤100000, 0 ≤K ≤100000
100%的数据中,0 < L ≤10000000
看懂题目的你们,是否找到最大值和最小值?
最小值是使两个路标的间距尽可能的小,最大值是这些最小值的最大值。
分析:
L为当两个路标间距是1 的情况。R为题目给出的路标距离的最大值,当两个路标的距离是x的时候,cheak()返回的是这些的路标的数量cnt,KK为题目给定的k。如果cnt大于k的时候,说明这个间距太小了,使得放入的路标大于了题目所给的k,因此要x的值要再大些,既往上找。如果cnt小于k的时候,说明这个间距太大了,使得放入的路标不足题目所给的k,因此要往下找。
代码如下:
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=1e6+10; ll num[N],l,n,k; ll cheak(ll x) { ll cnt=0; for(int i=0;i<n;i++) { if((num[i+1]-num[i])>x) { cnt+=(num[i+1]-num[i])/x; if((num[i+1]-num[i])%x==0) cnt--; } } return cnt; } int main() { ll R=0,L=1; scanf("%lld%lld%lld",&l,&n,&k); for(int i=0;i<n;i++) { scanf("%lld",&num[i]); R=max(R,num[i]); } while(L<R) { ll mid=(L+R)>>1,t=cheak(mid); if(t<=k) R=mid; else L=mid+1; } printf("%lld\n",R); return 0; }
一些方程的求解:
其实这个还是很好写的吧,就是公式不太好找。要是能找出公式,就能在一段的区间中筛选出答案了。
样例:
题目描述
有形如:ax^3+bx^2+cx^1+dx^0=0这样的一个一元三次方程。给出该方程中各项的系数(a,b,c,d均为实数),并约定该方程存在三个不同实根(根的范围在-100−100之间),且根与根之差的绝对值\≥1。要求由小到大依次在同一行输出这三个实根(根与根之间留有空格),并精确到小数点后22位。
提示:记方程f(x)=0,若存在2个数x1和x2,且x1<x2,f(x1)×f(x2)<0,则在(x1,x2)之间一定有一个根。
输入格式
一行,4个实数A,B,C,D。
输出格式
一行,3个实根,并精确到小数点后2位。
输入输出样例
输入 #1
1 -5 -4 20输出 #1
-2.00 2.00 5.00
分析:
先用for找出存在两个x1,x2能使得f(x1)×f(x2)<0,如果有,就在x1和x2之前使用二分答案找出一个答案。
代码如#include<bits/stdc++.h>
#define IO std::ios::sync_with_stdio(0),cin.tie(0) using namespace std; double a,b,c,d; double f(double i) { return a*i*i*i+b*i*i+c*i+d; } int main() { IO ; cin>>a>>b>>c>>d; int cnt=0; for(double i=-100;i<=100;i++) { double x1=f(i); double x2=f(i+1); if(!x1) { printf("%.2lf ",i); cnt++; } if(x1*x2<0) { double L=i,R=i+1; while((R-L)>=0.0001) { double mid=(L+R)/2; if(f(mid)*f(R)<=0) L=mid; else R=mid; } printf("%.2lf ",R); cnt++; } if(cnt==3) break; } return 0; }
-
未经许可,禁止转载,一旦发现,后果自负。
2020-05-30
12:28:06
标签:二分,num,路标,ll,int,最小值,查找,答案,最大值 来源: https://www.cnblogs.com/duhengerhou/p/12991974.html