LeetCode二分法的高级应用系列

最近刷到挺多巧用二分的题目,经常在hard题中出现。
先把我最近刷到的先记录一下,后期这个博客会不断地更新~
这类型的题下面会总结什么情况可以考虑二分法。
另外二分法看起来简单,细节是魔鬼,用的时候考虑清楚是需要找出target就好,还是要找左边界或者右边界。

总结

这种题目一般拿到手不大容易往二分上去想。在遇到跟一个未知数k相关的题目,而且关于k的函数能得到一个单调不减/增的序列,我们可以往二分上多考虑一下。

题解

1539. 第 k 个缺失的正整数

这题直接遍历寻找肯定是可以的,但是这样的时间复杂度为 O ( k + n ) O(k+n) O(k+n),最坏情况n个数组没有缺失,还需要再遍历k个。
能不能用二分呢?能不能写出关于k(缺失数)的函数?
这里考虑一下与arr平行的数组lost,对 i i i位置来讲lost[i] = arr[i]-i-1
lost是一个有重复数的单调非减序列
二分的任务就是要找到lost[i]=k的左边界(但是lost 中不一定包含k),或者说找到第一个大于等于k的左边界。

现在有了
lost[left-1]=n, lost[left]=m
n<k, m>=k
而我们要求的数应该是从arr[left-1]再数k-n个
即arr[left-1]+k-lost[left-1] 
= arr[left-1] + k - (arr[left-1] - (left-1) - 1)
= k+left
class Solution {
public:
    int findKthPositive(vector<int>& arr, int k) {
        if(arr[0] > k) return k;
        int left = 0, right = arr.size()-1;
        while(left <= right){
            int mid = left + (right - left) / 2;
            int lost = arr[mid] - mid - 1;
            if(lost == k)
                right = mid - 1;
            else if(lost < k)
                left = mid + 1;
            else
                right = mid - 1;
        }
        return k + left;
    }
};

793. 阶乘函数后 K 个零

先简单分析一下本题, x x x阶乘后面多少0,其实只跟 [ 1 , x ] [1,x] [1,x]中所有数拥有的5的因子的个数有关,因为只有 2 ∗ 5 2*5 25会构成10,而2的因子的个数肯定是超过5的,所以这里只需要考虑5。
这里就有了 f ( x ) f(x) f(x)的计算方法

f(x) = (x/5) + f(x/5)
f(0) = 0
f(1) = 0
f(2) = 0
f(3) = 0
f(4) = 0
f(5) = 1
f(6) = 1
...
f(10) = 2
...
f(24) = 4
f(25) = 6

f ( x ) f(x) f(x)的规律,每隔5会跳一下,但是具体跳多少需要计算一下。从 f ( 24 ) = 4 f(24)=4 f(24)=4跳到 f ( 5 ) = 6 f(5)=6 f(5)=6,从 f ( 74 ) = 16 f(74)=16 f(74)=16跳到 f ( 75 ) = 18 f(75)=18 f(75)=18,题目中的意思是计算 f ( x ) = K f(x)=K f(x)=K的个数,可以发现只有0或者5两种情况。而 f ( x ) f(x) f(x)是一个单调不减序列,那我们的问题就变成了能否在这个单调不减序列中找到 K K K
二分法!

class Solution {
public:
    int preimageSizeFZF(int K) {
        long long left = K, right = 5LL*K;
        while(left <= right){
            long long mid = left + (right - left)/2;
            int cnt = factorZero(mid);
            if(cnt == K)
                return 5;
            else if(cnt < K)
                left = mid + 1;
            else
                right = mid - 1;
        }
        return 0;
    }
    int factorZero(long long x){
        int cnt = 0;
        while(x){
            x /= 5;
            cnt += x;
        }
        return cnt;
    }
};

287. 寻找重复数

这题的常规思路,一是交换排序,时间复杂度为 O ( n ) O(n) O(n),但是会改变原数组(pass),其次哈希,时间复杂度 O ( n ) O(n) O(n),但是空间复杂度 O ( n ) O(n) O(n)(pass),另外还有暴力,空间复杂度 O ( 1 ) O(1) O(1),但是时间复杂度 O ( n 2 ) O(n^2) O(n2)(pass)。
一开始不容易想到二分,貌似找不到那个单调数组
重新读一下题,n+1长度的数组,所有元素均为1-n的数,只有1个元素重复。
那我们可以考虑这个单调数组设为,对位置i而言,小于等于i的个数cnt。假设我们要找的目标值为target,那么当i<target的时候有cnt[i] <= i,而当i>=target,就有cnt[i] > i;单调数组是不是就出现了!!
这个时候考虑二分法找到第一个cnt[i] >i的位置即可

(1)先说明一下为什么这是单调不减的序列
对于位置i+1,数组中只有两种情况,
一种是有i+1,一种没有i+1,
也就是说cnt[i+1]至少是大于等于cnt[i]的
(2)其次为什么target位置有cnt[target] > target
总共有n+1个数字,且只有一个重复的
cnt[n]处肯定是n+1
而从target+1-n,加上的数字只能比n-(target+1)+1要小
cnt[target]处肯定是大于target,且是第一个满足这个等式
若是target之前有位置pos满足cnt[pos]>pos
那只能说明target之前就有重复元素,矛盾
class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int n = nums.size()-1;
        int left = 1, right = n;
        while(left < right){
            int mid = left + (right - left) / 2;
            int cnt = 0;
            for(int i = 0; i <= n; ++ i)
                if(nums[i] <= mid)
                    ++ cnt;
            if(cnt <= mid)
                left = mid+1;
            else
                right = mid;
        }
        return left;
    }
};

1011. 在 D 天内送达包裹的能力

假定一个数组CanFinsh[x],表示在运载能力为x的情况下,能否在D天内将包裹送达完成。
不难想到CanFinsh数组应该是前面一串为False,后面一串为True,而我们要求的其实就是第一个为True对应的下标~显然是二分法

class Solution {
public:
    int shipWithinDays(vector<int>& weights, int D) {
        int left = 0, right = 1;
        for(int weight : weights){
            left = max(left, weight);
            right += weight;
        }
        while(left < right){
            int mid = left + (right - left) / 2;
            if(canfinish(weights, D, mid))
                right = mid;
            else
                left = mid+1;
        }
        return left;
    }
    bool canfinish(vector<int>weights, int D, int carry){
        int time = 1;
        int pos = 0, cur = 0;
        while(pos < weights.size()){
            if(cur + weights[pos] > carry){
                ++ time;
                cur = 0;
            }
            cur += weights[pos++];
        }
        return time <= D;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值