YBTOJ 数位dp合集

qwq

是半年前就学而未会的数位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(l1)

现在考虑计算 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。

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值