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>[n];
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++ 的中文版)