【马蹄集】—— 数论专题:筛法

数论专题



目录

  • 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 1n108


相关知识点:筛法


题解


题目的要求很简单,本质就是求指定区间内的质数。最常规的做法就是暴力枚举:对区间 [ 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 1n108 的数据范围下必定超时。同时,从数据范围看,实际上已经暗示了必须在线性时间复杂度内完成求解。因此,不得不用到筛法。筛法的主要思想是:对已经找出的质数,直接将其倍数从接下来的查找中筛出,而不必再去判断那些数是否为质数,从而节省时间。根据筛法的停止策略,主要可将其分为两类:

  • 埃式筛法;
  • 欧拉筛法。

下面分别对其进行分析。


埃拉托斯色尼筛选法

若要得到自然数 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 开始)):

  1. 对当前数 i i i,如果其尚未被访问,则其必为质数,故将其加入质数队列;
  2. 枚举已记录的质数:
    • 将当前质数与 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张图片

从 “选出的质数” 和 “标记被访问(不是质数)” 两列看,欧拉筛法处理的数字并没有出现任何重复。这也就是说,对 [ 1 , n ] \left[1,n\right] [1,n] 内的任何数,欧拉筛法都只会判断一次,因此也称欧拉筛法为线性筛法。

回到本题,可基于欧拉筛法求出 [ 1 , n ] \left[1,n\right] [1,n] 内的全部质数,并将其与区间长度求商即可,下面给出完整代码:

/*
    MT2213 质数率
    欧拉筛法
*/
#include 
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 1kn104


相关知识点:筛法


题解


方法二:暴力求解

本题实际就是求 “具有指定差距 k k k 的质数对”,在 10 4 {10}^4 104 范围下如果按暴力方式逐个求解,也是可以通过的:

/*
    MT2214 元素共鸣 
    暴力枚举
*/
#include 
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 
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
题目描述

小码哥不喜欢以下情况的数:

  1. 是7的倍数(包括7);
  2. 数字的某一位是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 1T2×105, 0x107


相关知识点:筛法


题解


小码哥不喜欢含有数字 7 的数(以及这些数的倍数)。对于数字是否含有 7,可通过循环取余的方式得到。对于这些数的倍数,则可通过类似于埃氏筛法的方式进行筛选。对于 “下一个他喜欢的数”,可通过在筛选数的过程中,构建一个 next 数组来完成(即每次都将一个小码哥喜欢的数作为索引,保存找到的下一个他喜欢的数)。

下面直接给出基于以上思路得到的完整代码:

/*
	MT2215 小码哥的喜欢数 
*/
#include 
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=100sk 可以为 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 1N107, 1K5000


相关知识点:筛法


题解


根据题目的意思可知,函数 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 
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 
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;
} 

END


你可能感兴趣的:(马蹄集试题题解,MT2213,质数率,MT2214,元素共鸣,MT2215,小码哥的喜欢数,MT2216,数的自我,MT2217,数字游戏,马蹄集试题题解,筛法)