任务

求解第 10,0000、100,0000、1000,0000 ... 个素数(要求精确解)。

想法

Sieve of Eratosthenes

学习初等数论的时候曾经学过埃拉托斯特尼筛法(Sieve of Eratosthenes),这是一种非常古老但是非常有效的求解\(p_n\)的方法,其原理非常简单:从2开始,将每个素数的各个倍数都标记成合数。

其原理如下图所示:

图引自维基百科

埃拉托斯特尼筛法相比于传统试除法最大的优势在于:筛法是将素数的各个倍数标记成合数,而非判定每个素数是否是素数的倍数,使用了加法代替了除法,降低了时间复杂度。

举个简单的例子,已知2、3、5、7是素数,求小于49的其余素数。

试除法

对于每一个数\(m ( 7 < m < 49 )\)而言,都要进行如下判定:

  • 求出 \({\lceil}\sqrt{m}{\rceil} + 1 = n\) ,试除 $ n $以内的素数即可判定 $ m $是否为素数。
  • m / 2 ... 不整除。
  • m / 3 ... 不整除。
  • ...

对于m而言,只有两种情况可以结束试除:

  1. m 是素数,要把所有候选素数都试除一遍。
  2. m 是合数,可以被某个素数整除。

对于试除法而言,假设新找到 x 个素数, y 个合数 ,需要试除的素数因子有 m 个,则至少需要做 $ x * m + y $次除法。

埃拉托斯特尼筛法

  • 2,2 + 2 = 4, 4 + 2 = 6 ... 48 + 2 = 50 > 49,下一个。
  • 3,3 + 3 = 6, 6 + 3 = 9 ... 48 + 3 = 51 > 49,下一个。
  • 5,5 + 5 = 10, 10 + 5 = 15 ... 45 + 5 = 50 > 49,下一个。
  • 7, 7 + 7 = 14, 14 + 7 = 21 ... 42 + 7 = 49 = 49,下一个。
  • 没有小于 8 的素数了,停止标记,没有标记到的都是素数。

实际上,假设在 \(\sqrt{x}\) 以内有 $ m $个素数,在 \((\sqrt{x},x]\)范围内又找到了 $n $个素数,可以明显看到试除法和筛法的差异:

  1. 试除法至少要做 $ n * m + x - n - m (1)$次除法
  2. 筛法需要做 $ x - n - m (2)$次加法

做一次除法运算的时间要大于加法,且有 \((1)\) > \((2)\),所以试除法开销远比筛法大。

现在回到问题本身,问题本身是求解第 n 个素数,所以我们一开始并不知道我们需要在多大的范围内求解素数,但是许多数学家给了我们很多定理,比如这个第\(n\)个素数\(p_n\)的不等式。

\[p_n < n \ln n + n \ln \ln n\! ( n ≥ 6 )
\]

有了这个函数,我们可以在输入 $ n $之后用它估算第 $ n $个素数的上界,继而求解。

优化一:朴素筛法

输入的 \(n\) 值为素数的序号,假设求得的素数上界为 limit。为了让筛法跑得更快,在内存允许的范围内,我们可以直接用一个limit大小的数组 sieve[limit] 求解:下标是int,值为bool,数组初始值均为 true,表示都未标记。假设下标为 index

  • index = 2,sieve[index] = true,开始标记,逐次将 sieve[index+2]标记为false,直到下标超过limit。
  • index = 3,sieve[index] = true,开始标记,逐次将 sieve[index+3]标记为false,直到下标超过limit。
  • index = 4, sieve[index] = false,跳过。

    ...

按照上述方式标记完成后,最终在数组中过滤一遍,求得第 n 个未被标记的下标值,即为第 n 个素数。

附代码如下:

public static void Normal_Sieve(int nth)
{
int startTime = System.Environment.TickCount;
int limit = nth < 6 ? 25 : (int)(nth * (Math.Log(nth) + Math.Log(Math.Log(nth))));
int count = 0; List<bool> is_prime = new List<bool>(limit+1); for (int i = 0; i < limit+1; i++)
is_prime.Add(true); for (int i = 2; i * i <= limit; i++)
if (is_prime[i])
for (int j = i * i; j <= limit; j += i)
is_prime[j] = false; for(int i=2;i<is_prime.Count();i++)
{
if (is_prime[i])
count++;
if(count == nth)
{
Console.WriteLine("The nth_prime is:{0} SpentTime:{1}ms",i,Environment.TickCount- startTime);
break;
}
}
}

优化二:位筛法

位筛法相比于简单筛法的改进就是:用1个比特位来标记某个下标是否是素数。1个bool类型要占 8 位,用1个比特位可以使程序在极限内存容量的情况下,比普通筛法能多计算一些素数。

举个例子,按照上面的普通筛法,因为内存大小的限制最多只能求解第 1000,000,000 个素数,那么位筛法就可以求解到第 7000,000,000 个素数。

下面附上位筛法的代码

public static void Bit_Sieve(int nth)
{
int startTime = Environment.TickCount;
int limit = nth < 6 ? 25 : (int)(nth * (Math.Log(nth) + Math.Log(Math.Log(nth))));
int count = 0; int total = limit + 1;
int sqrt = (int)Math.Sqrt(limit) + 1; //[31 30 29 ... 0] every number maps to a bit in uint.
List<uint> is_prime = new List<uint>((total >> 5) + 1); for (int i = 0; i < (total >> 5) + 1; i++)
is_prime.Add(0x0); for (int i = 2; i <= sqrt; i++)
// is_prime[i>>5] bit i % 32 == 0 means it is a prime
if ((is_prime[i >> 5] & (1 << (i & 31))) == 0)
{
for (int j = i * i; j <= total; j += i)
// is_prime[j>>5] bit j % 32 = 1;
is_prime[j >> 5] |= (uint)1 << (j & 31);
} for (int i = 2; i < total; i++)
{
if ((is_prime[i >> 5] & (1 << (i & 31))) == 0)
{
count++;
if (count == nth)
{
Console.WriteLine("The {0}th_prime is:{1} SpentTime:{2}ms",nth , i, Environment.TickCount - startTime);
break;
}
}
}
}

在位筛法中大部分运算都是移位、与和或的运算,所以在测试时发现要比简单的筛法更快一些。

优化三:局部筛法

假设计算得出的第\(n\)个素数上界为\(x\),在位筛法中,我们申请了一个大小为\(x bit\)的数组用来标记合数。随之而来一个问题,难道在筛法中,我们必须使用 \(x bit\)才能求出第 n 个素数吗?

当然不是,对于素数上界\(x\),其实不需要这么大的空间,我们只需把\(\sqrt{x}\)以前的素数保存下来即可。

为什么一定是 \(\sqrt{x}\)呢?想象一下最暴力的试除法:它在判定一个数是否为素数时,需要遍历试除\(\sqrt{x}\)及以内的素因子。如果\(x\)是一个合数,则必然存在一个素因子整除\(x\),且其值小于等于\(\sqrt{x}\)。那么反过来想一下,在筛法中,如果\(\sqrt{x}\)及以内的所有素因子都没有标记\(x\)为合数,那么它一定是一个素数了。

基于这样的思想,我们将\([0,\sqrt{x}]\)内的素数保存下来,然后对\([\sqrt{x},x]\)分段去扫,每扫一段就记录一下本段扫到了多少个素数。这样每次需要载入内存的就只有用于扫描的小素数数组被扫描的段数组,相比位筛法可以节省更多内存空间。

下面举个简单的例子来说明这种算法:

假设要求解第11个素数(31),我们估计出上界为 36,然后下面就是局部筛法的求解过程。

  • \(\sqrt{36}=6\),然后利用埃拉托斯特尼筛法求出[2,6]内的素数,即数组 [2,3,5],此时素数有 3 个。
  • 申请一个大小为10的数组 **A **存储被扫描段。
  • 将[7,36]内的元素按照10个元素一组的方式分成三组(Ps:最后一组不够10个元素)
  • 初始化数组A,此时数组A的下标[0,9]分别映射到[7,16]。
    • 用素数2开始标记,因为8、10、12...是2的倍数,所以标记A[1]、A[3]、A[5]...为合数。
    • 用素数3开始标记,因为9、12、15...是3的倍数,标记A[2]、A[5]、A[8]...为合数。
    • 用素数5开始标记,因为10、15是5的倍数,标记A[3]、A[8]为合数。
    • 扫描A数组内未被标记元素下标为:0、4、6、10,所以这一段有 4 个素数,把它们对应的实际数字存入素数数组。
    • 现在已经发现了 7 个素数,还未达到预期的11个,继续扫描。
  • 重新初始化数组A,此时数组A的下标[0,9]分别映射到[17,26]。
    • 与上述过程一样,用素数2、3、5,分别扫描一遍。
    • 记下未被标记的数字,这一段扫描到了 2个素数,到现在已经发现了 9个素数。
  • 重新初始化数组A,继续扫描寻找。

局部筛法的求解过程如上所述,因为每个时刻内存中只需要一个段的内存来存放需要扫描的段,而不需要一次性把所有的段都加载到内存中进行筛选,所以其对内存的要求更低。

下面附上局部筛法的代码

public static void Local_Bit_Sieve(int nth)
{
int startTime = Environment.TickCount;
int limit = nth < 6 ? 25 :(int)(nth * (Math.Log(nth) + Math.Log(Math.Log(nth))));
int sqrt = (int) Math.Sqrt(limit) + 1; //Get all primes which are less than \sqrt{limit}
List<uint> isPrime = new List<uint>((sqrt >> 5) +1);
for (int i = 0; i < (sqrt >> 5) + 1; i++)
isPrime.Add(0x0);
for (int i = 2; i * i <= sqrt; i++)
// is_prime[i>>5] bit i % 32 == 0 means it is a prime
if ((isPrime[i >> 5] & (1 << (i & 31))) == 0)
{
for (int j = i * i; j <= sqrt; j += i)
// is_prime[j>>5] bit j % 32 = 1;
isPrime[j >> 5] |= (uint)1 << (j & 31);
} //smallPrimes store the primes
List<int> smallPrimes = new List<int>();
for (int i = 2; i < sqrt; i++)
{
if ((isPrime[i >> 5] & (1 << (i & 31))) == 0)
{
smallPrimes.Add(i);
}
} int segSize = Math.Max(sqrt,256 * 256); uint[] primeSeg = new uint[segSize]; //allPrimes store all primes which are found.
List<int> allPrimes = new List<int>();
allPrimes.AddRange(smallPrimes); int high = segSize << 5;
//chunk [2,limit] into different segments
for (int low = sqrt; low <= limit; low += segSize << 5)
{
Array.Clear(primeSeg,0,segSize);
//for each prime, use them to mark the [low,low + segSize]
foreach (var sPrime in smallPrimes)
{
int initValue = low % sPrime;
for (int i = (initValue == 0 ? initValue : sPrime - initValue); i < high; i += sPrime)
{
primeSeg[i >> 5] |= (uint) 1 << (i & 31);
}
} for (int i = 0; i < high; i++)
{
if ((primeSeg[i >> 5] & (1 << (i & 31))) == 0)
allPrimes.Add(i + low);
} if (allPrimes.Count() > nth)
{
Console.WriteLine("The {0}th_prime is:{1} SpentTime:{2}ms",nth, allPrimes[nth-1],
Environment.TickCount - startTime);
break;
}
}
}

优化四:局部筛法段优化

我们之前之所以说使用朴素筛法可以跑得飞快,是因为从直觉上说,朴素筛法一次性将所有的数据都加载到内存中,一次筛完。而局部筛法却需要筛取多次,筛取多次好像时间开销就要比一次加载筛取要多。

看起来局部筛法好像需要筛多次,所以它所耗费的时间就要比一次筛的要慢,但实际上却不是这样。实际上,计算机系统中Cache的存在对局部筛法更加有利,所耗费的时间也更小。我们分段筛选,反而更加符合空间上的局部性,所以程序可以更高效地利用Cache,Cache的存取速度要远大于内存,所以局部筛法耗费的时间要比朴素筛更少。这个问题可以这么形容:是用一个振动频率比较慢,但是很大的筛子筛选比较快,还是用连续用多个振动频率较快,但是比较小的筛子筛快?

我们可以通过选择一个合理的段大小来减少 Cache miss的概率。我电脑上的L1Cache容量是 256KB,所以最后我选择了 256 * 256 作为段的大小。

Benchmark

n 简单筛法 位筛法 局部位筛法
1,000,000 1.75s 0.938s 0.5s
2,000,000 4.562s 4.328s 2.156s
3,000,000 7.063s 7.140s 3.140s
5,000,000 13.328s 11.407s 4.750s
8,000,000 22.484s 17.110s 9.140s
10,000,000 23.563s 22.347s 11.078s
20,000,000 57.219s 53.313s 22.734s

目前的测试只是大致估计,这个数字不是很准,仅供参考。

[软工课程博客] 求解第N个素数的更多相关文章

  1. [BUAA软工]第一次博客作业---阅读《构建之法》

    [BUAA软工]第一次博客作业 项目 内容 这个作业属于哪个课程 北航软工 这个作业的要求在哪里 第1次个人作业 我在这个课程的目标是 学习如何以团队的形式开发软件,提升个人软件开发能力 这个作业在哪 ...

  2. 2020BUAA软工个人博客作业-软件案例分析

    2020BUAA软工个人博客作业-软件案例分析 17373010 杜博玮 项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 个人博客作业-软件案例分 ...

  3. 2020BUAA软工个人博客作业

    2020BUAA软工个人博客作业 17373010 杜博玮 项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 个人博客作业 我在这个课程的目标是 学 ...

  4. [敏捷软工团队博客]Beta阶段事后分析

    设想和目标 我们的软件要解决什么问题?是否定义得很清楚?是否对典型用户和典型场景有清晰的描述? 我们的软件要解决的问题是:现在的软工课程的作业分布在博客园.GitHub上,没有一个集成多种功能的一体化 ...

  5. [敏捷软工团队博客]Beta阶段项目展示

    团队成员简介和个人博客地址 头像 姓名 博客园名称 自我介绍 PM 测试 前端 后端 dzx 秃头院的大闸蟹 大闸蟹是1706菜市场里无菜可卖的底层水货.大闸蟹喜欢音乐(但可惜不会),喜欢lol(可惜 ...

  6. [敏捷软工团队博客]项目介绍 & 需求分析 & 发布预测

    项目 内容 2020春季计算机学院软件工程(罗杰 任健) 博客园班级博客 作业要求 团队项目选择 我们在这个课程的目标是 在团队合作中锻炼自己 这个作业在哪个具体方面帮助我们实现目标 了解项目整体情况 ...

  7. [敏捷软工团队博客]The Agiles 团队介绍&团队采访

    项目 内容 课程:北航-2020-春-敏捷软工 博客园班级博客 作业要求 团队作业-团队介绍和采访 团队名称来源 The Agile is The Agile. 敏捷就是敏捷.我们只是敏捷的践行者罢了 ...

  8. 自我介绍&软工实践博客点评

    想想既然写了点评博客,那就顺便向同学们介绍下自己吧. 我是16届计科实验班的,水了两件小黄衫,于是就来当助教了_(:_」∠)_ 实话说身为同届生来当助教,我心里还是有点虚的,而且我还是计科的..感觉软 ...

  9. [敏捷软工团队博客]Beta阶段发布声明

    项目 内容 2020春季计算机学院软件工程(罗杰 任健) 博客园班级博客 作业要求 Beta阶段发布声明 我们在这个课程的目标是 在团队合作中锻炼自己 这个作业在哪个具体方面帮助我们实现目标 对Bet ...

随机推荐

  1. Failed to abandon session scope: Connection timed out

    系统log 出现  Failed to abandon session scope: Connection timed out  错误, reboot无法重启 解决办法就是让postfix只用IPv4 ...

  2. RMAN删除备份

    删除备份--DELETE命令 用于删除RMAN备份记录及相应的物理文件.当使用RMAN执行备份操作时,会在RMAN资料库(RMAN Repository)中生成RMAN备份记录,默认情况下RMAN备份 ...

  3. WorldWind源码剖析系列:设置类SettingsBase

    PluginSDK中的星球设置类WorldSettings 和WorldWind.程序设置类WorldWindSettings均继承自父类SettingsBase.类图如下所示.其中父类Setting ...

  4. Android使用正则表达式验证手机号

    国内手机号代码段分配如下: 移动:134.135.136.137.138.139.150.151.157(TD).158.159.187.188 联通:130.131.132.152.155.156. ...

  5. 剖析管理所有大数据组件的可视化利器:Hue

    日常的大数据使用都是在服务器命令行中进行的,可视化功能仅仅依靠各个组件自带的web界面来实现,不同组件对应不同的端口号,如:HDFS(50070),Yarn(8088),Hbase(16010)等等, ...

  6. 牛客练习赛44 B题 (思维)

    链接:https://ac.nowcoder.com/acm/contest/634/B 来源:牛客网 给出n条线段,第i条线段的长度为ai, 每次可以从第i条线段的j位置跳到第i + 1条线段的j+ ...

  7. Luogu T24242 购物券Ⅰ(数据已加强)

    这是一道比赛时的题目,但由于我没报名,所以浪费了一个大好的切水题的机会. 是经典的meet in middle(折半搜索)的模板题,但是之前一直没找到这种题目,今天终于看到了. 由于m的范围极大,因此 ...

  8. Python的进制等转换

    To 十进制 二进制: >>> int('110', 2) -> 6 八进制: >>> int('10', 8) -> 8 十六进制: >> ...

  9. mfc 线程的优先级

    知识点:  线程优先级  获取当前线程句柄  线程优先级设置  线程优先级变动  线程优先级获取 一.线程优先级(Thread priority ) 简单的说就是(线程)的优先级越高,那么就 ...

  10. PHP调用WCF提供的方法

    一.准备工作 1.安装wampserver:过程略 2.配置wampserver: 2.1打开php.ini文件,去掉 ;extension=php_soap.dll 这里那个分号. 也有说把这个 ; ...