[周末训练]数字计数
Posted machinerycountry
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[周末训练]数字计数相关的知识,希望对你有一定的参考价值。
题目
【题目描述】
原题来自:ZJOI 2010
给定两个正整数$a$和$b$,求在$[a,b]$中的所有整数中,每个数码$(digit)$各出现了多少次。
【输入格式】
仅包含一行两个整数 ,含义如上所述。
【输出格式】
包含一行$10$个整数,分别表示$0~9$在$[a,b]$中出现了多少次。
【样例】
样例输入
1 99
样例输出
9 20 20 20 20 20 20 20 20 20
【数据范围】
$30%$的数据中,$a,bin [1,10^6]$
$100%$的数据中,$a,bin [1,10^{12}]$
题解
【做题经历】
做这道题的时候,如果不是老师说这是个$dp$题,我可能还会直接上暴搜虽然最后我的正解代码也是暴搜
首先,我尝试了一下手推数据,发现还是多好想的,但是始终想不出来这个题和$dp$之间有什么联系。
然后我就两眼一抹黑了
【正解】
这是一道数位$dp$题
在某谷上逛了几圈,还是发现有大佬写的$dp$正解(orz膜拜大佬),然而我太笨了,看不懂$dp$的来历(我这个蒟蒻$dp$也就这样了),含泪的时候,发现了搜索的做法。
首先,我们先把问题转化一下,用与做琪露诺数一样的方法,将题目要求的区间$[a,b]$变成区间$[1,b]$与区间$[1,a-1]$中数码出现的次数。
现在我们要解决的就是怎么求区间$[1,k]$中每个数码出现的次数了。
先看我的$dfs$部分吧
int dfs(int len,bool small,int sum,bool prezero,int d){ if(len==0)return sum;int ret=0; for(int i=0;i<10;++i){ if(small&&i>num[len])break; ret+=dfs(len-1,small&&(i==num[len]),sum+((!prezero||i>0)&&(i==d)),prezero&&(i==0),d); } return ret; }
这个$dfs$是不是感觉很清晰啊。
咳咳,我来解释一下,这个$dfs$包含五个参数,他们分别是:
- $len$:枚举到这个数的第几位了
- $small$:是否需要小于等于右边界的对应位数
- $sum$:显而易见,已经有多少种情况了
- $prezero$:当前构造的数是否有前导$0$
- $d$:当前询问的数码
其中$small$可能有点难以理解,这里我们可以举一个例子
假设我们的右边界$k=1388$
而当前我们所构造的数是$Num=overline{13ix}$($x$是我们还没有枚举到的位数)
很显然,$i≤8$,不然$Num$就超过了右边界
而这就是$small$的用处
解决了这个问题,接下来就是搜索都会面临的问题——时间复杂度
显而易见,如果是这个代码裸交,是会被$T$飞的
所以我们需要加入记忆化减少大部分重复搜索的操作
这里而这个记忆化其实只需要把所有搜索的参数全部塞进定义和下标中就可以了(因为这个题的范围十分友善)
那么就有记忆化$dp[len][small][sum][prezero]$,其定义就是搜索对应的参数
因为我们是分字码搜索,所以最后一维可以省略了
然后就是你们最喜欢的代码
#include<bits/stdc++.h> using namespace std; #define int long long template<class T>inline void qread(T& x){ char c;bool f=false;x=0; while((c=getchar())<‘0‘||‘9‘<c)if(c==‘-‘)f=true; for(x=(c^48);‘0‘<=(c=getchar())&&c<=‘9‘;x=(x<<1)+(x<<3)+(c^48)); if(f)x=-x; } template<class T,class... Args>inline void qread(T& x,Args&... args){qread(x),qread(args...);} inline int rqread(){ char c;bool f=false;int x=0; while((c=getchar())<‘0‘||‘9‘<c)if(c==‘-‘)f=true; for(x=(c^48);‘0‘<=(c=getchar())&&c<=‘9‘;x=(x<<1)+(x<<3)+(c^48)); return f?-x:x; } const int MAXSIZE=20; int l,r,num[MAXSIZE+5]; int dp[MAXSIZE+5][2][MAXSIZE+5][2]; int dfs(int len,bool small,int sum,bool prezero,int d){ if(len==0)return sum; if(dp[len][small][sum][prezero]!=-1)return dp[len][small][sum][prezero]; int ret=0; for(int i=0;i<10;++i){ if(small&&i>num[len])break; ret+=dfs(len-1,small&&(i==num[len]),sum+((!prezero||i>0)&&(i==d)),prezero&&(i==0),d); } return dp[len][small][sum][prezero]=ret; } inline int solve(int var,int d){ int len=0; memset(dp,-1,sizeof dp); while(var)num[++len]=var%10,var/=10; return dfs(len,1,0,1,d); } signed main(){ // freopen(".in","r",stdin); // freopen(".out","w",stdout); qread(l,r); for(int i=0;i<10;++i)printf("%lld ",solve(r,i)-solve(l-1,i)); putchar(‘ ‘); return 0; }
以上是关于[周末训练]数字计数的主要内容,如果未能解决你的问题,请参考以下文章