算法:二分搜索

LeetCode题目

力扣难度
1011. 在 D 天内送达包裹的能力🟠
410. 分割数组的最大值🔴
875. 爱吃香蕉的珂珂🟠
剑指 Offer II 073. 狒狒吃香蕉🟠

解题分析

这类题目可能的解空间在一个范围内,不停二分遍历可能解。找到一个可行解后还要继续找,直到“最小值”的可行解。类比到二分搜索算法中就是满足条件的最左值。

以”410. 分割数组的最大值“问题为例,可能解空间在“最大的单个元素”和“所有元素和”之间。然后二分查找可行解空间,一个可行解是分组数等于或者小于目标组数。“小于目标组数”成立的原因是可以继续拆分,拆分后子数组元素和一定满足条件,显然这不一定是最优解。找到可行解后如果继续优化条件不成立,那么上一次的可行解就是最优解。

从可行解中找最左值“二分搜索”中边界条件很容易出错。比如下面找最左值的方式:

        while (left < right) {
            int mid = left + (right - left) / 2;

            int splits = split(nums, mid);
            if (splits > m) {
                // 如果分割数太多,说明「子数组各自的和的最大值」太小,此时需要将「子数组各自的和的最大值」调大
                // 下一轮搜索的区间是 [mid + 1, right]
                left = mid + 1;
            } else {
                // 下一轮搜索的区间是上一轮的反面区间 [left, mid]
                right = mid;
            }
        }

个人更喜欢下面的方式,容易理解,不易出错,缺点是代码有点冗余。

while(left <= right) {
	int mid = left + (right - left) / 2;
	int groups = split(nums, mid);

	if(groups > k) { // 分组和太小,分组数大于预定值
		left = mid + 1;
	} else if(groups < k) { // 分组和太大,分组数小于预定值
		right = mid - 1;
	} else { // 可能分组数 < k 走不到这里,因为 split 是贪心算法。但实际上可以从 < k 拆分到 k 组
		if(mid == max || split(nums, mid -1) > k) {
			return mid;
		} else {
			right = mid - 1;
		}
	}
}
package org.stone.study.algo.ex202403;  
  
/**  
 * [410. 分割数组的最大值](https://leetcode.cn/problems/split-array-largest-sum/)
 * * 给定一个非负整数数组 nums 和一个整数 k ,你需要将这个数组分成 k 个非空的连续子数组。  
 * 设计一个算法使得这 k 个子数组各自和的最大值最小。  
 */  
public class SplitArrayWithMinSum {  
  
    public static void main(String[] args) {  
        int[] nums = new int[] {7,2,5,10,8};  
        int k = 2;  
        // ans: 18  
        System.out.println("ans:" + new SplitArrayWithMinSum().splitArray(nums, k));  
    }  
  
    /**  
     * 给定一个非负整数数组 nums 和一个整数 k ,把这个数组分成 k 个非空的连续子数组。  
     * 找到一种分组方式,使得返回k 个子数组各自和的最大值最小  
     * @param nums  
     * @param k  
     * @return  
     */  
    public int splitArray(int[] nums, int k) {  
        int max = 0, sum = 0;  
        for(int num : nums) {  
            max = Math.max(max, num);  
            sum += num;  
        }  
  
        int left = max, right = sum;  
        while(left <= right) {  
            int mid = left + (right - left) / 2;  
  
            int groups = split(nums, mid);  
            //System.out.println("left:" + left + ", right:" + right + ", mid:" + mid + ", groups:" + groups);  
            if(groups > k) { // 分组和太小,分组数大于预定值  
                left = mid + 1;  
            } else if(groups < k) { // 分组和太大,分组数小于预定值  
                right = mid - 1;  
            } else { // 可能分组数 < k 走不到这里,因为 split 是贪心算法。但实际上可以从 < k 拆分到 k 组  
                if(mid == max || split(nums, mid -1) != k) {  
                    return mid;  
                } else {  
                    right = mid - 1;  
                }  
            }  
        }  
  
        return left;  
    }  
  
    /**  
     * 拆分数组,每组和最大为maxSum。返回可以拆分的组数  
     * 直接贪心算法实现  
     */  
    private int split(int[] nums, int maxSum) {  
        int curSum = 0;  
        int splitNum = 1;  
  
        for(int num : nums) {  
            if(curSum + num > maxSum) {  
                ++splitNum;  
                curSum = 0;  
            }  
  
            curSum += num;  
        }  
  
        return splitNum;  
    }  
}
package org.stone.study.algo.ex202403;  
  
import java.util.Arrays;  
  
/**  
 * [875. 爱吃香蕉的珂珂](https://leetcode.cn/problems/koko-eating-bananas/)
 * 珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 h 小时后回来。  
 * 珂珂可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。  
 * 珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。  
 * 返回她可以在 h 小时内吃掉所有香蕉的最小速度 k(k 为整数)。  
 */  
public class MinEatingSpeed {  
    /**  
     * 解法 1:二分查找最左值边界处理好理解一点。  
     * @param piles  
     * @param h  
     * @return  
     */  
    public int minEatingSpeed(int[] piles, int h) {  
        int max = Arrays.stream(piles).max().getAsInt();  
  
        int left = 1, right = max;  
        while(left <= right) {  
            int mid = left + (right - left) / 2;  
  
            long spent = spentHours(piles, mid);  
  
            if(spent > h) {  
                left = mid + 1;  
            } else if(spent < h){  
                right = mid - 1;  
            } else {  
                //System.out.println("spent:" + spent + ", left:" + left + ", right:" + right);  
                if(mid == 1 || spentHours(piles, mid - 1) > h) {  
                    return mid;  
                } else {  
                    right = mid - 1;  
                }  
            }  
        }  
        //System.out.println("left:" + left + ", right:" + right);  
        return left;  
    }  
  
    /**  
     * 以速度 speed 依次消耗 piles 数组。不足去整(往大的取)  
     */  
    private long spentHours(int[] piles, int speed) {  
        long spent = 0;  
        for(int pile : piles) {  
            spent += (pile + speed - 1) / speed;  
        }  
  
        return spent;  
    }  
  
  
    /**  
     * 解法 2,更简洁,但是二分查找最左值边界理解上有点困难  
     * @param piles  
     * @param h  
     * @return  
     */  
    public int minEatingSpeed2(int[] piles, int h) {  
        int max = 0;  
        for(int p : piles) {  
            max = Math.max(max, p);  
        }  
  
        int l = 1, r = max, ans = max;  
        while(l <= r) {  
            int total = 0;  
            int m = l + (r -l) / 2;  
            for(int p : piles) {  
                total += (p-1)/m + 1; //向上取整  
            }  
  
            if(total > h) {  
                l = m + 1;  
            } else {  
                ans = m;  
                r = m - 1;  
            }  
        }  
  
        return ans;  
    }  
}

通用的二分查找值,最左值,最右值方法请参考1


package org.stone.study.algo.search;  
  
/**  
 * @Author: shidonghua  
 * @Description:  
 * @Date: 3/8/21 09:07  
 * @Version: 1.0  
 */public class BinarySearch {  
  
    /**  
     * 二分搜索,并找最左边的元素  
     *  
     * @param a  
     * @param n  
     * @param value  
     * @return  
     */  
    public int bsearch_LeftMost(int[] a, int n, int value) {  
        int low = 0;  
        int high = n - 1;  
        while (low <= high) {  
            int mid = low + ((high - low) >> 1);  
            if (a[mid] > value) {  
                high = mid - 1;  
            } else if (a[mid] < value) {  
                low = mid + 1;  
            } else {  
                if ((mid == 0) || (a[mid - 1] != value)) return mid; // 最左逼近  
                else high = mid - 1;  
            }  
        }  
        return -1;  
    }  
  
    /**  
     * 二分搜索最右边的元素  
     *  
     * @param a  
     * @param n  
     * @param value  
     * @return  
     */  
    public int bsearch_RightMost(int[] a, int n, int value) {  
        int low = 0;  
        int high = n - 1;  
        while (low <= high) {  
            int mid = low + ((high - low) >> 1);  
            if (a[mid] > value) {  
                high = mid - 1;  
            } else if (a[mid] < value) {  
                low = mid + 1;  
            } else {  
                if ((mid == n - 1) || (a[mid + 1] != value)) return mid; // 往右边逼近  
                else low = mid + 1;  
            }  
        }  
        return -1;  
    }  
  
    /**  
     * 第一个大于等于元素  
     *  
     * @param a  
     * @param n  
     * @param value  
     * @return  
     */  
    public int bsearch_GE(int[] a, int n, int value) {  
        int low = 0;  
        int high = n - 1;  
        while (low <= high) {  
            int mid = low + ((high - low) >> 1);  
            if (a[mid] >= value) {  
                if ((mid == 0) || (a[mid - 1] < value)) return mid;  
                else high = mid - 1;  
            } else {  
                low = mid + 1;  
            }  
        }  
        return -1;  
    }  
  
    /**  
     * 最后一个小于等于  
     *  
     * @param a  
     * @param n  
     * @param value  
     * @return  
     */  
    public int bsearch_LE(int[] a, int n, int value) {  
        int low = 0;  
        int high = n - 1;  
        while (low <= high) {  
            int mid = low + ((high - low) >> 1);  
            if (a[mid] > value) {  
                high = mid - 1;  
            } else {  
                if ((mid == n - 1) || (a[mid + 1] > value)) return mid;  
                else low = mid + 1;  
            }  
        }  
        return -1;  
    }  
}

总结

二分搜索需要数组满足单调性,简单等值搜索的场景比较少,实际场景更多是最值搜索,映射到算法中就是满足条件的最左或者最右值。这时处理算法的边界条件很重要,也是最容易出错的地方。

算法题一般不会直接让写二分搜索,需要把具体问题映射到“二分搜索”的解题思路上。比如解空间在一个单调整数数组上,找最小解等。


  1. github ↩︎

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值