Core Logic and Mathematical Principles
Standard digit DP (Section 18.1) typically deals with counting problems that are highly dependent on decimal literal features such as "the specific shape of numbers, adjacent differences, and whether specific substrings are included." However, the frequently examined direction in the NOIP improvement group and provincial selections often merges digit DP with algebraic number theory (divisibility, congruence systems, and greatest common divisors).
When introducing the constraint of "divisibility" (for example, determining whether a number can be divided by the sum of its digits or a fixed number $M$), we cannot perform modulus checks after the number is fully assembled, as this would degrade into brute force enumeration. We must utilize the topological additivity and multiplicativity of congruences to maintain the divisibility state in real-time as we progress through the digits.
1. Dynamic Transmission of Digit State Machines in Congruence Systems
Let the value represented by the prefix digits filled from high to low be $X$. Now we are preparing to fill in a digit $d$ (where $0 \le d \le 9$) at the current position. According to the algebraic principles of positional notation, the new prefix value $X'$ will become:
$$X' = X \cdot 10 + d$$
If we are concerned about the divisibility of this number by a large prime $M$, since modular arithmetic satisfies:
$$(X \cdot 10 + d) \bmod M = \left( (X \bmod M) \cdot 10 + d \right) \bmod M$$
This means that we do not need to record the macro value of $X$ in the state, but only need to record the remainder state of $X \bmod M$. This algebraic dimensional reduction successfully collapses the infinite integer space into a finite congruence state machine of size only $M$.
2. Collapse of Dynamic Modulus Systems through Least Common Multiple (LCM)
Taking the classic problem "Hate Nineteen" (finding numbers in a range that can be divided by the sum of their digits) as an example.
- Core Contradiction: When filling in digits, we have no idea what the "final sum of digits" will be. If we do not know what the final modulus is, we cannot maintain a fixed remainder $X \bmod M$ as described above.
- Algebraic Reduction: Since in decimal, the maximum sum of digits for an 18-digit number does not exceed $9 \times 18 = 162$. This means the final modulus can only be in the range $[1, 162]$.
- Number Theoretic Dimensional Reduction: We can utilize the properties of least common multiples (LCM). Let $G = \text{lcm}(1, 2, 3, \dots, 162)$. According to the reducibility of congruences:
$$X \equiv R \pmod G \implies X \equiv R \pmod m \quad (\forall m \le 162)$$
Thus, we only need to maintain the remainder of the number $X$ against the global least common multiple $G$ during the search process. However, in practice, the LCM of $1 \sim 162$ is an enormous astronomical number. In fact, we can dynamically maintain the LCM of the digit sums that have already appeared, or directly include both the "current remainder" and the "current sum of digits" into the state dimensions, utilizing memoization for extremely sparse grid pruning.
State Design and Algorithm Derivation
Taking the classic number theory digit DP problem as an example: "How many numbers in the interval $[L, R]$ can be divided by the product of their non-zero digits?"
1. Definition of State Space
Since the least common multiple of all digits from $1$ to $9$ is $\text{lcm}(1, 2, 3, 4, 5, 6, 7, 8, 9) = 2520$. The prime factors of any non-zero digit product can only include $2, 3, 5, 7$, so the final product must be divisible by $2520$. We can uniformly take modulus $MOD = 2520$ during the search.
The DFS state space is designed as: int dfs(int pos, int sum_mod, int current_lcm, bool lead, bool limit)
pos: current digit position.sum_mod: the current prefix number's remainder when taken modulo $2520$.current_lcm: the least common multiple (LCM) of all non-zero digits that have been filled so far.
2. State Transition Equations and Congruence Advancement
When enumerating the digit $i$ at the current position:
- New remainder:
next_mod = (sum_mod * 10 + i) % 2520 - New LCM (if $i \neq 0$):
next_lcm = lcm(current_lcm, i)Termination condition (pos < 0): Ifsum_mod % current_lcm == 0, it indicates that the number is valid, returning 1; otherwise, return 0.
C++ Standard Source Code (NOIP/Provincial Selection High-Difficulty Number Theory Template)
The following code is an efficient implementation for "divisibility of digit products." To prevent the f array from experiencing memory overflow (MLE) due to the third dimension current_lcm reaching 2520, the code introduces a discretization mapping operator. Since the combination LCM of $1 \sim 9$ actually has only 48 possible values, the 2520 dimensions are mapped to 48 dimensions, reducing constants and space by a factor of 50.
#include <iostream>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int BASE_MOD = 2520;
int digits[20];
int f[20][BASE_MOD][50]; // 优化:第三维使用离散化后的 LCM 编号(共48个)
int lcm_map[BASE_MOD + 5]; // 实际 LCM 到离散化编号的映射表
// 代数算子:求最大公约数
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
// 代数算子:求最小公倍数
int lcm(int a, int b) {
if (a == 0 || b == 0) return a + b;
return (a * b) / gcd(a, b);
}
// 预处理:离散化 2520 的所有因子,精简状态空间
void preprocess() {
int id = 0;
for (int i = 1; i <= BASE_MOD; ++i) {
if (BASE_MOD % i == 0) {
lcm_map[i] = id++;
}
}
}
// pos: 数位; sum_mod: 统一对2520取模的余数; curr_lcm: 当前非零位的最大公倍数
int dfs(int pos, int sum_mod, int curr_lcm, bool lead, bool limit) {
if (pos < 0) {
// 终局数论判定:全局余数能否整除数字各有效位的最小公倍数
return sum_mod % curr_lcm == 0 ? 1 : 0;
}
// 只有在无限制且脱离前导零时,才能使用复用因子编号
int lcm_id = lcm_map[curr_lcm];
if (!limit && !lead && f[pos][sum_mod][lcm_id] != -1) {
return f[pos][sum_mod][lcm_id];
}
int up = limit ? digits[pos] : 9;
int res = 0;
for (int i = 0; i <= up; ++i) {
int next_mod = (sum_mod * 10 + i) % BASE_MOD;
int next_lcm = (lead && i == 0) ? curr_lcm : lcm(curr_lcm, i);
res += dfs(pos - 1, next_mod, next_lcm, lead && (i == 0), limit && (i == up));
}
if (!limit && !lead) {
f[pos][sum_mod][lcm_id] = res;
}
return res;
}
long long solve(long long n) {
if (n <= 0) return 0;
int len = 0;
while (n > 0) {
digits[len++] = n % 10;
n /= 10;
}
// 初始 LCM 设为 1(乘法恒等元)
return dfs(len - 1, 0, 1, true, true);
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
preprocess();
memset(f, -1, sizeof(f)); // 全局终身复用缓存
long long l, r;
if (cin >> l >> r) {
cout << solve(r) - solve(l - 1) << "\n";
}
return 0;
}
NOIP 实战避坑指南
1. 动态模数未做安全初始化(零模引发崩溃)
- 现象:在解决关于数位之和或乘积整除的问题时,若初始将
curr_lcm或sum_digits设为 0。当数字全是 0 触底时,代码执行sum_mod % curr_lcm,直接触发 Floating Point Exception(除以零异常),引发编译系统强制终止。 - 规避手段:在处理同余和公倍数时,初始状态必须赋予代数单位元(加法/数位和初始为
0,乘法/LCM 初始为1)。
2. 忽略模数顺延导致的拓扑状态错位
- 现象:在计算
next_mod时,错误地写成next_mod = sum_mod + i % BASE_MOD。这混淆了位置记数法的左移权值,直接摧毁了同余方程的正确性。 - 规避手段:牢记高位向低位推进的同余标准递推矩阵算子:
next_mod = (sum_mod * 10 + i) % MOD。
经典 NOIP/洛谷 真题
1. 洛谷 P4127 [AHOI2009] 同类分布
- 题意描述:给出两个正整数 $a$ 和 $b$,求区间 $[a, b]$ 内有多少个正整数满足:这个数能被它各个数位上的数字之和整除。$a, b \le 10^{18}$。
- 问题本质与解题思路:动态指定模数的同余数位 DP。
- 核心难点:模数(数位和)在搜索过程中动态改变,无法直接预处理出一个统一的全局大 LCM。
- 解决方案:由于 $10^{18}$ 的最大数位和为 $9 \times 18 = 162$。我们可以在外层直接强行枚举最终的数位之和究竟是多少(从 1 枚举到 162)。对于每一个指定的数位和 $M$,我们在内层跑一次标准的数位 DP。此时模数固定为 $M$,状态设计为
dfs(pos, rem, sum, limit),其中rem为当前对 $M$ 的余数,sum为当前实际的数位和。当触底且sum == M且rem == 0时才确认为合法解。利用外层枚举化动态为静态,思想极其精妙。
2. 洛谷 P2657 [SCOI2009] windy 数
- 题意描述:不含前导零且相邻两个数字之差至少为 2 的正整数被称为 windy 数。求区间 $[A, B]$ 内 windy 数的总数。$A, B \le 2 \times 10^9$。
- 问题本质与解题思路:邻域差值约束数位 DP。虽然不涉及高深数论,但它是检验前导零逻辑的标准基石题。
- 状态设计:
dfs(pos, pre, lead, limit)。其中pre记录上一位填写的数字。 - 核心细节:当前导零标志
lead = true时,说明当前位即使填写任何数字,都不会与上一位(实际上不存在的数字)发生差值冲突。因此,当lead = true且当前位填0时,继续向下传递lead = true,且pre码可以赋予任意安全值;只有当lead = false时,当前填写的数字 $i$ 必须严格满足abs(i - pre) >= 2。此题是全网数位 DP 选手的必经之路。