灵神关于数位Dp的讲解板子以及题目:B站灵神讲解视频
关于数位Dp的文章: OI-wiki
二进制与集合
在数位dp之前,由于这一题 周赛306 T4 需要查看曾经出现过的数字,要用到状态压缩,其实很简单
举例说明,对于一个二进制数 a = 1010,它的第2位和第4位为1,那就说明2和4这两个数字曾经出现过,将这个二进制数a转成十进制也就是10然后存储下来,就可以用一个数记录下有哪些数出现过了。当然这仅限于判断数字较少的情况,对于很多数的话就得不偿失。
另外,还有一个问题就是如何判断哪一位是1,还有就是怎么把本来没有的一位数添加进去:
首先,如何判断一个数d在不在集合中:将十进制状态下的a 直接 a>>d & 1 == 1
如果为True
则d这个数曾经出现过
其次如何将一个数添加进去:将十进制状态下的a直接 a = a|(1<<d)
就直接将第d个数字换掉了(如果本来就存在就没有变化)
数位Dp
这里只讨论Python 的数位dp,因为python有@cache可以记录下所有运算过的参数(答题机已经from functools import cache
),就不用另外定义dp数组了!
下面以周赛306 T4 这一题为例:
代码如下:
# 原题:[周赛306 T4 ](https://leetcode.cn/problems/count-special-integers/)
from functools import cache
def dp(n: int) -> int:
s = str(n)
# n是一个整数 以234为例子,那就是要求0~234中所有没有重复数字的个数比如123、143.
# 将n转换成字符串的形式是因为要利用n中的每一位数字,转成字符串的形式更方便
# 下面就是定义求解的函数了
# 各个参数的含义
# i: 现在这个f在s的哪一位,最左边是第0位,会一直调用到出去也就是第n位
# mask: 上面说的那个十进制数,用来记录那些数已经出现过了
# is_limit: 判断这一位之前的所有数是否都是顶着s的,如果是的话这一位最大只能是
# 顶着s上的这一位以234为例,如果前两位都是顶着的---23_ 那么第三位最大只能是4
# is_num: 判断这一位的前面是否已经出现了有效数字了(因为不能有前导零),如果这一
# 位的前面没有有效数字的话,这一位要不也直接跳过要不最小只能是1
@cache
def f(i: int, mask: int, is_limit: bool, is_num: bool) -> int:
# 如果已经递归到最后一位了,直接返回0或1,当没有有效数字时是0,当
# 有有效数字时是1,注:布尔值直接int是0或1
if i == len(s):
return int(is_num)
# res是这一步函数要返回的值
res = 0
# 如果前面都没有有效的数字,直接跳过,这这一位也啥也不填
# 最终目的是想让整体从最后面的情况递归上来
if not is_num:
res = f(i+1, mask, False, False)
# 这一位置能填的最大的数字是up, 能填的最小的数字是down
# 当前面的数字都是顶格取的话,我这一位也能顶格取,否则的话最大能到9
up = int(s[i]) if is_limit else 9
# 如果前面已经出现了有效数字的话这一位最小为0否则为1
down = 0 if is_num else 1
# 开始对这一位所有可能的情况进行枚举
for d in range(down, up+1):
# 前面介绍过了,这里就是判断d这个数有没有出现过
if mask >> d & 1 == 0:
# ==0 表示没有出现过
# 这一步就是dp的核心,与以往的写dp数组进行dp不同,这里是通过
# 递归的回退的过程+@cache的使用进行的dp
# 每一步的res都不是直接计算出来的,而是有i+1这个状态传出来的
res += f(i+1, mask | (1 << d), is_limit and d == up, True)
return res
# 注意初始化调用,第0位,也就是s的第一个数,初始化时没有任何出现过,
# 一定要记得第一个数是需要假定is_limit为True的,否则直接从1-9了
# is_num同样初始化为False,不可以有前导零!
return f(0, 0, True, False)
下面在给一个简洁背诵版:
s = '1234'
@cache
def f(i, m, is_limit, is_num):
if i == len(s):
return int(is_num)
res = 0
if not is_num:
res = f(i+1, m, False, False)
up = int(s[i]) if is_limit else 9
down = 1-int(is_num)
for d in range(down, up+1):
if m >> d & 1 == 0:
res += f(i+1, m | (1 << d), is_limit and d == up, True)
return res
# 调用方式:
ans = f(0,0,True,False)
几乎所有的都不会变,变得只有中间枚举的条件部分。
数位dp的适用范围
一般用于枚举一个范围内的符合某种条件的数,且数及其巨大,传统的简单的枚举不可能完成,就要用到数位dp!
对于[m,n]范围内的数可以转换成 [0,n-1] - [0,m]范围内的数,这样可以只考虑上边界!这一点很重要,因为这个数位dp的板子都是从0开始的,但是实际解题时大概率是一个[n,m]的区间