【问题标题】:Find all SUBSETS of a 1D array that have a specified number of UNIQUE elements (n) AND a specified sum (s)查找具有指定数量的唯一元素 (n) 和指定总和 (s) 的一维数组的所有子集
【发布时间】:2021-03-03 03:57:18
【问题描述】:

考虑一个一维 numpy 数组和两个常量,如图所示:

import numpy as np

arr = np.arange(60)

n = 5
s = 120

例如,arr 总是采用 [0,1,2,3,4, ... 59,60] 的形式。

问题:从一维数组 (arr) 中,我需要准确地找到所有子集 n UNIQUE 具有指定总和 s 的元素。 解决方案可以像这样开始:

          [[2, 10, 26, 35, 47], 
           [9, 14, 15, 40, 42],
           etc...

我已经按顺序显示了行元素。这会很好,但这不是必需的。 (即:组合,而不是排列)

目前,我通过使用相同表的叉积在 SQL 变体中处理此计算,每个表都包含 arr 的元素。 这可行,但非常慢,尤其是当 n 达到 12 左右时。

有没有办法在 Python/Numpy 中高效快速地做到这一点?

【问题讨论】:

  • arr 是否总是一个从 0 到某个最大值的数组,以 1 为增量?因此没有重复的元素,总是以 1 递增,从 0 开始(或者可能从 1 开始)。这样可以简化计算。
  • 是的。例如,“arr”总是采用 [0,1,2,3,4, ... 59,60] 的形式。我会在问题中澄清这一点。
  • 为什么要强调UNIQUEnp.arange(k) 总是独一无二的
  • @Gulzar: one could repeat elements from arr: [24, 24, 24, 24, 24] 满足要求,除了是唯一因素。

标签: python numpy


【解决方案1】:

根据this answer,我修改了代码以适应这个问题。
请注意,数组是按降序提供给函数的。

def combinations_cutoff(array, tuple_length, s, prev_array=[], n_skips=[]):
    if len(prev_array) == tuple_length:
        if sum(prev_array) == s:
            return [prev_array]
        return []
    combs = []
    for i, val in enumerate(array):
        prev_array_extended = prev_array.copy()
        prev_array_extended.append(val)
        if sum(prev_array_extended) > s:
            n_skips[0] += 1
            print(f"cutoff! arr={prev_array_extended}, s={sum(prev_array_extended)}, skip #{n_skips[0]}")
            continue
        combs += combinations_cutoff(array[i+1:], tuple_length, s, prev_array_extended, n_skips=n_skips)
    return combs


def main():
    n = 20
    k = 3
    s = 15
    arr = np.arange(n)[::-1]

    n_skips = [0]
    gen = combinations_cutoff(arr, k, s, n_skips=n_skips)

    lst = []
    for c in gen:
        lst.append(c)
        print(c)

    print(f"total solutions: {len(lst)}, skips={n_skips[0]}")


if __name__ == "__main__":
    main()

输出:

cutoff! arr=[19], s=19, skip #1
cutoff! arr=[18], s=18, skip #2
cutoff! arr=[17], s=17, skip #3
cutoff! arr=[16], s=16, skip #4
cutoff! arr=[15, 14], s=29, skip #5
cutoff! arr=[15, 13], s=28, skip #6
cutoff! arr=[15, 12], s=27, skip #7
cutoff! arr=[15, 11], s=26, skip #8
cutoff! arr=[15, 10], s=25, skip #9
cutoff! arr=[15, 9], s=24, skip #10
cutoff! arr=[15, 8], s=23, skip #11
cutoff! arr=[15, 7], s=22, skip #12
cutoff! arr=[15, 6], s=21, skip #13
cutoff! arr=[15, 5], s=20, skip #14
cutoff! arr=[15, 4], s=19, skip #15
cutoff! arr=[15, 3], s=18, skip #16
cutoff! arr=[15, 2], s=17, skip #17
cutoff! arr=[15, 1], s=16, skip #18
cutoff! arr=[14, 13], s=27, skip #19
cutoff! arr=[14, 12], s=26, skip #20
cutoff! arr=[14, 11], s=25, skip #21
cutoff! arr=[14, 10], s=24, skip #22
cutoff! arr=[14, 9], s=23, skip #23
cutoff! arr=[14, 8], s=22, skip #24
cutoff! arr=[14, 7], s=21, skip #25
cutoff! arr=[14, 6], s=20, skip #26
cutoff! arr=[14, 5], s=19, skip #27
cutoff! arr=[14, 4], s=18, skip #28
cutoff! arr=[14, 3], s=17, skip #29
cutoff! arr=[14, 2], s=16, skip #30
cutoff! arr=[13, 12], s=25, skip #31
cutoff! arr=[13, 11], s=24, skip #32
cutoff! arr=[13, 10], s=23, skip #33
cutoff! arr=[13, 9], s=22, skip #34
cutoff! arr=[13, 8], s=21, skip #35
cutoff! arr=[13, 7], s=20, skip #36
cutoff! arr=[13, 6], s=19, skip #37
cutoff! arr=[13, 5], s=18, skip #38
cutoff! arr=[13, 4], s=17, skip #39
cutoff! arr=[13, 3], s=16, skip #40
cutoff! arr=[13, 2, 1], s=16, skip #41
cutoff! arr=[12, 11], s=23, skip #42
cutoff! arr=[12, 10], s=22, skip #43
cutoff! arr=[12, 9], s=21, skip #44
cutoff! arr=[12, 8], s=20, skip #45
cutoff! arr=[12, 7], s=19, skip #46
cutoff! arr=[12, 6], s=18, skip #47
cutoff! arr=[12, 5], s=17, skip #48
cutoff! arr=[12, 4], s=16, skip #49
cutoff! arr=[12, 3, 2], s=17, skip #50
cutoff! arr=[12, 3, 1], s=16, skip #51
cutoff! arr=[11, 10], s=21, skip #52
cutoff! arr=[11, 9], s=20, skip #53
cutoff! arr=[11, 8], s=19, skip #54
cutoff! arr=[11, 7], s=18, skip #55
cutoff! arr=[11, 6], s=17, skip #56
cutoff! arr=[11, 5], s=16, skip #57
cutoff! arr=[11, 4, 3], s=18, skip #58
cutoff! arr=[11, 4, 2], s=17, skip #59
cutoff! arr=[11, 4, 1], s=16, skip #60
cutoff! arr=[11, 3, 2], s=16, skip #61
cutoff! arr=[10, 9], s=19, skip #62
cutoff! arr=[10, 8], s=18, skip #63
cutoff! arr=[10, 7], s=17, skip #64
cutoff! arr=[10, 6], s=16, skip #65
cutoff! arr=[10, 5, 4], s=19, skip #66
cutoff! arr=[10, 5, 3], s=18, skip #67
cutoff! arr=[10, 5, 2], s=17, skip #68
cutoff! arr=[10, 5, 1], s=16, skip #69
cutoff! arr=[10, 4, 3], s=17, skip #70
cutoff! arr=[10, 4, 2], s=16, skip #71
cutoff! arr=[9, 8], s=17, skip #72
cutoff! arr=[9, 7], s=16, skip #73
cutoff! arr=[9, 6, 5], s=20, skip #74
cutoff! arr=[9, 6, 4], s=19, skip #75
cutoff! arr=[9, 6, 3], s=18, skip #76
cutoff! arr=[9, 6, 2], s=17, skip #77
cutoff! arr=[9, 6, 1], s=16, skip #78
cutoff! arr=[9, 5, 4], s=18, skip #79
cutoff! arr=[9, 5, 3], s=17, skip #80
cutoff! arr=[9, 5, 2], s=16, skip #81
cutoff! arr=[9, 4, 3], s=16, skip #82
cutoff! arr=[8, 7, 6], s=21, skip #83
cutoff! arr=[8, 7, 5], s=20, skip #84
cutoff! arr=[8, 7, 4], s=19, skip #85
cutoff! arr=[8, 7, 3], s=18, skip #86
cutoff! arr=[8, 7, 2], s=17, skip #87
cutoff! arr=[8, 7, 1], s=16, skip #88
cutoff! arr=[8, 6, 5], s=19, skip #89
cutoff! arr=[8, 6, 4], s=18, skip #90
cutoff! arr=[8, 6, 3], s=17, skip #91
cutoff! arr=[8, 6, 2], s=16, skip #92
cutoff! arr=[8, 5, 4], s=17, skip #93
cutoff! arr=[8, 5, 3], s=16, skip #94
cutoff! arr=[7, 6, 5], s=18, skip #95
cutoff! arr=[7, 6, 4], s=17, skip #96
cutoff! arr=[7, 6, 3], s=16, skip #97
cutoff! arr=[7, 5, 4], s=16, skip #98
[14, 1, 0]
[13, 2, 0]
[12, 3, 0]
[12, 2, 1]
[11, 4, 0]
[11, 3, 1]
[10, 5, 0]
[10, 4, 1]
[10, 3, 2]
[9, 6, 0]
[9, 5, 1]
[9, 4, 2]
[8, 7, 0]
[8, 6, 1]
[8, 5, 2]
[8, 4, 3]
[7, 6, 2]
[7, 5, 3]
[6, 5, 4]
total solutions: 19, skips=98

该函数始终返回长度为tuple_length的所有组合的列表,以prev_array开头并以array的组合结尾,总和为s

在每一步,我们都会考虑prev_array 是否“准备就绪”(= 长度和总和正确)。如果是这样,它应该被退回。
如果是sum没有达到,并且数组已经足够长,那么它是无效的。

现在,我们留下了一个不够长的prev_array,我们现在考虑应该向其中添加哪个元素。

元素的选项由递归定义,所有array的元素,因此我们迭代它们并尝试它们。
当一个元素被试用时,以后只能尝试它后面的元素。
尝试一个元素意味着使用prev_array_extended 调用递归,即prev_array 后跟所选元素。

现在到主菜 - 截止 - 在调用递归之前,我们可以考虑 prev_array 的总和。由于array 保证是降序的[注意它是如何从main 调用的],如果候选元组的任何前缀已经通过s,那么继续检查该候选元组的可能性是没有意义的,我们可以停下来继续使用更小的值来推动prev_array的结束。

n_skips 只是一个丑陋的黑客来计算截止完成了多少次。

【讨论】:

    【解决方案2】:

    以下是递归解决方案。

    这可以在o(len(arr)^(max(n, len(arr)-n) 中解决。

    I will come back to this...

    但是,以下解决方案仍将比您的解决方案快得多。

    import numpy as np
    
    
    def perms(arr: np.array, n: int, s: int, used_inds: set):
        if s == 0 and n == 0:
            print(arr[np.array(list(used_inds))])
            return
    
        if s == 0:  # not enough elements in group
            return
    
        if n == 0:  # not reached sum too soon [all positive]
            return
    
        for i in range(len(arr)):
            if i in used_inds:
                continue
            new_used_inds = set(used_inds)
            new_used_inds.add(i)
            perms(arr=arr, n=n - 1, s=s - arr[i], used_inds=new_used_inds)
    
    
    def main():
        arr = np.arange(20)
    
        n = 3
        s = 15
    
        perms(arr=arr, n=n, s=s, used_inds=set())
    
    
    if __name__ == "__main__":
        main()
    
    [ 0  1 14]
    [ 0  2 13]
    [ 0  3 12]
    [ 0 11  4]
    [ 0 10  5]
    [0 9 6]
    [0 8 7]
    [0 8 7]
    [0 9 6]
    [ 0 10  5]
    [ 0 11  4]
    [ 0  3 12]
    [ 0  2 13]
    [ 0  1 14]
    [ 0  1 14]
    [ 1  2 12]
    [11  1  3]
    [ 1 10  4]
    [1 5 9]
    [8 1 6]
    [8 1 6]
    [1 5 9]
    [ 1 10  4]
    [ 3  1 11]
    [ 1  2 12]
    [ 0  2 13]
    [ 1  2 12]
    [10  2  3]
    [9 2 4]
    [8 2 5]
    [2 6 7]
    [2 6 7]
    [8 2 5]
    [9 2 4]
    [ 3  2 10]
    [ 1  2 12]
    [ 0  3 12]
    [11  1  3]
    [10  2  3]
    [8 3 4]
    [3 5 7]
    [3 5 7]
    [8 3 4]
    [ 2 10  3]
    [11  1  3]
    [ 0 11  4]
    [ 1 10  4]
    [9 2 4]
    [8 3 4]
    [4 5 6]
    [4 5 6]
    [8 3 4]
    [9 2 4]
    [ 1 10  4]
    [ 0 10  5]
    [1 5 9]
    [8 2 5]
    [3 5 7]
    [4 5 6]
    [4 5 6]
    [3 5 7]
    [8 2 5]
    [9 5 1]
    [0 9 6]
    [8 1 6]
    [2 6 7]
    [4 5 6]
    [4 5 6]
    [2 6 7]
    [8 1 6]
    [0 8 7]
    [2 6 7]
    [3 5 7]
    [3 5 7]
    [2 6 7]
    [8 0 7]
    [8 1 6]
    [8 2 5]
    [8 3 4]
    [8 3 4]
    [8 2 5]
    [8 1 6]
    [0 9 6]
    [9 5 1]
    [9 2 4]
    [9 2 4]
    [9 5 1]
    [ 0 10  5]
    [ 1 10  4]
    [ 3 10  2]
    [ 2 10  3]
    [ 1 10  4]
    [ 0 11  4]
    [ 3  1 11]
    [ 3  1 11]
    [ 0  3 12]
    [ 1  2 12]
    [ 1  2 12]
    [ 0  2 13]
    [ 0  1 14]
    

    这仍然很慢,但执行的计算量比您提出的解决方案少得多,因为它切断了已经知道更多计算是无价的分支。

    请注意,这会使订单的可读性降低。


    可读性稍差的代码,输出可读性更高:

    import numpy as np
    from collections import OrderedDict
    
    
    def perms(arr: np.array, n: int, s: int, used_inds: OrderedDict):
        if s == 0 and n == 0:
            print(arr[np.array(list(used_inds))])
            return
    
        if s == 0:  # not enough elements in group
            return
    
        if n == 0:  # not reached sum too soon [all positive]
            return
    
        for i in range(len(arr)):
            if i in used_inds:
                continue
            new_used_inds = OrderedDict(used_inds)
            new_used_inds[i] = None
            perms(arr=arr, n=n - 1, s=s - arr[i], used_inds=new_used_inds)
    
    
    def main():
        arr = np.arange(20)
    
        n = 3
        s = 15
    
        perms(arr=arr, n=n, s=s, used_inds=OrderedDict())
    
    
    if __name__ == "__main__":
        main()
    
    [ 0  1 14]
    [ 0  2 13]
    [ 0  3 12]
    [ 0  4 11]
    [ 0  5 10]
    [0 6 9]
    [0 7 8]
    [0 8 7]
    [0 9 6]
    [ 0 10  5]
    [ 0 11  4]
    [ 0 12  3]
    [ 0 13  2]
    [ 0 14  1]
    [ 1  0 14]
    [ 1  2 12]
    [ 1  3 11]
    [ 1  4 10]
    [1 5 9]
    [1 6 8]
    [1 8 6]
    [1 9 5]
    [ 1 10  4]
    [ 1 11  3]
    [ 1 12  2]
    [ 2  0 13]
    [ 2  1 12]
    [ 2  3 10]
    [2 4 9]
    [2 5 8]
    [2 6 7]
    [2 7 6]
    [2 8 5]
    [2 9 4]
    [ 2 10  3]
    [ 2 12  1]
    [ 3  0 12]
    [ 3  1 11]
    [ 3  2 10]
    [3 4 8]
    [3 5 7]
    [3 7 5]
    [3 8 4]
    [ 3 10  2]
    [ 3 11  1]
    [ 4  0 11]
    [ 4  1 10]
    [4 2 9]
    [4 3 8]
    [4 5 6]
    [4 6 5]
    [4 8 3]
    [4 9 2]
    [ 4 10  1]
    [ 5  0 10]
    [5 1 9]
    [5 2 8]
    [5 3 7]
    [5 4 6]
    [5 6 4]
    [5 7 3]
    [5 8 2]
    [5 9 1]
    [6 0 9]
    [6 1 8]
    [6 2 7]
    [6 4 5]
    [6 5 4]
    [6 7 2]
    [6 8 1]
    [7 0 8]
    [7 2 6]
    [7 3 5]
    [7 5 3]
    [7 6 2]
    [8 0 7]
    [8 1 6]
    [8 2 5]
    [8 3 4]
    [8 4 3]
    [8 5 2]
    [8 6 1]
    [9 0 6]
    [9 1 5]
    [9 2 4]
    [9 4 2]
    [9 5 1]
    [10  0  5]
    [10  1  4]
    [10  2  3]
    [10  3  2]
    [10  4  1]
    [11  0  4]
    [11  1  3]
    [11  3  1]
    [12  0  3]
    [12  1  2]
    [12  2  1]
    [13  0  2]
    [14  0  1]
    

    您的解决方案相当于

    from itertools import combinations
    
    
    def combs(arr: np.array, n: int, s: int):
        comb_generator = combinations(iterable=arr, r=n)
        for comb in comb_generator:
            total = sum(list(comb))
            if total == s:
                print(comb)
    
    (0, 1, 14)
    (0, 2, 13)
    (0, 3, 12)
    (0, 4, 11)
    (0, 5, 10)
    (0, 6, 9)
    (0, 7, 8)
    (1, 2, 12)
    (1, 3, 11)
    (1, 4, 10)
    (1, 5, 9)
    (1, 6, 8)
    (2, 3, 10)
    (2, 4, 9)
    (2, 5, 8)
    (2, 6, 7)
    (3, 4, 8)
    (3, 5, 7)
    (4, 5, 6)
    

    它不会按时中断计算,而只是迭代所有内容。
    这至少不会分配额外的内存。

    【讨论】:

    • 比我做的快得多!计算截止概念在许多其他设置中确实很有启发性和有用性。
    • 我注意到建议的解决方案会产生所有排列。例如,(8, 17, 25) 将以 6 种形式出现。问题提到顺序并不重要,所以我们可以只得到按升序排列的输出吗?谢谢
    • @user109387 我已经投入了比我更多的时间。要实现您想要的,只需将所有解决方案放入一个集合中 - 它会自动确保没有重复。注意set 只能处理可散列类型,因此它应该适用于int(因此适用于int 数组)但不适用于float。这可以处理,但我没有打扰,因为这不是这里的主要问题。或者,您可以只累积结果,然后过滤重复项。我会引用我讨厌的讲师的话:“我会把它作为练习留给读者”。对不起:)
    • 再思考 10 秒,更好的方法是将截止概念与排列结合使用。请参阅docs.python.org/3/library/itertools.html#itertools.combinations 并稍微修改代码以仅通过每个组合一次,并且在达到总和时仍然截止。将比我提出的解决方案更有效。我现在没时间做,抱歉。我相信你能做到。
    • @user109387 请注意itertools 解是多项式时间 (o(len(arr)^(max(n, len(arr)-n) == [len(arr) 选择 n ])。当我有时间的时候,一个也使用回溯的多项式时间解决方案会出现。
    猜你喜欢
    • 1970-01-01
    • 2017-09-23
    • 2010-12-02
    • 2017-01-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多