对于智商天花板略低的我来说,起初回溯法还是让我费了点功夫去理解内部流程,以防后面忘记了,现在记录一下吧。
首先上回溯法的一般框架:
int solution[MAX_DIMENSION]; //多维度解
void backtrack(int dimension)
{
/*检验当前维度数组是否是一个解*/
/* 临界判断,当前情况是否是一个解 */
if ( solution[] is well-generated )
{
check and record solution;
return;
}
/* 列举当前维度的所有取值的情况,并且进入到下一维度*/
for ( x = each value of current dimension )
{
solution[dimension] = x;
backtrack( dimension + 1 );
}
}
直接上例子。
先来个简单的, 401. Binary Watch
public List<String> readBinaryWatch(int num) {
List<String> res = new ArrayList<String>();
//表示10个led灯的位置
int[] watch = new int[10];
Trial(num, 0, watch, res);
return res;
}
//n 表示当前还需要点亮几个led灯, a 当前从第几个位置开始点亮
public static void Trial(int n,int a,int[] watch,List<String> times){
//参照模板, 首先进行临界条件判断,也就是recursion的边界条件判断。当n == 0时表示一次搜索走到底了,可以得到一个结果了。
if(n == 0){
int hour = 0, min = 0;
for(int i=0; i<4; i++){
hour += watch[i] * Math.pow(2, 3-i);
}
for(int i=4; i<10; i++){
min += watch[i] * Math.pow(2, 9-i);
}
String temp = Integer.toString(hour) + ":";
if(min < 10){
temp += "0";
}
temp += Integer.toString(min);
times.add(temp);
return;
}
//当前情况, 下一个点亮的位置可以在a~9当中选择,那就直接遍历
for(; a<=9; a++){
//点亮下一个led灯
watch[a] = 1;
//条件取舍,对于hour>11 和 min>59的情况舍去
if(! (watch[0] == 1 && watch[1] == 1 || watch[4] == 1 && watch[5] == 1 && watch[6] == 1 && watch[7] == 1)){
//继续深度搜索
Trial(n-1, a+1, watch, times);
}
//a~9的情况全部搞定,换个a继续
watch[a] = 0;
}
return;
}
接着来看 39. Combination Sum
public List<List<Integer>> combinationSum(int[] candidates, int target) {
//先对候选数字进行排序,避免结果重复
Arrays.sort(candidates);
List<List<Integer>> solutions = new ArrayList<>();
backtrack(candidates, target, new ArrayList<Integer>(), solutions, 0);
return solutions;
}
//其中remain表示还差多少能到target。chose是当前已经选择的数字的box。
public static void backtrack(int[] candidates, int remain, List<Integer> chose, List<List<Integer>> solutions, int start){
//如果chose内的和已经大于target, 考虑到候选数都是正数,没必要继续加了,直接回溯
if(remain < 0) return;
//如果chose内的和等于target, 那么这个chose就是我们需要的结果之一
else if(remain == 0) solutions.add(new ArrayList<>(chose));
else{
//当前情况, 下一个候选数可以在start~candidates.length当中选择,那就直接遍历
for(int i=start; i<candidates.length; i++){
//选择下一个候选数
chose.add(candidates[i]);
//继续深度搜索,注意到这里是i而不是i+1,因为候选数允许被多次选择
backtrack(candidates, remain - candidates[i], chose, solutions, i);
//回溯,继续
chose.remove(chose.size() - 1);
}
}
}
下一个例子, 40. Combination Sum II
相对于上一个例子, 这里只是有两个改变而已。首先, 候选数集里有重复的数,所以我们需要去除重复的数。然后,因为候选集里的数只能被用一次,所以递归那儿的i要变成i+1。代码如下
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
List<List<Integer>> solutions = new ArrayList<>();
backtrack(candidates, target, new ArrayList<Integer>(), solutions, 0);
return solutions;
}
public static void backtrack(int[] candidates, int remain, List<Integer> chose, List<List<Integer>> solutions, int start){
if(remain < 0) return;
else if(remain == 0) solutions.add(new ArrayList<>(chose));
else{
for(int i=start; i<candidates.length; i++){
if(i > start && candidates[i] == candidates[i-1]) continue;
chose.add(candidates[i]);
backtrack(candidates, remain - candidates[i], chose, solutions, i+1);
chose.remove(chose.size() - 1);
}
}
}
继续往下, 46. Permutations
public List<List<Integer>> permute(int[] nums) {
//套路如上,准备好答案的容器
List<List<Integer>> solutions = new ArrayList<>();
//占位符,待会儿会用到
boolean[] use = new boolean[nums.length];
backtrack(nums, use, nums.length, new ArrayList<Integer>(), solutions);
return solutions;
}
//use表示哪个位置已经被用了,count表示还剩几个位置没有被选到1
private static void backtrack(int[] nums, boolean[] use, int count, List<Integer> chose, List<List<Integer>> solutions){
//if...else...熟悉的套路
if(count == 0){
solutions.add(new ArrayList(chose));
}else{
for(int i=0; i<nums.length; i++){
if(use[i] == false){
chose.add(nums[i]);
use[i] = true;
backtrack(nums, use, count - 1, chose, solutions);
chose.remove(chose.size() - 1);
use[i] = false;
}
}
}
}