算法基础1.2.1整数二分

前言

如果第一次接触二分其实很难理解它的含义

我对二分的理解就是找到一个条件,能够保证所有数据对于这个条件要么是True要么是False。二分的作用是查找。

二分本质不是单调性,对于一个满足单调性(也就是有序)的数组,我们一定可以用二分来解决,但是这不代表着非单调的数组就不能使用二分。二分的本质是二段性或者说是边界,需要我们能对这个数组想出一个性质,使得数组左半边满足,右半边不满足,同时他们没有交点(因为这是整数二分),那么我们通过二分就可以寻找左半边和右半边各自的边界

第一篇二分搜索论文是 1946 年发表,然而第一个没有 bug 的二分查找法却是在 1962 年才出现,中间用了 16 年的时间。

整数二分我花了三天时间才消化完(主要中间去玩碧蓝幻想versus了),说明这个还是很难理解的,所以学这个不要急,慢慢分析。最后要多做几道题,更加熟练一些。

注意一下:整数二分的“整数”不是说数据都是整数,而是比如说数据在数组里面,那么由于整除会向下取整,所以中间值的赋值需要分类讨论(看下文就知道了)。而浮点数二分可以理解为是一个连续函数,不存在整除的问题,所以直接用同一个模板就好。所以整数二分比浮点数二分难,学会了这个,浮点数二分就很简单了。

正文

直接先上代码和题目

image-20230228182008030

#include <iostream>

using namespace std;

const int N = 100010;

int n, m;
int q[N];

int main()
{
    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 = l + r >> 1;
            if (q[mid] >= x) r = mid;
            else l = mid + 1;
        }

        if (q[l] != x) cout << "-1 -1" << endl;
        else
        {
            cout << l << ' ';

            int l = 0, r = n - 1;
            while (l < r)
            {
                int mid = l + r + 1 >> 1;
                if (q[mid] <= x) l = mid;
                else r = mid - 1;
            }

            cout << l << endl;
        }
    }

    return 0;
}

了解思路

这道题要求我们查找一个具体的数,找出它的起始位置和终止位置。需要注意的是这是一个有序的升序数组,也就是说如果有多个x,那么他们一定是连着的一串,而且这一串之前的都是小于x的,后面的都是大于x的。

假如我们要查找3,那么我们就可以利用二分,先通过x>=3作为条件二分出来两个部分,再通过x<=3二分出来另外两个部分,结合来看我们就可以得到3所在的那个区间,区间里面都是3(或者数组里根本没有3),如下图

image-20230228193000822

这其实就变成了一个集合的问题。图中第一部分我们的二分点是>=x的开始位置,其实也就是第一个x可能出现的位置(如果这个区间实际上都是>x那么就说明没有x)。第二部分我们的二分点是<=x的结束位置,其实就是最后一次可能出现x的位置。那么我们对两个橙色区间取交集,就是我们要查找的数据所在的位置,如果这是一个区间,那么区间里都是那个数;如果交集为一个元素,说明这个元素存在,而且只存在一个;如果交集为空,那么就说明数组里没有这个数据。

一定要明白,我们是进行了两次二分查询,如果你认为这是一次,那么说明还是对二分理解不够(两次的条件是不同的)。我们每次二分都只找了一个分界点,因为另一个找到了也没用处

抽象分析

上面只是根据题目来进行一个大致的过程了解,下面我们将这个问题抽象出来看

image-20230228195113666

我们通过某种性质将区间分为了两半部分,红色为不满足,绿色为满足。

红色部分

我们先分析红色部分,再次明确我们的目的:找到红色部分的边界点,也就是第一个箭头指向的位置

我们还是从中点位置开始试,我们设变量mid=l+r+1>>1,至于这里为什么要+1我们后面再说。

我们从中点开始入手,通过判断语句来检测这个mid是否满足红色区间的条件(也就是不满足我们设定的性质)

  • 如果满足,说明现在mid在红色区间内,那么我们就要更新我们区间,将多余的删除,也就是将左边界收缩,从l变为mid,更新语句为l=mid(注意更新后的区间是包含mid的,他在红色区间内,同时他也有可能就是那个边界点)
  • 如果不满足,说明现在mid在绿色区间内,我们让右边界收缩,从r变为mid,更新语句为r=mid-1(之所以这里不包含mid,是由于mid在绿色区间内,那么他绝对不可能是我们要找的红色区间边界点,又因为这是在数组中,所以它的上一位是不确定的,所以收缩到上一位mid-1处)

一直循环到区间两边相遇,这里的条件我们一般写i<j不成立,这样看起来更完善一些,其实经过测试,条件为i==j不成立也是可以实现的

那么现在来说明一下为什么mid要+1

我们举一个例子,这个在最后一次循环的时候很常见。假如现在l=r-1,也就是区间只有2个元素,左边界和右边界邻着。如果没有加一,那么这次的mid=l+r>>1=2l+1>>1=l,这是由整除向下取整的性质决定的,如果接下来判断为真,那么l=mid=l,相当于这次没分,这就是一个无限划分的开始,而加一可以让mid=r从而让两边界相遇结束循环。

绿色部分

整体思路同理,只不过这里的mid=r+l>>1不需要加一,这是由于此时的更新语句分别为r=midl=mid+1,不会发生上述问题

对于这道题

为了更加方便理解我们用了两个二分查询,同时也方便理解用俩个的意义以及这两个各自的意义,我画了这个草图

Snipaste_2023-02-28_21-25-16

我们通过两个二分找到了两个位置,那他们中间,也就是黄色区域,就是我们要查询的数

分析代码

    while (m -- )
    {
        int x;
        scanf("%d", &x);

这里m代表我们要查找的个数,通过while来进行循环,每次都代表一个查找的进程,当m为0时,循环结束,也代表我们查完了所有要查的数

int l = 0, r = n - 1;
while (l < r)
{
    int mid = l + r >> 1;
    if (q[mid] >= x) r = mid;
    else l = mid + 1;
}

image-20230228195113666

这里是第一次二分查询,显然,条件是>=x,可以认为这是上图中的绿色部分。通过更新语句我们不难发现,左边会收缩到最后一个不满足条件的位置,而右边即使满足条件,也会继续更新,直至两边界相遇。这说明我们找的是满足条件的最前面的位置,具体的讲就是假如区间中有一串x,显然他们都是满足>=x的,而我们最后找到的是这一串的开头,所以也就意味着我们找到了这个数第一次出现的位置

if (q[l] != x) cout << "-1 -1" << endl;

这句话用来判断这个数组中到底有没有这个要查询的数,如果我们的边界点!=x,那么也就说明他以及他后面的数据其实严格意义上满足的是>x。这就说明这个数组中就没有这个数。当然,这里也可以用q[r],毕竟循环结束的时候他俩相遇,他们指向的是同一个位置

        else
        {
            cout << l << ' ';

            int l = 0, r = n - 1;
            while (l < r)
            {
                int mid = l + r + 1 >> 1;
                if (q[mid] <= x) l = mid;
                else r = mid - 1;
            }

            cout << l << endl;
        }
    }

如果if不成立,说明数组中有这个数,现在我们找到了开始位置,接下来要去找结束位置。我们先将找到的开始位置输出,然后重新初始化左右边界,进行第二个二分查询。这次我们找到了满足<=x的最后一个位置,也就是这个数最后一次出现的位置。

结语

这个模板几乎包含了所有需要二分的问题,其实就是针对两种二分情况写了两个小模板,他们的缩进方式和mid的初始化语句不一样。

通过这两个二分,我们就可以在交集处找到一个数据所在的位置或区间了。

做题时我们就需要通过图形理解,去明白我们现在要去使用哪个模板(两个模板找的东西是不一样的)。网上有这么一种理解方式,男左女右(判断为true时),男是1所以要+1,女是0所以不用。不是我想出来的哈

我们要知道一个观点:二分是一定有解的(只要条件选的正确,就一定可以找到二分后两个区间的边界点)。无解是题目的无解,比如这道题里就会找不到要查找的数,这是题目设置的无解。但是题目的无解不影响正常我们二分找到边界点这个解。所以我们进行二分查询的时候不需要担心有没有可能找不到边界点(也就是二分无解),因为只要你条件给的对,是一定有解的。

整数二分注意两点

  1. 一个mid = (l+r)>>1
    一个mid = (l+r+1)>>1
    加不加1 完全取决于 l = mid 还是r = mid
    l等于mid时必须+1向上取整 不然会陷入l=l的死循环
    r = mid 时候不用加1 因为下一步l = r 直接会退出循环
  2. 这两个模板解决的是 找>=||<=||>||< 某个数的
    最左或最右的位置 但这个数不一定在二分的数组中
    如果在就能准确找到
    如果不在 找到的就是最接近答案的数(你要找大于等于5的第一个数)但
    数组中没有5 那找到的就是6的位置(如果有6的话)
    所以二分是一定有答案的
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值