任务

求解第 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. java求解第N个素数(质数)

    面试中,遇到一个题目:求解第N个素数. import java.util.Scanner; public class GetPrimeNumber { public static int NthPri ...

  2. [软工课程博客] 求解第N个素数

    任务 求解第 10,0000.100,0000.1000,0000 ... 个素数(要求精确解). 想法 Sieve of Eratosthenes 学习初等数论的时候曾经学过埃拉托斯特尼筛法(Sie ...

  3. poj 2992 Divisors (素数打表+阶乘因子求解)

    Divisors Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 9617   Accepted: 2821 Descript ...

  4. 『素数 Prime判定和线性欧拉筛法 The sieve of Euler』

    素数(Prime)及判定 定义 素数又称质数,一个大于1的自然数,除了1和它自身外,不能整除其他自然数的数叫做质数,否则称为合数. 1既不是素数也不是合数. 判定 如何判定一个数是否是素数呢?显然,我 ...

  5. Python练习题 035:Project Euler 007:第10001个素数

    本题来自 Project Euler 第7题:https://projecteuler.net/problem=7 # Project Euler: Problem 7: 10001st prime ...

  6. 数论 Day 13

    数论_CRT(中国剩余定理)& Lucas (卢卡斯定理) 前言 又是一脸懵逼的一天. 正文 按照道理来说,我们应该先做一个介绍. 中国剩余定理 中国剩余定理,Chinese Remainde ...

  7. Algorithm: CRT、EX-CRT & Lucas、Ex-Lucas

    中国剩余定理 中国剩余定理,Chinese Remainder Theorem,又称孙子定理,给出了一元线性同余方程组的有解判定条件,并用构造法给出了通解的具体形式. \[ \begin{aligne ...

  8. CRT中国剩余定理 & Lucas卢卡斯定理

    数论_CRT(中国剩余定理)& Lucas (卢卡斯定理) 前言 又是一脸懵逼的一天. 正文 按照道理来说,我们应该先做一个介绍. 中国剩余定理 中国剩余定理,Chinese Remainde ...

  9. 求解100以内的所有素数(问题来自PythonTip)

    求解100以内的所有素数 (AC/Submit)Ratio(4615|22542)20.47% 描述: 输出100以内的所有素数,素数之间以一个空格区分(注意,最后一个数字之后不能有空格). a=[2 ...

随机推荐

  1. 微软Azure 经典模式下创建内部负载均衡(ILB)

    微软Azure 经典模式下创建内部负载均衡(ILB) 使用之前一定要注意自己的Azure的模式,老版的为cloud service模式,新版为ARM模式(资源组模式) 本文适用于cloud servi ...

  2. Python标准模块--Iterators和Generators

    1 模块简介 当你开始使用Python编程时,你或许已经使用了iterators(迭代器)和generators(生成器),你当时可能并没有意识到.在本篇博文中,我们将会学习迭代器和生成器是什么.当然 ...

  3. 编写高质量代码:改善Java程序的151个建议(第7章:泛型和反射___建议106~109)

    建议106:动态代理可以使代理模式更加灵活 Java的反射框架提供了动态代理(Dynamic Proxy)机制,允许在运行期对目标类生成代理,避免重复开发.我们知道一个静态代理是通过主题角色(Prox ...

  4. TYPESDK手游聚合SDK服务端设计思路与架构之一:应用场景分析

    TYPESDK 服务端设计思路与架构之一:应用场景分析 作为一个渠道SDK统一接入框架,TYPESDK从一开始,所面对的需求场景就是多款游戏,通过一个统一的SDK服务端,能够同时接入几十个甚至几百个各 ...

  5. CSS中强悍的相对单位之em(em-and-elastic-layouts)学习小记

    使用相对单位em注意点 1.浏览器默认字体是16px,即1em = 16px,根元素设置如下 html{ font-size: 100%; /* WinIE text resize correctio ...

  6. MySQL加密

    MySQL字段加密和解密 1.加密:aes_encrypt('admin','key') 解密:aes_decrypt(password,'key') 2.双向加密 通过密钥去加密,解密的时候的只有知 ...

  7. mysql源码包手动安装、配置以及测试(亲测可行)

    笔记编者:小波/qq463431476博客首页:http://www.cnblogs.com/xiaobo-Linux/ 记下这篇mysql笔记,望日后有用! redhat6采用centos yum源 ...

  8. 挑子学习笔记:两步聚类算法(TwoStep Cluster Algorithm)——改进的BIRCH算法

    转载请标明出处:http://www.cnblogs.com/tiaozistudy/p/twostep_cluster_algorithm.html 两步聚类算法是在SPSS Modeler中使用的 ...

  9. linux下mono播放PCM音频

         测试环境: Ubuntu 14 MonoDevelop CodeBlocks 1.建立一个共享库(shared library) 这里用到了linux下的音频播放库,alsa-lib. al ...

  10. Lesson 23 A new house

    Text I had a letter from my sister yesterday. She lives in Nigeria. In her letter, she said that she ...