比赛支架放置算法

Posted

技术标签:

【中文标题】比赛支架放置算法【英文标题】:Tournament bracket placement algorithm 【发布时间】:2012-01-11 10:04:14 【问题描述】:

给定一个对手种子列表(例如种子 1 到 16),我正在尝试编写一个算法,该算法将导致该轮中头号种子玩最低种子,第二种子玩第二低种子等。

将 1 和 16、2 和 15 等分组为“比赛”相当容易,但我还需要确保较高的种子将在随后的回合中与较低的种子比赛。

正确放置的示例括号:

1 对 16
            1对8
8对9
                        1对4
4对13
            4对5
5对12
                                    1对2
2 对 15
            2 对 7
7对10
                        2 对 3
3 对 14
            3对6
6 比 11

如您所见,种子 1 和 2 只在决赛中相遇。

【问题讨论】:

这只是一个我完全没有考虑过的建议:从决赛开始。 这基本上是一个格雷码(如果您使用零索引)。要将标准(二进制反射)格雷码转换为您的编号系统,只需反转位并添加一个。 @Nabb – 我发现 this 看起来很有趣,但我无法理解代码(它是 Ruby,我一无所知) @darkangel:格雷码是到下一个码字的汉明距离为 1 时的代码,与二进制码不同,它仅在 1 位上有所不同。这是一个解释:dba.stackexchange.com/questions/7887/… 原理是正确的。但是,您可能更喜欢按以下特定顺序结束匹配:(1, 16), (9, 8), (5, 12), (13, 4), (3, 14), (11, 6) , (7, 10), (15, 2)。在这里查看我的答案:***.com/a/45566890/760777 【参考方案1】:

javascript 返回一个数组,其中每个偶数索引播放下一个奇数索引

function seeding(numPlayers)
  var rounds = Math.log(numPlayers)/Math.log(2)-1;
  var pls = [1,2];
  for(var i=0;i<rounds;i++)
    pls = nextLayer(pls);
  
  return pls;
  function nextLayer(pls)
    var out=[];
    var length = pls.length*2+1;
    pls.forEach(function(d)
      out.push(d);
      out.push(length-d);
    );
    return out;
  


> seeding(2)
[1, 2]
> seeding(4)
[1, 4, 2, 3]
> seeding(8)
[1, 8, 4, 5, 2, 7, 3, 6]
> seeding(16)
[1, 16, 8, 9, 4, 13, 5, 12, 2, 15, 7, 10, 3, 14, 6, 11]

【讨论】:

似乎正确。但是,您可能更喜欢按以下特定顺序结束匹配:(1, 16), (9, 8), (5, 12), (13, 4), (3, 14), (11, 6) , (7, 10), (15, 2)。在这里查看我的答案:***.com/a/45572051/760777【参考方案2】:

根据您的假设,选手 1 和 2 将参加决赛,选手 1-4 参加半决赛,选手 1-8 参加四分之一决赛等等,因此您可以按照 AakashM 的建议从决赛递归地构建锦标赛.将锦标赛想象成一棵树,其根是最终的。

在根节点中,您的玩家是 1, 2。

要递归扩展树到下一层,将树中最底层的所有节点,一个接一个,并为它们分别创建两个子节点,并将原始节点的玩家之一放置到每个节点创建的子节点。然后添加下一层玩家并将它们映射到游戏中,以便新添加的最差玩家与现有的最佳玩家对战。

这里是第一轮算法:

 1,2  --- create next layer

       1, _
      /         --- now fill the empty slots
 1,2
      \2, _

       1, 4   --- the slots filled in reverse order
      /         
 1,2
      \2, 3   --- create next layer again


             /1, _
       1, 4
      /      \4, _
 1,2                  --- again fill
      \      /2, _
       2, 3
             \3, _

             /1, 8
       1, 4
      /      \4, 5    --- ... and so on
 1,2
      \      /2, 7
       2, 3
             \3, 6

如您所见,它会生成与您发布的相同的树。

【讨论】:

非常有趣的想法,虽然我得考虑如何将它翻译成代码。 我有这个想法以及另一个关于如何在不倒退的情况下做到这一点的想法。我认为他们最终归结为同样的事情,虽然真的。当然,根据每个玩家的种子计算位置的方法确实非常复杂,翻译成代码可能比这更复杂。我当然会采用这种方法。【参考方案3】:

我想出了以下算法。它可能不是超级高效,但我认为它真的不需要。它是用 php 编写的。

<?php
    $players = range(1, 32);
    $count = count($players);
    $numberOfRounds = log($count / 2, 2);

    // Order players.
    for ($i = 0; $i < $numberOfRounds; $i++) 
        $out = array();
        $splice = pow(2, $i); 

        while (count($players) > 0) 

            $out = array_merge($out, array_splice($players, 0, $splice));
            $out = array_merge($out, array_splice($players, -$splice));

                    

        $players = $out;
    

    // Print match list.
    for ($i = 0; $i < $count; $i++) 
        printf('%s vs %s<br />%s', $players[$i], $players[++$i], PHP_EOL);
    
?>

【讨论】:

对此我有一个小问题。这对喂食后续回合有何作用? 我不太清楚你的意思——这只是确保最高种子将在每一轮中扮演最低种子(而第二高的将扮演第二低的种子,等等) 这是一个伟大而简单的解决方案。我做了一个小修改以提高效率。【参考方案4】:

我还写了一个用 PHP 编写的解决方案。我看到了 Patrik Bodin 的回答,但认为一定有更简单的方法。

它完成了黑暗天使的要求:它返回所有种子在正确的位置。比赛与他的示例相同,但按照 更漂亮 的顺序,种子 1 和种子 16 位于模式之外(如网球锦标赛中所见)。

如果没有冷门(意味着高种子玩家总是从低种子玩家手中获胜),您将在决赛中以 1 号种子对 2 号种子结束比赛。

它实际上做了两件事:

    它显示了正确的顺序(这是将轮空放在正确位置的要求)

    它会在正确的位置填充字节(如果需要)

关于单淘汰括号应该是什么样子的完美解释:http://blog.playdriven.com/2011/articles/the-not-so-simple-single-elimination-advantage-seeding/

16 位参与者的代码示例:

<?php

define('NUMBER_OF_PARTICIPANTS', 16);

$participants = range(1,NUMBER_OF_PARTICIPANTS);
$bracket = getBracket($participants);
var_dump($bracket);

function getBracket($participants)

    $participantsCount = count($participants);  
    $rounds = ceil(log($participantsCount)/log(2));
    $bracketSize = pow(2, $rounds);
    $requiredByes = $bracketSize - $participantsCount;

    echo sprintf('Number of participants: %d<br/>%s', $participantsCount, PHP_EOL);
    echo sprintf('Number of rounds: %d<br/>%s', $rounds, PHP_EOL);
    echo sprintf('Bracket size: %d<br/>%s', $bracketSize, PHP_EOL);
    echo sprintf('Required number of byes: %d<br/>%s', $requiredByes, PHP_EOL);    

    if($participantsCount < 2)
    
        return array();
    

    $matches = array(array(1,2));

    for($round=1; $round < $rounds; $round++)
    
        $roundMatches = array();
        $sum = pow(2, $round + 1) + 1;
        foreach($matches as $match)
        
            $home = changeIntoBye($match[0], $participantsCount);
            $away = changeIntoBye($sum - $match[0], $participantsCount);
            $roundMatches[] = array($home, $away);
            $home = changeIntoBye($sum - $match[1], $participantsCount);
            $away = changeIntoBye($match[1], $participantsCount);
            $roundMatches[] = array($home, $away);
        
        $matches = $roundMatches;
    

    return $matches;



function changeIntoBye($seed, $participantsCount)

    //return $seed <= $participantsCount ?  $seed : sprintf('%d (= bye)', $seed);  
    return $seed <= $participantsCount ?  $seed : null;


?>

输出:

Number of participants: 16
Number of rounds: 4
Bracket size: 16
Required number of byes: 0
C:\projects\draw\draw.php:7:
array (size=8)
  0 => 
    array (size=2)
      0 => int 1
      1 => int 16
  1 => 
    array (size=2)
      0 => int 9
      1 => int 8
  2 => 
    array (size=2)
      0 => int 5
      1 => int 12
  3 => 
    array (size=2)
      0 => int 13
      1 => int 4
  4 => 
    array (size=2)
      0 => int 3
      1 => int 14
  5 => 
    array (size=2)
      0 => int 11
      1 => int 6
  6 => 
    array (size=2)
      0 => int 7
      1 => int 10
  7 => 
    array (size=2)
      0 => int 15
      1 => int 2

如果你把 16 变成 6,你会得到:

Number of participants: 6
Number of rounds: 3
Bracket size: 8
Required number of byes: 2
C:\projects\draw\draw.php:7:
array (size=4)
  0 => 
    array (size=2)
      0 => int 1
      1 => null
  1 => 
    array (size=2)
      0 => int 5
      1 => int 4
  2 => 
    array (size=2)
      0 => int 3
      1 => int 6
  3 => 
    array (size=2)
      0 => null
      1 => int 2

【讨论】:

【参考方案5】:
# Here's one in python - it uses nested list comprehension to be succinct:

from math import log, ceil

def seed( n ):
    """ returns list of n in standard tournament seed order

    Note that n need not be a power of 2 - 'byes' are returned as zero
    """

    ol = [1]

    for i in range( ceil( log(n) / log(2) ) ):

        l = 2*len(ol) + 1

        ol = [e if e <= n else 0 for s in [[el, l-el] for el in ol] for e in s]

    return ol

【讨论】:

【参考方案6】:

对于 JavaScript 代码,请使用以下两个函数之一。前者体现了命令式风格并且速度更快。后者具有递归性和简洁性,但仅适用于相对较少的团队(

// imperative style
function foo(n) 
  const arr = new Array(n)
  arr[0] = 0
  for (let i = n >> 1, m = 1; i >= 1; i >>= 1, m = (m << 1) + 1) 
    for (let j = n - i; j > 0; j -= i) 
      arr[j] = m - arr[j -= i]
    
  
  return arr

在这里,您通过镜像已占用的点来一一填充。例如,一号种子队(即号码0)进入最高点。第二个 (1) 占据括号另一半的相反位置。第三队 (2) 在他们的一半括号中镜像1 等等。尽管存在嵌套循环,但该算法具有取决于团队数量的线性时间复杂度。

这里是递归方法:

// functional style
const foo = n =>
  n === 1 ? [0] : foo(n >> 1).reduce((p, c) => [...p, c, n - c - 1], [])

基本上,您执行与前一个函数相同的镜像,但递归:

对于n = 1 团队,它只是[0]

对于 n = 2 团队,将此函数应用于参数 n-1(即 1) 并获取 [0]。然后通过插入 mirrored 来使数组加倍 它们之间的元素在偶数位置。因此,[0] 变为 [0, 1]

对于n = 4 团队,您执行相同的操作,因此[0, 1] 变为[0, 3, 1, 2]

如果您想获得人类可读的输出,请将结果数组的每个元素加一:

const readableArr = arr.map(i => i + 1)

【讨论】:

【参考方案7】: 在每轮比赛中按种子标准对球队进行排序 (如果一轮有n队)第i个位置的队与第n-i+1队比赛

【讨论】:

我需要将球队安排在第一轮,这样进入下一轮的头号种子将自动匹配头号种子和尾号种子等。您可以假设头号种子出于算法的目的,总是赢得比赛。【参考方案8】:

由于在搜索该主题时会出现此问题,并且无法找到解决问题的另一个答案并将种子按“更漂亮”的顺序排列,因此我将添加来自 darkangel 的 PHP 代码版本。我还添加了与更高种子玩家轮空的可能性。

这是在 OO 环境中编码的,因此参与者的数量在 $this->finalists 中,而byes 的数量在 $this->byes 中。我只测试了没有 byes 和两个 byes 的代码。

  public function getBracket() 
      $players = range(1, $this->finalists);
      for ($i = 0; $i < log($this->finalists / 2, 2); $i++) 
        $out = array();
        $reverse = false;
        foreach ($players as $player) 
          $splice = pow(2, $i);
          if ($reverse) 
            $out = array_merge($out, array_splice($players, -$splice));
            $out = array_merge($out, array_splice($players, 0, $splice));
            $reverse = false;
           else 
            $out = array_merge($out, array_splice($players, 0, $splice));
            $out = array_merge($out, array_splice($players, -$splice));
            $reverse = true;
          
        
        $players = $out;
      
      if ($this->byes) 
        for ($i = 0; $i < $this->byes; $i++ ) 
          for ($j = (($this->finalists / pow(2, $i)) - 1); $j > 0; $j--) 
            $newPlace = ($this->finalists / pow(2, $i)) - 1;
            if ($players[$j] > ($this->finalists / (pow(2 ,($i + 1))))) 
              $player = $players[$j];
              unset($players[$j]);
              array_splice($players, $newPlace, 0, $player);
            
          
        
        for ($i = 0; $i < $this->finalists / (pow(2, $this->byes)); $i++ ) 
          $swap[] = $players[$i];
        
        for ($i = 0; $i < $this->finalists /(pow(2, $this->byes)); $i++ ) 
          $players[$i] = $swap[count($swap) - 1 - $i];
        
        return array_reverse($players);
      
      return $players;
    

【讨论】:

【参考方案9】:

我开发了一个 PHP / Laravel 插件,该插件生成带有/不带有初步循环的括号。也许它对你有用,我不知道你使用的是什么技术。这里是github。

https://github.com/xoco70/kendo-tournaments

希望对你有帮助!

【讨论】:

【参考方案10】:

A C 版本。

int * pctournamentSeedArray(int PlayerCnt)

    int * Array;
    int * PrevArray;
    int i;

    Array = meAlloc(sizeof(int) * PlayerCnt);

    if (PlayerCnt == 2)
    
        Array[0] = 0;
        Array[1] = 1;
        return Array;
    

    PrevArray = pctournamentSeedArray(PlayerCnt / 2);
    for (i = 0; i < PlayerCnt;i += 2)
    
        Array[i] = PrevArray[i / 2];
        Array[i + 1] = (PlayerCnt - 1) - Array[i] ;
    
    meFree(PrevArray);
    return Array;

【讨论】:

以上是关于比赛支架放置算法的主要内容,如果未能解决你的问题,请参考以下文章

算法:淘汰不再有机会赢得比赛的玩家

比赛调度算法起点

算法设计比赛做啥算法好

比赛日程安排算法,每个队员发挥特定的比赛数量

两队选手每队5人进行一对一的比赛(算法)

小比赛的总结吧