什么是二分查找?它与二分答案有区别吗?其实两者还是有区别的。
- 二分查找也称折半查找,它是查找顺序存储结构中的某一个元素。是一种高效的查找元素的方式。
- 二分答案是通过答案满足的某一性质,不断的减小答案所在的区间,找到一个最优满足题意的答案的过程。
我们可以对比一下二分查找的模板和二分答案的模板:
二分查找:
//我们要在(l,r)这个区间查找key这个数字
while(l<=r)
{
mid=(l+r)/2;
if(a[mid]>key)r=mid-1;
else if(a[mid]<key)l=mid+1;
else
{
printf("找到key,下标是%d\n",mid);
break;
}
}
if(l>r)printf("没有找到key\n");
整数二分:
因为区间(l,r)可以被分成(l,mid ]和(mid+1,r)或者被分成(l,mid-1)和 [ mid,r)所以二分答案的模板有两种形式。
模板一:
//区间(l,r)被划分成(l,mid)和(mid+1,r)时使用
while(l<r)
{
int mid = l+r >> 1;//(l+r)/2
if(check(mid))r = mid;//bool check();是我们答案满足的某一性质
else l = mid+1;
}
return l;//l就是我们查找数字的下标,终止的条件是l=r
模板二:
//区间(l,r)被分成(l,mid-1)和(mid,r)时使用
while(l<r)
{
int mid = l+r+1 >> 1;//(l+r+1)/2这里的mid是上取整
if(check(mid))l = mid;//bool check()函数检验答案是否满足条件
else r = mid-1;
}
return l;//返回答案的下标
所以二分答案的两种模板到底有什么区别呢?区别就在于mid是被划分在左边的区间还是右边的区间。如果划分到左边,也就是第一种模板,反之就是第二种模板。
那么我们如何记住这两种模板呢?其实关键在于mid是上取整还是下取整,如果mid是向上取整,待查的值一定是在右边的区间,如果mid是向下取整待查的值一定是左边的区间。
总结一下就是:
- 如果要查左边区间的点,就用第一个模板,r=mid,l=mid+1,mid=(l+r)/2
- 如果要查右边区间的点,就用第二个模板,l=mid,r=mid-1,mid=(l+r+1)/2
浮点数二分:
while(r-l>eps)//eps表示精度
{
double mid = (l+r)/2;
if(check(mid))r = mid;//check()函数看mid是否满足条件
else l = mid;//浮点数不需要考虑取整的问题
}
return l;
由于浮点数不需要考虑取整的问题,所以只有这一种情况。
我们先从一道简单的题入手
题目描述
输入 n 个不超过 的单调不减的(就是后面的数字不小于前面的数字)非负整数 ,,…,,然后进行 m 次询问。对于每次询问,给出一个整数 q,要求输出这个数字在序列中第一次出现的编号,如果没有找到的话输出 −1 。
输入格式
第一行 2 个整数 n 和 m,表示数字个数和询问次数。
第二行 n 个整数,表示这些待查询的数字。
第三行 m 个整数,表示询问这些数字的编号,从 1 开始编号。
输出格式
输出一行,m 个整数,以空格隔开,表示答案。
输入输出样例
输入 #1
11 3 1 3 3 3 5 7 9 11 13 15 15 1 3 6
输出 #1
1 2 -1
说明/提示
数据保证,1≤n≤,0≤,q≤,1≤m≤
本题输入输出量较大,请使用较快的 IO 方式。
参考代码:
直接套模板!!!
#include <cstdio>
#include <iostream>
using namespace std;
const int N=1000010;
int q[N];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for (int i=0; i<n; i++) {
scanf("%d",&q[i]);
}
while(m--)
{
int x;
scanf("%d",&x);
int l=0,r=n-1;
while(l<r)
{
int mid=r+l>>1;
if(q[mid]>=x)r=mid;
else l=mid+1;
}
if(q[l]!=x)cout<<"-1"<<' ';
else
cout<<l+1<<' ';
}
return 0;
}
下面再看一道二分查找的例题。
题目:
已知一维数组中的n个元素各不相同,但已按升序排列。查找数组中是否存在值为key的数组元素。如果存在,输出相应的下标并输出与key比较的次数,否则输出not found。
输入:
第1行:一个整数n(n<=20)。
第2行:从键盘输入n个有序整数。
第3行:要查找的数key。
输出:
输出对应结果。若不存在则输出not found。
样例输入:
10
6 7 9 10 16 18 20 35 141 150
21
样例输出:
not found
提示:
二分查找
分析:这道题直接套用二分查找的模板,每次记录比较次数。
参考代码:
#include <iostream>
using namespace std;
int main()
{
int a[20];
int key, i, n, m, c = 0;
cin >> n;
for(i=0; i<n; i++)
cin >> a[i];
cin >> key;
int low = 0, high = n-1;
while(low <= high )
{
c++;
m = (low + high)/2;
if ( a[m] > key ) high = m-1;
else if ( a[m] < key ) low = m+1;
else break;
}
if( low > high )
cout << "not found\n";
else
cout << m << " " << c << endl;
return 0;
}
下面再看一道二分的例题
题目背景
出题是一件痛苦的事情!
相同的题目看多了也会有审美疲劳,于是我舍弃了大家所熟悉的 A+B Problem,改用 A-B 了哈哈!
题目描述
给出一串正整数数列以及一个正整数 C,要求计算出所有满足 A−B=C 的数对的个数(不同位置的数字一样的数对算不同的数对)。
输入格式
输入共两行。
第一行,两个正整数 N,C。
第二行,N 个正整数,作为要求处理的那串数。
输出格式
一行,表示该串正整数中包含的满足 A−B=C 的数对的个数。
输入输出样例
输入 #
4 1
1 1 2 3
输出 #
3
说明/提示
对于 75%的数据,1≤N≤2000。
对于 100% 的数据,1≤N≤2×,0≤ai<,1≤C<。
2017/4/29 新添数据两组
原题链接:A-B 数对 - 洛谷
参考代码:
#include<bits/stdc++.h>
using namespace std;
const int N=200010;
long long a[N],n,c,cnt,st;
int main(){
cin>>n>>c;
for(int i=1;i<=n;i++) cin>>a[i];
sort(a+1,a+1+n); //先排序
for(int i=1;i<n;i++) //遍历每一个B
{
int l=i+1,r=n; //寻找A第一次出现的位置,使得A-B=C
while(l<r) //因为是第一次出现,尽量往左,模板1
{
int mid=l+r>>1;
if(a[mid]-a[i]>=c) r=mid; //判断:在目标值的右边,满足,往左来
else l=mid+1;
}
if(a[l]-a[i]==c) st=l; //能找到C就继续
else continue;
l=st-1,r=n; //查找A最后出现的位置
while(l<r) //因为是最后一次出现,尽量往右,模板2
{
int mid=l+r+1>>1;
if(a[mid]<=a[st]) l=mid; //判断:在目标值的左边,满足,往右去
else r=mid-1;
}
cnt+=l-st+1; //最后出现的位置减首次出现的位置就是区间长度,即A的个数
}
cout<<cnt;
return 0;
}
我们再看一道整数二分的例题
题目:
给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。
对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。
如果数组中不存在该元素,则返回 -1 -1
。
输入格式:
第一行包含整数 n 和 q,表示数组长度和询问个数。
第二行包含 n 个整数(均在 1∼10000 范围内),表示完整数组。
接下来 q 行,每行包含一个整数 k,表示一个询问元素。
输出格式:
共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。
如果数组中不存在该元素,则返回 -1 -1
。
数据范围:
1≤n≤100000
1≤q≤10000
1≤k≤10000
输入样例:
6 3
1 2 2 3 3 4
3
4
5
输出样例:
3 4
5 5
-1 -1
分析:
我们要在一个递增序列中找到x的最小下标和最大下标,找到x的最小下标其实就是在满足所有下标>=x且满足值等于x所有下标中找到一个最小者,所以我们的check函数条件是a[mid]>=x。同理, 我们可以找到x的最大下标,check函数满足的条件是a[mid]<=x
参考代码:
#include<iostream>
using namespace std;
const int N=100010;
int q[N];
int n,m;
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++)scanf("%d",&q[i]);
while(m--)
{
int x;
scanf("%d",&x);
int l=0,r=n-1;
while(l<r)
{
int mid=l+r>>1;
if(q[mid]>=x)r=mid;//如果有x,返回第一个x下标。如果没有x,找到比x大的第一个数
else l=mid+1;
}
if(q[l]!=x)cout<<"-1 -1"<<endl;//找不到x的下标
else
{
cout<<l<<' ';
int l=0,r=n-1;
while(l<r)
{
int mid=l+r+1>>1;
if(q[mid]<=x)l=mid;//如果有x,返回最后一个x下标。如果没有x,找到比x小的第一个数
else r=mid-1;
}
cout<<l<<endl;
}
}
return 0;
}
1.我们代码中的l=mid+1能否写成l=mid?
我们在浮点数二分模板中确实是,r=mid,l=mid。但是整数二分存在取整的问题,当l=2,r=3时,mid=(l+r)/2=2,如果取l=mid,那么新的值仍然是l=2,r=3,while会陷入死循环。如果写成l=mid+1,就不会陷入死循环。
2.我们代码中r=mid-1能否写成r=mid?
与上面的情况类似,当l=2,r=3时,mid=(l+r+1)/2=3,如果取r=mid,那么新的值仍然是l=2,r=3,while会陷入死循环,如果让r=mid-1,就不会陷入死循环。
注意:要谨慎使用mid=(l+r)/2,因为‘/’在取整时正负数的结果不一致。
一般的,l和r都为正数,但有时正负区间都会存在。
- 当l+r为正数时,mid=(l+r)/2是向下取整,取的是左中位数;当l+r为负数时,mid=(l+r)/2是向上取整,取的是右中位数,两者相互矛盾。而(l+r)>>1在正数和负数的情况下都是向下取整;l+(r-l)/2中的(r-l)在任何情况下都不可能是负数,相当于和(l+r)>>1一样。
- 当l+r+1为正数时,mid=(l+r+1)/2是向上取整,取的是右中位数;当l+r+1为负数时,mid=(l+r+1)/2是向下取整,取的是左中位数,两者相互矛盾。而(l+r+1)>>1在正数和负数的情况下都是向上取整;l+(r-l+1)/2中的(r-l+1)在任何情况下都不可能是负数,相当于和(l+r+1)>>1一样。
其实我们还可以用STL中的lower_bound()和upper_bound()函数。这里简单介绍一下lower_bound()和upper_bound()函数:
- 查找第一个大于x的元素位置:upper_bound(),如pos=upper_bound(a,a+n,x)-a
- 查找第一个等于或大于x的元素:lower_bound()
- 查找第一个与x相等的元素:lower_bound() 且等于x
- 查找最后一个与x相等的元素:upper_bound()的前一个且等于x
- 查找最后一个等于或者小于x的元素:upper_bound()的前一个
- 查找最后一个小于x的元素:lower_bound()的前一个
- 计算单调序列中x的个数:upper_bound()-lower_bound()
下面看一道浮点数二分的例题
题目:
给定一个浮点数 n,求它的三次方根。
输入格式:
共一行,包含一个浮点数 n。
输出格式:
共一行,包含一个浮点数,表示问题的解。
注意,结果保留 6 位小数。
数据范围:
−10000≤n≤10000
输入样例:
1000.00
输出样例:
10.000000
参考代码:
#include<iostream>
using namespace std;
const double eps=1e-7;
int main()
{
double n;
cin>>n;
double r=10000,l=-10000;
while(r-l>eps)
{
double mid=(l+r)/2;
if(mid*mid*mid<=n)l=mid;
else r=mid;
}
printf("%lf",l);
return 0;
}
是不是学会了?这里有些题可以拿来练手