参考书籍:算法设计与分析——C++语言描述(第二版)
一般而言,回溯法的求解目标是在状态空间树上找出满足约束条件的所有解,而分枝限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出最优解。
相关概念:
+ 未访问状态:结点尚未访问的状态;
+ x**未检测状态**:x已访问但其后继未访问;
+ x**已检测状态**:x已访问且其后继已访问;
+ 扩展结点(E-结点):算法正从x出发,访问x的某个后继结点y;x被称为扩展结点。
+ 活结点:未检测结点;
+ 死结点:已检测结点;
+ 活结点表:保存活结点的数据结构;
+ D-检索:以栈为活结点表按BFS的算法。
采用广度优先产生状态空间树的结点,并使用剪枝函数的方法称为分枝限界法。
按照广度优先的原则,一个活结点一旦成为扩展结点(E-结点)R后,算法将依次生成他的全部孩子结点,并将它们一一加入活节点表,此时R自身成为死结点。算法从活结点表中另选一个活结点作为E-结点。
不同的活结点表形成不同的分枝限界法,分为:FIFO分枝限界法、LIFO分枝限界法和LC分枝限界法。
三种不同的活结点表,规定了从活结点表中取下一个E-结点的不同次序。
//分枝限界法求解答案结点的算法框架
template<class T>
struct Node
{
T cost;
Node *parent;//状态空间树采用树的双亲表示法,parent是指向其双亲的指针
};
template
void BranchBound(Node *t)
{
//t是指向状态空间树的根结点指针
LiveList*> lst(mSize);//lst为活结点表,表中的元素为指针类型
Node *x,*E=t;//E指向根结点t
do{
//为方便起见,以下描述中不区分指针与其所指示的结点,用指针代表所指示的结点
for(对结点E的每一个不受限的孩子){
x=new Node;
x->parent=E;//构造E的孩子结点
if(x是一个答案结点){
输出从x到t的一条路径;
return ;//输出一个解后算法终止
}
lst.Append(x);//指向活结点的指针x进活结点表
}
if(lst.IsEmpty()){
cout<<"没有答案结点";
return;//搜索失败终止
}
lst.Serve(E);//从lst输出一个活结点为E-结点
}while(1);
}
显然,程序在求得一个可行解后就终止。LC分枝限界法也可用于求解最优化问题,如果增加一个全局变量cost,并在搜索中队每一个可行解计算目标函数值,并记录迄今为知最优值,最终可以得到问题的最优解。
回溯法、FIFO和LIFO分枝限界法从活结点表中选择一个活结点,作为新E-结点的做法是盲目的,它们只是机械地按照FIFO或LIFO原则选取下一个活结点。使用LC分枝限界法可根据每一个活结点的优先权进行选择。如果将一个问题状态的优先权定义为“在状态空间树上搜索一个答案结点所需的代价”,使得搜索代价小的活结点优先被检测,理论上应能较快搜索到一个答案结点。
回溯法和分枝限界法都可使用约束函数来剪去不含答案结点的分枝,并都可使用限界函数剪去那些不含最优解的分支。
采用LC分枝限界法时,为了尽快搜索到一个答案结点,需要对活结点使用一个“有智力的”评价函数作为优先权来选择下一个E-结点。该评价函数通过衡量一个活结点的搜索代价,来确定哪个活结点能够引导尽快到达一个答案结点。
一个答案结点X的搜索代价 cst(X) 定义为:从根节点开始,直到搜索到X为止所耗费的搜索时间。以下定义4个相关函数:
代价函数 c(⋅)
若X是答案结点,则 c(X) 是从根节点到X的搜索代价;若X不是答案结点且子树X上不含任何答案结点,则 c(X)=∞ ;若X不是答案结点但子树X上包含答案结点,则 c(X) 等于子树X上具有最小搜素代价的答案结点的代价。
相对代价函数 g(⋅)
衡量一个结点X的相对代价一般有两种标准:(1)在生成一个答案结点之前,子树X上需要生成的节点数目;(2)在子树X上,离X最近的答案结点到X的路径长度。如果采用标准(1),总是生成最小数目的结点;如果采用标准(2),则要成为E-结点的结点只是由根到最近的那个答案结点路径上的那些结点。
然而,计算每个结点的代价和相对代价都是十分困难的,他们的计算难度不亚于求取答案结点,多以通常只能采用他们的估计值来构建评价函数,实现LC分枝限界算法。
相对代价估计函数 g^(⋅)
g^(X) 作为 g(X) 的估计值,用于估计结点X的相对代价,它是由X到达一个答案结点所需代价的估计函数。一般,假定 g^(X) 满足如下特性:如果Y是X的孩子,则有 g^(Y)≤g^(X) 。
代价估计函数 c^(⋅)
c^(X) 是代价估计函数,它由两部分组成:从根到X的代价 f(X) 和从X到答案结点的估计代价 g^(X) ,即 c^(X)=f(X)+g^(X) 。一般而言,可令 f(X) 等于X在树中的层次。
以代价估计函数 c^(⋅) 作为选择下一个扩展结点的评价函数,即总是选取 c^(⋅) 值最小的活结点作为下一个E-结点。这种搜索策略称为最小成本检索(least cost search),简称LC-检索。LC分枝限界法采取LC检索方法,以优先权队列作为活结点表,并以代价估计函数 c^(⋅) 作为选择下一个扩展结点的评价函数。LC分枝限界法同样要使用剪枝函数。
如果 f(⋅)=0 ,则 c^(X)=g^(X) ,LC-检索表现出深度优先搜索特性,成为D-检索。
如果 g^(⋅)=0 ,且 f(X) 等于X在树中的层次,则LC-检索表现出广度优先搜索特性,成为FIFO检索。一般要求 f(⋅) 是一个非降函数。
使用剪枝函数的深度优先生成状态空间树中结点的求解方法称为回溯法(backtracking);广度优先生成状态空间树中结点,并用剪枝函数的方法称为分枝限界法(branch-and-bound)。
分枝限界法的基本做法:
分枝限界法和回溯法的共同点:
分枝限界法和回溯法的不同点:
求解目标不同:回溯法的求解目标是找出解空间树中满足约束条件的所有可行解;而分枝限界法的求解目标是找出满足约束条件的一个可行解,或某种意义下的最优解;
搜索方式不同:回溯法以深度优先的方式搜索解空间树,而分枝限界法则以广度优先或最小耗费优先的方式搜索解空间树;
15迷问题叙述如下:在一个 4×4 的方形棋盘上放置了15块编了号的牌,还剩下一个空格。要求从任意一个初始排列,通过一系列的合法移动,转换成目标排列(目标排列如下图)。
假定棋盘上16个方格所在位置的编号与上图所示的目标状态的牌号相同,即第i号牌所在的位置为i,并设空格的编号为16.设 Less(i) 为满足下列情况的号牌的数目:这些号牌的牌号小于i,但当前被放置在号牌i的位置之后。(Less(4):小于号牌4的号牌有1,2,3, 号牌1,2,3都在号牌4前面,号牌4后面没有一个号牌小于4,故Less(4)=0)
定理:对给定的初始状态,当且仅当 ∑16k=1Less(k)+i+j 为偶数时,可以由此初始状态到达目标状态,其中i和j分别是空格在棋盘上的行和列下标。
15迷问题的状态空间树是以给定的初始状态为根结点的树。每个状态结点X的孩子是从状态X通过一次合法的移动可以到达的状态。由于事实上移动号牌和移动空格是等价的,因此,从父状态到子状态的一次转换可以视为空格的一次向上、下、左或右的合法移动。
上面两种方式,无论广度优先还是深度优先都是盲目的。如果对每个结点X赋予某种搜索代价 c(X) ,不妨将一个答案结点的搜索代价定义为从根到该结点的路径长度, c(⋅) 的定义与前面相同,那么如果该结点不能到达目标状态则其代价为 ∞ 。如果能事先计算每个结点的 c(⋅) 值,则必然能够导致最高效的搜索过程,但是,快速计算每个结点的代价函数 c(⋅) 几乎是不可能的。
对于15迷问题,一种代价估计函数可以定义为: c^(X)=f(X)+g^(X) ;其中 f(X) 是从根到结点X的路径长度, g^(X) 是不在其位的非空白牌数目。这样定义的 g^(X) 值不会超过从X出发到达目标状态所需移动的牌数。因此, c^(X) 是 c(X) 的下界。
分枝限界法的三种形式:FIFO分枝限界法、LIFO分枝限界法和LC分枝限界法都可用于求解最优化问题。
当分枝限界法用于求最优解时,需要使用上下界函数作为限界函数。
在求解最优化问题时,这里的代价函数不再是前面所提的搜索代价,而是与最优化问题的目标函数相关的量。
定义:状态空间树上一个结点X的代价函数 c(⋅) 定义为:若X是答案结点,则 c(X) 为X所代表的可行解的目标函数值;若X为非可行解结点,则 c(X)=∞ ;若X代表部分向量,则 c(X) 是以X为根的子树上具有最小代价的结点代价。显然,这样定义的 c(X) 是难以计算的,它的计算难度与求得问题最优解的难度相当。
定义:函数 u(⋅) 和 c^(⋅) 分别是代价函数 c(⋅) 的上界和下界函数。对所有结点X,总有 c^(X)≤c(X)≤u(X) 。
假定目标函数取最小值时为最优解,那么算法需要一个上界变量,设为U,它在算法执行过程中,记录迄今为止已知的关于最小代价的上界值。这样对于任意结点X,若 c^(X)>U ,则X子树可以剪枝。在算法已经搜索到一个答案结点之后,所有满足 c^(X)≥U 的子树X都可以剪除。但在此之前,以 c^(X)≥U 作为剪枝条件会将最小答案结点误剪除掉。为了既能运用 c^(X)≥U 作为剪枝条件,又不至于误剪去包含最小结点的子树,可以对所有结点X,使用 u(X)+ϵ 作为该子树的最小代价上界值, ϵ 是一个小量。
基于上下界函数的分枝限界法的限界方法可描述如下:
//基于上下界函数的FIFO分枝限界法
template<class T>
Node<T>* FIFOBB(Node<T> *t, T& U)
{
//t是指向状态树根指针,U的初值应大于最优解值,U返回最优解值
//函数返回答案结点指针ans
LiveList*> lst(mSize);//lst为FIFO队列
Node *ans = NULL, *x, *E=t;//ans指向答案结点,E为扩展结点
do{
for(对结点E的每个孩子){
//对满足约束条件的孩子
x= new Node;
x->parent=E;
//构造E的孩子结点
if(\hat{c}(x)//未被限界函数剪枝的子树根x
lst.Append(x);//x进队列
if(x是一个答案结点 && cost(x)//x为答案结点时修正U
if(u(x)+\epsilon else{
U=cost(x);
ans=x;
}
} else if(u(x)+\epsilon//x为非答案结点时修正U
}
}
do{
if(lst.IsEmpty())
return ans;//若队列为空,返回指针ans
lst.Serve(E);//从队列中取出活结点
}while(\hat{c}(E)>=U);//当\hat{c}(E)
}while(1);
}
函数FIFOBB
时采用FIFO队列为活结点表的分枝限界法,算法使用上下界函数进行剪枝,算法在队列lst
为空的时候结束。函数返回指向答案结点的指针ans,状态空间树中从ans到t的路径是问题的最优解,变量U保存最优解值。
//基于上下界函数的LC分枝限界法
template<class T>
Node<T>* LCBB(Node<T> *t,T& U)
{
LiveList*> lst(mSize);//lst为优先权队列
Node *ans = NULL,*x,*E=t;
do{
for(对结点E的每个孩子){
//对满足约束条件的孩子
x= new Node;
x->parent=E;
//构造E的孩子结点
if(\hat{c}(x)//未被限界函数剪枝的子树根x
lst.Append(x);//x进队列
if(x是一个答案结点 && cost(x)//x为答案结点时修正U
if(u(x)+\epsilon else{
U=cost(x);
ans=x;
}
} else if(u(x)+\epsilon//x为非答案结点时修正U
}
}
if(!lst.IsEmpty()){
lst.Serve(E);//从队列中取出活结点
if(\hat{c}(E)>=U)
return ans;//若\hat{c}(E)>=U,算法结束
} else
return ans;//若队列为空,算法结束
}while(1);
}
与FIFOBB
相比,函数LCBB
采用优先权队列作为活结点表。两者的区别在于前者只有当活结点表为空时算法才结束;后者以优先权队列为空或 c^(X)≥U 为算法终止条件。 c^(X) 作为结点X的优先权。
分枝限界法可以用于求可行解,也可以求最优解。使用分枝限界法求最优化问题要求为状态空间树的每个结点适当地定义上下界函数,并记录迄今为止求得的最优解值。利用这两点通常可以有效地减少搜索到最优答案结点地时间。