13.3 Subset sum problem¶
13.3.1 Case without duplicate elements¶
Question
Given an array of positive integers nums
and a target positive integer target
, find all possible combinations such that the sum of the elements in the combination equals target
. The given array has no duplicate elements, and each element can be chosen multiple times. Please return these combinations as a list, which should not contain duplicate combinations.
For example, for the input set \(\{3, 4, 5\}\) and target integer \(9\), the solutions are \(\{3, 3, 3\}, \{4, 5\}\). Note the following two points.
- Elements in the input set can be chosen an unlimited number of times.
- Subsets do not distinguish the order of elements, for example \(\{4, 5\}\) and \(\{5, 4\}\) are the same subset.
1. Reference permutation solution¶
Similar to the permutation problem, we can imagine the generation of subsets as a series of choices, updating the "element sum" in real-time during the choice process. When the element sum equals target
, the subset is recorded in the result list.
Unlike the permutation problem, elements in this problem can be chosen an unlimited number of times, thus there is no need to use a selected
boolean list to record whether an element has been chosen. We can make minor modifications to the permutation code to initially solve the problem:
def backtrack(
state: list[int],
target: int,
total: int,
choices: list[int],
res: list[list[int]],
):
"""回溯算法:子集和 I"""
# 子集和等于 target 时,记录解
if total == target:
res.append(list(state))
return
# 遍历所有选择
for i in range(len(choices)):
# 剪枝:若子集和超过 target ,则跳过该选择
if total + choices[i] > target:
continue
# 尝试:做出选择,更新元素和 total
state.append(choices[i])
# 进行下一轮选择
backtrack(state, target, total + choices[i], choices, res)
# 回退:撤销选择,恢复到之前的状态
state.pop()
def subset_sum_i_naive(nums: list[int], target: int) -> list[list[int]]:
"""求解子集和 I(包含重复子集)"""
state = [] # 状态(子集)
total = 0 # 子集和
res = [] # 结果列表(子集列表)
backtrack(state, target, total, nums, res)
return res
/* 回溯算法:子集和 I */
void backtrack(vector<int> &state, int target, int total, vector<int> &choices, vector<vector<int>> &res) {
// 子集和等于 target 时,记录解
if (total == target) {
res.push_back(state);
return;
}
// 遍历所有选择
for (size_t i = 0; i < choices.size(); i++) {
// 剪枝:若子集和超过 target ,则跳过该选择
if (total + choices[i] > target) {
continue;
}
// 尝试:做出选择,更新元素和 total
state.push_back(choices[i]);
// 进行下一轮选择
backtrack(state, target, total + choices[i], choices, res);
// 回退:撤销选择,恢复到之前的状态
state.pop_back();
}
}
/* 求解子集和 I(包含重复子集) */
vector<vector<int>> subsetSumINaive(vector<int> &nums, int target) {
vector<int> state; // 状态(子集)
int total = 0; // 子集和
vector<vector<int>> res; // 结果列表(子集列表)
backtrack(state, target, total, nums, res);
return res;
}
/* 回溯算法:子集和 I */
void backtrack(List<Integer> state, int target, int total, int[] choices, List<List<Integer>> res) {
// 子集和等于 target 时,记录解
if (total == target) {
res.add(new ArrayList<>(state));
return;
}
// 遍历所有选择
for (int i = 0; i < choices.length; i++) {
// 剪枝:若子集和超过 target ,则跳过该选择
if (total + choices[i] > target) {
continue;
}
// 尝试:做出选择,更新元素和 total
state.add(choices[i]);
// 进行下一轮选择
backtrack(state, target, total + choices[i], choices, res);
// 回退:撤销选择,恢复到之前的状态
state.remove(state.size() - 1);
}
}
/* 求解子集和 I(包含重复子集) */
List<List<Integer>> subsetSumINaive(int[] nums, int target) {
List<Integer> state = new ArrayList<>(); // 状态(子集)
int total = 0; // 子集和
List<List<Integer>> res = new ArrayList<>(); // 结果列表(子集列表)
backtrack(state, target, total, nums, res);
return res;
}
/* 回溯算法:子集和 I */
void Backtrack(List<int> state, int target, int total, int[] choices, List<List<int>> res) {
// 子集和等于 target 时,记录解
if (total == target) {
res.Add(new List<int>(state));
return;
}
// 遍历所有选择
for (int i = 0; i < choices.Length; i++) {
// 剪枝:若子集和超过 target ,则跳过该选择
if (total + choices[i] > target) {
continue;
}
// 尝试:做出选择,更新元素和 total
state.Add(choices[i]);
// 进行下一轮选择
Backtrack(state, target, total + choices[i], choices, res);
// 回退:撤销选择,恢复到之前的状态
state.RemoveAt(state.Count - 1);
}
}
/* 求解子集和 I(包含重复子集) */
List<List<int>> SubsetSumINaive(int[] nums, int target) {
List<int> state = []; // 状态(子集)
int total = 0; // 子集和
List<List<int>> res = []; // 结果列表(子集列表)
Backtrack(state, target, total, nums, res);
return res;
}
/* 回溯算法:子集和 I */
func backtrackSubsetSumINaive(total, target int, state, choices *[]int, res *[][]int) {
// 子集和等于 target 时,记录解
if target == total {
newState := append([]int{}, *state...)
*res = append(*res, newState)
return
}
// 遍历所有选择
for i := 0; i < len(*choices); i++ {
// 剪枝:若子集和超过 target ,则跳过该选择
if total+(*choices)[i] > target {
continue
}
// 尝试:做出选择,更新元素和 total
*state = append(*state, (*choices)[i])
// 进行下一轮选择
backtrackSubsetSumINaive(total+(*choices)[i], target, state, choices, res)
// 回退:撤销选择,恢复到之前的状态
*state = (*state)[:len(*state)-1]
}
}
/* 求解子集和 I(包含重复子集) */
func subsetSumINaive(nums []int, target int) [][]int {
state := make([]int, 0) // 状态(子集)
total := 0 // 子集和
res := make([][]int, 0) // 结果列表(子集列表)
backtrackSubsetSumINaive(total, target, &state, &nums, &res)
return res
}
/* 回溯算法:子集和 I */
func backtrack(state: inout [Int], target: Int, total: Int, choices: [Int], res: inout [[Int]]) {
// 子集和等于 target 时,记录解
if total == target {
res.append(state)
return
}
// 遍历所有选择
for i in choices.indices {
// 剪枝:若子集和超过 target ,则跳过该选择
if total + choices[i] > target {
continue
}
// 尝试:做出选择,更新元素和 total
state.append(choices[i])
// 进行下一轮选择
backtrack(state: &state, target: target, total: total + choices[i], choices: choices, res: &res)
// 回退:撤销选择,恢复到之前的状态
state.removeLast()
}
}
/* 求解子集和 I(包含重复子集) */
func subsetSumINaive(nums: [Int], target: Int) -> [[Int]] {
var state: [Int] = [] // 状态(子集)
let total = 0 // 子集和
var res: [[Int]] = [] // 结果列表(子集列表)
backtrack(state: &state, target: target, total: total, choices: nums, res: &res)
return res
}
/* 回溯算法:子集和 I */
function backtrack(state, target, total, choices, res) {
// 子集和等于 target 时,记录解
if (total === target) {
res.push([...state]);
return;
}
// 遍历所有选择
for (let i = 0; i < choices.length; i++) {
// 剪枝:若子集和超过 target ,则跳过该选择
if (total + choices[i] > target) {
continue;
}
// 尝试:做出选择,更新元素和 total
state.push(choices[i]);
// 进行下一轮选择
backtrack(state, target, total + choices[i], choices, res);
// 回退:撤销选择,恢复到之前的状态
state.pop();
}
}
/* 求解子集和 I(包含重复子集) */
function subsetSumINaive(nums, target) {
const state = []; // 状态(子集)
const total = 0; // 子集和
const res = []; // 结果列表(子集列表)
backtrack(state, target, total, nums, res);
return res;
}
/* 回溯算法:子集和 I */
function backtrack(
state: number[],
target: number,
total: number,
choices: number[],
res: number[][]
): void {
// 子集和等于 target 时,记录解
if (total === target) {
res.push([...state]);
return;
}
// 遍历所有选择
for (let i = 0; i < choices.length; i++) {
// 剪枝:若子集和超过 target ,则跳过该选择
if (total + choices[i] > target) {
continue;
}
// 尝试:做出选择,更新元素和 total
state.push(choices[i]);
// 进行下一轮选择
backtrack(state, target, total + choices[i], choices, res);
// 回退:撤销选择,恢复到之前的状态
state.pop();
}
}
/* 求解子集和 I(包含重复子集) */
function subsetSumINaive(nums: number[], target: number): number[][] {
const state = []; // 状态(子集)
const total = 0; // 子集和
const res = []; // 结果列表(子集列表)
backtrack(state, target, total, nums, res);
return res;
}
/* 回溯算法:子集和 I */
void backtrack(
List<int> state,
int target,
int total,
List<int> choices,
List<List<int>> res,
) {
// 子集和等于 target 时,记录解
if (total == target) {
res.add(List.from(state));
return;
}
// 遍历所有选择
for (int i = 0; i < choices.length; i++) {
// 剪枝:若子集和超过 target ,则跳过该选择
if (total + choices[i] > target) {
continue;
}
// 尝试:做出选择,更新元素和 total
state.add(choices[i]);
// 进行下一轮选择
backtrack(state, target, total + choices[i], choices, res);
// 回退:撤销选择,恢复到之前的状态
state.removeLast();
}
}
/* 求解子集和 I(包含重复子集) */
List<List<int>> subsetSumINaive(List<int> nums, int target) {
List<int> state = []; // 状态(子集)
int total = 0; // 元素和
List<List<int>> res = []; // 结果列表(子集列表)
backtrack(state, target, total, nums, res);
return res;
}
/* 回溯算法:子集和 I */
fn backtrack(
mut state: Vec<i32>,
target: i32,
total: i32,
choices: &[i32],
res: &mut Vec<Vec<i32>>,
) {
// 子集和等于 target 时,记录解
if total == target {
res.push(state);
return;
}
// 遍历所有选择
for i in 0..choices.len() {
// 剪枝:若子集和超过 target ,则跳过该选择
if total + choices[i] > target {
continue;
}
// 尝试:做出选择,更新元素和 total
state.push(choices[i]);
// 进行下一轮选择
backtrack(state.clone(), target, total + choices[i], choices, res);
// 回退:撤销选择,恢复到之前的状态
state.pop();
}
}
/* 求解子集和 I(包含重复子集) */
fn subset_sum_i_naive(nums: &[i32], target: i32) -> Vec<Vec<i32>> {
let state = Vec::new(); // 状态(子集)
let total = 0; // 子集和
let mut res = Vec::new(); // 结果列表(子集列表)
backtrack(state, target, total, nums, &mut res);
res
}
/* 回溯算法:子集和 I */
void backtrack(int target, int total, int *choices, int choicesSize) {
// 子集和等于 target 时,记录解
if (total == target) {
for (int i = 0; i < stateSize; i++) {
res[resSize][i] = state[i];
}
resColSizes[resSize++] = stateSize;
return;
}
// 遍历所有选择
for (int i = 0; i < choicesSize; i++) {
// 剪枝:若子集和超过 target ,则跳过该选择
if (total + choices[i] > target) {
continue;
}
// 尝试:做出选择,更新元素和 total
state[stateSize++] = choices[i];
// 进行下一轮选择
backtrack(target, total + choices[i], choices, choicesSize);
// 回退:撤销选择,恢复到之前的状态
stateSize--;
}
}
/* 求解子集和 I(包含重复子集) */
void subsetSumINaive(int *nums, int numsSize, int target) {
resSize = 0; // 初始化解的数量为0
backtrack(target, 0, nums, numsSize);
}
/* 回溯算法:子集和 I */
fun backtrack(
state: MutableList<Int>,
target: Int,
total: Int,
choices: IntArray,
res: MutableList<MutableList<Int>?>
) {
// 子集和等于 target 时,记录解
if (total == target) {
res.add(state.toMutableList())
return
}
// 遍历所有选择
for (i in choices.indices) {
// 剪枝:若子集和超过 target ,则跳过该选择
if (total + choices[i] > target) {
continue
}
// 尝试:做出选择,更新元素和 total
state.add(choices[i])
// 进行下一轮选择
backtrack(state, target, total + choices[i], choices, res)
// 回退:撤销选择,恢复到之前的状态
state.removeAt(state.size - 1)
}
}
/* 求解子集和 I(包含重复子集) */
fun subsetSumINaive(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {
val state = mutableListOf<Int>() // 状态(子集)
val total = 0 // 子集和
val res = mutableListOf<MutableList<Int>?>() // 结果列表(子集列表)
backtrack(state, target, total, nums, res)
return res
}
Code Visualization
Inputting the array \([3, 4, 5]\) and target element \(9\) into the above code yields the results \([3, 3, 3], [4, 5], [5, 4]\). Although it successfully finds all subsets with a sum of \(9\), it includes the duplicate subset \([4, 5]\) and \([5, 4]\).
This is because the search process distinguishes the order of choices, however, subsets do not distinguish the choice order. As shown in Figure 13-10, choosing \(4\) before \(5\) and choosing \(5\) before \(4\) are different branches, but correspond to the same subset.
Figure 13-10 Subset search and pruning out of bounds
To eliminate duplicate subsets, a straightforward idea is to deduplicate the result list. However, this method is very inefficient for two reasons.
- When there are many array elements, especially when
target
is large, the search process produces a large number of duplicate subsets. - Comparing subsets (arrays) for differences is very time-consuming, requiring arrays to be sorted first, then comparing the differences of each element in the arrays.
2. Duplicate subset pruning¶
We consider deduplication during the search process through pruning. Observing Figure 13-11, duplicate subsets are generated when choosing array elements in different orders, for example in the following situations.
- When choosing \(3\) in the first round and \(4\) in the second round, all subsets containing these two elements are generated, denoted as \([3, 4, \dots]\).
- Later, when \(4\) is chosen in the first round, the second round should skip \(3\) because the subset \([4, 3, \dots]\) generated by this choice completely duplicates the subset from step
1.
.
In the search process, each layer's choices are tried one by one from left to right, so the more to the right a branch is, the more it is pruned.
- First two rounds choose \(3\) and \(5\), generating subset \([3, 5, \dots]\).
- First two rounds choose \(4\) and \(5\), generating subset \([4, 5, \dots]\).
- If \(5\) is chosen in the first round, then the second round should skip \(3\) and \(4\) as the subsets \([5, 3, \dots]\) and \([5, 4, \dots]\) completely duplicate the subsets described in steps
1.
and2.
.
Figure 13-11 Different choice orders leading to duplicate subsets
In summary, given the input array \([x_1, x_2, \dots, x_n]\), the choice sequence in the search process should be \([x_{i_1}, x_{i_2}, \dots, x_{i_m}]\), which needs to satisfy \(i_1 \leq i_2 \leq \dots \leq i_m\). Any choice sequence that does not meet this condition will cause duplicates and should be pruned.
3. Code implementation¶
To implement this pruning, we initialize the variable start
, which indicates the starting point for traversal. After making the choice \(x_{i}\), set the next round to start from index \(i\). This will ensure the choice sequence satisfies \(i_1 \leq i_2 \leq \dots \leq i_m\), thereby ensuring the uniqueness of the subsets.
Besides, we have made the following two optimizations to the code.
- Before starting the search, sort the array
nums
. In the traversal of all choices, end the loop directly when the subset sum exceedstarget
as subsequent elements are larger and their subset sum will definitely exceedtarget
. - Eliminate the element sum variable
total
, by performing subtraction ontarget
to count the element sum. Whentarget
equals \(0\), record the solution.
def backtrack(
state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]
):
"""回溯算法:子集和 I"""
# 子集和等于 target 时,记录解
if target == 0:
res.append(list(state))
return
# 遍历所有选择
# 剪枝二:从 start 开始遍历,避免生成重复子集
for i in range(start, len(choices)):
# 剪枝一:若子集和超过 target ,则直接结束循环
# 这是因为数组已排序,后边元素更大,子集和一定超过 target
if target - choices[i] < 0:
break
# 尝试:做出选择,更新 target, start
state.append(choices[i])
# 进行下一轮选择
backtrack(state, target - choices[i], choices, i, res)
# 回退:撤销选择,恢复到之前的状态
state.pop()
def subset_sum_i(nums: list[int], target: int) -> list[list[int]]:
"""求解子集和 I"""
state = [] # 状态(子集)
nums.sort() # 对 nums 进行排序
start = 0 # 遍历起始点
res = [] # 结果列表(子集列表)
backtrack(state, target, nums, start, res)
return res
/* 回溯算法:子集和 I */
void backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {
// 子集和等于 target 时,记录解
if (target == 0) {
res.push_back(state);
return;
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
for (int i = start; i < choices.size(); i++) {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if (target - choices[i] < 0) {
break;
}
// 尝试:做出选择,更新 target, start
state.push_back(choices[i]);
// 进行下一轮选择
backtrack(state, target - choices[i], choices, i, res);
// 回退:撤销选择,恢复到之前的状态
state.pop_back();
}
}
/* 求解子集和 I */
vector<vector<int>> subsetSumI(vector<int> &nums, int target) {
vector<int> state; // 状态(子集)
sort(nums.begin(), nums.end()); // 对 nums 进行排序
int start = 0; // 遍历起始点
vector<vector<int>> res; // 结果列表(子集列表)
backtrack(state, target, nums, start, res);
return res;
}
/* 回溯算法:子集和 I */
void backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {
// 子集和等于 target 时,记录解
if (target == 0) {
res.add(new ArrayList<>(state));
return;
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
for (int i = start; i < choices.length; i++) {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if (target - choices[i] < 0) {
break;
}
// 尝试:做出选择,更新 target, start
state.add(choices[i]);
// 进行下一轮选择
backtrack(state, target - choices[i], choices, i, res);
// 回退:撤销选择,恢复到之前的状态
state.remove(state.size() - 1);
}
}
/* 求解子集和 I */
List<List<Integer>> subsetSumI(int[] nums, int target) {
List<Integer> state = new ArrayList<>(); // 状态(子集)
Arrays.sort(nums); // 对 nums 进行排序
int start = 0; // 遍历起始点
List<List<Integer>> res = new ArrayList<>(); // 结果列表(子集列表)
backtrack(state, target, nums, start, res);
return res;
}
/* 回溯算法:子集和 I */
void Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {
// 子集和等于 target 时,记录解
if (target == 0) {
res.Add(new List<int>(state));
return;
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
for (int i = start; i < choices.Length; i++) {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if (target - choices[i] < 0) {
break;
}
// 尝试:做出选择,更新 target, start
state.Add(choices[i]);
// 进行下一轮选择
Backtrack(state, target - choices[i], choices, i, res);
// 回退:撤销选择,恢复到之前的状态
state.RemoveAt(state.Count - 1);
}
}
/* 求解子集和 I */
List<List<int>> SubsetSumI(int[] nums, int target) {
List<int> state = []; // 状态(子集)
Array.Sort(nums); // 对 nums 进行排序
int start = 0; // 遍历起始点
List<List<int>> res = []; // 结果列表(子集列表)
Backtrack(state, target, nums, start, res);
return res;
}
/* 回溯算法:子集和 I */
func backtrackSubsetSumI(start, target int, state, choices *[]int, res *[][]int) {
// 子集和等于 target 时,记录解
if target == 0 {
newState := append([]int{}, *state...)
*res = append(*res, newState)
return
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
for i := start; i < len(*choices); i++ {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if target-(*choices)[i] < 0 {
break
}
// 尝试:做出选择,更新 target, start
*state = append(*state, (*choices)[i])
// 进行下一轮选择
backtrackSubsetSumI(i, target-(*choices)[i], state, choices, res)
// 回退:撤销选择,恢复到之前的状态
*state = (*state)[:len(*state)-1]
}
}
/* 求解子集和 I */
func subsetSumI(nums []int, target int) [][]int {
state := make([]int, 0) // 状态(子集)
sort.Ints(nums) // 对 nums 进行排序
start := 0 // 遍历起始点
res := make([][]int, 0) // 结果列表(子集列表)
backtrackSubsetSumI(start, target, &state, &nums, &res)
return res
}
/* 回溯算法:子集和 I */
func backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {
// 子集和等于 target 时,记录解
if target == 0 {
res.append(state)
return
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
for i in choices.indices.dropFirst(start) {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if target - choices[i] < 0 {
break
}
// 尝试:做出选择,更新 target, start
state.append(choices[i])
// 进行下一轮选择
backtrack(state: &state, target: target - choices[i], choices: choices, start: i, res: &res)
// 回退:撤销选择,恢复到之前的状态
state.removeLast()
}
}
/* 求解子集和 I */
func subsetSumI(nums: [Int], target: Int) -> [[Int]] {
var state: [Int] = [] // 状态(子集)
let nums = nums.sorted() // 对 nums 进行排序
let start = 0 // 遍历起始点
var res: [[Int]] = [] // 结果列表(子集列表)
backtrack(state: &state, target: target, choices: nums, start: start, res: &res)
return res
}
/* 回溯算法:子集和 I */
function backtrack(state, target, choices, start, res) {
// 子集和等于 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, target) {
const state = []; // 状态(子集)
nums.sort((a, b) => a - b); // 对 nums 进行排序
const start = 0; // 遍历起始点
const res = []; // 结果列表(子集列表)
backtrack(state, target, nums, start, res);
return res;
}
/* 回溯算法:子集和 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;
}
/* 回溯算法:子集和 I */
void backtrack(
List<int> state,
int target,
List<int> choices,
int start,
List<List<int>> res,
) {
// 子集和等于 target 时,记录解
if (target == 0) {
res.add(List.from(state));
return;
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
for (int i = start; i < choices.length; i++) {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if (target - choices[i] < 0) {
break;
}
// 尝试:做出选择,更新 target, start
state.add(choices[i]);
// 进行下一轮选择
backtrack(state, target - choices[i], choices, i, res);
// 回退:撤销选择,恢复到之前的状态
state.removeLast();
}
}
/* 求解子集和 I */
List<List<int>> subsetSumI(List<int> nums, int target) {
List<int> state = []; // 状态(子集)
nums.sort(); // 对 nums 进行排序
int start = 0; // 遍历起始点
List<List<int>> res = []; // 结果列表(子集列表)
backtrack(state, target, nums, start, res);
return res;
}
/* 回溯算法:子集和 I */
fn backtrack(
mut state: Vec<i32>,
target: i32,
choices: &[i32],
start: usize,
res: &mut Vec<Vec<i32>>,
) {
// 子集和等于 target 时,记录解
if target == 0 {
res.push(state);
return;
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
for i in start..choices.len() {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if target - choices[i] < 0 {
break;
}
// 尝试:做出选择,更新 target, start
state.push(choices[i]);
// 进行下一轮选择
backtrack(state.clone(), target - choices[i], choices, i, res);
// 回退:撤销选择,恢复到之前的状态
state.pop();
}
}
/* 求解子集和 I */
fn subset_sum_i(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {
let state = Vec::new(); // 状态(子集)
nums.sort(); // 对 nums 进行排序
let start = 0; // 遍历起始点
let mut res = Vec::new(); // 结果列表(子集列表)
backtrack(state, target, nums, start, &mut res);
res
}
/* 回溯算法:子集和 I */
void backtrack(int target, int *choices, int choicesSize, int start) {
// 子集和等于 target 时,记录解
if (target == 0) {
for (int i = 0; i < stateSize; ++i) {
res[resSize][i] = state[i];
}
resColSizes[resSize++] = stateSize;
return;
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
for (int i = start; i < choicesSize; i++) {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if (target - choices[i] < 0) {
break;
}
// 尝试:做出选择,更新 target, start
state[stateSize] = choices[i];
stateSize++;
// 进行下一轮选择
backtrack(target - choices[i], choices, choicesSize, i);
// 回退:撤销选择,恢复到之前的状态
stateSize--;
}
}
/* 求解子集和 I */
void subsetSumI(int *nums, int numsSize, int target) {
qsort(nums, numsSize, sizeof(int), cmp); // 对 nums 进行排序
int start = 0; // 遍历起始点
backtrack(target, nums, numsSize, start);
}
/* 回溯算法:子集和 I */
fun backtrack(
state: MutableList<Int>,
target: Int,
choices: IntArray,
start: Int,
res: MutableList<MutableList<Int>?>
) {
// 子集和等于 target 时,记录解
if (target == 0) {
res.add(state.toMutableList())
return
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
for (i in start..<choices.size) {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if (target - choices[i] < 0) {
break
}
// 尝试:做出选择,更新 target, start
state.add(choices[i])
// 进行下一轮选择
backtrack(state, target - choices[i], choices, i, res)
// 回退:撤销选择,恢复到之前的状态
state.removeAt(state.size - 1)
}
}
/* 求解子集和 I */
fun subsetSumI(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {
val state = mutableListOf<Int>() // 状态(子集)
nums.sort() // 对 nums 进行排序
val start = 0 // 遍历起始点
val res = mutableListOf<MutableList<Int>?>() // 结果列表(子集列表)
backtrack(state, target, nums, start, res)
return res
}
Code Visualization
Figure 13-12 shows the overall backtracking process after inputting the array \([3, 4, 5]\) and target element \(9\) into the above code.
Figure 13-12 Subset sum I backtracking process
13.3.2 Considering cases with duplicate elements¶
Question
Given an array of positive integers nums
and a target positive integer target
, find all possible combinations such that the sum of the elements in the combination equals target
. The given array may contain duplicate elements, and each element can only be chosen once. Please return these combinations as a list, which should not contain duplicate combinations.
Compared to the previous question, this question's input array may contain duplicate elements, introducing new problems. For example, given the array \([4, \hat{4}, 5]\) and target element \(9\), the existing code's output results in \([4, 5], [\hat{4}, 5]\), resulting in duplicate subsets.
The reason for this duplication is that equal elements are chosen multiple times in a certain round. In Figure 13-13, the first round has three choices, two of which are \(4\), generating two duplicate search branches, thus outputting duplicate subsets; similarly, the two \(4\)s in the second round also produce duplicate subsets.
Figure 13-13 Duplicate subsets caused by equal elements
1. Equal element pruning¶
To solve this issue, we need to limit equal elements to being chosen only once per round. The implementation is quite clever: since the array is sorted, equal elements are adjacent. This means that in a certain round of choices, if the current element is equal to its left-hand element, it means it has already been chosen, so skip the current element directly.
At the same time, this question stipulates that each array element can only be chosen once. Fortunately, we can also use the variable start
to meet this constraint: after making the choice \(x_{i}\), set the next round to start from index \(i + 1\) going forward. This not only eliminates duplicate subsets but also avoids repeated selection of elements.
2. Code implementation¶
def backtrack(
state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]
):
"""回溯算法:子集和 II"""
# 子集和等于 target 时,记录解
if target == 0:
res.append(list(state))
return
# 遍历所有选择
# 剪枝二:从 start 开始遍历,避免生成重复子集
# 剪枝三:从 start 开始遍历,避免重复选择同一元素
for i in range(start, len(choices)):
# 剪枝一:若子集和超过 target ,则直接结束循环
# 这是因为数组已排序,后边元素更大,子集和一定超过 target
if target - choices[i] < 0:
break
# 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过
if i > start and choices[i] == choices[i - 1]:
continue
# 尝试:做出选择,更新 target, start
state.append(choices[i])
# 进行下一轮选择
backtrack(state, target - choices[i], choices, i + 1, res)
# 回退:撤销选择,恢复到之前的状态
state.pop()
def subset_sum_ii(nums: list[int], target: int) -> list[list[int]]:
"""求解子集和 II"""
state = [] # 状态(子集)
nums.sort() # 对 nums 进行排序
start = 0 # 遍历起始点
res = [] # 结果列表(子集列表)
backtrack(state, target, nums, start, res)
return res
/* 回溯算法:子集和 II */
void backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {
// 子集和等于 target 时,记录解
if (target == 0) {
res.push_back(state);
return;
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
// 剪枝三:从 start 开始遍历,避免重复选择同一元素
for (int i = start; i < choices.size(); i++) {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if (target - choices[i] < 0) {
break;
}
// 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过
if (i > start && choices[i] == choices[i - 1]) {
continue;
}
// 尝试:做出选择,更新 target, start
state.push_back(choices[i]);
// 进行下一轮选择
backtrack(state, target - choices[i], choices, i + 1, res);
// 回退:撤销选择,恢复到之前的状态
state.pop_back();
}
}
/* 求解子集和 II */
vector<vector<int>> subsetSumII(vector<int> &nums, int target) {
vector<int> state; // 状态(子集)
sort(nums.begin(), nums.end()); // 对 nums 进行排序
int start = 0; // 遍历起始点
vector<vector<int>> res; // 结果列表(子集列表)
backtrack(state, target, nums, start, res);
return res;
}
/* 回溯算法:子集和 II */
void backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {
// 子集和等于 target 时,记录解
if (target == 0) {
res.add(new ArrayList<>(state));
return;
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
// 剪枝三:从 start 开始遍历,避免重复选择同一元素
for (int i = start; i < choices.length; i++) {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if (target - choices[i] < 0) {
break;
}
// 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过
if (i > start && choices[i] == choices[i - 1]) {
continue;
}
// 尝试:做出选择,更新 target, start
state.add(choices[i]);
// 进行下一轮选择
backtrack(state, target - choices[i], choices, i + 1, res);
// 回退:撤销选择,恢复到之前的状态
state.remove(state.size() - 1);
}
}
/* 求解子集和 II */
List<List<Integer>> subsetSumII(int[] nums, int target) {
List<Integer> state = new ArrayList<>(); // 状态(子集)
Arrays.sort(nums); // 对 nums 进行排序
int start = 0; // 遍历起始点
List<List<Integer>> res = new ArrayList<>(); // 结果列表(子集列表)
backtrack(state, target, nums, start, res);
return res;
}
/* 回溯算法:子集和 II */
void Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {
// 子集和等于 target 时,记录解
if (target == 0) {
res.Add(new List<int>(state));
return;
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
// 剪枝三:从 start 开始遍历,避免重复选择同一元素
for (int i = start; i < choices.Length; i++) {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if (target - choices[i] < 0) {
break;
}
// 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过
if (i > start && choices[i] == choices[i - 1]) {
continue;
}
// 尝试:做出选择,更新 target, start
state.Add(choices[i]);
// 进行下一轮选择
Backtrack(state, target - choices[i], choices, i + 1, res);
// 回退:撤销选择,恢复到之前的状态
state.RemoveAt(state.Count - 1);
}
}
/* 求解子集和 II */
List<List<int>> SubsetSumII(int[] nums, int target) {
List<int> state = []; // 状态(子集)
Array.Sort(nums); // 对 nums 进行排序
int start = 0; // 遍历起始点
List<List<int>> res = []; // 结果列表(子集列表)
Backtrack(state, target, nums, start, res);
return res;
}
/* 回溯算法:子集和 II */
func backtrackSubsetSumII(start, target int, state, choices *[]int, res *[][]int) {
// 子集和等于 target 时,记录解
if target == 0 {
newState := append([]int{}, *state...)
*res = append(*res, newState)
return
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
// 剪枝三:从 start 开始遍历,避免重复选择同一元素
for i := start; i < len(*choices); i++ {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if target-(*choices)[i] < 0 {
break
}
// 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过
if i > start && (*choices)[i] == (*choices)[i-1] {
continue
}
// 尝试:做出选择,更新 target, start
*state = append(*state, (*choices)[i])
// 进行下一轮选择
backtrackSubsetSumII(i+1, target-(*choices)[i], state, choices, res)
// 回退:撤销选择,恢复到之前的状态
*state = (*state)[:len(*state)-1]
}
}
/* 求解子集和 II */
func subsetSumII(nums []int, target int) [][]int {
state := make([]int, 0) // 状态(子集)
sort.Ints(nums) // 对 nums 进行排序
start := 0 // 遍历起始点
res := make([][]int, 0) // 结果列表(子集列表)
backtrackSubsetSumII(start, target, &state, &nums, &res)
return res
}
/* 回溯算法:子集和 II */
func backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {
// 子集和等于 target 时,记录解
if target == 0 {
res.append(state)
return
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
// 剪枝三:从 start 开始遍历,避免重复选择同一元素
for i in choices.indices.dropFirst(start) {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if target - choices[i] < 0 {
break
}
// 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过
if i > start, choices[i] == choices[i - 1] {
continue
}
// 尝试:做出选择,更新 target, start
state.append(choices[i])
// 进行下一轮选择
backtrack(state: &state, target: target - choices[i], choices: choices, start: i + 1, res: &res)
// 回退:撤销选择,恢复到之前的状态
state.removeLast()
}
}
/* 求解子集和 II */
func subsetSumII(nums: [Int], target: Int) -> [[Int]] {
var state: [Int] = [] // 状态(子集)
let nums = nums.sorted() // 对 nums 进行排序
let start = 0 // 遍历起始点
var res: [[Int]] = [] // 结果列表(子集列表)
backtrack(state: &state, target: target, choices: nums, start: start, res: &res)
return res
}
/* 回溯算法:子集和 II */
function backtrack(state, target, choices, start, res) {
// 子集和等于 target 时,记录解
if (target === 0) {
res.push([...state]);
return;
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
// 剪枝三:从 start 开始遍历,避免重复选择同一元素
for (let i = start; i < choices.length; i++) {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if (target - choices[i] < 0) {
break;
}
// 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过
if (i > start && choices[i] === choices[i - 1]) {
continue;
}
// 尝试:做出选择,更新 target, start
state.push(choices[i]);
// 进行下一轮选择
backtrack(state, target - choices[i], choices, i + 1, res);
// 回退:撤销选择,恢复到之前的状态
state.pop();
}
}
/* 求解子集和 II */
function subsetSumII(nums, target) {
const state = []; // 状态(子集)
nums.sort((a, b) => a - b); // 对 nums 进行排序
const start = 0; // 遍历起始点
const res = []; // 结果列表(子集列表)
backtrack(state, target, nums, start, res);
return res;
}
/* 回溯算法:子集和 II */
function backtrack(
state: number[],
target: number,
choices: number[],
start: number,
res: number[][]
): void {
// 子集和等于 target 时,记录解
if (target === 0) {
res.push([...state]);
return;
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
// 剪枝三:从 start 开始遍历,避免重复选择同一元素
for (let i = start; i < choices.length; i++) {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if (target - choices[i] < 0) {
break;
}
// 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过
if (i > start && choices[i] === choices[i - 1]) {
continue;
}
// 尝试:做出选择,更新 target, start
state.push(choices[i]);
// 进行下一轮选择
backtrack(state, target - choices[i], choices, i + 1, res);
// 回退:撤销选择,恢复到之前的状态
state.pop();
}
}
/* 求解子集和 II */
function subsetSumII(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;
}
/* 回溯算法:子集和 II */
void backtrack(
List<int> state,
int target,
List<int> choices,
int start,
List<List<int>> res,
) {
// 子集和等于 target 时,记录解
if (target == 0) {
res.add(List.from(state));
return;
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
// 剪枝三:从 start 开始遍历,避免重复选择同一元素
for (int i = start; i < choices.length; i++) {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if (target - choices[i] < 0) {
break;
}
// 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过
if (i > start && choices[i] == choices[i - 1]) {
continue;
}
// 尝试:做出选择,更新 target, start
state.add(choices[i]);
// 进行下一轮选择
backtrack(state, target - choices[i], choices, i + 1, res);
// 回退:撤销选择,恢复到之前的状态
state.removeLast();
}
}
/* 求解子集和 II */
List<List<int>> subsetSumII(List<int> nums, int target) {
List<int> state = []; // 状态(子集)
nums.sort(); // 对 nums 进行排序
int start = 0; // 遍历起始点
List<List<int>> res = []; // 结果列表(子集列表)
backtrack(state, target, nums, start, res);
return res;
}
/* 回溯算法:子集和 II */
fn backtrack(
mut state: Vec<i32>,
target: i32,
choices: &[i32],
start: usize,
res: &mut Vec<Vec<i32>>,
) {
// 子集和等于 target 时,记录解
if target == 0 {
res.push(state);
return;
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
// 剪枝三:从 start 开始遍历,避免重复选择同一元素
for i in start..choices.len() {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if target - choices[i] < 0 {
break;
}
// 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过
if i > start && choices[i] == choices[i - 1] {
continue;
}
// 尝试:做出选择,更新 target, start
state.push(choices[i]);
// 进行下一轮选择
backtrack(state.clone(), target - choices[i], choices, i, res);
// 回退:撤销选择,恢复到之前的状态
state.pop();
}
}
/* 求解子集和 II */
fn subset_sum_ii(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {
let state = Vec::new(); // 状态(子集)
nums.sort(); // 对 nums 进行排序
let start = 0; // 遍历起始点
let mut res = Vec::new(); // 结果列表(子集列表)
backtrack(state, target, nums, start, &mut res);
res
}
/* 回溯算法:子集和 II */
void backtrack(int target, int *choices, int choicesSize, int start) {
// 子集和等于 target 时,记录解
if (target == 0) {
for (int i = 0; i < stateSize; i++) {
res[resSize][i] = state[i];
}
resColSizes[resSize++] = stateSize;
return;
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
// 剪枝三:从 start 开始遍历,避免重复选择同一元素
for (int i = start; i < choicesSize; i++) {
// 剪枝一:若子集和超过 target ,则直接跳过
if (target - choices[i] < 0) {
continue;
}
// 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过
if (i > start && choices[i] == choices[i - 1]) {
continue;
}
// 尝试:做出选择,更新 target, start
state[stateSize] = choices[i];
stateSize++;
// 进行下一轮选择
backtrack(target - choices[i], choices, choicesSize, i + 1);
// 回退:撤销选择,恢复到之前的状态
stateSize--;
}
}
/* 求解子集和 II */
void subsetSumII(int *nums, int numsSize, int target) {
// 对 nums 进行排序
qsort(nums, numsSize, sizeof(int), cmp);
// 开始回溯
backtrack(target, nums, numsSize, 0);
}
/* 回溯算法:子集和 II */
fun backtrack(
state: MutableList<Int>,
target: Int,
choices: IntArray,
start: Int,
res: MutableList<MutableList<Int>?>
) {
// 子集和等于 target 时,记录解
if (target == 0) {
res.add(state.toMutableList())
return
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
// 剪枝三:从 start 开始遍历,避免重复选择同一元素
for (i in start..<choices.size) {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if (target - choices[i] < 0) {
break
}
// 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过
if (i > start && choices[i] == choices[i - 1]) {
continue
}
// 尝试:做出选择,更新 target, start
state.add(choices[i])
// 进行下一轮选择
backtrack(state, target - choices[i], choices, i + 1, res)
// 回退:撤销选择,恢复到之前的状态
state.removeAt(state.size - 1)
}
}
/* 求解子集和 II */
fun subsetSumII(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {
val state = mutableListOf<Int>() // 状态(子集)
nums.sort() // 对 nums 进行排序
val start = 0 // 遍历起始点
val res = mutableListOf<MutableList<Int>?>() // 结果列表(子集列表)
backtrack(state, target, nums, start, res)
return res
}
Code Visualization
Figure 13-14 shows the backtracking process for the array \([4, 4, 5]\) and target element \(9\), including four types of pruning operations. Please combine the illustration with the code comments to understand the entire search process and how each type of pruning operation works.
Figure 13-14 Subset sum II backtracking process