数位 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:

 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 }
View Code

 

T2Windy 数

题干:

  $Windy$ 定义了一种 $Windy$ 数:不含前导零且相邻两个数字之差至少为 $2$ 的正整数被称为 $Windy$ 数。

  $Windy$ 想知道,在 $l$ 和 $r$ 之间,包括 $l$ 和 $r$ ,总共有多少个 $Windy$ 数?

题解:

  这道题要求考虑前导零,目前作者知道两种判掉前导零的方法:

  1、若要求数的位数一定,那我们就可以将第一位的判断提出 $dfs$,在主函数中手动不让最高位是 $0$。

  2、若对数的大小无限制,我们就可以像下传限制标记一样,若前面都是 $0$,而且这里也是 $0$,那就继承,否则不继承(注意考虑一下最终的数是 $0$ 的情况)

  我们就可以愉快地切掉这道题啦。

Code:

 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 }
View Code

 

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:

 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 }
View Code

 

T4:花神的数论题

题干:

   众所周知,花神多年来凭借无边的神力狂虐各大 $OJ$、$OI$、$CF$、$TC$ …… 当然也包括 $CH$ 啦。

  话说花神这天又来讲课了。课后照例有超级难的神题啦…… 我等蒟蒻又遭殃了。花神的题目是这样的:

  设 $sum(i)$ 表示 $i$ 的二进制表示中 $1$ 的个数。给出一个正整数 $N$ ,花神要问你 $sum_1-sum_N$ 的乘积。(答案模 $10000007$ )

题解:

   一开始看到要求乘积,我还想着如何将式子展开或者将数位 $dp$ 的状态定义为什么什么的乘积,结果都失败了。。。

  其实数位 $dp$ 不出意外都是计数类的状态转移定义,我们就想一下如何用某个东西的方案数来求解。

  我们可以发现,数字很大时,它所对应的二进制数的 $1$ 的个数并不一定很多,说明它并不一定单调,在上面放一个指针跑并不适用,我们就尝试统计出所有数对应的二进制数出现 $1$ 的次数为 $x$ 的方案数,最后快速幂求解即可。

Code:

 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 }
View Code

 

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:

 

转载于:https://www.cnblogs.com/OI-zzyy/p/11312068.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值