引言
数位动态规划(Digit DP)专门用于解决与数字的位数相关的计数问题。
当我们需要统计满足特定条件的数字个数,尤其是在一个较大范围内(如1到10^9)时,数位DP往往能提供优雅而高效的解决方案。
数位DP的基本思想
什么是数位动态规划
数位动态规划(Digit DP)是动态规划的一个分支,专门用于解决与数字的位数相关的计数问题。所谓"数位",是指数字的个位、十位、百位等每一位。数位DP通常用于解决以下类型的问题:
- 区间计数问题:给定区间[L, R],计算区间内满足特定条件的数字个数。
- 特定数字问题:计算满足特定数字条件(如包含特定数字、数位和等)的数量。
- 数字特征统计:统计数字的特定特征(如数位和、数位积等)。
数位DP的核心思想是将数字按位分解,然后从高位到低位依次考虑每一位的可能取值,并通过动态规划的方式累计结果。
为什么需要数位DP
当我们需要在一个较大范围内统计满足特定条件的数字时,暴力枚举显然是不可行的。例如,如果要统计1到10^9之间满足某条件的数字个数,直接枚举将会超时。
数位DP通过巧妙地利用数字的位结构,将问题分解为更小的子问题,从而大大减少计算量。它的优势在于:
- 高效处理大范围:能够高效处理范围很大的数字区间。
- 灵活适应条件:可以灵活适应各种与数位相关的条件。
- 状态转移清晰:通过明确的状态定义和转移方程,使问题求解变得清晰。
数位DP的基本框架
数位DP的基本框架通常包括以下几个步骤:
- 将数字转换为数位表示:将给定的数字转换为其各个数位的表示形式,通常是从高位到低位的数组。
- 定义状态:根据问题特点定义状态,通常包括当前处理到的位置、前面已经确定的数字状态等。
- 设计状态转移方程:根据问题条件,设计从前一状态到当前状态的转移方程。
- 处理数位限制:考虑数位上界的限制,确保生成的数字不超过给定范围。
- 计算最终结果:根据状态转移方程计算最终结果。
在实现上,数位DP通常采用记忆化搜索的方式,这样可以避免重复计算,提高效率。
数位统计问题的解决方法
状态定义与转移
在数位DP中,状态定义是解决问题的关键。常见的状态包括:
- pos:当前处理的位置(从高位到低位)。
- limit:当前位是否受到上界限制。
- lead:当前是否有前导零。
- state:根据具体问题定义的状态,如已经使用的数字集合、当前的数位和等。
状态转移方程根据具体问题而定,但通常遵循以下模式:
dp[pos][state][limit][lead] = sum(dp[pos+1][new_state][new_limit][new_lead])
其中,new_state
、new_limit
和new_lead
是根据当前位选择的数字计算得到的新状态。
记忆化搜索实现
数位DP通常使用记忆化搜索实现,基本框架如下:
def digit_dp(n):
# 将数字转换为数位表示
digits = []
while n > 0:
digits.append(n % 10)
n //= 10
digits.reverse() # 从高位到低位
@lru_cache(maxsize=None)
def dfs(pos, state, limit, lead):
# 递归终止条件
if pos == len(digits):
return 1 if valid(state) else 0
res = 0
# 计算当前位可以填的上限
up = digits[pos] if limit else 9
# 枚举当前位可以填的数字
for d in range(up + 1):
# 计算新的状态
new_state = update_state(state, d, lead)
# 计算是否仍有上限限制
new_limit = limit and (d == digits[pos])
# 计算是否仍有前导零
new_lead = lead and (d == 0)
# 递归计算结果
res += dfs(pos + 1, new_state, new_limit, new_lead)
return res
return dfs(0, initial_state, True, True)
这个框架可以根据具体问题进行调整,例如添加或删除状态,修改状态转移逻辑等。
处理区间问题
对于区间[L, R]的问题,我们可以使用前缀和的思想,即计算[1, R]的结果减去[1, L-1]的结果:
def count_in_range(L, R):
return count(R) - count