各位数之和

给定一个十进制正整数 ,从1开始到的所有整数,计算每个数各个位的数字总和。

例如:
N=4,则1+2+3+4=10。
N=14,则1+2+3+4+5+6+7+8+9+(1+0)+(1+1)+(1+2)+(1+3)+(1+4)=60。

int sum_digital(int N)

{

    int sum = 0;

    for (int i = 1; i <= N; i ++) {

       for (int j = i; j; j /= 10)

           sum += j%10;

    }

    return sum;

}

代码清单1

这样暴力的方法让人提不起任何兴趣,我们来点有挑战性的,假设N和sum都不会超过int整型数值范围,我们提出两个扩展问题:

  1. 设计一个时间复杂度更低,更有效的算法来解决这个问题。
  2. 假如给定sum的值,能否计算出N的大小?

问题一

解法一

代码清单1的方法很简单,一个编程的初学者估计都能写出那样的代码来,我们来分析下它的时间复杂度,N次的循环再乘上每个数的位数为Ο(N*logN ),这样的计算时间是跟N的大小呈线性增长的。

是否一定需要从1到N循环一遍后再计算呢?如果我们要得到更高效的算法,肯定要抛弃这样遍历1到N的方法。

考虑直接统计每个数各个位上的数字出现的次数。假设N=123,设Xi为N的第i位的数字,我们分析下十位上的数字Xi的各种情况。

1.如果X1<2时的情况:

当X1=0时,由于0对总和没有任何贡献,所以可以不考虑;当X1=1时,{0~1} 1 {0~9},左边可选集大小为2,右边可选集大小为10(因为X1=1,所以右边任意数字,组合后都不会超过123),那十位数上为1时,组合总数为2*10=20,总和为1*20=20。这种情况的公式为

2.如果X1=2时的情况:

{0} 2 {0~9},如果左边的可选集都小于1,则右边可选集大小一定为10;{1} 2 {0~3},如果左边的数字为1,那右边的可选集只能是0到3的数字,所以十位数上为2时,组合总数为1*10+1*4=14,总和为2*14=28。这种情况的公式为

3.如果X1>2时的情况:

当时X1=3,{0} 3 {0~9},左边可选集大小只有1,右边可选集大小为10。

 

对于每个位上的0~9都计算一次它的组合总数,即左边组合数乘上右边组合数,最低位和最高位时不用特殊处理,反正左或右的组合数总有一个是为0的。我们把它归纳成公式,则


这样得到的算法也很简单。

int sum_digital_3(int N)

{

    int sum = 0;

    int tmp = N;

    for (int i = 0; tmp; tmp /=10, i ++) {

       int X = tmp%10;

       int left = tmp / 10;

       int right = N % pow(10, i);

       for (int j = 1; j <= 9; j ++)

           sum += j * left * pow(10, i);

       for (int j = 1; j < X; j ++)

           sum += j * pow(10, i);

       sum += X * (right + 1);

    }

    return sum;

}

代码清单3

这个算法只是简单的组合数学知识,比起解法一的时间复杂度也只不过多了个常系数而已。从对每个位每数字出发去考虑组合总数,这样只考虑包含自己的组合即可,不需要担心是否有数字被忽略或重复计算。

 

问题一

解法二

我们拿N=14的例子来分析下,1+2+3+4+5+6+7+8+9+(1)+(1+1)+(1+2)+(1+3)+(1+4),我们可以重新排列下,(1+2+3+4+5+6+7+8+9)+(1+1+1+1+1)+(1+2+3+4),啊哈!是否觉得有点灵感了,我们可以用乘法去重新排列,这次我们拿N=20做例子,2*(1+2+3+4+5+6+7+8+9)+10*1+(2)。这个乘法的公式跟代码1的区别就出来了,在代码1中全部都是加法。

 

图1

简单总结下发现,计算两位数时,以个位数总和作为一个单位,假设有个两位数整数为X*10,则有公式。再来算算三位数X*100的情况,我们先把抽象为更广义的一个函数F(x),它表示1~(10x-1)各位数的总和,则对公式就是,归纳一下,我们针对X*10n (0≤X≤9)此类的数据就有了通项公式


。再来看看函数F(x),明显得,我们能很轻松的得出它的递推公式


,聪明的你一定看出了,其实F(x)就是X*10n通项公式当X=10的时候的特例。呼,世界大同了~

一切似乎都显得很顺利,但是我们的问题是当时的算法。相信你应该已经有想法了。

参考的通项公式,直接代入进去有问题吗?回答是当然有问题,问题在于还有一些数字没有被计算到。

图2

图2是N=121的图例,我们用X*10n的通项公式来分析。浅色区00~99和0~9由Xn*F(n)计算得到,深色区由计算所得,但剩下的无色区并不只有Xn这么简单,比如十位的2,它可以是20,21,所以这里应该把Xn改成Xn*(N%10n+1)。代码清单2就是这个算法。

int sum_digital_2(int N)

{

    int F[11] = {0};

    for (int i = 1; i < 10; i ++)

       F[i] = 10 * F[i-1] + 45 * pow(10, i-1);

   

    int sum = 0;

    int tmp = N;

    for (int i = 0; tmp; tmp /= 10, i ++) {

       int X = tmp%10;

       if (X == 0)

           continue;

       sum += X * F[i];

       sum += (X-1) * X / 2 * pow(10,i);

       sum += X * (N%pow(10,i) + 1);

    }

    return sum;

}

代码清单2

在代码中,F(x)被预先计算出来,当然使用pow求10的幂次也可以先预处理,这里只是为了做范例。这个代码中的循环次数只和的位数有关,它的时间复杂度为O(logN),这样的复杂度,即使N超出了32位整数的数值范围,也能迅速计算完成,但那时你得注意sum的溢出问题。

虽然这个高效算法实现了,但我们仍然有疑惑,比如,为何把Xn改成Xn*(N%10n+1)代入后,各个位计算求和就是正确答案了?这个过程似乎太自然,我们回头来验证下。

我先整理下我们的思考过程,这里体现了分治思想。将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。问题的规模越小,越容易直接求解,就如递推函数F(x),以它为基础再解决这样X*10n特殊的子问题。

从下而上分析,因为我们发现代码清单1的计算过程中存在大量的重复,如,周期性不停地循环着,而第n位的数字就是第n-1位的周期数,由0~9的循环我们能得出00~99的循环,进而得到000~999的循环。

从上而下分析,考虑可以把高位的数字统计完,再计算低位的数字,按位分段计算,我们肯定能得到形如类似这样的公式。

 

问题二

解法一

问题二是问题一的逆命题,对于逆命题,我们可以按着原命题的算法逐个计算再测试计算结果和给定sum的值是否相同,这样的算法只要一个for循环不停的枚举N,并调用sum_digital_2函数,当计算的值超过sum时就可以停止了,因为不可能有比正确答案更大的N而它的sum却小于或等于给定的sum,简单的说,sum是随着N增大而增大的,它是一个单调递增函数。

枚举法是没有效率可言的,乘上sum_digital_2复杂度,它的时间复杂度为O(N logN),为了得到更高效的算法,我们使用二分法来替换枚举法,知道二分查找的同学肯定不会陌生。

之所以二分法在这里能派上用场,就是因为sum值有单调递增的特点。设置left和right变量,每次取它们的中间值mid来测试,如果小了,按照sum值单调性我们可知肯定大于mid,则left往mid+1移动,否则right往mid移动。应用二分法后的时间复杂度为O(logN logN)。

int find_n(int sum)

{

    int l = 0;

    int r = sum;

    while (l < r) {

       int mid = (l+r) >> 1;

       int tmp = sum_digital_2(mid);

       if (tmp < sum)

           l = mid + 1;

       else

           r = mid;

    }

    return l;

}

代码清单4

这里还需要注意的是溢出问题,代码清单4中直接把sum当作right的值,这很容易使得sum_digital_2(mid)计算的结果溢出,有个简单的解决办法,可以拿sum和代码清单2中的F[i]数组去比较,就可以知道N是属于哪个X*10n范围之内,再拿X*10n作为right的值。

posted @ 2012-09-29 14:35  有深度的程序员面试题  阅读(4816)  评论(2编辑  收藏  举报