数位DP学习笔记
数位dp似乎是通过加状态实现的记忆化搜索。
一、 题目和暴力
数位dp的题目一般是这样的:求出区间 \([l,r]\) 中含有/不含数字 \(x\) 的数的个数。由此引申出来的还有 “从左到右各位不降” 或者 “相邻两位差为2” 等等。区间长度一般比较大,一般是 \(1 \leq l \leq r \leq 2^{31}-1\) 或者 \(1 \leq l \leq r \leq 2^{63}-1\) 。
显然的思路是转化为类似于前缀和的东西,这样就把问题转化为对于 \([1,n]\) 求了。
暴力1: \(O(n)\) 扫一遍区间,对于每个数 \(O(lgn)\) 扫一遍看看符不符合。时间复杂度 \(O(nlgn)\) ,不TLE就见鬼了。
暴力2: dfs每一位放什么数,时间复杂度\(O(n)\),少了个 lg 的原因是进入下一层的时候可以保证合法,但是没有本质区别。
对于暴力2,有一个小问题:怎么保证合法呢?加状态——前导零、前一位、前两位……
二、 优化
对于每一位,往下搜索时会出现大量重复状态。比如,第一位填了 1 ,第二位要考虑一遍 0~9 ;第一位填 2 的时候,第二位还是要考虑 0~9 。既然如此,为什么不记忆化呢?
再考虑有哪些状态显然不行:如果区间是 \([1,3567]\) ,第一位显然不能填大于 3 的数。那第二位呢?如果第一位填了 3 ,那第二位不能填大于 5 的数,但如果第一位是 2 ,那第二位就没有限制了。
一个方法出现了,记录下每一位的上限 \(a_i\) , 在dfs时记录状态 \(lim\) 并转移。如果上一位填到上限,下一位就不能填超。
三、 代码实现和注意事项
注意事项: 记忆化数组开多大?每增加一个状态是不是就要加一维?求得是满足的还是不满足的?
#include<stdio.h>
#include<string.h>
typedef long long ll;
int a[20];ll f[20][10];
ll dfs(int len,bool lim,int pre){
if(!len) return 1;
if(!lim&&f[len][pre]!=-1) return f[len][pre];
int up=lim?a[len]:9;ll ans=0;
for(int i=0;i<=up;++i){
if(pre==4&&i==9) continue;
ans+=dfs(len-1,lim&&i==up,i);
}
if(!lim) f[len][pre]=ans;
return ans;
}
inline ll calc(ll x){
memset(a,0,sizeof a);
memset(f,-1,sizeof f);
int len=0;
while(x) a[++len]=x%10,x/=10;
return dfs(len,1,0);
}
int main(){
int t;scanf("%d",&t);
while(t--){
ll n;scanf("%lld",&n);
printf("%lld\n",(ll)n-calc(n)+1);
}
return 0;
}
#include<stdio.h>
#include<string.h>
int a[11],f[11][11];
int dfs(int len,bool lim,int pre){
if(!len) return 1;
if(!lim&&f[len][pre]!=-1) return f[len][pre];
int ans=0,up=lim?a[len]:9;
for(int i=pre;i<=up;++i)
ans+=dfs(len-1,lim&&i==up,i);
if(!lim) f[len][pre]=ans;
return ans;
}
inline int calc(int x){
memset(a,0,sizeof a);
memset(f,-1,sizeof f);
int len=0;
while(x) a[++len]=x%10,x/=10;
return dfs(len,1,0);
}
int main(){
int a,b;
while(scanf("%d%d",&a,&b)==2)
printf("%d\n",calc(b)-calc(a-1));
return 0;
}
#include<stdio.h>
#include<string.h>
#include<math.h>
int a[11],f[11][11][2];
int dfs(int len,bool lim,int pre,bool zero){
if(!len) return 1;
if(!lim&&f[len][pre][zero]!=-1) return f[len][pre][zero];
int ans=0,up=lim?a[len]:9;
for(int i=0;i<=up;++i){
if(abs(i-pre)<2&&!zero) continue;
ans+=dfs(len-1,lim&&i==up,i,zero&&!i);
}
if(!lim) f[len][pre][zero]=ans;
return ans;
}
inline int calc(int x){
memset(a,0,sizeof a);
memset(f,-1,sizeof f);
int len=0;
while(x) a[++len]=x%10,x/=10;
return dfs(len,1,0,1);
}
int main(){
int a,b;
scanf("%d%d",&a,&b);
printf("%d\n",calc(b)-calc(a-1));
return 0;
}