求解阶乘乘积的最右非零值

I.问题的提出
                  整数n的阶乘写作n!,它是从l到n的所有整数的乘积。阶乘增长的速度很快:13!在大多数计算机上不能用32位的整数来存放。70!已经超出多数浮点类型的范围。你的任务是找出n!最右边的非零位。例如,5!=1*2*3*4*5=120,所以5!的最右非零位为2,同样,7!=1*2*3*4*5*6*7=5040,所以7!的最右非零位为4。50!=30414093201713378043612608166064768844377641568960512000000000000,所以最右非零位为2。
                 写一个程序,计算N(1<=N<=50,000,000)阶乘的最右边的非零位的值。注意:10,000,000!有2499999个零。
                 输入:一个正整数n。
                 输出:n!的最右非零位。
                 函数接口:int factorial(int n);

                 为表述方便,可以以“尾数”的概念指代最右非零值这一说法。

II.问题分析
                 对于这么一个问题,恐怕没有人真的把阶乘值算出来,稍大的数据的阶乘值就非常大了。既然是去求最后一位,那么我们可以只运算最后一位,其余数位无须过问。我们可以从数字1开始,依次做乘法运算,遇到零则舍去,然后继续考虑上一个数位。在这种思想下,可以轻松得到一个C语言的算法:
int factorial(int n)
{
         int result = 1;
         for(int i = 1; i <= n; i++)
                    {
                            result *= i;
                 while(result % 10 == 0)
                            {
                                    result /= 10;
                            }
                            result %= 10;
                    }
         return result;
}

                 代入数据50检验,得输出为4,这个答案是错误的。跟踪程序的运行并和正确结果做对比,可以发现在运算到数字15的时候首次出现了错误。我们来分析一下。
14!=87178291200
15!= 15*14!=1307674368000
                 按照我们刚才的分析,在运算到14!之后,我们在程序中保存了一个2,这是14!的最后一位,我们用2乘以15得30,这时再舍弃零,得结果3,但是真实的结果是8。很明显的,12*15和2*15结果是不样的。因此这个算法不正确,之所以会出现错误,是因为最后一位的运算中产生了一个整十,因此要向前一位找到新的非零数,而我们没有保留这一位的全部信息,因此将得到错误的结果。

III.整十进位产生的根源
                 对于一般的进位(不是整十)我们是不需要特别考虑的,这些进位发生以后,仍然保存着最后一位非零,不会影响我们的运算结果。那么整十进位是怎么产生的呢?
                 大于十的数计算阶乘的时候,中间就直接夹杂着10这一因子,它可以导致整十进位。所有的因子5同因子2乘起来,也得到10,数字10本身也是5和2的乘积。因此将整个乘式因子分解后,我们可以断定:成对出现的2和5是产生10的根源。单独的2或5和一切其它数字的乘法,都不能产生整十。我们可以先把所有的5*2从乘式中剔除,然后再使用上面提到的第一种算法计算结果就可以了。
                 然而在乘式中,5的个数和2的个数未必是相等的,实际上除了在1!和0!中两者都不存在因子2和因子5外,在其它乘式中两者数量根本不能相等,后面我们会证明这一点。按照直觉,我们会猜测2更多一点,所以在剔除了等量的因子5和因子2之后,因子5就不存在了,但是因子2还剩下一部分,我们只需要处理剩下的数。但是究竟是谁剩下呢?

IV.5和2,谁更关键?
                 我们试图确定在M的阶乘中,因子k(素数)到底出现了多少次。实际上,从1到M这些数中,每k个便会有一个k的倍数,这个序列是
                        k,2k,3k,4k,...
                 将这些数据中的k提取出去并计数,然后在余下的数据中每k*k个之中又存在k因子,继续提取计数。余下的数据中每k*k*k个数据内又有一个k因子,持续提取计数,直到k*k*...*k大于M。也就是说,M的阶乘中含有因子k的个数可以由以下公式确定:

 

                 其中用到了取整操作,在程序设计中可以方便的对应为整数的除法。
                 这样我们可以容易的判断,

                 由于5>2因此因子5的个数更少,因子5的个数决定了整十的个数,因此也决定了乘式结尾零的个数。下面我们设计一算法来计算因子5的个数:

/*By Shilyx 2007.9.19,计算对Number!做因子分解后Factor因子的个数并返回*/
int GetFactorNumber(unsigned Number, unsigned Factor)
{
         int result = 0;
                    if(Factor < 2 || Number < 1)
                            return -1;
         while(Number >= Factor)
                            result += Number /= Factor;
         return result;
}

V.计算最右非零位
                 按照上面的思路,我们现在要着手计算阶乘结果的尾数。对于一个给定的数字M,我们要求得M!中的因子5的个数N,然后从1到M依次处理数据,并在这些数据中除去所有的因子5和N个因子2,然后使用余下的数据按照最开始时提出的算法计算结果。代码及相关分析如下:

/* By Shilyx 2007.9.19,计算Number!的最右非零位并输出*/
int GetLastDigit(unsigned Number)
{
         int result = 1;/*这是连乘法的基础值,也是最后要返回的结果*/
         int i;/*循环变量*/
         int tmp;/*循环中用到的临时值*/
         int Count = 0;/*记录找到的因子2的个数,直到和因子5的个数相等*/
int TotalFactors = GetFactorNumber(Number, 5);/*因子5的总数*/
         for(i = Number; i >= 1; i--)/*从Number一直向下处理*/
                    {
                            tmp = i;
                            /*尽可能多的得到因子2,直到tmp是个奇数或者不需要更多的因子2*/
                 while(tmp % 2 == 0 && Count < TotalFactors)
                            {
                                    tmp /= 2;
                                    Count++;
                            }
                 while(tmp % 5 == 0)/*除去所有的因子5*/
                                    tmp /= 5;
                            tmp %= 10;/*取得个位数*/
                            result *= tmp;/*做阶乘*/
                            result %= 10;/*只保留个位数*/
                    }
         return result;/*返回运算结果*/
}

                 代入数据检验,输出结果完全吻合。
                 其实不仅仅是最右一位数可以求得,最右任意位数都可以按照上面的方法计算,只是要保留的数据位数不同而已。

VI.更深的探索
                 上面找到了计算阶乘积最后几位数的一般方法,这个方法对每一个数据都进行了处理,因此,速度上受到了一点限制。考虑阶乘的计算方法细节,我们可以发现一些有用的东西。
        (1). 阶乘结果的尾数只能是2,4,8之中的一个(除0!和1!外)
为什么会这样呢?我们可以这样来思考,对于7以下的数字,我们可以非常容易的验证这一点,就不赘述了;对于7及以上数字的阶乘,里面必然包含一个非素因子6,我们可以把阶乘结果表示6A,A是其余数字的乘积,设6A的最后一位数字是k。那么我们考虑6K*6这个数值,且不论这个数值有什么意义,我们先推断一下这个数值的特征。它的尾数是什么呢?6A*6=6*6A,而6*6=36的尾数依然是6,因此6A*6的尾数等同于6A的尾数,也是k。这也就是说,6*k的尾数也是k本身。
                 在数字1-9中,满足这一条的只有三个数字:2,4,8。
        (2).不同数字对阶乘尾数的影响有差异
                 考虑数字1和6,它们对尾数的变化是没有影响的,尾数与其相乘之后保持不变。
                 考虑数字9和4,它们对尾数的变化有影响,但是影响是周期性的。尾数同两个9相乘,相当于与1相乘,因为9*9=91;尾数同两个4相乘,相当于与6相乘,因为4*4=16。
                 考虑数字2,3,7,8,它们对尾数的变化有影响,但是影响也是周期性的。尾数同四个2或者四个8相乘,相当于与6相乘;尾数同四个3或者四个7相乘,相当于与1相乘;
                 数字0和5无须考虑,所有的5的倍数都已经被完全分解。
                 根据上面的条件分析,可以得到一个新的思路,这个思路是基于为尾数分布的分析的。比如对尾数9来说,如果阶乘中含有奇数个数字9那么就相当于一个数字9,偶数个数字9相当于一个数字1。
        (3).统计尾数的可行性
设法统计到尾数出现的次数,我们的工作会更加轻松。我们现在要确定的是从数字1到N,其间尾数为k的数有多少(k=1..9)。这种分布是以10为周期的,在N和与N接近的一个整十之间,也可能存在以k为尾数的数。那么我们可以根据下面是算法来统计这些数的数目:

/* By Shilyx 2007.9.19,统计1-Number之间的数字中以Digit为尾数的个数*/
int GetLastDigitTimes(unsigned Number, int Digit)
{
                    if(Number % 10 >= Digit)
                            return Number / 10 + 1;
         return Number / 10;
}

VII.更优的算法
                 基于以上的分析,我们可以设计一个新的算法,此算法不要求将每个数据处理一次,因此,时间上会有一定的节省。我们现在可以一开始就统计数据吗?还不可以。因为除掉因子5和一部分因子2的工作还没有做,这部分工作我没有找到更好的办法,因此还要使用上面的算法,直到除去了足够多的因子2,然后再使用新算法。对于M的新算法的流程如下:
                 (1).计算因子5的个数;
                 (2).从M向1依次处理数据,尽可能多的除去因子2,直到除去的足够多;
                 (3).从下一个数据开始,使用新算法。计算余下数字中的2,3,4,7,8,9的个数,并分别处理;
                 (4).分析统计结果,得出最终结果。
                 算法及其分析如下:

/* By Shilyx 2007.9.19,计算Number的Times次方*/
int Power(int Number, int Times)
{
         int result = 1;
                    while(Times--)
                            result *= Number;
         return result;
}

/* By Shilyx 2007.9.19, 计算Number!的最右非零位并输出方法2*/
int GetLastDigitB(unsigned Number)
{
         int result = 1;/*这是连乘法的基础值,也是最后要返回的结果*/
         int i;/*循环变量*/
         int tmp;/*循环中用到的临时值*/
         int Count = 0;/*记录找到的因子2的个数,直到和因子5的个数相等*/
int TotalFactors = GetFactorNumber(Number, 5);/*因子5的总数*/
         int T2, T3, T4, T7, T8, T9;/*指示以2,3,4,7,8,9为尾数的个数*/
                    /*下面依旧是排除因子2的过程*/
         for(i = Number; i >= 1; i--)
                    {
                            tmp = i;
                 while(tmp % 2 == 0 && Count < TotalFactors)
                            {
                                    tmp /= 2;
                                    Count++;
                            }
                 while(tmp % 5 == 0)
                                    tmp /= 5;
                            tmp %= 10;
                            result *= tmp;
                            result %= 10;
                            if(Count == TotalFactors && i % 5 == 1)
                         break;
                    }
                    i--;

                    /*因子2排除完毕,开始使用新方法统计各尾数出现次数并对自身的周期取模*/
                    T2 = GetLastDigitTimes(i, 2) % 4;
                    T3 = GetLastDigitTimes(i, 3) % 4;
                    T4 = GetLastDigitTimes(i, 4) % 2;
                    T7 = GetLastDigitTimes(i, 7) % 4;
                    T8 = GetLastDigitTimes(i, 8) % 4;
                    T9 = GetLastDigitTimes(i, 9) % 2;

                    /*处理余下数据中的所有因子5*/
         for(; i >= 1; i -= 5)
                    {
                            tmp = i;
                 while(tmp % 5 == 0)
                                    tmp /= 5;
                            tmp %= 10;
                            result *= tmp;
                            result %= 10;
                    }

                    /*处理得到的不同尾数的出现次数*/
                    result *= Power(2, T2);
                    result *= Power(3, T3);
                    result *= Power(4, T4);
                    result %= 10;
                    result *= Power(7, T7);
                    result %= 10;
                    result *= Power(8, T8);
                    result %= 10;
                    result *= Power(9, T9);
                    result %= 10;
         return result;
}

                 这个新的算法在很大程度上减小了循环体的长度,如果能找到更好的处理因子2的办法,那么算法还可以更优。

VIII.两种算法的比较
                 主要是从运行时间是比较两种算法,在Number值较小的情况下,不好判断哪一个速度更快,我们计算一亿的阶乘的尾数,并使用Windows提供的GetTickCount函数来标志运行时间,在我的机器上前一个算法耗费时间是10484,新的算法则是5016,由此可见,新的算法在大量数据处理上可以节约大约一半的时间。
                 正如文中所说,如果有更好的剔除因子2的算法,那么运算速度还可以得到进一步改进。如果各位朋友有新的想法,可以在此留言,也可以给我发邮件我的邮箱是OverSleep@163.com,QQ是303013274,欢迎交流,By Shilyx。


IX.再一次更优的算法(2007.9.25,中秋)
就在第二天,我找到了前面提到的更好的算法,利用上面的时间测试方法对新方法测试,结果是零,可见新算法的威力!我试过运算很大的无符号整数到40亿,几乎不耗费什么时间,瞬间输出结果。
下面贴出代码:
int GetLastDigitTimesDiv5(unsigned Number, int Digit)
{
if(Number < Digit)
   return 0;
if(Number % 10 >= Digit)
   return Number / 10 + 1 + GetLastDigitTimesDiv5(Number / 5, Digit);
return Number / 10 + GetLastDigitTimesDiv5(Number / 5, Digit);
}
int GetLastDigitC(unsigned Number)
{

int result = 1, i, tmp, Count = 0;
int TotalFactors = GetFactorNumber(Number, 5);
int T2, T3, T4, T42, T7, T8, T84, T842, T9;
if(Number > 6)                 //为了排除6的影响,先乘一个
        result = 6;
T2 = GetLastDigitTimesDiv5(Number, 2);
T3 = GetLastDigitTimesDiv5(Number, 3);
T4 = GetLastDigitTimesDiv5(Number, 4);
T7 = GetLastDigitTimesDiv5(Number, 7);
T8 = GetLastDigitTimesDiv5(Number, 8);
T9 = GetLastDigitTimesDiv5(Number, 9);
T84 = 0;                 //对尾数为8的数排2后的尾数为4的个数
T42 = 0;                 //对尾数为4的数排2后的尾数为2的个数
T842 = 0;                   //对所有T84的数排2后的尾数为2的个数
if(TotalFactors > 0)              //处理T8
{
   if(T8 >= TotalFactors)              //对T8做一次排2已足够
        {
         T8 -= TotalFactors;             //剩余的T8
         T9 += TotalFactors / 2;            //新生成的T9
         T4 += TotalFactors - TotalFactors / 2;         //新生成的T4
         TotalFactors = 0;               //不继续需要排2
        }
   else                 //T8不够用
        {
         TotalFactors -= T8;             //对所有的T8排2
         T9 += T8 / 2;                //新生成的T9
         T84 = T8 - T8 / 2;              //新生成的T84
         T8 = 0;                //没有T8了
    if(T84 >= TotalFactors)            //对T84做一次排2已足够
         {
          T84 -= TotalFactors;           //剩余的T84
          T7 += TotalFactors / 2;           //新生成的T7
          T2 += TotalFactors - TotalFactors / 2;        //新生成的T2
          TotalFactors = 0;              //不继续需要排2
         }
    else                //T84不够用
         {
          TotalFactors -= T84;           //对所有的T84排2
          T7 += T84 / 2;              //新生成的T7
          T842 = T84 - T84 / 2;             //新生成的T842
          T84 = 0;              //没有T84了
          if(T842 >= TotalFactors)          //对T842做一次排2已足够
          {
           T842 -= TotalFactors;            //剩余的T842
           TotalFactors = 0;             //不继续需要排2
          }
         }
        }
}
if(TotalFactors > 0)              //处理T4
{
   if(T4 >= TotalFactors)              //对T4做一次排2已足够
        {
         T4 -= TotalFactors;             //剩余的T4
         T7 += TotalFactors / 2;            //新生成的T7
         T2 += TotalFactors - TotalFactors / 2;         //新生成的T2
         TotalFactors = 0;               //不继续需要排2
        }
   else                 //T4不够用
        {
         TotalFactors -= T4;             //对所有的T4排2
         T7 += T4 / 2;                //新生成的T7
         T42 = T4 - T4 / 2;              //新生成的T42
         T4 = 0;                //没有T4了
    if(T42 >= TotalFactors)            //对T42做一次排2已足够
         {
          T42 -= TotalFactors;           //剩余的T42
          TotalFactors = 0;              //不继续需要排2
         }
        }
}
if(TotalFactors > 0)              //处理T2
{
   if(T2 >= TotalFactors)              //对T2做一次排2已足够
        {
         T2 -= TotalFactors;
         TotalFactors = 0;
        }
}
  
T2 %= 4;                 //处理计算得到的位数
T42 %= 4;
T842 %= 4;
T3 %= 4;
T4 %= 2;
T84 %= 2;
T7 %= 4;
T8 %= 4;
T9 %= 2;
result *= Power(2, T2);
result %= 10;
result *= Power(2, T42);
result %= 10;
result *= Power(2, T842);
result %= 10;
result *= Power(3, T3);
result *= Power(4, T4);
result %= 10;
result *= Power(4, T84);
result %= 10;
result *= Power(7, T7);
result %= 10;
result *= Power(8, T8);
result %= 10;
result *= Power(9, T9);
result %= 10;
return result;
}
 
 

下面是算法说明

如果用 C 表示 [n/5] + [n/25] + [n/125] + ..., 那么需要求的是下面的同余方程

         n ! ≡ x * 10^c (mod 10^(c+1))

的解 x , 0< x ≤ 9.
上面这个同余方程等价于下面的方程组

        n! ≡ x * 5^c * 2^c (mod 2^(c+1)), n! ≡ x * 2^c * 5^c (mod 5^(c+1))

当 n > 1 时所有的偶数都是上面的方程组中第一个方程的解,而且, n > 1 时该方程组的
第一个方程没有奇数解,因此 n > 1 时只需要考虑下面这个方程(即方程组的第二个方程)
的符合 0 < x <9 的偶数解:

         n! ≡ x * 2^c * 5^c (mod 5^(c+1)).                         (a)

用 h(n) 表示 所有与 5 互素且不大于 n 的正整数的连乘积, 则 n! 可以表为

h(n) * 5^[n/5] * h([n/5]) * 5^[n/25] * h([n/25]) * 5^[n/125] * h([n/125]) *... ,

代入 (a) 式,消去 5 的乘方后得到下面的同余方程

         h(n) * h([n/5]) * h([n/25]) * ... ≡ x * 2^c (mod 5), (b)

由于 3 * 2 ≡ 1 (mod5), 因此 (b) 式变为
   
         3^c * h(n) * h([n/5]) * h([n/25]) * ...≡ x (mod5),         (c)

由 Euler-Fermat 公式知( % 表示求模运算 )

         3^c ≡ 3 ^ (c % 4) (mod 5),

由 Wilson 定理有

         h(n) ≡ (-1)^([n/5]) * (( n % 5 )! ) (mod 5),

把上面两式代入 (c) 就得到了

         3^(c%4) * (-1)^c * ( n % 5 )! * ([n/5] % 5)! * ([n/25] % 5)!*...

≡ x (mod 5)                                                  (d)

由 c 的表达式可知, 我们可以在求 [n/5],[n/25],[n/125],... 的过程中
求出 c 和 (n % 5)!,([n/5] % 5)!,([n/25] % 5)!,..., 这样就可以求得
(d) 式的左边.把最后的结果 模 5 后即可以求得 x.

这个过程实际上是用连除法求 n 的 5 进制表示. 由于 5 进制表示的 各个 位上的数字是任意的,因此 (d) 式的 左边 已不能再简化.由于任意 求进制表示的 方法本质上都是连除法,因此这个算法 本质上 已是最优算法..

这是一个时间复杂度 O(log n) 的算法

用 C 语言写的该算法的实现

为了确保跨平台性,我使用了
C 99 规定的类型 uint_fast32_t, 为使用这个类型,请 包含 c 语言的 标准头文件 stdint.h. 下面是我的函数


uint_fast32_t        GetRightestNoZeroDigitA(uint_fast32_t        n)
{
          uint_fast32_t        m = 1,    c = 0,   i;
          uint_fast32_t        cr[] = { 1, 3, 4, 2 };
          uint_fast32_t        rr[] = { 1, 6, 2, 8, 4 };
          while( n > 1){
              if ( 2 == (i = n % 5) )
                  m <<= 1;
              else if ( 4 == i )
                  m <<= 2;
              c += n /= 5;
          }
          if ( c   & 1 )
              return        rr[ ( cr[ c % 4 ] * 4 * m ) % 5 ];
          return        rr[ ( cr[ c % 4 ] * m ) % 5 ];    
}

转自:http://hi.baidu.com/shilyx/blog/item/5bb7733e6313ec3e70cf6cd9.html

        http://hi.baidu.com/tianyuan006/blog/item/5c7c7bfaa5d5baddb48f3196.html

posted @ 2009-10-28 00:04  瀑布飞鹰  阅读(1118)  评论(2编辑  收藏  举报