根据大型列表的多个条件查找所有组合

Posted

技术标签:

【中文标题】根据大型列表的多个条件查找所有组合【英文标题】:Finding all combinations based on multiple conditions for a large list 【发布时间】:2019-07-17 14:36:39 【问题描述】:

我正在尝试为 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 人一组,这只是 too many calculations 让我的计算机处理的事情,即使 this 问题的答案暗示了其他事情。我做了一些研究,找不到任何方法来应用上述条件,而不必遍历每个选项。

有没有办法找到最佳团队,通过使上述方法发挥作用或使用替代方法?

编辑: 在文本中,可以找到完整的 CSV 文件 here

【问题讨论】:

这可能是一个根本性的难题。看起来像是背包问题的一个变种:en.wikipedia.org/wiki/Knapsack_problem 我添加了指向完整 CSV 文件的链接。 我相信为了首先生成所有组合,然后修剪无效的团队,您首先必须生成 20062118235172477959495 组合(n 选择 k,其中 n 为 176,k 为 16)。此值表示可能的组合数,其中从可能的 176 个元素中选择了 16 个元素。 看起来像一个离散优化问题。也许你会找到一些灵感here 或here。 目的是通过保持总成本 【参考方案1】:

这是一个经典的operations research 问题。

有大量的算法可以找到最优的(或者只是一个非常好的,取决于算法)解决方案:

混合整数规划 元启发式 约束编程 ...

这是一个使用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 &lt;= i &lt;= nb_cyclists)Yj (0 &lt;= j &lt;= 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 是连续变量,那么这个问题就是polynomial(线性规划),可以使用:

Interior point methodes(多项式解) Simplex(非多项式,但在实践中更有效)

因为这些变量是整数(整数规划或混合整数规划),所以该问题被称为NP_complete 类的一部分(除非您是genious,否则无法使用多项式解决方案解决)。像COIN-OR 这样的求解器使用复杂的Branch & Bound 或Branch & Cut 方法来有效地求解它们。 ortools 提供了一个很好的包装器,可以将 COIN 与 python 一起使用。这些工具是免费和开源的。

所有这些方法的优点是无需迭代所有可能的解决方案即可找到最佳解决方案(并大大减少了组合)。

【讨论】:

非常有趣,非常感谢您的全面回答! 我认为你添加的解释很清楚。但是,我发现here 提供的文档有点稀疏。我的最终目标是找到 16 人的最佳团队,每个阶段选出最好的 10 名车手。我发布的 CSV 文件实际上已经过修改,我原来的 CSV 文件还包含每个骑手的列表以及每个阶段的得分。这个列表看起来像这样[0, 40, 13, 0, 2, 55, 1, 17, 0, 14]。我是否可以通过简单地修改目标来做到这一点,还是必须大量更改代码? 这正是这个问题的难点所在。我正在努力寻找整体表现最好的团队。所以我有一个由 16 名自行车手组成的池子,其中 10 名自行车手的分数计入每天的分数。然后将每天的分数相加得到总分。目的是让这个最终的总分尽可能高。我目前认为这可以通过为每个阶段的目标设置一个系数来实现,但无法完全弄清楚如何实现这样一个事实,即每个阶段只有十名最佳骑手的分数才计入最终分数。 @Thakkennes 我针对新问题发布了新答案。【参考方案2】:

我为您的问题添加了另一个答案:

我发布的 CSV 文件实际上是经过修改的,我原来的 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(如果未选择骑自行车者 i,即如果 Xi = 0,则变量 Zik 不能 = 1)

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 之间的乘积)。幸运的是,这两个二进制文件都有,并且有一个技巧可以将这个公式转换为它的线性形式。

让我们再次引入新变量 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)

然后瞧。这就是数学的美妙之处,你可以用线性方程来模拟很多东西。我提出了一些高级概念,如果您第一眼看不懂,这很正常。


我在这个file 上模拟了每天的得分。

这是解决新问题的 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% 准确,通过一些近似值可以更轻松、更快速地求解模型。

【讨论】:

非常感谢!这看起来很棒。但是,当我尝试使用我的 CSV 以每天正确的分数运行它时,我在第 4 天得到一个 AssertionError,它只选择了 9 个骑手。我不太清楚是什么原因造成的,因为我还没有时间完全深入研究您的代码。你知道是什么原因造成的吗?我已经粘贴了我正在使用的 CSV here。 我找到了问题所在。每天对骑车人的限制设置为最多 10 名骑车人,但这也可以减少骑车人。因此这个约束应该是constraint_cyclist_per_day = solver.Constraint(10, 10),所以它除了选择 10 个骑自行车的人之外别无选择。为清楚起见,这应该是代码中的第 56 行。 @Thakkennes 完全正确!第 5 天,只有 6 名骑车人的积分 > 0。实际模型无需选择超过 6 分。您的修复非常完美,很高兴您找到了。

以上是关于根据大型列表的多个条件查找所有组合的主要内容,如果未能解决你的问题,请参考以下文章

从多个值列表中查找所有不冲突的值组合

Access 2010:根据特定组合框条件过滤字段中包含多个值的报表

如何根据多个条件将 1 个 pandas 数据帧合并或组合到另一个数据帧

根据条件组合列表中的列表

从包含 ACCESS 2013 中的多个表的表单中查找带有组合框的记录

根据多个条件更改班级组合