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

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(n1)] (向下取整)。由于共有n-1个内部节点,因此,最后一个内部节点编号为n-1。那么,最底层的内部节点数为 (n1)2s+1=n2s 个。而最底层的外部节点数(称为LowExt)为内部节点数的2倍,即 LowExt=2(n2s) 。若最底层的内部节点没有将该层填满,该层会存在外部节点,并且编号从 LowExt+1 开始。从而,该层外部节点个数为 n(LowExt+1)+1=nLowExt 。设中间变量 offset=2(s+1)1 。对于外部节点e[i],与其父节点t[p]间满足如下函数关系:
这里写图片描述

3、赢者树的C++描述

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

  1. 初始化赢者树(Initialize()函数实现);
  2. 获取最终的胜利者(Winner()函数实现);
  3. 获取在内部节点i比赛的获胜者(Winner(int i)函数);
  4. 若选手的得分发生改变,需重新进行比赛(RePlay()函数实现);
  5. 输出竞赛树(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++ 的中文版)

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值