洛谷题单指南-暴力枚举-P1217 [USACO1.5] 回文质数 Prime Palindromes

原题链接:https://www.luogu.com.cn/problem/P1217

题意解读:

本题要找[a, b]范围内的所有回文质数,千万不要被题目提示所干扰,如果按照提示先产生各个长度的回文数,再依次判断是否是素数,程序写起来比较繁琐,需要根据a、b的长度,写8个判断是否产生1~8位回文数,最后做素数判断。

换一个思路,先用素数筛法筛出1~最大范围的素数,再去判断每个素数是否是回文数即可。

解题思路:

由于5a<b100,000,000,因此要筛出108以内的素数,我们知道素数筛算法有三种(后面详解):朴素筛、埃氏筛、线性筛,

朴素筛时间复杂度O(n*logn),埃氏筛时间复杂度O(n*loglogn),线性筛时间复杂度O(n)

对于108数据量,只能用线性筛,而线性筛中涉及两个数组,一个用来保存所有素数,一个用来标记是否是合数,两个数组大小都要开到108,内存限制125M,长度为108的int数组占用空间约为400M,显然不够用。而且108即使用线性筛也基本到了时间复杂度的极限,还要判断回文等操作,有可能超时。

对于回文数,有一个很重要的特点,如果这个数有偶数位,那么一定能被11整除,肯定不是素数,因此,数据最大范围虽然是108,但是8位数肯定不是素数,所以数据最大范围可以缩小为107。对于107数据筛素数,埃氏筛、线性筛都能很好的胜任。即使要用提示中产生回文数的方法,也需要避开4/6/8位回文数的生成,避免超时。

下面重点介绍几个关键知识点:

1、素数筛

所谓素数(质数),是指除了1和它本身以外不再有其他因数的自然数,一般用试除法判断素数(时间复杂度:O(sqrt(n))):

bool isprime(int x)
{
    if(x <= 1) return false;
    for(int i = 2; i * i <= x; i++)
    {
        if(x % i == 0) return false;
    }
    return true;
}

注意:如果i比较大,i * i <= x有溢出风险,写成i <= x / i比较安全。

所谓素数筛,就是在一定范围内,筛出所有的素数。

例如要筛出1~n中所有的素数,如果枚举每一个数去判断是否是素数,时间复杂度为O(n*sqrt(n)),当n=106都无法应对,因此出现了素数筛算法。

素数筛算法的核心思想是通过遍历2~n,将每个数的若干倍数都标记为合数,这样剩下的就都是素数,根据优化程度不同有三种算法:

a、朴素筛法:时间复杂度O(n*logn)

对于2~n中每一个数,如果没有标记成合数,则保存为素数,再将该数的2倍、3倍、4倍...标记为合数,代码如下:

#include <bits/stdc++.h>
using namespace std;

const int N = 1e7;

int primes[N], cnt = 1; //保存筛出的素数
bool flag[N]; //标记合数

//朴素筛,筛出1 ~ N之间的素数,复杂度O(nlogn)
void getprimes1()
{
    for(int i = 2; i <= N; i++)
    {
        if(!flag[i]) primes[cnt++] = i; //如果i未被标记合数,则保存i为素数
        for(int j = i + i; j <= N; j += i) //把i的倍数都标记为合数
            flag[j] = true;
    }
}

int main()
{
    getprimes1(); 
    return 0;
}

b、埃氏筛法:时间复杂度O(n*loglogn)

由于在标记合数时,无论当前i是素数还是合数,都将其倍数进行了标记,这样必然导致大量重复标记。

例如:i=2时,i*4=8;而i=4时,又有i*2=8;8被重复标记

埃氏筛就是在朴素筛的基础上做出优化,只将素数的倍数进行标记,合数的倍数不标记,这样可以一定程度减少重复标记,代码如下:

#include <bits/stdc++.h>
using namespace std;

const int N = 1e7;

int primes[N], cnt = 1; //保存筛出的素数
bool flag[N]; //标记合数

//埃氏筛,筛出1 ~ N之间的素数,复杂度O(nloglogn)
void getprimes2()
{
    for(int i = 2; i <= N; i++)
    {
        if(!flag[i]) //如果没有被标记为合数
        {
            primes[cnt++] = i; //保存素数
            for(int j = i + i; j <= N; j += i) //只对素数的倍数进行标记
                flag[j] = true;
        }
    }
}

int main()
{
    getprimes2(); 
    return 0;
}

c、线性筛法:时间复杂度O(n)

尽管埃氏筛只针对素数的倍数进行标记,但是还是会有一定程度的重复标记。

例如:i=2时,2*3=6;i=3时,i*2=6;6被重复标记。

线性筛在埃氏筛的基础上又进一步优化,使得每个合数只被它最小的素因子标记,这样每个合数都只被标记一次,如何做到呢?

#include <bits/stdc++.h>
using namespace std;

const int N = 1e7;

int primes[N], cnt = 1; //保存筛出的素数
bool flag[N]; //标记合数

//线性筛,筛出1 ~ N之间的素数,复杂度O(n)
void getprimes3()
{
    for(int i = 2; i <= N; i++)
    {
        if(!flag[i]) primes[cnt++] = i; //如果i未标记为合数,则保存为素数
        for(int j = 1; j <= cnt; j++) //从小到大遍历每一个已保存的素数,要将i * primes[j]标记为合数
        {
            if(i * primes[j] > N) break; //超出最大值
            flag[i * primes[j]] = true; //将i * primes[j]标记为合数,primes[j]必然是i * primes[j]的最小素因子
            if(i % primes[j] == 0) break; //当i中包含一个primes[j]因子,就不能用此i去标记下一个数i * primes[j+1],因为i * primes[j+1]包含因子primes[j],而primes[j+1]不是最小素因子
        }
    }
}

int main()
{
    getprimes3(); 
    return 0;
}

此题任选埃氏筛或者线性筛算法都可以,由于埃氏筛更简单,这里采用埃氏筛。

2、判断回文数

只需要将数字进行翻转,比较翻转后的数字和原来的数字,如果相等则是回文数。

//判断x是否是回文数
bool ispalindrome(int x)
{
    int y = 0; //y是将x各个数字翻转后的数
    int t = x; //先将x复制一份
    while(t)
    {
        y = 10 * y + t % 10;
        t /= 10;
    }
    if(x == y) return true;
    else return false;
}

最后给出完整实现:

100分代码:

#include <bits/stdc++.h>
using namespace std;

const int N = 1e7;

int primes[N], cnt = 1; //保存筛出的素数
bool flag[N]; //标记非素数
int a, b;

//埃氏筛,筛出1 ~ N之间的素数,复杂度O(nloglogn)
void getprimes2()
{
    for(int i = 2; i <= N; i++)
    {
        if(!flag[i]) //如果没有被标记为合数
        {
            primes[cnt++] = i; //保存素数
            for(int j = i + i; j <= N; j += i) //只对素数的倍数进行标记
                flag[j] = true;
        }
    }
}

//判断x是否是回文数
bool ispalindrome(int x)
{
    int y = 0; //y是将x各个数字翻转后的数
    int t = x; //先将x复制一份
    while(t)
    {
        y = 10 * y + t % 10;
        t /= 10;
    }
    if(x == y) return true;
    else return false;
}

int main()
{
    getprimes2(); //getprimes3();
    cin >> a >> b;

    for(int i = 1; i <= cnt; i++)
    {
        if(primes[i] >= a && primes[i] <= b && ispalindrome(primes[i]))
            cout << primes[i] << endl;
    }

    return 0;
}

 

posted @ 2024-02-01 16:16  五月江城  阅读(108)  评论(0编辑  收藏  举报