基础概念
回溯法是一种基于搜索的算法,常用于解决组合问题和排列问题等。它基于“试错”的思想,通过不断尝试可能的解决方案来找到一个正确的解。
基本思路
将问题分解成多个子问题,然后对每个子问题进行尝试。如果在某个子问题中发现无法找到解决方案,则返回上一个子问题,重新尝试其它解决方案,直到找到正确的解决方案或者所有的解决方案都已尝试完毕。
function backtrack(问题, 解决方案, 结果集合) {
if (满足结束条件) {
结果集合.add(解决方案); // 将当前解决方案添加到结果集合中
return;
}
for (每个可行解) {
if (满足约束条件) {
执行操作,得到新的解决方案;
backtrack(问题, 新的解决方案, 结果集合); // 递归调用回溯法,处理下一个子问题
恢复状态; // 回溯到上一个状态,开始处理下一个可行解
}
}
}
其中, 问题 表示当前需要解决的问题, 解决方案 表示当前已经找到的解决方案, 结果集合 表示所有满足条件的解决方案集合。在算法流程中,我们首先判断是否满足结束条件,如果满足则将当前解决方案添加到结果集合中;否则,我们遍历所有可行解,如果满足约束条件,则执行操作得到新的解决方案,然后递归调用回溯法,处理下一个子问题。在递归调用结束后,我们需要恢复状态,回溯到上一个状态,开始处理下一个可行解。 需要注意的是,在回溯法中, 约束条件 和 恢复状态 这两个步骤非常重要。约束条件可以帮助我们判断当前解决方案是否满足条件,如果不满足则不必继续处理,直接跳过即可。恢复状态可以帮助我们回溯到上一个状态,继续尝试其它可行解。同时,在编写代码时,我们需要注意避免出现死循环和重复计算的问题。
使用场景
回溯法的使用场景非常广泛,例如:求解数独、N皇后问题、八皇后问题等。这些问题都有一个共同的特征,就是需要找到一组满足特定条件的解决方案。在这些问题中,回溯法可以非常有效地找到正确的解决方案。
问题实战
回溯法的核心是递归,因此在编写回溯法时需要注意一些问题。例如:如何设计递归函数、如何设置退出条件、如何剪枝等。
全排列
不重复数字的全排列
问题描述: 给定一组不同的数字,返回它们的所有排列方式。
解决思路:
- 我们使用一个列表来存储当前已经找到的解决方案,如果列表中的数字数量等于原始数组的长度,则说明已经找到一个完整的解决方案。
- 在遍历所有可行解时,我们需要判断当前数字是否已经包含在当前解决方案中,如果包含则需要跳过。
- 最后,我们需要在搜索完所有情况后,恢复状态,回溯到上一个状态,继续处理下一个可行解,以找到所有的解决方案。
代码实现
package algo;
import java.util.ArrayList;
import java.util.List;
public class BackTracking {
public static void main(String[] args) {
int[] nums = {1, 2, 3};
List<List<Integer>> res = new ArrayList<>();
backtrack(nums, new ArrayList<>(), res);
System.out.println(res);
}
/**
* 回溯
* @param nums 问题
* @param temp 解决方案
* @param res 结果合集
*/
public static void backtrack(int[] nums, List<Integer> temp, List<List<Integer>> res) {
// 将当前解决方案添加到结果集合中
if (temp.size() == nums.length) {
res.add(new ArrayList<>(temp));
return;
}
for (int i = 0; i < nums.length; i++) {
// 重复跳过
if (temp.contains(nums[i])) {
continue;
}
temp.add(nums[i]);
// 递归调用回溯法,处理下一个子问题
backtrack(nums, temp, res);
// 回溯到上一个状态,开始处理下一个可行解
temp.remove(temp.size() - 1);
}
}
}
可重复数字的全排列
问题描述: 给定一组可重复的数字,返回它们的所有排列方式。
解决思路:
- 我们使用一个列表来存储当前已经找到的解决方案,如果列表中的数字数量等于原始数组的长度,则说明已经找到一个完整的解决方案。
- 排序后的数组,在遍历所有可行解时,我们需要判断当前数字和下一个数字是否重复,重复跳过,同时,为了解决是否访问的问题,需要用k,v存储数组中每个数字和数字出现次数的映射关系。
- 最后,我们需要在搜索完所有情况后,恢复状态,回溯到上一个状态,继续处理下一个可行解,以找到所有的解决方案。
代码实现
package leetcode;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
public class QuanPailie47 {
public static void main(String[] args) {
int[] nums = {1, 2, 1};
System.out.println(permuteUnique(nums));
}
public static List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
Map<Integer, Long> map = Arrays.stream(nums).boxed()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
backtrack(nums, new ArrayList<>(), map, res);
return res;
}
/**
* 回溯
* @param nums 问题
* @param temp 解决方案
* @param map 每个数组元素及出现的次数,用于判断元素是否用过
* @param res 结果合集
*/
public static void backtrack(int[] nums, List<Integer> temp, Map<Integer, Long> map, List<List<Integer>> res) {
if (temp.size() == nums.length) {
res.add(new ArrayList<>(temp));
return;
}
for (int i = 0; i < nums.length; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
long count = map.get(nums[i]);
if (count > 0) {
map.put(nums[i], count - 1);
temp.add(nums[i]);
backtrack(nums, temp, map, res);
temp.remove(temp.size() - 1);
map.put(nums[i], count);
}
}
}
}