1、概念
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
2、基本思想
在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。
若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。
而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。
3、用回溯法解题的一般步骤:
(1)针对所给问题,确定问题的解空间:首先应明确定义问题的解空间,问题的解空间应至少包含问题的一个(最优)解。
(2)确定结点的扩展搜索规则
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
4、基本方法
1.递归
//针对B+树的递归回溯方法
void backtrack (int t)//初始条件
{
if (t == n) output(x); //从0开始,叶子节点,输出结果,x是可行解
else
for i=1 to k //当前节点的所有子节点
{
x[t]=value(i); //每个子节点的值赋值给x
if (constraint(t)&&bound(t)) //满足约束条件和限界条件
backtrack(t+1); //递归下一层
}
}
2.递推
//针对N叉树的迭代回溯方法
void iterativeBacktrack ()
{
int t=1;
while (t>0) {
if(ExistSubNode(t)) //当前节点的存在子节点
{
for i = 1 to k //遍历当前节点的所有子节点
{
x[t]=value(i);//每个子节点的值赋值给x
if (constraint(t)&&bound(t))//满足约束条件和限界条件
{
//solution表示在节点t处得到了一个解
if (solution(t)) output(x);//得到问题的一个可行解,输出
else t++;//没有得到解,继续向下搜索
}
}
}
else //不存在子节点,返回上一层
{
t--;
}
}
}
5、几种问题类型
1、子集树
所给的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间成为子集树。
如0-1背包问题,从所给重量、价值不同的物品中挑选几个物品放入背包,使得在满足背包不超重的情况下,背包内物品价值最大。它的解空间就是一个典型的子集树。
此类题型基本范式:
void backtrack (int t)
{
if (t == n) output(x); //输出结果
else
for (int i = 0;i <= n;i++) {
if (constraint(t)&&bound(t)) { //边界条件与问题限制条件
x[t] = i; //(t,i)处确定值
backtrack(t+1); //探测下一层
}
}
}
LeetCode例题:子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
class Solution {
List<List<Integer>> listAll = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
if (nums.length == 0) { return listAll; }
helper(nums,0,new ArrayList<>()); //从0个数开始直到n个数的排列
return listAll;
}
public void helper(int[] nums,int k,List<Integer> list) {
listAll.add(new ArrayList<>(list)); //记录K个数时的结果
for (int j = k;j < nums.length;j++) {
list.add(nums[j]);
helper(nums,j+1,list); //下一层子树
list.remove(list.size()-1);//回溯到上一层的状态
}
}
}
2、排列树
所给的问题是确定n个元素满足某种性质的排列时,相应的解空间就是排列树。
LeetCode例题:括号生成
给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。
例如,给出 n = 3,生成结果为:
[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
class Solution {
List<String> list = new ArrayList<>();
public List<String> generateParenthesis(int n) {
helper("",0,0,n);
return list;
}
void helper(String s,int l,int r,int n) {
if (s.length() == 2*n) { //用完所有括号,返回结果
list.add(s);
return;
}
if (l < n) { helper(s+"(",l+1,r,n); } //左括号小于总数,可以放左括号
if (r < l) { helper(s+")",l,r+1,n); } //右括号小于左括号数,可以放右括号
}
}
此类问题为排列树问题,即n个元素的全排列的满足条件子集,此类问题范式如下
void backtrack (int t)
{
if (t>n) output(x);
else
for (int i=t;i<=n;i++) {
swap(x[t], x[i]);
if (constraint(t)&&bound(t)) backtrack(t+1);
swap(x[t], x[i]); //回溯排列到上一层的位置
}
}
3、八皇后
问题描述:八皇后问题是一个以国际象棋为背景的问题:如何能够在 8×8 的国际象棋棋盘上放置八个皇后,使得任何一个皇后都无法直接吃掉其他的皇后?为了达到此目的,任两个皇后都不能处于同一条横行、纵行或斜线上。
约束条件:
1. a[0] ~ a[7]不能相同,存储棋子所在的列
2. 同一对角线上即坐标斜率为1,即(i,j)和(k,l)的位置,当|i-k|=|j-l| 时,两个皇后才在同一条对角线上。
递归回溯:
int[] a = new int[n]; //存储皇后位置的数组,下标为行,值位列
void Queen(int k,int n) {
if (k == n) { // k从0开始,到达了最后一行n-1之后
System.out.println(Arrays.toString(a)); //打印输出
return;
}
for (int i = 0;i < n;i++) { //k行的每一列都有可能放置皇后
if (isConflict(k,i)) { //判断此处的K行,i列能否满足放置要求
a[k] = i;
Queen(k+1,n); //(k,i)处放置完成后,进行下一行的探测
}
}
}
boolean isConflict(int k,int i) { //判断函数,扫描0到k-1行中是否有根(k,i)冲突
for (int j = 0;j < k;j++) {
if (a[j] == i || Math.abs(a[j]-i) == k-j) { //同列或者同一斜线
return false;
}
}
return true;
}
各种大神解法:https://blog.youkuaiyun.com/hacker00011000/article/details/51582300
参考文章:https://blog.youkuaiyun.com/JarvisChu/article/details/16067319
https://www.cnblogs.com/steven_oyj/archive/2010/05/22/1741376.html