【问题标题】:Smallest subset of array whose sum is no less than key和不小于键的数组的最小子集
【发布时间】:2012-10-12 22:57:47
【问题描述】:

给定一个数组(假设为非负整数),我们需要找到最小长度的子集,使得元素的总和不小于 K。K 是另一个作为输入提供的整数。

是否有可能有一个时间复杂度为 O(n) [big oh of n] 的解决方案?

我目前的想法是这样的: 我们可以在 O(n * log n) 中对数组进行排序,然后从最大数开始迭代排序后的数组并保持运行总和,直到运行总和变为 >= K。

但是,这将具有 O(n * (log n + 1)) 的最坏情况运行时间。

因此,如果有人能在 O(n) 时间内分享这样做的想法,我将不胜感激..

注意:子数组的元素在此上下文中不必是原始数组的连续序列

【问题讨论】:

  • 排序不会弄乱元素的顺序吗?子数组是什么意思?数组中元素的连续序列,还是数组中元素的子集?
  • 在这种情况下不能应用排序,因为它会改变项目的顺序。
  • 我假设顺序并不重要。即 {1,2,3} 和 {2,1,3} 被视为相同的子数组。 Subrarray 指的是元素的子集,在这种情况下不一定是连续的序列。
  • 另外,如果您认为排序不是可行的方法,您会建议使用什么算法来解决这个问题?
  • 正如我在下面对答案的评论中所说,这是一个非常具有误导性的问题。请将标题更改为“总和不小于 key 的最小子集”或类似名称,并将正文中的所有引用从“子数组”更改为“子集”,从“长度”更改为“大小”,以明确该元素顺序和连续性并不重要。然后我将删除 -1。

标签: arrays algorithm


【解决方案1】:

有一个线性时间算法可以找到 K 个最大的数 - http://en.wikipedia.org/wiki/Selection_algorithm。当然,您想要的只是足够大的数字,至少总和为 K。

在标准选择算法中,您随机选取一个轴,然后查看它的每一侧有多少个数字。然后你要么接受要么拒绝一半,然后继续处理另一半。您已经依次查看了每一半中的每个数字 - 每个枢轴阶段的成本是线性的,但在每个阶段考虑的数据量减少得足够快,以至于总成本仍然只是线性的。

如果您将枢轴上方的所有数字相加,则枢轴阶段的成本仍然是线性的。使用它,您可以计算出接受所有这些数字以及之前选择的任何数字是否会给您一个总计至少为 K 的数字集合。如果是这样,您可以放弃其他数字并使用上面的数字下一次传球的枢轴。如果没有,您可以接受枢轴上方的所有数字,并使用枢轴下方的数字进行下一次传递。与选择算法一样,枢轴本身和任何关系都会为您提供一些特殊情况以及尽早找到准确答案的可能性。

(所以我认为您可以使用修改后的选择算法在(随机)线性时间内执行此操作,在该算法中,您查看枢轴上方数字的总和,而不是枢轴上方有多少数字。

【讨论】:

  • 这当然是正确的(当你及时击败我时,我会投赞成票 -;))。处理一个支点(计算项、确定总和、存储索引以及您最后需要知道的任何其他内容)是一项与集合大小成线性关系的工作。在下一个步骤中,您处理原始集合的任何一半,即 N/2 中的线性努力。最坏的情况 - 没有尽早找到解决方案 - 然后是 N + N/2 + N/4 + ... = 2N 线性的整体努力,所以 O(N) 精确。
  • 在线性时间内查找 k 个最大数的算法需要对数组进行重新排序,所以我不明白它在这里如何应用。而且您的递归似乎也没有考虑子数组的宽度-即使枢轴上方的所有数字的总和> = k,也很可能解决方案位于枢轴下方的一半,因为这些数字位置更靠近。 -1.
  • 请尝试给出例子......关于边界情况
  • 我很抱歉——很明显,当 OP 说“最小长度子数组”时,他实际上是指“最小子集”,而不是(例如)“最小长度子数组”。 +2(这需要一个虚拟编辑)。
  • 谢谢。我已将其标记为已接受但值得一提,@Bert te Velde 的解释作为该解释的附录,有帮助!
【解决方案2】:

这似乎是动态规划的问题。构建数组时,您会构建另一个数组,其中包含每个特定索引的累积总和。所以该数组中的每个i 都有来自1..i 的总和。

现在很容易看出索引p..q 的值的总和是SUM(q) - SUM(p-1)(特殊情况SUM(0)0)。显然我在这里使用基于 1 的索引......这个操作是 O(1),所以现在你只需要一个 O(n) 算法来找到最好的。

一个简单的解决方案是跟踪 pq 并遍历数组。您使用 q 扩展开始。然后你收缩p并重复扩展q,就像一条毛毛虫爬过你的阵列。

扩展q

p <- 1
q <- 1

while SUM(q) - SUM(p-1) < K
    q <- q + 1
end while

现在q 位于子数组和刚刚超过(或等于)K 的位置。子数组的长度为q - p + 1

q 循环之后,您测试子数组长度是否小于您当前的最佳长度。然后你将p 前进一步(这样你就不会不小心跳过最佳解决方案)并重新开始。

您实际上并不需要创建 SUM 数组...您可以随时构建子数组总和...您需要返回使用“真实”p 而不是之前的一个。

subsum <- VAL(1)
p <- 1
q <- 1

while q <= N
    -- Expand
    while q < N and subsum < K
        q <- q + 1
        subsum <- subsum + VAL(q)
    end while

    -- Check the length against our current best
    len <- q - p + 1
    if len < bestlen
        ...
    end if

    -- Contract
    subsum <- subsum - VAL(p)
    p <- p + 1
end while

注意事项:

j_random_hacker 说:这将有助于准确解释为什么只检查 O(n) 个不同的子数组是可以接受的 算法检查,而不是所有 O(n^2) 可能的不同子数组

动态规划的哲学是:

  1. 不要遵循会导致非最佳结果的解决方案路径;和
  2. 使用先前解法的知识来计算新解法。

在这种情况下,通过对元素求和来计算单个候选解决方案(一些 (p,q)p &lt;= q)。因为这些元素都是正整数,所以我们知道对于任何候选解(p,q),候选解(p,q+1) 都会更大。

所以我们知道如果(p,q) 是一个最小的解决方案,那么(p,q+1) 不是。一旦我们有一个候选人,我们就会结束我们的搜索,并测试那个候选人是否比我们迄今为止看到的任何一个更好。这意味着对于每个p,我们只需要测试一个候选人。这导致pq 都只会增加,因此搜索是线性的。

另一部分(使用以前的解决方案)来自于认识到sum(p,q+1) = sum(p,q) + X(q+1) 和类似的sum(p+1,q) = sum(p,q) - X(p)。因此,我们不必在每一步都对pq 之间的所有元素求和。每当我们前进一个搜索指针时,我们只需要加或减一个值。

希望对您有所帮助。

【讨论】:

  • +1,但这将有助于准确解释为什么只检查该算法检查的 O(n) 个不同子数组而不是所有 O(n^2) 个可能的不同子数组是可以接受的。
  • 谢谢,这涵盖了部分内容,但我一直在寻找的特别之处是为什么从 (p, q+1) 开始查看是安全的(而不是从 (1, q +1)) 如果我们发现 (p, q) 太小。
  • 如果我错了,请纠正我,但你的答案不是寻找最小长度的“连续”子数组吗?我正在寻找一种解决方案,其中子数组不必与原始问题中我的 cmets 中的捕获连续。对不起,如果这不清楚。我也会将此细节添加到原始问题陈述中。
  • @AnuragKapur:你真的应该在你的问题中更清楚地说明这一点。如果“子数组”并不意味着“连续”,那么它并不意味着“子集”之外的任何东西——那么为什么不把你的问题称为“总和不小于键的元素的最小子集”呢? “最小长度子数组”是尽可能误导!
  • 确实在cmets里说清楚了,后来在OP语句里加了。无论如何,我明白你的意思,下次会记住这一点!
【解决方案3】:

OP 在他对 cme​​ts 的回答中明确表示,问题是要找到一个子集,而不一定是一个连续的序列(“子数组”这个词确实很糟糕)。那么,我认为mcdowella所指的方法是正确的,包括以下步骤:

从 N 个元素开始,找到 MEDIAN 元素(即想象一个排序数组的第 (N/2) 个元素,你没有也没有构造它)。这是通过“Median of Medians”算法实现的,被证明是 O(n),请参阅已经给出并在此处重复的 wiki 参考:Selection algorithm, see section on the Median of Median algorithm

具有中值元素:线性扫描整个集合,并在“下方”和“上方”中进行分区,同时对每个“一半”进行求和、计数并执行任何您想要跟踪的操作。这一步(也是)O(N)。

完成扫描后,如果“上半部分”总和高于目标 (K),您将忘记下半部分的所有内容并重复上半部分的过程,其大小为(大约)N/2。 另一方面,如果“上半部分”总和小于 K,则将上半部分添加到最终结果中,从 K 中减去其总和,然后对下半部分重复该过程。

总而言之,您处理大小为 N、N/2、N/4、N/8 等的集合,每个集合相对于它们各自的大小 M,都在 O(M) 中,因此总体内容在 N 中也是线性的,因为 N + N/2 + N/4 + N/8 ... 保持在 2N 以下。

【讨论】:

  • +1 用于建议中位数算法的中位数和更详细的解释。不过,我会根据他之前回答的事实将@mcdowella 的回答标记为已接受。谢谢!
  • 当然,mcDowella 值得称赞,事实上我在之前对他的帖子的评论中已经提出了建议。我提出“我的”答案只是因为其他一些人似乎对 mcdowella 的理解不够好。
【解决方案4】:

这是一个应该足够快的解决方案。 我猜它几乎是线性的。

def solve(A, k):
    assert sum(A) >= k
    max_ = max(A)
    min_ = min(A)
    n = len(A)
    if sum(A) - min_ < k:
        return A
    bucket_size = (max_ - min_)/n + 1
    buckets = []
    for i in range(n):
        buckets.append([])
    for item in A:
        bucket = (item - min_)/bucket_size
        buckets[bucket].append(item)

    solution = []

    while True:
        bucket = buckets.pop() #the last bucket
        sum_ = sum(bucket)
        if sum_ >= k:
            #don't need everything from this bucket
            return solution + solve(bucket, k)
        else:
            k -= sum_
            solution += bucket

print solve([5,2,7,52,30,12,18], 100)
"[52, 30, 18]"

【讨论】:

  • 这本质上是一个桶/箱排序,但只是递归排序顶部的桶。我认为随着空间复杂度的增加,这种方法平均比基于快速选择的解决方案要慢。
【解决方案5】:

我相信“子数组”一词意味着数组的连续部分(like here,另一个问题作为示例)。

所以有一个简单的 O(n) 算法来找到最小长度的子数组:

为第一个元素设置两个索引(左、右)并将它们移动到数组的末尾。检查这些索引之间的和,向右推进指针,如果和太小(或指针相等),如果和大则向左推进

【讨论】:

  • 抱歉造成混淆,但子数组不必像 OP 的 cmets 中明确说明的那样连续,我现在也将此注释添加到 OP 语句中。
【解决方案6】:

根据数组的定义,子数组必须是连续的。

使用 2 个指针(开始,结束)。将它们初始化到数组的开头。 跟踪 (start, end) 之间的当前总和,并将 end 一个一个向右移动。每次移动结束指针时,sum = sum + array[end]。

当 sum >= target 时,开始向右移动 start 并继续跟踪 sum 为 sum = sum - array[start]。

当开始向右移动时,继续检查总和仍然不小于目标。我们还需要通过 length = end - start + 1 以及 minLength = min(minLength, length) 来跟踪长度。

现在,当我们将两个指针都尽可能向右移动后,我们只需要返回 minLength。

一般的思路是先找到一个满足条件(sum >= target)的“窗口”,然后每次移动窗口时将窗口向右滑动一个元素,并保持窗口大小最小。

【讨论】:

    猜你喜欢
    • 2013-06-13
    • 2013-06-04
    • 2022-11-13
    • 1970-01-01
    • 2016-12-28
    • 1970-01-01
    • 2019-09-26
    • 2014-08-07
    • 2015-11-29
    相关资源
    最近更新 更多