数位dp杂记
突然想对数位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结果相同
我们可以用一个数组 \(\text{dpv}[np]\) 来记录剩下 \(n-np\) 位的方法数
\(\text{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{ABCD\cdots Z}\) 我们可以将其分开统计,即先统计 \(\overline{A000\cdots0}\) 再统计 \(\overline{B00\cdots0}\) 以此类推,直到最后
对于 \(n\) 位数 \(\overline{A00\cdots0}\) 不难发现就是将区间 \([000\cdots0,999\cdots9]\) 每个数码出现的次数统计起来,乘 \(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;
}
题意简介:
设 \(\text{sum}(i)\) 表示 \(i\) 的二进制中 \(1\) 的个数,求:
分析:
设 \(\text{num}(i)\) 为 \(1\sim N\) 中二进制中 \(1\) 的个数等于 \(i\) 的数的个数
即:\(\prod\limits^N_{i=1}\text{sum}(i)=\prod\limits_{i=1}i^{\text{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]\) 内能被它每个数位上的数整除的数
分析:
首先我们知道 \(1\sim10\) 的最小公倍数是 \(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;
}