文章目录
- 装载问题
- 回溯算法
- 优化算法
- 构造最优解
- 0-1背包问题
- 批处理作业调度问题
- 图的M着色问题
- N皇后问题
- 最大团问题
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
回溯法思想
回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。
若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束;
而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。
回溯法一般步骤
(1)针对所给问题,确定问题的解空间: 首先应明确定义问题的解空间,问题的解空间应至少包含问题的一个(最优)解。
(2)确定结点的扩展搜索规则。
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
- 问题的解空间
用回溯法解问题时,应明确定义问题的解空间,其至少应包含问题的一个(最优)解。
问题的解向量:回溯法希望一个问题的解能够表示成一个n元式(x1,x2,…,xn)的形式。
例如,n=3的0-1背包问题,其解空间由长度为3的0-1向量组成:
{(0,0,0),(0,1,0),(0,0,1),(1,0,0),
(0,1,1),(1,0,1),(1,1,0),(1,1,1)}
n=3的0-1背包问题的解空间可以用一颗完全二叉树来表示。
- 搜索过程
3. 递归回溯
回溯法对解空间作深度优先搜索,因此,在一般情况下用递归方法实现回溯法。
void backtrack (int t) // t表示递归深度,即当前扩展结点在解空间树中的深度。
{
if (t>n)
output(x); //已到叶子结点,输出结果
else // f(n,t) ,g(n,t) :表示当前扩展结点处未搜索过的子树的起始编号和终止编号。
for (int i=f(n,t);i<=g(n,t);i++)
{
x[t]=h(i); //当前扩展节点处x[t]的第i个可选值。
if (constraint(t)&&bound(t))
backtrack(t+1);
} //constraint(t)、bound(t)分别表示当前扩展结点处的约束函数和限界函数。
}
- 迭代回溯
采用树的非递归深度优先遍历算法,可将回溯法表示为一个非递归迭代过程。
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--;
}
}
- 子集树与排列树
子集树:当所给问题是从n个元素的集合中找出满足某种性质的子集时,相应的解空间树。
如0-1背包问题。
遍历子集树需O(2^n)计算时间
void backtrack (int t)
{ if (t>n) output(x);
else
for (int i=0;i<=1;i++) {
x[t]=i;
if (legal(t)) backtrack(t+1);
}
}
排列树:当所给的问题是确定n个元素满足某种性质的排列时,相应的解空间树。
如旅行售货员问题。
遍历排列树需要O(n!)计算时间
void backtrack (int t)
{ if (t>n) output(x);
else
for (int i=t;i<=n;i++) {
swap(x[t], x[i]);
if (legal(t)) backtrack(t+1);
swap(x[t], x[i]);
}
}
装载问题
有n个集装箱要装上2艘载重量分别为C1和C2的轮船。其中集装箱i的重量为Wi,且(W1+W2+….+Wn<=C1+C2)。
装载问题是,是否有一个合理装载方案,可将这n个集装箱都装上这2个轮船,若有,请给出解决方案。
例如:
C1=C2=50, W=(10,40,40) 可以装载(10,40) (40)
C1=C2=55, W=(20,40,40) 无法装载
容易证明,如果一个给定装载问题有解,则采用下面的策略可得到最优装载方案。
(1)首先将第一艘轮船尽可能装满;
(2)将剩余的集装箱装上第二艘轮船。
将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集,使该子集中集装箱重量之和最接近C1。由此可知,装载问题等价于特殊的0-1背包问题。
回溯算法
void backtrack( int t ) //搜索子集树中第t层子树
// n是集装箱数,cw是当前载重量,bestw是当前最优载重量,C是第一艘轮船载
重量
{
if (t>n) //到达叶结点
{
if (cw>bestw)
bestw=cw; //找到一个更好的方案
return;
}
if (cw+w[t]<=C) //搜索左子树
{
cw+=w[t]; x[t]=1;
backtrack( t +1);
cw-=w[t]; //还原到上层结点
}
x[t]=0;
backtrack( t +1); //搜索右子树
}
优化算法
void backtrack( int t )
// 引入上界函数cw+r,剪去不含最优解的子树;r是剩余未判定的集装箱重量
{
if (t>n)
{
if (cw>bestw)
bestw=cw; //找到一个更好的方案
return;
}
r-=w[t]; //进入本层前,先计算剩余未判断的物品重量
if (cw+w[t]<=C) //搜索左子树
{
cw+=w[t]; x[t]=1;
backtrack( t +1);
cw-=w[t];
} //还原到上层结点
if ( cw+r > bestw) //若余下的重量与已选择的重量之和可以超过前面已得的最优值
{
x[t]=0; backtrack( t +1);
} //搜索右子树
r+=w[t]; //返回上层前,还要还原剩余载重量和。
}
构造最优解
void backtrack( int t )
{
if (t>n)
{
if(cw >bestw)
{ bestw=cw;
bestx[1:n] <- x[1:n];
} // bestx[]记录当前最优解
return;
}
r-=w[t];
if (cw+w[t]<=C) //搜索左子树
{
cw+=w[t]; x[t]=1;
backtrack( t +1);
cw-=w[t];
}
if ( cw+r > bestw)
{
x[t]=0;
backtrack( t +1); //搜索右子树
}
r+=w[t]; //返回上层前,还要还原剩余载重量和。
}
0-1背包问题
0-1背包问题是子集选取问题,也是NP难问题,其解空间可用子集树表示。
初始:cp=0;//当前价值
cw=0;//当前重量
设r是当前剩余物品价值总和,bestp是当前最优价值。
当cp+r≤bestp时,可剪去右子树。
计算右子树中解的上界更好的方法是将剩余物品依其单位重量价值排序后依次装入,直至装不下时,再装入该物品的一部分而装满背包,此时得到的价值是右子树中解的上界。
例如:n=4,c=7,p={9,10,7,4},w={3,5,2,1},单位重量价值分别为{3,2,3.5,4}。
按上述方法得到x=[1,0.2,1,1],相应价值22,即所求上界。
此时,判断右子树要剪掉的条件由cp+30≤bestp变为cp+22≤bestp,更容易被满足。
template<class Typew, class Typep>
Typep Knap<Typew, Typep>::Bound(int i)
{ // 计算上界,cp当前价值,cw当前重量
Typew cleft = c - cw; // 剩余容量
Typep 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;
}
批处理作业调度问题
问题描述:
给定 n 个作业的集合 j = {j1, j2, …, jn}。每一个作业 j[i] 都必须先由机器1处理,然后由机器2处理。作业 j[i] 需要机器 j 的处理时间为 t[j][i] ,其中i = 1, 2, …, n, j = 1, 2。对于一个确定的作业调度,设F[j][i]是作业 i 在机器 j 上的完成处理的时间。所有作业在机器2上完成处理的时间之和 称为该作业调度的完成时间之和。
批处理作业调度是要从 n 个作业的所有排列中找出有最小完成时间和的作业调度,所以批处理调度问题的解空间是一棵排列树。按照回溯法搜索排列树的算法框架,设开始时x = [1, …, n]是所给的 n 个作业,则相应的排列树由所有排列构成。
void Flowshop::Backtrack(int i)
//x当前作业调度,bestx当前最优作业调度;
f完成时间和,bestf当前最优值。
{ 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]; //M[][]作业处理时间
f2[i]=((f2[i-1]>f1)?f2[i-1]:f1)+M[x[j]][2];
f+=f2[i];
if (f < bestf) { Swap(x[i], x[j]);
Backtrack(i+1);
Swap(x[i], x[j]); }
f1- =M[x[j]][1];
f- =f2[i];
}
}
图的M着色问题
给定无向连通图G和m种不同的颜色。用这些颜色为图G的各顶点着色,每个顶点着一种颜色。是否有一种着色法使G中每条边的2个顶点着不同颜色。这个问题是图的m可着色判定问题。
若一个图最少需要m种颜色才能使图中每条边连接的2个顶点着不同颜色,则称这个数m为该图的色数。求一个图的色数m的问题称为图的m可着色优化问题。
解向量:(x1, x2, … , xn)表示顶点i所着颜色x[i]
可行性约束函数:顶点i与已着色的相邻顶点颜色不重复。
int m; //给定的颜色数
int a[n][n]; //n个顶点图的邻接矩阵
int x[n]; //存放着色方案
int flag=0; //是否M可着色
解向量:(x1, x2, … , xn)表示顶点i所着颜色x[i]
可行性约束函数:顶点i与已着色的相邻顶点颜色不重复。
bool Ok(int k)
{ // 着色方案是否可行
for (int j=1;j<=n;j++)
if ((a[k][j]==1)&&(x[k]==x[j])) return false;
return true;
}
void backtrack(t) //判断图是否M可着色
{ if (t>n) //有着色方案
{ flag=1; return; }
for (i=1; i<=m; i++) //对M种颜色进行遍历
{ x[t]=i;
if ( ok(t)) backtrack(t+1) ; }
return;
}
N皇后问题
在n×n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n后问题等价于在n×n格的棋盘上放置n个皇后,任何2个皇后不放在同一行或同一列或同一斜线上。求N皇后问题的一种放法,或求N皇后问题的所有放法。
要素一: 解空间
一般想法:利用二维数组,用[i,j]确定一个皇后位置!
优化:
利用约束条件,只需一维数组即可!
x:array[1…n] of integer;
x[i]:i表示第i行皇后
x[i]表示第i行上皇后放第几列
要素二:约束条件
不同行:数组x的下标保证不重复
不同列:x[i]<>x[j] (i<=I,j<=n;i<>j)
不同对角线:abs(x[i]-x[j])<>abs(i-j)
要素三:状态树
将搜索过程中的每个状态用树的形式表示出来!
算法描述:
1.产生一种新放法
2.冲突,继续找,直到找到不冲突----不超范围
3.if 不冲突 then k<n ->k+1
k=n ->一组解
4.if 冲突 then 回溯
解向量:(x1, x2, … , xn)
显约束:xi=1,2, … ,n
隐约束:1)不同列:xi≠xj
2)不处于同一正、反对角线:|i-j≠|xi-xj|
bool Queen::Place(int k)
{
for (int j=1;j<k-1;j++) //填到第K行时,就与前1~(K-1)行都进行比较
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++;
else
for (int i=1;i<=n;i++) { //每层均有n种放法
x[t]=i; //放置本层皇后
if (Place(t)) Backtrack(t+1);
}
}
最大团问题
给定无向图G=(V,E)。如果UV,且对任意u,vU有(u,v)E,则称U是G的完全子图。G的完全子图U是G的团当且仅当U不包含在G的更大的完全子图中。G的最大团是指G中所含顶点数最多的团。
解题思路:
首先设最大团为一个空团,往其中加入一个顶点,然后依次考虑每个顶点,查看该顶点加入团之后仍然构成一个团。
如果可以,考虑将该顶点加入团或者舍弃两种情况;如果不行, 直接舍弃,然后递归判断下一顶点。
对于无连接或者直接舍弃两种情况,在递归前,可采用剪枝策略来避免无效搜索。
解空间:子集树
可行性约束函数:顶点i到已选入的顶点集中每一个顶点都有边相连。
上界函数:有足够多的可选择顶点使得算法有可能在右子树中找到更大的团。
当前团的顶点数cn,最大团顶点数bestn,从根结点R开始,按照1、2、3、4、5的顺序深度搜索。
void Clique::Backtrack(int i) { // 计算最大团
if (i > n) { // 到达叶结点
for (int j = 1; j <= n; j++) bestx[j] = x[j];
bestn = cn; return;} //当前团的顶点数cn
int OK = 1;
for (int j = 1; j < i; j++) // 检查顶点 i 与当前团的连接
if (x[j] && a[i][j] == 0) { // i与j不相连
OK = 0; break; }
if (OK) { // 进入左子树
x[i] = 1; cn++;
Backtrack(i+1);
x[i] = 0; cn--;}
if (cn + n - i > bestn) { // 进入右子树
x[i] = 0;
Backtrack(i+1);}
}
回溯算法的效率在很大程度上依赖于以下因素:
(1)产生x[k]的时间;
(2)满足显约束的x[k]值的个数;
(3)计算约束函数constraint的时间;
(4)计算上界函数bound的时间;
(5)满足约束函数和上界函数约束的所有x[k]的个数。
好的约束函数能显著地减少所生成的结点数。但这样的约束函数往往计算量较大。因此,在选择约束函数时通常存在生成结点数与约束函数计算量之间的折衷。