user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
数字和为质数
记D(n)为第n个各位数字和为质数的正整数。
例如,D(61) = 157,D(10^8) = 403539364。

求D(10^{16})。

我的思路:二分.
首先发现数字再大,数字各位和是极其有限的.
所以二分答案,对于这个答案计算<每个质数在答案范围内得到数字的贡献>,可以使用数位dp.

这是一个很麻烦的dp,首先不能有前导0,其次是枚举的数字和必须是枚举的质数,最后是枚举的数字要小于答案.

请给我c++代码实现,记得开int128,讲清楚数位dp部分是怎么写的(我不擅长数位dp)

gpt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
这是一个**典型但非常“硬”的数位 DP + 二分答案问题**,你的总体思路是**完全正确的**,而且已经站在“可解”的那一侧了 👍
下面我会:

1. **先把整体解法逻辑彻底理清**
2. **一步一步讲清楚数位 DP 怎么设计(不跳步,尽量白话)**
3. **给出一份完整、可直接跑的 C++(含 `__int128`)实现**

---

## 一、问题重新表述(方便统一语言)

* 定义:
( D(n) ) = 第 ( n ) 个 **各位数字和为质数** 的正整数(按从小到大排序)

* 要求:

[
D(10^{16})
]

---

## 二、整体解法(你思路的“标准版”)

### 1️⃣ 单调性 → 可以二分

定义函数:

[
f(x) = #{ 1 \le k \le x \mid \text{digit_sum}(k) \text{ 是质数} }
]

显然:

* ( f(x) ) 单调不减
* 我们要找最小的 ( x ),使得
[
f(x) \ge 10^{16}
]

➡ **标准二分答案**

---

### 2️⃣ 核心难点:如何算 ( f(x) )

这就是你说的那句:

> “对于这个答案计算 `<每个质数在答案范围内得到数字的贡献>`,可以使用数位 dp”

完全正确 ✔
我们要做的就是:

> **数位 DP:统计 ≤ x 的数中,数字和 = p(p 为质数)的个数**

---

## 三、数位 DP 设计(重点讲解)

### 1️⃣ 状态设计

我们把 ( x ) 转成字符串 `s`,长度为 `len`

定义 DP 状态:
1
dp[pos][sum][tight][started]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
含义:

| 维度 | 含义 |
| --------- | --------------------- |
| `pos` | 当前处理到第 pos 位(0-based) |
| `sum` | 当前已累积的各位数字和 |
| `tight` | 前缀是否仍与 x 相同(是否被上界限制) |
| `started` | 是否已经开始构造数字(避免前导 0) |

---

### 2️⃣ 为什么一定要 `started`

举例:
数字 `000123` 在数位 DP 中:

* 前面的 0 **不能计入数字和**
* 否则 `123` 会被算成 `0+0+0+1+2+3`

👉 `started = false` 表示:

> 目前还没遇到第一个非零数字

---

### 3️⃣ 转移逻辑(逐行解释)

假设当前在 `pos`

我们枚举当前位的数字 `d`:
1
2
int up = tight ? s[pos] - '0' : 9;
for (int d = 0; d <= up; d++) {
1
#### 更新状态:
1
2
new_tight = tight && (d == up)
new_started = started || (d != 0)
1
#### 更新数字和:
1
new_sum = sum + (new_started ? d : 0)
1
2
3
4
5
6
7
8
9
10
> 如果还没 started(前导 0),digit sum 不变

---

### 4️⃣ 终止条件(pos == len)

当所有位都处理完:

* 必须 `started == true`(不能是 0)
* `sum` 必须是 **质数**
1
2
if (started && isPrime[sum]) return 1;
else return 0;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
---

### 5️⃣ 数字和的上界(非常重要)

* 最大 18 位
* 每位最大 9

[
\text{max sum} = 18 \times 9 = 162
]

➡ 我们只需筛 **0~162 的质数**

---

## 四、完整 C++ 实现(含 `__int128`)

### ✅ 特点

* 支持 `10^16` 级别二分
* 使用 `__int128` 防止溢出
* 数位 DP + 记忆化
* 清晰、可复用

---

### 📌 完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <bits/stdc++.h>
using namespace std;

using i128 = __int128_t;
using u128 = __uint128_t;

string s;
i128 dp[20][170][2][2];
bool vis[20][170][2][2];
bool isPrime[170];

int len;

// 数位 DP
i128 dfs(int pos, int sum, bool tight, bool started) {
if (pos == len) {
if (started && isPrime[sum]) return 1;
return 0;
}

if (vis[pos][sum][tight][started])
return dp[pos][sum][tight][started];

vis[pos][sum][tight][started] = true;
i128 &res = dp[pos][sum][tight][started];
res = 0;

int up = tight ? (s[pos] - '0') : 9;

for (int d = 0; d <= up; d++) {
bool n_started = started || (d != 0);
int n_sum = sum + (n_started ? d : 0);
bool n_tight = tight && (d == up);

if (n_sum <= 162)
res += dfs(pos + 1, n_sum, n_tight, n_started);
}
return res;
}

// 计算 f(x)
i128 count_good(i128 x) {
s.clear();
while (x > 0) {
s.push_back('0' + (int)(x % 10));
x /= 10;
}
if (s.empty()) s = "0";
reverse(s.begin(), s.end());
len = s.size();

memset(vis, 0, sizeof(vis));
return dfs(0, 0, 1, 0);
}

int main() {
// 筛质数(0~162)
memset(isPrime, true, sizeof(isPrime));
isPrime[0] = isPrime[1] = false;
for (int i = 2; i * i <= 162; i++)
if (isPrime[i])
for (int j = i * i; j <= 162; j += i)
isPrime[j] = false;

i128 target = 1;
for (int i = 0; i < 16; i++) target *= 10; // 10^16

i128 L = 1, R = (i128)1e18;
while (L < R) {
i128 mid = (L + R) >> 1;
if (count_good(mid) >= target)
R = mid;
else
L = mid + 1;
}

// 输出 __int128
string out;
i128 ans = L;
while (ans > 0) {
out.push_back('0' + (int)(ans % 10));
ans /= 10;
}
reverse(out.begin(), out.end());
cout << out << "\n";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
---

## 五、复杂度 & 可行性

* 数位 DP 状态数:
[
20 \times 163 \times 2 \times 2 \approx 13,000
]

* 每次 `count_good(x)`:**极快**

* 二分次数:≈ 60 次

➡ **完全可在 1 秒内跑完**

---

## 六、总结一句话

> 这是一个**“二分 + 数位 DP 统计”的经典高阶问题**,你原本的思路已经非常接近标准解,只差把数位 DP 的状态和转移理顺。

如果你愿意,下一步我可以帮你:

* 把 `started` 的逻辑**进一步简化**
* 或者改写成 **“枚举质数 → 每个质数单独 DP”** 的版本
* 或教你一个**数位 DP 的通用模板心法**,以后见到就能直接套

随时说 👍

fun fact

这个题Flu思路对了,但是不会写数位dp,最后交给gpt了.
这个题先加杠杆,回头再看怎么实现的.