【问题标题】:Generating a numpy array with all combinations of numbers that sum to less than a given number使用总和小于给定数字的所有数字组合生成一个 numpy 数组
【发布时间】:2016-04-05 19:49:54
【问题描述】:

有几个在 Python 中使用 numpy 生成所有组合的数组的优雅示例。例如这里的答案:Using numpy to build an array of all combinations of two arrays

现在假设有一个额外的约束,即所有数字的总和不能超过给定常数K。使用生成器和itertools.product,例如K=3,我们希望三个变量的组合范围为0-1,0-3和0-2,我们可以这样做:

from itertools import product
K = 3
maxRange = np.array([1,3,2])
states = np.array([i for i in product(*(range(i+1) for i in maxRange)) if sum(i)<=K])

返回

array([[0, 0, 0],
       [0, 0, 1],
       [0, 0, 2],
       [0, 1, 0],
       [0, 1, 1],
       [0, 1, 2],
       [0, 2, 0],
       [0, 2, 1],
       [0, 3, 0],
       [1, 0, 0],
       [1, 0, 1],
       [1, 0, 2],
       [1, 1, 0],
       [1, 1, 1],
       [1, 2, 0]])

原则上,https://stackoverflow.com/a/25655090/1479342 的方法可用于生成所有可能的组合而不受约束,然后选择总和小于K 的组合子集。但是,这种方法会产生比必要更多的组合,尤其是当 Ksum(maxRange) 相比相对较小时。

必须有一种方法可以更快并降低内存使用率。如何使用矢量化方法(例如使用np.indices)来实现这一点?

【问题讨论】:

  • 从技术上讲,这些是(多)集合的子集,而不是组合。
  • 位置k的可能值的范围是否总是0..Nk,或者解决方案是否需要考虑每个位置的任意一组可能值?
  • @cxrodgers 对于 0...Nk 的情况的解决方案会很棒。 k 的范围将是 np.arange(minRange[k],maxRange[k]+1) 的解决方案会更好,但如果我没记错的话,可以直接从第一个解决方案中推导出来。对于任意集合,这会很复杂,因为可以利用的结构很少。

标签: python numpy combinations


【解决方案1】:

已编辑

  1. 为了完整起见,我在这里添加了 OP 的代码:

    def partition0(max_range, S):
        K = len(max_range)
        return np.array([i for i in itertools.product(*(range(i+1) for i in max_range)) if sum(i)<=S])
    
  2. 第一种方法是纯np.indices。小输入速度很快,但会消耗大量内存(OP 已经指出这不是他的意思)。

    def partition1(max_range, S):
        max_range = np.asarray(max_range, dtype = int)
        a = np.indices(max_range + 1)
        b = a.sum(axis = 0) <= S
        return (a[:,b].T)
    
  3. 循环方法似乎比上面的方法要好得多:

    def partition2(max_range, max_sum):
        max_range = np.asarray(max_range, dtype = int).ravel()        
        if(max_range.size == 1):
            return np.arange(min(max_range[0],max_sum) + 1, dtype = int).reshape(-1,1)
        P = partition2(max_range[1:], max_sum)
        # S[i] is the largest summand we can place in front of P[i]            
        S = np.minimum(max_sum - P.sum(axis = 1), max_range[0])
        offset, sz = 0, S.size
        out = np.empty(shape = (sz + S.sum(), P.shape[1]+1), dtype = int)
        out[:sz,0] = 0
        out[:sz,1:] = P
        for i in range(1, max_range[0]+1):
            ind, = np.nonzero(S)
            offset, sz = offset + sz, ind.size
            out[offset:offset+sz, 0] = i
            out[offset:offset+sz, 1:] = P[ind]
            S[ind] -= 1
        return out
    
  4. 经过短暂的思考,我能够更进一步。如果我们事先知道可能的分区数量,我们可以一次分配足够的内存。 (有点类似于an already linked thread中的cartesian。)

    首先,我们需要一个计算分区的函数。

    def number_of_partitions(max_range, max_sum):
        '''
        Returns an array arr of the same shape as max_range, where
        arr[j] = number of admissible partitions for 
                 j summands bounded by max_range[j:] and with sum <= max_sum
        '''
        M = max_sum + 1
        N = len(max_range) 
        arr = np.zeros(shape=(M,N), dtype = int)    
        arr[:,-1] = np.where(np.arange(M) <= min(max_range[-1], max_sum), 1, 0)
        for i in range(N-2,-1,-1):
            for j in range(max_range[i]+1):
                arr[j:,i] += arr[:M-j,i+1] 
        return arr.sum(axis = 0)
    

    主要功能:

    def partition3(max_range, max_sum, out = None, n_part = None):
        if out is None:
            max_range = np.asarray(max_range, dtype = int).ravel()
            n_part = number_of_partitions(max_range, max_sum)
            out = np.zeros(shape = (n_part[0], max_range.size), dtype = int)
    
        if(max_range.size == 1):
            out[:] = np.arange(min(max_range[0],max_sum) + 1, dtype = int).reshape(-1,1)
            return out
    
        P = partition3(max_range[1:], max_sum, out=out[:n_part[1],1:], n_part = n_part[1:])        
        # P is now a useful reference
    
        S = np.minimum(max_sum - P.sum(axis = 1), max_range[0])
        offset, sz  = 0, S.size
        out[:sz,0] = 0
        for i in range(1, max_range[0]+1):
            ind, = np.nonzero(S)
            offset, sz = offset + sz, ind.size
            out[offset:offset+sz, 0] = i
            out[offset:offset+sz, 1:] = P[ind]
            S[ind] -= 1
        return out
    
  5. 一些测试:

    max_range = [3, 4, 6, 3, 4, 6, 3, 4, 6]
    for f in [partition0, partition1, partition2, partition3]:
        print(f.__name__ + ':')
        for max_sum in [5, 15, 25]:
            print('Sum %2d: ' % max_sum, end = '')
            %timeit f(max_range, max_sum)
        print()
    
    partition0:
    Sum  5: 1 loops, best of 3: 859 ms per loop
    Sum 15: 1 loops, best of 3: 1.39 s per loop
    Sum 25: 1 loops, best of 3: 3.18 s per loop
    
    partition1:
    Sum  5: 10 loops, best of 3: 176 ms per loop
    Sum 15: 1 loops, best of 3: 224 ms per loop
    Sum 25: 1 loops, best of 3: 403 ms per loop
    
    partition2:
    Sum  5: 1000 loops, best of 3: 809 µs per loop
    Sum 15: 10 loops, best of 3: 62.5 ms per loop
    Sum 25: 1 loops, best of 3: 262 ms per loop
    
    partition3:
    Sum  5: 1000 loops, best of 3: 853 µs per loop
    Sum 15: 10 loops, best of 3: 59.1 ms per loop
    Sum 25: 1 loops, best of 3: 249 ms per loop
    

    还有更大的:

    %timeit partition0([3,6] * 5, 20)
    1 loops, best of 3: 11.9 s per loop
    
    %timeit partition1([3,6] * 5, 20)
    The slowest run took 12.68 times longer than the fastest. This could mean that an intermediate result is being cached 
    1 loops, best of 3: 2.33 s per loop
    # MemoryError in another test
    
    %timeit partition2([3,6] * 5, 20)
    1 loops, best of 3: 877 ms per loop
    
    %timeit partition3([3,6] * 5, 20)
    1 loops, best of 3: 739 ms per loop
    

【讨论】:

  • 这实际上是我在原始问题中提到的方法,由于它的内存消耗,我认为它不合适。
  • 哦,我明白了。我误解了你关于np.indices 的话。我会尝试找到更有用的东西,否则我稍后会删除此答案。
  • 更新了答案。我可以稍后添加一些测试。
  • 我想你在你的意思是 partition4 的地方写了 partition3。我建议使用更新的编号进行编辑。
  • @Forzaa 感谢您发现这一点 - 无意中引用了旧函数。我添加了另一项改进,更改了编号并更新了测试结果。
【解决方案2】:

我不知道numpy 方法是什么,但这是一个相当干净的解决方案。让A 是一个整数数组,让k 是一个输入数字。

从一个空数组B开始;将数组B 的总和保存在变量s 中(最初设置为零)。应用以下程序:

  • 如果数组B的总和s小于k (i) 将其添加到集合中,(ii) 并且对于每个元素从原始数组A 中,将该元素添加到B 并更新s,(iii)从A 中删除它,并且(iv)递归地应用该过程; (iv) 当调用返回时,将元素添加回A 并更新s否则什么也不做。

这种自下而上的方法会尽早修剪无效分支,并且只访问必要的子集(即几乎只有总和小于k 的子集)。

【讨论】:

  • 这种方法确实有效。在像 C++ 这样的语言中,这可能是最快的方法之一。但是,在 Python 中,通常使用矢量化代码可以达到最佳性能。我不知道如何用你的方法做到这一点。
  • 啊哈,现在我明白你所说的 numpy 方法是什么意思了。你介意我留下答案吗?我觉得还是有用的。
猜你喜欢
  • 1970-01-01
  • 2019-12-17
  • 1970-01-01
  • 1970-01-01
  • 2018-12-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多