本文是在刷题的过程中,对此类型的算法题进行的一个浅显的总结。故而,本文不在对概念进行解释,主要集中在解题的方法和思路上。
总的来说,回溯算法解题的过程,就是在决策树上遍历的过程。
问题类型
对于排列、组合、子集问题,我们可以根据题目的限制条件“元素是否重复”、“是否允许重复选择”,大致的将其分为三类:
①元素无重不可复选,即集合中的元素都是唯一的,每个元素最多只能被使用一次;
②元素可重不可复选,即集合中的元素可以存在重复,每个元素最多只能被使用一次;
③元素无重可复选,即集合中的元素都是唯一的,每个元素可以被使用若干次;
对于元素可以重复、可复选的情况,由于可复选,我们可以对其去重之后计算,此时等同于情况三
以上分类参考:【一文秒杀所有排列组合子集问题】
下图就是对于所有情况的搜索树:

从根节点到叶子节点的每一条路径就是对应的一个解(严格来说,应该是从根节点的儿子节点到叶子节点的路径,因为根节点代表原始的集合)
解题模板
回溯法的基本模板总结如下:
private void backtrack(选择列表){
// 1、判断是否满足结束条件,若满足,则添加进入结果集
// 2、遍历剩余可选择的列表
for (选择 in 选择列表) {
// 3、选择条件(视题意而定),是否选择当前的“选择”节点
// 4、递归调用
backtrack(选择列表);
// 5、撤销上一步的选择,回溯到上一步
}
}
问题分析
总的来看,对于不同的情况只需要更改回溯算法模板的步骤1、步骤2(for循环中的条件)以及步骤3 即可。现在,我们具体分析上文所说的三种情况,以及在三种不同的情况中如何去使用解题模板。
①元素无重不可复选
以 Leetcode 78题 为例:
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。 解集 不能包含重复的子集。你可以按 任意顺序 返回解集。
这种情况就属于最基本的情况,我们可以传递一个参数:当前元素的索引,令下一次搜索的索引为当前的索引+1,使得每次只遍历集合中当前索引之后的值从而避免重复选择(产生重复子集)。其余使用模板代入即可。
class Solution {
private LinkedList<Integer> list = new LinkedList<>();
private List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
backtrack(nums, 0);
return res;
}
// 回溯
private void backtrack(int[] nums, int index) {
// 满足条件,添加进入结果集
res.add(new ArrayList<>(list));
for (int i = index; i < nums.length; i++) {
// 选择
list.add(nums[i]);
// 递归调用
backtrack(nums, i + 1);
// 撤销选择
list.removeLast();
}
}
}
此问题的其他变种情况:如获取集合中,所有长度为 n 的子集或和等于 target 的子集。
以长度为 n 为例,长度为 n,即第n次选择的结果集。此时,只需要更改步骤一的结束条件即可,当满足 list.size() == n 时,添加进入结果集。
②元素可重不可复选
以 Leetcode 90题 为例:
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。 解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
此时,我们只需要对相同值的树枝剪枝即可。所以,我们可以先对集合中的元素进行排序,然后只需要判断下一次选择时,相邻的元素是否相等,若相等则跳过,进入下一次循环。
class Solution {
private LinkedList<Integer> list = new LinkedList<>();
private List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
// 先排序
Arrays.sort(nums);
backtrack(nums, 0);
return res;
}
// 回溯法
private void backtrack(int[] nums, int start){
res.add(new ArrayList<>(list));
for (int i = start; i < nums.length; i++) {
// 跳过重复的元素(并且确保边界条件index=0时,不溢出)
if (i> start && nums[i] == nums[i-1]) {
continue;
}
list.add(nums[i]);
backtrack(nums, i+1);
list.removeLast();
}
}
}
对于部分特殊情况,如不允许集合重复,可能还需要额外的剪枝
以 Leetcode 47题 为例:
此题需要通过保证集合中的数据相对固定,来进行去重。比如:集合{1,2,2,3}中,我们可以规定 2 和 2’ 在解集中,只能令 2 在前,2’ 在后。如果,2’ 先出现,则不加入解集中。
所以,我们通过使用额外的空间记录元素的访问标记,通过比较标记从而实现对位置的判定。
class Solution {
private LinkedList<Integer> list = new LinkedList<>();
private List<List<Integer>> res = new LinkedList<>();
private boolean[] flag;
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums);
// 用标记数组标记元素是否使用过
flag = new boolean[nums.length];
backtrack(nums);
return res;
}
private void backtrack(int[] nums){
if (list.size() == nums.length) {
res.add(new ArrayList<>(list));
return;
}
for (int i = 0; i < nums.length; i++) {
// 确保元素不会重复使用
if (flag[i]) {
continue;
}
// 固定相同的元素在排列中的相对位置
// 相同值的两个元素 a 和 a', 只需要保持结果集中的a 排列一定在 a' 以前, 就可以保证集合无重复
if (i > 0 && nums[i] == nums[i-1] && !flag[i-1]) {
continue;
}
list.add(nums[i]);
flag[i] = true;
backtrack(nums);
list.removeLast();
flag[i] = false;
}
}
}
③元素无重可复选
以 Leetcode 39题 为例:
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。 candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对照上文第一种情况,我们是通过控制 index+1,令得下一次搜索只遍历集合中当前索引之后的值从而避免重复选择,所以,反过来,如果我们想让当前值被重复使用,只需要把传递的参数 index +1 -> index即可重复使用当前值。
class Solution {
private LinkedList<Integer> list = new LinkedList<>();
private List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backtrack(candidates, 0, target);
return res;
}
// 回溯法
private void backtrack(int[] nums, int start, int target){
// 结束条件
if (target == 0) {
res.add(new LinkedList<>(list));
return;
}
if(target < 0){
return;
}
for (int i = start; i < nums.length; i++) {
// 选择
list.add(nums[i]);
backtrack(nums, i, target-nums[i]);
// 撤销选择
list.removeLast();
}
}
}
总的来说,不论那种情况,基本上只需要在模板的基础上更改步骤1:结束条件和步骤3:选择当前节点即可。
回溯算法详解:排列组合子集问题

本文详细介绍了如何使用回溯算法解决排列、组合、子集问题,包括元素无重不可复选、可重不可复选及无重可复选三种情况,并通过LeetCode题目实例解析解题模板及其变种。同时,文章提供了具体的Java代码实现,帮助读者理解并掌握回溯算法在不同场景下的应用。
1万+

被折叠的 条评论
为什么被折叠?



