回溯:排列、组合、子集相关问题总结
回溯:排列、组合、子集相关问题总结
回溯算法与深度优先遍历
以下是维基百科中「回溯算法」和「深度优先遍历」的定义。回溯法 采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
- 找到一个可能存在的正确的答案;
- 在尝试了所有可能的分步方法后宣告该问题没有答案。
深度优先搜索 算法(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。这个算法会 尽可能深 的搜索树的分支。当结点 v 的所在边v都己被探寻过,搜索将 回溯 到发现结点 v 的那条边的起始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被访问为止。
我刚开始学习「回溯算法」的时候觉得很抽象,一直不能理解为什么递归之后需要做和递归之前相同的逆向操作,在做了很多相关的问题以后,我发现其实「回溯算法」与「 深度优先遍历 」有着千丝万缕的联系。
「回溯算法」与「深度优先遍历」都有「不撞南墙不回头」的意思。我个人的理解是:「回溯算法」强调了「深度优先遍历」思想的用途,用一个 不断变化 的变量,在尝试各种可能的过程中,搜索需要的结果。强调了 回退 操作对于搜索的合理性。
在我看来,回溯其实就是递归 + 深度优先遍历 + 剪枝。递归和深度优先遍历已经有很多优质文章解释了,就不再赘述了,我想说说我对剪枝的理解。
剪枝
-
如何剪枝:使用
used
数组还是begin
变量?都是用来剪重复数字的枝,着这两个有什么区别呢?used
数组:- 一个
boolean
数组,用来标记nums
中对应元素是否已经使用过。 - 用于全排列问题中,对于这一类问题,我们是考虑数字顺序的,即[1,2,3]和[3,2,1]是两个不同的排列。它剪去的其实是当前层数以上的祖先节点,比如当我们第一位取了2时,2、3位只能取1和3,不能再取1了。
- 一个
begin
变量- 一个
int
类型,在递归时根据题意来决定下一层函数的选择列表中元素的起始位置,如果可重复即为begin
,不可重复为begin+1
- 用于组合问题中,对于这一类问题,我们是考虑数字顺序的,即[1,2,3]和[3,2,1]是两个相同的组合。它相对于
used
数组,剪去了更多的枝,这也就是我们都知道的排列的个数比组合个数多。因为它是控制每层对选择列表的起始位置,所以它不仅剪去了当前节点的祖先节点,还剪去了当祖先节点的左兄弟,这就是begin
在起作用。
- 一个
-
begin
- 剪父节点和父节点的兄弟节点,排在它前面的元素都被剪掉
-
used
- 剪父节点和兄弟节点
-
重复节点两者都可
-
剪枝前需要考虑是否要对
nums
进行排序,剪重复节点是必须要进行排序的。
模板
以下是46.全排列的模板,其他题目只需要根据题意来选择used
还是begin
,总体上来说都是一样的。
public List<List<Integer>> permute(int[] nums){
int len = nums.length;
List<List<Integer>> res = new ArrayList<>();
if (len == 0){
return res;
}
boolean[] used = new boolean[len];
Deque<Integer> path = new ArrayDeque<>();
dfs(nums, 0, len, used, path, res);
return res;
}
private void dfs(int[] nums, int depth, int len, boolean[] used, Deque<Integer> path, List<List<Integer>> res){
if (depth == len){
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < len; i++){
if (used[i]){
continue;
}
path.addLast(nums[i]);
used[i] = true;
dfs(nums, depth + 1, len, used, path, res);
path.removeLast();
used[i] = false;
}
}
相关题目
(力扣)39.组合总和
(力扣)40.组合总和Ⅱ
(力扣)47.全排列Ⅱ
(力扣)77.组合
(力扣)78.子集
(力扣)90.子集Ⅱ
总结
回溯问题才刚刚开始,没有想象中那么难,本质上还是dfs,继续加油吧!