数位DP学习笔记
数位DP学习笔记
22年学习这部分内容的时候阳了,网课基本没听,省赛也很少考查这部分内容,于是一直荒废到现在,直到它出现在 2025年校赛网络预选赛的最后一道题,这才又把之前的例题重新做一遍,顺便学习一下数位 DP 这方面的内容。
总结在前
数位DP这种算法主要的适用情况:
- 计算某种数字有多少个(计算数字的个数),或者是一些由计数衍生出来的求值(如下面例题2)。
- 数字的范围可能很大。
- 所需要计算的信息和 数位 直接或者间接关系。
例题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}\) 会导致多算,所以有两种解决办法,把多算的减去,或者是舍弃这种方法,想一想怎么才能不多算。
由于后者更方便模板化,所以这里就只写后者的写法。
-
首先计算 \(f_{6,1}+f_{6,2}+f_{6,3}\) 这样就涵盖了 \([100000,399999]\) 中的所有数,这是可以通过前缀和直接优化的。
-
然后把所有 \(i\le5,j\ne0\) 的 \(f\) 做一个前缀和,表示不满 \(6\) 位,并且没有前导0的数字个数,涵盖了 \([0,99999]\) 中的所有数。
-
从次高位开始逐位确定,对于第 \(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\) ,使得:
分析
首先肯定是套一个二分。
但是由于没有复习数位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;
}
本文来自博客园,作者:Hanggoash,转载请注明原文链接:https://www.cnblogs.com/Hanggoash/p/18810345

浙公网安备 33010602011771号