阶乘的位数之和
Posted
技术标签:
【中文标题】阶乘的位数之和【英文标题】:Sum of digits of a factorial 【发布时间】:2010-11-30 23:16:46 【问题描述】:Link to the original problem
这不是作业问题。我只是认为有人可能知道这个问题的真正解决方案。
我在 2004 年参加了一次编程比赛,遇到了这个问题:
给定n,求n的位数之和! n 可以是 0 到 10000。时间限制:1 秒。我认为每个测试集最多有 100 个数字。
我的解决方案非常快,但不够快,所以我让它运行了一段时间。它构建了一个预先计算的值数组,我可以在我的代码中使用这些值。这是一个 hack,但它有效。
但是有一个人用大约 10 行代码解决了这个问题,并且很快就会给出答案。我相信这是某种动态规划,或者来自数论的东西。那时我们 16 岁,所以它不应该是一门“火箭科学”。
有谁知道他可以使用什么样的算法?
编辑:如果我没有把问题说清楚,我很抱歉。正如 mquander 所说,应该有一个聪明的解决方案,没有 bugnum,只需要简单的 Pascal 代码、几个循环、O(n2) 或类似的东西。 1 秒不再是限制。
我发现here 如果 n > 5,则 9 除以阶乘的数字总和。我们还可以找到数字末尾有多少个零。我们可以使用它吗?
好的,来自俄罗斯的编程竞赛的另一个问题。给定 1
【问题讨论】:
你确定这不是数字的重复总和,比如 88 -> 8+8=16 -> 7?我可以用 10 行代码做到这一点。 @tom10:这不太可能是问题;因为解决方案只是“如果 n>=6 返回 9;否则返回 (1, 2, 6, 6, 3) 的第 n 个元素”。它只需要不到 10 行代码。 :-) @ShrevatsaR 和其他所有人:是的,是的,我意识到我的重新措辞让这个列表中的大多数人来说这是一个相当容易的问题,但对于一个 16 岁的孩子来说,这似乎不是一个坏问题。鉴于它已经坐在这里几个小时没有得到答复……最初的陈述似乎合理吗?或者这就是计算机科学测试的普特南? @tom10,我称之为“终极数字总和”。有一个很简单的规则就是UltimateDigitSum(A * B) = UltimateDigitSum(UltimateDigitSum(A) * UltimateDigitSum(B))
@DanThMan 这个方程很有趣。我为此编写了代码,它给出了 10000 的正确重复数字总和!这很接近,但不是问题所说的。谢谢你的好主意。
【参考方案1】:
1 秒?为什么你不能只计算 n!并把数字加起来?那是 10000 次乘法和不超过几万次加法,大约需要十亿分之一秒。
【讨论】:
在许多语言中,原生整数类型会很快溢出。此外,无疑存在更优雅、更高效的解决方案。 当然可以,但是也有很多语言原生支持 bignums。无论如何,我确信有一个聪明的解决方案,但问题一定是在某种程度上弄错了约束,否则这会很简单。 即使在 Python 中,有了原生的 bignum 支持,一个天真的(未记忆的)实现在几秒钟后就达到了递归深度。它永远不会达到要求的 10000。 @Chris Lutz:您不必递归地计算阶乘。只需使用 for 循环,就可以避免堆栈溢出。 我认为我们可以有把握地假设这个问题的作者没有想到“你所要做的就是使用 BigInt!”。这里有一些技巧。【参考方案2】:在http://www.penjuinlabs.com/blog/?p=44 找到的小而快的python 脚本。它很优雅,但仍然是蛮力。
import sys
for arg in sys.argv[1:]:
print reduce( lambda x,y: int(x)+int(y),
str( reduce( lambda x, y: x*y, range(1,int(arg)))))
$ time python sumoffactorialdigits.py 432 951 5436 606 14 9520
3798
9639
74484
5742
27
141651
real 0m1.252s
user 0m1.108s
sys 0m0.062s
【讨论】:
我相信lambda x, y: x * y
可以改成operator.mul
会更快,因为它会直接使用内置的乘法运算符,而不是通过lambda间接使用它。 lambda x, y: x + y
和 operator.add
也是如此
assert n > 0; sum(map(int, str(reduce(operator.mul, range(1, n+1)))))
更容易消化。注意:+1
.
为什么不直接使用 math.factorial??
上面的python脚本给出了错误的答案。正确答案是:sumdigits(432!) = 3897, sumdigits(951!) = 10035, sumdigits(5436!) = 74781, sumdigits(606!) = 5661, sumdigits(14!) = 45, sumdigits(9520!) = 140616【参考方案3】:
这是Online Encyclopedia of Integer Sequences 中的A004152。不幸的是,它没有任何关于如何有效计算它的有用提示 - 它的 maple 和 mathematica 配方采用了幼稚的方法。
【讨论】:
我不明白为什么有人在他的个人页面上链接复制而不是oeis.org/A004152 @virgo47 这是我第一次看到 oeis.org - 我最后一次使用它(当然是在 09 年),att 链接是最容易找到的链接。随意编辑答案!【参考方案4】:假设你有很大的数字(这是你的问题中最少的,假设 N 真的很大,而不是 10000),让我们从那里继续。
下面的技巧是分解 N!通过分解所有 n
有一个计数器向量;每个质数一个计数器,最多 N 个;将它们设置为 0。对于每个 n
计算直到N的所有素数,运行以下循环
for (j = 0; j< last_prime; ++j)
count[j] = 0;
for (i = N/ primes[j]; i; i /= primes[j])
count[j] += i;
请注意,在前面的代码块中,我们只使用了(非常)小的数字。
对于每个素因子 P,您必须计算 P 的适当计数器的幂,这需要使用迭代平方的 log(counter) 时间;现在你必须乘以所有这些素数的幂。
总而言之,您对小数(log N 素因数)进行了大约 N log(N) 操作,对大数进行了 Log N Log(Log N) 操作。
并且在编辑改进后,小数上只有N次操作。
HTH
【讨论】:
...这如何帮助找到数字的总和? 问题是通过很少的 bignum 乘法来计算数字(这些是扩展的)【参考方案5】:我将解决第二个问题,计算 N! mod (N+1),使用Wilson's theorem。这将问题简化为测试 N 是否为素数。
【讨论】:
所以如果 N+1 是素数,那么 N! mod N+1 是 N。如果 N+1 是合数且 N > 4,则 N! mod N+1 为 0。情况 0 哦,伙计,我想我在某个地方遇到过威尔逊定理,但对如何解释符号(N - 1)! ≡ -1 (mod n)
感到困惑。查找它,我现在看到它的意思是:(N - 1)! mod n ≡ -1 mod n
当且仅当n
是素数(注意:≡
表示“全等”)。
所以,这意味着如果n = 10000
那么n + 1 = 10001
,它不是素数(它是73 * 137
),所以10000! mod 10001 = 0
。这意味着如果我们将10000!
除以10001
,则没有余数。这很酷,但现在呢?我不知道如何从这个跳转到得到10000!
的数字总和。
@DanThMan:我认为这个问题与第一个问题有关。事实证明不是。 @Jitse Niesen:感谢您的定理。【参考方案6】:
让我们看看。我们知道n的计算!因为任何相当大的数字最终都会导致一个带有很多尾随零的数字,这些零对总和没有贡献。一路砍掉零点怎么样?那会让数字的大小保持更小一点吗?
嗯。没有。我刚查了一下,整数溢出还是个大问题……
【讨论】:
好的,快速检查似乎表明 n!总是能被 3 整除(至少对于 n 大约 7% 的数字尾随 0。忽略它们确实有帮助,但不是那么多。【参考方案7】:你必须计算脂肪。
1 * 2 * 3 * 4 * 5 = 120。
如果你只想计算数字的总和,你可以忽略结尾的零。
6 人!你可以做 12 x 6 = 72 而不是 120 * 6
7 人!你可以使用 (72 * 7) MOD 10
编辑。
我写的太快了……
10 是两个素数 2 和 5 的结果。
每当你有这两个因素时,你可以忽略它们。
1 * 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 * 10 * 11 * 12 * 13 * 14 * 15...
1 2 3 2 5 2 7 2 3 2 11 2 13 2 3
2 3 2 3 5 2 7 5
2 3
因子 5 出现在 5、10、15... 那么在乘以5、10、15之后会出现一个结尾的零……
我们有很多 2 和 3... 我们很快就会溢出 :-(
那么,你仍然需要一个大数字库。
我应该被否决!
【讨论】:
@Luc,我经历了你一半的思考过程。摆脱了所有的零。正如你所说,没有太大区别。它在 60 多岁的阶乘后溢出 :(【参考方案8】:即使没有任意精度的整数,这也应该是暴力破解的。在您链接到的问题陈述中,需要计算的最大阶乘将是 1000!。这是一个大约有 2500 位数字的数字。所以就这样做吧:
-
分配一个 3000 字节的数组,每个字节代表阶乘中的一个数字。从值 1 开始。
在数组上反复运行小学乘法,以计算阶乘。
将数字相加。
重复乘法是唯一可能很慢的步骤,但我确信可以在一秒钟内完成 1000 次乘法,这是最坏的情况。如果没有,您可以提前计算一些“里程碑”值,然后将它们粘贴到您的程序中。
一个潜在的优化:消除数组中出现的尾随零。它们不会影响答案。
明显的注意:我在这里采用编程竞赛的方法。在专业工作中你可能永远不会这样做。
【讨论】:
【参考方案9】:我不确定还有谁在关注这个话题,但无论如何都会这样。
首先,在官方外观的链接版本中,它只需是 1000 阶乘,而不是 10000 阶阶。另外,当这个问题在另一个编程比赛中被重用时,时间限制是 3 秒,而不是 1 秒。这对您获得足够快的解决方案所付出的努力有很大的影响。
其次,对于比赛的实际参数,Peter 的解决方案是合理的,但是通过额外的改进,您可以在 32 位架构下将其速度提高 5 倍。 (如果只需要 1000,甚至是 6 倍!)也就是说,不是使用单个数字,而是在基数 100000 中实现乘法。然后在最后,对每个超数字内的数字求和。我不知道你在比赛中被允许使用的电脑有多好,但我家里有一台与比赛一样古老的台式机。下面的示例代码 1000 需要 16 毫秒! 10000 需要 2.15 秒!该代码还忽略了尾随 0 的出现,但这仅节省了大约 7% 的工作量。
#include <stdio.h>
int main()
unsigned int dig[10000], first=0, last=0, carry, n, x, sum=0;
dig[0] = 1;
for(n=2; n <= 9999; n++)
carry = 0;
for(x=first; x <= last; x++)
carry = dig[x]*n + carry;
dig[x] = carry%100000;
if(x == first && !(carry%100000)) first++;
carry /= 100000;
if(carry) dig[++last] = carry;
for(x=first; x <= last; x++)
sum += dig[x]%10 + (dig[x]/10)%10 + (dig[x]/100)%10 + (dig[x]/1000)%10
+ (dig[x]/10000)%10;
printf("Sum: %d\n",sum);
第三,有一种惊人且相当简单的方法可以通过另一个相当大的因素来加速计算。使用现代的大数相乘方法,计算 n! 不需要二次时间。相反,您可以在 O-波浪号 (n) 时间内执行此操作,其中波浪号表示您可以加入对数因子。有一个简单的加速due to Karatsuba,它不会将时间复杂度降低到那个水平,但仍然可以改进它,并且可以节省大约 4 倍。为了使用它,您还需要将阶乘本身划分为相等大小的范围。您制作了一个递归算法 prod(k,n),将 k 到 n 的数字乘以伪代码公式
prod(k,n) = prod(k,floor((k+n)/2))*prod(floor((k+n)/2)+1,n)
然后你用 Karatsuba 做大乘法。
比 Karatsuba 更好的是基于傅里叶变换的 Schonhage-Strassen 乘法算法。碰巧的是,这两种算法都是现代大数库的一部分。快速计算巨大的阶乘对于某些纯数学应用可能很重要。我认为 Schonhage-Strassen 对于编程竞赛来说太过分了。 Karatsuba 真的很简单,你可以想象它是解决问题的 A+ 解决方案。
提出的部分问题是一些猜测,即有一个简单的数论技巧可以完全改变比赛问题。例如,如果问题是要确定 n! mod n+1,然后威尔逊定理说当 n+1 是素数时答案是 -1,而且当 n=3 时它是 2 并且当 n+1 是合数时它是 0 是一个非常容易的练习。这也有变化;例如 n!也是高度可预测的 mod 2n+1。同余和数字和之间也有一些联系。 x mod 9 的位数之和也是 x mod 9,这就是为什么当 x = n 时和为 0 mod 9 的原因!对于 n >= 6。x mod 11 的数字的交替和等于 x mod 11。
问题是,如果你想要一个大数的数字总和,而不是任何模数,那么数论中的技巧很快就会用完。将数字的数字相加与进位的加法和乘法不太吻合。通常很难保证快速算法不存在数学,但在这种情况下,我认为没有任何已知的公式。例如,我敢打赌,没有人知道 googol 阶乘的位数之和,即使它只是一个大约 100 位数的数字。
【讨论】:
【参考方案10】:使用 BigInteger 的另一种解决方案
static long q20()
long sum = 0;
String factorial = factorial(new BigInteger("100")).toString();
for(int i=0;i<factorial.length();i++)
sum += Long.parseLong(factorial.charAt(i)+"");
return sum;
static BigInteger factorial(BigInteger n)
BigInteger one = new BigInteger("1");
if(n.equals(one)) return one;
return n.multiply(factorial(n.subtract(one)));
【讨论】:
问题中明确排除了 bignum 的使用。以上是关于阶乘的位数之和的主要内容,如果未能解决你的问题,请参考以下文章