【力扣roadmap】2376. 统计特殊整数

16 阅读3分钟

题目描述

image.png

思路描述

清华出版社出版的那本《算法竞赛》(白皮的,分上下册,作者罗勇军+郭卫斌)在对数位DP的描述的中,我觉得还是太生涩了,这让人感觉数位DP好像需要很大的思考量。 但其实数位DP只需板子就能解决。

这道题目需要求出[1,n]区间内,特殊整数的个数。 特殊整数的定义是,整数的每个数位互不相同。 我们的板子代码直接从模拟填充的思路入手,将所有合法的填充情况统统相加,代码量小而且思考量低。

  • 将右区间n视为一个字符串s,有len(s)个数位。我们模拟从左向右进行填充(或者说从高位向低位进行填充)。

  • 如果填充数位的下标已经达到了len(s),说明已经完全填充完毕。但判断当前填充完毕的情况是否为合法的条件,在于填充的所有位是不是全都是前导零?如果是,那么is_num从顶到底一路都是False,最终填充完毕的情况也是一个非法情况,直接返回0;否则是某个合法的情况,返回1.

  • 如果当前is_numFalse,说明这一路都是前导零,你可以选择跳过当前位不填充,继续深搜。

  • 决定当前的上限up是多少?如果当前is_limitTrue,说明从上一个情况传到当前情况的这一路下来,填充数位都顶到了右边界s的相应数位的最高数值,自然当前up就应该继续是右边界s的当前数位的最高数值int(s[idx])

  • 那么当前枚举的下限是多少呢?如果前面一直是前导零,那么我当前想要填充数字,必须从1开始填;如果前面不是前导零,说明我当前可以从0开始填。因此有1-int(is_num)

  • mask表示这一路以来已经填充了哪些数字,因为题目要求填充的数字各不相同。这个mask当一个set来使用。

  • 额外解释1:为什么12行第三个参数是False。因为你一路以来都是前导零,已经对后面的填充没有了约束。你想想,这个约束因何存在?只有从最高数位开始非前导零且一直顶着右边界对应高位进行填充,这个约束才一直会生效。 否则不生效,你可以随便填。

  • 额外解释2:16行的is_limit and d == up是个什么意思?还是那句话,只有从最高数位开始非前导零且一直顶着右边界对应高位进行填充,这个约束才一直会生效 , 我们就靠着这个将约束传递下去。只有这一路之前是一直被约束着的,而且当前再次顶到了右边界对应数位最高值,那么这个约束再次成立,将继续传递。除此之外,约束都不会生效。

  • 额外解释3:为什么答案是dfs(0,0,True,False)? 第一个0表示从第0下标开始填充(对应右边界最高数位,对应右边界字符串第0下标位)。第二个0表示当前谁也没有进行选择,所以是空的集合。第三个参数为True表示当前最高位需要进行限制,你如果为False那上限就是9了,显然不对,上限应该为int(s[0])。最后一个参数为False表示当前还不构成数字。

代码

class Solution:
    def countSpecialNumbers(self, n: int) -> int:
        s = str(n) 

        @cache 
        def dfs(idx : int , mask : int , is_limit : bool , is_num : bool) -> int :
            if idx == len(s) :
                return int(is_num) 
            
            res = 0 
            if not is_num :
                res = dfs(idx + 1 , mask , False , False) 
            up = int(s[idx]) if is_limit else 9 
            for d in range(1 - int(is_num) ,  up + 1) :
                if mask >> d & 1 == 0 :
                    res += dfs(idx + 1 , mask | (1 << d) , is_limit and d == up , True)
            return res 
        
        return dfs(0 , 0 , True , False)