前言
对于回溯算法,起初刷leetcode题目时,每次编写都需要看题解或者评论,很少自己独立写出代码。后来通过自己的不断领悟,总算是找到了一个技巧编写回溯算法。下面通过分析如何从问题到代码的转变,来总结一下自己对于回溯算法的理解。
概述
刷了几道算法题之后,发现回溯算法解决的主要问题是组合问题。比如数字 1 2 3 4 5的不重复的全排列,字母a b c d的全排列。这类问题先从人类思维入手,然后再转为代码,个人认为是有一套公式的。
问题分析
人类思维
对于数字 [1, 2, 3 , 4, 5]的全排列,先以数字1开头的排列为例子,首先来梳理一下人如何解决这个问题。
-
首先选取数字1作为第一位。
1.0 选取2作为第二位。 1.0.1 选取3作为第三位 1.0.1.1 选取4作为第四位 1.0.1.1.1 选取5作为第五位 1.0.1.2 选取5作为第四位 1.0.1.2.1 选取4作为第五位 1.0.2选取4作为第三位 1.0.3选取5作为第三位 1.1 选取3作为第二位 1.2 选取4作为第二位 1.3 选取5作为第二位
根据上面的分析可以得到结果为12345和12354,其他的情况以此类推。
计算机算法思维
在上文的分析中,我们可以看出问题的解决是按照层次来解决的,每一层代表的是位数,每一位上选择从未被选择的任意数字。那么这里,可以使用树这种数据结构来将上文的思路进行转换一下。树的简图如下
结果的位数是已知的,所以树的层数也是有限的而且已知。从根节点到达每一个叶子节点的路径都能组成一个排列。例如 1 -> 2 -> 3 -> 4 -> 5组成排列12345。那么这个数字全排列问题就可以转化为构建树,然后遍历所有根节点到叶子节点的路径即可得到结果。
具体实现
通过Java语音中的数据结构以及递归可以对上述思想作具体的代码实现。一般这种题目数字都是放到一个数组中,然后返回一个List结果集。Java代码如下:
public static void main(String[] args) {
System.out.println(new QuanPaiLie().fullArrangement(new int[]{1, 2, 3}));
}
public List<String> fullArrangement(int[] nums) {
List<String> result = new ArrayList<>();//结果集
boolean[] flag = new boolean[nums.length];//标志位,标志当前数字是否可用
dfs(result, nums, flag, "");
return result;
}
/**
* @param result 结果集
* @param nums 数字集合
* @param flag 数字标记
* @param path 路径
*/
private void dfs(List<String> result, int[] nums, boolean[] flag, String path) {
//递归结束条件:由于path是记录路径节点的,那么只要path的长度等于nums的长度,说明当前路径已经到了叶子节点处
//结束递归
if (path.length() == nums.length) {
result.add(path);//将当前结果添加到结果集中,结束本次递归
return;
}
//以每个数字作为根节点构建树
//一共需要构建三棵树,根节点分别为1 2 3
for (int i = 0; i < nums.length; i++) {
if (flag[i])//如果当前数字被使用过,则不构建这个数字节点
continue;
flag[i] = true;//当前数字被使用,树下面的每一层都不会出现这个数字节点,避免重复
path += nums[i];//将当前数字添加到路径中
dfs(result, nums, flag, path);//进入当前树的下一层,下一层每个节点又被当成一棵新树的根节点被处理
//当前树遍历完成,将数字标记为可使用状态,在其他树中可被作为一个节点。
flag[i] = false;
//退回当前数字,避免出现重复
path = path.substring(0, path.length() - 1);
}
}
总结
通过最近刷的几题关于回溯算法的题目,发现跟上面这个代码都非常相似。不同的地方就是可能会用到剪枝或者树的层数不限但是有限定条件,但是上面这个回溯代码可以用作最基础的回溯代码框架使用。剪枝一般都是在for循环中加上限制条件,减少递归层数。带有限定条件的一般都是递归结束条件发生变化。回溯算法一般都是使用递归求解,编写递归代码首先要找到递归结束条件,其次就是递归体代码的编写。