【问题标题】:For a list of numbers, find all combinations whose cumulative sum stays within bounds对于数字列表,找到累积和保持在范围内的所有组合
【发布时间】:2020-11-26 11:43:43
【问题描述】:

假设我有以下列表: [A, A, A, A, A, B, B, B] 其中A=1, B=-1

查找此列表的所有组合通常很容易 (8nCr3),但是当累积总和达到某些界限值(例如 0 和 4)时,我想省略排列。

因此上面的例子是不正确的,因为累积和是 [1、2、3、454、3、2]。

[A, A, B, B, A, A, B, A] 的排列也很糟糕,因为这里的累积和是 [1, 2, 1, 0, 1, 2, 1, 2]。

我的问题是:我如何计算两个组件的任何有限列表的排列数?

如果存在一个简单地插入分析解决方案的函数,我会非常满意,但迭代/递归函数也应该没问题。

编辑:我实际上并不需要知道排列是什么样的。只要他们的数量就足够了。

【问题讨论】:

  • 到目前为止你尝试了什么?

标签: python combinations permutation combinatorics


【解决方案1】:

一种可能的解决方案是使用itertools.permutations 生成排列,然后检查每个排列。

这种方法存在一些问题:

  • 无效排列的数量可能非常庞大,因此我们将生成大量无效排列。
  • 如果排列的第一项使我们超出了界限,我们知道所有具有相同开头的排列都将无效,但是没有办法让itertools.permutations 跳过它们,所以我们必须生成它们无论如何
  • 我们必须重新计算每个新排列的所有部分和。

因此,我们宁愿构建排列并在每一步测试它们。这样一来,我们就可以在项目越界时中止整个分支。

递归解决方案:

 def bounded_permutations(data, lower, upper, start=None, curr_sum=0):
    if start is None:
        start = []
        
    for idx_first, first in enumerate(data):
        new_sum = curr_sum + first
        if not lower < new_sum < upper:
            continue
        new_data = data[:]
        del new_data[idx_first]
        new_start = start + [first]
        if not new_data:  # we used all values in the list!
            yield new_start
        else:
            yield from bounded_permutations(new_data, lower, upper, new_start, new_sum)

使用您的数据运行示例(好吧,几乎,我在复制粘贴时丢失了一个“A”):

A=1
B=-1
data = [A, A, A, A, B, B, B]
            
count=0
for p in bounded_permutations(data, 0, 4):
    count += 1
    print(p, count)
    

输出:

[1, 1, 1, -1, 1, -1, -1] 1
[1, 1, 1, -1, 1, -1, -1] 2
[1, 1, 1, -1, -1, 1, -1] 3
[1, 1, 1, -1, -1, 1, -1] 4
[1, 1, 1, -1, 1, -1, -1] 5
.
.
.
[1, 1, -1, 1, -1, 1, -1] 575
[1, 1, -1, 1, -1, 1, -1] 576

在这里,我们得到了 576 个有效排列,总共 7 个! = 5040。我们也可以用len(list(bounded_permutations(data, 0, 4))) 来计算它们。

由于您的数据中有重复的值,您会得到许多相同的排列。如果您只想保留唯一的,可以使用set。请注意,您必须将不可散列的列表转换为可散列的元组才能在集合中使用它们:

uniques = set(map(tuple, bounded_permutations(data, 0, 4)))
print(uniques)
print('number of unique permutations:', len(uniques))

输出:

{(1, 1, -1, 1, 1, -1, -1), 
 (1, 1, -1, 1, -1, 1, -1),
 (1, 1, 1, -1, 1, -1, -1),
 (1, 1, 1, -1, -1, 1, -1)}
number of unique permutations: 4

【讨论】:

  • 非常感谢。我相信它有效,但如果我将列表大小增加到 20 的某个地方,该功能将永远存在。
  • 是的,当然,20个元素的排列数是20! = 2432902008176640000,所以大约为 2.4*10^18。如果您每秒可以生成 10^6 个排列,则需要 77147 年才能生成所有排列 - 而且您无法存储它们。该解决方案避免了生成我们确定会失败的解决方案,因此可以避免生成许多解决方案,但根据值和界限,解决方案的数量可能很大。作为旁注,如果您认为其中一个答案回答了您的问题,您可以接受它(左侧的复选标记)和/或投票。
【解决方案2】:

您可以使用itertools 来生成排列和累积列表。返回其累积列表不包含某些特定值的唯一排列的紧凑解决方案是:

import itertools

def contain_specific_value(lst, value_lst):
    return any([True if (item in lst) else False for item in value_lst])

def accumulated_permutations_without_specific_numbers(main_lst, specific_number_lst):
    p = list(itertools.permutations(main_lst, len(main_lst)))
    res = [y for y in p if not contain_specific_value(list(itertools.accumulate(y)), specific_number_lst)]
    return list(set(res))

您只需要将某些绑定值作为列表传递给函数。

为您的清单:

A=1
B=-1
my_list = [A, A, A, A, B, B, B]

res = accumulated_permutations_without_specific_numbers(my_list, [0,4])

输出:

(1, 1, 1, -1, -1, 1, -1)
(1, 1, -1, 1, -1, 1, -1)
(1, 1, -1, 1, 1, -1, -1)
(1, 1, 1, -1, 1, -1, -1)

累计名单:

[1, 2, 3, 2, 1, 2, 1]
[1, 2, 1, 2, 1, 2, 1]
[1, 2, 1, 2, 3, 2, 1]
[1, 2, 3, 2, 3, 2, 1]

【讨论】:

  • 就像 Thierry Lathuille 的回答一样,我认为这个函数对于相当小的列表来说已经很长时间了。不过你的回答很有见地,谢谢。
【解决方案3】:

这是一个使用动态编程(记忆)的递归解决方案,以便立即处理非常大的列表:

from functools import lru_cache

def boundCumSum(A,loBound,hiBound):
    sRange = range(loBound+1,hiBound)
    
    @lru_cache()
    def count(s,nPos,nNeg):
        if nPos==0 and nNeg==0: return int(s == 0)
        if nPos<0 or nNeg<0 or not s in sRange: return 0
        return count(s-1,nPos-1,nNeg)+count(s+1,nPos,nNeg-1)
    
    nPos   = sum(a>0 for a in A)
    nNeg   = sum(a<0 for a in A)
    return count(sum(A),nPos,nNeg)

它的工作方式是从值的总和(始终是累积总和中的最后一个值)开始,然后通过可用的 +1 和 -1 向后工作。递归部分只需要知道有多少正数和负数,并且目标是在它们用尽时达到零,而不会超出界限。这允许使用 lru_cache 装饰器(来自 functools)将以前的结果保存在内存中并避免重新计算它们。这种记忆是获得良好性能的关键,因为递归过程通常具有相同的值模式来计算。

示例输出:

boundCumSum([1,1,1,1,1,-1,-1,-1],0,4)
# 8

boundCumSum([1,1,1,1,1,-1,-1,-1],-2,3)
# 21

boundCumSum([1]*15+[-1]*12,0,4)
# 4096

boundCumSum([1]*150+[-1]*120,0,40)
# 1772031749108746260736075351494970301882401049674141497351113308699264759272661

为了验证这一点,可以使用生成器函数来显示实际解决方案及其累积和:

def genCumSum(A,loBound,hiBound,combo=[]):
    if not A: yield combo; return
    for v,i in {v:i for i,v in enumerate(A)}.items():
        if v<=loBound or v >=hiBound: continue
        yield from genCumSum(A[:i]+A[i+1:],loBound-v,hiBound-v,combo+[v])

...

from itertools import accumulate

for i,solution in enumerate(genCumSum([1,1,1,1,1,-1,-1,-1],0,4),1):
    print(i,solution,list(accumulate(solution)))

1 [1, 1, 1, -1, 1, -1, 1, -1] [1, 2, 3, 2, 3, 2, 3, 2]
2 [1, 1, 1, -1, 1, -1, -1, 1] [1, 2, 3, 2, 3, 2, 1, 2]
3 [1, 1, 1, -1, -1, 1, 1, -1] [1, 2, 3, 2, 1, 2, 3, 2]
4 [1, 1, 1, -1, -1, 1, -1, 1] [1, 2, 3, 2, 1, 2, 1, 2]
5 [1, 1, -1, 1, 1, -1, 1, -1] [1, 2, 1, 2, 3, 2, 3, 2]
6 [1, 1, -1, 1, 1, -1, -1, 1] [1, 2, 1, 2, 3, 2, 1, 2]
7 [1, 1, -1, 1, -1, 1, 1, -1] [1, 2, 1, 2, 1, 2, 3, 2]
8 [1, 1, -1, 1, -1, 1, -1, 1] [1, 2, 1, 2, 1, 2, 1, 2]

...

for i,solution in enumerate(genCumSum([1,1,1,1,1,-1,-1,-1],-2,3),1):
    print(i,solution,list(accumulate(solution)))

1 [1, 1, -1, 1, -1, 1, -1, 1] [1, 2, 1, 2, 1, 2, 1, 2]
2 [1, 1, -1, 1, -1, -1, 1, 1] [1, 2, 1, 2, 1, 0, 1, 2]
3 [1, 1, -1, -1, 1, 1, -1, 1] [1, 2, 1, 0, 1, 2, 1, 2]
4 [1, 1, -1, -1, 1, -1, 1, 1] [1, 2, 1, 0, 1, 0, 1, 2]
5 [1, 1, -1, -1, -1, 1, 1, 1] [1, 2, 1, 0, -1, 0, 1, 2]
6 [1, -1, 1, 1, -1, 1, -1, 1] [1, 0, 1, 2, 1, 2, 1, 2]
7 [1, -1, 1, 1, -1, -1, 1, 1] [1, 0, 1, 2, 1, 0, 1, 2]
8 [1, -1, 1, -1, 1, 1, -1, 1] [1, 0, 1, 0, 1, 2, 1, 2]
9 [1, -1, 1, -1, 1, -1, 1, 1] [1, 0, 1, 0, 1, 0, 1, 2]
10 [1, -1, 1, -1, -1, 1, 1, 1] [1, 0, 1, 0, -1, 0, 1, 2]
11 [1, -1, -1, 1, 1, 1, -1, 1] [1, 0, -1, 0, 1, 2, 1, 2]
12 [1, -1, -1, 1, 1, -1, 1, 1] [1, 0, -1, 0, 1, 0, 1, 2]
13 [1, -1, -1, 1, -1, 1, 1, 1] [1, 0, -1, 0, -1, 0, 1, 2]
14 [-1, 1, 1, 1, -1, 1, -1, 1] [-1, 0, 1, 2, 1, 2, 1, 2]
15 [-1, 1, 1, 1, -1, -1, 1, 1] [-1, 0, 1, 2, 1, 0, 1, 2]
16 [-1, 1, 1, -1, 1, 1, -1, 1] [-1, 0, 1, 0, 1, 2, 1, 2]
17 [-1, 1, 1, -1, 1, -1, 1, 1] [-1, 0, 1, 0, 1, 0, 1, 2]
18 [-1, 1, 1, -1, -1, 1, 1, 1] [-1, 0, 1, 0, -1, 0, 1, 2]
19 [-1, 1, -1, 1, 1, 1, -1, 1] [-1, 0, -1, 0, 1, 2, 1, 2]
20 [-1, 1, -1, 1, 1, -1, 1, 1] [-1, 0, -1, 0, 1, 0, 1, 2]
21 [-1, 1, -1, 1, -1, 1, 1, 1] [-1, 0, -1, 0, -1, 0, 1, 2]

...

print(sum(1 for _ in genCumSum([1]*15+[-1]*12,0,4)))
# 4096

# The last example (with 270 items in the list) would take forever to check

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-09-03
    • 2022-07-12
    • 2018-11-19
    • 1970-01-01
    • 1970-01-01
    • 2013-03-31
    • 2023-03-06
    • 1970-01-01
    相关资源
    最近更新 更多