《数据结构、算法与应用 —— C++语言描述》学习笔记 — 竞赛树

  • 一、赢者树
  • 二、二叉树的数组描述(补充)
    • 1、声明
    • 2、实现
  • 三、赢者树
    • 1、抽象数据类型
    • 2、赢者树的表示
    • 3、声明
    • 4、初始化
    • 5、重新组织比赛
    • 6、获取胜者

一、赢者树

假定有 n 个选手参加一次网球比赛。比赛规则是“突然死亡法”:一名选手只要输掉一场球,就被淘汰。一对一对选手进行比赛,最终只剩下一个选手保持不败。我们使用二叉树来描述这个比赛过程。每个外部节点表示一名选手,每个内部节点表示一场比赛,该内部节点的孩子表示比赛的选手。同一层的内部节点代表一轮比赛,可以同时进行:

如图,第一轮比赛,bc 的胜者为 bde 的胜者为 d;第二轮比赛,ab 的胜者为 bdf 的胜者为 d
最后一轮比赛,bd 的胜者为 b

事实上,如果我们使用完全二叉树实现,可以使比赛的场次达到最小( ⌈ log ⁡ 2 n ⌉ \lceil \log_2n \rceil ⌈log2​n⌉):

前面所列出的竞赛树称为赢者树,因为每一个内部节点所记录的都是比赛的赢者。除此之外,还有一种输者树,每一个内部节点所记录的都是比赛的输者。竞赛树也称为选择树。

赢者树的定义如下(为了便于实现,我们限制赢者树为完全二叉树):
有n个选手的一棵赢者树是一棵完全二叉树,它有n个外部节点和n-1个内部节点,每个内部节点记录的是在该节点比赛的赢者。

为了确定一场比赛的赢者树,我们假设每个选手都有一个分数,而且有一个规则用来比较两个选手的分数以确定赢者。在最小赢者树中,分数大的选手获胜。在分数相等,即平局的时候,左孩子表示的选手获胜。

赢者树的一个优点在于,当一名选手的分数改变时,修改竞赛树比较容易。例如,当选手 d 的分数由9改为1时,只有从 d 到根的路径上的节点所标示的比赛可能需要重赛,而其他比赛的结果不受影响。有时,甚至连这种路径上的一些比赛也不需要重赛。当被改变的结果不影响某个内部节点的比赛结果时,该节点的所有祖先节点都不受影响。

在一棵 n 个选手的赢者树中,当一个选手的分数发生变化时,需要修改的比赛场次介于 1 ~ ⌈ log ⁡ 2 n ⌉ \lceil \log_2n \rceil ⌈log2​n⌉ 之间,因此,赢者树的重构需要耗时 ⌈ log ⁡ 2 n ⌉ \lceil \log_2n \rceil ⌈log2​n⌉。此外,n 个选手的赢者树可以在 O ( n ) O(n) O(n)时间内初始化,方法是沿着从叶子到根的方向,在内部节点进行 n − 1 n-1 n−1 场比赛。

二、二叉树的数组描述(补充)

为了实现赢者树,显然我们不能前面实现的链式二叉树不能满足要求。因为对于赢者树来说,内部节点和外部节点的数据类型并不一致。因此,我们选择使用数组实现赢者二叉树。为此,我们首先需要先用实现二叉树的数组描述。

1、声明

#pragma once
#include "binaryTree.h"
#include <deque>
#include <algorithm>template<typename T>
class ArrayBinaryTree : public binaryTree<T>
{public:ArrayBinaryTree();virtual ~ArrayBinaryTree();ArrayBinaryTree(const ArrayBinaryTree& other);ArrayBinaryTree(ArrayBinaryTree&& other);ArrayBinaryTree& operator=(const ArrayBinaryTree& other);ArrayBinaryTree& operator=(ArrayBinaryTree&& other);virtual bool empty() const override;virtual int size() const override;virtual void preOrder(std::function<void(T)> visitFunc) override;virtual void inOrder(std::function<void(T)> visitFunc) override;virtual void postOrder(std::function<void(T)> visitFunc) override;virtual void levelOrder(std::function<void(T)> visitFunc) override;int height();void makeTree(T* elements, int treeSize);protected:T* elements = nullptr;int treeSize = 0;std::function<void(T)> visitFunc;void makeCopyAndSwap(const ArrayBinaryTree& other);ArrayBinaryTree makeCopy(const ArrayBinaryTree& other);void swap(ArrayBinaryTree& other);void clear();void preOrder(int index);void inOrder(int index);void postOrder(int index);void levelOrder(int index);int height(int index);
};

2、实现

大部分逻辑和链式二叉树类似,以中序遍历为例(注意树中的数据对应的数组元素从1开始):

template<typename T>
inline void ArrayBinaryTree<T>::inOrder(std::function<void(T)> visitFunc)
{this->visitFunc.swap(visitFunc);inOrder(1);
}template<typename T>
inline void ArrayBinaryTree<T>::inOrder(int index)
{if (index >= treeSize + 1){return;}inOrder(index * 2);visitFunc(elements[index]);inOrder(index * 2 + 1);
}

三、赢者树

1、抽象数据类型

#pragma once
#include <memory>template<typename T>
class AbstractWinnerTree
{public:virtual ~AbstractWinnerTree() {}virtual void initialize(T* player, int playerNum) = 0;virtual int winner() const = 0;virtual void replay(int playerIndex, T newValue) = 0;
};

2、赢者树的表示

假设用完全二叉树的数组表示来表示赢者树。一棵赢者树有 n 名选手(player[1:n]),需要 n-1 个内部节点(tree[1:n-1])。

这里的关键点在于找到外部节点与相关联的内部节点的映射关系。首先,我们可以把外部节点分成两部分,底层外部节点(即1-6)和上层外部节点(即7)。

对于底层外部节点,我们不难看出其编号与父节点编号呈线性关系,偏移量取决于最下层内部节点的开始编号(即4)。那么这个开始编号是怎么算出来的呢?根据内部节点个数 n-1,我们可以得到树的高度为 h = ⌊ l o g 2 n − 1 ⌋ + 1 h=\lfloor log_2{n-1} \rfloor +1 h=⌊log2​n−1⌋+1。那么开始编号应该为 s = 2 h − 1 s = 2^{h-1} s=2h−1,底部外部节点对应的父节点编号为: i − 1 2 + s \frac{i-1}{2}+s 2i−1​+s。除此之外,我们可以得到下层外部节点的总数为 ( n − 1 − s + 1 ) ∗ 2 = ( n − s ) ∗ 2 (n-1-s+1)*2=(n-s)*2 (n−1−s+1)∗2=(n−s)∗2

对于上层节点,其编号取决于树的节点数。确切地来说,上层节点相当于将二叉树补充为满二叉树。那么我们可以计算每个上层内部节点的编号与其转化为内部节点应该有的编号的对应关系。也就是说,如果我们将编号为7的外部节点转化为内部节点,其编号为7(一样是巧合)。二者之间实际是存在一个偏移量的。以补充的第 n 个内部节点为例,其外部节点编号为 ( n − s ) ∗ 2 + 1 (n-s)*2+1 (n−s)∗2+1。因此,二者的偏移量为 n − 2 ∗ s + 1 n-2*s+1 n−2∗s+1。因此,上层节点对应的外部节点为 p − n + 2 ∗ s − 1 p-n+2*s-1 p−n+2∗s−1。我们可以进而得到上层节点对应的父节点。

综上,我们得出内部节点和外部节点的转化关系为:
p l a y e r = { i − 1 2 + s i ≤ ( n − s ) ∗ 2 p − n + 2 ∗ s − 1 2 i > ( n − s ) ∗ 2 player=\left\{ \begin{array}{rcl} \frac{i-1}{2}+s& {i \leq (n-s)*2}\\ \frac{p-n+2*s-1}{2}& {i > (n-s)*2}\\ \end{array} \right. player={2i−1​+s2p−n+2∗s−1​​i≤(n−s)∗2i>(n−s)∗2​

3、声明

为了简单起见,我们这里不实现拷贝赋值等功能。

#pragma once
#include "AbstractWinnerTree.h"
#include "../binaryTree/ArrayBinaryTree.h"
#include <cmath>template<typename T>
class WinnerTree : public AbstractWinnerTree<T>, public ArrayBinaryTree<int>
{public:WinnerTree();WinnerTree(const WinnerTree& other) = delete;WinnerTree& operator=(const WinnerTree& other) = delete;~WinnerTree();void initialize(T* player, int playerNum) override;int winner() const override;void replay(int playerIndex, T newValue) override;private:T* player = nullptr;int playerNum = 0;int startIndexOfBottomInternalNodes = 0;int bottomExternalNodesNum = 0;void initExternalNodes(T* player, int playerNum);void initInternalNodes();  void initInternalNodesWithExternalNodes(std::unique_ptr<int[]>& internalNodes, int internalNodesNum);int initBottomInternalNodes(std::unique_ptr<int[]>& internalNodes);int initBorderInternalNodes(std::unique_ptr<int[]>& internalNodes, int internalNodesNum, int currentExternalIndex);void initUpperInternalNodes(std::unique_ptr<int[]>& internalNodes, int internalNodesNum, int currentExternalIndex);void initPureInternalNodes(std::unique_ptr<int[]>& internalNodes, int internalNodesNum);int replayBottomInternalNode(int playerIndex);int replayUpperInternalNode(int playerIndex, int parentNode);int replayBorderInternalNode(int playerIndex, int parentNode);void replayPureInternalNode(int parentNode);void comparePlayersAndSetInternal(int leftPlayer, int rightPlayer, int* internalNodes, int internalNodeIndex);
};

4、初始化

构造的过程比较复杂。首先,我们要对外部节点进行一次拷贝(initExternalNodes),因为我们支持修改外部节点数值以重新比赛。然后我们才可以初始化内部节点(initInternalNodes)。内部节点的初始化同样分为两部分:初始化有外部节点为叶子的内部节点(initInternalNodesWithExternalNodes)以及纯内部节点(initPureInternalNodes)。初始化外部节点需要根据不同的内部节点进行分类初始化。首先是底层外部节点(initBottomInternalNodes);然后是边界外部节点(initBottomInternalNodes),注意边界外部节点指其兄弟节点为内部节点的外部节点;最后是上层外部节点(initUpperInternalNodes)。这三种的编号映射关系计算各不相同。初始化纯内部节点的方式和前面初始化堆的方式类似。

template<typename T>
inline void WinnerTree<T>::initialize(T* player, int playerNum)
{initExternalNodes(player, playerNum);initInternalNodes();
}template<typename T>
inline void WinnerTree<T>::initExternalNodes(T* player, int playerNum)
{this->playerNum = playerNum;this->player = new T[playerNum + 1];std::copy(player, player + playerNum + 1, this->player);
}template<typename T>
inline void WinnerTree<T>::initInternalNodes()
{int internalNodesNum = playerNum - 1;std::unique_ptr<int[]> internalNodes(new int[internalNodesNum + 1]);startIndexOfBottomInternalNodes = std::pow(2, std::floor(std::log2(internalNodesNum)));bottomExternalNodesNum = (playerNum - startIndexOfBottomInternalNodes) * 2;initInternalNodesWithExternalNodes(internalNodes, internalNodesNum);initPureInternalNodes(internalNodes, internalNodesNum);this->makeTree(internalNodes.get(), internalNodesNum);
}template<typename T>
inline void WinnerTree<T>::initInternalNodesWithExternalNodes(std::unique_ptr<int[]>& internalNodes, int internalNodesNum)
{int currentExternalIndex = initBottomInternalNodes(internalNodes);currentExternalIndex = initBorderInternalNodes(internalNodes, internalNodesNum, currentExternalIndex);initUpperInternalNodes(internalNodes, internalNodesNum, currentExternalIndex);
}template<typename T>
inline int WinnerTree<T>::initBottomInternalNodes(std::unique_ptr<int[]>& internalNodes)
{int currentExternalIndex = 1;for (; currentExternalIndex < bottomExternalNodesNum + 1; currentExternalIndex += 2){// 实际实现时,由于c++中取整默认向0取整,因此可以考虑将s值放到分子上int internalIndex = (currentExternalIndex + 2 * startIndexOfBottomInternalNodes - 1) / 2;comparePlayersAndSetInternal(currentExternalIndex, currentExternalIndex + 1, internalNodes.get(), internalIndex);}return currentExternalIndex;
}template<typename T>
inline int WinnerTree<T>::initBorderInternalNodes(std::unique_ptr<int[]>& internalNodes, int internalNodesNum, int currentExternalIndex)
{if ((playerNum - currentExternalIndex) % 2 == 0){int index = internalNodes[internalNodesNum];comparePlayersAndSetInternal(index, currentExternalIndex, internalNodes.get(), internalNodesNum / 2);currentExternalIndex++;}return currentExternalIndex;
}template<typename T>
inline void WinnerTree<T>::initUpperInternalNodes(std::unique_ptr<int[]>& internalNodes, int internalNodesNum, int currentExternalIndex)
{for (; currentExternalIndex < playerNum + 1; currentExternalIndex += 2){int internalIndex = (currentExternalIndex - playerNum - 1 + 2 * startIndexOfBottomInternalNodes) / 2;comparePlayersAndSetInternal(currentExternalIndex, currentExternalIndex + 1, internalNodes.get(), internalIndex);}
}template<typename T>
inline void WinnerTree<T>::initPureInternalNodes(std::unique_ptr<int[]>& internalNodes, int internalNodesNum)
{for (int index = (internalNodesNum - 1) / 2; index > 0; --index){auto leftIndex = internalNodes[index * 2];auto rightIndex = internalNodes[index * 2 + 1];this->player[leftIndex] > this->player[rightIndex] ? internalNodes[index] = leftIndex : internalNodes[index] = rightIndex;}
}template<typename T>
inline void WinnerTree<T>::comparePlayersAndSetInternal(int leftPlayer, int rightPlayer, int* internalNodes, int internalNodeIndex)
{if (this->player[leftPlayer] > this->player[rightPlayer]){internalNodes[internalNodeIndex] = leftPlayer;}else{internalNodes[internalNodeIndex] = rightPlayer;}
}

5、重新组织比赛

重新组织比赛的计算方式我们前面已经学习过。需要注意,重新组织比赛也需要分三种情况分析,和上面类似。

template<typename T>
inline void WinnerTree<T>::replay(int playerIndex, T newValue)
{this->player[playerIndex] = newValue;int parentNode = replayBottomInternalNode(playerIndex);parentNode = replayUpperInternalNode(playerIndex, parentNode);parentNode = replayBorderInternalNode(playerIndex, parentNode);replayPureInternalNode(parentNode);
}template<typename T>
inline int WinnerTree<T>::replayBottomInternalNode(int playerIndex)
{if (playerIndex < bottomExternalNodesNum + 1){int parentNode = (playerIndex + 2 * startIndexOfBottomInternalNodes - 1) / 2;int leftPlayer = (parentNode - startIndexOfBottomInternalNodes + 1) * 2 - 1;int rightPlayer = leftPlayer + 1;comparePlayersAndSetInternal(leftPlayer, rightPlayer, this->elements, parentNode);parentNode /= 2;return parentNode;}return 0;
}template<typename T>
inline int WinnerTree<T>::replayUpperInternalNode(int playerIndex, int parentNode)
{if (playerIndex > bottomExternalNodesNum){parentNode = (playerIndex - playerNum - 1 + 2 * startIndexOfBottomInternalNodes) / 2;if (parentNode * 2 > this->treeSize){int leftPlayer = parentNode * 2 - this->treeSize + bottomExternalNodesNum;int rightPlayer = leftPlayer + 1;comparePlayersAndSetInternal(leftPlayer, rightPlayer, this->elements, parentNode);parentNode /= 2;}}return parentNode;
}template<typename T>
inline int WinnerTree<T>::replayBorderInternalNode(int playerIndex, int parentNode)
{if (parentNode * 2 == this->treeSize){int leftPlayer = this->elements[parentNode * 2];int rightPlayer = bottomExternalNodesNum + 1;comparePlayersAndSetInternal(leftPlayer, rightPlayer, this->elements, parentNode);parentNode /= 2;}return parentNode;
}template<typename T>
inline void WinnerTree<T>::replayPureInternalNode(int parentNode)
{while (parentNode >= 1){int leftPlayer = this->elements[parentNode * 2];int rightPlayer = this->elements[parentNode * 2 + 1];comparePlayersAndSetInternal(leftPlayer, rightPlayer, this->elements, parentNode);parentNode /= 2;}
}

6、获取胜者

template<typename T>
inline int WinnerTree<T>::winner() const
{if (this->treeSize == 0){throw std::overflow_error("no element in the tree");}return this->elements[1];
}

《数据结构、算法与应用 —— C++语言描述》学习笔记 — 竞赛树相关推荐

  1. r语言实现关联分析--关联规则挖掘(Apriori算法) (r语言预测学习笔记)

    r语言实现关联分析–关联规则挖掘 关联分析: 引子: 我们一般把一件事情发生,对另一间事情也会产生影响的关系叫做关联.而关联分析就是在大量数据中发现项集之间有趣的关联和相关联系(形如"由于某 ...

  2. 数据结构 算法与应用C 语言描述第六章,数据结构算法与应用-C语言描述002.pdf

    下载 下载 第2 章 程 序 性 能 以下是本章中所介绍的有关程序性能分析与测量的概念: • 确定一个程序对内存及时间的需求. • 使用操作数和执行步数来测量一个程序的时间需求. • 采用渐进符号描述 ...

  3. 数据结构 算法与应用C 语言描述答案,数据结构算法与应用-C语言描述.pdf

    下载 下载 第1 6章 回 溯 寻找问题的解的一种可靠的方法是首先列出所有候选解,然后依次检查每一个,在检查完 所有或部分候选解后,即可找到所需要的解.理论上,当候选解数量有限并且通过检查所有或 部分 ...

  4. 《数据结构、算法与应用 —— C++语言描述》学习笔记 — 优先级队列 — 左高树

    <数据结构.算法与应用 -- C++语言描述>学习笔记 - 优先级队列 - 左高树 一.左高树 1.外部节点 2.高度优先左高树 (1)定义 (2)特性 (3)HBLT 与 大小根树 3. ...

  5. 《数据结构、算法与应用 —— C++语言描述》学习笔记 — 回溯法

    <数据结构.算法与应用 -- C++语言描述>学习笔记 - 回溯法 一.算法思想 二.货箱装载 1.问题描述 2.回溯算法 3.实现 4.测试代码 一.算法思想 回溯法是搜索问题解的一种系 ...

  6. 数据结构、算法与应用c++语言描述(答案)

    数据结构.算法与应用c++语言描述(答案) https://www.cise.ufl.edu/~sahni/dsaac/view.htm   本身不是计算机专业的,属于那种自学半路出家的,最近刚开始看 ...

  7. 资料 | O‘Reilly精品图书系列:算法精解 C 语言描述 (简体中文)

    下载地址:资料 | O'Reilly精品图书系列:算法精解 C 语言描述 (简体中文) 内容简介 · · · · · · 本书是数据结构和算法领域的经典之作,十余年来,畅销不衰! 全书共分为三部分:第 ...

  8. 数据结构与算法分析:C语言描述(原书第2版 简体中文版!!!) PDF+源代码+习题答案...

    转自:http://www.linuxidc.com/Linux/2014-04/99735.htm 数据结构与算法分析:C语言描述(原书第2版中文版!!!) PDF+源代码+习题答案 数据结构与算法 ...

  9. 《数据结构与抽象:Java语言描述(原书第4版)》一Java插曲1

    本节书摘来华章计算机<数据结构与抽象:Java语言描述(原书第4版)>一书中的第1章 ,第1.1节,[美]弗兰克M.卡拉诺(Frank M. Carrano) 蒂莫西M.亨利(Timoth ...

最新文章

  1. 一些改进模型速度/精度的工程方法
  2. 本人使用Intelij idea问题及解决汇总
  3. delphi回调函数
  4. O(n)级选排名第k位数(附上算法复杂度分析)
  5. 【问题】14500充电锂电池电量问题及测试方案
  6. 动态加载JS脚本的4种方法
  7. vxworks 实时操作系统
  8. bootstrap框架菜单栏颜色设置_Bootstrap Icons - bootstrap专用的漂亮图标库,可以免费商用...
  9. arcgis图层合并裁剪
  10. qt:qt5.12警告消除大法之 warning: zero as null pointer constant
  11. NuGet是什么?为什么.NET项目中会有NuGet?如何使用NuGet程序包?
  12. (十进制快速幂+矩阵优化)BZOJ 3240 3240: [Noi2013]矩阵游戏
  13. c++实现图书管理系统v2.0
  14. mp4v2再学习 -- Linux 下安装说明
  15. 数字温湿度传感器DHT11
  16. DIY超好吃的橙子果酱
  17. 游戏开发学习路线图(2023最新版)建议收藏
  18. Myeclipse配置Tomcat
  19. 物流行业的大数据发展与应用
  20. ultraiso制作u盘启动盘教程详解

热门文章

  1. 每个 Web 开发者应该知道的 jQuery i18n 知识!——爱创课堂
  2. 连续不一定可导的例子
  3. 菜鸟学习Jmock测试-全解(四)
  4. 如何布局私域社群营销?做好这两点就够了
  5. 为什么朋友圈总有些环游世界的人? 可能大部分是...
  6. Ormlite 基本使用(关联表)
  7. Pod状态:Evicted Message提示:The node was low on resource: ephemeral-storage
  8. C#--编写旅行社程序
  9. jvm之AccessController.doPrivileged
  10. 吉利联手阿里云落地“中国制造2025”