二分类问题套路

二分查找套路

好久没水文了,今天来水一篇。。。
最近练成了二分查找的套路,来吹一波

套路总纲

  1. 知道此题适不适合二分
    1)目标在一个连续有界区间内(即使是0~MAX,这个基本都能达到);
    2)单调问题:问题会告诉你需要满足某个条件,比如最多多少天完成(设为d),而让你求最少的目标(设为x),那么d的取值f(x)必须是一个单调函数,才能使用二分去除一半的解空间;
  2. 找到此单调函数f(x), 我一般使用count(nums, m)来表示这个函数;
  3. 二分模板:二分支二分,一定不满足条件的一边缩小(比如f(x)>d);满足的可以设为新的临界值(lo/hi=mid);
  4. 退出循环后 验证lo的合法性

道理很绕,变成题目就好理解了

题目

lc69 sqrt()

求解sqrt(n),要求下取整。

按照套路求解:

  1. 排除0和1的话,可以确定解在(1, n/2)之间;f(x)=x^2, 为单调递增函数(x>1)
  2. f(x)=x^2
    剩下的看代码
	class Solution {
    public int mySqrt(int x) {
        /**
            0. 【0,x】的连续整数
            1. 单调性:f(x)=sqrt(x), 单调递增;
            2. =mid的是:left
         */
                 // 特殊值判断
        if (x == 0) {
            return 0;
        }
        if (x == 1) {
            return 1;
        }

         int lo = 0, hi = x / 2;
         while (lo < hi) {
             int mid = (hi - lo + 1) / 2 + lo;
             if (mid > x / mid) {
                 hi = mid - 1;
             } else {
                 lo = mid;
             }
         }

         return lo;
    }
}

这题要注意,求解f(x)应该转为除法,否则越界,别问我怎么知道的。。。。

lc1011

这是一道标准的二分套路题,也是一道入门题,大家可以用来练手,熟悉套路

在这里插入图片描述

  1. 可以猜到:如果一天只运送一个货物,那么最起码要max(nums)那么大的运载能力,否则不可能完成;如果想一天完成,那么需要运载能力是sum(nums), 因此目标x有界,x∈[max(nums), sum(nums)];尽管不知道f(x)如何求解,但可以肯定:x若增大,需要的天数f(x)必然降低,反之升高,满足单调递减性,适合使用二分;
  2. 求解f(x)很简单,只需要在cur+nums[i]>m时另起一天来送货即可;
  3. 代码模板,不细说,看代码
	class Solution {
    public int shipWithinDays(int[] weights, int days) {
        int lo = 0, hi = 0;
        for (int n:weights) { //lo为最大重量,因为必须要保证能够载得动;hi为累计,可以一天装完
            lo = Math.max(lo, n);
            hi += n;
        }
    //x:最低装载能力,f(x)需要运输天数;单调递减函数
        while (lo < hi) {
            int m = (hi - lo) / 2 + lo;
            int cnt = count(weights, m);
            if (cnt > days) {
                //装载能力不足
                lo = m + 1;
            } else {
                //可以适当降低装载量达到最优
                hi = m;
            }
        }
        return lo;
    }

    private int count(int[] nums, int m) {
        int cnt = 1, //至少要装一天
        cur = 0;

        for (int i = 0; i < nums.length; i++) {
            if (nums[i] + cur > m) {
                cnt++;
                cur = 0;
            }
            cur += nums[i];
        }

        return cnt;
    }
}

lc410 410. 分割数组的最大值

在这里插入图片描述虽然标为困难,但是和上面一题如出一辙
难点在于理解题意,可真狗绕口的。。。
这么简单写完一道困难题,想必你也很有成就感吧~

  1. 连续有界性:最好的情况下,也是将所有的数组元素一个下标一份,此时最大值就是max(nums),因此下界为max(nums), 最差情况下,只化为一份,最大值为sum(max),满足;可以料到,若增大数组最大值,那么数组切分的数量必然随之降低,反之也成立;
  2. f(x), 求的是切分数量;
  3. 若f(x)>题目给的条件,必然要舍弃该区间;否则应当在此范围内继续优化,直到lo==hi
	class Solution {
        public int splitArray(int[] nums, int m) {
            /** 2.二分
            二分的场景:在有范围的整数中查找一个数;
            二分查找的依据:问题具有单调性
            即:若要查找元素x,定义的查找依据是f(x),那么一定满足f(a)>f(x), a>x...
    
            1、查找分割数组最大值的最小值,设为x;
            2. 函数:f(x)为x可以确定的分割数量
    
            任意位置a,若f(a)<f(x),证明分割数量太少,比最佳方案还少,必然导致a>x;
            若f(a)=f(x),也无法确定a==x;(分割数正确,分割方式不唯一)
    
            若f(a)=f(x)+1,且f(a-1)=f(x) (a-1是临界点)
            则:x=a-1
    
            f(x)单调递减:
            f(x) < m, x > b;
                 == m, x∈[a, b]
                 > m, x < a
            所求:b
             */
            int max = 0, sum = 0;
             for (int num: nums) {
                 max = Math.max(max, num);
                 sum += num;
             }
             //left分割为len段(f(left)=len),right化为1段(f(right)=1)
             int left = max, right = sum;
            while (left < right) {
                //当前设定的a(片段最大值)
                int mid = (right - left) / 2 + left;
                int splits = split(nums, mid);//等价于f(x)
                if (splits < m) {
                    //片段不足,分割度不够,证明最大值太小,缩小a
                    right = mid - 1;
                } else {
                    left = mid;
                }
            }
            return left;
        }
        //设定一个片段最大值为max,看能划分为几个片段
        public int split(int[] nums, int max) {
            int splits = 1, cur = 0;
            for (int n: nums) {
                if (cur + n > max) {
                    //不能加上这个数,此片段到此结束
                    splits++;
                    cur = 0;
                }
                cur += n;
            }
            return splits;
        }
    }

LCP 12. 小张刷题计划

在这里插入图片描述

这题是这几题中最难的一道,关于在f(x),即count()函数的写法,这里也可以猜到,二分问题最难的两点在于:
1)知道这题要用二分;
2)count()函数的编写。

不再题解,读者自己考虑

	class Solution {
        public int minTime(int[] time, int m) {
            /**
                寻找刷题最大耗时x,可以得到f(x):刷题天数,单调递减函数
             */
             int lo = 0, //全都靠求助
             hi = 0; //一天之内靠自己完成所有题
             for (int t: time) {
                 hi += t;
             }
             while (lo < hi) {
                 int mid = (hi - lo) / 2 + lo;
                 int cnt = count(time, mid);
                 if (cnt > m) {
                     //刷題耗時不足
                     lo = mid + 1;
                 } else {
                     hi = mid;
              }
             }
    
             System.out.println(count(time, 3));
             return lo;
        }
        //t:当前规定每天最大耗时
        public int count(int[] nums, int t) {
            //为了最小化时间,记录每轮刷题最大耗时,利用求助减掉他
            int max = 0, cnt = 1, cur = 0;
            boolean used = false; //当天有咩有用掉求助
            for (int i = 0; i < nums.length; i++) {
                int n = nums[i];
                if (n + cur > t) {
                    if (!used) {
                        //用掉求助
                        used = true;
                        max = Math.max(max, n);
                        cur -= max;
                    } else {
                        //只能第二天再刷
                        cnt++;
                        cur = 0;
                        used = false;
                        max = 0;
                    }
                    i--;
                    continue;
                }
                max = Math.max(max, n);
                cur += n;
            }
            return cnt;
        }
    }

我这代码比较糙,大伙肯定有更好的写法。。。

总结

二分问题最难的两点在:
1)知道这题要用二分;
2)count()函数的编写。

第一点需要在反复刷题中提升反应度,比如我以前很难想到使用并查集去解题,在练习了一些之后,现在看到有些题目我就会觉得“这不是明摆着用xx嘛”,practise make prefect;

第二点就依赖算法能力了,但是基本上也是一个套路,可能多加一些条件而已。

附上其他的习题,自行练习:
lc875 :爱吃香蕉的珂珂;
lc1482 :制作 m 束花所需的最少天数
lc1552: 两球之间的磁力

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值