标签:
数学是科学的女王,数论是数学的女王——高斯。
然后我再狗尾续个貂——素数是数论的女王。
谈及素数,可以牵扯出很多数学史上的美谈,例如前几天在知乎上看到关于“除去酒色,人类还怎么享受生活”的问题,在一个回答中就举个几个大科学家的例子,其中提到某个钟爱素数的数学家,选择再每月的素数天和妻子同居,一个月刚开始还好,但越到月末素数间隔变大,同居的日子也就变少。
在素数这块小地方,有很多著名的猜想,能证出来一些真的是非常非常的厉害,因为当今世界流传着这样一个传说,集齐七大世界数学难题,便可以召唤神龙,帮助你实现一个愿望。
扯远了,这里先介绍几个关于素数的猜想。
孪生素数猜想:存在无穷多的形如p,p+2的素数对。
1966年陈景润用复杂的筛法证明了存在无穷多对孪生素数,现在寻找孪生素数成为了一种趣味竞赛,目前最大的孪生素数对是33218925.2^169690 +1 , 33218925.2^169690 -1 。华人数学家张益唐在这方面有很杰出的成就。
哥德巴赫猜想:每个大于2的正偶数可以写成两个素数的和。
这个猜想是1742年哥德巴赫写给欧拉的信中给出,目前已经验证了所有小于4 * 10^14的偶数满足这一猜想。
n^2 + 1猜想:存在无穷多个形如n^2 + 1的素数,其中n是正整数。
人们不仅对素数的形式着迷,也热衷于探索素数的分布规律。这里我们引出素数定理——设π(x)为小于整数x的素数个数,那么随着x的增大,有π(x) = x/lnx。由于时间原因,这里暂且不探讨证明过程。
那么基于这个定理,我们来解决一个实际的问题。(Problem source : nufu 117)
针对这个问题如此大的数据量,我们显然无法直接运算,那么就要用到我们的素数定理里。但是这里需要注意的是,这里要求的是素数个数的位数,并且这里的n表示的是10^n,其含义是与定理中的x有区别的。
另外,求一个整数x的位数,有公式lg(x) + 1,那么lg(10^n/ln(10^n)) + 1即是本题目所求,在进行简单的化简、编程实现即可。
参考代码如下。
#include<iostream> #include<math.h> using namespace std; int main() { int n; double e = 2.71828; while(cin >> n) { double m = (n - log10(n)-log10(log(10))); cout << int(m) + 1 << endl; } }
除了素数的分布规律,对于判定一个数是否是素数也是一个最基础的问题。
对于素数的判定,我们最先想到的是根据其定义进行暴力穷举。即给定数字n,我们遍历一下[1,n]的整数,判断有可以整除的因子。
但是我们发现,n的因子是成对出现的,即n = i * j,当我们遍历到i的时候,其实是找到了一个因子对——i,j,那么这样我们遍历j的时候,再次访问了因子对——i,j,这就造成了时间上的浪费。从数轴上来看,这些成对的因子是“成对”的,而这个对称点显然是sqrt(n)。因此我们可以对上面判断素数的方法进行优化,遍历[1,sqrt(n)]。
但是还有更高效的算法——埃拉托色尼筛法。它不需要证明,很易懂。就是我们假定[1,N]上的所有整数是素数,然后从2开始往后遍历,如果当前的i是一个素数,那么进入循环,循环中把[1,N]中所有i的倍数筛选出来,标记为合数。对比前两种方法,这种方法显然高效了许多。
以上的方法处理数据的上限是依次递增的,但如果一个数据练第三种筛法都处理不了呢?那我们就灵活的结合第二种普通筛法和第三种高效筛法。假设给定了一个很大的整数n,我们遍历[1,sqrt(n)]间的整数,这里用到的是第二种普通筛法。但是我们只需要遍历这个区间的素数,因为如果是合数的话,它一定有一个更小的素因子,这在之前就一定会遍历到,因此造成了重复遍历引起了时间上的浪费。而如何找到这个区间上的素数,就靠搞笑的埃拉托色尼筛法了。
只要充分理解了筛法本身的原理,编程实现上就是简单的模拟,很容易实现。
那么我们来结合一个题目具体实现以下代码。(Problem source : nefu 109)
可以看到这就是我们提及到的处理数据非常大的题目,此时就需要两种筛法结合起来。
参考代码如下。
#include<stdio.h> #include<iostream> #include<cmath> #include<cstring> const int N = 50001; using namespace std; bool isprime[N]; int prime[N],nprime; void doprime() //埃拉托色尼筛选法 { long long i , j; nprime = 0; memset(isprime,true,sizeof(isprime)); isprime[1] = 0; for(i = 2;i < N;i++) { if(isprime[i]) { prime[++nprime] = i; for(j = i+i;j < N ;j += i) { prime[j] = false; } } } } bool isp(int n) //基本筛法 { int i , k = (int)sqrt(double(n)); for(i = 1;prime[i] <= k;i++) if(n%prime[i] == 0) return 0; return 1; } int main() { doprime(); long long n; while(cin >> n) { if(n == 1) { cout << "NO"<<endl; continue; } if(isp(n)) cout << "YES" << endl; else cout << "NO" << endl; } return 0; }
从素数的角度来分析整个数域,数学家发现了很多规律,通过归纳法我们很容易得知n可以表示成素数之积,即n = p1*p2*p3……pn,这里的pi是素数。下面我们给出算术基本定理。
定理:每个大于1的正整数n,都可以被唯一地写成素数的乘积:n = p1^a1 * p2^a2 * …… * pk^ak。基于这个式子,我们可以得到很多有用的性质,在文章的后面我们将依次得介绍。
性质1:n!的素因子分解中的素数p的幂为:[n/p] + [n/p^2] + [n/p^3]……。在这里由于时间原因和笔者的能力原因,我们暂且不提它的详尽证明。而是通过先通过实践来学会应用它。
那么让我们来看一道需要应用到这条性质的问题。(Problem source : nefu 118)
我们来看这个问题,显然我们直接求出n!的值而后%10计算的暴力方法是不合理的,我们要灵巧的利用我们刚才得知的性质。因为这条性质中和这道题目一样,都包含n!。我们从素数基本定理的角度来看n!这个数,它可以写成多个素数的乘积的形式,即n! = p1^a1 * p2^a2 * p3^a3…… * pm ^ am的形式。那么此时我们想要知道这个数末尾有多少个零,转化一下,就是通过将n!写成乘积的形式,然后找到10这个因子的幂次,而10又可以继续拆分成两个素数——2和5,即n! = 2^a * 5^b *……,min(a,b)就这这道题目的答案。我们很容易看到,n!这个数字,拆成素数相乘的形式,2的幂次肯定大于5的幂次,因此这里我们只要找到5的幂次,即可作为这道题的答案。
有了这层数理的分析,我们就可以应用到上面的性质了。而在编程实现上,也只需简单的设计循环节来求解即可。
参考代码如下。
#include<cstdlib> #include<iostream> using namespace std; int main() { int n,m,t,sum,five; cin>>n; while(n--) { cin>>m; t = m; five = 5; sum = 0; while(five <= t) { sum = sum + t/five; five = five * 5; } cout << sum << endl; } return 0; }
参考系:《数论及其应用》 陈宇
——<未完>
标签:
原文地址:http://www.cnblogs.com/rhythmic/p/5199117.html