(力扣官方解释)
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就 “回溯” 返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为 “回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
回溯算法的基本思想是:
从一条路往前走,能进则进,不能进则退回来,换一条路再试。
当找到问题的某个解时,回溯返回。
遍历当前状态的所有子节点,并判断下一状态是否满足条件,若满足则进入下一状态。
(若当前状态不满足条件),则返回到上一状态。
回溯函数体的递归调用。直到所有可能的解已经产生时,递归结束。
题目分析
全排列算是回溯算法的入门级例题,按照回溯算法来分析时,其实回溯找题解的过程便能画出其对应二叉树。
每一步回溯都是相当于二叉树的某个结点的所有子节点已经全部访问,不能继续访问之后便回到该结点的父节点,并继续访问这个父节点的其他子节点。
上图就是按照回溯算法的步骤而产生的二叉树,其过程可以联想一下深度优先搜索或者是二叉树的先序遍历。
回溯函数编写
分析了回溯算法的执行过程之后,该怎样写递归函数主题?
其实在编写函数的时候要回想刚才说过的回溯函数的三大组成部分:回溯出口,回溯主体,状态返回这三个部分。
回溯出口:
对于全排列,当产生出来的一组符合要求的排列的长度达到原先给定的数组长度时,也就相当于产生了一个符合要求的题解,这时便可以结束一次递归并将题解加入到最终结果集中。
//向结果集中添加list时,不能直接将list加入,
// 应该new一个新的list并将list的内容拷贝进去
if (list.size() == len) {
outputs.add(new ArrayList<>(list));
return;
} else {
回溯主体
回溯主体也就是遍历每一个子节点,当下一个结点满足条件时,就可以递归调用回溯函数,如果不满足,就跳过本次循环。
对于全排列,怎样就相当于满足回溯的条件?
对于每一个有效的排列,需要满足的是这个排列中的元素没有“重复”。(这里说的没有重复是说每一个数字在本排列中没有重复被使用)当题设给定的数组中的数字全都不同时,产生的每一个有效排列都是没有重复数字的。
状态返回
根据刚才在二叉树分析“回溯”的条件,也就是某条“路径”走不下去的时候,也就是不满足继续回溯的时候,就要进行状态返回。
所以,我们在状态返回的时候,要做的就是将当前加入list的最后一个元素“移除掉”,就可以返回到“当前结点的父节点”。并继续访问该父节点的其他子节点。
//回溯主体
if(!list.contains(a)){
list.add(a);
backTrack(list,nums);
//回溯(状态返回)
list.remove(list.size()-1);
}
//全排列
public class Permute {
public static void main(String[] args) {
int nums[] = {1,2,3};
Solution46 s = new Solution46();
System.out.println(s.permute(nums));
}
}
class Solution46 {
private List> outputs = new ArrayList<>();
private int len;
private List list1 = new ArrayList<>();
public List> permute(int[] nums) {
len = nums.length;
backTrack(list1,nums);
return outputs;
}
private void backTrack(List list,int[] nums) {
//回溯终止条件
//向结果集中添加list时,不能直接将list加入,
// 应该new一个新的list并将list的内容拷贝进去
if (list.size() == len) {
outputs.add(new ArrayList<>(list));
return;
} else {
for(int a:nums){
if(!list.contains(a)){
list.add(a);
backTrack(list,nums);
//回溯(状态返回)
list.remove(list.size()-1);
}
}
}
}
}
题目描述
题目分析
全排列||相对于第一个全排列对给定的数组中的数字做了一个“推广”,即数组中的数字可能是有重复数字的,并且要求产生的结果中没有重复的排列。
所以,很重要的一点就是–排重。
其实,上一题的题解中,回溯主体循环的判断条件是判断当前遍历到的数字在list结果集中是否存在,这种判断方式在本题中是行不通的,因为存在重复的数字,即使当前数字在结果集中已经存在,也不代表一定“使用”过该数字。
应该怎样设定每次循环的判定条件?
应该将判断条件改成:判断当前数字上一步是否访问过。这时就需要新建一个用来标记访问的数组。数组的大小和题设给定的数组相同,0代表没有访问,1代表已经访问过,每次访问某个数字,都将该数字对应的访问数组中对应下标下的值设为1。
如何修改状态返回的条件?
同样,状态返回时需要先将list中加入的最后一个值删掉,并且需要将当前访问到过的值返回成“未访问”。
回溯出口需要加什么条件?
该情况下,回溯出口不仅要求产生的结果集长度和给定数组长度一样,同时也需要该结果集没有出现过。
public class PermuteUnique {
public static void main(String[] args) {
Solution47 s = new Solution47();
int nums[] = {1,1,2};
System.out.println(s.permuteUnique(nums));
}
}
class Solution47 {
private List> outputs = new ArrayList<>();
private int len;
private List list = new ArrayList<>();
public List> permuteUnique(int[] nums) {
this.len = nums.length;
int flag[] = new int[len];
backTrack(list,nums,flag);
return outputs;
}
private void backTrack(List list,int[] nums,int[] flag){
//回溯出口
if(list.size() == len && !outputs.contains(list)){
outputs.add(new ArrayList<>(list));
return;
}
for(int i =0;i
对于回溯算法,还有很多很多复杂的题型。但是掌握这两种基础题型的思路,对之后应付更复杂的题型是很重要的。