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代码如下: