📄 分枝定界算法.txt
字号:
// 为b e s t x分配空间
bestx = new int [n+1];
// 初始化层1
int i = 1;
E = 0;
cw = cp = 0;
Tp bestp = 0; // 目前的最优收益
Tp up = Bound(1); // 在根为E的子树中最大可能的收益
// 搜索子集空间树
while (i != n+1) { // 不是叶子
// 检查左孩子
Tw wt = cw + w[i];
if (wt <= c) {// 可行的左孩子
if (cp+p[i] > bestp) bestp = cp+p[i];
AddLiveNode(up, cp+p[i], cw+w[i], true, i+1);}
up = Bound(i+1);
// 检查右孩子
if (up >= bestp) // 右孩子有希望
AddLiveNode(up, cp, cw, false, i+1);
// 取下一个E-节点
HeapNode<Tp, Tw> N;
H->DeleteMax(N); // 不能为空
E = N.ptr;
cw = N.weight;
cp = N.profit;
up = N.uprofit;
i = N.level;
}
// 沿着从E-节点E到根的路径构造bestx[]
for (int j = n; j > 0; j--) {
bestx[j] = E-> L C h i l d ;
E = E-> p a r e n t ;
}
return cp;
}
函数M a x P r o f i t K n a p s a c k在子集树中执行最大收益分枝定界搜索。函数假定所有的物品都是按收益密度值的顺序排列,可以使用类似于程序1 6 - 9中回溯算法所使用的预处理代码来完成这种排序。函数M a x P r o f i t K n a p s a c k首先初始化活节点的最大堆,并使用一个数组b e s t x来记录最优解。由于需要不断地利用收益密度来排序,物品的索引值会随之变化,因此必须将M a x P r o f i t K n a p s a c k所生成的结果映射回初始时的物品索引。可以用Q的I D域来实现上述映射(见程序1 6 - 9)。
在函数M a x P r o f i t K n a p S a c k中,E是当前E-节点,c w是节点对应的重量, c p是收益值,u p是以E为根的子树中任一节点的收益值上限。w h i l e循环一直执行到一个叶节点成为E-节点为止。由于最大堆中的任何剩余节点都不可能具有超过当前叶节点的收益值,因此当前叶即对应了一个最优解。可以从叶返回到根来确定这个最优解。
M a x P r o f i t K n a p s a c k中w h i l e循环的结构很类似于程序1 7 - 6的w h i l e循环。首先,检验E-节点左孩子的可行性,如它是可行的,则将它加入子集树及活节点队列(即最大堆);仅当节点右孩子的B o u n d值指明有可能找到一个最优解时才将右孩子加入子集树和队列中。
5.2.3 最大完备子图
4 . 2 . 3节完备子图问题的解空间树也是一个子集树,故可以使用与装箱问题、背包问题相同的最大收益分枝定界方法来求解这种问题。解空间树中的节点类型为b b n o d e,而最大优先队列中元素的类型则是C l i q u e N o d e。C l i q u e N o d e有如下域:c n(该节点对应的完备子图中的顶点数目),u n(该节点的子树中任意叶节点所对应的完备子图的最大尺寸),l e v e l(节点在解空间树中的层),c n(当且仅当该节点是其父节点的左孩子时, c n为1),p t r(指向节点在解空间树中的位置)。u n的值等于c n + n - l e v e + 1。因为根据un 和c n(或l e v e l)可以求出l e v e l(或c n),所以可以去掉c n或l e v e l域。当从最大优先队列中选取元素时,选取的是具有最大u n值的元素。在程序1 7 - 8中,C l i q u e N o d e包含了所有的三个域:c n,un 和l e v e l,这样便于尝试为u n赋予不同的含义。函数A d d C l i q u e N o d e用于向生成的子树和最大堆中加入节点,由于其代码非常类似于装箱和背包问题中的对应函数,故将它略去。
函数B B M a x C l i q u e在解空间树中执行最大收益分枝定界搜索,树的根作为初始的E-节点,该节点并没有在所构造的树中明确存储。对于这个节点来说,其cn 值(E-节点对应的完备子图的大小)为0,因为还没有任何顶点被加入完备子图中。E-节点的层由变量i 指示,它的初值为1,对应于树的根节点。当前所找到的最大完备子图的大小保存在b e s t n中。
在while 循环中,不断展开E-节点直到一个叶节点变成E-节点。对于叶节点,u n=c n。由于所有其他节点的un 值都小于等于当前叶节点对应的un 值,所以它们不可能产生更大的完备子图,因此最大完备子图已经找到。沿着生成的树中从叶节点到根的路径,即可构造出这个最大完备子图。
为了展开一个非叶E-节点,应首先检查它的左孩子,如果左孩子对应的顶点i与当前E-节点所包含的所有顶点之间都有一条边,则i 被加入当前的完备子图之中。为了检查左孩子的可行性,可以沿着从E-节点到根的路径,判断哪些顶点包含在E-节点之中,同时检查这些顶点中每个顶点是否都存在一条到i 的边。如果左孩子是可行的,则把它加入到最大优先队列和正在构造的树中。下一步,如果右孩子的子树中包含最大完备子图对应的叶节点,则把右孩子也加入。
由于每个图都有一个最大完备子图,因此从堆中删除节点时,不需要检验堆是否为空。仅当到达一个可行的叶节点时,w h i l e循环终止。
程序17-8 最大完备子图问题的分枝定界算法
int AdjacencyGraph::BBMaxClique(int bestx[])
{// 寻找一个最大完备子图的最大收益分枝定界程序
// 定义一个最多可容纳1 0 0 0个活节点的最大堆
MaxHeap<CliqueNode> H(1000);
// 初始化层1
bbnode *E = 0; // 当前的E-节点为根
int i = 1, // E-节点的层
cn = 0, // 完备子图的大小
bestn = 0; // 目前最大完备子图的大小
// 搜索子集空间树
while (i != n+1) {// 不是叶子
// 在当前完备子图中检查顶点i 是否与其它顶点相连
bool OK = true;
bbnode *B = E;
for (int j = i - 1; j > 0; B = B->parent, j--)
if (B->LChild && a[i][j] == NoEdge) {
OK = false;
b r e a k ; }
if (OK) {// 左孩子可行
if (cn + 1 > bestn) bestn = cn + 1;
AddCliqueNode(H, cn+1, cn+n-i+1, i+1, E, true);}
if (cn + n - i >= bestn)
// 右孩子有希望
AddCliqueNode(H, cn, cn+n-i, i+1, E, false);
// 取下一个E-节点
CliqueNode N;
H.DeleteMax(N); // 不能为空
E = N.ptr;
cn = N.cn;
i = N.level;
}
// 沿着从E到根的路径构造bestx[]
for (int j = n; j > 0; j--) {
bestx[j] = E-> L C h i l d ;
E = E-> p a r e n t ;
}
return bestn;
}
5.2.4 旅行商问题
旅行商问题的介绍见4 . 2 . 4节,它的解空间是一个排列树。与在子集树中进行最大收益和最小耗费分枝定界搜索类似,该问题有两种实现的方法。第一种是只使用一个优先队列,队列中的每个元素中都包含到达根的路径。另一种是保留一个部分解空间树和一个优先队列,优先队列中的元素并不包含到达根的路径。本节只实现前一种方法。
由于我们要寻找的是最小耗费的旅行路径,因此可以使用最小耗费分枝定界法。在实现过程中,使用一个最小优先队列来记录活节点,队列中每个节点的类型为M i n H e a p N o d e。每个节点包括如下区域: x(从1到n的整数排列,其中x [ 0 ] = 1 ),s(一个整数,使得从排列树的根节点到当前节点的路径定义了旅行路径的前缀x[0:s], 而剩余待访问的节点是x [ s + 1 : n - 1 ]),c c(旅行路径前缀,即解空间树中从根节点到当前节点的耗费),l c o s t(该节点子树中任意叶节点中的最小耗费), r c o s t(从顶点x [ s : n - 1 ]出发的所有边的最小耗费之和)。当类型为M i n H e a p N o d e ( T )的数据被转换成为类型T时,其结果即为l c o s t的值。分枝定界算法的代码见程序1 7 - 9。
程序1 7 - 9首先生成一个容量为1 0 0 0的最小堆,用来表示活节点的最小优先队列。活节点按其l c o s t值从最小堆中取出。接下来,计算有向图中从每个顶点出发的边中耗费最小的边所具有的耗费M i n O u t。如果某些顶点没有出边,则有向图中没有旅行路径,搜索终止。如果所有的顶点都有出边,则可以启动最小耗费分枝定界搜索。根的孩子(图1 6 - 5的节点B)作为第一个E-节点,在此节点上,所生成的旅行路径前缀只有一个顶点1,因此s=0, x[0]=1, x[1:n-1]是剩余的顶点(即顶点2 , 3 ,., n )。旅行路径前缀1 的开销为0 ,即c c = 0 ,并且,r c o st=n ?i=1M i n O u t [i]。在程序中,bestc 给出了当前能找到的最少的耗费值。初始时,由于没有找到任何旅行路径,因此b e s t c的值被设为N o E d g e。
程序17-9 旅行商问题的最小耗费分枝定界算法
template<class T>
T AdjacencyWDigraph<T>::BBTSP(int v[])
{// 旅行商问题的最小耗费分枝定界算法
// 定义一个最多可容纳1 0 0 0个活节点的最小堆
MinHeap<MinHeapNode<T> > H(1000);
T *MinOut = new T [n+1];
// 计算MinOut[i] = 离开顶点i的最小耗费边的耗费
T MinSum = 0; // 离开顶点i的最小耗费边的数目
for (int i = 1; i <= n; i++) {
T Min = NoEdge;
for (int j = 1; j <= n; j++)
if (a[i][j] != NoEdge &&
(a[i][j] < Min || Min == NoEdge))
Min = a[i][j];
if (Min == NoEdge) return NoEdge; // 此路不通
MinOut[i] = Min;
MinSum += Min;
}
// 把E-节点初始化为树根
MinHeapNode<T> E;
E.x = new int [n];
for (i = 0; i < n; i++)
E.x[i] = i + 1;
E.s = 0; // 局部旅行路径为x [ 1 : 0 ]
E.cc = 0; // 其耗费为0
E.rcost = MinSum;
T bestc = NoEdge; // 目前没有找到旅行路径
// 搜索排列树
while (E.s < n - 1) {// 不是叶子
if (E.s == n - 2) {// 叶子的父节点
// 通过添加两条边来完成旅行
// 检查新的旅行路径是不是更好
if (a[E.x[n-2]][E.x[n-1]] != NoEdge && a[E.x[n-1]][1] != NoEdge && (E.cc +
a[E.x[n-2]][E.x[n-1]] + a[E.x[n-1]][1] < bestc || bestc == NoEdge)) {
// 找到更优的旅行路径
bestc = E.cc + a[E.x[n-2]][E.x[n-1]] + a[E.x[n-1]][1];
E.cc = bestc;
E.lcost = bestc;
E . s + + ;
H . I n s e r t ( E ) ; }
else delete [] E.x;}
else {// 产生孩子
for (int i = E.s + 1; i < n; i++)
if (a[E.x[E.s]][E.x[i]] != NoEdge) {
// 可行的孩子, 限定了路径的耗费
T cc = E.cc + a[E.x[E.s]][E.x[i]];
T rcost = E.rcost - MinOut[E.x[E.s]];
T b = cc + rcost; //下限
if (b < bestc || bestc == NoEdge) {
// 子树可能有更好的叶子
// 把根保存到最大堆中
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -