数位 dp 总结
T1:不要 62
题干:
杭州人称那些傻乎乎粘嗒嗒的人为 $62$(音:$laoer$)。
杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了,这样一来,就可以消除个别的士司机和乘客的心理障碍,更安全地服务大众。
不吉利的数字为所有含有 $62$ 或 $4$ 的号码。例如: $62315$、$73418$、$88914$ 都属于不吉利号码。但是,$69842$ 虽然含有 $6$ 和 $2$ ,但不是 $6$ $2$ 连号,所以不属于不吉利数字之列。
你的任务是,对于每次给出的一个牌照区间号,推断出交管局今后又要实际上给多少辆新的士车上牌照了。
题解:
这道题只考虑每一位的数,而不是整体大小,我们就可以想到数位 $dp$ 。我们先将左右两端点十进制拆分一下,总答案就是 $1~r$ 的答案减去 $1~l-1$ 的答案就是 $[l.r]$ 区间的解。
这道题作者采取的是记忆化搜索的方法。(所谓记忆化搜索,就是每搜到一个情况记录一下这个情况的解,下次搜到时不再重新计算)
有两个限制条件:
1、‘$4$’:直接在枚举时判掉即可。
2、‘$6$ $2$’:我们需要维护一下上一个数是什么以供判断,并且记录下这个‘与众不同’的情况——再开半维数组来存储一下上一位是‘$6$’的情况。
(为什么不存‘$4$’而存‘$6$ $2$’的情况呢?其实根本就不存在包含‘$4$’的情况,但是包含上一位是‘$6$’的情况,可以记忆化搜索)
Code:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<cstdio>
2 #include<cstring>
3 #define $ 10
4 #define int long long
5 #define inf 0x7fffffff
6 using namespace std;
7 int m,n,k,t,sta[$],dp[$][2],up;
8 inline int dfs(int lct,int pre,bool sx,bool lmt){
9 if(!lct) return 1;
10 if(!lmt&&dp[lct][sx]!=-1) return dp[lct][sx];
11 int top=(lmt)?sta[lct]:9, ans=0;
12 for(register int i=0;i<=top;++i){
13 if(i==4||(pre==6&&i==2)) continue;
14 ans+=dfs(lct-1,i,i==6,(lmt&&i==top));
15 }
16 if(!lmt) dp[lct][sx]=ans;
17 return ans;
18 }
19 inline int work(int x){
20 memset(dp,-1,sizeof(dp)); up=0;
21 for(;x;x/=10) sta[++up]=x%10;
22 return dfs(up,-1,0,1);
23 }
24 signed main(){
25 while(~scanf("%lld%lld",&n,&m)){
26 if(m+n==0) break;
27 printf("%lld\n",work(m)-work(n-1));
28 }
29 }
T2:Windy 数
题干:
$Windy$ 定义了一种 $Windy$ 数:不含前导零且相邻两个数字之差至少为 $2$ 的正整数被称为 $Windy$ 数。
$Windy$ 想知道,在 $l$ 和 $r$ 之间,包括 $l$ 和 $r$ ,总共有多少个 $Windy$ 数?
题解:
这道题要求考虑前导零,目前作者知道两种判掉前导零的方法:
1、若要求数的位数一定,那我们就可以将第一位的判断提出 $dfs$,在主函数中手动不让最高位是 $0$。
2、若对数的大小无限制,我们就可以像下传限制标记一样,若前面都是 $0$,而且这里也是 $0$,那就继承,否则不继承(注意考虑一下最终的数是 $0$ 的情况)
我们就可以愉快地切掉这道题啦。
Code:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<cstdio>
2 #include<cstring>
3 #define $ 21
4 #define inf 0x3f3f3f3f
5 using namespace std;
6 int m,n,k,t,dp[$][$],sta[$],up;
7 inline int abs(int x){ return (x<0)?-x:x; }
8 inline int dfs(int lct,int pre,bool lmt){
9 if(!lct) return 1;
10 if(!lmt&&pre>=0&&dp[lct][pre]!=-1) return dp[lct][pre];
11 int top=(lmt)?sta[lct]:9, ans=0;
12 for(register int i=0,tmp;i<=top;++i){
13 if(abs(i-pre)<2) continue;
14 tmp=(i==0&&pre==-inf)?pre:i;
15 ans+=dfs(lct-1,tmp,(lmt&&i==top));
16 }
17 if(!lmt&&pre>=0) dp[lct][pre]=ans;
18 return ans;
19 }
20 inline int work(int x){
21 memset(dp,-1,sizeof(dp)); up=0;
22 for(;x;x/=10) sta[++up]=x%10;
23 return dfs(up,-inf,1);
24 }
25 signed main(){
26 scanf("%d%d",&n,&m);
27 printf("%d\n",work(m)-work(n-1));
28 }
T3:手机号码
题干:
人们选择手机号码时都希望号码好记、吉利。比如号码中含有几位相邻的相同数字、不含谐音不吉利的数字等。手机运营商在发行新号码时也会考虑这些因素,从号段中选取含有某些特征的号码单独出售。为了便于前期规划,运营商希望开发一个工具来自动统计号段中满足特征的号码数量。
工具需要检测的号码特征有两个:号码中要出现至少 $3$ 个相邻的相同数字;号码中不能同时出现 $8$ 和 $4$ 。
号码必须同时包含两个特征才满足条件。满足条件的号码例如:$13000988721$、$23333333333$、$14444101000$。
而不满足条件的号码例如:$1015400080$、$10010012022$。
手机号码一定是 $11$ 位数,前不含前导的 $0$ 。工具接收两个数 $l$ 和 $r$,自动统计出 $[ l , r ]$ 区间内所有满足条件的号码数量。 $l$ 和 $r$ 也是 $11$ 位的手机号码。
$10^{10}<=l<=r<10^{11}$
题解:
这道题同样要判断前导零,但我们发现前导零并不影响转移,而且在本题中前导零并不存在,直接前缀和相减就好了。
(注意左端点有可能等于 $10^{10}$,我们若用前缀和解决,可以发现左端点减 $1$ 后有可能变成 $10$ 位。。。我们需要特判掉 $l==10^{10}$ 的情况,$return$ $0$ ——因为不存在比 $10^{10}$ 还小的手机号码)
状态:转移到现在的位置,上一位的数值,上上位的数值,是否出现过 8 ,是否出现过 4 ,是否出现过三连,是否有限制。
Code:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<cstdio>
2 #include<cstring>
3 #define int long long
4 #define $ 12
5 using namespace std;
6 int l,r,sta[$],dp[$][$][$][2][2][2],up;
7 inline int dfs(int lct,int pre,int ppre,bool cnnct,bool eigh,bool fur,bool lmt){
8 if(eigh&&fur) return 0;
9 if(!lct) return cnnct;
10 if(!lmt&&dp[lct][pre][ppre][cnnct][eigh][fur]!=-1)
11 return dp[lct][pre][ppre][cnnct][eigh][fur];
12 int top=(lmt)?sta[lct]:9, ans=0;
13 for(register int i=0;i<=top;++i)
14 ans+=dfs(lct-1,i,pre,(cnnct||(i==pre&&i==ppre)),(eigh||i==8),(fur||i==4),(lmt&&(i==top)));
15 if(!lmt) dp[lct][pre][ppre][cnnct][eigh][fur]=ans;
16 return ans;
17 }
18 inline int work(int x,int ans=0){
19 if(x<10000000000) return ans;
20 memset(dp,-1,sizeof(dp)); up=0;
21 for(;x;x/=10) sta[++up]=x%10;
22 for(register int i=1;i<=sta[up];++i)
23 ans+=dfs(10,i,0,0,i==8,i==4,i==sta[up]);
24 return ans;
25 }
26 signed main(){
27 scanf("%lld%lld",&l,&r);
28 printf("%lld\n",work(r)-work(l-1));
29 }
T4:花神的数论题
题干:
众所周知,花神多年来凭借无边的神力狂虐各大 $OJ$、$OI$、$CF$、$TC$ …… 当然也包括 $CH$ 啦。
话说花神这天又来讲课了。课后照例有超级难的神题啦…… 我等蒟蒻又遭殃了。花神的题目是这样的:
设 $sum(i)$ 表示 $i$ 的二进制表示中 $1$ 的个数。给出一个正整数 $N$ ,花神要问你 $sum_1-sum_N$ 的乘积。(答案模 $10000007$ )
题解:
一开始看到要求乘积,我还想着如何将式子展开或者将数位 $dp$ 的状态定义为什么什么的乘积,结果都失败了。。。
其实数位 $dp$ 不出意外都是计数类的状态转移定义,我们就想一下如何用某个东西的方案数来求解。
我们可以发现,数字很大时,它所对应的二进制数的 $1$ 的个数并不一定很多,说明它并不一定单调,在上面放一个指针跑并不适用,我们就尝试统计出所有数对应的二进制数出现 $1$ 的次数为 $x$ 的方案数,最后快速幂求解即可。
Code:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<cstdio>
2 #include<cstring>
3 #define int long long
4 #define mod 10000007
5 #define $ 52
6 using namespace std;
7 int n,sta[$],up,dp[$][$][$],sum=1,ans[$];
8 inline int dfs(int lct,int now,int tot,bool lmt){
9 if(lct==0) return tot==now;
10 if(!lmt&&dp[lct][now][tot]!=-1) return dp[lct][now][tot];
11 int top=(lmt)?sta[lct]:1, cnt=0;
12 for(register int i=0;i<=top;++i)
13 cnt+=dfs(lct-1,now+i,tot,(lmt&&(i==top)));
14 if(!lmt) dp[lct][now][tot]=cnt;
15 return cnt;
16 }
17 inline int qpow(int a,int x,int ans=1){
18 for(;x;x>>=1,a=a*a%mod) if(x&1) ans=ans*a%mod;
19 return ans;
20 }
21 signed main(){
22 scanf("%lld",&n);
23 for(;n;n>>=1) sta[++up]=n&1;
24 for(register int i=1;i<=50;++i)
25 memset(dp,-1,sizeof(dp)), ans[i]=dfs(up,0,i,1);
26 for(register int i=1;i<=50;++i)
27 sum=sum*qpow(i,ans[i])%mod;
28 printf("%lld\n",sum);
29 }
T5:数数
题干:
Fish 是一条生活在海里的鱼,有一天他很无聊,就开始数数玩。 他数数玩的具体规则是:
1、确定数数的进制B
2、确定一个数数的区间[L, R]
3、对于[L, R] 间的每一个数,把该数视为一个字符串,列出该字符串的每一个(连续的)子串对应的B进制数的值。
4、对所有列出的数求和。
现在Fish 数了一遍数,但是不确定自己的结果是否正确了。由于[L, R] 较大,他没有多余精力去验证是否正确,你能写一个程序来帮他验证吗?
输入格式:
第一行仅有一个数B,表示数数的进制。
第二行有N +1 个数,第一个数为N,表示数L 在B 进制下的长度为N,接下里的N个数从高位到低位的表示数L 的具体每一位。
第三行有M+ 1 个数,第一个数为M,表示数R 在B 进制下的长度为M,接下里的M个数从高位到低位的表示数R 的具体每一位。
输出格式:输出仅一行,即按照Fish 数数规则的结果,结果用10 进制表示,由于该数可能很大,输出该数模上 20130427 的模数。
题解:
Code: