数位dp
一、思想
将一个数字按照数位拆开,接着关注每一位数字。
在信息学题目中,有关数位dp问题通常具有以下特征:
- 可以通过数位思想设计动态规划算法。
- 大部分数位dp用以解决“给定区间内符合一个条件的数的个数”一类计数问题。
- 上界很大,枚举会超时。
数位dp通常可以用计数问题的技巧如 [ l , r ] = [ 0 , r ] − [ 0 , l − 1 ] [l,r]=[0,r]-[0,l-1] [l,r]=[0,r]−[0,l−1] 等。
数位中,无论是1~3位、10~12位,还是1000~1002位,性质在大多数题目中都相同。
所以通常将一些数据处理在一个通用的数组内,然后利用这个数组加加减减乘乘除除求解答案。
求解答案的过程可以采用记忆化搜索,或循环dp,具体视具体题目的实现难度而定。
接下来看几道题目。
二、例题
例1:P2602 [ZJOI2010] 数字计数
给定两个正整数 l l l 和 r r r ,求在 [ l , r ] [l,r] [l,r] 中的所有整数中,数码0~9各出现了多少次。
1 ≤ l ≤ r ≤ 1 0 12 1\le l\le r\le 10^{12} 1≤l≤r≤1012。
此题可以通过 [ 0 , r ] − [ 0 , l − 1 ] [0,r]-[0,l-1] [0,r]−[0,l−1] 求解,接下来记录思路。
要注意,第 i i i 位的位权是 1 0 i − 1 10^{i-1} 10i−1 , i i i 位数共有 1 0 i 10^i 10i 个。
为了方便书写,规定以下记号:
-
g ( n , k ) g(n,k) g(n,k) 表示 n n n 的 1 1 1 ~ k k k 位所构成的数。
-
a i a_i ai 表示数字 a a a 从低到高的第 i i i 位数字。
-
发现在一个 i i i 位数中,每个数码的出现次数相同,记为 f i f_i fi 。显然有 f i = 10 f i − 1 + 1 0 i − 1 f_i=10f_{i-1}+10^{i-1} fi=10fi−1+10i−1 。
不难想到,答案可以表示为“1位数中每个数码的出现次数+2位数中……+ log 10 n \log_{10}n log10n 位数中每个数码的出现次数”。
问题在于 i i i 位数中每个数码的出现次数。
分两类情况讨论:
-
第 i i i 位数不取上界:
有 0 ∼ a i − 1 0\sim a_i-1 0∼ai−1 共 a i a_i ai 种取法。带来两种贡献: i − 1 i-1 i−1 位数拿满共 f i − 1 × a i f_{i-1}\times a_i fi−1×ai 次、取的每个数各出现了 1 0 i − 1 10^{i-1} 10i−1 次。
-
第 i i i 位数取上界:
上界出现了 g ( 原数 , i − 1 ) g(原数,i-1) g(原数,i−1) 次。
问题看似解决,实际上写完代码会发现0的计数多了许多。
原因很简单,在情况一的第二种贡献中0作为前导零被计入了答案。
于是减去0的计数。
目前有一个问题,当 a i = 0 a_i=0 ai=0 ,直观上来说会多减去0的计数,但是过了,判掉就WA,不知为甚。
此题精华在于 f f f 。
代码见文末。
例2:
坑
Code
例1:
#include<cstdio>
#define ll long long
using namespace std;
const int N=15;
ll l,r,p10[N],ans[2][N],f[N],a[N];
void init() {
p10[0]=1;
for(int i=1;i<=14;i++) {
p10[i]=p10[i-1]*10;
}
for(int i=1;i<=14;i++) {
f[i]=10*f[i-1]+p10[i-1];
}
}
void solve(ll x,int kd) {
ll xx=x;
int len=0;
while(x) {
a[++len]=x%10;
x/=10;
}
for(int i=len;i>=1;i--) {
for(int j=0;j<=9;j++) {
ans[kd][j]+=f[i-1]*a[i];
}
for(int j=0;j<a[i];j++) {
ans[kd][j]+=p10[i-1];
}
xx-=a[i]*p10[i-1];
ans[kd][a[i]]+=xx+1;
// if(a[i]!=0)
ans[kd][0]-=p10[i-1];
}
}
int main() {
init();
scanf("%lld%lld",&l,&r);
solve(l-1,0),solve(r,1);
for(int i=0;i<=9;i++) {
printf("%lld ",ans[1][i]-ans[0][i]);
}
}