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
0−−9,
0
−
−
99
0--99
0−−99,
0
−
−
999
0--999
0−−999…
0
−
−
1
0
n
−
1
0--10^n-1
0−−10n−1这些区间的结果,然后就可以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
0−−10i−1区间内数的个数
为什么这么设置,因为
0
−
−
1
0
i
−
1
0--10^i-1
0−−10i−1这个区间内,每个数的出现次数是一样的
所有的题解都教你瞪眼法,没有眼睛怎么办,我来证明
使用数学归纳法,
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}
fi−1集合中的数,都可以在首位插入
0
−
9
0-9
0−9的任意整数,使这个数成为
f
i
f_{i}
fi集合中的数
考虑上
f
i
−
1
f_{i-1}
fi−1中的数在首位插入任何数,其本身每个数位的数量不会变化,即得
f
i
f_{i}
fi中数
k
k
k的个数为
f
i
−
1
∗
10
+
1
0
i
−
1
f_{i-1}*10+10^{i-1}
fi−1∗10+10i−1
由此,不仅证明了对于所有
f
i
f_{i}
fi,每个数位出现个数一样,还得出了状态转移方程
顺便说一下,可以预处理
1
0
i
10^i
10i,因为用处较多
预处理的部分解决了
5数位dp怎么跑–part2计算状态
1状态设置
我们的最终问题是求
1
−
a
1-a
1−a区间内每个数字的数量
不妨设
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
1−a区间内k的个数
每次枚举到一位,把当前位结果加上就可以了
2状态转移
前文说过,对于子问题
A
B
C
D
ABCD
ABCD,即
1
−
A
B
C
D
1-ABCD
1−ABCD的数字个数
我们拆分问题为
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×fi−1(j=0−>9)(此处i指A处于
1
0
i
10^i
10i位)
再考虑
A
A
A位本身,没有前导
0
0
0,数字
1
−
(
A
−
1
)
1-(A-1)
1−(A−1)各有
1
0
i
−
1
10^{i-1}
10i−1个
得状态转移方程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+10i−1(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
k−1个前导
0
0
0,后面每一位是
0
−
9
0-9
0−9,一共
k
−
1
k-1
k−1位,这种情况为
1
0
k
−
1
10^{k-1}
10k−1
但是后面
k
−
1
k-1
k−1位还有可能含有前导
0
0
0,我们把后面
k
−
1
k-1
k−1位看做子问题,
0
0
0的数量再减去
1
0
k
−
2
10^{k-2}
10k−2
这样不断减去即可
得状态转移方程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=cnt0−10j(1<j<A−1)
这里的位数变为了原来的
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就自然成形了
本文作者是蒟蒻,如有错误请各位神犇指点
森林古猿出品,必属精品,请认准优快云森林古猿1