*回溯法(探索与回溯法)是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
*回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解:如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。
*若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。
一、回溯法的算法框架
1、问题的解空间
复杂问题常常有很多的可能解,这些可能解构成了问题的解空间。解空间也就是进行穷举的搜索空间,所以,解空间中应该包括所有的可能解。确定正确的解空间很重要,如果没有确定正确的解空间就开始搜索,可能会增加很多重复解,或者根本就搜索不到正确的解。
例如,对于有n个物品的0/1背包问题,当n=3时,其解空间是:
{(0,0, 0), (0, 0, 1), (0, 1, 0), (1, 0, 0), (0, 1, 1), (1, 0, 1), (1, 1, 0), (1, 1,1) }
2、回溯法的基本思想
回溯法的基本步骤:
(1)针对所给问题,定义问题的解空间;
(2)确定易于搜索的解空间结构(一般有三种结构,组合树,排列树,n叉树);
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
常用剪枝函数:
用约束函数(constraint)在扩展结点处剪去不满足约束的子树;
用限界函数(bound)剪去得不到最优解的子树。
需要注意的是,问题的解空间树是虚拟的,并不需要在算法运行时构造一棵真正的树结构,只需要存储从根结点到当前结点的路径。
例如,对于n=3的0/1背包问题,三个物品的重量为{20, 15, 10},价值为{20, 30, 25},背包容量为25。其解空间树如下,其解空间树中的8个叶子结点分别代表该问题的8个可能解:
生成问题状态的基本方法
*扩展结点:一个正在产生儿子的结点称为扩展结点。
*活结点:一个自身已生成但其儿子还没有全部生成的节点称做活结点。
*死结点:一个所有儿子已经产生的结点称做死结点。
*深度优先的问题状态生成法:如果对一个扩展结点R,一旦产生了它的一个儿子C,就把C当做新的扩展结点。在完成对子树C(以C为根的子树)的穷尽搜索之后,将R重新变成扩展结点,继续生成R的下一个儿子(如果存在)。
*回溯法:为了避免生成那些不可能产生最佳解的问题状态,要不断地利用限界函数(boundingfunction)来处死那些实际上不可能产生所需解的活结点,以减少问题的计算量。
3.递归回溯
回溯法对解空间作深度优先搜索,因此,在一般情况下用递归方法实现回溯法。
//t表示递归深度,该函数用于对第t层的某个节点搜索其所有子树
void backtrack (int t)
{
if (t>n) output(x); //算法已搜索到某个叶节点,输出该搜索结果
else
for (int i=f(n,t);i<=g(n,t);i++)
//f(n,t), g(n,t)分别表示拓展节点的子树的起始编号和终止编号
{
x[t]=h(i); //h(i)表示第i个可选值
if (constraint(t)&&bound(t)) backtrack(t+1);
//constraint(t),bound(t)分别表示约束函数和限界函数
}
}
4.子集树与排列树
在用回溯法求解问题时,常常遇到两种典型的解空间树:
子集树:当所给的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间树成为子集树。例:0-1背包问题。
排列树:当所给问题是确定 n 个元素的满足某种性质的排列 时,相应的解空间树称为排列树。例:旅行售货员问题。
遍历子集树需O(2n)计算时间
void backtrack (int t)
{
if (t>n) output(x);
else {
x[t]=1;
if (legal(t)) backtrack(t+1);
x[t]=0;
if (legal(t)) backtrack(t+1);
}
遍历排列树需要O(n!)计算时间
void backtrack (int t)
{
if (t>n) output(x);
else
for (int i=t;i<=n;i++) {
if (legal(i)) {
swap(x[t], x[i]);
backtrack(t+1);
swap(x[t], x[i]); }
}
}
二、例子解析
1、 0-1背包问题
问题描述 : 01背包是在M件物品取出若干件放在空间为C的背包里,每件物品的体积为W1,W2……Wn,与之相对应的价值为P1,P2……Pn。求出获得最大价值的方案,注意在本题中所有的体积值均为整数。
第一步:找出问题的解空间,共有 2 ^n种方案。
第二步:问题的解空间结构为组合树。
第三步:找到约束函数以及限界函数。本问题明显的约束函数为,装入物品的体积∑Wi <= C, 而限界函数可以设计判断当前路径能否产生更优的解。(按照价值重量比从大到小排序,将物品装入背包。如果产生的价值更大,则有可能产生最优解)
以下为本人写的JAVA代码,不保证最好。
package OJ;
import java.util.*;
/**
* input: 5 n 2 6 2 3 6 5 5 4 4 6 10 c output: 15
*
*/
// 0-1背包问题,回溯法求解
public class ZeroOnePackage {
static int n; // 物品个数
static int c; // 背包容量
static Thing[] things; // 物品
static int bestp; // 最大价值量
static int cp; // 当前价值量
static int cw; // 当前背包容量
static int[] x; // 表示搜索路径
static class Thing {
int w; // 重量
int p; // 价值
float ratio; // 价值重量比,即单位重量所占价值
public Thing(int w, int p, float ratio) {
this.w = w;
this.p = p;
this.ratio = ratio;
}
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
n = input.nextInt();
x = new int[n + 1];
things = new Thing[n + 1];
int tmpw;
int tmpp;
float r = 0;
for (int i = 1; i <= n; ++i) {
tmpw = input.nextInt();
tmpp = input.nextInt();
r = (float) (tmpp) / tmpw;
things[i] = new Thing(tmpp, tmpw, r);
}
c = input.nextInt();
Arrays.sort(things, 1, n + 1, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
// 从大到小排列
Thing t1 = (Thing) o1;
Thing t2 = (Thing) o2;
if (t1.ratio < t2.ratio)
return 1;
else if (t1.ratio > t2.ratio)
return -1;
else
return 0;
}
});
backtrack(1);
System.out.println(bestp);
}
public static void backtrack(int t) {
if (t > n) {
if (bestp < cp)
bestp = cp;
} else {
if (cw + things[t].w <= c) {
x[t] = 1;
cw += things[t].w;
cp += things[t].p;
backtrack(t + 1);
cw -= things[t].w;
cp -= things[t].p;
x[t] = 0;
}
if (bestp < bound(t + 1))
backtrack(t + 1);
}
}
public static int bound(int k) {
int cleft = c - cw;
int curValue = cp;
while (k <= n && things[k].w <= cleft) {
cleft -= things[k].w;
curValue += things[k].p;
k++;
}
if (k <= n)
curValue += things[k].p * cleft / things[k].w;
return curValue;
}
}
2、 旅行售货商问题 (TSP)
待补充
3、 n皇后问题
待补充