Hard
题目描述
给你一个整数数组 nums 和一个整数 goal。
你需要从 nums 中选择一个子序列,使得子序列元素的和尽可能接近 goal。也就是说,如果子序列元素的和为 sum,那么你要使 abs(sum - goal) 的值最小化。
返回 abs(sum - goal) 可能的最小值。
注意,数组的子序列是通过移除原数组中的某些元素(可能全部或无元素)而形成的数组。
示例 1:
输入:nums = [5,-7,3,5], goal = 6
输出:0
解释:选择整个数组作为选中的子序列,和为 6 。
这等于目标值,所以绝对差值为 0 。
示例 2:
输入:nums = [7,-9,15,-2], goal = -5
输出:1
解释:选择子序列 [7,-9,-2] ,和为 -4 。
绝对差值为 abs(-4 - (-5)) = abs(1) = 1 ,是可能的最小值。
示例 3:
输入:nums = [1,2,3], goal = -7
输出:7
提示:
1 <= nums.length <= 40-10^7 <= nums[i] <= 10^7-10^9 <= goal <= 10^9
解题思路
这道题的关键在于数组长度最大为 40,如果直接枚举所有子序列会有 2^40 种可能,时间复杂度太高。
**中途相遇法(Meet in the Middle)**是解决此问题的最佳策略:
- 分割数组:将数组分成两部分,长度大致相等,每部分最多 20 个元素
- 生成子集和:对每一部分分别生成所有可能的子集和,复杂度降为 2^20
- 排序优化:将第一部分的所有子集和排序,便于后续二分查找
- 双指针搜索:遍历第二部分的每个子集和,用二分查找在第一部分中找到最接近
goal - sum2的值
具体步骤:
- 对于第二部分的每个子集和
sum2,我们需要在第一部分找到最接近goal - sum2的值 - 使用二分查找找到不超过
goal - sum2的最大值和不小于goal - sum2的最小值 - 计算这两种组合与
goal的差的绝对值,取最小值
这种方法将时间复杂度从 O(2^n) 优化到 O(2^(n/2) * log(2^(n/2))),在 n=40 时有显著改善。
代码实现
class Solution {
public:
int minAbsDifference(vector<int>& nums, int goal) {
int n = nums.size();
int mid = n / 2;
// 生成前半部分所有可能的子集和
vector<int> leftSums;
for (int mask = 0; mask < (1 << mid); mask++) {
int sum = 0;
for (int i = 0; i < mid; i++) {
if (mask & (1 << i)) {
sum += nums[i];
}
}
leftSums.push_back(sum);
}
sort(leftSums.begin(), leftSums.end());
int result = abs(goal);
// 生成后半部分所有可能的子集和,并与前半部分组合
for (int mask = 0; mask < (1 << (n - mid)); mask++) {
int sum = 0;
for (int i = 0; i < n - mid; i++) {
if (mask & (1 << i)) {
sum += nums[mid + i];
}
}
int target = goal - sum;
// 二分查找最接近target的值
auto it = lower_bound(leftSums.begin(), leftSums.end(), target);
if (it != leftSums.end()) {
result = min(result, abs(sum + *it - goal));
}
if (it != leftSums.begin()) {
--it;
result = min(result, abs(sum + *it - goal));
}
}
return result;
}
};
class Solution:
def minAbsDifference(self, nums: List[int], goal: int) -> int:
n = len(nums)
mid = n // 2
# 生成前半部分所有可能的子集和
left_sums = []
for mask in range(1 << mid):
sum_val = 0
for i in range(mid):
if mask & (1 << i):
sum_val += nums[i]
left_sums.append(sum_val)
left_sums.sort()
result = abs(goal)
# 生成后半部分所有可能的子集和,并与前半部分组合
for mask in range(1 << (n - mid)):
sum_val = 0
for i in range(n - mid):
if mask & (1 << i):
sum_val += nums[mid + i]
target = goal - sum_val
# 二分查找最接近target的值
import bisect
pos = bisect.bisect_left(left_sums, target)
if pos < len(left_sums):
result = min(result, abs(sum_val + left_sums[pos] - goal))
if pos > 0:
result = min(result, abs(sum_val + left_sums[pos - 1] - goal))
return result
public class Solution {
public int MinAbsDifference(int[] nums, int goal) {
int n = nums.Length;
int mid = n / 2;
// 生成前半部分所有可能的子集和
List<int> leftSums = new List<int>();
for (int mask = 0; mask < (1 << mid); mask++) {
int sum = 0;
for (int i = 0; i < mid; i++) {
if ((mask & (1 << i)) != 0) {
sum += nums[i];
}
}
leftSums.Add(sum);
}
leftSums.Sort();
int result = Math.Abs(goal);
// 生成后半部分所有可能的子集和,并与前半部分组合
for (int mask = 0; mask < (1 << (n - mid)); mask++) {
int sum = 0;
for (int i = 0; i < n - mid; i++) {
if ((mask & (1 << i)) != 0) {
sum += nums[mid + i];
}
}
int target = goal - sum;
// 二分查找最接近target的值
int pos = leftSums.BinarySearch(target);
if (pos < 0) {
pos = ~pos;
}
if (pos < leftSums.Count) {
result = Math.Min(result, Math.Abs(sum + leftSums[pos] - goal));
}
if (pos > 0) {
result = Math.Min(result, Math.Abs(sum + leftSums[pos - 1] - goal));
}
}
return result;
}
}
var minAbsDifference = function(nums, goal) {
const n = nums.length;
const mid = Math.floor(n / 2);
// 生成前半部分所有可能的子集和
const leftSums = [];
for (let mask = 0; mask < (1 << mid); mask++) {
let sum = 0;
for (let i = 0; i < mid; i++) {
if (mask & (1 << i)) {
sum += nums[i];
}
}
leftSums.push(sum);
}
leftSums.sort((a, b) => a - b);
let result = Math.abs(goal);
// 生成后半部分所有可能的子集和,并与前半部分组合
for (let mask = 0; mask < (1 << (n - mid)); mask++) {
let sum = 0;
for (let i = 0; i < n - mid; i++) {
if (mask & (1 << i)) {
sum += nums[mid + i];
}
}
const target = goal - sum;
// 二分查找最接近target的值
let left = 0, right = leftSums.length;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (leftSums[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
if (left < leftSums.length) {
result = Math.min(result, Math.abs(sum + leftSums[left] - goal));
}
if (left > 0) {
result = Math.min(result, Math.abs(sum + leftSums[left - 1] - goal));
}
}
return result;
};
复杂度分析
| 复杂度类型 | 大小 |
|---|---|
| 时间复杂度 | O(2^(n/2) × log(2^(n/2))) = O(2^(n/2) × n/2) |
| 空间复杂度 | O(2^(n/2)) |
其中 n 是数组长度。空间复杂度主要用于存储前半部分的所有子集和。