数位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
",A[9]-B[9]);
     
    return 0;
}
View Code

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
",cB.a[9]-cA.a[9]);
    
    return 0;
}
View Code

高位限制 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

原文地址:https://www.cnblogs.com/EvfX/p/8763279.html