数据结构学习笔记(七)竞赛树

一、竞赛树

         假设有n个选手参加一场网球比赛,比赛规则是“突然死亡法”,即只要输一场就会被淘汰。一对一进行比拼,最后只剩下一个选手保持不败。下图13-1中显示了比赛过程,有a~h一共8名选手。这个比赛使用二叉树进行表示,每一个外部节点表示一名选手,每一个内部节点表示一场比赛,该节点的孩子表示比赛的选手。在同一层的内部节点代表一轮比赛,可以同时进行。在第一轮比赛中,对阵的选手有ab之间、cd之间、ef之间以及gh之间。每一场比赛的胜利者被记录在代表该比赛的内部节点中。在下图a中,第一轮比赛的胜利者为b、d、e和h,其余四位选手直接淘汰;下一轮比赛的对阵是bd之间、eh之间,胜利者是b和e,并且进入决赛;最后胜利者是e,放在根节点上。整个比赛过程见图a所示。图b中给出5名选手参加的比赛,从a到e,最后的胜利者是c。

         数据结构学习笔记(七)竞赛树_第1张图片

       上图的两颗树都是完全二叉树。现实中竞赛所对应的树不一定是完全二叉树,但是这样做会使得比赛的场次最少。而且上面的竞赛树每一个内部节点记录的是比赛的赢者,我们称之为赢者树。如果内部节点记录的是比赛的输者,我们称之为输者树。应用中赢者树更加直观,但是输者树的实现效率更高。

       我们定义赢者树:有n个选手的一颗赢者树是一颗完全二叉树,它有n个外部节点和n-1个内部节点,每一个内部节点记录的是在该节点进行比赛的胜利者。我们定义最小赢者树,每一场比赛分数少的选手获胜;最大赢者树中,每一场比赛分数多的选手获胜。在分数相等时,左孩纸表示的选手获胜。下图13-2中a是一颗有8名选手的最小赢者树,b是一颗有5名选手的最大赢者树。每一个外部节点下面的数字表示选手的分数。

        数据结构学习笔记(七)竞赛树_第2张图片

二、赢者树

       我们使用完全二叉树的数组表示赢者树。一颗赢者树有n名选手,需要n-1个内部节点 tree[1:n-1]。选手或者外部节点用数组 player[1:n] 表示,因此 tree[i] 是数组player的一个索引,类型为int。在赢者树的节点 i 对应比赛中的赢者 tree[i] 。图13-4给出在5个选手的赢者树中,各节点与数组 tree 和 player 之间的对应关系。

       数据结构学习笔记(七)竞赛树_第3张图片

      为实现这种对应关系,我们必须能够确定外部节点 player[i]  的父节点 tree[p]。当外部节点的个数为n时,内部节点的个数为n-1。最低层最左端的内部节点,其编号为s,并且有s=2^log(n-1)。因此,最底层内部节点的个数为n-s,而最底层外部节点的个数lowExt是这个数的2倍。例如,在图13-4中,n=5,s=4,最底层最左端的内部节点是 tree[4],这一层的内部节点个数为n-4=1个。最底层外部节点个数lowExt=2,倒数第二层最左端的外部节点编号是lowExt+1.。令offset=2*s-1。对于任何一个外部节点 player[i] ,其父节点tree[p]有一下公式给出:

      

      赢者树关键的两个操作是初始化和重新组织比赛。为了初始化一个赢者树,我们从右孩纸选手开始,进行他所参加的比赛,而且逐层网上,只要是从右孩纸上升到比赛节点,就可以进行在该节点的比赛。为此,要从左往右的考察右孩纸选手。在图13-4的树种,我们首先进行选手 player[2] 的比赛,然后进行 player[3] 的比赛,最后进行 player[5]的比赛。首先,我们进行选手 player[2] 参加在节点 tree[4]  的比赛,但是接下来,我们不能在上一层节点 tree[2]  的比赛,因为 tree[4] 是左孩纸。然后我们进行选手 player[3] 参加在tree[2] 的比赛,但是接下来不能进行在节点tree[1]的比赛,因为tree[2]是左孩纸。最后我们进行选手 play[5] 参加的在节点tree[3]的比赛和在节点 tree[1] 的比赛。注意,节点 tree[i] 节点记录的是比赛的赢者。

      当选手 thePlayer 的值改变,在从外部节点 player[ thePlayer ] 到根节点 tree[1] 的路径上,一部分或者全部比赛都需要进行重赛。为简单起见,我们要路径上的全部比赛重赛。具体的实现方案如下:

三、赢者树的实现

#pragma once 
#include

using namespace std;

template 
class winnerTree
{
public:
	//构造函数和析构函数
	winnerTree(T* thePlayer, int theNumberOfPlayer)
	{
		tree = NULL;
		initializer(thePlayer, theNumberOfPlayer);
	}
	~winnerTree(){ delete[] tree; }

	//使用数组thePlayer初始化一个赢者树
	void initializer(T *, int );
	//返回比赛最后赢者的下标
	int winner() const { return tree[1]; }
	//返回比赛中某一环节的比赛的赢者
	int winner(int i) const { return (i < numberofPlayer) ? tree[i] : 0; }
	//因改变某一个player的值进而重赛
	void rePlay(int);
	//输出
	void output() const;

private:
	T* player;//参赛成员组成的数组
	int numberofPlayer;//参赛成员的数量
	int* tree;//赢者树,数组表示,赢者树只包含比赛结果,节点表示赢者的下标

	int lowExt;//用于生成赢者树,表示最底层外部节点的个数
	int offset;//用于生成赢者树,=2^log(n-1) - 1,详见解析
	void play(int, int, int);//初始化需要用到,
};

//使用数组thePlayer初始化为一个赢者树
template
void winnerTree::initializer(T *thePlayer, int theNumberOfPlayers)
{
	int n = theNumberOfPlayers;
	if (n < 2)
	{
		cerr << "Must have at least 2 players"; exit(0);
	}

	player = thePlayer;
	numberofPlayer = n;
	delete[] tree;
	tree = new int[n];//n个外部节点,需要n-1内部节点,而数组0空置不用,所以需要n的空间

	int s;
	for (s = 1; 2 * s <= n - 1; s += s);//最底层最左端的内部节点编号s = 2^log (n-1),取log的下整
	lowExt = 2 * (n - s);//最底层外部节点个数=2*最底层内部节点个数,最底层内部节点个数n-s
	offset = 2 * s - 1;//令offset=2*s-1
	//对于任何一个外部节点player[i],其父节点tree[p]满足:
	//当i<=lowExt时,p=(i+offset)/2;  当i>lowExt时,p=(i-lowExt+n-1)/2;
	
	int i;
	//对最底层的外部节点比赛,从2,4,6。。开始知道判断
	for (i = 2; i <= lowExt; i += 2)
		play((offset + i) / 2, i - 1, i);
	//处理剩余的外部节点
	if (n % 2 == 1)
	{//如果外部节点n为奇数,需要特殊处理
		play(n / 2, tree[n - 1], lowExt + 1);
		i = lowExt + 3;
	}
	else//如果外部节点n为偶数
		i = lowExt + 2;
	for (; i <= n; i += 2)
		play((i - lowExt + n - 1) / 2, i - 1, i);
}

//play操作,从最开始的底层进行比赛,直到完成所有比赛决胜出最后赢者
template
void winnerTree::play(int p, int leftchild, int rightchild)
{//从tree[p]开始的比赛,一直往上冒泡,直至根节点
	tree[p] = (player[leftchild] <= player[rightchild]) ? leftchild : rightchild;
	while (p % 2 == 1 && p > 1)
	{//p作为右孩纸出现,才继续往上走,因为最开始play中也是由右孩纸判断的
		tree[p / 2] = (player[tree[p - 1]] <= player[tree[p]]) ? tree[p - 1] : tree[p];
		p /= 2;
	}
}

//当thePlayer的值改变时,需要重赛,我们可以打乱然后重新初始化,但一般不这么做
template
void winnerTree::rePlay(int thePlayer)
{
	int n = this->numberofPlayer;
	if (thePlayer <= 0 || thePlayer > n)
	{
		cerr << "Player index is illegal"; exit(0);
	}
	int matchNode,leftchid,	rightchild;//下一个需要重赛的节点以及他的孩纸

	//找出第一个需要重赛的节点以及他的左右孩纸
	if (thePlayer <= lowExt)
	{
		matchNode = (offset + thePlayer) / 2;
		leftchid = 2 * matchNode - offset;
		rightchild = leftchid + 1;
	}
	else
	{
		matchNode = (thePlayer - lowExt + n - 1) / 2;
		if (2 * matchNode == n - 1)
		{
			leftchid = tree[2 * matchNode];
			rightchild = thePlayer;
		}
		else
		{
			leftchid = 2 * matchNode - n + 1 + lowExt;
			rightchild = leftchid + 1;
		}
	}
	tree[matchNode] = (player[leftchid] <= player[rightchild]) ? leftchid : rightchild;

	//第二个需要重赛的节点中的特殊情况,即包含内部和外部节点
	if (matchNode == n - 1 && n % 2 == 1)
	{
		matchNode /= 2;
		tree[matchNode] = (player[tree[n - 1]] <= player[lowExt + 1]) ? tree[n - 1] : lowExt + 1;
	}
	//剩余的需要重赛的节点处理
	matchNode /= 2;
	for (; matchNode >= 1; matchNode /= 2)
		tree[matchNode] = (player[tree[2 * matchNode]] <= player[tree[2 * matchNode + 1]]) ?
							tree[2 * matchNode] : tree[2 * matchNode + 1];
}

//输出
template
void winnerTree::output() const
{
	cout << "number of players  = " << numberofPlayer
		<< " lowExt = " << lowExt
		<< " offset = " << offset << endl;
	cout << "complete winner tree pointers are" << endl;
	for (int i = 1; i < numberofPlayer; i++)
		cout << tree[i] << ' ';
	cout << endl;
}

   测试代码:

#include 
#include "WinnerTree.h"
using namespace std;

struct player
{
	int id, key;

	operator int() const { return key; }
};

void main(void)
{
	int n;
	cout << "Enter number of players, >= 2" << endl;
	cin >> n;
	if (n < 2)
	{
		cout << "Bad input" << endl;
		exit(1);
	}


	player *thePlayer = new player[n + 1];

	cout << "Enter player values" << endl;
	for (int i = 1; i <= n; i++)
	{
		cin >> thePlayer[i].key;
		thePlayer[i].id = i;
	}

	winnerTree *w =	new winnerTree(thePlayer, n);
	cout << "The winner tree is" << endl;
	w->output();

	thePlayer[2].key = 0;
	w->rePlay(2);
	cout << "Changed player 2 to zero, new tree is" << endl;
	w->output();
}


四、输者树

       输者树的内部节点记录的是比赛的失败者,我们可以使用 tree[0] 来记录比赛最后的赢者。当一个赢者发生变化,使用输者树可以简化重新比赛的过程。但是,其他选手发生改变时,还是使用赢者树比较方便,毕竟赢者树在概念上更容易理解。因此,只有选手 player[i] 为前一次比赛的赢家时,对于函数 rePlay(i)采用输者树比赢者树执行效率高。

你可能感兴趣的:(数据结构,数据结构)