数论 筛法笔记

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

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数论 筛法思想1

1.求解[1,n][1,n][1,n] 内每个数的约数的个数的和,n=1e6n = 1e6n=1e6

例如:

n=4n=4n=4

d(1)=1,d(2)=2,d(3)=2,d(4)=3,1+2+2+3=8d(1)=1,d(2)=2,d(3)=2,d(4)=3 , 1+2+2+3=8d(1)=1,d(2)=2,d(3)=2,d(4)=3,1+2+2+3=8

思路:

定义valival_ivaliiii的约数个数

那么我们只需要对valvalval数组求前缀和就是[1,n][1,n][1,n]内每个数的约数的个数的和

求约数个数

定义一个iii循环1−>n1->n1>n

每次都用jjj从往后面筛,jjj每次加iii

那么每次jjj一定是iii的倍数

所以val[j]++val[j]+ +val[j]++即可

再求和dp[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]之间每个约数的出现次数为ni\frac{n}{i}in
cont=∑i=1n⌊ni⌋ 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<=1010n<=10^{10}n<=1010的话显然O(n)O(n)O(n)的写法会超时

优化

每次输出n/in/in/i的值,不难发现n/in/in/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

通过输出我们可以看出做了非常多重复的计算

我们只需要枚举值等于当前块的iii的最小值减去上一个块的右端点再乘它的次数就是答案

如果能简化成

val = 1 * 30 + 1 * 15 + 1 * 10 + 1 * 7 + 1 * 6 + 1 * 5 + 1 * 4 + 3 * 3 + 5 * 2 + 15 * 1

那么外循环的次数就是块数,内循环使用二分寻找当前快的右端点

那么复杂度就能降到O(n∗logn)O(\sqrt n * logn)O(nlogn)

这样就能处理1e101e101e10的数据

代码:

#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,ji,ji,j 满足 iiijjj的约数,n=1e6n = 1e6n=1e6

多少对约数,枚举每一个约数有多少数字符合

n=5,ans=5n = 5 , ans = 5n=5,ans=5

n=6,ans=8n=6,ans=8n=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;
}

不难发现,这个与上面那个题相比每次少加了一次

公式为
cont=∑i=1n(⌊ni⌋−1)=∑i=1n⌊ni⌋−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 给定一个序列aia_iai , 有多少对数ai,aja_i,a_jai,aj 成倍数。输出对数

序列长度n=2e5n = 2e5n=2e5,ai≤2e5a_i \leq 2e5ai2e5 ,保证序列中每个数两两不同

例如:

n=4,a=[1,3,6,2]n = 4 , a = [1,3,6,2]n=4,a=[1,3,6,2]

输出:555

思路:

成倍数就是有多少对ai,aja_i,a_jai,aj,aia_iaiaja_jaj的约数

预处理每一个数的约数数组

d[i]d[i]d[i]iii的所有约数

定义cnt[i]cnt[i]cnt[i]iii出现的次数,因为题目保证两两不同,所以我们遇到arr[i]arr[i]arr[i]cnt[arr[i]]=1cnt[arr[i]]=1cnt[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;
}

二:

定义cnt[i]cnt[i]cnt[i]iii是否出现

循环枚举i−>[1,MAX]i->[1,MAX]i>[1,MAX],jjji+ii+ii+i开始枚举到MAX,这样jjj一定是iii的倍数

valvalval直接加cnt[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<=1e6n <= 1e6n<=1e6

筛法

定义 miimi_imiiiii的最小质因子 mximx_imxiiii的最大质因子 cnticnt_icntiiii的不同质因子个数

#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=1e6n = 1e6n=1e6

例如:

16=24=416 = 2^4=416=24=4

60=22∗3∗5=460=2^2*3*5=460=2235=4

定义dpidp_idpiiii质因子的个数

不难发现 对于每个iii
dp[i]=dp[i/mi[i]]+1; dp[i] = dp[i/mi[i]]+1; dp[i]=dp[i/mi[i]]+1;

其中mi[i]mi[i]mi[i]iii的最小质因子,我们只需要预处理mi[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 <= 1e51e51e5)的数列(1 <= ai <= 1e51e51e5),问最多能找出长度为多少的子序列,满足子序列中任意两个数不互素?

∀i,j,gcd(ai,aj)≠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、付费专栏及课程。

余额充值