初衷
在教材上看到这个问题的时候,对于奇数的处理百思不得其解,然而网上的答案要么就是n=2k的情况,要么就是自己根本都没有理解,给你讲了一大堆,各种情况,很麻烦,甚至有些是错的误人子弟。所以写下这篇思路,分享给各位。其实这个问题的核心就是分治的治该怎么去构造的问题。
问题
设有N个运动员要进行网球循环赛,设计一个满足以下要求的比赛日程表
(1)每个选手必须与其他n-1个选手各赛一次
(2)每个选手一天只能赛一次
(3)当n 是偶数,循环赛进行n-1天,当n是奇数,循环赛进行n天。
算法分析
- 我们采用分治法,先算出n/2的情况,然后进行合并,构造出n的情况。难点就在于构造过程,设第i个选手第j天比赛的队员为A[i][j]。
- 若m = n/2为偶数,这时候我们知道偶数人数已经算出了前m个队员的前passed_days天(对于偶数而言是m-1,对于奇数是m)的比赛情况,我们怎么构造呢?
- 先横向构造,也就是构造出后m个队员在前m-1天的比赛情况,那么为了保证不重复我们采用递增的构造方式,让i+m号选手与比A[i][j]大m的选手比赛,也是是说
- A[i + m][j] = A[i][j] + m; (1≤i≤m,1≤j≤passed_days,i代表队员,j代表当前比赛的天数)
- 可以看出来一定不会重复,因为前m个队员之前从未与后m个队员比赛过,然后A[i][j]彼此又是互不相同的,所以A[i+m]彼此也一定不相同
- 再纵向构造,设n个队员比赛所需总天数为days(对于偶数而言是n-1,对于奇数是n),也就是说构造n个队员在后(days - passed_days)天的比赛情况,同样为了保证不重复我们也采用增量构造,
- passed_days +1≤j≤days,
- rvalue = (count + i-1)%m + m+1;//保证i队员与后面的队员(rvalue必然大于m)比赛,这样就与前面passed_days天的比赛不重复
- count为增量初始值为0,j每加1,count++
- A[i][j] = rvalue;
- A[rvalue][j] = i; //因为是两两比赛,后面m对用中的与之对应个队员直接构造出来
- 需要注意的是纵向构造的时候我们先构造A[1][j]也就是说先保证第一个队员在(days - passed_days)比赛的队员肯定与之前(passed_days)是不同的,那么由于i也是递增的所以,A[i][j]彼此之间必定也是互不相同的!
- 先横向构造,也就是构造出后m个队员在前m-1天的比赛情况,那么为了保证不重复我们采用递增的构造方式,让i+m号选手与比A[i][j]大m的选手比赛,也是是说
- 举个栗子:假设n=4.
先计算n/2 = 2,我们知道A[1][1] = 2,A[2][1] = 1(偶数比赛只有一天)
1 2(队员编号)
2 1(第一天)
接下来我们构造,n=4,此时days = 3,passed_days = 1,m=2
横向构造:
1 2 3 4
2 1 4 3
纵向构造A[1][j]:
1 2 3 4
2 1 4 3
3 1
4 1
接着纵向构造A[2][j]:
1 2 3 4
2 1 4 3
3 4 1 2
4 3 2 1
-
若m = n/2为奇数的话,我们需要特殊处理下
- 由于我们是按照偶数的边界构造的,也就是奇数的时候实际上扩充为了m+1列,那么我们需要把与m+1比赛的队员置0,删掉多余的m+1列
- 其次由于置0,那么横向构造的时候如果A[i][j]=0,说明i队员在j天没有比赛,我们直接让他与i+m号选手比赛(保证与之前的横向构造的增量一致)
-
A[i + m][j] = i; A[i][j] = i + m; - 但是上面也引起了问题就是,我们在纵向构造的时候A[1][passed_days]可能会重复(因为在为0的位置可能填入了m+1的元素,所以我们需要标记一下,如果是奇数的话纵向构造的其实增量+1),只要保证了A[1][j]不重复,后面因为都是增量构造肯定不会重复 r_value = (count + (i - 1) + 1) % m + m + 1;
还是举两个例子:
n=3的构造情况
1.先算n=2
1 2
2 1
2.m=2,passed_days=1,days = 3横向构造
1 2 3 4
2 1 4 3
3.纵向构造A[1][j],j=2,3
1 2 3 4
2 1 4 3
3 1
4 1
4.纵向构造A[2][j],j=2,3
1 2 3 4
2 1 4 3
3 4 1 2
4 3 2 1
5.把扩充的置0,同时删掉多余的第4列
1 2 3
2 1 0
3 0 1
0 3 2
n=6的构造情况
1.首先n/2=3已经算出
2.m=3,passed_days=3,days = 5横向构造A[i][1],即第一天的
1 2 3 4 5 6
2 1 6 5 4 3
3 0 1
0 3 2
3.接着横向构造完所有passed_days天的
1 2 3 4 5 6
2 1 6 5 4 3
3 5 1 6 2 4
4 3 2 1 6 5
4.纵向构造A[1][4],A[1[5],由于m是奇数所以,构造增量加了1即A[1][4] = (0 + (1 - 1) + 1) % 3 + 3 + 1 = 5;
1 2 3 4 5 6
2 1 6 5 4 3
3 5 1 6 2 4
4 3 2 1 6 5
5 1
6 1
5.纵向构造完(由于n是偶数,不需要再进行置0操作)
2 1 6 5 4 3
3 5 1 6 2 4
4 3 2 1 6 5
5 6 4 3 1 2
6 4 5 2 3 1
C++代码
#include <iostream>
#include <iomanip>
#include <cmath>
using namespace std;
const int MAX_NUM = 100;
int A[MAX_NUM+2][MAX_NUM+2];
/* 合并子问题 */
void merge(int n)
{
/*
* n 为偶数时,比赛 n - 1 天
* n 为奇数时,比赛 n 天
*/
int days = n&1 ? n : n-1;
/*
* 中间值,若n为奇数,则使 m = (n / 2) + 1,
* 即,前半部分不小于后半部分
*/
int m = (int)ceil(n / 2.0);
int passed_days = m& 1? m : m - 1; /* 已经安排的天数 */
/*
* 通过前 n/2 的比赛安排,构造后n/2的比赛安排
* 如果 n 为奇数,则会产生一个虚拟选手
*/
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= passed_days; j++)
{
if (A[i][j] != 0) /* 如果 i 号在第 j 天有对手 */
{
/*
* 那么,(i + m) 号在第 j 天的对手为 i号的
* 对手往后数 m 号
*/
A[i + m][j] = A[i][j] + m;
}
else /* 如果 i 号在第 j 天没有对手*/
{
/*
* 那么就让 i 号和 (i + m)号互为对手
*/
A[i + m][j] = i;
A[i][j] = i + m;
}
}
}
int add_one = 0; /*标志子问题是否是奇数,如果是的话构造增量加1 */
if (A[1][passed_days] == m + 1)
add_one = 1;
for (int i = 1; i <= m; i++)
{
for (int j = passed_days + 1, count = 0; j <= days; j++, count++)
{
/*
* 构造i 号在第 j 天的对手
*/
int r_value = (count + (i - 1) + add_one) % m + m + 1;
A[i][j] = r_value;
A[r_value][j] = i;
}
}
if ( n & 1 ) /* 如果 n 为奇数,消除虚拟的选手 */
{
for (int i = 1; i <= 2 * m; i++)
{
for (int j = 1; j <= days; j++)
if (A[i][j] == n + 1)
A[i][j] = 0; /* A[i][j] = 0 ,表示 i 号选手在第 j 天没有比赛 */
}
}
}
/* 分治求解循环赛问题 */
void tournament(int n)
{
if (n <= 1)
return;
else if (n == 2) /* 2 个选手 */
{
A[1][1] = 2;
A[2][1] = 1;
}
else
{
tournament((int)ceil(n / 2.0));
merge(n);
}
}
/* 打印循环赛日程表 */
void show_result(int n)
{
cout << " " << n << "人循环赛" << endl;
int days = n&1 ?n : n-1;
cout.flags(ios::left);
cout << setw(8) << "";
for (int i = 1; i <= n; i++)
cout << setw(2)<<i << "号";
cout << endl;
cout.flags(ios::left);
for (int j = 1; j <= days; j++)
{
cout << "第"<<setw(2)<<j<< "天 ";
for (int i = 1; i <= n; i++)
{
cout << setw(4) << A[i][j];
}
cout << endl;
}
cout << endl;
}
int main()
{
int num;
while(true){
cout << "请输入参赛人数(小于100):(0结束程序)";
cin >> num;
if(num == 0) break;
tournament(num);
show_result(num);
}
return 1;
}
算法复杂度
设n=2k,第i次循环需要计算(2k/2i)2,2≤i≤k,总共的计算次数粗略的表示为 12+22 +...+ 22j + ... + 22(k-1) 等比数列求和为(22k-1)/3,粗略等于22k=n^2。 所以算法时间复杂度为O(n^2).
由于只需要一个数组,所以空间代价为:O(n^2)