<编程之美>计算0到N中包含数字1的个数[转]

有这样一个函数f(n),对于任意正整数n,它表示从 0 到 n 之间出现“1”的个数,比如 f(1) = 1, f(13) = 6,请列出从 1 到 1234567890 中所有的 f(n) = n 的n, 要求准确快速.

 

相信很多人都能立刻得出以下的解法:

  for(n:N)

  {

          判断n包含1的个数;

          累加计数器;

  }

这是最直接的解法,但遗憾的是,时间复杂程度为O(N*logN)。因为还需要循环判断当前的n的各位数,该判断的时间复杂程度为O(logN)。

接下来就应该思考效率更高的解法了。说实话,这道题让我想起另外一道简单的算法题:

N为正整数,计算从1到N的整数和。

很多人都采用了循环求解。然后利用初等数学知识就知道S=N*(N+1)/2,所以用O(1)的时间就可以处理。

再回到本道题目,同理应该去寻找到结果R与N之间的映射关系。

分析如下:

假设N表示为a[n]a[n-1]...a[1],其中a[i](1<=i<=n)表示N的各位数上的数字。

c[i]表示从整数1到整数a[i]...a[1]中包含数字1的个数。

x[i]表示从整数1到10^i - 1中包含数字1的个数,例如,x[1]表示从1到9的个数,结果为1;x[2]表示从1到99的个数,结果为20;

当a[1]=0时,c[1] = 0;

当a[1]=1时,c[1] = 1;

当a[1]>1时,c[1] = 1;

 

当a[2]=1时,c[2] = a[1] +1+ c[1] + x[1];

当a[2]>1时,c[2] = a[2]*x[1]+c[1]+10;

 

当a[3]=1时,c[3] = a[2]*a[1] +1+ c[2] + x[2];

当a[3]>1时,c[3] = a[3]*x[2]+c[2]+10^2;

......

 

以此类推

当a[i]=1时,c[i] = a[i-1]*...*a[1] +1+ c[i-1]+x[i-1];

当a[i]>1时,c[i] = a[i]x[i-1]+c[i-1]+10^(i-1);

 

 

实现的代码如下:

 

Java代码 
  1. public static int search(int _n)      
  2. {      
  3.     int N = _n/10;      
  4.     int a1 = _n%10,a2;      
  5.     int x = 1;   
  6.     int ten = 10;   
  7.     int c = a1 == 0?0:1;   
  8.     while(N > 0)      
  9.     {      
  10.         a2 = N%10;   
  11.         if(a2 == 0);   
  12.         else if(a2 == 1)c = a1 + 1 + x + c;      
  13.         else c = a2*x + c + ten;      
  14.         a1 = 10*a1 + a2;        
  15.         N /=10;      
  16.         x = 10*x + ten;   
  17.         ten *= 10;    
  18.     }      
  19.     return c;      
  20. }  

 

 

而以上解法的时间复杂程度只有O(logN)

 

 

 

 

 

 

/////////////////////////////////////////////////

编程之美之1的数目

1 的数目

给定一个十进制正整数 N,写下从 1 开始,到 N 的所有整数,
然后数一下其中出现的所有“1”的个数。
例如:
N= 2,写下 1,2。这样只出现了 1 个“1”。
N= 12,我们会写下 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12。这样,1的个数是 5。
问题是:
写一个函数f(N) 返回1到N之间出现的“1”的个数,比如f(12)=5。

这个问题看上去并不是一个困难的问题,因为不需要太多的思考,大家都能找到一个最简单的方法来计算 f(N),那就
是从1开始遍历到N,将其中每一个数中含有“1”的个数加起来,
自然就得到了从1到N所有“1”的个数的和.C语言实现如下:

#include<stdio.h>
int Count1(int n)
{
    int iNum=0;
    while(n!=0)
    {
        iNum+=(n%10==1)?1:0;
        n/=10;
              
    }
    return iNum;
}

int Count2(int n)
{
    int iCount=0;
    for(int i=0;i<=n;i++)
    {
        iCount+=Count1(i);
    } 
    return iCount; 
}

int main()
{
     int x;
     scanf("%d",&x);
     printf("%d",Count2(x));
     return 0;
}

但是这个算法的致命问题是效率,它的时间复杂度是O(N)×计算一个整数数字里面“1”的个数的复杂度 = O(N *log2 N),如果给定的 N 比较大,则需要很长的运算时间才能得到计算结果。我试着输入100000000,一共用了大概12秒,貌似有点长,不够比作者说的40S快多了,作者的电脑有点旧…….- .-

解法二

<编程之美>先从一些简单的情况开始观察:

如果N是一位数,可以确定f(N)=1

如过是二位数,如果 N=13,那么从 1 到 13 的所有数字:1、2、3、4、5、6、
7、8、9、10、11、12、13,个位和十位的数字上都可能有 1,我们可以将它们分开来考虑,个位出现 1 的次数有两次:1 和 11,十位出现 1 的次数有 4 次:10、11、12 和 13,所以 f(N)=2+4=6。要注意的是 11 这个数字在十位和个位都出现了 1, 但是 11 恰好在个位为 1 和十位为 1 中被计算了两次,所以不用特殊处理,是对的。再考虑 N=23 的情况,它和 N=13 有点不同,十位出现 1 的次数为 10 次,从 10 到 19,个位出现 1 的次数为 1、11 和 21,所以f(N)=3+10=13。通过对两位数进行分析,我们发现,个位数出现 1 的次数不仅和个位数字有关,还和十位数有关:如果 N 的个位数大于等于 1,则个位出现 1 的次数为十位数的数字加 1;如果N 的个位数为 0,则个位出现 1 的次数等于十位数的数字。而十位数上出现 1 的次数不仅和十位数有关,还和个位数有关:如果十位数字等于 1,则十位数上出现 1 的次数为个位数的数字加 1;如
果十位数大于 1,则十位数上出现 1 的次数为 10。
f(13) = 个位出现1的个数 + 十位出现1的个数 = 2 + 4 = 6;
f(23) = 个位出现1的个数 + 十位出现1的个数 = 3 + 10 = 13;
f(33) = 个位出现1的个数 + 十位出现1的个数 = 4 + 10 = 14;

f(93) = 个位出现1的个数 + 十位出现1的个数 = 10 + 10 =
20;

接着分析 3 位数,

如果 N = 123:

个位出现 1 的个数为 13:1, 11, 21, …, 91, 101, 111, 121
 十位出现 1 的个数为 20:10~19, 110~119
 百位出现 1 的个数为 24:100~123

 f(23)= 个位出现 1 的个数 + 十位出现 1 的个数 + 百位出现 1 的次数 = 13 + 20 + 24 = 57;同理我们可以再分析 4 位数、 位数。   根据上面的一些尝试,下面我们推导出一般情况下,从 N 得
到 f(N)的计算方法: 假设 N=abcde,这里 a、b、c、d、e 分别是十进制数 N 的各个数位上的数字。如果要计算百位上出现 1 的次数,它将会受到三个因素的影响:百位上的数字,百位以下(低位)的数字,百
位(更高位)以上的数字。如果百位上的数字为 0,则可以知道,百位上可能出现 1 的次
数由更高位决定,比如 12 013,则可以知道百位出现 1 的情况可能
是 100~199,1 100~1 199,2 100~2 199,…,11 100~11 199,
一共有 1 200 个。也就是由更高位数字(12)决定,并且等于更高
位数字(12)×当前位数(100)。

如果百位上的数字为 1,则可以知道,百位上可能出现 1 的次数不仅受更高位影响,还受低位影响,也就是由更高位和低位共同决定。例如对于 12 113,受更高位影响,百位出现 1 的情况是 100~199,1 100~1 199,2 100~2 199,…,11 100~11 199,一共 1 200个,和上面第一种情况一样,等于更高位数字(12)×当前位数(100)。但是它还受低位影响,百位出现 1 的情况是 12 100~12 113,一共114 个,等于低位数字(123)+1。 如果百位上数字大于 1(即为 2~9),则百位上可能出现 1的次数也仅由更高位决定,比如 12 213,则百位出现 1 的可能性为:100~199,1 100~1 199,2 100~2 199,…,11 100~11 199,12 100~12 199,一共有 1 300 个,并且等于更高位数字+1(12+1)
×当前位数(100)。通过上面的归纳和总结,我们可以写出如下的更高效算法来
计算 f(N):

#include<stdio.h>
int Sumls(int n)
{
    int iCount=0,iFactor=1,iLowerNum=0,iCurrNum=0,iHigherNum=0;
    while(n/iFactor!=0)
    {
        iLowerNum=n-(n/iFactor)*iFactor;
        iCurrNum=(n/iFactor)%10;
        iHigherNum=n/(iFactor*10);
       
        switch(iCurrNum)
        {
            case 0:
                iCount+=iHigherNum*iFactor;
                break;
            case 1:
                iCount+=iHigherNum*iFactor+iLowerNum+1;
                break;
            default:
                iCount+=(iHigherNum+1)*iFactor;
                break;
                       
        }
        iFactor*=10;
       
    }
    return iCount; 
}

int main()
{
    int x;
    scanf("%d",&x);   
    printf("%d",Sumls(x));
    return 0;
}

我试着输入100000000瞬间就显示出结果了,编程之美说效率至少提高了40000倍…

posted on 2012-05-31 11:00  温柔的暴力  阅读(3224)  评论(0编辑  收藏  举报

导航