给你一个整数数组 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
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/closest-subsequence-sum
解法:
折半查找,状态压缩, 双指针/二分。求最接近目标的子序列和,数组nums的最大长度为40,所以应该用折半查找+状态压缩。将数组nums分为两部分A和B,分别在A与B中利用状态压缩枚举,求子序列和ASum与BSum。那么答案可能存在于:
1)ASum中的某一元素
2)BSum中的某一元素
3)ASum中的某一元素+Bsum中的某一元素
得到ASum与Bsum后,求解答案就变为了 “给定两个数组,两个数组中分别取一个元素,使得它们的和尽可能接近目标值”。可以通过双指针或者二分求解。
其中状态压缩枚举子集和:
比如数组A有n个元素,那么我们可以枚举出(1<<n)个子集,从小到大枚举子集,其中第i位为1表示取下标为i的数组元素。
class Solution {
public:
int minAbsDifference(vector<int>& nums, int goal) {
int n=nums.size();
int len1=n/2+n%2, len2=n-len1;
auto getSum=[&](int x, int start, int len, vector<int> &arr)
{
for(int i=0;i<len;++i)
{
if((x&1<<i)==0)
continue;
arr[x]=arr[x^(1<<i)]+nums[start+i]; //可以通过前面已经计算过的子集转移而来,避免重复计算
break; //有一个就行了,不break的话,效率差别很大
}
return;
};
vector<int> left(1<<len1);
int ans=INT_MAX;
for(int i=0;i<1<<len1;++i)
{
getSum(i, 0, len1, left);
ans=min(ans, abs(goal-left[i]));
}
sort(left.begin(), left.end());
vector<int> right(1<<len2);
for(int i=0;i<(1<<len2);++i)
{
getSum(i, len1, len2, right);
ans=min(ans, abs(goal-right[i]));
}
sort(right.begin(), right.end());
int l=0, r=(1<<len2)-1;
while(l<(1<<len1)&&r>=0) //双指针找元素
{
int temp=left[l]+right[r]-goal;
ans=min(ans, abs(temp));
if(temp>0)
--r;
else
++l;
}
return ans;
}
};