数论专题
目录
- MT2213 质数率
- MT2214 元素共鸣
- MT2215 小码哥的喜欢数
- MT2216 数的自我
- MT2217 数字游戏
MT2213 质数率
难度:黄金 时间限制:1秒 占用内存:256M
题目描述
请求出 [ 1 , n ] \left[1,n\right] [1,n] 范围内质数占比率。
格式
输入格式:一样一个整数 n n n,含义如题描述。。
输出格式:输出 [ 1 , n ] \left[1,n\right] [1,n] 范围内的质数占比,保留 3 位小数。样例 1
输入:
10输出:
0.400备注
对于 100% 的数据: 1 ≤ n ≤ 1 0 8 1\le n \le 10^8 1≤n≤108。
相关知识点:
筛法
题解
题目的要求很简单,本质就是求指定区间内的质数。最常规的做法就是暴力枚举:对区间 [ 1 , n ] \left[1,n\right] [1,n] 内的每个数进行判断,统计其是否为质数。即:
// 判断一个数是否为质数
bool isPrime(int n)
{
int limit = sqrt(n);
for(int i=2; i<=limit; i++)
if(n%i == 0)
return false;
return true;
}
// 统计区间 1-n 内的质数个数
int getPrimes(int n)
{
int ans = 0;
for(int i=2; i<=n; i++)
if(isPrime(i))
ans++;
return ans;
}
其中,判断一个数 n n n 是否为质数的时间复杂度为 O ( n ) O\left(\sqrt n\right) O(n),统计 n n n 个数中的质数个数的时间复杂度则为 O ( n 3 2 ) O\left(n^\frac{3}{2}\right) O(n23) ,这在 1 ≤ n ≤ 10 8 1\le n\le{10}^8 1≤n≤108 的数据范围下必定超时。同时,从数据范围看,实际上已经暗示了必须在线性时间复杂度内完成求解。因此,不得不用到筛法。筛法的主要思想是:对已经找出的质数,直接将其倍数从接下来的查找中筛出,而不必再去判断那些数是否为质数,从而节省时间。根据筛法的停止策略,主要可将其分为两类:
- 埃式筛法;
- 欧拉筛法。
下面分别对其进行分析。
埃拉托斯色尼筛选法
若要得到自然数 n n n 以内的全部质数,必须把不大于 n \sqrt n n 的所有质数的倍数剔除,则剩余数均为质数。例如,若要得到 [ 1 , 100 ] \left[1,100\right] [1,100] 范围内的全部质数,其求解步骤如下:
- 对 n n n 开方得到 100 = 10 \sqrt{100}=10 100=10;
- 求 [ 1 , 10 ] \left[1,10\right] [1,10] 内的全部质数,即 2、3、5、7;
- 从 [ 1 , 100 ] \left[1,100\right] [1,100] 将 2、3、5、7 的倍数全部剔除。
剩下的数均为质数。
埃式筛法的特点是简单易懂,其时间复杂度为 O ( n log log n ) O\left(n\log{\log{n}}\right) O(nloglogn),从上面的描述可直接得到其对应代码为:
const int MAX = 1e8+5;
bool vis[MAX];
int prime[MAX], cnt;
// 埃式筛法统计区间质数
int sieveByElatoseni(int n)
{
int limit = sqrt(n);
for(int i=2; i<=limit; i++){
// 若存在某个数尚未被访问,则其为质数
if(!vis[i]) prime[cnt++] = i;
// 将这个数的倍数全部置为已被访问
for(int j=i*2; j<=n; j+=i)
vis[j] = true;
}
// 剩余尚未被访问的数均为质数
for(int i=limit+1; i<=n; i++)
if(!vis[i]) prime[cnt++] = i;
return cnt;
}
欧拉筛选法
注意到在埃式筛法过程中,存在相当一部分重复筛除工作。例如,当确定 2 为质数时,后续会将其倍数:4、6、8、10、12、……全部筛除。而接下来当确定 3 为质数时,后续会将 6、9、12、……全部筛除。这时,所有以 2×3=6 为因数的数,如 6、12、18、24、……等都会被重复纳入筛除进程中,这无疑浪费了相当一部分计算资源。
因此,出现了一种更节约时间的筛法——欧拉筛法(线性筛法)。欧拉筛法的整体思路和埃式筛法相似,都是通过将已得到质数的倍数从数据集中筛除来减少判断时间。不过为了让某个数只执行一次筛除操作,欧拉筛法规定一个数只能被其最小的质因数给筛掉。例如,对 12 而言,质数 2 和 3 都能将其筛掉,但是欧拉筛法的执行过程中,只会让 12 在面对质数 2 时被筛除,而不再被 3 筛去。
为了实现 “合数只被其最小质因数筛除一次”,欧拉筛法不再遍历已确定质数的全部倍数,而是遍历已得到的全部质数,并在遍历过程中加入对当前乘数是否为质数的判断,以实现提前停止策略。正是这个提前停止策略,使得欧拉筛法具有线性复杂度。这个过程的详细步骤如下(对自然数从小到大枚举(从 2 开始)):
- 对当前数 i i i,如果其尚未被访问,则其必为质数,故将其加入质数队列;
- 枚举已记录的质数:
- 将当前质数与 i i i 相乘,其乘积得到的数必为合数,标记该数为已被访问;
- 判断 i i i 是否为质数。如果 i i i 是质数,则继续枚举质数队列中的数进行筛选;如果 i i i 是合数(即 i i i 存在某个约数,假设该约数为 p p p),则说明由当前质数与 i i i 相乘得到数有可能会在后续被其他更大的以 p p p 为约数的数筛掉,而欧拉筛法规定 “合数只被其最小质因数筛除一次”。由于我们的枚举过程是从小到大进行的,因此第一次筛掉某个数时,其总数满足欧拉筛法的要求的。因此,为了避免后续再对以 p p p 为最小质因数的数进行重复筛选,在此就必须强制退出当前循环。
循环结束。
这便是欧拉筛法的执行步骤,它对每个数的筛选都仅进行一次,故其时间复杂度为 O ( n ) O\left(n\right) O(n)。下面给出代码:
const int MAX = 1e8+5;
bool vis[MAX];
int prime[MAX], cnt;
// 欧拉筛法统计区间质数
int sieveByEuler(int n)
{
// 遍历全部数
for(int i=2; i<=n; i++){
// 如果当前数未被访问,则其为一个质数
if(!vis[i]) prime[cnt++] = i;
// 枚举已记录的质数:该质数的整倍数都不可能是质数
for(int j = 0; prime[j]*i<=n; j++){
// 将合数置为被访问
vis[prime[j]*i] = true;
// 下面的代码是精髓:每次筛除合数时,选择恰当的时机及时中断,避免后面重复筛选
// 如果 i 是质数,则继续对已选出的质数进行筛选
// 如果 i 是合数,则后续会出现重复筛选,故强制中断
if(i % prime[j] == 0) break;
}
}
return cnt;
}
下面通过一个实际例子来模拟欧拉筛法的具体执行流程,假设现在取 n = 25 n=25 n=25 :
从 “选出的质数” 和 “标记被访问(不是质数)” 两列看,欧拉筛法处理的数字并没有出现任何重复。这也就是说,对 [ 1 , n ] \left[1,n\right] [1,n] 内的任何数,欧拉筛法都只会判断一次,因此也称欧拉筛法为线性筛法。
回到本题,可基于欧拉筛法求出 [ 1 , n ] \left[1,n\right] [1,n] 内的全部质数,并将其与区间长度求商即可,下面给出完整代码:
/*
MT2213 质数率
欧拉筛法
*/
#include<bits/stdc++.h>
using namespace std;
const int MAX = 1e8+5;
bool vis[MAX];
int prime[MAX], cnt;
// 求 1-n 中的质数个数
int getPrime(int n)
{
// 从 2 开始向后查找质数
for(int i=2; i<=n; i++){
// 如果当前数未被访问,则其为一个质数
if(!vis[i]) prime[cnt++] = i;
// 枚举已记录的质数:该质数的整倍数都不可能是质数
for(int j = 0; prime[j]*i<=n; j++){
// 将合数置为被访问
vis[prime[j]*i] = true;
// 下面的代码是精髓:每次划掉合数时选择恰当的时机中断,避免后面重复划掉合数,提高算法效率
// 如果 i 是质数,则最多枚举到自身中断
// 如果 i 是合数,则最多枚举到自身的最小质数中断
if(i % prime[j] == 0) break;
}
}
return cnt;
}
int main()
{
// 获取输入
int n; cin>>n;
// 输出质数率
printf("%.3f",(double)getPrime(n)/n);
return 0;
}
MT2214 元素共鸣
难度:黄金 时间限制:1秒 占用内存:128M
题目描述
遥远的大陆上存在着元素共鸣的机制。
建立一个一维坐标系,其中只有素数对应的点的坐标存在着元素力,而相距为 k k k 的两个元素力之间能形成元素共鸣。现在,需要求出 n n n 范围内所有能元素共鸣的点对,并将他们以第一个点的坐标大小为关键字排序后输出(小的在前)。格式
输入格式:一行两个整数 n , k n,k n,k。
输出格式:所有小于等于 n n n 的素数对。每对素数对输出一行,中间用单个空格隔开。
若没有找到任何素数对,输出empty。样例 1
输入:
6924 809
输出:
2 811
备注
其中: 1 ≤ k ≤ n ≤ 10 4 1\le k\le n\le{10}^4 1≤k≤n≤104。
相关知识点:
筛法
题解
方法二:暴力求解
本题实际就是求 “具有指定差距 k k k 的质数对”,在 10 4 {10}^4 104 范围下如果按暴力方式逐个求解,也是可以通过的:
/*
MT2214 元素共鸣
暴力枚举
*/
#include<bits/stdc++.h>
using namespace std;
// 判断一个数是否为质数
bool isPrime(int n)
{
int limit = sqrt(n);
for(int i=2; i<=limit; i++)
if(n%i == 0)
return false;
return true;
}
int main( )
{
// 获取输入
int n, k, tmp; cin>>n>>k;
// 查找产生共鸣的元素
bool flag = false;
for(int i=2; i<n; i++) {
// 如果当前数为质数,则进一步判断目标数是否也为质数
if(isPrime(i)){
// 目标数
tmp = i + k;
// 不能超过数据范围,且必须是质数
if(tmp<=n && isPrime(tmp)){
flag = true;
cout<<i<<" "<<tmp<<endl;
}
}
}
if(!flag) cout<<"empty"<<endl;
return 0;
}
方法二:筛法求解
也可以利用筛法,提前算出所有的质数,然后再在质数里枚举能产生共鸣的元素:
/*
MT2214 元素共鸣
欧拉筛法
*/
#include<bits/stdc++.h>
using namespace std;
const int MAX = 1e4+5;
bool vis[MAX];
int prime[MAX], cnt;
int getPrime(int n)
{
for(int i=2; i<=n; i++){
if(!vis[i]) prime[cnt++] = i;
for(int j = 0; prime[j]*i<=n; j++){
vis[prime[j]*i] = true;
if(i % prime[j] == 0) break;
}
}
return cnt;
}
int main( )
{
// 获取输入
int n, k, tmp; cin>>n>>k;
// 获取全部的质数
getPrime(n);
// 查找产生共鸣的元素
bool flag = false;
for(int i=0; i<cnt-1; i++) {
// 目标数
tmp = prime[i] + k;
// 不能超过数据范围,且必须是质数
if(tmp<=n && !vis[tmp]){
flag = true;
cout<<prime[i]<<" "<<tmp<<endl;
}
}
if(!flag) cout<<"empty"<<endl;
return 0;
}
MT2215 小码哥的喜欢数
难度:钻石 时间限制:1秒 占用内存:128M
题目描述
小码哥不喜欢以下情况的数:
- 是7的倍数(包括7);
- 数字的某一位是7,这种数字的倍数,小码哥也不喜欢。
小码哥会给你 t t t个数,对其中每个数,如果这个数是他喜欢的数,就告诉他下一个他喜欢的数是多少(即大于这个数的下一个他喜欢的数)。如果这个数他不喜欢,那你要告诉他。
格式
输入格式:第一行,一个正整数 T T T 表示小码哥接下来要给你的数的总量;
接下来 T T T 行,每行一个整数 x x x,表示这一次小码哥给的数。
输出格式:输出共 T T T 行,每行一个整数。如果这个数是他喜欢的数,那么告诉他下一个他喜欢的数是多少(即大于这个数的下一个他喜欢的数);
如果这个数他不喜欢,那你要输出-1
;
注:定义 0 不为小码哥喜欢的数。样例 1
输入:
4
6
33
69
300输出:
8
36
80
-1备注
其中: 1 ≤ T ≤ 2 × 10 5 , 0 ≤ x ≤ 10 7 1\le T\le{2\times10}^5,\ 0\le x\le{10}^7 1≤T≤2×105, 0≤x≤107。
相关知识点:
筛法
题解
小码哥不喜欢含有数字 7 的数(以及这些数的倍数)。对于数字是否含有 7,可通过循环取余的方式得到。对于这些数的倍数,则可通过类似于埃氏筛法的方式进行筛选。对于 “下一个他喜欢的数”,可通过在筛选数的过程中,构建一个 next 数组来完成(即每次都将一个小码哥喜欢的数作为索引,保存找到的下一个他喜欢的数)。
下面直接给出基于以上思路得到的完整代码:
/*
MT2215 小码哥的喜欢数
*/
#include<bits/stdc++.h>
using namespace std;
const int MAX = 1e7+5;
// nxt 数组既充当了 vis 数组的作用,也用于保存“下一个数”的位置
int nums[MAX], nxt[MAX];
// 判断一个数是否含有数字 7
bool isContained(int n)
{
while(n){
if(n % 10 == 7)
return true;
n /= 10;
}
return false;
}
// 通过筛法求出所有小码哥喜欢的数
void getNums(int n)
{
// 初始化第一个小码哥喜欢的数(用于构建next数组)
int cur = 1, cnt = 0;
// 从 2 开始向后枚举
for(int i=2; i<=n; i++){
// 果当前数未被访问,则执行进一步判断
if(nxt[i] == 0){
// 如果这个数不包含 7 ,则其为一个小码哥喜欢的数
if(!isContained(i)){
// 记录当前数
nums[cnt++] = i;
// 将当前数作为前一个数的下一个数
nxt[cur] = i;
// 更新记录“下一个数”的指针
cur = i;
}
// 否则将这个数的所有倍数标记为小码哥不喜欢的数
else{
for(int j=i; j<=n; j+=i)
nxt[j] = -1;
}
}
}
// 注:0 也是小码哥不喜欢的数
nxt[0] = -1;
}
int main( )
{
// 提前打表获取小码哥喜欢的数
getNums(MAX-1);
// 获取输入
int T, n; cin>>T;
while(T--){
cin>>n;
cout<<nxt[n]<<endl;
}
return 0;
}
MT2216 数的自我
难度:钻石 时间限制:0.75秒 占用内存:128M
题目描述
提瓦特大陆上有一个贫穷的占星术士小码哥,出于占星术的要求,他时常要解决一些困难的数学问题。这天,上天给了他一个启示:有一类称作 Self-Numbers 的数。对于每一个正整数 n n n,我们定义 d ( n ) d\left(n\right) d(n) 为 n n n 加上它每一位数字的和。例如, d ( 75 ) = 75 + 7 + 5 = 87 d\left(75\right)=75+7+5=87 d(75)=75+7+5=87。给定任意正整数 n n n 作为一个起点,都能构造出一个无限递增的序列: n , d ( n ) , d ( d ( n ) ) , d ( d ( d ( n ) ) ) , … n,\ d\left(n\right),\ d\left(d\left(n\right)\right),\ d\left(d\left(d\left(n\right)\right)\right),\ldots n, d(n), d(d(n)), d(d(d(n))),… 例如,如果你从 33 开始,下一个数是 33+33=39,再下一个为 39+3+9=51,再再下一个为 51+5+1=57,因此你所产生的序列就像这样: 33,39,51,57,69,84,96,111,114,120,123,129,141……数字 n n n 被称作 d ( n ) d\left(n\right) d(n) 的发生器。在上面的这个序列中,33 是 39 的发生器,39 是 51 的发生器,51 是 57 的发生器等等。有一些数有超过一个发生器,如 101 的发生器可以是 91 和 100。一个没有发生器的数被称作 Self-Number。如前 13 个 Self-Number 为 1,3,5,7,9,20,31,42,53,64,75,86,97。我们将第 i i i 个 Self-Number 表示为 a [ i ] a\left[i\right] a[i],所以 a [ 1 ] = 1 , a [ 2 ] = 3 , a [ 3 ] = 5 , … a\left[1\right]=1,\ a\left[2\right]=3,\ a\left[3\right]=5,\ldots a[1]=1, a[2]=3, a[3]=5,…
现在小码哥需要找到一个 [ 1 , N ] \left[1,N\right] [1,N] 的区间内所有的 Self-Number,请你帮帮他。
格式
输入格式:第一行输入以空格隔开的两个整数 N N N 和 K K K;
第二行输入 K K K 个以空格隔开的整数 s 1 , s 2 , s 3 , … , s k s_1,\ s_2,\ s_3,\ldots,s_k s1, s2, s3,…,sk。输出格式:第一行你需要输出一个数,这个数表示在闭区间 [ 1 , N ] \left[1,N\right] [1,N] 中 Self-Number 的数量;
第二行必须包含以空格分隔的 K K K 个数,表示 a [ s 1 ] , … , a [ s k ] a\left[s_1\right],\ldots,a\left[s_k\right] a[s1],…,a[sk],这里保证所有的 a [ s i ] a\left[s_i\right] a[si] 都小于$ N$。
(例如,如果 N = 100 , s k N=100,s_k N=100,sk 可以为 1~13,但不能为 14,因为 a [ 14 ] = 108 > 100 a\left[14\right]=108>100 a[14]=108>100)。样例 1
输入:
100 10
1 2 3 4 5 6 7 11 12 13输出:
13
1 3 5 7 9 20 31 75 86 97备注
其中: 1 ≤ N ≤ 10 7 , 1 ≤ K ≤ 5000 1\le N\le{10}^7,\ 1\le K\le5000 1≤N≤107, 1≤K≤5000。
相关知识点:
筛法
题解
根据题目的意思可知,函数 d ( n ) d\left(n\right) d(n) 产生的结果总满足 d ( n ) ≥ n d\left(n\right)\geq n d(n)≥n 。因此,要找出所有的 SelfNumber,可通过和筛法一样的思路:即根据数字大小枚举每个数,那些尚未被访问的数显然都是 SelfNumber,而通过这些数求出的 d ( n ) d\left(n\right) d(n) 则需要被标记为已被访问(即不是 SelfNumber)。最终扫描结束时,所有标记为未被访问的,就是题目所说的 SelfNumber。
基于这样的思路,可写出求解本题的完整代码:
/*
MT2216 数的自我
*/
#include<bits/stdc++.h>
using namespace std;
const int MAX = 1e7+5;
bool vis[MAX];
int selfNumbers[MAX], cnt;
// 函数D(n)
int D(int n)
{
int ans = n;
while(n){
ans += n%10;
n /= 10;
}
return ans;
}
// 通过筛法求出 selfNumber
int getSelfNumbers(int n)
{
int Dn;
for(int i=1; i<=n; i++){
Dn = D(i);
if(Dn <= n) vis[Dn] = true;
if(!vis[i]) selfNumbers[cnt++] = i;
}
return cnt;
}
int main()
{
// 获取输入
int n, k; cin>>n>>k;
// 输出 selfNumber 的数量
cout<<getSelfNumbers(n)<<endl;
// 输出对应的 selfNumber
while(k--){
cin>>n;
cout<<selfNumbers[n-1]<<" ";
}
return 0;
}
MT2217 数字游戏
难度:黄金 时间限制:1秒 占用内存:128M
题目描述
小码哥和小码妹正在玩一个小游戏,小码哥先展示一个正整数 n n n,如果小码妹可以写出 k k k 个正整数 x 1 , … , x k x_1,\ldots,x_k x1,…,xk。满足 ∏ i = 1 k ( x i + 1 ) \prod_{i=1}^{k}\left(x_i+1\right) ∏i=1k(xi+1),则她可以得到 k k k 分。小码妹的数学并不好,所以请你写一个程序帮忙计算她最多可以得到多少分。
格式
输入格式:一行一个正整数 n ∈ [ 2 , 1 × 10 8 ] n\in\left[2,1\times{10}^8\right] n∈[2,1×108]。
输出格式:一行一个正整数。样例 1
输入:
12输出:
3
相关知识点:
分解质因数
题解
注意到题目给出的要求是 n = ∏ i = 1 k ( x i + 1 ) n=\prod_{i=1}^{k}\left(x_i+1\right) n=∏i=1k(xi+1) ,而 x i x_i xi 为正整数,因此这就等价于要求等式右侧各项的最低值为 2。我们知道,任何一个数都可以表达为若干个数相乘的形式,例如:
24 = 2 3 × 3 1 24=2^3\times3^1 24=23×31
所以对 24 而言,它有 8×3、2×2×6、2×2×2×3 等乘积形式。而本题的要求是,对任意给定数,让你求出其最长的乘积形式(该长度即为本题要求的得分)。例如对 24 而言,其最大得分为 4,即 24=2×2×2×3。实际上,我们要做的就是分解质因数。因为要让等式的长度最长,其中的各乘数就应该尽可能小,而最小的因数,当然就是质因数。
前面已经说过如何求一个数的质因数(短除法求一个数的质因数),在此就不再赘述,下面直接给出求解本题的完整代码:
/*
MT2217 数字游戏
要想得分最大,那叠乘的各项应尽可能小,其实就是要算质因数
*/
#include<bits/stdc++.h>
using namespace std;
// 通过短除法获取一个数的质因数个数
int getPrimeFactors(int n){
int ans = 0;
for(int i=2; i*i<=n; i++){
// 当前数为数 n 的因数时,要不断用该数进行分解
while(n%i==0){
// 统计个数
ans++;
n /= i;
}
}
// 如果分解得到的最后结果不为 1 ,则最终状态的 n 也是原数的因数
if(n!=1) ans++;
return ans;
}
int main()
{
// 获取输入
int n; cin>>n;
// 输出最大得分
cout<<getPrimeFactors(n)<<endl;
return 0;
}