43

限られた数のプレーヤーと限られた数のテニスコートがあります。各ラウンドでは、最大でコート数と同じ数の試合を行うことができます。休憩なしで 2 ラウンドをプレイする人はいません。全員が他の全員と対戦します。ラウンド数ができるだけ少ないスケジュールを作成します。(全員のラウンド間に休憩が必要であるという規則のため、試合のないラウンドが存在する可能性があります。) 5 人のプレーヤーと 2 つのコートの出力は次のようになります。

 |  1   2   3   4   5
-|------------------- 
2|  1   - 
3|  5   3   - 
4|  7   9   1   - 
5|  3   7   9   5   -

この出力では、列と行はプレイヤー番号であり、行列内の数字はこれら 2 人のプレイヤーが競うラウンド数です。

問題は、実行可能な時間でより大きなインスタンスに対してこれを実行できるアルゴリズムを見つけることです。これを Prolog で行うように依頼されましたが、どの言語の (疑似) コードでも役に立ちます。

私の最初の試行は貪欲なアルゴリズムでしたが、それではラウンド数が多すぎる結果が得られました。次に、友人が実装した反復的な深さ優先探索を提案しましたが、それでも 7 人のプレイヤーという小さなインスタンスでは時間がかかりすぎました。

(これは古い試験問題からのものです。私が話した誰も解決策を持っていませんでした。)

4

6 に答える 6

38

序文

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,-]

指定されたタスク、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" )、行)。
%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 92-5
 10 3 1 75-

さらなる例: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 ")、行)。
%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 31-

バリエーション2:SICStusPrologを使用したソリューション

互換性のために次の追加の定義を使用すると、同じプログラムがSICStusPrologでも実行されます。

:-use_module(library(lists))。
:-use_module(library(between))。

:-op(700、xfx、ins)。

Vs ins D:-maplist(in_(D)、Vs)。

in_(D、V):-VinD。

鎖([]、 _)。
チェーン([L | Ls]、Pred):-
        chain_(Ls、L、Pred)。

鎖_([]、 _、 _)。
chain _([L | Ls]、Prev、Pred):-
        call(Pred、Prev、L)、
        chain_(Ls、L、Pred)。

pair_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)。

時間(目標):-
        統計(ランタイム、[T0 | _])、
        呼び出し(目標)、
        統計(ランタイム、[T1 | _])、
        T#= T1-T0、
        format( "%Runtime:〜Dms \ n"、[T])。

主な違い:SICStusは、本格的なCLP(FD)システムを搭載した商用グレードのPrologであり、このユースケースやその他のユースケースでは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" )、行)。
%ランタイム:34ms(!)
  --1 3 5 7 10
  1-6 11 9 3
  3 6-9 11 1
  5 11 9-2 7
  7 9 112-5
 10 3 1 75-

より大きな例:

| ?-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 ")、行)。
%ランタイム: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 113-5
 11 13 7 9 15-

閉会の辞

どちらのシステムでglobal_cardinality/3も、グローバルカーディナリティ制約の伝播強度を変更するオプションを指定できるため、より弱く、より効率的なフィルタリングが可能になります。特定の例に適切なオプションを選択すると、Prologシステムの選択よりもさらに大きな影響を与える可能性があります。

于 2011-01-26T19:26:04.003 に答える
13

これは、サッカー チームのスケジュールに関するTraveling Tournament Problemと非常によく似ています。TTP では、最大 8 チームまでしか最適解を見つけることができません。10 チーム以上の継続的な記録を更新した人は、研究ジャーナルに掲載されやすくなります。

それは NP 困難であり、秘訣は、タブー検索、シミュレートされたアニーリングなどのメタヒューリスティックを使用することです...総当たりや分岐限定の代わりに。

Drools Planner (オープン ソース、Java) を使用した私の実装を見てください。ここに制約があります。これを、誰も休憩なしで 2 ラウンドをプレイするなどの制約に置き換えるのは簡単です。

于 2011-01-20T13:48:42.690 に答える
5

各プレイヤーは、少なくとも n - 1 マッチをプレイする必要があります。ここで、n はプレイヤーの数です。したがって、すべてのプレイヤーが試合を休む必要があるため、ラウンドの最小数は 2(n - 1) - 1 です。最小値は、(n(n-1))/2 総試合数をコート数で割った値にも制限されます。これら 2 つの最小値を使用すると、最適解の長さが得られます。次に、より低い推定式 ((マッチ数 + 残りの休息数)/裁判所) を考え出し、A* searchを実行します。

Geoffrey が言ったように、問題は NP Hard だと思いますが、A* などのメタヒューリスティックは非常に適用可能です。

于 2011-01-26T14:10:38.380 に答える
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ラウンドかかることを意味します。リストには、ラウンドでプレイされるゲームが逆の順序で表示されます。(同じスケジュールが前向きと後向きに機能すると思いますが。)私は戻ってきて、なぜチャンスがあるのか​​を説明します。

1つのコート、5人のプレーヤーに対して間違った答えを取得します。

于 2011-01-21T01:45:38.933 に答える
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 (プレイヤーの残りゲーム数を最大にするマッチを選択する)です円形。

このアルゴリズムの出力は次のようになります。

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

よく調べてみると、ラウンド 5 でコート 2 の試合が [23] だった場合、ラウンド 6 で試合 [46] がプレーされた可能性があることがわかります。後のラウンド。

私は別の解決策に取り組んでいますが、それは後で待つ必要があります。

于 2011-01-21T17:40:39.633 に答える
0

これが問題かどうかはわかりません。「5 人のプレーヤーと 2 つのコート」の例のデータには、[1,3]、[2,4]、[3,5] の 3 つの一致がありません。指示に基づいて:「誰もが他の人と試合をします」。

于 2011-01-25T20:19:24.393 に答える