循环赛日程安排(构造、分治)

问题描述

N支队伍进行比赛,要求:

  • 任意两支队伍交且仅交过一次手
  • 每天每支队伍至多打一场比赛
  • 比赛尽快完成

问题等价于构造一个N×M矩阵,表示N支队伍比赛M天,第一列固定为1到N,表示每支队伍的编号,第i行表示第i支队伍每天的对手,-1表示没有对手不比赛。
第i行第j列的数字表示第i队在第j天跟a[i][j]打比赛,那么a[a[i][j]][j]=i必然成立。

先说结论

如果N为2的幂,则比赛日程为N×N的矩阵,也就是比赛在N天内完成,这是最完美的方法
如果N为偶数,则比赛日程也为N×N的矩阵
如果N为奇数,则比赛日程为N×(N)的矩阵

当N为2的幂

当N为2的幂,比赛日程为N阶方阵。
分治问题为N/2,得到一个N/2的方阵。相当于大方阵均分成四份,求出来左上角那一份来。把那一份进行一些平移变换复制到另外三份。

from pprint import pprint


def go(x):
    if x == 2:
        return [[0, 1], [1, 0]]
    half = x // 2
    a = go(half)
    t = [[0] * x for _ in range(x)]
    for i in range(half):
        for j in range(half):
            t[i][j] = a[i][j]
            t[i + half][j] = a[i][j] + half
            t[i][j + half] = a[i][j] + half
            t[i + half][j + half] = a[i][j]
    return t

N = 2 ** 3
ans = go(N)
pprint(ans)

当N不是2的幂

当N不是2的幂时,分类讨论:

  • 当N为偶数,直接构造N阶方阵
  • 当N为奇数,直接构造N+1阶的方阵,然后删掉最后一行

所以将一切情况都转化成了偶数阶方阵的构造。下面分类讨论偶数阶方阵的构造:
记half=N/2

  • 当half为偶数时,直接计算half阶方阵,构成左上角,完成了1/4的任务,然后平移到另外三个位置
  • 当half为奇数时,分两部分完成,左半部分和右半部分。左半部分为前half+1天的安排,右半部分为后half-1天的安排。
    左上角相当于构造half+1阶(half+1必为偶数)的方阵,右上角相当于循环移位操作。
    实际上,只要知道前half队的比赛,就知道了后half队的比赛

下面看一个11个队参加方阵的例子

[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, -1],
 [1, 0, 4, 2, 5, 3, 7, 6, 10, 8, -1, 9],
 [2, 5, 0, 1, 3, 4, 8, -1, 6, 7, 9, 10],
 [3, 4, 5, 0, 2, 1, 9, 10, -1, 6, 8, 7],
 [4, 3, 1, 5, 0, 2, 10, 9, 7, -1, 6, 8],
 [5, 2, 3, 4, 1, 0, -1, 8, 9, 10, 7, 6],
 [6, 7, 8, 9, 10, -1, 0, 1, 2, 3, 4, 5],
 [7, 6, 10, 8, -1, 9, 1, 0, 4, 2, 5, 3],
 [8, -1, 6, 7, 9, 10, 2, 5, 0, 1, 3, 4],
 [9, 10, -1, 6, 8, 7, 3, 4, 5, 0, 2, 1],
 [10, 9, 7, -1, 6, 8, 4, 3, 1, 5, 0, 2]]
from pprint import pprint


def check(a):
    # 检查答案是否正确
    print('{}行{}列'.format(len(a), len(a[0])))
    for i in range(0, len(a)):
        for j in range(1, len(a[0])):
            if a[i][j] != -1:
                if i != a[a[i][j]][j]:
                    print("第{}天第{}人错误".format(j, a[i][j]))
                    return
    print("完全正确")


def f(N):  # N一定为偶数
    if N == 2:
        return [[0, 1], [1, 0]]
    half = N // 2
    a = go(half)
    if half % 2 == 0:
        t = [[-1] * N for _ in range(N)]
        for i in range(half):
            for j in range(half):
                t[i][j] = a[i][j]
                t[i + half][j] = a[i][j] + half
                t[i][j + half] = a[i][j] + half
                t[i + half][j + half] = a[i][j]
        return t
    else:
        t = [[-1] * N for _ in range(N)]
        for i in range(half):
            for j in range(half + 1):  # 对于前half+1项
                t[i][j] = a[i][j]
                t[i + half][j] = (a[i][j] + half) % N
            t[i][half - i] = half + i
            t[half + i][half - i] = i
            for j in range(half + 1, N):  # 对于后half-1项
                t[i][j] = (i + j) % half + half
                t[t[i][j]][j] = i
        return t


def go(N):
    if N % 2 == 0:
        a = f(N)
        return a
    else:
        a = f(N + 1)  # 需要去除最下面一行和最左面一列
        t = [[a[i][j] if a[i][j] != N else -1 for j in range(N + 1)] for i in range(N)]
        return t


N = 5
ans = go(N)
check(ans)
pprint(ans)


参考资料

李健勇, 李晔, 刘艳红,等. 任意数量选手的循环赛赛程分治算法[J]. 郑州轻工业学院学报自然科学版, 2007, 22(4):122-125.

原文地址:https://www.cnblogs.com/weiyinfu/p/6783379.html