赞
踩
以下为算法设计与分析赵老师上课的课件,用于自己复习和理解
4.1基本思想
4.2 0-1背包问题
4.3 旅行商问题
4.4 装载问题
分支界限法类似于回溯法,也是在问题的解空间树上搜索问题解的一类算法。
分支界限法采用广度优先及最小耗费/最大价值优先的策略。依次生成扩展节点的所有分支,即将其所有子节点加入活节点(在生成节点中,抛弃/剪去不满足约束条件或者不可能导出最优解的节点,其余节点加入活结点表)。然后按一定的规则从活节点表中选择一个节点作为下一个扩展节点。
求解目标:回溯法-找出解空间树中满足约束条件的所有解或者满足特定条件的一组逐渐更优解;
分支界限法-尽快找出满足约束条件并符合某种性质的一个解,即是满足某目标函数值阶段性最大/最小的解,可以说是某种意义下的最优解。
回溯法:深度优先搜索解空间
设N=3,W=(16,15,15),P=(45,25,25),C=30
1.活节点:{A};扩展节点{A}
2.活节点:{A,B};扩展节点{B}
3.活节点:{A,B,E};扩展节点{E}
4.活节点:{A,B,E,K};扩展节点{K}
5.活节点:{A,B,E};扩展节点{E}
6.活节点:{A,B};扩展节点{B}
7.活节点:{A};扩展节点{A}
8.活节点:{A,C};扩展节点{C}
9.活节点:{A,C,F};扩展节点{F}
10.。。。。。。
广度优先搜索解空间
设N=3,W=(16,15,15),P=(45,25,25),C=30
1.活节点:{A};扩展节点{A}
2.活节点:{B,C};扩展节点{B}
3.活节点:{C,E};扩展节点{C}
4.活节点:{E,F,G};扩展节点{E}
5.活节点:{F,G,K};扩展节点{F}
6.活节点:{G,K,L,M};扩展节点{G}
7.活节点:{K,L,M,N,O};扩展节点{K}
8.活节点:{L,M,N,O};扩展节点{L}
9.活节点:{M,N,O};扩展节点{M}
10.。。。。。。
节点扩展原则;
活节点删除原则;
扩展节点选择原则。
扩展节点选择原则:选择下一个活节点作为扩展节点的方式不同,导致两种不同的分支搜索方式:
1.队列式(FIFO)搜索
2.优先队列式搜索(最小耗费/最大价值优先的策略)
1.FIFO搜索
先进先出(First In First Out,FIFO)搜索算法依赖“队列”做基本的数据结构。一开始,根节点是唯一的活结点,根节点入队。从活节点队中取出根节点后,作为当前扩展节点。对当前扩展结点,先从左到右地产生它的所有儿子,用约束条件检查,把所有满足约束函数的子节点加入活节点队列中。再从活节点队列中取出队首节点(队中最先进来的节点)为当前扩展结点,…,直到找到一个解或活节点队列为空为止。
先进先出(First In First Out,FIFO)搜索算法图列
扩展过程(满足约束和界限函数节点可加入队列)
2.优先队列式搜索
为了加快搜索的进程,应采用有效方式选择扩展节点进行扩展。优先队列式搜索就是对每一活节点计算一个优先级(某些信息的函数值),并根据这些优先级,从当前活节点表中选择一个优先级最高(最有利)的节点作为扩展节点,使搜索朝着解空间树上有最优解的分支推进,以便尽快地找出一个最优解。关键是使用数据结构“最大/最小堆(heap)”
堆的一些相关概念
(一)堆的引入
在许多算法中,需要支持下面二种运算:
!插入元素
!!寻找最大值元素(或最小值元素)
支持这二种运算的数据结构称为优先队列。
可采用下述三种方法实现优先队列:
(1)使用普通数组,插入容易(尾部),但寻找最大值需搜索整个队列,开销比较大。
(2)使用排序数组,寻找最大值元素容易,插入操作需移动很多元素。
(3)使用堆,寻找最大值元素容易,插入操作仅需移动少量元素。
(二)堆的定义(二叉堆)
定义:一个(二叉)堆是一颗几乎完全的二叉树,它的每个节点都满足堆的特性:设v是一个节点,p(v)是v的父节点,那么存储在p(v)中的数据项键值大于或等于存储在v中的数据项键值。
几乎完全二叉树
如果一棵二叉树,(在底层)除了最右边位置上的一个或几个叶子可能缺少外,它是丰满的,我们定义它为几乎完全二叉树。
堆数据结构支持下列运算
1.removeMax/Min[H]:从一个非空堆H中,删除最大/小键值的数据项,并返回该数据;
2.put[H,x]:将数据项x插入堆H中;
3.delete[H,i]:从堆中删除第i项;
4.makeHeap[A]:将数组转换为堆。
堆的蕴含特性:
沿着每条从根到叶子的路径,元素的键值以非降序/非升序排列。
堆和它的数组表示法
把存储在堆中的数据项视为键值。按树的节点“自顶向下”,“从左至右”,“按1到n”的顺序进行编号,那么数组元素H[i]对应树中编号为i的节点。
堆的数组表示以下的性质
有n个节点堆T,可以用一个数组H[1…n]用下面方式来表示:
(1)T的根节点存储在H[1]中;
(2)设T的节点存储在H[j]中,如果它有左子节点,则这个左子节点存储在H[2j]中;如果它还有右子节点,这个右子节点存储在H[2j+1];
(3)若元素H[j]不是根节点,它的父节点存储在H[j/2(取下界)]中。
由“几乎完全二叉树”的定义可知,如果堆中某节点有右子节点,则它一定也有左子节点。
堆具有如下性质:
key(H[j/2(取下界)])>=key(H[j]) 2<=j<=n
**[问题陈述]**设有n个物体和一个背包,物体i的重量为wi,价值为pi,背包的载荷为M,找一个装载方案,使得能放入背包的物体总价值U最高。
**[算法思路]**将问题的解表为n元向量:{x1,…,xn},xi属于{0,1}用子集树表示解空间,在树中做优先队列式搜索
约束条件:
目标函数:
目标函数界限初值:U=0
c(x):从根至x的部分路径的权值(优先策略,当前价值)
c(x)’:以x为根的子树中路径权值最大者(所有剩余物品)界限函数 上述两者和。//没看懂?
设N=3W=(20,15,15),P=(40,25,25),M=30,当前价值为C
优先式活节点变化:
{}->{A}->{B,C}->{C,D(舍),E}->{C,J(舍),K(40)}->{C}->{F,G(舍)}->{L(50),M(25)(舍)}->{M}->{}
[问题陈述]设G(V,E)是带权有向图,|V|=n,耗费矩阵C=(ci,j),当(i,j)不属于E时,ci,j=∞且cj,i=∞。选择周游路线使耗费最小。
[算法思路]设周游路线从节点1开始,解为等长数组:X(1,X2,…,Xn),xi属于{2,…,n}.解空间树为排列树。
约束条件:xi≠xj ,i≠j;
目标函数:解向量对应的边权之和
目标函数界限初值:U=∞
c(x):从根至x的部分路径的权值
c(x)’:以x为根到叶子节点路径中叶节点外所有节点的最小费用出边和
下界:上述两者和
优先策略:当前路径权值(当前路径费用)
活结点表:{A}-{B}-{C,D,E}-{C,D,J,K}-{C,J,K,H,I}-{C,J,K,I,N}-{C,K,I,N,P}-{C,K,I,P}-{C,K,I}-{C,I}-{C}-{}
只求一个最优解时算法的终止条件是:遇到第一个叶节点称为当前的扩展节点。
分支界限法小结
1.组合优化问题解空间中按广度优先和特定的策略进行扩展搜索
2.特定的策略用于活节点列表中节点的选择和搜索
3.两类主要扩展搜索策略包括FIFO队列式搜索和优先队列式搜索
4.优先队列式搜索中往往采用阶段性的最小耗费/最大价值优先的策略(类似贪心),从而使得搜索向着符合策略性质的解(或近似解)尽快靠近
小结(用于搜索方式求解最优化问题)
在问题的解空间中进行搜索,找一个可行叶节点(问题的可行解),使其对应问题值最大/最小。当搜索到一个活节点时,一次性扩展它的所有儿子,将满足约束条件及限界函数的子结点,插入活结点表中,再从活结点表中按一定策略取一个节点同样扩展,直到找到所需的解或活动节点表为空。
花费/价值界限函数c(x):以c为根的子树所包含的可行解中,花费最小或价值最高的可能值函数。对叶节点,即是可行解值。
目标界限值U:初始U可取0/∞,若x是任一叶节点,且c(x)小于U,则更新U=c(x*),可保持x*为已知可行叶节点中值最大/最小节点。当搜索到节点x,而c(x)小于/大于U时,x将不必扩展(剪枝)。
与回溯法的区别
1)对解空间树的搜索方式:
回溯法:深度优先搜索
分支界限法:广度优先搜索或最小消耗优先搜索
2)存储节点的常用数据结构
回溯法:堆栈
分支界限法:队列,优先队列
3)节点存储特性
回溯法:活结点的所有可行子节点被遍历后才被从栈中弹出
分支界限法:每个节点只有一次成为活节点的机会
4)常用应用
回溯法:找出满足条件的所有解或最优解
分支界限法:找出满足条件的一个解或特定意义下的近似最优解
分支界限法的存储空间比回溯法大很多,因此当内存容量有限时,回溯法成功的可能性更大
例:有两艘船,n个货箱,第一艘船的载重是c1,第二艘船的载重是c2,wi是货箱i的重量,所有货箱的重量和小于等于c1+c2.若有一种可将所有n个货箱全部装船的方法,找出该方法。
思路:首先将第一艘船尽可能装满,然后将剩余货箱装入第二艘船。即第一艘船的最优装载问题。
将问题转化为一艘船的最优装载问题(特殊的0-1背包问题)后,问题的解空间为一个子集树。也就是算法要考虑所有物品取舍情况的组合,n个物品的取舍组合为2^n个分支。
搜索子集树是NP-C问题。
n=3时,船A装货方案的所有可能方案,用一个子集树表示。物品的重量:w={10,30,50}船C1=60
当前实际装在船A上的货物总重量用ew表示,需要满足约束:ew+xi<=c1,bestw为当前已产生的最有装载值。
FIFO问题解过程
物品的重量W={10,30,50},船C1=60
算法1(先进先出策略实现)
MaxLoading(int c,int [] w)//返回最有装载值 { n=w.lngth-1;//货物数量 int i=1;//节点的层,对应货物编号,根节点可理解为第0层 ew=0;//当前船的装载量 bestw=0;//当前船目前的最优值 queue=new ArrayQueue();//活节点队列 queue.put(new Integer(-1));//层尾标记节点,用来辅助控制层号i while(true){//搜索子集树 if(ew+w[i]<=c)//检查扩展节点的左子节点,约束函数 enQueue(ew+w[i],i);//物品i可以装载 enQueue(ew,i);//右子节点总是可行的,即不装载物品i ew=queue.remove().intvalue;//从活节点队列取下一个扩展节点 if(ew=-1){//如果取出节点为层尾标记节点 if(queue.isEmpty()) return bestw; queue.put(new Integer(-1));//标记新层尾 ew=queue..remove().intvalue;//再从队列取下一个扩展节点 i++;//进入下一层 } } } enQueue(int ew,int i) { if(i==n)//叶节点只记录当前最优值 { if(ew>bestw) bestw=ew; } else queue.put(new Integer(ew));//非叶节点加入队列 }
算法1可改进之处:
算法1是对子集树进行枚举搜索,我们虽然不能将搜索算法改进为多项式复杂度,但如果在算法中加入了“限界”技巧,通过“剪枝”方法还是能降低算法的复杂度。
一个简单的现象,若当前节点的“装载上界”,比现有的最优装载bestw小,则以该节点为根的子树(分支)就无需继续搜索。而一个节点的“装载上界”是容易求解的,比如用当前扩展节点装载量ew加上所有剩余货物重量作为“装载上界”。
在可解的情况下,算法1没有给出装载物品的方案,只求出了最优装载量值。而要想记录第一艘船最大装载的方案,不能像回溯法中用n个元素来实现(深度优先是一直扩展产生一条路径)。
因此,为了记录活节点到根节点的路径,可在每个节点处存储有指向其父节点的指针,以便从叶节点回溯找解的方案。
此外,为了方便知道当前节点对物品的取舍情况,还必须记录当前节点是父节点的哪一个子节点。
class QNode { int weight; QNode parent; boolean leftChild;//true为左子节点,false为右子节点 } MaxLoading(int c,int [] w,int [] xx)//返回最优装载值 { n=w.lngth-1;//货物数,也是层数 int i=1; Qnode e=null; ew=0; bestw=0; r=0;//r为剩余货物重量 Qnode bestE=null;//bestE为当前最优扩展节点 int bestx[]=xx;// bestx存最优方案 queue=new ArrayQueue(); queue.put(null);//标记分层 for(int j=2;j<=n;j++) r+=w[j];//初始为去除第一个货物后重量 while(true){ if(ew+w[i]<=c){//检查左子节点 if(ew+w[i]>bestw) bestw=wt;//尽早实现剪枝不必等到第一个可行解 enQueue(wt,i,e,true);//物品i可以装载,传入parent和leftChild } if(ew+r>bestw) enQueue(ew,i,e,false);//右子节点上界判定 e=(QNode)queue.remove();//从活节点队列取下一个扩展节点 if(r=null){//到达层的尾部 if(queue.isEmpty()) break; queue.put(null);//标记分层 e=(QNode)queue.remove();//从队列取下一个扩展节点 i++;//进入下一层 r-=w[i];//修改剩余货物重量 } ew=e.weight;//取扩展结点处已装载的重量 } for(int j=n-1;j>0;j--){ bestx[j]=(bestE.leftChild)?1:0; bestE=bestE.parent; }//构造最优装载方案 return bestw; } enQueue(int ew,int i,Qnode parent,boolean leftChild) { if(i==n)//是叶节点 { if(ew>bestw){ bestE=parent; bestx[n]=(leftChild)?1:0; } return ; } else { QNode b=new QNode(ew,i,parent,leftChild); queue.put(b); } }
优先队列求解
可采用的两种优先策略:
1.当前状态时船的装载量:当前状态船A上已经装入的货箱总重量(ew)。
2.当前状态时船的最优装载量上界:ew+剩余货箱重量和
class BNode//节点类 { BNode parent; boolean leftChild; } class HeapNode//最大堆中节点类 { BNode liveNode; int uweight;//活节点上界:ew+剩余物品重量 int level;//活节点层次 HeapNode(Bnode node,int up,int lev){} compare To(Object x){ if uweight<x.uweight return -1; if uweight>x.uweight return 1; return 0; } } maxLoading(int c,int [] w,int [] xx)//返回最有装载值 { n=w.length-1;//w[0]无意义,货物数,也是层数,根为0层 int i=0; BNode e=root; ew=0; bestw=0; HeapNode node=null; int []r=new int[n+1];//r[]为各层剩余货物重量数组 r[n]=0 bestx=xx;//bestx存最优解 heap=new MaxHeap();//heap最大堆 for(int j=n-1;j>=0;j--) r[j]=j[j+1]+w[j+1]; while(i!=n+1){ int wt=ew+w[i]; if(wt<=c){//左子节点可行 if(wt>bestw) bestw=wt; BNode b=new BNode(e,true);//记录选择节点的父节点和是否左子节点 node=new HeapNode(b,wt+r[i+1],i+1); heap.put(node); } if(ew+r>bestw){//右子节点满足界限 BNode b=new BNode(e,false);//记录选择节点的父节点和是否左子节点 node=new HeapNode(b,ew+r[i+1],i+1); heap.put(node); } HeapNode node=new(HeapNode)heap.removeMax(); //从堆中取下一个扩展节点 i=node.level; e=node.liveNode; ew=node.uweight-r[i]; } for(int j=n-1;j>0;j--){ bestx[j]=(e.leftChild)?1:0; e=e.parent; } return bestw; }
[解题步骤]
1.确定解空间结构(子集树和排列树)
2.确定约束条件和限界函数
3.计算当前最优值.
4.按广度优先得到扩展节点(从根节点开始)的所有儿子.对每一子节点x判定其是否满足约束条件,对满足约束条件的 x 计算其是否满足限界条件,将满足条件的x 加入活节点表.
4.1 若x为叶节点时,检查当前解是否需更新已得到的最优值.
5. 按照一定策略取活节点表中的一个节点为新扩展节点重复步骤4,直到活节点表为空.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。