Optimization 循环函数花费的时间太长

Optimization 循环函数花费的时间太长,optimization,clojure,Optimization,Clojure,我正在尝试执行一个函数,该函数实现了n个多维数据集的总和: 1^3+2^3+3^3+…+n^3=总和 我的函数应该接收一个sum,如果n不存在,则返回一个n或-1 一些例子: (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 经过一些工作,我完

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

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

我的函数应该接收一个
sum
,如果
n
不存在,则返回一个
n
-1

一些例子:

(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

我问这个问题是为了提出一些建议,如何使这个函数更快。

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

(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,您可以跳跃9(或任意2k+1)。当你碰到大于n的数字时,你可以检查准确的数字


另一种改进方法是,通过一次批量计算并在函数find-n中稍后使用此表,创建一个包含ns和sum的表。

只需避免
应用
(在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)))))

这都是关于代数的,与Clojure或编程无关。因为这个网站不支持数学排版,让我们用Clojure来表达它

定义

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

(或

)

然后问题是,给定
n
,找到
i
,这样
(=(sigma-1-to-n#(*%%)i)n)

快速实现这一点的关键在于立方体。它告诉我们,对于任何自然数
i
,以下各项是相等的:

(#(*' % %) (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 + 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
(请注意,时间大约比以前快了20倍,这可能是因为HotSpot必须进行
find-n
,这已经通过附加的测试得到了彻底的验证)

这显然比原来的要快得多


警告

我担心由于浮点精度有限,上述过程可能会产生假阴性(它永远不会产生假阳性)。然而,测试表明,对于问题使用的数字类型,该程序是不可破解的


Java
double
有52位精度,大约15.6位小数。令人担忧的是,对于比这个大得多的数字,该过程可能会错过精确的整数解,因为舍入只能与它开始时的浮点数一样精确

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


为了测试该解决方案,我们生成了一个(惰性)的
[limit cube sum]
对序列:

(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 10 (generator 45001001 1025247423603083074023000250000N)))
=>
([45001001 1025247423603083074023000250000N]
 [45001002 1025247514734170359564546262008N]
 [45001003 1025247605865263720376770289035N]
 [45001004 1025247696996363156459942337099N]
 [45001005 1025247788127468667814332412224N]
 [45001006 1025247879258580254440210520440N]
 [45001007 1025247970389697916337846667783N]
 [45001008 1025248061520821653507510860295N]
 [45001009 1025248152651951465949473104024N]
 [45001010 1025248243783087353664003405024N])

一切都应该失败,他们确实失败了

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

(定义多维数据集的和
“(n*(n+1)/2)^2”
[n]
(让[n'(/(*'n(inc n))2]
(*‘n’n’))
(定义查找第n个立方体)
[n]
((fn[开始-结束前]
(let[avg(bigint(/(+'开始-结束)2))
立方体(立方体平均值之和)]
(条件(=立方n)平均值
(==多维数据集上一页)-1
(>多维数据集n)(重复开始平均多维数据集)
(45001000N
我们想找到一个数N,这样1..N个立方体的和就是某个数X。要找到这样一个数是否存在,我们可以通过应用上面的公式在某个范围内对它进行二元搜索,看看公式的结果是否等于X。这种方法是有效的,因为顶部的函数是递增的,因此任何值
f(N)
太大意味着我们必须寻找一个较小的数字
n
,而任何
f(n)
太小的值意味着我们必须寻找一个较大的数字
n

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


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

在Clojure中,如果不存在某个值,则返回
nil
。我对该值的有效性表示怀疑。你可能会更安全。看起来很健壮。我不明白为什么,但我不能打破它。我会在答案的最后扔掉证据。我没有意识到
apply
这么慢。。。我将使用
apply仔细考虑(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])
(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