Google面试题详解(0~n之间1的个数,f(n)=n)

0. 问题描述

Consider a function which, for a given whole number n, returns the number of ones required when writing out all numbers between 0 and n. For example, f(13)=6. Notice that f(1)=1. What is the next largest n such that f(n)=n? 

翻译过来大体是这样: 给定一个整数n,写一个函数f(n),返回0到n之间出现的"1"的个数。比如f(13)=6,现在f(1)=1,问下一个最大的f(n)=n的n是多少?(注解:为什么f(13)=6, 因为1,2,3,4,5,6,7,8,9,10,11,12,13中“1”的个数,正好是6)


1. 解题思路

1位数:0~9
      个位数为1:1,共1次
      故0~9之间,1的个数为1

2位数:10~99
      个位数为1:11, 21, 31, …, 91,共9次
      十位数为1:10, 11, 12, …, 19,共10次
      故0~99之间,1的个数为1+9+10=20次

      也可以这样考虑:
      在0~99之间个位有10个0~9,故有10*1=10次,而十位为1的有10次,共20次
      20=10*1+10

3位数:100~999
      个位数为1:101, 111, 121, …, 191  ——  10个
      201, 211, 221, …, 291  ——  10个
      …
      901, 211, 221, …, 291  ——  10个
      共9个10,是90次
      十位数为1:110, 111, 112, …, 119  ——  10个
      210, 211, 212, …, 219  ——  10个
      …
      910, 911, 912, …, 919  ——  10个
      共9个10,是90次
      百位数为1:100, 101, 102, …, 199  ——  100个
      共100次
      故0~999之间,1的个数为20+90+90+100=300次

      也可以这样考虑:
      0~999之间:十位个位共有10个0~99,故有10*20=200次,而百位为1的有100次
      300=10*20+100

4位数:1000~9999
      个位数为1:1001, 1011, 1021, …, 1091  ——  10个
      1101, 1111, 1121, …, 1191  ——  10个
      1201, 1211, 1221, …, 1291  ——  10个
      …
      9001, 9011, 9021, …, 9091  ——  10个
      …
      9801, 9811, 9821, …, 9891  ——  10个
      9901, 9911, 9921, …, 9991  ——  10个
      共90个10,是900次

      十位数为1:1010, 1011, 1012, …, 1019  ——  10个
      1110, 1111, 1112, …, 1119  ——  10个
      1210, 1211, 1212, …, 1219  ——  10个
      …
      9010, 9011, 9012, …, 9019  ——  10个
      …
      9810, 9811, 9812, …, 9819  ——  10个
      9910, 9911, 9912, …, 9919  ——  10个
      共90个10,是900次

      百位数为1:1100, 1101, 1102, …, 1199  ——  100个
      2100, 2101, 2102, …, 2199  ——  100个
      …
      9100, 9101, 9102, …, 9199  ——  100个
      共9个100,是900次

      千位数为1:1000, 1001, 1002, …, 1999  ——  1000个
      共1000次

      故0~9999之间,1的个数为300+900*900*900+1000=4000

      也可以这样考虑:
      0~9999之间:百位十位个位共有10个0~999,故有10*300=3000次,而千位为1的有1000次
      共3000+1000=4000次

      4000=10*300+1000


2. 规律

      为方便观察,将这些数据列出,如下:

      0~9:1
      0~99:20=10*1+10
      0~999:300=10*20+100
      0~9999:4000=10*300+1000
      0~99999:50000=10*4000+10000
      0~999999:600000=10*50000+100000
      …

      从中,我们不难发现规律:
      a1=1
      a2=10*a1+10
      …
      am=10*am-1+10m-1
 

      即0~9…9(m个9)之间1的个数为am=10*am-1+10m-1个;


3. 实际计算

      找到这些规律并不难,可是对于任意给定的数n,怎样能快速得到0~n之间1的个数呢?

      例如,n=1234,计算步骤如下:
      (1) 将0~1234分为2个部分:0~999,1000~1234;
      (2) 有1个0~999,共300次;
      (3) 应该加上千位为1的次数,共234+1次,即n%1000+1=235次;
      (4) 此时,就剩下0~234了,不满1个0~999,故继续拆开:0~99,100~199,200~234;共有2个0~99,共2*20=40次;
      (5) 因百位数为2>1,故应该加上百位为1的次数,共100次;(从拆分的结果也很容易得出)
      (6) 此时,就剩下200~234了,实际上就是剩下0~34了,不满1个0~99,故继续拆开:0~9,10~19,20~29,30~34;共有3个0~9,共3*1=3次;
      (7) 因十位数为3>1,故应该加上十位为1的次数,共10次;
      (8) 此时,就剩下30~34了,实际上就是剩下0~4了,共1次(即因个位数为4>1,故应该加上个位为1的次数,共1次);

      故0~1234之间1的个数为300+235+40+100+3+10+1=689个

      如果,某位的数字为1或者0,情况又是怎样呢?

      例如,n=20140,计算步骤如下:
      (1) 将0~20140分为3个部分:0~9999,10000~19999,20000~20140;
      (2) 有2个0~9999,共2*4000=8000次;
      (3) 应该加上万位为1的次数,共10000次,
      (4) 此时,就剩下0~140了,不满1个0~999,故拆为0~99,100~140;共有1个0~99,共1*20=20次;
      (5) 因百位为1=1,故应该加上百位为1的次数,共40+1次,即n%100+1=41次;
      (6) 此时,就剩下0~40了,共有4个0~9,共4*1=4次;
      (7) 因十位为4>1,故应该加上十位为1的次数,共10次;
      (8) 此时就剩下40了,实际上就是0,共0次;

      故0~20140之间1的个数为8000+10000+20+41+4+10=18075


4. 求解算法

      n=1234,0~n之间1的个数计算方法如下:


-543210
a5000040003002010
x-1234Empty

      count = 0
      count += x[4] * a[3] + 1000, if x[4] > 1
      count += x[4] * a[3] + n%1000 + 1, if x[4] = 1

      count += x[3] * a[2] + 100, if x[3] > 1
      count += x[3] * a[2] + n%100 + 1, if x[3] = 1

      count += x[2] * a[1] + 10, if x[2] > 1
      count += x[2] * a[1] + n%10 + 1, if x[2] = 1

      count += x[1] * a[0] + 1, if x[1] > 1
      count += x[1] * a[0] + n%1 + 1, if x[1] = 1

      即count += x[4] * a[3] + (x[4]>1) ? 1000 : ((x[4] =1) ? (n%1000 + 1) : 0)
      count += x[3] * a[2] + (x[3]>1) ? 100 : ((x[3] =1) ? (n%100 + 1) : 0)
      count += x[2] * a[1] + (x[2]>1) ? 10 : ((x[2] =1) ? (n%10 + 1) : 0)
      count += x[1] * a[0] + (x[1]>1) ? 1 : ((x[1] =1) ? (n%1 + 1) : 0)


5. 剪枝思路

      对一个数 m = abc*10^n 已求出 m-1 的1的个数为 ncount
      则计算一下 abc*10^n + (10^n - 1) 的1的个数 maxcount
      如果 maxcount 比 m 小或者 ncount 比 abc*10^n + (10^n - 1) 大,则这些数肯定不满足要求,直接跳到 abc*10^n + 10^n。如果不满足跳过的条件,则做一个0-9的循环递归。

      另外,在main的while循环中,对 m 使用了贪婪的递增算法,即m += 10^n,从而更高效的剪枝(一次尽可能剪掉多的)。


6. 算法实现

#include <sys/time.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>


uint32_t XTickCount(void) 
{ 
    uint32_t currentTime; 
#ifdef _WIN32
    #include <windows.h>
    currentTime = GetTickCount(); 
#elif __linux__
    struct timeval current; 
    gettimeofday(¤t, NULL); 
    currentTime = current.tv_sec * 1000 + current.tv_usec/1000; 
#elif __APPLE__
    struct timeval current; 
    gettimeofday(¤t, NULL); 
    currentTime = current.tv_sec * 1000 + current.tv_usec/1000; 
#endif 

    return currentTime; 
}


unsigned int f(unsigned int n);
int count1(unsigned int n);
unsigned int cal(unsigned int number,int nwei,int count1,unsigned int ncount);


int gTable[10];
const unsigned int gMAX = 4000000000L;


int main(int argc, char* argv[])
{
    int i;
    unsigned int n;
    unsigned int ncount = 0;
    int nwei = 0;
    int ncount1 = 0;


    unsigned int beginTime=XTickCount();

    //init gTable
    n=1;
    for(i=0;i<10;++i)
    {
        n *= 10;
        gTable[i] = f(n-1);
    }

    n=0;
    while(n<=gMAX)
    {
        ncount = cal(n,nwei,ncount1,ncount);

        unsigned int temp = 1;
        for(i=0;i<nwei;++i)
            temp *= 10;
        n += temp;

        if((n/temp)/10 == 1)
            ++nwei;

        ncount1 = count1(n);
        
        /* Too slow!!!
        if (f(n) == n)
            printf("f(%d) = %d\n",n,n);

        n++;
        */
    }

    unsigned int endTime=XTickCount();
    endTime-=beginTime;
    printf("time: %u ms\n",endTime);

    return 0;
}


unsigned int f(unsigned int n)
{  
    int count=0;  
  
    //0     :0   
    //0~9   :1    = 10 * 0   + 1   
    //0~99  :20   = 10 * 1   + 10   
    //0~999 :300  = 10 * 20  + 100;   
    //0~9999:4000 = 10 * 300 + 1000   
    int a=0;      //0,1,20,300,4000,...   
    int weight=1; //1,10,100,1000,...   
      
    int x;        //数的当前末位   
  
    unsigned int temp=n;  
    while(temp)  
    {  
        x=temp%10;  
  
        count+=x*a;  
  
        //加上当前位为1的个数   
        if(x>1)         //若该位>1, 则当前位为1的个数为10^(m-1), m为位数 
            count+=weight;  
        else if(x==1)   //若该位=1, 则当前位为1的个数为该位右边的数字组成的数+1   
            count+=n%weight+1;  
          
        a=10*a+weight;  //an=10 * a(n-1) + 10^(m-1), m为位数   
        weight*=10;  
  
        temp/=10;  
    }  

    return count;  
}


int count1(unsigned int n)
{
    int count = 0;

    while(n)
    {
        if((n%10) == 1)
            ++count;

        n /= 10;
    }


    return count;
}


unsigned int cal(unsigned int number,int nwei,int count1,unsigned int ncount)
{
    if(nwei==0)
    {
        ncount += count1;
        if(number == ncount)
            printf("f(%d) = %d\n",number,number);

        return ncount;
    }

    int i, n=1;
    for(i=0;i<nwei;++i)
        n *= 10;

    unsigned int maxcount = ncount + gTable[nwei-1] + count1*n;

    char s[256];
    if(ncount > (number + (n-1)))
    {   
        sprintf(s, "%u > (%u + (%d-1)), input(%u, %d, %d, %u)\n", ncount, number, n, number, nwei, count1, ncount);
        perror(s);
        return maxcount;
    }

    if(maxcount < number)
    {
        sprintf(s, "%u < %u, input(%u, %d, %d, %u)\n", maxcount, number, number, nwei, count1, ncount);
        perror(s);
        return maxcount;
    }

    n /= 10;

    for(i=0;i<10;++i)
    {
        if(i==1)
            ncount = cal(number+i*n,nwei-1,count1+1,ncount);
        else
            ncount = cal(number+i*n,nwei-1,count1,ncount);
    }

    return ncount;
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EnjoyCodingAndGame

愿我的知识,成为您的财富!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值