统计数字问题
1.问题概述
问题描述:一本书的页码从自然数1开始顺序编码直到自然数n。书的页码按照通常的习惯编排,每个页码都不含多余的前导数字0。例如,第6页用数字6表示而不是06或006等。数字计数问题要求对给定书的总页码n,计算书的全部页码分别用到多少次数字0,1,2,…,9。
算法设计:给定表示书的总页码的十进制整数n (
1
≤
n
≤
1
0
9
1≤n≤10^9
1≤n≤109),计算书的全部页码中分别用到多少次数字0, 1,2,…,9。
数据输入:输入数据由文件名为input.txt 的文本文件提供。每个文件只有1行,给出表示书的总页码的整数n。
结果输出:将计算结果输出到文件output.txt。输出文件共10行,在第
k
(
k
=
1
,
2
,
…
,
10
)
k(k=1,2,…,10)
k(k=1,2,…,10)行输出页码中用到数字
k
−
1
k-1
k−1的次数。
2.暴力循环解决
根据题目要求,可以设定两个循环,最外层循环来遍历1~n,用来枚举整个过程的页码。内层循环对每个数进行位数拆分,设定一个数组用来记录拆分后每位数字出现的次数,最后打印即可。
代码实现
下面的代码路径根据实际情况进行修改。引入了chrono头文件,计算了程序运行的时间,方便与递归方法进行运行时间的比较。
#include <iostream>
#include <fstream>
#include <chrono>
using namespace std;
using ll = long long;
const int N = 10;
int a[N]; // 当N = 10时, 下标范围为[0,9], a[i]用来代表i出现的次数
// 从文本文件中读取
int read();
// 向文本文件中写入
void write();
// check(i) 用来计算i这个数每位出现的次数
void check(int i);
// 从文本文件中读取
int read() {
// 打开文件 input.txt, 若无法打开进行判别
ifstream input_file("F:/Application/untitled/input.txt");
if (!input_file) {
cerr << "无法打开文件 input.txt" << endl;
cout << "程序即将终止!" << endl;
exit(EXIT_FAILURE); // 使用 exit() 终止整个程序
}
// 读取 input.txt中的输入整数n
int n;
input_file >> n;
cout << "正常打开文件 input.txt, 成功读取输入的页码为: " << n << endl;
// 关闭文件
input_file.close();
cout << "关闭文件 input.txt" << endl;
return n;
}
// 向文本文件中写入
void write() {
// 打开文件 output.txt
ofstream output_file("./output.txt", ios::out | ios::trunc); // 打开文件 output.txt,如果不存在则创建
if (!output_file) {
cerr << "无法创建或打开文件 output.txt" << endl;
cout << "程序即将终止!" << endl;
exit(EXIT_FAILURE); // 使用 exit() 终止整个程序
}
// 将结果输出到 output.txt
for(int i = 0; i <= 9; i ++) {
output_file << a[i] << "\n";
}
cout << "数字0-9出现的次数分别为:" << endl;
// 将结果输出到黑框
for(int i = 0; i <= 9; i ++) {
cout << a[i] << " ";
}
cout << endl;
cout << "成功创建或打开文件 output.txt, 已完成输出 " << endl;
// 关闭文件
output_file.close();
cout << "关闭文件 output.txt" << endl;
}
// check(i) 用来计算i这个数每位出现的次数
void check(int i) {
while(i) {
a[i%10] ++;
i /= 10;
}
}
int main() {
// 记录程序开始时间
auto start_time = chrono::high_resolution_clock::now();
// 读取文件
int n = read();
// 进行计算
for(int i = 1; i <= n; i ++) {
check(i);
}
// 输出到文件
write();
// 记录程序结束时间
auto end_time = chrono::high_resolution_clock::now();
// 计算运行时间
chrono::duration<double> execution_time = end_time - start_time;
cout << "程序执行时间:" << execution_time.count() << " 秒" << endl;
return 0;
}
3. 数学分析法/递归方法
数字分析法是通过分析0 ~ 9 , 00 ~ 99 , 000 ~ 999 …等规律的数字中每位数字出现的次数(每位数字出现的次数相同),进而将任意的数字拆分为规律数字的多个区间与剩余数字,而剩余数字又可以进行拆分,其中包含了递归的思想。
3.1 规律分析
0~
9范围中,每位数字出现1次。
00~
99范围中,每位数字出现几次呢?我们可以逐一分析,先分析十位、再分析个位。
-
针对于十位来说,假设固定十位为0,个位可以取0~9,0取了10次。如果固定十位为其他数字,也会得到相同的结果。故十位中0
~
9的出现次数均为10次。 -
固定住十位,假设固定十位为0,则个位可以取0
~
9,各取1次。如果固定十位为其他数字,也会得到相同的结果,其个位可以取0~
9。由于固定十位有10种可能(0~
9),在每一种可能下,个位均可取0~
9,0~
9出现1次。故个位中0~
9的出现次数均为10次。固定十位,0(0~
9)、1(0~
9)…….9(0~
9),分成了10个0~
9范围内每位数字出现次数的探讨,而0~9范围内,每位数字出现1次。 -
00~99范围内,每位数字出现次数 = 最高位出现的次数 + 10 乘以 除去最高位后此数内每位数字出现的次数(分成了10个0
~
9)= 10 + 10 = 20次
000~
999范围内, 每位数字出现几次呢?仍逐一分析,先分析百位。
- 对于百位来说,假设固定百位为0,后两位可以取00
~
99,共100种情况,故百位的0出现100次。固定百位为1,后两位可以取00~
99,故百位的1出现100次。…依次类推,百位中0~
9的出现次数,由后两位能够表示的范围决定,故为100次。假设在0000~
9999范围内,千位的0~9,由后三位能够表示的范围决定,故为1000次。 - 0(00
~
99)、1(00~
99)、2(00~
99)…9(00~
99),分成了10个00~
99范围的探讨,而00~
99范围内,每位出现20次(来源于上面推导的结论)。 - 000~999范围中,每位数字出现次数 = 最高位出现的次数 + 10 乘以 除去最高位后此数内每位数字出现次数 = 100 + 10 x 20 = 300
f(n)指的是n位数字中每个数字出现的次数(单指一个数字,并不是总和)
通过观察可以得出递推式:
f
(
n
)
=
{
1
,
n
=
1
10
f
(
n
−
1
)
+
1
0
n
−
1
,
n
>
1
f(n) = \begin{cases} 1, & n = 1 \\ 10f(n-1)+10^{n-1}, & n>1 \end{cases}
f(n)={1,10f(n−1)+10n−1,n=1n>1
其中:
1 0 n − 1 10^{n-1} 10n−1:代表当前最高位出现的次数
10 f ( n − 1 ) 10f(n-1) 10f(n−1):代表可以划分为10个子问题,子问题为除去最高位后此数每位数字出现次数
整理可得:
f
(
n
)
=
n
1
0
n
−
1
f(n)=n10^{n-1}
f(n)=n10n−1
3.2 实现思路
注意:上面图片颜色的对应
设定输入的变量为 n n n,c代表n的位数,m代表n的最高位,r代表m除了最高位,其余位。
例如: n = 2222 , c ( 位数 ) = 4 , m ( 最高位 ) = 2 , r ( 其余位 ) = 222 n = 2222, c(位数) = 4, m(最高位) = 2,r(其余位) = 222 n=2222,c(位数)=4,m(最高位)=2,r(其余位)=222
第一部分:计算除了n的最高位,其他位0~9出现的次数
其他位0~9出现的次数 = = = n n n的最高位 × × × f ( c − 1 ) f(c-1) f(c−1)
例如 n = 2222,最高位m可以代表有几组000~
999的数,
f
(
c
−
1
)
=
f
(
4
−
1
)
=
f
(
3
)
f(c-1)=f(4-1)=f(3)
f(c−1)=f(4−1)=f(3)代表000~
999的0~
9每位出现多少次。这一部分处理,是通过递归逐步进行的。
第二部分:统计最高位出现的次数
以n = 2222为例,最高位的值为2,则最高位出现的范围0~2
由于0,1不等于最高位的值,出现次数
=
=
=
1
0
c
−
1
10^{c-1}
10c−1
2等于最高位的值, 出现次数
=
=
=
r
+
1
r + 1
r+1
解释:n = 2222, 最高位2出现的情况2000~2222,为222 + 1(包括2000)。
第三部分:处理其余位(去掉最高位)
-
假设n=2222,则剩下的其余位222,直接递归处理即可,跳转到第一部分。
-
例如当 n n n= 1003 1003 1003时,计算机计算 r r r的结果为3(不带前导0),但我们知道 r = 003 r = 003 r=003 (带上前导0)才是正确的结果,中间的0要补上。
其中:2为前导0个数,3为当前r的大小(代表001~
003) + 1(代表000的情况) -
假设n=8000,则剩下的其余位000,其余位为0,这说明程序结束。
第四部分:删去多于的0
前三部分是一直进行递归的求解,当前三部分求解完毕后。进入第四部分,在前面的求解过程中,我们使用了 f ( n ) f(n) f(n)函数,但此函数包括了前导0。例如,00~99范围内,
01、02…等情况中的0是多余的,应该删除这样的0。
例如,0000~1000的前导0如下所示:
3.3 实现代码
在代码中,solve函数对应着3.2的前三部分的不断递归过程。
#include <iostream>
#include <cmath>
#include <fstream>
#include <cstdlib>
#include <chrono>
using namespace std;
const int N = 10;
int c[N]; // 记录次数的数组, c[i]记录i出现的次数, 范围为0~9
// 从文本文件中读取输入变量
int read();
// 向文本文件中写入
void write();
// getDigitCount函数用来获取数字n的位数
int getDigitCount(int n);
// getFirstDigit函数用来获取数字n的最高位
int getFirstDigit(int n);
// getRemainder函数用来获取数字n除最高位的其他数字
int getRemainder(int n);
// getZero函数用来获取m位数中前导0的个数
int getZero(int m);
// 从文本文件中读取
int read() {
// 打开文件 input.txt, 若无法打开进行判别
ifstream input_file("F:/Application/untitled/input.txt");
if (!input_file) {
cerr << "无法打开文件 input.txt" << endl;
cout << "程序即将终止!" << endl;
exit(EXIT_FAILURE); // 使用 exit() 终止整个程序
}
// 读取 input.txt中的输入整数n
int n;
input_file >> n;
cout << "正常打开文件 input.txt, 成功读取输入的页码为: " << n << endl;
// 关闭文件
input_file.close();
cout << "关闭文件 input.txt" << endl;
return n;
}
// 向文本文件中写入
void write() {
// 打开文件 output.txt
ofstream output_file("./output.txt", ios::out | ios::trunc); // 打开文件 output.txt,如果不存在则创建
if (!output_file) {
cerr << "无法创建或打开文件 output.txt" << endl;
cout << "程序即将终止!" << endl;
exit(EXIT_FAILURE); // 使用 exit() 终止整个程序
}
// 将结果输出到 output.txt
for(int i = 0; i <= 9; i ++) {
output_file << c[i] << "\n";
}
cout << "数字0-9出现的次数分别为:" << endl;
// 将结果输出到黑窗口
for(int i = 0; i <= 9; i ++) {
cout << c[i] << " ";
}
cout << endl;
cout << "成功创建或打开文件 output.txt, 已完成输出 " << endl;
// 关闭文件
output_file.close();
cout << "关闭文件 output.txt" << endl;
}
// getDigitCount函数用来获取数字n的位数
int getDigitCount(int n) {
return int(log10(n) + 1);
}
// getFirstDigit函数用来获取数字n的最高位
int getFirstDigit(int n) {
return n / int(pow(10, getDigitCount(n)-1)); // pow会返回浮点数类型, 使用int进行强制性转换
}
// getRemainder函数用来获取数字n除最高位的其他数字
// 例如:传入4321 返回321
int getRemainder(int n) {
return n % int(pow(10, getDigitCount(n)-1));
}
// getZero函数用来获取m位数中0的个数
// 计算出前导0的个数: \sum_{i=0}^{n-1}10^{i}
int getZero(int m) {
if(m == 1) {
return 1;
}
return getZero(m-1) + int(pow(10, m-1));
}
// 解决函数:进行计算
void solve(int n) {
// 除最高位之外, 其他位数字出现的次数 --- 对应第一部分
for(int i = 0; i < 10; i ++) {
c[i] += getFirstDigit(n) * (getDigitCount(n) - 1) * int(pow(10, getDigitCount(n)-2));
}
// 计算最高位除最大数字之外其余数字出现的次数 --- 对应第二部分
for(int i = 0; i < getFirstDigit(n); i ++) {
c[i] += int(pow(10, getDigitCount(n)-1));
}
// 最高位最大数出现的次数 = 等于余数 + 1, 这里加1是为了包括 最高位+后面全0的情况
c[getFirstDigit(n)] += getRemainder(n) + 1;
// 开始处理余数
int remainder = getRemainder(n);
// 余数为0, 则0的次数增加此数字的位数-1, 也就是不计最高位
if (remainder == 0) {
c[0] += getDigitCount(n) - 1;
return ;
}
// 如果余数不为0, 进行下面的处理
int length = getDigitCount(remainder); // 记录remainder的位数
if(length != getDigitCount(n) - 1) { // 如果不等, 说明次数中间存在0, 这个0是需要计算的
c[0] += (getDigitCount(n) - length - 1) * (remainder + 1); // 由于从0开始, 所以remainder+1, getDigitCount(n) - length - 1 统计中间0的个数
}
return solve(remainder);
}
int main() {
// 记录程序开始时间
auto start_time = chrono::high_resolution_clock::now();
// 读取input.txt
int n = read();
// 进行计算处理, 同时计算前导0, 删去多于的前导0
solve(n);
c[0] -= getZero(getDigitCount(n));
write();
// 记录程序结束时间
auto end_time = chrono::high_resolution_clock::now();
// 计算运行时间
chrono::duration<double> execution_time = end_time - start_time;
cout << "程序执行时间:" << execution_time.count() << " 秒" << endl;
return 0;
}
// 进行计算处理, 同时计算前导0, 删去多于的前导0
solve(n);
c[0] -= getZero(getDigitCount(n));
write();
// 记录程序结束时间
auto end_time = chrono::high_resolution_clock::now();
// 计算运行时间
chrono::duration<double> execution_time = end_time - start_time;
cout << "程序执行时间:" << execution_time.count() << " 秒" << endl;
return 0;
}