七月集训(9)二分答案+二分查找

1.LeetCode:1146. 快照数组

原题链接


        实现支持下列接口的「快照数组」- SnapshotArray:

        SnapshotArray(int length) - 初始化一个与指定长度相等的 类数组 的数据结构。初始时,每个元素都等于 0。

        void set(index, val) - 会将指定索引 index 处的元素设置为 val。

        int snap() - 获取该数组的快照,并返回快照的编号 snap_id(快照号是调用 snap() 的总次数减去 1)。

        int get(index, snap_id) - 根据指定的 snap_id 选择快照,并返回该快照指定索引 index 的值。

        示例:

        输入:[“SnapshotArray”,“set”,“snap”,“set”,“get”]

         [[3],[0,5],[],[0,6],[0,0]]

        输出:[null,null,0,null,5]

        提示:

        1 <= length <= 50000

        题目最多进行50000 次set,snap,和 get的调用 。

        0 <= index < length

        0 <= snap_id < 我们调用 snap() 的总次数

        0 <= val <= 1e^9


        本题其实并没有完全说明题意,所谓的快照数组就是每次插入一个数都是一个新的编号,并且插入的时候又需要按照索引来进行插入,所以这里图省事的话可以直接建一个50000的三维数组,第一维为索引,第二维为快照编号,第三维为值。

        插入,初始化都没有什么说的必要,snap返回编号+1即可,get操作就直接在index索引的数组里面二分查找出对应的snap_id即可

class SnapshotArray {
    int snapid;
public:
    vector<vector<int>> arr[50010];
    SnapshotArray(int length) {
        snapid=-1;
        for(int i=0;i<length;++i){
            arr[i].push_back({snapid,0});
        }
    }
    
    void set(int index, int val) {
        arr[index].push_back({snapid,val});
    }
    
    int snap() {
        return ++snapid;
    }
    
    int get(int index, int snap_id) {
        int l=0;
        int r=arr[index].size()-1;
        int ans=0;
        while(l<=r){
            int mid=l+((r-l)>>1);
            if(arr[index][mid][0]<snap_id){
                l=mid+1;
                ans=arr[index][mid][1];
            }else {
                r=mid-1;
            }
        }
        return ans;
    }
};

2.LeetCode:1498. 满足条件的子序列数目

原题链接


        给你一个整数数组 nums 和一个整数 target 。

        请你统计并返回 nums 中能满足其最小元素与最大元素的 和 小于或等于 target 的 非空 子序列的数目。

        由于答案可能很大,请将结果对 109 + 7 取余后返回。

        示例 1:

        输入:nums = [3,5,6,7], target = 9

        输出:4

        示例 2:

        输入:nums = [3,3,6,8], target = 10

        输出:6

        示例 3:

        输入:nums = [2,3,3,4,6,7], target = 12

        输出:61

        提示:

        1 <= nums.length <= 1e5

        1 <= nums[i] <= 1e6

        1 <= target <= 1e6


        其实本题刚开始我感觉跟二分没什么关系只是看到子序列第一时间往dp方面想了,不过思考了好久好久才反应过来一个序列的结果只与当前子序列的最大和最小值有关,也即是只要确定了一个合法序列的最大最小值,中间可以随意的增删数字。

        那么明白了这点本题就简单了,先对数组从小到大排序,然后遍历数组,对于每一个元素去[i,n-1]这个范围内找出使得一个合法序列是以当前元素为最小值,最大值为查找到的元素的最大值,在他们中间的数字每个都有两种选择,选或者不选(包括查找到的这个数字),查找出来之后利用快速幂组合即可。比如[3,5,6,7],target为9这个例子,我们合法的情况只有对3进行查找,找到合法的最大值为6,那么这之间的 5,6都可以不选,得到[3],也可以选5 ,得到[3,5],也可以都选得到[3,5,6],也可以只选6不选5得到[3,6]。

typedef long long ll;
class Solution {
    #define mod 1000000007
    ll power(long long a,long long b,long long c){
        ll ans=1;
        while(b){
            if(b&1) ans=ans*a%c;
            b>>=1;
            a=a*a%c;
        }
        return ans;
    }
public:
    int numSubseq(vector<int>& nums, int target) {
        sort(nums.begin(),nums.end());
        int num=0;
        for(int i=0;i<nums.size();++i){
                int l=i,r=nums.size()-1;
                int ans=-1;
                while(l<=r){
                    int mid=l+((r-l)>>1);
                    if(nums[i]+nums[mid]<=target){
                        ans=mid;
                        l=mid+1;
                    }else r=mid-1;
                }
                if(ans!=-1){
                    num+=power(2,ans-i,mod);
                    num%=mod;
                }
        }
        return num;
    }
};

3.LeetCode:1802. 有界数组中指定下标处的最大值

原题链接


        给你三个正整数 n、
        index 和 maxSum 。你需要构造一个同时满足下述所有条件的数组 nums(下标 从 0 开始 计数):

        nums.length == n

        nums[i] 是 正整数 ,其中 0 <= i < n

        abs(nums[i] - nums[i+1]) <= 1 ,其中 0 <= i < n-1

        nums 中所有元素之和不超过 maxSum

        nums[index] 的值被 最大化

        返回你所构造的数组中的 nums[index] 。

        注意:abs(x) 等于 x 的前提是 x >= 0 ;否则,abs(x) 等于 -x 。

        示例 1:

        输入:n = 4, index = 2, maxSum = 6

        输出:2

        示例 2:

        输入:n = 6, index = 1,
        maxSum = 10

        输出:3

        提示:

        1 <= n <= maxSum <= 1e9

        0 <= index < n


        由于规则2我们会发现数组中元素最小为1,不能为0(这点很重要) ,又由于规则3,abs(nums[i-1]-nums[i])<=1,也就是说可以为0,1。如果使得构造出的数组索引处元素最大,那么我们可以这样做,二分出一个答案,作为索引处的值,然后往左构建一个非严格递增的数列,往右构造一个非严格递减的序列,什么意思呢? 比如我们二分出的结果是5,那么从左到他的数列应该是 1,1,1。。。2,3,4,5。从他开始他右边的序列应该是5,4,3,2,1,1,1。。。。。。1,1,1。

        所以接下来要做的就简单了,首先从-1e9到1e9之间不断二分出一个答案,然后构造一个函数用来检查这个答案的合法性,具体检查的过程就是从他往左构造如上的序列,如果在过程中maxsum小于0了返回false,再往右边构造,同样如果中间总和小于0了返回false。如果检查合法,由于要找最大值,继续向右二分,如果检查失败就向左二分,最后返回答案即可。同时由于二分出来的答案很大,index也很大的时候,构造出来的1会很多很多,这里可以加速,如果遇到了1直接减去剩下的1然后跳出当前构造即可。

class Solution {
    bool check(int n,int index,int mid,int maxSum){
        maxSum-=mid;
        int i=index-1;
        int pre=mid;
        while(i>=0){
            pre=max(1,pre-1);
            if(pre==1){
                maxSum-=(i+1);
                i=-1;
            }
            else {
                maxSum-=pre;
                --i;
            }
            if(maxSum<0){
                return false;
            }
        }
        pre=mid;
        i=index+1;
        while(i<n){
            pre=max(1,pre-1);
            if(pre==1){
                maxSum-=n-i;
                i=n;
            }
            else {
                maxSum-=pre;
                ++i;
            }
            if(maxSum<0){
                return false;
            }
        }
        return true;
    }
public:
    int maxValue(int n, int index, int maxSum) {
        int l=1,r=maxSum;
        int ans=0;
        while(l<=r){
            int mid=((r-l)>>1)+l;
            if(check(n,index,mid,maxSum)){
                l=mid+1;
                ans=mid;
            }else {
                r=mid-1;
            }
        }
        return ans;
    }
};

4.LeetCode:2040. 两个有序数组的第 K 小乘积

原题链接


        给你两个 从小到大排好序 且下标从 0 开始的整数数组 nums1 和 nums2 以及一个整数 k ,请你返回第 k (从 1 开始编号)小的 nums1[i] * nums2[j] 的乘积,其中 0 <= i < nums1.length 且 0 <= j < nums2.length 。

        示例 1:

        输入:nums1 = [2,5], nums2 = [3,4], k = 2

        输出:8

        示例 2:

        输入:nums1 = [-4,-2,0,3], nums2 = [2,4], k = 6

        输出:0

        示例 3:

        输入:nums1 = [-2,-1,0,1,2], nums2 = [-3,-1,2,4,5], k = 3

        输出:-6

        提示:

        1 <= nums1.length, nums2.length <= 5 * 1e4

        -1e5 <= nums1[i], nums2[j] <= 1e5

        1 <= k <= nums1.length * nums2.length

        nums1 和 nums2 都是从小到大排好序的。


        可以说之前的三道题目都是为本题做铺垫的,对于寻找k大(小)数,我们一般是先考虑二分答案的,本题恰好完美符合。

        二分答案的基本过程就是,在合适的范围内进行二分出一个答案,然后用某种方式检查在题目要求下的合法性,合法了就更新答案,最后返回这个答案即可。

        本题也不例外,先二分出一个答案作为乘积,然后利用check函数进行检查合法性,这里先看两个数组,由于数组均是递增排序,固定一个数组,如果数组第一个元素是大于等于0的情况下两个数组乘积的排序就是a[1]*b[1],a[1]*b[2],a[1]*b[3]。。。。。。a[n]*b[n-1],a[n]*b[n-1],这种情况下如何得到我们答案对应的位置呢?遍历a数组的所有元素,然后二分出b数组相乘的乘积,得到这个元素下的答案应在的位置,如果答案比所有乘积大那么对应的就应该是该组乘积的最后一个位置,对答案进行累加,如果枚举的答案比该组乘积都小不进行累加。最后累加出来的结果就是当前二分出来的答案在两个数组乘积序列中的位置。(这样按乘积序列来解释只是为了方便理解,其实当遍历完所有的乘积之后我们的答案相当于跟所有乘积都比较了一遍,那么他的在乘积中的位置自然就是比这些乘积大的个数了。也就是说这里并没有按照乘积的序列来遍历,只是正数的时候可以这样理解)。


        那么如果我们固定的数组中出现了负数该怎么办呢? 本组内的乘积逆置即可,也就是本组在二分的时候向在正数的时候反方向进行即可。

        可能有人会对二分出来坐标之后序列怎么累加有疑问,如果是固定的数组当前元素大于等于0的情况下,答案累加 i+1即可,从下标为0到下标为i有i+1个数。如果是负数就 n-i即可(i到下标n-1有 n-1-i+1=n-i个数)

typedef long long ll;
class Solution {
    ll get_small(ll val,vector<int>& nums1, vector<int>& nums2){
        ll ret=0;
        for(int i=0;i<nums1.size();++i){
            ll v=nums1[i];
            int l=0,r=nums2.size()-1;
            int ans=-1;
            if(v>=0){
                while(l<=r){
                    int mid=l+((r-l)>>1);
                    if(v*nums2[mid]<=val){
                        ans=mid;
                        l=mid+1;
                    }
                    else r=mid-1;
                }
                if(ans!=-1){
                    ret+=ans+1;
                }
            }else {
                while(l<=r){
                    int mid=l+((r-l)>>1);
                    if(v*nums2[mid]<=val){
                        ans=mid;
                        r=mid-1;
                    }else l=mid+1;
                }
                if(ans!=-1){
                    ret+=nums2.size()-ans;
                }
            }
        }
        return ret;
    }
public:
    ll kthSmallestProduct(vector<int>& nums1, vector<int>& nums2, long long k) {
        ll l=100000;
        l*=l,l=-l;
        ll r=100000;
        r*=r;
        ll ans=-1;
        while(l<=r){
            ll mid=l+((r-l)>>1);
            if(get_small(mid,nums1,nums2)>=k){
                ans=mid;
                r=mid-1;
            }else l=mid+1;
        }
        return ans;
    }   
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值