考虑区间 [2 i .. 2 i+1 - 1]中的所有整数。并假设所有低于 2 i的整数都可以由给定数组中的数字之和形成。还假设我们已经知道 C,它是小于 2 i的所有数字的总和。如果 C >= 2 i+1 - 1,则此区间中的每个数字都可以表示为给定数字的总和。否则,我们可以检查区间 [2 i .. C + 1] 是否包含给定数组中的任何数字。如果没有这样的数字,C + 1 就是我们搜索的。
这是一个算法的草图:
- 对于每个输入的数字,确定它属于哪个区间,并更新对应的 sum:
S[int_log(x)] += x
。
- 计算数组 S: 的前缀和
foreach i: C[i] = C[i-1] + S[i]
。
- 过滤数组 C 以仅保留值低于 2 的下一个幂的条目。
- 再次扫描输入数组并注意哪个区间 [2 i .. C + 1] 包含至少一个输入数:
i = int_log(x) - 1; B[i] |= (x <= C[i] + 1)
.
- 找到在步骤 #3 中未过滤掉的第一个区间以及
B[]
在步骤 #4 中未设置的相应元素。
如果不清楚为什么我们可以应用第 3 步,这里就是证明。选择 2 i和 C 之间的任意一个数,然后依次减去 2 i以下的所有数以递减顺序。最终我们得到的数字要么小于最后一个减去的数字,要么为零。如果结果为零,只需将所有减去的数字相加,我们就有了所选数字的表示。如果结果非零且小于最后一个减数,则此结果也小于 2 i,所以它是“可表示的”,并且没有一个减去的数字用于表示它。当我们将这些减去的数字加回来时,我们就有了所选数字的表示。这也表明,我们可以通过直接跳转到 C 的 int_log 来一次跳过多个间隔,而不是一个一个过滤间隔。
时间复杂度由函数确定,函数int_log()
是整数对数或数字中最高设置位的索引。如果我们的指令集包含整数对数或任何其等价物(计算前导零或浮点数技巧),则复杂度为 O(n)。否则,我们可以使用一些黑客技术int_log()
在 O(log log U) 中实现并获得 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 是机器字中的位数。