「学习笔记」数位DP
虽然叫 DP,但是基本所有数位 DP 题我们都可以用好打好想好理解的 记忆化搜索 来做。
记搜模板
有一个大致的记忆化搜索模板, AK ALL 数位 DP
int dfs(int len, bool lead, bool limit, ...){
if(!len) return 1; //len = 0,即所有位都搜完
if(~f[len][lead][limit]...) return f[len][lead][limit]...; //记忆化,搜过当前情况直接返回
int r = limit ? lim[len] : 9, ans = 0; //若有上限搜到当前位的限制,否则搜到9
for(int i=0; i<=r; i++){
ans += dfs(len-1, lead&&i==0, limit&&i==r, ...);
}
return f[len][lead][limit]... = ans;
}
$ f $ 数组用来记忆化,\(lim[i ]\) 表示当前数第 \(i\) 位的上限。
\(dfs 中的变量 lead 和 limit\) 分别表示有无前导零和是否是上限,\(len\) 表示现在搜到第几位(从高位到低位搜)。
预处理 如下:
int work(int x){
int cnt = 0;
while(x)
{
lim[++cnt] = x % 10;
x /= 10;
}
memset(f, -1, sizeof f);
return dfs(cnt, -10, true, true);
}
注意
数据范围看清,记得开 long long
解释
主要用了填数的思想。
\(limit\)
对于很多题,我们都需要求 1~n 中有多少符合条件的数,举个例子,\(n = 866666\),如果最高位搜的数 \(<8\) 时,好说,后面几位随便是什么都可以,但若最高位搜的数 \(=8\) 时,后一位需要保证搜 \(<=6\) 的数,不然就超出 \(n\) 的范围了。这就是我们记 \(limit\) 的原因。
\(lead\)
有些题保证不能有前导零,需要记一下。
常见题型
连续3个6
启示录
题意:含有连续 3 个 6 的数为魔鬼数,给定 \(x\),求第 \(x\) 小的魔鬼数。
该数整除其各位数字之和
记一个 \(sum\) 表示各位数字之和,\(s\) 表示当前模数,\(mod\) 表示当前数 mod s 之后的值。
设有 \(cnt\) 位数,那么 \(sum\) 最大可以到 \(9 * cnt\),我们将从 \(0 - 9*cnt\) 的所有数作为模数都搜一遍,搜索结束时判断一下 是否有 \(sum == s && mod == 0\) 即可 。
long long dfs(int len, int sum, int mod, int limit){
if(!len) return sum == s and !mod ? 1 : 0;
if(f[len][sum][mod]!=-1 and !limit) return f[len][sum][mod];
long long ans = 0, m = limit ? lim[len] : 9;
for(int i=0; i<=m; i++){
ans += dfs(len-1, sum+i, (mod*10+i)%s, limit and (i == m));
}
return limit ? ans : f[len][sum][mod] = ans;
}
long long work(long long x){
int cnt = 0;
long long ans = 0;
while(x)
{
lim[++cnt] = x % 10;
x /= 10;
}
for(s=1; s<=cnt*9; s++){
memset(f, -1, sizeof f);
ans += dfs(cnt, 0, 0, 1);
}
return ans;
}
code
#include<bits/stdc++.h>
#define in(n) scanf("%lld", &n)
using namespace std;
long long L, R;
int s, T;
long long f[20][200][200], lim[200];
long long dfs(int len, int sum, int mod, int limit){
if(!len) return sum == s and !mod ? 1 : 0;
if(f[len][sum][mod]!=-1 and !limit) return f[len][sum][mod];
long long ans = 0, m = limit ? lim[len] : 9;
for(int i=0; i<=m; i++){
ans += dfs(len-1, sum+i, (mod*10+i)%s, limit and (i == m));
}
return limit ? ans : f[len][sum][mod] = ans;
}
long long work(long long x){
int cnt = 0;
long long ans = 0;
while(x)
{
lim[++cnt] = x % 10;
x /= 10;
}
for(s=1; s<=cnt*9; s++){
memset(f, -1, sizeof f);
ans += dfs(cnt, 0, 0, 1);
}
return ans;
}
int main(){
// freopen("in.in", "r", stdin); freopen("out.out", "w", stdout);
in(R);
printf("%lld\n", work(R));
return 0;
}
二进制
处理 \(lim[]\) 时换成二进制的处理即可。
int dfs(int len, int _0, int _1, bool limit, bool lead){
if(!len&&!lead)return _0>=_1;
if(!len)return 0;
if(~f[len][_0][_1][limit][lead]) return f[len][_0][_1][limit][lead];
int r = limit ? lim[len] : 1;
int ans = 0;
for(int i=0; i<=r; i++){
ans += dfs(len-1, (i==0&&!lead)?(_0+1):_0, (i==1)?(_1+1):_1, limit&&(i==r), lead&&(i==0));
}
return f[len][_0][_1][limit][lead] = ans;
}
int work(int x){
int cnt=0;
while(x)lim[++cnt]=x&1,x>>=1;
memset(f, -1, sizeof f);
return dfs(cnt,0,0,1,1);
}
code
#include<bits/stdc++.h>
#define int long long
#define ll long long
using namespace std;
int L, R;
int f[35][35][35][2][2];
int lim[35];
int dfs(int len, int _0, int _1, bool limit, bool lead){
if(!len&&!lead)return _0>=_1;
if(!len)return 0;
if(~f[len][_0][_1][limit][lead]) return f[len][_0][_1][limit][lead];
int r = limit ? lim[len] : 1;
int ans = 0;
for(int i=0; i<=r; i++){
ans += dfs(len-1, (i==0&&!lead)?(_0+1):_0, (i==1)?(_1+1):_1, limit&&(i==r), lead&&(i==0));
}
return f[len][_0][_1][limit][lead] = ans;
}
int work(int x){
int cnt=0;
while(x)lim[++cnt]=x&1,x>>=1;
memset(f, -1, sizeof f);
return dfs(cnt,0,0,1,1);
}
signed main(){
scanf("%lld%lld", &L, &R);
printf("%lld ", work(R) - work(L-1));
return 0;
}
各位数字都被该数本身整除
我们知道 \(0,1,2,3,4,5,6,7,8,9\) 的最小公倍数为 2520,所以记该数本身 mod 2520 的值,再记一个各位数字之和的最小公倍数即可。
inline int get_gvc(int gvc, int now){
if(!now) return gvc;
else return gvc * now / __gcd(gvc, now);
}
inline void yuen(){
int tot = 0;
for(int i=1; i<=2520; i++){
if(2520 % i == 0) a[i] = ++tot;
}
for(int i=0; i<=18; i++) g[i] = pow(10, i);
}
inline ll dfs(int len, int sum, int gvc, bool limit, bool lead){
if(!len) return (sum % gvc == 0);
if(~f[len][sum][a[gvc]] and !limit and !lead) return f[len][sum][a[gvc]];
int r = limit ? lim[len] : 9;
ll ans = 0;
for(int i=0; i<=r; i++){
ans += dfs(len-1, (sum*10+i)%2520, get_gvc(gvc, i), limit&&(i==r), lead&&(i==0));
}
if(limit or lead) return ans;
return f[len][sum][a[gvc]] = ans;
}
inline ll work(ll x){
int cnt = 0;
while(x){ lim[++cnt] = x % 10; x /= 10;}
return dfs(cnt, 0, 1, true, true);
}
code
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll L, R;
int lim[22];
int T, a[2530];
ll f[20][2530][60], g[20];
inline int get_gvc(int gvc, int now){
if(!now) return gvc;
else return gvc * now / __gcd(gvc, now);
}
inline void yuen(){
int tot = 0;
for(int i=1; i<=2520; i++){
if(2520 % i == 0) a[i] = ++tot;
}
for(int i=0; i<=18; i++) g[i] = pow(10, i);
}
inline ll dfs(int len, int sum, int gvc, bool limit, bool lead){
if(!len) return (sum % gvc == 0);
if(~f[len][sum][a[gvc]] and !limit and !lead) return f[len][sum][a[gvc]];
int r = limit ? lim[len] : 9;
ll ans = 0;
for(int i=0; i<=r; i++){
ans += dfs(len-1, (sum*10+i)%2520, get_gvc(gvc, i), limit&&(i==r), lead&&(i==0));
}
if(limit or lead) return ans;
return f[len][sum][a[gvc]] = ans;
}
inline ll work(ll x){
int cnt = 0;
while(x){ lim[++cnt] = x % 10; x /= 10;}
return dfs(cnt, 0, 1, true, true);
}
signed main(){
//freopen("in.in", "r", stdin); freopen("out.out", "w", stdout);
memset(f, -1, sizeof f); yuen();
cin>>T;
while(T--)
{
scanf("%lld%lld", &L, &R);
printf("%lld\n", work(R) - work(L-1));
}
return 0;
}