1-1统计数字问题
(一)题目
问题描述
一本书的页码从自然数 1 开始顺序编码到自然数 n n n。书的页码按照通 常的习惯编排,每个页码都不含多余的前导 0。例如,第 6 页用数字 6 表示而不 是 06 或 006 等。数字计数问题要求对给定书的总页码 n n n,计算书的全部页码分别 用到多少次数字 0,1,2,……,9。
算法设计
给定表示书的总页码的十进制整数 n ( 1 ≤ n ≤ 1 0 9 ) n(1\leq n\leq10^{9}) n(1≤n≤109),计算书的全部页 码中分别用到多少次数字 0,1,2,……,9。 数据输入:输入数据由文件名为input.txt的文本文件提供。每个文件只有1行, 给出表示书的总页码的整数 n n n。
结果输出
将计算结果输出到文件 output.txt。输出文件共 10 行,在第 k ( k = 1 , 2 , … … , 10 ) k(k=1,2,……,10) k(k=1,2,……,10)行输出页码中用到数字$ k-1$ 的次数。
(二)解答
方法1.暴力
算法思路
从 0 到 n n n依次遍历每一页的页码,把页码从低位到高位用循环依次拆开,记录每个数字的出现次数。
举例
源代码
#include<iostream>
#include<cstdio>
#include<fstream>
using namespace std;
//读取
int read();
//写入
void write(int a[]);
//计算总页码0-9出现的次数
void solution(int n, int a[]);
int main()
{
int a[10] = {0};
//从输入文件获取总页数n
int n = read();
//计算0-9出现的次数
solution(n, a);
//将0-9的出现次数写入输出文件
write(a);
return 0;
}
int read()
{
ifstream ifs;
//打开输入文件
ifs.open("G:\\algorithm\\data\\1_1_input.txt", ios::in);
//读取数据
int n;
ifs>>n;
//关闭输入文件
ifs.close();
//返回总页数n;
return n;
}
void write(int a[])
{
ofstream ofs;
//创建输出文件
ofs.open("G:\\algorithm\\data\\1_1_1out.txt", ios::out);
//写入数据
for (int i = 0; i < 10; ++i)
{
ofs<<a[i]<<endl;
}
//关闭输出文件
ofs.close();
}
void solution(int n, int a[])
{
//依次遍历每一页的页码i
for (int i = 1; i <= n; ++i)
{
//用t暂时存储页码i
int t = i;
//从右往左依次记录t每一位上的数字的出现次数,直到t=0
while (t)
{
a[t % 10]++;
t /= 10;
}
}
}
方法2.递归
算法思路
对于从 n n n 个 0 到 n n n 个 9 的数,它们中 0-9 出现的次数相同,记为 f ( n ) f(n) f(n)
通过观察可以得出递归式:
f
(
n
)
=
{
1
,
n
=
1
10
f
(
n
−
1
)
+
1
0
n
−
1
,
n
>
1
f(n)=\begin{cases} 1,\;\;\qquad \qquad \qquad \quad \qquad n=1\\ 10f(n-1)+10^{n-1},\qquad n>1\\ \end{cases}
f(n)={1,n=110f(n−1)+10n−1,n>1
整理可得:
f
(
n
)
=
n
1
0
n
−
1
f(n)=n10^{n-1}
f(n)=n10n−1
因此,我们可以利用这个规律进行运算:
(1)递归计算 0(包含前导 0)到𝑛各数字出现的次数
设页码数 n n n的位数为 c c c,最高位为 m m m,除最高位外的部分为 r r r。 0(包含前导 0)到 n n n的数由三部分构成:
第一部分是 m m m组 c − 1 c-1 c−1个 0 到 9 的数,这部分 0 到 9 出现的次数相同,均为 m ( c − 1 ) 1 0 c − 2 m(c-1)10^{c-2} m(c−1)10c−2次;
第二部分为最高位,0 到 m − 1 m-1 m−1各出现 1 0 c − 1 10^{c-1} 10c−1次, m m m出现 r + 1 r+1 r+1次;
第三部分为 0(包含前导 0)到 r r r 的数,按照同样的方法进行递归,直到 r 的位数为 1,0 到 r r r 各出现一次。
值得注意的是,计算机计算 r r r时是不带前导 0 的,如果 n n n中间出现 0,这部分的 0 将被略去,因此,我们要把这部分的 0 补上。设 r r r的位数为 c r cr cr,则需要补上 ( c − c r − 1 ) ( r + 1 ) (c-cr-1)(r+1) (c−cr−1)(r+1)个 0。
(2)减去多余的前导 0
对于位数为 c c c的页码数 n n n多余的前导 0 个数为 1 0 0 + ⋯ + 1 0 c − 1 10^0 + ⋯ + 10^{c−1} 100+⋯+10c−1
举例
以 n n n=2222为例,先递归计算 0(包含前导 0)到𝑛各数字出现的次数
注意 n n n的中间有0的情况
例如当
n
n
n=1003时,计算机计算
r
r
r的结果为3(不带前导0),但我们知道
r
=
003
r=003
r=003(带上前导0)才是正确的结果,中间的0要补上
然后删去多余的前导0,对于一个位数为
c
c
c数
n
n
n,它的前导0个数是固定的
将所有位置上的多余前导0加起来删去即可得最终结果
源代码
#include<iostream>
#include<cstdio>
#include<fstream>
#include<cmath>
using namespace std;
//读取
int read();
//写入
void write(int a[]);
//求n的位数
int digit(int n);
//计算n个0到n个9中0-9各出现的次数
int f(int n);
//递归计算总页码0-9出现的次数
void solution(int n, int a[]);
//减去多余的0
void substract(int n, int a[]);
int main()
{
int a[10] = {0};
//从输入文件获取总页数n
int n = read();
//递归计算0-9出现的次数
solution(n, a);
//减去多余的0
substract(n, a);
//将0-9的出现次数写入输出文件
write(a);
return 0;
}
int read()
{
ifstream ifs;
//打开输入文件
ifs.open("G:\\algorithm\\data\\1_1_input.txt", ios::in);
//读取数据
int n;
ifs>>n;
//关闭输入文件
ifs.close();
//返回总页数n;
return n;
}
void write(int a[])
{
ofstream ofs;
//创建输出文件
ofs.open("G:\\algorithm\\data\\1_1_2out.txt", ios::out);
//写入数据
for (int i = 0; i < 10; ++i)
{
ofs<<a[i]<<endl;
}
//关闭输出文件
ofs.close();
}
int digit(int n)
{
return (int)log10(n) + 1;
}
int f(int n)
{
//n=0时无法通过公式求n的位数
if (n == 0)
{
return 1;
}
return n * (int)pow(10.0, n - 1);
}
void solution(int n, int a[])
{
//求n的位数c,c-1个0到9中0-9出现的次数相同
int c = digit(n);
//递归到只剩个位的情况
if (c == 1)
{
for (int i = 0; i <= n; ++i)
{
a[i]++;
}
return;
}
//求n的最高位m,共有m组c-1个0到9的数
int m = n / (int)pow(10.0, c - 1);
//求n的最高位以外的数r
int r = n % (int)pow(10.0, c - 1);
//求m组c-1个0到9中0-9出现的次数
for (int i = 0; i <= 9; ++i)
{
a[i] += m * f(c - 1);
}
//再处理最高位
for (int i = 0; i < m; ++i)
{
a[i] += (int)pow(10.0, c - 1);
}
a[m] += r + 1;
//如果r的位数不是c-1,说明n中间含有0,把这些0补上
int cr = digit(r);
if (cr != c - 1)
{
a[0] += (c -cr - 1) * (r + 1);
}
//递归处理r中0-9出现的次数
solution(r, a);
}
void substract(int n, int a[])
{
int c = digit(n);
for (int i = 0; i < c; ++i)
{
a[0] -= (int)pow(10.0, i);
}
}
结果示例
输入:
输出:
(三)总结
对两种方法进行复杂度分析,结果如下:
方法1.暴力
t
(
n
)
=
n
×
(
(
l
o
g
10
1
+
1
)
+
(
l
o
g
10
2
+
1
)
+
.
.
.
+
(
l
o
g
10
n
+
1
)
)
+
10
=
n
×
(
l
o
g
10
1
+
l
o
g
10
2
+
+
.
.
.
+
l
o
g
10
n
+
n
)
+
10
<
n
×
(
1
+
2
+
.
.
.
+
n
+
n
)
+
10
=
O
(
n
2
)
\begin{aligned} t(n)&=n\times((log_{10}1+1)+(log_{10}2+1)+...+(log_{10}n+1))+10\\ &=n\times(log_{10}1+log_{10}2++...+log_{10}n+n)+10\\ &<n\times(1+2+...+n+n)+10\\ &=\Omicron(n^2) \end{aligned}
t(n)=n×((log101+1)+(log102+1)+...+(log10n+1))+10=n×(log101+log102++...+log10n+n)+10<n×(1+2+...+n+n)+10=O(n2)
方法2.递归
t
(
n
)
=
⌊
l
o
g
10
n
⌋
×
(
10
+
⌊
n
1
0
⌊
l
o
g
10
⌋
+
1
⌋
)
+
n
M
O
D
10
+
(
⌊
l
o
g
10
n
⌋
+
1
)
+
10
=
O
(
n
l
o
g
n
)
\begin{aligned} t(n)&=\lfloor log_{10}n\rfloor\times(10+\lfloor\frac{n}{10^{\lfloor log_{10}\rfloor+1}}\rfloor)+n MOD 10+(\lfloor log_{10}n\rfloor+1)+10\\ &=\Omicron(nlogn) \end{aligned}
t(n)=⌊log10n⌋×(10+⌊10⌊log10⌋+1n⌋)+nMOD10+(⌊log10n⌋+1)+10=O(nlogn)
可以看到递归的复杂度小于暴力