数位DP

数位DP

核心思想

当答案和某个数的每一位之间有强相关联系时,可以以每一个位置作为阶段来DP。

P2602 数字计数

给定两个正整数 \(a,b\) , 求在 \([a,b]\) 中每个数码出现了多少次。

暂且不考虑前导零,那么对于满 \(i\) 位的数:

  • 所有数字的出现次数相同

设数组 \(dp_i\) 为满 \(i\) 位数中每个数字出现的次数,那么就可以有转移:

\(dp_i=10\times dp_{i-1}+10^{i-1}\)

然后需要考虑怎么处理(这部分较简单(差分,对每个数单独求),直接跳过)。

还需要单独考虑前导零怎么处理,第 \(i\) 位为前导零时,此时 \(1\)\(i-1\) 位都是 \(0\) , 额外减去 \(i-1\) 位填满的贡献。

P13085 windy数

不含前导零且相邻两个数字之差至少为 \(2\) 的正整数被称为 windy 数。求在 \(a\) 和 \(b\) 之间,包括 \(a\) 和 \(b\),总共有多少个 windy 数?

\(f[i][j]\) 表示前 \(i\) 位,最后一位为 \(j\) 的方案数,发现非常好求。

接着考虑怎么做,

仍然是前导零非常难处理。

设要处理的数字 \(x\) 的位数为 \(len\) ,先处理位数 \(<len\) 的数,可以直接求出来。

接下来处理位数刚好等于 \(len\) 的情形。

那么 \(for\ i:len-1\)

\(i\) 个循环用来求解前 \(i\) 位都与 \(x\) 的前 \(i\) 位相同的方案数。

第一位不能为 \(0\) 其余可以。

就做完了:

ll solve(ll x){
    for (int i=1;i<len;i++){
        for (int j=1;j<=9;j++){
            ans+=f[i][j];
        }
    }
    int last=-2;//首位都可以选
    for (int i=len-1;i>=0;i--){
        int num=x/arr[i];//当前数字
        int st;
        if (i==len-1) st=1;
        else st=0;//首位不能选0,其余可以
        if (i==0){//最后一位只用考虑自己
            for (int j=st;j<num;j++){
                if (abs(j-last)<2) continue;
                ans++;
            }
      if (abs(last-num)<2){
                flag=false;
                break;
            }
            continue;
        }
        for (int j=st;j<num;j++) {
            if (abs(j-last)<2) continue;
            for (int k=0;k<=9;k++) {
                  if (abs(j-k)<2) continue;
                   ans+=f[i][k];
            }
        }
        if (abs(last-num)<2){
            flag=false;
            break;
        }//这说明前i位不能做到紧贴上限,那么后面都不用继续循环了
        last=num;
        x%=arr[i];
    }
    return ans;
}

阶段总结:

由此注意到递推的方法中非常难处理前导0,由此改用记忆化搜索版本。

这种版本非常好写好想(尤其是在前导0的处理上),且不用初始化。

记忆化搜索版本:

\(dfs(int\ pos,int\ last,bool\ limit,bool\ lead)\) 表示当前处理的是 \(pos\) 位,上一位是 \(last\) , 是否紧贴上限,前面是否是前导零。

具体直接处理就行了。

ll dp[20][22][2][2];
ll arr[22];//每一位的数
ll dfs(int pos,int last,bool limit,bool lead){
    if (pos==0) return lead?0:1;
    if (dp[pos][last][limit][lead]!=-1){
        return dp[pos][last][limit][lead];//记忆化
    }
    ll ans=0;
    ll up=limit?arr[pos]:9;//如果前面紧贴那么最多取到arr[pos],否则可以最多取到9
    for (int i=0;i<=up;i++){
        if (abs(last-i)<2) continue;
        int if_lead=lead&&(i==0);//当前还是不是前导零
        int if_limit=limit&&(i==arr[pos]);//当前还有没有限制
        if (if_lead){
            ans+=dfs(pos-1,20,if_limit,1);
        }
        else{
            ans+=dfs(pos-1,i,if_limit,0);
        }
    }
    return dp[pos][last][limit][lead]=ans;
}
ll solve(ll x){
    memset(dp,-1,sizeof dp);
    arr[0]=0;
    while(x){
        arr[++arr[0]]=x%10;
        x/=10;
    }//获取每一位的数以及初始化dp
    return dfs(arr[0],20,1,1); 
}
int main(){
    cin >> a >> b;
    cout << solve(b)-solve(a-1);
    return 0;
}


posted @ 2026-09-06 16:59  wlhao  阅读(3)  评论(0)    收藏  举报