数论 筛法笔记

本文探讨了数论筛法在解决数的约数个数及其和的问题上的应用,以及如何优化算法以提高效率。通过枚举约数出现的次数和优化求和过程,实现了从O(n^2)到O(n*logn)的时间复杂度转换。此外,还讨论了在序列中查找成对质因数关系的计数问题,并提供了两种不同的解决方案。最后,介绍了如何预处理质因子信息以快速获取数的质因子个数及最大、最小质因子。
摘要由CSDN通过智能技术生成

数论 筛法思想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=51422314151
不难发现 [ 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=1nin
所以我们代码可以改成

#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=1n(in1)=i=1ninn

所以可以直接化简为

#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 ai2e5 ,保证序列中每个数两两不同

例如:

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=2235=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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值