记一次筛素数算法的优化

求出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\n", 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\n", 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\n", 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优化来得比较麻烦。

时间: 2024-12-15 07:09:49

记一次筛素数算法的优化的相关文章

素数算法的优化之路

一.素数的定义 质数又称素数.指在一个大于1的自然数中,除了1和此整数自身外,不能被其他自然数(不包括0)整除的数.因为合数是由若干个质数相乘而得来的,所以,没有质数就没有合数,由此可见质数在数论中有着很重要的地位. 比如:2,3,5,7,9.....都是素数. 二.构造素数算法 写算法之前,先来说说以下这个东西: 对于任意一个合数n,如果它有两个质因子x,y,显然n = x*y, 所以,由不等式性质可得,x <= sqrt(n), 即 x <= n^(1/2). 推广一下,对于任意一个合数,

最短路的几种算法及其优化(模板)

一.Dijkstra 算法 dijkstra算法适用于边权为正的情况,求单源最短路,适用于有向图和无向图 模板伪代码: 清除所有点的标号 设d[0]=0,其余d[i]=INF: 循环n次{ 在所有未标记的节点中,寻找d[i]最小的点x 给x做标记 对于从x出发的所有边(x,y)更新d[y]=min(d[y],d[x]+w[x,y]); } memset(v,0,sizeof(v)); for(int i=0;i<n;++i) d[i]=(i==0?0:INF); for(int i=0;i<n

几种简单的求素数算法的复杂度分析

素数的算法有很多种,现在主要讲两种算法及其改进版本的复杂度分析,解释性能提升的幅度.现以求100000内素数为例,两种算法分别是: 1.基础思路是去掉偶数,包括取模的范围,代码如下: print(2) for i in range(3,100000,2): for a in range(3,int(i*0.5)+1,2): if i%a == 0: break else: print(i,end = ' ')此两层循环的算法的复杂度为0.5n((n**0.5+1)/2) 2.应用一个素数定理:大

试探究一种查找素数算法

解题思路:构造链表,使用筛除法 例如:求10以内素数 链表初始化:2 3 4 5 6 7 8 9 10 进行第一轮筛选后:2 3 5 7 9 也就是用2后面的数去除2, 第二轮筛选后:2 3 5 7 也就是用3后面的数去除3, 第三轮筛选后:2 3 5 7 也就是用5后面的数去除5 第四轮筛选后:2 3 5 7 代码: #include <stdio.h> #include <stdlib.h> #define N 1e5 // over this it is so slowly

算法的优化(C语言描述)

算法的优化 算法的优化分为全局优化和局部优化两个层次.全局优化也称为结构优化,主要是从基本控制结构优化.算法.数据结构的选择上考虑:局部优化即为代码优化,包括使用尽量小的数据类型.优化表达式.优化赋值语句.优化函数参数.全局变量及宏的使用等内容. 一.全局优化 1.优化算法设计 例如,在排序中用快速排序或者堆排序代替插入排序或冒泡排序:用较快的折半查找代替顺序查找法等,都可以极大地提高程序的执行效率. 2.优化数据结构 例如在一堆随机存放的数中使用了大量的插入和删除指令,那么使用链表要快得多.数

Light 1289 LCM from 1 to n 素数筛选位优化

题目来源:Light 1289 LCM from 1 to n 题意:.. 思路:从1到n 打过某个数是以一个素数的几次方 那么答案就乘以这个素数 主要是筛选素数 存不下 位优化 一个整数32位标记32个数 内存缩小32倍 是学习别人的 #include <cstdio> #include <cstring> #include <cstdio> #include <cmath> using namespace std; const int maxn = 10

java算法插入排序优化代码

原文:java算法插入排序优化代码 代码下载地址:http://www.zuidaima.com/share/1550463280630784.htm 一个细节让插入排序更具效率 运行此方法需要为main方法传递参数 package com.zuidaima.sort; /** *@author www.zuidaima.com **/ public class TestSort { public static void main(String args[]){ int l = args.len

结对测试算法性能优化(用例设计层面)

在<结对测试算法性能优化(代码层面)>一文中, 对原来算法代码进行了一些优化, 对于笛卡尔积后千条数据,是能满足使用需要的. 但在实际业务中,会碰到百万数据. 比如某接口共18个参数,每个参数均可为空,其中8个只需要单个值,10个为多选项,需要多个值. 对于多选项,我的设计是,全选+随机n个多选(1<=n<=len-1)+空. 按照这个策略,笛卡尔积的结果就是3^8*2^10=6718464. 671万数据! parewise根本处理不动. 该怎么处理? 调整用例设计. 1.为空的

关于SPFA算法的优化方式

关于SPFA算法的优化方式 这篇随笔讲解信息学奥林匹克竞赛中图论部分的求最短路算法SPFA的两种优化方式.学习这两种优化算法需要有SPFA朴素算法的学习经验.在本随笔中SPFA朴素算法的相关知识将不予赘述. 上课! No.1 SLF优化(Small Label First) 顾名思义,这种优化采用的方式是把较小元素提前. 就像dijkstra算法的堆优化一样.我们在求解最短路算法的时候是采取对图的遍历,每次求最小边的一个过程,为了寻找最小边,我们需要枚举每一条出边,如果我们一上来就找到这个边,那