13

いくつかの背景:
バレーボールでは、プレーヤーはランキングを決定するためにプールでプレーします。チームはプレイヤーのペアです。試合は、プレーヤーのペア対別のプレーヤーのペアです。この例では、試合を行うことができるコートが 1 つしかなく、プレーヤーがプレーしていないときは、座っている/待っていると仮定します。プール内のプレーヤー数は 4 ~ 7 人です。プール内に 8 人のプレーヤーがいる場合は、代わりに 4 人の 2 つのプールに分割します。

各プレイヤーが他のすべてのプレイヤーとプレイするために、最小の試合数を計算したいと考えています。

たとえば、4 プレーヤー プールには次のチームがあります。

import itertools
players = [1,2,3,4]
teams = [t for t in itertools.combinations(players,2)]
print 'teams:'
for t in teams:
    print t

出力:

teams:
(1, 2)
(1, 3)
(1, 4)
(2, 3)
(2, 4)
(3, 4)

および一致の数:

matches = []
for match in itertools.combinations(teams,2):
    # A player cannot be on both teams at the same time
    if set(match[0]) & set(match[1]) == set():
        matches.append(match)

for match in matches:
    print match

出力:

((1, 2), (3, 4))
((1, 3), (2, 4))
((1, 4), (2, 3))

これは正しいですが、プールに 5 人目のプレーヤーを追加すると、このアルゴリズムが壊れます。

((1, 2), (3, 4))
((1, 2), (3, 5))
((1, 2), (4, 5))
((1, 3), (2, 4))
((1, 3), (2, 5))
((1, 3), (4, 5))
((1, 4), (2, 3))
((1, 4), (2, 5))
((1, 4), (3, 5))
((1, 5), (2, 3))
((1, 5), (2, 4))
((1, 5), (3, 4))
((2, 3), (4, 5))
((2, 4), (3, 5))
((2, 5), (3, 4))

チームは何度も複製されます。

プレーするチームのリストを保持しようとしましたが、そのアルゴリズムは貪欲であることが判明しました。つまり、(1,5) チームに到達したとき、他のすべてのチーム [(2,3),(2,4),(3,4)] はすでにプレーしており、(1,5) は決して獲得されません。遊ぶ。

私が探しているもの:

((1,2), (3,4)) (player 5 waits)
((1,3), (2,5)) (player 4 waits)
((1,4), (3,5)) (player 2 waits)
((1,5), (4,2)) (player 3 waits)
((2,3), (4,5)) (player 1 waits)

プールサイズごとにこれを手で計算する方が簡単ですか、それともPythonで簡単に実行できますか?

助けてくれてありがとう!


編集: Python タグを削除しました。どの言語でも十分で、Python に変換できます。

4

7 に答える 7

3

エグゼクティブサマリー:

  • NP 完全最小集合被覆問題との類似性にもかかわらず、この問題は決して扱いにくいものではありません。特に、最小集合被覆率とはかなり異なりますが、事前に自明ではない最良の答えを知っています。

  • その答えは、チームの数を 2 で割ったものです (チームの数が奇数の場合はプラス 1)。これ以上のことはできません。

  • 問題の構造上、可能な限り最良の答えを得るために受け入れられる解決策は多数あります。基本的なランダム化された貪欲なアルゴリズムを使用して、それらに出くわすことができます。チームの N が大きくなると、最初のランダムな試みはほとんどの場合成功します。

  • このアプローチは、多数のチームの場合でも高速です (たとえば、1000 チームの場合はわずか数秒)。

詳細:

k-combinations の式を使用して、必要なチーム数を決定し、すべてのプレーヤーが他のすべてのプレーヤーとペアになるようにすることができます (k = 2)。

n_teams = n! / ( (n - k)! k! )

n     n_teams
--    --------
4     6
5     10
6     15
7     21
8     28
9     36
10    45
11    55      # n_teams always equals the sum of values in previous row

最小マッチ数はどうですか?単純に n_teams を 2 で割ったものだと思います (奇数のチームを処理するためのパディングがあります)。

min_n_matches = (n_teams + (n_teams % 2)) / 2

これについて厳密な証拠はありませんが、直感は正しいようです。新しいプレーヤーを追加するたびに、それを追加の制約と考えることができます。指定された試合の両側に登場できないプレーヤーをもう 1 人追加しただけです。同時に、その新しいプレーヤーは、新しいチームの組み合わせの束を生成します。これらの新しいチームは、反制約のようなものです。その存在により、有効な試合を形成しやすくなります。

上記の数式とデータ テーブルからわかるように、制約 ( n) は直線的に増加しますが、反制約 ( n_teams) ははるかに速く増加します。

それが本当なら、問題を解決するのにスマートなアルゴリズムは必要ありません。チームをランダムに (ただし有効に) 一致させます。最初の試みが失敗した場合は、もう一度やり直してください。チームの数が増えるにつれて、最初の試行で失敗することはめったになくなります。

そのアイデアを実装するためのより良い方法があるかもしれませんが、チームと一致を生成し、上記の主張を確認する図を次に示します。

import sys
import itertools
import random

def main():
    maxN = int(sys.argv[1])
    for n in range(4, maxN + 1):
        run_scenario(n)

def run_scenario(n):
    # Takes n of players.
    # Generates matches and confirms our expectations.
    k = 2
    players = list(range(1, n + 1))
    teams   = list(set(t) for t in itertools.combinations(players, k))

    # Create the matches, and count how many attempts are needed.
    n_calls = 0
    matches = None
    while matches is None:
        matches = create_matches(teams)
        n_calls += 1

    # Print some info.
    print dict(
        n       = n,
        teams   = len(teams),
        matches = len(matches),
        n_calls = n_calls,
    )

    # Confirm expected N of matches and that all matches are valid.
    T = len(teams)
    assert len(matches) == (T + (T % 2)) / 2
    for t1, t2 in matches:
        assert t1 & t2 == set()

def create_matches(teams):
    # Get a shuffled copy of the list of teams.
    ts = list(teams)
    random.shuffle(ts)

    # Create the matches, greedily.
    matches = []
    while ts:
        # Grab the last team and the first valid opponent.
        t1 = ts.pop()
        t2 = get_opponent(t1, ts)
        # If we did not get a valid opponent and if there are still
        # teams remaining, the greedy matching failed.
        # Otherwise, we must be dealing with an odd N of teams.
        # In that case, pair up the last team with any valid opponent.
        if t2 is None:
            if ts: return None
            else:  t2 = get_opponent(t1, list(teams))
        matches.append((t1, t2))

    return matches

def get_opponent(t1, ts):
    # Takes a team and a list of teams.
    # Search list (from the end) until it finds a valid opponent.
    # Removes opponent from list and returns it.
    for i in xrange(len(ts) - 1, -1, -1):
        if not t1 & ts[i]:
            return ts.pop(i)
    return None

main()

出力のサンプル。コール数が非常に急速に 1 に近づく傾向があることに注目してください。

> python volleyball_matches.py 100
{'matches': 3, 'n_calls': 1, 'teams': 6, 'n': 4}
{'matches': 5, 'n_calls': 7, 'teams': 10, 'n': 5}
{'matches': 8, 'n_calls': 1, 'teams': 15, 'n': 6}
{'matches': 11, 'n_calls': 1, 'teams': 21, 'n': 7}
{'matches': 14, 'n_calls': 4, 'teams': 28, 'n': 8}
{'matches': 18, 'n_calls': 1, 'teams': 36, 'n': 9}
{'matches': 23, 'n_calls': 1, 'teams': 45, 'n': 10}
{'matches': 28, 'n_calls': 1, 'teams': 55, 'n': 11}
{'matches': 33, 'n_calls': 1, 'teams': 66, 'n': 12}
...
{'matches': 2186, 'n_calls': 1, 'teams': 4371, 'n': 94}
{'matches': 2233, 'n_calls': 1, 'teams': 4465, 'n': 95}
{'matches': 2280, 'n_calls': 1, 'teams': 4560, 'n': 96}
{'matches': 2328, 'n_calls': 1, 'teams': 4656, 'n': 97}
{'matches': 2377, 'n_calls': 1, 'teams': 4753, 'n': 98}
{'matches': 2426, 'n_calls': 1, 'teams': 4851, 'n': 99}
{'matches': 2475, 'n_calls': 1, 'teams': 4950, 'n': 100}
于 2013-07-21T18:31:03.677 に答える
1

これは集合被覆問題として表現できます。プレーヤーが 4 人の場合、すべての (順不同) プレーヤーのペアのセットを取得します。

PP := {{0,1}, {0,2}, {0,3}, {1,2}, {1,3}, {2,3}}

可能な一致は、これらのペアの順序付けられていないペアであり、両側に同じプレーヤーがいないことです。ここで、可能な一致は次のとおりです。

M := {{{0,1},{2,3}}, {{0,2},{1,3}}, {{0,3},{1,2}}}

問題は、このセットの最小のサブセットを見つけて、その和集合がすべてのプレーヤー ペアのセット PP になるようにすることです。

これは、 NP 完全である最小集合カバー問題のインスタンスです。おそらく、セットをペアに制限することでより簡単な解決策が得られますが、そうでない場合でも驚くことではありません。

小さいセットに制限しているので、力ずくで解決することは非常に実行可能です。

少なくともceil(N * (N-1) / 4)マッチが必要であることはわかっています (N * (N-1) / 2異なるペアがあり、各マッチは最大 2 つの新しいペアをカバーできるため)。これにより、アルゴリズムが得られます。

import itertools

def mincover(n):
    pairs = set(map(tuple, itertools.combinations(range(n), 2)))
    matches = itertools.combinations(pairs, 2)
    matches = [m for m in matches if not(set(m[0]) & set(m[1]))]
    for subset_size in xrange((len(pairs) + 1) // 2, len(pairs) + 1):
        for subset in itertools.combinations(matches, subset_size):
            cover = set()
            for s in subset: cover |= set(s)
            if cover == pairs:
                return subset

for i in xrange(4, 8):
    print i, mincover(i)

特に 6 ~ 7 人のプレーヤーでは、かなり遅いです。これは、新しいプレーヤー ペアを追加しない一致を考慮しない手作業でコーディングされた検索と、対称性を使用し、常に{{0,1}, {2,3}}.

于 2013-07-21T09:43:08.710 に答える
1

私は Python を知りませんが、Ruby で試してみることに抵抗できませんでした。うまくいけば、これはすぐに Python に変換されます。Ruby を知らない場合は、ここで何が起こっているのかを喜んで説明します。

num_players = gets.to_i
players = (1..num_players).to_a
teams = players.combination(2).to_a

def shuffle_teams( teams, players )
  shuffled_teams = teams.shuffle
  x = 0
  while x < shuffled_teams.length
    if shuffled_teams[x] - shuffled_teams[x + 1] == shuffled_teams[x]
      x += 2
    else
      return shuffle_teams( teams, players )
    end
  end
  x = 0
  while x < shuffled_teams.length
    team_1 = shuffled_teams[x]
    team_2 = shuffled_teams[x + 1]
    waiting = players.select do |player|
      ![team_1, team_2].flatten.include?(player)
    end
    print "(#{team_1}, #{team_2}), waiting: #{waiting}\n"
    x += 2
  end
end

shuffle_teams( teams, players )

これにより、4 人のプレーヤーの正しい出力が生成されます。

([3, 4], [1, 2]), waiting: []
([1, 3], [2, 4]), waiting: []
([2, 3], [1, 4]), waiting: []

5 人のプレーヤーの場合:

([2, 4], [1, 3]), waiting: [5]
([1, 5], [3, 4]), waiting: [2]
([1, 4], [2, 5]), waiting: [3]
([3, 5], [1, 2]), waiting: [4]
([2, 3], [4, 5]), waiting: [1]

ただし、6 人または 7 人でプレイすると、組み合わせが奇数になるため、うまくいきません。この問題は実際の生活の中でどのように扱われますか? どういうわけか、1 つのチームが 2 回プレイする必要があります。

編集:このスクリプトは、チームの 1 つを複製することにより、6 人または 7 人のプレイヤー プールを処理するようになりました。適切な順序に落ち着くまでチームの配列をシャッフルするだけなので、Python で複製するのは簡単なはずです。最初は、そのアプローチで少しごまかしているように感じましたが、これがNP完全問題であるというAnonymousの説明を考えると(それが何を意味するのかを正しく理解していると仮定して:-)、これが問題を解決する最良の方法かもしれません小さなプール (システムによっては、9 かそこらを超えるプールで爆発しますが、幸いなことに、それはシナリオの範囲を超えています)。さらに、ランダムな順序付けには非個人的であるという利点があり、プレーヤーが 2 回目のスコアを獲得せずに 2 回プレイしなければならないことに腹を立てている場合に役立ちます! スクリプトは次のとおりです。

num_players = gets.to_i
players = (1..num_players).to_a
teams = players.combination(2).to_a

def shuffle_teams( teams, players )
  shuffled_teams = teams.shuffle
  x = 0
  while x < shuffled_teams.length
    if !shuffled_teams[x + 1]
      shuffled_teams[x + 1] = shuffled_teams.find do |team|
        shuffled_teams[x] - team == shuffled_teams[x]
      end
    end
    if shuffled_teams[x] - shuffled_teams[x + 1] == shuffled_teams[x]
      x += 2
    else
      return shuffle_teams( teams, players )
    end   
  end
  x = 0
  while x < shuffled_teams.length
    team_1 = shuffled_teams[x]
    team_2 = shuffled_teams[x + 1]
    waiting = players.select do |player|
      ![team_1, team_2].flatten.include?(player)
    end
    print "(#{team_1}, #{team_2}), waiting: #{waiting}\n"
    x += 2
  end
end

shuffle_teams( teams, players )

そして、これが時間付きの出力です:

4
([1, 4], [2, 3]), waiting: []
([1, 2], [3, 4]), waiting: []
([2, 4], [1, 3]), waiting: []

real    0m0.293s
user    0m0.035s
sys 0m0.015s

5
([4, 5], [1, 2]), waiting: [3]
([1, 4], [2, 3]), waiting: [5]
([2, 5], [1, 3]), waiting: [4]
([2, 4], [3, 5]), waiting: [1]
([3, 4], [1, 5]), waiting: [2]

real    0m0.346s
user    0m0.040s
sys 0m0.010s

6
([3, 4], [1, 2]), waiting: [5, 6]
([3, 5], [2, 4]), waiting: [1, 6]
([3, 6], [1, 5]), waiting: [2, 4]
([1, 6], [2, 5]), waiting: [3, 4]
([2, 3], [4, 6]), waiting: [1, 5]
([2, 6], [4, 5]), waiting: [1, 3]
([5, 6], [1, 4]), waiting: [2, 3]
([1, 3], [2, 4]), waiting: [5, 6]

real    0m0.348s
user    0m0.035s
sys 0m0.020s

7
([1, 6], [4, 5]), waiting: [2, 3, 7]
([2, 6], [1, 4]), waiting: [3, 5, 7]
([2, 7], [1, 3]), waiting: [4, 5, 6]
([3, 4], [2, 5]), waiting: [1, 6, 7]
([3, 5], [2, 4]), waiting: [1, 6, 7]
([1, 7], [5, 6]), waiting: [2, 3, 4]
([6, 7], [1, 5]), waiting: [2, 3, 4]
([3, 6], [4, 7]), waiting: [1, 2, 5]
([1, 2], [5, 7]), waiting: [3, 4, 6]
([3, 7], [4, 6]), waiting: [1, 2, 5]
([2, 3], [1, 6]), waiting: [4, 5, 7]

real    0m0.332s
user    0m0.050s
sys 0m0.010s
于 2013-07-21T08:13:56.200 に答える
0

Ruby で投稿し続けて申し訳ありませんが、クラックしただけだと思います。共有する必要があります。うまくいけば、私のすべての努力が Python での実装に役立つことを願っています :)。

このアルゴリズムは、以前依存していたランダム シャッフルと再帰関数なしで機能します。したがって、この状況で心配する必要はありませんが、はるかに大きなプールで機能します。

num_players = gets.to_i
players = (1..num_players).to_a
teams = players.combination(2).to_a
first_half = Float(teams.length / 2.0).ceil
first_half_teams = teams[0..(first_half - 1)]
second_half_teams = teams[first_half..-1]
possible_lineups = []
matches = []
matched = []

first_half_teams.each do |team|
  opponents = second_half_teams.select do |team_2|
    team - team_2 == team
  end
  possible_lineups << [team, opponents]
end

possible_lineups.each do |lineup|
  team_1 = lineup[0]
  team_2 = lineup[1].find do |team|
    !matched.include?(team)
  end
  if !team_2
    thief_team = possible_lineups.find do |test_team|
      test_team[1] - lineup[1] != test_team[1] &&
      test_team[1].find{ |opponent| !matched.include?(opponent) }
    end
    if thief_team
      new_opponent = thief_team[1].find{ |opponent| !matched.include?(opponent) }
      matched << new_opponent
      old_lineup = matches.find do |match|
        match[0] == thief_team[0]
      end
      team_2 = old_lineup[1]
      matches.find{ |match| match[0] == thief_team[0]}[1] = new_opponent
    else
      team_2 = second_half_teams.find do |team|
        lineup[0] - team == lineup[0]
      end
    end
  end
  matches << [team_1, team_2]
  matched << team_2
end

matches.each do |match|
  left_out = players.select{ |player| !match.flatten.include?(player) }
  print match, ", waiting: ", left_out, "\n"
end

print "greater: ", matches.flatten(1).find{ |team| matches.flatten(1).count(team) > teams.count(team) }, "\n"
print "less: ", matches.flatten(1).find{ |team| matches.flatten(1).count(team) < teams.count(team) }, "\n"

現実を確認するために、スクリプトを使用して、マッチの最終的な配列を、一意のプレーヤー ペアの組み合わせの元の配列と比較します。プレーヤーとペアの組み合わせの数が偶数の場合 (例: プール サイズ = 4 または 5)、元の組み合わせ配列に出現する回数より多い回数または少ない回数で最終試合配列に出現するペアがあってはなりません (つまり、各ペアは各配列に 1 回だけ出現する必要があります)。組み合わせの数が奇数 (n = 6 または 7) の場合、組み合わせ配列に現れる回数よりも、matches 配列に 1 回多く現れるペアが 1 つだけ存在するはずです。組み合わせ配列よりも一致配列に出現する回数が少ないペアがあってはなりません。出力は次のとおりです。

4
[[1, 2], [3, 4]], waiting: []
[[1, 3], [2, 4]], waiting: []
[[1, 4], [2, 3]], waiting: []
greater: 
less: 

5
[[1, 2], [3, 5]], waiting: [4]
[[1, 3], [2, 4]], waiting: [5]
[[1, 4], [2, 5]], waiting: [3]
[[1, 5], [3, 4]], waiting: [2]
[[2, 3], [4, 5]], waiting: [1]
greater: 
less: 

6
[[1, 2], [3, 4]], waiting: [5, 6]
[[1, 3], [2, 6]], waiting: [4, 5]
[[1, 4], [3, 5]], waiting: [2, 6]
[[1, 5], [3, 6]], waiting: [2, 4]
[[1, 6], [4, 5]], waiting: [2, 3]
[[2, 3], [4, 6]], waiting: [1, 5]
[[2, 4], [5, 6]], waiting: [1, 3]
[[2, 5], [3, 4]], waiting: [1, 6]
greater: [3, 4]
less: 

7
[[1, 2], [3, 4]], waiting: [5, 6, 7]
[[1, 3], [4, 5]], waiting: [2, 6, 7]
[[1, 4], [3, 5]], waiting: [2, 6, 7]
[[1, 5], [3, 6]], waiting: [2, 4, 7]
[[1, 6], [3, 7]], waiting: [2, 4, 5]
[[1, 7], [4, 6]], waiting: [2, 3, 5]
[[2, 3], [4, 7]], waiting: [1, 5, 6]
[[2, 4], [5, 6]], waiting: [1, 3, 7]
[[2, 5], [6, 7]], waiting: [1, 3, 4]
[[2, 6], [5, 7]], waiting: [1, 3, 4]
[[2, 7], [3, 4]], waiting: [1, 5, 6]
greater: [3, 4]
less:

FMc へのメモ: あなたのコメントは、この問題についての私の考えを改善するのに役立ちました。「ブレインデッド」アルゴリズムはあなたを近づけますが、完全な解決策にはなりません. n > 4 の場合、純粋に貪欲なアルゴリズムを使用すると、常に 1 ペアのプレイヤーが残り、相手のペアが存在しないように見えます。私のアルゴリズムは、前に戻って、すでにマッチしたチームから適格な相手のペアを取得することでこれを処理します。ただし、後者のチームがまだ使用されていない別の相手のペアを使用できる場合に限ります。これは必要な唯一の修正のようです (奇数の組み合わせの調整を除いて)。

于 2013-07-22T03:23:37.923 に答える
0

組み合わせ論では、これはWhist Round Robinと呼ばれます。おそらく、コミトロニクスに関心のある数学の専門家は、ビーチバレーボールよりもホイストをする傾向がありますか? しかし、それは別の話です。

ホイストラウンドロビン

別の 2 人チームと対戦する 2 人で構成されたペアでトーナメントをスケジュールする問題は、ホイスト ラウンド ロビンと呼ばれます

プレイヤー数が4nの場合に実装するのが最も簡単です。残りの 3 つのケースは、ゴースト プレイヤーとゴースト チームを使用して作成されます。ゴーストチームと対戦することになっているプレーヤーは、そのラウンドに座ります。

基本的な考え方は、1 人のプレイヤーがロックされているということです。たとえば、プレイヤー 1 としましょう。次に、他のプレイヤーが「回転」し、最初のラウンドでプレイヤー 2 がプレイヤー 1 とチームを組み、プレイヤー 2 & 3 と対戦します。次のラウンドでは、プレイヤー 1 が残り、プレイヤー 3 とチームを組み、プレイヤー 2 & 4 と対戦します。上のリンクからお借りしました。

ここに画像の説明を入力

そこで説明されているアルゴリズムを実装して、ビーチバレー ボールと同様の特徴を持つフーズボールトーナメントをスケジュールしましたが、うまく機能しました。

Python でこれを実装するのに問題はないはずです。もしそうなら - いくつかの具体的な質問をして戻ってきてください. 幸運を!:)

于 2013-07-21T09:08:04.777 に答える
0

問題の適切なモデルは、完全な無向グラフのモデルです。

  • 頂点はプレイヤーを表します。

  • エッジは 2 人のプレーヤーのチームを表します。

マッチのセットアップごとに、頂点を共有しない一連のエッジから 2 つのエッジを描画します。 各エッジが少なくとも 1 回描画されるまで、エッジ ペアを描画し続けます。

n=5の場合の試合スケジュール例

n 頂点の完全なグラフの辺の数は であるため(n * (n - 1)) / 2、この数が奇数の場合、1 つの辺を 2 回使用する必要があることは明らかです。

于 2013-07-21T21:42:46.647 に答える