如何以每位玩家的最大休息时间对循环赛进行排序?

问题描述

循环赛有 n 名玩家。在每一轮中,所有玩家面对对方一次。每轮游戏数为 n * (n-1) / 2。回合数不限。一局一局,不休息,唯一的休息方式就是不要连续打。

如何找到具有以下目标的最佳游戏顺序(按优先顺序)?

  1. 最大化最小的休息时间,即同一个人再次玩之前的游戏次数
  2. 尽量减少每轮最小休止次数
  3. 在玩家之间尽可能平均地分配最小的休息时间

除了蛮力方法之外,我没有想出任何其他方法来实现这一点:检查每种可能的排列并将最好的排列保留在内存中。

我的场景中有 7 个人。对于 n = 7,游戏计数为 7 * 6 / 2 = 21,这意味着排列计数为 21! = 51090942171709440000。当然,检查这么多排列实际上是不可能的,所以我最终只是实现了一个程序,该程序可以创建最多 m随机列表。这对我当时的目的来说已经足够了。我用这种方法找到的最好的排列(在 1 亿个中)有 9 次一天的休息时间,而且它们在不同的玩家之间平均分配。

获得最佳排列的最有效方法是什么?

对于可能的代码示例,我更喜欢 Java、JavaScript 或 Swift。

解决方法

我们可以应用有偏随机密钥遗传算法 (BRKGA),而不是尝试随机排列。这种通用优化技术可以找到 n=7 的解决方案,其中只有 4 场比赛休息,全部由不同的球员进行:

1 4 1
1 3
0 4
2 5
1 6
2 3
4 5
0 6
3 5
1 2
4 6
0 3
1 5
2 6
3 4
0 1
2 4
3 6
0 5
1 4
0 2
5 6

C++ 代码:

#include <algorithm>
#include <array>
#include <iostream>
#include <limits>
#include <numeric>
#include <random>
#include <tuple>
#include <utility>
#include <vector>

namespace {

constexpr int kNumPlayers = 7;
constexpr int kNumMatches = kNumPlayers * (kNumPlayers - 1) / 2;

class Solution {
public:
  template <typename Generator> static Solution Random(Generator &generator);

  template <typename Generator>
  Solution MateWith(const Solution &that,Generator &generator) const;

  std::array<std::tuple<int,int>,kNumMatches> Matches() const;

private:
  Solution() = default;

  std::array<double,kNumMatches> keys_;
};

template <typename Generator> Solution Solution::Random(Generator &generator) {
  Solution solution;
  std::uniform_real_distribution<double> uniform;
  for (int k = 0; k < kNumMatches; k++) {
    solution.keys_[k] = uniform(generator);
  }
  return solution;
}

template <typename Generator>
Solution Solution::MateWith(const Solution &that,Generator &generator) const {
  Solution child;
  std::bernoulli_distribution biased_coin(0.7);
  for (int k = 0; k < kNumMatches; k++) {
    child.keys_[k] = biased_coin(generator) ? this->keys_[k] : that.keys_[k];
  }
  return child;
}

std::array<std::tuple<int,kNumMatches> Solution::Matches() const {
  std::array<std::tuple<double,std::tuple<int,int>>,kNumMatches> rankings;
  {
    int k = 0;
    for (int i = 0; i < kNumPlayers; i++) {
      for (int j = i + 1; j < kNumPlayers; j++) {
        rankings[k] = {keys_[k],{i,j}};
        k++;
      }
    }
  }
  std::sort(rankings.begin(),rankings.end());
  std::array<std::tuple<int,kNumMatches> matches;
  for (int k = 0; k < kNumMatches; k++) {
    matches[k] = std::get<1>(rankings[k]);
  }
  return matches;
}

std::vector<std::tuple<int,int>>
Rests(const std::array<std::tuple<int,kNumMatches> &matches) {
  std::array<int,kNumMatches> last_match;
  for (int k = 0; k < kNumMatches; k++) {
    last_match[std::get<0>(matches[k])] = k - kNumMatches;
    last_match[std::get<1>(matches[k])] = k - kNumMatches;
  }
  std::vector<std::tuple<int,int>> rests;
  for (int k = 0; k < kNumMatches; k++) {
    auto plays = [&](int i) {
      rests.push_back({k - 1 - last_match[i],i});
      last_match[i] = k;
    };
    plays(std::get<0>(matches[k]));
    plays(std::get<1>(matches[k]));
  }
  return rests;
}

std::tuple<int,int,int>
Objective(const std::array<std::tuple<int,kNumMatches> &matches) {
  auto rests = Rests(matches);
  int min_rest = std::get<0>(*std::min_element(rests.begin(),rests.end()));
  std::array<int,kNumPlayers> player_to_min_rest_count;
  std::fill(player_to_min_rest_count.begin(),player_to_min_rest_count.end(),0);
  for (auto [rest,player] : rests) {
    if (rest == min_rest) {
      player_to_min_rest_count[player]++;
    }
  }
  return {-min_rest,std::accumulate(player_to_min_rest_count.begin(),0),*std::max_element(player_to_min_rest_count.begin(),player_to_min_rest_count.end())};
}

std::vector<Solution> SortByFitness(const std::vector<Solution> &population) {
  std::vector<std::tuple<std::tuple<int,const Solution *>> tagged;
  tagged.reserve(population.size());
  for (const Solution &solution : population) {
    tagged.push_back({Objective(solution.Matches()),&solution});
  }
  std::sort(tagged.begin(),tagged.end());
  std::vector<Solution> sorted_population;
  sorted_population.reserve(population.size());
  for (auto [objective,solution] : tagged) {
    sorted_population.push_back(*solution);
  }
  return sorted_population;
}

template <typename Generator> Solution BRKGA(Generator &generator) {
  static constexpr int kRounds = 20000;
  static constexpr int kNumEliteSolutions = 300;
  static constexpr int kNumMatedSolutions = 600;
  static constexpr int kNumRandomSolutions = 100;
  static constexpr int kNumSolutions =
      kNumEliteSolutions + kNumMatedSolutions + kNumRandomSolutions;
  std::vector<Solution> population;
  population.reserve(kNumSolutions);
  for (int i = 0; i < kNumSolutions; i++) {
    population.push_back(Solution::Random(generator));
  }
  for (int r = 0; r < kRounds; r++) {
    population = SortByFitness(population);
    std::vector<Solution> new_population;
    new_population.reserve(kNumSolutions);
    for (int i = 0; i < kNumEliteSolutions; i++) {
      new_population.push_back(population[i]);
    }
    std::uniform_int_distribution<int> elite(0,kNumEliteSolutions - 1);
    std::uniform_int_distribution<int> non_elite(kNumEliteSolutions,kNumSolutions - 1);
    for (int i = 0; i < kNumMatedSolutions; i++) {
      int j = elite(generator);
      int k = non_elite(generator);
      new_population.push_back(
          population[j].MateWith(population[k],generator));
    }
    for (int i = 0; i < kNumRandomSolutions; i++) {
      new_population.push_back(Solution::Random(generator));
    }
    population = std::move(new_population);
  }
  return SortByFitness(population)[0];
}

void PrintSolution(const Solution &solution) {
  auto matches = solution.Matches();
  auto objective = Objective(matches);
  std::cout << -std::get<0>(objective) << ' ' << std::get<1>(objective) << ' '
            << std::get<2>(objective) << '\n';
  for (auto [i,j] : solution.Matches()) {
    std::cout << i << ' ' << j << '\n';
  }
}

} // namespace

int main() {
  std::default_random_engine generator;
  PrintSolution(BRKGA(generator));
}
,

生成所有对的算法相当简单:

将玩家分成两排。在每一轮顶级玩家都会遇到相应的下排玩家。如果玩家人数为奇数,则剩下一名玩家。

A B C
D E F
A:D B:E C:F

回合结束后,除了第一个玩家以外的所有玩家都以循环方式移动

A D B
E F C
A:E D:F B:C 

A E D
F C B
A:F E:C D:B
...

注意如果我们使用上面的游戏顺序(从左到右配对),同一个玩家不会参加后续的两场比赛(小数情况除外)

似乎对于 2*N 位玩家,最小的休息时间是 N-1(最长的是 N+1):

A B C D E F
G H I J K L
AG BH CI DJ EK FL 

A G B C D E 
H I J K L F 
AH GI BJ CK DL EF