【问题标题】:Sum of all prime numbers below 2 million小于 200 万的所有素数之和
【发布时间】:2013-05-28 12:22:38
【问题描述】:

来自 Project Euler 的问题 10:

该程序针对较小的数字运行,并在数十万中缓慢爬行。 在 200 万时,即使程序似乎仍在运行,也无法显示答案。

我正在尝试实施埃拉托色尼筛法。它应该非常快。我的方法有什么问题?

import java.util.ArrayList;

public class p010
{
  /**
   * The sum of the primes below 10 is 2 + 3 + 5 + 7 = 17
   * Find the sum of all the primes below two million.
   * @param args
   */
  public static void main(String[] args)
  {
    ArrayList<Integer> primes = new ArrayList<Integer>();
    int upper = 2000000;
    for (int i = 2; i < upper; i++)
    {
      primes.add(i);
    }
    int sum = 0;
    for (int i = 0; i < primes.size(); i++)
    {
      if (isPrime(primes.get(i)))
      {
        for (int k = 2; k*primes.get(i) < upper; k++)
        {
          if (primes.contains(k*primes.get(i)))
          {
            primes.remove(primes.indexOf(k*primes.get(i)));
          }
        }
      }
    }
    for (int i = 0; i < primes.size(); i++)
    {
      sum += primes.get(i);
    }
    System.out.println(sum);
  }

  public static boolean isPrime(int number)
  {
    boolean returnVal = true;
    for (int i = 2; i <= Math.sqrt(number); i ++)
    {
      if (number % i == 0)
      {
        returnVal = false;
      }
    }
    return returnVal;
  }

}

【问题讨论】:

  • 也许你的内存快用完了?
  • 埃拉托色尼筛的实施很糟糕。请参考wikipedia link 并查看 gif 示例。
  • isPrime() 方法中,您可以只检查所有主要分隔符(最多sqrt(number))。如果一个数不是素数,它将被素数整除(如果不能被 2 或 3 整除,则不能被 6 整除)。
  • 这不是筛子的低效实施。它只是一个蛮力的主要测试者。
  • @SJuan76:这就是 {he,she} 正在尝试做的事情,但循环被严重破坏:应该在第一次成功测试时返回 false。如果仅测试奇数除数,可能会快 50%。将素数存储在位图中可能会快很多 (long[])

标签: java primes sieve-of-eratosthenes


【解决方案1】:

您似乎正在尝试实施 Eratosthenes 筛,它的性能应该比 O(N^2) 更好(事实上,维基百科说它是 O(N log(log N)) ...)。

根本问题是您对数据结构的选择。您已选择将剩余的素数候选集表示为素数的ArrayList。这意味着您要查看一个数字是否仍在集合中的测试需要 O(N) 比较...其中 N 是剩余素数的数量。然后你正在使用ArrayList.remove(int) 删除非素数......这也是O(N)

所有这些加起来使您的 Sieve 实现O(N^2) 更糟糕

解决方案是将ArrayList&lt;Integer&gt; 替换为boolean[],其中boolean 数组中的位置(索引)表示数字,布尔值表示该数字是质数/可能是质数,或者不是素数。

(还有其他我没有注意到的问题......请参阅其他答案。)

【讨论】:

  • 感谢好心的先生和所有帮助回答的人。我最终使用了一个 boolean[] 数组,将我的机器上的运行时间减少到微不足道的 2-3 秒。 142913828922 是我的正确答案。
  • 我会说 基本 问题是 OP 试图从候选列表中删除复合材料(而不是仅仅标记它们),因此破坏了直接寻址的可能性。即使在int[] 上,在 first 删除之后,直接寻址也是不可能的。 -- 当然有了boolean[],没有任何删除,索引和值的合并导致了极大的简化,所有的问题都像自己一样消失了。 :) 但是,如果一个人一开始就尝试删除复合材料,那么他们最后会使用boolean[]。 :)
  • 并且由于ArrayList&lt;E&gt; implements RandomAccess可以用于,如果使用正确(即没有remove,只有@ 987654339@)。只是内存消耗会比它必须的大,但对于 200 万不是一个大问题。
  • @WillNess - 随意写下你自己的答案:-)
【解决方案2】:

这里有几个问题。首先,让我们谈谈算法。您的 isPrime 方法实际上正是筛子旨在避免的事情。当您在筛子中找到一个数字时,您已经知道它是素数,您无需对其进行测试。如果它不是素数,它已经作为一个较小数字的因素被消除了。

所以,第 1 点:

  • 您可以完全消除isPrime 方法。它永远不应该返回 false。

然后,存在实施问题。 primes.containsprimes.remove 是问题。它们在 ArrayList 上以线性时间运行,因为它们需要检查每个元素或重写后备数组的大部分。

第 2 点:

  • 将值标记到位(使用boolean[],或使用其他更合适的数据结构。)

我通常使用boolean primes = new boolean[upper+1] 之类的东西,并定义n 以包含在!(primes[n]) 中。 (我只是忽略元素 0 和 1,所以我不必减去索引。)要“删除”一个元素,我将其设置为 true。我想你也可以使用TreeSet&lt;Integer&gt; 之类的东西。使用boolean[],该方法几乎是即时的。

第 3 点:

  • sum 需要很长。答案(大约 1.429e11)大于整数的最大值(2^31-1)

如果你愿意,我可以发布工作代码,但这是一个测试输出,没有剧透:

public static void main(String[] args) {
    long value;
    long start;
    long finish;

    start = System.nanoTime();
    value = arrayMethod(2000000);
    finish = System.nanoTime();
    System.out.printf("Value: %.3e, time: %4d ms\n", (double)value, (finish-start)/1000000);

    start = System.nanoTime();
    value = treeMethod(2000000);
    finish = System.nanoTime();
    System.out.printf("Value: %.3e, time: %4d ms\n", (double)value, (finish-start)/1000000);
}

输出:

Using boolean[]
    Value: 1.429e+11, time:   17 ms
Using TreeSet<Integer>
    Value: 1.429e+11, time: 4869 ms

编辑: 由于已发布剧透,这是我的代码:

public static long arrayMethod(int upper) {
    boolean[] primes = new boolean[upper+1]; 
    long sum = 0;
    for (int i = 2; i <=upper; i++) {
        if (!primes[i]) {
            sum += i;
            for (int k = 2*i; k <= upper; k+=i) {
                primes[k] = true;
            }
        }
    }
    return sum;
}

public static long treeMethod(int upper) {
    TreeSet<Integer> primes = new TreeSet<Integer>();
    for (int i = 2; i <= upper; i++) {
        primes.add(i);
    }
    long sum = 0;
    for (Integer i = 2; i != null; i=primes.higher(i)) {
        sum += i;
        for (int k = 2*i; k <= upper; k+=i) {
            primes.remove(k);
        }
    }
    return sum;
}

【讨论】:

  • 他需要isPrime 方法。只是不要将它们应用于prime 列表的成员,而是应用于新的其他成员,即主循环的连续整数(i)。对于它,一个列表更有效(只需迭代它以查找是否有任何素数是被检查的i 的除数)。
  • @SJuan76 不,他没有。按原样尝试他的代码(不过,upper 的值较小),然后将isPrime 中的代码替换为return true;。你的答案不会改变。
  • 你不应该在筛子里remove 任何东西。你的时间证明了这一点。所以,你的第二点没有either 的位置。 :) 不过,IMO,您的回答是最中肯的。
  • 我同意——树形结构更像是一个玩具。
  • 我已经强调了你在那里提出的非常重要的一点。 :)
【解决方案3】:

两件事:

您的代码很难遵循。您有一个名为“素数”的列表,其中包含非素数!

此外,您应该认真考虑数组列表是否合适。在这种情况下,LinkedList 会更有效率。

这是为什么?数组列表必须通过以下方式不断调整数组的大小:请求新内存来创建数组,然后将旧内存复制到新创建的数组中。链接列表只会通过更改指针来调整内存大小。这要快很多!但是,我不认为通过进行此更改可以挽救您的算法。

如果您需要非顺序访问项目,您应该使用数组列表,在这里,(使用合适的算法)您需要顺序访问项目。

另外,你的算法很慢。听听 SJuan76(或 gyrogearless)的建议,谢谢 sjuan76

【讨论】:

  • GyroGearless 建议也是一个很好的建议,isPrime() 方法应该尽快返回,因为他发现这个数字是素数。如果您发现数字在第一次迭代时是偶数,那么检查所有数字是没有意义的!
  • LinkedList 会加速 remove,但不会加速 contains
  • 包含速度是否与 LinkedList 或 ArrayList 相同?
  • 在名为primes 的筛子中存在非质数是正常的,因为在此过程中会去除非质数。 Even I do it that way.
  • @starblue 如果你删除 anything 那么它不是筛子。但是you do not remove, you use primes.clear(j);。这标记组合,因为它应该。 :) 言语会误导;试图 remove 复合材料首先让 OP 陷入困境。
【解决方案4】:

the sieve of Eratosthenes 在现代 CPU 上的经典实现效率的关键是直接(即非顺序)内存访问。幸运的是,ArrayList&lt;E&gt; does implement RandomAccess

筛子效率的另一个关键是它结合了索引和值,就像在integer sorting 中一样。实际上删除序列中的任何数字破坏这种无需任何计算即可直接寻址的能力。我们必须在找到它们时标记而不是删除任何组合,因此任何大于它的数字都将保留在序列中的位置。

ArrayList&lt;Integer&gt; 可用于此(除非占用的内存超出了绝对必要的范围,但对于 200 万而言这是无关紧要的)。

因此,您的代码具有最小的编辑修复(也将 sum 更改为 long,正如其他人所指出的那样),变为

import java.util.ArrayList;

public class Main
{
  /**
   * The sum of the primes below 10 is 2 + 3 + 5 + 7 = 17
   * Find the sum of all the primes below two million.
   * @param args
   */
  public static void main(String[] args)
  {
    ArrayList<Integer> primes = new ArrayList<Integer>();
    int upper = 5000;
    primes.ensureCapacity(upper);
    for (int i = 0; i < upper; i++) {
      primes.add(i);
    }
    long sum = 0;
    for (int i = 2; i <= upper / i; i++) {
      if ( primes.get(i) > 0 ) {
        for (int k = i*i; k < upper ; k+=i) {
          primes.set(k, 0);
        }
      }
    }
    for (int i = 2; i < upper; i++) {
      sum += primes.get(i);
    }
    System.out.println(sum);
  }
}

查找 2000000 in half a second on Ideone 的结果。 projected run time for your original code there:在 10 到 400 小时之间 (!)。

要粗略估计遇到慢代码时的运行时间,您应该始终尝试找出它的empirical orders of growth:运行一些小尺寸n1,然后运行更大尺寸n2,记录运行时间 t1t2。如果t ~ n^a,那么a = log(t2/t1) / log(n2/n1)

对于您的原始代码,在上限值N10k .. 20k .. 40k 范围内测量的经验增长顺序是~ N^1.7 .. N^1.9 .. N^2.1。对于固定代码,它比~ N 快(实际上,它在测试范围0.5 mln .. 1 mln .. 2 mln 内是~ N^0.9)。理论复杂度为O(N log (log N))

【讨论】:

    【解决方案5】:

    您的程序不是埃拉托色尼的筛子;模运算符给出了它。您的程序将是 O(n^2),其中适当的 Eratosthenes 筛子是 O(n log log n),本质上是 n。这是我的版本;我会留给你用适当的数字数据类型翻译成 Java:

    function sumPrimes(n)
        sum := 0
        sieve := makeArray(2..n, True)
        for p from 2 to n step 1
            if sieve[p]
                sum := sum + p
                for i from p * p to n step p
                    sieve[i] := False
        return sum
    

    如果您对使用素数进行编程感兴趣,我在我的博客中谦虚地推荐 this essay

    【讨论】:

      猜你喜欢
      • 2011-05-29
      • 1970-01-01
      • 1970-01-01
      • 2015-07-01
      • 2018-10-09
      • 2013-05-15
      • 1970-01-01
      相关资源
      最近更新 更多