数位dp小结
数位dp
对,你没有看错,我最近才会数位dp,我真是太菜了
引入
数位dp显著特点是按数位dp,也就是说按个位十位百位。通常解决 位与位之间有限制,或与位上的数相关的问题。还有个特点就是输入少
通常问题格式为:在区间\([l,r]\)内有多少个满足限制的数,\(l,r<=10^{18}\)
显然的结论为:区间\([l,r]\)的答案为\((区间[1,r]-区间[1,l-1])\)
问题转化为求区间\([1,x]\)的答案(然鹅并没有什么卵用)
求答案的方法即为按数位去dp计算答案。
……
\(f[i]\)表示在第\(i\)位时的答案,从\(f[i-1]\)转移而来
好像没什么好讲的
讲一下细节:
1.实现使用记忆化搜索会更简易
2.一般从高位开始记忆化搜索,方便处理超出上界的情况
例如:求\([1,21]\)的数,十位不能取大于\(2\)的数,如果十位是\(2\),个位不能取大于\(1\)的数,否则个位取啥都行。
实现时用\(limit\)记录\(0或1\)表示到当前状态之前数位有没有触碰上界
3.看题目对前导\(0\)有没有什么限制,不一定每个题目都有
给个不伪的伪代码理解一下:(仅供理解)
//f[x][y]为前x位,状态为y(随题目改变)的答案
int dfs(int x,int y,int st,int limit){
if(到底了)返回值;//if(!x) return 1;
if(已经记忆化过并且没有上限限制)返回当前状态值
int maxx=9; //当前数位取值上限
long long sum=0; //记录状态答案
if(limit==1)maxx=a[x]; //如果有上界,数位上限缩小
for(枚举数位值)
if(不符合限制)continue;
if(前导0有特殊影响)sum+=dfs(x-1,更新状态,1,0);
else sum+=dfs(x-1,更新状态,0,是否还触碰上界);
if(!limit&&!st) f[x][y]=sum; //如果有限制,说明前x位还有限制,不记忆化
return sum;
}
int solve(long long x){
memset(f,-1,sizeof(f));len=0;
while(x)a[++len]=x%10,x/=10; //拆位
return dfs(len,-2,1,1);
}
要点:理解清\(limit\)和\(st\)限制
还是要结合题目才行
P2657[SCOI2009]windy数
简单的数位dp练手题
自己按模板打打看
注意判前导\(0\)
代码:相当于代码版的模板了
#include<bits/stdc++.h>
#define maxn 20
using namespace std;
long long a[maxn],n,m,len;
long long f[maxn][maxn];
int dfs(int x,int y,int st,int limit){
if(!x)return 1; //到底了
if(f[x][y]!=-1&&limit==0)return f[x][y]; //记忆化过了且没有限制
int maxx=9;long long sum=0;
if(limit==1)maxx=a[x]; //更新数位值上限
for(int i=0;i<=maxx;i++){
if(abs(y-i)<2)continue; //不符合限制
if(st==1&&i==0)sum+=dfs(x-1,-2,1,0); //前导0处理
else sum+=dfs(x-1,i,0,limit&&i==a[x]); //正常处理
}
if(!limit&&!st) f[x][y]=sum; //没有限制记忆化
return sum;
}
int solve(long long x){
memset(f,-1,sizeof(f));len=0;
while(x)a[++len]=x%10,x/=10;
return dfs(len,-2,1,1);
}
int main(){
scanf("%lld%lld",&n,&m);
printf("%d\n",solve(m)-solve(n-1));
return 0;
}
P4127[AHOI2009]同类分布
题意:给出两个数\(a,b\),求出\(a,b\)中各位数字之和能整除原数的数的个数。
不是位与位的限制了,而是原数与数位的限制了
我们设\(f[x][y][val]\)表示dp到前\(x\)位,此时数位和为\(y\),数的值为\(val\)的答案
但很明显这是不可行的,\(val\)高达\(10^{18}\) 时空爆炸!
我们观察,数位和只有\([1,18*9]\) 种可能。
那么我们可以考虑枚举数位和\(sum\),那么\(val\)变为在模\(sum\)意义下的数的值,\(val\)的范围便只有\(162\)这么大了,便可以数位dp了
边界为当 \(x=0\)时,当前数位和\(y==sum\),数的值\(val=0(mod\ sum)\) 为 \(1\) 否则为 \(0\)
long long dfs(int x,long long y,long long val,int limit,int mod){
if(!x&&y==0)return 0; //特判0
if(!x) return val==0&&y==mod?1:0; //边界
if(!limit&&f[x][y][val]!=-1)return f[x][y][val]; //记忆化
int maxx=9;long long res=0;
if(limit) maxx=a[x];
for(int i=0;i<=maxx;i++){
res+=dfs(x-1,y+i,(val*10+i)%mod,(limit&&i==maxx)?1:0,mod);
}
if(!limit)f[x][y][val]=res;
return res;
}
long long solve(long long x){
int len=0;long long ans=0;
while(x)a[++len]=x%10,x/=10;
for(int i=1;i<=9*len;i++){ //枚举数位和
memset(f,-1,sizeof(f));
ans+=dfs(len,0,0,1,i);
}
return ans;
}
CF55DBeautiful numbers
看大佬博客吧,我太懒了
here T1,另强推并%%%
放个代码
#include<bits/stdc++.h>
#define maxn 51
using namespace std;
int a[2530];
long long f[20][2530][50],l,r;
int Map[2530];
long long dfs(int x,int y,int lcm,bool limit){
if(!x)return y%lcm==0;
if(f[x][y][Map[lcm]]!=-1&&!limit)return f[x][y][Map[lcm]];
int maxx=9;long long sum=0;
if(limit==1)maxx=a[x];
for(int i=0;i<=maxx;i++){
sum+=dfs(x-1,(y*10+i)%2520,lcm*max(i,1)/__gcd(lcm,max(i,1)),limit&&i==maxx);
}
if(!limit) f[x][y][Map[lcm]]=sum;
return sum;
}
long long solve(long long x){
int len=0;
if(!x)a[len=1]=0;
while(x)a[++len]=x%10,x/=10;
return dfs(len,0,1,1);
}
int main(){
memset(f,-1,sizeof(f));
int T,tot=0;
scanf("%d",&T);
for(int i=1;i<=2520;i++)if(2520%i==0)Map[i]=++tot;
while(T--){
scanf("%lld%lld",&l,&r);
printf("%lld\n",solve(r)-solve(l-1));
}
return 0;
}
P4884 多少个1?
To be continue……