第一篇 - 统计数字问题 (数位DP)

2023-12-30 02:18

本文主要是介绍第一篇 - 统计数字问题 (数位DP),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

一、问题描述:

二、问题分析:

三、编写代码: 

四、相关例题:

 

Tips:如果你是真的不理解,不要只看,拿出笔来跟着步骤自己分析。 

一、 问题描述:

一本书的页码从自然数 1 开始顺序编码直到自然数 n 。书的页码按照通常的习惯编排, 每个页码不含多余的前导数字 0。 例如, 第 6 页用数字 6 表示而不是 06 或 006等。 数字计数问题要求对给定书的总页码 n,计算书的全部页码分别用到多少次数字 0、 1、... 、9。

二、问题分析:

1. 抽取题意:简单来说就是就是给定一个数字 n,统计 1~n 中用到了多少次数字0~9。

2. 初步思考:

很容易想到要分数位(如个位、十位、百位)来考虑。

为了方便思考,我们做一些约定:

从 0 开始考虑,即考虑0~n中用到了多少次数字0~9,同时计算前导0。

用一个长度为10的数组ans[10] 来存储各个数字的数量。

从最低位开始分析还是最高位开始分析?

应该从最高位开始分析。为什么不先从个位开始考虑呢?因为数字在有除个位外高位(如十位、百位)的情况下,低位是作用于高位的,低位是高位的补充。在这个问题中,单纯的低位并不能说明什么。

3. 示例分析:

① 对于一位数 D 来说,0~D 中用到的数字个数即 0~D 各一次。

② 对于两位数 CD 来说,我们先考虑C,即C0,先不管C有多大,当C可以为0时,有00,01, 02, ... , 09 这样的数字,则我们知道,C在为0的时候个位0~9的数字各用了一次,同时C本身被用到了10次;当C为1时,有这样的数字10,11,12, ..., 19 ,我们知道 C 为1的时候个位0~9的数字各用了一次,同时C 被用了10次。

以此类推我们知道,C每大一,个位数字0~9就都会被用到1 次。并且本身作为十位的C,C每大一,当前C表示的数字就会被用到10次。

注意此时C不能为最大值,因为C为最大值的话按照上述方法考虑,可能超过CD本来的值。

再来考虑D,

当C为最大值的时候,用到的数字即C0,C1,...,CD ,由此我们知道C为最大值时C会被用D+1次,0~D各用了一次。

经过总结得到,在考虑前导0的情况下,一个十位数字CD 0~9的数字用到的情况如下:


考虑十位得到的ans [ 0~9 ] + 1*C  (虽然此时不能考虑十位最大值,但是有0啊)ans [ 0~C-1 ] + 10^1  ans [C] + (D+1)  (此时十位取最大值)考虑个位得到的ans[ 0~D ] + 1

③ 有了上面的经验,我们来考虑三位数 ABC

首先对于百位A,即A00, A01, ... , A99

我们来统计一下个位和十位上的数字即 00,01,...,99加起来一共用了多少次0~9

按照上面的想法我们很容易知道,十位从0开始每加一,个位0~9就都会被用 1 次,同时十位本身,会被用到10次。这样我们知道每个数字都被用了 10*1 + 10 次,即20次。

同理并根据上面的结论,对于 000,001,...,999 ,我们知道,如果百位从0开始每加一,个位和十位的数字组合在一次0~9各会被用到 20次,同时作为百位本身,会被用到100次。这样我们知道每个数字都被用了 10*20 + 10^2 次,即300次。

根据这个思想很容易发现规律,对于n 位的数字 0 到 n 位的数字9,设 0~9 各数字会被用到的次数为F(n),则有 F(n) = 10 * F(n-1) + 10^(n-1),其中F(1)= 1

我们把结果记入一个名为dp 的数组:dp[0] = 0, dp[1]=1, dp[2]=20, dp[3]=300 , ....

这样我们可以知道

仅考虑百位A,有ans [ 0~9 ] + 20*Aans [0~A-1] + 10^2ans [ A ] + (BC + 1)仅考虑十位,有ans[ 0~9] + 1*Bans[ 0~B-1] + 10^1ans[ B ] + (C+1)仅考虑个位ans[ 0~C ] +1

4. 总结规律:

总结上方的规律可知,

在计入前导0的情况下,要考虑0~n 的数用到了多少各0~9的数字,应该自高向低逐次取出每一位分析

设取出的一位为数字为 X,同时其位于从个位数起的第 Y 位,剩余的低位构成的数字为 Z ,

则答案计算方法为:

ans[0~9] + dp[Y-1]*X   (当X < max 时考虑低位)ans[0~X-1] + 10^(Y-1)  (当X < max 时考虑X)ans[X] + (Z+1)    (当 X = max 时考虑 X)经检查,该方法适用于个位及特殊情况。

5. 解除约定:

我们来考虑如何除去前导0

首先要明白一件事,前导0只对0的计数产生影响。

那么前导0是在哪里产生的呢?

如果上面说的你都明白了,那么很容易知道就在计算最高位时的这两步

ans[0~9] + dp[Y-1]*X   (当X < max 时考虑低位)ans[0~X-1] + 10^(Y-1)  (当X < max 时考虑X)

在最高位为0的时候多余出来的

按照上述方法考虑,设 n 位数多余出来的0的个数为 G(n)

你可以想象把所有数字都右对齐,得到:

G (1) = 0

G (2) = 10

G (3) = 10 + 100

......

G (n) = 10^1 + 10^2 + ... + 10^(n-1)  ,其中 n>2

② 或者这样想:

两位数可以对所有个位数在十位补0,三位数可以在两位数的基础上再在百位上对所有两位数再补一个0,以此类推,易知这也是一个dp

得到 G (n) = G(n-1) + 10^(n-1),其中 G(1) = 0,n>2

至此,思考部分完成。

三、 编写代码: 

算法时间复杂度仅为 O ( log10(n) ) ,接近 O (1) ,吊打暴力 O (nlog10n) 的算法。

#include <bits/stdc++.h>using namespace std;typedef long long LL;LL n;
LL dp[20], ans[20],zeroNum[20];  //zeroNum 用于记录 n 位数的前导 0 个数
LL pow10[20];  //pow10 用于记录 10 的次方数void makeDp(){pow10[0]=1;for(int i=1;i<15;i++) pow10[i] = pow10[i-1]*10;dp[0]=0,dp[1]=1;zeroNum[0]=zeroNum[1]=0;for(int i=2;i<15;i++){pow10[i]=pow10[i-1]*10;zeroNum[i]=zeroNum[i-1] + pow10[i-1];dp[i]=10*dp[i-1] + pow10[i-1];}}void solve(LL n,LL ans[]){if(n==0){ans[0]=1;return;}LL bitNum = log10(n) + 1;LL num[20];LL nTmp = n;for(int i=1;i<=bitNum;i++){num[i] = nTmp%10;nTmp/=10;}for(int i=bitNum;i>=1;i--){LL x = num[i];LL y = i;n -= x*pow10[y-1];LL z = n;for(int j=0;j<10;j++){ans[j] += dp[y-1]*x;}for(int j=0;j<x;j++){ans[j]+=pow10[y-1];}ans[x] += z+1;}ans[0]-=zeroNum[bitNum];
}int main(){cin>>n;  makeDp();   solve(n,ans);for(int i=0;i<10;i++){if(i==0) printf("%lld\n", ans[i]-1);else printf("%lld\n",ans[i]);}
}

 四、 相关例题:

洛谷P2602:https://www.luogu.com.cn/problem/P2602

题目描述

给定两个正整数 aa 和 bb,求在 [a,b][a,b] 中的所有整数中,每个数码(digit)各出现了多少次。

输入格式

仅包含一行两个整数 a,ba,b,含义如上所述。

输出格式

包含一行十个整数,分别表示 0\sim 90∼9 在 [a,b][a,b] 中出现了多少次。

输入输出样例

输入 

1 99

输出 

9 20 20 20 20 20 20 20 20 20

说明/提示

数据规模与约定

  • 对于 30% 的数据,保证  a≤ b ≤ 10^6;
  • 对于 100% 的数据,保证 1≤a≤b≤10^12。

改改代码直接交:

#include <bits/stdc++.h>using namespace std;typedef long long LL;LL a,b;
LL dp[20], ans1[20],ans2[20],zeroNum[20];
LL pow10[20];void makeDp(){pow10[0]=1;for(int i=1;i<15;i++) pow10[i] = pow10[i-1]*10;dp[0]=0,dp[1]=1;zeroNum[0]=zeroNum[1]=0;for(int i=2;i<15;i++){pow10[i]=pow10[i-1]*10;zeroNum[i]=zeroNum[i-1] + pow10[i-1];dp[i]=10*dp[i-1] + pow10[i-1];}}void solve(LL n,LL ans[]){if(n==0){ans[0]=1;return;}LL bitNum = log10(n) + 1;LL num[20];LL nTmp = n;for(int i=1;i<=bitNum;i++){num[i] = nTmp%10;nTmp/=10;}for(int i=bitNum;i>=1;i--){LL x = num[i];LL y = i;n -= x*pow10[y-1];LL z = n;for(int j=0;j<10;j++){ans[j] += dp[y-1]*x;}for(int j=0;j<x;j++){ans[j]+=pow10[y-1];}ans[x] += z+1;}ans[0]-=zeroNum[bitNum];
}int main(){cin>>a>>b;makeDp();solve(a-1,ans1);solve(b,ans2);for(int i=0;i<10;i++){printf("%lld ",ans2[i]-ans1[i]);}
}

 

这篇关于第一篇 - 统计数字问题 (数位DP)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/551402

相关文章

好题——hdu2522(小数问题:求1/n的第一个循环节)

好喜欢这题,第一次做小数问题,一开始真心没思路,然后参考了网上的一些资料。 知识点***********************************无限不循环小数即无理数,不能写作两整数之比*****************************(一开始没想到,小学没学好) 此题1/n肯定是一个有限循环小数,了解这些后就能做此题了。 按照除法的机制,用一个函数表示出来就可以了,代码如下

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

hdu4826(三维DP)

这是一个百度之星的资格赛第四题 题目链接:http://acm.hdu.edu.cn/contests/contest_showproblem.php?pid=1004&cid=500 题意:从左上角的点到右上角的点,每个点只能走一遍,走的方向有三个:向上,向下,向右,求最大值。 咋一看像搜索题,先暴搜,TLE,然后剪枝,还是TLE.然后我就改方法,用DP来做,这题和普通dp相比,多个个向上

hdu1011(背包树形DP)

没有完全理解这题, m个人,攻打一个map,map的入口是1,在攻打某个结点之前要先攻打其他一个结点 dp[i][j]表示m个人攻打以第i个结点为根节点的子树得到的最优解 状态转移dp[i][ j ] = max(dp[i][j], dp[i][k]+dp[t][j-k]),其中t是i结点的子节点 代码如下: #include<iostream>#include<algorithm

hdu4865(概率DP)

题意:已知前一天和今天的天气概率,某天的天气概率和叶子的潮湿程度的概率,n天叶子的湿度,求n天最有可能的天气情况。 思路:概率DP,dp[i][j]表示第i天天气为j的概率,状态转移如下:dp[i][j] = max(dp[i][j, dp[i-1][k]*table2[k][j]*table1[j][col] )  代码如下: #include <stdio.h>#include

usaco 1.1 Broken Necklace(DP)

直接上代码 接触的第一道dp ps.大概的思路就是 先从左往右用一个数组在每个点记下蓝或黑的个数 再从右到左算一遍 最后取出最大的即可 核心语句在于: 如果 str[i] = 'r'  ,   rl[i]=rl[i-1]+1, bl[i]=0 如果 str[i] = 'b' ,  bl[i]=bl[i-1]+1, rl[i]=0 如果 str[i] = 'w',  bl[i]=b

购买磨轮平衡机时应该注意什么问题和技巧

在购买磨轮平衡机时,您应该注意以下几个关键点: 平衡精度 平衡精度是衡量平衡机性能的核心指标,直接影响到不平衡量的检测与校准的准确性,从而决定磨轮的振动和噪声水平。高精度的平衡机能显著减少振动和噪声,提高磨削加工的精度。 转速范围 宽广的转速范围意味着平衡机能够处理更多种类的磨轮,适应不同的工作条件和规格要求。 振动监测能力 振动监测能力是评估平衡机性能的重要因素。通过传感器实时监

缓存雪崩问题

缓存雪崩是缓存中大量key失效后当高并发到来时导致大量请求到数据库,瞬间耗尽数据库资源,导致数据库无法使用。 解决方案: 1、使用锁进行控制 2、对同一类型信息的key设置不同的过期时间 3、缓存预热 1. 什么是缓存雪崩 缓存雪崩是指在短时间内,大量缓存数据同时失效,导致所有请求直接涌向数据库,瞬间增加数据库的负载压力,可能导致数据库性能下降甚至崩溃。这种情况往往发生在缓存中大量 k

uva 10154 DP 叠乌龟

题意: 给你几只乌龟,每只乌龟有自身的重量和力量。 每只乌龟的力量可以承受自身体重和在其上的几只乌龟的体重和内。 问最多能叠放几只乌龟。 解析: 先将乌龟按力量从小到大排列。 然后dp的时候从前往后叠,状态转移方程: dp[i][j] = dp[i - 1][j];if (dp[i - 1][j - 1] != inf && dp[i - 1][j - 1] <= t[i]

uva 10118 dP

题意: 给4列篮子,每次从某一列开始无放回拿蜡烛放入篮子里,并且篮子最多只能放5支蜡烛,数字代表蜡烛的颜色。 当拿出当前颜色的蜡烛在篮子里存在时,猪脚可以把蜡烛带回家。 问最多拿多少只蜡烛。 代码: #include <iostream>#include <cstdio>#include <cstdlib>#include <algorithm>#include <cs