记忆化搜索写数位DP就是爽
优点: 1.代码短 2.细节比较少
总结了一下,数位DP也有模板的
int dfs记忆化搜索
(int 当前位数,int 记忆化搜索要保留的条件(一些特殊情况对答案有影响),int limit(前面有没有放满)){
if(当前位数==0) 如果满足答案要求 return 1;否则return 0;
if(!limit&&~f[当前位数][...]) return f[当前位数][...];
int end=limit?a[当前位数]:9;下一位从0枚举到end,如果前面放满了就只能放到a[当前位数]
int ans=0;
for(int i=0;i<=end;i++){
ans+=dfs(当前位数-1,i是不是特殊情况,limit&&i==end);
//需要满足前面都放满,又要满足这位放满
}
if(!limit) f[当前位数][...]=ans;
return ans;
}
中间那个点是很灵活的,需要根据自己的需求来加
另外,f[i][...]是当前位数为i的所有情况,如果前面放满了,f[i][...]也就不是所有情况了
先来看看例题理解一下
不要62
求l-r不含62和4的所有个数
分析
我们枚举下一位时,如果是4可以直接跳过
但如果是2,且前面一位是6呢
我们发现我们需要记录前面一位是不是6
如果前面一位是6与枚举的下一位是2就跳过
#include<bits/stdc++.h>
using namespace std;
int a[30],l,r;
int f[30][2];
int dfs(int d,bool is6,bool limit){
if(d==0) return 1;
if(!limit&&~f[d][is6]) return f[d][is6];
int cur=0,end=(limit?a[d]:9);
for(int i=0;i<=end;i++){
if((is6&&i==2)||i==4) continue;
cur+=dfs(d-1,i==6,limit&&i==end);
}
if(!limit) f[d][is6]=cur;
return cur;
}
int solve(int x){
memset(a,0,sizeof(0));
int cnt=0;
if(!x) a[++cnt]=0;
while(x){a[++cnt]=x%10;x/=10;}
return dfs(cnt,0,1);
}
int main(){
memset(f,-1,sizeof(f));
cin>>l>>r;
cout<<solve(r)-solve(l-1);
return 0;
}
注意a数组一般这样设
原数 18734
a[1]=4,a[2]=3,a[3]=7,a[4]=8,a[5]=1
Balenced Number
l-r之间,有多少个可以选一个支点,左右两边力矩和相等的数
如4139 3为支点,4*2+1*1=9*1
分析
我们分析发现,需要增添一个左右的差,当全部位放满之后,若差为0 return1
另外,我们枚举支点的位置,每一种位置对应有一些答案
全部为0的情况需要减去
代码
#include<bits/stdc++.h>
#define LL long long
using namespace std;
LL dp[20][20][2000];//当前位,支点,左边比右边大多少
LL l,r,a[20];
LL dfs(LL cur,LL pos,LL sum,LL limit){
if(cur==0) return sum==0;
if(sum<0) return 0;//我们先把左边的加上,右边的来减,还没完就减不够了就return了
if(!limit&&~dp[cur][pos][sum]) return dp[cur][pos][sum];
LL ans=0,end=limit?a[cur]:9;
for(LL i=0;i<=end;i++){
ans+=dfs(cur-1,pos,sum+(cur-pos)*i,limit&&i==end);
}
if(!limit) dp[cur][pos][sum]=ans;
return ans;
}
LL solve(LL x){
LL cnt=0,ans=0;
while(x){a[++cnt]=x%10;x/=10;}
for(int i=1;i<=cnt;i++){//枚举支点
ans+=dfs(cnt,i,0,1);
}
return ans-(cnt-1);
}
int main(){
memset(dp,-1,sizeof(dp));
scanf("%lld%lld",&l,&r);
printf("%lld",solve(r)-solve(l-1));
return 0;
}
Beautiful Number
如果一个数能够被其每个数位的数都整除,那么这个数就叫做美丽数。
给定一个区间 [x,y] ,计算该区间内有多少个美丽数。
分析
如果能全部整除,相当于一个数被它的全部数位的最小公倍数整除
另外,设这个数为x,最小公倍数为y
因为y|2520,2520为1-9的最小公倍数
若y|x,则y|(x%2520)
设ay=2520,by=x
x%2520=by-k*ay,y*(b-a*k)=x%2520所以y|(x%2520)
因此,我们需要保留一个mod,当到下一位时,mod=(mod*10+i)%2520
实验发现,数位DP带模之类的,都有一个mod(除以2520的余数)
并且这个mod的上限可以优化到252,这里不再赘述
现在的空间20*2520*2520 还需要优化
我们发现,lcm一定是2520的约数,因此我们可以离散化一下
发现2520的约数只有48个
因此空间优化到20*2520*50 OK
#include<bits/stdc++.h>
#define LL long long
using namespace std;
LL f[20][2525][50];//位数,模2520是多少,前面最小公约数离散化之后
LL l,r; int a[20],hash[2525];
int gcd(int a,int b){return b?gcd(b,a%b):a;}
int LCM(int a,int b){return a/gcd(a,b)*b;}
LL dfs(int cur,int mod,int lcm,int limit){
if(cur==0) return (mod%lcm)==0;
LL &res=f[cur][mod][hash[lcm]];
if(!limit&&~res) return res;
LL ans=0; int end=limit?a[cur]:9;
for(int i=0;i<=end;i++){
ans+=dfs(cur-1,(mod*10+i)%2520,i?LCM(lcm,i):lcm,limit&&i==end);
}
if(!limit) res=ans;
return ans;
}
LL solve(LL x){
int cnt=0;
while(x){a[++cnt]=x%10;x/=10;}
return dfs(cnt,0,1,1);
}
int main(){
memset(f,-1,sizeof(f));
int cnt=0;
for(int i=1;i<=2520;i++)//离散化
if(2520%i==0) hash[i]=++cnt;
scanf("%lld%lld",&l,&r);
printf("%lld",solve(r)-solve(l-1));
return 0;
}
Windy数
不含前导零且自身相邻两个数字之差至少为 2 的正整数(1位或多位数)被称为 Windy 数。注:单个数字 1~9 都是 Windy 数。
分析
我们发现需要记录上一位是什么,代码也很容易写出
#include<bits/stdc++.h>
#define LL long long
using namespace std;
LL l,r,f[20][11];int a[20];
LL dfs(int cur,int last,int limit){
if(cur==0) return 1;
LL &res=f[cur][last];
if(!limit&&last>=0&&~res) return res;//last小于0是指前面的都是0
int end=limit?a[cur]:9; LL ans=0;
for(int i=0;i<=end;i++){
if(abs(i-last)<2) continue;
ans+=dfs(cur-1,(!i&&last==-10)?-10:i,limit&&i==end);
}
if(last>=0&&!limit) res=ans;
return ans;
}
LL solve(int x){
int cnt=0;
while(x){a[++cnt]=x%10;x/=10;}
return dfs(cnt,-10,1);
}
int main(){
memset(f,-1,sizeof(f));
scanf("%lld%lld",&l,&r);
printf("%lld",solve(r)-solve(l-1));
return 0;
}