论数位dp--胎教级教学

1前言

本文将从零开始介绍数位 dp,前置知识为线性 dp
例题为洛谷P2602->传送门

2问题

例题如下图
在这里插入图片描述
我们发现, a a a b b b非常的大

3数位dp

我们分析问题, a a a b b b甚至都无法支持 O ( n ) O(n) O(n)的复杂度
线性dp的时间复杂度起步就是 O ( n ) O(n) O(n),那怎么dp?
我们发现,本题不需要考虑数字的具体值为多大,只需要考虑每一位上的数字即可
一个数,有若干位,在这些数位上跑线性dp,就是数位dp
也就是说,数位dp通过将数值转化为数位,来加快速度
原来数组的长度 n n n,在数位dp中变成了 l o g 10 n log_{10}^{n} log10n,这个值在本题中最高为 12 12 12,小了太多了

4数位dp怎么跑–part1预处理

对于一个数 A B C D ABCD ABCD,我们发现,可以吧 A B C D ABCD ABCD拆分成 A 000 A000 A000 B C D BCD BCD B C D BCD BCD是什么并不影响 A 000 A000 A000的求解
我们先考虑 A 000 A000 A000,所有问题都可以拆分出除第一位都为 0 0 0,我们直接预处理 0 − − 9 0--9 09 0 − − 99 0--99 099 0 − − 999 0--999 0999 0 − − 1 0 n − 1 0--10^n-1 010n1这些区间的结果,然后就可以O(1)求出 A 000 A000 A000的子问题
注意,这里求出的结果是包含前导 0 0 0的,因为这个结果是接在数 A A A后面的!
现在的问题是怎么预处理
预处理本质上也是数位dp!
f i f_{i} fi 0 − − 1 0 i − 1 0--10^i-1 010i1区间内数的个数
为什么这么设置,因为 0 − − 1 0 i − 1 0--10^i-1 010i1这个区间内,每个数的出现次数是一样的
所有的题解都教你瞪眼法,没有眼睛怎么办,我来证明
使用数学归纳法, i = 1 i = 1 i=1时,集合为 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 {0,1,2,3,4,5,6,7,8,9} 0,1,2,3,4,5,6,7,8,9,显然成立
i > 1 i>1 i>1时,对于每一个 f i − 1 f_{i-1} fi1集合中的数,都可以在首位插入 0 − 9 0-9 09的任意整数,使这个数成为 f i f_{i} fi集合中的数
考虑上 f i − 1 f_{i-1} fi1中的数在首位插入任何数,其本身每个数位的数量不会变化,即得 f i f_{i} fi中数 k k k的个数为 f i − 1 ∗ 10 + 1 0 i − 1 f_{i-1}*10+10^{i-1} fi110+10i1
由此,不仅证明了对于所有 f i f_{i} fi,每个数位出现个数一样,还得出了状态转移方程
顺便说一下,可以预处理 1 0 i 10^i 10i,因为用处较多
预处理的部分解决了

5数位dp怎么跑–part2计算状态

1状态设置

我们的最终问题是求 1 − a 1-a 1a区间内每个数字的数量
不妨设 c n t i , j cnt_{i,j} cnti,j为当前 a a a i i i位数字 j j j的个数
但是,显然dp属性为 C O U N T COUNT COUNT,并且在维度 i i i具有前缀和性质
所以我们直接降维,设 d p k dp_{k} dpk为当前 1 − a 1-a 1a区间内k的个数
每次枚举到一位,把当前位结果加上就可以了

2状态转移

前文说过,对于子问题 A B C D ABCD ABCD,即 1 − A B C D 1-ABCD 1ABCD的数字个数
我们拆分问题为 A 000 A000 A000 B C D BCD BCD
对于形似 A 000 A000 A000的问题,相当于 A A A 1000 1000 1000子问题
得状态转移方程1
c n t j = c n t j + A × f i − 1 ( j = 0 − > 9 ) cnt_{j} = cnt_{j}+A \times f_{i-1}(j = 0->9) cntj=cntj+A×fi1(j=0>9)(此处i指A处于 1 0 i 10^i 10i位)

再考虑 A A A位本身,没有前导 0 0 0,数字 1 − ( A − 1 ) 1-(A-1) 1(A1)各有 1 0 i − 1 10^{i-1} 10i1
得状态转移方程2
c n t j = c n t j + 1 0 i − 1 ( 0 < j < A ) cnt_{j} = cnt{j}+10^{i-1}(0<j<A) cntj=cntj+10i1(0<j<A)

A A A位本身加上 B C D BCD BCD+1个数字A即可
得状态转移方程3
c n t A = c n t A + B C D + 1 cnt_{A} =cnt_{A}+BCD+1 cntA=cntA+BCD+1

最后,去除前导 0 0 0
这个才是最难的
我们分类讨论,设子问题一共 k k k
k k k个前导 0 0 0,不存在是显然的
k − 1 k-1 k1个前导 0 0 0,后面每一位是 0 − 9 0-9 09,一共 k − 1 k-1 k1位,这种情况为 1 0 k − 1 10^{k-1} 10k1
但是后面 k − 1 k-1 k1位还有可能含有前导 0 0 0,我们把后面 k − 1 k-1 k1位看做子问题, 0 0 0的数量再减去 1 0 k − 2 10^{k-2} 10k2
这样不断减去即可
得状态转移方程4
c n t 0 = c n t 0 − 1 0 j ( 1 < j < A − 1 ) cnt_{0} = cnt_{0} - 10^j(1<j<A-1) cnt0=cnt010j(1<j<A1)

这里的位数变为了原来的 A A A,是为了和其他状态转移方程统一格式
注意 j j j不为 0 0 0,我们在求 f f f时没考虑 0 0 0,(就算考虑 0 0 0, 0 0 0本身也不是前导 0 0 0
我们的数位dp就这样完成了
附代码(c++)

#include<bits/stdc++.h>
using namespace std;
unsigned long long a,b;
unsigned long long cnt[20];
unsigned long long f[20],e[20];
unsigned long long ans1[20];
void dp(long long x){
	long long num1[20],idx = 0;
	num1[0] = 0;
	while(x){
		num1[++idx] = x%10;
		x/=10;
	}
	for(int i = idx;i>=1;i--){
		for(int j = 0;j<=9;j++){
			cnt[j]+=num1[i]*f[i-1];
		}
		for(int j = 0;j<num1[i];j++){
			cnt[j]+=e[i-1];
		}
		unsigned long long h = 0;
		for(int j = i-1;j>=1;j--){
			h*=10;
			h+=num1[j];
		}
		cnt[num1[i]]+=h+1;
		cnt[0]-=e[i-1];
	} 
}
int main(){
	cin>>a>>b;
	e[0] = 1;
	for(int i = 1;i<=15;i++){
		f[i] = f[i-1]*10+e[i-1];
		e[i] = e[i-1]*10;
	}
	dp(a-1);
	for(int i = 0;i<=9;i++){
		ans1[i] = cnt[i];
		cnt[i] = 0;
	}
	dp(b);
	for(int i = 0;i<=9;i++){
		cout<<(cnt[i]-ans1[i])<<" ";
	}
	return 0;
}

6后记

作者认为,数位dp之所以能直接在数位上跑,就是因为一些特殊的性质
找到数的性质,就可以考虑数位,而不是只关注数的本身
数位dp的核心不在dp,在数位,发现了数位的性质,dp就自然成形了
本文作者是蒟蒻,如有错误请各位神犇指点
森林古猿出品,必属精品,请认准CSDN森林古猿1

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值