|
|
@ -1,12 +1,14 @@
|
|
|
|
# 子集和问题
|
|
|
|
# 子集和问题
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 无重复元素的情况
|
|
|
|
|
|
|
|
|
|
|
|
!!! question
|
|
|
|
!!! question
|
|
|
|
|
|
|
|
|
|
|
|
给定一个正整数数组 `nums` 和一个目标正整数 `target` ,请找出所有可能的组合,使得组合中的元素和等于 `target` 。给定数组无重复元素,每个元素可以被选取多次。请以列表形式返回这些组合,列表中不应包含重复组合。
|
|
|
|
给定一个正整数数组 `nums` 和一个目标正整数 `target` ,请找出所有可能的组合,使得组合中的元素和等于 `target` 。给定数组无重复元素,每个元素可以被选取多次。请以列表形式返回这些组合,列表中不应包含重复组合。
|
|
|
|
|
|
|
|
|
|
|
|
例如,输入集合 $\{3, 4, 5\}$ 和目标整数 $9$ ,由于集合中的数字可以被重复选取,因此解为 $\{3, 3, 3\}, \{4, 5\}$ 。请注意,子集是不区分元素顺序的,例如 $\{4, 5\}$ 和 $\{5, 4\}$ 是同一个子集。
|
|
|
|
例如,输入集合 $\{3, 4, 5\}$ 和目标整数 $9$ ,由于集合中的数字可以被重复选取,因此解为 $\{3, 3, 3\}, \{4, 5\}$ 。请注意,子集是不区分元素顺序的,例如 $\{4, 5\}$ 和 $\{5, 4\}$ 是同一个子集。
|
|
|
|
|
|
|
|
|
|
|
|
## 从全排列引出解法
|
|
|
|
### 从全排列引出解法
|
|
|
|
|
|
|
|
|
|
|
|
类似于上节全排列问题的解法,我们可以把子集的生成过程想象成一系列选择的结果,并在选择过程中实时更新“元素和”,当元素和等于 `target` 时,就将子集记录至结果列表。
|
|
|
|
类似于上节全排列问题的解法,我们可以把子集的生成过程想象成一系列选择的结果,并在选择过程中实时更新“元素和”,当元素和等于 `target` 时,就将子集记录至结果列表。
|
|
|
|
|
|
|
|
|
|
|
@ -104,7 +106,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
![子集搜索与越界剪枝](subset_sum_problem.assets/subset_sum_i_naive.png)
|
|
|
|
![子集搜索与越界剪枝](subset_sum_problem.assets/subset_sum_i_naive.png)
|
|
|
|
|
|
|
|
|
|
|
|
## 重复子集剪枝
|
|
|
|
### 重复子集剪枝
|
|
|
|
|
|
|
|
|
|
|
|
为了去除重复子集,**一种直接的思路是对结果列表进行去重**。但这个方法效率很低,因为:
|
|
|
|
为了去除重复子集,**一种直接的思路是对结果列表进行去重**。但这个方法效率很低,因为:
|
|
|
|
|
|
|
|
|
|
|
@ -121,6 +123,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
总结来看,给定输入数组 $[x_1, x_2, \cdots, x_n]$ ,设搜索过程中的选择序列为 $[x_{i_1}, x_{i_2}, \cdots , x_{i_m}]$ ,则该选择序列需要满足 $i_1 \leq i_2 \leq \cdots \leq i_m$ 。**不满足该条件的选择序列都是重复子集**。
|
|
|
|
总结来看,给定输入数组 $[x_1, x_2, \cdots, x_n]$ ,设搜索过程中的选择序列为 $[x_{i_1}, x_{i_2}, \cdots , x_{i_m}]$ ,则该选择序列需要满足 $i_1 \leq i_2 \leq \cdots \leq i_m$ 。**不满足该条件的选择序列都是重复子集**。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 代码实现
|
|
|
|
|
|
|
|
|
|
|
|
为实现该剪枝,我们初始化变量 `start` ,用于指示遍历起点。**当做出选择 $x_{i}$ 后,设定下一轮从索引 $i$ 开始遍历**,从而完成子集去重。
|
|
|
|
为实现该剪枝,我们初始化变量 `start` ,用于指示遍历起点。**当做出选择 $x_{i}$ 后,设定下一轮从索引 $i$ 开始遍历**,从而完成子集去重。
|
|
|
|
|
|
|
|
|
|
|
|
除此之外,我们还对代码进行了两项优化。首先,我们在开启搜索前将数组 `nums` 排序,在搜索过程中,**当子集和超过 `target` 时直接结束循环**,因为后边的元素更大,其子集和都一定会超过 `target` 。其次,**我们通过在 `target` 上执行减法来统计元素和**,当 `target` 等于 $0$ 时记录解,省去了元素和变量 `total` 。
|
|
|
|
除此之外,我们还对代码进行了两项优化。首先,我们在开启搜索前将数组 `nums` 排序,在搜索过程中,**当子集和超过 `target` 时直接结束循环**,因为后边的元素更大,其子集和都一定会超过 `target` 。其次,**我们通过在 `target` 上执行减法来统计元素和**,当 `target` 等于 $0$ 时记录解,省去了元素和变量 `total` 。
|
|
|
@ -217,7 +221,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
![子集和 I 回溯过程](subset_sum_problem.assets/subset_sum_i.png)
|
|
|
|
![子集和 I 回溯过程](subset_sum_problem.assets/subset_sum_i.png)
|
|
|
|
|
|
|
|
|
|
|
|
## 相等元素剪枝
|
|
|
|
## 考虑重复元素的情况
|
|
|
|
|
|
|
|
|
|
|
|
!!! question
|
|
|
|
!!! question
|
|
|
|
|
|
|
|
|
|
|
@ -227,10 +231,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
![相等元素导致的重复子集](subset_sum_problem.assets/subset_sum_ii_repeat.png)
|
|
|
|
![相等元素导致的重复子集](subset_sum_problem.assets/subset_sum_ii_repeat.png)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 相等元素剪枝
|
|
|
|
|
|
|
|
|
|
|
|
为解决此问题,**我们需要限制相等元素在每一轮中只被选择一次**。实现方式比较巧妙:由于数组是已排序的,因此相等元素都是相邻的。利用该特性,在某轮选择中,若当前元素与其左边元素相等,则说明它已经被选择过,因此直接跳过当前元素。
|
|
|
|
为解决此问题,**我们需要限制相等元素在每一轮中只被选择一次**。实现方式比较巧妙:由于数组是已排序的,因此相等元素都是相邻的。利用该特性,在某轮选择中,若当前元素与其左边元素相等,则说明它已经被选择过,因此直接跳过当前元素。
|
|
|
|
|
|
|
|
|
|
|
|
与此同时,**本题规定数组元素只能被选择一次**。幸运的是,我们也可以利用变量 `start` 来满足该约束:当做出选择 $x_{i}$ 后,设定下一轮从索引 $i + 1$ 开始向后遍历。这样即能去除重复子集,也能避免重复选择相等元素。
|
|
|
|
与此同时,**本题规定数组元素只能被选择一次**。幸运的是,我们也可以利用变量 `start` 来满足该约束:当做出选择 $x_{i}$ 后,设定下一轮从索引 $i + 1$ 开始向后遍历。这样即能去除重复子集,也能避免重复选择相等元素。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 代码实现
|
|
|
|
|
|
|
|
|
|
|
|
=== "Java"
|
|
|
|
=== "Java"
|
|
|
|
|
|
|
|
|
|
|
|
```java title="subset_sum_ii.java"
|
|
|
|
```java title="subset_sum_ii.java"
|
|
|
|