【问题标题】:How can I improve my prime number program with Sieve of Eratosthenes algorithm?如何使用 Sieve of Eratosthenes 算法改进我的素数程序?
【发布时间】:2019-01-07 10:42:40
【问题描述】:

我的程序从这个表达式中打印出所有素数:

((1 + sin(0.1*i))*k) + 1, i = 1, 2, ..., N.

输入格式:

不超过 100 个示例。每个示例在同一行上都有 2 个正整数。

输出格式:

在单独的行上打印每个数字。

示例输入:

4 10

500 100

样本输出:

5

17

但是我的算法效率不够。如何添加 Eratosthenes 筛子,使其足够高效,不打印“因超时而终止”。

#include <iostream>
#include <cmath>
using namespace std;

int main() {
long long k, n;
int j;
    while (cin >> k >> n) {
    if (n>1000 && k>1000000000000000000) continue;

    int count = 0;
    for (int i = 1; i <= n; i++) {
        int res = ((1 + sin(0.1*i)) * k) + 1;
        for (j = 2; j < res; j++) {
            if (res % j == 0) break;
        }
        if (j == res) count++;
    }
    cout << count << endl;

}
system("pause");

【问题讨论】:

  • 我认为您应该将您的问题迁移到codereview.stackexchange.com
  • 我是否理解正确,您需要计算数字 r=((1+sin(0.1*i))*k)+1 中的素数数量为 i=1..n 和一组最多 100 对 (n,k)
  • r 将是表达式的数字结果。然后检查它是否是素数,忽略最多100对的集合。
  • 在您的代码中,res 通常有多大?
  • 在尝试提高速度之前,我们必须确保程序提供正确的输出。这就是我提到这个问题的原因。使用大的kint 可以溢出

标签: c++ primes


【解决方案1】:

只需在试用部门做得更好,您就可以将速度提高 10 倍。您正在测试从 2 到 res 的所有整数,而不是将 2 视为特殊情况并仅测试从 3 到 res 平方根的奇数:

// k <= 10^3, n <= 10^9

int main() {
    unsigned k;
    unsigned long long n;

    while (cin >> k >> n) {

        unsigned count = 0;

        for (unsigned long long i = 1; i <= n; i++) {
            unsigned long long j, res = (1 + sin(0.1 * i)) * k + 1;

            bool is_prime = true;

            if (res <= 2 || res % 2 == 0) {
                is_prime = (res == 2);
            } else {
                for (j = 3; j * j <= res; j += 2) {
                    if (res % j == 0) {
                        is_prime = false;
                        break;
                    }
                }
            }

            if (is_prime) {
                count++;
            }
        }

    cout << count << endl;
    }
}

虽然 k = 500 和 n = 500000000 仍然需要 40 秒左右。

【讨论】:

  • 您的代码适用于(k &lt;= 10^18, N &lt;= 10^3),但不适用于(k &lt;= 10^3, N &lt;= 10^9)。我将n 更改为long long,但它仍然打印“因超时而终止”。
  • @Erj,限制的来源是什么?请提供链接。我是根据你的代码测试的,否则我怎么知道?
  • k &lt;= 10^3, N &lt;= 10^9。其他的都是一样的。
  • @Erj,我用修改后的(反向)限制重新编写了代码。当您接近这些限制时,我还添加了一个时间示例。
【解决方案2】:

编辑:我添加了第三种方法来提高效率
EDIT2:添加了为什么 Sieve 不应该是解决方案和一些三角关系的解释。此外,我添加了一个关于问题历史的注释

您的问题不是要计算给定范围内的所有素数,而是仅计算由您的函数生成的素数。

因此,我不认为埃拉托色尼筛法是这个特定练习的解决方案,原因如下:n 总是相当小,而k 可能非常大。如果k非常大,那么 Sieve 算法将不得不生成大量素数,最终将其用于少数候选者。

您可以通过三种方式提高程序的效率:

  • 避免每次都计算sin(.)。例如,您可以使用三角关系。此外,第一次计算这些值时,将它们存储在一个数组中并重复使用这些值。计算sin()非常耗时
  • 在检查数字是否为素数的测试中,将搜索限制为sqrt(res)。此外,考虑仅使用奇数j 进行测试,加上2
  • 如果候选人 res 等于前一个,避免重做测试

一些三角函数
如果 c = cos(0.1) 和 s = sin(0.1),则可以使用关系:

  • sin (0.1(i+1)) = s*cos (0.1*i) + c*sin(0.1*i))
  • cos (0.1(i+1)) = c*cos (0.1*i) - s*sin(0.1*i))

如果n很大,则需要定期通过函数重新计算sin(),以避免过多的舍入误差计算。但这里不应该是这样,因为n 总是相当小。

但是,正如我所提到的,最好在第一步中只使用“记忆”技巧并检查它是否足够。

关于这个问题的历史和为什么这个答案的注释:

最近,这个网站收到了几个问题“如何改进我的程序,计算这个k*sin()函数生成的素数的数量......”据我所知,这些问题都被关闭为重复,原因是Sieve 是解决方案,并在之前的类似(但略有不同)问题中进行了解释。现在,同样的问题以稍微不同的形式再次出现“我如何在这个程序中插入 Sieve 算法......(再次使用 k*sin())”。然后我意识到筛子不是解决方案。这不是对先前关闭的批评,因为我在对问题的理解上犯了同样的错误。但是,我认为是时候提出一个新的解决方案了,即使它与新问题不完全匹配

【讨论】:

  • 问题是,我必须使用 sin 表达式 ((1 + sin(0.1*i))*k) + 1
  • 我知道。但是使用三角函数,您可以从sin(0.1 * i) 计算sin(0.1 * (i+1))。问题不是避免计算正弦值,而是尽可能避免调用sin(.)函数
  • 无论如何,第一步,你可以尝试使用记忆技巧,看看你有什么收获
  • 我不明白如何每次都避开sin(.)
【解决方案3】:

当您使用简单的Wheel factorization 时,您可以获得非常好的代码加速。 2 阶轮因式分解利用了所有大于 3 的素数都可以写成 6n+16n+5 来表示自然 n。这意味着您只需对每 6 个数字进行 2 个除法。或者更进一步,所有大于 5 的素数都可以写成 30n+m,其中 m{1,7,11,13,17,19, 23,29}。 (每 30 个数字 8 个分区)。

使用这个简单的原理,您可以编写以下函数来测试您的素数(轮 {2,3}):

bool isPrime(long long num) {
  if (num == 1)     return false;   // 1 is not prime
  if (num  < 4)     return true;    // 2 and 3 are prime
  if (num % 2 == 0) return false;   // divisible by 2
  if (num % 3 == 0) return false;   // divisible by 3
  int w = 5;
  while (w*w <= num) {
      if(num % w     == 0) return false; // not prime
      if(num % (w+2) == 0) return false; // not prime
      w += 6;
  }
  return true;                     // must be prime
}

您可以将上述内容调整为轮子 {2,3,5}。这个函数在主程序中可以这样使用:

int main() {
  long long k, n;

  while (cin >> k >> n) {
    if (n>1000 && k>1000000000000000000) continue;
    int count = 0;
    for (int i = 1; i <= n; i++) {
      long long res = ((1 + sin(0.1*i)) * k) + 1;
      if (isPrime(res)) { count++; }
    }
    cout << count << endl;
  }
  return 0;
}

一个简单的时间给我原始代码 (g++ prime.cpp)

 % time echo "6000 100000000" | ./a.out 
 12999811
 echo "6000 100000000"  0.00s user 0.00s system 48% cpu 0.002 total
 ./a.out  209.66s user 0.00s system 99% cpu 3:29.70 total

虽然优化版给了我

% time echo "6000 100000000" | ./a.out                                                                                                                                                                                                        
12999811
echo "6000 100000000"  0.00s user 0.00s system 51% cpu 0.002 total
./a.out  10.12s user 0.00s system 99% cpu 10.124 total

可以进行其他改进,但可能会产生轻微影响:

  1. 预先计算您的正弦表sin(0.1*i) for i 从0 到1000。这将避免一遍又一遍地重新计算这些正弦。不过,这影响很小,因为大部分时间都浪费在了最重要的时刻。
  2. 检查res(i) == res(i+1):这几乎没有任何影响,因为nk 最连续的res 不相等。
  3. 使用查找表,可能更方便,这确实有影响。

原答案:

我的建议如下:

  1. i 预先计算正弦表sin(0.1*i) 从0 到1000。这样可以避免一遍又一遍地重新计算这些正弦。另外,要聪明一点(见第 3 点)
  2. 找到res 的最大可能值,即res_max=(2*k)+1
  3. 使用Sieve of Eratosthenes 查找res_max 的所有质数。另外,要意识到所有大于 3 的素数都可以写成 6n+16n+5 来表示自然 n。或者更进一步,所有大于 5 的素数都可以写成 30n+m,其中 m{1,7,11,13,17,19, 23,29}。这就是所谓的Wheel factorization。所以不要费心检查任何其他号码。 (更多信息here

  4. 有一个查找表来说明一个数字是否为素数。

  5. 对查找表执行所有循环。

【讨论】:

  • 对于大的k 和小的n,这可能仍然是低效的,因为素数查找表中的大多数条目从未使用过。您可以使用primality test 而不是那个查找表和 Eratosthenes 筛子,尽管这对于您的问题来说可能太高级了。
  • 我做了与你类似的功能,但它仍然给出“因超时而终止”。
  • @Erj 你能给我那个超时的输入吗?
  • @kvantour 它对hackerrank隐藏。
  • @cdlane 的程序有效。感谢所有提供帮助的人。
猜你喜欢
  • 2012-02-21
  • 1970-01-01
  • 1970-01-01
  • 2012-02-21
  • 1970-01-01
  • 2018-08-10
  • 1970-01-01
  • 2013-05-12
  • 2016-04-05
相关资源
最近更新 更多