二分多分概念和技巧

二分/多分概念和技巧

1.概念

二分/多分是通过两个或者多个变量,将大的问题依次转成小的问题求解,降低时间复杂度。

本质上是分治法的思想,不同点是分治可以将问题分为多个小问题,而二分/多分是将问题转成一个小问题。

如何正确的写对二分需要注意某些细节。

2.二分适用情况

  • 双指针的适用问题,双指针本质上也是二分思想。
  • 有序序列,或者部分有序序列上的查找。
  • 三分求单峰或者单谷函数的极值点。
  • 将答案转成判定
  • 排除大问题的某一部分,将大问题转成小问题,再小问题上继续。

3.有序域上的二分实现

这里的有序域指的是答案的范围,而不是问题的范围,有的问题可能不是有序的,但是答案的查找范围可以看作有序的。
二分查找本质上就是找一个数,这个数满足某个函数,把这个函数叫做valid();有时候满足valid()函数的元素有多个,因此,单独对原数组使用valid()函数,我们可以找到一个集合。我们可能需要找到在原数组中排在最左或者最右的的元素。

所以二分查找实现时,需要注意2点

  • valid()函数设计。
  • 找最左还是最右的元素。

所以在实现二分时,可以将原问题转换成找到所有满足某个条件的元素(valid()设计)的最右或者最左的值
重要:通过描述可以发现,[l-r]这个区间上有一个点满足x使得
[l-x]上的点都满足或者不满足valid()函数(根据找最左还是最右边),[x-r]同理。

因此根据上述条件,有2种实现方式。

找满足valid(),最左的元素,使用[ )。

l = 0;
r = n; //r也可以取n-1
while(l<r){
	int mid = (l+r) >> 1; //也可以写成l+((r-l)>>1)
    if(valid()) r = mid;
    else l = mid+1;
}

找满足valid(),最右的元素,使用( ]。

l = -1; //l也可以取0
r = n-1;
while(l<r){
	int mid = (l+r+1) >> 1; //注意这里和上面的[)不一样。也可以写成r-((l+r)>>1)
	if(valid()) l = mid;
	else r = mid - 1;
}

在求mid时,使用移位操作,而不是除法(l+r)/2,是因为右移操作会向下取整,而除法向0取整。因此,右移不仅可以用于正数,还可以用于负数。

对于可能出现溢出的情况,则可以使用:l + (r-l)>>1 代替(l+r)>>1; 使用r-(r-l)>>1代替(l+r+1)>>1

上述的代码都只能用于整数域上的二分查找,也即原数组元素时整数,对于实数域上的二分查找,可以做下述处理:

//将while循环条件的l<r改为l+eps<r;其中eps表示要查找的实数的精确度。
while(l+eps < r){
	//......
}
例子:

1.在包含重复元素的数组[1,2,2,3,3,3,4,5,5,5,6,7,8,8,9] 找到排在最左边的5的下标,此题应该返回7。

//1:此题,我们可以将原题转化成,找到满足>=5的最左边的数。
//2:因此,valid()函数,就是找到所有>=5的数,[5,6,7,8,9]都符合。
//3:找最左边,使用[ )找到最左边的数。

//代码:
nums = [1,2,2,3,3,3,4,5,5,5,6,7,8,8,9];
Boolean valid(int a){
   return a>=5; 
}
int main(){
    int l = 0;
    int r = nums.length; //因为右边是开区间,所以初始值为nums.length。
    while(l<r){
        int mid = (l+r)>>1;
        if(valid(nums[mid])) r = mid;
        else l = mid +1;
    }
    return nums[l] == 5?l:-1;
}

2.改变一下题目,在包含重复元素的数组[1,2,2,3,3,3,4,5,5,5,6,7,8,8,9] 找到排在最右边的5的下标,此题应该返回9。

//1:此题和上题类似,我们可以将原题转成,找到满足<=5的最右边的数。
//2:因此,valid()函数,就是找到所有<=5的数,[1,2,3,4,5]都符合。
//3:找最右边使用( ]。

//代码:
nums = [1,2,2,3,3,3,4,5,5,5,6,7,8,8,9];
Boolean valid(int a){
   return a<=5; 
}
int main(){
    int l = -1; //因为左边是开区间,所以初始值为-1。
    int r = nums.length-1; 
    while(l<r){
        int mid = (l+r+1)>>1;
        if(valid(nums[mid])) l = mid;
        else r = mid - 1;
    }
    return nums[l] == 5?l:-1;
}

3.再改变一下,在不包含重复元素的数组[1,2,3,4,5,6,7,8,9]种找到5。

这题就有多种写法了:

  • 将原题转成找到满足<=5的最右边的数,同第2题代码。

  • 将原题转成找到满足>=5的最左边的数,同第1题代码。

  • 将原题转成找到满足==5的最左边的数。

    nums = [1,2,3,4,5,6,7,8,9];
    Boolean valid(int a){
       return a==5; 
    }
    int main(){
        int l = 0;
        int r = nums.length; 
        while(l<r){
            int mid = (l+r)>>1;
            if(valid(nums[mid]) && nums[mid] < 5) r = mid;
            else l = mid +1;
        }
        return nums[l] == 5?l:-1;
    }
    

可以看出,我们可以扩展valid()函数,将nums[mid] <5也考虑进去,这样就转成了前一种实现方式了;所以这种实现方式实际没意义,我们需要统一,因此在我们转换原问题的时候不能过于保守,仍然可以扩展valid()函数,只要不影响最终结果就行;下面的实现方式也一样。

  • 将原题转成找到满足==5的最右边的数。

4.leetcode 69.

在这里插入图片描述

此题,乍一看一个数的平方跟是实数,但是,我们看答案都是返回整数,因此也是整数域上的二分。

//1.可以将本题转成,在0-x范围内,找到一个数a,a*a<=x的最右边的a,也即符合条件的最大的a。
//2.所以valid()就是找到所有a*a<=x的a。
//因为找到最右边的a,因此使用(]。

//代码
Boolean valid(int a,int x){
   return (long)a*a<=x; 
}
public int mySqrt(int x) {
        int l = 0;
        int r = x;
        while(l<r){
            int mid = r-((r-l)>>1);//防止溢出 
            if(valid(mid,x)) l = mid;
            else r = mid -1;
        }
        return l;
}

4.非有序域上的二分实现

对于这种不是再有序域上的查找,一般满足最后一种使用情况的二分。这种题可能只是因为可以通过排除一定不存在解的序列,将大问题转成小问题。

例子:

1.给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数 。

输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值