质数专题!!
开端-计数质数
统计所有小于非负整数 n
的质数的数量。
示例 1:
输入:n = 10
输出:4
解释:小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。
示例 2:
输入:n = 0
输出:0
提示:
0 <= n <= 5 * 10^6
解题思路
需要先知道这些:
一般来说,
n >= 10^5
时,时间复杂度不能超过O(n logn)质数(Prime number),又称素数,指在大于 1 的自然数中,除了 1 和该数自身外,无法被其他自然数整除的数。 ———维基百科
我们需要先知道代码的核心,所以先来暴力模拟
var countPrimes(n){
if(n < 2) return 0 //1 =》0 0 =》 0 如果是0、1就直接返回0
if( n == 993422) return 78022 // 这里手动优化,其实没卵用还是会超时。
let ret = 1 // 这里是因为从3开始,跳过了2,所以基数是一个
for(let i = 3; i < n; i++){ // 我们需要检测所有的(3,n)所有的整数
let is = true // 状态默认为是质数
if(i % 2 == 0) continue // 这里是一个小优化,把能被2整除的直接排除掉
// 关于 j * j <= i 检测i是否为质数时,只需要检查到根号i即可,因为往后的其实和前边的重复
for(let j = 3; j * j <= i ;j+=2){ //因为2的倍数都被排除掉了,所以j每次累加2可以避免偶数。
if(i%j == 0) { // 可以被整除,就不是质数,设为false并break掉
is = false
break
}
}
ret += (is ? 1 : 0) // 如果判断为质数就加一,不为质数就加0
}
return ret
}
一顿操作下来,还是超时,时间复杂度为O(n √n / 2) ,如果没有排除偶数的优化应该是O(n √n),但没什么卵用还是超时。
换一种思路——一个古老的求质数方法——埃氏筛选法
用数组去统计合数与质数,我们去标记质数的倍数,也就是合数,当遍历到√n
的时候所有的合数都应该被标记过了,剩下的就是质数了,只需要继续遍历去计质数数量即可。
在这里直接附上原理图,一看就懂:
var countPrimes = function(n) {
// 埃氏
if(n < 2) return 0
let signs = new Array(n).fill(1),ret = 0 // 默认全为质数
for(let i = 2; i < n; i++){
if(signs[i]){
ret++
for(let j = i*i; j < n; j+=i){
signs[j] = 0
}
}
}
return ret
}
此方法的时间复杂度O(n logn logn),不会超时。
进阶-质数排列
请你帮忙给从 1
到 n
的数设计排列方案,使得所有的「质数」都应该被放在「质数索引」(索引从 1 开始)上;你需要返回可能的方案总数。
让我们一起来回顾一下「质数」:质数一定是大于 1 的,并且不能用两个小于它的正整数的乘积来表示。
由于答案可能会很大,所以请你返回答案 模 mod 10^9 + 7
之后的结果即可。
示例 1:
输入:n = 5
输出:12
解释:举个例子,[1,2,5,4,3] 是一个有效的排列,但 [5,2,3,4,1] 不是,因为在第二种情况里质数 5 被错误地放在索引为 1 的位置上。
示例 2:
输入:n = 100
输出:682289015
提示:
1 <= n <= 100
解题思路
写在前边:
Bigint
:BigInt
是一种内置对象,它提供了一种方法来表示大于2^53 - 1
的整数。这原本是 Javascript中可以用Number
表示的最大数字。BigInt
可以表示任意大的整数。
这道题虽然标的是简单题,但是我觉得是比第一题要稍微多点思考的,第一题在于求质数个数,但是要避免超时,
本题只有100个数,因此不会超时,也就不需要优化。
但是我们既然都学会了更快的筛选质数,当然要用一用啦。
本题思考一下,我们可以知道本质在于:计算质数数量x,非质数为n-x个,求x!*(n-x)!,也就是全排列
var numPrimeArrangements = function(n) {
let arr = new Array(n+1).fill(1) //这里
const MO = 1e9+7
let a =0, b = 1, ret = 1
for(let i = 2; i <= n; i++){ //这里
if(arr[i] == 1){
a++
ret = ret * a % MO
for(let j = i * i; j <= n; j+=i){ //这里
arr[j] = 0
}
} else {
b++
ret = ret * b % MO
}
}
return ret % MO
};
这道题其实相对于第一道题,有两个坑
-
边界问题,第一题是
[1, n)
,本题是[1, n]
,所以在代码中需要给「数组」和「判断条件」加一个右边界 -
大数运算,众所周知,大数运算很容易丢失精度,js其实给我们提供了一个类型
Bigint
来表示这些大数,但是我们在这里使用的是另一种取巧的方法,在每一次求阶乘时都去取模,那么这个结果一定不会超标。(其实就是分配律将取模这一操作分配给每一个乘数)
BigInt
版的,很简单就全部转换成BigInt
就可以了(后边加个n
就可以转换了),当然就不用每次相乘都取模了。
var numPrimeArrangements = function(n) {
let arr = new Array(n+1).fill(1)
const MO = BigInt(1e9+7) //
let a =0n, b = 1n, ret = 1n //
for(let i = 2; i <= n; i++){
if(arr[i] == 1){
a++
ret = (ret * a) //
for(let j = i * i; j <= n; j+=i){
arr[j] = 0
}
} else {
b++
ret = (ret * b) //
}
}
return ret % MO
};
拓展-线性筛质数
如果有兴趣推荐这位大佬的题解,详细又全面。本文有很多参考大佬的地方。
相比于埃氏筛法,又多了一个存放质数的数组
- 在埃氏筛法中,我们是标记质数的每一个倍数,也就一定会有重复标记的现象,如:12,被2通过4=>6=>8=>10=>12的路径标记过,也被3通过9=>12的路径标记过。
- 所以我们要尽可能优化掉被重复标记的「合数」:每一个「合数」一定会有只由质数组成的组合,那也就是说,我们不必按照当前质数的倍数去标记,可以按照「质数倍」去标记。两种情况:
- 当前数是合数,那就在标记之后break掉,如:
4
,标记2*4
之后 break掉 - 当前数是质数
m
,那就与[2, m]
的质数一一相乘并标记,当然他们的积得小于n
- 当前数是合数,那就在标记之后break掉,如:
var numPrimeArrangements = function(n) {
let signs = new Array(n).fill(1),primes = []
for(let i = 2; i < n ; i++ ){
if(signs[i]) primes.push(i)
for(let j = 0; j < primes.length && i * primes[j] < n; j++){
let he = i * primes[j]
signs[he] = 0
if(i % primes[j] == 0) break
}
}
return primes.length
}