回溯算法之排列组合
回溯算法特点
- 回溯算法是一种暴力穷举算法
- 穷举的过程是遍历一颗多叉树的过程
- 回溯算法的框架和多叉树遍历相似
回溯算法框架
List<Value> result;
void backtrace(路径, 选择列表) {
if (结束条件) {
result.add(路径);
return;
}
for (选择 : 选择列表) {
做选择;
backtrace(路径, 选择列表);
撤销选择;
}
}
多叉树遍历
void traverse(TreeNode root) {
if (root == null) {
return;
}
for (TreeNode child : root.children) {
traverse(child);
}
}
全排列问题
1,2,3,4 四个数字,给出全排列。按排列组合算法应该是4 * 3 * 2 * 1=24种。
import java.util.LinkedList;
import java.util.List;
public class BackTrace {
static List<List<Integer>> res = new LinkedList<>();
public static void main(String[] args) {
permute(new int[]{1, 2, 3, 4});
System.out.println(res);
System.out.println(res.size());
}
/**
* 主处理函数,输入一组不重复的数字,返回它们的全排列
*
* @param nums
* @return
*/
static List<List<Integer>> permute(int[] nums) {
// 记录「路径」
LinkedList<Integer> track = new LinkedList<>();
backtrack(nums, track);
return res;
}
/**
* 路径:记录在 track 中
* 选择列表:nums 中不存在于 track 的那些元素 -- not contains, then do it
* 结束条件:nums 中的元素全都在 track 中出现 -- size比对
*
* @param nums 选择列表
* @param track 多叉树的路径
*/
static void backtrack(int[] nums, LinkedList<Integer> track) {
// 触发结束条件
if (track.size() == nums.length) {
res.add(new LinkedList(track));
return;
}
for (int i = 0; i < nums.length; i++) {
// 排除不合法的选择
if (track.contains(nums[i])) {
continue;
}
// 递归前 做选择
track.add(nums[i]);
// 递归 进入下一层决策树
backtrack(nums, track);
// 递归后 取消选择
track.removeLast();
}
}
}
或者这种写法,和组合类比:
public class Test {
static List<List<Integer>> result = new LinkedList<>();
static LinkedList<Integer> track = new LinkedList<>();
public static void main(String[] args) {
int[] arr = {1,2,3,4};
boolean[] used = new boolean[arr.length];
backtrace(arr, used);
result.forEach(e -> System.out.println(e));
System.out.println(result.size());
}
static void backtrace(int[] arr, boolean[] used) {
if (arr.length == track.size()) {
result.add(new LinkedList<>(track));
return;
}
for (int i = 0; i < arr.length; i++) {
// 元素有了 continue
if (used[i]) {
continue;
}
track.add(arr[i]);
used[i] = true;
backtrace(arr, used);
track.removeLast();
used[i] = false;
}
}
}
组合子集问题
1,2,3 三个数字,给出各种组合情况。按排列组合算法应该是 C3,0 + C3,1 + C3,2 + C3,3 = 1+3+3+1 = 8种
import java.util.LinkedList;
import java.util.List;
public class Subsets {
List<List<Integer>> res = new LinkedList<>();
// 记录回溯算法的递归路径
LinkedList<Integer> track = new LinkedList<>();
public static void main(String[] args) {
List<List<Integer>> subsets = new Subsets().subsets(new int[]{1, 2, 3});
System.out.println(subsets);
}
// 主函数
public List<List<Integer>> subsets(int[] nums) {
backtrack(nums, 0);
return res;
}
// 回溯算法核心函数,遍历子集问题的回溯树
void backtrack(int[] nums, int start) {
// 前序位置,每个节点的值都是一个子集
res.add(new LinkedList<>(track));
// 回溯算法标准框架
for (int i = start; i < nums.length; i++) {
// 做选择
track.addLast(nums[i]);
// 通过 start 参数控制树枝的遍历,避免产生重复的子集
backtrack(nums, i + 1);
// 撤销选择
Integer integer = track.removeLast();
System.out.println(integer);
}
}
}
可复选的排列组合
给出的元素可以重复选择
可复选的排列
如 [1,2] 复选可以是 [1, 1] [1, 2] [2, 1] [2, 2]
去掉 boolean[] used 控制即可
static void backtrace(int[] arr) {
// base case 结束条件
if (track.size() == arr.length) {
result.add(new LinkedList<>(track));
return;
}
for (int i = 0; i < arr.length; i++) {
track.add(arr[i]);
backtrace(arr);
track.removeLast();
}
}
可复选的组合
使用 i ,而不是 i + 1 控制可复选,但复选时要注意base case 控制结束条件,避免无限制递归。
static void backtrace(int[] arr, int start) {
// base case 结束条件,比如只选两个元素
if (track.size() == 2) {
result.add(new LinkedList<>(track));
return;
}
for (int i = start; i < arr.length; i++) {
track.add(arr[i]);
// i 允许重复,i+1 不允许
backtrace(arr, i);
// backtrace(arr, i + 1);
track.removeLast();
}
}