双指针题目:满足条件的子序列数目

文章目录

题目

标题和出处

标题:满足条件的子序列数目

出处:1498. 满足条件的子序列数目

难度

7 级

题目描述

要求

给定一个整数数组 nums \texttt{nums} nums 和一个整数 target \texttt{target} target。

返回 nums \texttt{nums} nums 中满足最小元素与最大元素的和小于或等于 target \texttt{target} target 的非空 子序列的数目。由于答案可能非常大,返回答案模 10 9 + 7 \texttt{10}^\texttt{9} + \texttt{7} 109+7 的余数。

示例

示例 1:

输入: nums = 3,5,6,7, target = 9 \texttt{nums = 3,5,6,7, target = 9} nums = 3,5,6,7, target = 9

输出: 4 \texttt{4} 4

解释:有 4 \texttt{4} 4 个子序列满足该条件。
3 → 3 + 3 ≤ 9 \texttt{3} \rightarrow \texttt{3} + \texttt{3} \le \texttt{9} 3→3+3≤9
3,5 → 3 + 5 ≤ 9 \texttt{3,5} \rightarrow \texttt{3} + \texttt{5} \le \texttt{9} 3,5→3+5≤9
3,5,6 → 3 + 6 ≤ 9 \texttt{3,5,6} \rightarrow \texttt{3} + \texttt{6} \le \texttt{9} 3,5,6→3+6≤9
3,6 → 3 + 6 ≤ 9 \texttt{3,6} \rightarrow \texttt{3} + \texttt{6} \le \texttt{9} 3,6→3+6≤9

示例 2:

输入: nums = 3,3,6,8, target = 10 \texttt{nums = 3,3,6,8, target = 10} nums = 3,3,6,8, target = 10

输出: 6 \texttt{6} 6

解释:有 6 \texttt{6} 6 个子序列满足该条件: 3 \texttt{3} 33 \texttt{3} 33,3 \texttt{3,3} 3,33,6 \texttt{3,6} 3,63,6 \texttt{3,6} 3,63,3,6 \texttt{3,3,6} 3,3,6

示例 3:

输入: nums = 2,3,3,4,6,7, target = 12 \texttt{nums = 2,3,3,4,6,7, target = 12} nums = 2,3,3,4,6,7, target = 12

输出: 61 \texttt{61} 61

解释:共有 63 \texttt{63} 63 个非空子序列,其中 2 \texttt{2} 2 个不满足条件( 6,7 \texttt{6,7} 6,77 \texttt{7} 7)。有效子序列总数为 63 − 2 = 61 \texttt{63} - \texttt{2} = \texttt{61} 63−2=61。

数据范围

  • 1 ≤ nums.length ≤ 10 5 \texttt{1} \le \texttt{nums.length} \le \texttt{10}^\texttt{5} 1≤nums.length≤105
  • 1 ≤ numsi ≤ 10 6 \texttt{1} \le \texttt{numsi} \le \texttt{10}^\texttt{6} 1≤numsi≤106
  • 1 ≤ target ≤ 10 6 \texttt{1} \le \texttt{target} \le \texttt{10}^\texttt{6} 1≤target≤106

前言

这道题要求计算数组 nums \textit{nums} nums 中满足最小值与最大值之和不超过 target \textit{target} target 的非空子序列的数目。当一个子序列的最小值与最大值确定时,在最小值与最大值之间的其余元素是否加入子序列不会影响子序列的最小值与最大值。如果最小值与最大值之间的其余元素有 x x x 个,则当前最小值与最大值确定的子序列个数是 2 x 2^x 2x,其中 x x x 不超过数组 nums \textit{nums} nums 的长度。为了快速计算任意 x x x 对应的 2 x 2^x 2x,需要预计算从 0 0 0 到数组长度的每个整数对应的 2 2 2 的幂。在预计算 2 2 2 的幂之后,再计算满足条件的子序列数目。

解法一

思路和算法

由于这道题要求计算满足条件的子序列数目,子序列不要求在原数组中连续,因此可以将数组 nums \textit{nums} nums 按升序排序,然后在排序后的数组中计算满足条件的子序列数目。

将数组 nums \textit{nums} nums 排序之后,从左到右遍历数组 nums \textit{nums} nums,对于下标 i i i,当 nums i × 2 ≤ target \textit{nums}i \times 2 \le \textit{target} numsi×2≤target 时,一定存在下标 j ≥ i j \ge i j≥i 使得 nums i + nums j ≤ target \textit{nums}i + \textit{nums}j \le \textit{target} numsi+numsj≤target,此时 nums i \textit{nums}i numsi 和 nums j \textit{nums}j numsj 可以分别作为满足条件的子序列中的最小值和最大值。

由于数组 nums \textit{nums} nums 按升序排序,因此当 nums i × 2 ≤ target \textit{nums}i \times 2 \le \textit{target} numsi×2≤target 时,对于下标 i i i 可以使用二分查找的做法找到满足 j ≥ i j \ge i j≥i 且 nums i + nums j ≤ target \textit{nums}i + \textit{nums}j \le \textit{target} numsi+numsj≤target 的最大下标 j j j。二分查找的下界和上界分别记为 low \textit{low} low 和 high \textit{high} high,初始时 low \textit{low} low 位于下标 i i i, high \textit{high} high 位于数组的最大下标,每次查找取 mid \textit{mid} mid 为 low \textit{low} low 和 high \textit{high} high 的平均数向上取整,执行如下操作。

  • 如果 nums mid ≤ target \textit{nums}\\textit{mid} \le \textit{target} numsmid≤target,则最大下标大于等于 mid \textit{mid} mid,因此在下标范围 mid , high \\textit{mid}, \\textit{high} mid,high 中继续查找。

  • 如果 nums mid > target \textit{nums}\\textit{mid} > \textit{target} numsmid>target,则最大下标小于 mid \textit{mid} mid,因此在下标范围 low , mid − 1 \\textit{low}, \\textit{mid} - 1 low,mid−1 中继续查找。

当 low = high \textit{low} = \textit{high} low=high 时,查找结束,此时 low \textit{low} low 为满足 j ≥ i j \ge i j≥i 且 nums i + nums j ≤ target \textit{nums}i + \textit{nums}j \le \textit{target} numsi+numsj≤target 的最大下标 j j j。

当 nums i \textit{nums}i numsi 是子序列中的最小值时, nums i \textit{nums}i numsi 必须在子序列中出现,满足条件的子序列中的最大值可以是大于等于 nums i \textit{nums}i numsi 且小于等于 nums j \textit{nums}j numsj 的任意值,对于任意 i < k ≤ j i < k \le j i<k≤j, nums k \textit{nums}k numsk 都可以在子序列中出现或不出现,因此以 nums i \textit{nums}i numsi 作为最小值的满足条件的子序列数目是 2 j − i 2^{j - i} 2j−i。

对数组 nums \textit{nums} nums 中的每个下标 i i i 计算以 nums i \textit{nums}i numsi 作为最小值的满足条件的子序列数目之后,即可得到数组 nums \textit{nums} nums 中满足条件的子序列数目。

代码

java 复制代码
class Solution {
    static final int MODULO = 1000000007;

    public int numSubseq(int[] nums, int target) {
        int subsequences = 0;
        int length = nums.length;
        int[] power2 = new int[length];
        power2[0] = 1;
        for (int i = 1; i < length; i++) {
            power2[i] = power2[i - 1] * 2 % MODULO;
        }
        Arrays.sort(nums);
        for (int i = 0; i < length && nums[i] * 2 <= target; i++) {
            int j = searchEnd(nums, target - nums[i], i);
            int count = power2[j - i];
            subsequences = (subsequences + count) % MODULO;
        }
        return subsequences;
    }

    public int searchEnd(int[] nums, int target, int low) {
        int high = nums.length - 1;
        while (low < high) {
            int mid = low + (high - low + 1) / 2;
            if (nums[mid] <= target) {
                low = mid;
            } else {
                high = mid - 1;
            }
        }
        return low;
    }
}

复杂度分析

  • 时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn),其中 n n n 是数组 nums \textit{nums} nums 的长度。需要 O ( n ) O(n) O(n) 的时间预计算 2 2 2 的幂,需要 O ( n log ⁡ n ) O(n \log n) O(nlogn) 的时间将数组 nums \textit{nums} nums 排序,对于数组 nums \textit{nums} nums 中的每个元素需要 O ( log ⁡ n ) O(\log n) O(logn) 的时间二分查找,因此时间复杂度是 O ( n + n log ⁡ n + n log ⁡ n ) = O ( n log ⁡ n ) O(n + n \log n + n \log n) = O(n \log n) O(n+nlogn+nlogn)=O(nlogn)。

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums 的长度。需要创建长度为 n n n 的数组存储 2 2 2 的幂,排序需要 O ( log ⁡ n ) O(\log n) O(logn) 的递归调用栈空间,因此空间复杂度是 O ( n + log ⁡ n ) = O ( n ) O(n + \log n) = O(n) O(n+logn)=O(n)。

解法二

思路和算法

将数组 nums \textit{nums} nums 排序之后,当 nums i × 2 ≤ target \textit{nums}i \times 2 \le \textit{target} numsi×2≤target 时,对于下标 i i i 存在满足 j ≥ i j \ge i j≥i 且 nums i + nums j ≤ target \textit{nums}i + \textit{nums}j \le \textit{target} numsi+numsj≤target 的最大下标 j j j。当下标 i i i 向右移动时, nums i \textit{nums}i numsi 不变或增加,满足 nums i + nums j ≤ target \textit{nums}i + \textit{nums}j \le \textit{target} numsi+numsj≤target 的最大下标 j j j 不变或减少。因此可以使用相向双指针的做法计算满足条件的子序列数目。

用 left \textit{left} left 和 right \textit{right} right 表示双指针,初始时 left \textit{left} left 指向下标 0 0 0, right \textit{right} right 指向数组的最大下标。当 left ≤ right \textit{left} \le \textit{right} left≤right 且 nums i × 2 ≤ target \textit{nums}i \times 2 \le \textit{target} numsi×2≤target 时,从左到右遍历 left \textit{left} left,对于每个 left \textit{left} left,执行如下操作。

  1. 当 nums left + nums right > target \textit{nums}\\textit{left} + \textit{nums}\\textit{right} > \textit{target} numsleft+numsright>target 时,将 right \textit{right} right 向左移动,直到 nums left + nums right ≤ target \textit{nums}\\textit{left} + \textit{nums}\\textit{right} \le \textit{target} numsleft+numsright≤target,此时 right \textit{right} right 是当前 left \textit{left} left 对应的使得 nums left + nums right ≤ target \textit{nums}\\textit{left} + \textit{nums}\\textit{right} \le \textit{target} numsleft+numsright≤target 的最大下标。

  2. 当 nums left \textit{nums}\\textit{left} numsleft 是子序列中的最小值时, nums left \textit{nums}\\textit{left} numsleft 必须在子序列中出现,满足条件的子序列中的最大值可以是大于等于 nums left \textit{nums}\\textit{left} numsleft 且小于等于 nums right \textit{nums}\\textit{right} numsright 的任意值,对于任意 left < k ≤ right \textit{left} < k \le \textit{right} left<k≤right, nums k \textit{nums}k numsk 都可以在子序列中出现或不出现,因此以 nums left \textit{nums}\\textit{left} numsleft 作为最小值的满足条件的子序列数目是 2 right − left 2^{\textit{right} - \textit{left}} 2right−left。

遍历结束之后,即可得到数组 nums \textit{nums} nums 中满足条件的子序列数目。

代码

java 复制代码
class Solution {
    static final int MODULO = 1000000007;

    public int numSubseq(int[] nums, int target) {
        int subsequences = 0;
        int length = nums.length;
        int[] power2 = new int[length];
        power2[0] = 1;
        for (int i = 1; i < length; i++) {
            power2[i] = power2[i - 1] * 2 % MODULO;
        }
        Arrays.sort(nums);
        int left = 0, right = length - 1;
        while (left <= right && nums[left] * 2 <= target) {
            while (nums[left] + nums[right] > target) {
                right--;
            }
            int count = power2[right - left];
            subsequences = (subsequences + count) % MODULO;
            left++;
        }
        return subsequences;
    }
}

复杂度分析

  • 时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn),其中 n n n 是数组 nums \textit{nums} nums 的长度。需要 O ( n ) O(n) O(n) 的时间预计算 2 2 2 的幂,需要 O ( n log ⁡ n ) O(n \log n) O(nlogn) 的时间将数组 nums \textit{nums} nums 排序,排序后需要使用双指针遍历数组 nums \textit{nums} nums,因此时间复杂度是 O ( n + n log ⁡ n + n ) = O ( n log ⁡ n ) O(n + n \log n + n) = O(n \log n) O(n+nlogn+n)=O(nlogn)。

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums 的长度。需要创建长度为 n n n 的数组存储 2 2 2 的幂,排序需要 O ( log ⁡ n ) O(\log n) O(logn) 的递归调用栈空间,因此空间复杂度是 O ( n + log ⁡ n ) = O ( n ) O(n + \log n) = O(n) O(n+logn)=O(n)。

相关推荐
j7~9 小时前
【算法】专题一:双指针之移动零,复写零,快乐数
数据结构·c++·算法·双指针·快乐数·移动零·复写零
csdn_aspnet3 天前
C# list集合 多属性排序
c#·list·linq·排序
8Qi86 天前
LeetCode 75:颜色分类(荷兰国旗问题)—— Java 题解 ✅
java·算法·leetcode·指针·排序
Misnearch6 天前
3635. 最早完成陆地和水上游乐设施的时间II
leetcode·贪心·排序
江屿风8 天前
C++OJ题经验总结(竞赛)4
开发语言·c++·笔记·算法·dp·双指针
8Qi89 天前
LeetCode 209. 长度最小的子数组(Minimum Size Subarray Sum)
java·算法·leetcode·双指针·滑动窗口
happymaker062611 天前
LeetCodeHot100——盛水最多的容器
数据结构·算法·leetcode·双指针·hot100
像素猎人14 天前
洛谷题B3882:求回文数【双指针】
算法·双指针
Dlrb121115 天前
数据结构-单链表与双链表
c语言·数据结构·链表·排序·双链表
我还记得那天16 天前
数组的2个应用举例
c语言·开发语言·二分查找·数组