不能由数组中的数字之和形成的最小数字

Posted

技术标签:

【中文标题】不能由数组中的数字之和形成的最小数字【英文标题】:Smallest number that can not be formed from sum of numbers from array 【发布时间】:2014-01-31 09:36:39 【问题描述】:

这个问题是在亚马逊面试中问我的-

给定一个正整数数组,你必须找到不能由数组中的数字之和组成的最小正整数。

例子:

Array:[4 13 2 3 1]
result= 11  Since 11 was smallest positive number which can not be formed from the given array elements 

我所做的是:

    对数组进行排序 计算前缀总和 遍历 sum 数组并检查下一个元素是否小于 1 大于总和,即 A[j]总和+1

但这是 nlog(n) 解决方案。

面试官对此并不满意,并在不到 O(n log n) 的时间内提出了解决方案。

【问题讨论】:

你是说面试官要求 O(logn) 解决方案吗?这显然是不可能的,因为您必须查看每个数组值一次,这至少需要 O(n)。 这里可能需要更具体一些:大于零的最小整数,不能通过对数组元素的任何组合求和来创建,也许? 数组元素都是正整数吗?可以重复吗? 问题规范是否保证最大可能整数值远小于 INT_MAX? 这不是巧合地和昨天问的这个问题很相似吗? ***.com/questions/21060873/… 【参考方案1】:

如果您对数组进行排序,它将为您工作。计数排序可以在 O(n) 中完成,但如果您考虑在一个实际较大的场景中,范围可能会非常高。

Quicksort O(n*logn) 将为您完成工作:

def smallestPositiveInteger(self, array): 
    candidate = 1
    n = len(array)
    array = sorted(array)
    for i in range(0, n):
        if array[i] <= candidate:
            candidate += array[i]
        else:
            break
    return candidate

【讨论】:

【参考方案2】:

有一个漂亮的算法可以在 O(n + Sort) 时间内解决这个问题,其中 Sort 是对输入数组进行排序所需的时间。

该算法背后的思想是对数组进行排序,然后提出以下问题:使用数组的前 k 个元素,你不能做出的最小正整数是多少?然后你从左到右向前扫描数组,更新你对这个问题的答案,直到你找到你不能做的最小数字。

这是它的工作原理。最初,你不能做的最小数字是 1。然后,从左到右,执行以下操作:

如果当前数字大于您目前无法制作的最小数字,那么您知道您无法制作的最小数字 - 这是您已记录的数字,您已经完成了。李> 否则,当前数字小于或等于您无法制作的最小数字。声称你确实可以制作这个数字。现在,您知道数组的前 k 个元素无法生成的最小数字(称为 candidate),现在正在查看值 A[k]。因此,数字candidate - A[k] 必须是您确实可以使用数组的前 k 个元素生成的某个数字,因为否则 candidate - A[k] 将是一个小于您据称无法使用前 k 个数字生成的最小数字数组。此外,您可以在candidatecandidate + A[k](含)范围内创建任何数字,因为您可以从1 到A[k](含)范围内的任何数字开始,然后添加candidate - 1。因此,将candidate 设置为candidate + A[k] 并增加k

在伪代码中:

Sort(A)
candidate = 1
for i from 1 to length(A):
   if A[i] > candidate: return candidate
   else: candidate = candidate + A[i]
return candidate

这是在[4, 13, 2, 1, 3] 上运行的测试。对数组进行排序得到[1, 2, 3, 4, 13]。然后,将candidate 设置为 1。然后我们执行以下操作:

A[1] = 1,candidate = 1: A[1] ≤ candidate,所以设置candidate = candidate + A[1] = 2 A[2] = 2,candidate = 2: A[2] ≤ candidate,所以设置candidate = candidate + A[2] = 4 A[3] = 3,candidate = 4: A[3] ≤ candidate,所以设置candidate = candidate + A[3] = 7 A[4] = 4,candidate = 7: A[4] ≤ candidate,所以设置candidate = candidate + A[4] = 11 A[5] = 13,candidate = 11: A[4] > candidate,所以返回 candidate (11)。

所以答案是 11。

这里的运行时间是 O(n + Sort),因为在排序之外,运行时间是 O(n)。您可以使用堆排序在 O(n log n) 时间内清楚地排序,如果您知道数字的一些上限,则可以使用基数排序在时间 O(n log U)(其中 U 是最大可能数)中排序。如果 U 是一个固定常数,(比如 109),那么基数排序在 O(n) 时间内运行,而整个算法也在 O(n) 时间运行。

希望这会有所帮助!

【讨论】:

应该是else中的candidate = candidate + A[i],没有-1。这与 OP 给出的算法完全相同,但解释非常有帮助。 @user3187810- 这个解决方案非常快 - 它的运行时间不会比 O(n log n) 差,如果您可以使用基数排序之类的方法对整数进行排序,它可能会好很多。 @interjay:我更新了答案。当我写这篇文章时,我没有意识到它最终与 OP 的答案相同。既然我意识到了这一点,我认为答案仍然有用,因为它为答案提供了理由,还展​​示了如何加快速度(即改进排序步骤)。但是,如果您认为没有必要,我可以删除此答案。 @user3187810- 如果整数有一些固定的上界(例如 10^9),您可以使用计数排序或基数排序在时间 O(n) 内对它们进行排序。这会将总运行时间降低到 O(n)。 如果数组中的数字是随机生成的,那么只需在执行算法的其余部分之前检查 1 是否存在,就可以在统计上显着提高。【参考方案3】:

使用位向量在线性时间内完成此操作。

从一个空的位向量开始 b。然后对于数组中的每个元素 k,执行以下操作:

b = b | b

为了明确,第i个元素设置为1代表数字i,| k是设置第k个元素为1。

处理完数组后,b 中第一个零的索引就是你的答案(从右数,从 1 开始)。

    b=0 过程 4:b = b | b 过程 13:b = b | b 过程 2:b = b | b 过程 3:b = b | b 过程 1:b = b | b

第一个零:位置 11。

【讨论】:

请注意,如果位向量操作是恒定时间,则这是线性时间,但可能不是。 据我所知,没有任何计算机支持在恒定时间内对任意宽度数字进行按位运算。这绝对是一个很酷的想法,但我不认为它真的是 O(n)。 @templatetypedef:公平点。 OP 在 cmets 中回答整数保证在 [1,10^9] 的范围内,因此可以在开始时在恒定时间内保留足够大的位向量来占据整个空间。即使没有这个限制,每次超出分配空间时将保留大小加倍应该会将您限制为 O(lg n) 分配。 @DaveGalvin &gt;&gt; 是换班吗?因为那是右移而不是左移。即使是左移,我也一定不明白,因为在您的第 3 步中:1|8192|1 不等于 8209。 @JonathanMee 我写了一个镜像宇宙版本的算法!令人惊讶的是,没有其他人注意到或提到它。现在是正确的。谢谢!【参考方案4】:

考虑区间 [2i .. 2i+1 - 1] 中的所有整数。并假设所有低于 2i 的整数都可以由给定数组中的数字之和形成。还假设我们已经知道 C,它是所有小于 2i 的数字的总和。如果 C >= 2i+1 - 1,则此区间中的每个数字都可以表示为给定数字的总和。否则,我们可以检查区间 [2i .. C + 1] 是否包含给定数组中的任何数字。如果没有这样的数字,我们搜索的是 C + 1。

这是一个算法的草图:

    对于每个输入的数字,确定它属于哪个区间,并更新对应的总和:S[int_log(x)] += x。 计算数组 S 的前缀总和:foreach i: C[i] = C[i-1] + S[i]。 过滤数组 C 以仅保留值低于 2 次幂的条目。 再次扫描输入数组并注意哪个区间 [2i .. C + 1] 至少包含一个输入数字:i = int_log(x) - 1; B[i] |= (x &lt;= C[i] + 1)。 查找在第 3 步中未过滤掉的第一个区间以及在第 4 步中未设置的 B[] 的对应元素。

如果我们不明白为什么我们可以应用第 3 步,这里就是证明。选择 2i 和 C 之间的任意一个数,然后依次减去 2i 以下的所有数。最终我们得到的数字要么小于最后一个减去的数字,要么为零。如果结果为零,只需将所有减去的数字相加,我们就有了所选数字的表示。如果结果非零且小于最后一个减数,则此结果也小于 2i,因此它是“可表示的”,并且没有一个减数用于表示。当我们将这些减去的数字加回来时,我们就有了所选数字的表示。这也表明,我们可以通过直接跳转到 C 的 int_log 来一次跳过多个间隔,而不是逐个过滤间隔。

时间复杂度由函数int_log()确定,它是整数对数或数字中最高设置位的索引。如果我们的指令集包含整数对数或任何其等价物(计算前导零或浮点数技巧),则复杂度为 O(n)。否则,我们可以使用一些小技巧在 O(log log U) 中实现 int_log() 并获得 O(n * log log U) 时间复杂度。 (这里 U 是数组中最大的数)。

如果步骤 1(除了更新总和)还将更新给定范围内的最小值,则不再需要步骤 4。我们可以将 C[i] 与 Min[i+1] 进行比较。这意味着我们只需要对输入数组进行单次传递。或者我们可以将此算法应用到数字流而不是数组。

几个例子:

Input:       [ 4 13  2  3  1]    [ 1  2  3  9]    [ 1  1  2  9]
int_log:       2  3  1  1  0       0  1  1  3       0  0  1  3

int_log:     0  1  2  3          0  1  2  3       0  1  2  3
S:           1  5  4 13          1  5  0  9       2  2  0  9
C:           1  6 10 23          1  6  6 15       2  4  4 13
filtered(C): n  n  n  n          n  n  n  n       n  n  n  n
number in
[2^i..C+1]:  2  4  -             2  -  -          2  -  -
C+1:              11                7                5

对于多精度输入数字,这种方法需要 O(n * log M) 时间和 O(log M) 空间。其中 M 是数组中的最大数。读取所有数字需要相同的时间(在最坏的情况下,我们需要它们的每一位)。

这个结果仍然可以改进到 O(n * log R),其中 R 是这个算法找到的值(实际上是它的输出敏感变体)。此优化所需的唯一修改不是一次处理整数,而是逐位处理它们:第一遍处理每个数字的低位(如位 0..63),第二遍 - 下一位(如64..127)等。我们可以在找到结果后忽略所有高位。这也将空间需求减少到 O(K) 个数字,其中 K 是机器字中的位数。

【讨论】:

你能解释一下这对 1 2 3 9 和 1 1 2 9 是如何工作的 好的。添加了几个示例。 @EvgenyKluev 我正在查看您的示例我无法弄清楚您的“S:”行是如何计算的。在您的描述中,您提到了前缀总和,但这肯定不是前缀总和。 @JonathanMee:实际上,“C”是前缀和,而不是“S”。 “S[i]”是来自输入数组的值的总和,其整数对数等于“i”。而“C[i]”是整数对数小于或等于“i​​”的值的总和。 @EvgenyKluev 感谢您的解释,我现在了解CS。但我又被困在第 3 步了。我不明白你所说的“2 的下一个幂”是什么意思。

以上是关于不能由数组中的数字之和形成的最小数字的主要内容,如果未能解决你的问题,请参考以下文章

如何将一个集合分成两个子集,以使两个集合中数字之和之间的差异最小?

3-动态规划求最小数字之和

如何以最小的复杂性识别数组中的重复数字?

剑指Offer:把数组排成最小的数45

华为OD机试 - 数组组成的最小数字(Python)

如何删除数组中的最小数字,如果有多个最小数字,则删除第一个