1-1统计数字问题

(一)题目

问题描述

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

算法设计

给定表示书的总页码的十进制整数n(1≤n≤109)n(1\leq n\leq10^{9})n(1n109),计算书的全部页 码中分别用到多少次数字 0,1,2,……,9。 数据输入:输入数据由文件名为input.txt的文本文件提供。每个文件只有1行, 给出表示书的总页码的整数nnn

结果输出

将计算结果输出到文件 output.txt。输出文件共 10 行,在第 k(k=1,2,……,10)k(k=1,2,……,10)k(k=1,2,,10)行输出页码中用到数字$ k-1$ 的次数。

(二)解答

方法1.暴力
算法思路

从 0 到nnn依次遍历每一页的页码,把页码从低位到高位用循环依次拆开,记录每个数字的出现次数。

举例

在这里插入图片描述

源代码
#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.递归
算法思路

对于从 nnn 个 0 到 nnn 个 9 的数,它们中 0-9 出现的次数相同,记为f(n)f(n)f(n)

在这里插入图片描述

通过观察可以得出递归式:
f(n)={1,    n=110f(n−1)+10n−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(n1)+10n1,n>1
整理可得:
f(n)=n10n−1 f(n)=n10^{n-1} f(n)=n10n1
因此,我们可以利用这个规律进行运算:

(1)递归计算 0(包含前导 0)到𝑛各数字出现的次数

设页码数nnn的位数为ccc,最高位为mmm,除最高位外的部分为rrr。 0(包含前导 0)到nnn的数由三部分构成:

第一部分是mmmc−1c-1c1个 0 到 9 的数,这部分 0 到 9 出现的次数相同,均为m(c−1)10c−2m(c-1)10^{c-2}m(c1)10c2次;

第二部分为最高位,0 到 m−1m-1m1各出现10c−110^{c-1}10c1次,mmm出现r+1r+1r+1次;

第三部分为 0(包含前导 0)到 rrr 的数,按照同样的方法进行递归,直到 r 的位数为 1,0 到rrr 各出现一次。

值得注意的是,计算机计算rrr时是不带前导 0 的,如果nnn中间出现 0,这部分的 0 将被略去,因此,我们要把这部分的 0 补上。设rrr的位数为crcrcr,则需要补上(c−cr−1)(r+1)(c-cr-1)(r+1)(ccr1)(r+1)个 0。

(2)减去多余的前导 0

对于位数为ccc的页码数nnn多余的前导 0 个数为100+⋯+10c−110^0 + ⋯ + 10^{c−1}100++10c1

举例

nnn=2222为例,先递归计算 0(包含前导 0)到𝑛各数字出现的次数

在这里插入图片描述

注意nnn的中间有0的情况

例如当nnn=1003时,计算机计算rrr的结果为3(不带前导0),但我们知道r=003r=003r=003(带上前导0)才是正确的结果,中间的0要补上
在这里插入图片描述

然后删去多余的前导0,对于一个位数为cccnnn,它的前导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×((log101+1)+(log102+1)+...+(log10n+1))+10=n×(log101+log102++...+log10n+n)+10<n×(1+2+...+n+n)+10=O(n2) \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)=⌊log10n⌋×(10+⌊n10⌊log10⌋+1⌋)+nMOD10+(⌊log10n⌋+1)+10=O(nlogn) \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+10log10+1n)+nMOD10+(log10n+1)+10=O(nlogn)
可以看到递归的复杂度小于暴力

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐