Dialecticus's algorithm 是解决此类问题的经典 O(m)-time, O(1)-space 方式(这里我将使用 m 来表示小于 n 的素数的个数)。它不依赖于素数的任何神秘属性。 (有趣的是,对于素数的特殊情况,AlexAlvarez's algorithm 也是线性时间!) Dialecticus 给出了清晰而正确的描述,但似乎无法解释为什么它是正确的,所以我'我会在这里尝试这样做。我真的认为花时间理解这个特定算法的正确性证明是很有价值的:虽然在它最终“陷入”之前我必须阅读一些解释,但它是一个真正的“啊哈!”发生的那一刻! :)(另外,可以用同样的方式有效解决的问题也很多。)
该算法尝试的候选解决方案可以表示为数字范围(i,j),其中 i 和 j 只是素数列表中第一个和最后一个素数的索引。该算法通过以两种不同的方式排除(即不考虑)数字范围集来获得其效率。为了证明它总是给出正确的答案,我们需要证明它永远不会排除具有正确总和的唯一范围。为此,足以证明它永远不会排除 第一个(最左边的)范围与正确的总和,这就是我们将在这里做的。
它应用的第一条规则是,每当我们找到 sum(i, j) > n 的范围 (i, j) 时,我们排除所有 k > j 的范围 (i, k)。很容易看出为什么这是合理的:随着我们添加更多项,总和只会变得更大,而且我们已经确定它已经太大了。
对于线性时间复杂度至关重要的第二个更棘手的规则是,每当我们将范围 (i, j) 的起点从 i 推进到 i+1 时,而不是从 (i+1 , i+1),我们从 (i+1, j) 开始——也就是说,对于所有 i+1
[编辑:下一段的原始版本掩盖了一个微妙之处:我们可能在 any 上一步中将范围端点提前到了 j。]
要看到它永远不会跳过有效范围,我们需要考虑范围 (i, j-1)。算法推进当前范围的起点,使其从(i, j)变为(i+1, j),必然是sum(i, j) > n;正如我们将看到的,要进入首先考虑范围 (i, j) 的程序状态,它必须是 sum(i, j-1)
然而,我们所知道的是,无论终点是否在上一步从 j-1 增加到 j,它肯定是在当前步骤之前的某个时间增加的 - - 所以让我们称触发这个端点增加的范围(k,j-1)。显然 sum(k, j-1) = k,sum(i, j-1) 与 sum(k, j-1) 相同,但从左端删除了零个或多个项,并且所有这些项都是正数,所以它必须是 sum(i, j-1)
所以我们已经确定,每当我们将 i 增加到 i+1 时,我们就知道 sum(i, j-1) 从这个总和的任何一端删除项都不能使它变得更大。删除第一项会得到 sum(i+1, j-1) that sum 开始,然后从另一端依次删除项,我们得到 sum(i+1, j-2), sum(i+1, j-3), ..., sum( i+1, i+1),我们知道的所有这些都必须小于 n——也就是说,与这些和对应的范围都不能是有效的解决方案。因此,我们可以安全地避免首先考虑它们,而这正是算法所做的。
最后一个潜在的绊脚石是,由于我们正在推进两个循环索引,时间复杂度应该是 O(m^2)。但是请注意,每次通过循环体时,我们都会将其中一个索引(i 或 j)前移 1,并且我们永远不会将它们中的任何一个向后移动,所以如果我们在 2m 循环迭代后仍在运行我们必须有 i + j = 2m。由于两个索引都不能超过 m,因此唯一的保持方法是如果 i = j = m,这意味着我们已经到达终点:即我们保证在最多 2m 次迭代后终止。