使用数位dp解决计数问题

参考:https://blog.csdn.net/jk211766/article/details/81474632

数位dp

什么是数位dp

数位dp是一种计数用的dp,一般就是要统计一个区间[le,ri]内满足一些条件数的个数。所谓数位dp,字面意思就是在数位上进行dp咯。数位还算是比较好听的名字,数位的含义:一个数有个位、十位、百位、千位…数的每一位就是数位啦!

数位dp的实质就是换一种暴力枚举的方式,使得新的枚举方式满足dp的性质,然后记忆化就可以了。

最简单的暴力枚举法:

for i in range(arr):
	if i == XXX:
		ans++

显然使用这种方法在数据量非常大的时候肯定会超时。

数位dp分析

新的枚举:控制上界枚举,从最高位开始往下枚举,例如:ri=213,那么我们从百位开始枚举:百位可能的情况有0,1,2(觉得这里枚举0有问题的继续看)

然后每一位枚举都不能让枚举的这个数超过上界213(下界就是0或者1,这个次要),当百位枚举了1,那么十位枚举就是从0到9,因为百位1已经比上界2小了,后面数位枚举什么都不可能超过上界。所以问题就在于:当高位枚举刚好达到上界时,那么紧接着的一位枚举就有上界限制了。具体的这里如果百位枚举了2,那么十位的枚举情况就是0到1,如果前两位枚举了21,最后一位之是0到3(这一点正好对于代码模板里的一个变量limit 专门用来判断枚举范围)。最后一个问题:最高位枚举0:百位枚举0,相当于此时我枚举的这个数最多是两位数,如果十位继续枚举0,那么我枚举的就是一位数,因为我们要枚举的是小于等于ri的所有数,当然不能少了位数比ri小的咯!(这样枚举是为了无遗漏的枚举,不过可能会带来一个问题,就是前导零的问题,模板里用lead变量表示,不过这个不是每个题目都是会有影响的,可能前导零不会影响我们计数,具体要看题目)

我们的计算范围是 [ l e , r i ] [le,ri] [le,ri], 在算法中我们可以这样表示:

solve(ri)-solve(le-1)

数位dp的模板

d p [ i ] [ j ] : = 数 位 为 i , 状 态 为 j 时 满 足 条 件 的 个 数 。 dp[i][j] := 数位为i,状态为j时满足条件的个数。 dp[i][j]:=ij

基本的动态模板:

a = [0]
dp = [[0]*state for i in range(20)] 

def solve(x):
	global a
	x = list(str(x))
	x =x.reverse()
	a = x
	retrun dfs(len(a)-1,true,true)

def dfs(pos, lead, limit):
	#limit 专门用来判断枚举范围; lead前导零;pos数的第几位
	if pos == -1 :return 1 
	# 从最高位循环到最低位,返回1表示枚举的这个数合法的,不过返回值根据具体情况而不同
	if not limit  and not lead and dp[pos][state]!=-1:
		return dp[pos][state]
	if limit:
		up = a[pos]
	else:
		up = 9
	
	ans = 0
	for i in range(up+1):
		if :
			pass
		elif:
			pass
		
		lead = lead and i ==1
		limit = limit and i = a[pos]
		ans += dfs(pos-1,lead ,limit)
		
		if not limit and not lead:
			dp[pos][state]=ans
		
		return ans

实例

不要62

杭州人称那些傻乎乎粘嗒嗒的人为62(音:laoer)。

杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了,这样一来,就可以消除个别的士司机和乘客的心理障碍,更安全地服务大众。

不吉利的数字为所有含有4或62的号码。例如:62315 73418 88914
都属于不吉利号码。但是,61152虽然含有6和2,但不是62连号,所以不属于不吉利数字之列。
你的任务是,对于每次给出的一个牌照区间号,推断出交管局今次又要实际上给多少辆新的士车上牌照了。

分析:

该问题总结起来就是数位上不能有4和62

  • 没有4:不枚举4,所以不需要 记忆化搜索
  • 没有62:分为两种情况:当前一位是6和不是6。所以要记录不同状态。

令: d p [ p o s ] [ s t a t e ] : = 当 前 第 p o s 位 , 前 一 位 是 否 为 6 , 所 以 s t a t e 位 只 需 要 0 , 1 两 种 状 态 。 dp[pos][state]:= 当前第pos位,前一位是否为6,所以state位只需要0,1两种状态。 dp[pos][state]:=pos6state01


dp =[]
a= []

def dfs(pos,pre,sta,limit):  # 当前一位是6,sta=1,否则=0
    if pos == -1:
        return 1
    if not limit and dp[pos][sta] != -1:
        return dp[pos][sta]

    up = a[pos] if limit else 9

    tmp = 0

    for i in range(up+1):
        if pre == 6 and i ==2:
            continue
        if i ==4:
            continue
        tmp += dfs(pos-1,i,i==6,limit and i==a[pos])
    
    if not limit:
        dp[pos][sta] = tmp
    
    return tmp

def solve(x):
    global a
    x = list(str(x))
    x= [int(i) for i in x]
    x.reverse()
    a = x
    pos = len(a)
    return dfs(pos-1,-1,0,True)

if __name__ == "__main__":
    
    le,ri=[int(i) for i in input().split()]
    dp = [[-1]*2 for i in range(len(str(ri)))]

    print(solve(ri)-solve(le-1))
    print(dp)
    
```


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值