(1)分支限界法和回溯法的不同
· 首先说一下分支限界法和回溯法的不同。第一是求解目标不同,回溯法的求解目标是得出满足解空间树约束条件的所有 解, 一般来说是求一个问题有没有解,或者解都是哪些情况;分支限界法的目标是求出满足约束条件的一个解亦或者是 最优解,一般来说是求某种情况下的最短路程啊,最大策略啊,最优方法之类的。第二是搜索方式的不同,回溯法以深度 优先方式搜索解空间树,因为一般来说回溯法是判断一个问题有没有解,它找到一个解就跳出了,证明这个问题是有解的 (当然不是所有的题都这样,一些题可能会要求输出所有解,这时候就要循环完了),所以它找的是具体的每一个解,从 这个解的根节点到最底部的子节点,一个答案一个答案的找;分支限界法不同,它是广度优先或者最小耗费优先的方式搜 索解空间树。它的目的是找出最优解,体现为花同样的时间和步骤,哪个解最先找到就说明哪个解是最优解,在找的过程 中同时记录步数,最后得到最先找到的解输出这个最优解的步数即可。这也决定了二者的实现工具不同。前者使用递归或 者是栈,后者采用队列。
· 这里我再顺便说一下栈和队列的区别和联系。两者都是一种数据结构,栈的特点是先进后出,后进先出,先进的放在最底 层,然后逐次向上叠加新放进来的,就像一摞书一样,先放的压在底下,后放的在上边,取得时候先取上面的,最先放的 也就是最底下的肯定是最后才取到。在栈里面叫做栈底和栈顶。联系一下回溯的深搜方法,从第一个节点开始找,如果这 个节点有解就找他的第一个子节点,此时把第一个节点作为栈底,上面他的第一个子节点入栈,如果第一个子节点不包含 最终的解,就把第一个子节点出栈即踢掉,此时栈顶就是第一个节点,然后找这个节点的第二个子节点,里面包含了解, 然后逐层深入,包含解就往下走,不包含出栈,然后找到包含的那一层从另一个方向出发入栈再找,最终找到那个解。队 列是另一种数据结构,先进先出,后进后出,先进的为对队列头,后进的是队列尾,联想一下广搜。先把第一层的都入队 列,然后检查,没有找到,再把第二层的入队列,没找到,再把第三层的入队列,招到了,花了三步完成,输出最优解是 三步。每次出队列的是花相同时间精力找到的一层,如果没找到就是时间更长的下一层的节点,直到找到解,那么它肯定 是最少步得到的解。
(2)分支限界法的基本思想
· 常以广度优先或最小耗费(最大效益)优先的方式搜索解空间树。
· 在分支限界法中,每个活节点只有一次机会成为扩展节点,活结点一旦成为扩展节点,便会一次性产生所有的扩展节点, 在这些子节点中,导致不可行解的子节点被抛弃,其余子节点被加入活结点表中。此后,从活结点表中取下一节点成为当 前扩展节点,并重复上面扩展过程,一直到找到所需解或者活结点表为空为止。
· 一般代码形式:
void search()
{
open表初始化为空;
起点加入到open表;
while( open表非空 )
{
取open表中的一个结点u;
从open表中删除u;
for( 对扩展结点u得到的每个新结点vi )
{
if(vi是目标结点)
输出结果并返回;
If(notused(vi))
vi进入open表;
}
}
}
例一:电子老鼠创迷宫
如下图12×12方格图,找出一条自入口(1,8)到出口(10,7)的最短路径。
分析:求最短路径,则肯定使用分支限界的广搜方法(注:如果是求是否能走出迷宫,那么既可以使用分支限界广搜法,也能使 用回溯的深搜)。首先将起点坐标加入队列,步数记为0。开始起点出队列,然后从四个方向分别走出一步,四个方向新 的单元格入队列,步数记为1, 第二步从四个新的单元格分别走出四个方向,步数记为2入队列,然后他们自身出队列。每 次队列中原有的元素都是同一个步数到达的,是同一个阶层的。本次循环完了之后这一阶层的都出去了,这一阶层所有的 子节点共同构成了新的队列中的阶层,并且步数加了一。循环直到所有的单元格都走完,或者先找到目标节点就直接输出 步数,因为肯定是最短步数找到的。
解答:下面只写了最重要的一些代码,具体实现自己再写一下即可:
int dirx[4]={0,0,1,-1},diry[4]={1,-1,0,0};
int step[15][15],used[15][15];
struct addr
{
queue<int> x;
queue<int> y;
}addr;
void readdata();//读入数据
void empty();//清空队列
void init()//初始化
{
addr.x.push(sx);
addr.y.push(sy);//把起点坐标输入到队列中
used[sx][sy]=1;//标记起点是已经走过的点
step[sx][sy]=0;//在起点已经走了0步
}
int bfs()
{
while(!addr.x.empty()&&!addr.y.empty())
{
int cx,cy;
cx=addr.x.front();
cy=addr.y.front();//取出队首坐标进行本次操作
addr.x.pop();
addr.y.pop();//队首坐标出队列
for(int i=0;i<4;i++)//四个方向都走一步
{
int vx,vy;
vx=cx+dirx[i];
vy=cy+diry[i];
if(vx==tx&&vy==ty)//若已到达终点
return step[cx][cy]+1;
if(vx>=1&&vx<=12&&vy>=1&&vy<=12&&used[vx][vy]==0)//没出边界而且没来过
{
step[vx][vy]=step[cx][cy]+1;
used[vx][vy]=1;
addr.x.push(vx);//入栈,步数为上一步加一,标记已来过
addr.y.push(vy);
}
}
}
return 0;//要是没找到,就说明到不了
}
例二:跳马
给一个200×200的棋盘,问国际象棋的马从给定的起点到给定的终点最少需要几步。
分析:这个其实跟上边的走迷宫基本思想和实现方法是一样的。有一点细微的差别是走马有八个方位,所以方向数组应该是8, 而且bfs内的循环应该是8次,其它的没有差别。
解答:
int dirx[8]={1,1,2,2,-1,-1,-2,-2};
int diry[8]={2,-2,1,-1,2,-2,-1,1};
struct addr
{
queue<int> x;
queue<int> y;
}addr;
void readdata();//读入数据
void empty();//清空队列
void init()//初始化队列和used,step数组
int bfs()
{
while(!addr.x.empty()&&!addr.y.empty())
{
int cx,cy;
cx=addr.x.front();
cy=addr.y.front();//取出队首坐标进行本次操作
addr.x.pop();
addr.y.pop();//队首坐标出队列
for(int i=0;i<8;i++)//四个方向都走一步
{
int vx,vy;
vx=cx+dirx[i];
vy=cy+diry[i];
if(vx==tx&&vy==ty)//若已到达终点
return step[cx][cy]+1;
if(vx>=1&&vx<=200&&vy>=1&&vy<=200&&used[vx][vy]==0)//没出边界而且没来过
{
step[vx][vy]=step[cx][cy]+1;
used[vx][vy]=1;
addr.x.push(vx);//入栈,步数为上一步加一,标记已来过
addr.y.push(vy);
}
}
}
return 0;
}
例三:独轮车
独轮车的轮子上有5种颜色,每走一格颜色变化一次,独轮车只能往前推,也可以在原地旋转,每走一格,需要一个单位 的时间,每转90度需要一个单位的时间,转180度需要两个单位的时间。现给定一个20×20的迷宫、一个起点、一个终点 和到达终点的颜色,问独轮车最少需要多少时间到达。
· 状态:独轮车所在的行、列、当前颜色、方向。
· 另外为了方便在结点中加上到达该点的最小步数。
分析:
解答:
例四:八数码难题
八数码问题大家应该都知道,就是给定一个3*3的格子,把八个数字1-8按顺序排列好,如图:
分析:
解答:
例五:装载问题
· 有一批共个集装箱要装上2艘载重量分别为C1和C2的轮船,其中集装箱 i 的重量为 Wi,且,
· 装载问题要求确定是否有一个合理的装载方案可将这个集装箱装上这2艘轮船。如果有,找出一种装载方案。
· 容易证明:如果一个给定装载问题有解,则采用下面的策略可得到最优装载方案。
(1)首先将第一艘轮船尽可能装满;
(2)将剩余的集装箱装上第二艘轮船。
分析:
解答:
例六:分油问题
一个1斤的油瓶内装满了油。有两个空瓶,容量分别是7两和3两。要求在三个瓶子间进行倒油,使一斤油平分为两个五两。
分析:
解答: