HDOJ 1066 题解

HDOJ 1066  题解


Last non-zero Digit in N!


由于网络上的题解或模版诸多互相抄袭, 一知半解, 晦涩难懂. 难以有效的作为参考弄懂此题. 笔者作为一位ACM初学者水平能力有限, 但喜欢真正的理解与解决每道自己可以AC的题目, 所以结合自己2天的琢磨与分析总结了这篇题解. 

此题数目较大, n可能是有百位是无法直接计算n!的. 所以最先映入脑海的想法是从1开始进行乘数累加的分步阶乘计算., 每次只保留当前结果的最右非0值进行n次计算. 从初始的x=1, i=1开始计算x * i, 将结果的最右非0值赋值给x且i=i+1, 一直计算到i=n. 可是这个方法不但会在计算到乘以14(或者13, 笔者记不清了. )的时候因为进位出错导致之后的结果全为0之外, n次的循环计算也会导致程序超时不能AC. 正确的做法在于找出数字的规律, 是一道属于找规律的题. 弄懂此题比较复杂, 需要一些离散数学的知识. 

第一种情况  n<10  :直接枚举即可:int FirstTen[10]={1,1,2,6,4,2,2,4,2,8}. 

          第二种情况 n>=10 :需要详细的分析如下: 我们不难想到n!的尾部的0都来源于因子5与因子2(一对2与5产生一个0)。如果将这些因子去掉,则上述n次乘积的分步阶乘计算就会产生正确结果(虽然还是存在超时问题). 


定义1: G(n)为计算n!时将所有5的倍数均换成1(把可整除5的因子全忽略)后的各项乘积的总值. 


如: G(15)=1 * 2 * 3 * 4 *1 * 6 * 7 * 8 * 9 * 1 * 11 * 12 * 13 * 14 *1.

若我们忽略 <把n!中5的倍数都提取出来了> 这一先决条件, 那么n!的最右非0数就是G(n)值的最后一位, 即为 G(n)%10. 经过只求计算可以很容易的列举G(1)—G(20)的最右值(一定都是非0的):

            n:  0   1   2   3   4   5   6   7   8   9 

G(n)%10:  1   1   2   6   4   4   4   8   4   6

(因为前10个数的最终结果值是第一种情况可以直接计算出来,所以我们其实关心的是从10开始之后的情况)

   

           n:  10   11   12   13   14   15   16   17   18   19 

G(n)%10:  6     6     2     6     4    4     4     8     4     6


            n:  20   21   22   23   24   25   26   27   28   29 

G(n)%10:  6     6     2     6     4    4     4     8     4     6


好了,到这里已经露出一些端倪了。类似G(10)—G(19), G(20)—G(29)等之后的所有一组10个数的最右值都和上面给出的G(10)—G(19)一致。我们把这10个值作为基准表示为:int table[10] = {6, 6, 2, 6, 4, 4, 4, 8, 4, 6}. 我们通过找规律, 只需要1步计算就可以确定对于任意的n, G(n)的最右值: table[n%10]


现在只需要把忽略的那些5的倍数全乘回来就是最终结果. 由于G(n)的最右非0值已经很容易求了, 就是G(n)的最后一位. 所以我们不想再因乘以因子5导致出现最后一位变成了0, 需要去考虑次低位是什么, 甚至次次低位是什么的复杂情况了.因为在取最右非0值的时候, 乘以1个因子5等同于除以1个因子2.  而且因子2的数目绝对足够匹配需要补乘上的因子5的数目. 因为无论是n!还是G(n), 其中都包含的因子2一定比忽略的因子5多(阶乘中2的倍数显然比5的倍数多). 比如10!中因子5只有5和10中各包含1个, 但是2, 4, 6, 8, 10中包含1+2+1+3+1个因子2. 


因为n!=(n/5)! * 5^(n/5) * G(n).例如15!=(n/5)! * 5^(n/5) * G(15)=3! * 5^3 * G(15). 所以求n!的最右非0值的问题可拆解为求(n/5)!的最右非0值的子问题 乘以 G(n)补乘(n/5)个5后的最右非0值 再取最右非0值的问题. 


定义2: F(n)为取n!的最右非0值, 即目标结果. 

定义3: C(n)为取 5^(n/5) *G(n) 的最右非0值, 也就是取G(n)除以 (n/5) 个因子2之后得到的最右值. 


所以有F(n) = ( F(n/5) * C(n) ) % 10. 


分析到这里思路已经很清晰了. 子问题用递归可以得到很简洁的解决. G(n)的最右非0值(也就是最右值)的已知给求取G(n)除以(n/5)个因子2后得到的C(n)提供了先决条件. 接下来我们来分析”除以”若干个2会发生什么变化. 这是一个特殊的除法. 


已G(10)%10=6为例: 

(1)”除以”1个2:  G(10)%10 / 2 = 8. 

在这里为什么结果为8而不是3呢,是因为虽然G(10)是第二种情况下求出的第一个结果,但G(10)已经包含乘数因子2,4,6,8,也就是包含了1+2+1+3 = 7个乘数因子2了。所以G(10) / 2 的结果一定是一个偶数(还有6个因子2),所以肯定是最后一位6向高位借位(变为16/2)得出结果8. (笔者认为这是一个很凑巧但正确的解释, 实际上这个特殊除法的规律是在n的值很小可直接求出n!的值时, 对比真正n!的最右非0值与G(n)的最右值得到的. 但是因为上述解释简明正确易懂, 即符合抽象规则又能给出很形象的解释, 笔者也只是在总结时偶然发现. ) 

(2)”除以”2个2:  (1) / 2 = 8 / 2 = 4. (如果向高位借位则为18 / 2 = 9显然不符)

(3)”除以”3个2:  (2) / 2 = 4 / 2 = 2. (同上)

(4)”除以”4个2:  (3) / 2 = 2 / 2 = 6. (向高位借位了) 

(5)”除以”5个2:  (4) / 2 = 6 / 2 = 8. (同(1)一致,出现循环了)


  综上所述, 这个规律被找到了, 便是G(n)%10”除以”(n/5)个2的这个特殊的除法结果4次一循环,如果G(n)%10 = 6, 则其循环为: 

—>6-/2-> 8 -/2-> 4 -/2->2 -/2->6

同理G(n)%10 = 2 or 4 or 8的情况均符合这个除以2的四次循环. 例如G(n)%10 = 8, “除以”7个2时,等同于除以 7%4 =3个2 ==> 8 -> 4 ->2, 所以结果为2. 

所以循环基准为:int Circle[4] = {2, 6, 8, 4}. (当然也可以是8, 4, 2, 6, 保持4个数字先后顺序即可. )


具体算法:  已知n, 先求G(n)的最右值:  table[n%10].

                 找到table[n%10]的值 在 Circle[4] 中的位置i. (i = 0,1,2,3)

                 C(n) = Circle[ (i + n/5 ) % 4].  F(n) = ( F(n/5) * C(n) % 10). 


AC代码如下:

//
//  main.c
//  HDOJ1066
//
//  Created by Egger on 18/1/15.
//  Copyright (c) 2015年 Egger. All rights reserved.
//

#include <stdio.h>
#include <string.h>
#define SIZE 10
#define MAXH 1000

char Str[MAXH];
int Num[MAXH];

const int FirstTen[SIZE] = {1,1,2,6,4,2,2,4,2,8};
const int Table[SIZE] = {6,6,2,6,4,4,4,8,4,6};
const int DivisionCircle[4] = {2,6,8,4};


int H(int X, int Y)
{

    int i;

    for(i = 0; i < 4; i++)
    {
        if (X == DivisionCircle[i])
        {
            break;
        }
    }

    return DivisionCircle[(i+Y)%4];
}

int F(char* s)
{
    int i,k;
    int Bit,Carry,Len,Result;
    
    Len = (int)strlen(s);
    Result = 1;
    
    if(Len < 2)
    {
        return FirstTen[s[0]-'0'];
    }
    
    for(i = Len - 1; i >= 0; i--)
    {
        Num[Len-1-i] = s[i] - '0';
    }
    while(Len > 1)
    {
        k = Num[0];
        Carry = 0;
        Num[Len] = 0;
        for(i = 0; i <= Len; i++)
        {
            Bit = Num[i] * 2 +Carry;
            Num[i] = Bit % 10;
            Carry = Bit / 10;
        }
        for(i = 1; i <= Len; i++)
        {
            Num[i-1] = Num[i];
        }
        if(Num[Len] == 0)
            Len --;
        
        Result = Result * H(Table[k], Num[0]+Num[1]*10) % 10;
    }
    
    Result = Result * FirstTen[Num[0]] % 10;
    
    return Result;
}

int main(int argc, const char* argv[])
{

    while(scanf("%s",Str) != EOF)
    {
        printf("%d\n",F(Str));
    }
    return 0;
}







评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值