如何计算N!的最后一位非零数字(POJ 1604 Just the Facts)

题目来源: http://acm.pku.edu.cn/JudgeOnline/problem?id=1604
Sample Input

1
2
26
125
3125
9999

Sample Output

      1 -> 1
      2 -> 2
    26 -> 4
  125 -> 8
 3125 -> 2
 9999 -> 8

例如 N = 26,N! =  403291461126605635584000000,最后一位非零数字为 4。
对于 N <= 10000 的时候,可以这样做:
把 1 ~ N 的 2, 5 因子除尽,然后每个数都模 10。
对于一次查询 n (n <= N),有 n! = 2a * 5b * cc 为所有非 2,5 因子的乘积,
那么 n! 的最后一个不是 10 的数字等于 2a-b * c % 10
接下来就是统计 1 ~ n 中 3,7,9 的个数,以及 因子 2 和 5 的个数,然后,
利用 (a * b) % 10 = (a % 10) * (b % 10) ,以及
2 4 % 10 = 2
3 4 % 10 = 1
7 4 % 10 = 1
9 4 % 10 = 1
可以化简计算。
#include  < cstdio >
using   namespace  std;

int  main ()
{
    
const   int  N  =   10001 ;
    
int  num [N];
    
int  cnt [ 10 ];
    
for  ( int  i  =   0 ; i  <  N; i  ++ )
        num [i] 
=  i;
    
for  ( int  i  =   5 ; i  <  N; i  *=   5 )
        {
        
for  ( int  j  =  i; j  <  N; j  +=  i)
            num [j] 
/=   5 ;     //  remove all the factor 5
        }
    
for  ( int  i  =   2 ; i  <  N; i  *=   2 )
        {
        
for  ( int  j  =  i; j  <  N; j  +=  i)
            num [j] 
/=   2 ;     //  remove all the factor 2
        }
    
for  ( int  i  =   1 ; i  <  N; i  ++ )
        num [i] 
%=   10 ;
    
int  map2 [ 4 =  { 6 2 4 8 };     //  map2 [i] = 2^(i+4) % 10
     int  map3 [ 4 =  { 1 3 9 7 };
    
int  map7 [ 4 =  { 1 7 9 3 };
    
int  map9 [ 4 =  { 1 9 1 9 };
    
int  n;
    
while  (scanf ( " %d " & n)  !=  EOF)
        {
        
if  (n  ==   1 )
            {
            printf (
"     1 -> 1 " );     //  when n == 1, it's an exception
             continue ;
            }
        cnt [
3 =  cnt [ 7 =  cnt [ 9 =   0 ;
        
for  ( int  i  =   1 ; i  <=  n; i  ++ )
            cnt [num [i]] 
++ ;
        cnt [
3 %=   4 ;
        cnt [
7 %=   4 ;
        cnt [
9 %=   4 ;
        cnt [
2 =  cnt [ 5 =   0 ;
        
for  ( int  i  =   2 ; i  <=  n; i  *=   2 )
            cnt [
2 +=  n  /  i;     //  count the factor 2 in n!
         for  ( int  i  =   5 ; i  <=  n; i  *=   5 )
            cnt [
5 +=  n  /  i;     //  count the factor 5 in n!
        cnt [ 2 =  (cnt [ 2 -  cnt [ 5 ])  %   4 ;
        printf (
" %5d -> %d " , n, map2 [cnt [ 2 ]]  *  map3 [cnt [ 3 ]]  *  map7 [cnt [ 7 ]]  *  map9 [cnt [ 9 ]]  %   10 );
        }
    
return   0 ;
}
/*
Run ID    User    Problem    Result    Memory    Time    Language    Code Length    Submit Time
2945042    rappizit    1604    Accepted    204K    0MS    G++    1083B    2007-11-26 00:04:13
*/


以上是自己想到的,进行了 O(N) 时间 的预处理以及每次 O(n) 时间的查询,而且使用 O(N) 的空间。
对于 AC 本题已经足够,但是考虑到 N 上亿的时候就没有足够空间了。参考一下别人的算法。
============以下引用自 http://gz0531.org/sub/article.asp?id=3&page=1===================

比如
 1 2 (3) 4 5 6 (7) 8 (9) 10 11 12 (13) 14 15 16 (17) 18 (19) 20 21 22 (23) 24 25 26

其中3个3,2个7和9。同时还有3个5,这里只考虑5结尾的5的倍数,因为末尾是0的在后面递归解决。
然后递归(循环其实也行)检查5的倍数:将所有5的倍数除以5,得到int(26/5)个数
 1 2 (3) 4 5
我们又获得了一个3和一个5。
再检查5的倍数。数列只剩下1了。
 1
再递归一次就啥都没有了——回归条件。
检查5倍数的递归结束。
好了,下面可以抛弃所有奇数了,开始处理偶数。

所有偶数提取2得到
 1 2 (3) 4 5 6 (7) 8 (9) 10 11 12 (13)
到此进入下一层递归,将此数列进行同样操作。显然这些数列都是1到N的自然数列,只要传递数列最后
一个数就可以了,空间复杂度很低。回归条件就是这个数列被如此反复折磨得一个不剩。
 
============以上引用自 http://gz0531.org/sub/article.asp?id=3&page=1===================


于是写下如下代码:

#include  < cstdio >
using   namespace  std;

int  cnt2, cnt3, cnt5, cnt7, cnt9;
int  map2 [ 4 =  { 6 2 4 8 };     //  map2 [i] = 2^(i+4) % 10
int  map3 [ 4 =  { 1 3 9 7 };
int  map7 [ 4 =  { 1 7 9 3 };
int  map9 [ 4 =  { 1 9 1 9 };

void  rec ( int  n)
{
    
if  ( ! n)  return ;
    
for  ( int  m  =  n; m; m  /=   5 )
        {
        
int  q  =  m  /   10 , r  =  m  %   10 ;
        cnt3 
+=  q  +  (r  >=   3 );
        cnt5 
+=  q  +  (r  >=   5 );     //  count the num whose last digit is 5
        cnt7  +=  q  +  (r  >=   7 );
        cnt9 
+=  q  +  (r  >=   9 );
        }
    cnt2 
+=  n  /   2 ;
    rec (n 
/   2 );
}

int  f ( int  n)
{
    
if  (n  ==   1 return   1 ;
    cnt2 
=  cnt3  =  cnt5  =  cnt7  =  cnt9  =   0 ;
    rec (n);
    
// printf ("%d %d %d %d %d ", cnt2, cnt3, cnt5, cnt7, cnt9);
     return  map2 [(cnt2  -  cnt5)  %   4 *  map3 [cnt3  %   4 *  map7 [cnt7  %   4 *  map9 [cnt9  %   4 %   10 ;
}

int  main ()
{
    
int  n;
    
while  (scanf ( " %d " & n)  !=  EOF)
        {
        printf (
" %5d -> %d " , n, f (n));
        }
    
return   0 ;
}
/*
Run ID    User    Problem    Result    Memory    Time    Language    Code Length    Submit Time
2945147    rappizit    1604    Accepted    168K    0MS    G++    896B    2007-11-26 01:40:2
*/


每次查询的时间复杂度是 O(log2n * log5n),空间复杂度为 O(1)。其中 log2n 是用于递归,log5n 是用于每层
递归把因子 5 “消灭”。
这种算法在查询比较多的时候其实比前一种算法慢,但是空间复杂度可是大大降低!
那么假如 当 N 达到 10100 的时候呢?例如这道题:
http://acm.hziee.edu.cn/showproblem.php?pid=1066
每次查询可能要执行上百万次操作(涉及高精度运算),有没有更好的算法?很幸运搜到了,呵呵,其实在
tenshi 例程里面就有代码,不过光是看代码是很难看懂其中的奥妙的。
以下是自己的描述,参考了http://mcqsmall.yculblog.com/post.1249397.html

首先对数列 d [10] = {1, 1, 2, 3, 4, 1, 6, 7, 8, 9} 和
ff [10] = {1, 1, 2, 6, 4, 4, 4, 8, 4, 6}
有 d [0] * ... * d [i] % 10 = ff [i],0 <= i < 10。

对于 n < 5 直接输出 ff [n] 即可。
对于 n >= 5,例如 n = 26,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
的乘积等于
1 2 3 4 1 6 7 8 9  1 11 12 13 14  1 16 17 18 19  1 21 22 23 24  1 26
的乘积再乘上 5 10 15 20 25 的乘积,而5 10 15 20 25 的乘积等于
1~26/5 的乘积再乘上 526/5

先考虑
1 2 3 4 1 6 7 8 9  1 11 12 13 14  1 16 17 18 19  1 21 22 23 24  1 26,
可以 10 个 10 个地分成几组。
其中 1 2 3 4 1 6 7 8 9 1,11 12 13 14  1 16 17 18 19  1 这两组数的乘积
都为 10*q + 6 的形式,两个乘积乘起来还是 10*q + 6 的形式。
而 21 22 23 24  1 26 这组数的乘积则是 10*q + ff [26%10] = 10*q + 4的形式,
因此 3 组数的乘积是 10*q + 4 的形式,这个 4 由 ff [26%10] * 6 % 10 所得。

因此 26! = (26/5)! * 526/5 * (10*q+4) = (26/5)! * 1026/5 * [(10*q+4) / 226/5],
则 26! 的最后一个非零数字为 (26/5)! * [(10*q+4) / 226/5]
注意到除了0! 和 1!,阶乘的最后一个非零数字必为偶数,所以有一个规律:
(10*q + 2) / 2 = 10*q' + 6
(10*q + 6) / 2 = 10*q' + 8
(10*q + 8) / 2 = 10*q' + 4
(10*q + 4) / 2 = 10*q' + 2
每除以 2 四次,尾数就循环一次。因此
(10*q+4) / 226/5 的尾数即 (10*q+4) / 226/5%4,这个可以用以下代码计算:
t = ff [n % 10] * 6 % 10;
for (int i = 1; i <= n / 5 % 4; i ++)
    {
    if (t == 2 || t == 6) t += 10;
    t /= 2;
    }

计算出来的 t 即为 (10*q+4) / 2^(26/5) 的尾数,然后用 t 乘以 (26 / 5)! 的最后
一个非零数字再对 10 取模即得到 26! 的最后一个非零数字而计算 (26 / 5)! 的最后
一个非零数字可以使用递归处理。

综上,设 F(N) 为 N! 最后一个非零数字,则有以下递归式:
F(N) = ff [N]   (N < 5)

             F([N/5]) * ff [N的尾数] * 6
F(N) = ----------------------------------- (N >= 5)

                 2[N/5] % 4

因此算法的时间复杂度是 O(log5N) 的。
即使 N 达到 10100,也可以很快计算出来,不过需要使用高精度整数,整除 5 也即
乘以 2 再整除 10,而乘以 2 也即自加,整除 10 也即截掉最后一位数字。
而对 4 取模只要取最后两位数字对 4 取模即可,例如 1234 % 4 = 34 % 4 = 2。
因此实现起来相当方便。

#include  < cstdio >
using   namespace  std;

const   int  ff [ 10 =  { 1 1 2 6 4 4 4 8 4 6 };

int  f ( int  n)
    {
    
if  (n  <   5 return  ff [n];
    
int  t  =  ff [n  %   10 *   6   %   10 ;
    
for  ( int  i  =   1 , r  =  n  /   5   %   4 ; i  <=  r; i  ++ )
        {
        
if  (t  ==   2   ||  t  ==   6 ) t  +=   10 ;
        t 
/=   2 ;
        }
    
return  f (n  /   5 *  t  %   10 ;
    }

int  main ()
{
    
int  n;
    
while  (scanf ( " %d " & n)  !=  EOF)
        {
        printf (
" %5d -> %d " , n, f (n));
        }
    
return   0 ;
}
/*
Run ID    User    Problem    Result    Memory    Time    Language    Code Length    Submit Time
2945954    rappizit    1604    Accepted    168K    0MS    G++    435B    2007-11-26 13:46:54
*/

哈,没空再写一个高精度的,所以只在 POJ 上重新提交一次以验证正确性而已。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值