目录
该文章是算法设计与分析(第二版)中分支限界法中的代码进行整理 主编 : 李春葆
在本文中,主要是对该书第六章-分支限界法章节中的示例代码进行复现。本文没有去解决该章的课后习题,忘谅解。
代码运行环境是:DEVc++
什么是分支限界法:
分枝限界法类似于回溯法,也是一种在问题的解空间树上搜索问题解的算法,但在一般情况下分枝限界法和回溯法的求解目标不同。回溯法的求解目标是找出解空间树中满足约束条件的所有解,而分枝限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解。
所谓“分枝”,就是采用广度优先的策略依次搜索活结点的所有分枝,也就是所有相邻结点,如下图所示。为了有效地选择下一扩展结点,以加速搜索的进程,在每一活结点处计算一个函数值(限界函数),并根据这些已计算出的函数值从当前活结点表中选择一个最有利的结点作为扩展结点,使搜索朝着解空间树上有最优解的分枝推进,以便尽快地找出一个最优解。
分枝限界法和回溯法的主要区别如下表:
分枝限界法的设计思想:
1、设计合适的限界函数
在搜索解空间树时每个活结点可能有很多子结点,其中有些子结点搜索下去是不可能产生问题解或最优解的,可以设计好的限界函数在扩展时删除这些不必要的子结点,从而提高搜索效率。
一般先要确定问题解的特性,如果目标函数是求最大值,则设计上界限界函数ub(根结点的ub值通常大于或等于最优解的ub值),若 si 是 sj ,的双亲结点,则应满足ub(si)≥ub(sj),找到一个可行解ub(sk)后将所有小于ub(sk)的结点剪枝。
如果目标函数是求最小值,则设计下界限界函数lb(根结点的lb值一定要小于或等于最优解的lb值),若 si 是 sj ,的双亲结点,则应满足1b(si)≤lb(sj),找到一个可行解lb(sk)后将所有大于 lb(sk)的结点剪枝。
2、组织活结点表
常见的有队列式分枝限界法以及优先队列式分枝限界法
队列式分枝限界法:
将活结点表组织成一个队列(queue),并按照队列先进先出(First InFirst Out,FIFO)原则选取下一个结点为扩展结点。步骤如下:
(1)将根结点加入活结点队列。
(2)从活结点队列中取出队头结点作为当前扩展结点。
(3)对于当前扩展结点,先从左到右产生它的所有子结点,用约束条件检查,把所有满足约束条件的子结点加人活结点队列。
(4)重复步骤(2)和(3),直到找到一个解或活结点队列为空为止。
优先队列式分枝限界法:
优先队列式分枝限界法的主要特点是将活结点表组成一个优先队列(priority queue),并选取优先级最高的活结点作为当前扩展结点。步骤如下:
(1)计算起始结点(根结点)的优先级并加人优先队列(与特定问题相关的信息的函数值决定优先级)。
(2)从优先队列中取出优先级最高的结点作为当前扩展结点,使搜索朝着解空间树上可能有最优解的分枝推进,以便尽快地找出一个最优解。
(3)对于当前扩展结点,先从左到右产生它的所有子结点,然后用约束条件检查,对所有满足约束条件的子结点计算优先级并加入优先队列。
(4)重复步骤(2)和(3),直到找到一个解或优先队列为空为止。
在一般情况下,结点的优先级用与该结点相关的一个数值p来表示,如价值、费用、重量等。最大优先队列规定p值越大优先级越高,常用大根堆来实现;最小优先队列规定 值越小优先级越高,常用小根堆来实现。
3、确定最优解的解向量
问题一:求解0/1背包问题
问题描述:有 N件物品和一个容量是 V的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <stdio.h>
#include <queue>
using namespace std;
#define MAXN 20
#define INF 0x3f3f3f3f3
int n = 4,W = 10;
int w[MAXN] = {0,4,7,5,3};
int v[MAXN] = {0,40,42,25,12};
//int n = 3,W = 30;
//int w[] = {0,16,15,15};
//int v[] = {0,45,25,25};
//求解结果表示
int maxv = -9999;
int bestx[MAXN];
int total = 1;
struct NodeType{
int no;
int i;
int w;
int v;
int x[MAXN];
double ub;
bool operator<(const NodeType &s) const{
return ub < s.ub;
}
};
void bound(NodeType &e) //计算分枝结点e的上界
{ int i=e.i+1; //考虑结点e的余下物品
int sumw=e.w; //求已装入的总重量
double sumv=e.v; //求已装入的总价值
while ((sumw+w[i]<=W) && i<=n)
{ sumw+=w[i]; //计算背包已装入载重
sumv+=v[i]; //计算背包已装入价值
i++;
}
if (i<=n) //余下物品只能部分装入
e.ub=sumv+(W-sumw)*v[i]/w[i];
else //余下物品全部可以装入
e.ub=sumv;
}
void EnQueue(NodeType e,priority_queue<NodeType> & qu){
if(e.i == n){
if(e.v > maxv){
maxv = e.v;
for( int j = 1 ; j <= n;j++){
bestx[j] = e.x[j];
}
}
}else{
qu.push(e);
}
}
void bfs() //求0/1背包的最优解
{
int j;
NodeType e,e1,e2; //定义3个结点
priority_queue<NodeType> qu; //定义一个队列
e.i=0; //根结点置初值,其层次计为0
e.w=0; e.v=0;
e.no=total++;
for (j=1;j<=n;j++)
e.x[j]=0;
bound(e); //求根结点的上界
qu.push(e); //根结点进队
while (!qu.empty()) //队不空循环
{
e=qu.top(); qu.pop(); //出队结点e
//能装下第i个物品
if (e.w+w[e.i+1]<=W) //剪枝:检查左孩子结点
{
e1.no=total++;
e1.i=e.i+1; //建立左孩子结点
e1.w=e.w+w[e1.i];
e1.v=e.v+v[e1.i];
for (j=1;j<=n;j++) //复制解向量
e1.x[j]=e.x[j];
e1.x[e1.i]=1;
bound(e1); //求左孩子结点的上界
EnQueue(e1,qu); //左孩子结点进队操作
}
//不能装下第i个物品时
e2.no=total++; //建立右孩子结点
e2.i=e.i+1;
e2.w=e.w; e2.v=e.v;
for (j=1;j<=n;j++) //复制解向量
e2.x[j]=e.x[j];
e2.x[e2.i]=0;
bound(e2); //求右孩子结点的上界
if (e2.ub>maxv) //若右孩子结点可行,则进队,否则被剪枝
EnQueue(e2,qu);
}
}
int main(){
bfs();
cout<<"结果为:"<<endl;
for(int i = 1;i <= n;i++){
cout<<bestx[i]<<" ";
}
cout<<endl<<"总价值为:"<<maxv<<endl;
return 0;
}
运行截图:
问题二:流水作业调度问题
问题描述:n个作业 N={1,2,…,n}要在2台机器M1和M2组成的流水线上完成加工。每个作业须先在M1上加工,然后在M2上加工。M1和M2加工作业 i 所需的时间分别为 ai 和bi,每台机器同一时间最多只能执行一个作业。流水作业调度问题要求确定这n个作业的最优加工顺序,使得所有作业在两台机器上都加工完成所需最少时间。最优调度应该是?
#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
#include <string>
#include <algorithm>
using namespace std;
#define INF 0x3f3f3f3f
#define MAX 21
int n = 4;
int a[MAX] = {0,5,10,9,7};
int b[MAX] = {0,7,5,9,8};
//int a[MAX] = {0,5,12,4,8};
//int b[MAX] = {0,6,2,14,7};
int bestf = INF;
int bestx[MAX];
int total = 1;
struct NodeType{
int no;
int i;
int x[MAX]; // x[i]表示第i步分配的作业编号
int y[MAX]; // y[i]==1表示作业编号为i的作业已经分配了
int f1;
int f2;
double lb;
bool operator<(const NodeType &s) const{
return lb > s.lb;
}
};
void bound(NodeType &e){
int sum = 0;
for(int i =1;i <= n;i++){
if(e.y[i] == 0){
sum += b[i];
}
}
//已经执行了时间 + 未执行作业i在二号机器b上的执行时间
e.lb = e.f1 + sum;
}
void bfs(){
NodeType e,e1;
priority_queue<NodeType> qu;
memset(e.x,0,sizeof(e.x));
memset(e.y,0,sizeof(e.y));
e.i = 0;
e.f1 = 0;
e.f2 = 0;
bound(e);
e.no = total++;
qu.push(e);
while(!qu.empty()){
e = qu.top();
qu.pop();
e1.i = e.i + 1;
//将剩下的作业,依次求lb,当lb <= bestf时入队
//优先队列:将lb最小的作业出队,再计算下一个入队作业
for(int j = 1;j<=n;j++){
if(e.y[j] == 1){
continue;
}
for(int i1 = 0 ; i1 <= n; i1++){
e1.x[i1] = e.x[i1];
e1.y[i1] = e.y[i1];
}
e1.x[e1.i] = j; //为第i步分配作业
e1.y[j] = 1; //记录作业j已经分配
e1.f1 =e.f1 + a[j];
e1.f2 = max(e.f2,e1.f1) + b[j];
bound(e1);
if(e1.i == n){
if(e1.f2 < bestf){
bestf = e1.f2;
for(int j1 = 1;j1 <= n;j1++){
bestx[j1] = e1.x[j1];
}
}
}
if(e1.lb <= bestf){
e1.no = total++;
qu.push(e1);
}
}
}
}
int main(){
bfs();
for(int i = 1;i<=n;i++){
cout<<bestx[i]<<" ";
}
cout<<endl<<"总时间 = "<<bestf <<endl;
return 0;
}
运行截图:
问题三:最优装载问题
问题描述:有一批集装箱要装上一艘载重量为 W 的轮船。其中集装箱 i 的重量为wi。最优装载问题要求确定在装载体积不受限制的情况下,将尽可能多的集装箱装上轮船。
#include <iostream>
#include <vector>
#include <queue>
#include <string>
#include <algorithm>
#include <cstring>
using namespace std;
#define MAX 11
#define INF 0x3f3f3f3f
int w[]={0,5,2,6,4,3};
int n = 5;
int W = 10;
//求解结果表示
int bestw=0; //存放最大重量,全局变量
int bestx[MAX]; //存放最优解,全局变量
int Count=1; //搜索空间中结点数累计,全局变量
struct NodeType{
int no;
int i;
int w; //重量
int up;
int x[MAX]; //当前结点包含的解向量
bool operator<(const NodeType &s)const{
return up > s.up;
}
};
void bound(NodeType &e){
int i = e.i+1;
int r = 0;
while(i <= n){
r += w[i];
i++;
}
e.up= e.w + r;
}
void Loading(){
NodeType e,e1,e2;
priority_queue<NodeType> qu;
e.no = Count++;
e.i = 0;
e.w = 0;
memset(e.x,0,sizeof(e.x));
bound(e);
qu.push(e);
while(!qu.empty()){
e = qu.top();
qu.pop();
if(e.i == n){
if (e.i==n){ //e是一个叶子结点
if ((e.w>bestw) || (e.w==bestw && e.x[0]<bestx[0])) //比较找最优解
{ bestw=e.w; //更新bestw
for (int j=0;j<=e.i;j++)
bestx[j]=e.x[j]; //复制解向量e.x->bestx
}
}
}else{
if (e.w+w[e.i+1]<=W){ //检查左孩子结点
e1.no=Count++; //设置结点编号
e1.i=e.i+1; //建立左孩子结点
e1.w=e.w+w[e1.i];
for (int j=0; j<=e.i; j++)
e1.x[j]=e.x[j]; //复制解向量e.x->e1.x
e1.x[e1.i]=1; //选择集装箱i
e1.x[0]++; //装入集装箱数增1
bound(e1); //求左孩子结点的上界
qu.push(e1); //左孩子结点进队
}
e2.no=Count++; //设置结点编号
e2.i=e.i+1; //建立右孩子结点
e2.w=e.w;
for (int j=0; j<=e.i; j++) //复制解向量e.x->e2.x
e2.x[j]=e.x[j];
e2.x[e2.i]=0; //不选择集装箱i
bound(e2); //求右孩子结点的上界
if (e2.up>bestw) //若右孩子结点可行,则进队,否则被剪枝
qu.push(e2);
}
}
}
void disparr(int x[],int len) //输出一个解向量
{ for (int i=1;i<=len;i++)
printf("%2d",x[i]);
}
void dispLoading() //输出最优解
{ printf(" X=[");
disparr(bestx,n);
printf("],装入总价值为%d\n",bestw);
}
int main(){
Loading();
printf("求解结果:\n");
dispLoading();
}
运行截图:
问题四:任务分配问题
#include <iostream>
#include <vector>
#include <string>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
#define INF 0x3f3f3f3f
#define MAXN 21
//问题表示
int n =4;
int c[MAXN][MAXN] = {{0},{0,9,2,7,8},{0,6,4,3,7},
{0,5,8,1,8},{0,7,6,9,4}};
int bestx[MAXN];
int mincost = INF;
int total = 1;
struct NodeType{
int no;
int i;
int x[MAXN];
bool worker[MAXN];
int cost;
int lb;
bool operator<(const NodeType & s)const{
//优先队列 从小到大排序
return lb > s.lb;
}
};
//边界函数
void bound(NodeType &e){
int minsum = 0;
for(int i1 = e.i+1;i1<=n;i1++){
int minc = INF;
for(int j1= 1;j1<=n;j1++){
if(e.worker[j1]==false && c[i1][j1]<minc){
minc = c[i1][j1];
}
}
minsum += minc;
}
e.lb = e.cost + minsum;
}
void bfs(){
int j;
NodeType e,e1;
priority_queue<NodeType> qu;
//e.x e.worker是数组,所以用memset进行初始化,
//因为memset是C语言的库函数,在C++中使用时要引入头文件<cstring>
memset(e.x,0,sizeof(e.x));
memset(e.worker,0,sizeof(e.worker));
e.i = 0;
e.cost = 0;
bound(e);
e.no = total++;
qu.push(e);
while(!qu.empty()){
e = qu.top();
qu.pop();
//当到达叶子节点时
if(e.i == n){
if(e.cost < mincost){
mincost = e.cost;
for(j = 1 ; j <= n;j++){
bestx[j] = e.x[j];
}
}
}
e1.i = e.i+1;
for(j = 1;j<=n;j++){
if(e.worker[j]){
continue;
}
for(int i1 = 1;i1<= n;i1++){
e1.x[i1] = e.x[i1];
}
e1.x[e1.i] = j;
for(int i2=1;i2<=n;i2++){
e1.worker[i2] = e.worker[i2];
}
e1.worker[j] = true;
e1.cost = e.cost + c[e1.i][j];
bound(e1);
e1.no = total++;
if(e1.lb <= mincost){
qu.push(e1);
}
}
}
}
int main(){
bfs();
for(int k = 1 ;k<=n;k++){
cout<<"第 "<<k<<" 个人执行 -> 第 "<<bestx[k]<<" 个任务"<<endl;
}
cout<<"总成本 == "<<mincost<<endl;
return 0;
}
运行截图: