算法设计与分析-统计数字问题(暴力、分治递归)(通俗易懂,附源码和图解,含时间复杂度分析)(c++)
1-1统计数字问题(一)题目问题描述一本书的页码从自然数 1 开始顺序编码到自然数nnn。书的页码按照通 常的习惯编排,每个页码都不含多余的前导 0。例如,第 6 页用数字 6 表示而不 是 06 或 006 等。数字计数问题要求对给定书的总页码nnn,计算书的全部页码分别 用到多少次数字 0,1,2,……,9。算法设计给定表示书的总页码的十进制整数n(1≤n≤109)n(1\leq n\leq1
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(1≤n≤109),计算书的全部页 码中分别用到多少次数字 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(n−1)+10n−1,n>1
整理可得:
f(n)=n10n−1 f(n)=n10^{n-1} f(n)=n10n−1
因此,我们可以利用这个规律进行运算:
(1)递归计算 0(包含前导 0)到𝑛各数字出现的次数
设页码数nnn的位数为ccc,最高位为mmm,除最高位外的部分为rrr。 0(包含前导 0)到nnn的数由三部分构成:
第一部分是mmm组c−1c-1c−1个 0 到 9 的数,这部分 0 到 9 出现的次数相同,均为m(c−1)10c−2m(c-1)10^{c-2}m(c−1)10c−2次;
第二部分为最高位,0 到 m−1m-1m−1各出现10c−110^{c-1}10c−1次,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)(c−cr−1)(r+1)个 0。
(2)减去多余的前导 0
对于位数为ccc的页码数nnn多余的前导 0 个数为100+⋯+10c−110^0 + ⋯ + 10^{c−1}100+⋯+10c−1
举例
以nnn=2222为例,先递归计算 0(包含前导 0)到𝑛各数字出现的次数
注意nnn的中间有0的情况
例如当nnn=1003时,计算机计算rrr的结果为3(不带前导0),但我们知道r=003r=003r=003(带上前导0)才是正确的结果,中间的0要补上
然后删去多余的前导0,对于一个位数为ccc数nnn,它的前导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+⌊10⌊log10⌋+1n⌋)+nMOD10+(⌊log10n⌋+1)+10=O(nlogn)
可以看到递归的复杂度小于暴力
更多推荐
所有评论(0)