分支限界法(2)
二、装载问题
1. 问题描述
有一批共个集装箱要装上2艘载重量分别为C1和C2的轮船,其中集装箱i的重量为Wi,且。
装载问题要求确定是否有一个合理的装载方案可将这个集装箱装上这2艘轮船。如果有,找出一种装载方案。
容易证明:如果一个给定装载问题有解,则采用下面的策略可得到最优装载方案。
(1)首先将第一艘轮船尽可能装满;
(2)将剩余的集装箱装上第二艘轮船。
将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集,使该子集中集装箱重量之和最接近。由此可知,装载问题等价于以下特殊的0-1背包问题。
例如:W = <10,8,5> , C = 16
2. 队列式分支限界法
在算法的while循环中,首先检测当前扩展结点的左儿子结点是否为可行结点。如果是则将其加入到活结点队列中。然后将其右儿子结点加入到活结点队列中(右儿子结点一定是可行结点)。2个儿子结点都产生后,当前扩展结点被舍弃。
活结点队列中的队首元素被取出作为当前扩展结点,由于队列中每一层结点之后都有一个尾部标记-1,故在取队首元素时,活结点队列一定不空。当取出的元素是-1时,再判断当前队列是否为空。如果队列非空,则将尾部标记-1加入活结点队列,算法开始处理下一层的活结点。
while (true) {
// 检查左儿子结点
if (Ew + w[i] <= c) //x[i] = 1
EnQueue(Q, Ew + w[i], bestw, i, n);
// 右儿子结点总是可行的
EnQueue(Q, Ew, bestw, i, n); //x[i] = 0
Q.Delete(Ew); // 取下一扩展结点
if (Ew == -1) { // 同层结点尾部
if (Q.IsEmpty())
return bestw;
Q.Add(-1); // 同层结点尾部标志
Q.Delete(Ew); // 取下一扩展结点
i++; // 进入下一层
}
}
变量含义:
Ew: 扩展节点的载重量
W: 重量数组
Q: 活节点队列
bestw: 当前最优载重量
i: 当前处理到的层数
n: 总货物数
3. 算法的改进
节点的左子树表示将此集装箱装上船,右子树表示不将此集装箱装上船。设bestw是当前最优解;ew是当前扩展结点所相应的重量;r是剩余集装箱的重量。则当ew + r £ bestw时,可将其右子树剪去,因为此时若要船装最多集装箱,就应该把此箱装上船。
另外,为了确保右子树成功剪枝,应该在算法每一次进入左子树的时候更新bestw的值。
// 检查左儿子结点
Type wt = Ew +w[i]; //左儿子结点的重量
if (wt <= c) { //可行结点
if (wt > bestw)
bestw = wt;
// 加入活结点队列
if (i < n)
Q.Add(wt);
}
// 检查右儿子结点
if (Ew + r > bestw&& i < n)
Q.Add(Ew); // 可能含最优解
Q.Delete(Ew); //取下一扩展结点
4. 构造最优解
为了在算法结束后能方便地构造出与最优值相应的最优解,算法必须存储相应子集树中从活结点到根结点的路径。为此目的,可在每个结点处设置指向其父结点的指针,并设置左、右儿子标志。
class QNode
{
QNode *parent ; // 指向父结点的指针
bool LChild ; // 左儿子标志
Type weight ; // 结点所相应的载重量
}
找到最优值后,可以根据parent回溯到根节点,找到最优解。
// 构造当前最优解
for (int j = n - 1; j> 0; j--) {
bestx[j] = bestE->LChild;
bestE = bestE->parent;
}
LChild是左子树标志,1表示左子树,0表示右子树;
bestx[i]取值为0/1,表示是否取该货物。
备注:下面仅是我个人的理解:
以W = <10,8,5> , C = 16 问题为例,
最后遍历路径队列找出路径(office学的不好,别见怪啊)。从分析可知如果集装箱数量为n,那么需要的存储空间为(2^n-1),无疑是很费内存空间的,而且代码要复杂的多,所以在我的代码中没有实现。
5. 优先队列式分支限界法
解装载问题的优先队列式分支限界法用最大优先队列存储活结点表。活结点x在优先队列中的优先级定义为从根结点到结点x的路径所相应的载重量再加上剩余集装箱的重量之和。
优先队列中优先级最大的活结点成为下一个扩展结点。以结点x为根的子树中所有结点相应的路径的载重量不超过它的优先级。子集树中叶结点所相应的载重量与其优先级相同。
在优先队列式分支限界法中,一旦有一个叶结点成为当前扩展结点,则可以断言该叶结点所相应的解即为最优解。此时可终止算法。
两种实现方式:
(1) 在结点优先队列的每一个活结点中保存从解空间树的根节点到该活结点的路径。 算法确定了达到最优值的叶结点时,在该叶结点处同时得到相同的最优解。
(2) 在算法的搜索进程中保存当前以构造出的部分解空间树。这样在算法确定了达到最优值的叶结点时,就可以在解空间树种该叶结点开始向根结点回溯,构造出相应的最优解。
实现:
/* 主题:装载问题(分支限界法)
* 作者:chinazhangjie
* 邮箱:chinajiezhang@gmail.com
* 开发语言:C++
* 开发环境:Microsoft Visual Studio 2005
* 时间: 2010.11.07
*/
#include <iostream>
#include <vector>
#include <queue>
#include <numeric>
using namespace std;
// BAB for "branch and bound method"
// FIFO队列式分支限界法
class load_BAB
{
public:
load_BAB (const vector<int>& w, int c)
: weight (w), capacity (c), c_count ((int)w.size()), best_w(0) {
}
int get_best_w () const {
return best_w ;
}
// 队列式分支限界法
int queue_BAB () {
live_node_q.push (-1); // 同层节点尾部标识
int i = 0;
int cw = 0;
while (true) {
// 检查左子结点
if (cw + weight[i] <= capacity) {
__en_queue (cw + weight[i], i) ;
/*if ((cw + weight[i]) > best_w) {
best_w = cw + weight[i];
}*/
}
// 检查右子节点(可能产生最优解)
int best_rest = accumulate (weight.begin() + i + 1, weight.end(), 0) ;
if (best_rest > best_w) {
__en_queue (cw, i) ;
}
// 取下一个结点
cw = live_node_q.front ();
live_node_q.pop ();
if (cw == -1) {
if (live_node_q.empty ()) {
return best_w ;
}
live_node_q.push (-1);
cw = live_node_q.front ();
live_node_q.pop ();
++ i ;
}
}
}
private:
void __en_queue (int cw, int i) {
// 将活结点加入到活结点队列Q中
if (i >= c_count - 1) {
if (cw > best_w) {
best_w = cw ;
}
}
else {
live_node_q.push (cw) ;
}
}
private:
vector<int> weight; // 集装箱重量
queue<int> live_node_q; // 活结点队列
int c_count; // 集装箱 (container) 个数
int capacity; // 轮船承载量
int best_w; // 最优载重量
};
// 子集空间树中结点
class BB_node
{
public:
BB_node (BB_node* par, bool lc) {
parent = par ;
left_child = lc ;
}
public:
BB_node* parent ; // 父结点
bool left_child ; // 左儿子结点标志
} ;
// 优先级队列结点
class heap_node
{
public:
heap_node (BB_node* node, int uw, int lev) {
live_node = node ;
upper_weight = uw ;
level = lev ;
}
friend
bool operator < (const heap_node& lth, const heap_node& rth) {
return lth.upper_weight < rth.upper_weight ;
}
friend
bool operator > (const heap_node& lth, const heap_node& rhs) {
return lth.upper_weight > rhs.upper_weight ;
}
public:
BB_node* live_node ; //
int upper_weight ; // 活结点优先级(上界)
int level ; // 活结点在子集树中所处的层序号
};
// 优先权队列式分支限界法
class load_PQBAB
{
public:
load_PQBAB (const vector<int>& w, int c)
: weight (w), capacity (c), c_count (static_cast<int>(w.size())) {
}
~load_PQBAB () {
}
void max_loading () {
BB_node* pbn = NULL ; // 当前扩展结点
int i = 0 ; // 当前扩展结点所处的层
int ew = 0 ; // 扩展结点所相应的载重量
vector<int> r (c_count, 0);// 剩余重量数组
for (int j = c_count - 2; j >= 0; -- j) {
r[j] = r[j + 1] + weight[j + 1] ;
}
/*copy (r.begin(), r.end(), ostream_iterator<int>(cout, " ")) ;
cout << endl; */
// 搜索子集空间树
while (i != c_count) {
// 非叶结点,检查当前扩展结点的儿子结点
if (ew + weight[i] <= capacity) {
// 左儿子为可行结点
__add_live_node (ew + weight[i] + r[i], i + 1, pbn, true) ;
}
// 右儿子结点为可行结点
__add_live_node (ew + r[i], i + 1, pbn, false) ;
// 释放内存
while (pbn != NULL) {
BB_node *p = pbn ;
pbn = pbn->parent ;
delete p ;
}
// 取下一扩展结点
heap_node node = pri_queue.top () ;
pri_queue.pop ();
// cout << node.upper_weight <<endl;
i = node.level ;
pbn = node.live_node ;
ew = node.upper_weight - r[i - 1] ;
}
// 释放内存
while (pri_queue.size() != 0) {
heap_node node = pri_queue.top () ;
pri_queue.pop () ;
while (node.live_node != NULL) {
BB_node* temp = node.live_node ;
node.live_node = node.live_node->parent ;
delete temp ;
}
}
// 构造最优解
cout << "best capacity: " << ew << endl ;
cout << "path: " ;
vector<bool> temp_path ;
while (pbn != NULL) {
temp_path.push_back (pbn->left_child) ;
BB_node *temp = pbn ;
pbn = pbn->parent ;
delete temp ;
}
copy (temp_path.rbegin(), temp_path.rend(), ostream_iterator<bool> (cout, " "));
cout << endl ;
}
private:
// 产生新的活结点,加入到子集树中
void __add_live_node (int uw, int lev, BB_node* par, bool lc) {
// 深拷贝
BB_node *first = NULL;
BB_node *end = NULL ;
while (par != NULL) {
BB_node* p = new BB_node(NULL, par->left_child) ;
if (first == NULL) {
first = p ;
end = p ;
}
else {
end->parent = p ;
end = end->parent ;
}
par = par->parent ;
}
BB_node* p = new BB_node (first, lc) ;
pri_queue.push (heap_node (p, uw, lev)) ;
}
private :
vector<int> weight; // 集装箱重量
int c_count; // 集装箱 (container) 个数
int capacity; // 轮船承载量
priority_queue<heap_node> pri_queue ; // 活结点优先级队列
} ;
int main()
{
const int capacity = 20 ;
vector<int> weight ;
weight.push_back (10);
weight.push_back (8);
weight.push_back (5);
weight.push_back (1);
weight.push_back (3);
load_PQBAB l (weight, capacity) ;
l.max_loading ();
/*load_BAB lb (weight, capacity) ;
lb.queue_BAB () ;
cout << lb.get_best_w() << endl ;*/
return 0;
}
参考书籍 《算法分析与设计(第二版)》 王晓东 编著
授课教师 张阳教授