找到所有有序三元组的不同正整数i,j和k小于或等于给定整数n,它总和为给定整数s

时间:2013-05-09 23:07:31

标签: clojure sicp

这是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循环无法实现这一点?不确定如何动态添加i j k绑定。

3 个答案:

答案 0 :(得分: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函数的点是产生一个元组的seq,它总结到某个目标值(它的初始目标减去当前迭代中i的值。 for理解);然而,我们仍然验证从递归调用go返回的元组的总和,就好像我们不确定它们是否真的完成了它们的工作一样。

相反,我们可以确保生成的元组是正确的 by

(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

答案 1 :(得分: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))

假设订单在答案中很重要

答案 2 :(得分: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)))

模式是可见的,内部最多的地图被外部mapcat覆盖,依此类推,你想要对嵌套级别进行参数化,因此:

(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)))))