为什么我会二分但是不知道怎么用?

文章适合已经大概了解或者学习了二分搜索的套路,但是在实际题目上却很难想到用二分的同学。

下面会从两道题教大家如何在一道题里用上快速知道用二分。重点就放在应用上啦。

  • 力扣 -704. 二分查找(简单)
  • 蓝桥杯 JavaB_2021省赛 --杨辉三角形(第八题)

力扣 -704. 二分查找(简单)

在这里插入图片描述

文章重点不讲解边界的判断,while是用<还是<=。这里对二分搜索还不了解的同学可以去看文章末尾推荐的算法书,里面讲解的很透彻。

int binarySearch(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1; // 注意
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target)
                return mid;
            else if (nums[mid] < target)
                left = mid + 1; // 注意
            else if (nums[mid] > target)
                right = mid - 1; // 注意
        }
        return -1;
    }

当然,题目一般不会以上面的形式出现。一般会让你求数字“第一次出现” “第二次出现” “最后一次出现”
求第一次出现:

    int binary_search_left(int []arr ,int target ){
        int left =0,right = arr.length-1;
        while (left<=right){//注意我这里用的等于
            int mid = left+(right-left)/2;
            if(arr[mid]>=target){
                right=mid-1;
            }else {
                left=mid+1;
            }
        }
        //搜索哪个边界就检查哪个索引是否越界
        if(left>=arr.length||arr[left]!=target)return -1;
        return left;
    }

最后一次出现:

   int binary_search_right(int []arr ,int target ){
        int left =0,right = arr.length-1;
        while (left<=right){//注意我这里用的等于
            int mid = left+(right-left)/2;
            if(arr[mid]>target){
                right=mid-1;
            }else {
                left=mid+1;
            }
        }
        //搜索哪个边界就检查哪个索引是否越界
        if(right<0||arr[right]!=target)return -1;
        return right;
    }

所以到这里可能你都明白,但是就是做题的时候不知道应该用到二分搜索的技巧。接下来,我们来刨析一下题目。

思路刨析

题目有几个要点:(数据重新假设一下)

  • num={1,2,3,3,5,7}(递增)
  • 条件 num[i]==target
  • 求 i

我们将上面的要点画成坐标图,如果说求左边界,也就是要得到索引i=2
在这里插入图片描述
这里 num[i]就相当于是i 的一个函数,我们把它写成 fun(i),这就是初中的一一映射,没什么高大上的。
在这里插入图片描述
上面说的题目要点就变成了:

  • 函数—>fun(i)(递增/减)
  • 条件----> fun(i)==target
  • 求值-----> i

在这里插入图片描述
这里我们的做题思想就出来了,题目给一个fun(i) (可能是一个递增递减的数组、数学表达式等等),然后让你找到这个数在哪个位置。这个i就是题目要你求的,每次做题就想想,关于i的关系是不是递增、递减,并且需要遍历,找到一个值(条件)下的i值。

  • 题目求的i
  • i 的关系是否递增递减
  • 是不是找到关系上的一个条件

所以遇到想不出来的时候,不如问问自己这三个问题,如果都满足的话,大概率就可以用二分搜索了。

实战:蓝桥杯 JavaB_2021省赛 --杨辉三角形(第八题)

在这里插入图片描述
题目考察一般也没有那么直接,所以需要一些其他知识的储备

  • 知识储备:
    杨辉三角形其实是组合问题值值映射表,为什么这么说?
    规定: Cij 为 从i个里选j个的组合数目
    在这里插入图片描述
    上述公式是组合的一个公式,换成杨辉三角的描述就是,第 n 行 第 m 列 + 第 n 行 第 m-1 列 = 第 n+1 行 第 m

  • 思考
    其次,如果知道一个数在第几行 第几列 我们就能知道题目问的,这个数在是第几个数。
    比较简单,大家可以想一想,我们设所在行为 i, 列为 m
    所求 = (1 + i) * i/ 2 + m + 1

(1 + i) * i/ 2===> 1+2+3+…+n 的等差数列求和
m+1 ===>这个数在该行第几个
在这里插入图片描述

  • 开始做题
    如果这题能用二分做的话,我该怎么思考?
    当然是按照那三个要素来问自己:
  1. 求 i
    这里要求的是该数第几个出现,根据上面的分析,只要知道 行号 、列号,就能知道,所以问题转化 ,求 行号 n ,列号 m。
  2. 找函数 fun (i)
    将上面的 i 替换成我们要求的,不久变成了 fun(n,m) ,想想这个函数是什么? 输入行号和列号,我们求出来的不就是该行该列所对应的值么?比如 fun (4,1)=1,fun(5,1)=2。
  3. 看fun函数是否递增递减
    这题如果使用二分来做,稍微难想一些也就是在找递增的关系上。
    单独拿出一列来看,不难发现每一列都是递增的,这里就符合我们想要找的条件。但是要注意的是,题目求第一次出现。我们发现,从左到右找的时候,黄色的部分都会在前一列出现,所以不能存在我们所要找的范围内。
    在这里插入图片描述
    先把框架写出来:
    (N是我们要求的数字)
while (l <= r) {
                mid = l + (r - l) / 2;
                long midNum= fun(mid, m);
                if (midNum== N) {
                    return ...;
                } else if (num > N) {
                    r = mid - 1;
                } else {
                    l = mid + 1;
                }
            }

这里fun函数需要两个参数,行数和列数,行数我们通过二分获得,那列数呢?
我们做过一列递增的情况,这种多列递增的情况怎么办?那当然也是遍历列数。那应该从左到右遍历还是从右到左?那还是再看看上面的图,第一个数出现的位置,哪一列在前,我们是不是就应该先遍历那一列。比如说数字 6 ,第一列第6行出现,第二列第四行出现,如果从左到右遍历,要找的数在第一列的数永远会出现,所以应该右往左遍历。也就是在我们二分里嵌套一个for循环,遍历每一列。
在这里插入图片描述
右边应该从哪一行开始遍历?
在这里插入图片描述
我们的数是不是都靠组合来完成,所以只要组合数的值略大于10`(9) ,如果算阶乘的话,多少的阶乘能大于 上面的数值?不想算的话可以直接想一下,1010就会有两个0 ,101112…*19最少也有 9 个 0 ,所以直接可以从19行开始,但事实上只需要16行就足够。

for (int m = 16; m >= 0; m--) {
            long l = .., r =.., mid;
            while (l <= r) {
                mid = l + (r - l) / 2;
                   long midNum= fun(mid, m);
                if (midNum== N) {
                    return (1 + mid ) * mid / 2 + m + 1;//前文说过啦
                } else if (num > N) {
                    r = mid - 1;
                } else {
                    l = mid + 1;
                }
            }
        }

接着看看左和右区间,也就是一列的第一行和最后一行怎么找,第一行好说,找一下规律,每次第一个有用的数字是 在 2*m 行,那右边呢? 最大是不是我们要找的那个数字。

long l = 2 * i, r = Math.max(N, l), mid;

我们只需要写一下fun(int n ,int m)函数,前面说了,这是一个组合数函数:

   static long fun(long n, long m) {
        long res = 1;
        //利用了最朴素的公式n!/m!(n-m)!
        for (long i = n, j = 1; j <= m; i--, j++) {
            res = res * i / j;
            if (res > n) return res;
        }
        return res;
    }

到此,已经题也已经完成,完整代码如下。

    static long fun(int N) {
        for (int m = 16; m >= 0; m--) {
            long l = 2 * m, r = Math.max(N, l), mid;
            while (l <= r) {
                mid = l + (r - l) / 2;
                long midNum = fun(mid, m);
                if (midNum == N) {
                    return (1 + mid) * mid / 2 + m + 1;
                } else if (midNum > N) {
                    r = mid - 1;
                } else {
                    l = mid + 1;
                }
            }
        }
        return 0;
    }

    static long fun(long n, long m) {
        long res = 1;
        for (long i = n, j = 1; j <= m; i--, j++) {
            res = res * i / j;
            if (res > n) return res;
        }
        return res;
    }

回归本质的话,也就三句话,其他的都是一些附加的条件。
我把上面三条顺序改一改,会更加符合做题思路:

  • 求值-----> i (题目要求的)
  • 函数—>fun(i)(递增/减) (要求的是否存在一个递增递减的关系)
  • 条件----> fun(i)==target (找到的条件)

于是,当你能画出下面这副图的时候,大概率就能使用二分了!

在这里插入图片描述

参考:

《labuladong的算法秘籍V1.3》

https://blog.csdn.net/zjsru_Beginner/article/details/121875334
http://lx.lanqiao.cn/problem.page?gpid=T2912

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

L-->R

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

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

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

打赏作者

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

抵扣说明:

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

余额充值