2007年5月9日星期三

常用算法---第 5 章 分枝定界【Part2】

5.2.2 0/1背包问题

0 / 1背包问题的最大收益分枝定界算法可以由程序1 6 - 6发展而来。可以使用程序1 6 - 6的B o u n d函数来计算活节点N的收益上限u p ,使得以N为根的子树中的任一节点的收益值都不可能超过u p r o f i t。活节点的最大堆使用u p r o f i t作为关键值域,最大堆的每个入口都以H e a p N o d e作为其类型,H e a p N o d e有如下私有成员:uprofit, profit, weight,l e v e l,p t r,其中l e v e l和p t r的定义与装箱问题(见程序1 7 - 5)中的含义相同。对任一节点N,N . p r o f i t是N的收益值,N uprofit是它的收益上限, N. weight 是它对应的重量。b b n o d e类型如程序1 7 - 5中的定义,各节点按其u p r o f i t值从最大堆中取出。

程序1 7 - 7使用了类Knap, 它类似于回溯法中的类K n a p(见程序1 6 - 5)。两个K n a p版本中数据成员之间的区别见程序1 7 - 7:1) bestp 不再是一个成员; 2) bestx 是一个指向int 的新成员。新增成员的作用是:当且仅当物品j 包含在最优解中时, b e s t x [ j ] = 1。函数A d d L i v e N o d e用于将新的b b n o d e类型的活节点插入子集树中,同时将H e a p N o d e类型的活节点插入到最大堆中。这个函数与装箱问题(见程序1 7 - 6)中的对应函数非常类似,因此相应的代码被省略。

程序17-7 0/1背包问题的最大收益分枝定界算法

template

Tp Knap::MaxProfitKnapsack()

{// 返回背包最优装载的收益

// bestx[i] = 1 当且仅当物品i 属于最优装载

// 使用最大收益分枝定界算法

// 定义一个最多可容纳1 0 0 0个活节点的最大堆

H = new MaxHeap > (1000);

// 为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 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 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

T AdjacencyWDigraph::BBTSP(int v[])

{// 旅行商问题的最小耗费分枝定界算法

// 定义一个最多可容纳1 0 0 0个活节点的最小堆

MinHeap > 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 &&amp; (a[i][j] < min ="="" min =" a[i][j];" min ="=""> E;

E.x = new int [n];

for (i = 0; i < s =" 0;" cc =" 0;" rcost =" MinSum;" bestc =" NoEdge;" s ="="" bestc ="="" bestc =" E.cc" cc =" bestc;" lcost =" bestc;" i =" E.s" cc =" E.cc" rcost =" E.rcost" b =" cc" bestc ="=""> N;

N.x = new int [n];

for (int j = 0; j < cc =" cc;" s =" E.s" lcost =" b;" rcost =" rcost;" bestc ="="" i =" 0;" s =" n" s =" n" s =" n"> 是有向图的边且x [ i ]是路径x [ s + 1 : n - 1 ]上的顶点时,它的子节点可行。对于每个可行的孩子节点,将边 的耗费加上E.cc 即可得到此孩子节点的路径前缀( x [ 0 : s ],x[i]) 的耗费c c。由于每个包含此前缀的旅行路径都必须包含离开每个剩余顶点的出边,因此任何叶节点对应的耗费都不可能小于cc 加上离开各剩余顶点的出边耗费的最小值之和,因而可以把这个下限值作为E-节点所生成孩子的lcost 值。如果新生成孩子的lcost 值小于目前找到的最优旅行路径的耗费b e s t c,则把新生成的孩子加入活节点队列(即最小堆)中。

如果有向图没有旅行路径,程序1 7 - 9返回N o E d g e;否则,返回最优旅行路径的耗费,而最优旅行路径的顶点序列存储在数组v 中。

常用算法---第 1 章 贪婪算法【Part1】

常用算法---第 1 章 贪婪算法【Part2】

常用算法---第 1 章 贪婪算法【Part3】

常用算法---第 2 章 分而治之算法【Part1】

常用算法---第 2 章 分而治之算法【Part2】

常用算法---第 2 章 分而治之算法【Part3】

常用算法---第 2 章 分而治之算法【Part4】

常用算法---第 3 章 动态规划【Part1】

常用算法---第 3 章 动态规划【Part2】

常用算法---第 3 章 动态规划【Part3】

常用算法---第 4 章 回溯【Part1】

常用算法---第 4 章 回溯【Part2】

常用算法---第 4 章 回溯【Part3】

常用算法---第 5 章 分枝定界【Part1】

常用算法---第 5 章 分枝定界【Part2】

常用算法---第 5 章 分枝定界【Part3】

没有评论: