首次适应轮转算法的程序实现的正确性需要一些数学理论基础,这个文档就是总结我对方面的一些分析。当然,针对循环赛日程还有一些更加简单的基于轮转的算法
定义:
循环赛日程是对n(n为偶数)个选手比赛进行安排的过程,比赛安排成n-1轮,n个选手组成n/2个唯一的(注:单循环)组,使得每一轮中由若二不同组组成。
总选手数:n
总轮数:n-1;
总组数:m=n/2
一次单循环赛的比赛总数:np=n(n-1)/2=nm
以整数1-n标识每一场比赛,以Pij标识选手i和j的比赛,0<i<j,以Pij的值表示比赛的安排序列1<=Pij<=np,则P组成以下标识组:
令Pij=E(i,j)是一个枚举映射函数,要注意的是由于E是任意np组比赛的映射,所以映射的可能数为np!
定义这样一个迭代函数Eu,他表示下图矩阵右上部分的元素,迭代顺序为从左到右,从上到下:
定义Sx,它代表包含有np个比赛日程安排的矩阵,如下图:
Sx表示所有比赛安排,当然也包括不符合单循环赛规则的
Sij=E(i', j')表示S i' j'能从E中找到,
考虑任意一个Sx,当n=6时
S3,5=10=E(4,5), i=3;j=5;i'=4;j'=5
首次适应算法的目标不断检验比赛日程对战安排Sij,直到有符合单循环规则日程安排产生。对战安排Sij通过枚举E(i' j')产生.
问题:刚开始时,首次适应算法以哪场比赛作为轮转的开始?
令S*=所有的Sx, 则S*中总共有np!种可能的日程矩阵.np!即E中排列总数
定义
Gi={Si1,Si2…Sim}
表示第i轮比赛的对战安排
定义
等价划分 为所有Sx=Sy的比赛安排
也就是说对两个日程矩阵,如果它们二者通过在Gi中重排Sij并在Sx中重排Gi最终一模一样的话,那么说这两个日程矩阵是等价的。
问题:总共有多少个等价划分?每个等价划分里有多少种对战安排?
我们知道Sx由Gi组成,i=1,2…n-1
考虑轮与轮之间的次序和每一轮内的对战顺序,则总共m!(n-1) 种可能的对战日程安排
分析:
假如轮次之间次序固定:第一轮对战有m!种排列方式,总共有n-1轮.所以按照排列组合的原理,共有m!(n-1) 种可能的对战日程安排
假如轮次之间次序不固定:第一轮对战有m!种排列方式,总共有n-1轮,轮次安排有(n-1)!种.所以按照排列组合的原理,共有m!(n-1)(n-1)种可能的对战日程安排
由此可以推测任何Sx所处的等价分类总共有m!(n-1)(n-1)种日程矩阵,更进一步可以推测
等价分类的总数为np!/[ m!(n-1)(n-1)]=[m(n-1)]!/ [ m!(n-1)(n-1)].
从分析中可以得到的是,如果一个循环比赛日程存在的话,那么它肯定属于[m(n-1)]!/ [ m!(n-1)(n-1)]个等价分类中的一个。可以肯定的是对n个选手的循环比赛日程至少存在一个解决方案。
结论:只有由Si1…Sim对每一个i,i=1…n-1是唯一的,Sx才是一个循环比赛日程矩阵
问题:这个算法是怎么实现的?
假设有一个n位的长整数i,每一位对应一个选手,设选手号码是d,则i的值就是2<<d-1
令Group为一个n位整数,用来存放某轮比赛参与的选手。如果Group所有的位都为1则所有的选手都已经参与了这轮比赛。
请看用整数枚举对战的一个向量表。向量表中每一个元素都有两个字段one和two,这此字段表示对战参与的选手,向量表是表来获取对战组和用来检查对战组是否可以放入当前对战轮次的。
图.选手数为6时所有可能的对战分组
一轮对战日程安排Group的建立就是通过不断地从此向量表中选择对战组和检验实现的。如果当前向量合适当前轮次,则就奖其加入到对战日程中,否则就检查下一个向量. If the enumeration vector is exhausted
without a pair being added to the plan vector, then the last pair in the plan vector is
‘unplanned’ and the hunt continues using the next pair of the enumeration vector.
当检查到当前向量v可以放入当前轮次时,则将v与Group作或运算赋值给Group即达到加入Group的目的。
Group|=v;
同时,从Group中移出某个对战组用与运算,如:
Group&=~v
一个产生Pij序列值的例子:
2.c/c++代码实现
#include <stdio.h>
#include <stdlib.h>
#define FALSE 0
#define TRUE 1
#define Maxplayers 20
#define MaxCombinations (Maxplayers/2)*(Maxplayers-1)
struct game { int one, two; };
int players; /* 选手总数 */
long combinations; /* 比赛场数,一场有两人参加,故起名combinations */
int a, b, c, i, m,
startC,
matchCount,
roundCount,
index;
long round_set; /*轮次选择的标志,如果某位被置1则相应的选手已经被加入当前轮次 */
long totalChecks;
struct game tourn[1+MaxCombinations]; /* 单循环比赛tournament */
int mList[1+Maxplayers/2]; /* matches */
struct game cList[1+MaxCombinations]; /* 所有比赛的对战组(一组两人) */
int cUsed[1+MaxCombinations]; /* 已经使用的对战组 */
void ShowSchedule(int event)//显示日程表
{
int index, r, m ;
fprintf( stdout, "共有/n%d 个选手", players-event );
fprintf( stdout, "/n ");
for (r=1; r <= players/2; r++) fprintf( stdout, " 好戏%d", r);
fprintf( stdout, "/n" );
fprintf( stdout, " +-");
for (r=1; r <= (players/2)*6-2; r++) fprintf( stdout, "-" );
fprintf( stdout, "/n" );
index = 1;
for (r=1; r <= players-1; r++) {
fprintf( stdout, "第%2d轮 |", r);
for (m=1; m <= players/2; m++) {
fprintf( stdout, "%2d&%2d ", tourn[index].one, tourn[index].two );
index++;
}
fprintf( stdout, "/n" );
}
fprintf( stdout, "/n%d 次尝试对战分组/n/n", totalChecks );
}
void ClearArrays()//清除以前的标记
{
int i;
for (i=0; i <= MaxCombinations; i++) { tourn[i].one = 0; tourn[i].two = 0; }
for (i=0; i <= Maxplayers/2; i++) mList[i] = 0;
for (i=0; i <= MaxCombinations; i++) { cList[i].one = 0; cList[i].two = 0; }
for (i=0; i <= MaxCombinations; i++) cUsed[i] = 0;
}
void doSchedule(int flag)//安排比赛日程
{
players = 4;
while (players <= Maxplayers) {
combinations = players/2 * (players-1);
totalChecks = 0;
ClearArrays();
/* 初始化所有比赛对战图 */ /* a */
m = 1; /* b 1 2 3 4 5 */
for (a=1; a < players; a++) /* 1 */
for (b=a+1; b <=players; b++) { /* 2 . */
cList[m].one = a; /* 3 . . */
cList[m].two = b; /* 4 . . . */
m++; /* 5 . . . . */
}
roundCount = 1;
index = 1;
while (roundCount <= players-1) {
matchCount = 1;
round_set = 0;
for (i=0; i <= Maxplayers/2; i++) mList[i] = 0;
startC = roundCount;
/* 开始查找,找到合适的对战组加入当前的比赛轮次*/
/*
注:因为算法已经被验证对任何一个选手数目,总会有一个解决方案,所以这里不怕会有死循环
*/
while (matchCount <= players/2) {
c = combinations + 1;
while (c > combinations) {
c = startC;
/* 查找下一个可以加入当前轮次的对战组 */
while ((c <= combinations) &&
((round_set & (1 << cList[c].one)) ||
(round_set & (1 << cList[c].two)) ||
(cUsed[c])
)
)
c++;
if (c > combinations) {
/* 没有找到合适的对战组,故回 */
do {
mList[matchCount] = 0;
matchCount--;
index--;
round_set &= ~(1 << cList[mList[matchCount]].one);
round_set &= ~(1 << cList[mList[matchCount]].two);
cUsed[mList[matchCount]] = FALSE;
tourn[index].one = 0;
tourn[index].two = 0;
/*cList:已经使用的组,mList:合适的对战组*/
} while (cList[mList[matchCount] ].one !=
cList[mList[matchCount]+1].one);
startC = mList[matchCount] + 1;
}
}
/* 找到一个合适的对战组,并放到当前比赛轮次中
*/
tourn[index] = cList[c];
totalChecks++;
/*动态显示进度*/
if ((totalChecks % 1000) == 0) fprintf( stdout, "%d/033A/n", totalChecks );
cUsed[c] = TRUE;
mList[matchCount] = c;
startC = 1;
round_set |= (1 << cList[c].one);
round_set |= (1 << cList[c].two);
index++;
matchCount++;
}
/* 进入下一轮比赛的安排 */
roundCount++;
}
fprintf( stdout, " " );
ShowSchedule(flag);
if(flag==0)
printf("按回车自动演示下一个选手数目");
else printf("奇数情况,遇到虚拟选手%d作为轮空处理!/n按回车自动演示下一个选手数目",players);
getchar();
players += 2;
}
}
main()
{
char c;
while(true)
{
printf("按e演示偶数,q退出,其它键演示奇数");
scanf("%c",&c);
if(c=='e' || c=='E')
doSchedule(0);
else if (c=='y'||c=='Y')break;
else doSchedule(1);
}
}
运行效果:
3.参考文章 http://www.devenezia.com/downloads/round-robin/