网球比赛安排

44

球员和网球场都是有限的。每一轮比赛中,最多只能有与场地数量相等的比赛进行。 没有人可以连续参加两轮比赛。每个人都要与其他人进行比赛。 生成尽可能少的轮次下的比赛安排。(由于每个人必须在两轮比赛之间休息,因此可能出现没有比赛的轮次。) 5名球员和2个场地的输出结果如下:

 |  1   2   3   4   5
-|------------------- 
2|  1   - 
3|  5   3   - 
4|  7   9   1   - 
5|  3   7   9   5   -
在这个输出中,列和行是选手编号,矩阵内的数字是这两个选手竞争的回合数。
问题是要找到一种算法,在合理的时间内处理更大规模的实例。我们被要求用Prolog完成,但(伪)代码可以使用任何语言。
我的第一个尝试是贪婪算法,但那会得出太多回合的结果。 然后我建议使用迭代加深深度优先搜索,我的一个朋友实现了它,但即使在7名选手的实例上也需要太多时间。
(这是一个旧的考试问题。我问过的人都没有解决方案。)

我和我交谈的人都没有找到这个问题的解决方案,这听起来很奇怪,因为有一个非常简单的解决方案,但当然不是最好的。 - Andrey
在你的例子中,为什么没有人在第2、4、6、8轮玩? - Dr. belisarius
2
我忘了说问题中指定的6名球员2个球场的情况必须在一分钟内完成。没人能做到。有几个人找到了问题的解决方案,但没有人找到可以在一分钟内完成的解决方案。(@belisarius,没有人在第二轮比赛,因为1、2、3、4号选手在第一轮比赛中参加了比赛,所以他们必须在下一轮休息,因此没有人有空和5号选手在第二轮比赛中匹配)。 - Ingdas
@belisarius 这是因为其中一个玩家需要休息。1、2、3、4号玩家已经参加了第一轮比赛并需要休息。第五个人不能单独参赛。 - Andrey
您可以将所有匹配视为长度相等。所有匹配和最小休息时间都需要1个时间单位。每轮比赛持续1个时间单位。 - Ingdas
显示剩余10条评论
6个回答

39

前言

在Prolog中,CLP(FD)约束是解决此类调度任务的正确选择。

更多信息请参见

在这种情况下,建议使用强大的global_cardinality/2约束来限制每轮比赛的出现次数,具体取决于可用场地的数量。我们可以使用迭代加深来找到最小可接受轮数。

免费的Prolog系统足以满意地解决此任务。商业级系统将运行几十倍快。

变种1:使用SWI-Prolog解决方案

:- use_module(library(clpfd)).

tennis(N, Courts, Rows) :-
        length(Rows, N),
        maplist(same_length(Rows), Rows),
        transpose(Rows, Rows),
        Rows = [[_|First]|_],
        chain(First, #<),
        length(_, MaxRounds),
        numlist(1, MaxRounds, Rounds),
        pairs_keys_values(Pairs, Rounds, Counts),
        Counts ins 0..Courts,
        foldl(triangle, Rows, Vss, Dss, 0, _),
        append(Vss, Vs),
        global_cardinality(Vs, Pairs),
        maplist(breaks, Dss),
        labeling([ff], Vs).

triangle(Row, Vs, Ds, N0, N) :-
        length(Prefix, N0),
        append(Prefix, [-|Vs], Row),
        append(Prefix, Vs, Ds),
        N #= N0 + 1.

breaks([]).
breaks([P|Ps]) :- maplist(breaks_(P), Ps), breaks(Ps).

breaks_(P0, P) :- abs(P0-P) #> 1.

示例查询:2个场地上的5名球员:

?- time(tennis(5, 2, Rows)), maplist(writeln, Rows).
% 827,838 inferences, 0.257 CPU in 0.270 seconds (95% CPU, 3223518 Lips)
[-,1,3,5,7]
[1,-,5,7,9]
[3,5,-,9,1]
[5,7,9,-,3]
[7,9,1,3,-]
指定任务为:6名球员在2个场地上进行比赛,程序在1分钟内解决了问题;
进一步的例子是:7名球员在5个场地上比赛。下面是使用SICStus Prolog的解决方案,它比SWI-Prolog快得多。两个系统都可以使用global_cardinality/3约束来实现更高效的过滤。选择针对特定示例的正确选项可能会比选择Prolog系统有更大的影响。

2
哇,这是一个非常有趣的库!谢谢你的回答!不幸的是,你的示例查询在我的SWI-Prolog版本5.8.0 for i386上返回了“false”。你知道为什么吗?也许这会是一个提示:第一个查询在5544个推理之后返回了“false”,第二个和第三个查询在42个推理之后返回了“false”。 - Bolo
5
升级到较新版本;transpose/2以前仅由library(upgraphs)导出,但是library(clpfd)(在更新版本中)导出了一个针对列表而不是图形的不同实现的transpose/2。另外,您可以实现自己的transpose/2版本或从library(clpfd)复制谓词源代码。通常,如果您想知道查询Q失败的原因,请尝试查询?-gtrace,Q。 - mat
我已经从更新版本的 SWI-Prolog 中复制了 transpose/2,它可以正常工作。谢谢! - Bolo
2
实际上,您可以通过从SWI开发存储库中获取版本来替换整个clpfd.pl;当前的git版本支持global_cardinality/3中的“consistency(value)”选项,该选项实现了一种较弱的一致性形式,在许多示例中都能更快地执行,包括上面的查询“tennis(7,5,Rs)”,使用此选项可在不到10秒钟内找到解决方案。 - mat

12
这与旅行锦标赛问题非常相似,后者是关于足球队的安排。在TTP中,他们只能找到最多8个团队的最优解。任何打破10个或更多团队的持续记录的人,都更容易在研究杂志上发表文章。
它是NP难的,诀窍是使用元启发式算法,如禁忌搜索、模拟退火,而不是暴力或分支和界限。
看看我的实现Drools Planner(开源,Java)。 这里是约束条件,应该很容易用没有休息就打2轮。等约束条件替换。

2
这可能是NP-Hard问题,但我怀疑,因为我所知道的所有NP问题都有比2个整数更多的输入。此外,我们必须保证确切的解决方案,因此启发式方法并不是一个真正的选择。Drools Planner解决方案可能可行,但我希望有一种解决方案,我可以理解它的工作方式,而不需要使用高级库。 - Ingdas
2
@Ingdas 我们可以将任意数量的整数编码为一个整数。 - Peter G.
像“没有人连续玩两轮而不休息”这样的限制(以及他可能尚未提到的其他限制)也使得问题很快变成NP难题。证明某个问题是P难的很容易:展示一个在P时间内运行的算法即可。但反过来呢? - Geoffrey De Smet
3
整数分解? :) - biziclop
@GeoffreyDeSmet 如果我说将您的得分点赞以打破当前的6666分是我的动机之一,那我就在撒谎了 :) 不过回答真的很棒! - Jesse Carter
显示剩余2条评论

5
每个玩家必须至少参加n-1场比赛,其中n是玩家数量。因此,回合的最小值为2(n-1)-1,因为每个玩家都需要休息一场比赛。最小值也受到(n(n-1))/ 2总比赛数除以场地数量的限制。使用这两个中的最小值可以得出最佳解的长度。然后,就是想出一个好的下限估算公式((剩余比赛+休息)/场地数),并运行A* search。正如Geoffrey所说,我认为这个问题是NP难问题,但A*等元启发式方法非常适用。

3

Python解决方案:

import itertools

def subsets(items, count = None):
    if count is None:
        count = len(items)

    for idx in range(count + 1):
        for group in itertools.combinations(items, idx):
            yield frozenset(group)

def to_players(games):
    return [game[0] for game in games] + [game[1] for game in games]

def rounds(games, court_count):
    for round in subsets(games, court_count):
        players = to_players(round)
        if len(set(players)) == len(players):
            yield round

def is_canonical(player_count, games_played):
    played = [0] * player_count
    for players in games_played:
        for player in players:
            played[player] += 1

    return sorted(played) == played



def solve(court_count, player_count):
    courts = range(court_count)
    players = range(player_count)

    games = list( itertools.combinations(players, 2) )
    possible_rounds = list( rounds(games, court_count) )

    rounds_last = {}
    rounds_all = {}
    choices_last = {}
    choices_all = {}



    def update(target, choices, name, value, choice):
        try:
            current = target[name]
        except KeyError:
            target[name] = value
            choices[name] = choice
        else:
            if current > value:
                target[name] = value
                choices[name] = choice

    def solution(games_played, players, score, choice, last_players):
        games_played = frozenset(games_played)
        players = frozenset(players)

        choice = (choice, last_players)

        update(rounds_last.setdefault(games_played, {}), 
                choices_last.setdefault(games_played, {}), 
                players, score, choice)
        update(rounds_all, choices_all, games_played, score, choice)

    solution( [], [], 0, None, None)

    for games_played in subsets(games):
        if is_canonical(player_count, games_played):
            try:
                best = rounds_all[games_played]
            except KeyError:
                pass
            else:
                for next_round in possible_rounds:
                    next_games_played = games_played.union(next_round)

                    solution( 
                        next_games_played, to_players(next_round), best + 2,
                        next_round, [])

                for last_players, score in rounds_last[games_played].items():
                    for next_round in possible_rounds:
                        if not last_players.intersection( to_players(next_round) ):
                            next_games_played = games_played.union(next_round)
                            solution( next_games_played, to_players(next_round), score + 1,
                                    next_round, last_players)

    all_games = frozenset(games)


    print rounds_all[ all_games ]
    round, prev = choices_all[ frozenset(games) ]
    while all_games:
        print "X ", list(round)
        all_games = all_games - round
        if not all_games:
            break
        round, prev = choices_last[all_games][ frozenset(prev) ]

solve(2, 6)

输出:

11
X  [(1, 2), (0, 3)]
X  [(4, 5)]
X  [(1, 3), (0, 2)]
X  []
X  [(0, 5), (1, 4)]
X  [(2, 3)]
X  [(1, 5), (0, 4)]
X  []
X  [(2, 5), (3, 4)]
X  [(0, 1)]
X  [(2, 4), (3, 5)]

这意味着需要进行11轮比赛。列表以相反的顺序显示每轮比赛要进行的游戏。(虽然我认为同样的比赛安排也可以正向和反向进行。)后面我会回来解释为什么我有这个机会。 对于一个场地,五名玩家得到错误答案。

运行这个程序,我得到了输出“11”。但是我不知道为什么。在最后,games的值是[(0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (1, 2), (1, 3), (1, 4), (1, 5), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (4, 5)],但是打印rounds_all[ ]失败了。 - Warren P
@Warren P,11应该是所需的最小回合数。 (正确吗?)它并没有给出实际玩的游戏。我可以添加,但我没有。games是所有必须玩的游戏的列表。 - Winston Ewert

1
一些想法,也许是一个解决方案...
将问题扩展到X个球员和Y个球场后,我认为我们可以安全地说,在选择时,我们必须选择完成比赛最少的球员,否则我们就有可能最终只剩下一个球员,每两周才能打一次比赛,并且我们之间会有很多空闲周。设想一下20名球员和3个球场的情况。我们可以看到在第1轮中,球员1-6相遇,然后在第2轮中,球员7-12相遇,而在第3轮中,我们可以重新使用球员1-6,留下球员13-20以后再安排。因此,我认为我们的解决方案不能贪心,必须平衡球员。
在这个假设的基础上,这是对解决方案的初步尝试:
 1. Create master-list of all matches ([12][13][14][15][16][23][24]...[45][56].)
 2. While (master-list > 0) {
 3.     Create sub-list containing only eligible players (eliminate all players who played the previous round.)
 4.     While (available-courts > 0) {
 5.         Select match from sub-list where player1.games_remaining plus player2.games_remaining is maximized.
 6.         Place selected match in next available court, and 
 7.         decrement available-courts.
 8.         Remove selected match from master-list.
 9.         Remove all matches that contain either player1 or player2 from sub-list.
10.     } Next available-court
11.     Print schedule for ++Round.
12. } Next master-list

我不能证明这个算法会得出最少轮次的赛程,但它应该会接近。可能会带来问题的是第5步(选择使选手剩余比赛最多的比赛)。我可以想象有一种情况,即在下一轮留下更多选择时,最好选择一个几乎最大化了“games_remaining”的比赛。

这个算法的输出大致如下:

Round    Court1    Court2
  1       [12]      [34]
  2       [56]       --
  3       [13]      [24]
  4        --        --
  5       [15]      [26]
  6        --        --
  7       [35]      [46]
  .         .         .

仔细检查会发现,在第5轮比赛中,如果Court2上的比赛是[23],那么第6轮比赛中就可以进行第[46]场比赛。然而,这并不能保证以后不会出现类似的问题。
我正在研究另一种解决方案,但需要等待一段时间。

0

我不知道这是否重要,但“5名球员和2个场地”示例数据缺少另外三场比赛:[1,3]、[2,4]和[3,5]。根据指示:“每个人都要与其他所有人进行比赛。”


不,1号和3号玩家是在第5轮进行游戏的,也许你读错了图表 ;) - David Mårtensson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接