【问题标题】:Top K subset sum without sorting前 K 个子集总和,没有排序
【发布时间】:2020-04-09 05:48:11
【问题描述】:

给定一个大小为 N 的数组,以元素和的递增顺序打印大小为 K (0<K<=N) 的所有子集

Array:
  [6,8,3,9], N=4, K=3
Sorted Subsets:
  [3, 6, 8] (sum=17)
  [3, 6, 9] (sum=18)
  [3, 8, 9] (sum=20)
  [6, 8, 9] (sum=23)

我不需要整个排序列表,而是需要前 T 个条目(T 很小)。列出所有子集(nCk)并对它们进行排序对于大 N 来说将非常昂贵。有没有办法在不实际枚举所有子集的情况下获得前 T 个子集?我正在考虑选择最小的 K 元素,这是最小的子集,然后通过替换一个或多个元素找到下一个最小子集的方法,但是替换的选择又太多了。

【问题讨论】:

  • 只是一个想法。如果K 为 3,并且您取输入数组中最小的 4 个元素,则通过选择这 4 个元素中的 3 个,您将得到四个最小的子集。取 5 个元素,您可以形成最小的 5C3 子集。
  • @user3386109 这不适用于例如[3, 6, 7, 8, 9] - 您的算法会在3, 6, 9 之前生成6, 7, 8
  • 我不确定我错过了哪一步。我要说的是,对于某些输入,可以有一个包含数组最后一个元素的子集,该子集小于不包括数组第一个元素的任何子集,因此任何基于迭代产生子集的算法- 数组的增长前缀不起作用。 “取输入数组中最小的 4 个元素,您将通过选择这 4 个中的 3 个来获得四个最小的子集” 的陈述是错误的。
  • “我想说的是,对于某些输入,可以有一个包含数组最后一个元素的子集” 我假设你的意思是一些输入,可以有一个包含数组的最大元素的子集。那是对的吗? @kaya3
  • @kaya3 好的,所以我最初的想法是,在一般情况下,不需要数组的最大元素。例如,如果 k=3T=10N=100,则不需要数组的第 100 个元素。需要考虑的是,“你需要多少数组元素?”

标签: arrays algorithm subset


【解决方案1】:

我会这样解决这个问题:

  1. 对数组进行排序,让s 为前k 元素的总和。
  2. 使用 backtracking search 生成 sum 的所有子集,等于 s
  3. 使用branch-and-bound algorithm,找到最小的s2 > s,使得有一个子集的和等于s2
  4. 如果有这样的s2,则设置s = s2,然后转到步骤2。否则,停止。

这是 Python 中的一个实现:它按总和的顺序懒惰地生成每个子集,因此您可以只取它产生的前 T 个子集。

def subsets_in_sum_order(lst, k):
    """
    Returns a generator yielding the k-element subsets
    of lst, in increasing order of their sum.
    """
    lst = sorted(lst)
    s = sum(lst[:k])
    max_s = sum(lst[-k:])
    while s is not None:
        yield from subsets_of_sum(lst, k, s)
        s = smallest_sum_in_range(lst, k, s+1, max_s)

def subsets_of_sum(lst, k, s, t=(), i=0):
    """
    Returns a generator yielding tuples t + tt, where tt
    is a k-element subset of lst[i:] whose sum is s. The
    subsets are yielded in lexicographic order. The list
    lst must be sorted.
    """
    if k < 0:
        raise ValueError()
    elif k == 0:
        if s == 0:
            yield t
    else:
        for j in range(i, len(lst) - k + 1):
            if sum(lst[j:j+k]) > s: break
            v = lst[j]
            s2 = s - v
            t2 = t + (v,)
            yield from subsets_of_sum(lst, k-1, s2, t2, j+1)

def smallest_sum_in_range(lst, k, min_s, max_s, i=0):
    """
    Returns the smallest s such that min_s <= s <= max_s,
    and there is a k-element subset of lst[i:] with sum s.
    The list lst must be sorted.
    Returns None if there is no such s.
    """
    result = None
    if k < 0:
        raise ValueError()
    elif k == 0:
        if min_s <= 0:
            result = 0
    elif min_s <= max_s and sum(lst[-k:]) >= min_s:
        for j in range(i, len(lst) - k + 1):
            v = lst[j]
            if k * v > max_s: break
            s = smallest_sum_in_range(lst, k-1, min_s-v, max_s-v, j+1)
            if s is not None:
                s += v
                result = s
                max_s = s - 1
    return result

例子:

>>> subsets = subsets_in_sum_order([1, 2, 3, 4, 5], 3)
>>> for subset in subsets:
...     print(subset, sum(subset))
... 
(1, 2, 3) 6
(1, 2, 4) 7
(1, 2, 5) 8
(1, 3, 4) 8
(1, 3, 5) 9
(2, 3, 4) 9
(1, 4, 5) 10
(2, 3, 5) 10
(2, 4, 5) 11
(3, 4, 5) 12

@user3386109 观察到,如果列表长度远大于您要生成的子集的数量,那么我们实际上不需要整个列表,因为列表中较大的元素不会出现在前 T 个子集。前 T 个子集必须只使用列表中的前 T + k - 1 个元素,所以我们可以通过使用 heapq.nsmallest 来提高一点效率:

import heapq
from itertools import islice

def smallest_subsets(lst, k, num_subsets):
    lst = heapq.nsmallest(num_subsets + k - 1, lst)
    subsets = subsets_in_sum_order(lst, k)
    return islice(subsets, num_subsets)

这使您不必对长度为 N 的整个列表进行排序。但是,回溯搜索和分支定界算法并没有从中受益太多,因为它们都已经使用总和的边界来尽早消除分支;当 T 很小时,两者都不需要迭代到长列表的末尾。

【讨论】:

    【解决方案2】:

    一种方法是动态编程。

    首先,假设我们有一个如下所示的数据结构:

    for each count of elements to use
        for each possible sum
            for each starting index
                count of ways to get there (with or without that starting index)
    

    编写代码来填写它并不难。对于[6,8,3,9],您会得到如下内容:

    counts_by_count_by_sum_by_index = [
        { # empty sets
            0: [1, 1, 1, 1]
        },
        { # 1 element sets
            3: [1, 1, 1, 0],
            6: [1, 0, 0, 0],
            8: [1, 1, 0, 0],
            9: [1, 1, 1, 1],
        },
        { # 2 element sets
            9: [1, 0, 0, 0],
           11: [1, 1, 0, 0],
           12: [1, 1, 1, 0],
           14: [1, 0, 0, 0],
           15: [1, 0, 0, 0],
           17: [1, 1, 0, 0],
        },
        { # 3 element sets
           17: [1, 0, 0, 0],
           18: [1, 0, 0, 0],
           20: [1, 1, 0, 0],
           23: [1, 0, 0, 0],
        },
        { # 4 element sets
           26: [1, 0, 0, 0]
        }
    ]
    

    如果您有更多元素,此数据结构可能会变大,但会以伪多项式方式扩展。特别是O((size of elements) * (size of set) ^ 3)

    使用这种数据结构,可以很容易地编写一个求和搜索,然后递归地按字典(按使用的索引)顺序找到解决方案。

    如果你愿意,也可以找到,比如说,百万分之一的解决方案是什么,而不必生成以前的解决方案。

    【讨论】:

    • 这是一个不错的解决方案,但是提前生成所有可能的和就相当于在最坏的情况下生成所有(n 选择 k)个组合,不是吗?
    • 可以用优先队列的数据结构的“及时”实现来编写这个算法。这听起来很有趣。但我必须做很多思考如何让它发挥作用。
    • 我一定错过了什么。这似乎效率极低。这将如何处理 1000 个元素的数组,并且您需要找到长度为 100 且总和最小的前一百万个子集。例如,您有 for each possible sum 暗示所有的总和都已生成。我们不是要避免这种情况吗?我非常清楚@btilly 的实力,我真的相信我在这里缺少一些东西。
    • @JosephWood 我认为T=1000000 不符合(T 很小) 约束。
    • @JosephWood 在优先队列评论中我说,但我必须做很多思考如何让它发挥作用。 这意味着我有一个关于如何做到这一点的好主意,但实际实施很复杂。如果您想尝试一下,您需要将k-element sets 替换为返回对(value, counts) 的流。该流应该使用生成的值(如果存在),如果不存在则生成更多值。生成更多应该是从优先队列中挑选部分计算。
    猜你喜欢
    • 1970-01-01
    • 2022-06-15
    • 2014-11-19
    • 2012-03-27
    • 2011-06-27
    • 1970-01-01
    • 1970-01-01
    • 2014-05-18
    • 2013-03-05
    相关资源
    最近更新 更多