循环功能耗时太长

时间:2017-07-19 02:48:24

标签: optimization clojure

我正在尝试执行一个实现 n个多维数据集总和的函数

  

1 ^ 3 + 2 ^ 3 + 3 ^ 3 + ... + n ^ 3 =总和

如果sum不存在,我的功能应该会收到n并返回-1n

一些例子:

(find-n 9)   ; should return 2 because 1^3 + 2^3 = 9
(find-n 100) ; should return 4 because 1^3 + 2^3 + 3^3 + 4^3 = 100
(find-n 10)  ; should return -1

经过一番工作,我完成了这两个功能:

; aux function
(defn exp-3 [base] (apply *' (take 3 (repeat base))))

; main function
(defn find-n [m]
  (loop [sum 0
         actual-base 0]
       (if (= sum m) 
           actual-base
           (if (> sum m)
               -1
               (recur (+' sum (exp-3 (inc actual-base))) (inc actual-base))))))

这些功能正常运行,但使用BigNumbers评估操作的时间过长,例如:

(def sum 1025247423603083074023000250000N)
(time (find-n sum))
; => "Elapsed time: 42655.138544 msecs"
; => 45001000

我是在问这个问题,提出一些如何更快地完成这项功能的建议。

4 个答案:

答案 0 :(得分:5)

这完全是关于代数的,与Clojure或编程关系不大。由于本网站不支持数学排版,请在Clojure中表达。

定义

(defn sigma [coll] (reduce + coll))

(defn sigma-1-to-n [f n]
  (sigma (map f (rest (range (inc n))))))

(或

(defn sigma-1-to-n [f n]
  (->> n inc range rest (map f) sigma))

然后问题是ni (= (sigma-1-to-n #(* % % %) i) n)i

对于多维数据集,快速执行此操作的关键是Faulhaber's formula。它告诉我们以下是相同的,对于任何自然数(#(*' % %) (sigma-1-to-n identity i)) (sigma-1-to-n #(* % % %) i) (#(*' % %) (/ (*' i (inc i)) 2))

(defn perfect-square-root [n]
  (let [candidate (-> n double Math/sqrt Math/round)]
    (when (= (*' candidate candidate) n)
      candidate)))

所以,作为立方体的总和,数字

  • 必须是一个完美的广场
  • 其平方根是第一个这么多数字的总和。

为了确定整数是否是一个完美的平方,我们采用它的近似浮点平方根,看看是否平方最接近的整数恢复了我们的整数:

nil

如果参数不是完美的正方形,则返回(j (j + 1)) / 2

现在我们有了平方根,我们必须确定它是否是一系列自然数的总和:在普通代数中,是j,对于某个自然数j (j + 1) = (j + 1/2)^2 + 1/4

我们可以使用类似的技巧直接回答这个问题。

(defn perfect-sum-of [n]
  (let [j (-> n (*' 2)
                (- 1/4)
                double
                Math/sqrt
                (- 0.5)
                Math/round)]
    (when (= (/ (*' j (inc j)) 2) n)
      j)))

因此,以下函数返回与参数相加的连续数字的数量(如果有的话):

(defn find-n [big-i]
  {:pre [(integer? big-i) ((complement neg?) big-i)]}
  (let [sqrt (perfect-square-root big-i)]
    (and sqrt (perfect-sum-of sqrt))))

(def sum 1025247423603083074023000250000N)

(time (find-n sum))
"Elapsed time: 0.043095 msecs"
=> 45001000

我们可以将这些结合起来做你想做的事:

find-n

(请注意,时间比以前快了大约20倍,可能是因为HotSpot必须在double上工作,已通过附加测试进行了彻底的练习)

这显然比原版快很多。

<强>买者

我担心由于浮点的有限精度,上述过程可能会产生漏报(它永远不会产生误报)。但是,测试表明该程序对于问题使用的那种数字是不可破解的。

Java [limit cube-sum]具有52位精度,大约15.6位小数。值得关注的是,如果数字大于此数,则该过程可能会错过精确的整数解,因为舍入只能与它开始的浮点数一样准确。

但是,该过程正确地解决了31位整数的示例。并且使用许多(一千万个!)相似的数字进行测试不会产生一次失败。

为了测试解决方案,我们生成(defn generator [limit cube-sum] (iterate (fn [[l cs]] (let [l (inc l) cs (+' cs (*' l l l))] [limit cs])) [limit cube-sum])) 对的(懒惰)序列:

(take 10 (generator 0 0))
=> ([0 0] [1 1] [2 9] [3 36] [4 100] [5 225] [6 441] [7 784] [8 1296] [9 2025])

例如,

(remove (fn [[l cs]] (= (find-n cs) l)) (take 10000000 (generator 45001000 1025247423603083074023000250000N)))
=> () 

现在我们

  • 从给定示例开始,
  • 尝试接下来的一千万个案例和
  • 删除有效的。

所以

(remove (fn [[l cs]] (= (find-n cs) l)) (take 10 (generator 45001001 1025247423603083074023000250000N)))
=>
([45001001 1025247423603083074023000250000N]
 [45001002 1025247514734170359564546262008N]
 [45001003 1025247605865263720376770289035N]
 [45001004 1025247696996363156459942337099N]
 [45001005 1025247788127468667814332412224N]
 [45001006 1025247879258580254440210520440N]
 [45001007 1025247970389697916337846667783N]
 [45001008 1025248061520821653507510860295N]
 [45001009 1025248152651951465949473104024N]
 [45001010 1025248243783087353664003405024N])

他们都工作。没有失败。只是为了确保我们的测试有效:

await Task.Delay(your seconds);

一切都应该失败,他们会这样做。

答案 1 :(得分:2)

仅仅避开apply(在CLJ中并不是那么快)会给你4倍的加速:

(defn exp-3 [base]
  (*' base base base))

另外10%:

(defn find-n [m]
  (loop [sum 0
         actual-base 0]
    (if (>= sum m)
      (if (= sum m) actual-base -1)
      (let [nb (inc actual-base)]
        (recur (+' sum (*' nb nb nb)) nb)))))

答案 2 :(得分:2)

以下基于算法的方法依赖于一个简单的公式,该公式表示前N个自然数的立方总和为:(N*(N+1)/2)^2

(defn sum-of-cube
  "(n*(n+1)/2)^2"
  [n]
  (let [n' (/ (*' n (inc n)) 2)]
    (*' n' n')))

(defn find-nth-cube
  [n]
  ((fn [start end prev]
     (let [avg (bigint (/ (+' start end) 2))
           cube (sum-of-cube avg)]
       (cond (== cube n) avg
             (== cube prev) -1
             (> cube n) (recur start avg cube)
             (< cube n) (recur avg end cube))))
    1 n -1))

(time (find-nth-cube 1025247423603083074023000250000N))
"Elapsed time: 0.355177 msecs"
=> 45001000N

我们想要找到数字N,使得1..N立方体的总和是某个数字X.要查找是否存在这样的数字,我们可以通过应用上述公式在某个范围内执行二元搜索看看公式的结果是否等于X.这种方法有效,因为顶部的函数正在增加,因此任何值太大的f(n)意味着我们必须寻找较低的数字n,任何值太小的f(n)意味着我们必须寻找更大的数字n

我们选择一个(大于必要但容易且安全的)0到X的范围。如果我们的公式应用于给定的候选数字得到X,我们将知道该数字存在。如果不存在,我们继续二进制搜索,直到我们找到该号码,或直到我们尝试了两次相同的号码,这表示该号码不存在。

上限为logN,计算1E100(1 googol)只需1毫秒,因此对于算法方法非常有效。

答案 3 :(得分:1)

您可能想要使用一些数学技巧。

(a-k)^3 + (a+k)^3 = 2a^3+(6k^2)a

所以,总和如:

(a-4)^3+(a-3)^3+(a-2)^3+(a-1)^3+a^3+(a+1)^3+(a+2)^3+(a+3)^3+(a+4)^3 
= 9a^3+180a

(请确认计算的正确性。)

使用这个等式,你可以每次增加1,而不是你喜欢的任何2 k +1。每当您输入的数字大于 n 时,您都可以检查确切的数字。

其他改进方法是通过进行一次计算一次得到 n sum 的表,稍后在函数find-n中使用此表