一、题目叙述
循环赛日程表问题:
设有n=2k 个运动员要进行网球循环赛。现在要设计一个满足以下要求的比赛日程表:(1)每个选手必须与其他 n-1 个选手各赛一次;(2)每个选手一天只能赛一次;(3)循环赛一共进行 n-1 天。请给出基于分治思想的日程表设计方案,并且编程实现。

二、题目分析
为了分析题目的含义,拿8个运动员举例,对运动员依次编号为1、2、3、......、8,比赛天数为n-1天,依题意,1号运动员要在7天内和其他运动员各比赛一次,且每天只能比赛一场,如果只分析题目的含义,可以简单的用顺次排序的方法制定日程,每个运动员的比赛对象从他的下一位开始依次去比,排满再从头接着排,这样就可以满足题意。具体如下表所示:

现在,我们换一种思路,按照要求,用分治的思想来思考一下这道题的解决思路。我们先拿4个运动员来探寻规律,之后,我们再用8个运动员的规模来推演一遍用分治思想的求解过程。
1、规律探寻
对于4个运动员的场合,按照刚才的设计方法,我们不难得到如下的日程方案:

我们现在换一种方式,我们先把4个人分成2组,每个组里是两名运动员,第一组:1、2;第二组:3、4。我们考虑2个人的日程安排,2个人的时候,比赛是一天,根本不用安排,就让这两名运动员比就行。
第一组:

第二组:

四个人的场合,我们可以把表格看成是一个4 * 4的棋盘,然后将已排好序的2个运动员的日程分别填到棋盘里,我们发现,这样放置后一样是满足题意的。

发现了这个规律,我们就可以考虑运用分治思想了。现在以8个运动员举例,推导一下算法的过程。
思路:
将规模为8的运动员的循环赛日程表问题进行分解,分为2组,第一组:1、2、3、4;第二组:5、6、7、8,得到两个4的运动员的循环赛日程表子问题。分别求解两个子问题,再将得到的子问题的日程安排结果按照上面的摆放方法铺满棋盘,8个运动员的日程表就安排好了。
为了得到规模为4的的运动员的循环赛日程表子问题的答案,对于规模为4的的运动员的循环赛日程表问题,进行分解,分解成2组,说到这里,思路已经明确了,递归分解原问题,每次分为2组,直到长度(运动员数)为1,此为初值,此时就是运动员自己。

我们再来想一个更简单的分治:我们知道,8个运动员的日程表是一个8*8的表格,我们把这个表格看成一个8*8的棋盘,类似于棋盘覆盖,我们直接对它进行切分,分成四块,有了上面的经验,我们知道,左上角和右下角一样,是1到4号运动员的日程,右上角和左下角一样,是5到8号运动员的日程。这样,我们就可以分别对4个子棋盘进行递归求解,直至规模等于1,这是初值。这样分解的好处是省却了合并的操作,所有规模为1的子棋盘填好后,整个问题的解就出来了。



程序清单:
思路一:
cpp
#include <stdio.h>
#include <math.h>
// copy函数:把 row, col 开始的 m*m 棋盘复制到 targetRow, targetCol
void copy(int **table, int m, int row, int col, int targetRow, int targetCol)
{
for(int i = 0; i < m; i++)
{
for(int j = 0; j < m; j++)
{
table[targetRow + i][targetCol + j] = table[row + i][col + j];
}
}
}
// 合并函数:完成两块棋盘拷贝合并
void merge(int **table, int m, int row, int col)
{
// 右下棋盘 = 左上棋盘
copy(table, m, row, col, row+m, col+m);
// 左下棋盘 = 右上棋盘
copy(table, m, row, col+m, row+m, col);
}
void schedule(int **table, int n, int startNumber,int row, int col){
// 把日程表看成一个棋盘
// 递归到初值时,求解,即只有一个运动员,棋盘规模为 n=1 时只有1个格时置入运动员编号
if(n == 1){
table[row][col] = startNumber;
return;
}
int m = n / 2;
// 递归求解左上棋盘
schedule(table, m, startNumber, row, col);
// 递归求解右上棋盘
schedule(table, m, startNumber + m, row, col + m);
// 合并:填充下半部分
merge(table, m, row, col);
}
// 输出打印函数
void printTable(int **table, int n)
{
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
printf("%2d\t",table[i][j]);
}
printf("\n");
}
}
int main()
{
int k, n;
printf("输入k:");
scanf("%d", &k);
printf("\n");
n = pow(2, k); // 运动员数量
// 根据运动员数量生成 n 行 n 列的二维数组
int **table = (int**)malloc(n*sizeof(int*)); // 分配 n 个行指针,拿一个整形指针的值(地址)的大小乘以 n,申请空间
for(int i=0;i<n;i++){
table[i] = (int*)malloc(n*sizeof(int)); // 每行 n 个整形元素,拿一个整形的大小乘以 n,申请空间
}
schedule(table,n,1,0,0);
printTable(table, n); // 调用输出函数输出结果
for(int i=0;i<n;i++){
free(table[i]);
}
free(table);
return 0;
}
思路二:
cpp
#include <stdio.h>
#include <math.h>
// row:子表行开始位置,col:子表列开始位置
void schedule(int **table, int n, int startNumber, int row, int col)
{
// 把日程表看成一个棋盘
// 递归到初值时,即只有一个运动员,棋盘规模为 n=1 时只有1个格时置入运动员编号
if(n == 1)
{
table[row][col] = startNumber;
return;
}else{
int m = n / 2; // 问题规模减半,均分为四个子问题,对应四个子棋盘
// 左上角子棋盘:开始于(row, col),起始运动员编号 startNumber
schedule(table, m, startNumber, row, col);
// 右上子块:列偏移 + m,起始运动员编号 startNumber + m(startNumber 改变)
schedule(table, m, startNumber + m, row, col + m);
// 左下角子棋盘:行偏移 + m,起始运动员编号 startNumber + m(startNumber 改变)
schedule(table, m, startNumber + m, row + m, col);
// 右下角子棋盘:行列都偏移 + m,起始运动员编号 startNumber
schedule(table, m, startNumber, row + m, col + m);
}
}
// 输出打印函数
void printTable(int **table, int n)
{
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
printf("%2d\t",table[i][j]);
}
printf("\n");
}
}
int main()
{
int k, n;
printf("输入k:");
scanf("%d", &k);
printf("\n");
n = pow(2, k); // 运动员数量
// 根据运动员数量生成 n 行 n 列的二维数组
int **table = (int**)malloc(n*sizeof(int*)); // 分配 n 个行指针,拿一个整形指针的值(地址)的大小乘以 n,申请空间
for(int i=0;i<n;i++){
table[i] = (int*)malloc(n*sizeof(int)); // 每行 n 个整形元素,拿一个整形的大小乘以 n,申请空间
}
schedule(table,n,1,0,0);
printTable(table, n); // 调用输出函数输出结果
for(int i=0;i<n;i++){
free(table[i]);
}
free(table);
return 0;
}