这是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绑定。
答案 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)))))