如何计算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
    评论
题目描述 给定两个长度均为 $n$ 的数组 $A,B$,其中 $A$ 数组中的元素两两不同,$B$ 数组中元素也两两不同。 定义 $A$ 数组的第 $i$ 个元素为 $a_i$,$B$ 数组的第 $i$ 个元素为 $b_i$。定义一个四元组 $(i,j,k,l)$ 符合条件当且仅当 $1 \leq i < j \leq n$,$1 \leq k < l \leq n$,$$ a_i+b_j=k+l $$ 问有多少个符合条件的四元组。 输入 输入的第一行为一个整数 $n$,表示 $A$ 和 $B$ 数组的长度。 接下来 $n$ 行,第 $i$ 行包含两个整数 $A_i$ 和 $B_i$。 输出 输出一行,一个整数,表示符合条件的四元组个数。 输入样例 3 1 1 2 2 3 3 输出样例 6 提示 $1 \leq n \leq 1500$,$1 \leq A_i,B_i \leq 10^9$ 这一题要解符合条件的四元组。其中有一个直观的想法就是将四元组分类讨论,如下所示: $$a_i + b_j = k + l$$ 分类讨论,当 $i < k, j < l$ 时就是一种方案,当 $i > k, j > l$ 时是另一种情况。 因此,对于每一种 $a_i + b_j$ 的和,记录下它的出现次数,同时记录下这个和所对应的 $i, j$ 的值。在计算的过程中,如果遇到相同的和的时候,再次遇到时是可以直接忽略的,因为等式是对称的。 统计符合条件的四元组当然要对和进行枚举,但是值得注意的是,在构造符合条件的 $k, l$ 的时候,数组 $C$ 和 $D$ 的记录顺序是无关紧要的,因为等式 $a_i + b_j = k + l$ 已经将每个数字都制约了,它们可以出现任意的顺序。因此,在统计 $C$ 和 $D$ 对于计算答案来说是无区别的。 这一题值得特别注意的是,当数组中有数据的时候,要注意考虑到数据越界可能导致结果错误。在本题中,由于 $a_i, b_j$ 的上限是 $10^9$,因此 $a_i + b_j$ 的上限最大可能会达到 $2 \times 10^9$,因此在计算时一定要使用 long long 类型,否则很容易产生错误。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值