Python3:计算两个列表的所有排列总和为 100 的最有效方法是啥?
Posted
技术标签:
【中文标题】Python3:计算两个列表的所有排列总和为 100 的最有效方法是啥?【英文标题】:Python3: What is the most efficient way to calculate all permutations of two lists summing to 100?Python3:计算两个列表的所有排列总和为 100 的最有效方法是什么? 【发布时间】:2018-07-29 20:20:20 【问题描述】:假设我们有一个股票列表:
stocks = ['AAPL','GOOGL','IBM']
具体的股票无关紧要,重要的是我们在这个列表中有 n 个项目。
假设我们还有一个权重列表,从 0% 到 100%:
weights = list(range(101))
给定 n = 3(或任何其他数字),我需要生成一个矩阵,其中包含所有可能的权重组合,总和为 100%。例如。
0%, 0%, 100%
1%, 0%, 99%
0%, 1%, 99%
etc...
是否有一些 itertools 方法可以做到这一点? numpy的东西?最有效的方法是什么?
【问题讨论】:
你真的需要这个矩阵吗?如果我粗略的计算是正确的,N=3 有大约 30K 的值,N=4 有 4M 的值,并且从那里继续上升。你将如何处理所有这些价值观?您确定您不仅仅需要生成 1000 个可能的值并在它们之间很好地分布,以便您可以绘制某种图形吗?weights
是否总是列表 [0, 1, 2, ..., 100]?
【参考方案1】:
对此进行优化的方法不是找出一种更快的方法来生成排列,而是生成尽可能少的排列。
首先,如果您只想要按排序顺序排列的组合,您会怎么做?
您不需要生成 0 到 100 的所有可能组合,然后对其进行过滤。第一个数字 a
可以是 0 到 100 之间的任意值。第二个数字 b
可以是 0 到 (100-a) 之间的任意值。第三个数字,c
,只能是 100-a-b。所以:
for a in range(0, 101):
for b in range(0, 101-a):
c = 100-a-b
yield a, b, c
现在,我们不再生成 100*100*100
组合以将它们过滤到 100*50*1+1
,而是生成 100*50*1+1
,以实现 2000 倍的加速。
但是,请记住,X * (X/2)**N
周围仍有答案。因此,在X * (X/2)**N
时间而不是X**N
中计算它们可能是最佳的——但它仍然是指数时间。没有办法解决这个问题;毕竟,您想要指数级的结果。
您可以通过itertools.product
结合reduce
或accumulate
寻找使第一部分更简洁的方法,但我认为它最终会变得不那么可读,并且您希望能够扩展到任何任意N
,并且还可以获得所有排列,而不仅仅是排序的排列。所以在你这样做之前保持它是可以理解的,然后在你完成之后寻找方法来浓缩它。
你显然需要经历 N 个步骤。我认为递归比循环更容易理解。
当n
为1时,唯一的组合是(x,)
。
否则,对于从 0 到 x 的每个值 a,您可以拥有该值,以及总和为 x-a 的 n-1 个数字的所有组合。所以:
def sum_to_x(x, n):
if n == 1:
yield (x,)
return
for a in range(x+1):
for result in sum_to_x(x-a, n-1):
yield (a, *result)
现在您只需添加排列,就完成了:
def perm_sum_to_x(x, n):
for combi in sum_to_x(x, n):
yield from itertools.permutations(combi)
但有一个问题:permutations
置换位置,而不是值。所以如果你有,比如说,(100, 0, 0)
,它的六个排列是(100, 0, 0)
,(100, 0, 0)
,(0, 100, 0)
,(0, 0, 100)
,(0, 100, 0)
,(0, 0, 100)
。
如果 N 非常小(就像在您的示例中那样,N=3 和 X=100),只需生成每个组合的所有 6 个排列并过滤它们就可以了:
def perm_sum_to_x(x, n):
for combi in sum_to_x(x, n):
yield from set(itertools.permutations(combi))
...但是如果 N 可以变大,我们也正在谈论很多浪费的工作。
这里有很多关于如何在没有重复值的情况下进行排列的好答案。例如,请参阅this question。从那个答案中借用一个实现:
def perm_sum_to_x(x, n):
for combi in sum_to_x(x, n):
yield from unique_permutations(combi)
或者,如果我们可以拖入SymPy 或more-itertools
:
def perm_sum_to_x(x, n):
for combi in sum_to_x(x, n):
yield from sympy.multiset_permutations(combi)
def perm_sum_to_x(x, n):
for combi in sum_to_x(x, n):
yield from more_itertools.distinct_permutations(combi)
【讨论】:
出色的答案,非常清楚。感谢您抽出宝贵时间。 我认为您的perm_sum_to_x
是不必要的。 sum_to_x
函数已经 只生成一次所有需要的排列(不仅仅是按排序的排列)。例如,list(sum_to_x(2, 3))
给出[(0, 0, 2), (0, 1, 1), (0, 2, 0), (1, 0, 1), (1, 1, 0), (2, 0, 0)]
。所以perm_sum_to_x
增加了对现有排列的不必要重复。【参考方案2】:
您正在寻找的是来自itertools
模块的product
你可以如下图使用它
from itertools import product
weights = list(range(101))
n = 3
lst_of_weights = [i for i in product(weights,repeat=n) if sum(i)==100]
【讨论】:
OP 要求最有效的方法。这将计算数百万个值以产生几千个正确的值,这几乎不是最有效的。 @abarnert,我喜欢你的回答,但 OP 还提到“是否有一些 itertools 方法可以做到这一点”。所以,我的答案是指向那个【参考方案3】:您需要的是combinations_with_replacement
因为在您的问题中您写了 0, 0, 100 这意味着您希望重复,例如 20, 20, 60 等
from itertools import combinations_with_replacement
weights = range(11)
n = 3
list = [i for i in combinations_with_replacement(weights, n) if sum(i) == 10]
print (list)
上面的代码导致
[(0, 0, 10), (0, 1, 9), (0, 2, 8), (0, 3, 7), (0, 4, 6), (0, 5, 5), (1, 1, 8), (1, 2, 7), (1, 3, 6), (1, 4, 5), (2, 2, 6), (2, 3, 5), (2, 4, 4), (3, 3, 4)]
将range(10)
、n
和sum(i) == 10
替换为您需要的任何内容。
【讨论】:
这不会生成所有答案,因为 OP 需要像(0, 1, 99)
和 (1, 0, 99)
这样的排列。而且它和InAFlash's answer 一样低效——它会生成 1000 个组合以将它们过滤到 14 个;随着 X 或 N 的增加,情况会变得更糟; OP 的示例问题已经有 1000000 个组合。【参考方案4】:
这是一个经典的Stars and bars 问题,Python 的itertools
模块确实提供了一个既简单又高效的解决方案,不需要任何额外的过滤。
先解释一下:你想以所有可能的方式在 3 只股票之间分配 100 个“点”。出于说明目的,让我们减少到 10 个点而不是 100 个点,每个点值 10% 而不是 1%。想象一下将这些点写成一个由十个*
字符组成的字符串:
**********
这些是“明星和酒吧”中的“明星”。现在要在 3 只股票中划分十颗星,我们插入两个 |
分隔符(“星和条”中的“条”)。例如,这样一种划分可能如下所示::
**|*******|*
这种特殊的星形和条形组合对应于 20% AAPL、70% GOOGL、10% IBM 的部门。另一个部门可能如下所示:
******||****
这相当于 60% AAPL、0% GOOGL、40% IBM。
很容易说服自己,由十个 *
字符和两个 |
字符组成的每个字符串恰好对应于三只股票中十个点的一个可能的划分。
所以要解决您的问题,我们需要做的就是生成包含十个*
星号和两个|
条形字符的所有可能字符串。或者,换一种方式来考虑,我们想在一个总长度为 12 的字符串中找到所有可能的位置对,我们可以放置两个小节字符。 Python 的itertools.combinations
函数可用于为我们提供那些可能的位置(例如itertools.combinations(range(12), 2)
),然后将每对位置转换回range(10)
的划分很简单,分为三部分:插入一个额外的假想分隔线字符串开头和结尾的字符,然后找到每对分隔符之间的星数。这个星星的数量只是比两个分隔线之间的距离少一。
代码如下:
import itertools
def all_partitions(n, k):
"""
Generate all partitions of range(n) into k pieces.
"""
for c in itertools.combinations(range(n+k-1), k-1):
yield tuple(y-x-1 for x, y in zip((-1,) + c, c + (n+k-1,)))
对于您在问题中给出的情况,您需要all_partitions(100, 3)
。但这会产生5151
分区,以(0, 0, 100)
开头并以(100, 0, 0)
结尾,因此在这里显示结果是不切实际的。相反,以下是较小情况下的结果:
>>> for partition in all_partitions(5, 3):
... print(partition)
...
(0, 0, 5)
(0, 1, 4)
(0, 2, 3)
(0, 3, 2)
(0, 4, 1)
(0, 5, 0)
(1, 0, 4)
(1, 1, 3)
(1, 2, 2)
(1, 3, 1)
(1, 4, 0)
(2, 0, 3)
(2, 1, 2)
(2, 2, 1)
(2, 3, 0)
(3, 0, 2)
(3, 1, 1)
(3, 2, 0)
(4, 0, 1)
(4, 1, 0)
(5, 0, 0)
【讨论】:
以上是关于Python3:计算两个列表的所有排列总和为 100 的最有效方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章