《数位DP》基础概念

一、算法概述

  1. 定义:数位DP(Digit Dynamic Programming)是一种处理数字各位特征的计数问题的算法,通过将数字拆分为各个数位,利用动态规划记录状态,高效统计满足特定条件的数字数量。
  2. 核心思想:将数字按位拆解,通过递归+记忆化搜索处理每一位的选择,同时跟踪前导零、数位限制等状态,避免重复计算。
  3. 应用场景
    • 统计区间 [ L , R ] [L, R] [L,R] 中满足各位数字和为 K K K 的数的个数
    • 计算二进制中 1 的个数不超过 M M M 的数字数量
    • 查找满足数位递增/递减条件的数字

二、算法思路

  1. 状态定义
    • dp[depth][is_leadingZero][is_limit][data0][data1]:表示处理到第depth位时,前导零状态、数位限制状态,以及自定义数据data0data1的最优解
    • DpData结构体:封装数位DP的核心状态数据(如1的个数、各位数位之和 等等)
  2. 关键状态
    • is_leadingZero:是否为前导零状态(0000就是true,0002就是false,所以默认是 true
    • is_limit:当前位选择是否受原数字限制(如数字4123中,如果有其中某一位比对应位小,is_limit就是true,如果都是正好等于最大的那个数位,就是false,所以默认是false
  3. 状态转移
    • 对每一位数字,枚举 0maxdigit 的可能取值
    • 根据前导零和限制状态更新新的状态
    • 通过DpData.getNextDpData传递状态特征
  4. 记忆化搜索:利用dp数组存储已计算状态,避免重复计算

三、伪代码实现

1. 数据结构定义

结构体 DpData:
    data0: 长整数  # 存储数位特征数据(如1的个数)
    data1: 长整数  # 扩展数据位
    静态常量 K: 长整数  # 问题参数(如目标1的个数)
    静态常量 base: 长整数  # 进制基数(2/10等)

    函数 init(): 初始化data0和data1
    函数 dfsReturn(is_leadingZero): 返回当前状态是否满足条件
    函数 getNextDpData(is_leadingZero, digit): 生成下一位的状态

数组 dp[maxd][2][2][data0_max][data1_max]  # 记忆化数组
常量 maxd = 65  # 最大数位长度

2. 核心算法框架

算法:数位DP通用模板
输入:数字n,问题参数K
输出:[1, n]中满足条件的数字个数

函数 DpData.init():
    data0 = 0  # 示例:表示二进制中1的个数
    data1 = 0  # 预留扩展位

函数 DpData.dfsReturn(is_leadingZero):
    if is_leadingZero:
        return K == 0  # 前导零情况下的特殊处理
    return data0 == K  # 示例:判断1的个数是否等于K

函数 DpData.getNextDpData(is_leadingZero, digit):
    ret = 当前DpData对象
    if 非前导零:
        if digit == 1:
            ret.data0 += 1  # 示例:二进制中1的个数计数
    return ret

函数 dfs(num_str, depth, is_leadingZero, is_limit, dpdata):
    if depth == num_str长度:
        return dpdata.dfsReturn(is_leadingZero)  # 到达末位,返回是否满足条件
    
    # 记忆化检索
    ans = dp[depth][is_leadingZero][is_limit][dpdata.data0][dpdata.data1]
    if ans != -1:
        return ans
    
    ans = 0
    maxdigit = if is_limit: base-1 else (num_str[depth] - '0')  # 计算当前位最大可选数字
    for i from 0 to maxdigit:
        # 计算新的前导零状态和限制状态
        new_leadingZero = is_leadingZero and (i == 0)
        new_limit = is_limit or (i < maxdigit)
        # 生成下一位的状态数据
        new_dpdata = dpdata.getNextDpData(new_leadingZero, i)
        # 递归处理下一位
        ans += dfs(num_str, depth+1, new_leadingZero, new_limit, new_dpdata)
    
    dp[depth][is_leadingZero][is_limit][dpdata.data0][dpdata.data1] = ans  # 记忆化存储
    return ans

函数 getans(n):
    初始化dp数组为-1
    转换数字n为字符串num_str(按进制base)
    创建DpData对象dpd
    return dfs(num_str, 0, true, false, dpd)

函数 getans(l, r):
    return getans(r) - getans(l-1)  # 差分法计算区间[L, R]

四、算法解释

1. 状态初始化

  • DpData.init()根据问题需求初始化状态参数(如data0记录1的个数)
  • dp数组初始化为-1,表示未计算的状态

2. 递归处理流程

  • 参数说明
    • num_str:数字的字符串表示(如二进制/十进制)
    • depth:当前处理的数位位置
    • is_leadingZero:是否处于前导零状态
    • is_limit:当前位选择是否受原数字限制
    • dpdata:当前状态的特征数据
  • 终止条件:处理完所有数位,通过dfsReturn判断是否满足条件
  • 数位枚举:对当前位枚举0到maxdigit的所有可能取值,更新状态并递归

3. 关键状态转移

  • 前导零处理:若当前位是前导零(i=0且is_leadingZero为true),则新状态仍为前导零
  • 限制状态更新:若当前位选择的数字小于maxdigit,则后续位不再受限制
  • 特征数据传递:通过getNextDpData更新状态特征(如data0计数1的个数)

4. 示例场景:统计二进制中1的个数等于K的数

  • 问题设定:DpData.base=2,DpData.K=K
  • 状态定义:data0记录二进制中1的个数
  • 转移逻辑
    • 遇到数字1时,data0+=1
    • 最终通过dfsReturn判断data0是否等于K
  • 输入示例:n=5(二进制101),K=2
    • 符合条件的数:3(11)、5(101),共2个

五、复杂度分析

  1. 时间复杂度
    • 状态数: O ( d × 2 × 2 × d a t a 0 m a x × d a t a 1 m a x ) O(d × 2 × 2 × data0_{max} × data1_{max}) O(d×2×2×data0max×data1max),其中d为最大数位长度(如二进制64位)
    • 每个状态计算时间:O(base)(枚举每位数字的可能取值)
    • 总时间复杂度: O ( b a s e × d × 2 × 2 × d a t a 0 m a x × d a t a 1 m a x ) O(base × d × 2 × 2 × data0_{max} × data1_{max}) O(base×d×2×2×data0max×data1max)
  2. 空间复杂度
    • 记忆化数组dp O ( d × 2 × 2 × d a t a 0 m a x × d a t a 1 m a x ) O(d × 2 × 2 × data0_{max} × data1_{max}) O(d×2×2×data0max×data1max)
    • 递归栈深度:O(d)
  3. 优化点
    • 状态压缩:若data1未使用,可省略该维度
    • 动态规划迭代实现:将递归改为迭代,避免栈溢出
  4. 适用范围
    • 数字范围:理论上支持任意大整数(受限于字符串转换)
    • 时间效率:适用于 10 18 10^{18} 1018 以内的数字计数问题

  本文为作者(英雄哪里出来)在抖音的独家课程《英雄C++入门到精通》、《英雄C语言入门到精通》、《英雄Python入门到精通》三个课程的配套文字讲解,如需了解算法视频课程,请移步 作者本人 的抖音直播间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

英雄哪里出来

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值