质数拼接好数:基于我的代码的完整题解
日期:2026-09-22
说明:本文所有变量名、代码结构和解释都按照未命名1.cpp中的实现展开,只修复影响正确性的边界问题。
一、题目核心
\(A\) 和 \(B\) 都是质数,将它们的十进制表示直接拼接,得到:
如果 \(x\le n\),那么 \(x\) 是一个“好数”。
题目要求求不同好数的数量。
需要注意:
同一个好数可以由多种质数拆分方式得到,但只能统计一次。
例如:
同时又有:
虽然有两种合法拆分,但 237 只能算一次。
所以这道题需要:
枚举所有合法的 A、B 质数对
+ 对拼接结果 x 去重
二、我代码中的整体结构
我的代码主要分成四个部分:
- 筛出不超过 \(10^7\) 的质数,保存在数组
p中; - 枚举
B的位数,计算H、d; - 枚举质数
A=p[j],计算B的合法上界U; - 用位图
v对拼接结果num去重。
完整修正后的代码如下:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<vector>
#include<cstdint>
#define MAXN 10000000
using namespace std;
int n, p[10000005], m, ans;
bool vis[10000005];
int find(int x) {
int L = 1, R = m;
while (L < R) {
int mid = (L + R) / 2;
if (p[mid] == x) {
return mid;
} else if (p[mid] < x) {
L = mid + 1;
} else {
R = mid;
}
}
return R;
}
int main() {
scanf("%d", &n);
// 筛质数
for (int i = 2; i <= min(MAXN, n); i++) {
if (!vis[i]) {
p[++m] = i;
}
for (int j = 1;
i * p[j] <= min(MAXN, n) && j <= m;
j++) {
vis[i * p[j]] = 1;
}
}
// 位图:v[x >> 6] 的第 x & 63 位表示 x 是否出现
vector<uint64_t> v((n >> 6) + 1, 0);
// D 表示 B 的位数
for (int D = 1; D <= 7; D++) {
int H = 1;
for (int i = 1; i < D; i++) {
H *= 10;
}
// d = 10^D,用于把 A 拼接到 B 前面
int d = H * 10;
if (H > n) {
break;
}
// A = p[j]
for (int j = 1; j <= m; j++) {
// B 的最大合法值
int U = min(d, n - p[j] * d);
if (H > U) {
break;
}
// 找到第一个大于等于 H 的质数 B
int x = find(H);
if (p[x] > U || p[x] < H) {
break;
}
// 枚举 [H, U] 中的质数 B
while (x <= m && p[x] <= U) {
int num = p[j] * d + p[x];
int k = num >> 6;
int b = num & 63;
uint64_t msk = 1ULL << b;
if ((v[k] & msk) == 0) {
v[k] |= msk;
ans++;
}
x++;
}
}
}
printf("%d", ans);
return 0;
}
三、筛质数部分
我的筛法代码是:
for (int i = 2; i <= min(MAXN, n); i++) {
if (!vis[i]) {
p[++m] = i;
}
for (int j = 1;
i * p[j] <= min(MAXN, n) && j <= m;
j++) {
vis[i * p[j]] = 1;
}
}
1. vis[i] 表示什么
vis[i] == 1 表示数字 i 是合数。
如果:
if (!vis[i])
说明 i 没有被标记为合数,因此它是质数,加入质数表:
p[++m] = i;
这里 m 表示目前已经找到多少个质数,所以质数表是:
p[1], p[2], ..., p[m]
2. 为什么只用筛到 min(MAXN, n)
题目中 \(n\le10^8\),实际需要用到的质数不超过 \(10^7\)。
原因:
- 如果 \(B\) 至少有 8 位,那么 \(A\ge2\),拼接结果至少是 \(2\times10^8>10^8\);
- 如果 \(A\) 至少有 8 位,拼接结果同样会超过 \(10^8\)。
因此只需要:
min(MAXN, n)
以内的质数。
3. 内层循环在标记什么
vis[i * p[j]] = 1;
表示把:
标记为合数。
因为 \(i\ge2\),而 \(p[j]\) 是质数,所以它们的乘积一定有至少两个质因数,不是质数。
四、find() 函数的作用
我的代码里:
int find(int x) {
int L = 1, R = m;
while (L < R) {
int mid = (L + R) / 2;
if (p[mid] == x) {
return mid;
} else if (p[mid] < x) {
L = mid + 1;
} else {
R = mid;
}
}
return R;
}
这个函数的实际作用类似于:
lower_bound(p + 1, p + m + 1, x)
也就是:
返回第一个满足 \(p[x]\ge H\) 的位置。
例如当前要枚举的是:
那么执行:
int x = find(100);
就会找到质数表中第一个不小于 100 的质数 101。
后面的:
if (p[x] > U || p[x] < H) {
break;
}
用来判断当前区间里是否根本没有合法的质数 \(B\)。
五、拼接时各位变量的含义
外层循环:
for (int D = 1; D <= 7; D++)
其中 D 表示 \(B\) 的位数。
例如:
D = 1:B是一位质数;D = 2:B是两位质数;D = 3:B是三位质数。
接着计算:
int H = 1;
for (int i = 1; i < D; i++) {
H *= 10;
}
int d = H * 10;
含义如下:
| 变量 | 含义 | 例子 |
|---|---|---|
D |
\(B\) 的位数 | D=3 |
H |
\(D\) 位整数的最小值 | 100 |
d |
\(10^D\),用于左移 \(A\) | 1000 |
拼接计算为:
int num = p[j] * d + p[x];
等价于:
例如:
A = 23
B = 7
D = 1
d = 10
则:
六、为什么计算 U
代码中是:
int U = min(d, n - p[j] * d);
这个表达式有两层限制。
1. \(B\) 必须小于 \(10^D\)
因为 \(B\) 有 D 位,所以:
代码中原本想写 \(d-1\),但你的代码使用了:
min(d, n - p[j] * d)
严格来说,这里写成 d - 1 更直观。但由于 d = 10^D 一定是偶数、不是质数,所以即使上界放宽到 d,也不会真正枚举到 B = d,不会影响答案。
2. 拼接结果不能超过 \(n\)
因为:
所以:
对应代码:
B <= n - p[j] * d
于是 U 是当前 A 下,B 能取的最大值。
七、枚举 B 并提前退出
代码:
for (int j = 1; j <= m; j++) {
int U = min(d, n - p[j] * d);
if (H > U) {
break;
}
int x = find(H);
if (p[x] > U || p[x] < H) {
break;
}
while (x <= m && p[x] <= U) {
int num = p[j] * d + p[x];
// 去重
x++;
}
}
这里有两层“提前退出”。
1. H > U
当 \(A\) 增大时:
会不断减小。
一旦:
说明即使是 \(D\) 位整数里最小的数,也已经无法满足拼接结果不超过 \(n\)。
由于后面的 A 只会更大,所以可以直接:
break;
2. p[x] > U
如果第一个不小于 H 的质数已经大于 U,说明该区间内没有任何质数,也可以退出当前 \(A\) 的枚举。
八、位图去重
我的去重代码是:
int k = num >> 6;
int b = num & 63;
uint64_t msk = 1ULL << b;
if ((v[k] & msk) == 0) {
v[k] |= msk;
ans++;
}
1. v 是什么
vector<uint64_t> v((n >> 6) + 1, 0);
v 是一个位图。
每个 uint64_t 有 64 个 bit,可以记录 64 个数字是否出现过。
2. num 对应哪个 bit
对于数字 num:
因为 \(64=2^6\),所以:
int k = num >> 6; // num / 64
int b = num & 63; // num % 64
num 对应:
v[k] 的第 b 位
3. 掩码 msk
uint64_t msk = 1ULL << b;
例如:
b = 0 -> msk 的第 0 位为 1
b = 1 -> msk 的第 1 位为 1
b = 2 -> msk 的第 2 位为 1
4. 判断是否出现过
if ((v[k] & msk) == 0)
表示:
v[k]的第b位还是 0,也就是num以前没有出现过。
此时:
v[k] |= msk;
ans++;
v[k] |= msk:把第b位置成 1;ans++:这是一个新的好数,答案加一。
如果这一位已经是 1,就什么都不做,实现去重。
5. 为什么一定要加括号
错误写法:
if (v[k] & msk == 0)
会被解析为:
if (v[k] & (msk == 0))
因为 == 的优先级高于 &。
而 msk 不为 0:
msk == 0 -> false -> 0
v[k] & 0 -> 0
所以条件永远不成立,ans 永远加不上去。
正确写法是:
if ((v[k] & msk) == 0)
九、为什么时间复杂度够
这段代码看起来有两层循环,但并不是枚举所有质数笛卡尔积。
质数表大小约为:
如果真的写:
for (int A : p) {
for (int B : p) {
// ...
}
}
需要:
次操作,肯定不够。
但实际上,随着 A 增大,U 快速减少,循环会提前 break。
不同 B 位数下,枚举 A 的次数约为:
D,即 B 的位数 |
枚举 A 的次数 |
|---|---|
| 1 位 | 664579 |
| 2 位 | 78498 |
| 3 位 | 9592 |
| 4 位 | 1229 |
| 5 位 | 168 |
| 6 位 | 25 |
| 7 位 | 4 |
合计约为:
所以 find(H) 只会调用大约 75 万次。
每次 find 是二分查找:
总比较次数约为:
真正枚举出来的合法质数对数量为:
因此内层 while (x <= m && p[x] <= U) 的总循环次数约为 1245 万次,而不是几百亿次。
整个程序的复杂度大致为:
其中:
- \(V=10^7\):筛数范围;
- \(S\approx7.54\times10^5\):枚举 \(A\) 的次数;
- \(R\approx1.245\times10^7\):合法质数对数量。
每次位图去重都是固定的位运算,所以是 \(O(1)\)。
整体大约是几千万次操作,因此时间复杂度足够。
用同样的算法测试:
n = 100000000
开启 -O2 后,本机运行时间约为:
0.24 秒
十、你的代码中实际遇到的两个错误
1. 原来的条件没有给位运算加括号
错误:
if (v[k] & msk == 0)
正确:
if ((v[k] & msk) == 0)
这会导致所有新数字都统计不到,ans 一直是 0。
2. 枚举右端点写成了 < U
错误:
while (p[x] < U)
正确:
while (x <= m && p[x] <= U)
左手边要保证不越界,右手边要包含 B=U。
十一、Dev-C++ 编译 C++11
由于代码使用了:
#include <cstdint>
#include <vector>
uint64_t
需要在 Dev-C++ 中启用 C++11。
在:
工具 -> 编译选项 -> 编译器
找到“编译时加入以下命令”,添加:
-std=c++11
否则可能会出现:
This file requires compiler and library support for the ISO C++ 2011 standard.
十二、最大输入结果
当:
n = 100000000
代码输出:
11097823
其中合法质数有序对数量是:
12454357
经过位图去重后,得到:
11097823
十三、总结
按照我的代码结构,这道题可以概括为:
- 筛出 \(10^7\) 以内的质数,保存在
p[1..m]; - 枚举
B的位数D; - 根据
D计算H和d; - 枚举质数
A=p[j],算出B的合法上界U; - 用
find(H)找到第一个可能的质数B; - 枚举
[H,U]中所有质数; - 对拼接结果
num使用位图去重; - 如果是第一次出现,就让
ans++。
最关键的两个细节是:
if ((v[k] & msk) == 0)
以及:
while (x <= m && p[x] <= U)
修正这两个地方后,代码就能正确处理最大输入,并输出:
11097823

浙公网安备 33010602011771号