组合数少于 100
Posted
技术标签:
【中文标题】组合数少于 100【英文标题】:Number of combinations less than 100 【发布时间】:2020-08-28 06:18:48 【问题描述】:我有 13 个属于不同组的列表:
A 组(列表 1)
B 组(列表 2)、(列表 3)、(列表 4)、(列表 5)、(列表 6)
C 组(列表 7)、(列表 8)、(列表 9)、(列表 10)、(列表 11)
D组(列表12),(列表13)
所有组的总和必须为 1
A 组的取值范围为 0-0.7
B 组的取值范围为 0-0.6
C 组的取值范围为 0-0.9
D 组的取值范围为 0-0.1
我想找到这些列表可以在不超出其组限制的情况下进行的所有不同组合。
例如:
如果对于一个组合 List2 元素 = 0.6,则 List3、List4、List5 和 List6 必须为 0
有没有简单的方法可以做到这一点? (我可以使用 R 或 Python)
(列表取值从 0 到其组的限制,增量为 .1
)
# i.e.
List1=[0.0,0.1,0.2,...,0.7]
List2 = [0.0,0.1,0.2,...,0.6]
# etc.
【问题讨论】:
在我看来,这更像是替换组合,而不是排列。除了最后一个示例外,结果列表似乎已排序。结果列表中的顺序重要吗? 对每个中间列表求和,并检查总和是否小于 60。 @MichaelButscher 结果列表中的顺序并不重要。只是我必须对多个列表执行此操作,然后将它们组合在一起。因为我有 4 个不同的组(所有组加起来必须达到 100)。 A 组(1 个列表)最多可取 70,B 组(5 个列表)最多可取 60(所有 5 个列表加起来不得超过 60),C 组(5 个列表)最多 90 (所有 5 个列表的总和不得超过 90)和 D 组(2 个列表)最多 10 个(2 个列表的总和不得超过 10)。所有这些加起来必须达到 100 请编辑您的帖子以包含此信息,而且如上所述有点令人困惑,提供一些示例数据可能有助于获得答案 @gold_cy 我现在已经编辑了!让我知道是否更容易解决:) 【参考方案1】:根据新问题编辑答案
您可以使用相当快的列表推导式。下面的代码在我的电脑上只用了几秒钟。我使用 John Coleman 的想法首先找到每组总和的可能组合。我还使用了整数而不是浮点数。要将解决方案转换回问题中所述的问题,请将每个列表值除以 10。
from itertools import product
A = range(8) # 1 value from this group
B = range(7) # 5 values from this group (with replacement)
C = range(10) # 5 values from this group (with replacement)
D = range(2) # 2 values from this group (with replacement)
# use John Coleman's idea:
# first find all possible combinations of sums per group
groupsums = [sums for sums in product(A, B, C, D) if sum(sums) == 10]
print(len(groupsums)) # -> 95
def picks(maxi, n):
"""Returns list of combinations of n integers <= maxi
that sum to maxi."""
return [combi for combi in product(range(maxi + 1), repeat=n)
if sum(combi) == maxi]
# make list of lists that each have 13 items from the above ranges,
# with constraints as stated in the question
samples = [[a, b0, b1, b2, b3, b4, c0, c1, c2, c3, c4, d0, d1]
for a, b, c, d in groupsums
for b0, b1, b2, b3, b4 in picks(b, 5)
for c0, c1, c2, c3, c4 in picks(c, 5)
for d0, d1 in picks(d, 2)]
# show the first 5 and last 5 results
for i in range(5):
print(samples[i])
print('...')
for i in range(1, 6):
print(samples[-i])
# show the number of solutions
print(len(samples))
95
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 1]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 1, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 8, 0, 1]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 8, 1, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 7, 0, 1]
...
[7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[7, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[7, 2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[7, 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]
[7, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
313027
【讨论】:
感谢@Arne 的回答!正如gold_cy所说,我已经改变了问题,因为每个人都不够清楚!您认为您对已编辑的问题有类似的解决方案吗? :) @JohnColeman 这就是为什么我要问是否有办法限制总和超过 100 的任何组合的计算 不错的答案。当我将答案中的范围缩小(并将目标总和更改为 10)时,我会得到与您相同的313027
解决方案。【参考方案2】:
(我使用的是整数而不是浮点数,例如 4 代表 4%)
基本思想是找到 4 个数字 a,b,c,d
(每个都满足组界限),总和为 100。我通过蛮力完成了这部分,但它可以优化。然后,对于每个这样的 4 个数字列表,将 a
与所有选择 5 个总和为 b
的数字的方法、所有选择 5 个总和为 c
的数字的方法以及所有方法结合起来选择 2 个总和为 d
的数字。我不想过多地涉及数学,但请参阅 Stars and bars 上的 Wikipedia 文章以解压缩以下代码,其中包含计算组合的函数和生成它们的生成器。
import itertools, math
def count_combos():
count = 0
for a,b,c,d in itertools.product(range(71),range(61),range(91),range(11)):
if a+b+c+d == 100:
count += math.comb(b+4,4)*math.comb(c+4,4)*(d+1)
return count
#uses stars and bars to enumerate k-tuples of nonnegative numbers which sum to n:
#assumes k > 1
def terms(n,k):
for combo in itertools.combinations(range(n+k-1),k-1):
s = [combo[0]]
for i in range(1,k-1):
s.append(combo[i]-combo[i-1]-1)
s.append(n+k - 2 - combo[k-2])
yield s
def all_combos():
for a,b,c,d in itertools.product(range(71),range(61),range(91),range(11)):
if a+b+c+d == 100:
for p in terms(b,5):
for q in terms(c,5):
for r in terms(d,2):
yield [a]+p+q+r
对于解决方案的数量:count_combos()
计算结果为 1470771090600747
,大约是 1.5 万亿。请注意,此函数需要 Python 3.8,因为它使用最近添加的 math.comb
。
实际上完全使用生成器是不可行的,但是,例如:
import random
some_combos = [list(c) for c in itertools.islice(all_combos(),1000000)]
for _ in range(5): print(random.choice(some_combos))
典型输出:
[0, 0, 0, 0, 0, 2, 0, 13, 13, 32, 31, 8, 1]
[0, 0, 0, 0, 0, 2, 0, 5, 36, 28, 20, 6, 3]
[0, 0, 0, 0, 0, 2, 0, 8, 42, 16, 23, 4, 5]
[0, 0, 0, 0, 0, 2, 0, 13, 42, 12, 22, 1, 8]
[0, 0, 0, 0, 0, 2, 0, 10, 69, 7, 3, 8, 1]
编辑时 在我第一次实现count_combos
时有一个错误,我已修复。除此之外,我将保持原样。它指的是问题的早期版本,例如A 组有 71 种可能性,而不仅仅是 8 种。这个答案强调了 1% 的增量会导致解决方案的数量不可行。当我调整代码时,例如range(71)
变为 range(8)
,我得到的计数与 Arne 在他们的回答中所做的完全相同。
【讨论】:
感谢您对我的回答发表评论。当然,对于更复杂的输入,您的答案可以更好地扩展。无论如何,能够独立确认解决方案的数量是件好事。【参考方案3】:正如我在comment 中所建议的那样,我们可以使用Google OR-Tools 来解决诸如此类的优化问题。我将尝试分解每个步骤以使其尽可能清晰,并展示如何扩展以满足您的需求。
from ortools.sat.python import cp_model
model = cp_model.CpModel()
class Data:
def __init__(self, name, ub, model):
self.name = name
self.ub = ub
self.model = model
def make_var(self, repeat):
vars = [self.model.NewIntVar(0, self.ub, f"self.namei") for i in range(repeat)]
return vars
我们定义了一个类Data
,它将保存有关您的每个数组的信息。它将包含名称、上限和对将使用此变量的模型的引用。我们还定义了一个函数make_var
,它将创建一个整数变量供模型求解。
如果您从***别考虑,您的模型实际上归结为以下约束:
a + b1 + b2 + b3 + b4 + b5 + c1 + c2 + c3 + c4 + c5 + d1 + d2 == 100
每组数组都有自己的条件要满足,但从***别来看,这就是我们要解决的问题。 make_var
将创建每个变量并将其附加到模型。如下所示。
# Variables
a = Data("a", 70, model)
var_a = a.make_var(1)
b = Data("b", 60, model)
var_b = b.make_var(5)
c = Data("c", 90, model)
var_c = c.make_var(5)
d = Data("d", 10, model)
var_d = d.make_var(2)
接下来我们将添加每组数组所受的约束。
# Constraints
model.Add(sum(var_a) <= 70)
model.Add(sum(var_b) <= 60)
model.Add(sum(var_c) <= 90)
model.Add(sum(var_d) <= 10)
model.Add(sum(var_a) + sum(var_b) + sum(var_c) + sum(var_d) == 100)
之后,我们将定义一个类来打印我们的解决方案,并限制我们想要返回的解决方案的数量。这是可配置的,您可以更改它,但它对您有意义。该类是该类的修改版本,可以在here找到。
class VarArraySolutionPrinterWithLimit(cp_model.CpSolverSolutionCallback):
"""Print intermediate solutions."""
def __init__(self, variables, limit):
cp_model.CpSolverSolutionCallback.__init__(self)
self.__variables = variables
self.__solution_count = 0
self.__solution_limit = limit
def on_solution_callback(self):
self.__solution_count += 1
for v in self.__variables:
for val in v:
print('%s = %i' % (val, self.Value(val)), end=' ')
print()
if self.__solution_count >= self.__solution_limit:
print('Stop search after %i solutions' % self.__solution_limit)
self.StopSearch()
def solution_count(self):
return self.__solution_count
最后我们为您解决问题。
solver = cp_model.CpSolver()
solution_printer = VarArraySolutionPrinterWithLimit([var_a, var_b, var_c, var_d], 10)
status = solver.SearchForAllSolutions(model, solution_printer)
a0 = 0 b0 = 10 b1 = 0 b2 = 0 b3 = 0 b4 = 0 c0 = 0 c1 = 0 c2 = 90 c3 = 0 c4 = 0 d0 = 0 d1 = 0
a0 = 0 b0 = 9 b1 = 0 b2 = 0 b3 = 0 b4 = 0 c0 = 0 c1 = 0 c2 = 90 c3 = 0 c4 = 0 d0 = 1 d1 = 0
a0 = 0 b0 = 60 b1 = 0 b2 = 0 b3 = 0 b4 = 0 c0 = 0 c1 = 0 c2 = 30 c3 = 0 c4 = 0 d0 = 10 d1 = 0
a0 = 0 b0 = 60 b1 = 0 b2 = 0 b3 = 0 b4 = 0 c0 = 0 c1 = 0 c2 = 30 c3 = 0 c4 = 0 d0 = 9 d1 = 1
a0 = 0 b0 = 0 b1 = 0 b2 = 0 b3 = 0 b4 = 0 c0 = 0 c1 = 0 c2 = 90 c3 = 0 c4 = 0 d0 = 0 d1 = 10
a0 = 0 b0 = 0 b1 = 1 b2 = 0 b3 = 0 b4 = 0 c0 = 0 c1 = 0 c2 = 90 c3 = 0 c4 = 0 d0 = 0 d1 = 9
a0 = 0 b0 = 0 b1 = 0 b2 = 1 b3 = 0 b4 = 0 c0 = 0 c1 = 0 c2 = 90 c3 = 0 c4 = 0 d0 = 0 d1 = 9
a0 = 0 b0 = 0 b1 = 0 b2 = 0 b3 = 1 b4 = 0 c0 = 0 c1 = 0 c2 = 90 c3 = 0 c4 = 0 d0 = 0 d1 = 9
a0 = 0 b0 = 0 b1 = 0 b2 = 0 b3 = 0 b4 = 1 c0 = 0 c1 = 0 c2 = 90 c3 = 0 c4 = 0 d0 = 0 d1 = 9
a0 = 0 b0 = 1 b1 = 0 b2 = 0 b3 = 0 b4 = 0 c0 = 0 c1 = 0 c2 = 90 c3 = 0 c4 = 0 d0 = 0 d1 = 9
Stop search after 10 solutions
我将限制设置为 10 个解决方案,但如前所述,您可以根据需要进行更改。获得解决方案大约需要 9 毫秒,因此这对于您的用例应该有效且快速。
【讨论】:
不错的解决方案。我不知道那个图书馆。 谢谢,这是一个有用的库,它有许多基于优化问题的不同求解器。 太完美了!非常感谢@gold_cy。有没有办法将这些数据写入 CSV 或 XLS 文件以便我可以导出它们? @MonicaOdysseosCY 更改on_solution_callback
所做的事情,改为写入 CSV
我正在尝试先将其设置为 pandas 数据框,但它似乎不起作用def on_solution_callback(self): self.__solution_count += 1 for v in self.__variables: for val in v: df = pd.DataFrame(self.Value(val))
以上是关于组合数少于 100的主要内容,如果未能解决你的问题,请参考以下文章