数位DP总结 BY----LZM
当你学会了数位DP,那么你会发现,在考场上,你也写不出来。
首先给出一道例题:
给出一个区间 \([l,r]\) ,求出区间内每个数字的数位和,答案 \(\bmod \ 998244353\),\(1\le l,r \le 10^{18}\)。
样例输入 样例输出
24 69 411
考虑枚举从 \(l\) 到 \(r\) ,对于每个数都拆分成数位,然后暴力的加。
复杂度很显然是 \(O(rlogr)\)爆chao,非常痛苦。
考虑优化,先想从 \(1\) 到 \(l-1\) ,再想从 \(1\) 到 \(r\) ,然后发现答案是一个前缀和的形式,两者相减即可得到答案。
所以我们只要有一个东西能够解决从 \(1\) 到 \(x\) 的答案就可以轻松的求出来,这里给出方法。
假设从高位枚举到低位,对于每一个数位都可能有10个数字(0,1,2....8,9),为什么说可能呢,因为可能这个数字太大了,会超过 \(x\) ,的上界,比如在枚举 \(x=123\)时,我们已经枚举到了第二位,
我们枚举的---》 \(1 \Box \Box\)
枚举的第一个数不能超过1,同样的,第二位不能超过二。
还有一种情况,我们枚举的---》 \(0 \Box \Box\)
发现有前导零的情况,那么对于这一种我们就要记录下来。
那么怎么实现呢???
记忆化!搜!索!
我们在 \(dfs\) 中记录几个参数:
- \(len\):代表目前从高位到低位枚举的位数
- \(lead\):代表是否有前导零
- \(top\):代表是否到了上界
- \(sum\):代表此时的数位和
然后在开一个 \(4\) 维的 \(dp\) 记忆化数组。
为什么记忆化可以?!
因为 \(dp\) 数组记录的状态是唯一的,如果当前状态已经被搜过了,那么后面搜的一定是一样的。
以下给出代码。
#include<bits/stdc++.h>
#define int long long
const int p=1e9+7;
int a[20];
int f[20][3][3][200];
int dfs(int len,int lead,int top,int sum)
{
if(!len)return sum;//没有位数了
if(!lead && !top && f[len][lead][top][sum]!=-1)return f[len][lead][top][sum];//状态被搜过了,直接回去
int bound=top?a[len]:9,res=0;//如果上界到了就枚举到这一位最多能枚举到的
for(int i=0;i<=bound;i++)res=(res+dfs(len-1,lead && !i,top && i==bound,sum+i))%p;
if(!lead && !top)f[len][lead][top][sum]=res;
return res;
}
int read()//快读就是好
{
int x=0,s=1;
char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')s=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-48;
return x*s;
}
signed main()
{
memset(f,-1,sizeof(f));//记得清空
int n=read()-1,ans=0,cnt=0;//初始n记得减1
while(n)a[++cnt]=n%10,n/=10;//从高位到低位
ans-=dfs(cnt,1,1,0);
cnt=0;
n=read();
memset(f,-1,sizeof(f));
while(n)a[++cnt]=n%10,n/=10;
ans+=dfs(cnt,1,1,0);
printf("%lld\n",(ans%p+p)%p);
return 0;
}
接着下一道题
不含前导零且相邻两个数字之差至少为 \(2\) 的正整数被称为 \(windy\) 数。在 \(a\) 和 \(b\) 之间,包括 \(a\) 和 \(b\) ,总共有多少个 \(windy\) 数?
这个和上面的差不多,只是要换一下参数
记录前一个数,以及前导零和上界就可以和上一个题一样了,但是要记得一开始传参要从-2开始,因为一开始可以随便填。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=30;
int n;
int a[N];
int ans;
int f[N][N][N][N];
void init(int x)
{
memset(a,0,sizeof(a));
n=0;
while(x)a[++n]=x%10,x/=10;
}
int dfs(int len,int pos,int lead,int top)
{
if(!len)return 1;
if(!top && !lead && f[len][pos][lead][top]!=-1)return f[len][pos][lead][top];
int bound=top?a[len]:9,res=0;
for(int i=0;i<=bound;i++)
{
if(abs(pos-i)<2)continue;
if(lead && !i)res+=dfs(len-1,-2,1,top && i==bound);
else res+=dfs(len-1,i,0,top && i==bound);
}
if(!lead && !top)f[len][pos][lead][top]=res;
return res;
}
int read()
{
int x=0,s=1;
char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')s=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-48;
return x*s;
}
signed main()
{
memset(f,-1,sizeof(f));
int x=read();
x--;
init(x);
ans-=dfs(n,-2,1,1);
x=read();
init(x);
memset(f,-1,sizeof(f));
ans+=dfs(n,-2,1,1);
cout<<ans;
return 0;
}
那么其他的就差不多了。
总结:记忆化搜索!
然后你就可以AK \(LSYOI\) 上面的数位DP了