【问题标题】:write a number as sum of a consecutive primes写一个数作为连续素数之和
【发布时间】:2014-10-30 13:53:40
【问题描述】:

如何检查n 是否可以划分为一系列连续素数的总和。

例如,12 等于 5+7,其中 5 和 7 是连续的素数,但 20 等于 3+17,其中 3 和 17 是不连续的。

注意,不允许重复。

我的想法是找到并列出n 下面的所有素数,然后使用 2 个循环对所有素数求和。前 2 个号码,第二个 2 个号码,第三个 2 个号码等,然后是前 3 个号码,第二个 3 个号码,一直到此为止。但这需要大量的时间和内存。

【问题讨论】:

  • 提示:没有必要单独构建前 2 个素数的列表,然后再构建前 3 个素数的列表,依此类推——如果前 2 个素数之和太小,您知道您至少需要再添加 1 个,同样,如果到目前为止您的序列的总和太大,您知道进一步扩展该序列是没有意义的。
  • 直觉表明,从素数列表中计算所有可能的总和比逐个检查候选数字更容易(更有效)。
  • @aruisdante:它的时间复杂度为 O(n^2),比 O(n!) 要好得多。不过,不同的算法可以在 O(n) 时间内解决这个问题。
  • @aruisdante:不,如果要包含的素数可以是任何素数,情况就是如此,但它们被限制为属于连续范围。但我也错了:他/她的算法实际上是 O(n^3),因为它需要为每个 O(n^2) 端点对做 O(n) 加法。
  • 不要忘记在您提交作业时感谢该网站帮助您开发算法,这样您就不会违反学校的学术诚信规则。

标签: algorithm primes


【解决方案1】:

意识到一个连续的素数列表仅由两条信息定义,即起始素数和结束素数。你只需要找到这两个数字。

我假设您可以使用所有素数,并在名为primes 的数组中排序。在内存中保留三个变量:sum,初始为 2(最小素数),first_indexlast_index,初始为 0(数组primes 中最小素数的索引)。

现在您必须“调整”这两个索引,并在循环中沿途“移动”数组:

如果sum == n 则结束。你已经找到了你的素数序列。

如果sum < n 则通过添加下一个可用素数来扩大列表。将last_index 增加一,然后将sum 增加新素数的值,即primes[last_index]。重复循环。但如果primes[last_index] 大于n 则无解,必须完成。

如果sum > n 则通过从列表中删除最小的素数来减少列表。将sum 递减该值,即primes[first_index],然后将first_index 递增1。重复循环。

【讨论】:

  • 谢谢,它就像一个魅力,但是,怎么样?为什么?你能再解释一下吗?
  • @kikio 不知道怎么解释。这对我来说似乎很明显。我刚刚描述了没有计算机我会做什么,即尝试通过在一端添加数字并从另一端删除它们来找到列表。
  • 但是现在,当我认为它是体力劳动时,不使用计算机,我认为有一些小的改进可以加快搜索速度。例如,算法可能会在某个点得出结论,无法构造 p 素数的总和,并且结果必须小于 p 素数。算法可以以某种方式“跳转”到数组中p-1 素数之和不小于n 的第一个这样的位置,从而跳过数组的“无聊”部分。当您从 2 个素数跳到单个素数时,效果最为显着,跳过了数组的一半。
  • 这个“跳跃”也完全改变了算法,因为现在我意识到p primes 的第一个这样的地方也是最后一个地方。算法应该只是“跳转”到下一个位置,并在此过程中递减 p
  • @kikio:我添加了一个答案,详细说明了为什么这个算法是正确的。这可能是我理解的第一个非平凡的正确性证明,所以我希望它对你有帮助:)
【解决方案2】:

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 次迭代后终止。

【讨论】:

    【解决方案3】:

    素数必须是连续的这一事实允许用 n 非常有效地解决这个问题。假设我们之前已经计算了所有小于或等于 n 的素数。因此,我们可以很容易地将 sum(i) 计算为前 i 个素数的和。

    预先计算好这个函数,我们可以遍历小于或等于 n 的素数,看看是否存在一个长度,使得从那个素数开始我们可以总结为 n时间>。但是请注意,对于一个固定的起始素数,和的序列是单调的,所以我们可以对长度进行二分搜索。

    因此,令k 为小于或等于n 的素数数。预计算总和的成本为 O(k),而循环的成本为 O(klogk),占主导地位。使用Prime number theorem,我们知道k = O(n/logn),那么整个算法的开销O(n/logn log(n/logn)) = O( n).

    让我在 C++ 中放一个代码,以便更清楚,希望没有错误:

    #include <iostream>
    #include <vector>
    using namespace std;
    
    typedef long long ll;
    
    int main() {
      //Get the limit for the numbers
      int MAX_N;
      cin >> MAX_N;
    
      //Compute the primes less or equal than MAX_N
      vector<bool> is_prime(MAX_N + 1,  true);
      for (int i = 2; i*i <= MAX_N; ++i) {
        if (is_prime[i]) {
          for (int j = i*i; j <= MAX_N; j += i) is_prime[j] = false;
        }
      }
      vector<int> prime;
      for (int i = 2; i <= MAX_N; ++i) if (is_prime[i]) prime.push_back(i);
    
      //Compute the prefixed sums
      vector<ll> sum(prime.size() + 1, 0);
      for (int i = 0; i < prime.size(); ++i) sum[i + 1] = sum[i] + prime[i];
    
      //Get the number of queries
      int n_queries;  
      cin >> n_queries;
      for (int z = 1; z <= n_queries; ++z) {
        int n;
        cin >> n;
    
        //Solve the query
        bool found = false;
        for (int i = 0; i < prime.size() and prime[i] <= n and not found; ++i) {
    
          //Do binary search over the lenght of the sum:
          //For all x < ini, [i, x] sums <= n
          int ini = i, fin = int(prime.size()) - 1;
          while (ini <= fin) {
            int mid = (ini + fin)/2;
            int value = sum[mid + 1] - sum[i];
            if (value <= n) ini = mid + 1;
            else fin = mid - 1;
          }
    
          //Check the candidate of the binary search
          int candidate = ini - 1;
          if (candidate >= i and sum[candidate + 1] - sum[i] == n) {
            found = true;
            cout << n << " =";
            for (int j = i; j <= candidate; ++j) {
              cout << " ";
              if (j > i) cout << "+ ";
              cout << prime[j];
            }
            cout << endl;
          }
        }
    
        if (not found) cout << "No solution" << endl;
      }
    }
    

    示例输入:

    1000
    5
    12
    20
    28
    17
    29
    

    样本输出:

    12 = 5 + 7
    No solution
    28 = 2 + 3 + 5 + 7 + 11
    17 = 2 + 3 + 5 + 7
    29 = 29
    

    【讨论】:

    • 有趣的方法。我喜欢你如何使用素数的密度来表明这种通常为 O(n log n) 的方法在这种情况下实际上适合 O(n)!
    【解决方案4】:

    我首先要注意,要使一对连续素数相加,其中一个素数必须小于 N/2,而另一个素数必须大于 N/2。要成为连续素数,它们必须是最接近 N/2 的素数,一个小一个大。

    如果你从一个素数表开始,你基本上是对 N/2 进行二进制搜索。立即查看比这更大和更小的素数。将这些数字加在一起,看看它们是否与您的目标数字相加。如果不是,那么它不可能是两个连续素数之和。

    如果你不从素数表开始,它的工作方式几乎相同——你仍然从 N/2 开始并找到下一个更大的素数(我们称之为 prime1)。然后你减去 N-prime1 得到 prime2 的候选。检查这是否是素数,如果是,则在 prime2...N/2 范围内搜索其他素数以查看其间是否存在素数。如果您的数字之间有一个素数,则它是非连续素数的总和。如果该范围内没有其他素数,则它是连续素数的和。

    相同的基本思想适用于 3 个或更多素数的序列,除了(当然)您的搜索从 N/3(或您想要求和以得到该数字的任何数量的素数)开始。

    因此,对于三个连续的素数求和为 N,三个中的 2 个必须是第一个小于 N/3 的素数和第一个大于 N/3 的素数。所以,我们首先找到那些,然后计算 N-(prime1+prime2)。这给出了使用我们的第三个候选人。我们知道这三个数之和为 N。我们仍然需要证明这第三个数是素数。如果它是素数,我们需要验证它是否与其他两个连续。

    举一个具体的例子,对于 10,我们从 3.333 开始。下一个较小的素数是 3,下一个较大的素数是 5。它们加到 8。10-8 = 2。2 是素数并且与 3 连续,所以我们找到了与 10 相加的三个连续素数。

    您还可以进行其他一些改进。最明显的是基于所有素数(除 2 之外)都是奇数这一事实。因此(假设我们可以忽略2),偶数只能是偶数个素数之和,奇数只能是奇数个素数之和。所以,给定123456789,我们立即知道它不可能是 2 个(或 4、6、8、10、...)个连续素数的总和,所以唯一要考虑的候选者是 3、5、7 , 9, ... 素数。当然,反之亦然:假设12345678,它甚至可以让我们立即排除它可能是 3、5、7 或 9 个连续素数之和的可能性;我们只需要考虑 2, 4, 6, 8, ... 素数的序列。只有当我们得到足够多的素数,我们可以将 2 作为序列的一部分时,我们才会违反这个基本规则。

    我还没有通过数学计算出给定数字的确切数量,但我很确定它应该相当容易并且这是我们想要的无论如何都知道(因为它是寻找给定数字的连续素数数量的上限)。如果我们使用 M 来表示素数的数量,那么极限应该是大约 M

    【讨论】:

    • 谢谢,但我不明白如何处理 3 个素数。例如,10,而 10/3 是 3.34。之后怎么办?
    【解决方案5】:

    我知道这个问题有点老了,但我不能不回复之前答案中的分析。实际上,已经强调了所有三个提议的算法在n 中的运行时间基本上是线性的。但事实上,产生一个以严格小于n 的幂运行的算法并不难。

    要了解如何操作,让我们选择一个介于 1 和 n 之间的参数 K,并假设我们需要的素数已经制成表格(如果必须从头开始计算,请参见下文)。然后,这就是我们要做的,将n 的表示搜索为k 连续素数之和:

    • 首先我们使用 Jerry Coffin 的答案中的想法搜索k&lt;K;也就是说,我们搜索位于n/k 周围的k 素数。
    • 然后为了探索k&gt;=K素数的和,我们使用辩证法答案中解释的算法;也就是说,我们从第一个元素为 2 的和开始,然后我们一次将第一个元素推进一步。

    第一部分,关于大素数的短和,需要O(log n) 操作来二进制搜索一个接近n/k 的素数,然后O(k) 操作来搜索另一个k 素数(有一些简单的可能实现)。总的来说,这是一个运行时间

    R_1=O(K^2)+O(Klog n).

    第二部分,关于小素数的长和,要求我们考虑连续素数的和p_1&lt;\dots&lt;p_k,其中第一个元素最多为n/K。 因此,它最多需要访问n/K+K 质数(实际上可以通过质数定理的弱版本保存一个对数因子)。由于在该算法中每个素数最多被访问O(1) 次,因此运行时间为

    R_2=O(n/K) + O(K).

    现在,如果log n &lt; K &lt; \sqrt n 我们有第一部分运行O(K^2) 操作,第二部分运行O(n/K)。我们用选择K=n^{1/3}进行优化,使得整体运行时间为

    R_1+R_2=O(n^{2/3}).

    如果素数没有列表

    如果我们还必须找到素数,我们就是这样做的。 首先我们使用 Erathostenes,它在C_2=O(T log log T) 操作中找到直到T 的所有素数,其中T=O(n/K) 是算法第二部分中访问的小素数的上限。

    为了执行算法的第一部分,我们需要为每个k&lt;K 找到位于n/k 周围的O(k) 素数。黎曼假设暗示在区间[x,x+y] 中至少有k 素数,如果y&gt;c log x (k+\sqrt x) 为某个常数c&gt;0。因此,我们需要先验地找到以n/k 为中心、宽度为|I_k|= O(k log n)+O(\sqrt {n/k} log n) 的区间I_k 中包含的素数。

    使用筛子 Eratosthenes 筛分区间I_k 需要O(|I_k|log log n) + O(\sqrt n) 操作。如果k&lt;K&lt;\sqrt n 我们得到每个k&lt;K 的时间复杂度C_1=O(\sqrt n log n log log n)

    总结一下,时间复杂度C_1+C_2+R_1+R_2在最大的时候

    K = n^{1/4} / (log n \sqrt{log log n}).

    有了这个选择就有了次线性时间复杂度

    R_1+R_2+C_1+C_2 = O(n^{3/4}\sqrt{log log n}.

    如果我们不假设黎曼假设,我们将不得不在更大的区间上进行搜索,但最终我们仍然会得到亚线性时间复杂度。相反,如果我们假设对素数间隙有更强的猜想,我们可能只需要在宽度为|I_k|=k (log n)^A 的区间I_k 上搜索一些A&gt;0。然后,我们可以使用其他确定性素性检验来代替 Erathostenes。例如,假设您可以在O((log n)^B) 操作中测试单个数字的素数,对于某些B&gt;0。 然后你可以在O(k(log n)^{A+B})操作中搜索区间I_k。在这种情况下,最佳K 仍然是K\approx n^{1/3},直到对数因子,因此对于某些D&gt;0,总复杂度为O(n^{2/3}(log n)^D

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-01-25
      • 1970-01-01
      • 2016-12-27
      • 2016-05-03
      • 1970-01-01
      • 1970-01-01
      • 2020-10-21
      相关资源
      最近更新 更多