解决排列和子集问题的回溯算法
最编程
2024-10-04 14:59:44
...
216. 组合总和 III |
39. 组合总和 |
40. 组合总和 II |
46. 全排列 |
47. 全排列 II |
77. 组合 |
78. 子集 |
90. 子集 II |
以上是力扣设计相关问题的题目。排列组合还是子集问题无非就是从序列 nums
中以给定规则取若干元素,主要有以下几类:
- 元素无重不可复选,即
nums
中的元素都是唯一的,每个元素最多只能被使用一次,这也是最基本的形式。 - 元素可重不可复选,即
nums
中的元素可以存在重复,每个元素最多只能被使用一次。 - 元素无重可复选,即
nums
中的元素都是唯一的,每个元素可以被使用若干次。
以组合为例:
1.如果输入
nums = [2,3,6,7]
,和为 7 的组合应该只有[7]
2.如果输入
nums = [2,5,2,1,2]
,和为 7 的组合应该有两种[2,2,2,1]
和[5,2]
3.如果输入
nums = [2,3,6,7]
,和为 7 的组合应该有两种[2,2,3]
和[7]
上面用组合问题举的例子,但排列、组合、子集问题都可以有这三种基本形式,所以共有 9 种变化。
除此之外,题目也可以再添加各种限制条件,比如让你求和为 target
且元素个数为 k
的组合,那这么一来又可以衍生出一堆变体,所以一般笔试很喜欢出这种题。
但无论怎么变化,其本质就是穷举所有解,而这些解呈现树形结构,使用回溯算法框架再稍微修改一些细节即可把这些问题一网打尽。
回溯算法框架代码如下:
import java.util.ArrayList;
import java.util.List;
public class BacktrackExample {
private List<List<Object>> result = new ArrayList<>();
public void backtrack(List<Object> path, List<Object> choices) {
if (满足结束条件(path)) {
result.add(new ArrayList<>(path));
return;
}
for (Object choice : choices) {
// 做选择
path.add(choice);
// 递归
backtrack(path, choices);
// 撤销选择
path.remove(path.size() - 1);
}
}
private boolean 满足结束条件(List<Object> path) {
// 这里实现满足结束条件的逻辑
return false; // 示例返回,替换为实际逻辑
}
public List<List<Object>> getResult() {
return result;
}
}
问题一:当元素无重不可复选时,即 nums
中的元素都是唯一的,每个元素最多只能被使用一次:
// 组合/子集问题回溯算法框架
void backtrack(int[] nums, int start) {
// 回溯算法标准框架
for (int i = start; i < nums.length; i++) {
// 做选择
track.addLast(nums[i]);
// 注意参数
backtrack(nums, i + 1);
// 撤销选择
track.removeLast();
}
}
// 排列问题回溯算法框架
void backtrack(int[] nums) {
for (int i = 0; i < nums.length; i++) {
// 剪枝逻辑
if (used[i]) {
continue;
}
// 做选择
used[i] = true;
track.addLast(nums[i]);
backtrack(nums);
// 撤销选择
track.removeLast();
used[i] = false;
}
}
问题二:元素可重不可复选,即 nums
中的元素可以存在重复,每个元素最多只能被使用一次,其关键在于排序和剪枝
Arrays.sort(nums);
// 组合/子集问题回溯算法框架
void backtrack(int[] nums, int start) {
// 回溯算法标准框架
for (int i = start; i < nums.length; i++) {
// 剪枝逻辑,跳过值相同的相邻树枝
if (i > start && nums[i] == nums[i - 1]) {
continue;
}
// 做选择
track.addLast(nums[i]);
// 注意参数
backtrack(nums, i + 1);
// 撤销选择
track.removeLast();
}
}
Arrays.sort(nums);
// 排列问题回溯算法框架
void backtrack(int[] nums) {
for (int i = 0; i < nums.length; i++) {
// 剪枝逻辑
if (used[i]) {
continue;
}
// 剪枝逻辑,固定相同的元素在排列中的相对位置
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
continue;
}
// 做选择
used[i] = true;
track.addLast(nums[i]);
backtrack(nums);
// 撤销选择
track.removeLast();
used[i] = false;
}
}
问题三:元素无重可复选,即 nums
中的元素都是唯一的,每个元素可以被使用若干次,只要删掉去重逻辑即可:
// 组合/子集问题回溯算法框架
void backtrack(int[] nums, int start) {
// 回溯算法标准框架
for (int i = start; i < nums.length; i++) {
// 做选择
track.addLast(nums[i]);
// 注意参数
backtrack(nums, i);
// 撤销选择
track.removeLast();
}
}
// 排列问题回溯算法框架
void backtrack(int[] nums) {
for (int i = 0; i < nums.length; i++) {
// 做选择
track.addLast(nums[i]);
backtrack(nums);
// 撤销选择
track.removeLast();
}
}
只要从树的角度思考,这些问题看似复杂多变,实则改改 base case 就能解决。只要熟悉了该框架,再细致了解一下细节问题,相信排列组合子集问题都不是问题。
推荐阅读
-
解决排列和子集问题的回溯算法
-
使用分层图和最短路径算法解决BZOJ 2763:JLOI2011的飞行路线问题
-
用回溯法解决0-1背包问题的实战算法与完整代码示例
-
01 关于knapsack问题的动态编程算法和回溯算法的比较研究
-
组合数学--排列和组合--一个混淆点、关于连续抽签数字问题的解决方案、循环数列与 n 次无向完整图的哈密顿循环之间的关系
-
回溯的综合分析:算法框架与问题解决
-
一篇解释回溯算法如何解决拆分码垛问题的文章 | Java Brushups
-
[哈里-霍克算法(NCHHO)的改进]利用混沌和非线性控制参数改进哈里-霍克算法的优化性能,以解决与远程信息处理相关的路由问题(Matlab 代码实现)
-
CMS 和 G1 缺失标记问题的解决方案及三色标记算法图解
-
问题解决思路的贪婪算法、回溯算法和动态编程分析 - 决策方法