质数
1. 试除法判定质数
1.1 算法描述
试除法判定质数的思路比较简单,即对于给定的数字num,遍历从2到num的所有数字,若存在i使得num%i==0,那么num就不是质数。
但事实上,合数的因子是成对出现的,例如12的约数3,4,所以我们没有必要遍历到num,遍历到sqrt(num)即可。
1.2 代码实现
需要注意的是,在对num遍历的时候有以下几种写法:
for(int i=2;i*i<num;i++)
for(int i=2;i<=sqrt(num);i++)
这两种写法都不合适。第一种写法存在一种情况为,在num非常大的时候。ii有可能会溢出,导致ii<num,有可能会产生错误的结果;第二种写法是调用sqrt(num)会花费比较多的时间,因而建议的写法是:
for(int i=2;i<=n/i;i++)
具体代码如下:
//试除法判定质数
#include<iostream>
using namespace std;
int n;
int main(){
cin>>n;
while(n--){
int num;
cin>>num;
if(num<2){
cout<<"No"<<endl;
continue;
}
else{
int flag=true;
for(int i=2;i<=num/i;i++){
if(num%i==0){
cout<<"No"<<endl;
flag=false;
break;
}
}
if(flag) cout<<"Yes"<<endl;
}
}
}
对于小于2的数直接判定为不是质数,输出No并且判断退出本次循环判定下一个数;当num大于2时,先默认是质数,将flag变量置于true,在遇到可以整除的i时,将flag置于false,并且退出本次对i的遍历。
1.3 算法时间复杂度
试除法判断质数的时间复杂度为O(sqrt(n))
2. 分解质因数
2.1 算法描述
对于任意一个数,都可以写成质数幂的乘积。分解质因数就是求出所有的底数和指数。
代码实现同样很简单,也是试除法,对于给定的数字num,遍历从2到sqrt(num)的所有数,若存在i使得num%i==0,找到了底数i,而后只需要求它的指数,num=num/i,并且用变量s来维护除以i的次数,即s为底数i的指数。
我们会发现,看起来代码将从2到sqrt(num)的所有数都试了一遍,但事实上合数并不会满足num%i=0的条件。举个例子,对于36,在遍历到6的时候,若此时还能够满足num%6=0,那最起码我还可以整除2,可以整除3,那在求质数2的时候就没有除干净。总之,满足条件的i都会是质数。
另外,我们还需要注意到一个性质,对于任意的一个数num,最多只会有一个大于sqrt(num)的质因子,因而我们在让i遍历到i<=num/i结束时,若此时num不为1,那么此时num的值就是大于sqrt(1)的最后一个质因子,且指数是1.
2.2代码实现
#include<iostream>
using namespace std;
int n;
int main(){
cin>>n;
while(n--){
int num;
cin>>num;
for(int i=2;i<=num/i;i++){
if(num%i==0){//如果可以被这个质数除尽
int s=0;//s记录次数
while(num%i==0){//除不尽了就退出循环
num=num/i;
s++;
}
cout<<i<<" "<<s<<endl;
}
}
if(num>1) cout<<num<<" "<<1<<endl;
cout<<endl;
}
}
2.3时间复杂度分析
试除法分解质因数的时间复杂度近似为O(n)。
3 筛质数
3.1算法描述
对于一个给定的数组num,筛质数就是要找出所有小于num的质数。基本步骤就是,遍历从2到num的所有数,若i是质数,首先将其加入存放质数的数组prime中,再将小于num且是i的倍数的数字剔除掉。
3.2 代码实现
这里我们用str[N]来记录某个数字是否被我剔除掉了。若str[i]=false,说明在2~i-1中所有的质数的若干倍并没有包含i,那么i一定是质数,可以将其加入数组prime[]中,并且用for循环将i的倍数都剔除掉。
#include<iostream>
using namespace std;
const int N=1000010;
bool str[N];//来存储是否被“剔除”
int prime[N];//存放质数
int cnt;//质数的个数
int main(){
int n;
cin>>n;
for(int i=2;i<=n;i++){
if(!str[i]){
prime[cnt++]=i;
}
for(int j=i+i;j<=n;j=j+i) str[j]=true;
}
cout<<cnt;
}
用该种算法时间复杂度:在将所有2的倍数删除时,所用的时间为n/2;将3所有的倍数删除时,所用的时间为n/3;因而对于for循环总共花费的时间为:T=n/2+n/3+n/4+……+n/n=n(1/2+1/3+1/4+……1)=nln(n),所以时间复杂度为O(nln(n))。
3.3算法优化
在上述实现过程中,对于小于n的所有数,都要执行一个比较耗时的操作:
for(int j=i+i;j<=n;j=j+i) str[j]=true;
但事实上,我们只要删除质数的倍数即可,对于一个合数p,也就是说在2~p-1,存在整数q可以整除,那这个时候在i=q的时候,p已经被剔除掉了,因而我们可以将这个语句放到if(!str[i])中,只剔除质数的倍数。
修改后的代码为:
#include<iostream>
using namespace std;
const int N=1000010;
bool str[N];//来存储是否被“剔除”
int prime[N];//存放质数
int cnt;//质数的个数
int main(){
int n;
cin>>n;
for(int i=2;i<=n;i++){
if(!str[i]){
prime[cnt++]=i;
for(int j=i+i;j<=n;j=j+i) str[j]=true;
}
}
cout<<cnt;
}
现分析在优化算法后的时间复杂度:首先1~n中会有 n/ln(n)个质数,所以时间复杂度为O(nln(n)/ln(n)=O(n)
事实上,用朴素筛质数的算法,执行时间为:
优化后的筛质数的算法:
3.4线性筛质数
在3.3中,尽管对朴素筛质数做出了优化,但仍然有合数被重复标记,也就是说,也许某个合数本来就是true了,再给其赋予true。例如15,既是3的5倍,也是5的3倍,重复标记。
因而下给出线性筛质数的策略,保证每一个合数只会被它的最小的质因子与某个数的乘积删除。具体代码为:
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) primes[cnt ++ ] = i;
for (int j = 0; primes[j] <= n / i; j ++ )
{
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
首先我们关注st[primes[j] * i] = true;
这里就保证了每个合数只会被它的最小质因子删除。primes是从质数数组取出来的某个质数,分为两种情况:
1.primes[j]是i的最小质因子,那么primes[j]也一定是primes[j]*i的最小质因子,因为primes[j]是质数呀,所以primes[j]*i最多只能整除primes[j]
2.primes[j]不是i的最小质因子,说明primes[j]一定会小于i的最小质因子,这时候primes[j]仍然为primes[j]*i的最小质因子,首先primes[j]一定是primes[j]*i的质因子,假设i的最小质因子是q,那下一个primes[j]*i可以整除的数应该是primes[j]*q大于primes[j]。
综上,st[primes[j] * i] = true
将合数primes[j]*i用它的最小质因子的倍数剔除了。
那怎么可以保证不被重复标记?注意if (i % primes[j] == 0) break
语句,如果i可以整除primes[j],也就是说后续i * primes[j+1]可以不用做了,因为primes[j]是i的最小质因子,说明i一定是个合数,这样无论i乘以什么数,都是primes[j]的倍数,假设这个倍数是q把,那i在遍历到q的时候,那str[primes[j]*i]=true
又会把它剔除掉。那我们会想,会不会有比primes[j]更小的质数p,使得q可以整除p,导致primes[j]*q没有被标记到呢
显然是不会的,因为iprimes[j+1]=qprimes[j],那qprimes[j]的最小质因子仍然是i的最小质因子(primes[j])为质数,所以iprimes[j+1]也就是i遍历到q的时候,p就是primes[j],因此一定会被标记。
综上,在i可以整除primes[j]时,可以退出循环。
最后一个问题,为什么循环条件为:primes[j] <= n / i
而不是j<=cnt
,这是因为我总是可以保证在j=cnt前我就可以退出循环。
对于遍历到了i,如果i本身就是质数,那么由于先前!str[i]的判断,i已经加入了primes数组,那么遍历到primes[j]==i时可以退出。
如果i是合数,但是我已经把2~i-1的所有质数都加入到primes数组了,i的最小质因子肯定在primes数组中,因而也是可以退出循环的。
每个数都只筛一次,因此是线性的。
我们可以比较一下线性筛法和优化后的朴素筛法(埃氏筛法)的时间复杂度:
时间为 36ms,其实与埃氏筛法不会相差太多,但是我们将数据从10的6次方修改到到10的7次方:
线性筛法:
埃氏筛法:
可以看到,线性筛法的时间为141ms,埃氏筛法的时间为354ms,线性筛法的时间是可以节省的。