1

这是 SICP 中的练习 2.41 我自己写了这个幼稚的版本:

(defn sum-three [n s]
  (for [i (range n)
        j (range n)
        k (range n)
        :when (and (= s (+ i j k))
                   (< 1 k j i n))]
    [i j k]))

问题是:这在clojure中被认为是惯用的吗?以及如何优化这段代码?因为计算需要很长时间(sum-three 500 500)

另外,我怎样才能让这个函数接受一个额外的参数来指定整数个数来计算总和?因此,它应该处理更一般的情况,而不是三的总和,如二的总和、四的总和或五的总和等。

我想这不能通过使用for循环来实现?不确定如何动态添加 ijk 绑定。

4

3 回答 3

3

(更新:完全优化的版本sum-c-opt在底部。)

我会说这是惯用的,如果不是在保持惯用语的同时做到这一点的最快方法的话。好吧,也许在输入已知为数字时使用==代替会更惯用(注意。这些在数字上并不完全等价;不过在这里没关系。)=

作为第一个优化过程,您可以从更高的范围开始并替换=为特定于数字的==

(defn sum-three [n s]
  (for [k (range n)
        j (range (inc k) n)
        i (range (inc j) n)
        :when (== s (+ i j k))]
    [i j k]))

(更改了绑定的顺序,因为您希望最后的数字最小。)

至于将整数的数量作为参数,这是一种方法:

(defn sum-c [c n s]
  (letfn [(go [c n s b]
            (if (zero? c)
            [[]]
            (for [i  (range b n)
                  is (go (dec c) n (- s i) (inc i))
                  :when (== s (apply + i is))]
              (conj is i))))]
    (go c n s 0)))

;; from the REPL:
user=> (sum-c 3 6 10)
([5 4 1] [5 3 2])
user=> (sum-c 3 7 10)
([6 4 0] [6 3 1] [5 4 1] [5 3 2])

更新:宁可破坏使用它的练习,但math.combinatorics提供了一个combinations为解决这个问题而量身定制的函数:

(require '[clojure.math.combinatorics :as c])

(c/combinations (range 10) 3)
;=> all combinations of 3 distinct numbers less than 10;
;   will be returned as lists, but in fact will also be distinct
;   as sets, so no (0 1 2) / (2 1 0) "duplicates modulo ordering";
;   it also so happens that the individual lists will maintain the
;   relative ordering of elements from the input, although the docs
;   don't guarantee this

filter输出适当。

进一步更新:通过sum-c上述工作方式的思考给出了进一步的优化想法。内部go函数的重点sum-c是生成一个元组序列,总和为某个目标值(其初始目标减去i当前迭代中的值for);然而,我们仍然验证从递归调用返回的元组的总和,就go好像我们不确定它们是否真的完成了他们的工作一样。

相反,我们可以通过构造确保生成的元组是正确的:

(defn sum-c-opt [c n s]
  (let [m (max 0 (- s (* (dec c) (dec n))))]
    (if (>= m n)
      ()
      (letfn [(go [c s t]
                (if (zero? c)
                  (list t)
                  (mapcat #(go (dec c) (- s %) (conj t %))
                          (range (max (inc (peek t))
                                      (- s (* (dec c) (dec n)))) 
                                 (min n (inc s))))))]
        (mapcat #(go (dec c) (- s %) (list %)) (range m n))))))

此版本将元组作为列表返回,以便保留预期的结果顺序,同时保持代码结构,这在这种方法下是很自然的。您可以通过传递将它们转换为向量map vec

对于较小的参数值,这实际上会比 慢sum-c,但对于较大的值,它要快得多:

user> (time (last (sum-c-opt 3 500 500)))
"Elapsed time: 88.110716 msecs"
(168 167 165)
user> (time (last (sum-c 3 500 500)))
"Elapsed time: 13792.312323 msecs"
[168 167 165]

并且只是为了进一步保证它做同样的事情(除了在两种情况下归纳证明正确性之外):

; NB. this illustrates Clojure's notion of equality as applied
;     to vectors and lists
user> (= (sum-c 3 100 100) (sum-c-opt 3 100 100))
true
user> (= (sum-c 4 50 50) (sum-c-opt 4 50 50))
true
于 2013-05-09T23:18:57.830 回答
1

for 是一个宏,因此很难扩展您的惯用答案以涵盖一般情况。幸运的是,clojure.math.combinatorics 提供了笛卡尔积函数,它将产生数字集的所有组合。这减少了过滤组合的问题:

(ns hello.core
  (:require [clojure.math.combinatorics :as combo]))

(defn sum-three [n s i]
  (filter #(= s (reduce + %))
          (apply combo/cartesian-product (repeat i (range 1 (inc n)))))) 

hello.core> (sum-three 7 10 3)
((1 2 7) (1 3 6) (1 4 5) (1 5 4) (1 6 3) (1 7 2) (2 1 7) 
 (2 2 6) (2 3 5) (2 4 4) (2 5 3) (2 6 2) (2 7 1) (3 1 6) 
 (3 2 5) (3 3 4) (3 4 3) (3 5 2) (3 6 1) (4 1 5) (4 2 4) 
 (4 3 3) (4 4 2) (4 5 1) (5 1 4) (5 2 3) (5 3 2) (5 4 1) 
 (6 1 3) (6 2 2) (6 3 1) (7 1 2) (7 2 1))

假设顺序在答案中很重要

于 2013-05-10T00:11:09.647 回答
1

为了使您现有的代码参数化,您可以使用reduce。此代码显示了一种模式,可以在您希望参数化for宏使用情况的情况下使用该模式。

不使用for宏(仅使用函数)的代码将是:

(defn sum-three [n s]
  (mapcat (fn [i]
            (mapcat (fn [j]
                      (filter (fn [[i j k]]
                                (and (= s (+ i j k))
                                     (< 1 k j i n)))
                              (map (fn [k] [i j k]) (range n))))
                    (range n)))
          (range n)))

该模式是可见的,最里面的地图被外部地图猫等覆盖,您想要参数化嵌套级别,因此:

(defn sum-c [c n s]
  ((reduce (fn [s _]
             (fn [& i] (mapcat #(apply s (concat i [%])) (range n))))
           (fn [& i] (filter #(and (= s (apply + %))
                                  (apply < 1 (reverse %)))
                            (map #(concat i [%]) (range n))))
           (range (dec c)))))
于 2013-05-10T08:45:47.360 回答