数位DP 学习笔记
概述
数位DP解决的是有关数字每一位的问题。一般形式为统计一个区间内满足一些限制的数的数量。
例题
首先把数字拆位:
for(maxn=0;temp1;)n[++maxn]=temp1%10,temp1/=10;
数位DP可以采用记搜的方式转移。反向统计不含 \(49\) 的个数,设 \(f_{pos,p}\),\(pos\) 表示当前的位,\(p\) 表示上一位是否为 \(4\)。转移方程 \(f_{pos,p}=\sum_{i,\neg p\vee i\ne9}f_{pos-1,i}\)。
对于上界需要一个布尔值 \(lim\),表示当前位是否能取到上界。\(lim\) 为真仅当上一位 \(lim\) 为真并且这一位取到上界。若 \(lim\) 为真,\(i\) 不能超过当前位的值。
最后加上记忆化即可。
#include<bits/stdc++.h>
using namespace std;
int t,n[25],maxn;
long long temp,temp1,f[25][2];
long long dfs(int pos,bool lim,bool p){
if(f[pos][p]!=-1&&!lim)return f[pos][p];
if(!pos)return 1;
long long ans=0;
for(int i=0;i<=(lim?n[pos]:9);i++){
if(p&&i==9)continue;
ans+=dfs(pos-1,lim&&i==n[pos],i==4);
}
if(!lim)f[pos][p]=ans;
return ans;
}
int main(){
memset(f,-1,sizeof(f)),cin>>t;
while(t--){
cin>>temp,temp1=temp;
for(maxn=0;temp1;)n[++maxn]=temp1%10,temp1/=10;
cout<<temp-dfs(maxn,1,0)+1<<'\n';
}
return 0;
}
注意这里 \(f\) 仅初始化了一次,因为这里 DP 数组可以复用,在多测时降低了复杂度。dp 数组复用需要满足数组的意义不变。因此要从低位向高位存数,并且 \(lim\) 为真时不记忆化。不过当没有多测或者 \(lim\) 为真的状态很多,可以选择将 \(lim\) 记忆化。
首先差分,转化为两个从一开始的询问。
注意到这题中,前导 \(0\) 不能够统计入答案,需要多一维表示前导 \(0\)。设 \(f_{pos,cnt,flag}\),\(pos\) 表示当前的位,\(cnt\) 表示当前出现了多少个目标数字。\(flag\) 表示当前位是否属于前导 \(0\)。
转移时将后继状态求和,边界是 \(pos\) 为 \(0\) 时返回 \(cnt\)。
#include<bits/stdc++.h>
using namespace std;
long long t1,t2,f[17][17][2];
int maxa,maxb,a[17],b[17];
long long dfs(int pos,int cnt,bool lim,bool f0,int a[],int d){
long long ans=0;
if(!pos)return cnt;
if(f[pos][cnt][f0]!=-1&&!lim)return f[pos][cnt][f0];
for(int i=0;i<=(lim?a[pos]:9);i++)ans+=dfs(pos-1,cnt+(i==d&&!(f0&&!i)),lim&&i==a[pos],f0&&!i,a,d);
if(!lim)f[pos][cnt][f0]=ans;
return ans;
}
int main(){
cin>>t1>>t2,t1--;
while(t1)a[++maxa]=t1%10,t1/=10;
while(t2)b[++maxb]=t2%10,t2/=10;
for(int i=0;i<=9;i++)memset(f,-1,sizeof(f)),cout<<dfs(maxb,0,1,1,b,i)-dfs(maxa,0,1,1,a,i)<<' ';
return 0;
}
[[动态规划]]

浙公网安备 33010602011771号