二分猜答案杂记

67 阅读3分钟

基础问题

  • 找到这个数
  • 找到第一个/最后一个 <= target 的数字/所在位置【<= <= <= <= > > >】
  • 找到第一个/最后一个 >= target 的数字/所在位置【< < < >= >= >=】

什么情况可以二分

题目 满足 0 / 1 性质(关于条件单调) ,或者函数满足单调性 都可以进行二分。

二分模板 来自 acwing

bool check(int x) {/* ... */} // 检查x是否满足某种性质

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;    // check()判断mid是否满足性质
        else l = mid + 1;
    }
    return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

实战

剑指 Offer 53 - I. 在排序数组中查找数字 I

    public int search(int[] nums, int target) {
        if(nums.length == 0) return 0;
            int l = 0;
            int r = nums.length - 1;
            // 找到第一个 >= target 的数字 根据 01 性质来分析,找到·第一个·大于等于target ,既然是第一个 那么就要 逼近 小端点 r = mid; mid 可能是答案。 因为有等于号了。 mid 是否加一问题 : l 不甘心自己 不是 l = mid + 1, 缺少 · +1 · 就补在 mid 计算上。
            while (l < r) {
                int mid = l + r >> 1;
                if (check(nums,mid,target)) r = mid;
                else l = mid + 1; 
            }
            int lAns = l;
            if (nums[lAns] != target) return 0;
            l = 0 ;
            r = nums.length - 1;
            while (l < r) {
                int mid = l + r + 1>> 1;
                if (check1(nums,mid,target)) l = mid;
                else r = mid - 1;
            }
            int rAns = l;
            
            return rAns - lAns + 1;
    }


    boolean check(int[] nums , int mid , int target) {
        return nums[mid] >= target;
    }

     boolean check1(int[] nums , int mid , int target) {
        return nums[mid] <= target;
    }
}

x 的平方根

class Solution {
    public int mySqrt(int x) {
        int l = 0 ;
        int r = x;
        // 2 * 2 = 4 < 8 ;3 * 3 = 9 > 8
        // 小于等于 x 的最后一个数字
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (check(mid , x)) l = mid;
            else r = mid - 1; 
        }
        return (int)l;
    }
    boolean check(long mid , int x) {
        return mid <= x / mid;
    }
}

剑指 Offer 11. 旋转数组的最小数字

输入: numbers = [3,4,5,1,2]
输出: 1

分段 3,4,5 | 1, 2 : n[r] < 左 n[r] >= 右 可能重复的情况 : 3 , 3 | 2 ,3 n[r] <= 左 、 n[r] >= 右 == 怎么确定? 很显然 不能确定左右了 只有 n[r] < 左n[r] > 右 满足 0 | 1, 那么 我们n[r] 处于 == 这个状态时,直接抛弃

class Solution {
    public int minArray(int[] numbers) {
        if (numbers.length == 0) return -1;
        int l = 0,r = numbers.length - 1;
        while ( l < r) {
            int mid = l + r >> 1;
            if (numbers[mid] < numbers[r]) r = mid;
            else if (numbers[mid] > numbers[r]) l = mid + 1;
            else r --;
        }
        return numbers[l];
    }
}

寻找峰值

分析 : 摘自 leetcod 热评

为什么二分查找大的那一半一定会有峰值呢?(即nums[mid]<nums[mid+1]时,mid+1~N一定存在峰值) 我的理解是,首先已知 nums[mid+1]>nums[mid],那么mid+2只有两种可能,一个是大于mid+1,一个是小于mid+1,小于mid+1的情况,那么mid+1就是峰值,大于mid+1的情况,继续向右推,如果一直到数组的末尾都是大于的,那么可以肯定最后一个元素是峰值,因为nums[nums.length]=负无穷

image.png

class Solution {
    public int findPeakElement(int[] nums) {
            int l = 0 , r = nums.length - 1;
            while (l < r) {
                int mid = l + r >> 1;
                //  峰值一定在最左边
                if (nums[mid] >= nums[mid + 1]) r = mid ;
                else l = mid + 1; 
            }
            return l;
    }
}

二分答案

  • 对于最大值/最小值问题/给个方案 是否可行 ? 0 / 1 性质 二段性
  • 给他一个解、判断它是否合法。二分+判定实现猜答案。

410. 分割数组的最大值

思路参考 : liweiwei1419

class Solution {
    public int splitArray(int[] nums, int k) {
        int l = 0 , r = 0; // l 每个分一组 r 全部放一起
        for (int i : nums){
            r += i;
            l = Math.max(l,i);
        }
        // 答案 一定在 l -- 总和之间
        while (l < r) {
            int mid = l + r >> 1;
            if (check(nums,mid,k)) r = mid;
            else l = mid + 1;
        }
        return r;
    }


    boolean check(int[] nums , int maxSum ,int m) {
        // 刚开始是有一组的
        int cnt = 1;
        int sum = 0;
        for (int i = 0 ; i < nums.length ; i ++) {
            if (sum + nums[i] <= maxSum) {
                sum += nums[i];
            }else {
                cnt ++;
                sum = nums[i];
            }
        }
        // 需要的分组 如果小于 m 就可行
        return cnt <= m;
    }

    /*
    将数组分为 m 个非空连续子数组 使得各自和的最大值最小
    [7,2,5,10,8] m = 2; 所有情况如下。
    [7 | 2 5 10 8] lsum = 7;  rsum = 25; max : 25;
    [7 2 | 5 10 8] lsum = 9;  rsum = 23; max : 23;
    [7 2 5 | 10 8] lsum = 14; rsum = 18; max : 18;
    [7 2 5 10 | 8] lsum = 24; rsum = 8;  max : 24
    18 23 24 25 是合法的解空间  18 之前全是不合法的 
    */
}

875. 爱吃香蕉的珂珂

类似于上一题的思路

class Solution {
    public int minEatingSpeed(int[] piles, int h) {
        int l = 1 , r = 0;
        for (int i : piles) r = Math.max(i,r);
        while (l < r) {
            System.out.println ( " l " + l  + " r " + r);
            int mid = l + (r - l >> 1);
            if (check(piles,mid,h)) { 
                 r = mid;
            }
            else l = mid + 1;
        }
        return l;
    }

    boolean check(int[] piles , int speed , int h) {
        int needH = 0;
        for (int i = 0; i < piles.length; i ++) {
                // 这堆香蕉需要 几小时吃完?
                needH += piles[i] % speed == 0 ? piles[i] / speed : piles[i] / speed + 1;
                // System.out.println("吃第" + i + "堆香蕉" + "花费" + needH + "小时");
        }
        // System.out.println("每次吃" + speed + "根香蕉" + "吃全部香蕉花费" + needH + "小时");
        return needH <= h;
    }
}

/** 警卫在 8 小时回来
[3,6,7,11] h = 8; 每个小时 可以吃 k 根 我每次吃 1 根 达到最慢 
速度最快 我一小时吃一堆的最大值。
*/

写不下去了 明天写