您的位置:首页 > 理论基础 > 数据结构算法

数据结构与算法C++描述(13)---竞赛树及其在箱子装载问题中的应用

2017-11-01 20:48 537 查看

1、竞赛树的相关概念

一般将竞赛树分为赢者树和输者树。所谓赢者树,就是对于n名选手,赢者树是一棵含n个外部节点,n-1个内部节点的完全二叉树,其中每个内部节点记录了相应赛局的赢家。同理,对于输者树,每个内部节点记录了相应赛局的输家。一个赢者树如下图所示。



上图中,黑色框中字母a,b,c,…,h为选手编号,下面的数字为选手得分。

为了利用计算机更方便的描述赢者树,假定每个赢者树都是完全二叉树。

2、赢者树的公式化描述

利用完全二叉树的公式化描述方法来定义赢者树。n名选手(用e[1:n]表示)的赢者树需要n-1个内部节点(用t[1:n-1]表示)。如下图所示。



下面探讨外部节点e[i]与其父节点t[p]间的关系。

根据完全二叉树的节点排列关系可知,最底层最左端的内部节点编号为2s,其中s=[log2(n−1)](向下取整)。由于共有n-1个内部节点,因此,最后一个内部节点编号为n-1。那么,最底层的内部节点数为(n−1)−2s+1=n−2s个。而最底层的外部节点数(称为LowExt)为内部节点数的2倍,即LowExt=2∗(n−2s)。若最底层的内部节点没有将该层填满,该层会存在外部节点,并且编号从LowExt+1开始。从而,该层外部节点个数为n−(LowExt+1)+1=n−LowExt。设中间变量offset=2(s+1)−1。对于外部节点e[i],与其父节点t[p]间满足如下函数关系:



3、赢者树的C++描述

对一个赢者树的操作主要包括:

初始化赢者树(Initialize()函数实现);

获取最终的胜利者(Winner()函数实现);

获取在内部节点i比赛的获胜者(Winner(int i)函数);

若选手的得分发生改变,需重新进行比赛(RePlay()函数实现);

输出竞赛树(Output()函数实现)。

3.1 赢者树类声明及简单函数的实现

对于赢者树类,私有成员包括树的最大容量MaxSize、树的当前元素个数CurrentSize、最底层的外部节点数LowExt、中间变量offset、赢者树数组*t、选手数组 *e,已经在节点p进行比赛函数Play()。程序如下:

/*-------------------------------竞赛树类--------------------------*/
template <class T>
class WinnerTree
{
public:
WinnerTree(int TreeSize = 10)
{
//构造赢者树
MaxSize = TreeSize;
t = new int[MaxSize];
CurrentSize = 0;
}
~WinnerTree() { delete[] t; };
//初始化赢者树,a为选手数组,size为选手数,Winner用于得到a[b]和a[c]之间的赢家
void Initialize(T a[], int size, int(*winner)(T a[], int b, int c));
//获取胜利者
int Winner()const { return (CurrentSize) ? t[1] : 0; }
//返回在内部节点i比赛的赢者
int Winner(int i)const { return (i < CurrentSize) ? t[i] : 0; }
//重新比赛
void RePlay(int i, int(*winner)(T a[], int b, int c));
//输出赢者树
void Output(ostream &out)
{
for (int i = 1; i < CurrentSize; i++)
out << t[i]<< "  ";
cout << endl;
}

private:
int MaxSize;                 //树的最大容量
int CurrentSize;             //树的当前容量
int LowExt;                  //最底层的外部节点数
int offset;                  //中间变量,等于2^k-1
int *t;                      //赢者树数组
T *e;                        //选手数组
//比赛
void Play(int p, int lc, int rc, int(*winner)(T a[], int b, int c));
};


程序中的winner函数为获取数组a[]中,b、c两节点的赢者函数,以大者获胜为例,代码如下:

//获得a[b]和a[c]中的最大者,返回最大者的索引值
int winner(WinnerNode a[], int b, int c)
{
if (a[b].data >= a[c].data)                  //较大者获胜
return b;
else
return c;
}


程序中WinnerNode类为赢者树节点类,描述了每个节点的索引值和数据值。

/*-----------------------------竞赛树节点类--------------------------*/
template <class T> class WinnerTree;
class WinnerNode
{
//声明友元函数,实现winner对类成员的访问
friend int winner(WinnerNode [], int, int);
friend void main(void);
friend void FirstFitPack(int [], int, int);

private:
int key,         //索引值
data;        //数据值
};


3.2 比赛函数—Play()函数

函数模拟节点p处的比赛,lc和rc是t[p]的左右孩子。

//比赛,在t[p]处开始比赛,lc和rc是t[p]的孩子
template <class T>
void WinnerTree<T>::Play(int p, int lc, int rc, int(*winner)(T a[], int b, int c))
{
t[p] = winner(e, lc, rc);

//若在右孩子处,则可能还有多场比赛
while (p>1 && p%2)               //在右孩子处
{
t[p / 2] = winner(e, t[p - 1], t[p]);
p /= 2;                     //到父节点
}
}


3.3 初始化赢者树—Initialize()函数

函数将一个元素为赢者树节点的数组a[],转化成一个赢者树。依据公式(1),遍历所有的内部节点。根据winner函数,给出每个内部节点的比赛结果。最终,形成一个赢者树。

//初始化赢者树,a为选手数组,size为选手数,Winner用于得到a[b]和a[c]之间的赢家
template <class T>
void WinnerTree<T>::Initialize(T a[], int size, int(*winner)(T a[], int b, int c))
{
if (size > MaxSize || size < 2)
throw BadInput();
CurrentSize = size;
e = a;

//计算s=2^log(n-1),赢者树中最后一个元素的编号
int i, s;
for (s = 1; 2 * s <= CurrentSize - 1; s += s);

LowExt = 2 * (CurrentSize - s);                   //最外层选手数
offset = 2 * s - 1;                               //中间变量

//最外层外部节点的比赛
for (i = 2; i <= LowExt; i += 2)                  //选取右孩子进行比赛
Play((offset + i) / 2, i - 1, i, winner);
//外部其余节点间进行比赛
if (CurrentSize % 2)                              //当n为奇数时,内部节点和外部节点进行比赛
{
Play(CurrentSize / 2, t[CurrentSize - 1], LowExt + 1, winner);
i = LowExt + 3;                               //下一个外部节点
}
else
i = LowExt + 2;                               //若n为偶数,加2后为右孩子

//i为右边剩余节点
for (; i <= CurrentSize; i += 2)
Play((i - LowExt + CurrentSize - 1) / 2, i - 1, i, winner);
}


3.4 重新比赛—RePlay()

选手i的比分改变后,从该选手的父节点开始,重新进行比赛,调整赢者树中的内部节点。

//选手i的值改变后,重新比赛
template <class T>
void WinnerTree<T>::RePlay(int i, int(*winner)(T a[], int b, int c))
{
if (i <= 0 || i > CurrentSize)
throw OutOfRange();

int p,                           //比赛节点
lc,                          //p的左孩子
rc;                          //p的右孩子
//找到第一个比赛节点及其子女
if (i <= LowExt)                 //若i位于最外层
{
p = (offset + i) / 2;        //p在竞赛树中的位置
lc = 2 * p - offset;         //p的左孩子
rc = lc + 1;                 //p的右孩子
}
else                             //p不在最外层
{
p = (i - LowExt + CurrentSize - 1) / 2;
if (2 * p == CurrentSize - 1)//与竞赛树中内部节点比赛
{
lc = t[2 * p];           //左孩子
rc = i;                  //右孩子
}
else
{
//左孩子
lc = 2 * p - CurrentSize + 1 + LowExt;
rc = lc + 1;             //右孩子
}
}
t[p] = winner(e, lc, rc);

//剩余节点的比赛
p /= 2;                          //移到其父节点
for (; p >= 1; p /= 2)
t[p] = winner(e, t[2 * p], t[2 * p + 1]);
}


3.4 重载操作符”<<”

//重载操作符"<<"
template <class T>
ostream &operator<<(ostream &out, WinnerTree<T> &WT)
{
WT.Output(out);
return out;
}


3.5 测试

int a[] = { 4,2,5,7,10,13,3,6,11,8 };
WinnerTree<WinnerNode> WT;
WinnerNode element[20];
for (int i = 1; i <= 10; i++)
element[i].data = a[i - 1];
WT.Initialize(element, 10, winner);
cout << "当前选手中,获胜者的编号为:" << WT.Winner() << endl;
cout << "输出赢者树" << WT;
element[2].data = 12;
WT.RePlay(2, winner);
cout << "当前选手中,获胜者的编号为:" << WT.Winner() << endl;
cout << "输出赢者树" << WT;


测试结果:



4、箱子装载问题的最先匹配算法

在箱子装载问题中,有若干个容量为c的箱子和n个待装入箱子中的物品。物品i需占s[i]个单元(0<s[i]<=c)。所谓成功装载,是指能把所有物品都装入箱子而不溢出,而最优装载是指使用了最少箱子的成功装载。

最先匹配算法是指:物品按1,2,3,…,n的顺序装入箱子。假设箱子从左到右排列。每一物品i放入可承载它的最左箱子。

利用赢者树实现最先匹配算法时,假设有n个箱子,每个箱子的初始容量相同,都为c。

4.1 算法流程图如下:



4.2 代码如下:

//放置箱子的最先匹配算法,s[]为各物品所需要的空间,n为物品数量,c为箱子容量
void FirstFitPack(int s[],int n,int c)
{
WinnerTree<WinnerNode> *W = new WinnerTree<WinnerNode>
;
WinnerNode *avail = new WinnerNode[n+1];           //箱子

//初始化n个箱子和赢者树
for (int i = 1; i <= n; i++)
avail[i].data = c;                            //初始可用容量
W->Initialize(avail, n, winner);

//将物品放入箱子中
for (int i = 1; i <= n; i++)
{
//找到有足够容量的第一个箱子
int p = 2;
while (p<n)
{
int winp = W->Winner(p);
if (avail[winp].data < s[i])             //左子树容量小于物品大小
p++;
p *= 2;                                  //移动到其左孩子
}

int b;
p /= 2;                                      //当前节点索引
if (p < n)                                   //在内部节点
{
b = W->Winner(p);
//若b是右孩子,需要检查箱子b-1
if (b > 1 && avail[b - 1].data >= s[i])
b--;
}
else    //p==n,即物品数为奇数,有一个颗树只有左孩子,取其根节点的赢者
b = W->Winner(p / 2);

cout << "物品 " << i << " 放入箱子 " << b << endl;
avail[b].data -= s[i];                       //更新箱子容量
W->RePlay(b, winner);                        //重新生成竞赛树
}
}


4.3 测试

/*-------------------------------箱子放置的最先匹配算法----------------------------*/
int n, c;                             //n为物品个数,c为箱子容量
cout << "请输入待放置的物品个数n:   ";
cin >> n;
cout << "请输入箱子的容量c:          ";
cin >> c;
int *s = new int[n + 1];                //各物品所需的放置空间
cout << "请输入各物品所需空间:       ";
for (int i = 1; i <= n; i++)
{
cin >> s[i];
if (s[i] <= 0 || s[i]>c)
throw BadInput();
}
FirstFitPack(s, n, c);


测试结果:





参考:

[1] 数据结构算法与应用:C++描述(Data Structures, Algorithms and Applications in C++ 的中文版)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐