假期最后两天不想做什么太难的,就把数位DP开了吧!正好填之前挖的坑
数位DP看起来貌似都比较裸...而且题目简短,注意一下代码的细节就好
本篇记录里全部使用记忆化搜索
记忆化搜索复杂度=状态数*枚举数
目录-
A. 【例题1】B数计数
-
B. 【例题2】区间圆数
-
C. 【例题3】数字计数
-
D. 【例题4】数字整除
-
F. 1.幸运数字
A. 【例题1】B数计数
分析:
此题是第一道ybt的数位dp题,引入一些模板的写法
对于所有求1~n,L~R的 xx数 个数的,一般都是从高位到低位搜索,而且记录一个 ok 变量来表示当前数位可不可以随便填数字(每一位数字填完最后不能比 n 大)
由于ok 变量不需要在 dp 数组里记录,可以直接传参到记忆化搜索里,但是ok==0和ok==1时候 dp 数组的记忆化值是不一样的,所以规定只记忆化ok==1(即可以随便填)时的 dp 值,这是因为ok==0的情况很少,所以不用记忆化问题也不大(ps:测试时发现枚举 i 的时候倒序枚举也可以使记忆化不重复,有点玄学,本质是因为先算完了所有ok==0的情况再算ok==1的情况,所以不会重复。但不推荐这么写)
实际上,应该也可以在 dp 数组中记录 ok ,理论上空间会变大一倍(ok取值0,1),但是搜索部分会快一点点
那么回归本题,设计dp状态为 dp[pos][res][op]
pos表示第几位,res表示余数,op表示13数出现的状态(op==0没出现过,op==1上一位是1而且之前没出现过完整的13,op==2表示出现过13),maxn表示当前最大能填的数字,ok表示当前这位是否可以随便填
(一开始我想用check表示是否出现过13,其实不需要,可以用op代替)
代码:
A. 【例题1】B数计数#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int INF = 0x3f3f3f3f;
int n,dp[11][14][3],dig[11],m,mi[11];
void init()
{
//memset(dp,-1,sizeof(dp));
//memset(dig,0,sizeof(dig));
m=0;
while(n)
{
int x=n%10;
dig[++m]=x;
n/=10;
}
}
//可以有前导零,不用记录zero
//pos表示第几位,res表示余数,op表示13数出现的状态,maxn表示当前最大能填的数字
//(一开始我想用check表示是否出现过13,其实不需要,可以用op代替)
//ok表示当前这位是否可以随便填
int solve(int pos,int res,int op,bool ok)
{
if(pos==0) return dp[pos][res][op]=(op==2&&res==0);
if(dp[pos][res][op]!=-1&&ok) return dp[pos][res][op];
int ret=0,maxn=9;
if(!ok) maxn=dig[pos];
for(int i=maxn;i>=0;i--)
{
int tmp=(res+mi[pos]*i)%13;
//分类讨论当前填的数字大小
if(i<maxn)
{
//if(check) ret+=solve(pos-1,tmp,op,1,1);
if(op==2) {ret+=solve(pos-1,tmp,2,1);continue;}
if(i==1) ret+=solve(pos-1,tmp,1,1);
else if(i==3&&op==1) ret+=solve(pos-1,tmp,2,1);
else ret+=solve(pos-1,tmp,0,1);
}
else
{
if(op==2) {ret+=solve(pos-1,tmp,2,ok);continue;}
if(i==1) ret+=solve(pos-1,tmp,1,ok);
else if(i==3&&op==1) ret+=solve(pos-1,tmp,2,ok);
else ret+=solve(pos-1,tmp,0,ok);
}
}
// printf("dp[%d][%d][%d]=%d\n",pos,res,op,ret);
if(ok) dp[pos][res][op]=ret;
return ret;
}
int main()
{
mi[1]=1;
for(int i=2;i<=10;i++) mi[i]=mi[i-1]*10;
while(scanf("%d",&n)!=EOF)
{
init();
printf("%d\n",solve(m,0,0,0));
}
return 0;
}
/*
input:
131312
131313
13333
13332
13338
output:
550
551
60
60
61
*/
B. 【例题2】区间圆数
分析:
题目里面要求二进制,那就改成二进制的数位DP就可以啦
这道题目引入了前导零的处理方法:okz 判断前导零是否结束,prezero记录前导零数量
设计 dp[pos][cntzero][prezero] (回去看题解,发现可以压成两维 dp[pos][cntzero])
pos填到第几位,cntzero填的0数量,prezero前导0数量,maxn当前填的最大数,okz前导0是否结束,okm表示当前这位是否可以随便填
op表示操作的这个数字是 L 还是 R
记忆化的时候不用分别记忆okz==0和okz==1的情况(当然记录也没问题),因为对于一个okz==0的情况,在这个dp状态一定是形如 dp[m-k][k][k]这样的样子,这样的状态不会被okz==1的情况覆盖
(其实如果想不明白就把okz也记忆化,也是没有问题的)
代码:
B. 【例题2】区间圆数#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int INF = 0x3f3f3f3f;
int L,R;
int dig[3][35],m[3];
int dp[35][35][35];
inline void init()
{
//mi[1]=1;
//for(int i=2;i<=33;i++) mi[i]=mi[i-1]<<1;
memset(dp,-1,sizeof(dp));
L--;//忘了这个...
while(L)
{
int x=L%2;
dig[1][++m[1]]=x;
L>>=1;
}
while(R)
{
int x=R%2;
dig[2][++m[2]]=x;
R>>=1;
}
}
//pos填到第几位,cntzero填的0数量,prezero前导0数量,maxn当前填的最大数,okz前导0是否结束,okm表示当前这位是否可以随便填
//op表示dig[op]...
int solve(int pos,int cntzero,int prezero,int okz,int okm,int op)
{
if(pos==0) return cntzero-prezero>=m[op]-cntzero;
if(dp[pos][cntzero][prezero]!=-1&&okm&&okz) return dp[pos][cntzero][prezero];
int ret=0,maxn=1;
if(!okm) maxn=dig[op][pos];
for(int i=0;i<=maxn;i++)
{
if(i<maxn)
{
if(okz) ret+=solve(pos-1,cntzero+(i==0),prezero,1,1,op);
else ret+=solve(pos-1,cntzero+(i==0),prezero+(i==0),i,1,op);
}
else
{
if(okz) ret+=solve(pos-1,cntzero+(i==0),prezero,1,okm,op);
else ret+=solve(pos-1,cntzero+(i==0),prezero+(i==0),i,okm,op);
}
}
if(okm&&okz) dp[pos][cntzero][prezero]=ret;
return ret;
}
int main()
{
scanf("%d%d",&L,&R);
init();
int ans1=solve(m[1],0,0,0,0,1);
memset(dp,-1,sizeof(dp));
int ans2=solve(m[2],0,0,0,0,2);
printf("%d\n",ans2-ans1);
return 0;
}
C. 【例题3】数字计数
分析:
做了两道题就会发现,数位DP的套路还是很清晰的
这道题无非就是填数的过程中记录一下某一个数字出现的次数,在边界的时候返回这个次数作为答案
不过需要特殊记录 0 出现的次数,因为要除去前导零的个数,类似例题2
设计dp[pos][num][cnt][zero] (回去看题解,发现实际上可以压成两维 dp[pos][cnt])
pos表示当前填到第几位,num表示当前计算的数字,cnt表示num的数量,ok判断是否可以随便填,op判断是l还是r
okz判断前导零是否结束,zero记录前导零数量
再考虑一下记忆化的问题,这道题和上一道题不一样,必须只记忆化 okz==1&&ok==1 的情况,如果不记录 okz 会 90pts WA
至于为什么...还得再研究研究
代码:
C. 【例题3】数字计数#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int INF = 0x3f3f3f3f,N = 14;
ll l,r;
int dig[2][N],m[2];
ll dp[N][10][N][N];
//dp[pos][num][cnt]
void init()
{
memset(dp,-1,sizeof(dp));
l--;
while(l)
{
int x=l%10;
dig[0][++m[0]]=x;
l/=10;
}
while(r)
{
int x=r%10;
dig[1][++m[1]]=x;
r/=10;
}
}
//pos表示当前填到第几位,num表示当前计算的数字,cnt表示num的数量,ok判断是否可以随便填,op判断是l还是r
//okz判断前导零是否结束,zero记录前导零数量
ll solve(int pos,int num,int cnt,int ok,int op,int okz,int zero)
{
//printf("%d\n",(!num)*zero);
if(!pos) return dp[pos][num][cnt][zero]=cnt-(!num)*zero;//如果num是0,那计数要减去前导零
if(dp[pos][num][cnt][zero]!=-1&&ok&&okz) return dp[pos][num][cnt][zero];
ll ret=0;int maxn=9;
if(!ok) maxn=dig[op][pos];
for(int i=0;i<=maxn;i++)
ret+=solve(pos-1,num,cnt+(i==num),ok||i<maxn,op,okz||i,zero+(!okz&&!i));
if(ok&&okz) dp[pos][num][cnt][zero]=ret;
return ret;
}
int main()
{
scanf("%lld%lld",&l,&r);
init();
ll ans1=0,ans2=0;
for(int i=0;i<=9;i++)
{
ans1=0,ans2=0;
//memset(dp,-1,sizeof(dp));
ans1=solve(m[0],i,0,0,0,0,0);
//memset(dp,-1,sizeof(dp));
ans2=solve(m[1],i,0,0,1,0,0);
printf("%lld ",ans2-ans1);
}
return 0;
}
D. 【例题4】数字整除
分析:
这道题还是有点小技巧的
首先我们可以很简单地设计出 dp[pos][res][sum],pos表示第几位,res表示余数,sum表示数位之和。但是由于数位之和一直在变(也就是模数一直在变),所以这样记录出来的 res 是无效的
那么我们多枚举一维 mod 表示模数,边界的时候判断 sum 是否==mod就可以啦
再考虑 mod 这一维加在哪里好
本题3000组输入,肯定每次记忆化搜索不清空,才不会TLE,所以本题中 mod 肯定要记录在dp数组里面,空间可以承受
但是洛谷有一道题P4127 [AHOI2009]同类分布,只有一组输入,但是数据范围到了1018,那 mod 记录在dp数组里就会MLE,不过由于只有一组输入,所以洛谷上只在记忆化搜索和主函数里枚举 mod 之后 每次清空就可以了
以上是典型的时间换空间
代码:
D. 【例题4】数字整除#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int INF = 0x3f3f3f3f;
int L,R;
int dig[3][12],m[3],mi[11];
int dp[12][100][100][100];
//复杂度:1e6......
//dp[pos][res][sum][mod]
inline void init()
{
m[1]=m[2]=0;
L--;//忘了这个...
while(L)
{
int x=L%10;
dig[1][++m[1]]=x;
L/=10;
}
while(R)
{
int x=R%10;
dig[2][++m[2]]=x;
R/=10;
}
}
//op判断是L还是R
int solve(int pos,int res,int sum,int mod,bool ok,int op)
{
if(sum>mod) return dp[pos][res][sum][mod]=0;
if(pos==0) return dp[pos][res][sum][mod]=(!res&&mod==sum);
if(ok&&dp[pos][res][sum][mod]!=-1) return dp[pos][res][sum][mod];
int ret=0,maxn=9;
if(!ok) maxn=dig[op][pos];
for(int i=0;i<=maxn;i++)
{
int tmp=(res+i*mi[pos])%mod;
ret+=solve(pos-1,tmp,sum+i,mod,ok||(i<maxn),op);
}
if(ok) dp[pos][res][sum][mod]=ret;
return ret;
}
int main()
{
mi[1]=1;
for(int i=2;i<=10;i++) mi[i]=mi[i-1]*10;
memset(dp,-1,sizeof(dp));
while(scanf("%d%d",&L,&R)!=EOF)
{
init();
int ans1=0,ans2=0;
for(int mod=1;mod<=95;mod++)
ans1+=solve(m[1],0,0,mod,0,1);
//memset(dp,-1,sizeof(dp));
for(int mod=1;mod<=95;mod++)
ans2+=solve(m[2],0,0,mod,0,2);
//printf("ans1=%d,ans2=%d\n",ans1,ans2);
printf("%d\n",ans2-ans1);
}
return 0;
}
/*
11 819
11 459
20 743
18 725
9 920
13 877
15 932
6 454
10 533
16 547
*/
P4127 [AHOI2009]同类分布#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int INF = 0x3f3f3f3f;
ll L,R;
int dig[3][20],m[3];
ll mi[20];
ll dp[20][200][200];
//复杂度:1e6......
//dp[pos][res][sum][mod]
inline void init()
{
m[1]=m[2]=0;
L--;//忘了这个...
while(L)
{
int x=L%10;
dig[1][++m[1]]=x;
L/=10;
}
while(R)
{
int x=R%10;
dig[2][++m[2]]=x;
R/=10;
}
}
//op判断是L还是R
int solve(int pos,int res,int sum,int mod,bool ok,int op)
{
if(sum>mod) return dp[pos][res][sum]=0;
if(pos==0) return dp[pos][res][sum]=(!res&&mod==sum);
if(ok&&dp[pos][res][sum]!=-1) return dp[pos][res][sum];
int ret=0,maxn=9;
if(!ok) maxn=dig[op][pos];
for(int i=0;i<=maxn;i++)
{
int tmp=(res+mi[pos]%mod*i)%mod;
ret+=solve(pos-1,tmp,sum+i,mod,ok||(i<maxn),op);
}
if(ok) dp[pos][res][sum]=ret;
return ret;
}
int main()
{
mi[1]=1;
for(int i=2;i<=19;i++) mi[i]=mi[i-1]*10;
memset(dp,-1,sizeof(dp));
scanf("%lld%lld",&L,&R);
{
init();
ll ans1=0,ans2=0;
for(int mod=1;mod<=200;mod++)
{
ans1+=solve(m[1],0,0,mod,0,1);
memset(dp,-1,sizeof(dp));
}
for(int mod=1;mod<=200;mod++)
{
ans2+=solve(m[2],0,0,mod,0,2);
memset(dp,-1,sizeof(dp));
}
//printf("ans1=%d,ans2=%d\n",ans1,ans2);
printf("%lld\n",ans2-ans1);
}
return 0;
}
/*
11 819
11 459
20 743
18 725
9 920
13 877
15 932
6 454
10 533
16 547
*/
F. 1.幸运数字
分析:
妥妥的例题1的弱化版,信心题
不过刚看到这个数据范围还吓了一跳,N这么大都输入不进去,难道要高精?
想到高精之后发现除了字符串输入,没有任何别的操作了,这也告诉我们N的大小不重要,因为预处理的时候都要拆成一位一位的
dp状态把例题1削弱一下就出来了
代码:
F. 1.幸运数字#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int INF = 0x3f3f3f3f;
ll L,R;
int dig[3][20],m[3];
ll mi[20];
ll dp[20][200][200];
//复杂度:1e6......
//dp[pos][res][sum][mod]
inline void init()
{
m[1]=m[2]=0;
L--;//忘了这个...
while(L)
{
int x=L%10;
dig[1][++m[1]]=x;
L/=10;
}
while(R)
{
int x=R%10;
dig[2][++m[2]]=x;
R/=10;
}
}
//op判断是L还是R
int solve(int pos,int res,int sum,int mod,bool ok,int op)
{
if(sum>mod) return dp[pos][res][sum]=0;
if(pos==0) return dp[pos][res][sum]=(!res&&mod==sum);
if(ok&&dp[pos][res][sum]!=-1) return dp[pos][res][sum];
int ret=0,maxn=9;
if(!ok) maxn=dig[op][pos];
for(int i=0;i<=maxn;i++)
{
int tmp=(res+mi[pos]%mod*i)%mod;
ret+=solve(pos-1,tmp,sum+i,mod,ok||(i<maxn),op);
}
if(ok) dp[pos][res][sum]=ret;
return ret;
}
int main()
{
mi[1]=1;
for(int i=2;i<=19;i++) mi[i]=mi[i-1]*10;
memset(dp,-1,sizeof(dp));
scanf("%lld%lld",&L,&R);
{
init();
ll ans1=0,ans2=0;
for(int mod=1;mod<=200;mod++)
{
ans1+=solve(m[1],0,0,mod,0,1);
memset(dp,-1,sizeof(dp));
}
for(int mod=1;mod<=200;mod++)
{
ans2+=solve(m[2],0,0,mod,0,2);
memset(dp,-1,sizeof(dp));
}
//printf("ans1=%d,ans2=%d\n",ans1,ans2);
printf("%lld\n",ans2-ans1);
}
return 0;
}
/*
11 819
11 459
20 743
18 725
9 920
13 877
15 932
6 454
10 533
16 547
*/