【问题标题】:Clojure - Sliding Window Minimum in Log TimeClojure - 日志时间中的滑动窗口最小值
【发布时间】:2017-05-22 10:16:41
【问题描述】:

给定向量大小 n 和窗口大小 k,如何在 n log k 时间内有效地计算滑动窗口最小值?即,对于向量 [1 4 3 2 5 4 2] 和窗口大小 2,输出将是 [1 3 2 2 4 2]。

显然我可以使用分区和映射来做到这一点,但那是 n * k 时间。

我认为我需要跟踪已排序地图中的最小值,并在地图位于窗口外时对其进行更新。但是,虽然我可以在日志时间内获得排序地图的最小值,但在地图中搜索以查找任何过期的索引并不是记录时间。

谢谢。

【问题讨论】:

  • 这似乎更像是一个算法问题,而不是 Clojure 问题。如果您有一个算法,有人可以帮助您在 Clojure 中实现它。但事实上,这有点像“帮我解决面试问题”之类的事情。

标签: clojure sliding-window


【解决方案1】:

您可以在线性时间内求解 --O(n),而不是 O(n*log k)),如 1.http://articles.leetcode.com/sliding-window-maximum/(很容易从查找最大值更改为查找最小值)和 2.https://people.cs.uct.ac.za/~ksmith/articles/sliding_window_minimum.html 所述

这些方法需要一个双端队列来管理以前的值,大多数队列操作(即推送/弹出/窥视等)使用 O(1) 时间,而不是使用优先级队列(即优先级)时的 O(log K)地图)。我使用了来自https://github.com/pjstadig/deque-clojure的双端队列

实现上述第一个参考中的代码的主代码(用于最小值而不是最大值):

(defn windowed-min-queue [w a]
  (let [
        deque-init (fn deque-init [] (reduce (fn [dq i]
                                               (dq-push-back i (prune-back a i dq)))
                                             empty-deque (range w)))

        process-min (fn process-min [dq] (reductions (fn [q i]
                                                       (->> q
                                                            (prune-back a i)
                                                            (prune-front i w)
                                                            (dq-push-back i)))
                                                     dq (range w (count a))))
        init (deque-init)
        result (process-min init)] ;(process-min init)]
    (map #(nth a (dq-front %)) result)))

将此方法的速度与我们拥有的使用优先级图的其他解决方案进行比较(注意:我也喜欢其他解决方案,因为它更简单)。

; Test using Random arrays of data
(def N 1000000)
(def a (into [] (take N (repeatedly #(rand-int 50)))))
(def b (into [] (take N (repeatedly #(rand-int 50)))))
(def w 1024)
; Solution based upon Priority Map (see other solution which is also great since its simpler)
(time (doall (windowed-min-queue w a)))
;=> "Elapsed time: 1820.526521 msecs"
; Solution based upon  double-ended queue
(time (doall (windowed-min w b)))
;=> "Elapsed time: 8290.671121 msecs"

这快了 4 倍以上,考虑到 PriorityMap 是用 Java 编写的,而双端队列代码是纯 Clojure 代码,这非常棒(请参阅 https://github.com/pjstadig/deque-clojure

包括用于双端队列的其他包装器/实用程序以供参考。

(defn dq-push-front [e dq]
  (conj dq e))

(defn dq-push-back [e dq]
  (proto/inject dq e))

(defn dq-front [dq]
  (first dq))

(defn dq-pop-front [dq]
  (pop dq))

(defn dq-pop-back [dq]
  (proto/eject dq))

(defn deque-empty? [dq]
  (identical? empty-deque dq))

(defn dq-back [dq]
  (proto/last dq))

(defn dq-front [dq]
  (first dq))

(defn prune-back [a i dq]
  (cond
    (deque-empty? dq) dq
    (< (nth a i) (nth a (dq-back dq))) (recur a i (dq-pop-back dq))
    :else dq))

(defn prune-front [i w dq]
  (cond
    (deque-empty? dq) dq
    (<= (dq-front dq) (- i w)) (recur i w (dq-pop-front dq))
    :else dq))

【讨论】:

    【解决方案2】:

    我的解决方案使用两个辅助映射来实现快速性能。我将键映射到它们的值,并将值存储到它们在排序映射中出现的位置。每次移动窗口时,我都会更新地图,并在日志时间内获取排序地图的最小值。

    缺点是代码更丑陋,不懒惰,也不惯用。好处是它的性能比优先级映射解决方案高出约 2 倍。不过,我认为这在很大程度上可以归咎于上述解决方案的懒惰。

    (defn- init-aux-maps [w v]
      (let [sv (subvec v 0 w)
            km (->> sv (map-indexed vector) (into (sorted-map)))
            vm (->> sv frequencies (into (sorted-map)))]
        [km vm]))
    
    (defn- update-aux-maps [[km vm] j x]
      (let [[ai av] (first km)
            km (-> km (dissoc ai) (assoc j x))
            vm (if (= (vm av) 1) (dissoc vm av) (update vm av dec))
            vm (if (nil? (get vm x)) (assoc vm x 1) (update vm x inc))]
        [km vm]))
    
    (defn- get-minimum [[_ vm]] (ffirst vm))
    
    (defn sliding-minimum [w v]
      (loop [i 0, j w, am (init-aux-maps w v), acc []]
        (let [acc (conj acc (get-minimum am))]
          (if (< j (count v))
            (recur (inc i) (inc j) (update-aux-maps am j (v j)) acc)
            acc))))
    

    【讨论】:

      【解决方案3】:

      您可以使用基于Clojure's priority map 数据结构的优先级队列来解决此问题。我们用它们在向量中的位置来索引窗口中的值。

      • 它的第一个条目的值是窗口最小值。
      • 我们添加新条目并通过键/向量位置删除最旧的条目。

      一个可能的实现是

      (use [clojure.data.priority-map :only [priority-map]])
      
      (defn windowed-min [k coll]
        (let [numbered (map-indexed list coll)
              [head tail] (split-at k numbered)
              init-win (into (priority-map) head)
              win-seq (reductions
                        (fn [w [i n]]
                          (-> w (dissoc (- i k)) (assoc i n)))
                        init-win
                        tail)]
          (map (comp val first) win-seq)))
      

      例如,

      (windowed-min 2 [1 4 3 2 5 4 2])
      => (1 3 2 2 4 2)
      

      该解决方案是惰性开发的,因此可以应用于无限序列。


      在初始化之后,即 O(k),函数在 O(log k) 时间内计算序列中的每个元素,如 here 所述。

      【讨论】:

      • “初始化后,O(k) […]”值得一票。
      • 非常感谢,这是一个很好的解决方案。我最终拼凑出一个我自己的运行速度更快的解决方案,但我认为这是因为它非常渴望。
      • 初始化也是 O(k*log k),因为 (into (priority-map) head) 等于 (reduce conj (priority-map) head),即William's method。优先级映射似乎没有提供Floyd's method,这将是 O(k) 来构建优先级映射。
      猜你喜欢
      • 2012-06-03
      • 2012-05-30
      • 2019-12-04
      • 2017-04-03
      • 1970-01-01
      • 1970-01-01
      • 2011-05-06
      • 2019-08-21
      • 2013-01-17
      相关资源
      最近更新 更多