当前位置: 首页 > news >正文

【小白笔记】虚拟货币挖矿算力匹配

一个虚拟货币挖矿系统中,每个矿工拥有一定的算力值n(范围在1到 1018 之间)。系统需要为每个矿工分配一个算力档位,这个档位必须是小于等于矿工当前算力n的最大"稳定算力档",并且这个档位的算力值各个数位之和必须是一个质数(质数又称素数。一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数)。"稳定算力档"定义为从左到右每一位数字都不小于前一位数字,例如 123、111、399 都是符合要求的稳定算力档,像 121、897这种则不符合要求。合理分配算力档位有助于提高挖矿效率和稳定性。

这类“小于等于 NNN 的最大/最小满足约束 XXX”的题目,都有一个核心模板,只要记住这个模板,就能快速应对:

  1. 分治策略:位数 vs. 各位数字
    • 先考虑位数
      • 第一步:检查 NNN 本身。
      • 第二步:尝试构造与 NNN 位数相同<N\mathbf{< N}<N 的最大数 XXX
      • 第三步(备选):如果找不到,考虑位数比 NNN 少一位的最大数 X′X'X
  2. 构造策略:高位匹配 + 低位决定 (贪心)。
    • 从最高位 i=1i=1i=1 开始,依次尝试保持前缀 X1…i−1=N1…i−1X_{1\dots i-1} = N_{1\dots i-1}X1i1=N1i1
    • 枚举第一个不匹配位 iii:从 Xi=Ni−1X_i = N_i-1Xi=Ni1 从大到小尝试。
    • 一旦 XiX_iXi 确定后,为了使 XXX 最大,剩余的低位 Xi+1…LX_{i+1 \dots L}Xi+1L 必须填入能满足约束的(且尽可能大的)数字。
      • 本题约束是“非递减”:所以直接将所有低位都填入 XiX_iXi(即 Xi+1=⋯=XL=XiX_{i+1}=\dots=X_L=X_iXi+1==XL=Xi),这样才能保证 XXX 尽可能大且满足稳定算力档要求。

口诀:

先验 NNN (Check NNN),再寻 XXX
高位尽跟 NNN (Prefix Match),低位向下找 XiX_iXi (Try Ni−1→N_i-1 \toNi1 lower)。
一旦 XiX_iXi 降,后缀全铺满 (Fill Suffix with XiX_iXi),最大即刻返 (Return XXX)。

掌握了这个高位贪心降位的套路,就能快速构思出这道题的高效解法。


1. 明确目标与约束

  • 输入 NNN (矿工算力 nnn): 1≤N≤10181 \le N \le 10^{18}1N1018(这是一个非常大的数,意味着我们需要处理最多 181818 位的整数,需要使用 646464 位整数类型)。
  • 约束 A (稳定算力档):从左到右,每一位数字不小于前一位数字(非递减数列)。例如 123123123, 447447447, 135791357913579
  • 约束 B (数字和为质数):稳定算力档的各位数字之和(SSS)必须是一个质数Prime\text{Prime}Prime)。
  • 目标 (Maximum Stable Power Level):找到小于或等于 NNN 的最大整数 XXX,使 XXX 同时满足约束 A 和约束 B。

2. 核心难点分析

A. 质数 (Prime Number)

质数(Prime Number, 又称素数):一个大于 1 的自然数,除了 1 和它自身外,不能被其他自然数整除。这是一个基础数学概念。

由于 NNN 最大是 101810^{18}1018,其各位数字之和最大是 9×18=1629 \times 18 = 1629×18=162。我们需要判断的质数范围非常小(1 到 162),可以预先生成或硬编码这个范围内的质数表,以提高判断效率。

KaTeX parse error: Expected 'EOF', got '}' at position 181: …, 149, 151, 157}̲
Prime(≤162)={2,3,5,…,157}\text{Prime}(\le 162) = \{2, 3, 5, \dots, 157\}Prime(162)={2,3,5,,157}

B. 稳定算力档 (Non-decreasing Number)

这是一个典型的数字型动态规划 (Digit DP)深度优先搜索 (DFS) 结合记忆化 的范畴,因为需要生成满足特定位值约束的数字。但在这里,我们是要找最大且小于等于 NNN 的数 XXX,这提示我们使用贪心 (Greedy)从高位到低位的尝试 (Trial from MSB) 的思路。

3. 算法设计与思路 (找最大值 X≤NX \le NXN)

由于要求找到最大X≤NX \le NXN,并且 NNN 的位数是固定的(最多 18 位),我们应该从 NNN 的最高位开始,尽量构造一个与 NNN 相似的数字 XXX,并尝试让 XXX 的位数与 NNN 的位数相同。

思路:从 NNN 的高位开始构造
  1. 位数相同(优先):尝试构造一个 LLL 位(LLLNNN 的位数)的稳定算力档 X≤NX \le NXN
  2. 位数减少(备选):如果所有 LLL 位的稳定算力档都不满足,或者构造 LLL 位的数过于复杂,我们可以直接考虑最大的 L−1L-1L1 位的稳定算力档。最大的 L−1L-1L1 位的稳定算力档肯定是 99…999\dots9999 ( L−1L-1L1999),但它不一定是稳定算力档,且数字和不一定是质数。因此,我们需要找最大的 L−1L-1L1 位的稳定算力档 X′≤10L−1−1X' \le 10^{L-1}-1X10L11,且其数字和是质数。最大的 L−1L-1L1 位的稳定算力档是 11…1⏟L−1\underbrace{11\dots1}_{L-1}L1111 (数字和 L−1L-1L1) 或 99…9⏟L−1\underbrace{99\dots9}_{L-1}L1999 (如果 L−1L-1L1999 的和是质数),但最好的 L−1L-1L1 位的稳定算力档应该是 99…9⏟L−1\underbrace{99\dots9}_{L-1}L1999我们应该找小于 NNN 的、且位数与 NNN 相同的最大数 XXX
主要策略:位构造 + 回溯/迭代 (Digit Construction + Backtracking/Iteration)

我们构造 XXX 的策略是:尽量让 XXX 的前缀与 NNN 的前缀相同,直到某一位 iii,让 X[i]<N[i]X[i] < N[i]X[i]<N[i],然后从 i+1i+1i+1 位到末位,都填入 X[i]X[i]X[i],使得 XXX 尽可能大且满足稳定算力档的要求。

假设 NNNLLL 位,我们尝试构造 X=X1X2…XLX = X_1 X_2 \dots X_LX=X1X2XL

  1. 枚举不匹配位 iii (Mismatch Position iii):从最高位 i=1i=1i=1LLL
  2. 前缀匹配X1X2…Xi−1=N1N2…Ni−1X_1 X_2 \dots X_{i-1} = N_1 N_2 \dots N_{i-1}X1X2Xi1=N1N2Ni1
  3. iii 位向下尝试XiX_iXiNi−1N_i-1Ni1 向下遍历到 111(或 Xi−1X_{i-1}Xi1,如果是 i>1i>1i>1)。
  4. 后缀填充:一旦确定了 XiX_iXi,那么为了让 XXX 最大,剩下的位 Xi+1…XLX_{i+1} \dots X_LXi+1XL 必须全部填入 XiX_iXi(因为稳定算力档要求非递减)。即 Xi+1=Xi+2=⋯=XL=XiX_{i+1} = X_{i+2} = \dots = X_L = X_iXi+1=Xi+2==XL=Xi
    X=N1N2…Ni−1XiXiXi…Xi⏟L−i 个X = N_1 N_2 \dots N_{i-1} X_i \underbrace{X_i X_i \dots X_i}_{L-i \text{ 个}}X=N1N2Ni1XiLi XiXiXi
  5. 校验
    • 稳定算力档:检查 Xi−1≤XiX_{i-1} \le X_iXi1Xi 是否成立(对于 i=1i=1i=1 恒成立)。如果 XiX_iXi 不小于前一位 Xi−1X_{i-1}Xi1,则满足稳定算力档要求。
    • 数字和质数:计算 XXX 的各位数字之和 SSS,检查 SSS 是否为质数。
  6. 结果:一旦找到第一个满足条件的 XXX,它就是小于 NNN 的最大稳定算力档。因为我们是从高位到低位,从大到小地尝试 XXX 的值。

例:N=1245N=1245N=1245

不匹配位 iiiNiN_iNiXiX_iXi (从 Ni−1N_i-1Ni1 开始向下)XXX (后缀填充 XiX_iXi)稳定算力档?数字和 SSSSSS 是质数?结果 XXX
i=4i=4i=4555444124412441244111111124412441244
i=3i=3i=3444333123312331233999
222122212221222777122212221222
i=2i=2i=2222111111111111111444
000100010001000111
i=1i=1i=1111(无,因为 X1X_1X1 最小为 111 )

在这个例子中,124412441244 是满足要求的最大值。

边界情况:X=NX=NX=N 满足条件

在上述查找 X<NX < NX<N 的过程之前,我们应该先检查 NNN 本身是否满足约束:

  1. NNN 是否是稳定算力档?
  2. NNN 的数字和 SNS_NSN 是否是质数?

如果 NNN 满足,那么答案就是 NNN

4. Python实现细节与代码

在 Python 中,由于 101810^{18}1018 仍然在标准 int 的处理范围内,所以不用担心大数问题。

在很多主流编程语言中,处理 101810^{18}1018 这样的数值确实需要使用 long long 或类似的 64 位整数类型。

但是,对于 Python 来说,这句话是正确的:

  • Python 的 int 类型
    Python 标准的 int 类型没有固定的大小限制(unlimited precision)。它会根据需要,自动分配内存来存储任意大的整数。只要计算机的内存允许,Python 的 int 就可以处理。

  • 其他语言 (C/C++/Java)
    在 C/C++ 或 Java 中:

    • int 通常是 32 位,最大值约为 2×1092 \times 10^92×109
    • long long (C/C++) 或 long (Java) 是 64 位,最大值约为 9×10189 \times 10^{18}9×1018
    • 因此,在这些语言中,要存储 101810^{18}1018必须使用 long longlong

结论

  • 在 Python 中10**18 可以直接用标准 int 存储和计算,不需要担心溢出问题,所以不用担心大数问题的说法是准确的。
  • 在底层原理上:Python 在内部处理 101810^{18}1018 时,实际上是使用了类似于其他语言的 64 位甚至更高位的机制,但这个细节对程序员是透明的,无需手动声明为 long long

总结一下:

语言如何处理 101810^{18}1018程序员需要做什么
Python标准 int 自动支持(无上限)像处理普通数字一样使用 int
C/C++必须使用 long long必须声明为 long long
Java必须使用 long必须声明为 long
步骤一:质数预处理
def is_prime(n):"""质数判断函数,用于数字和S <= 162"""if n <= 1:return False# 只需要检查到 sqrt(162) 约 12.7for i in range(2, int(n**0.5) + 1):if n % i == 0:return Falsereturn True# 预先计算出所有可能的数字和(1到162)中的质数集合
MAX_DIGIT_SUM = 9 * 18 # 10^18 - 1 的数字和最大是 9 * 18 = 162
PRIME_SUMS = {i for i in range(2, MAX_DIGIT_SUM + 1) if is_prime(i)}def check_sum(num_str):"""检查字符串表示的数字的各位和是否为质数"""s = sum(int(d) for d in num_str)return s in PRIME_SUMS

这段代码逻辑非常清晰且高效,它负责解决原问题中的第二个核心约束:检查一个数 XXX 的各位数字之和是否为一个质数

这段代码的核心思想是:预计算(Pre-computation)。由于数字和 SSS 的最大值非常小(最大 162),可以先算出所有可能的质数和,然后通过查表(PRIME_SUMS\text{PRIME\_SUMS}PRIME_SUMS 集合)来快速判断,避免重复计算。

下面我将详细解释这段代码的三个部分:

1. is_prime(n) 函数:质数判断

作用:判断输入的整数 nnn 是否为质数(Prime Number)。

def is_prime(n):"""质数判断函数,用于数字和S <= 162"""if n <= 1:return False# 只需要检查到 sqrt(162) 约 12.7for i in range(2, int(n**0.5) + 1):if n % i == 0:return Falsereturn True
  • 第一步:边界条件

    if n <= 1:return False
    

    根据质数(Prime\text{Prime}Prime)的定义:一个大于 111 的自然数,除了 111 和它自身外,不能被其他自然数整除。因此 111 和所有小于等于 000 的数都不是质数。

  • 第二步:试除法(Trial Division)

    for i in range(2, int(n**0.5) + 1):if n % i == 0:return False
    return True
    

    这是判断质数最常用的方法之一:试除法

    • 原理:如果一个数 nnn 不是质数,那么它一定可以分解为两个因子 a×b=na \times b = na×b=n。这两个因子中,至少有一个因子 ≤n\le \sqrt{n}n
    • 效率优化:因此,我们只需要从 222 开始,检查到 n\sqrt{n}n(即 n**0.5)为止的整数,如果 nnn 能被其中任何一个数整除(n % i == 0),则 nnn 不是质数,立即返回 False
    • 如果循环结束,都没有找到因子,则 nnn 是质数,返回 True

在面试中,如果因为紧张或遗忘而想不起“只需要检查到 n\sqrt{n}n”这个优化定理,我们应该如何应对,才能既保证代码的正确性,又展示出良好的解决问题思路呢?

以下是你可以采取的策略和回答:

策略一:保证正确性,牺牲效率 (完整试除)

如果你忘记了 n\sqrt{n}n 这个优化,首先要做的是保证代码逻辑的绝对正确

面试回答示例:

“如果我暂时想不起质数判断的最佳优化方法,为了保证程序逻辑的正确性,我会先采用最直观的试除法,即从 2 一直检查到 n−1n-1n1

def is_prime_simple(n):if n <= 1:return False# 从 2 检查到 n-1for i in range(2, n): if n % i == 0:return Falsereturn True

但是,我会立刻向面试官指出:我知道这种方法效率低下,需要优化。

策略二:分析并提出优化思路 (现场推导 n\sqrt{n}n)

接着,你应该尝试在不依赖记忆的情况下,现场推导出优化为什么只需要到 n\sqrt{n}n。这比直接背出公式更有价值。

面试回答示例:

“这种 O(n) 的方法对于大数来说效率太低了。我们知道,如果一个数 nnn 不是质数,它一定可以分解为两个因子 aaabbb,使得 a×b=na \times b = na×b=n

现在我们考虑这两个因子的大小关系:

  1. 如果 a>na > \sqrt{n}a>nb>nb > \sqrt{n}b>n:那么 a×ba \times ba×b 必然会大于 n×n=n\sqrt{n} \times \sqrt{n} = nn×n=n
    a×b>na \times b > na×b>n
    但这与 a×b=na \times b = na×b=n 的前提相矛盾。
  2. 因此,两个因子不可能都大于 n\sqrt{n}n

结论就是:如果 nnn 有因子,它必然至少有一个因子 aaa 满足 a≤na \le \sqrt{n}an。我们只需要找到这个较小的因子 aaa 即可证明 nnn 不是质数。

这样,我就能现场推导出优化后的代码,并将循环范围从 nnn 缩小到 n\sqrt{n}n,使得复杂度降为 O(n)O(\sqrt{n})O(n)。”

策略三:结合本题的特殊性 (数字和范围极小)

在这个特定的题目中,数字和 SSS 的最大值只有 162

面试回答示例:

“在这个具体问题中,我们判断质数的数 SSS 最大只有 162162162

  • 162≈12.7\sqrt{162} \approx 12.716212.7。这意味着,即使采用 n\sqrt{n}n 的优化,循环最多也只执行 12 次
  • 即使我们忘记了 n\sqrt{n}n,采用 O(n)O(n)O(n) 的朴素方法,循环最多也只执行 162 次

由于 NNN 的各位数字和是一个非常小的数,两种方法在时间上的差异微乎其微。在实际工程中,我会选择 O(n)O(\sqrt{n})O(n) 以养成良好的编码习惯;但在面试中,如果时间紧张,使用 O(n)O(n)O(n) 方法来处理 S≤162S \le 162S162 这样的小范围数字,是完全可以接受的,因为**预计算(Pre-computation)**的总耗时仍然非常低。”


编程规范函数设计的优秀实践。

1. 为什么返回 TrueFalse 而不是 000111

在现代编程实践中,尤其是在像 Python 这样的高级语言中,布尔值(Boolean values) 是判断函数(is_xxx 函数)的标准返回值,而不是整数 000111

返回值类型Python 中的表示含义
布尔值TrueFalse最直接、最清晰地表示“是/否”或“真/假”的状态。
整数111000在底层或早期的 C 语言中常用,111 代表真,000 代表假。

选择 True/False 的理由(可读性与规范):

  1. 极高的可读性 (Readability):当代码写成 if is_prime(s): 时,任何人都能立刻理解“如果 sss 是质数”的意思。如果写成 if is_prime(s) == 1:,则多了一层转换,可读性下降。
  2. 符合 Python 语言习惯 (Idiomatic Python):Python 强烈推荐使用布尔值进行逻辑判断。
  3. 函数名称决定 (Naming Convention):在编程中,以 is_has_can_ 开头的函数,其约定俗成的规范就是返回布尔值(True/False),表示对某个状态的判断结果。

2. 返回 000111 是否可行?

可行,但强烈不推荐。

在 Python 中,000111 可以被解释为布尔值(称为布尔上下文):

  • 000 会被解释为 False
  • 任何非零数字(包括 111)都会被解释为 True

因此,如果函数返回 000111,代码 if is_prime(s): 仍然可以正常工作

# 假设函数返回 1 和 0
def is_prime_int(n):if n <= 1:return 0  # 0 代表 False# ... 循环 ...if n % i == 0:return 0return 1 # 1 代表 True# 这样使用是完全有效的
if is_prime_int(13):print("是质数")

但是,您不应该这样做。 如果在面试中写了返回 000111 的代码,面试官可能会认为您对 Python 的最佳实践或编程规范不够熟悉。

3. 返回值和报错(Error)的关系

返回 True/False 和程序报错(Error/Exception) 是两码事。

概念目的/用途示例
返回值 (True/False)表示正常的逻辑结果。函数完成了它的判断工作并返回了判断结论。is_prime(5) 返回 True
报错/异常 (Exception)表示程序在执行过程中遇到了非预期的、不正常的情况,导致函数无法完成其设计目标。如果你调用 is_prime("abc"),程序会因为无法将字符串转为整数而报错(抛出 TypeError)。

在这个 is_prime(n) 函数中,我们期望 nnn 是一个整数,所以它只会返回 TrueFalse不会在正常情况下返回错误代码(如 −1-11)来表示“不是质数”

总结

因为函数名是 is_prime,且用于表示一个状态判断,所以最规范、最清晰、最符合 Python 习惯的返回值是布尔值 TrueFalse


1. ** 运算符的含义

  • 代码中的符号n**0.5
  • 含义:在 Python 中,**幂运算符(Exponentiation Operator),用于计算乘方。
运算符含义数学表示举例结果
**乘方(幂)xyx^yxy2 ** 323=82^3 = 823=8

因此,n**0.5 的含义是计算 nnn0.50.50.5 次方,即计算 nnn 的平方根 n\sqrt{n}n

注意: 很多其他编程语言(如 C/C++/Java)使用 ^ 符号来表示按位异或(Bitwise XOR),而不是乘方。在 Python 中,^ 也代表按位异或。

2. 为什么不用 math.sqrt() 函数?

在 Python 中,有两种主要的方法计算平方根:

  1. 使用 math.sqrt(n) 函数:这是标准库 math 中的函数,专门用于计算平方根。
  2. 使用幂运算符 n**0.5:这是 Python 语言内置的运算符,更为简洁。

选择 n**0.5 的原因:

  • 简洁性:它不需要在文件开头导入 math 模块,代码更精简。
  • 性能(在本例中):对于内置类型,** 运算符通常经过高度优化,性能接近甚至有时优于库函数。

在实际编码中,两种方法都是正确的。选择 n**0.5 只是追求简洁和 Pythonic(符合 Python 风格)的一种方式。

3. % 运算符的含义

  • 代码中的符号n % i == 0
  • 含义:在 Python 和大多数编程语言中,%取模运算符(Modulo Operator)取余运算符
运算符含义举例结果
%取余数10 % 310÷3=310 \div 3 = 310÷3=3111

在判断整除中的作用:

  • n % i == 0 意为:“nnn 除以 iii 的余数等于 000”。
  • 这正是判断整除关系的数学表达:如果余数为 000,则 nnn 能被 iii 整除。
对比 / 运算符:
运算符含义举例结果目的
/浮点除法 (True Division)10 / 33.333…3.333\dots3.333计算商(精确值)
//整数除法 (Floor Division)10 // 3333计算商(向下取整)
%取模/取余 (Modulo)10 % 3111计算余数

因此,在判断质数的过程中,我们关心的是能否整除,所以必须使用 % 来获取余数,而不是使用 /// 来获取商。


2. 预计算质数集合:PRIME_SUMS

作用:根据题目中各位数字和的范围,计算出一个包含所有可能质数和的集合,用于快速查表。

MAX_DIGIT_SUM = 9 * 18 # 10^18 - 1 的数字和最大是 9 * 18 = 162
PRIME_SUMS = {i for i in range(2, MAX_DIGIT_SUM + 1) if is_prime(i)}

这行代码的核心作用是:“从 2 遍历到 162,把其中所有的质数都挑出来,放到 PRIME_SUMS 这个集合里。”

这样,PRIME_SUMS 集合就存储了所有可能的质数和,后续的判断就非常快了。

  • MAX_DIGIT_SUM = 9 * 18:只需要考虑数字和在 [1,162][1, 162][1,162] 范围内的质数。
  • PRIME_SUMS = {...}

这行代码是一个典型的 集合推导式 (Set Comprehension)。它本质上是三步操作的浓缩:

部分含义对应英文作用
i输出表达式EEE (Expression)每次循环要放入集合的元素。
for i in range(2, MAX_DIGIT_SUM + 1)迭代循环FFF (For)告诉程序要从哪里遍历元素。
if is_prime(i)过滤条件CCC (Condition)告诉程序哪些元素可以被放入集合。
对应到传统的 for 循环:

这行推导式等价于以下多行代码:

PRIME_SUMS = set()  # 1. 初始化一个空集合
MAX_DIGIT_SUM = 162 # 假设值为 162# 2. 遍历所有可能的数字和
for i in range(2, MAX_DIGIT_SUM + 1): # 3. 检查是否满足条件(是否为质数)if is_prime(i): # 4. 如果满足条件,将元素加入集合PRIME_SUMS.add(i) 

对比:可以看到,推导式将 4 步操作浓缩为 1 行,大大提升了代码简洁度。

2. 记忆口诀(推导式模板)

记住推导式的顺序是:[表达式] + [循环] + [条件]

用中文口诀来记忆:

“想放什么(EEE),从哪开始(FFF),满足什么(CCC)。”

推导式口诀对应代码
{ EEE想放什么?(放入遍历出的元素 iiii\mathbf{i}i
FFF从哪开始?(从 2 到 162 遍历 iiifor i in range(… )\mathbf{for\ i\ in\ range(\dots)}for i in range()
CCC满足什么?(只有 iii 是质数时才放入)if is_prime(i)\mathbf{if\ is\_prime(i)}if is_prime(i)
}

所以这行代码就是:

“构建一个集合,我要放进去(iii),是从这个范围(for i in range(...))里找出来的,并且要满足(if is_prime(i))是质数这个条件。”


3. check_sum(num_str) 函数:快速查表判断

作用:接受一个表示数字的字符串,计算其各位数字之和 SSS,然后检查 SSS 是否在预计算的质数集合中。

def check_sum(num_str):"""检查字符串表示的数字的各位和是否为质数"""s = sum(int(d) for d in num_str)return s in PRIME_SUMS
  • 计算数字和 SSS

    s = sum(int(d) for d in num_str)
    
    • 这里使用了 Python 的生成器表达式(Generator Expression):int(d) for d in num_str
    • 它遍历字符串 num_str 中的每一个字符 ddd(即每一位数字)。
    • 将字符 ddd 转换为整数(int(d))。
    • 最后,sum() 函数将所有转换后的整数加起来,得到各位数字之和 SSS
  • 查表判断

    return s in PRIME_SUMS
    
    • 检查计算出的数字和 sss 是否存在于预先计算好的 PRIME_SUMS 集合中。
    • 如果存在,则各位数字之和是质数,返回 True;否则返回 False

整个流程的优势:这种预计算和查表的方法,比在每次检查时都去计算 sss 的质数性要快得多,极大地提高了算法的整体效率。

Python 代码:check_sum(num_str)

这个函数的目的是计算一个数字字符串(例如 "1244")的各位数字之和,然后快速判断这个和是否为质数。

1. 函数定义与文档字符串
代码解释
def check_sum(num_str):定义函数:定义了一个名为 check_sum 的函数,它接受一个字符串参数 num_str
"""检查字符串表示的数字的各位和是否为质数"""文档字符串 (Docstring):说明函数的功能是检查各位数字之和的质数性。
2. 计算各位数字之和
代码解释
s = sum(int(d) for d in num_str)核心计算:这一行代码高效地计算了字符串 num_str 中所有数字字符的整数和,并将结果赋值给变量 sss
细节拆解(生成器表达式)
for d in num_str:遍历字符串 num_str 中的每一个字符 ddd。例如,如果 num_str"1244"ddd 会依次是 '1', '2', '4', '4'
int(d):将字符 ddd 转换为它对应的整数值。例如,将 '1' 转换为 111
sum(...):将生成器表达式产生的所有整数值(即各位数字)累加起来,得到最终的和 sss
举例:对于 "1244"s=1+2+4+4=11s = 1 + 2 + 4 + 4 = 11s=1+2+4+4=11
3. 查表与返回结果
代码解释
return s in PRIME_SUMS最终返回:将计算得到的数字和 sss,与预先计算好的质数集合 PRIME_SUMS 进行比较。
s in PRIME_SUMS:这是一个成员测试运算符。它检查 sss 是否是 PRIME_SUMS 集合中的一个元素。
返回值
* 如果 sss 在集合中,说明这个和是一个质数,表达式返回布尔值 True
* 如果 sss 不在集合中,说明这个和不是质数,表达式返回布尔值 False

总结

输入(num_str各位数字之和(ssssPRIME_SUMS 中吗?返回值结论
"1244"111111是(111111 是质数)True各位和是质数
"1233"999否(999 不是质数)False各位和不是质数

这个函数体现了高效查表的优势:它避免了每次都重新进行质数判断,而是通过一次简单的集合查找(O(1)O(1)O(1) 复杂度)完成了判断。

步骤二:稳定算力档判断
def is_stable(num_str):"""检查字符串表示的数字是否为稳定算力档(非递减)"""for i in range(1, len(num_str)):if num_str[i] < num_str[i-1]:return Falsereturn True

它通过遍历数字字符串的每一位,确保从左到右的数字是非递减的。

以下是对 is_stable(num_str) 函数的详细解释:

Python 代码:is_stable(num_str)

1. 函数定义与文档字符串
代码解释
def is_stable(num_str):定义函数:定义了一个名为 is_stable 的函数,它接受一个字符串参数 num_str(代表矿工算力 NNN 或候选算力 XXX)。
"""检查字符串表示的数字是否为稳定算力档(非递减)"""文档字符串 (Docstring):说明函数的功能是判断数字是否满足“稳定算力档”的要求。稳定算力档的定义就是数字串是非递减的(Non-decreasing)。
2. 遍历检查核心逻辑
代码解释
for i in range(1, len(num_str)):循环开始:遍历数字字符串 num_str
len(num_str):获取字符串的长度(即数字的位数)。
range(1, ...):循环索引 iii111 开始,直到字符串的末尾。
为什么从 i=1i=1i=1 开始? 因为我们要比较当前位 num_str[i] 与它的前一位 num_str[i-1]。从 i=1i=1i=1 开始,可以确保 i−1i-1i1(即 000)是字符串的有效索引(第一位)。
if num_str[i] < num_str[i-1]:条件判断:检查“非递减”的约束是否被破坏。
num_str[i]:当前位数字(例如,"121" 中的第二个 '2' 或第三个 '1')。
num_str[i-1]:当前位的前一位数字。
num_str[i] < num_str[i-1]:如果当前位小于前一位(即出现了递减),则违反了“非递减”的约束。
return False返回值:如果找到任何一位数字小于它的前一位,函数立即返回 False含义:这个数不是稳定算力档。
3. 最终判断
代码解释
return True返回值:如果循环执行完毕,没有触发 return False(即在整个数字串中,没有发现递减的情况),则函数返回 True含义:这个数稳定算力档。

举例说明

输入(num_str循环过程结果结论
"123"i=1i=1i=1: 2 ≥\ge 1 (继续);i=2i=2i=2: 3 ≥\ge 2 (继续)循环结束,返回 True是稳定算力档
"111"i=1i=1i=1: 1 ≥\ge 1 (继续);i=2i=2i=2: 1 ≥\ge 1 (继续)循环结束,返回 True是稳定算力档
"121"i=1i=1i=1: 2 ≥\ge 1 (继续);i=2i=2i=2: 1 $< 2` (条件满足)立即返回 False不是稳定算力档
"897"i=1i=1i=1: 9 ≥\ge 8 (继续);i=2i=2i=2: 7 $< 9` (条件满足)立即返回 False不是稳定算力档
步骤三:主算法实现

核心部分:数位 DP + 记忆化搜索 + 主函数调用


✅ 完整版(在你提供的前置代码后面直接接上)

from functools import lru_cache
# =======================
# 数位 DP 主体部分
# =======================def solve(n: int) -> int:digits = list(map(int, str(n)))length = len(digits)@lru_cache(None)def dfs(pos: int, prev: int, digit_sum: int, tight: bool) -> str | None:if pos == length:return "" if digit_sum in PRIME_SUMS else Noneupper = digits[pos] if tight else 9# 从大到小枚举for d in range(upper, prev - 1, -1):next_tight = tight and (d == upper)sub = dfs(pos + 1, d, digit_sum + d, next_tight)if sub is not None:return str(d) + subreturn Noneresult = dfs(0, 0, 0, True)return int(result) if result is not None else -1

这段代码是经典的**数位 DP(Digit Dynamic Programming)算法,它通过记忆化搜索(Memoized Search)**来高效地解决“在小于等于 NNN 的数中找最大值”这类问题。

要理解和记忆这段代码,关键在于掌握 dfs 函数的四个参数它如何实现三大约束


一:四位“守卫者”与三大约束

这段代码的核心是 dfs 函数,它就像一个复杂的搜索机器人,由四个“守卫者”参数控制,同时检查三项任务(约束)。

1. 四位“守卫者”(参数)

参数含义角色(记忆点)
pos位置 (Position)进度尺:代表当前在构造数字的第几位,用于控制搜索的长度。
prev前一位 (Previous Digit)稳定尺:记录前一个数字,用于确保当前位 d≥prevd \ge \text{prev}dprev,保证非递减
digit_sum数字和 (Sum)质数尺:记录各位数字之和,用于在终点检查质数和约束。
tight紧边界 (Tightness)上限尺:布尔值,代表当前构造的前缀是否与 NNN 的前缀完全相同,用于确保 X≤NX \le NXN

2. 三大约束的实现

dfs 函数体内的逻辑是围绕这三大约束展开的:

A. 约束一:X≤N\mathbf{X \le N}XN (由 tight 决定上限)

这是数位 DP 的核心。

代码片段作用记忆点
upper = digits[pos] if tight else 9确定当前位的遍历上限。如果 tight 为真(前缀紧贴 NNN),上限就是 NNN 的当前位;否则(前缀已经小于 NNN),上限是 999紧边界决定天花板
next_tight = tight and (d == upper)紧边界的传递。只有当 ddd 达到了上限 upper,且前一位也是紧边界时,next_tight 才是真。只有贴着走才继续紧
B. 约束二:稳定算力档(非递减)
代码片段作用记忆点
for d in range(upper, prev - 1, -1):循环从 upper 开始,到 prev - 1 结束。这保证了 d≥prev\mathbf{d \ge prev}dprevprev\mathbf{prev}prev 开始爬坡。
C. 约束三:数字和为质数
代码片段作用记忆点
if pos == length:终点检查跑到终点再查和。
return "" if digit_sum in PRIME_SUMS else None质数和检查。如果和是质数,返回有效解("");否则返回 None是质数才放行。

3. 整体流程与最大化原则

要理解 为什么这个函数能找到最大值,关键在于循环方向返回机制

  1. 从大到小枚举for d in range(upper, prev - 1, -1) →\rightarrow 这确保了我们总是优先尝试最大的数字 ddd
  2. 找到即返回if sub is not None: return str(d) + sub →\rightarrow 由于是从高位到低位,且每一位都是从大到小尝试的,所以找到的第一个完整解 XXX 必然是最大的

记忆口诀(自顶向下搜索):

“从高位(pos=0)开始,用 NNN 的上限(tight=True)框住。
每一位(d)都要从大到小(upperprev\mathbf{prev}prev)试。
一旦找到一个有效数字 ddd,立即递归(dfs),拼接(str(d) + sub),第一个返回的就是最大值。”

通过分解参数的角色和对应约束,这段复杂的数位 DP 代码的逻辑结构就会变得清晰而容易记忆。


1️⃣ 输入和准备

digits = list(map(int, str(n)))
length = len(digits)
  • 将数字 n 转成 每位数字的列表,方便按位处理。
  • length 是数字的位数。

2️⃣ 内部递归函数 dfs

@lru_cache(None)
def dfs(pos: int, prev: int, digit_sum: int, tight: bool) -> str | None:
参数解释:
  • pos:当前处理的数字位索引(从左到右)。

  • prev:前一位数字,保证生成的数字 非递减

  • digit_sum:当前已经生成数字的 数字和

  • tight:是否受 n 的上界限制。

    • tight=True 表示当前位置的数字不能超过 digits[pos]
    • tight=False 表示当前位置可以自由选数字 0~9
返回值:
  • 返回 从当前位置生成的最大数字字符串,或者 None 表示无解。

3️⃣ 递归终止条件

if pos == length:return "" if digit_sum in PRIME_SUMS else None
  • pos == length 表示 递归已经处理到数字的最后一位之后,也就是整个数字已经生成完成。
return "" if digit_sum in PRIME_SUMS else None
  • digit_sum 是当前生成数字的 数字和
  • PRIME_SUMS 是预先计算好的 质数集合(1到162以内的质数)。

return "" if digit_sum in PRIME_SUMS else None 可以分成两种情况:

  1. 数字和是质数

    • digit_sum in PRIME_SUMS 为 True
    • 返回空字符串 ""
      ✅ 这里返回空字符串,是为了递归向上一层构建完整的数字。例如上一位加上当前位形成数字字符串。
  2. 数字和不是质数

    • 返回 None
    • 表示这个生成的数字 不合法,需要回溯到上一层尝试其他可能。

3️⃣ 为什么返回 "" 而不是数字

  • 递归构造数字是从高位到低位:

    str(d) + sub
    
  • 当递归到末位:

    • 如果合法,用空字符串作为“基础”,上一层再加上当前位即可。
    • 如果不合法,用 None 表示没有解,这样上一层就知道要尝试下一个数字。

4️⃣ 举例

假设目标数字是 7

  • dfs(pos=1, prev=7, digit_sum=7, tight=True)

    • 已经到末位 pos == length
    • digit_sum = 7,7 是质数
    • 返回 ""
  • 上一层:

    return str(7) + ""  # 返回 "7"
    

如果数字是 4

  • dfs(pos=1, prev=4, digit_sum=4, tight=True)

    • digit_sum = 4,不是质数
    • 返回 None
  • 上一层知道这个数字不合法,会尝试别的数字。


🔹 总结

这句代码的核心作用:

  • 判断生成的数字是否合法(数字和是质数)。

  • 返回递归构造的基础

    • 合法 → "" 作为字符串的基础
    • 不合法 → None,触发回溯

4️⃣ 当前位的上界

upper = digits[pos] if tight else 9
  • 如果 tight=True,当前位不能超过 n 的对应位 digits[pos]
  • 如果 tight=False,当前位可以自由取最大 9。

5️⃣ 核心逻辑(循环尝试当前位)

for d in range(prev, upper + 1):next_tight = tight and (d == upper)sub = dfs(pos + 1, d, digit_sum + d, next_tight)if sub is not None:return str(d) + sub
  • range(prev, upper + 1)

    • 保证当前位 >= 前一位,满足 非递减
  • next_tight = tight and (d == upper)

    • 如果当前位选的数字等于上界,下一位仍受限制。
    • 否则下一位可以自由选。
  • dfs(pos + 1, d, digit_sum + d, next_tight)

    • 递归处理下一位,更新 digit_sumprev
  • if sub is not None: return str(d) + sub

    • 一旦找到合法解,返回这个组合,因为我们是从大到小枚举,所以第一个合法解就是最大解。

🔹 总结

这个 solve 函数做的事情就是:

  1. 按位构造不超过 n 的数字。
  2. 保证数字非递减(每位 ≥ 前一位)。
  3. 保证数字和是质数。
  4. 利用 记忆化 避免重复计算相同状态,加速递归搜索。
  5. 使用 tight 控制是否受 n 限制,保证生成数字 ≤ n。
  6. 枚举顺序从大到小 → 第一个合法数字就是 最大数字

完全可以说这是 通用的方法,尤其在 数位 DP(Digit DP)题里非常常见

1️⃣ 方法本质

这个 solve 函数实现的是 数位 DP + 记忆化搜索(带剪枝)

  • 数位 DP 的思想是:

    1. 数字的每一位 从高位到低位递归处理。
    2. 每一位可以选择的数字范围,受前一位数字限制(如非递减)以及目标数字 n 的前缀约束。
    3. 状态记录 避免重复计算,常用 lru_cache 或显式 DP 表。
  • 状态设计

    pos       -> 当前处理到第几位
    prev      -> 上一位数字,保证非递减
    digit_sum -> 当前数字和
    tight     -> 是否还受到 n 前缀限制
    
  • 递归逻辑

    • 遍历当前位可能取的数字
    • 累加数字和
    • 根据 tight 决定下一位是否还受约束
    • 递归结束时判断数字和是否满足要求(这里是质数)
  • 从大到小枚举保证第一个合法解就是 最大值


2️⃣ 为什么常见

这种模式在很多面试题和竞赛题里出现,例如:

  1. 求最大/最小满足条件的数字 ≤ n

    • 题目条件可以是:

      • 数字和满足某种约束(质数、能被 k 整除、能组成回文等)
      • 数字按某种规律排列(非递减、交替大小)
  2. 计数问题

    • 不求具体数字,而是求 满足条件的数字有多少个
    • 状态类似:dfs(pos, prev_digit, digit_sum, tight) → 返回数量
  3. 组合优化问题

    • 题目要求在约束下选出最大/最小值
    • DP 记录状态,递归从大到小/小到大枚举即可得到最优解

3️⃣ 这个方法的通用特点

  • 高位到低位递归:保证顺序、方便处理前缀约束
  • tight 标记:处理数字不能超过 n
  • prev 或其他状态变量:处理数字间关系(非递减/非递增)
  • digit_sum 或其他累加量:处理全局条件(和、模、特定模式等)
  • 记忆化/DP:防止重复计算,保证 O(位数 * 状态数) 的复杂度

4️⃣ 总结

  • 这是 数位 DP 的标准模板,很多代码题都可以套用。

  • 变形非常灵活:

    • 可以统计数量或求最大值/最小值
    • 可以改成 数组 DP 而不是递归 + lru_cache
    • 可以添加更多状态,如奇偶、模数、前缀标记等
from functools import lru_cachedef is_prime(n):"""判断是否为质数"""if n <= 1:return Falsefor i in range(2, int(n**0.5) + 1):if n % i == 0:return Falsereturn True# 预先计算数字和可能的质数集合
MAX_DIGIT_SUM = 9 * 18
PRIME_SUMS = {i for i in range(2, MAX_DIGIT_SUM + 1) if is_prime(i)}def is_stable(num_str):"""判断是否为非递减数字"""for i in range(1, len(num_str)):if num_str[i] < num_str[i-1]:return Falsereturn True# =======================
# 数位 DP 主体部分
# =======================def solve(n: int) -> int:digits = list(map(int, str(n)))length = len(digits)@lru_cache(None)def dfs(pos: int, prev: int, digit_sum: int, tight: bool) -> str | None:if pos == length:return "" if digit_sum in PRIME_SUMS else Noneupper = digits[pos] if tight else 9# 从大到小枚举for d in range(upper, prev - 1, -1):next_tight = tight and (d == upper)sub = dfs(pos + 1, d, digit_sum + d, next_tight)if sub is not None:return str(d) + subreturn Noneresult = dfs(0, 0, 0, True)return int(result) if result is not None else -1# 测试
for n in [7, 123, 1,100,20]:print(n, "->", solve(n))
http://www.dtcms.com/a/512222.html

相关文章:

  • 威胁系统(Threat System)概述
  • vue 大型网站开发让网站对搜索引擎友好
  • Blazor核心:Razor组件开发全解析
  • 服务好的合肥网站建设网站开发运作
  • 下载安装sqlite
  • DAX中的MMM月份格式按排序列进行排序
  • python不用框架做网站xps13适合网站开发吗
  • wordpress 多站点 主站点wordpress网站放icp
  • Angular如何让整个项目的所有页面能够整体缩小一定的比例?
  • 深入理解 Java 中的字符串、包装类与日期处理
  • 条件竞争漏洞全解析:从原理到突破
  • 面试_场景方案设计_联系
  • 判断网站首页阿里巴巴做网站营销有没有用
  • uniapp 请求携带数据 \\接口传值 \\ map遍历数据
  • 宝安沙井网站建设网站开发证书
  • 物联网卡为什么要支持双栈
  • 国外美容院网站建设监理工程师网站
  • 一键修复工具背后的机制:如何自动解决常见网络故障
  • MySQL 创建和授权用户
  • 遥控器KC模块技术解析
  • 申请域名建立网站做网站需要购买网站空间吗
  • 网页的创新型网站策划陵川网站建设
  • 个人网站备案代理wordpress文章发布函数
  • 陕西省建设厅网站wap网站开发视频教程
  • 网站开发过程有几个阶段溧水网站建设
  • C++11----模板可变参数
  • 怎么做网站数据库备份公众号软文推广多少钱一篇
  • triton backend 模式docker 部署 pytorch gpu模型 镜像选择
  • RabbitMQ 自动化脚本安装方案
  • 前端三驾马车(HTML/CSS/JS)核心概念深度解析