Skip to content

子集和问题

给定一个正整数数组 nums 和一个目标正整数 target ,请找出所有可能的组合,使得组合中的元素和等于 target 。给定数组无重复元素,每个元素可以被选取多次。请以列表形式返回这些组合,列表中不应包含重复组合。

例如,输入集合 { 3,4,5 } 和 目标 9,可以获得解位 {3,3,3}, {4,5}。

回溯方案找到解

这里实际上,我们是对元素去进行重复选择和排序,并且计算元素和。

  • 当元素和 === 目标值,则记录数组。
  • 当元素和 > 目标值,则剪枝。
  • 当元素和 < 目标值,则下钻。

于是,我们可以得到下方代码。

ts
function backtrack(
  state: number[],
  target: number,
  total: number,
  choices: number[],
  res: number[][]
) {
  if (total === target) {
    res.push([...state]);
    return;
  }

  for(let i = 0; i < choices.length; i++) {
    if (total + choices[i] > target) {
      continue;
    }

    state.push(choices[i]);

    backtrack(state, target, total + choices[i], choices, res);

    state.pop();
  }
}

function subsetSumINaive(nums: number[], target: number): number[][] {
  const state = []; // 状态(子集)
  const total = 0; // 子集和
  const res = []; // 结果列表(子集列表)
  backtrack(state, target, total, nums, res);
  return res;
}

向以上代码输入数组 [3,4,5] 和目标元素 9 ,输出结果为 [3,3,3],[4,5],[5,4]。虽然成功找出了所有和为 9 的子集,但其中存在重复的子集 [4,5]和 [5,4] 。

这个时候,其实,我们可以直接对列表去重,但有点不优雅。

重复子集剪枝

这里除了做重复子集剪枝外,我们还做排序进行优化。

排序

排序是指在处理集合中的元素之前,先将集合中的元素按升序(或降序)排列。排序有以下几个好处:

  • 简化重复子集剪枝:排序后,相同的元素会聚集在一起,更容易检测和跳过重复的子集。
  • 早期剪枝:当元素按升序排列时,可以更早地判断当前子集是否可能超过目标和,从而提前结束不必要的递归,节省计算时间。

重复子集剪枝

重复子集剪枝是指在回溯算法中避免生成重复的子集。具体来说,主要通过以下方式进行剪枝:

  • 跳过相同的元素:在递归过程中,如果当前元素和前一个元素相同,则跳过当前元素。这是因为前一个元素已经考虑过了,再次考虑当前元素会产生重复的子集。
  • 避免从同一个位置重复选择:在递归过程中,每次选择元素时,只选择从当前索引之后的元素,这样可以避免选择相同的元素组合。
ts
/* 回溯算法:子集和 I */
function backtrack(
    state: number[],
    target: number,
    choices: number[],
    start: number,
    res: number[][]
): void {
    // 子集和等于 target 时,记录解
    if (target === 0) {
        res.push([...state]);
        return;
    }
    // 遍历所有选择
    // 剪枝二:从 start 开始遍历,避免生成重复子集
    for (let i = start; i < choices.length; i++) {
        // 剪枝一:若子集和超过 target ,则直接结束循环
        // 这是因为数组已排序,后边元素更大,子集和一定超过 target
        if (target - choices[i] < 0) {
            break;
        }
        // 尝试:做出选择,更新 target, start
        state.push(choices[i]);
        // 进行下一轮选择
        backtrack(state, target - choices[i], choices, i, res);
        // 回退:撤销选择,恢复到之前的状态
        state.pop();
    }
}

/* 求解子集和 I */
function subsetSumI(nums: number[], target: number): number[][] {
    const state = []; // 状态(子集)
    nums.sort((a, b) => a - b); // 对 nums 进行排序
    const start = 0; // 遍历起始点
    const res = []; // 结果列表(子集列表)
    backtrack(state, target, nums, start, res);
    return res;
}

参考

Released under the MIT License.