Как отсортировать круговой турнир с максимальным количеством отдыха на игрока?

В круговом турнире участвует n игроков. В каждом раунде все игроки встречаются друг с другом один раз. Количество игр в раунде n * (n-1) / 2. Количество раундов не ограничено. Партии играются по одной без перерывов, поэтому единственный способ отдохнуть — не играть подряд.

Как найти лучший порядок игр со следующими целями (в порядке приоритета)?

  1. Максимально увеличьте наименьшую продолжительность отдыха, т. е. количество игр, прежде чем тот же человек снова сыграет.
  2. Минимизируйте количество наименьших пауз за раунд
  3. Разделите наименьшие паузы как можно поровну между игроками.

Я не нашел другого способа добиться этого, кроме как методом грубой силы: проверить каждую возможную перестановку и сохранить в памяти лучшую.

В моем сценарии было 7 человек. Для n = 7 количество игр равно 7 * 6 / 2 = 21, что означает, что количество перестановок равно 21! = 51090942171709440000. Конечно, проверить такое количество перестановок практически невозможно, поэтому я просто реализовал программу, которая создает случайные списки до m. Для моих целей на тот момент этого было достаточно. Лучшая перестановка, которую я нашел с помощью этого метода (из 100 миллионов), имела 9 однодневных перерывов, и они были разделены между разными игроками примерно поровну.

Каков наиболее эффективный способ получить наилучшую перестановку?

Из возможных примеров кода я бы предпочел Java, JavaScript или Swift.


person KrohnicDev    schedule 28.01.2021    source источник
comment
Вы реализовали циклический алгоритм циклического сдвига? Вы знаете об этом?   -  person MBo    schedule 28.01.2021
comment
Я не реализовал никакого алгоритма, я только рандомизировал порядок m раз, а затем проверил, насколько хорошо случайный порядок удовлетворяет моим критериям. Проделав это 100 миллионов раз, наилучший результат был удовлетворительным, но, конечно, вероятно, есть гораздо лучшие результаты, которые не были обнаружены. Я не знаю об алгоритме циклического сдвига. Не могли бы вы дать ссылку на какую-нибудь статью об этом? Быстрым поиском ничего похожего не нашел.   -  person KrohnicDev    schedule 28.01.2021
comment
Это только одна игра за раз?   -  person user58697    schedule 29.01.2021
comment
Да, игры последовательные.   -  person KrohnicDev    schedule 30.01.2021


Ответы (2)


Вместо того, чтобы пробовать случайные перестановки, мы можем применить генетический алгоритм со случайным ключом (BRKGA). Этот общий метод оптимизации может найти решение для n = 7 только с четырьмя паузами на один матч, все разными игроками:

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

Код С++:

#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, 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, 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, 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, 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(),
                          player_to_min_rest_count.end(), 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, int, 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));
}
person David Eisenstat    schedule 29.01.2021
comment
Будет ли работать такой алгоритм, если количество раундов не ограничено? В этом случае у игрока 5 будет две игры подряд (последняя и первая). - person KrohnicDev; 30.01.2021
comment
@KrohnicDev да, я обновил код. - person David Eisenstat; 30.01.2021

Существует довольно простой алгоритм генерации всех пар:

Ставьте игроков в два ряда. В каждом раунде верхний игрок встречается с соответствующим игроком из нижнего ряда. Если количество игроков нечетное, остальные остаются у одного игрока.

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
person MBo    schedule 28.01.2021
comment
Кажется, это хороший способ совместить пары. Тем не менее, это не лучший по моим 2-м критериям. Для n = 7 наименьший остаток составляет 1 день, но их количество равно 10. Я смог получить 9, просто попробовав случайные порядки. Хорошо, что этот метод распределяет остатки между игроками более равномерно, чем случайный порядок. У каждого игрока есть максимум 2 однодневных отдыха. - person KrohnicDev; 28.01.2021