难度困难
给你一个整数数组 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
-107 <= nums[i] <= 107
-109 <= goal <= 109
关键字:位运算、指数遍历、二分
这道题需要注意的是数据量限制为n<=40,一般对于O(n),O(n^2),O(n^3)这样的复杂度来说,40太小了,因此应是指数复杂度O(2^n),而2^40太过大了,有经验的比赛者知道者会超时,而2^20不会,因此这道题首先应该将数据二分进行遍历子集。
对于2^n的遍历,可以采用位运算来快速实现,1 << n即为遍历的大小,对应 i 的范围为[0, 2^n -1],其中对n个数取否可通过(i >> j) & 1判断每一位是否为1,来代表这个子集是否包含 j 。
随后,原数组的一个子序列和,必然为下列三者之一:
- left中的某个元素;
- right中的某个元素;
- left中的某个元素与right 中的某个元素之和。
而为了获取最小的abs(Sum - goal),对每一个left中的子集和,通过二分找到right最接近的两个子集和使得与goal的差的绝对值最小,遍历left,更新最小的差的绝对值,即为结果。
class Solution {
public:
int minAbsDifference(vector<int>& nums, int goal) {
int N = nums.size(),n_left = N / 2, n_right = N - n_left;
vector<int>left(1 << n_left, 0), rigjt(1 << n_right, 0);
for (int i = 0; i < (1 << n_left); ++i) {
for (int j = 0; j < n_left; ++j) {
if ((i >> j) & 1) {
left[i] += nums[j];
}
}
}
sort(left.begin(), left.end());
int res = 1000000000;
for (int i = 0; i < (1 << n_right); ++i) {
for (int j = 0; j < n_right; ++j) {
if ((i >> j) & 1) {
rigjt[i] += nums[j + n_left];
}
}
int index = lower_bound(left.begin(), left.end(), goal - rigjt[i]) - left.begin();
if (index - 1 >= 0) {
res = min(res, abs(rigjt[i] + left[index - 1] - goal));
}
if (index < left.size()) {
res = min(res, abs(rigjt[i] + left[index] - goal));
}
}
return res;
}
};