项目欧拉#211 - 效率问题

时间:2008-12-03 01:02:17

标签: math common-lisp

我一直在努力解决项目Euler问题列表,我来到了一个我知道如何解决的问题,但似乎我不能(根据我的解决方案的编写方式)

我正在使用Common Lisp执行此操作,并且我的脚本已运行超过24小时(远超过他们的一分钟目标)。

为了简洁起见,这是我的解决方案(这是一个扰流板,但只有你有一个快速处理器的地狱):

(defun square? (num)
  (if (integerp (sqrt num)) T))

(defun factors (num)
  (let ((l '()))
    (do ((current 1 (1+ current)))
        ((> current (/ num current)))
      (if (= 0 (mod num current))
          (if (= current (/ num current))
              (setf l (append l (list current)))
              (setf l (append l (list current (/ num current)))))))
    (sort l #'< )))

(defun o_2 (n)
  (reduce #'+ (mapcar (lambda (x) (* x x)) (factors n))))

(defun sum-divisor-squares (limit)
  (loop for i from 1 to limit when (square? (o_2 i)) summing i))

(defun euler-211 ()
 (sum-divisor-squares 64000000))

使用更小,更友好的测试参数解决问题所需的时间似乎比exponentialy大得多......这是一个真正的问题。

花了:

  • 0.007秒来解决100
  • 0.107秒解决1000
  • 2.020秒解决10000
  • 56.61秒解决100000
  • 1835.385秒解决1000000
  • 24小时以上解决64000000

我真的想弄清楚脚本的哪个部分导致它花了这么长时间。我已经考虑了记住因子函数,但我对如何实际实现它感到茫然。

对于那些想要查看问题本身的人,here it be

关于如何让这件事变得更快的任何想法都将非常感激。

**对不起,如果这对任何人都是一个扰流板,它并不意味着....但是如果你有足够的计算能力在相当长的时间内运行它,那么你就有更多的力量。

7 个答案:

答案 0 :(得分:14)

这是一个解决方案,牢记[Project] Euler的精神。 [警告:剧透。我试图保持缓慢的提示,这样你只能阅读部分答案并根据自己的想法自己思考。 :)]

当你面临与数字有关的问题时,一个好的策略(正如你可能从解决210项目Euler问题中已经知道的那样)就是看一些小例子,找到一个模式并证明它。 [最后一部分可能是可选的,取决于你对数学的态度; - )]

在这个问题中,看一些小例子 - 对于n = 1,2,3,4,......可能不会给你任何提示。但是在处理数论问题时还有另一种“小例子”的意义,你现在也可能知道这些问题 - 素数是自然数的构成块,所以从素数开始。

对于素数p,其唯一的除数是1和p,因此其除数的平方和为1 + p 2
对于素数p k ,其唯一的除数是1,p,p 2 ,... p k ,因此,它的除数是1 + p + p 2 + ... + p k =(p k + 1 -1)/(p-1) 。
这是最简单的情况:你已经解决了只有一个素因子的所有数字的问题。

到目前为止没什么特别的。现在假设你的数字n有两个素因子,比如说n = pq。那么它的因子是1,p,q和pq,所以它的除数的平方和是1 + p 2 + q 2 + p 2 q 2 =(1个+ p 2 )(1个+ q 2 )。
那么n = p a q b ?它的因素的平方和是多少?

[............................危险阅读下面的这行............ .......]

Σ0≤c≤a,0≤d≤b(p c q d 2 =((p a + 1 -1)/(p-1))((q b + 1 -1)/(q-1))。

这应该给你提示,无论是答案是什么以及如何证明它:n的除数之和仅仅是其因子分解中每个主要权力的(答案)的乘积,所以所有你需要做的是分解64000000(即使在一个人的头脑中很容易做到:-))并将每个的答案(=两者,因为唯一的素数是2和5)乘以其主要权力。

解决了Project Euler问题;现在是剥夺它的道德。

这里更普遍的事实是关于乘法函数 - 自然数上的函数,只要gcd(m,n)= 1,f(mn)= f(m)f(n) ,即m和n没有共同的素因子。如果你有这样一个函数,那么特定数字的函数值完全取决于它在主要权力下的值(你能证明这一点吗?)

稍微更难的事实,你可以试图证明[它不是 很难],这是:如果你有一个乘法函数f [here,f(n)= n 2 ]并将函数F定义为F(n)=Σ d除n f(d),(如此问题所示)则F(n)也是乘法功能

[事实上something very beautiful是真的,但是暂时不要看它,你可能永远不需要它。 : - )]

答案 1 :(得分:3)

我认为您的算法不是最有效的算法。提示:你可能从错误的一方开始。

编辑:我想补充一点,选择64000000作为上限可能是问题海报告诉你更好地思考问题的方法。

编辑:一些效率提示:

  • 而不是
(setf l (append l (...)))

你可以使用

(push (...) l)

通过使用您的值为car且将前l作为cdr的新单元格破坏性地修改您的列表,然后将l指向此单元格。这比每次必须遍历列表的追加要快得多。如果您需要其他顺序的列表,则可以在完成后对其进行反转(但这里不需要)。

  • 为什么要对l进行排序?

  • 您可以通过与num的平方根进行比较来提高(> current (/ num current))的效率(只需要为每个数字计算一次)。

  • 是否有可能更有效地找到数字的因子?

一个样式提示:您可以将l的范围放入do声明:

(do ((l ())
     (current 1 (+ current 1)))
    ((> current (/ num current))
     l)
  ...)

答案 2 :(得分:2)

我会通过对数字进行素数因子分解来对此进行攻击(例如:300 = 2 ^ 2 * 3 ^ 1 * 5 ^ 2),这是相对较快的,特别是如果你通过筛子生成它。由此,通过迭代i = 0..2生成因子相对简单; J = 0..1; k = 0..2,做2 ^ i * 3 ^ j * 5 ^ k。

5 3 2
-----
0 0 0 = 1
0 0 1 = 2
0 0 2 = 4
0 1 0 = 3
0 1 1 = 6
0 1 2 = 12
1 0 0 = 5
1 0 1 = 10
1 0 2 = 20
1 1 0 = 15
1 1 1 = 30
1 1 2 = 60
2 0 0 = 25
2 0 1 = 50
2 0 2 = 100
2 1 0 = 75
2 1 1 = 150
2 1 2 = 300

这可能不够快

答案 3 :(得分:2)

你缺少的聪明伎俩是你根本不需要考虑数字 1..N中有多少个数字是1的倍数? ñ 1..N中有多少个数是2的倍数? N / 2

诀窍是在列表中对每个数字的因子求和。 对于1,将1 ^ 2添加到列表中的每个数字。对于2,将2 ^ 2添加到每个其他数字。 对于3,每3个数字加3 ^ 2。

根本不检查是否可分。 最后,你必须检查总和是否是一个完美的正方形,就是这样。 在C ++中,我在58秒内工作。

答案 4 :(得分:1)

抱歉,我不太了解LISP,以便您阅读答案。但我的第一印象是蛮力解决方案的时间成本应该是:

打开括号

sqrt(k)找到k的除数(通过试验除法),每个除数(每个因子的常数时间),并求它们(每个因子的恒定时间)。这是σ 2 (k),我将其称为x。

不确定良好的整数平方根算法的复杂性是什么,但肯定不比sqrt(x)(哑试验乘法)差。 x可能是大于O的大O,所以我在这里保留判断,但是x显然在k ^ 3之上,因为k最多有k个除数,每个除数本身不大于k,因此它的正方形不大于k ^ 2。我的数学学位已经很久了,我不知道Newton-Raphson收敛的速度有多快,但我怀疑它比sqrt(x)更快,如果所有其他方法都失败了,二元斩是log(x)。

关闭括号

乘以n(k范围为1 .. n)。

因此,如果您的算法比O(n * sqrt(n ^ 3))= O(n ^(5/2))更差,则在dumb-sqrt情况下,或者O(n *(sqrt(n)) + log(n ^ 3))= O(n ^ 3/2)在聪明的sqrt情况下,我认为出现了一些错误,应该可以在算法中识别出来。此时我被卡住了因为我不能调试你的LISP。

哦,我假设算术是使用中的数字的常数时间。它应该适用于小到6400万的数字,并且它的立方体很适合64位无符号整数。但即使你的LISP实现使算法比O(1)差,它也不应该比O(log n)差,所以它不会对复杂性产生太大影响。当然不会使它成为超多项式。

这是有人出现并告诉我我有多错误的地方。

哎呀,我只看了你的实际时间数字。它们并不比指数更差。忽略第一个和最后一个值(因为小的时间不能精确测量而你还没有完成),将n乘以10会使时间乘以不超过30-ish。 30是大约10 ^ 1.5,这对于如上所述的强力是正确的。

答案 5 :(得分:0)

我认为你可以用类似筛子的东西来解决这个问题。这只是我的第一印象。

答案 6 :(得分:0)

我已经使用这里的评论中的一些注释重新编写了该程序。 '因素'功能现在效率稍高,我还必须修改σ_(2)(n)函数以接受新输出。

'因素'来自于输出:

$ (factors 10) => (1 2 5 10)

有一个喜欢

$ (factors 10) => ((2 5) (1 10))

修改后的功能如下:

(defun o_2 (n)
"sum of squares of divisors"
  (reduce #'+ (mapcar (lambda (x) (* x x)) (reduce #'append (factors n)))))

在我做了适度的重写之后,我在计算中只保存了大约7秒钟。

看起来我将不得不离开我的屁股并写一个更直接的方法。