数位dp
数位dp
简介
数位 \(dp\) 是一种在数位上进行的 \(dp\),通常用于解决值域 \([L,R]\) 中有几个数满足条件,且 \([L,R]\) 极大 (如 \(1\le L\le R\le 1e18\)) 的问题,这时我们就会在数位上进行 \(dp\),问题规模变为 \(\lg R\) 的
数位 \(dp\) 就是一次考虑数的每一位,且从高到低枚举 (因为高位限制低位的取值范围)
我们通常用 \(lim\) 变量来表示更高的数位是否都与 \(R\) 的对应位相同
如:\(R=123\),若前面两位取的 \(12\),那第 \(3\) 位只能为 \(0\sim 3\),若不是,则可以取 \(0\sim 9\)
我们通常使用记忆化搜索来实现数位 \(dp\)
数位 \(dp\) 模板:
inline ll dfs(int pos,int sum,int lim,...){
if(!pos) {...}//数字填完了
if(~f[pos][sum][rm][lim]) return f[pos][sum][rm][lim];//记忆化
int mx=lim?a[pos]:9;//当前位最大是多少
ll res=0;
for(int i=0;i<=mx;++i)
res+=dfs(pos+1,sum+i,lim&(i==mx),...);//填数
f[pos][sum][rm][lim]=res;//记忆化
return res;
}
题
https://vjudge.net/contest/513260
同类分布
以这道题为例说明一下数位 \(dp\) 的常规解法
记搜的时候我们记录 \(4\) 个变量:
\(pos\):当前搜到第几位
\(sum\):各位数字之和
\(rm\):原数对当前枚举的模数 \(mod\) 取模的结果
\(lim\):前几位是否和原数相同
这题我们的思路是枚举模数 \(mod\),然后通过记搜记录有几个符合要求,并统计答案
#include<bits/stdc++.h>
using namespace std;
#define ll long long
int len,a[20],mod;
ll l,r,f[20][180][180][2];
inline ll dfs(int pos,int sum,int rm,int lim){
if(pos>len&&!sum) return 0;
if(pos>len) return (rm==0&&sum==mod);
if(~f[pos][sum][rm][lim]) return f[pos][sum][rm][lim];
int mx=lim?a[len-pos+1]:9;
ll res=0;
for(int i=0;i<=mx;++i)
res+=dfs(pos+1,sum+i,1ll*(1ll*rm*10ll+i)%mod,lim&(i==mx));
f[pos][sum][rm][lim]=res;
return res;
}
inline ll solve(ll x){
len=0;
do{
a[++len]=x%10;
x/=10;
}while(x);
ll res=0;
for(mod=1;mod<=9*len;++mod){
memset(f,-1,sizeof(f));
res+=dfs(1,0,0,1);
}
return res;
}
signed main(){
cin>>l>>r;
cout<<solve(r)-solve(l-1);
}
\(\text{Balanced Number}\)
这个中心十分特殊,且只有 \(18\) 种选择,尝试从这个中心入手
我们可以枚举以哪一位为中心,就方便统计答案了
我们记搜时记录一个变量 \(sum\) 表示当前中心左边的值-中心右边的值
当枚举到最后一位且 \(sum=0\) 时就可以统计答案
然后就是常规套路了
#include<bits/stdc++.h>
using namespace std;
#define ll long long
int len,cur,a[20];
ll l,r,f[20][2800][2];
inline ll dfs(int pos,ll sum,int lim){
if(pos>len) return (!sum);
if(sum<0) return 0;//小剪枝
if(~f[pos][sum][lim]) return f[pos][sum][lim];
int mx=lim?a[len-pos+1]:9;
ll res=0;
for(int i=0;i<=mx;++i)
res+=dfs(pos+1,sum+i*(cur-pos),lim&(i==mx));
return f[pos][sum][lim]=res;
}
inline ll solve(ll x){
if(x==-1) return 0;
if(!x) return 1;
len=0;
do{
a[++len]=x%10;
x/=10;
}while(x);
ll res=0;
for(cur=1;cur<=len;++cur){
memset(f,-1,sizeof(f));
res+=dfs(1,0,1);
}
return res-len+1;
}
signed main(){
int T;
cin>>T;
while(T--){
cin>>l>>r;
cout<<solve(r)-solve(l-1)<<endl;
}
}
吉哥系列故事--恨7不成妻
这个题需要记忆化 \(3\) 个东西:
\(cnt_s,sum_s,sqr_s\),分别表示当前状态 \(s\) 下有多少个数满足条件,这些数的和是多少,平方和是多少
然后再拿平方和公式去推
反正很复杂,知道大概思路就行了
\(\text{Beautiful numbers}\)
各个数位上的数字都是原数的因数等价于各个数位上的数字的最小公倍数都是原数的因数
我们显然需要记录各个数位上的数字的最小公倍数 (因为数组开不下)
见小套路,我们也需要记录取模的结果 \(rm\)
但是由于模数在变,我们需要一些变通
有一个小小的性质:
如果 \(b\bmod a=0\) 那么 \(c\bmod a=(c\bmod b)\bmod a\)
那么我们就可以记录 \(rm=\) 原数模 \(2520\) 的值,枚举到最后一位时判断 \(rm\equiv 0\pmod {lcm}\) 是否成立即可
那就需要记录 \(pos\) (最大为 \(18\)),\(rm\) 和 \(lcm\) (最大为 \(2520\))
空间:\(19\times 2521^2\times 4=483013516\text{ B}=471692.9\text{ KB}=460.6 \text{MB}\)
炸空间了
通过题解我们得知可以:\(lcm\) 是 \(2520\) 的约数,最多只有 \(\sqrt{2520}\) 种取值,我们开个桶记录一下就可以了
这题有多组数据,但是你记录 \(lim\) 的话每组数据要清空 \(dp\) 数组,会 \(TLE\)
所以不记 \(lim\),这样得到的 \(dp\) 数组可以通用,不用清空,就可以通过此题
好毒瘤!好多细节!
#include<bits/stdc++.h>
using namespace std;
#define ll long long
int len,cnt,a[20],id[2521];
ll l,r,f[20][2521][50];
inline ll dfs(int pos,int rm,int lcm,int lim){
if(!pos) return rm%lcm==0;
if(!lim&&~f[pos][rm][id[lcm]]) return f[pos][rm][id[lcm]];
int mx=lim?a[pos]:9;
ll res=0;
for(int i=0;i<=mx;++i)
res+=dfs(pos-1,(rm*10+i)%2520,i?lcm/__gcd(lcm,i)*i:lcm,lim&(i==mx));
if(!lim) f[pos][rm][id[lcm]]=res;
return res;
}
inline ll solve(ll x){
len=0;
do{
a[++len]=x%10;
x/=10;
}while(x);
return dfs(len,0,1,1);
}
signed main(){
memset(f,-1,sizeof(f));
for(int i=1;i<=2520;++i)
if(2520%i==0) id[i]=++cnt;
ios::sync_with_stdio(0);
int T;
cin>>T;
while(T--){
cin>>l>>r;
cout<<solve(r)-solve(l-1)<<endl;
}
}
小套路
有关原数的因数
你会发现,这种题目我们需要记录一个变量 \(rm\) 表示原数对当前模数取模的结果
而且我们可能会枚举模数 (范围小的时候) 或是动态更新模数 (可能会卡空间)