满足条件的子序列数目 图解题解
这道题到底在问什么
- 输入
- nums=[3,5,6,7], target=9
- 输出
- 4
- 输入
- nums=[3,3,6,8], target=10
- 输出
- 6
最优解:一步一步想明白
- 3记住「排序 → 和 ≤ target 就固定最小、中间自由选 2^(r−l) 个、l 右移;和太大就 r 左移」,下面每帧都在套它。
- 4第一步排序。原数组 [7, 3, 2, 6, 4, 3] 排好序变成 [2, 3, 3, 4, 6, 7]。因为子序列的最小值、最大值只看「选了哪些数」、跟原顺序无关,排序完全不影响答案,反而让双指针能从两端对撞。
- 5左指针 l=0(最小值 2)、右指针 r=5(最大值 7)。最小加最大 = 2+7 = 9,不超过 target 9,说明只要把最小值钉在 2、最大值不超过 7,这一批子序列都合法。
- 6关键一步,把计数讲透。最小值钉死在下标 0 的 2(绿色),它一定要选;剩下下标 1 到 5 这 5 个数(灰色),每一个都可以选或不选,互不影响,所以一共 2 的 5 次方 = 32 种取法。每一种取法都是一个合法子序列:最小值就是 2、最大值最多到 7,和不超过 9。一次把这 32 个全部计入 ans。
- 7以下标 0 这个数作为「被选中的最左元素」的那一批已经数完,左指针右移到下标 1;换成更大的数当最左元素,继续考察。
- 8左指针 l=1(当前最小值 3)、右指针 r=5(最大值 7)。最小加最大 = 10,已经超过 target 9;连当前范围里最小的 3 配上 7 都超标,说明在「最左元素从下标 1 起」这段还没统计的范围里,7 当最大值已经配不出新的合法子序列了(7 跟更小元素的组合早在前面批次算过)。于是右指针左移、换更小的最大值。
- 9把最大值换小一点:右指针左移一格到 4(值 6),再看新的最小加最大能不能落进 target 以内。
- 10左指针 l=1(最小值 3)、右指针 r=4(最大值 6)。最小加最大 = 3+6 = 9,不超过 target 9,说明只要把最小值钉在 3、最大值不超过 6,这一批子序列都合法。
- 11同样地,最小值固定为 3,中间 3 个数自由选,2 的 3 次方 = 8 个合法子序列,ans 累加到 40。
- 12以下标 1 这个数作为「被选中的最左元素」的那一批已经数完,左指针右移到下标 2;这里的值和上一个一样,但它代表的是另一批、不含下标 1 那个数的子序列,继续考察。
- 13左指针 l=2(最小值 3)、右指针 r=4(最大值 6)。最小加最大 = 3+6 = 9,不超过 target 9,说明只要把最小值钉在 3、最大值不超过 6,这一批子序列都合法。
- 14同样地,最小值固定为 3,中间 2 个数自由选,2 的 2 次方 = 4 个合法子序列,ans 累加到 44。
- 15以下标 2 这个数作为「被选中的最左元素」的那一批已经数完,左指针右移到下标 3;换成更大的数当最左元素,继续考察。
- 16左指针 l=3(当前最小值 4)、右指针 r=4(最大值 6)。最小加最大 = 10,已经超过 target 9;连当前范围里最小的 4 配上 6 都超标,说明在「最左元素从下标 3 起」这段还没统计的范围里,6 当最大值已经配不出新的合法子序列了(6 跟更小元素的组合早在前面批次算过)。于是右指针左移、换更小的最大值。
- 17把最大值换小一点:右指针左移一格到 3(值 4),再看新的最小加最大能不能落进 target 以内。
- 18左指针 l=3(最小值 4)、右指针 r=3(最大值 4)。最小加最大 = 4+4 = 8,不超过 target 9,说明只要把最小值钉在 4、最大值不超过 4,这一批子序列都合法。
- 19同样地,最小值固定为 4,中间 0 个数自由选,2 的 0 次方 = 1 个合法子序列,ans 累加到 45。
- 20左指针 l=4 已经越过右指针 r=3,所有可能的最左元素都考察完了,扫描结束,最终 ans = 45。
⚠️ 容易写错的地方
✗ 错:担心排序打乱子序列
✓ 对:子序列只看 min+max,与顺序无关,可排序
排序后才能用「最小固定、最大在右」的双指针
✗ 错:命中时 ans++
✓ 对:命中要 ans += 2^(r−l) 一次加一批
固定最小后中间 r−l 个数各自可选可不选,共 2^(r−l) 个子序列
✗ 错:直接用 1 << (r−l)
✓ 对:预处理幂表并每步取模
r−l 可达数万,2 的幂会溢出,必须边算边对 1e9+7 取模
完整代码(Python / C++ / Java)
Python
from typing import List
class Solution:
def numSubseq(self, nums: List[int], target: int) -> int:
MOD = 10**9 + 7
nums.sort()
pow2 = [1] * (len(nums) + 1)
for i in range(1, len(pow2)):
pow2[i] = pow2[i-1] * 2 % MOD
l, r = 0, len(nums) - 1
ans = 0
while l <= r:
if nums[l] + nums[r] <= target:
ans = (ans + pow2[r-l]) % MOD
l += 1
else:
r -= 1
return ansC++
#include <algorithm>
#include <vector>
using namespace std;
class Solution {
public:
int numSubseq(vector<int>& nums, int target) {
const int MOD = 1000000007;
sort(nums.begin(), nums.end());
vector<int> pow2(nums.size() + 1, 1);
for (int i = 1; i < (int)pow2.size(); ++i) pow2[i] = pow2[i-1] * 2LL % MOD;
int l = 0, r = nums.size() - 1, ans = 0;
while (l <= r) {
if (nums[l] + nums[r] <= target) { ans = (ans + pow2[r-l]) % MOD; l++; }
else r--;
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int numSubseq(int[] nums, int target) {
int MOD = 1_000_000_007;
Arrays.sort(nums);
int[] pow2 = new int[nums.length + 1];
pow2[0] = 1;
for (int i = 1; i < pow2.length; i++) pow2[i] = (int)((long)pow2[i - 1] * 2 % MOD);
int l = 0, r = nums.length - 1, ans = 0;
while (l <= r) {
if (nums[l] + nums[r] <= target) { ans = (ans + pow2[r - l]) % MOD; l++; }
else r--;
}
return ans;
}
}复杂度
时间
O(n log n)
排序 O(n log n) 是瓶颈;之后双指针从两端对撞、各扫一遍是 O(n);预处理幂表 O(n)
空间
O(n)
预处理的 2 的幂表 pow2 占 O(n);排序栈空间另算
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 满足条件的子序列数目 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么是 2 的幂,而不是别的组合数?+
因为最小值固定后,中间那 r−l 个数互相独立、每个只有「选 / 不选」两种状态,乘起来就是 2 的 (r−l) 次方。最大值是谁不用单独枚举,它自然由「选中的数里最大的那个」决定,且一定不超过 nums[r]。
和「两数之和 / 三数之和」类双指针有什么异同?+
同样是排序 + 对撞双指针。区别在命中后的处理:两数之和找到一对就收;本题命中后要一次性把「固定最小、中间自由选」的 2^(r−l) 个子序列全部计入,再移左指针。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 满足条件的子序列数目 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。