1-1统计数字问题

统计数字问题

1.问题概述

问题描述:一本书的页码从自然数1开始顺序编码直到自然数n。书的页码按照通常的习惯编排,每个页码都不含多余的前导数字0。例如,第6页用数字6表示而不是06或006等。数字计数问题要求对给定书的总页码n,计算书的全部页码分别用到多少次数字0,1,2,…,9。
算法设计:给定表示书的总页码的十进制整数n ( 1 ≤ n ≤ 1 0 9 1≤n≤10^9 1n109),计算书的全部页码中分别用到多少次数字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 k1的次数。

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(n1)+10n1,n=1n>1
其中:

1 0 n − 1 10^{n-1} 10n1:代表当前最高位出现的次数

10 f ( n − 1 ) 10f(n-1) 10f(n1):代表可以划分为10个子问题,子问题为除去最高位后此数每位数字出现次数

整理可得:
f ( n ) = n 1 0 n − 1 f(n)=n10^{n-1} f(n)=n10n1

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=2222c(位数)=4,m(最高位)=2r(其余位)=222

第一部分:计算除了n的最高位,其他位0~9出现的次数

其他位0~9出现的次数 = = = n n n的最高位 × × × f ( c − 1 ) f(c-1) f(c1)

例如 n = 2222,最高位m可以代表有几组000~999的数, f ( c − 1 ) = f ( 4 − 1 ) = f ( 3 ) f(c-1)=f(4-1)=f(3) f(c1)=f(41)=f(3)代表000~999的0~9每位出现多少次。这一部分处理,是通过递归逐步进行的。

第二部分:统计最高位出现的次数

以n = 2222为例,最高位的值为2,则最高位出现的范围0~2
由于0,1不等于最高位的值,出现次数 = = = 1 0 c − 1 10^{c-1} 10c1
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;
}
  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值