关于f(n)=n的几种算法

来源:互联网 发布:apache日志分析工具 编辑:程序博客网 时间:2024/06/06 04:05

题目:
有一个整数n,写一个函数f(n),返回0到n之间出现的"1"的个数。比如f(13)=6,现在f(1)=1,问下一个最大的f(n)=n的n是什么?

/*p228面试题5
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? 

e.g. 
f(13)=6,  
because the number of "1" in 1,2,3,4,5,6,7,8,9,10,11,12,13 is 6. (1, 11, 12, 13) 

tks  
现在要我们写一个函数,计算4,000,000,000以内的 最大的那个f(n)=n的值,要求程序能在100ms里面运行完毕

 

算法1。大家自然想到一个最简单的算法如下:

#include "stdafx.h"
#include "stdio.h"
#include <windows.h>

//统计一个数字中1的个数
int NumberofOne(int num)
{
int result = 0;
while(num>0)
{
int r = num;
if(r == 1) result++;
num /= 10;
}
return result;
}

int main()
{
int sum = 0;
for(int i=0;i<4000000000;i++)
{
sum += NumberofOne(i);
if(sum == i)
{
printf("f(%d) = %d\n",i,sum);
}
}
return 0;
}

算法二(这个偶看得晕晕乎乎的):基于以下考虑:

1、两个函数i和f(i)都是单增函数,故有:

1、计算n位最大数的f值,例如f(9) = 1;

所有小于和等于两位的数含1的总和f(99) = 100*2/10;

类似的,f(999) = 1000*3/10=300
...

这样子的话,比如f(40001),从0-9999很显然有4000个1,从10000-19999有4000+10000个1,从20000-29999还是4000个1,...
所以f(40001)=4*4000+10000+1
称以上优化为后段优化

2、计算i和f(i)之间的差值。
假设:diff = i - f(i);
(1)如果diff>0,说明f(i) < i。
考虑f(i)的增长速度:显然此时f(i)的增加速度<=i的位数。例如i是二位数,f(20) = 11; 差值diff = 20-11 = 9;要让下一个f(i)有可能追上i,想一下,i每增加一,即多一个数字,这个数字含1的个数<=它的位数,现在要让下一个i满足f(i)增加11,这里i是两位数,所以一个数字至多多两个1,所以要让至少i要增加9/2=4
至少i要增加9/2=4次,f(i)才有可能追上i的增长。因此可以直接计算f(20+4);


(2)如果diff<0, 说明f(i) > i。例如f(1622581) = 1642401;差值diff = 1622581 - 1642401 = -19820;假如下一个i,使得f(i)增加了x,那么i要增加19820+x才能达到f(i)=i,由于x>=0,所以i至少要增加19820,所以可以直接考虑计算f(1622581 + 19820),也就是计算f(1642401)即可。
称以上优化为前段优化

综合以上两点,写出如下算法:
#include "stdafx.h"
#include "stdio.h"
#include <windows.h>

//计算从1到num的1的个数
UINT NumberOneBelow(UINT num)
{
UINT mod = 1;
UINT add = 0;
UINT result = 0;
while(num>0)
{
UINT chu = num/10;
UINT yu = num%10;
result += chu * mod;
if(yu > 1) result += mod;
if(yu ==1) result += (add+1);
add += yu * mod;
mod *= 10;
num /= 10;
}
return result;
}

//计算n位数之内1的总数统计值,这里的num只考虑它的位数
UINT Total(UINT num)
{
UINT key = num;
UINT add = 1;
while(key>9)
{
add*=10;
key/=10;
}

if(num<10)return 1;
else return 10*Total(num/10)+add;
}
//计算num相关的最大值和位数。例如num是二位数,则max = 99, bit = 2
void MaxAndBit(UINT num, UINT &max, UINT &bit)
{
max = 9;
bit = 1;
while(num>9)
{
num/=10;
max*=10; max+=9;
bit++;
}
return ;
}

int main()
{
UINT max, bit;

UINT TotalOnes = Total(1);
MaxAndBit(1,max,bit);

int beginTime=GetTickCount();

for(UINT i=1;i<4000000000;i++)
{
//如果i超过n位之内所有1的个数,则跳过后面的计算
if(i>TotalOnes)
{
i = max;
TotalOnes = Total(i+1);
MaxAndBit(i+1,max,bit);

//超过最大整数表示范围,用固定数替换
if(TotalOnes>1000000000)
{
TotalOnes = 4000000000; //实际上,应该是100亿
}
continue;
}
else
{
UINT one = NumberOneBelow(i);
if(one == i)
{
printf("f(%d)=%d\n",i,one);
}
else
{
//计算差值
int diff = i-one;
if(diff > 0)
{
i+= diff/bit;
}
else
{
i = one-1;//下个循环i自动加1后,自动从one开始计算
}
}
}
}

int endTime=GetTickCount();
endTime-=beginTime;

printf("time: %d ms\n",endTime);
return 0;
}


计算到最后一个结果f(1111111110) = 1111111110; 所用时间在16 - 47毫秒之间。

 注:算法三只是一个求0~N的1的个数的快速算法,不是上面的题目的要求。

算法三:

如果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倍…