Medium
题目描述
给定一个大小为 n 的整数数组 nums 和一个正整数 k。
通过将每个元素 nums[i] 替换为 min(nums[i], x) 来获得由值 x 限制的数组。
对于从 1 到 n 的每个整数 x,确定是否可以从由 x 限制的数组中选择一个子序列,使得所选元素的和恰好为 k。
返回一个大小为 n 的 0 索引布尔数组 answer,其中 answer[i] 在使用 x = i + 1 时为 true(如果可能),否则为 false。
示例 1:
输入:nums = [4,3,2,4], k = 5
输出:[false,false,true,true]
解释:
- 对于 x = 1,限制后的数组是 [1, 1, 1, 1]。可能的和为 1, 2, 3, 4,所以无法形成和为 5。
- 对于 x = 2,限制后的数组是 [2, 2, 2, 2]。可能的和为 2, 4, 6, 8,所以无法形成和为 5。
- 对于 x = 3,限制后的数组是 [3, 3, 2, 3]。子序列 [2, 3] 的和为 5,所以是可能的。
- 对于 x = 4,限制后的数组是 [4, 3, 2, 4]。子序列 [3, 2] 的和为 5,所以是可能的。
示例 2:
输入:nums = [1,2,3,4,5], k = 3
输出:[true,true,true,true,true]
解释:对于每个 x 值,总是可以从限制后的数组中选择一个和恰好为 3 的子序列。
约束条件:
1 <= n == nums.length <= 40001 <= nums[i] <= n1 <= k <= 4000
解题思路
这道题的关键在于理解限制操作的本质和优化动态规划的计算。
核心思路:
排序优化:将数组按降序排列。这样对于限制值 x,所有大于 x 的元素都在数组前部,可以统一处理。
动态规划状态设计:使用
dp[i][sum]表示从索引 i 到数组末尾是否可以选择子序列使和为 sum。限制操作的等价转换:对于限制值 x,将所有大于 x 的元素都变为 x。设有 t 个元素大于 x,那么这 t 个元素可以选择 0 到 t 个,每个贡献值为 x。
分层计算:
- 首先计算原数组(不限制的情况下)的 DP 表
- 对于每个限制值 x,找到有多少个元素 t 大于 x
- 枚举所有可能的「原始部分和」s(来自索引 t 之后的元素)
- 枚举选择多少个被限制的元素 m(0 ≤ m ≤ t)
- 检查是否存在 s + m × x = k
优化点:
- 通过排序避免重复计算
- 利用 DP 表的递推性质减少状态转移
- 预计算可达和的集合,避免不必要的枚举
这种方法将时间复杂度控制在 O(n²k) 范围内,适合题目的约束条件。
代码实现
class Solution {
public:
vector<bool> subsequenceSumAfterCapping(vector<int>& nums, int k) {
int n = nums.size();
sort(nums.rbegin(), nums.rend()); // 降序排列
// dp[i][sum] = true 表示从索引i到末尾可以选择子序列使和为sum
vector<vector<bool>> dp(n + 1, vector<bool>(k + 1, false));
dp[n][0] = true;
// 从后往前填充DP表
for (int i = n - 1; i >= 0; i--) {
for (int sum = 0; sum <= k; sum++) {
dp[i][sum] = dp[i + 1][sum]; // 不选当前元素
if (sum >= nums[i]) {
dp[i][sum] = dp[i][sum] || dp[i + 1][sum - nums[i]]; // 选择当前元素
}
}
}
vector<bool> answer(n);
for (int x = 1; x <= n; x++) {
// 找到第一个 <= x 的位置
int t = 0;
while (t < n && nums[t] > x) {
t++;
}
// 检查是否可能达到和k
for (int sum = 0; sum <= k; sum++) {
if (dp[t][sum]) {
int remaining = k - sum;
if (remaining >= 0 && remaining % x == 0 && remaining / x <= t) {
answer[x - 1] = true;
break;
}
}
}
}
return answer;
}
};
class Solution:
def subsequenceSumAfterCapping(self, nums: List[int], k: int) -> List[bool]:
n = len(nums)
nums.sort(reverse=True) # 降序排列
# dp[i][sum] = True 表示从索引i到末尾可以选择子序列使和为sum
dp = [[False] * (k + 1) for _ in range(n + 1)]
dp[n][0] = True
# 从后往前填充DP表
for i in range(n - 1, -1, -1):
for sum_val in range(k + 1):
dp[i][sum_val] = dp[i + 1][sum_val] # 不选当前元素
if sum_val >= nums[i]:
dp[i][sum_val] = dp[i][sum_val] or dp[i + 1][sum_val - nums[i]] # 选择当前元素
answer = [False] * n
for x in range(1, n + 1):
# 找到第一个 <= x 的位置
t = 0
while t < n and nums[t] > x:
t += 1
# 检查是否可能达到和k
for sum_val in range(k + 1):
if dp[t][sum_val]:
remaining = k - sum_val
if remaining >= 0 and remaining % x == 0 and remaining // x <= t:
answer[x - 1] = True
break
return answer
public class Solution {
public bool[] SubsequenceSumAfterCapping(int[] nums, int k) {
int n = nums.Length;
Array.Sort(nums, (a, b) => b.CompareTo(a)); // 降序排列
// dp[i,sum] = true 表示从索引i到末尾可以选择子序列使和为sum
bool[,] dp = new bool[n + 1, k + 1];
dp[n, 0] = true;
// 从后往前填充DP表
for (int i = n - 1; i >= 0; i--) {
for (int sum = 0; sum <= k; sum++) {
dp[i, sum] = dp[i + 1, sum]; // 不选当前元素
if (sum >= nums[i]) {
dp[i, sum] = dp[i, sum] || dp[i + 1, sum - nums[i]]; // 选择当前元素
}
}
}
bool[] answer = new bool[n];
for (int x = 1; x <= n; x++) {
// 找到第一个 <= x 的位置
int t = 0;
while (t < n && nums[t] > x) {
t++;
}
// 检查是否可能达到和k
for (int sum = 0; sum <= k; sum++) {
if (dp[t, sum]) {
int remaining = k - sum;
if (remaining >= 0 && remaining % x == 0 && remaining / x <= t) {
answer[x - 1] = true;
break;
}
}
}
}
return answer;
}
}
var subsequenceSumAfterCapping = function(nums, k) {
const n = nums.length;
const answer = new Array(n);
for (let x = 1; x <= n; x++) {
const capped = nums.map(num => Math.min(num, x));
const dp = new Array(k + 1).fill(false);
dp[0] = true;
for (const val of capped) {
for (let sum = k; sum >= val; sum--) {
if (dp[sum - val]) {
dp[sum] = true;
}
}
}
answer[x - 1] = dp[k];
}
return answer;
};
复杂度分析
| 复杂度类型 | 分析 |
|---|---|
| 时间复杂度 | O(n²k) |
| 空间复杂度 | O(nk) |
详细说明:
- 时间复杂度:排序 O(n log n),构建 DP 表 O(nk),对每个限制值 x 查询 O(nk),总共 O(n²k)
- 空间复杂度:DP 表占用 O(nk) 空间,其他辅助空间为 O(n)