如何将该算法从Ruby转换为Clojure?

时间:2019-01-26 21:49:30

标签: ruby algorithm recursion clojure

我一直在解决Ruby中的编程任务,完成任务后,我认为我应该尝试运用有限的Clojure知识来实现​​同一件事。我花了很多时间,但没有设法使它工作。

任务是:我们有硬币类型和期望金额的数组。用我们有能力花费最少数量硬币的硬币类型来表示该总和的最佳方法是什么。因此,对于用硬币类型[100,50,20,10,5]表示325,我们将产生以下结果:[100,100,100,20,5]。

这是我的Ruby代码,似乎可以正常工作:

  def calc(coin_types, expected)
    calc_iter(coin_types.sort.reverse, expected, [])
  end

  def calc_iter(coin_types, expected, coins)
    sum = coins.sum

    return coins if sum == expected
    return nil if sum > expected

    coin_types.each do |type|
      result = calc_iter(coin_types, expected, coins + [type])
      return result if result
    end

    nil
  end

  # test
  calc([25, 10], 65) # should return [25, 10, 10, 10, 10]

现在有两个我失败的Clojure实现:

1)(它需要永远运行,所以我不得不杀死它):

  (defn calc [types expected]
    (let [types (reverse (sort types))]
      (loop [coins []]
        (let [sum (count coins)]
          (if (= sum expected)
            coins
            (if (> sum expected)
              nil
              (first (filter #(not (nil? %))
                             (map #(recur (cons % coins))
                                  types)))))))))

2)(此操作确实在合理的时间内完成了,但返回了错误的结果):

  (defn calc-iter [types expected coins]
    (let [sum (count coins)]
      (if (= sum expected)
        coins
        (if (> sum expected)
          nil
          (first (filter #(not (nil? %))
                         (map #(calc-iter types
                                          expected
                                          (cons % coins))
                              types)))))))

  (defn calc [types expected]
    (calc-iter (reverse (sort types))
               expected
               []))

3 个答案:

答案 0 :(得分:3)

这是一个简单的例子:

(def coin-values [100, 50, 20, 10, 5])

(defn coins-for-amount [amount]
  (loop [amount-remaining amount
         coins-avail      coin-values
         coins-used       []]
    (cond
      (zero? amount-remaining) coins-used ; success
      (empty? coins-avail) nil ; ran out of coin types w/o finding answer
      :else (let [coin-val              (first coins-avail)
                  num-coins             (quot amount-remaining coin-val)
                  curr-amount           (* coin-val num-coins)
                  amount-remaining-next (- amount-remaining curr-amount)
                  coins-avail-next      (rest coins-avail)
                  coins-used-next       (conj coins-used num-coins)]
              (recur amount-remaining-next coins-avail-next coins-used-next)))))


(coins-for-amount 325) => [3 0 1 0 1]
(coins-for-amount 326) => nil
(coins-for-amount 360) => [3 1 0 1]

请注意,在当前形式下,它不会累积尾随零。


更新

在上面的原始答案中,我从未考虑过可能会选择像[25 10]这样的棘手硬币值,因此您需要四分之一硬币和四角硬币才能达到$ 0.65的总和。上面的算法将选择2个季度,然后停留在0.15美元的剩余位置,并且只能使用一角硬币。

如果允许使用棘手的硬币值,则需要使用详尽的搜索算法。这是Clojure的一个版本:

(ns tst.demo.core
  (:use tupelo.core demo.core tupelo.test))

(defn total-amount [coins-used]
  (let [amounts (mapv (fn [[coin-value num-coins]] (* coin-value num-coins))
                  coins-used)
        total   (reduce + amounts)]
    total))

(defn coins-for-amount-impl
  [coins-used coin-values amount-remaining]
  (when-not (empty? coin-values)
    (let [curr-coin-value       (first coin-values)
          coin-values-remaining (rest coin-values)
          max-coins             (quot amount-remaining curr-coin-value)]
      (vec (for [curr-num-coins (range (inc max-coins))]
             (let [coins-used-new       (conj coins-used {curr-coin-value curr-num-coins})
                   amount-remaining-new (- amount-remaining (* curr-coin-value curr-num-coins))]
               (if (zero? amount-remaining-new)
                 coins-used-new
                 (coins-for-amount-impl
                   coins-used-new coin-values-remaining amount-remaining-new))))))))

(defn coins-for-amount [coin-values amount]
  (remove nil?
    (flatten
      (coins-for-amount-impl {} coin-values amount))))

以及一些简短的单元测试:

(dotest
  (is= 48 (total-amount {25 1    ; quarter
                         10 2    ; dime
                         1  3})) ; penny


  (let [results (coins-for-amount [10 5 1], 17)]
    (is= results
      [{10 0, 5 0, 1 17}
       {10 0, 5 1, 1 12}
       {10 0, 5 2, 1 7}
       {10 0, 5 3, 1 2}
       {10 1, 5 0, 1 7}
       {10 1, 5 1, 1 2}]))

  (is= (coins-for-amount [25 10], 65)
    [{25 1, 10 4}] ))

因此,它将找到达到正确总数的所有可能组合。算数的工作和找到最少硬币(不要忘记领带!)的解决方案留给读者作为练习。 ;)

答案 1 :(得分:2)

这是一个很酷的问题,可以通过逻辑编程和Clojure的core.logic解决。

首先定义一个递归目标productsumo,该目标采用一系列新的逻辑变量,一组面额和一个我们想要达到的总和。当目标实现时,vars中的那些新鲜逻辑变量将等于每种面额的硬币数量。

  • varsdens绑定 head tail 变量。在每次递归中,我们只会求解 head 变量,并在 tail 上递归,直到耗尽vars。
  • 使用有限域名称空间中的算术函数来约束新的新鲜变量productrun-sum,以便将每个面额乘以某个数字,然后将其加到一个运行总和上最终达到我们想要的总和(或不达到该总和) )
(require '[clojure.core.logic.fd :as fd])

(defn productsumo [vars dens sum]
  (fresh [vhead vtail dhead dtail product run-sum]
    (conde
      [(emptyo vars) (== sum 0)]
      [(conso vhead vtail vars)
       (conso dhead dtail dens)
       (fd/* vhead dhead product)
       (fd/+ product run-sum sum)
       (productsumo vtail dtail run-sum)])))

然后使用该目标,告知目标数量和面额,并将每个面额的计数压缩到包含面额的地图中:

(defn change [amount denoms]
  (let [dens (sort > denoms)
        vars (repeatedly (count dens) lvar)]
    (run* [q]
      (== q (zipmap dens vars))
      ;; prune problem space: must be 0 <= n <= amount
      (everyg #(fd/in % (fd/interval 0 amount)) vars)
      (productsumo vars dens amount))))

您可以通过致电change并获取数量和面额来获得所有解决方案:

(change 325 [100 50 20 10 5])
=>
({100 0, 50 0, 20 0, 10 0, 5 65}
 {100 1, 50 0, 20 0, 10 0, 5 45}
 {100 0, 50 1, 20 0, 10 0, 5 55}
 ...)

每个解决方案都是从硬币面额到用于解决问题的硬币数量的映射。

要找到硬币数量最少的解决方案,可以sort-by在每个地图中的值/计数。要返回硬币值列表,可以按值repeat键。

(->> (change 325 [100 50 20 10 5])
     (sort-by #(apply + (vals %)))
     (first)
     (mapcat #(repeat (val %) (key %))))
=> (100 100 100 20 5)

答案 2 :(得分:1)

我喜欢Taylor's answer,但作为Ruby的直接翻译,这是我的版本:

(defn calc-iter [coin-types expected coins]
  (let [sum (reduce + coins)]
    (cond
      (= expected sum) coins
      (< expected sum) nil
      :else (->> coin-types
                (keep #(calc-iter coin-types expected (cons % coins)))
                first))))

(defn calc [coin-types expected]
  (calc-iter (->> coin-types sort reverse) expected nil))

(calc [25 10] 65)
;; => (10 10 10 10 25)

原始作者几乎明白了-只需要固定硬币线的总和即可。