数论 筛法思想1
1.求解 [ 1 , n ] [1,n] [1,n] 内每个数的约数的个数的和, n = 1 e 6 n = 1e6 n=1e6
例如:
n = 4 n=4 n=4
d ( 1 ) = 1 , d ( 2 ) = 2 , d ( 3 ) = 2 , d ( 4 ) = 3 , 1 + 2 + 2 + 3 = 8 d(1)=1,d(2)=2,d(3)=2,d(4)=3 , 1+2+2+3=8 d(1)=1,d(2)=2,d(3)=2,d(4)=3,1+2+2+3=8
思路:
定义 v a l i val_i vali为 i i i的约数个数
那么我们只需要对 v a l val val数组求前缀和就是 [ 1 , n ] [1,n] [1,n]内每个数的约数的个数的和
求约数个数
定义一个 i i i循环 1 − > n 1->n 1−>n
每次都用 j j j从往后面筛, j j j每次加 i i i
那么每次 j j j一定是 i i i的倍数
所以 v a l [ j ] + + val[j]+ + val[j]++即可
再求和 d p [ 1 , n ] dp[1,n] dp[1,n]就是每个数的约数的个数和
#include <bits/stdc++.h>
using namespace std;
const int MAX = 1e6 + 10;
int val[MAX];
int n,S=0;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = i; j <= n; j += i) {
val[j]++;
}
S += val[i];
}
cout << dp[n];
return 0;
}
优化:
之前我们是枚举每位数有多少个约数,然后求和就是答案
现在我们枚举约数出现的次数
n
=
5
时
1
出
现
了
4
次
2
出
现
了
2
次
3
出
现
了
1
次
4
出
现
了
1
次
5
出
现
了
1
次
n=5时\\ 1出现了4次\\ 2出现了2次\\ 3出现了1次\\ 4出现了1次\\ 5出现了1次
n=5时1出现了4次2出现了2次3出现了1次4出现了1次5出现了1次
不难发现
[
1
,
n
]
[1,n]
[1,n]之间每个约数的出现次数为
n
i
\frac{n}{i}
in
c
o
n
t
=
∑
i
=
1
n
⌊
n
i
⌋
cont=\sum_{i=1}^n\lfloor \frac{n}{i} \rfloor
cont=i=1∑n⌊in⌋
所以我们代码可以改成
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, val = 0;
cin >> n;
for (int i = 1; i <= n; i++) {
val += n / i;
}
cout << val;
return 0;
}
这样的话复杂度为 O ( n ) O(n) O(n)
但如果 n < = 1 0 10 n<=10^{10} n<=1010的话显然 O ( n ) O(n) O(n)的写法会超时
优化
每次输出 n / i n/i n/i的值,不难发现 n / i n/i n/i的值都是成块出现的
30
30 15 10 7 6 5 4 3 3 3 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
111
通过输出我们可以看出做了非常多重复的计算
我们只需要枚举值等于当前块的 i i i的最小值减去上一个块的右端点再乘它的次数就是答案
如果能简化成
val = 1 * 30 + 1 * 15 + 1 * 10 + 1 * 7 + 1 * 6 + 1 * 5 + 1 * 4 + 3 * 3 + 5 * 2 + 15 * 1
那么外循环的次数就是块数,内循环使用二分寻找当前快的右端点
那么复杂度就能降到 O ( n ∗ l o g n ) O(\sqrt n * logn) O(n∗logn)
这样就能处理 1 e 10 1e10 1e10的数据
代码:
#include <bits/stdc++.h>
#define ll long long
using namespace std;
int main() {
ll n;
cin >> n;
ll val = 0;
ll R = 0;//记录上一个块的右端点
//右端点大于n就退出循环
for (int i = 1; R + 1 <= n; i++) {
ll l = R + 1, r = n;
while (l <= r) {
ll mid = (l + r) >> 1;
if (n / mid >= n / (R+1)) l = mid + 1;
else r = mid - 1;
}
val += (r - R) * (n / r);//块长度(值的个数) * 值
R = r;//更新右端点
}
cout << val;
return 0;
}
2 求解 [ 1 , n ] [1,n] [1,n] 内有多少对数 i , j i,j i,j 满足 i i i 是 j j j的约数, n = 1 e 6 n = 1e6 n=1e6
多少对约数,枚举每一个约数有多少数字符合
n = 5 , a n s = 5 n = 5 , ans = 5 n=5,ans=5
n = 6 , a n s = 8 n=6,ans=8 n=6,ans=8
暴力
#include <bits/stdc++.h>
using namespace std;
const int MAX = 1e6 + 10;
int val;
int n,S=0;
int main() {
cin >> n;
//外循环枚举 [1,n]
for (int i = 1; i <= n; i++) {
//从 i+i 开始枚举 i 的倍数
for(int j=i+i;j<=n;j+=i){
val++;
}
}
cout << val;;
return 0;
}
不难发现,这个与上面那个题相比每次少加了一次
公式为
c
o
n
t
=
∑
i
=
1
n
(
⌊
n
i
⌋
−
1
)
=
∑
i
=
1
n
⌊
n
i
⌋
−
n
cont=\sum_{i=1}^n(\lfloor \frac{n}{i} \rfloor-1)\\ =\sum_{i=1}^n\lfloor \frac{n}{i} \rfloor-n
cont=i=1∑n(⌊in⌋−1)=i=1∑n⌊in⌋−n
所以可以直接化简为
#include <bits/stdc++.h>
#define ll long long
using namespace std;
int main() {
ll n;
cin >> n;
ll val = 0;
ll R = 0;//记录上一个块的右端点
//右端点大于n就退出循环
for (int i = 1; R + 1 <= n; i++) {
ll l = R + 1, r = n;
while (l <= r) {
ll mid = (l + r) >> 1;
if (n / mid >= n / (R+1)) l = mid + 1;
else r = mid - 1;
}
val += (r - R) * (n / r);//块长度(值的个数) * 值
R = r;//更新右端点
}
cout << val - n;//输出减 n 就是对数
return 0;
}
3 给定一个序列 a i a_i ai , 有多少对数 a i , a j a_i,a_j ai,aj 成倍数。输出对数
序列长度 n = 2 e 5 n = 2e5 n=2e5, a i ≤ 2 e 5 a_i \leq 2e5 ai≤2e5 ,保证序列中每个数两两不同
例如:
n = 4 , a = [ 1 , 3 , 6 , 2 ] n = 4 , a = [1,3,6,2] n=4,a=[1,3,6,2]
输出: 5 5 5
思路:
成倍数就是有多少对 a i , a j a_i,a_j ai,aj, a i a_i ai是 a j a_j aj的约数
预处理每一个数的约数数组
d [ i ] d[i] d[i]为 i i i的所有约数
定义 c n t [ i ] cnt[i] cnt[i]为 i i i出现的次数,因为题目保证两两不同,所以我们遇到 a r r [ i ] arr[i] arr[i]让 c n t [ a r r [ i ] ] = 1 cnt[arr[i]]=1 cnt[arr[i]]=1即可
最后遍历数组,加上每一个数的约数出现的次数就是答案
#include <bits/stdc++.h>
#define ll long long
const int MAX = 1e5 + 10;
using namespace std;
vector<int> d[MAX];
int arr[MAX];
int cnt[MAX];
int n, val;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> arr[i];
cnt[arr[i]] = 1;
}
//预处理每个数的约数数组
for (int i = 1; i < MAX; i++) {
for (int j = i + i; j < MAX; j += i) {
d[j].push_back(i);
}
}
//遍历每个数的约数 加上约数出现的次数
for (int i = 1; i <= n; i++) {
for (int j = 0; j < d[arr[i]].size(); j++) {
val += cnt[d[arr[i]][j]];
}
}
cout << val;
return 0;
}
二:
定义 c n t [ i ] cnt[i] cnt[i]为 i i i是否出现
循环枚举 i − > [ 1 , M A X ] i->[1,MAX] i−>[1,MAX], j j j从 i + i i+i i+i开始枚举到MAX,这样 j j j一定是 i i i的倍数
v a l val val直接加 c n t [ j ] cnt[j] cnt[j]就行
#include <bits/stdc++.h>
#define ll long long
const ll MAX = 2e5;
using namespace std;
int cnt[MAX+10], arr[MAX+10];
int n, val;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> arr[i];
cnt[arr[i]] = 1;
}
for (int i = 1; i <= MAX; i++) {
if(cnt[i]){
for (int j = i + i; j <= MAX; j += i) {
val += cnt[j];
}
}
}
cout << val;
return 0;
}
4 输出 [ 1 , n ] [1,n] [1,n]每个数质因分解后的最小质因子,最大质因子以及不同质因子的个数 , n < = 1 e 6 n <= 1e6 n<=1e6
筛法
定义 m i i mi_i mii为 i i i的最小质因子 m x i mx_i mxi为 i i i的最大质因子 c n t i cnt_i cnti为 i i i的不同质因子个数
#include <bits/stdc++.h>
const int MAX = 1e6;
using namespace std;
int mi[MAX + 10], mx[MAX + 10], cnt[MAX + 10];
int n;
int main() {
cin >> n;
for (int i = 2; i <= n; i++) {
if (mi[i] == 0) {//没有被筛过 当前 i 是质数
mi[i] = i;//质数的最小质因子就是自己
mx[i] = i;//质数的最大质因子也是自己
cnt[i] ++;//质因子个数为一个, 1 不是质数
//以当前质数为起点 开始筛 j 一定是 i 的倍数
for (int j = i + i; j <= n; j += i) {
mx[j] = i;//最大值每次都要覆盖 最后一次覆盖的值一定是最大值
cnt[j] ++;
if (mi[j] != 0) continue;//第一次筛的就是最小值 所以最小值填过了直接跳过
mi[j] = i;
}
}
}
return 0;
}
5 输出 [ 1 , n ] [1,n] [1,n]输出每个数质因子的个数(相同的也算) , n = 1 e 6 n = 1e6 n=1e6
例如:
16 = 2 4 = 4 16 = 2^4=4 16=24=4
60 = 2 2 ∗ 3 ∗ 5 = 4 60=2^2*3*5=4 60=22∗3∗5=4
定义 d p i dp_i dpi为 i i i质因子的个数
不难发现 对于每个
i
i
i
d
p
[
i
]
=
d
p
[
i
/
m
i
[
i
]
]
+
1
;
dp[i] = dp[i/mi[i]]+1;
dp[i]=dp[i/mi[i]]+1;
其中 m i [ i ] mi[i] mi[i]是 i i i的最小质因子,我们只需要预处理 m i [ i ] mi[i] mi[i]就能 O ( n ) O(n) O(n)的处理
#include <bits/stdc++.h>
const int MAX = 1e6;
using namespace std;
int dp[MAX + 10];
int mi[MAX + 10];
int n;
int main() {
cin >> n;
for (int i = 2; i <= MAX; i++) {
if (mi[i] == 0) {
mi[i] = i;
for (int j = i + i; j <= MAX; j += i) {
if (mi[j] != 0) continue;
mi[j] = i;
}
}
}
for (int i = 2; i <= n; i++) {
dp[i] = dp[i / mi[i]] + 1;
}
return 0;
}
6 给出长度为n(1 <= n <= 1 e 5 1e5 1e5)的数列(1 <= ai <= 1 e 5 1e5 1e5),问最多能找出长度为多少的子序列,满足子序列中任意两个数不互素?
∀ i , j , g c d ( a i , a j ) ≠ 1 \forall i,j ,gcd(a_i,a_j) \neq 1 ∀i,j,gcd(ai,aj)=1
a = [ 2 , 6 , 4 , 5 , 7 , 8 , 1 ] \Huge a=[2,6,4,5,7,8,1] a=[2,6,4,5,7,8,1]
1.枚举数i,扫序列看有多少个数能够被i整除,然后取他们的最大值
#include <bits/stdc++.h>
const int MAX = 1e5;
using namespace std;
int arr[MAX + 10];
int prime[MAX + 10];
int n, Mx;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> arr[i];
for (int i = 2; i <= MAX; i++) {
if (!prime[i]) {
int val = 0;
for (int j = i + i; j <= MAX; j++) prime[j] = 1;
for (int j = 1; j <= n; j++) val += arr[j] % i == 0;
Mx = max(val, Mx);
}
}
cout << Mx;
return 0;
}
2.枚举每个数a_i,统计质因子出现的次数。出现最多的就是答案。
#include <bits/stdc++.h>
const int MAX = 1e5;
using namespace std;
int cnt_prime[MAX + 10];
int prime[MAX + 10];
vector<int> Pr;
int n, len, Mx;
int main() {
cin >> n;
// 预处理 [1,MAX] 之间的所有质数
for (int i = 2; i <= MAX; i++) {
if (!prime[i]) {
Pr.push_back(i);
for (int j = i + i; j <= MAX; j++) prime[j] = 1;
}
}
len = Pr.size();
for (int i = 1; i <= n; i++) {
int val;
cin >> val;
for (int j = 0; j < len; j++) {
if (val < Pr[j]) break;
if (!(val % Pr[j])) {
cnt_prime[Pr[j]]++;
while (!(val % Pr[j])) val /= Pr[j];
}
}
}
for (int i = 0; i < len; i++) {
Mx = max(Mx, cnt_prime[Pr[i]]);
}
cout << Mx;
return 0;
}