运动员赛程安排问题

运动员赛程安排问题

题目要求

有N个运动员进行单循环赛,要求每个运动员都要和其他所有运动员进行一次比赛。 如果为N偶数,则需要在N-1天内结束;如果N为奇数,则需要在N天内结束。

前言

一些网站也有类似的OJ题目,但是都只要求解决当N为2的n次幂的情况(下文都直接简称2的n次幂条件)。所以,如果需要安排的是任意N个运动员的赛程,那么之前的适用于n次幂条件的算法就不能满足要求了。于是,我查看了许多博客,终于从下面这篇博客中找到了解决这个问题的思路。

博客链接,由此入

上面博客(下文都直接简称参考博客)里面的思路十分巧妙,并且提供了伪代码。我在看了他的博客之后就按照伪代码将程序实现了一遍,但结果并不正确。于是我对博主的代码进行了一些修改,终于得出了正确的结果。于是也就有了现在这篇博客的诞生。

下面即将进入正题,讲讲如何解决这个题目。(内容多有省略,希望读者先查阅一些相关资料再往下阅读,最好能把上面参考的那篇参考博客也仔细看一遍)

题目分析

这个题目实质上就是让我们填满下面这个行数为N,列数暂时不确定的矩阵。

1578187623004

用左边第一列的1 2 3 4 表示运动员编号,之后的每一列表示当天的赛程。我们所要做的事情就是把这个表格中空白的地方填满。

不难得出(全排列的知识加上比赛要在N-1或者N天快速结束)该矩阵具有下面两个特性:

  1. 行之间不能有重复数字
  2. 列之间不能有重复数字
  3. 当天对称性质,某一天运动员A和B比赛,那么B一定当天也和A比赛

那么如何填满这个矩阵呢?

起初我第一次看见这个题目,我就想着全排列然后把行、列重复的排列去除掉,剩下的就是满足条件的矩阵了。理论上是可行的,我也按着这个思路写出了代码,结果是正确的,但无奈复杂度过高,N达到16时就已经算不出来了(16! = 20,922,789,888,000‬)

那么如何填满这个矩阵才是最高效的呢?

从参考博客里我得到了思路。

用到了一个很简单的思想----对称!

先看N=2时的矩阵和N=4时的矩阵

1578189018529 1578189043769

他们都关于红线部分对称。 ???哪里对称了?完全没看出来啊。别急,这里的对称并不常规意义上的对称,且来看。在N=4的图中,我把前两行的所有数字都加上2,然后拷贝到第3,4行,再把超过4的数字做适当变换(因为并不存在5号,6号运动员,所以怎么变换都可以,只要结果不冲突就可以),这里可以直接这么变换:第二天1号和3号比,那么3号也是和1号比赛呀;第三天2号和3号比,那么3号也是和2号比赛呀;就这样一个一个地把不存在的运动员进行补充变换,就可以把表填满了。

1578189317345

进行简单的思考就可以得到以下结论,当满足2的n次幂条件时,赛程表一定是一个N*N大小的矩阵。(因为整个矩阵的右半部分一定也与左半部分“对称”)

这是思考时所经历的过程,但程序实现时却不用这么走一遍,程序的写法是很简洁的。只要把左上角拷贝到右下角,右上角拷贝到左下角,递归实现即可。后面我会给出相关代码。

上面我讨论的都是满足2的n次幂的情况。

那么如果N为任意偶数应该怎么解决呢?

我推荐去看一下参考博客的思路方法。

但这里我仍然把思路再梳理一遍。开始。

首先为了求出N=6的赛程表,我们先得到下面这个最初矩阵

1578190330489

接下来,把矩阵上下对半分,上面3行所有数字加上3拷贝到下半矩阵

1578190630653

绿色的为不存在的运动员,画红圈圈的为当天冲突的运动员(当天已经比过一次了)。这两种类型的运动员没有办法必须进行变换。如何变换?就让他们两两配对即可(1号--4号,2号--5号,3号--6号)。

最终就得出部分的赛程表

1578190825460

革命尚未完成,右边还有剩余的空格。

1578191079380

蓝色的数字是怎么填出来的呢?

根据推理,每一行的数字不能重复,那么红色数字就是每一行所能填入的数字。但是,又为了保持着每一列数字不能重复的特性,就需要使用一个循环队列【4 5 6 4 5 6】。(推荐去看参考博客)

填了蓝色数字之后,再根据“当天对称性质”,整个表也就填完了。如下图

1578191722692

上面做法实际上告诉了我们: 当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

蓝色部分的状态实际上并没有做什么实际的事情,就如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两个成员变量来表示实际的有效矩阵大小)

重要的是思想

posted @ 2020-01-05 11:41  virgil_devil  阅读(495)  评论(0编辑  收藏  举报