初衷
在教材上看到这个问题的时候,对于奇数的处理百思不得其解,然而网上的答案要么就是n=2k的状况,要么就是本身根本都没有理解,给你讲了一大堆,各类状况,很麻烦,甚至有些是错的误人子弟。因此写下这篇思路,分享给各位。其实这个问题的核心就是分治的治该怎么去构造的问题。ios
问题
设有N个运动员要进行网球循环赛,设计一个知足如下要求的比赛日程表算法
(1)每一个选手必须与其余n-1个选手各赛一次数组
(2)每一个选手一天只能赛一次bash
(3)当n 是偶数,循环赛进行n-1天,当n是奇数,循环赛进行n天。ui
算法分析
咱们采用分治法,先算出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]彼此之间一定也是互不相同的!
举个栗子:假设n=4.
先计算n/2 = 2,咱们知道A[1][1] = 2,A[2][1] = 1(偶数比赛只有一天)spa
1 2(队员编号)
2 1(第一天)
复制代码
接下来咱们构造,n=4,此时days = 3,passed_days = 1,m=2设计
横向构造:code
1 2 3 4
2 1 4 3
复制代码
纵向构造A[1][j]:ip
1 2 3 4
2 1 4 3
3 1
4 1
复制代码
接着纵向构造A[2][j]:ci
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
#include
#include
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)<
cout << endl;
cout.flags(ios::left);
for (int j = 1; j <= days; j++)
{
cout << "第"<
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)