数论(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)
输出:
- 输出位于区间的位素数的个数
- 输入输出要使用
scanf
和printf
,使用cin
,cout
太慢,会导致超时
核心算法:
-
筛选出位于范围的素数,并在筛子中对应下标用
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
问题描述:
两个连续的素数p
,p+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
是一个合数,那么他的两个约数p1
,p2
必然满足以下条件: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
(反过来不一定哦)-
其实这个很容易判断:
6x
,6x+2
,6x+3
,6x+4
都是合数,只剩下6x+1
和6x+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是素数
- 当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是合数
- 如果n是素数,且与a互质,则
-
基于上述理论,产生了Miller_Rabin方法
- 重复k次计算,每次都在[1,n-1]范围内选取一个a,若
a<sup>n-1</sup> mod n ≠1
,则返回n是素数或者伪素数的信息- k为选取a的次数
- 重复k次计算,每次都在[1,n-1]范围内选取一个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
- 迭代就好
- ab mod n = [(a mod n)×(b mod n)] 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
) - 退出程序
- 如果
- 枚举首指针j(1<=j<=n-i+1)
- 输出失败信息
- 输入长度为
在线测试地址: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按照顺序滑动,这样相较于暴力法节省了很多时间
####################################################
以上总结皆来自网络以及笔者最近想法总结
装在请注明
####################################################