突然想对数位dp做个总结,奈何有些做过的题目忘记题号了,待发现的时候会加上,所以本文会随时更新
数位dp,顾名思义,就是对数字的每一位进行dp,但是它的本质就是记忆化搜索
所以首先我们要把数字拆开:
fx(int,solve)(int num){
set(dig,0,dig,1);high=0;
while(num){
dig[++high]=num%10;
num/=10;
}
return dp(...);
}
这种dp通常与数字的大小无关,而与状态数和数字位数有关
例如我们要找一个区间 ([l,r]) 中不含数字 (4) 的数的个数
很容易想到枚举每一位,之后对每一位进行讨论
但是状态数是 (9^n) 的,需要优化
我们很容易想到:假设现在考虑到了第 (np) 位,并且前 (np-1) 位都没有 (4)
那么这一位不管选多少(除 (4) 外),都是剩下 (n-np) 位,并且它们的dp结果相同
我们可以用一个数组 ( ext{dpv}[np]) 来记录剩下 (n-np) 位的方法数
( ext{dpv}) 数组经常用来记录某个状态下得到的结果,以防止搜索重复状态,不同情况下可能要多开几维
在对上界或是前导 (0) 进行处理的时候,我们可以多传几个参数来记录当前情况,以方便用来判断
具体情况具体分析
题意简介:
求出给定区间中满足:(6) 与 (2) 不能相邻且不含 (4) 的数的个数
分析:
超级简单的入门题
对于不含 (4) 我们可以在枚举的时候判断
(6) 与 (2) 不能相邻的情况我们可以用变量 (pre) 记录上一位选的数,之后如果 pre==6&&i==2
就跳过,统计即可
#define fx(l,n) inline l n
fx(int,dp)(int np,int pre,bool limit){
if(!np) return 1;
if(!limit&&dpv[np][pre==6]) return dpv[np][pre==6];
int al=limit?digit[np]:9,sum=0;
for(int i=0;i<=al;i++){
if(i==4||(pre==6&&i==2)) continue;
sum+=dp(np-1,i,limit&&i==al);
}
if(!limit) dpv[np][pre==6]=sum;
return sum;
}
题意简介:
求出在给定区间的所有正整数中,每个数码出现的次数
分析:
对于一个整数 (overline{ABCDcdots Z}) 我们可以将其分开统计,即先统计 (overline{A000cdots0}) 再统计 (overline{B00cdots0}) 以此类推,直到最后
对于 (n) 位数 (overline{A00cdots0}) 不难发现就是将区间 ([000cdots0,999cdots9]) 每个数码出现的次数统计起来,乘 (A),最后加上 (A) 出现的次数即可
之后我们将统计的前导 (0) 减掉,顺次统计
#define fx(l,n) inline l n
#define set(l,n,ty,len) memset(l,n,sizeof(ty)*len)
fx(void,solve)(int num){
int cp=num;
set(digit,0,int,50);high=0;
set(ans,0,int,N);
while(num){
digit[++high]=num%10;
num/=10;
}
for(R i=high;i>0;i--){
for(int o=0;o<=9;o++) ans[o]+=pz[i-1]*digit[i];
for(int o=0;o<digit[i];o++) ans[o]+=pow[i-1];
ans[digit[i]]+=cp%pow[i-1]+1;
ans[0]-=pow[i-1];
}
}
题意简介:
求区间 ([l,r]) 内相邻两个数字之差至少为 (2) 的正整数
分析:
也是一个比较裸的题目,但是本题前导 (0) 会影响答案,所以我们要增加一个前导 (0) 标记
之后就是原样枚举,注意判断的时候要取绝对值
#define fx(l,n) inline l n
fx(int,dp)(int np,int pre,bool pz,bool limit){
if(!np) return 1;
if(!limit&&!pz&&dpv[np][pre]) return dpv[np][pre];
int al=limit?dig[np]:9,sum=0;
for(int i=0;i<=al;i++){
if((llabs(pre-i)<2)&&!pz) continue;
int po=dp(np-1,i,pz&&!i,limit&&(i==al));
sum+=po;
}
if(!limit&&!pz) dpv[np][pre]=sum;
return sum;
}
题意简介:
设 ( ext{sum}(i)) 表示 (i) 的二进制中 (1) 的个数,求:
分析:
设 ( ext{num}(i)) 为 (1sim N) 中二进制中 (1) 的个数等于 (i) 的数的个数
即:(prodlimits^N_{i=1} ext{sum}(i)=prodlimits_{i=1}i^{ ext{num}(i)})
数位dp即可,代码较垃圾就不放了
众所周知在 (n) 位二进制中,二进制中 (1) 的个数等于 (i) 的数的个数共有 (dbinom{n}{i}) 个
所以我们可以将递归转为枚举位
for(int i=50,o;i!=-1;i--){
for(o=50;o;o--) num[o]+=num[o-1];
if((n>>i)&1){
num[high]+=1;
high+=1;
}
}
num[high]+=1;
for(int i=1;i<=50;i++) (ans*=pow(i,num[i]))%=mod;
题意简介:
求出区间 ([l,r]) 内能被它每个数位上的数整除的数
分析:
首先我们知道 (1sim10) 的最小公倍数是 (2520),于是我们对于每层递归进行计算后 (\%2520) 即可
我们还需统计当前已经选择的数的最小公倍数,这可以预处理直接拿来用
之后我们在第 (0) 位判断余数是否整除,如果是 (0) 就统计
暴力数位dp即可
之后会TLE与MLE双丰收
考虑如何优化,我们用来进行数位dp的数组有两维,即现在位数和当前递归层的模数已经不可优化了,考虑优化第三维
我们发现有效的状态当且仅当在第 (0) 位的时候余数模最小公倍数为 (0)
而这个最小公倍数一定是 (2520) 的一个因数
而不管我们选择了哪些数,只要它们的最小公倍数相同,它们就一定是同一种状态
我们成功的将第三维的大小优化至 (50)
预处理 (2520) 的每一个因数,之后进行数位dp即可
fx(int,dp)(int np,int amn,int q,bool lmt){
if(!np) return amn%q==0;
if(!lmt&&dpv[amn][mp[q]][np]!=-1) return dpv[amn][mp[q]][np];
int al=lmt?dig[np]:9,sum=0;
for(int i=0;i<=al;i++){
sum+=dp(np-1,(amn*10+i)%2520,i?(q*i/__gcd(q,i)):q,lmt&&i==al);
}
if(!lmt) dpv[amn][mp[q]][np]=sum;
return sum;
}
signed main(){
set(dpv,-1,dpv,1);
for(int i=1;i<=2520;i++) if(2520%i==0) mp[i]=++ys;
}