• 记一次筛素数算法的优化


    求出1..n中有多少个素数的算法我们通常用筛数法,一种比较简单的实现如下:

     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 
     4 int main(const int argc, const char *argv[], const char *envp[])
     5 {
     6     int64_t n;
     7     int64_t i, j;
     8     int64_t *arr;
     9     int64_t cnt = 0;
    10 
    11     if (argc != 2) {
    12         return -1;
    13     }
    14 
    15     sscanf(argv[1], "%lld", &n);
    16 
    17     arr = (int64_t *)calloc(n + 1, sizeof(int64_t));
    18 
    19     for (i = 2; i <= n; i++) {
    20         arr[i] = 1;
    21     }
    22 
    23     for (i = 2; i < n; i++) {    // outer for-loop
    24 
    25         if (arr[i] == 0) {
    26             continue;
    27         }
    28 
    29         for (j = i + i; j <= n; j += i) {    // inner for-loop
    30             arr[j] = 0;
    31         }
    32     }
    33 
    34     for (i = 2; i <= n; i++) {
    35         cnt += arr[i] != 0;
    36     }
    37 
    38     printf("%lld
    ", cnt);
    39 
    40     return 0;
    41 }

    这里首先解释一下为什么外层循环不用取n,因为如果n是合数,显然n已经在i为n最小素因数的时候就已经被筛掉了;否则n是素数不用筛。

    取n=100000000测测运行时间:

    1  ~/ time ./test1 100000000
    2 5761455
    3 ./test1 100000000  2.92s user 0.23s system 99% cpu 3.157 total

    现在我们注意到:每次进入内层for循环的时候,实际上对于素数i,所有比i2小的i的倍数{j * i | j < i}都一定有一个比i更小的素因数,那我们其实可以将j从i2开始迭代,这样就可以节约一定的时间,简易实现如下:

     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 
     4 int main(const int argc, const char *argv[], const char *envp[])
     5 {
     6     int64_t n;
     7     int64_t i, j;
     8     int64_t *arr;
     9     int64_t cnt = 0;
    10 
    11     if (argc != 2) {
    12         return -1;
    13     }
    14 
    15     sscanf(argv[1], "%lld", &n);
    16 
    17     arr = (int64_t *)calloc(n + 1, sizeof(int64_t));
    18 
    19     for (i = 2; i <= n; i++) {
    20         arr[i] = 1;
    21     }
    22 
    23     for (i = 2; i < n; i++) {    // outer for-loop
    24 
    25         if (arr[i] == 0) {
    26             continue;
    27         }
    28 
    29         for (j = i * i; j <= n; j += i) {    // inner for-loop
    30             arr[j] = 0;
    31         }
    32     }
    33 
    34     for (i = 2; i <= n; i++) {
    35         cnt += arr[i] != 0;
    36     }
    37 
    38     printf("%lld
    ", cnt);
    39 
    40     return 0;
    41 }

    仅仅改动了一个字符('+' → '*'),有多大的时间收益呢?

    1  ~/ time ./test2 100000000
    2 5761455
    3 ./test2 100000000  2.02s user 0.25s system 99% cpu 2.263 total

    时间减少了近1s,几乎节约了1/3的时间,这个收效很明显啊。而且从输出来看,确实应该是正确的。(我知道单个测试样例不能证明程序的正确性,但是对于筛素数的算法而言,n取得足够大还能对,这就是一种佐证)

    那我们再来思考思考还能不能再有所提升?

    我们继续考虑内层循环,其对arr[j]的访问实际上相当没有空间连续性,并且因此也失掉了向量化的可能性。考虑到进入内层循环时所有比i小的素数的倍数都已经被筛过了,所以对于i的在[i, n/i]范围内的倍数中的也有很大部分被筛过了,也就是说我们仅需要筛掉[i, n/i]中尚未被筛掉的数的i倍的那个数{j * i | j ∈ [i, n/i],且j未被筛掉}。这样的话我们首先可以得出一个推论:外层循环只需要取到sqrt(n)即可,实际中我直接用i * i <= n来作为外层循环的条件。另外,我们应当尽量消除条件分支,由于判断j是否被筛掉会造成一个if语句,所以我们可以改成内存循环中始终将arr[k]置0,若j未被筛掉则k = j * i,否则k = 0,综合起来就是k = arr[j] * j * i,这样就去掉了if语句并且利用了arr[0]留空这点,同时还使得如果不需要筛掉j * i的情况有很好的空间局部性。更进一步的,由于总是会计算arr[j] * j,所以我们的arr[]不要仅用来存放{0, 1},而是直接用arr[j]存放j,这样k就是arr[j] * i。这样程序就变成了:

     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 
     4 int main(const int argc, const char *argv[], const char *envp[])
     5 {
     6     int64_t n;
     7     int64_t i, j;
     8     int64_t *arr;
     9     int64_t cnt = 0L;
    10 
    11     if (argc != 2) {
    12         return -1;
    13     }
    14 
    15     sscanf(argv[1], "%lld", &n);
    16 
    17     arr = (int64_t *)calloc(n + 1, sizeof(int64_t));
    18 
    19     for (i = 2; i <= n; i++) {
    20         arr[i] = i;
    21     }
    22 
    23     for (i = 2; i * i <= n; i++) {    // outer for-loop
    24 
    25         if (arr[i] == 0) {
    26             continue;
    27         }
    28 
    29         for (j = n / i; j >= i; j--) {    // inner for-loop
    30             const int64_t k = arr[j] * i;
    31             arr[k] = 0;
    32         }
    33     }
    34 
    35     for (i = 2; i <= n; i++) {
    36         cnt += arr[i] != 0;
    37     }
    38 
    39     printf("%lld
    ", cnt);
    40 
    41     return 0;
    42 }

    之所以这里内存循环变成了降序的迭代,是因为避免j和j * i同时都没被筛掉过的话,且j * i2 < n的话,会引起错误。

    这次程序改动较大,那有没有较大的时间上的收益呢?

    1  ~/ time ./test3 100000000
    2 5761455
    3 ./test3 100000000  1.01s user 0.25s system 99% cpu 1.263 total

    又减少了1s,只不过这1s优化来得比较麻烦。

  • 相关阅读:
    守卫者的挑战
    黑魔法师之门
    noip2015 普及组
    noip2015 提高组day1、day2
    40026118素数的个数
    高精度模板
    经典背包系列问题
    修篱笆
    [LintCode] Linked List Cycle
    [LintCode] Merge Two Sorted Lists
  • 原文地址:https://www.cnblogs.com/FreeBirdLjj/p/4470749.html
Copyright © 2020-2023  润新知