二分查找和二分答案

什么是二分查找?它与二分答案有区别吗?其实两者还是有区别的。

  • 二分查找也称折半查找,它是查找顺序存储结构中的某一个元素。是一种高效的查找元素的方式。
  • 二分答案是通过答案满足的某一性质,不断的减小答案所在的区间,找到一个最优满足题意的答案的过程。

我们可以对比一下二分查找的模板和二分答案的模板:

二分查找:

//我们要在(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 个不超过 gif.latex?10%5E9 的单调不减的(就是后面的数字不小于前面的数字)非负整数 gif.latex?a_1,gif.latex?a_2​,…,gif.latex?a_n​,然后进行 m 次询问。对于每次询问,给出一个整数 q,要求输出这个数字在序列中第一次出现的编号,如果没有找到的话输出 −1 。

输入格式

第一行 2 个整数 n 和 m,表示数字个数和询问次数。

第二行 n 个整数,表示这些待查询的数字。

第三行 m 个整数,表示询问这些数字的编号,从 开始编号。

输出格式

输出一行,个整数,以空格隔开,表示答案。

输入输出样例

输入 #1

11 3
1 3 3 3 5 7 9 11 13 15 15
1 3 6

输出 #1

1 2 -1 

说明/提示

数据保证,1≤n≤gif.latex?10%5E6,0≤gif.latex?a_i​,qgif.latex?10%5E9,1≤mgif.latex?10%5E5

本题输入输出量较大,请使用较快的 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×gif.latex?10%5E50≤ai​<gif.latex?2%5E3%5E0,1≤C<gif.latex?2%5E3%5E0

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()函数:

  1. 查找第一个大于x的元素位置:upper_bound(),如pos=upper_bound(a,a+n,x)-a
  2. 查找第一个等于或大于x的元素:lower_bound()
  3. 查找第一个与x相等的元素:lower_bound() 且等于x
  4. 查找最后一个与x相等的元素:upper_bound()的前一个且等于x
  5. 查找最后一个等于或者小于x的元素:upper_bound()的前一个
  6. 查找最后一个小于x的元素:lower_bound()的前一个
  7. 计算单调序列中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;
}

是不是学会了?这里有些题可以拿来练手

【算法1-6】二分查找与二分答案 - 题单 - 洛谷

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无限酸奶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值