【问题标题】:Cartesian product in clojureclojure 中的笛卡尔积
【发布时间】:2013-08-17 06:43:28
【问题描述】:

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

这是我目前所拥有的:

(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 和函数式编程还很陌生,所以我可能走在完全错误的轨道上。请帮忙! :)

【问题讨论】:

标签: clojure functional-programming cartesian-product


【解决方案1】:

这比手动计算递归要容易得多:

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

【讨论】:

  • 我理解 for 不是 LISP/Clojure 的“精神”?这很重要吗?
  • IMO 这种for 的使用绝对是惯用的。
  • @SamJarman 有什么反对意见?注意(for [x coll1, y coll2] (f x y)) ~ (mapcat (fn [x] (map (fn [y] (f x y)) coll2)) coll1)。您可以将其翻译为使用mapconcat,但我认为这不太清楚。
  • 也许你在想像 C 这样的语言叫for。 Clojure 的 for 是 100% 在精神 clojure 中。
  • 嗨!我刚读到这个,我想知道是否有人知道,这个超短函数和相应的实现 math.combinatorics.cartesian-product 有什么区别。这篇文章的实现是 100% 懒惰的,所以这不可能有区别。也看看这篇文章:stackoverflow.com/questions/35874072/…
【解决方案2】:

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

(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】:

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

    (defn cart
      ([] '(()))
      ([xs & more]
        (mapcat #(map (partial cons %)
                      (apply cart more))
                xs)))
    

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

    第二,只有两种情况。我们可以在一个空列表上很好地调用该函数,所以我们关心的是空与非空。

    第三,正如 amalloy 解释的那样,(cart) 的正确值是'(())。这实际上是相当微妙的,当我编写这样的函数时,我确实把它搞砸了。如果您非常仔细地浏览一个简单的案例,您应该能够看到为什么该值使递归起作用。

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

    第五,一些风格说明。最大的问题是缩进。这里最好的建议是找到一个自动缩进 lisp 代码的编辑器。自动缩进是编辑器提供的最重要的东西之一,因为当你的括号不匹配时,它会变得非常明显。此外,关闭括号永远不会单独使用,fns 不需要内部名称,除非您计划递归,而且我通常比您有更多的换行符。我喜欢认为我上面的代码风格相当体面,作为另一个例子,我将如何格式化您的代码:

    (defn cart
      ([] '())
      ([l1] (map list l1))
      ([l1 l2] 
        (map (fn [x]
               (map (fn [y]
                      (list x y))
                    l2))
             l1)))
    
    (defn cartesian-product [& lists] 
      (reduce cart lists))
    

    【讨论】:

      【解决方案4】:

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

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

      【讨论】:

        【解决方案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
        

        【讨论】:

        • +1 用于显示for 如何对预定数量的列表进行笛卡尔积。这是这个问题的几个答案的基础。
        【解决方案6】:

        对于大多数用途来说,艾伦的回答很好,因为你得到了一个惰性理解,并且一个惰性序列在你意识到它的成员时不会导致堆栈溢出,即使你不使用 (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)))))
        

        【讨论】:

          猜你喜欢
          • 2011-02-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-04-07
          • 2011-01-29
          • 2019-04-20
          • 2015-03-27
          相关资源
          最近更新 更多