数位DP学习笔记

数位DP学习笔记

22年学习这部分内容的时候阳了,网课基本没听,省赛也很少考查这部分内容,于是一直荒废到现在,直到它出现在 2025年校赛网络预选赛的最后一道题,这才又把之前的例题重新做一遍,顺便学习一下数位 DP 这方面的内容。

总结在前

数位DP这种算法主要的适用情况:

  1. 计算某种数字有多少个(计算数字的个数),或者是一些由计数衍生出来的求值(如下面例题2)。
  2. 数字的范围可能很大。
  3. 所需要计算的信息和 数位 直接或者间接关系。

例题1

P2657 Windy数

题意

定义 Windy数 为:相邻两个数码相差至少为 \(2\) 的数。

给定 \([a,b]\) ,求其中有多少个 Windy 数。

分析

直接说算法,定义 \(f_{i,j}\) :满 \(i\) 位的,最高位填的是 \(j\) 的 Windy数 个数(可以有前导0)

上述也是数位DP中非常模板化的一种设计方式,也是必不可少的,因为在接下来统计答案的过程中至少会用到这两维信息。

预处理 f

这样的话,转移方程是十分容易写出的,阶段为 \(i\) ,直接枚举当前这一位填 \(0-9\) 中哪一个数字,上一位填哪一个数字,根据条件转移即可。

计算答案

这里用到了计数里面常见的前缀和思想,\(ans_{[l,r]}\) 等同于计算 \(ans_{[0,r]}-ans_{[0,l-1]}\)

于是我们考虑对于 \(x\) ,该如何计算出所有小于等于 \(x\) 的数中满足条件的数(利用 \(f\) 数组)。

为了方便讲话,举一个特例,\(x=456249\)

发现直接调用 \(f_{6,4}\) 会导致多算,所以有两种解决办法,把多算的减去,或者是舍弃这种方法,想一想怎么才能不多算。

由于后者更方便模板化,所以这里就只写后者的写法。

  1. 首先计算 \(f_{6,1}+f_{6,2}+f_{6,3}\) 这样就涵盖了 \([100000,399999]\) 中的所有数,这是可以通过前缀和直接优化的。

  2. 然后把所有 \(i\le5,j\ne0\)\(f\) 做一个前缀和,表示不满 \(6\) 位,并且没有前导0的数字个数,涵盖了 \([0,99999]\) 中的所有数。

  3. 从次高位开始逐位确定,对于第 \(5\) 位,我们考虑 \(\sum_{i=0}^9 f_{5,i},|i-4|\ge2\) ,表示可以在这一位取前导0,并且这一位必须和上一已经确定的位不冲突。相同的,对于第 \(4\) 为,考虑 \(\sum_{i=0}^9 f_{4,i},|i-5|\ge2\) 。同理可推知其他位。

上述三种求和,算出来的实际上是 \([0,x-1]\) 的结果,流程使然,我们只能考虑到小于 \(x\) 的数,不过无伤大雅,调用的时候 \(+1\) 即可。

Code

#include<bits/stdc++.h>
using namespace std;
const int N=20;
int f[N][N],sum[N];
inline void pre()
{
    for(int i=0;i<=9;++i)f[1][i]=1;
    sum[1]=9;
    for(int i=2;i<=10;++i)
    {
        for(int j=0;j<=9;++j)
        {
            for(int k=0;k<=9;++k)
            {
                if(abs(j-k)>1)f[i][j]+=f[i-1][k];
            }
            if(j)sum[i]+=f[i][j];
        }
    }
    for(int i=1;i<=10;++i)sum[i]+=sum[i-1];
}
inline int solve(int x)
{
    int len=0;
    int num[13];
    while(x)
    {
        num[++len]=x%10;
        x/=10;
    }
    int ans=sum[len-1];
    for(int i=1;i<num[len];++i)ans+=f[len][i];
    for(int i=len-1;i>=1;--i)
    {
        for(int j=0;j<num[i];++j)
        {
            if(abs(j-num[i+1])<2)continue;
            ans+=f[i][j];
        }
        if(abs(num[i]-num[i+1])<2)break;//这里要注意一下
    }  
    return ans;  
}
int main()
{
    pre();
    int a,b;
    cin>>a>>b;
    cout<<solve(b+1)-solve(a);
    return 0;
}

例题2

P2062数字计数

题意

给定 \([a,b]\) ,求在这个区间的所有正整数中,\(0-9\) 每个数码分别出现了多少次。

分析

不再是简单的计算数的个数,而是计算贡献,不过发现修改 \(f\) 数组的定义为 “出现的次数” 之后,仍然是可以进行正常转移的。

统计答案的时候也同样的分成三种情况考虑,然后求和就行。虽然说数位DP题目代码写出来都大差不差,但是在一些细节处的判断不同就尤为关键,比如这个题在统计答案的时候就得额外加一句话,windy数也是这样。

Code

#include<bits/stdc++.h>
using namespace std;
#define int unsigned long long
const int N=20;
int f[N][N][N];
int sum[N][N],p[N];
inline void pre()
{
    p[0]=1;
    for(int i=1;i<=15;++i)p[i]=p[i-1]*10ll;
    for(int i=0;i<=9;++i)f[1][i][i]=1;   
    for(int i=2;i<=12;++i)
    {
        for(int j=0;j<=9;++j)
        {
            for(int k=0;k<=9;++k)
            {
                for(int t=0;t<=9;++t)
                {
                    f[i][j][k]+=f[i-1][t][k];
                }
                if(j==k)f[i][j][k]+=p[i-1];
            }
        } 
    }
}
inline int calc(int x,int k)
{
    vector<int> num;
    int tmp=x;
    while(x)
    {
        num.push_back(x%10);
        x/=10;
    } 
    int ans=0;
    for(int i=1;i<num[num.size()-1];++i)ans+=f[num.size()][i][k];
    for(int i=num.size()-1;i>=1;--i)
        for(int j=1;j<=9;++j)
            ans+=f[i][j][k];
    for(int i=num.size()-1;i>=1;--i)
    {
        int v=num[i-1];
        for(int j=0;j<v;++j)ans+=f[i][j][k];
        for(int j=num.size();j>i;--j)
            if(num[j-1]==k)ans+=v*p[i-1];
    }


    return ans;
}
signed main()
{
    int a,b;
    cin>>a>>b;
    pre();
    for(int k=0;k<=9;++k)
    {
        cout<<calc(b+1,k)-calc(a,k)<<' ';
    }
    return 0;
}

例题3

2025校赛网络预选赛 D 题

题意

定义 \(f(x)\)\(x\) 的数位和,那么就有嵌套的定义 \(f(f(x))\) ...,对于某一个 \(x\) ,我们记录一下每次作用 \(f\) 之后的结果 \(res\) ,并把 \(x\) 变为 \(res\) ,直到 \(x<10\) ,以上 \(\sum res\) 就是 \(x\)答案。 举例来说, \(1999\) 的答案就是 \(28+10+1\)

现在给定一个 \(s\) ,保证 \(s=\sum_{i=1}^x ans_i\) ,求这个 \(x\)\(1\le s\le 10^{12}\)

形式化来讲,给定 \(s\) ,确定一个 \(x\) ,使得:

\[\sum_{i=1}^x\sum_{j=1}^{\infty}f^j(i)=s \]

分析

首先肯定是套一个二分。

但是由于没有复习数位DP,大概把这道题往 模 \(9\) 同余 的角度考虑了,也就是把所有可能的数字按照余数分类,发现这样一个树形结构从 个位数,十位数,百位数,然后突然就可以变得十分巨大,然后就不知道该怎么处理了。

学了之后,发现给DP多开一个状态,大概可以做一做这个题。

猜一猜,发现 可能最大的 \(x\) ,数位求和之后也不超过 \(200\) ,我们可以按照作用一次 \(f\) 函数的结果给所有的数分类,定义 \(f_{i,j,k}\) 为 “满 \(i\) 位数,最高位填 \(j\) ,作用一次后为 \(k\)” 的数字个数,这样就可以先预处理一下每个 \(k\) 对应的答案,然后 \(f_{i,j,k}\times ans_k\) 可以快速计算出求和的内容。

然后转移方程和上面提到的大差不差,注意一下考虑 \(ans_{1-9}\) 的特殊情况就可以了。

Code

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll f[20][10][154],ans[160],sum[20][160],ss[20][160];
inline ll getans(int x)
{
    ll ret=x;
    while(x>9)
    {
        int tmp=0;
        while(x)
        {
            tmp+=x%10;
            x/=10;
        }
        x=tmp;
        ret+=x;
    }
    return ret;
}
inline void pre()
{
    for(int i=1;i<=150;++i)ans[i]=getans(i);
    memset(f,0,sizeof f);
    for(int i=0;i<=9;++i)f[1][i][i]=1,ss[1][i]=1;
    for(int i=2;i<=15;++i)
    {
        for(int j=0;j<=9;++j)
        {
            for(int k=j;k<=150;++k)
            {
                f[i][j][k]+=ss[i-1][k-j];
                ss[i][k]+=f[i][j][k];
                if(j)sum[i][k]+=f[i][j][k];
            }
        }        
    }
    for(int k=1;k<=150;++k)
        for(int i=1;i<=15;++i)sum[i][k]+=sum[i-1][k];
}
inline ll calc(ll x)
{
    if(x<10)return 0;
    ll ret=0;
    int num[16],len=0;
    while(x)
    {
        num[++len]=x%10;
        x/=10;
    }
    for(int k=1;k<=150;++k)ret+=sum[len-1][k]*ans[k];
    for(int i=1;i<num[len];++i)
        for(int k=1;k<=150;++k)ret+=f[len][i][k]*ans[k];
    int pres=num[len];
    for(int i=len-1;i;--i)
    {
        for(int j=0;j<num[i];++j)
        {
            for(int k=pres;k<=150;++k)
            {
                ret+=f[i][j][k-pres]*ans[k];
            }
        }
        pres+=num[i];
    }
    return ret;
}//调用x+1
inline void solve()
{
    ll s;
    cin>>s;
    ll l=1,r=1e12;
    while(l<r)
    {
        ll mid=(l+r+1)>>1ll;
        if(calc(mid+1)<=s)l=mid;
        else r=mid-1;
    }
    cout<<l<<'\n';
}
int main()
{
    pre();
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    int T;cin>>T;
    while(T--)solve();
    // for(int i=10;i<=19;++i)cout<<calc(i)<<' ';
    return 0;
}
posted @ 2025-04-05 18:41  Hanggoash  阅读(37)  评论(0)    收藏  举报
动态线条
动态线条end