十进制整数区间[1, N]中数字1出现的个数

        毫无疑问,数字历来都是高深的科学家和日常小市民谈论的对象,著名的哥德巴赫猜想便是举世瞩目的数字难题,而人们在超市购物总喜欢计较价钱中的一角半块。《编程之美》中一节有如下两个数字问题:

        问题一:定义函数f(N)表示十进制整数区间[1,N]中数字1出现的个数,利用程序实现函数f(N)。

        问题二:是否存在满足f(N)=N的最大整数Nmax?进行简单分析。

        问题需要一一解决,不能操之过急,往往脑海中第一个闪现的方法效率并不高,三思之后你会发现换条路走其实更快。世界的本质或许并不复杂,平时遇到的问题大多比较简单;而世界或许又不如此单纯得简单,以至于人们对不解之谜进行孜孜以求的探索。

        先来解决问题一。f(N)到底是什么呢?给出简单的特例:当N=2时,f(N)是整数序列{1,2中数字1出现的个数,即f(N)=f(2)=1;当N=12时,f(N)是整数序列{1,2,…,11,12}中数字1出现的个数,即f(N)=f(12)=5。没错,仅仅是计算1的个数,无论1在个位还是在十位,或是百位。要解决的问题很简单吧!下面我们选用C/C++语言解决这个问题。

#include <iostream>
using namespace std;

unsigned int Count1InAInteger(unsigned int n);
unsigned int f(unsigned int n);

int main(int argc, char* argv[])
{
	unsigned int n = 0;
	cin>>n;

	// Quit if input 0
	while (n > 0)
	{
		cout<<"f(n) = f("<<n<<") = "<<f(n)<<endl;
		cin>>n;
	}

	return 0;
}

unsigned int Count1InAInteger(unsigned int n)
{
	unsigned int iNum = 0;
	while(n != 0)
	{
		iNum += (n % 10 == 1) ? 1 : 0;
		n /= 10;
	}
	
	return iNum;
}

unsigned int f(unsigned int n)
{
	unsigned int iCount = 0;
	for (unsigned int i = 1; i <= n; i++)
	{
		iCount += Count1InAInteger(i);
	}
	
	return iCount;
}

        问题当然是解决了,然而总觉得这种穷举法是最愚笨的方法,为下下之策,其时间复杂度为O(Nlog10N)=O(Nlog2N)。

        有没有更好的办法呢?经过一阵思考后,在脑海中我将从1到N的整数按序形成一个N×1且元素右对齐的矩阵,然后分析个位、十位、百位等位上数字1出现的规律。结果出来了:个位上每10个数有一个1;十位上,每100个数有10个1;百位上,每1000个数有100个1……没错,就是这样优美!生活中很多事情虽然简单,但简单中却总存在着优美的规律,能够发现这些美的人便会享受生活。同样,真正的数学家总是能够发现并享受到数学之美,他们的心与数学融为一体,而这样的人很少,所以真正的数学家很少。当然,我们周围总存在一些数学专业的毕业生自称可以感受到数学之美,若果真如此,中国的数学大师应该令世界羡慕不已,可惜的是当前中国的高校尚未培养出真正的大师,因此谦逊的我们并不应妄自菲薄。闲话少说,接着上面的规律讨论。既然有如此周期性的规律,那么对于给定的N,只要得到有多少个位、十位、百位等便可以利用周期性数值得到答案。当然,实际程序设计中因该考虑更具体的特例,例如,N=12时,个位有两个1,十位仅有一个1;当N=22时,个位有三个1,十位确有十个1。

        问题一详细的分析可参见《编程之美》相关内容。对于L位整数N,按其第k位数值X将N分解为三部分:X的低位XL=Nmod(10k-1)、第k位数值X=rounddown(N/10k-1) mod(10)、X的高位XH=rounddown(N/10k),其中mod()为取余操作,rounddown()为向下取整操作。如此,分类讨论如下:

        当X=0时,f(N)=XH*10k-1

        当X=1时,f(N)=XH*10k-1+XL+1;

        当2≤X≤9时,f(N)=(XH+1)10k-1

        如下C/C++代码按照上述规律可以更加有效地解决问题一,时间复杂度为O(log 10N)=O(log 2N),较之简单的穷举法大大地降低。

#include <iostream>
using namespace std;

unsigned int Sum1s(unsigned int n);

int main(int argc, char* argv[])
{
	unsigned int n = 0;
	cin>>n;

	// Quit if input 0
	while (n > 0)
	{
		cout<<"f(n) = f("<<n<<") = "<<Sum1s(n)<<endl;
		cin>>n;
	}

	return 0;
}

unsigned int Sum1s(unsigned int n)
{
	unsigned int iCount = 0;
	unsigned int iFactor = 1;
	
	unsigned int iLowerNum = 0;
	unsigned int iCurrNum = 0;
	unsigned int 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;
}

        下面来分析讨论问题二的解决方法。对于问题二,我们需要回答是否存在满足f(N)=N的最大整数Nmax。首先要理解的是f(N)=N,如f(1)=1。因此,存在满足f(N)=N的整数N,且这样的整数最小为1。接下来,按照特例进行分析 的变化规律:

f(9)=1

f(99)=20

f(999)=300

……

f(999999999)=900000000

f(9999999999)=10000000000

        由此得到归纳公式:f(10N-1)=N*10N-1。特例分析中当N=9999999999,f(N)>N。归纳公式可猜想:当N大于某一整数ε时,始终有f(N)>N。事实上确实如此,具体证明可参见《编程之美》,在此不再赘述。至此问题二得到了最终答案,即[1,ε]中存在满足f(N)=N的最大整数Nmax,ε是满足f(N)=N的整数N集合的上界,而N=1011-1=9999999999便是这样一个上界。用程序计算满足f(N)=N的最大整数Nmax十分简单,只需要对上述计算f(N)的程序进行修改,使的N从N=1011-1=9999999999递减并检查条件f(N)=N即可。

        至此,两个问题得到全部解决。我们发现,对于给定的问题,只要针对特定要求进行仔细分析,便可抽丝剥茧地一步步解决问题。往往第一答案并不是最佳的,而大多数时候有必要进行认真分析以寻求更优的加法以减少时间复杂度,而不能简单地依靠高性能计算机。数学家可以发现数学之美,我们在解决问题的过程中也可以发现待解决问题中的规律之美,二者似乎冥冥中总有些许联系,至于此等联系恐怕便只能意会而不便言传了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值