运动员赛程安排问题
运动员赛程安排问题
题目要求
有N个运动员进行单循环赛,要求每个运动员都要和其他所有运动员进行一次比赛。 如果为N偶数,则需要在N-1天内结束;如果N为奇数,则需要在N天内结束。
前言
一些网站也有类似的OJ题目,但是都只要求解决当N为2的n次幂的情况(下文都直接简称2的n次幂条件)。所以,如果需要安排的是任意N个运动员的赛程,那么之前的适用于n次幂条件的算法就不能满足要求了。于是,我查看了许多博客,终于从下面这篇博客中找到了解决这个问题的思路。
上面博客(下文都直接简称参考博客)里面的思路十分巧妙,并且提供了伪代码。我在看了他的博客之后就按照伪代码将程序实现了一遍,但结果并不正确。于是我对博主的代码进行了一些修改,终于得出了正确的结果。于是也就有了现在这篇博客的诞生。
下面即将进入正题,讲讲如何解决这个题目。(内容多有省略,希望读者先查阅一些相关资料再往下阅读,最好能把上面参考的那篇参考博客也仔细看一遍)
题目分析
这个题目实质上就是让我们填满下面这个行数为N,列数暂时不确定的矩阵。
![1578187623004](http://devilvirgil.top/1578187623004.png)
用左边第一列的1 2 3 4 表示运动员编号,之后的每一列表示当天的赛程。我们所要做的事情就是把这个表格中空白的地方填满。
不难得出(全排列的知识加上比赛要在N-1或者N天快速结束)该矩阵具有下面两个特性:
- 行之间不能有重复数字
- 列之间不能有重复数字
- 当天对称性质,某一天运动员A和B比赛,那么B一定当天也和A比赛
那么如何填满这个矩阵呢?
起初我第一次看见这个题目,我就想着全排列然后把行、列重复的排列去除掉,剩下的就是满足条件的矩阵了。理论上是可行的,我也按着这个思路写出了代码,结果是正确的,但无奈复杂度过高,N达到16时就已经算不出来了(16! = 20,922,789,888,000)
那么如何填满这个矩阵才是最高效的呢?
从参考博客里我得到了思路。
用到了一个很简单的思想----对称!
先看N=2时的矩阵和N=4时的矩阵
![1578189018529](http://devilvirgil.top/1578189018529.png)
![1578189043769](http://devilvirgil.top/1578189043769.png)
他们都关于红线部分对称。 ???哪里对称了?完全没看出来啊。别急,这里的对称并不常规意义上的对称,且来看。在N=4的图中,我把前两行的所有数字都加上2,然后拷贝到第3,4行,再把超过4的数字做适当变换(因为并不存在5号,6号运动员,所以怎么变换都可以,只要结果不冲突就可以),这里可以直接这么变换:第二天1号和3号比,那么3号也是和1号比赛呀;第三天2号和3号比,那么3号也是和2号比赛呀;就这样一个一个地把不存在的运动员进行补充变换,就可以把表填满了。
![1578189317345](http://devilvirgil.top/1578189317345.png)
进行简单的思考就可以得到以下结论,当满足2的n次幂条件时,赛程表一定是一个N*N大小的矩阵。(因为整个矩阵的右半部分一定也与左半部分“对称”)
这是思考时所经历的过程,但程序实现时却不用这么走一遍,程序的写法是很简洁的。只要把左上角拷贝到右下角,右上角拷贝到左下角,递归实现即可。后面我会给出相关代码。
上面我讨论的都是满足2的n次幂的情况。
那么如果N为任意偶数应该怎么解决呢?
我推荐去看一下参考博客的思路方法。
但这里我仍然把思路再梳理一遍。开始。
首先为了求出N=6的赛程表,我们先得到下面这个最初矩阵
![1578190330489](http://devilvirgil.top/1578190350489.png)
接下来,把矩阵上下对半分,上面3行所有数字加上3拷贝到下半矩阵
![1578190630653](http://devilvirgil.top/1578190630653.png)
绿色的为不存在的运动员,画红圈圈的为当天冲突的运动员(当天已经比过一次了)。这两种类型的运动员没有办法必须进行变换。如何变换?就让他们两两配对即可(1号--4号,2号--5号,3号--6号)。
最终就得出部分的赛程表
![1578190825460](http://devilvirgil.top/1578190825460.png)
革命尚未完成,右边还有剩余的空格。
![1578191079380](http://devilvirgil.top/1578191079380.png)
蓝色的数字是怎么填出来的呢?
根据推理,每一行的数字不能重复,那么红色数字就是每一行所能填入的数字。但是,又为了保持着每一列数字不能重复的特性,就需要使用一个循环队列【4 5 6 4 5 6】。(推荐去看参考博客)
填了蓝色数字之后,再根据“当天对称性质”,整个表也就填完了。如下图
![1578191722692](http://devilvirgil.top/1578191722692.png)
上面做法实际上告诉了我们: 当N/2的矩阵已知时,就必定能得出N的矩阵
因此,最佳做法是使用递归。
这里举一个例子,当N等于26时的计算过程
26的矩阵我不知道,那26/2=13的矩阵呢?不知道
那26/2=13的矩阵呢?不知道
那13+1=14的矩阵呢?不知道
那14/2=7的矩阵呢?不知道
那7+1=8的矩阵呢?似乎知道,不就是2的n次幂条件吗?但是还是继续问问吧
那8/2=4的矩阵呢?知道!
也可以从4往26思考,4我会求,那么8我也能求得出,接下来我就能根据“对称”求14,求得14那么再根据“对称”求26,就可以了。
![1578192689085](http://devilvirgil.top/1578192689085.png)
蓝色部分的状态实际上并没有做什么实际的事情,就如N=6时的3一样,只是作为一个分割的作用。
似乎还有一个遗留的问题,当N为奇数时怎么办?
这个简单,只要求出N+1的矩阵,然后把N+1的数全部置为0,表示当天不比赛即可。
呼~,现在任意数量的都可解了,后面附上代码。
2的n次幂条件解法(代码)
#include <iostream>
#include <vector>
using namespace std;
vector<vector<int>> matrix;
void recur(int x1, int y1, int x2, int y2)
{
int len_x = x2 - x1 + 1;
int len_y = y2 - y1 + 1;
if (x1==x2||y1==y2)return;
recur(x1, y1, (x1+x2) / 2, (y1+y2) / 2);
recur((x1 + x2) / 2 + 1, y1, x2, (y1 + y2) / 2);
//左上角拷贝到右下角
for (int i = y1; i < y1+len_y/2; i++)
{
for (int j = x1; j < x1+len_x/2; j++)
{
matrix[i + len_y / 2][j + len_x / 2] = matrix[i][j] ;
//print_matrix();
}
}
//右上角拷贝到左下角
for (int i = y1; i < y1 + len_y/2 ; i++)
{
for (int j = x1 + len_x/2; j < x1+len_x; j++)
{
matrix[i + len_y/2][j - len_x/2] = matrix[i][j] ;
//print_matrix();
}
}
}
void update_matrix(int N)
{
//创建(N+1)*(N+1)数组
//这里第0行和第0列为了方便理解,不使用
matrix.resize(N + 1);
for (int i = 0; i < matrix.size(); i++) {
matrix[i].resize(N + 1);
}
//{{填写必要元素
//0 0 0 0 0
//0 1 2 3 4
//0 0 0 0 0
//0 0 0 0 0
//0 0 0 0 0
for (int i = 1; i <= N; i++)
{
matrix[1][i] = i;
}
//0 0 0 0 0
//0 1 2 3 4
//0 2 0 0 0
//0 3 0 0 0
//0 4 0 0 0
for (int i = 1; i <= N; i++)
{
matrix[i][1] = i;
}
recur(1, 1, N, N);//初始化赛程表
}
int main()
{
update_matrix(8);//N=8
return 0;
}
任意数量情况解法(代码)
#include <iostream>
#include <vector>
#include <windows.h>
using namespace std;
class Table {
public:
vector<vector<int>> matrix;
const int width = 20;
int x2, y2;
void UpdateTable(int n)
{
if (n < 2)return;
matrix.clear();
matrix.resize(n + 1);
for (int i = 0; i < n + 1; i++)
matrix[i].resize(n + 1);
matrix[0][0] = 1;
matrix[0][1] = 2;
matrix[1][0] = 2;
matrix[1][1] = 1;
if (isodd(n))
{
tournament(n + 1);
for (int i = 0; i <=n; i++)
{
for (int j = 1; j <= n; j++)
{
if (matrix[i][j] >= n)
{
matrix[i][j] = 0;
}
}
}
x2 = n+1;
y2 = n;
print_matrix();
}
else {
tournament(n);
x2 = n;
y2 = n;
print_matrix();
}
}
private:
bool isodd(int n)
{
return n % 2 != 0;
}
void copy_odd(int n)//6
{
vector<int> b;
b.resize(matrix[0].size());
int m = n / 2;
for (int i = 0; i < m; i++)//4 5 6 4 5 6
{
b[i] = m + i + 1;
}
for (int i = 0; i < m; i++)//4 5 6 4 5 6
{
b[i + m] = m + i + 1;
}
//上半部分加上m拷贝到下半部分
for (int i = 0; i < m; i++)
{
for (int j = 0; j < m + 1; j++)
{
matrix[i + m][j] = matrix[i][j] + m;
}
}
//对上半部分编号大于m的运动员进行处理,同时处理下半部分大于n的运动员
//如果大于m,那么下半部分加上m之后一定会大于n
for (int i = 0; i < m; i++)
{
for (int j = 0; j < m + 1; j++)
{
if (matrix[i][j] > m)
{
matrix[i][j] = b[i];
matrix[b[i] - 1][j] = i + 1;
}
}
}
//对右边空白的部分进行处理,以循环数组的方式
for (int i = 0; i < m; i++)
{
for (int j = 0; j < m - 1; j++)
{
matrix[i][j + m + 1] = b[i + j + 1];
matrix[b[i + j + 1] - 1][j + m + 1] = i + 1;
}
}
}
//上半部分+m后拷贝到下半部分
void copy_ever(int n)
{
int m = n / 2;
for (int i = 0; i < m; i++)
{
for (int j = 0; j < m; j++)
{
matrix[i][j + m] = matrix[i][j] + m;
matrix[i + m][j] = matrix[i][j + m];
matrix[i + m][j + m] = matrix[i][j];
}
}
}
void makecopy(int n)
{
int m = n / 2;
if (isodd(m))
{
copy_odd(n);
}
else {
copy_ever(n);
}
}
void tournament(int n)
{
if (n == 1)
{
matrix[0][0] = 1;
return;
}
if (isodd(n))
{
tournament(n + 1);
}
else {
tournament(n / 2);
}
//必须加上这一句,奇数时不做任何事情
if (isodd(n))return;
makecopy(n);
}
void print_matrix()
{
system("cls");
const int width = 3;
for (int i = 0; i < matrix.size(); i++)
{
for (int j = 0; j < matrix.size(); j++)
{
cout.width(width);
cout << matrix[i][j] << " ";
}
cout.width(width);
cout << endl;
}
}
};
总结
尚存在的问题
得到的结果,但是程序中的vector数组的大小还存在着一些小问题,封装时并不是很友好。(代码中使用x2,y2两个成员变量来表示实际的有效矩阵大小)
重要的是思想