zoukankan      html  css  js  c++  java
  • 统计数字

     题目

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

    来源:王晓东编著的《算法设计与实验题解》

    网上有几篇相关的文章:

    文章1

    来自http://blog.csdn.net/jcwKyl/article/details/3009244

    这个题目有个最容易想到的n*log10(n)的算法。这是自己写的复杂度为O(n*log10(n))的代码:

    1. void statNumber(int n) {
    2.     int i, t;
    3.     int count[10] = {0};
    4.     for(i = 1; i <= n; i++) {
    5.     t = i;
    6.     while(t) {
    7.         count[t%10]++;
    8.         t/=10;
    9.     }
    10.     }
    11.     for(i = 0; i < 10; i++) {
    12.     printf("%d/n", count[i]);
    13.     }
    14. }

    仔细考虑m个n位十进制数的特点,在一个n位十进制数的由低到高的第i个数位上,总是连续出现10^i个0,然后是10^i个1……一直到10^i个9,9之后又是连续的10^i个0,这样循环出现。找到这个规律,就可以在常数时间内算出第i个数位上每个数字出现的次数。而在第i个数位上,最前面的10^i个0是前导0,应该把它们减掉。

    这样,可以只分析给定的输入整数n的每个数位,从面可以得到一个log10(n)的算法,代码如下:

    1. void statNumber(int n) {
    2.     int m, i, j, k, t, x, len = log10(n);
    3.     char d[16];
    4.     int pow10[12] = {1}, count[10] = {0};
    5.     for(i = 1; i < 12; i++) {
    6.     pow10[i] = pow10[i-1] * 10;
    7.     }
    8.     sprintf(d, "%d", n);
    9.     m = n+1;
    10.     for(i = 0; i <= len; i++) {
    11.     x = d[i] - '0';
    12.     t = (m-1) / pow10[len-i]; 
    13.     count[x] += m - t * pow10[len-i]; 
    14.     t /= 10;
    15.     j = 0;
    16.     while(j <= x-1) {
    17.         count[j] += (t + 1) * pow10[len-i];
    18.         j++;
    19.     }
    20.     while(j < 10) {
    21.         count[j] += t * pow10[len - i];
    22.         j++;
    23.     }
    24.     count[0] -= pow10[len-i]; /* 第i个数位上前10^i个0是无意义的 */
    25.     }
    26.     for(j = 0; j < 10; j++) {
    27.     printf("%d/n", count[j]);
    28.     }
    29. }

    通过对随机生成的测试数据的比较,可以验证第二段代码是正确的。
    对两段代码做效率测试,第一次随机产生20万个整数,结果在我的电脑上,第二段代码执行1.744秒。第一段代码等我吃完钣回来看还是没反应,就强行关了它。
    第二次产生了1000个整数,再次测试,结果第一段代码在我的电脑上执行的时间是
    10.1440秒,而第二段代码的执行时间是0.0800秒。
    其原因是第一段代码时间复杂度为O(n*log10(n)),对m个输入整数进行计算,则需要的时间为 1*log10(1) + 2*log10(2) + ... + m*log10(m), 当n > 10时,有
    n*log10(n) > n,所以上式的下界为11+12+....+m,其渐近界为m*m。对于20万个测试数据,其运行时间的下界就是4*10^10。
    同样可得第二段代码对于n个输入数据的运行时间界是n*log10(n)的。

    上面的代码中有个pow10数组用来记录10^i,但10^10左右就已经超过了2^32,但是题目给定的输入整数的范围在10^9以内,所以没有影响。
    原著中给出的分析如下:
    考察由0,1,2...9组成的所有n位数。从n个0到n个9共有10^n个n位数。在这10^n个n位数中,0,1,2.....9第个数字使用次数相同,设为f(n)。f(n)满足如下递推式:
    n>1:
    f(n) = 10f(n-1)+10^(n-1)
    n = 1:
    f(n) =1
    由此可知,f(n) = n*10^(n-1)。
    据此,可从高位向低位进行统计,再减去多余的0的个数即可。

    著者的思想说的更清楚些应该是这样:

    对于一个m位整数,我们可以把0到n之间的n+1个整数从小到大这样来排列:

    000......0

    .............

    199......9

    200......0

    299......9

    .........

    这样一直排到自然数n。对于从0到199......9这个区间来说,抛去最高位的数字不看,其低m-1位恰好

    就是m-1个0到m-1个9共10^(m-1)个数。利用原著中的递推公式,在这个区间里,每个数字出现的次数

    (不包括最高位数字)为(m-1)*10^(m-2)。假设n的最高位数字是x,那么在n之间上述所说的区间共有

    x个。那么每个数字出现的次数x倍就可以统计完这些区间。再看最高位数字的情况,显然0到x-1这些

    数字在最高位上再现的次数为10^(m-1),因为一个区间长度为10^(m-1)。而x在最高位上出现次数就是

    n%10^(m-1)+1了。接下来对n%10^(m-1),即n去掉最高位后的那个数字再继续重复上面的方法。直到

    个位,就可以完成题目要求了。

    比如,对于一个数字34567,我们可以这样来计算从1到34567之间所有数字中每个数字出现的次数:
    从0到9999,这个区间的每个数字的出现次数可以使用原著中给出的递推公式,即每个数字出现4000次。

    从10000到19999,中间除去万位的1不算,又是一个从0000到9999的排列,这样的话,从0到34567之间

    的这样的区间共有3个。所以从00000到29999之间除万位外每个数字出现次数为3*4000次。然后再统计

    万位数字,每个区间长度为10000,所以0,1,2在万位上各出现10000次。而3则出现4567+1=4568次。

    之后,抛掉万位数字,对于4567,再使用上面的方法计算,一直计算到个位即可。

    下面是自己的实现代码:

    1. void statNumber_iterative(int n) {
    2.     int len, i, k, h, m;
    3.     int count[10] = {0};
    4.     int pow10[12] = {1,10,100,1000,10000,100000,1000000,10000000,100000000,1000000000};
    5.     char d[16];
    6.     len = log10(n);     /* len表示当前数字的位权 */
    7.     m = len;
    8.     sprintf(d, "%d", n);
    9.     k = 0;          /* k记录当前最高位数字在d数组中的下标 */
    10.     h = d[k] - '0';     /* h表示当前最高位的数字 */
    11.     n %= pow10[len];        /* 去掉n的最高位 */
    12.     while(len > 0) {
    13.     if(h == 0) {
    14.         count[0] += n + 1;
    15.         h = d[++k] - '0';
    16.         --len;
    17.         n %= pow10[len];
    18.         continue;
    19.     }
    20.     for(i = 0; i < 10; i++) {
    21.         count[i] += h * len * pow10[len-1];
    22.     }
    23.     for(i = 0; i < h; i++) {
    24.         count[i] += pow10[len];
    25.     }
    26.     count[h] += n + 1;
    27.     --len;
    28.     h = d[++k] - '0';
    29.     n %= pow10[len];
    30.     }
    31.     for(i = 0; i <= h; i++) {
    32.     count[i] += 1;
    33.     }
    34.     /* 减去前导0的个数 */
    35.     for(i = 0; i <= m; i++) {  
    36.     count[0] -= pow10[i];
    37.     }
    38.     for(i = 0; i < 10; i++) {
    39.     printf("%d/n", count[i]);
    40.     }
    41. }

    文章2

    http://blog.sina.com.cn/s/blog_6f883b5e0100siih.html

    程序实例1:
    思想:穷举法,就是对从1开始到n进行每个数字的拆分,对每个数字拆分一位就对应统计一次。
    程序实例2:
    思想:排列组合
    原著中给出的解答思想;
    考察由0,1,2...9组成的所有n位数。从n个0到n个9共有10^n个n位数。在这10^n个n位数中,0,1,2.....9第个数字使用次数相同,设为f(n)。f(n)满足如下递推式:
    f(n) = 10f(n-1)+10^(n-1)……………………(n>1)
    f(n) =1…………………………………………(n=1)
    由此可知,f(n)=n*10^(n-1)。
    据此,可从高位向低位进行统计,再减去多余的0的个数即可。

    从个位开始统计
    对于第k位有数字为c情况,需要考虑四个方面:
    (1) 第k位是c的时候,前k-1位里面各个位增加了多少?
         c*(k-1)*10^(k-2)
    (2) 第k位中比c小的各个数字增加各出现多少次?
        10^(k-1)
    (3) 第k位中此数字增加了多少次?
    1+(k-1位对应的数字),例如:325,第三位的3数字增加出现了1+25=26
    (4) 抠除此时出现的第k位为0的数字?
    由于(2)里面增加了以0开始的数字,所以现在抠除,即从0数字出现次数,减去10^(k-1)
    这些是怎么来的呢?(-----可参见《编程之美》--1的数目)
    附上pascal程序:
      program count;
    var a:array[0..9]of longint;
        n,t,s,i,j,k,c,s2:longint;s1:string;
    begin
    readln(n);
    t:=0;str(n,s1);//----------//t表示n的位数,为便于操作将n转化为字符串
    while n<>0 do
      begin n:=n div 10;inc(t);end;//------------//求位数
    for i:=1 to t do//------------//枚举位数
      begin
       val(s1[t+1-i],c);//------------//提取第i位的数字c。注意枚举的i跟实际字符串中的位置有一定出入要用s1[t+1-i],而不是s1[i]!
       s:=1;for j:=1 to i-1 do s:=s*10;//------------//s=10^(i-1)
       for k:=0 to 9 do
        a[k]:=a[k]+(i-1)*c*(s div 10);//------------//上述第一个方面:第k位是c的时候,前k-1位里面各个数字增加的个数
       for k:=0 to c-1 do
        a[k]:=a[k]+s;//------------//上述第二个方面: 第k位中比c小的各个数字增加的个数
       val(copy(s1,t-i+2,i-1),s2);//------------//这里要注意截取第i位后的数时(即前i-1位的数),字符串copy处理要从t-i+2(因为第i为对应其中第t-i+1),长度为i-1!!!用字符串这里比较容易错!
       a[c]:=a[c]+s2+1;//------------//上述第三个方面:第k位中此数字增加的个数
       a[0]:=a[0]-s;//------------//上述第四个方面:挖掉多余的0
      end;
    for i:=0 to 9 do writeln(a[i]);
    end.


    作者:撞破南墙
    出处:http://www.cnblogs.com/facingwaller/
    关于作者:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

  • 相关阅读:
    限制结果集行数
    函数的差异
    运算符的差异
    数据类型的差异
    简介
    Spring源码阅读入门指引
    Spring的三种注入方式
    AOP面向切面编程
    leetcode771
    leetcode669
  • 原文地址:https://www.cnblogs.com/facingwaller/p/2377999.html
Copyright © 2011-2022 走看看