[BZOJ 1833] count 数字计数 数位DP(附数位DP总结)

题目传送门:【BZOJ 1833】


题目大意:给定两个正整数 a 和 b,求在 [a,b] 中的所有整数中,每个数码 (digit,指 0-9 ) 各出现了多少次。其中 a ≤ b ≤ 10 12


题目分析:

真正的题解在下面,前面为总结。


由题,观察这道题的题目性质及数据范围,那么,这道题用线性的 for 一遍肯定是不行的了;于是,我们考虑缩小数据处理范围。又因为这道题满足“前缀和相减”的性质,所以我们考虑使用数位 DP。

那么确定了方法之后,我们先要了解数位 DP 的做法。它通常是遍历每一位数,对每一位数及其之后的数统计答案。

以下的“第几位”指从低到高的位数。

实际上,数位 DP 有一个大体模型:
对这个数进行 dfs,通常,dfs 有两个必要的参数 dep(th) 和 limit;dep 表示现在遍历到第几位了,而 limit 表示这一位是否达到需要被处理到的极限。

  1. int dfs(int dep,bool limit, … ){  
  2.     if (dep==0){  
  3.         return 1;  
  4.     }  
  5.     if (!limit){  
  6.         处理每个ans;   
  7.         return ans;  
  8.     }  
  9.       
  10.     int cnt=0,tot=0;  
  11.     int lim=(limit==true ? wei[dep] : 9);  
  12.       
  13.     for (int i=0;i<=lim;i++){  
  14.         cnt=0;  
  15.         cnt+=dfs(dep-1,limit && i==lim,lead && i==0);     
  16.         处理每个cnt;  
  17.         tot+=cnt;  
  18.     }  
  19.     return tot;  
  20. }  

假设现在没有其他的附加条件。
例如,对于一个数 3456,当最高位(千位)为 0,1,2 时,后面几位可以随意取值(0-9皆可);而当千位为 3 时,百位只能取到 0-4,所以这时我们要进入下一层查找。

当某一位取了一个值之后,接下来的几位都可以随意取值时,我们就可以将“可以随意取值”这个状态记录下来,这时我们就可以直接返回,避免浪费过多时间;所以,我们仅在无法随意取值的时候才进行递归查找操作。(当然也可以先预处理“可以随意取值”的状态)

而如果题目中有其他条件时(例如不能连续两位都是 1),我们可以增加一些新的参数;例如 prev 表示上一位取的数是多少,这时对于这个问题而言,如果上一个取了 1,那么这一位再取 1 将不是合法状态,所以此时我们就要把选 1 的情况排除掉(for 里面用一个 continue)。


那么对于这道题而言,它要求我们把每个数的出现次数统计出来,所以我们需要开两个桶,对于 [a,b],一个用来装 [1,a] 的答案,另一个用来装 [1,b] 的答案。同时,我们在 dfs 里面新增一个参数 lead,表示它是否为“存在前导 0 ”的数。
设目前的这个数的位数为 i ,令所有 i 位及以下的数(含前导 0)的答案为 fi ,则 fi=10fi1+10i1 ;又令所有 i 位及以下的数(不含前导 0)的答案为 gi,则 gi=9gi1+gi1

如果 dfs 在当前位 dep 含有前导 0,对于“0”这个数,我们只统计 gdep 的答案;否则,我们统计 fdep 的答案。对于 1-9,由于它们不存在前导“0”的情况,直接统计 fdep 即可。根据前辈们的经验,最后输出的时候不能有多余的空格。

下面附上代码:

  1. #include<cstdio>  
  2. #include<cstring>  
  3. #include<iostream>  
  4. #include<algorithm>  
  5. using namespace std;  
  6. typedef long long LL;  
  7. const int MX=13;  
  8. const int INF=0x3f3f3f3f;  
  9.   
  10. LL f[MX],g[MX];                 //f表示无前导0时,0-9的总方案数;  
  11.                                 //g表示有前导0时,0这个数的总方案数   
  12. int wei[MX],top;                //wei:每一位数的值   
  13.                                 //top:总位数   
  14. LL bucket[10],temp[10];         //bucket:桶,存下0-9每一个数出现的次数   
  15.                                 //temp: 临时的桶,开两个桶方便最后计算   
  16.   
  17. LL fastpow(LL a,LL b){          //快速幂   
  18.     LL r=1,base=a;  
  19.     while (b){  
  20.         if (b&1) r*=base;  
  21.         base*=base;  
  22.         b>>=1;  
  23.     }  
  24.     return r;  
  25. }  
  26. LL dfs(int dep,bool limit,bool lead){       //对每一位进行处理   
  27.                                 //dep:当前位  limit:是否达到这道题的限制  
  28.                                 //lead:是否有前导0   
  29.     if (dep==0){  
  30.         if (!lead) return 1;  
  31.         else return 0;  
  32.     }  
  33.     if (!limit){  
  34.         if (!lead) bucket[0]+=f[dep];  
  35.         else bucket[0]+=g[dep];  
  36.         for (int i=1;i<=9;i++){  
  37.             bucket[i]+=f[dep];  
  38.         }  
  39.         return fastpow(10,dep)*(!lead);  
  40.     }  
  41.     LL cnt=0,tot=0;             //cnt:这一位的计数  
  42.                                 //tot:之后所有位的计数   
  43.     int lim=(limit==true ? wei[dep] : 9);  
  44.       
  45.     for (int i=0;i<=lim;i++){  
  46.         cnt=0;  
  47.         cnt+=dfs(dep-1,limit && i==lim,lead && i==0);  
  48.         bucket[i]+=cnt;  
  49.         tot+=cnt;  
  50.     }  
  51.     return tot;  
  52. }  
  53.   
  54. int main(){  
  55.     LL a,b;  
  56.     scanf(”%I64d%I64d”,&a,&b);  
  57.     f[0]=f[1]=1;  
  58.     for (int i=2;i<=12;i++){  
  59.         f[i]=10*f[i-1]+fastpow(10,i-1);  
  60.         g[i]=9*f[i-1]+g[i-1];  
  61.           
  62.     }  
  63.       
  64.     LL tempa=a-1,tempb=b;               //根据前缀和计算   
  65.     int w=0;  
  66.       
  67.     while (tempa){  
  68.         wei[++w]=tempa%10;  
  69.         tempa/=10;  
  70.     }  
  71.     top=w;  
  72.     dfs(top,true,true);  
  73.     for (int i=0;i<=9;i++){  
  74.         temp[i]=bucket[i];  
  75.         bucket[i]=0;  
  76.     }  
  77.       
  78.     w=0;memset(wei,0,sizeof(wei));  
  79.       
  80.     while (tempb) {  
  81.         wei[++w]=tempb%10;  
  82.         tempb/=10;  
  83.     }  
  84.     top=w;  
  85.     dfs(top,true,true);  
  86.       
  87.     for (int i=0;i<=9;i++){              //统计答案   
  88.         printf(”%I64d”,bucket[i]-temp[i]);  
  89.         if (i!=9) printf(“ ”);  
  90.     }  
  91.     return 0;  
  92. }  
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
题目描述 有一个 $n$ 个点的棋盘,每个点上有一个数字 $a_i$,你需要从 $(1,1)$ 走到 $(n,n)$,每次只能往右或往下走,每个格子只能经过一次,路径上的数字和为 $S$。定义一个点 $(x,y)$ 的权值为 $a_x+a_y$,求所有满足条件的路径中,所有点的权值和的最小值。 输入格式 第一行一个整数 $n$。 接下来 $n$ 行,每行 $n$ 个整数,表示棋盘上每个点的数字。 输出格式 输出一个整数,表示所有满足条件的路径中,所有点的权值和的最小值。 数据范围 $1\leq n\leq 300$ 输入样例 3 1 2 3 4 5 6 7 8 9 输出样例 25 算法1 (树形dp) $O(n^3)$ 我们可以先将所有点的权值求出来,然后将其看作是一个有权值的图,问题就转化为了在这个图中求从 $(1,1)$ 到 $(n,n)$ 的所有路径中,所有点的权值和的最小值。 我们可以使用树形dp来解决这个问题,具体来说,我们可以将这个图看作是一棵树,每个点的父节点是它的前驱或者后继,然后我们从根节点开始,依次向下遍历,对于每个节点,我们可以考虑它的两个儿子,如果它的两个儿子都被遍历过了,那么我们就可以计算出从它的左儿子到它的右儿子的路径中,所有点的权值和的最小值,然后再将这个值加上当前节点的权值,就可以得到从根节点到当前节点的路径中,所有点的权值和的最小值。 时间复杂度 树形dp的时间复杂度是 $O(n^3)$。 C++ 代码 算法2 (动态规划) $O(n^3)$ 我们可以使用动态规划来解决这个问题,具体来说,我们可以定义 $f(i,j,s)$ 表示从 $(1,1)$ 到 $(i,j)$ 的所有路径中,所有点的权值和为 $s$ 的最小值,那么我们就可以得到如下的状态转移方程: $$ f(i,j,s)=\min\{f(i-1,j,s-a_{i,j}),f(i,j-1,s-a_{i,j})\} $$ 其中 $a_{i,j}$ 表示点 $(i,j)$ 的权值。 时间复杂度 动态规划的时间复杂度是 $O(n^3)$。 C++ 代码

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值