排球运动员组合

时间:2013-07-21 02:50:05

标签: algorithm combinations

一些背景:
在排球中,球员在游泳池中进行比赛以确定排名。球队是一对球员。比赛是一对球员与另一对球员。对于这个例子,我们假设只有一个球场可以进行比赛,当球员没有比赛时,他们坐着/等待。池中玩家的数量将在4到7之间。如果池中有8个玩家,他们只会将其分成2个4个池。

我想计算每个玩家与其他玩家一起玩的最少匹配数。

例如,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。

7 个答案:

答案 0 :(得分:3)

执行摘要:

  • 尽管它与NP完全最小集合覆盖问题相似,但这个问题远非棘手。特别是 - 与最低限度套装完全不同 - 我们事先知道一个非平凡的最佳答案。

  • 答案是球队数除以2(当N队是奇数时加1)。我们永远做不到这一点。

  • 由于问题的结构,有许多可接受的解决方案可以实现最佳答案。您可以使用基本的随机贪婪算法偶然发现它们。随着N队变得越来越大,你的第一次随机尝试几乎总是成功。

  • 即使对于大量的团队来说,这种方法也很快(例如,对于1000支团队来说只需几秒钟)。

<强>详细信息:

您可以使用k组合的公式来确定所需的球队数量,以便每个球员与其他球员配对(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

我没有这方面的严格证据,但直觉似乎是合理的。每次添加新玩家时,您都可以将其视为额外的约束:您刚刚添加了一个无法出现在给定匹配两侧的玩家。与此同时,这位新玩家会产生一系列新的团队组合。这些新团队就像反约束:他们的存在使得更容易形成有效的匹配。

从上面的公式和数据表中可以看出,约束(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}

答案 1 :(得分: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名玩家,因为每个玩家都会产生奇数组合。这个问题在现实生活中是如何处理的?不知何故,一支球队将不得不两次打球。

编辑:此脚本现在可以通过复制其中一个团队来处理6个或7个播放器池。它应该很容易在Python中复制,因为它只是依赖于改组团队,直到他们达到适当的顺序。起初,我觉得我用这种方法做了一些作弊,但鉴于Anonymous的解释是这是一个NP完全问题(假设我正确理解了这意味着:-),这可能是解决问题的最佳方法。小池(它会大于9个左右的池,这取决于你的系统,但幸运的是,这超出了我们的场景范围)。另外,随机排序的优势在于没有人情味,如果玩家在第二次没有得分的情况下必须打两次就会感到不安,这可能会派上用场!这是脚本:

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

答案 2 :(得分: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完成的the minimum set cover problem的实例。也许将集合限制为对可以提供更简单的解决方案,但如果不是这样也不会令人惊讶。

由于你只限于小集,所以只能用蛮力来解决它。

我们知道它至少需要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}}

答案 3 :(得分:0)

必须有更好的方法来做到这一点,但这是一个开始:

import itertools
import operator
from copy import deepcopy as clone

def getPossibleOpponents(numPlayers):
    matches = list(itertools.combinations(itertools.combinations(range(1,numPlayers+1), 2), 2))
    possibleMatches = [match for match in matches if len(set(itertools.chain.from_iterable(match)))==4]
    answer, playedTeams = {}, set()
    opponents = {}
    for team, teams in itertools.groupby(possibleMatches, key=operator.itemgetter(0)):
        playedTeams.add(team)
        opponents[team] = [t for t in next(teams) if t!=team]

    return opponents

def updateOpponents(opponents, playedTeams):
    for team in playedTeams:
        if team in opponents:
            opponents.pop(team)
    for k,v in opponents.items():
        opponents[k] = [team for team in v if team not in playedTeams]

def teamSeatings(opponents, answer=None):
    if answer is None:
        answer = {}
    if not len(opponents):
        if not(len(answer)):
            return None
        print(answer)
            sys.exit(0)

    for k,v in opponents.items():
        if not v:
            return None

        newOpponents = clone(opponents)
        for away in opponents[k]:
            if k in newOpponents:
                newOpponents.pop(k)
            answer[k] = away
            updateOpponents(newOpponents, {itertools.chain.from_iterable(i[0] for i in answer.items())})
            teamSeatings(newOpponents, answer)

if __name__ == "__main__":
    opps = getPossibleOpponents(5)
    teamSeatings(opps)

答案 4 :(得分:0)

在组合学中,这被称为 Whist Round Robin 。也许对于有趣的数学专家比沙滩排球更倾向于吹口哨?但这是另一个故事。

Whist Round Robin

安排锦标赛的问题由两个人组成,面对另一个两人组,称为Whist Round Robin - 阅读更多内容并找到算法here

4n 玩家数量的情况下实施最简单。其他三个案例是使用幽灵玩家和幽灵团队构建的。那个应该面对幽灵队的球员就坐在那一轮。

基本的想法是一个玩家被锁定,让我们说一个玩家。然后其他玩家“旋转”,以便玩家2在第一轮与玩家1合作并且遇到玩家2&amp; 3.下一轮选手1留下并与球员3合作,他们面对2&amp; 2的球员。 4.参见我从上面的链接借来的这个可视化。

enter image description here

我已经实施了那里描述的算法来安排一个具有与沙滩排球相似特征的foosball种类,它就像一个魅力。

你应该没有问题在python中使用它。如果你这样做 - 请回答一些具体问题。祝好运! :)

答案 5 :(得分:0)

问题的一个好模型是完整的无向图

  • 顶点代表玩家。

  • 边缘代表一个由两名球员组成的队伍。

对于每个匹配设置,您希望从不共享顶点的边集中绘制两条边
您将继续绘制边对,直到每条边至少绘制一次。

sample match schedule for the case n=5

由于n个顶点的完整图形中的边数是(n * (n - 1)) / 2,很明显,在这个数字为奇数的情况下,必须使用两个边。

答案 6 :(得分: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),则最终匹配阵列中不应出现比原始组合阵列中出现的次数更多或更少的对(即每对在每个阵列中应恰好出现一次)。在组合数为奇数(n = 6或7)的情况下,匹配数组中应该只有一对出现在组合数组中。永远不应该在匹配数组中出现的次数少于组合数组中的次数。这是输出:

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,当你使用一个纯粹贪婪的算法时,似乎总是有一对没有对手的玩家。我的算法通过返回并从已经匹配的团队中取得一个符合条件的对方来解决这个问题,当且仅当后一个团队可以使用另一个尚未使用的对手时。这似乎是唯一需要的修复(除了调整奇数组合),据我所知,它只需要做一次。