数位dp的学习
统计一个区间[le,ri]内满足一些条件数的个数
实质:
暴力枚举->记忆化->数位dp
通常两种解法:
(以bzoj1833为例)
1、记忆化搜索
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<cmath> #include<queue> #include<vector> using namespace std; #define REG register #define REP(i,x,y) for(REG int i=x;i<=y;i++) #define UP(i,x,y) for(REG int i=x;i>=y;i--) #define IN inline #define ll long long #define mm(x) memset(x,0,sizeof(x)) ll A[10],B[10],f[20][11],a[20],p[10]; ll dfs(ll* A,ll h,bool limit,bool lead){ if(h==0) return 1; if(!limit && !lead && f[h][10]!=-1){ REP(i,0,9) A[i]+=f[h][i]; return f[h][10]; } int end=limit?a[h]:9; ll ans=0; REP(i,0,end){ if(limit && i==end){ p[i ]=dfs(A,h-1,1,lead&&i==0); A[i]+=p[i]; ans+=p[i]; } else{ p[i]=dfs(A,h-1,0,lead&&i==0); A[i]+=p[i]; ans+=p[i]; } if(lead && i==0) A[i]-=p[i]; } if(!limit && !lead){ REP(i,0,9) f[h][i]=f[h-1][i]*10+p[i]; f[h][10]=ans; } return ans; } void solve(ll* A,ll x){ ll n=0; if(x==0){return;} while(x>0){ a[++n]=x%10; x/=10; } dfs(A,n,1,1); } int main(){ ll l,r; scanf("%lld %lld",&l,&r); REP(i,0,20) f[i][10]=-1; solve(B,l-1);solve(A,r); REP(i,0,8) printf("%lld ",A[i]-B[i]); printf("%lld\n",A[9]-B[9]); return 0; }
2、dp+递归
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<cmath> #include<queue> #include<vector> using namespace std; #define REG register #define REP(i,x,y) for(REG int i=x;i<=y;i++) #define UP(i,x,y) for(REG int i=x;i>=y;i--) #define IN inline #define ll long long #define mm(x) memset(x,0,sizeof(x)) const int maxn=16,maxm=20; struct sw{ ll a[10]; }; int a[maxm]; ll A,B,fac[maxm]; sw f[maxn][maxn]; sw operator +(sw a,sw b){ sw c; REP(i,0,9) c.a[i]=a.a[i]+b.a[i]; return c; } sw dp(ll num){ int len=0; sw ans; REP(i,0,9) ans.a[i]=0; ans.a[0]=1; if(!num) return ans; ll number=num; while(num){ a[++len]=num%10; num/=10; } REP(i,1,len-1){ REP(j,1,9) ans=ans+f[i][j]; } UP(i,len,1){ for(int j=(i==len);j<a[i];j++){ ans=ans+f[i][j]; } number%=fac[i-1]; ans.a[a[i]]+=number+1; } //for(int k=0;k<=9;k++)printf("%d ",ans.a[k]);puts(""); return ans; } int main(){ fac[0]=1; REP(i,1,maxn) fac[i]=fac[i-1]*10; REP(j,0,9) f[1][j].a[j]=1; REP(i,2,maxn){ REP(j,0,9){ REP(k,0,9){ f[i][j]=f[i][j]+f[i-1][k]; f[i][j].a[j]+=fac[i-2]; } //printf("[%d][%d]",i,j); //for(int k=0;k<=9;k++)printf("%d ",f[i][j].a[k]); //puts(""); } } scanf("%lld %lld",&A,&B); sw cA=dp(A-1),cB=dp(B); REP(i,0,8) printf("%lld ",cB.a[i]-cA.a[i]); printf("%lld\n",cB.a[9]-cA.a[9]); return 0; }
高位限制 imit&&i==a[h]
前导零 lead&&i==0
数位 h=h-1(第0位是个位,第-1位直接返回)
前缀状态 state(表示(h,len]的状态)
tips:
1、减法的艺术:
记忆化前缀和为sum
对后面还有all-sum需求时满足条件的个数
2、计数转求和:
求解满足条件的数字的和
对于每一位符合条件的i,即sum+=i*10^pos*cnt(cnt为数字个数),同时记忆化数字个数num和总和sum即可
求第n个数字,预处理f[i]表示长度为i的数字(不含前导0)的个数,然后从最高位开始按位确定
例题:
数字1的数量(nod51 1009
数字计数(bzoj 1833
windy数(bzoj 1026
self同类分布(bzoj 1799
储能表(bzoj 4513