51

链接到原始问题

这不是家庭作业问题。我只是认为有人可能知道这个问题的真正解决方案。

早在 2004 年我参加了一场编程比赛,就遇到了这个问题:

给定 n,求 n! 的位数之和。n 可以是 0 到 10000。时间限制:1 秒。我认为每个测试集最多有 100 个数字。

我的解决方案非常快但不够快,所以我让它运行了一段时间。它构建了一个预先计算的值数组,我可以在我的代码中使用这些值。这是一个黑客,但它有效。

但是有一个人用大约 10 行代码解决了这个问题,它很快就会给出答案。我相信这是某种动态规划,或者来自数论的东西。那时我们 16 岁,所以它不应该是一门“火箭科学”。

有谁知道他可以使用什么样的算法?

编辑:如果我没有把问题说清楚,我很抱歉。正如 mquander 所说,应该有一个聪明的解决方案,没有 bugnum,只需简单的 Pascal 代码、几个循环、O(n 2 ) 或类似的东西。1 秒不再是限制。

我在这里发现如果 n > 5,则 9 除以阶乘的数字总和。我们还可以找到数字末尾有多少个零。我们可以使用它吗?

好的,来自俄罗斯的编程竞赛的另一个问题。给定 1 <= N <= 2 000 000 000,输出 N!模(N+1)。这有什么关系吗?

4

10 回答 10

31

我不确定谁还在关注这个线程,但无论如何都会这样。

首先,在官方外观的链接版本中,它只需 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) 时间内执行此操作,其中波浪号表示您可以加入对数因子。由于 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 位数的数字。

于 2009-12-09T18:00:47.150 回答
8

这是整数序列在线百科全书中A004152。不幸的是,它没有任何关于如何有效计算它的有用技巧——它的 maple 和 mathematica 配方采用了幼稚的方法。

于 2009-09-24T07:43:38.883 回答
6

我将解决第二个问题,计算 N!mod (N+1),使用威尔逊定理。这将问题简化为测试 N 是否为素数。

于 2009-09-24T14:30:24.757 回答
3

可在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
于 2009-09-24T05:09:00.303 回答
3

假设你有很大的数字(这是你最少的问题,假设 N 真的很大,而不是 10000),让我们从那里继续。

下面的技巧是分解N!通过分解所有 n<=N,然后计算因子的幂。

有一个计数器向量;每个质数一个计数器,最多 N 个;将它们设置为 0。对于每个 n<= N,因子 n 并相应地增加素因子的计数器(智能因子:从小的素数开始,在分解时构造素数,并记住除以 2 是移位)。从 2 的计数器中减去 5 的计数器,使 5 的计数器为零(这里没有人关心 10 的因数)。

计算直到 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 次操作。

高温高压

于 2009-09-24T14:01:25.930 回答
2

1秒?为什么你不能只计算 n!并把数字加起来?那是 10000 次乘法和不超过几万次加法,这大约需要十亿分之一秒。

于 2009-09-24T02:57:55.133 回答
1

你必须计算脂肪。

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 ......我们很快就会溢出 :-(

然后,您仍然需要一个用于大数字的库。

我应该被否决!

于 2009-09-25T16:56:49.020 回答
0

让我们来看看。我们知道n的计算!因为任何相当大的数字最终都会导致一个带有很多尾随零的数字,这些零对总和没有贡献。一路砍掉零点怎么样?那会使数字的大小保持更小吗?

唔。没有。我刚刚检查过,即使那样整数溢出仍然是一个大问题......

于 2009-09-25T16:44:03.850 回答
0

即使没有任意精度的整数,这也应该是暴力破解的。在您链接到的问题陈述中,需要计算的最大阶乘将是 1000!。这是一个大约有 2500 位数字的数字。所以只需这样做:

  1. 分配一个 3000 字节的数组,每个字节代表阶乘中的一个数字。从值 1 开始。
  2. 在数组上重复运行小学乘法,以计算阶乘。
  3. 对数字求和。

重复乘法是唯一可能很慢的步骤,但我确信可以在一秒钟内完成 1000 次乘法,这是最坏的情况。如果没有,您可以提前计算一些“里程碑”值,然后将它们粘贴到您的程序中。

一种潜在的优化:当它们出现时从数组中消除尾随零。它们不会影响答案。

明显的注意:我在这里采用编程竞赛的方法。你可能永远不会在专业工作中这样做。

于 2009-10-15T18:04:58.100 回答
-1

使用 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)));
}
于 2015-08-14T22:38:49.443 回答