7.2 KiB
一、题目描述
给定两个整数 L
和 U
,你需要在闭区间 [L,U]
内找到距离最接近的两个相邻质数 C_1
和 C_2
(即 C_2−C_1
是最小的),如果存在相同距离的其他相邻质数对,则输出第一对。
同时,你还需要找到距离最远的两个相邻质数 D_1
和 D_2
(即 D_1−D_2
是最大的),如果存在相同距离的其他相邻质数对,则输出第一对。
输入格式
每行输入两个整数 L
和 U
,其中 L
和 U
的差值不会超过 10^6
。
输出格式
对于每个 L
和 U
,输出一个结果,结果占一行。
结果包括距离最近的相邻质数对和距离最远的相邻质数对。(具体格式参照样例)
如果 L
和 U
之间不存在质数对,则输出 There are no adjacent primes.。
数据范围
1≤L<U≤2^{31}−1
输入样例
2 17
14 17
输出样例
2,3 are closest, 7,11 are most distant.
There are no adjacent primes.
二、二次筛法
这里的区间范围给定的最大值是2^{31} - 1
,而用线性筛法求的是[1,n]
中的所有质数,因此直接用线性筛法求肯定会直接挂掉,因此需要通过挖掘某些性质,才能完成。
1.挖掘性质
-
性质
1
若一个数n
是一个合数,必然存在2
个因子d
,\large \frac{n}{d}
,假设d <=\frac{n}{d}
,则d <= \sqrt{n}
,因此必然存在一个小于等于\sqrt{n}
的因子 -
性质
2
若x∈[L,R]
,且x
是合数,则一定存在P <= \sqrt{2^{31}-1} (< 50000)
,使得P
能整除x
,其中P < x
。\sqrt{2147483647}=46340
,这个值是小于50000
的,所以,yxc
老师一般喜欢使用直接写上上限值50000
,就是因为这个数字够用,而且够大,好记。
2.实现步骤
- 找出
1 \sim \sqrt{2^{31}-1}
(< 50000)
中的所有质因子 - 对于
1 \sim 50000
中每个质数P
,将[L,R]
中所有P
的倍数筛掉(至少2
倍,1
倍的就是自己,是质数不是合数) 找到大于等于L
的最小的P
的倍数P_0
,找下一个倍数时只需要+= P
即可
3.引理
在寻找大于L
的第一个P
的倍数时,采用了下面的引理:
引理(分数的上取整转换下取整)

4.细节
Q
: 每个质数是2\sim 50000
中的数,为啥 LL p = primes[i]
; 这里的p
的LL
啊, int
不就够了吗?
A
: LL p = primes[i]
,这里p
用LL
是因为如果p
也是用int
类型,本身l
也是用int
类型,如果l
取得足够大,下面的l + p - 1
会有可能直接爆int
变成负数
Q
: for (LL j = max(p * 2, (l + p - 1) / p * p); j <= r; j += p)
这里的 j
是小于等于 r
的,而 r
的取值范围是小于2 ^ {31}
,也是在int
范围内啊, 这里为啥用LL
啊?
A
: 这里的 j
是小于等于 r
的,而 r
的取值范围是小于2 ^ {31}
,这里确实是这样,可是这个循环跳出的条件是j <= r
,也就是说如果r
是最大的int
,那么当j += p
,要超过最大的int
的时候需要比它还大才能跳出循环,因此直接爆int
变成负数,然后j <= r
依然成立,会一直死循环下去。其实本质上这个问题与问题1
是一回事,在做数论时一次要小心LL
和int
的加法、乘法,小心爆int
是一条死规则,尽量多想想是不是应该用LL
来设置变量~
Q
: 为什么要写上常数50000
呢?
A
:
- 写法
1
:get_primes(50000)
- 写法
2
:get_primes(sqrt(r))
- 写法
3
:get_primes(sqrt(INT32_MAX))
其实都行,但有了经验后,发现,直接写50000
最简单粗暴效果好~
Q
: 为什么要使用偏移量st[j - l] = true
?
A
: 因为数据范围太大,直接用数组进行桶计数,会超空间,需要离散化一下,就是把1e6
范围内的数据映射到0\sim 1e6
Q
:为什么for (int i = 1; i < cl; i++)
这里要用cl-1
呢?为什么不是cl
呢?
A
:cl
个质数,下标是[1,cl]
,因为现在枚举的是前一个质数,要保证还有后一个,所以取不到cl
5.时间复杂度
O(n)
三、实现代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
// 线性筛
const int N = 1e6 + 10;
int primes[N], cnt;
bool st[N];
void get_primes(int n) {
memset(primes, 0, sizeof primes);
memset(st, 0, sizeof st);
cnt = 0;
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[cnt++] = i;
for (int j = 0; primes[j] * i <= n; j++) {
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
bool b[N]; // 记录区间内有哪些数是合数
int c[N], cl; // 区间内有哪些质数
int l, r; // 区间范围
int main() {
// 加快读入
ios::sync_with_stdio(false), cin.tie(0);
// 一次性筛质数
get_primes(50000);
while (cin >> l >> r) {
memset(b, 0, sizeof b); // 清除合数数组
memset(c, 0, sizeof c); // 清除质数数组
cl = 0;
// 标识质数的倍数,也就是合数有哪些
for (int i = 0; i < cnt; i++) {
LL p = primes[i];
for (LL j = max(p * 2, (l + p - 1) / p * p); j <= r; j += p) // 枚举p的2倍以上倍数,最小要比l大,最大不能超过r
b[j - l] = true; // 采用偏移量记录剔除标识
}
// Hack data: 1 100
// 输出: 1,2 are closest, 89,97 are most distant.
// 答案: 2,3 are closest, 89,97 are most distant.
// 原因: 下面的逻辑判断b[i]是否等于0,表示i+l不是某个质数的倍数(合数),也就是是合数,但这样判断是有问题的,因为考虑边界问题
// 1通过上面的代码是无法剔除掉的,也就是说,上面的剔除的是合数,但数字除了合数,可不是只有质数,还有一个特殊的数字1,1即不是质
// 数也不是合数,这句话可不是说说而已。
for (int i = 0; i <= r - l; i++)
if (!b[i] && i + l > 1) // 如果不是合数,并且,不是1,才是质数
c[++cl] = i + l; // 区间内的质数多了一个i+l质数
if (cl < 2)
puts("There are no adjacent primes.");
else {
int mi = 1, mx = 1; // 第一只猴子就是大王
for (int i = 1; i < cl; i++) { // 范围是1~cl,而此处因为要枚举的是每个当前i与后一个的关系,所以需要枚举到cl-1
int d = c[i + 1] - c[i];
if (d < c[mi + 1] - c[mi]) mi = i;
if (d > c[mx + 1] - c[mx]) mx = i;
}
printf("%d,%d are closest, %d,%d are most distant.\n", c[mi], c[mi + 1], c[mx], c[mx + 1]);
}
}
return 0;
}