质数:
质数的定义:
在大于1的整数(不包含1)中,如果只包含1和本身这两个约数,就被成为质数,或者叫素数。
质数的判定:
试除法:时间复杂度o(sqrt(n))
bool IsPrime(int n){
if(n < 2) return false;
// 推荐使用n/i,不是sqrt(n)以及i*i<=n
for(int i = 2; i <= n / i; i++){
if(n % i == 0) return false;
}
return true;
}
分解质因数:
每个合数都可以写成几个质数相乘的形式,其中每个质数都是这个合数的因数,把一个合数用质因数相乘的形式表示出来,叫做分解质因数。
题目:
给定 n个正整数 ai,将每个数分解质因数,并按照质因数从小到大的顺序输出每个质因数的底数和指数。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个正整数 ai。
输出格式
对于每个正整数 ai,按照从小到大的顺序输出其分解质因数后,每个质因数的底数和指数,每个底数和指数占一行。
每个正整数的质因数全部输出完毕后,输出一个空行。
数据范围
1≤n≤100,
2≤ai≤2e9
试除法:
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
// 输出 n 的所有质因数以及每个质因数的次数
void divid(ll n){
// n中最多只包含一个大于sqrt(n)的质因子
for(int i = 2; i < n / i; i++){
if(n % i == 0){// i一定是质数
int s = 0;
while(n % i == 0){
n/=i;
s++;
}
cout << i << " " << s << '\n';
}
}
// 这个时候n是那个大于sqrt(n)的质因子
if(n > 1) cout << n << " 1\n";
}
int main() {
int t; cin >> t;
while(t--){
ll n; cin >> n;
divid(n);
cout << '\n';
}
return 0;
}
代码解析:
为什么for循环是到sqrt(n)?
质因数成对出现:
对于一个数 n
,如果它能被某个小于 sqrt(n)
的数 i
整除,那么它必然也能被大于 sqrt(n)
的另一个数 n/i
整除。比如:
- 假设
n = 36
,则6
是sqrt(36)
,36的因数有2
和18
(其中2
小于sqrt(36)
,而18
大于sqrt(36)
),以及3
和12
,6
和6
。
因此,在分解质因数时,如果你已经检查到 sqrt(n)
,那么 n
中的所有可能的较小质因数已经被处理了,剩下的就是大于 sqrt(n)
的质因数。
为什么for循环要从2开始?
我们从 2
开始枚举,因为 2
是最小的质数,而 n
的所有质因数都必须小于等于 sqrt(n)
或等于 n
本身。
通过从最小的质数开始,我们能够有效地将 n
中的所有质因数剔除。这种剔除是通过反复将 n
除以当前因子 i
来完成的。这样,后续的 i
不论是质数还是合数,都不会在 n
中再有重复的因数。
为什么我们需要判断质数是否是n的质因子却还是枚举数,这样不会有合数的影响吗?
程序对每个 i
都不断地除 n
,直到 n
不能再被 i
整除。
在代码中,i
可能是一个合数,但由于算法的顺序性和对 n
的分解策略,合数不会影响结果。关键点在于:当 i
是一个合数时,其因数早在更小的质数 '
被处理时已经剔除掉了。如果 i
是合数,例如 4
,由于在 i = 2
的时候,所有的 2
作为因数都已经被除去,因此 4
这种合数在后面判断时将不再能整除 n
,因此也不会进入 if
块。所以合数并不会影响。
筛质数:
题目:
给定一个正整数 n,请你求出 1∼n 中质数的个数。
输入格式
共一行,包含整数 n。
输出格式
共一行,包含一个整数,表示 1∼n1∼ 中质数的个数。
数据范围
1≤n≤1e6
埃氏筛法:
-
时间复杂度:O(nlog(logn))。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
bool st[N];
int prime[N],cnt; //cnt表示质数的个数
// 计算小于或等于 n 的所有质数,并将其存储在 prime 数组中
void get_Prime(int n){
for(int i = 2; i <= n; i++){
if(!st[i]){
prime[cnt++] = i;
//由于合数是由质数组成的,所以只要质数的倍数一定是合数
for(int j = i + i; j <= n; j+=i) st[j] = true;
}
}
}
int main() {
int n; cin >> n;
get_Prime(n);
cout << cnt <<'\n';
return 0;
}
线性筛法:
-
若n≈1e6,线性筛和埃氏筛的时间效率差不多,若 n≈1e7,线性筛会比埃氏筛快了大概一倍。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
bool st[N];
int prime[N],cnt; //cnt表示质数的个数
// 计算小于或等于 n 的所有质数,并将其存储在 prime 数组中
void get_Prime(int n){
for(int i = 2; i <= n; i++){
if(!st[i]) prime[cnt++] = i;
for(int j = 0; prime[j] <= n / i; j++){
st[prime[j]*i] = true;// 标记由质数prime[j]和当前数i构成的合数
if(i % prime[j] == 0) break; //primes[j]一定是i的最小质因子
}
}
}
int main() {
int n; cin >> n;
get_Prime(n);
cout << cnt <<'\n';
return 0;
}
1. 为什么使用 prime[j] * i
?
对于每个数 i
,我们尝试用所有已经找到的质数 prime[j]
来生成合数(也就是 prime[j] * i
)并将其标记为合数。这是因为:
- 如果
i
是质数,它的所有倍数显然是合数,所以我们要标记这些合数。 - 如果
i
是合数,它已经是某个较小质数的倍数,接下来的倍数(例如prime[j] * i
)也应该标记为合数。
2. 为什么要判断 prime[j] <= n / i
?
prime[j] <= n / i
这一条件确保我们不会生成超出范围 n
的合数。因为 prime[j] * i
必须小于等于 n
,所以 prime[j]
不能太大,否则 prime[j] * i
会超过 n
。
3. 为什么要使用 if(i % prime[j] == 0) break;
?
if(i % prime[j] == 0) break;
语句的作用在于避免生成重复的合数标记
如果当前数字 i
能被质数 prime[j]
整除(即 i
是 prime[j]
的倍数),在这种情况下,i * prime[j]
之后的乘积会包含更大的质因子,而这些合数会在后续处理中由其他更大的质因子与某个数乘积时标记到。因此,n只会被其最小质因子删掉
举例: n = 3
-
i = 2:
st[2]
为false
,因此2
是质数。- 将
2
加入prime[]
,此时prime[] = {2}
。 - 遍历
prime[j]
:prime[0] = 2
,计算2 * 2 = 4
,将st[4]
标记为true
。- 因为
2 % 2 == 0
,执行break
,跳出内循环。后续的6可以由更大的质因子和某个数相乘标记到
-
i = 3:
st[3]
为false
,因此3
是质数。- 将
3
加入prime[]
,此时prime[] = {2, 3}
。 - 遍历
prime[j]
:prime[0] = 2
,计算2 * 3 = 6
,将st[6]
标记为true
。prime[1] = 3
,计算3 * 3 = 9
,将st[9]
标记为true
。- 因为
3 % 3 == 0
,执行break
,跳出内循环。
约数:
定义:一个数的约数是能整除该数的整数。
例如:12 的约数包括 1,2,3,4,6,12,因为这些数都能整除 12。
题目:
给定 n 个正整数 ai,对于每个整数 ai,请你按照从小到大的顺序输出它的所有约数。
输入格式
第一行包含整数 n。
接下来 n行,每行包含一个整数 ai。
输出格式
输出共 n行,其中第 i行输出第 i 个整数 ai的所有约数。
数据范围
1≤n≤100,
1≤ai≤2e9
试除法:
一个数d
要是能被n
整除,那么n/d
也能被n
整除,即约数也是成对存在的,我们只需枚举较小的约数即可,另一个通过 n / d求出!从1
枚举到sqrt(n)
即可,即i <= n /i
。
#include<bits/stdc++.h>
using namespace std;
vector<int> get_divisors(int n){
vector<int> res;
// 枚举一个较小的因子,另一个直接用原数去除。
for(int i = 1; i <= n / i; i++){
if(n % i == 0){
res.push_back(i);// 小的约数
// 如果重复的话就只需要添加一个,不重复就添加较大的约数
if(i != n/ i) res.push_back(n / i);
}
}
sort(res.begin(),res.end());
return res;
}
int main() {
int n; cin >> n;
while(n--){
int x; cin >> x;
auto res = get_divisors(x);
for(auto i : res) cout << i <<' ';
cout << '\n';
}
return 0;
}
约数个数:
来源百度百科
#include <iostream>
#include <algorithm>
#include <unordered_map>
/*
先用试除法分解质因子,把每个质因子和质因子出现的次数都记录在哈希中
然后再运用公式所有质因子出现次数都加一再相乘
*/
using namespace std;
typedef long long LL;
const int mod = 1e9 + 7;
int main()
{
int n;
cin >> n;
unordered_map<int,int> primes;// 哈希表
while(n --)
{
int x;
cin >> x;
// 试除法将n分解质因数
for(int i = 2; i <= x / i; i ++ )
{
while(x % i == 0)
{
x /= i;
// 质因子的指数为primes[i],底数为i
primes[i] ++;
}
}
if(x > 1) primes[x] ++;
//如果x>1,说明x是一个比较大的质因数,然后把剩下的这个数加上就可以了
}
LL res = 1;
// prime.first是底数,prime.second是指数
for(auto prime : primes) res = res * (prime.second + 1) % mod;
cout << res << endl;
return 0;
}
约数之和:
#include <iostream>
#include <algorithm>
#include <unordered_map>
using namespace std;
typedef long long LL;
const int mod = 1e9 + 7;
/*
先用试除法分解质因子,把每个质因子和质因子出现的次数都记录在哈希中
*/
int main()
{
int n;
cin >> n;
unordered_map<int,int> primes;
while(n --)
{
int x;
cin >> x;
for(int i = 2; i <= x / i; i ++ )
{
while(x % i == 0)
{
x /= i;
primes[i] ++;
}
}
if(x > 1) primes[x] ++;
}
LL res = 1;
for(auto prime : primes)
{
int p = prime.first, a = prime.second; //p为底数,a为指数
LL t = 1;
// 求p的0次方一直加到p的a次方
// 首先是1->1+p->p^2+p+1直到加到a次方
while(a --) t = (t * p + 1) % mod;
res = res * t % mod;
}
cout << res << endl;
return 0;
}
最大公约数(辗转相除法/欧几里得算法):
结论: gcd(a,b)=gcd(b,amodb)
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}