【问题标题】:Efficient cartesian product algorithm ignoring terms忽略项的高效笛卡尔积算法
【发布时间】:2013-10-22 13:36:05
【问题描述】:

假设我有集合 A_1,...A_n, 例如[[a b c][d e][f]]。我想找到这些集合的笛卡尔积,但不包括任何作为某些忽略列表元素超集的术语。

例如,如果我的忽略列表是[[a e][c]],那么笛卡尔积的结果将是[[a d f][b d f][b e f]]。请注意,任何带有c 的术语都没有,[a e f] 也没有。

当然,我可以做到这一点的一种方法是找到完整的笛卡尔积,然后删除有问题的项目,但我想要一种更有效的方法,这样我就可以避免首先检查解决方案。

我有一个初始解决方案,其中涉及逐步构建购物车产品中的每个术语,并且在每个阶段我从 A_i 中删除任何元素,如果将它们添加到我正在构建的术语中会导致它成为任何元素的超集忽略之一。 这工作得很好,并且比简单的解决方案更好,但是仍然有大量的冗余检查,这也取决于集合的呈现顺序。例如。如果[f] 在我的忽略列表中,我仍会继续尝试创建术语,直到达到[f] 然后丢弃。

具体来说,我的 clojure 实现是

(defn first-elements
  "Get the first elements of a set of sets, unless ignored"
  [sets ignores in-ignore?]
  (loop [product-tuple [] sets sets]
    (println "sets " sets)
    (cond
      (or (nil? sets) (nil? (first sets)))
      product-tuple

      :else
      (if-let [set-op (remove #(in-ignore? product-tuple ignores %) (first sets))]
        (if (and (coll? set-op) (empty? set-op))
            product-tuple
            (recur (conj product-tuple (first set-op)) (next sets)))
        product-tuple))))

(defn in-ignore?
  "if I add elem to this build will it become a superset of any of the ignores"
  [build ignores elem]
  (some  #(clojure.set/superset? (conj (set build) elem) %) ignores))

(defn cartesian-product-ignore
  "All the ways to take one item from each sequence, except for ignore"
  [ignores original-sets]
  (loop [cart-prod #{} sets original-sets]
    (let [firsts (first-elements sets ignores in-ignore?)]
      (print "firsts " firsts "-cart-prod " cart-prod " sets " sets "\n")
      (cond
        (zero? (count firsts))
        cart-prod

        (= (count sets) (count firsts))
        (recur (conj cart-prod firsts) (update-in sets [(dec (count sets))] next))

        :else
        (recur cart-prod (assoc
                           (update-in sets [(dec (count firsts))] next)
                           (count firsts)
                           (original-sets (count firsts))))))))

【问题讨论】:

    标签: performance algorithm clojure cartesian-product


    【解决方案1】:

    我认为可以对您当前的方法进行一些改进。但首先,让我们实现一个基本的cartisian-product。然后我们可以调整它以接受忽略列表。这很容易使用for 和一些递归:

    (defn cartesian-product [colls]
      (if (empty? colls)
        (list ())
        (for [e (first colls)
              sub-product (cartesian-product (rest colls))]
          (cons e sub-product))))
    
    ;; Quick test run
    (cartesian-product [[:a :b :c] [:d :e] [:f]])
    => ((:a :d :f) (:a :e :f) (:b :d :f) (:b :e :f) (:c :d :f) (:c :e :f))
    

    很好。而且由于我们使用的是for,因此我们具有懒惰的优势。如果您需要您的结果不是一系列序列,那么将其转换为其他内容很容易。

    现在,最难的部分——实现忽略集。根据您的描述,您当前的方法是从 A_i 中删除元素,如果将它们添加到您正在构建的术语中会导致该术语成为任何忽略集的超集。正如您的代码所示,这不仅效率低下(例如,superset? 是最坏情况下的线性时间,其第一个参数的大小),而且它也使代码比它需要的复杂。

    所以让我们采用不同的方法。不是从 A_i 中删除元素,而是从忽略集中删除我们添加到术语的任何元素。然后,如果任何忽略集为空,我们就可以修剪一个术语。作为奖励,它只需要对我们之前的 cartesian-product 实现进行一些更改:

    (defn cartesian-product-ignore [ignore-sets colls]
      (cond (some empty? ignore-sets) () ; prune
            (empty? colls) (list ())     ; base case
            :else                        ; recursive case
            (for [e (first colls)
                  sub-product (cartesian-product-ignore (map (fn [s]
                                                               (disj s e))
                                                             ignore-sets)
                                                        (rest colls))]
              (cons e sub-product))))
    
    ;; test without any ignore sets
    (cartesian-product-ignore [] [[:a :b :c] [:d :e] [:f]])
    => ((:a :d :f) (:a :e :f) (:b :d :f) (:b :e :f) (:c :d :f) (:c :e :f))
    
    ;; Now the moment of truth
    (cartesian-product-ignore [(set [:a :e]) (set [:c])] [[:a :b :c] [:d :e] [:f]])
    => ((:a :d :f) (:b :d :f) (:b :e :f))
    

    当然,可能需要进行细微的更改才能满足您的确切需求。例如,您可能希望接受忽略集作为向量或序列,并在内部将它们转换为集。但这就是算法的本质..

    【讨论】:

    • 这个算法太棒了!
    • 这似乎并没有解决 OP 的担忧。也就是说,在最坏的情况下,工作量与未修剪的笛卡尔积的大小成正比,这可能是最终答案的大小的许多倍。
    • @n.m,我认为这里提出的最终算法所做的工作应该与最终答案的大小成正比。为什么你认为不是?
    【解决方案2】:

    这里是 core.logic (naive) 方法

    (ns testing
      (:refer-clojure :exclude [==])
      (:use [clojure.core.logic])
    )
    (run* [q]
           (fresh [x y z]
                  (membero x [:a :b :c])
                  (membero y [:d :e])
                  (membero z [:f])
                  (== q [x y z])
                  (!= q [:a :e z] )
                  (!= q [:c y z] )
    
                  )
           )
    
    ==> ([:a :d :f] [:b :d :f] [:b :e :f])
    

    虽然它比 @Nathan_Davis 算法慢得多,但 23.263 毫秒 vs 0.109 毫秒

    【讨论】:

      【解决方案3】:

      【讨论】:

      • 我在clojure.math.combinatorics 中看不到任何允许跳过某些术语的功能。
      • 是的,对不起,我没有正确阅读问题 - 这是 math.combinatorics 中的一个很好的功能
      • 没问题。嘿,作为开源,也许你可以添加它——那么它就是一个正确的答案;-)
      猜你喜欢
      • 2020-05-02
      • 2020-09-26
      • 2015-10-21
      • 2011-02-03
      • 2018-02-03
      • 2011-03-24
      • 2017-03-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多