数位DP入门讲解 byLuan
-
说在前面:数位DP本质上是一种递推(仅限于我个人理解),它只与数字的组成有关,而不关心这个数字本身的数学运算含义,而且这个递推过程可以用状态进行刻画,并且分出明显的阶段,所以给这种处理方式套上了DP的标签。本篇文章全部由博主本人撰写,若有不当之处则请指出,我将感激不尽。
-
数位DP既然带有DP二字,那么状态以及转移就是必不可少的。对于数位DP来讲,状态一般有以下几种:当前处理到了哪一位,是否有前导0,当前位数字的选择是否有限制,以及需要继承的前面的状态。这时我们就可以利用记搜解决这样的问题。(也是我个人建议,建议初学者利用记忆化搜索解决基础的数位DP问题。
其实是我不会递推做法。) -
对于记忆化搜索来讲,我们首先从高到低枚举每一位,枚举当前位填什么,然后传下当前位状态。例如:加入一个数字是1234,那么我们固定第一位1,若第二位是1,那么剩余位置可以随便填,但是若第二位是2,那么下一位只可以填到3,若再大就会超出数字限制的大小。
-
我要重点提一下为什么转移时必须无限制,因为DP数组内部存储的是当前这一位所有可能并且合法的填法方案数,一旦出现限制条件,这一位的数字就不会被全部遍历到,自然就不可以直接转移全部遍历到的方案,这样会多出不合法的情况。比如说满足一位数内是2的倍数的数有5个,即f[1]=5,但是上一位已经填满,这一位的上界是7,就不可以直接转移。(欢迎大家给出更好的例子。)
-
再额外提一下前导0的事,一般来讲,相邻两位之间的数字会互相影响的时候需要考虑前导0,因为第一位不需要考虑前方限制,所以记录前导0用于特殊判断。
-
一般情况下,我们就可以写下这样的记忆化搜索框架:
int dfs(int pos,bool qian_dao_0,bool limit,int pre_situation){
if(!pos) return (the situation is ok)?1:0;
//如果直接搜到了最后,说明已经填进去的数字都是合法的,只需返回是否满足数字的要求就可以了。
if(!qian_dao_0 && !limit && visit[pos][situation]) return f[pos][situation];
//如果这一位的状态已经搜过并且没有前导0,没有填数字的限制,那么就可以直接返回记忆化的值了。
int lim=limit?numbre[pos]:9,val=0;
//若这一位有限制,那么只可以填到当前位数字,否则就可以随便填。
for(int i=0;i<=lim;i++){
if(situation is not ok) continue;
//检查当前位填进去的数字是否合法。
ret+=dfs(pos-1,qian_dao_0&(i==0),limit&(i==lim),situation);
/*
前面有提到过,数位DP的记忆化搜索都是从高位向低位确定,这样可以很方便的维护数字大小的上界问题。
判断有前导0的条件有两个,一个是当前位填0,一个是当前位有前导0。
判断前面填的数字是否已经到达上界的条件同理,前面和当前位都要达到上界。
*/
}
if(!limit && !qian_dao_0){
visit[pos][situation]=1,f[pos][situation]=val;
}
//若没有限制就可以把结果记忆化下来,便于以后调用。
return val;
}
我们看几道例题了解一下。
-
T1:P2657 [SCOI2009]windy数
-
题意:求一个区间内满足相邻两位数之差至少为2的数的个数。
-
我们首先定义状态,设
f[pos][pre] 为没有限制条件,当前搜索到哪一位,上一位的数字填的是什么的方案数。转移时只需枚举下一位填什么,保证其合法即可。对于最高位,需要特判其不需要与前一位做比较。
Code
inline LL dfs(int pos,bool limit,LL val,bool qd){
if(pos==0) return 1;
if((!limit)&&(!qd)&&(f[pos][val])) return f[p][val];
LL lim=limit?num[p]:9,ans=0;
for(LL i=0;i<=lim;i++){
if(abs(val-i)<2) continue;
ans+=dfs(pos-1,limit&&(i==lim),qd&&(i==0)?-233:i,qd&&(i==0));
}
if((!limit)&&(!qd)) f[pos][val]=ans;
return ans;
}
-
T2:P3413 SAC#1 - 萌数
-
题意:求一个区间内满足存在长度至少为2的回文子串的数字个数。
-
注意到回文子串的长度至少为2,那么大致就只有两种情况:相邻的两位相等,或者间隔一位的两个数位相等。再普遍的情况也是在这两种情况的基础上得来。所以我们设
f[pos][pre][pre-pre][0/1] 表示没有限制条件,当前我搜到哪一位,上一位填什么,上上一位填什么,是否已有回文的方案数。转移就比较好写了。
Code
inline int dfs(int p,int pre,int ppre,bool qd,bool lim,bool hui){
if(!p) return hui;
if((!lim)&&(!qd)&&(ppre!=-1)&&(visit[p][pre][ppre][hui])){
return f[p][pre][ppre][hui];
}
int limit=lim?num[p]:9,ret=0;
for(int i=0;i<=limit;i++){
bool _hui,_qd;
_hui=qd?0:hui|(i==pre||i==ppre);
_qd=qd&(i==0);
(ret+=dfs(p-1,_qd?-1:i,pre,_qd,lim&(i==limit),_hui))%=mod;
}
if((!lim)&&(!qd)&&(ppre!=-1)){
visit[p][pre][ppre][hui]=true;
f[p][pre][ppre][hui]=ret;
}
return ret;
}
-
T3:P4124 [CQOI2016]手机号码
-
题意:求区间内满足至少有三个相连的相同数字,且数字内部不能同时含有4与8的数字的数目。
-
好像看明白上面以后,就有一点思路了。虽然说状态设计比较恶心。。我们设
f[pos][pre][pre-pre][0/1][0/1][0/1] 表示没有限制的条件下,上一位填了什么,上上一位填了什么,是否出现三个一样的数,是否出现4,是否出现8时的方案数。转移的时候大力讨论就好。
Code
inline LL dfs(LL pos,bool qd,bool lim,LL pre,LL ppre,bool fl,bool ei,bool th){
if(pos<=0) return ((fl==0||ei==0)&th);
if((!lim)&&(!qd)&&visit[pos][pre][ppre][fl][ei][th]){
return f[pos][pre][ppre][fl][ei][th];
}
LL limit=lim?num[pos]:9,ret=0;
for(LL i=0;i<=limit;i++){
if((i==4&&ei)||(i==8&&fl)) continue;
bool _qd=qd&(i==0),_lim=lim&(i==limit);
ret+=dfs(pos-1,_qd,_lim,_qd?-1:i,pre,fl|(i==4),ei|(i==8),th|(i==pre&&i==ppre));
}
if((!lim)&&(!qd)){
visit[pos][pre][ppre][fl][ei][th]=true;
f[pos][pre][ppre][fl][ei][th]=ret;
}
return ret;
}
-
T4:牛客网NOIP赛前集训营-提高组(第一场)T2:数数字
-
题意:求一个区间内满足所有数位之积在一个给定区间内部的数的数目。
-
这是我唯一一道在考场上切掉(?)的数位题,还没判边界,导致丢了10分。。。考虑每一次乘上的数只会在0-9范围内,分解质因数后只会有4种质因子可能,所以我们把已经处理过的乘积的质数的指数压起来,转移时进行累加即可,注意判断0。给大家看考场代码把。
Code
const int z[10][4]={
0,0,0,0, 0,0,0,0, 1,0,0,0, 0,1,0,0, 2,0,0,0,
0,0,1,0, 1,1,0,0, 0,0,0,1, 3,0,0,0, 0,2,0,0
};
inline LL dfs(int pos,int tw,int th,int fi,int se,bool qd,bool lim,LL PI){
if(pos<=0) return (PI<=r1&&PI>=l1)?1:0;
if((!lim)&&visit[pos][tw][th][fi][se]){
return f[pos][tw][th][fi][se];
}
LL limit=lim?num[pos]:9,ret=0;
for(LL i=0;i<=limit;i++){
bool _qd=qd&(i==0),_lim=lim&(i==limit);
if((!qd)&&(PI==0||i==0)&&(l1>0)) continue;
ret+=dfs(pos-1,tw+z[i][0],th+z[i][1],fi+z[i][2],se+z[i][3],_qd,_lim,_qd?1:PI*i);
}
if(!lim){
visit[pos][tw][th][fi][se]=true;
f[pos][tw][th][fi][se]=ret;
}
return ret;
}
-
T5:P2602 [ZJOI2010]数字计数
-
这真是好题啊!需要通过自己手玩找到一些规律,也就是对于9999……99这种数字及其以下的所有数字之中的每一位数的出现次数满足递推关系。我强力推荐第一篇题解,讲的递推的方法,并且对于前导0、高位数字的统计答案以及如何转化为一个子问题讨论的非常详细清楚,代码我就不放了。
-
T6:P4127 [AHOI2009]同类分布
-
题意:求一个区间内各位数字之和能整除原数的数的个数。
-
注意到上限只有
10^{18} ,数位之和不超过180,所以不妨枚举一下数位之和,每一遍都单独计算其状态,设f[pos][sum][vi] 表示当前搜到第几位,已经填上的数字之和是多少,前面的数字对mod 取余数是多少的方案数。
Code
inline LL dfs(LL pos,LL v,LL sum,bool lim,LL left,LL mod){
if(left>pos*9) return 0;
if(!pos) return (!v);
if((!lim)&&(~f[pos][sum][v])) return f[pos][sum][v];
LL limit=lim?num[pos]:9,ret=0;
for(int i=0;i<=limit;++i){
if(left<i) break;
ret+=dfs(pos-1,(v*10+i)%mod,sum+i,lim&(i==limit),left-i,mod);
}
if(!lim) f[pos][sum][v]=ret;
return ret;
}
-
T7:CF628D Magic Numbers
-
题意:求一个区间内,满足从高位到低位上的每一个偶数数位的值为一个给定的数字d,其余为上不允许有d,并且可以整除给定的数字m的数的个数。
-
我还没有过。。。。。卡到一个点上不知道怎么做。状态就是
f[pos][vmod][0/1] 表示当前填到那一位,前面处理完的的数模m的余数是vmod,当前位是偶数位/奇数位的方案数,转移时的特判有点多啊。。。代码等我过了再放吧。
持更中。