概述
数位动态规划(数位DP)主要用于解决“在区间这个范围内,满足某种约束的数字的数量、总和、平方“这一类问题。
这种问题一般有两种解法,记忆化搜索和迭代法,本文主要讲解记忆化搜索。
本文总结了数位的
搜索函数的几种常见形参,及对于数字的约束与形参之间转换,后有几道例题,用于更加系统化、套路化的攻克这一类题目。
例题
例题1烦人的数学作业
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int mod=1e9+7,N=20;
int a[N];
int f[N][9*18+5];
ll dfs(int pos,bool limit,int sum)
{
if(!pos)
return sum;
if(!limit && (~f[pos][sum]))
return f[pos][sum];
int up=limit ? a[pos]:9;
ll res=0;
for(int i=0;i<=up;i++)
res=(res+dfs(pos-1,limit&&i==up,sum+i))%mod;
if(!limit)
f[pos][sum]=res;
return res;
}
ll solve(ll x)
{
int cnt=0;
while(x>0)
{
a[++cnt]=x%10;
x/=10;
}
return dfs(cnt,true,0);
}
int t;
int main()
{
cin>>t;
while(t--)
{
memset(f,-1,sizeof f);
ll l,r;cin>>l>>r;
ll ans=(solve(r)-solve(l-1)+mod)%mod;
if(ans<0)
ans+=mod;
cout<<ans<<"\n";
}
return 0;
}
还有一题与之类类似活页码
看完上面可以练一下熟悉熟悉
//首先我们先来看一下有哪些状态,sum数字之和,当前位置
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int N=45*9+7;
ll f[10][N];
int a[10];
ll dfs(int pos,bool limit,int sum)
{
if(!pos)
return sum;
auto &v=f[pos][sum];
if(!limit&&~v)
return v;
int up=limit?a[pos]:9;
ll res=0;
for(int i=0;i<=up;i++)
{
res=res+dfs(pos-1,limit&&i==up,sum+i);
}
if(!limit)
v=res;
return res;
}
ll solve(ll x)
{
int len=0;
while(x>0)
{
a[++len]=x%10;
x/=10;
}
return dfs(len,true,0);
}
int main()
{
memset(f,-1,sizeof f);
ll l;
cin>>l;
cout<<solve(l);
return 0;
}
例题2数字计数
思路
首先我们需要注意的一点是,对于数码0,我们需要考虑一下前导为0的情况,例如 ,
这个数字出现的次数为1,而并非3。
因此我们的形参里面需要一个参数
表示有没有前导零
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
int a[12],ans[12];
ll f[12][12][2];//最后一位表示是否含有前导0
int digit;
ll dfs(int pos,bool limit,bool lead0,int cnt)
{
if(!pos)
return cnt;
auto &v=f[pos][cnt][digit!=0];//如果这个数字不等于0,那么不可能成为前导0
if(!limit&&!lead0&&~v)//取反操作,表示不等于-1,即说明已经搜过他了就没必要再继续搞他
return v;
int up=limit?a[pos]:9;//当前数字是否处于最大位上,如果处于,那么后一位就只能达到他输入的那位,而不能随便取取到9
int res=0;
for(int i=0;i<=up;i++)
{
int tmp=cnt+(i==digit);
if(lead0&&i==0&&digit==0)
tmp=0;
res+=dfs(pos-1,limit&&i==up,lead0&&i==0,tmp);
}
if(!limit&&!lead0)
v=res;//这里联系到上面的return v那句话,用v来保存了答案,然后下一层循环如果循环到他了就直接返回了,形成记忆化,减少代码运行时间
return res;
}
void solve(ll x,int f)
{
int len=0;
while(x>0)
{
a[++len]=x%10;
x/=10;
}
for(int i=0;i<=9;i++)
{
digit=i;
ans[i]+=f*dfs(len,true,true,0);
}
}
int main()
{
ll n,m;
memset(f,-1,sizeof f);
//memset(a,0,sizeof a);
//memset(ans,0,sizeof ans);
//if(m<n)
// swap(n,m);//有多组数据一定要记得初始化
solve(m,1);solve(n-1,-1);
for(int i=0;i<=9;i++)
{
cout<<ans[i]<<" ";
}
return 0;
}
例题3windy 数
#include<iostream>
#include<cmath>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=10,inf=1e9;
int a[N];
int f[N][N];
int dfs(int pos,bool limit,bool lead0,int last)
{
if(!pos)
return 1;
if(!limit&&last!=inf&&~f[pos][last])
return f[pos][last];
int up=limit?a[pos]:9;
int res=0;
for(int i=0;i<=up;i++)
{
if(lead0)
res=res+dfs(pos-1,limit&&i==up,lead0&&i==0,i==0?last:i);
else
if(abs(last-i)>=2)
res=res+dfs(pos-1,limit&&i==up,false,i);
}
if(!limit&&last!=inf)
f[pos][last]=res;
return res;
}
int solve(int x)
{
int len=0;
while(x>0)
{
a[++len]=x%10;
x/=10;
}
return dfs(len,true,true,inf);
}
int main()
{
memset(f,-1,sizeof f);
int l,r;
cin>>l>>r;
cout<<solve(r)-solve(l-1);
return 0;
}
例题4花神的数论题
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long LL;
const int N=60,mod=10000007;
int a[60];
LL f[N][N];
LL dfs(int pos,bool limit,int cnt)//只需要限制不超过最大位数
{
if(!pos)
return max(cnt,1);//因为是乘法,所以我们得考虑到如果不满足的时候,等于0,会把所有答案都变成0
if(!limit && ~f[pos][cnt])
return f[pos][cnt];
LL res=1;
int up=limit?a[pos]:1;
for(int i=0;i<=up;i++)
{
res=res*dfs(pos-1,limit &&(i==up),cnt+(i==1))%mod;//只有等于1的时候才相加,这样写省去一个if
}
if(!limit)
f[pos][cnt]=res;
return res;
}
LL solve(LL x)
{
int len=0;
while(x>0)
{
a[++len]=x%2;
x/=2;
}
return dfs(len,true,0);
}
int main()
{
memset(f,-1,sizeof f);
LL n;
cin>>n;
cout<<solve(n);
return 0;
}
例题5Round Numbers S
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=32;
int a[N];
int f[N][N][N];
int dfs(int pos,bool limit,bool lead0,int num0,int num1)
{
if(!pos)
return num0>=num1;
auto &v=f[pos][num0][num1];
if(!limit&&~v&&!lead0)
return v;
int up=limit?a[pos]:1;
int res=0;
for(int i=0;i<=up;i++)
{
bool next0=lead0&&i==0;
int s0=next0?0:num0+(i==0);
int s1=next0?0:num1+(i==1);
res=res+dfs(pos-1,limit&&i==up,next0,s0,s1);
}
if(!limit)
v=res;
return res;
}
int solve(int x)
{
int len=0;
while(x>0)
{
a[++len]=x%2;
x/=2;
}
return dfs(len,true,true,0,0);
}
int main()
{
memset(f,-1,sizeof f);
int l,r;
cin>>l>>r;
cout<<solve(r)-solve(l-1);
return 0;
}
例题6手机号码
//我们先确定一下参数pos表示当前位置,last1上一位,last2上两位,same表示是否相等,have4是否出现过4,have8是否出现国8
//f[pos][last1][last2][same][have4][have8]
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int N=12;
int a[N];
ll f[N][N][N][2][2][2];
int len;
//dfs需要的参数同样有pos当前位置,limit限制不能超过当前位的最大值,last1上一位,last2上两位,same表示是否相等,have4是否出现过4,have8是否出现国8
ll dfs(int pos,bool limit,int last1,int last2,bool same,bool have4,bool have8)
{
if(!pos)
return same&&((have4&&have8)?0:1);//have4和have8同时成立那就不满足,返回0
auto &v=f[pos][last1][last2][same][have4][have8];
if(!limit&&~v)//记忆化返回减少运行时间,
return v;
int up=limit?a[pos]:9;
ll res=0;
int falst=pos==len?1:0;//首位必须从1开始,就是不能含有前导0,长度等于当前传入的位相等的时候就是最高位
for(int i=falst;i<=up;i++)
{
bool tmp=same||(last1==i&&last2==i);//用same是因为后续要把tmp传给same,后面是same的定义
if(i==4&&!have8)//have4的定义不能直接用have4
res+=dfs(pos-1,limit&&i==up,i,last1,tmp,true,false);
//下一位的上一位就是现在取的这位数,所以传i,依次类推
else if(!have4&&i==8)
res+=dfs(pos-1,limit&&i==up,i,last1,tmp,false,true);
else if(i!=4&&i!=8)
res+=dfs(pos-1,limit&&i==up,i,last1,tmp,have4,have8);
//还需要判断一下,因为如果i不是4也不是8也要递归一下,后面可能会出现是4或8,不加可能会漏数
}
if(!limit)
v=res;
return res;
}
ll solve(ll x)
{
if(x<1e10)
return 0;//记得特判一下不合法号码
len=0;
while(x>0)
{
a[++len]=x%10;
x/=10;
}
return dfs(len,true,10,10,false,false,false);
//填大于9或者小于0的都可以,不要跟后面的数字重了
}
int main()
{
memset(f,-1,sizeof f);
ll l,r;
cin>>l>>r;
cout<<solve(r)-solve(l-1);
return 0;
}
例题7Magic Numbers
//我们先来定义一些参数,r表示余数,当余数为0的时候我们就可以判断该数字满足条件
//我们定义odd来判断当前位是奇数还是偶数
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int N=2005,mod=1e9+7;
int a[N],len;
ll f[N][N];
int d,m;
ll dfs(int pos,bool limit,int r)
{
if(!pos)
return r==0;
if(!limit&&~f[pos][r])
return f[pos][r];
int up=limit?a[pos]:9;
ll res=0;
bool t=(len+1-pos)&1;//举个例子,1234,pos刚开始是等于4,代表第一位,使用我们这里还需要反转一下第几位
if(t)
{//奇数位置,不能让他等于d,
for(int i=0;i<=up;i++)
{
if(i==d)
continue;
res=(res+dfs(pos-1,limit&&i==up,(r*10+i)%m))%mod;
}
}
else//偶数位置必须让他等于d
{
if(d<=up)//减轻代码量,如果一个数的最高位都还没有d大的话,必然就不是
res=(res+dfs(pos-1,limit&&d==up,(r*10+d)%m))%mod;
}
if(!limit)
f[pos][r]=res;
return res;
}
ll solve(string x)
{
len=x.size();
for(int i=0;i<len;i++)
a[len-i]=x[i]-'0';
return dfs(len,true,0);
}
bool cheak(string x)
{
int r=0;
len=x.size();
for(int i=0;i<len;i++)
a[i+1]=x[i]-'0';
for(int i=1;i<=len;i++)
{
if(i&1)//是奇数位
{
if(d==a[i])
return false;
}
else
{
if(d!=a[i])
return false;
}
r=(r*10+a[i])%m;//每一位都取余数防止数字过大
}
return r==0;//如果余数等于0,那就说明满足条件了,返回1
}
int main()
{
memset(f,-1,sizeof f);
string l,r;
cin>>m>>d>>l>>r;
cout<<(solve(r)-solve(l)+cheak(l)+mod)%mod;//数据范围太大
}
例题8Salazar Slytherin's Locket
//定义参数,st表示是奇数还是偶数,lead0表示有没有前导0的情况
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int N=61;
int b,a[N];
ll f[11][N][(1<<11)];//f[pos][st]当前位置
ll dfs(int pos,bool limit,bool lead0,int st)
{
if(!pos)
return (st==0&&!lead0) ?1:0;
auto &v=f[b][pos][st];
if(!limit&&!lead0&&~v)
return v;
ll res=0;
int up=limit?a[pos]:b-1;
for(int i=0;i<=up;i++)
{
int tmp=(lead0&&i==0)?0:st^(1<<i);//这里&&的优先级高于?:
//如果每个数都出现偶数次那取异或的结果就是0,否则就为1,对应边界判断是否为0
res+=dfs(pos-1,limit&&i==up,lead0&&i==0,tmp);
}
if(!limit&&!lead0)
v=res;
return res;
}
ll solve(ll x)
{
int len=0;
while(x>0)
{
a[++len]=x%b;
x/=b;
}
return dfs(len,true,true,0);
}
int main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int t;cin>>t;
memset(f,-1,sizeof f); //如果 每次都初始化t很大的时候就会超时,所以我们需要在f上多开一维,开10层,比起t最大到10的三次方更划算
while(t--)
{
ll l,r;cin>>b>>l>>r;
cout<<solve(r)-solve(l-1)<<"\n";
}
return 0;
}
近期较忙,后续会给出详解,敬请期待