我们希望在最短的时间内看遍所有的景点,而且同一个景点只能经过一次,如何计划能在最短的时间内看完全部景点回家呢?
这个问题可以把景点看作顶点,把景点间的路径看作边,这样景点地图就可以抽象为一个无向带权图。本题就是求经过所有顶点最后回到起点的最短路径。
分支限界法就是先将根结点放入活结点表中,然后循环取出表头结点,如果满足约束条件和限界条件就可以将当前结点的子结点(或者说下一级结点)放入活结点表中,直至得到所求解或者是活结点表为空为止。根据活结点表的存储方式可以将分支限界法分为队列式分支限界法和优先队列式分支限界法。这两种方法使用到的数据结构来是队列和堆,如有需要可以自己写,当然用直接用STL中的queue和priority_queue会方便很多。
本题的解空间是排列树,就是用一维数组遍历全排列,但是因为有剪枝,所以不会生成所有结点,也就不会得到全排列,只能得到其中的一部分。
本题采用优先队列式分支限界法,因为使用普通队列的话,在叶子节点的上一层(即倒数第二层)之前会生成所有的结点,这样达不到剪枝的目的。所以算法核心就是优先队列,约束条件和限界条件。
优先队列就是采用优先队列,或者说是最小堆来存储活结点表,结点当前路径长度短的优先级高。这里想说一句关于queue和priority_queue取队首元素的区别,queue是q.front(),而priority_queue是q.top(),我就在这里有过bug。
约束条件在两个地方用到,一个是处理到某一路径的倒数第二个结点的时候,还有一个是在判断能否生成下一级结点并放入优先队列的时候。第一个情况是要求当前路径的倒数第二个顶点与倒数第一个顶点之间有边,还有倒数第一个顶点和起点之间有有边。第二种情况是当前路径的上一个已经确定的顶点和选择的顶点之间有边,如果没有就不能生成下一级结点。
限界条件就是当前结点的走过的路径长度<最短路径长度,这里的最短路径长度并不一定是最优解,而只是计算到某一步得到的最优解。如果当前结点不满足限界条件可以直接进行下一次循环,如果通过计算得到下一级结点不满足限界条件,那么就不能生成下一级结点。
#include
#include
#include
#define INF 1e7
#define N 10
using namespace std;
struct node
{
int cl;//当前走过的路径长度
int id;//处理的第几个景点
int x[N];//记录当前路径
node() {}
node(int c,int i)
{
cl=c;
id=i;
memset(x,0,sizeof(x));
}
};
int m[N][N];//邻接矩阵存储无向带权图
int bestx[N];//最优解路径
int bestl;//最优解长度
int n,M;//景点数目,路径数目
void init()
{
int i,j;
for(i=0; in2.cl;//最小堆
}
};
void bfs()
{
priority_queue,cmp> q;
node temp(0,2);//起点已经确定,从第2个景点开始
int t;
for(int i=1; i<=n; ++i)
temp.x[i]=i;//初始化解向量
q.push(temp);
node live;//活结点
while(!q.empty())
{
live=q.top();
q.pop();
t=live.id;
if(t==n)//处理到倒数第二个景点
{
if(m[live.x[t-1]][live.x[t]]!=INF&&m[live.x[t]][1]!=INF)//满足约束条件,有路径
{
if(live.cl+m[live.x[t-1]][live.x[t]]+m[live.x[t]][1]=bestl)//不满足限界条件
continue;
for(int j=t; j<=n; ++j) //排列树,j不能定义为整个函数的局部变量,循环过程中会出现混乱
{
if(m[live.x[t-1]][live.x[j]]!=INF&&live.cl+m[live.x[t-1]][live.x[j]]";
cout<>n;
cout<<"请输入路径数目:";
cin>>M;
cout<<"请输入景点间的路径:";
int i,a,b,c;
for(i=0; i>a>>b>>c;
if(c
优化之后,我们不以当前走过的路径长度作为优先级的评价标准,而是以当前走过的路径长度+剩余顶点最小边权值之和(当前路径长度下界)作为优先级的评价标准。还是使用最小堆,也就是说,当前路径长度下界越小的,优先级越高。所以要在刚开始的地方计算每个顶点的最小边权值,用数组minv[]存储,以便之后的计算。当然限界条件也发生了改变,当前路径长度下界<计算得到的最短路径长度,可以这样理解,这个路径走到某个位置之后,如果剩余结点都是走最小边还会大于等于最短路径长度,那么这个路径的总长度一定会大于等于最短路径长度,肯定不是最优解,可以直接剪掉,这样可以提高剪枝效率。
笔者还想提醒的一点是,初始化和计算minv[]的函数一定要分开,不然会出现严重的错误。初始化是在输入每条边之前进行的,而计算minv[]时在输入边之后进行的,这里一定要注意,我就因为这个bug把核心函数翻来覆去看了好几遍。。。
#include
#include
#include
#define N 10
#define INF 1e7
using namespace std;
struct node
{
int cl;//当前走过的路径长度
int rl;//剩余结点最小边权值之和
int zl;//cl+rl,路径长度下界
int id;//处理的第几个景点
int x[N];//记录当前路径
node() {}
node(int c,int r,int z,int i)
{
cl=c;
rl=r;
zl=z;
id=i;
memset(x,0,sizeof(x));
}
};
int m[N][N];//邻接矩阵存储无向带权图
int bestx[N];//最优解路径
int bestl;//最优解长度
int n,M;//景点数目,路径数目
int minv[N];//每个顶点最小边权值
int minsum;//每个顶点最小边权值之和
void init()//初始化,注意初始化和计算函数调用的位置
{
int i,j;
for(i=0; in2.zl;//最小堆
}
};
void bfs()
{
priority_queue,cmp> q;
node temp(0,minsum,minsum,2);//起点已经确定,从第2个景点开始
int t;
for(int i=1; i<=n; ++i)
temp.x[i]=i;//初始化解向量
q.push(temp);
node live;//活结点
while(!q.empty())
{
live=q.top();
q.pop();
t=live.id;
if(t==n)//处理到倒数第二个景点
{
if(m[live.x[t-1]][live.x[t]]!=INF&&m[live.x[t]][1]!=INF)//满足约束条件,有路径
{
if(live.cl+m[live.x[t-1]][live.x[t]]+m[live.x[t]][1]=bestl)//不满足限界条件
continue;
for(int j=t; j<=n; ++j) //排列树,j不能定义为整个函数的局部变量,循环过程中会出现混乱
{
if(m[live.x[t-1]][live.x[j]]!=INF)//满足约束条件
{
int cl=live.cl+m[live.x[t-1]][live.x[j]];
int rl=live.rl-minv[live.x[j]];
int zl=cl+rl;
if(zl";
cout<>n;
cout<<"请输入路径数目:";
cin>>M;
cout<<"请输入景点间的路径:";
int i,a,b,c;
for(i=0; i>a>>b>>c;
if(c
参考文献:《趣学算法》