13

一些背景:
在排球中,球员们在台球中打球来确定排名。团队是成对的玩家。比赛是一对球员对另一对球员。对于这个例子,假设只有一个球场可以进行比赛,当一名球员不比赛时,他们正在坐下/等待。池中的玩家人数将在 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。

4

7 回答 7

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

编辑:此脚本现在将通过复制其中一个团队来处理 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
于 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),则不应有任何对出现在最终匹配数组中的次数多于或少于它们在原始组合数组中出现的次数 (即每对应该在每个数组中恰好出现一次)。在组合数为奇数(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 时,当您使用纯贪心算法时,似乎总是有一对玩家没有对手。我的算法通过返回并从已经匹配的团队中取出符合条件的对手来处理这个问题,当且仅当后一个团队可以使用另一个尚未使用的对手对。这似乎是唯一需要的修复(除了调整奇数的组合),据我所知,它只需要完成一次。

于 2013-07-22T03:23:37.923 回答
0

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

  • 顶点代表玩家。

  • 边代表由两名球员组成的球队。

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

案例 n=5 的样本匹配时间表

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

于 2013-07-21T21:42:46.647 回答
0

There's got to be a better way to do this, but here's a start:

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)
于 2013-07-21T03:26:04.227 回答
0

在组合学中,这称为Whist Round Robin。也许对漫画感兴趣的数学专家更倾向于打惠斯特而不是沙滩排球?但那是另一回事了。

惠斯特循环赛

安排由两个人组成的配对面对另一个两人团队的锦标赛的问题称为 Whist Round Robin - 阅读更多并在此处查找算法。

在有4n个玩家的情况下最容易实现。其他三个案例是使用幽灵玩家和幽灵团队构建的。应该面对幽灵队的球员只是坐在那一轮。

基本思想是一个玩家被锁定,比如说玩家一。然后其他玩家“轮换”,以便玩家 2 在第一轮与玩家 1 组队并与玩家 2 和 3 会面。下一轮玩家 1 留下并与玩家 3 组队,他们面对玩家 2 和 4。看这个可视化我从上面的链接借来的。

在此处输入图像描述

我已经实现了那里描述的算法来安排与沙滩排球具有相似特征的桌上足球比赛,它就像一个魅力。

在 python 中实现它应该没有问题。如果你这样做 - 请回来提出一些具体问题。祝你好运!:)

于 2013-07-21T09:08:04.777 回答