5675. 最接近目标值的子序列和(二分、位运算)

难度困难

给你一个整数数组 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 。

随后,原数组的一个子序列和,必然为下列三者之一:

  1. left中的某个元素;
  2. right中的某个元素;
  3. 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;
	}
};

猜你喜欢

转载自blog.csdn.net/Yanpr919/article/details/113746190