二分查找/二分答案:最长上升子序列(LIS)、最大的最小值、最小的最大值

二分法用途

二分搜索的核心在于,存在一个分界点,小于分界点的不合法,大于分界点的不如他优。那么理所当然他可以解决一系列最优化问题,比如最值。将求值问题转化为判定性问题。

常见题目:最大的最小值、最小的最大值、求满足条件的最长子序列长度……

STL函数实现普通二分

vector<int> a={……};
upper_bound(a.begin(),a.end(),value);//首个大于
lower_bound(a.begin(),a.end(),value);//首个大于等于
int index=lower_bound(nums.begin(),nums.end(),target)-nums.begin();

返回指向范围 [first, last) 中首个大于/大于等于 value 的元素的迭代器,或若找不到这种元素则返回 last 。采用二分实现(logn时间复杂度),所以调用前必须保证容器有序
upper_bound(a.begin(),a.end(),value,cmp);
比较器cmp可以自己定义,因此可以用这两个函数实现二分查找。

模板

[l, r]区间中前面的部分可以看作0(check(mid)为false),后面的部分可以看作1(check(mid)为true),寻找第一个1的下标。

int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
    return l;
}
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;
}
int bsearch_3(int l, int r) {//搜索[l, r],找第一个1
    int ans = -1;
    while (l <= r) {
        int mid = l + r >> 1;
        if (check(mid)) {
            ans = mid;
            r = mid - 1;
        } else
            l = mid + 1;
    }
    return ans;
}


例题

【题目一】
在这里插入图片描述
【注意点】二分查找的目的是为了找出可以插入的位置,这个位置的范围是[0,nums.size()],不需要顾及数组是否越界,因为由于while(left<right),当left==right时,while已退出,不需要对此时的left\right位置元素进行访问、进一步处理,即可得到答案。

//STL写法
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        return lower_bound(nums.begin(),nums.end(),target)-nums.begin();
    }
};
//传统二分
class Solution {
public:
    int searchInsert(vector<int> &nums, int target) {
        int size = nums.size();
        if (size == 0) {
            return 0;
        }

        int left = 0;
        int right = size;

        while (left < right) {
            int mid = left + (right - left) / 2;
            // 小于 target 的元素一定不是解
            if (nums[mid] < target) {
                // 下一轮搜索区间是 [mid + 1, right]
                left = mid + 1;
            } else if (nums[mid] == target) {
                // 根据本题特殊性,看到等于 target 的元素,返回任意一个即可
                return mid;
            } else {
                right = mid;
            }
        }
        return left;
    }
};


【题目二】
在这里插入图片描述
【思路】题目可以变为找出第一个大于等于target的下标 和 最后一个小于等于target的下标,否则直接用三个条件判断会出现越界情况。

【完整代码】

//STL写法
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int left=lower_bound(nums.begin(),nums.end(),target)-nums.begin();
        if(left==nums.size() || nums[left]!=target)  
            return {-1,-1};
        int right=upper_bound(nums.begin(),nums.end(),target)-nums.begin()-1;
        return {left,right};
    }
};
//传统二分
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if(nums.size()==0)  return vector<int>(2,-1);
        vector<int> res(2);
        res[0]=helper_left(nums,target);
        res[1]=helper_right(nums,target);
        return res;
    }
    int helper_left(vector<int>& nums, int target){
        int left=0,right=nums.size()-1;
        while(left<right){//找到第一个大于等于target的下标
            int mid=left+(right-left)/2;
            //小于target的元素一定不是解
            if(nums[mid]<target) //[mid+1,right]
                left=mid+1;
            else if(nums[mid]>=target)//[left,mid]
                right=mid;
        }
        if(nums[left]!=target)
            return -1;
        return left;
    }
    int helper_right(vector<int>& nums, int target){
        int left=0,right=nums.size()-1;
        while(left<right){//找最后一个小于等于target的下标
            int mid=left+(right-left+1)/2;
            //大于target的元素一定不是解
            if(nums[mid]>target)//[left,mid-1]
                right=mid-1;
            else if(nums[mid]<=target)//[mid,right]
            	left=mid;
        }
        if(nums[left]!=target)
            return -1;
        return right;
    }
};


【题目三】
在这里插入图片描述【思路】先猜一个数(有效范围 [left, right]里的中间数 mid),然后统计原始数组中小于等于这个中间数的元素的个数 cnt,如果 cnt 严格大于 mid,(注意我加了着重号的部分“小于等于”、“严格大于”)依然根据抽屉原理,重复元素就应该在区间 [left, mid] 里。

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int left=1,right=nums.size()-1;
        while(left<right){
            int mid=left+(right-left)/2,count=0;
            for(int i:nums){
                if(i<=mid)
                    count++;
            }
            if(count<=mid)
                left=mid+1;
            else
                right=mid;
        }
        return left;
    }
};

时间复杂度O(nlgn)符合题目要求。

【题目四】
最长上升子序列
在这里插入图片描述
【思路】考虑维护一个数组 tail,其中每个元素 tails[k] 的值代表 长度为 k 的所有上升子序列的结尾的最小值,并且这个数组为升序数组,可以用二分法搜索第一个元素大于等于当前位置元素的tail数组元素下标。时间复杂度O(nlgn)。

class Solution {
   public:
    int lengthOfLIS(vector<int>& nums) {
        vector<int> tail;
        for (int num : nums) {
            auto r = lower_bound(tail.begin(), tail.end(), num);
            if (r == tail.end())
                tail.push_back(num);
            else
                *r = num;
        }
        return tail.size();
    }
};

类似的题目:【最大上升子序列和】
dp[i]表示s[0…i]数组中包含s[i]的最大 升序子序列和

int dp[1005]{};
dp[0]=s[0];
for(int i =1 ;i<n;i++){
    dp[i]=s[i];
    for(int j=0;j<i;j++){
        if(s[j]<=s[i]){
            dp[i]=max(dp[i],dp[j]+s[i]);
        }
    }
}

【最大的最小值】
在这里插入图片描述【思路】二分找到可能的答案,判断该答案成不成立,成立则把范围缩小到后半部分,否则缩小到前半部分。

class Solution {
public:
    int maxDistance(vector<int>& position, int m) {
        sort(position.begin(),position.end());
        int n=position.size();
        int left=0,right=(position[n-1]-position[0])/(m-1)+1;
        while(left<right){
            int mid=left+right+1 >>1;
            if(check(position,mid,m))
                left=mid;
            else
                right=mid-1;
        }
        return left;
    }
    bool check(vector<int>& position,int dis,int m){
        int n=position.size();
        int pre=position[0],cnt=1;
        for(int i=1;i<n;i++){
            if(position[i]-pre<dis)   continue;
            cnt++;
            pre=position[i];
            if(cnt>=m)  return true;
        }
        return false;
    }
};

参考:
https://leetcode-cn.com/problems/search-insert-position/solution/te-bie-hao-yong-de-er-fen-cha-fa-fa-mo-ban-python-/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值