数论基础

数论(1)

初等数论主要包括整数理论、同于理论,连分数理论;从本质上说,他的研究方法就是利用整数环的整除理论。

三个方面:

  • 素数运算
  • 求解不定方程和同余方程
  • 积性函数的应用

1 素数运算

素数又称为质数,指的是在大于1的自然数中除了1和它本身外无法被其他自然数整除的数。简而言之,只有两个因数(1和它本身)的自然数即为素数。比1大但是不是素数的数称为合数;**1和0既非素数也非合数。**合数是由若干个素数相乘得来的,所以素数是合数的基础,没有素数就没有合数。整数的基本单元是素数,数论作为研究整数性质的一门理论,其本质就是对素数性质的研究

1.1 筛法生成素数

1.1.1 艾拉托尼特斯尼筛法

设筛子为u[],初始时区间内所有的数都在筛子中。按照递增顺序搜索筛中的最小数,将其倍数从筛中筛去,最终筛中留下的数即为素数

int i,j,k;
for(i=2;i<=n;i++){//或者一句话代替		memset(u,true,sizeof(u));
    u[i]=true;
}
for(i=2;i<=n;i++){//顺序搜索筛中的最小数
    if(u[i]){
        for(j=2;j*i<=n;j++){
            u[i*j]=false;
        }
    }
}
for(i=2;i<=n;i++){//将筛中的所有素数放入su[]中
    if(u[i])su[++sum]=i;
}

算法中合数是作为素数的倍数被筛去的,时间复杂度为O(n*long log n)。如果能保证每一个合数仅被最小的素数筛去,算法效率将大幅度提高,下面就是算法的优化

1.1.2 欧拉筛法
int i,j,num=1;
memset(u,true,sizeof(u));//u[] is initialed by the element "true"
for(i=2;i<+n;i++){//Sequential analysis of each number in an integer interval
    if(u[i])su[num++]=i;//Feed the minimum number in the sieve into the prime table
    for(j=1;j<num;j++){//Search the prime table
        if(i*su[j]>n)break;//Out of range,the next number is analyzed
        u[i*su[j]]=false;//Sift the prime product terms out of the sieve
        if(i%su[j]==0)break;//If the current prime number is the minimum prime factor of i, the next number is analyzed
    }
}

素数筛选法是数论运算的核心子程序。

1.1.3 Goldbach’s Conjecture

猜想: 每个大于4的偶数都可以写成是两个奇素数的和。

例如: 8=3+5;20=3+17=7+13;42=5+37=11+31=19+23;

验证10000以内的哥德巴赫猜想

输入

  • 多行,偶整数,大于4
  • 输入0结束

输出

  • 形式为n=a+b
  • 对于有多对奇素数的和为n,输出差绝对值最大的那个
  • 如果没有这样的数对,则输出"Glodbach’s conjecture is wrong."

在线测试地址:ZOJ 1951

#include <iostream>
#include <cstdlib> //包含system的库函数
#include <cstring> //包含memset的库函数

using namespace std;

bool u[1111111]; //筛子
int su[1111111]; //素数表
int num, n;      //素数表重元素的个数和输入的数字

void prepare() //筛选[2..n]素数
{
    int i, j;
    memset(u, true, sizeof(u));    //筛子初始化
    for (i = 2; i <= 1000000; i++) //递增分析筛子中的每一个数
    {
        if (u[i])
            su[++num] = i;         //将最小数赋值到素数表
        for (j = 1; j <= num; j++) //搜索素数表
        {
            if (i * su[j] > 1000000) //超出范围则分析下一个数
                break;
            u[i * su[j]] = false; //将i和su[j]乘积产生的合数筛去
            if (i % su[j] == 0)   //su[j]如果是i的最小素因子,分析下一个数
                break;
        }
    }
}
int main()
{
    int i;
    prepare(); //得出素数表
    while (cin >> n && n)
    {
        bool ok = false;
        for (i = 2; i <= num; i++)
        {
            if (su[i] * 2 > n) //搜索完所有素数和的形式,尽管不知道为什么,但是经过验证是对的
                break;
            if (u[n - su[i]])
            {
                ok = true;
                break;
            }
        }
        if (ok)
            cout << n << " = " << su[i] << " + " << n - su[i] << endl;
        else
            cout << "Goldbach's conjecture is wrong." << endl;
    }
    system("pause");
    return 0;
}

1.1.4 Summation of Four primes

问题: Euler证明的经典理论之一就是素数在数量上是无限的。每一个整数能否用四个素数的和来描述

输入:

  • 一个正整数n,n<=1000000,请将这一个整数用四个素数的和表示
  • 多行,EOF结束

输出:

  • 对每一个输入的整数,输出一行允许存在重复的一行4个素数。
  • 如果存在多解,输出其中一个就可

算法核心:

  • 最小素数为2,故而所能用四个数表示最小和为2+2+2+2=8;
  • 分情况讨论:
    • 当n<8时,无法表示,输出“Impossible.”
    • 当n>=8时
      • 若n是偶数,则输出“2 2 ”,然后对n-4处理
      • 若n是奇数,则输出“2 3 ”,然后对n-5处理
      • (这里处理的好处就是处理后n的值是大于等于4的偶数)
      • 对处理后得到的n进行哥德巴赫猜想验证

在线测试地址:UVA 10168

代码:

#include <iostream>
#include <cstdlib>
#include <cstring>
const int ma = 9999999; //定义数据范围

using namespace std;

bool u[ma]; //筛子
int su[ma]; //素数表
int num;    //素数表长度

void prepare() //筛选函数得出素数表
{
    int i, j;
    memset(u, true, sizeof(u)); //初始化筛子
    for (i = 2; i <= ma; i++)   //按照递增顺序搜寻筛子
    {
        if (u[i]) //将最小素数放入素数表
            su[++num] = i;
        for (j = 1; j <= num; j++)
        {
            if (i * su[j] > ma) //超出范围则分析下一个数
                break;
            u[i * su[j]] = false; //将最小质因子为i的合数筛除
            if (i % su[j] == 0)   //如果i是最小质因子,分析下一个数
                break;
        }
    }
}

int main()
{
    int i, n;
    prepare();
    while (cin >> n)
    {
        if (n < 8) //根据题目要求,判断出来8是最小的素数(2)的累加和,故而小于8的数都不能用四个素数的和表示
        {
            cout << "Impossible." << endl;
            continue;
        }
        //当n>=8时,将其转化为大于4的偶数,故而使用哥德巴赫猜想去求解
        if (n % 2 == 0)
        {
            cout << "2 2 ";
            n -= 4;
        }
        else
        {
            cout << "2 3 ";
            n -= 5;
        }
        for (i = 1; i <= num; i++) //哥德巴赫猜想验证部分
        {
            if (su[i] * 2 > n)
                break;
            if (u[n - su[i]])
            {
                cout << su[i] << " " << n - su[i] << endl;
                break;
            }
        }
    }
    system("pause");
    return 0;
}

1.1.5 Digit Primes

问题描述:

  • 位素数是一种特殊的素数,他满足本身是素数并且各位相加之后依然是素数

输入:

  • 输入n,(0<n<=500000)
  • 下面的n行,每行依次输入区间左界和右界t1,t2,(0<t1<=t2<1000000)

输出:

  • 输出位于区间的位素数的个数
  • 输入输出要使用scanfprintf,使用cincout太慢,会导致超时

核心算法:

  • 筛选出位于范围的素数,并在筛子中对应下标用true表示

  • 对素数表进行判断,按照递增顺序判断每一个素数是不是位素数

    • 如果是,则在u2[]中相应的下标置一
    • 否则继续判断下一个数
  • 再对u2[]进行处理,现在位素数已经标识出来,如果要确定某一个区间内位素数的个数

    • 第一个方法是遍历区间,计数其中一的个数
    • 第二个方法是按顺序依次累加u2[],即u2[i]+=u2[i-1](其余位都是0)
      • 这样的话,u2[i]表示的就是[2,i]之间位素数的个数
      • 那么[i,j]之间位素数的个数就可以表示为u2[j]-u2[i-1](满足i<j)

在线测试地址:UVA 10533

代码部分:

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>

const int ma = 1100001; //范围

using namespace std;

bool u[ma]; //筛子
int u2[ma]; //位素数表

void prepare() //筛选素数函数
{
    int i, j;
    memset(u, true, sizeof(u)); //初始化筛子
    for (i = 2; i < ma; i++)
    {
        if (u[i]) //取出最小素数
        {
            for (j = i + i; j < ma; j += i) //将其倍数去除
            {
                u[j] = false;
            }
        }
    }
}

bool ok(int x) //判断一个数位之和是不是素数
{
    int temp = 0;
    while (x)
    {
        temp += x % 10; //取余数得各位和
        x /= 10;        //取最低位后算数右移即降权操作
    }
    return u[temp]; //判断次数在不在筛子中
}

int main()
{
    int i, j, k;
    int n;
    memset(u2, 0, sizeof(u2));
    prepare();
    for (i = 2; i < ma; i++)
    {
        if (u[i] && ok(i)) //判断是不是位素数
            u2[i]++;
        u2[i] += u2[i - 1]; //u2[i]即为[2,i]所有位素数的个数
    }
    scanf("%d", &n);
    while (n--)
    {
        scanf("%d %d", &j, &k);
        printf("%d\n", (u2[k] - u2[j - 1]));
    }
    system("pause");
    return 0;
}

1.1.6 Primes Gap

问题描述:

两个连续的素数pp+n之间,有n-1个连续合数(不是素数也不是1的整数),这个间隔被称为长度为n的素数间隔。现在给定一个素数,请判断其位于的素数间隔的长度。

输入:

  • 多行输入,每一行输入一个整数
  • 输入0结束

输出:

  • 多行输出,每一行输出对应的素数间隔的长度

核心算法:

  • 筛选出所有的素数
  • 使用双指针法
    • 起始状态下,低指针low和高指针high都指向n
    • 如果n不是素数,low向左遍历,直到找到第一个出现的素数
    • 如果n不是素数,high向右遍历,直到找到第一个出现的素数
    • j-i就是素数间隔的长度

在线测试地址:POJ 3518

代码:

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cstdio>

const int ma = 1300000; //范围

using namespace std;

bool u[ma]; //筛子
int su[ma]; //素数表
int num;    //素数表长度

void prepare() //筛选函数
{
    int i, j;
    memset(u, true, sizeof(u)); //初始化素数表
    for (i = 2; i < ma; i++)    //按照递增顺序遍历筛子
    {
        su[++num] = i;             //将最小数放入素数表
        for (j = 1; j <= num; j++) //按顺序遍历素数表,将其倍数去掉
        {
            if (i * su[j] > ma) //超出范围则分析下一个数
                break;
            u[i * su[j]] = false; //将倍数从筛中去除
            if (i % su[j] == 0)   //当i是最小素数时,分析下一个数
                break;
        }
    }
}

int main()
{
    int low, high; //双指针,确定左界和右界
    int n;         //输入的数
    prepare();
    while (scanf("%d", &n) > 0 && n)
    {
        low = n;
        high = n;
        while (!u[low]) //从n开始,向左遍历筛子
            low--;
        while (!u[high]) //从又开始,向右遍历筛子
            high++;
        cout << high - low << endl;
    }
    system("pause");
    return 0;
}

1.2 测试大素数的实验范例

解决测试素数的问题最简便的方法就是使用2...n去除n,但是对于将大的n而言,容易出现超时的现象,下面就根据此提出几种优化

  • 2...⌊√n⌋试除n

    • 理论依据:如果n是一个合数,那么他的两个约数p1p2必然满足以下条件:

      • p1<p2
      • p1<⌊√n⌋&&p2>⌊√n⌋
    • 故而可以缩小试除范围到2...⌊√n⌋

    • 代码

    • bool isPrime(int x){
          if(x<=3){
              return x>1;
          }
          int sqr=(int)sqrt(x);
          for(int i=2;i<=sqr;i++){
              if(x%i==0)return false;
          }
          return true;
      }
      
  • 素数必然是6x-1或者6x+1(反过来不一定哦)

    • 其实这个很容易判断:6x6x+26x+36x+4都是合数,只剩下6x+16x+5无法判断是不是素数,显然6x+5=6x-1,故而很容易想到循环步长为6,在其左右两侧进行判断是不是素数

    • 一般在数据很大时,这个方法的用处才能充分体现出来

    • program

    • bool isPrime(int n){
          if(n<=3){  //小于等于3的数作特殊处理
              return n>1;
          }
          if(n%6!=1||n%6!=5)return false; //不在6两侧的数一定不是素数
          int sqr=(int)sqrt(n);
          for(int i=5;i<=sqr;i+=6){  //循环步长为6
              if(n%i==0||n%(i+2)==0)return false;
          }
          return true;
      }
      
  • 筛选法+试除法

    • 这个其实也很容易想明白:对于x上限较大的情况而言,先离线计算出2...|√x|的筛子u[n]和素数表su[num]

      • 当x处于范围内时,若u[x]==true,则x是素数
      • 若x大于范围上界,对su[i](i:su.size())进行除运算,如果不能被整除,则x是素数
    • 时间复杂度是O(|√n|)

    • program

    • const int ma = 10010; //定义的最大范围
      
      bool u[ma];
      int su[ma];
      int num;
      
      void prepare()
      { //离线素数表
          int i, j;
          memset(u, true, sizeof(u));
          for (i = 2; i <= ma; i++)
          {
              if (u[i])
                  su[++num] = i;
              for (j = 1; j <= num; j++)
              {
                  if (i * su[j] > ma)
                      break;
                  u[i * su[j]] = false;
                  if (i % su[j] == 0)
                      break;
              }
          }
      }
      
      bool isPrime(int x)
      { //判断
          if (x < ma)
              return u[x];
          for (int i = 1; i <= num; i++)
          {
              if (x % su[i] == 0)
                  return false;
          }
          return true;
      }
      
  • Miller_Rabin方法

    • (事实证明,我有点征服不了)

    • 这个方法理论依据是费马小定理

      • 如果n是素数,且与a互质,则a<sup>n-1</sup>≡1(mod n) (1<=a<=n)
      • 如果出现足够多的不同的a能使上述同余式成立,则可以说n是素数,也可能是伪素数
        • 所谓的伪素数是指,虽然选取的所有的a都能让等式成立,但事实上n却是一个合数
      • 如果出现了任意一个a使得同余式不成立,即a<sup>n-1</sup>!≡1(mod n),则判定n是合数
    • 基于上述理论,产生了Miller_Rabin方法

      • 重复k次计算,每次都在[1,n-1]范围内选取一个a,若a<sup>n-1</sup> mod n ≠1,则返回n是素数或者伪素数的信息
        • k为选取a的次数
    • 事实上经过反复检验,对于32位内的人一个整数n,如果其通过了以2、7、61为底的Miller_Rabin测试,那么a一定是素数;反之一定不是

    • 代码:

    • 快速幂取模理论依据

      • ab mod n = [(a mod n)×(b mod n)] mod n
        • 令a mod n = k1,b mod n = k2;则 a = n×c + k1,b = n×d+k2;
        • ab mod n = [(n×c+k1)*(n×d+k2)] mod n
          • =[cdn2+(ck1+dk2)×n+k1×k2] mod n
          • =(k1×k2) mod n
          • =[(a mod n)×(b mod n)] mod n
      • ab mod n = [(a mod n)×(ab-1 mod n)] mod n
        • = [∏(a mod n)] mod n
        • = [(a mod n)b] mod n
      • 当b为偶数时
        • ab mod n = [(a2)b/2] mod n
      • 当n为奇数时
        • ab mod n = [(a2)b/2 × a] mod n
      • 迭代就好
    • int64 qpow(int64 a,int64 b,int64 m){ //通过快速幂取模计算a^b mod m
          int64 ans=1;
          while(b){
              if(b&1){ //奇偶判定,若最低位为1则为奇数,反之则为偶数
                  ans*=a;
                  ans%=m;
              }
              a*=a;
              a%=m;
              b>>=1; //右移一位,除二操作,并赋值
          }
          return ans;
      }
      
    • bool MillerRabinTest(int64 x,int64 n){ //取x为底,判定n是否可能为素数
          int64 y=n-1;
          while(!(y&1)) y>>=1; //略去n-1有右端连续的0,将其调整为d:111100->1111
          x=qpow(x,y,n); //x=x^y mod n;
          while(y<n-1&&x!=1&&x!=n-1){ //将x反复平方,直到出现n-1或1为止
              x=(x*x)%n;
              y<<=(int64)1;
          }
          return x==n-1||y&1==1; //若x为n-1或者y为奇数时,可能是素数,反之一定不是
      }
      
    • bool isPrime(int64 n){ //判断32位内的整数n是否为素数
          if(n==2||n==7||n=61) return true; //如果是2、7、61,n为素数
          if(n==1||(n&1)==0) return false; //若n为1或者n为非2偶数,则n为合数
          return MillerRabinTest(2,n)&&MillerRabinTest(7,n)&&MillerRabinTest(61,n);
          //若n通过以2、7、61位基准的MillerRabin测试,则n一定是素数;否则为合数
      }
      
1.2.1 Primed Subsequence

问题描述:

  • 给出一个长度为n的正整数序列,一个素序列是一个长度至少为2的连续的子序列,总和是大于或等于2的素数。

输入:

  • 第一行给定测试用例个数t
  • 以下t行,每行首先给定序列长度n(0<n<10001),然后给出n个小于10000的整数

输出:

  • 如果有最短素序列,则输出其长度和这个素序列,用空格隔开,按照以下格式:
    • Shortest primed subsequence is length 2: 5 6
  • 如果不存在素序列,则输出:
    • This sequence is anti-primed.

核心算法:

  • 判断一个数字是不是素数,主要在于较大的数字判断是不是素数,可能会出现超时现象
  • 为了防止过多数量的遍历输入的序列,采用动态规划的方法:
    • 输入长度为n的序列,递推序列中前i个整数的和s[i]
    • 动态规划计算最短素序列
    • 枚举长度i(2:n)
      • 枚举首指针j(1<=j<=n-i+1)
        • 如果s[i+j-1]-s[j-1]是素数
        • 输出第j...j+i-1个整数(s[j+k-1]-s[j+k-2],1<=k<=i)
        • 退出程序
    • 输出失败信息

在线测试地址:UVA 10871

代码:

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cmath>
//#include <ctime>

using namespace std;

bool isPrime(int n)
{
    if (n <= 3)
    { //小于等于3的数作特殊处理
        return n > 1;
    }
    if (n % 6 != 1 && n % 6 != 5)
        return false; //不在6两侧的数一定不是素数
    int sqr = (int)sqrt(n);
    for (int i = 5; i <= sqr; i += 6)
    { //循环步长为6
        if (n % i == 0 || n % (i + 2) == 0)
            return false;
    }
    return true;
}

int main()
{
    //clock_t start, end;
    //start = clock();
    int t, i, j, temp;
    //prepare();
    scanf("%d", &t);
    while (t--)
    {
        int n;
        scanf("%d", &n);
        //int *s = new int[n + 1];
        int s[10010];
        s[0] = 0;
        for (i = 1; i <= n; i++)
        {
            scanf("%d", &s[i]);
            s[i] += s[i - 1];
        }
        bool ok = false;
        for (i = 2; i <= n; i++)
        { //枚举长度
            for (j = 1; j + i - 1 <= n; j++)
            {
                temp = s[j + i - 1] - s[j - 1];
                if (isPrime(temp))
                {
                    ok = true;
                    printf("Shortest primed subquence is length %d:", i);
                    for (temp = 1; temp <= i; temp++)
                        printf(" %d", s[j + temp - 1] - s[j + temp - 2]);
                    puts("");
                    break;
                }
            }
            if (ok)
                break;
        }
        //delete[] s;
        if (!ok)
            puts("This sequencee is anti-primed.");
    }
    //end = clock();
    //double endtime = (double)(end - start) / CLOCKS_PER_SEC;
    //printf("%.f\n", endtime * 1000);
    system("pause");
    return 0;
}

/**
 * 根据测试,显然静态数组比动态数组要节省时间
 * 说实在的,是通过降低空间复杂度来优化时间复杂度的
*/

*滑窗法:

固定距离的窗子步长为1按照顺序滑动,这样相较于暴力法节省了很多时间

####################################################
以上总结皆来自网络以及笔者最近想法总结
装在请注明
####################################################

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值