【参考资料:信息学奥赛一本通 提高篇】
为什么说是浅谈呢?因为楼主对于数位dp也不过只有很浅薄的理解,也就只能浅谈一下了。。。
一、简介
“在信息学竞赛中,有一类与数位有关的区间统计问题。这类问题往往具有比较浓厚的数学味道,无法暴力求解,需要在数位上进行递推等操作。”——刘聪《浅谈数位类统计问题》
- 对于这样的问题,我们需要借用DP的思想,以数位为阶段,在数位上进行递推,这就是数位DP
二、一个例子
- 这里以一道经典例题作为例子来进行讲解 HDU2089 不要62
- 题意:问在区间 [ l , r ] [l,r] [l,r]中有多少个数既不包含4也不包含62
- 解析:数位dp入门题
- 基本思想:
- 预处理
f
f
f数组,
f
[
i
,
s
]
f[i,s]
f[i,s]代表位数为i(可能允许前导0。如00058也是个5
位数),状态为 s s s的方案数。这里 s s s根据题目需要确定。 - 例如 f [ 4 , s ] f[4,s] f[4,s]表示0-9999中所有符合条件的数的个数
- 显然有 f [ i , s ] = f [ i − 1 , s 1 ] , 其 中 s 1 是 对 应 的 状 态 f[i,s]=f[i-1,s1],其中s1是对应的状态 f[i,s]=f[i−1,s1],其中s1是对应的状态
- 预处理
f
f
f数组,
f
[
i
,
s
]
f[i,s]
f[i,s]代表位数为i(可能允许前导0。如00058也是个5
- 在本题中 f [ i ] [ j ] f[i][j] f[i][j]表示位数为i最高位为j的数中有多少个符合条件
- 如 f [ 2 , 6 ] f[2,6] f[2,6]包含60,61,63,65,66,67,68,69
- 预处理f数组如下
inline void init(){
f[0][0]=1;
for(int i=1;i<=10;i++){
for(int j=0;j<10;j++){
if(j==4) continue;
for(int k=0;k<10;k++){
if(j==6&&k==2) continue;
f[i][j]+=f[i-1][k];
}
}
}
}
- 接着,考虑如何统计 [ l , r ] [l,r] [l,r]中符合条件的数的个数
- 显然满足可减性,即 a n s [ l , r ] = a n s [ 0 , r ] − a n s [ 0 , l − 1 ] ans[l,r]=ans[0,r]-ans[0,l-1] ans[l,r]=ans[0,r]−ans[0,l−1]
- 问题转为求
a
n
s
[
0
,
n
]
ans[0,n]
ans[0,n],从高到低枚举哪一位比n小,例如对于n=3456
显然[0,n]包含所有以0,1,2,3开头的四位数,则 a n s [ 0 , 3456 ] + = f [ 4 ] [ 0 ] + f [ 4 ] [ 1 ] + f [ 4 ] [ 2 ] ans[0,3456]+=f[4][0]+f[4][1]+f[4][2] ans[0,3456]+=f[4][0]+f[4][1]+f[4][2]
同理对于3开头的四位数,答案包括所有以0,1,2,3开头的三位数…
以此类推即可 - CODE
#include<bits/stdc++.h>
using namespace std;
int f[11][11],L[11],len,l,r;
inline int read(){
char c=getchar();
int res=0;
while(c<'0'||c>'9')c=getchar();
while(c>='0'&&c<='9'){
res=res*10+c-'0';
c=getchar();
}
return res;
}
inline void init(){
f[0][0]=1;
for(int i=1;i<=10;i++){
for(int j=0;j<10;j++){
if(j==4) continue;
for(int k=0;k<10;k++){
if(j==6&&k==2) continue;
f[i][j]+=f[i-1][k];
}
}
}
}
inline int solve(int n){
len=0;
while(n){
L[++len]=n%10;
n/=10;
}
L[len+1]=0;
int ans=0,i=len;
for(i=len;i>0;i--){
for(int j=0;j<L[i];j++){
if(j==4||(j==2&&L[i+1]==6)) continue;
ans+=f[i][j];
}
if(L[i]==4||(L[i]==2&&L[i+1]==6)) break;
}
if(i==0) ans++;
return ans;
}
int main(){
memset(f,0,sizeof(f));
init();
scanf("%d%d",&l,&r);
printf("%d",solve(r)-solve(l-1));
return 0;
}
三、小结
- 数位dp常见形式是:求在区间[l,r]中,有多少个数满足条件P(i)
- 其中条件P(i)一般与数的大小无关,而与 数的组成 有关,常见的有以下几种:
- 数i是递增/递减的:1234, 2579,…
- 双峰的:19280,26193,…
- 含/不含某一数字的,比如含49:49, 149, 1492,…
- 被某一数m整除的,比如m=13:39,130,650
- 【思路】采用记忆化搜索实现。
- 搜索:dfs(i,j,k,ismax)枚举第i位的数,状态为j,前一位是k,是否达到上限(ismax=true/false)
- 达到了上限只能统计cnt=sum(0num[i]),否则可以统计cnt=sum(09)
- 记忆化:return ismax ? cnt : dp[i][j][k] = cnt
- 搜索入口:dfs(len,0,0,true)
- 记忆化搜索详解参见巨佬LJY的博客