是半年前就学而未会的数位dp,虽迟但到。
不要靠近递推,会变得不幸。和记搜贴贴!
一些套路
数位dp通常是一些区间上的问题,区间 [ l , r ] [l,r] [l,r] 的计数一般转化为前缀和相减的形式解决,即 s o l v e ( r ) − s o l v e ( l − 1 ) solve(r)-solve(l-1) solve(r)−solve(l−1)。
现在考虑计算 s o l v e ( x ) solve(x) solve(x):
-
数位dp,顾名思义,要先把数按位拆开:
int len=0; while(x) num[++len]=x%10,x/=10;
这样我们得到的数应该从后往前读,所以dp也要从后往前进行。
-
然后是记搜部分:
inline int dfs(int pos,int lim,int lead,int ...){ //pos为当前搜到第几位,lim表示是否有对最高位的限制,lead表示是否有前导零 if(pos==0) return ...;//dp边界 if(!lim&&!lead&&f[pos][...]!=-1) return f[pos][...];//记搜 int ans=0;//当前答案 int up=lim?num[pos]:9;//当前数位的上界根据是否有最大值限定判断 for(int i=0;i<=up;++i){ ans+=dfs(pos-1,lim&&i==up,lead&&!i,...); //往下一位搜索 //当之前位都达到限制且当前位也达到限制的时候才要限制下一位,即lim&&i==up //当之前位都是0且当前位也是0,下一位才受前导零限制 } if(!lim&&!lead) f[pos][...]=ans;//记录答案 return ans; }
l i m lim lim 的限制在于比如 x = 12345 x=12345 x=12345,我们从高位到低位填,如果之前已经填了 123 123 123,那么现在第四位就只能填 [ 1 , 4 ] [1,4] [1,4] 了;而如果之前填了 122 122 122,那么第四位就可以 [ 0 , 9 ] [0,9] [0,9] 随便填。
前导零对于一些问题的计数会产生影响,所以一般都会维护 l e a d lead lead 用于更新答案。
这里只对没有前导零和范围限制,即 [ 0.9 ] [0.9] [0.9] 可以任选的情况做了记忆化处理,有限制的部分我们选择单独算。
一般dp数组初始化 − 1 -1 −1,对于多组询问要想清楚用不用每次都清空。
-
最后是调用:
return dfs(len,1,1,...);
知道 l i m lim lim 和 l e a d lead lead 的初始化均为 1 1 1 就行。
这就是我曾学不会的数位dp(悲)
B数计数
这里对于数位的限制一个是在于出现过子串 13 13 13,一个是能被 13 13 13 整除。
那么我们在模板里加上这两个限制就好了:定义 f [ p o s ] [ m o d ] [ s t ] f[pos][mod][st] f[pos][mod][st] 为满足当前搜到第 p o s pos pos 位,当前数模 13 13 13 值为 m o d mod mod,子串 13 13 13 出现的状态为 s t st st 的数字个数。这里定义 s t = 0 st=0 st=0 为 13 13 13 没有出现, s t = 1 st=1 st=1 为上一位出现了 1 1 1, s t = 2 st=2 st=2 为 13 13 13 出现过,那么就可以套模板转移了。
#include<bits/stdc++.h>
#define ll long long
#define ff(i,s,e) for(int i=(s);i<=(e);++i)
using namespace std;
inline int read(){
int x=0,f=1;
char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
const int N=20;
int num[N];
//0:none 1:1 2:13
int f[N][15][3];
inline int dfs(int pos,int lim,int lead,int mod,int st){
if(!pos) return !lead&&st==2&&!mod;
if(!lim&&!lead&&f[pos][mod][st]!=-1) return f[pos][mod][st];
int ans=0,up=lim?num[pos]:9;
ff(i,0,up){
int stnow=0;
if(st==1&&i==3) stnow=2;
else if(i==1) stnow=1;
if(st==2) stnow=2;
ans+=dfs(pos-1,lim&&i==up,lead&&!i,(mod*10+i)%13,stnow);
}
if(!lim&&!lead) f[pos][mod][st]=ans;
return ans;
}
inline int solve(int x){
int len=0;
while(x) num[++len]=x%10,x/=10;
return dfs(len,1,1,0,0);
}
signed main(){
int n;
memset(f,-1,sizeof(f));
while(scanf("%d",&n)!=EOF) printf("%d\n",solve(n));
return 0;
}
区间圆数
依然模板,把拆位改为二进制拆位,dp维护一下 0 0 0 和 1 1 1 出现次数的差即可。
细节:出现次数差可能为负数,代码里统一加了 32 32 32;前导零不算 0 0 0 出现。
#include<bits/stdc++.h>
#define ll long long
#define ff(i,s,e) for(int i=(s);i<=(e);++i)
using namespace std;
inline int read(){
int x=0,f=1;
char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
const int N=70;
int n,num[N],f[N][N];
inline int dfs(int pos,int lim,int lead,int cha){
if(!pos) return cha>=0&&!lead;
if(!lim&&!lead&&f[pos][cha+32]!=-1) return f[pos][cha+32];
int ans=0;
int up=lim?num[pos]:1;
ff(i,0,up){
int tmp=i?cha-1:cha+1,now=lead&&!i;
if(now) tmp=0;
ans+=dfs(pos-1,lim&&(i==up),now,tmp);
}
if(!lim&&!lead) f[pos][cha+32]=ans;
return ans;
}
inline int solve(int x){
int len=0;
while(x) num[++len]=x&1,x>>=1;
return dfs(len,1,1,0);
}
signed main(){
memset(f,-1,sizeof(f));
int a=read(),b=read();
printf("%d",solve(b)-solve(a-1));
return 0;
}
数字计数
对每个数位分别统计即可,dp维护当前数中目标数字出现次数。
由于每个数字搜的结果不同,所以每次都要清空dp数组。
#include<bits/stdc++.h>
#define int long long
#define ff(i,s,e) for(int i=(s);i<=(e);++i)
using namespace std;
inline int read(){
int x=0,f=1;
char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
const int N=20;
int f[N][N],len,num[N];
inline int dfs(int pos,int lim,int lead,int dig,int sum){
if(!pos) return sum;
if(!lim&&!lead&&(f[pos][sum]!=-1)) return f[pos][sum];
int ans=0;
int up=(lim)?num[pos]:9;
ff(j,0,up)
ans+=dfs(pos-1,lim&&(j==up),lead&&!j,dig,sum+((j==dig)&&(j||!lead)));
if(!lim&&!lead) f[pos][sum]=ans;
return ans;
}
inline int solve(int x,int dig){
memset(f,-1,sizeof(f));
len=0;
while(x) num[++len]=x%10,x/=10;
return dfs(len,1,1,dig,0);
}
signed main(){
int a=read(),b=read();
ff(i,0,9) printf("%lld ",solve(b,i)-solve(a-1,i));
return 0;
}
数字整除
模数不固定有点难办,但是发现由于值域不超过 i n t int int,数字按位相加之和最多在 83 83 83 左右,可以每次枚举模数计算。dp维护数字按位相加的和以及余数即可,这里的dp数组显然也要清空。
然后TLE了 998244353 998244353 998244353 次,最后发现由于进行了过量的 m e m s e t memset memset,警钟敲烂/fn。最后dp多加了一维模数,跑的飞飞快。
#include<bits/stdc++.h>
#define ll long long
#define ff(i,s,e) for(int i=(s);i<=(e);++i)
using namespace std;
inline int read(){
int x=0,f=1;
char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
const int N=20;
int num[N];
int f[N][105][105][105],dig;
inline int dfs(int pos,int lim,int sum,int now){
if(!pos) return sum==dig&&!now;
if(sum>dig) return 0;
if(!lim&&f[pos][dig][sum][now]!=-1) return f[pos][dig][sum][now];
int ans=0,up=lim?num[pos]:9;
ff(i,0,up){
ans+=dfs(pos-1,lim&&(i==up),sum+i,(now*10ll+i)%dig);
}
if(!lim) f[pos][dig][sum][now]=ans;
return ans;
}
inline int solve(int x){
int len=0;
while(x) num[++len]=x%10,x/=10;
int ans=0;
for(dig=1;dig<=83;++dig) ans+=dfs(len,1,0,0);
return ans;
}
signed main(){
int a,b;
memset(f,-1,sizeof(f));
while(scanf("%d",&a)!=EOF){
b=read();
printf("%d\n",solve(b)-solve(a-1));
}
return 0;
}
山谷数
题里没说要模 1 e 9 + 7 1e9+7 1e9+7,使得这道题看上去很难/fn
很容易想到正难则反,统计不合法方案数。
类似B数计数,定义一个状态 s t st st, s t = 0 st=0 st=0 表示什么也没有, s t = 1 st=1 st=1 表示之前填的序列递增, s t = 2 st=2 st=2 表示序列已经满足递增再递减,记录前一位数字 p r e pre pre 就可以转移了。
细节:只填了一个非零数字的情况不能说之前的序列是递增的,要根据前导零特判;形如 1221 1221 1221 的数也要计入,要特判相等。
开完unsigned long long懒得改了qwq
#include<bits/stdc++.h>
#define ll long long
#define ff(i,s,e) for(int i=(s);i<=(e);++i)
using namespace std;
inline int read(){
int x=0,f=1;
char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
#define ull unsigned long long
const int N=105,mod=1e9+7;
int num[N];
char s[N];
ull f[N][12][3];
//0:none 1:increasing 2:decreasing
inline ull dfs(int pos,int lim,int lead,int st,int pre){
if(!pos) return st==2;
if(!lim&&!lead&&f[pos][pre][st]!=-1) return f[pos][pre][st];
ull ans=0;
int up=lim?num[pos]:9;
// cout<<up<<endl;
ff(i,0,up){
int stnow=0;
if(st==2||st==1&&i<pre) stnow=2;
else if(i>pre&&!lead) stnow=1;
else if(i==pre&&st==1) stnow=1;
ans=(ans+dfs(pos-1,lim&&i==up,lead&&!i,stnow,i))%mod;
}
if(!lim&&!lead) f[pos][pre][st]=ans;
return ans;
}
inline void solve(){
scanf("%s",s+1);
int len=strlen(s+1);
ull tot=0;
ff(i,1,len) tot=((tot<<1)+(tot<<3)+(num[len-i+1]=(s[i]^48)))%mod;
printf("%llu\n",(tot-dfs(len,1,1,0,0)+mod)%mod);
}
signed main(){
memset(f,-1,sizeof(f));
int T=read();
while(T--) solve();
return 0;
}
/*
1
32120
6775
*/
幸运数字
模板略。
幸运666
换了一种问法,但本质没有什么区别。
考虑从高位到低位试填,边填边计算在剩下的位上填数似的最终得到的数合法的方案数 c n t cnt cnt。假设当前位置填 i i i,若得到的 c n t < n cnt<n cnt<n,则当前位必须填一个大于 i i i 的数, n − = c n t n-=cnt n−=cnt 之后继续枚举;否则当前位答案一定为 i i i,试填下一位。
c n t cnt cnt 的计算就可以用记搜/递推进行数位dp解决,且不用考虑前导零和上界,非常好写。
我依然选择记搜。
#include<bits/stdc++.h>
#define int long long
#define ff(i,s,e) for(int i=(s);i<=(e);++i)
using namespace std;
inline int read(){
int x=0,f=1;
char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
const int N=20;
vector<int> ans;
int f[N][4];
inline int get(int st,int i){
if(st==3||st==2&&i==6) return 3;
if(st==1&&i==6) return 2;
else if(i==6) return 1;
return 0;
}
inline int dp(int pos,int st){
if(!pos) return st==3;
if(~f[pos][st]) return f[pos][st];
int res=0;
ff(i,0,9) res+=dp(pos-1,get(st,i));
return f[pos][st]=res;
}
inline void find(int pos,int st,int k){
if(!pos) return;
ff(i,0,9){
int nowst=get(st,i);
if(dp(pos-1,nowst)>=k){
ans.push_back(i);
return find(pos-1,nowst,k);
}
else k-=dp(pos-1,nowst);
}
}
inline void solve(){
int n=read();
ans.clear();
find(18ll,0ll,n);
int lead=1;
for(int tmp:ans){
if(tmp) lead=0;
if(!lead) printf("%lld",tmp);
}
putchar('\n');
}
signed main(){
memset(f,-1,sizeof(f));
int T=read();
while(T--) solve();
return 0;
}
乘积计算
依然模板,只需要注意一个细节:正常dp会把 0 0 0 算进去,当二进制中 1 1 1 的出现次数为 0 0 0 时记搜要返回 1 1 1。
奶牛编号
和练习2差不多,注意dp得到的值会很大很大,但是我们最多只需要 1 e 7 1e7 1e7,将dp值对 1 e 7 1e7 1e7 取 min \min min 即可避免long long。
魔法数字
这里dp很容易想到状压,但状态里不能直接将数作为dp的一维,要对一个数取个模。根据数论知识发现对 l c m ( 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ) = 2520 lcm(1,2,3,4,5,6,7,8,9)=2520 lcm(1,2,3,4,5,6,7,8,9)=2520 取模是不会影响整个状态的,于是定义 f [ p o s ] [ s t ] [ m o d ] f[pos][st][mod] f[pos][st][mod] 为搜到第 p o s pos pos 位, 1 1 1 到 9 9 9 出现状态为 s t st st,当前数模 2520 2520 2520 值为 m o d mod mod 时的方案数,这样就很容易转移了。
算上每次枚举 0 0 0到 9 9 9,时间复杂度 2 e 8 2e8 2e8,有点悬,但只要注意一下常数,不进行#define int long long等作死行为,还是能996ms卡过去的/kx
ll f[N][M][2521];
inline int check(int st,int x){
int cnt=0;
ff(i,0,8) if((st&(1<<i))&&x%(i+1)==0) ++cnt;
if(cnt<k) return 0;
return 1;
}
inline ll dfs(int pos,int lim,int lead,int st,int yu){
if(!pos) return check(st,yu);
if(!lead&&!lim&&f[pos][st][yu]!=-1) return f[pos][st][yu];
ll ans=0;
int up=lim?num[pos]:9;
ff(i,0,up){
int now=i?st|(1<<i-1):st;
ans+=dfs(pos-1,lim&&i==up,lead&&!i,now,(yu*10+i)%mod);
}
if(!lead&&!lim) f[pos][st][yu]=ans;
return ans;
}
如果正经优化的话,发现模 5 5 5 的余数可以根据数字的最后一位直接判断,那么dp的最后一维变为 504 504 504,稍微改一下就可以跑得飞飞快了。
ll f[N][M][505];
inline int check(int st,int x,int qwq){
int cnt=0;
ff(i,0,8) if(i!=4) if((st&(1<<i))&&x%(i+1)==0) ++cnt;
cnt+=(qwq&&(st&16));
return cnt>=k;
}
inline ll dfs(int pos,int lim,int lead,int st,int yu,int pre){
if(!pos) return check(st,yu,(pre==0)||(pre==5));
if(!lead&&!lim&&f[pos][st][yu]!=-1) return f[pos][st][yu];
ll ans=0;
int up=lim?num[pos]:9;
ff(i,0,up){
int now=i?st|(1<<i-1):st;
ans+=dfs(pos-1,lim&&i==up,lead&&!i,now,(yu*10+i)%mod,i);
}
if(!lead&&!lim) f[pos][st][yu]=ans;
return ans;
}
如果还想优化的话,发现 8 8 8 也是可以优化掉的。具体地:设填到倒数第二位的数对 4 4 4 取模的结果为 k k k,则填到倒数第二位的数: 4 × p + k 4\times p+k 4×p+k,最后一位填 i i i 得到的数: 40 × p + 10 × k + i 40\times p+10\times k+i 40×p+10×k+i,第一项可以消掉,根据最后两位判断即可。(懒得写代码(逃
数位dp很常规,但这个对时间的优化真的好巧妙qwq。