1的数目

来源:互联网 发布:手机自动开启数据连接 编辑:程序博客网 时间:2024/04/29 16:22

题目出自:《程序员面试宝典》(第三版) P228页  面试例题5

        这道题目已经加了一个小小的限制条件,因此变得简单许多,少去了大量的理论推导证明。《面试宝典》只贴出了一段代码,没有任何分析和注释,书上代码完全是来自CSDN的oo大神的源代码,链接如右:http://bbs.csdn.net/topics/70522458。下面内容包括2个方面:1是主要是解释《面试宝典》上的繁琐的代码含义。2是介绍另外一个大神求解f(n)=n的惊人且高效简单的剪枝方法。

     原题在《编程之美》P132页:

        给定一个十进制正整数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。

       问题是:

       1.写一个函数f(N),返回1到N之间出现的“1”的个数,比如f(12)=5;

       2.满足条件“f(N)=N”的最大的N是多少?

       

       对于问题1:

       1) 最简单的方法就是先写个函数【int count1(int n)】能够统计一个数字n中1的个数,然后从1到N依次调用这个函数,并把结果累加起来就得到f(N)。《编程之美》上提供了这种简单方法的代码。毫无疑问这是非常费时的。《面试宝典》中的函数:int count1(int n)的功能就是统计数字n中的1的个数。这段代码一目了然。

//返回数字n中1的个数int count1(int n){int count=0;while(n){if(n%10==1)++count;n/=10;}return count;}
        还有同志想到把数字[1~N]转换成字符串[1~N],然后把字符串顺序连接起来后,遍历整个字符串统计1的个数。目测效率也会不高,因为字符串转换成数字以及字符串的连接都会比较耗时。

      2) 高效的方法就是分别统计每个位上1的个数,例如一个三位数的1的个数就是把个位,十位,百位上1的个数加起来。当然,每个位上1的个数分为3种情况讨论,分别是=0,=1,>1。

       以一个三位数abc为例子。同时分析的时候用数字模型XYZ。说明一下,形如[m ~ n]这种符号式的意思是表示的是a到b之间的整数,包括a和b。而形如[m,n]这种符号式等同于数学上的解释。

       个位为1的情况即Z=1,此时算出XY的取值情况数,就是从1到abc数字序列中所有个位上为1的数字的数量。

       十位为1的情况即Y=1,此时算出XZ的取值情况数,就是从1到abc数字序列中所有十位上为1的数字的数量。

       百位为1的情况即X=1,此时算出YZ的取值情况数,就是从1到abc数字序列中所有百位上为1的数字的数量。

       最后,把上面的三种情况数量加起来,就是从1到abc数字序列中1的数量。

       (1) 首先统计个位上的1的个数。即XYZ中Z=1,当X=a时,Y属于[0,b]。当X属于[0,a-1]时,Y属于[0,9]。XY可能情况数如下分析:

           1.个位上的数字等于0,即abc中c=0,相当于ab0。接下来要讨论XY1属于[0,ab0]中X,Y的情况数。        

                X=a,Y=[0 ~ b-1]:情况数为b。

                X=[0 ~ a-1],Y=[0 ~ 9]:情况数为a*10。

                所以,总的情况数为a*10+b=ab。即当c=0时,在[0 ~ abc]数字序列中形如XY1(个位上为1)的数有ab个。

           2.个位上的数字等于1,即abc中c=1,相当于ab1。接下来要讨论XY1属于[0,ab1]中X,Y的情况数。        

                X=a,Y=[0 ~ b]:情况数为b+1。

                X=[0 ~ a-1],Y=[0 ~ 9]:情况数为a*10。

                所以,总的情况数为a*10+b+1=ab+1。即当c=1时,在[0 ~ abc]数字序列中形如XY1(个位上为1)的数有ab+1个。

           3.个位上的数字大于1,即abc中c>1。接下来要讨论XY1属于[0,abc]中X,Y的情况数。        

                X=a,Y=[0 ~ b]:情况数为b+1。

                X=[0 ~ a-1],Y=[0 ~ 9]:情况数为a*10。

                所以,总的情况数为a*10+b+1=ab+1。即当c>1时,在[0 ~ abc]数字序列中形如XY1(个位上为1)的数有ab+1个。

         我们可以发现,在统计那些个位上为1的数字个数时,发现c=1和c>1的情况数相等。但十位上的1的个数则不相等了。


       (2) 其次统计十位上的1的个数。即XYZ中Y=1,当X=a时,Z属于[0,c]。当X属于[0,a-1]时,Z属于[0,9]。XZ可能情况数如下分析:

           1.十位上的数字等于0,即abc中b=0,相当于a0c。接下来要讨论X1Z属于[0,a0c]中X,Z的情况数。        

                X=a,Z取不了任何值。因为此时X1Z=a1Z>a0c。不在数字1~a0c这个数字序列范围内。suo

                X=[0 ~ a-1],Z=[0 ~ 9]:情况数为a*10。

                所以,总的情况数为a*10。即当b=0时,在[0 ~ abc]数字序列中形如X1Z(十位上为1)的数有a*10个。

           2.十位上的数字等于1,即abc中b=1,相当于a1c。接下来要讨论X1Z属于[0,a1c]中X,Z的情况数。        

                X=a,Z=[0 ~ c]:情况数为c+1。

                X=[0 ~ a-1],Z=[0 ~ 9]:情况数为a*10。

                所以,总的情况数为a*10+c+1。即当b=1时,在[0 ~ abc]数字序列中形如X1Z(十位上为1)的数有a*10+c+1个。

           3.十位上的数字大于1,即abc中b>1。接下来要讨论X1Z属于[0,abc]中X,Z的情况数。        

                X=[0 ~ a],Z=[0 ~ 9]:情况数为(a+1)*10。

                所以,总的情况数为(a+1)*10。即当b>1时,在[0 ~ abc]数字序列中形如X1Z(个位上为1)的数有(a+1)*10个。     

       (3) 最后统计百位上的1的个数。即XYZ中X=1,当Y=b时,Z属于[0,c]。当Y属于[0,b-1]时,Z属于[0,9]。BZ情况分析如前,这里省略。


      3) 由前面三位数abc的模型我们可以推广扩展到任意数,设N=ak-1ak-2..a1a0,为k位数。

       现在求从1到N数字序列中第i位上的数ai等于1的情况数,0<=i<=k-1。我们以ai左右为分界点,把ak-1ak-2..a1a0砍下2块,记head=ak-1ak-2..ai+1,记tail=ai-1..a1a0

       (1) ai=0;则情况数为:head*10^i    

       (2) ai=1;则情况数为:head*10^i+tail+1    

       (3) ai>1;则情况数为:(head+1)*10^i

      编程之美上阐述的很详细,也提供源码,这里不再赘述。关键是《面试宝典》中的函数:int f(int n)没有任何注释,比较难以看明白。其实,int f(int n)函数的功能也是返回1到n之间出现的“1”的个数,而且思想和编程之美的思想相一致。只不过oo大神没有分3种情况讨论,它是把这3种情况合并起来了。

      首先把3)的情况(1)归并到3)的情况(3),这通过一个映射((ntemp-1)/10+1)*i; 来实现。再把3)的情况(2)归并到3)的情况(3),当ai=1,在情况(3)的结果上减去一个10^i再加上一个tail+1  也就是3)的情况(2)的结果。下面是书中源码,加了注释版

       

//从1到n数字序列中1的个数//假设存在N位数,An-1..A0,求Ak位上1的个数,例如A1是十位上的数//若第K位上的数字小于1,An-1..Ak+1 0 Ak-1..A0 ,则Ak位上1的个数为 An-1..Ak+1 * 10^k.//若第K位上的数字大于1,An-1..Ak+1 3 Ak-1..A0 ,则Ak位上1的个数为 (An-1..Ak+1 + 1) * 10^k.//若第K位上的数字等于1,An-1..Ak+1 1 Ak-1..A0 ,则Ak位上1的个数为 An-1..Ak+1 * 10^k + Ak-1..A0 +1int f(int n){int ret=0; //相当于存储(head+1)*10^iint ntemp=n; //相当于存储headint ntemp2=1; //相当于存储tail+1int i=1;    //相当于存储10^i while(ntemp){ret+= ((ntemp-1)/10+1)*i; if((ntemp%10)==1){ret-=i;ret+=ntemp2;}ntemp=ntemp/10;i*=10;ntemp2=n%i+1;}return ret;}


       对于问题2:满足条件“f(N)=N”的最大的N是多少?

       1) 《面试宝典》上的版本题目之所以变得简单,是因为题目中给了一个上限,即统计4 000 000 000以内的最大的那个满足f(n)=n的n值。如果不提供这个上限,那么,

       首先需要证明满足条件f(N)=N的数存在一个上界;  其次需要自己估算这个上界是多少。

       这两步《编程之美》中说的很清楚。书上推导的上界值为N=10^11-1=99 999 999 999,让n从N往0递减,依次检查是否有f(n)=n,第一个满足条件的数就是我们的答案,即n=1 111 111 110是满足f(n)=n的最大整数。即使《面试宝典》提供的上界值为4 000 000 000,缩小了很多,但如果让n从4 000 000 000开始往0递减,依次检查是否有f(n)=n,必定会很费时。因此对于问题2,《编程之美》上并没有给出好的解决办法。

       2) 而《面试宝典》上的方法非常难懂,oo大神在其讨论帖子中简单地说了一下他的思路,如下:http://bbs.csdn.net/topics/70522458

         “对一个数 m = abc*10^n 已求出m-1的1的个数为count,则计算一下abc*10^n +(10^n - 1) 的1的个数 maxcount

         如果maxcount比m小或者count比 abc*10^n +(10^n - 1) 大,则这些数肯定不满足要求,直接跳到abc*10^n +10^n 。

         如果不满足跳过的条件,则做一个0-9的循环递归。”

       三句话可谓是言简意赅,但省略了很多重点。我花了几个小时才看明白他的代码所表达的意图。我再把他的意图阐明一下,如果路人看不明白,那建议直接跳到下一块更惊人的简单高效的解法。因为他这个方法相比下面要介绍的方法,显得臃肿,而且递归效率不高,最重要的是剪枝的策略相比之下显得不那么高明(即筛选哪些范围的n一看就知道不符合f(n)=n)。

         假设m为一个N+1位数,m=abc*10^n,f(m-1)=ncount, maxcount=f(abc*10^n+10^n-1)=f(m-1+10^n),步长为10^n,每次求步增之后的maxcount.

             1.if(ncount>(number+(n-1))) return maxcount,即f(m-1)>  {m+(10^nwei-1)=abc*10^n+10^n-1=m-1+10^n}
                即如果f(m-1)>m-1+10^n;那么必有f(m)>m-1+10^n,f(m+1)>m-1+10^n...f(m-1+10^n)>m-1+10^n,当然也有f(m+10^n)>m-1+10^n,但这一步没有意义,
                题目是要检测f(n)=n,现在判断了f(n+1)>n也于事无补,需要判断的是f(n+1)=n+1??
                所以返回maxcount=f(m-1+10^n),下一个测试点便是f(m-1+10^n + 1)=f(m+10^n),也就是退出cal,重新调用cal(m+10^n,,,maxcount)
             2.if(maxcount<m) return maxcount.即如果步增之后的f值依然小于m的话,那么不必一一测试步长范围的内每一个x,因为必有f(x)<x.
             再者,f(n)是统计1到n数字序列中1的个数,那么随着n的增加,它必定是一个非递减函数。

             如果if(maxcount<m),那么有f(m)<m;f(m+1)<m;...f(m-1+10^n)=maxcount<m.

             因此跨过这个步长,返回maxcount=f(m-1+10^n).下一个测试点便是f(m-1+10^n + 1)=f(m+10^n),也就是退出cal,重新调用cal(m+10^n,,,maxcount)
            3.如果满足上面的要求,即剪枝没成功的话,则不能跨过步长,做一个递归依次检测。

//number为当前检测的数字//nwei为数字的位数,留意10的话为1,100的话为2//count1为步长基数,用于求取maxcount//ncount为f(number-1)//返回f(number)的值int cal(unsigned int number,int nwei,int count1,unsigned int ncount){int i,n=1;unsigned int maxcount;if(nwei==0){ncount+=count1;if(number==ncount){printf("f(%d)=%d\n",number,number);}return ncount;}for(i=0;i<nwei;++i)n*=10;maxcount=ncount+gTable[nwei-1];maxcount+=count1*n;if(ncount>(number+(n-1))){return maxcount;}if(maxcount<number){return maxcount;}n/=10;for(i=0;i<10;++i){if(i==1) //等于1的情况ncount=cal(number+i*n,nwei-1,count1+1,ncount);else //不等于1的情况ncount=cal(number+i*n,nwei-1,count1,ncount);}return ncount;}

       3) 最后介绍另外一位大神Kayven写的简单高效的方法,代码如下:来自原帖:http://blog.csdn.net/hilyoo/article/details/4466680

    /***************************************************************     ** Contact: Kayven <hilyhoo@gmail.com>     ***************************************************************/      #include <stdio.h>      unsigned long f(unsigned long n){          unsigned long fn = 0, ntemp = n;          unsigned long step;          for(step = 1; ntemp > 0; step *= 10, ntemp /= 10){              fn += (((ntemp -1 ) /10) + 1) * step;              if(( ntemp % 10 ) ==1){                  fn -= step - (n % step + 1);              }          }          return fn;      }      unsigned long get_max_fn_equal_n(unsigned long upper_bound){          unsigned long n = 1, fn = 0;          unsigned long max = 1;          while(n <= upper_bound ) {              fn = f(n);              if(fn == n){                  max = n;                  printf("%10lu\t" , n++);              }              else if( fn < n )                  n += (n-fn)/10 + 1;              else                   n = fn;           }          return max;      }      int main()      {          unsigned long upper_bound = 4000000000UL;          printf("[::test] f(%lu) = %lu.\n", 13, f(13));          printf("\n[::max] max({f(n)=n, n<=%lu}) = %lu.\n", upper_bound, get_max_fn_equal_n(upper_bound));          return 0;      }  
   
        他的剪枝方法优雅简单,高效快捷!就是下面一个if语句判断。
            if(fn == n){                  max = n;                  printf("%10lu\t" , n++);              }              else if( fn < n )                  n += (n-fn)/10 + 1;              else                   n = fn;
         但大神不屑于详细解释为神马这样剪枝,我来代替他阐明一下。主要分两点:

         1. 当fn>n的时候,为神马选取 n=fn。 

         当f(n)>n的时候,令c=f(n)-n>0. 。设b属于[0,c),即0<=b<c。因为f(n)是一个非递减函数,当n2>n1时,必有f(n2)>=f(n1)。那么有f(n+b)>=f(n)。

         又因为 b<c且c=f(n)-n,所以b<f(n)-n,所以有f(n)>n+b。所以最后有f(n+b)>=f(n)>n+b。

          也就是说只要b属于[0,c),当n递增b的时候,必定有f(n+b)>n+b。因此这些值都可以被剪枝忽略掉。我们取b的上确界值c来说。则有f(n+c)>=f(n)>=n+c。这时才可能出现f(n+c)=n+c的情况。这里是可能,而不是一定。

         所以当fn>n的时候,选取递增步长c=f(n)-n,令n=n+c=n+fn-n=fn。

         2.当fn<n的时候,为神马选取n += (n-fn)/10 + 1。

         首先,题目给的上限是4 000 000 000,这是一个10位数。可以得出结论,当n增加1的时候,f(n)最多增加10。这是一种极端情况,即新增加的那个数是1 111 111 111,所以多了10个1,那么f(n)最多增加10。

         目前,我要选取某个步长b,当n+b时,依然有f(n+b)<n+b,但是依然迅速逼近f(N)=N。

         假设现在f(n1)<n1,那么想要达到f(n1)=n1的情况,f(n1)至少得增加n1-f(n1)。

         而此时,在之前推出的结论基础上(当n增加1的时候,f(n)最多增加10),可以得出n1最少增加了(n1-f(n1))/10+1。令n1增加后结果记为,n2=n1+(n1-f(n1))/10+1。

         因此有n2>n1,所以有f(n2)>=f(n1),而f(n2)=f{ n1 + (n1-f(n1))/10+1 }<f(n1)+n1-f(n1)=n1。所以有n2>n1>f(n2)。

         因此,当fn<n的时候,取步长为 (n-fn)/10 + 1,这样的话可以迅速逼近f(N)=N。

        这是我目前为止,看到的最高效简单的方法,能写出这样简洁优雅的代码,是基于藏在题目下的大量逻辑推理过程,用人脑的智慧代替的大量的电脑计算。 

         如果还有大神有更好的方法,希望能共享之。

 

        

 


     

原创粉丝点击