网球比赛安排
数量有限的球员和数量有限的网球场。 每场比赛最多可以有多less场比赛。 没有人rest2轮。 每个人都和其他人玩。 产生尽可能less的轮次的时间表。 (因为每个人之间必须有轮休的规则,所以可以有一轮没有比赛。)5个球员和2个球场的输出可以是:
| 1 2 3 4 5 -|------------------- 2| 1 - 3| 5 3 - 4| 7 9 1 - 5| 3 7 9 5 -
在这个输出中,列和行是玩家号码,matrix内的数字是这两个玩家竞争的轮数。
问题是find一个algorithm,可以在一个可行的时间更大的情况下做到这一点。 我们被要求在Prolog中这样做,但任何语言的(伪)代码都是有用的。
我的第一个尝试是一个贪婪的algorithm,但是结果太多了。 然后我提出了一个迭代深化的深度优先search,这是我的一个朋友实现的,但是对于7个玩家来说,这个search仍然花费了太多的时间。
(这是从一个旧的考试题目,我没有任何解决scheme。)
前言
在Prolog中, CLP(FD)约束是解决这种调度任务的正确select。
有关更多信息,请参阅clpfd 。
在这种情况下,我build议使用强大的global_cardinality/2
约束来限制每轮的发生次数 ,具体取决于可用法院的数量。 我们可以使用迭代加深来find最小允许轮次数。
自由可用的Prolog系统足以令人满意地解决任务。 商业级系统的运行速度要快几十倍。
变体1:使用SWI-Prolog的解决scheme
:- 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个法院的6名球员 ,在1分钟的时限内解决了:
(网球(6,2,行)), MAPLIST(格式( “〜T〜W〜3 +〜T〜W〜3 +〜T〜W〜3 +〜T〜W〜3 +〜T〜W〜3 +〜T〜W〜3 + \ n” 个),行)。 %6,675,665推论,在0.977秒0.970 CPU(99%CPU,6884940嘴唇) - 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名球员:
(网球(7,5,行)), MAPLIST(格式(“〜T〜W〜3 +〜T〜W〜3 +〜T〜W〜3 +〜T〜W〜3 +〜T〜W〜3 +〜T〜W〜3 +〜吨〜 w〜3 + \ n“),行)。 %125,581,090推断,18.208秒17.476 CPU(96%CPU,7185927嘴唇) - 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 -
变体2:使用SICStus Prolog的解决scheme
有了以下附加的兼容性定义, 相同的程序也可以在SICStus Prolog中运行:
: - use_module(库(列表))。 : - use_module(库(之间))。 : - op(700,xfx,ins)。 Vs ins D: - maplist(in_(D),Vs)。 in_(D,V): - V in D 链([],_)。 链([L | Ls],Pred): - chain_(Ls,L,Pred)。 链_([],_,_)。 链_([L | Ls],Prev,Pred): - 调用(Pred,Prev,L), chain_(Ls,L,Pred)。 pairs_keys_values(Ps,Ks,Vs): - keys_and_values(Ps,Ks,Vs)。 (Pred,Ls1,Ls2,Ls3,S0,S): - (Ls1,Ls2,Ls3,Pred,S0,S)。 foldl _([],[],[],_,S,S)。 ([L1 | Ls1],[L2 | Ls2],[L3 | Ls3],Pred,S0,S): - 调用(Pred,L1,L2,L3,S0,S1), (Ls1,Ls2,Ls3,Pred,S1,S)。 时间(目标): - 统计信息(运行时,[T0 | _]), 调用(目标), 统计信息(运行时,[T1 | _]), T#= T1-T0, 格式(“%运行时间:〜Dms \ n”,[T])。
主要区别:SICStus是一个商业级的Prolog,带有一个严重的CLP(FD)系统,比SWI-Prolog在这个用例和其他类似的软件中要快得多 。
指定的任务,2个法院的6名球员:
(网球(6,2,行)), MAPLIST(格式( “〜T〜W〜3 +〜T〜W〜3 +〜T〜W〜3 +〜T〜W〜3 +〜T〜W〜3 +〜T〜W〜3 + \ n” 个),行)。 % 运行时间: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 -
更大的例子:
| (网球(7,5,行)), MAPLIST(格式(“〜T〜W〜3 +〜T〜W〜3 +〜T〜W〜3 +〜T〜W〜3 +〜T〜W〜3 +〜T〜W〜3 +〜吨〜 w〜3 + \ n“),行)。 % 运行时间: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
允许您指定改变全局基数约束的传播强度的选项,从而实现更弱和更有效的过滤。 为特定示例select正确的选项可能比selectProlog系统的影响更大。
这与旅行锦标赛问题非常相似,这是关于安排足球队的问题 。 在TTP中,他们可以find最多只有8支队伍的最佳解决scheme。 任何人打破10个或更多队伍的持续logging,在研究期刊上发表要容易得多。
NP是困难的,诀窍是使用元启发式,如禁忌search,模拟退火,…而不是暴力或分支和界限。
看看我的 Drools Planner (开源,Java)的实现。 这里是约束条件 ,应该很直接地用诸如没有人没有rest地玩两轮的限制来取代。
每个玩家必须至less玩n – 1场比赛,其中n是玩家人数。 所以最小回合数是2(n – 1) – 1,因为每个玩家都需要rest一下。 最小值也受(n(n-1))/ 2个总比赛数除以法院数的限制。 使用这两个中最小的一个可以给出最佳解决scheme的长度。 那么这是一个很好的较低的估算公式((比赛数+剩下的)/法院)和运行A *search的问题 。
正如杰弗里所说,我认为这个问题是NP难题,但是元启发式algorithm如A *是非常适用的。
Python解决scheme:
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轮。 该列表以相反顺序显示要在回合中玩的游戏。 (虽然我认为同样的时间表是前向和后向的)我会回来解释我为什么有这个机会。
得到一个法庭,五名球员不正确的答案。
有些想法,也许是解决scheme…
把这个问题扩大到X球员和Y球场,我想我们可以放心地说,在给出select的时候,我们必须select最less完成比赛的球员,否则我们会面临只剩下一名球员的风险另外一个星期,我们结束了许多空的周。 图片与20名球员和3个法院的情况。 我们可以看到,在第一回合中,第一回合select了1-6,然后在第二回合中select了7-12回合,而在第三回合中,我们可以重复使用1-6回到第三回合。 因此,我认为我们的解决scheme不能贪婪,必须平衡球员。
有了这个假设,这是一个解决scheme的第一次尝试:
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
我不能certificate这会产生一个最less的轮次,但它应该是接近的。 可能会导致问题的步骤是#5(select匹配,最大化玩家的游戏剩余。)我可以想象,可能会有一个情况下,select一个匹配几乎最大化'games_remaining'为了留下更多选项回合。
这个algorithm的输出看起来像这样:
Round Court1 Court2 1 [12] [34] 2 [56] -- 3 [13] [24] 4 -- -- 5 [15] [26] 6 -- -- 7 [35] [46] . . .
仔细检查会发现,在第五轮中,如果第二轮的比赛是[23],那么在第六轮期间可以打出匹配[46]。然而,这并不保证在第6轮中不会有类似的问题稍后一轮。
我正在研究另一个解决scheme,但是这将不得不等待以后。
我不知道这个问题是否重要,“五位球员和两位球场”的例子中的数据缺less三个其他的比赛:[1,3],[2,4]和[3,5]。 根据指示:“每个人都和其他人玩。”