AIGC标识 质数拼接好数:基于我的代码的完整题解

日期:2026-09-22
说明:本文所有变量名、代码结构和解释都按照 未命名1.cpp 中的实现展开,只修复影响正确性的边界问题。

一、题目核心

\(A\) 和 \(B\) 都是质数,将它们的十进制表示直接拼接,得到:

\[x=A\mid B \]

如果 \(x\le n\),那么 \(x\) 是一个“好数”。

题目要求求不同好数的数量。

需要注意:

同一个好数可以由多种质数拆分方式得到,但只能统计一次。

例如:

\[237=2\mid37 \]

同时又有:

\[237=23\mid7 \]

虽然有两种合法拆分,但 237 只能算一次。

所以这道题需要:

枚举所有合法的 A、B 质数对
+ 对拼接结果 x 去重

二、我代码中的整体结构

我的代码主要分成四个部分:

  1. 筛出不超过 \(10^7\) 的质数,保存在数组 p 中;
  2. 枚举 B 的位数,计算 H、d;
  3. 枚举质数 A=p[j],计算 B 的合法上界 U;
  4. 用位图 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\times p[j] \]

标记为合数。

因为 \(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\) 的位置。

例如当前要枚举的是:

\[B\in[100,999] \]

那么执行:

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];

等价于:

\[num=A\times10^D+B \]

例如:

A = 23
B = 7
D = 1
d = 10

则:

\[num=23\times10+7=237 \]


六、为什么计算 U

代码中是:

int U = min(d, n - p[j] * d);

这个表达式有两层限制。

1. \(B\) 必须小于 \(10^D\)

因为 \(B\) 有 D 位,所以:

\[B<10^D \]

代码中原本想写 \(d-1\),但你的代码使用了:

min(d, n - p[j] * d)

严格来说,这里写成 d - 1 更直观。但由于 d = 10^D 一定是偶数、不是质数,所以即使上界放宽到 d,也不会真正枚举到 B = d,不会影响答案。

2. 拼接结果不能超过 \(n\)

因为:

\[A\times10^D+B\le n \]

所以:

\[B\le n-A\times10^D \]

对应代码:

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\) 增大时:

\[U=n-A\times10^D \]

会不断减小。

一旦:

\[U<H \]

说明即使是 \(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:

\[块号=\left\lfloor\frac{num}{64}\right\rfloor \]

\[块内偏移=num\bmod64 \]

因为 \(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)

九、为什么时间复杂度够

这段代码看起来有两层循环,但并不是枚举所有质数笛卡尔积。

质数表大小约为:

\[\pi(10^7)\approx664579 \]

如果真的写:

for (int A : p) {
    for (int B : p) {
        // ...
    }
}

需要:

\[664579^2\approx4.4\times10^{11} \]

次操作,肯定不够。

但实际上,随着 A 增大,U 快速减少,循环会提前 break。

不同 B 位数下,枚举 A 的次数约为:

D,即 B 的位数 枚举 A 的次数
1 位 664579
2 位 78498
3 位 9592
4 位 1229
5 位 168
6 位 25
7 位 4

合计约为:

\[S\approx754095 \]

所以 find(H) 只会调用大约 75 万次。

每次 find 是二分查找:

\[O(\log m)\approx O(\log664579)\approx20 \]

总比较次数约为:

\[754095\times20\approx1.5\times10^7 \]

真正枚举出来的合法质数对数量为:

\[R=12454357 \]

因此内层 while (x <= m && p[x] <= U) 的总循环次数约为 1245 万次,而不是几百亿次。

整个程序的复杂度大致为:

\[O(V\log\log V+S\log m+R) \]

其中:

  • \(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

十三、总结

按照我的代码结构,这道题可以概括为:

  1. 筛出 \(10^7\) 以内的质数,保存在 p[1..m];
  2. 枚举 B 的位数 D;
  3. 根据 D 计算 H 和 d;
  4. 枚举质数 A=p[j],算出 B 的合法上界 U;
  5. 用 find(H) 找到第一个可能的质数 B;
  6. 枚举 [H,U] 中所有质数;
  7. 对拼接结果 num 使用位图去重;
  8. 如果是第一次出现,就让 ans++。

最关键的两个细节是:

if ((v[k] & msk) == 0)

以及:

while (x <= m && p[x] <= U)

修正这两个地方后,代码就能正确处理最大输入,并输出:

11097823
posted @ 2026-10-12 22:02  k_stefani  阅读(13)  评论(0)    收藏  举报