N!的最末位非零数

  给定一个数N(N <= 10 ^ 1000),如何快速求得N!的最末位非零数是一个经典的问题。一直以来都被这个问题困扰,今天仔细想了下,终于给想通了,尽管可能有些笨拙,现把想法记录于此。
  在N很小的情况下,有一个简便的方法:求出1到N之间每个数的2的因子数和5的因子数,记为F(2)和F(5),显然F(2) >= F(5)。由于在末尾只有2和5相乘才能产生0,如果我们把2和5抛去,那么肯定不会有0,这样就可以一边乘一边模10,防止溢出。剩下的一堆2和5如何处理呢?因为2肯定比5多,因此最末位肯定是偶数(0的阶乘和1的阶乘除外)。而一个偶数不停地乘2,最末位的规律是:2 -> 4 -> 8 -> 6 -> 2 -> ...出现了4位1循环,这样我们先用F(2) - F(5),使得一部分2和5匹配上,2 * 5 = 10,对末尾不产生影响,剩下的2就模一下4,剩几再乘几次2就可以了。
  但是这个方法在N非常大的时候肯定就不行了,但是可以利用找循环这个思想继续做。如果算阶乘的时候跳过5的倍数,记G(n)为跳过5的倍数的时候,从1乘到n的最末非零位,也就是把5的倍数当1乘。可以发现:
G(1) = 1, G(2) = 2, G(3) = 6, G(4) = 4, G(5) = 4, G(6) = 4, G(7) = 8, G(8) = 4, G(9) = 6, G(10) = 6, G(11) = 6, G(12) = 2, G(13) = 6...
  又出现了循环,每10个数循环一次。如何计算G(n)就变的很简单,求出n的最末位,就知道对应的G(n)是多少了,当然需要特判n = 1的情况。由于我们把5的倍数的数都提出来了,提出来的这些数(5、10、15、20、25、30...)每个除以5后又组成了一个阶乘序列!除完5一共提出了n / 5个5,根据之前的分析,每个5都可以拿出一个2和它配对然后把它消去,这样一个5就相当于少一个2,我们就要把原来的数乘以3个2(模四循环)。这样一来5的个数其实也可以模四,模完四之后剩k的话,就可以乘以k个8,就把所有的5消去了。现在总结一下:对一个数n的阶乘,计算它的末尾非零位,先计算G(n),相当于非5的倍数的数的乘积最末非零位先算好了,然后乘以n / 5 % 4个8,处理了提出的n / 5个5,这样之后还剩下n / 5的阶乘没有算。递归的求解n / 5的阶乘的最末位非零数,再乘上去就得到结果了。
  这个做法的复杂度就很低了,达到O(log n),对于10 ^ 1000的数据,利用高精度做就行了。利用这种循环的思想,算排列数P(n, k)的最末非零数也就可以做到了。
附HOJ 1013代码:
 1 #include  < cstdio >
 2 #include  < cstring >
 3 const   int  N  =   1024 ;
 4
 5 int  hash[ 10 =   {6626444846} ;
 6 int  one_digit_hash[ 10 =   {1126422428} ;
 7
 8 int  last_digit( char  str[N],  int  st,  int  to)
 9 {
10    int i, tmp = 0, ret, num_of_five = 0;
11
12    if (st == to)
13        return one_digit_hash[str[st]-'0'];
14
15    ret = hash[str[to]-'0'];
16    for (i = st; i <= to; i++)
17    {
18        tmp = tmp * 10 + str[i] - '0';
19        str[i] = tmp / 5 + '0';
20        tmp %= 5;
21        num_of_five = (num_of_five * 10 + str[i] - '0'% 4;
22    }

23    if (str[st] == '0')  st++;
24    ret = last_digit(str, st, to) * ret % 10;
25    while (num_of_five--)   ret = ret * 8 % 10;     //mul one 5 equals mul one 8
26
27    return ret;
28}

29
30 int  main()
31 {
32    char str[N];
33
34    while (scanf("%s", str) == 1)
35        printf("%d\n", last_digit(str, 0, strlen(str) - 1));
36
37    return 0;
38}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值