【每日一题】1498. 满足条件的子序列数目

1498. 满足条件的子序列数目 - 力扣(LeetCode)

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

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

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

示例 1:

输入:nums = [3,5,6,7], target = 9
输出:4
解释:有 4 个子序列满足该条件。
[3] -> 最小元素 + 最大元素 <= target (3 + 3 <= 9)
[3,5] -> (3 + 5 <= 9)
[3,5,6] -> (3 + 6 <= 9)
[3,6] -> (3 + 6 <= 9)

示例 2:

输入:nums = [3,3,6,8], target = 10
输出:6
解释:有 6 个子序列满足该条件。(nums 中可以有重复数字)
[3] , [3] , [3,3], [3,6] , [3,6] , [3,3,6]

示例 3:

输入:nums = [2,3,3,4,6,7], target = 12
输出:61
解释:共有 63 个非空子序列,其中 2 个不满足条件([6,7], [7])
有效序列总数为(63 - 2 = 61)

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 106
  • 1 <= target <= 106
class Solution {
    int mod = (int)1e9 + 7;
    public int numSubseq(int[] nums, int target) {
        Arrays.sort(nums);
        int len = nums.length;
        int low = 0;
        int high = len - 1;
        long sum = 0;
        int prel = high;
        int preh = 0;
        int flag = 0;
        int[] pow = new int[len+1];
        pow[0] = 1;
        for (int i = 1; i <= len; i++) {
            pow[i] = pow[i - 1] * 2 % mod;
        }
        while(prel!=low||preh!=high) {
            int left = low;
            int right = high;
            prel = low;
            preh = high;
            while(left < right) {
                int mid = (left+right) / 2;
                if(nums[prel]+nums[mid] > target) right = mid;
                else left = mid+1;
            }//这里出来之后,left是满足的条件的后一项。
            if(nums[prel]+nums[left] > target ) right = left - 1; //right这时就是目标下标。
            int fps;
            if(right < prel) {
                break;
            }
            else fps = right;
            high = fps;
            left = prel;
            while(left < right) {
                int mid = (left+right) / 2;
                if(nums[fps]+nums[mid] > target) right = mid;
                else  left = mid+1;
            }//这里出来后,left是第一次区间内不满足条件的后一项
            low = left;
            if(nums[fps]+nums[left] > target)  left -= 1; 
            int sps;
            sps = fps-left;
            left += 1;
            fps=fps-prel+1;      
            if(prel!=low||preh!=high||flag==0)
            {
                long tmp = pow[fps]-pow[sps];
                if(pow[fps]-pow[sps] < 0) tmp = pow[fps]-pow[sps]+mod; 
                 sum = (sum+tmp)%mod;
                // System.out.println("fps:"+fps);
                // System.out.println("sps:"+sps);
                // System.out.println("每次tmp1结果:"+tmp1);
                // //System.out.println("每次tmp2结果:"+tmp2);
                // System.out.println("每次sum结果:"+sum);
                // System.out.println("------------------");
            }
            flag++;
        } 
        return (int)sum;
    }
}

         每日一题,今天是中等题。虽然是中等题,但是使用到了多种知识,难度估计能比肩困难题了。

        读题。首先要了解子序列的概念!子序列的定义:子序列就是在原来序列中找出一部分组成的序列(子序列不一定连续)。也就是说,子序列对顺序没有要求,本质是求组合数。而对于序列的排列求法,有公式:2的个数次方,减一是不包含空集,不减一是包含空集。所以,求子序列的个数实际上可以转化为求集合个数,由集合个数再去处理。而题目要求的只有集合内最大数和最小数的和小于target即可。所以,我们可以将数组排序,接下来就是找个数的问题,看一眼数量,就知道不可能使用O(n)的方法处理了。所以自然就想到二分

        抛去循环,我们先来看看一次的过程。由于已经排完序了,所以要找最大值和最小值的和小于target,实际上的最小值就是下标为0的数值了,所以实际上是不用改变的。要找的只有右边的最大值的地方。所以我们可以写一个二分。二分的写法有很多,但是大家要注意处理好边界的问题。博主这里使用的是我最常用的写left在所求值后一个的位置。(这种方法要处理的边界一般在于两侧。)至于后面用于循环的prel暂且可以先当成0(下标)。所以,出第一个二分循环之后的left,实际上会是目标值的前一个,但有可能left在len-1的位置,这个位置就需要边界处理。有可能是最后一个不符合,也可能都符合,所以需要多处理一个判断。这里使用fps(first position)来存储第一个循环的位置,这个位置代表了,从当前最小值开始所能组成的符合要求的最多元素的集合。即【0,left】是目前符合要求的。

        不一定这个集合里所有的数都可以,比如【0,5,8,12,16】taget是12,实际上对于0到12的集合是可以处理的,但5到12的集合是不可以的,所以第二个循环就用来处理实际循环内不符合要求的返回,也就是【5,12】这个位置,例子中12这个位置会在第一次的fps的位置就找到,第二次找到的位置也需要存储。博主这里采用的是存储个数,所以第一个fps不需要处理。而第二个sps【second position】就需要使用fps-left的位置。那么这时候符合要求的集合个数实际上就知道了,是pow(2,fps)-pow(2,sps)。因为两边都会计算空集,所以相减完就可以相互抵消,不需要做多余的处理。那第一次循环的处理就完了,接下来是多次的循环。

        在首次循环中不符合条件的集合中也有可能出现符合条件的集合,如【0,5,6,8,12,16】 target为12。在这个集合中,第一次找到的仍然是【0,12】的集合,但实际上【5,6】的集合也是符合条件的,也可以找到相应的子序列。所以一次显然是不够的。那有循环就需要循环终止的条件,用什么来做循环终止的条件呢?就是两次找到的集合边界都一样的时候。这时候就说明,这个集合里不存在符合条件的子集了。所以,需要prel(prelow)和preh(prehigh)来记录一下边界。而且需要一个flag来记录循环了。这涉及到了对最终数据的处理。如果循环的边界和前一次相同就不需要在继续循环了。但一开始的边界是会相同的,所以一开始需要使用flag来让第一次的结果可以计入sum。而后续flag就不需要了。判断边界是否相同即可。不同则可以计入sum。相同就不需要计入。这里还需要对fps的位置做一次判断,如果fps的位置<0,那么说明这个集合里没有子集是符合条件的。(在这之前会对fps的位置(实际上是right)做一个判断)

        处理完循环。就到了处理最终数据的处理了。这会涉及到大数处理和前缀和的知识。

由于数值过大,基本上确定会超过int的计数范围,所以需要开long,而且在计算过程中就需要对其进行处理。使用一个数组来存储各个pow(2,n)的数值,并且在数值过程中对其进行mod运算(这个会对我们后续处理有点影响)。这个会使用到前缀和的知识,开一个len+1的数组,给pow[0]赋值1也就是2的0次方,之后对每个位置的值乘2处理,并乘2过程中进行mod处理。这样在最后求职时就可以常数时间拿到数值。

        这里就要填坑了,由于过程中mod的处理,所以有可能出现2的高次方减去2的低次方出现负数的情况,这种情况我们需要一点数学知识,如果高次方模完减低次方出现了负数,就需要加上一个mod(模数),这样才能得到正确答案。

        最后,运行,通过。

【每日一题】1498. 满足条件的子序列数目_第1张图片

     最后,祝大家国庆快乐呀!!!!! 

你可能感兴趣的:(每日一题,算法,java,leetcode,数据结构,力扣)