回溯法在问题的解空间树中,按深度优先策略,从根 结点出发搜索解空间树。算法搜索至解空间树的任意 一点时,先判断该结点是否包含问题的解。如果肯定 不包含,则跳过对该结点为根的子树的搜索,逐层向 其祖先结点回溯;否则,进入该子树,继续按深度优 先策略搜索。
回溯法希望一个问题的解能够表示为一个n元式的样子(x1,x2,…,xn)
- 显约束:对分量的取值限定
- 隐约束:为满足问题的解而对不同分量之间施加的约束
- 解空间:解向量满足所有显式约束条件的多元组,构成一个解空间
生成问题状态的基本方法
扩展结点:一个正在产生儿子的结点称为扩展结点
活结点:一个自身已经生成但是其儿子还没有全部生成的结点
死结点:儿子已经全部产生的结点
深度优先的问题状态生成法:如果对一个扩展结点R,一旦 产生了它的一个儿子C,就把C当做新的扩展结点。在完成 对子树C(以C为根的子树)的穷尽搜索之后,将R重新变 成扩展结点,继续生成R的下一个儿子(如果存在)
宽度优先的问题状态生成法:在一个扩展结点变成死结点 之前,它一直是扩展结点
回溯法:为了避免生成那些不可能产生最佳解的问题状态, 要不断地利用限界函数(bounding function)来处死那些实际 上不可能产生所需解的活结点,以减少问题的计算量。
具 有限界函数的深度优先生成法称为回溯法
回溯法基本思想
- 针对所给问题,定义问题的解空间
- 确定易于搜索的解空间结构
- 以深度优先方式搜索解空间,并在搜索过程中使用剪枝函数避免无效搜索
用约束函数在扩展结点处剪去不满足约束的子树; 用限界函数剪去得不到最优解的子树。
在任何时刻,算法只保存从根结点到当前扩展结点的 路径。如果解空间树中从根结点到叶结点的最长路径的长度为 h(n),则回溯法所需的计算空间通常为O(h(n))。
递归回溯
void backtrack (int t)//t为递归深度
{
if (t>n) output(x);//算法已搜索至叶结点
else
{
for (int i=f(n,t); i<=g(n,t); i++)//f,g 表示当前扩展节点处未搜索过的子树的起始编号和终止编号
{
x[t]=h(i);//h(i)表示在当前扩展结点处x[t]的第i个可选值;
if (constraint(t)&&bound(t))//constraint(t)和bound(t))分别是当前扩展结点处的约束函数和限界函数。
backtrack(t+1);
}
}
}
解释一下就是从自己的第一个子节点开始进行深度优先遍历,如果当前子节点满足约束函数和限界函数,继续向下递归,否则就不管改子节点,称为剪枝
迭代回溯
采用树的非递归深度优先遍历算法,可将回溯法表示为一个非 递归迭代过程。
void iterativeBacktrack ()
{
int t=1;
while (t>0) {
if (f(n,t)<=g(n,t))
for (int i=f(n,t);i<=g(n,t);i++) {
x[t]=h(i);
if (constraint(t)&&bound(t)) {
if (solution(t)) output(x);
else t++;}
}
else t--;//如果改节点不满足c和b,回溯
}
}
涉及到回溯算法大部分的时间复杂度都为O(n2^n)
1.装载问题
有一批共n个集装箱要装上2艘载重量分别为c1和c2的轮船,其 中集装箱i的重量为wi,且
,装载问题要求确定是否有一个合理的装载方案可将这个集装箱 装上这2艘轮船。如果有,找出一种装载方案。
问题可等价转换为
- 首先将第一艘轮船尽可能装满;
- 将剩余的集装箱装上第二艘轮船。
将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集, 使该子集中集装箱重量之和最接近。由此可知,装载问题等价 于以下特殊的0-1背包问题。
void backtrack (int i)
{// 搜索第i层结点
if (i > n) // 到达叶结点
{更新最优解bestx,bestw;return;}//bestx为最优路径,bestw为最优载重量
r -= w[i];//r为剩余重量
if (cw + w[i] <= c) {// 搜索左子树:当前载重量cw+剩余集装箱的重量r<=当前最优载重量bestw
x[i] = 1;
cw += w[i];
backtrack(i + 1);
cw -= w[i]; }
if (cw + r > bestw) {
x[i] = 0; // 搜索右子树
backtrack(i + 1); }
r += w[i];//如果没有选择该子树,回溯时要将质量加回去
}
代码如下
函数类定义
template <class Type>
class Loading
{
friend Type MaxLoading(Type [],Type,int);//定义友元函数
private:
void Backtrack(int i);
int n;
Type * w,
c,cw,bestw,
r;//剩余集装箱重量
};
template <class Type>
void Loading<Type>::Backtrack(int i)//这里对类内函数实现
{
if(i>n)
{if(cw>bestw) bestw = cw;
return;}
r-=w[i];//计算剩余(未考察)的集装箱的重量,减去当前考察过的对象的重量
if (cw + w[i] <= c) {// 搜索左子树:当前载重量cw+当前集装箱重量<=当前最优载重量bestw
x[i] = 1;
cw += w[i];
backtrack(i + 1);
cw -= w[i]; }
if (cw + r > bestw) {//如果当前重量加上剩余集装箱质量大于当前最优载重量,那么就需要继续向下遍历,进行集装箱的挑选
x[i] = 0; // 搜索右子树
Backtrack(i + 1); }
r += w[i];//如果没有选择该子树,回溯时要将质量加回去
}
template <class Type>
Type MaxLoading(Type w[],Type c,int n)
{
Loading<Type> X; //初始化
X.w = w;
X.c = c;
X.n = n;
X.bestw = 0;
X.cw = 0;
X.r = 0; //初始化r
for(int i=1;i<=n;i++) //计算总共的剩余(当前为考察过的)集装箱重量
X.r += w[i];//r初始情况下为所有集装箱的质量
X.Backtrack(1);
return X.bestw;
}
2.批处理作业调度
给定n个作业的集合{J1,J2,…,Jn}。每个作业必须先由机器1处 理,然后由机器2处理。作业Ji需要机器j的处理时间为tji。对 于一个确定的作业调度,设Fji是作业i在机器j上完成处理的时 间。所有作业在机器2上完成处理的时间和称为该作业调度的 完成时间和。 批处理作业调度问题要求对于给定的n个作业,制定最佳作业 调度方案,使其完成时间和达到最小。
代码如下
void Flowshop::Backtrack(int i)
{
if (i > n) {//更改当前最优作业调度
for (int j = 1; j <= n; j++)
bestx[j] = x[j];
bestf = f;
else
for (int j = i; j <= n; j++) {
f1+=M[x[j]][1]; //作业j在M1上的加工时间
f2[i]=((f2[i-1]>f1)?f2[i-1]:f1)+M[x[j]][2];
f+=f2[i]; //作业1…j完成的加工时间之和
if (f < bestf) {//在i层找到更优的作业调度
Swap(x[i], x[j]);
Backtrack(i+1);
Swap(x[i], x[j]);
}
f1- =M[x[j]][1];
f- =f2[i];
}
//对于这个代码的解释在于x内存放的为当前作业调度,
//每次遍历到最末节点,都会更新bestx(最优路径),
//而向下遍历的条件在于当前时间小于之前的最优时间,
//保证可以遍历到底部的为新的最优路径,
//而又由于只存储一条路径,所以通过交换来进行路径选择
3.符号三角形问题
下图是由14个“+”和14个“-”组成的符号三角形。2个同号下 面都是“+” ,2个异号下面都是“-” 。
在一般情况下,符号三角形的第一行有n个符号。符号三角形 问题要求对于给定的n,计算有多少个不同的符号三角形,使 其所含的“+”和“-”的个数相同。
void Triangle::Backtrack(int t)
{
if ((count>half) || (t*(t-1)/2-count>half)) return;//如果加号减号相同,那么就不会有一个的数量大于一半n*(n+1)/4
if (t>n) sum++;//sum代表共有多少符号三角形,所以到达n+1的时候++
else//当深度t没有到达底部,选择下一个符号为+还是-
for (int i=0;i<2;i++)
{//以0表示-,以1表示+
p[1][t]=i;
count+=i;
for (int j=2; j<=t; j++)
{
p[j][t-j+1]=p[j-1][t-j+1]^p[j-1][t-j+2];
count+=p[j][t-j+1];
}
Backtrack(t+1);
for (int j=2;j<=t;j++){ count-=p[j][t-j+1]; }
count-=i;
}
4.n后问题
在n×n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象 棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线 上的棋子。n后问题等价于在n×n格的棋盘上放置n个皇后,任 何2个皇后不放在同一行或同一列或同一斜线上。
设第i行的皇后,放置在Xi列上。则n后问题就是发现所有 可能的序列(x1, x2, … , xn)。
要使得n个皇后放置均不在同一行、同一列,同一个对角线上, 则只需要满足如下条件:
bool Queen::Place(int k)
{//第i行的皇后,放置在Xi列上
for (int j=1; j<k; j++)
if ((abs(k-j)==abs(x[j]-x[k])) || (x[j]==x[k]))//保证任意两个不在同一列和斜线
return false;
return true;
}
void Queen::Backtrack(int t)
{//递归回溯
if (t>n) sum++; //输出结果x[0:n]
else
for (int i=1;i<=n;i++)
{
x[t]=i;
if (Place(t)) Backtrack(t+1);
}
}
---------------------------------------------------------------------------------
//迭代回溯
public static long Queen::nQueen(int nn)
{
n=nn;
sum=0;
x=new int[n+1];
for (i=0;i<=n; i++) x[i]=0;
backtrack(1);
return sum++;
}
private static void nQueen:backtrack()
{// k表示当前第k层,x[k]表示第k行皇后的列号
x[1]=0;
int k=1;
while (k>0)
{
x[k]+=1;
while ( (x[k]<=n && !(Place(k) ) x[k]+=1;
if (x[k]<=n)
{//可以放置在k位置
if (k==n)
{ //最后一个皇后
sum++;
//得到一个解,输出x[1:n]
}
else
{//不是最后一个皇后
k++; x[k]=0; //放置下一个(层)皇后
}
}
else
k--; //不可以放置,回退到上一层(回溯)
}
}
5.0-1背包问题
double c; //背包容量
int n; //物品数
double []w; //物品重量数组
double []p; //物品价值数组
double cw; //当前重量
double cp; //当前价值
double bestp; //当前最优价值
Bound()计算后半段最大价值的时候,使
用的是一个贪心算法。尽管切割的情况是不
被同意的,可是能够用这个结果来进行估算
Private static double Bound(int i)//这个是进入右子树的条件
{// 计算上界
double cleft = c - cw; // 剩余容量
double b = cp;
// 以物品单位重量价值递减序装入物品
while (i <= n && w[i] <= cleft) {
cleft -= w[i];
b += p[i];
i++;
}
// 装满背包
if (i <= n) b += p[i]/w[i] * cleft;
return b;
}
//递归回溯
private static double Backtrack(int i) {
if(i>n)
{//到达叶结点
bestp=cp;
return
}
//搜素子树
if (cw+w[i]<=c)
{//进入左子树
cw+=w[i];
cp+=p[i];
Backtrack(i+1);
cw-=w[i];
cp-=p[i];
}
if (Bound(i+1)>bestp)
Backtrack(i+1); //进入右子树:预测价值大于最大价值
}
6.最大团问题
对于最大团的理解:顶点i到已选入的顶点集中每一个顶点都有边相连。
先画出G杠(包含G的所有顶点,以及G中不存在的边),然后找寻最大独立集(没有互相连接的边的点)
下面代码的变量包括:
void Clique::Backtrack(int i)
{// 计算最大团
if (i > n) {// 到达叶结点
for (int j = 1; j <= n; j++) bestx[j] = x[j];
bestn = cn; return;}
// 检查顶点 i 与当前团的连接
int OK = 1;
for (int j = 1; j < i; j++)
if (x[j]!=0 && a[i][j] ) {
//j是最大团中的节点,且 i与j不相连
OK = 0; break;}
if (OK)
{//节点i满足最大团, 进入左子树
x[i] = 1; cn++;
Backtrack(i+1);
x[i] = 0; cn--; //恢复状态,为回溯做准备
}
if (cn + n - i > bestn) {// 进入右子树
x[i] = 0;
Backtrack(i+1);}
}
7.图的m着色问题
给定无向连通图G和m种不同的颜色。用这些颜色为图 G的各顶点着色,每个顶点着一种颜色。是否有一种 着色法使G中每条边的2个顶点着不同颜色。这个问题 是图的m可着色判定问题。若一个图最少需要m种颜色 才能使图中每条边连接的2个顶点着不同颜色,则称 这个数m为该图的色数。求一个图的色数m的问题称为 图的m可着色优化问题。
void Color::Backtrack(int t)
{
if (t>n) {
sum++;
for (int i=1; i<=n; i++)
cout << x[i] << ' ';
cout << endl;
}
else
for (int i=1;i<=m;i++) {
x[t]=i;
if (Ok(t)) Backtrack(t+1);
}
}
bool Color::Ok(int k)
{// 检查颜色可用性
for (int j=1;j<k; j++) //化前j<=n
if ((a[k][j]==1)&&(x[j]==x[k])) return false;
return true;
}
8.旅行售货员问题
旅行售货员问题(travelling salesman problem)是一类 组合最优化问题,设有一个售货员从城市1出发,到城市2, 3,…,n去推销货物,最后回到城市1.假定任意两个城市 i,j间的距离dij(dij=dji)是已知的,问他应沿着什么样的 路线走,才能使走过的路线最短(总旅费最小)?
旅行售货员问题就是在一个完全网络中,找出一个具有最 小权的哈密顿圈,寻求旅行售货员问题的有效算法似乎是 没有希望的,它属于NP完全类,一个可行的办法是首先求 一个哈密顿圈,然后适当修改,以得到具较小权的另一个 哈密顿圈。
用CC来记录当前路径X[1:i]的费用
template<class Type>
void Traveling<Type>::Backtrack(int i)
{//float a[][]为邻接矩阵,若a[i][j]为MAX_VALUE则城市i与城市j之间旅费极大
if (i == n) {
if (a[x[n-1]][x[n]] <MAX_VALUE && a[x[n]][1] < MAX_VALUE &&(cc + a[x[n-1]][x[n]] + a[x[n]][1] < bestc || bestc>MAX_VALUE)) {
for (int j = 1; j <= n; j++) bestx[j] = x[j];
bestc = cc + a[x[n-1]][x[n]] + a[x[n]][1];}
}
else {
for (int j = i; j <= n; j++)
// 是否可进入x[j]子树?
if ( a[x[i-1]][x[j]]<MAX_VALUE &&(cc + a[x[i-1]][x[i]] < bestc || bestc==MAX_VALUE)) {
// 搜索子树
Swap(x[i], x[j]);
cc += a[x[i-1]][x[i]];
Backtrack(i+1);
cc -= a[x[i-1]][x[i]];
Swap(x[i], x[j]);}
}
}
9.圆排列问题
float Circle::Center(int t)
{// 计算当前所选择圆的圆心横坐标
float temp=0;
for (int j=1;j<t;j++) {
float valuex=x[j]+2.0*sqrt(r[t]*r[j]);
if (valuex>temp) temp=valuex;
}
return temp;
}
void Circle::Compute(void)
{// 计算当前圆排列的长度
float low=0, high=0;
for (int i=1;i<=n;i++) {
if (x[i]-r[i]<low) low=x[i]-r[i];
if (x[i]+r[i]>high) high=x[i]+r[i];
}
if (high-low<min) min=high-low;
}
void Circle::Backtrack(int t)
{
if (t>n) Compute();
else
for (int j = t; j <= n; j++) {
Swap(r[t], r[j]);
float centerx=Center(t);
if (centerx+r[t]+r[1]<min) {//下界约束
x[t]=centerx;
Backtrack(t+1);
}
Swap(r[t], r[j]);
}
}
10.连续邮资问题
假设国家发行了n种不同面值的邮票,并且规定每张信 封上最多只允许贴m张邮票。连续邮资问题要求对于给 定的n和m的值,给出邮票面值的最佳设计,在1张信封 上可贴出从邮资1开始,增量为1的最大连续邮资区间。
对于连续邮资的问题,由于实验开始是仅给出面值的数量,而面值的具体值是未知的。 但是由于邮资需要从1开始,因此,面值具体值中必然有1。可以建立一个数组用于存储具体 的面值。X[1:n]表示从小到大存储具体面值。
当面值为1时,可形成的连续邮资区间为1-m;在此基础上,若要增加面值,为保证区 间连续,第二个面值必然要在2到m+1中取(第二个面值不能为1,并且若为m+2或者更大时, 只用一个就变为m+2,此时不连续);第三个面值则需要根据前两个面值能达到的最大值来 确定。假设第i个面值x[i]的连续区间若为1~r时,则第x[i+1]的个的取值必然为x[i]+1到 r+1。则从第一个面值开始,接下来的各个面值的取值都由上一个面值以及能形成的最大值 来取。
为了求解最大连续区间,需要将面值的所有情况进行考虑,则对面值可能取值的语法 树进行递归遍历,即回溯。递归到叶子节点时,将当前情况的最大值进行记录并与之前的进 行比较,若更大则保存最大值,之后回溯到上一层继续求解,直到将所有情况计算完成。
解向量:用n元组x[1:n]表示n种不同的邮票面值,并约定它 们从小到大排列。x[1]=1是唯一的选择。
可行性约束函数:已选定x[1:i-1],最大连续邮资区间是[1:r], 接下来x[i]的可取值范围是[x[i-1]+1:r+1]。