您的位置:首页 > 其它

算法篇-11-分支限界-布线&装载&旅行售货员

2016-12-28 12:27 387 查看
本系列所有代码https://github.com/YIWANFENG/Algorithm-github

分支限界

回溯法以深度优先搜索解空间树,分支限界法以广度优先搜索。

即在当前结点处生成其所有子节点,然后从当前活结点的列表中选择下一个扩展结点重复如此,当然不要忘记了判断是否存在可行解。

由于采用树的广度优先遍历,所以我们一般用队列实现。队列可分为一般队列FIFO与优先队列(人工指定优先级)。

在选择扩展结点时我们一般加限界条件加以取舍。

关于剪枝与限界策略即判断扩展结点的子树是否可存在最优值。当然依据限界函数所得到的可能最优值又可以作为优先队列优先依据。

布线问题

 印刷电路板不限区域划分成n*m个方格阵列。如下图所示

  


 精确的电路布线问题要求确定连接方格a的中点,到连接方格b的中点的最短布线方案。

布线时,电路只能沿直线或直角布线。为了避免线路相交,已布的线的方格做了封锁标记,其他线路不允许穿过被封锁的方格。

  

这题明显可以作为一广搜基础题,即以初始位置向四周扩散,扩展结点的是由其父结点向某个方向走了一步,那么他的计步等于其父亲加1,若走到目标位置即可停止,因为以该层次的其他点若也可达目标,结果并不会比该点更优。限界函数在这里简化为不走已经走过的点。

#include <iostream>
#include <queue>
#include <vector>

//布线问题 --简单分支限界
using namespace std;

class Point{
public:
int x,y;
Point *parent;
int level;
int cost;
Point(int x_,int y_) { x = x_; y = y_;	}
Point() { x=0; y = 0; }
};

class FindPath {
private:
int n,			//n行
m;			//m列
bool *a;		//可行矩阵
Point end;
Point start;
int cc;			//目前步数
int bestc;		//最优步数
Point *best;
private:

public:
int Find(bool *a_,int n_,int m_,Point s,Point e,Point *re)
{
//a_可行矩阵,s开始位置,e目标位置,re返回路径坐标数组
//n_  a_的行,m_ a_的列    返回所需步数
a = a_;
n = n_;
m = m_;
start = s;
end = e;
bestc = 65535;
queue<Point*> q,recl;
int dx[] = {-1,0,1,0};
int dy[] = {0,1,0,-1};
Point * E = new Point();
E->x = s.x;
E->y = s.y;
E->level = 0;
E->parent = NULL;
//recl.push(E);
while(true) {
//达到,且所走步数优
if(E->x==e.x && E->y==e.y && E->level<bestc) {
bestc = E->level;
best = E;
break;
}
for(int i=0;i<4;++i) {
//该节点可走
if( (E->x+dx[i])>0 && (E->x+dx[i])<=n &&
E->y+dy[i]<=m && E->y+dy[i]>0 &&
a[(E->x+dx[i])*(m+1)+E->y+dy[i]]) {
Point *N =  new Point();
N->level = E->level+1;
N->x = E->x+dx[i];
N->y = E->y+dy[i];
N->parent = E;
a[(N->x)*(m+1)+N->y] = false;		//成为一次扩展结点即可
q.push(N);
recl.push(N);
}
}
if(q.empty()) break;
E = q.front();
q.pop();
}
int ii=0;
while(best) {
re[best->level-ii].x = best->x ;
re[best->level-ii].y = best->y ;
//cout<<"("<<best->x<<','<<best->y<<")"<<endl;
best=best->parent;
}
while(!recl.empty()) {
delete recl.front();
recl.pop();
}
return bestc;
}

};

int main()
{
int n = 7,m=7;
bool a[(n+1)*(m+1)];
for(int i=0;i<(n+1)*(m+1);++i) a[i]= true;
a[1*(m+1)+3] =a[2*(m+1)+3] = a[2*(m+1)+4] = a[3*(m+1)+5] = false;
a[4*(m+1)+4] = a[4*(m+1)+5]	=a[5*(m+1)+5] = false;
a[5*(m+1)+1] = a[6*(m+1)+2] =a[6*(m+1)+3] = a[7*(m+1)+3] = false;

Point *re = new Point[(n+1)*(m+1)];
int n_re = 0;
FindPath fp;
n_re = fp.Find(a,n,m,Point(3,2),Point(4,6),re);

cout<<"共"<<n_re<<"步"<<endl;
for(int i=0;i<=n_re;++i) {
cout<<"("<<re[i].x<<','<<re[i].y<<")"<<endl;
}
delete[] re;
return 0;
}


最优装载

此题目与之前相同,直接说如何用分支限界法来思考。

欲求最优装载,即求一解向量来使装载重量最大化,那么需要一Ew标志当前装载量,

从第i件物品开始,若可以装下(左枝),那么Ew+=w[i],并把当前载重量保存到某一节点中,然后送至队列中。若不可以装下(右枝),那么也应把当前重量Ew(未加w[i])保存到一节点中,然后送至队列。然后完成此层的操作后(说明需要一标志来标识队列中此层完毕)进入下一层,如此反复直至到达叶子节点。这样我们可以得到最优值以及对应的叶子节点,但是没有最优解,所以我们需要一父指针来指向其父节点,一标志位标示是其左or右孩子。这样可以反推出最优解。

 

#include <iostream>
#include <queue>

using namespace std;

//////普通队列实现
class QNode
{
public:
QNode *parent;		//指向父结点的指针
bool LChild;		//左孩子
float weight;		//载重量

};

void EnQueue(queue<QNode*> &q, float wt, int i,int n,float bestw,
QNode *E,QNode *&bestE,int bestx[],bool ch ,queue<QNode*> &q2)
{
//将活结点加入到队列q中
//q 指向队列的指针 	//wt 当前载重量 		//i 当前子集树的层数
// n 集装箱总数量   // bestw 当前最优值  	//E	 当前扩展节点
//bestE 最优值对应的结点 //bestx 最优解  	//ch 左孩子标志    //q2垃圾回收用
if(i==n) {
if(wt==bestw) {
bestE = E;
bestx
= ch?1:0;
}
return ;
}

QNode *b = new QNode;
b->weight = wt;
b->parent = E;
b->LChild = ch;
q.push(b);
q2.push(b);
}

float MaxLoading(float w[], float c,int n,int bestx[])
{
//w[] 集装箱重量  //c 轮船载重量
//n 集装箱数量    //bestx[] 最优解
//返回最优值
queue<QNode*> q,q2;
q.push(NULL);
int i=1;			//当前层
float bestw = 0; 	//最优载重量
float Ew = 0;		//当前载重量
float r = 0;		//剩余载重量
for(int j=2;j<=n;j++) r+=w[j];
QNode *E = NULL,	//当前扩展结点
*bestE; 		//最优解对应的结点

while(true)  {
float wt = Ew+w[i];
//检查左孩子
if(wt <= c) {
if(wt>bestw) bestw=wt;	//提前更新最优解
EnQueue(q,wt,i,n,bestw,E,bestE,bestx,true,q2);
}
//右孩子
if(Ew+r>=bestw) {
EnQueue(q,Ew,i,n,bestw,E,bestE,bestx,false,q2);
}
E = q.front();
q.pop();
if(!E) {
//遇到层的结尾标志
if(q.empty()) break;
q.push(NULL);	//加入当前层结尾标志
E = q.front();
q.pop();
++i;			//增加层数
r -= w[i]; 		//更新剩余集装箱重量
}
Ew = E->weight; //更新当前载重量
}
for(int j=n-1;j>0;--j) {
bestx[j] = (bestE->LChild)?1:0;
bestE = bestE->parent;
}
while(q2.empty()) {
delete q2.front();
q2.pop();
}
return bestw;
}

///////优先队列实现
class QNode
{
public:
QNode *parent;		//指向父结点的指针
bool LChild;		//左孩子
float uweight;		//载重量上界
int level;			//所在层数

};

class mycmp{
public:
bool operator() (const QNode* a1,const QNode* a2) const
{
if( (a1->uweight) < (a2->uweight) ) return true;
return false;
}
};

void EnLiveQueue(priority_queue<QNode*,vector<QNode*>,mycmp > &q,
QNode *E,float wt, bool ch ,int i,queue<QNode*> &q2)
{
//将活结点加入到队列q中
//q 指向队列的指针 	//wt 当前载重量 		//i 当前子集树的层数
//E	 当前扩展节点
//ch 左孩子标志    //q2垃圾回收用

QNode *b = new QNode;
b->parent =E;
b->LChild = ch;
b->level = i;
b->uweight = wt;

q.push(b);
q2.push(b);
}

float MaxLoading(float w[], float c,int n,int bestx[])
{
//w[] 集装箱重量  //c 轮船载重量
//n 集装箱数量    //bestx[] 最优解
//返回最优值
queue<QNode*> q2;
priority_queue<QNode*,vector<QNode*>,mycmp > q;
int i=1;			//当前层
float Ew = 0;		//当前载重量
float r[n+1];		//剩余载重量
r
= 0;
for(int j=n-1;j>0;j--) r[j]=r[j+1]+w[j+1];
QNode *E = NULL;	//当前扩展结点

while(i!=n+1)  {
float wt = Ew+w[i];
//检查左孩子
if(wt <= c) {
EnLiveQueue(q,E,wt+r[i],true,i+1,q2);
}
//右孩子
EnLiveQueue(q,E,Ew+r[i],false,i+1,q2);
E = q.top();
q.pop();
i = E->level;
Ew = E->uweight-r[i-1];
}
for(int j=n;j>0;--j) {
bestx[j] = (E->LChild)?1:0;
E = E->parent;
}
while(q2.empty()) {
delete q2.front();
q2.pop();
}
return Ew;
}

int main()
{
int n = 3,sum;
float w[] = {0,10,40,40};
int x[n+1];
//float bestw = ml.Solve(n,60.0,w,x);
float bestw = MaxLoading(w,60.0,n,x);
cout<<"最优值:"<<bestw<<endl<<"装载方式:\n";
for(int i=1; i<=n; ++i) {
cout<<x[i]<<' ';
}
cin.get();

return 0;
}


 旅行售货员

之前我们了解到,此问题的解空间时一棵排列树。

我们采用优先队列法求解,确定优先级,以“走该结点时的所需费用下界”作为优先级。

选择这个的原因:走到最后该费用即为所需费用,即为我们需求最优费用。他的组成为已经走过的结点的费用+走了这结点后再走下一步最少所需费用(出边费用)。

 

我们最终追求的就是求得一个排列(大小为n即n个地方),使其总费用cost最小,走到某一结点i时必然需要一个cc来记录当前按序走到这里的费用,以及如何行走的顺序x[0,i] ,需要进一步选择的是结点x[i+1,n-1],如何选择呢?即是从当前剩余结点中选择一个出边费用最小的结点来走,首先需要判断刚走过的节点是否与目前要走的节点连通,若连通再看若走此结点后是否可获得最优值,若比目前最优值bestc优,就将其加入到优先队列中。如此循环直到走到倒数第二个结点,此时判断是否可以构成回路以及费用是否最优以便取舍。

 

以上书面写法:

 算法的while循环体完成对排列树内部结点的扩展。对于当前扩展结点,算法分2种情况进行处理:

 

     1、首先考虑s=n-2的情形,此时当前扩展结点是排列树中某个叶结点的父结点。如果该叶结点相应一条可行回路且费用小于当前最小费用,则将该叶结点插入到优先队列中,否则舍去该叶结点。

     2、当s<n-2时,算法依次产生当前扩展结点的所有儿子结点。由于当前扩展结点所相应的路径是x[0:s],其可行儿子结点是从剩余顶点x[s+1:n-1]中选取的顶点x[i],且(x[s],x[i])是所给有向图G中的一条边。对于当前扩展结点的每一个可行儿子结点,计算出其前缀(x[0:s],x[i])的费用cc和相应的下界lcost。当lcost<bestc时,将这个可行儿子结点插入到活结点优先队列中。 

     算法中while循环的终止条件是排列树的一个叶结点成为当前扩展结点。当s=n-1时,已找到的回路前缀是x[0:n-1],它已包含图G的所有n个顶点。因此,当s=n-1时,相应的扩展结点表示一个叶结点。此时该叶结点所相应的回路的费用等于cc和lcost的值。剩余的活结点的lcost值不小于已找到的回路的费用。它们都不可能导致费用更小的回路。因此已找到的叶结点所相应的回路是一个最小费用旅行售货员回路,算法可以结束。

 

 

此处虽然选定了如何比较优先级,而且出边不一定构成回路,但是他一定是一个下界(或许不可能达到),在程序中每次计算总会找到最优,因为最终的结果中是已走过的地方所需费用,并不包含出边费用。

程序源代码:

#include <iostream>
#include <queue>
#include <vector>
using namespace std;

class Node {
public:
int lcost, 		//lcost = cc + rcost 子树费用下界
cc,				//当前费用
rcost;			//x[s:n-1] 中顶点最小出边费用
int s,			//根节点到当前节点的路径为x[0:s]
*x; 		//需要进一步搜索的顶点是x[s+1:n-1]

};

class cmp{
public:
bool operator()(const Node &a1,const Node &a2)
{
return a1.lcost>a2.lcost;
}
};

class TSP {
private:
int n;		//图的顶点数量
int *a;		//图的邻接矩阵
int NoEdge;	//无边标记

int *x ;	//当前解
int cc;		//当前耗费
int *bestx;	//最优解、
int bestc;	//最短路长

public:
int Solve(int n_,int *a_,int NoEdge_,int v[])
{
n = n_;
a = a_;
NoEdge = NoEdge_;
priority_queue<Node,vector<Node>,cmp> q;
int  MinOut[n+1]; //i节点与其他节点的路径中最小的耗费
int MinSum = 0;
for(int i=1;i<=n;++i) {
int Min = NoEdge;
for(int j=1; j<=n; ++j) {
if(a[i*(n+1)+j]!=NoEdge && (a[i*(n+1)+j]<Min || Min==NoEdge))
Min = a[i*(n+1)+j];
}
if(Min==NoEdge) return NoEdge;			//无回路
MinOut[i] = Min;
MinSum+=Min;
}
//初始化
Node E;
E.x = new int
;
for(int i=0;i<n;++i) E.x[i] = i+1;	//初始顺序1-n
E.s = 0;						  	//初始走到0
E.cc = 0;						  	//初始费用0
E.rcost = MinSum;					//
int bestc = NoEdge;
//搜索解空间
while(E.s<n-1) {	//非叶结点
if(E.s == n-2) { //当前扩展结点是叶结点的父亲
//所构成回路是否是最优解
if(a[E.x[n-2]*(n+1)+E.x[n-1]]!=NoEdge && a[E.x[n-1]*(n+1)+1]!=NoEdge &&
(E.cc+a[E.x[n-1]*(n+1)+1]+a[E.x[n-1]*(n+1)+1] <bestc || bestc==NoEdge)) {
//费用更小的路
bestc = E.cc +a[E.x[n-2]*(n+1)+E.x[n-1]] + a[E.x[n-1]*(n+1)+1];
E.cc = bestc;
E.lcost = bestc;
E.s++;
q.push(E);
}
else { // 舍弃扩展节点
delete [] E.x;
}
}
else { // 产生当前扩展节点的孩子节点
for(int i=E.s+1; i<n;i++) {
if(a[E.x[E.s]*(n+1)+E.x[i]]!=NoEdge) {
//可行结点
int cc = E.cc + a[E.x[E.s]*(n+1)+E.x[i]];
int rcost = E.rcost - MinOut[E.x[E.s]];
int b = cc+rcost;	//下界
if(b<bestc || bestc == NoEdge) {
//子树可能含有最优解
//结点插入最小堆
Node N;
N.x = new int
;
for(int j=0;j<n;++j) N.x[j]=E.x[j];
N.x[E.s+1] =E.x[i];
N.x[i]  = E.x[E.s+1];	//节点交换
N.cc =cc;				//费用更新
N.s = E.s+1;			//层数+1
N.lcost = b;
N.rcost =rcost;
q.push(N);
}
}
}
delete [] E.x;
}
if(q.empty()) break;
E = q.top();
q.pop();
}
if(bestc==NoEdge) return NoEdge;	//无回路

//复制最优解到v[1:n]
for(int i=0;i<n;++i) v[i+1] = E.x[i];

//释放队列中的剩余元素
while(!q.empty()) {
E = q.top();
if(E.x)
delete [] E.x;
q.pop();
}
return bestc;
}
};

int main()
{
int n=4;
int NoEdge = 0x7fffffff;
int a[] = {
0,0,0,0,0,
0,0,30,6,4,
0,30,0,5,10,
0,6,5,0,20,
0,4,10,20,0
};
int v[5];
TSP  tsp;
int c = tsp.Solve(n,a,NoEdge,v);
cout<<"最短路长"<<c<<endl;
cout<<"路线:";
for(int i=1; i<=n; ++i)
cout<<' '<<v[i];
cout<<" 1"<<'\n';
cin.get();
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: