素数一小坨


1. 素数的个数无限多(不存在最大的素数)
  证明:反证法,假设存在最大的素数P,那么我们可以构造一个新的数2 * 3 * 5 * 7 * ... * P + 1(所有的素数乘起来加1)。
显然这个数不能被任一素数整除(所有素数除它都余1),这说明我们找到了一个更大的素数。

2. 存在任意长的一段连续数,其中的所有数都是合数(相邻素数之间的间隔任意大)
  证明:当0<a<=n时,n!+a能被a整除。长度为n-1的数列n!+2, n!+3, n!+4, ..., n!+n中,所有的数都是合数。这个结论对所有
大于1的整数n都成立,而n可以取到任意大。

3. 所有大于2的素数都可以唯一地表示成两个平方数之差。
  证明:大于2的素数都是奇数。假设这个数是2n+1。由于(n+1)^2=n^2+2n+1,(n+1)^2和n^2就是我们要找的两个平方数。
下面证明这个方案是唯一的。如果素数p能表示成a^2-b^2,则p=a^2-b^2=(a+b)(a-b)。由于p是素数,那么只可能a+b=p且
a-b=1,这给出了a和b的唯一解。

4. 当n为大于2的整数时,2^n+1和2^n-1两个数中,如果其中一个数是素数,那么另一个数一定是合数。
  证明:2^n不能被3整除。如果它被3除余1,那么2^n-1就能被3整除;如果被3除余2,那么2^n+1就能被3整除。总之,
2^n+1和2^n-1中至少有一个是合数。










素数基础篇 之 素数的判断 - czyuan原创




素数:只有两个正因数(1和自己)的自然数。素数,作为数论中最基础的理论之一,又是许多著名定理的根源。

几个可以无视的性质:
1. 1不是素数
2. 除了2以为所有偶数都是合数。
提高素数,第一反应都会是如果判断素数,找到我们想要的素数。这里列举几种最常用的判断素数的方法。

1. 朴素判别素数
  简述:即判断一个数N是不是素数,只需要判断从2到N^0.5的数是否能整除N,如果能则不是素数,否则就是素数。
  代码:(无视之...)
  评价:从定义出发,简单易懂,适合初初初学者,但要理解上限为啥是N^0.5。

2. 朴素筛法
    简述:筛法的原理这里就不赘述了,思想就是去掉当前的素数的倍数(因为他们一定是合数)。
    代码:
const int maxn = 10000000;
bool IsNotPrime[maxn]; // 判断是否为素数.

void PrimeNormal(void)
{ // 朴素的筛法. 10000000内0.7s出解.
     int i, j;
     memset(IsNotPrime, 0, sizeof(IsNotPrime)); // 初始赋值所有数都是素数.
     IsNotPrime[1] = 1; IsNotPrime[0] = 1; // 0和1不是素数.
     for (i = 2; i < maxn; i++)
     {
         if (!IsNotPrime[i])
         { // 注意:这里只用素数来筛,因为合数筛时肯定被素数筛过了,所以没必要.
            if (1LL * i * i > 1LL * maxn) continue;
            for(j = i * i; j < maxn; j += i) IsNotPrime[j] = 1;
          }
     }
}

     评价:相对朴素的判断素数,速度上有了质的飞跃,但是仍有些素数的性质没有用上,导致速度不够理想。
     优化:我们可以在朴素筛法上做些显而易见的优化:用到前面提到的可以无视的性质,我们可以把除了2以外的偶数提前判断,这样只要循环奇数即可。


3. 线性筛法
    简述:朴素筛法虽然是筛素数的倍数,但是所有倍数都要筛过去,这是完全没必要的。
    我们可以利用,每个合数必有一个最小质因数,每个合数仅被它的最小质因数筛去正好一次这个性质来优化筛法.
    代码:
const int maxn = 10000000;
bool IsNotPrime[maxn]; // 判断是否为素数.
int PrimeList[maxn]; // 素数列表.
int PrimeNum;

void Prime_Linear(void)
{ // 速度比朴素筛法快2倍以上,该筛法进行稍微修改即可用于求欧拉函数Phi[].
     int i, j;
     memset(IsNotPrime, 0, sizeof(IsNotPrime)); // 初始赋值所有数都是素数.
     IsNotPrime[1] = 1; IsNotPrime[0] = 1; // 0和1不是素数.
     for (i = 4; i < maxn; i += 2) IsNotPrime[i] = 1; // 除2以外的所有偶数都不是素数.
     PrimeNum = 0;
     for (i = 3; i < maxn; i += 2)
     {
         if (!IsNotPrime[i])
          { // 如果是素数则加入素数列表.
             PrimeList[PrimeNum++] = i;
         }
            // 注意:这里与朴素筛法不同,即使合数也要进行筛.
           // 因为这里素数只筛它的素数倍,那么有些合数就可能没被筛掉.
           // 而这些合数就需要合数来晒,而且只要筛到它的最小质因子倍即可(想想为什么?).
       for (j = 0; j < PrimeNum && i * PrimeList[j] < maxn; j++)
       {
              IsNotPrime[i * PrimeList[j]] = 1;
             if (i % PrimeList[j] == 0)
              { // 说明PrimeList[j]是i的最小质因子,即i * PrimeList[j]的最小质因子,则跳出.
                  break;
              }
        }
     }
}
    评价:该筛法利用了每个合数必有一个最小质因数,素数只晒素数倍,合数筛到最小质因子倍,复杂度是线性的.

4. 区间筛素数
    简述:有的时候,我们需要知道某个特定区间的素数(区间大小较小,但数可能很大)。
    那么数组就开不下,这时候我们仍然可以使用筛法,只是所有的下标都进行了偏移。
    大家理解下面这段代码可以先用普通筛法写,然后数组下标集体移动即可。

const int maxn = 100000;

int PrimeList[maxn];
int PrimeNum;
bool IsNotPrime[maxn]; // IsNotPrime[i] = 1表示i + L这个数是素数.

void SegmentPrime(int L, int U)
{ // 求区间[L, U]中的素数.
     int i, j;
     int SU = sqrt(1.0 * U);
     int d = U - L + 1;
     for (i = 0; i < d; i++) IsNotPrime[i] = 0; // 一开始全是素数.
     for (i = (L % 2 != 0); i < d; i += 2) IsNotPrime[i] = 1; // 把偶数的直接去掉.
     for (i = 3; i <= SU; i += 2)
     {
           if (i > L && IsNotPrime[i - L]) continue; // IsNotPrime[i - L] == 1说明i不是素数.
           j = (L / i) * i; // j为i的倍数,且最接近L的数.
           if (j < L) j += i;
           if (j == i) j += i; // i为素数,j = i说明j也是素数,所以直接 + i.
          j = j - L;
          for (; j < d; j += i) IsNotPrime[j] = 1; // 说明j不是素数(IsNotPrime[j - L] = 1).
     }
     if (L <= 1) IsNotPrime[1 - L] = 1;
     if (L <= 2) IsNotPrime[2 - L] = 0;
     PrimeNum = 0;
     for (i = 0; i < d; i++) if (!IsNotPrime[i]) PrimeList[PrimeNum++] = i + L;
}

5. Miller_Rabin
简述:很数论的一种方法,需要费马小定理,还要a ^ b的快速幂,还要注意Carmicheal number.
有兴趣可以专门找相关文章看,没兴趣的直接用就哦啦~~
int Modular_Exponent(int a, int b, int MOD)
{ // a ^ b mod MOD.
     int temp(1);
     int aa(a);
     while (b)
     {
          if (b & 1) temp = 1LL * temp * aa % MOD;
          aa = 1LL * aa * aa % MOD;
           b >>= 1;
     }
     return temp;
}

// Carmicheal number: 561,41041,825265,321197185
bool Miller_Rabin(int n, int time = 20)
{ // 如果是素数,则返回1,否则返回0.
   if (n == 1 || (n != 2 && !(n % 2)) || (n != 3 && !(n % 3))
   || (n != 5 && !(n % 5)) || (n != 7 && !(n % 7)))
    return 0;
   while (time--)
     {
           if (Modular_Exponent(((rand() & 0x7fff << 16) +
          rand() & 0x7fff + rand() & 0x7fff) % (n-1) + 1, n - 1, n) != 1)
     return 0;
     }
   return 1;
}

评价:解决了筛法需要连续性判素数的确定,可以在很高概率(多次判断,实际效果很好)判断出素数.


6. 筛法的额外用途:求每个数的最小质因数
简述:原理与筛法相通,只是factor存的是最小质因数,为0当然就是素数啦.
void PrimeFactor(void)
{ // 求每个数最小的质因数.
     int i, j;
     mem(factor, 0);
     factor[1] = 1; factor[0] = 1; // 0和1不是素数.
     PrimeNum = 0;
     for (i = 2; i < maxn; i++)
     {
           if (!factor[i]) PrimeList[PrimeNum++] = i;
          for (j = 0; j < PrimeNum && i * PrimeList[j] < maxn
          && (PrimeList[j] <= factor[i] || factor[i] == 0); j++)
          {     // 当PrimeList[j] > factor[i] && factor[i] != 0时,那么最小质因数为factor[i].
               // PrimeList数组中的素数是递增的, 当i % PrimeList[j] == 0时,就break,理由同上面的线性筛法.
             factor[i * PrimeList[j]] = PrimeList[j];
             if (i % PrimeList[j] == 0) break;
          }
     }
//    cout << PrimeNum << endl;
}
评价:该方法使用十分巧妙,可以快速地一个数的质因数个数,强烈推荐~~

7. java大数素数判断
简述:此方法极度猥琐...话说导致09年合肥站regional的一堆杯具...
函数:IsProbablePrime(int certainty)
评价:可以用来判断大数是否为素数。


以上总结了素数的一些常用的判断方法及用途,素数的判断,特别是大素数的判断一直是数论未能突破的地方。
本文仅是抛砖引玉,供大家讨论素数的相关理论~~


   了解了基本的判断方法后,你是不是有个疑问:“我们能判断素数的个数吗?”总所周知,素数的个数是无限的,且没有固定的公式…但如果我们只要判断[a, b]区间(a, b范围为1到1亿)内的素数的个数呢?

     首先,我们可以想到,如果要求的素数个数区间[a, b],当区间长度比较小(10^6内),我们可以用筛法求出区间内的所有的素数,然后统计个数即可。
但如果区间长度很长或者要求询问的次数很多,那该怎么办呢? [a,b]区间内素数的个数 = [1, b]的个数 - [1, a - 1]的个数,所以我们这里只讨论求[1, a]区间内的素数。以下提供个人的两种方法,时限都是1s内产生结果。如果哪位大牛有更好的方法,大家一起交流下~~

     1. 我们可以扩展上面的思想,当区间小的时候,我们可以很好的求出素数的个数。那我们可以把大的区间划分成一块块小的区间,比如把一个长度为1亿的区间划分1,000个长度为100,000的区间。我们可以利用Miller-Rabin事先把[1, 100000], [100001, 200000], [200001, 300000]的区间内的素数个数统计好,然后存在一个数组中。
完成这步后,思路就比较清晰:对于区间[1, a],可以拆分为一个个长度为100000的小区间([1, 100000], [100001, 200000]…),加上尾部的小区间[c * 100000, a]。前面的小区间只要数组的值相加即可,而后面的小区间[c * 100000, a],长度在100000内,直接用区间的筛法求出素数,统计个数即可。
     代码:参见上一篇文章的Miller-Rabin,区间求素数的代码。
     该方法速度很快,主要时间都花在数组打表上,然后直接存在数组里,求1到1亿的素数个数时间为0.06s。
     评价:优点是方法速度快,且直接套模板即可。缺点是需要事先打表,且代码长度很长(因为要给长度为1000的数组赋初值)。

     2. 第二种方法涉及到容斥原理(inclusion-exclusion principle),容斥原理参见(http://en.wikipedia.org/wiki/Inclusion-exclusion_principle)。
      当一个数是合数,那么它可以分解成几个素数的乘积。如30 = 2 * 3 * 5。我们可以统计合数的个数,然后拿总数减它就是素数的个数(注意还要去掉1的)。我们可以利用类似筛法的原理,去除2的倍数(它们肯定是合数,不包括2),然后去除3的倍数,5的倍数,知道去除到Sqrt(a)的倍数为止。但你会发现6 = 2 * 3,被去除了2次,而这正是容斥原理解决的问题。合数的个数 = 1个素数筛完的合数个数 – 2个素数筛完的合数个数 + 3个素数筛完的合数个数...
而容斥原理的累加过程,即可用DFS来解决。你可能会认为sqrt(1亿) = 10000,其中素数有很多,DFS要跑很长时间。但我们只需要加一些简单的优化即可很大程度地提高程序的效率。

    首先,我们写筛法出1到sqrt(a)的素数表,然后从小到大DFS。
    如果当前的乘积 > a,那么直接退到上一层。
    如果该层的所有乘积不能使总数发生变化(即所有乘积都 > a),那么直接退回第一层。(因为是从小到大,该层下面的乘积必将 > a)
    如果是第一层的所有乘积不能使总数发生变化,那么程序运行结束。(原理同上)
    经过这样优化后,求1到1亿的时间为0.4秒,1到10亿的时间为3.5s。

     核心代码:
     void Solve(int index, int lcm, int K)
     {
         int i;
         int t, t_temp;
         if (K == 0)
         {
              temp += n / lcm;
               return ;
         }
         for (i = index; i < total - K + 1; i++)
         {
               t = lcm * primelist[i];
               t_temp = temp;
               if (t <= n)
               {
                  Solve(i + 1, t, K - 1);
               }
               if (t_temp == temp) return ; // 剪枝:同样道理,说明以后的K - 1个不能组成我们想要的值
          }
    }

    main()中:
    for (k = 1; k <= total; k++)
    { // 计算size中选k个的总数.
          temp = 0;
          Solve(0, 1, k);
          if (temp == 0) break; // 说明最小的k个乘积都大于n了,那么可以直接break了.
          if (k & 1) ans += temp;
          else ans -= temp;
    }

    评价:该方法巧妙地使用了容斥原理来计数,且DFS应用于容斥原理的剪枝十分重要。
     
    提到容斥原理,推荐一道前不久做的题目SRM 453.5 DIV 1 1000(http://www.topcoder.com/stat?c=problem_statement&pm=10420&rd=14174),两题的容斥原理思想差不多,但剪枝方法不同,而且两题的方法交换都会产生超时...(有兴趣一起交流下~~)


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值