二分算法(二)-- 二分答案入门

系列合集:
二分算法(一) 新手可看,图解二分入门
二分算法(二) 二分答案入门

二分答案思想

和二分查找相似,二分查找在一个有序范围内查找某个值并返回,二分答案就是在有序范围内查找答案。需要满足以下条件(答案写成 ans):

  • 条件一:ans 在一个范围内,没范围就没法二分查找了
  • 条件二:ans 范围里面的元素具有二分性
  • 条件三:每个可能的 ans 是可以验证是否有效的

假设我们能知道问题的答案一定在某个区间 v 内,且区间具有二分性质,就可以使用二分思想了。至于什么是二分性,我不知道怎么解释,我简单理解成可以将数组划分为两种状态。

练习一

问题:287. 寻找重复数

分析

分析:以 [1,3,4,2,2] 这个用例来分析, n == 4,有 n + 1 个数字。

  • 条件一:很显然答案是有范围的一定在 [1, n] 这个范围内,这个用例就是 [1, 4],这里满足了上述条件一。
  • 条件二:这个范围是否具有二分性呢,通过分析可以发现,如果统计 [1,3,4,2,2] 中小于等于每个元素的元素个数,那么最后答案是有单调性的,比如:统计 <= 1 的元素,有 1 个,<= 2 的元素有3 个,<= 3 的元素有 4 个,<= 4 的元素有 5 个。将 <= x 的元素个数计为 cnt,那么 就有两种状态(分别用蓝色和红色表示),一种是 x <= cnt(x 在 ans 的左侧,即 x < ans),另一种是 x > cnt (x 为 ans 或者 ans 的右侧,即 x >= ans),具有二分性。
  • 条件三:给定一个 x ,根据上述分析,通过统计 <= x 的元素个数,就可以得出 x 在 答案的左侧还是右侧。
  • 我们已经将答案数组划分为蓝色(< ans)和红色(>= ans)了,最后 ans 就是红色的第一个,假设 L 的左侧元素都 < ans,R 自身和右侧元素都 >= ans,那么就可以得出变化关系。

实现

半开半闭区间写法
class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        // 假设 l 左侧均 < ans,r 及其右侧均 >= ans
        int n = nums.size(), l = 0, r = n, mid;
        
        // x 这个数是否 >= ans
        function<bool(int)> check = [&](int x) -> bool {
            int cnt = 0;
            // 统计 <= x 的元素数量
            for(int i : nums) cnt += i <= x;

            return cnt > x;
        };

        while(l < r) {
            mid = l + (r - l) / 2;
            if(check(mid)) r = mid;
            else l = mid + 1;
        }

		// 返回 l 或者 r 都可以
        return l;
    }
};
双开区间写法

也可以假设L及其左侧都 < ans,R 及其右侧都 >= ans,不清楚这种写法的可以看 二分算法(一) – 新手可看,图解二分入门 ,图解如下所示:
开区间写法

  • 二分循环条件需要改变,如上图所示,当 L 和 R 相邻的时候就结束了,条件就改为 while(l + 1 < r),根据假设,L 和 R 的初始值为 -1 和 n。
  • L 和 R 变化规则变了
    • 当 check(mid) 时,R = mid
    • 当 !check(mid) 时,L = mid
    • 上述变化维护了 L 和 R 的假设
  • 如上图所示,结果返回 R
class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        // 假设 l 及其左侧均 < ans,r 及其右侧均 >= ans
        int n = nums.size(), l = -1, r = n, mid;
        
        // x 这个数是否 >= ans
        function<bool(int)> check = [&](int x) -> bool {
            int cnt = 0;
            // 统计 <= x 的元素数量
            for(int i : nums) cnt += i <= x;

            return cnt > x;
        };

        while(l + 1 < r) {
            mid = l + (r - l) / 2;
            if(check(mid)) r = mid;
            else l = mid;
        }

		// 返回 r 
        return r;
    }
};

练习二

410. 分割数组的最大值

分析

  • 条件一:答案有范围,ans 一定在 [sum / m, sum] 里(sum 表示 nums 的求和)。
  • 条件三:能否判断 nums 是否可以分为 m 个子数组,且每一个子数组的和 <= x,我们可以写一个 check(x) 来判断
  • 条件二:可以发现 ans 是刚好满足 check(ans) 的,若 x < ans,那么 check(x) == false;若 x >= ans,那么 check(x) == true。就可以把答案数组 [sum / m, sum] 分为两份,一份 < ans,另一份 >= ans。

实现

半开半闭写法

假设 L 左侧元素都 < ans,R 及其右侧元素都 >= ans,初始值为 sum / m 和 sum,变化关系为:

  • 当 check(mid) 时,r = mid
  • 当 !check(mid) 时,l = mid + 1
class Solution {
public:
    int splitArray(vector<int>& nums, int m) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        int l = sum / m, r = sum, mid;

        // 二分答案, l 左侧代表不可以,r 及其右侧代表可以,l 和 r 相邻代表所有的数字都判断过了
        while(l < r) {
            mid = l + (r - l) / 2;
            if(check(nums, mid, m)) r = mid;
            else l = mid + 1;
        }

        return r;
    }

    // 分成 k 段, 判断是否每段都可能 <= x
    bool check(vector<int>& nums, int x, int k) {
        int tmp = 0, cnt = 0, i, n = nums.size();
        for(i = 0; i < n && k - 1 >= 0; ) {
            if(tmp + nums[i] > x) {
                k--;
                tmp = 0;
            } else {
                tmp += nums[i++];
            }
        }

        return k - 1 >= 0;
    }
};
双开区间写法

假设 L 及其左侧元素都 < ans,R 及其右侧元素都 >= ans,初始值为 sum / m - 1 和 sum,循环条件是 while(l+1 < r),变化关系为:

  • 当 check(mid) 时,r = mid
  • 当 !check(mid) 时,l = mid
class Solution {
public:
    int splitArray(vector<int>& nums, int m) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        int l = sum / m - 1, r = sum, mid;

        // 二分答案, l 及其左侧代表不可以,r 及其右侧代表可以,l 和 r 相邻代表所有的数字都判断过了
        while(l + 1 < r) {
            mid = l + (r - l) / 2;
            if(check(nums, mid, m)) r = mid;
            else l = mid;
        }

        return r;
    }

    // 分成 k 段, 判断是否每段都可能 <= x
    bool check(vector<int>& nums, int x, int k) {
        int tmp = 0, cnt = 0, i, n = nums.size();
        for(i = 0; i < n && k - 1 >= 0; ) {
            if(tmp + nums[i] > x) {
                k--;
                tmp = 0;
            } else {
                tmp += nums[i++];
            }
        }

        return k - 1 >= 0;
    }
};
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值