咒语中的笛卡尔积

时间:2013-08-15 04:55:10

标签: clojure functional-programming cartesian-product

我正在尝试实现一个方法,该方法将获取列表列表并返回这些列表的笛卡尔积。

这是我到目前为止所拥有的:

(defn cart


([] '())
 ([l1] (map list l1))
 ([l1 l2] 
  (map 
    (fn f[x] (map
    (fn g [y] (list x y))
    l2))
      l1)
      )

)

(defn cartesian-product [& lists] 
      (reduce cart lists)

 )





;test cases 
(println (cartesian-product '(a b) '(c d))) ; ((a c) (a d) (b c) (b d))
(println (cartesian-product ())) ;()
(println (cartesian-product '(0 1)))    ; ((0) (1))
(println (cartesian-product '(0 1) '(0 1))) ; ((0 0) (0 1) (1 0) (1 1))
(println (apply cartesian-product (take 4 (repeat (range 2))))) ;((0 0 0 0) (0 0 0 1) (0 0 1 0) (0 0 1 1) (0 1 0 0) (0 1 0 1) (0 1 1 0) (0 1 1 1) (1 0 0 0) (1 0 0 1) (1 0 1 0) (1 0 1 1) (1 1 0 0) (1 1 0 1) (1 1 1 0) (1 1 1 1))

问题是我的解决方案真的很“安全”。

(((a c) (a d)) ((b c) (b d)))
()
(0 1)
(((0 0) (0 1)) ((1 0) (1 1)))
(((((((0 0) (0 1)) 0) (((0 0) (0 1)) 1)) 0) (((((0 0) (0 1)) 0) (((0 0) (0 1)) 1)) 1)) ((((((1 0) (1 1)) 0) (((1 0) (1 1)) 1)) 0) (((((1 0) (1 1)) 0) (((1 0) (1 1)) 1)) 1)))

我尝试添加

      (apply concat(reduce cart lists))

然后我就这样崩溃了:

((a c) (a d) (b c) (b d))
()
IllegalArgumentException Don't know how to create ISeq from: java.lang.Long clojure.lang.RT.seqFrom (RT.java:494)

所以,我认为我很接近但却缺少一些东西。然而,由于我对clojure和函数式编程这么新,我可能会走上完全错误的轨道。请帮忙! :)

6 个答案:

答案 0 :(得分:21)

作为一种理解,这比通过手动计算递归更容易:

(defn cart [colls]
  (if (empty? colls)
    '(())
    (for [more (cart (rest colls))
          x (first colls)]
      (cons x more))))

user> (cart '((a b c) (1 2 3) (black white)))
((a 1 black) (a 1 white) (a 2 black) (a 2 white) (a 3 black) (a 3 white) 
 (b 1 black) (b 1 white) (b 2 black) (b 2 white) (b 3 black) (b 3 white) 
 (c 1 black) (c 1 white) (c 2 black) (c 2 white) (c 3 black) (c 3 white))

基本情况很明显(它需要是一个包含空列表的列表,而不是空列表本身,因为采用无列表的笛卡尔积的一种方法)。在递归的情况下,您只需迭代第一个集合的每个元素x,然后遍历其余列表的每个笛卡尔积,前面加上您选择的x

请注意,以这种略微不自然的顺序编写for理解的两个子句很重要:交换它们会导致显着减速。这样做的原因是为了避免重复工作。对于第一个绑定中的每个项目,将对第二个绑定的主体进行一次评估,如果您以错误的顺序编写了条款,则意味着昂贵的递归子句的许多浪费副本。如果你想要格外小心,你可以明确表示这两个条款是独立的,而是写下:

(let [c1 (first colls)]
  (for [more (cart (rest colls))
        x c1]
    (cons x more)))

答案 1 :(得分:8)

我会检查https://github.com/clojure/math.combinatorics它有

(组合/笛卡尔积[1 2] [3 4]) ;; => ((1 3)(1 4)(2 3)(2 4))

答案 2 :(得分:7)

为了比较,本着原始的精神

(defn cart 
  ([xs] 
   xs) 
  ([xs ys] 
   (mapcat (fn [x] (map (fn [y] (list x y)) ys)) xs)) 
  ([xs ys & more] 
   (mapcat (fn [x] (map (fn [z] (cons x z)) (apply cart (cons ys more)))) xs)))

(cart '(a b c) '(d e f) '(g h i))
;=> ((a d g) (a d h) (a d i) (a e g) (a e h) (a e i) (a f g) (a f h) (a f i)
;    (b d g) (b d h) (b d i) (b e g) (b e h) (b e i) (b f g) (b f h) (b f i) 
;    (c d g) (c d h) (c d i) (c e g) (c e h) (c e i) (c f g) (c f h) (c f i))

答案 3 :(得分:5)

我知道我迟到了 - 为了完整起见,我只是想添加一种不同的方法。

与amalloy的方法相比,它也是懒惰的(虽然参数列表是热切评估的),并且在需要所有结果时稍微快一些(我用下面的演示代码测试它们),但它容易出现堆栈溢出(随着列表数量的增加,它更像是它生成和评估的基础for理解力。另请注意,eval对可传递给代码的大小有限制。

首先考虑问题的一个实例:您想要找到[:a :b :c]'(1 2 3)的笛卡尔积。显而易见的解决方案是使用for理解,如下所示:

(for [e1 [:a :b :c]
      e2 '(1 2 3)]
  (list e1 e2))

; ((:a 1) (:a 2) (:a 3) (:b 1) (:b 2) (:b 3) (:c 1) (:c 2) (:c 3))

现在,问题是:是否有可能以适用于任意数量的列表的方式对此进行概括?这里的答案是肯定的。这就是以下宏的作用:

(defmacro cart [& lists]
  (let [syms (for [_ lists] (gensym))]
    `(for [~@(mapcat list syms lists)]
       (list ~@syms))))

(macroexpand-1 '(cart [:a :b :c] '(1 2 3)))

; (clojure.core/for [G__4356 [:a :b :c] 
;                    G__4357 (quote (1 2 3))] 
;   (clojure.core/list G__4356 G__4357))

(cart [:a :b :c] '(1 2 3))

; ((:a 1) (:a 2) (:a 3) (:b 1) (:b 2) (:b 3) (:c 1) (:c 2) (:c 3))

基本上,您可以让编译器为您生成适当的for理解。将其转换为函数非常简单,但有一个小问题:

(defn cart [& lists]
  (let [syms (for [_ lists] (gensym))]
    (eval `(for [~@(mapcat #(list %1 `'~%2) syms lists)]
             (list ~@syms)))))

(cart [:a :b :c] '(1 2 3))

; ((:a 1) (:a 2) (:a 3) (:b 1) (:b 2) (:b 3) (:c 1) (:c 2) (:c 3))

未加引号的列表被视为函数调用,这就是为什么在这里引用%2的原因。

Online Demo

; https://projecteuler.net/problem=205

(defn cart [& lists]
  (let [syms (for [_ lists] (gensym))]
    (eval `(for [~@(mapcat #(list %1 `'~%2) syms lists)]
             (list ~@syms)))))

(defn project-euler-205 []

  (let [rolls (fn [n d]
                (->> (range 1 (inc d))
                  (repeat n)
                  (apply cart)
                  (map #(apply + %))
                  frequencies))

        peter-rolls (rolls 9 4)
        colin-rolls (rolls 6 6)

        all-results (* (apply + (vals peter-rolls))
                       (apply + (vals colin-rolls)))

        peter-wins (apply + (for [[pk pv] peter-rolls
                                  [ck cv] colin-rolls
                                  :when (> pk ck)]
                              (* pv cv)))]

    (/ peter-wins all-results)))

(println (project-euler-205)) ; 48679795/84934656

答案 4 :(得分:4)

就个人而言,我会使用amalloy的for解决方案。我的一般经验法则是,如果我的循环可以表示为带有简单函数参数的单个map / filter / etc调用(所以函数名称或短fn / {{ 1}}形式),它更好地使用该功能。一旦它变得更复杂,#()表达式就更容易阅读。特别是,for远比嵌套地图好。也就是说,如果我在这里没有使用for,那就是我写这个函数的方式:

for

注意事项:首先,不需要减少。递归可以很好地处理它。

第二,只有两个案例。我们可以在空列表上调用该函数,所以我们关心的是空的非空的。

第三,正如合金所解释的那样,(defn cart ([] '(())) ([xs & more] (mapcat #(map (partial cons %) (apply cart more)) xs))) 的正确值是(cart)。这实际上相当微妙,当我编写这样的函数时,我可靠地搞砸了。如果您非常仔细地浏览一个简单的案例,您应该能够看到为什么该值使递归工作。

第四,我一般不喜欢使用'(())。这更多是个人偏好,但如果我可以逃脱,我总是使用fn#()partialcomp对于较小的函数来说绝对是惯用的,尽管其他两个函数不太常见。

第五,一些风格笔记。最大的问题是缩进。这里最好的建议是找到一个自动缩进lisp代码的编辑器。自动缩进是编辑器提供的最重要的事情之一,因为当你的parens不匹配时它会让人眼花缭乱。此外,关闭parens永远不会自行,#() s不需要内部名称,除非你计划递归,而且我通常还有一些比你更新的新行。我喜欢认为我上面的代码设计得相当合理,而另一个例子,这就是我如何格式化你的代码:

fn

答案 5 :(得分:0)

在大多数情况下,Alan的答案很棒,因为你得到一个懒惰的理解,懒惰的seq不会导致堆栈溢出,因为你意识到它的成员,即使你不使用(recur)。

我有兴趣尝试用明确的复发来制作尾递归版本,其中最重要的是因为懒惰在我的应用程序中没有任何帮助,但也有趣和嘻嘻:

(defn cartesian-product
  ([cols] (cartesian-product '([]) cols))
  ([samples cols]
    (if (empty? cols)
      samples
      (recur (mapcat #(for [item (first cols)]
                        (conj % item)) samples)
             (rest cols)))))