Поиск всех комбинаций на основе нескольких условий для большого списка

Я пытаюсь рассчитать оптимальную команду для игры Fantasy Cycling. У меня есть csv файл, содержащий 176 велосипедистов, их команды, количество набранных ими очков и цену, которую они могли бы получить в моей команде. Я пытаюсь найти самую результативную команду из 16 велосипедистов.

Правила, которые применяются к составу любой команды:

  • Общая стоимость команды не может превышать 100.
  • В фантазийной команде может быть не более 4 велосипедистов из одной команды.

Короткая выдержка из моего csv файла может быть найдена ниже.

THOMAS Geraint,Team INEOS,142,13
SAGAN Peter,BORA - hansgrohe,522,11.5
GROENEWEGEN Dylan,Team Jumbo-Visma,205,11
FUGLSANG Jakob,Astana Pro Team,46,10
BERNAL Egan,Team INEOS,110,10
BARDET Romain,AG2R La Mondiale,21,9.5
QUINTANA Nairo,Movistar Team,58,9.5
YATES Adam,Mitchelton-Scott,40,9.5
VIVIANI Elia,Deceuninck - Quick Step,273,9.5
YATES Simon,Mitchelton-Scott,13,9
EWAN Caleb,Lotto Soudal,13,9

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

database_csv = pd.read_csv('renner_db_example.csv')
renners = database_csv.to_dict(orient='records')

budget = 100
max_same_team = 4
team_total = 16

combos = itertools.combinations(renners,team_total)
usable_combos = []

for i in combos:
    if sum(persoon["Waarde"] for persoon in i)  < budget and all(z <= max_same_team for z in [len(list(group)) for key, group in groupby([persoon["Ploeg"] for persoon in i])]) == True:   
    usable_combos.append(i)    

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

Есть ли способ найти оптимальную команду, либо с помощью вышеуказанного подхода, либо с помощью альтернативного подхода?

Редактировать: в тексте полный CSV файл можно найти здесь

Ответ 1

Это классическая задача исследования операций.

Существует множество алгоритмов, которые позволяют найти оптимальное (или просто очень хорошее в зависимости от алгоритма) решение:

  • Смешанное целочисленное программирование
  • метаэвристики
  • Ограничение программирования
  • ...

Вот код, который найдет оптимальное решение с использованием MIP, библиотеки ortools и решателя по умолчанию COIN-OR:

from ortools.linear_solver import pywraplp
import pandas as pd


solver = pywraplp.Solver('cyclist', pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING)    
cyclist_df = pd.read_csv('cyclists.csv')

# Variables

variables_name = {}
variables_team = {}

for _, row in cyclist_df.iterrows():
    variables_name[row['Naam']] = solver.IntVar(0, 1, 'x_{}'.format(row['Naam']))
    if row['Ploeg'] not in variables_team:
        variables_team[row['Ploeg']] = solver.IntVar(0, solver.infinity(), 'y_{}'.format(row['Ploeg']))

# Constraints

# Link cyclist <-> team
for team, var in variables_team.items():
    constraint = solver.Constraint(0, solver.infinity())
    constraint.SetCoefficient(var, 1)
    for cyclist in cyclist_df[cyclist_df.Ploeg == team]['Naam']:
        constraint.SetCoefficient(variables_name[cyclist], -1)

# Max 4 cyclist per team
for team, var in variables_team.items():
    constraint = solver.Constraint(0, 4)
    constraint.SetCoefficient(var, 1)

# Max cyclists
constraint_max_cyclists = solver.Constraint(16, 16)
for cyclist in variables_name.values():
    constraint_max_cyclists.SetCoefficient(cyclist, 1)

# Max cost
constraint_max_cost = solver.Constraint(0, 100)
for _, row in cyclist_df.iterrows():
    constraint_max_cost.SetCoefficient(variables_name[row['Naam']], row['Waarde'])    

# Objective 
objective = solver.Objective()
objective.SetMaximization()

for _, row in cyclist_df.iterrows():
    objective.SetCoefficient(variables_name[row['Naam']], row['Punten totaal:'])

# Solve and retrieve solution     
solver.Solve()

chosen_cyclists = [key for key, variable in variables_name.items() if variable.solution_value() > 0.5]

print(cyclist_df[cyclist_df.Naam.isin(chosen_cyclists)])

Принты:

    Naam                Ploeg                       Punten totaal:  Waarde
1   SAGAN Peter         BORA - hansgrohe            522             11.5
2   GROENEWEGEN         Dylan   Team Jumbo-Visma    205             11.0
8   VIVIANI Elia        Deceuninck - Quick Step     273             9.5
11  ALAPHILIPPE Julian  Deceuninck - Quick Step     399             9.0
14  PINOT Thibaut       Groupama - FDJ              155             8.5
15  MATTHEWS Michael    Team Sunweb                 323             8.5
22  TRENTIN Matteo      Mitchelton-Scott            218             7.5
24  COLBRELLI Sonny     Bahrain Merida              238             6.5
25  VAN AVERMAET Greg   CCC Team                    192             6.5
44  STUYVEN Jasper      Trek - Segafredo            201             4.5
51  CICCONE Giulio      Trek - Segafredo            153             4.0
82  TEUNISSEN Mike      Team Jumbo-Visma            255             3.0
83  HERRADA Jesús       Cofidis, Solutions Crédits  255             3.0
104 NIZZOLO Giacomo     Dimension Data              121             2.5
123 MEURISSE Xandro     Wanty - Groupe Gobert       141             2.0
151 TRATNIK Jan Bahrain Merida                      87              1.0

Как этот код решает проблему? Как сказал @KyleParsons, это похоже на проблему ранца и может быть смоделировано с помощью целочисленного программирования.

Определим переменные Xi (0 <= я <= nb_cyclists) и Yj (0 <= j <= nb_teams).

Xi = 1 if cyclist n°i is chosen, =0 otherwise

Yj = n where n is the number of cyclists chosen within team j

Чтобы определить связь между этими переменными, вы можете смоделировать эти ограничения:

# Link cyclist <-> team
For all j, Yj >= sum(Xi, for all i where Xi is part of team j)

Чтобы выбрать только 4 велосипедиста на команду максимум, вы создаете эти ограничения:

# Max 4 cyclist per team
For all j, Yj <= 4

Чтобы выбрать 16 велосипедистов, вот соответствующие ограничения:

# Min 16 cyclists 
sum(Xi, 1<=i<=nb_cyclists) >= 16
# Max 16 cyclists 
sum(Xi, 1<=i<=nb_cyclists) <= 16

Ограничение стоимости:

# Max cost 
sum(ci * Xi, 1<=i<=n_cyclists) <= 100 
# where ci = cost of cyclist i

Тогда вы можете максимизировать

# Objective
max sum(pi * Xi, 1<=i<=n_cyclists)
# where pi = nb_points of cyclist i

Обратите внимание, что мы моделируем задачу с использованием линейных объективных и линейных неравенств. Если Xi и Yj будут непрерывными переменными, эта проблема будет полиномиальной (линейное программирование) и может быть решена с помощью:

Поскольку эти переменные являются целыми числами (целочисленное программирование или смешанное целочисленное программирование), проблема известна как часть класса NP_complete (не может быть решена с помощью полиномиальных решений, если вы не гениальный). Решатели, такие как COIN-OR используют сложные методы Branch & Bound или Branch & Cut для их эффективного решения. ortools предоставляет хорошую оболочку для использования COIN с python. Эти инструменты являются бесплатными и с открытым исходным кодом.

Все эти методы имеют преимущество в нахождении оптимального решения без итерации всех возможных решений (и значительно сокращают комбинаторику).

Ответ 2

Я добавляю другой ответ на ваш вопрос:

CSV, который я разместил, был фактически изменен, мой оригинальный также содержит список для каждого гонщика с их оценкой для каждого этапа. Этот список выглядит следующим образом [0, 40, 13, 0, 2, 55, 1, 17, 0, 14]. Я пытаюсь найти команду, которая показывает лучшие результаты в целом. Таким образом, у меня есть пул из 16 велосипедистов, из которых счет в 10 велосипедистов засчитывается в счет за каждый день. Баллы за каждый день затем суммируются для получения общего балла. Цель состоит в том, чтобы получить этот итоговый общий балл как можно выше.

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

Давайте введем новую переменную:

Zik = 1 if cyclist i is selected and is one of the top 10 in your team on day k

Вам нужно добавить эти ограничения, чтобы связать переменные Zik и Xi (переменная Zik не может быть = 1, если велосипедист я не выбран, т.е. если Xi = 0)

For all i, sum(Zik, 1<=k<=n_days) <= n_days * Xi

И эти ограничения для выбора 10 велосипедистов в день:

For all k, sum(Zik, 1<=i<=n_cyclists) <= 10

Наконец, ваша цель может быть записана так:

Maximize sum(pik * Xi * Zik, 1<=i<=n_cyclists, 1 <= k <= n_days)
# where pik = nb_points of cyclist i at day k

И здесь мыслящая часть. Цель, написанная таким образом, не является линейной (обратите внимание на умножение между двумя переменными X и Z). К счастью, есть и двоичные файлы, и есть хитрость, чтобы преобразовать эту формулу в ее линейную форму.

enter image description here

Давайте снова введем новые переменные Lik (Lik = Xi * Zik) для линеаризации цели.

Теперь цель может быть записана следующим образом и быть линейной:

Maximize sum(pik * Lik, 1<=i<=n_cyclists, 1 <= k <= n_days)
# where pik = nb_points of cyclist i at day k

И теперь нам нужно добавить эти ограничения, чтобы Lik равнялся Xi * Zik:

For all i,k : Xi + Zik - 1 <= Lik
For all i,k : Lik <= 1/2 * (Xi + Zik)

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


В этом файле я смоделировал колонку баллов за день.

Вот код Python для решения новой проблемы:

import ast
from ortools.linear_solver import pywraplp
import pandas as pd


solver = pywraplp.Solver('cyclist', pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING)
cyclist_df = pd.read_csv('cyclists_day.csv')
cyclist_df['Punten_day'] = cyclist_df['Punten_day'].apply(ast.literal_eval)

# Variables
variables_name = {}
variables_team = {}
variables_name_per_day = {}
variables_linear = {}

for _, row in cyclist_df.iterrows():
    variables_name[row['Naam']] = solver.IntVar(0, 1, 'x_{}'.format(row['Naam']))
    if row['Ploeg'] not in variables_team:
        variables_team[row['Ploeg']] = solver.IntVar(0, solver.infinity(), 'y_{}'.format(row['Ploeg']))

    for k in range(10):
        variables_name_per_day[(row['Naam'], k)] = solver.IntVar(0, 1, 'z_{}_{}'.format(row['Naam'], k))
        variables_linear[(row['Naam'], k)] = solver.IntVar(0, 1, 'l_{}_{}'.format(row['Naam'], k))

# Link cyclist <-> team
for team, var in variables_team.items():
    constraint = solver.Constraint(0, solver.infinity())
    constraint.SetCoefficient(var, 1)
    for cyclist in cyclist_df[cyclist_df.Ploeg == team]['Naam']:
        constraint.SetCoefficient(variables_name[cyclist], -1)

# Max 4 cyclist per team
for team, var in variables_team.items():
    constraint = solver.Constraint(0, 4)
    constraint.SetCoefficient(var, 1)

# Max cyclists
constraint_max_cyclists = solver.Constraint(16, 16)
for cyclist in variables_name.values():
    constraint_max_cyclists.SetCoefficient(cyclist, 1)

# Max cost
constraint_max_cost = solver.Constraint(0, 100)
for _, row in cyclist_df.iterrows():
    constraint_max_cost.SetCoefficient(variables_name[row['Naam']], row['Waarde'])

# Link Zik and Xi
for name, cyclist in variables_name.items():
    constraint_link_cyclist_day = solver.Constraint(-solver.infinity(), 0)
    constraint_link_cyclist_day.SetCoefficient(cyclist, - 10)
    for k in range(10):
        constraint_link_cyclist_day.SetCoefficient(variables_name_per_day[name, k], 1)

# Min/Max 10 cyclists per day
for k in range(10):
    constraint_cyclist_per_day = solver.Constraint(10, 10)
    for name in cyclist_df.Naam:
        constraint_cyclist_per_day.SetCoefficient(variables_name_per_day[name, k], 1)

# Linearization constraints 
for name, cyclist in variables_name.items():
    for k in range(10):
        constraint_linearization1 = solver.Constraint(-solver.infinity(), 1)
        constraint_linearization2 = solver.Constraint(-solver.infinity(), 0)

        constraint_linearization1.SetCoefficient(cyclist, 1)
        constraint_linearization1.SetCoefficient(variables_name_per_day[name, k], 1)
        constraint_linearization1.SetCoefficient(variables_linear[name, k], -1)

        constraint_linearization2.SetCoefficient(cyclist, -1/2)
        constraint_linearization2.SetCoefficient(variables_name_per_day[name, k], -1/2)
        constraint_linearization2.SetCoefficient(variables_linear[name, k], 1)

# Objective 
objective = solver.Objective()
objective.SetMaximization()

for _, row in cyclist_df.iterrows():
    for k in range(10):
        objective.SetCoefficient(variables_linear[row['Naam'], k], row['Punten_day'][k])

solver.Solve()

chosen_cyclists = [key for key, variable in variables_name.items() if variable.solution_value() > 0.5]

print('\n'.join(chosen_cyclists))

for k in range(10):
    print('\nDay {} :'.format(k + 1))
    chosen_cyclists_day = [name for (name, day), variable in variables_name_per_day.items() 
                       if (day == k and variable.solution_value() > 0.5)]
    assert len(chosen_cyclists_day) == 10
    assert all(chosen_cyclists_day[i] in chosen_cyclists for i in range(10))
    print('\n'.join(chosen_cyclists_day))

Вот результаты:

Твоя команда:

SAGAN Peter
GROENEWEGEN Dylan
VIVIANI Elia
ALAPHILIPPE Julian
PINOT Thibaut
MATTHEWS Michael
TRENTIN Matteo
COLBRELLI Sonny
VAN AVERMAET Greg
STUYVEN Jasper
BENOOT Tiesj
CICCONE Giulio
TEUNISSEN Mike
HERRADA Jesús
MEURISSE Xandro
GRELLIER Fabien

Выбранные велосипедисты в день

Day 1 :
SAGAN Peter
VIVIANI Elia
ALAPHILIPPE Julian
MATTHEWS Michael
COLBRELLI Sonny
VAN AVERMAET Greg
STUYVEN Jasper
CICCONE Giulio
TEUNISSEN Mike
HERRADA Jesús

Day 2 :
SAGAN Peter
ALAPHILIPPE Julian
MATTHEWS Michael
TRENTIN Matteo
COLBRELLI Sonny
VAN AVERMAET Greg
STUYVEN Jasper
TEUNISSEN Mike
NIZZOLO Giacomo
MEURISSE Xandro

Day 3 :
SAGAN Peter
GROENEWEGEN Dylan
VIVIANI Elia
MATTHEWS Michael
TRENTIN Matteo
VAN AVERMAET Greg
STUYVEN Jasper
CICCONE Giulio
TEUNISSEN Mike
HERRADA Jesús

Day 4 :
SAGAN Peter
VIVIANI Elia
PINOT Thibaut
MATTHEWS Michael
TRENTIN Matteo
COLBRELLI Sonny
VAN AVERMAET Greg
STUYVEN Jasper
TEUNISSEN Mike
HERRADA Jesús

Day 5 :
SAGAN Peter
VIVIANI Elia
ALAPHILIPPE Julian
PINOT Thibaut
MATTHEWS Michael
TRENTIN Matteo
COLBRELLI Sonny
VAN AVERMAET Greg
CICCONE Giulio
HERRADA Jesús

Day 6 :
SAGAN Peter
GROENEWEGEN Dylan
VIVIANI Elia
ALAPHILIPPE Julian
MATTHEWS Michael
TRENTIN Matteo
COLBRELLI Sonny
STUYVEN Jasper
CICCONE Giulio
TEUNISSEN Mike

Day 7 :
SAGAN Peter
VIVIANI Elia
ALAPHILIPPE Julian
MATTHEWS Michael
COLBRELLI Sonny
VAN AVERMAET Greg
STUYVEN Jasper
TEUNISSEN Mike
HERRADA Jesús
MEURISSE Xandro

Day 8 :
SAGAN Peter
GROENEWEGEN Dylan
VIVIANI Elia
ALAPHILIPPE Julian
MATTHEWS Michael
STUYVEN Jasper
TEUNISSEN Mike
HERRADA Jesús
NIZZOLO Giacomo
MEURISSE Xandro

Day 9 :
SAGAN Peter
GROENEWEGEN Dylan
VIVIANI Elia
ALAPHILIPPE Julian
PINOT Thibaut
TRENTIN Matteo
COLBRELLI Sonny
VAN AVERMAET Greg
TEUNISSEN Mike
HERRADA Jesús

Day 10 :
SAGAN Peter
GROENEWEGEN Dylan
VIVIANI Elia
PINOT Thibaut
COLBRELLI Sonny
STUYVEN Jasper
CICCONE Giulio
TEUNISSEN Mike
HERRADA Jesús
NIZZOLO Giacomo

Давайте сравним результаты ответа 1 и ответа 2 print(solver.Objective().Value()):

Вы получаете 3738.0 с первой моделью, 3129.087388325567 со второй. Значение ниже, потому что вы выбираете только 10 велосипедистов на ступень вместо 16.

Теперь, если оставить первое решение и использовать новый метод оценки, мы получим 3122.9477585307413

Мы могли бы считать, что первая модель достаточно хороша: нам не нужно было вводить новые переменные/ограничения, модель остается простой, и мы получили решение, почти такое же хорошее, как и сложная модель. Иногда нет необходимости быть на 100% точным, и модель может быть решена более легко и быстро с некоторыми приближениями.