0x09算法设计与分析复习(二):算法设计策略-分枝限界法1
参考书籍:算法设计与分析——C++语言描述(第二版)
算法设计策略-分枝限界法
一般而言,回溯法的求解目标是在状态空间树上找出满足约束条件的所有解,而分枝限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出最优解。
分枝限界法
分枝限界法概述
相关概念:
+ 未访问状态:结点尚未访问的状态;
+ x**未检测状态**:x已访问但其后继未访问;
+ x**已检测状态**:x已访问且其后继已访问;
+ 扩展结点(E-结点):算法正从x出发,访问x的某个后继结点y;x被称为扩展结点。
+ 活结点:未检测结点;
+ 死结点:已检测结点;
+ 活结点表:保存活结点的数据结构;
+ D-检索:以栈为活结点表按BFS的算法。
采用广度优先产生状态空间树的结点,并使用剪枝函数的方法称为分枝限界法。
按照广度优先的原则,一个活结点一旦成为扩展结点(E-结点)R后,算法将依次生成他的全部孩子结点,并将它们一一加入活节点表,此时R自身成为死结点。算法从活结点表中另选一个活结点作为E-结点。
不同的活结点表形成不同的分枝限界法,分为:FIFO分枝限界法、LIFO分枝限界法和LC分枝限界法。
- FIFO分枝限界法的活结点表是先进先出队列,按队列的FIFO原则选取下一个结点为当前扩展结点;
- LIFO分枝限界法的活结点表是堆栈(即D-搜索),按栈的LIFO原则选取下一个结点为当前扩展结点;
- LC(least cost)分枝限界法的活结点表是优先权队列,按优先权队列中规定的结点优先级选取优先级最高的下一个结点为当前扩展结点。
三种不同的活结点表,规定了从活结点表中取下一个E-结点的不同次序。
//分枝限界法求解答案结点的算法框架 template<class T> struct Node { T cost; Node *parent;//状态空间树采用树的双亲表示法,parent是指向其双亲的指针 }; template<calss T> void BranchBound(Node<T> *t) { //t是指向状态空间树的根结点指针 LiveList<Node<T>*> lst(mSize);//lst为活结点表,表中的元素为指针类型 Node<T> *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分枝限界法
采用LC分枝限界法时,为了尽快搜索到一个答案结点,需要对活结点使用一个“有智力的”评价函数作为优先权来选择下一个E-结点。该评价函数通过衡量一个活结点的搜索代价,来确定哪个活结点能够引导尽快到达一个答案结点。
一个答案结点X的搜索代价
代价函数
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在树中的层次。
以代价估计函数
如果
如果
分枝限界法和回溯法
使用剪枝函数的深度优先生成状态空间树中结点的求解方法称为回溯法(backtracking);广度优先生成状态空间树中结点,并用剪枝函数的方法称为分枝限界法(branch-and-bound)。
分枝限界法的基本做法:
- 以广度优先的方式搜索问题的状态空间树。每一个活结点只有一次机会成为E-结点;
- 按照广度优先原则,活结点一旦成为E-结点R后,就依次生成它的所有孩子结点。在这些孩子结点中,导致不可行解或导致非最优解的孩子结点被舍弃,其余孩子结点被一一加入活结点表中;
- 然后R自身成为死节点,从活结点表中取下一结点成为当前E-结点,重复上述结点扩展过程,直到找到所需的解或活结点表为空时为止。
分枝限界法和回溯法的共同点:
- 都是在问题的状态空间树上搜索问题解的做法;
- 都用活结点表,都可用约束函数剪去不含答案结点的分枝,都可用限界函数剪去不含最优解的分枝;
分枝限界法和回溯法的不同点:
求解目标不同:回溯法的求解目标是找出解空间树中满足约束条件的所有可行解;而分枝限界法的求解目标是找出满足约束条件的一个可行解,或某种意义下的最优解;
搜索方式不同:回溯法以深度优先的方式搜索解空间树,而分枝限界法则以广度优先或最小耗费优先的方式搜索解空间树;
- 对当前扩展结点的扩展方式不同:回溯法中的每个活结点可能多次成为当前扩展结点,纵深方向扩展其一个孩子,然后再回溯后扩展其他孩子;而分枝限界法中每个活结点只有一次机会成为扩展结点,一次产生所有其他孩子结点,自身成为死结点,之后无需再返回该结点处。
15迷问题
问题描述
15迷问题叙述如下:在一个
假定棋盘上16个方格所在位置的编号与上图所示的目标状态的牌号相同,即第i号牌所在的位置为i,并设空格的编号为16.设
定理:对给定的初始状态,当且仅当
15迷问题的状态空间树是以给定的初始状态为根结点的树。每个状态结点X的孩子是从状态X通过一次合法的移动可以到达的状态。由于事实上移动号牌和移动空格是等价的,因此,从父状态到子状态的一次转换可以视为空格的一次向上、下、左或右的合法移动。
- 如果采用FIFO分枝限界法搜索状态空间树,它将按图中结点编号的顺序逐一生成树中结点,直至到达结点的目标状态。
- 如果采用深度优先生成状态空间树的方式,搜索从根结点开始,始终沿着树中最左边的那条路径搜索。
上面两种方式,无论广度优先还是深度优先都是盲目的。如果对每个结点X赋予某种搜索代价
对于15迷问题,一种代价估计函数可以定义为:
求最优解的分枝限界法
分枝限界法的三种形式:FIFO分枝限界法、LIFO分枝限界法和LC分枝限界法都可用于求解最优化问题。
上下界函数
当分枝限界法用于求最优解时,需要使用上下界函数作为限界函数。
在求解最优化问题时,这里的代价函数不再是前面所提的搜索代价,而是与最优化问题的目标函数相关的量。
定义:状态空间树上一个结点X的代价函数
定义:函数
假定目标函数取最小值时为最优解,那么算法需要一个上界变量,设为U,它在算法执行过程中,记录迄今为止已知的关于最小代价的上界值。这样对于任意结点X,若
基于上下界函数的分枝限界法的限界方法可描述如下:
- 算法要求U的初值大于最优解的代价,并在搜索状态空间树的过程中不断修正U的值。对于某个结点X,U的值可以按下列原则修正:
- 如果X是答案结点,
cost(X) 是X所代表的可行解的目标函数值,u(X) 是该子树上最小代价答案结点代价的上界值,则U=min{cost(X),u(X)+ϵ,U} ; - 如果X代表部分向量,则
U=min{u(X)+ϵ,U} 。
- 如果X是答案结点,
- 于是,算法可使用
c^(X)≥U 作为剪枝条件尽可能剪除多余分枝。
FIFO分枝限界法
//基于上下界函数的FIFO分枝限界法 template<class T> Node<T>* FIFOBB(Node<T> *t, T& U) { //t是指向状态树根指针,U的初值应大于最优解值,U返回最优解值 //函数返回答案结点指针ans LiveList<Node<T>*> lst(mSize);//lst为FIFO队列 Node<T> *ans = NULL, *x, *E=t;//ans指向答案结点,E为扩展结点 do{ for(对结点E的每个孩子){ //对满足约束条件的孩子 x= new Node; x->parent=E; //构造E的孩子结点 if(\hat{c}(x)<U){ //未被限界函数剪枝的子树根x lst.Append(x);//x进队列 if(x是一个答案结点 && cost(x)<U){ //x为答案结点时修正U if(u(x)+\epsilon <cost(x)) U=u(x)+\epsilon else{ U=cost(x); ans=x; } } else if(u(x)+\epsilon<U) U=u(x)+\epsilon;//x为非答案结点时修正U } } do{ if(lst.IsEmpty()) return ans;//若队列为空,返回指针ans lst.Serve(E);//从队列中取出活结点 }while(\hat{c}(E)>=U);//当\hat{c}(E)<U时,E成为扩展结点 }while(1); }
函数FIFOBB
时采用FIFO队列为活结点表的分枝限界法,算法使用上下界函数进行剪枝,算法在队列lst
为空的时候结束。函数返回指向答案结点的指针ans,状态空间树中从ans到t的路径是问题的最优解,变量U保存最优解值。
LC分枝限界法
//基于上下界函数的LC分枝限界法 template<class T> Node<T>* LCBB(Node<T> *t,T& U) { LiveList<Node<T>*> lst(mSize);//lst为优先权队列 Node<T> *ans = NULL,*x,*E=t; do{ for(对结点E的每个孩子){ //对满足约束条件的孩子 x= new Node; x->parent=E; //构造E的孩子结点 if(\hat{c}(x)<U){ //未被限界函数剪枝的子树根x lst.Append(x);//x进队列 if(x是一个答案结点 && cost(x)<U){ //x为答案结点时修正U if(u(x)+\epsilon <cost(x)) U=u(x)+\epsilon else{ U=cost(x); ans=x; } } else if(u(x)+\epsilon<U) U=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
采用优先权队列作为活结点表。两者的区别在于前者只有当活结点表为空时算法才结束;后者以优先权队列为空或
小结
分枝限界法可以用于求可行解,也可以求最优解。使用分枝限界法求最优化问题要求为状态空间树的每个结点适当地定义上下界函数,并记录迄今为止求得的最优解值。利用这两点通常可以有效地减少搜索到最优答案结点地时间。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)