发布于 

循环赛赛程安排

一、问题重述

设有n个运动员要进行网球循环赛。设计一个满足下列条件的比赛日程表:

  • 每个选手必须与其他n-1个选手各赛一次;
  • 每个选手一天只能赛一次;
  • 当n是偶数时,循环赛进行n-1天。
  • 当n是奇数时,循环赛进行n天。

二、问题分析

1.当n是2的次幂时

$ n=2^k,k=1,2,3,4… $时,此时问题比较简单。按照==分治==的策略,可将所有参赛的选手分为两部分,$ n=2k $个选手的比赛日程表可以通过为 $ n/2=2k-1 $ 个选手设计的比赛日程表来决定。递归地执行这种分割,直到只剩下 2 个选手时,比赛日程表的制定就变得很简单:只要让这 2 个选手进行比赛就可以了。再逐步合并子问题的解即可得到原问题的解。
示意图如下:

1.1

此时的分治算法如下所示:

void tourna(int n) //基本的分治算法
{
if(n==1){a[0][0]=1;return;}
tourna(n/2); //分治
copy(n); //合并
}

void copy(int n)
{
int m=n/2;
for(int i=0;i<m;i++)
for(int j=0;j<m;j++){
//由左上角小块的值算出对应的右上角小块的值
a[i][j+m]=a[i][j]+m;
//由右上角小块的值算出对应的左下角小块的值
a[i+m][j]=a[i][j+m];
//由左上角小块的值算出对应的右下角小块的值
a[i+m][j+m]=a[i][j];
}
}

我们用a[i][j]表示第i支队伍在第j天遇到的对手。

2.当n不是2的次幂时

下面讨论当n不是2的次幂时的情况。
我们发现当n为奇数时,每天必定有一支队伍轮空。此时我们==假定还有一只不存在的队伍与轮空的队伍比赛==,将我们的奇偶数情况的模型统一。此时n的赛程表与偶数n+1时的赛程表是相似的。
比如,当n=4时
| 0 | 1 | 2 | 3 |
|—-|—-|—-|—-|
| 1 | 0 | 3 | 2 |
| 2 | 3 | 0 | 1 |
| 3 | 2 | 1 | 0 |

当n=3时
| 0 | 1 | 2 | / |
|—-|—-|—-|—-|
| 1 | 0 | / | 2 |
| 2 | / | 0 | 1 |
| / | 2 | 1 | 0 |

(删去最后一行)其中“/”表示轮空。

综上,当遇到n为奇数的情况,我们便可以转化为偶数来考虑。

接下来我们遇到问题的难点==矩阵的合并==
当n/2为偶数时,合并比较容易,就像$ n=2^k $那样。
下面我们来考虑n/2为奇数的情况。
此时合并的过程我参考了猪一戒的博客

我们考虑当n=6时的情况。
我们先将6个人分成2组,每组3个人([0,1,2],[3,4,5]),然后发现3是个奇数,然后在每组中+1个虚拟人:X和Y;这样,每组就变成了4个人,然后将这4个人在除以2,我们就得到了一个两两组合的小的组。

首先来看[0,1]; [2,x]

0 1
1 0
2 x
x 2

将这两组合起来:

0 1 2 x
1 0 x 2
2 x 0 1
x 2 1 0

这里要得到3个选手的比赛安排,所以,我们将假想的X去掉,并将它的位置以/代替:

0 1 2 /
1 0 / 2
2 / 0 1

然后我们也按照这个规律,安排[3,4,5]的日程,得到表格
| 3 | 4 | 5 | / |
|—-|—-|—-|—-|
| 4 | 3 | / | 5 |
| 5 | / | 3 | 4 |

我们得到了两个3x4的矩阵(其中第一列表示每个队伍,实际上只有三天),我们最终想得到6*6(其中第一列表示每个队伍,实际上只有五天)的矩阵。

我们先将上面两个矩阵合并

0 1 2 /
1 0 / 2
2 / 0 1
3 4 5 /
4 3 / 5
5 / 3 4

前三天的比赛已经基本排完了,我们只需要在斜杠/的地方填上相应的比赛。很显然可以让每天轮空的两支队伍比赛。(在程序中没有斜杠表示,还是假想的队伍,方便进行监测)

0 1 2 3
1 0 4 2
2 5 0 1
3 4 5 0
4 3 1 5
5 2 3 4

上面的矩阵中[0,1,2]和[3,4,5]组内已经比完了,组间比了一次,剩下的只需要轮换两次即可得到后两天的比赛情况。

0 1 2 3 4 5
1 0 4 2 5 3
2 5 0 1 3 4
3 4 5 0 2 1
4 3 1 5 0 2
5 2 3 4 1 0

于是我们便可以得到两个奇数矩阵的合并情况。

在程序中,我们的思路不是严格意义上的所谓的“合并”,而是“扩展”。比如对于“合并2个3x4的矩阵”,我们是“将1个3x4的矩阵扩展为6x6的矩阵”(因为这两个3x4的矩阵规格相同,排序顺序一样,所以只需要做一遍)

三、代码展示

(代码里有一些打印行号的printf语句,不知道为啥一去掉就过不了编译,所以没有删掉。。)

#include<stdio.h>
#include<stdlib.h>
#include"Check.cpp"

void copy(int n, int **a)//偶数情况
{
//printf("当前行号%05d\n",__LINE__);
int m=n/2;
for(int i=0;i<m;i++){
for(int j=0;j<m;j++){
//由左上角小块的值算出对应的右上角小块的值
a[i][j+m]=a[i][j]+m;
//由右上角小块的值算出对应的左下角小块的值
a[i+m][j]=a[i][j+m];
//由左上角小块的值算出对应的右下角小块的值
a[i+m][j+m]=a[i][j];
}
}
}

void copyodd(int n, int **a) // n/2 为奇数时的合并算法
{
printf("当前行号%05d\n",__LINE__);
int m=n/2;
int b[m];
for(int i=0;i<m;i++){
b[i]=m+i;
b[m+i]=b[i];
}
for(int i=0;i<m;i++){
//由左上角小块的值算出相应的左下角小块的值
for(int j=0;j<m+1;j++){
if(a[i][j]>=m){
a[i][j]=b[i];
a[m+i][j]=(b[i]+m)%n;
} else a[m+i][j]=a[i][j]+m;
}
//由左上角小块的值算出相应的右上角和右下角小块的值
for(int j=1;j<m;j++){
a[i][m+j]=b[i+j];
a[b[i+j]][m+j]=i;
}
}
}



void merge(int n, int **a) //合并算法
{
if((n/2)>1 && (n/2)%2 == 1) copyodd(n,a); //n/2 为奇数时,注意是 (n/2)%2 == 1,n别忘了/2
else copy(n,a);
}


void tournament(int n, int **a) //循环赛算法
{
printf("当前行号%05d\n",__LINE__);
if(n==1){a[0][0]=0;return;}
if(n%2 == 1) {tournament(n+1,a);return;} //n为奇数,分治
tournament(n/2,a); //n为偶数,分治
merge(n,a); //合并
}


main()
{
int n;
scanf("%d",&n);

//创建数组
int **a;
a = (int**)malloc(sizeof(int*)*n);

if(n%2==1){
for(int i=0; i<n+1; i++) a[i] = (int*)malloc(sizeof(int)*(n+1));
} else {
for(int i=0; i<n; i++) a[i] = (int*)malloc(sizeof(int)*n);
}

//生成循坏赛矩阵
tournament(n,a);

//打印
printf("当前行号:%05d\n",__LINE__);
for(int i=0; i<n; i++){
for(int j=1; j<(n%2 == 1 ? n+1 : n); j++){
if(a[i][j]<n) printf("%d ",a[i][j]);
else printf("x ");
}
printf("\n");
}

//检验程序
if(Check(a,n)==1) printf("This gametable is availuable.\n");
else printf("This gametable is unavailuable.\n");
if(n%2 ==1){
for(int i=0; i<n; i++) free(a[i]);
} else {
for(int i=0; i<n+1; i++) free(a[i]);
}
free(a);
}

四、测试程序

测试程序对我们生成的矩阵进行检验。从两个角度进行。

  • 每个队伍都要和其他队伍进行一场比赛
  • 每个队伍每天仅进行一场比赛

测试程序代码如下

int Check(int **a, int n)
{
int column;
if(n%2==1) column=n+1;
else column=n;
int flag=0;
int check=1;//check为0说明不符合条件,停止检验。
//检验每个队伍都与其他队伍比赛
for(int i=0; i<n&&check==1; i++){
for(int k=0; k<n&&check==1; k++){
flag=0;
for(int j=0;j<column&&flag==0;j++){
if(a[i][j] == k) flag=1;
}
if(flag==0) check=0;
}
}
//检验某天是否有队伍重复比赛
int times[n];
for(int j=1; j<column&&check==1; j++){
for(int w=0; w<n; w++) times[w]=0;
for(int i=0; i<n&&check==1; i++){
times[a[i][j]]++;
if(times[a[i][j]]>=2) check==0;
}
}
if(check==1) return 1;
else return 0;
}

五、实验结果

  • 当n=6时

1.2

  • 当n=9时

1.3

测试结果均正确。

参考文献
[1]王民川,田永轩.分治法在循环赛日程表设计中的应用[J].光盘技术,2009(05):45-46.