数位DP

灵神关于数位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]的区间

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值