最接近目标值的子序列和

标签: 位运算 数组 双指针 动态规划 状态压缩

难度: 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
  • -107 <= nums[i] <= 107
  • -109 <= goal <= 109

Submission

运行时间: 545 ms

内存: 147.6 MB

class Solution:
    def sub_squence_sum(self, nums):
        sum_result = set([0])
        new_result = set()

        for n in nums:
            for s in sum_result:
                new_result.add(s + n)
            sum_result.update(new_result)
            new_result.clear()

        sum_result = list(sum_result)
        sum_result.sort()

        return sum_result

    def minAbsDifference(self, nums: List[int], goal: int) -> int:
        if len(nums) == 0:
            return abs(goal)

        split_pos = len(nums) // 2
        res1 = self.sub_squence_sum(nums[:split_pos])
        res2 = self.sub_squence_sum(nums[split_pos:])

        i = 0
        j = len(res2) - 1

        min_diff = math.inf
        while True:
            if i < len(res1) and j >= 0:
                diff = res1[i] + res2[j] - goal
                min_diff = min(abs(diff), min_diff)
                if diff == 0:
                    return 0
                elif diff > 0:
                    j -= 1
                elif diff < 0:
                    i += 1
            elif i >= len(res1) and j >= 0:
                min_diff = min(abs(res1[-1] + res2[j] - goal), min_diff)
                j -= 1
            elif i < len(res1) and j < 0:
                min_diff = min(abs(res1[i] + res2[0] - goal), min_diff)
                i += 1
            else:
                break

        return min_diff

Explain

此题解采用了分治与双指针的策略。首先,它将输入数组分为两部分,分别计算每部分可能的子序列和,存储在两个列表中。对于计算子序列和的过程,使用了一个集合来避免重复和。每次从当前子序列和集合中取一个元素,并与当前数字相加,形成新的子序列和,更新到集合中。然后对两个结果列表排序,并使用双指针技术来找到两个列表中元素之和最接近目标值的组合。指针的移动根据当前和与目标值的比较结果进行:如果和小于目标值,则移动较小列表的指针;如果和大于目标值,则移动较大列表的指针。这种方法可以有效地缩小搜索范围,直到找到最小的绝对差。

时间复杂度: O(2^(n/2) log(2^(n/2)))

空间复杂度: O(2^(n/2))

class Solution:
    def sub_squence_sum(self, nums):
        sum_result = set([0])  # 存储所有可能的子序列和
        new_result = set()  # 临时存储新增的子序列和

        for n in nums:  # 遍历数组中的每个元素
            for s in sum_result:  # 遍历已存在的子序列和
                new_result.add(s + n)  # 将当前元素添加到已有子序列和中
            sum_result.update(new_result)  # 更新子序列和集合
            new_result.clear()  # 清空临时集合

        sum_result = list(sum_result)  # 转换为列表以便排序
        sum_result.sort()  # 排序

        return sum_result

    def minAbsDifference(self, nums: List[int], goal: int) -> int:
        if len(nums) == 0:
            return abs(goal)

        split_pos = len(nums) // 2  # 将数组分为两部分
        res1 = self.sub_squence_sum(nums[:split_pos])  # 计算前半部分的子序列和
        res2 = self.sub_squence_sum(nums[split_pos:])  # 计算后半部分的子序列和

        i = 0  # res1 的索引
        j = len(res2) - 1  # res2 的索引

        min_diff = math.inf  # 初始化最小差值
        while True:  # 使用双指针查找最接近的和
            if i < len(res1) and j >= 0:
                diff = res1[i] + res2[j] - goal  # 计算当前子序列和与目标值的差
                min_diff = min(abs(diff), min_diff)  # 更新最小差值
                if diff == 0:  # 如果差值为0,直接返回
                    return 0
                elif diff > 0:  # 如果差值大于0,移动 j 减小和
                    j -= 1
                elif diff < 0:  # 如果差值小于0,移动 i 增大和
                    i += 1
            elif i >= len(res1) and j >= 0:
                min_diff = min(abs(res1[-1] + res2[j] - goal), min_diff)  # 仅移动 j
                j -= 1
            elif i < len(res1) and j < 0:
                min_diff = min(abs(res1[i] + res2[0] - goal), min_diff)  # 仅移动 i
                i += 1
            else:
                break  # 所有可能已遍历完毕

        return min_diff

Explore

在生成子序列和的过程中使用集合而不是列表的主要原因是为了避免重复的子序列和。由于集合的元素是唯一的,这样可以自动去除重复的和,从而减少了处理的数据量。使用集合插入和查询操作平均时间复杂度为O(1),相比于列表在检查或插入新元素时可能需要O(n)的复杂度,性能更优。因此,使用集合可以提高算法的效率,尤其是在处理大数据量时更为显著。

这种指针移动策略基于两个已排序列表的特性。当和小于目标值时,增加较小列表的指针可以使总和增大,这是因为较小列表的下一个元素比当前元素大;反之,当和大于目标值时,减少较大列表的指针可以使总和减小,因为较大列表的前一个元素比当前元素小。这种策略利用了排序和双指针的特点,可以有效地调整和的大小,同时避免了不必要的组合,因此能更快地找到接近目标值的和。

双指针技术在这种场景中有效,因为它可以在两个已排序的列表中以线性时间复杂度找到最接近目标的和。相较于遍历所有可能的子序列和的组合(时间复杂度可能达到O(n^2)),双指针技术通过每次只移动一个指针,并基于当前和与目标值的关系来决定移动哪一个指针,大幅减少了需要考虑的组合数,从而提高了效率。这种方法既减少了计算量,也降低了时间复杂度,使得算法在处理大量数据时更为高效。

当指针越界时,算法通过检查是否还有其他可能的组合来继续调整和。例如,如果i指针越界(i>=len(res1)),则只能移动j指针来寻找可能的更小和;相反,如果j指针越界(j<0),则只能移动i指针来寻找可能的更大和。这种边界处理确保了算法在到达任一列表末端时仍然能尝试所有有效的组合,保证了算法的正确性和完整性,避免了因忽略边界情况而导致的错误结果。