有限数量的球员和数量有限的网球场。在每轮比赛中,最多可以有比赛更多的比赛。 没有休息,没有人打2轮。每个人都与其他人比赛。 制定尽可能少轮次的计划。 (由于每个人的轮次之间必须休息的规则,可以有一轮没有匹配。) 5名球员和2名球员的输出可能是:
| 1 2 3 4 5
-|-------------------
2| 1 -
3| 5 3 -
4| 7 9 1 -
5| 3 7 9 5 -
在此输出中,列和行是播放器编号,矩阵内的数字是这两个玩家竞争的圆数。
问题是找到一种算法,可以在可行的时间内为更大的实例执行此操作。我们被要求在Prolog中执行此操作,但任何语言的(伪)代码都很有用。
我的第一次尝试是一种贪婪的算法,但这会产生太多轮次的结果。 然后我建议迭代加深深度优先搜索,我的一个朋友实现了,但是仍然花了太多时间在小到7个玩家的实例上。
(这是一个旧的考试问题。我接触过的任何人都没有任何解决方案。)
答案 0 :(得分:35)
在Prolog中, CLP(FD)约束是解决此类调度任务的正确选择。
有关详细信息,请参阅 clpfd 。
在这种情况下,我建议使用强大的global_cardinality/2
约束来限制每轮的出现次数,具体取决于可用的法院数量。我们可以使用迭代深化来查找最小数量的可接受轮次。
可自由使用的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,-]
指定的任务,<2> 2个球场上的6名球员,在1分钟的时间限制内完好无损:
?- time(tennis(6, 2, Rows)), maplist(format("~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+\n"), Rows). % 6,675,665 inferences, 0.970 CPU in 0.977 seconds (99% CPU, 6884940 Lips) - 1 3 5 7 10 1 - 6 9 11 3 3 6 - 11 9 1 5 9 11 - 2 7 7 11 9 2 - 5 10 3 1 7 5 -
进一步的例子:5个球场上的7名球员:
?- time(tennis(7, 5, Rows)), maplist(format("~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+\n"), Rows). % 125,581,090 inferences, 17.476 CPU in 18.208 seconds (96% CPU, 7185927 Lips) - 1 3 5 7 9 11 1 - 5 3 11 13 9 3 5 - 9 1 7 13 5 3 9 - 13 11 7 7 11 1 13 - 5 3 9 13 7 11 5 - 1 11 9 13 7 3 1 -
使用以下附加的兼容性定义,相同的程序也在SICStus Prolog中运行:
:- use_module(library(lists)). :- use_module(library(between)). :- op(700, xfx, ins). Vs ins D :- maplist(in_(D), Vs). in_(D, V) :- V in D. chain([], _). chain([L|Ls], Pred) :- chain_(Ls, L, Pred). chain_([], _, _). chain_([L|Ls], Prev, Pred) :- call(Pred, Prev, L), chain_(Ls, L, Pred). pairs_keys_values(Ps, Ks, Vs) :- keys_and_values(Ps, Ks, Vs). foldl(Pred, Ls1, Ls2, Ls3, S0, S) :- foldl_(Ls1, Ls2, Ls3, Pred, S0, S). foldl_([], [], [], _, S, S). foldl_([L1|Ls1], [L2|Ls2], [L3|Ls3], Pred, S0, S) :- call(Pred, L1, L2, L3, S0, S1), foldl_(Ls1, Ls2, Ls3, Pred, S1, S). time(Goal) :- statistics(runtime, [T0|_]), call(Goal), statistics(runtime, [T1|_]), T #= T1 - T0, format("% Runtime: ~Dms\n", [T]).
主要区别:作为商业级Prolog的SICStus,配备了严格的CLP(FD)系统,在此用例和其他类似用户中,SWI-Prolog 快得多。
指定的任务,2个球场上的6名球员:
?- time(tennis(6, 2, Rows)), maplist(format("~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+\n"), Rows). % Runtime: 34ms (!) - 1 3 5 7 10 1 - 6 11 9 3 3 6 - 9 11 1 5 11 9 - 2 7 7 9 11 2 - 5 10 3 1 7 5 -
更大的例子:
| ?- time(tennis(7, 5, Rows)), maplist(format("~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+~t~w~3+\n"), Rows). % Runtime: 884ms - 1 3 5 7 9 11 1 - 5 3 9 7 13 3 5 - 1 11 13 7 5 3 1 - 13 11 9 7 9 11 13 - 3 1 9 7 13 11 3 - 5 11 13 7 9 1 5 -
在这两个系统中,global_cardinality/3
允许您指定用于更改全局基数约束的传播强度的选项,从而实现更弱且可能更有效的过滤。为特定示例选择正确的选项可能比选择Prolog系统产生更大的影响。
答案 1 :(得分:12)
这与Traveling Tournament Problem非常相似,这是关于安排足球队的。在TTP中,他们可以找到最多只有8个团队的最佳解决方案。任何打破10个或更多团队持续记录的人,都可以更容易地在研究期刊上发表。
这是NP很难,诀窍是使用元启发式算法,例如禁忌搜索,模拟退火,......而不是强力或分支和束缚。
使用my implementation(开源,java)查看Drools Planner。 Here are the constraints,用等约束替换它应该很简单。没有人在没有休息的情况下玩2轮。
答案 2 :(得分:5)
每位玩家必须至少玩n - 1场比赛,其中n是玩家数量。所以最小轮数是2(n - 1) - 1,因为每个球员都需要休息一场比赛。最低限度也受(n(n-1))/ 2总匹配除以法院数量的约束。使用这两者中最小的一个可以得到最优解的长度。然后,这是一个提出一个良好的较低估计公式((匹配数+余数)/法院)并运行A* search的问题。
正如Geoffrey所说,我认为问题是NP Hard,但是像A *这样的元启发式非常适用。
答案 3 :(得分: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轮。该列表以相反的顺序显示要在轮次中播放的游戏。 (虽然我认为相同的时间表适用于前进和后退。) 我会回来解释为什么我有机会。
获得一个球场,五名球员的错误答案。
答案 4 :(得分: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]。但是,这并不能保证不会有类似的问题在后一轮。
我正在研究另一种解决方案,但是必须等待以后。
答案 5 :(得分:0)
我不知道这是否重要,“5个玩家和2个法院”的示例数据缺少其他三个匹配:[1,3],[2,4]和[3,5]。根据指示:“每个人都与其他人进行比赛。”