2022年蓝桥杯国赛A组真题——选素数

今天不讲知识点,给大家分享一道蓝桥杯数论的真题——选素数。个人觉得是一道非常经典的,同时通过这个解题过程相信可以加深我们一些关于算法优化的理解,小伙伴们在看博主解题可以尝试自己先做一下。题目链接:https://www.lanqiao.cn/problems/2179/learning/

注:以下讲解是纯白话讲解,纯C语言解题,分层次进行分析,哪里卡脖看哪里,小白也能学,安全易吸收!

第一步:题目解析:

注意:对于60%测试用例:1 <= n <= 5000

           对于所有测试用例:1 <= n <= 10^6

样例解读:

对于x=8先取质数p=7,得到14;再取质数p=11,得到22。经检验,8是这个情况下的最小x的取值。

第二步:解题分析与算法原理:

第一层分析:

这个题目的大概意思就是给我们一个整数n,这个整数n是经过两次取质数并累加x得到的,让我们去逆推这个x所有的可能取值中最小的那一种取值。

这个过程隐含了一个条件:n可不可能是质数?显然n必须是合数。因为如果n是质数,则对于任意小于n的质数p,n%p == 0始终不成立。而题意告诉我们的是,n必须是一个小于它的质数的倍数。有老铁说:1可不可以呢,啊注意,1既不是质数也不是合数。所以什么时候会无解呢,显然n是质数的时候,这个题是无解的。

第二层分析:

很明显,从x到n的这个过程不是一蹴而就的,当中有一个中间值,我们不妨假设一下这个中间值是m。即这个过程可以用下面这段表达式来进行表示:x→m→n.

第二点,我们不妨先从正推的角度先来分析一下这里的问题:从x开始,先取小于x的质数p。p是任意一个小于x的质数,然x的取值不可知。我们不妨以p及其倍数来划分实整数区间,对x的所有可能取值进行分类讨论:

情况一:

x位于区间(p,2p]之间,若x != 2p,则m = 2p,m-p = p < x;

                                          若x == 2p,则x = 2p,m-p = p < x;

情况二:

x位于区间(2p,3p]之间,若x != 3p,则m=3p,m-p = 2p < x;

                                            若x == 3p,则m=3p,m-p=2p < x;

相信你已经看到了:m不小于x,且由于m是p的倍数,所以m-p仍然是p的倍数,且是小于x的最大的p的倍数。所以x至少得是m-p+1。因此我们可以得到m-p = kp(k是整数,k=1, 2, 3, 4, 5)且m - p + 1 <= x。

第三层分析:

这些条件有什么用呢?诶,你会发现对于给定的数n,它的初始值m的最小值可由上述条件推得为n - p + 1。其中p是n的因子,且p是质数。即p是n的质因子。

另外你会发现,对于给定的数n,n+1是常数,如果p越大则n+1-p的值就越小,得到的m的值就越小。

所以这一题寻找最小x的过程,就是不断得找某一个数的最大质因子的过程。

第四层分析:

另外如果n是通过一次这样的过程得到的话,你分析逆推到这里,好像也就可以了,但是问题就是n是逆推两次得到的结果。以n = 22为例,22的最大质因子是11,这个情况下m的最小值就是22-11+1=12。如果m=12,12的最大质因子是3,那同理可得x的最小值是12-3+1=10。

10是x的最小可能取值吗?显然样例输出已经告诉了我们x的最小值是8,这个值是在m取14的时候得到的,因此这也告诉我们m的值越小,推出来的x的值未必是最小的那个。但是由于从m逆推到x的这个过程当中,是没有规律可供推导的,因此这一部分的逆推只能针对m的所有可能取值,进行一一枚举,最后取最小的x的值。

最后一个问题,作为中间值的m可不可能为质数,显然m也是不可能为质数的,m至少也要有一个小于它的质数因子。

第三步:C语言源代码与算法优化:

我们的讲解面向大众,因此选择最为大众的语言——C语言,进行源代码的书写:

经过我们上述的分析,我们知道我们要找一个数的最大质因子,这个过程我们需要一个判断两个数谁大的函数Max。另外我们还要枚举m的所有可能取值,找到里面最小的x的取值,这个过程需要我们提供一个判断两个数谁小的函数Min。这些其实C++上都提供了的,但是C语言没有,你得自己写,但是这两个代码也不难,所以不单独进行分析。

其二,我们需要一个用于判断某个数是不是质数的函数。因为要找最大质因子嘛,那么就不免要判断质数。所谓质数就是除了1和它本身以外,没有其他因子的数。基于此我们可以写出下面这个程序:

//判断一个数是不是素数:
bool PrimeNum(int a)
{
    bool ret = true;
    for (int i = 2; i < a; i++)
    {
        if (a % i == 0)
        {
            ret = false;
            break;
        }
    }
    return ret;
}

但是这个就是最优解吗?显然你实际上是不需要遍历到a-1的,因为任意一个数x的因子总是成对出现的。而且你去多验证看一下,你会发现这其中一个x的因子会小于✓x,另一个因子则会大于✓x,不可能两个因子同时大于✓x。这里的极端情况就是两个因子相等了,那这个时候,就算其中的某一个因子大了那怕那么一点点,就会导致整体的乘积超过x。

我说上面那段话的意思是:你实际遍历的区间在[2,✓a]之间。值得注意的是,我两边用的都是闭区间,因此上述代码可以优化为下面这个版本:

//判断一个数是不是素数:
bool PrimeNum(int a)
{
    bool ret = true;
    for (int i = 2; i <= (int)sqrt(a); i++)
    {
        if (a % i == 0)
        {
            ret = false;
            break;
        }
    }
    return ret;
}

当然了,你要用C语言的开方函数sqrt,要用到头文件#include<math.h>,这里不再赘述。

那我判断好质数了,那至少找质因子没问题了。但是实际问题需要我们找最大质因子啊,这怎么办?那好!我们有一个老铁就率先发言表态说:我们可以从这个数的n-1的开始,往下遍历,找到的第一个能整除n的质数,就是它的最大质因子。考虑到一个数,只要它本身不是质数,那它的最大质因子不会超过n/2,所以我还优化一手,把初始值从n-1改成n/2。于是据此写出下面这个函数:

//找最大质因子:
int FindPrime(int n)
{
    int i = 0;
    for (i = n / 2; i > 1; i--)
    {
        if (PrimeNum(i) && n % i == 0)
        {
            break;
        }
    }
    return i;
}

这可不可以,这很可以,首先它确实可以找到一个合数的最大质因子。虽然找不到质数的。但是问题大不大在这里,因为我们已经说了,无论是m还是n只要是质数就没有必要找它的最大质因子了。这是第一。第二而且这个代码还有优化,所以说很可以是没有问题。

但是这是不是最优解,好,这里直接说结论,这还不是最优解,为什么。你别忘了,对于60%的测试用例1 <= n <=5000,就是n非常大,就以5000,先对5000进行质因数分解:5000=2 x 2 x 2 x 5 x 5 x 5 x 5,它的最大质因子是5,这个时候你再用上面那个方法好不好,不好,即便你做了优化,那从2500开始往下走,走到5,要走2400多次去了。效率不高。

但是我们人算,通过对5000进行分解是不是很方便就找到了,为什么会很方便呢?其实这个地方运用了数论里一个理论是说:任意一个合数可以被拆分为有限个质数的乘积。我们把这段话告诉给我们的计算机听,就是我们可以不断对我们要操作的数,从小到大开始进行质因数分解。最后留下来的数就是这个数的最大质因数。

据此,我们可以写出下面这个代码:

//找最大质因子:
int FindPrime(int n)
{
    int ret = 0;
    int i = 2;
    while (n > 1)
    {
        if (n % i != 0)
        {
            i++;
        }
        else
        {
            n /= i;
            ret = Max(i, ret);
        }
    }
    return ret;
}

这个代码从性能和检索范围都优于前面那个代码,性能就毋庸置疑了,检索范围上,你会发现对于一个质数,它也可以准确地找到这个数地最大质因数,就是这个数本身。

所有的准备工作做完,整合好的C语言代码就是下面这个啦!

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include<stdbool.h>
#include<math.h>

//判断一个数是不是素数:
bool PrimeNum(int a)
{
    bool ret = true;
    for (int i = 2; i <= (int)sqrt(a); i++)
    {
        if (a % i == 0)
        {
            ret = false;
            break;
        }
    }
    return ret;
}

//选两个数中较大的数
int Max(int a, int b)
{
    int max = a;
    if (a < b)
    {
        max = b;
    }
    return max;
}

//找最大质因子:
int FindPrime(int n)
{
    int ret = 0;
    int i = 2;
    while (n > 1)
    {
        if (n % i != 0)
        {
            i++;
        }
        else
        {
            n /= i;
            ret = Max(i, ret);
        }
    }
    return ret;
}

//选两个数中的较小的数:
int Min(int a, int b)
{
    int min = a;
    if (a > b)
    {
        min = b;
    }
    return min;
}

int main(int argc, char* argv[])
{
    int n = 0;
    int ret = 0;//将要返回的结果
    int pos = 0;//记录n的最大质因子
    scanf("%d", &n);
    if (PrimeNum(n))
    {
        ret = -1;
    }
    else
    {
        pos = FindPrime(n);
        for (int j = n - pos + 1; j <= n; j++)
        {
            if (PrimeNum(j))
            {
                continue;
            }
            else
            {
                int max = FindPrime(j);
                if (ret == 0)
                {
                    ret = j - max + 1;
                }
                else
                {
                    ret = Min(j - max + 1, ret);
                }
            }
        }
    }
    printf("%d", ret);
    return 0;
}

  • 30
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值