【问题标题】:Decrease list values by ratio in Clojure在 Clojure 中按比率减少列表值
【发布时间】:2019-07-31 13:33:52
【问题描述】:

我有一个小的编程问题,我正试图在 Clojure 中解决。

比如说,我有一个包含整数值的列表(它们也包括零)。这些值有一个总和,我想减少某个值。为了得到这个较低的总和,我想按比率减少列表中的值。

比如说,我有以下列表:[0, 10, 30, 40, 20, 0]。总和是 100,我想将总和减少到 90。我想按比例减少值,所以新列表将是 [0, 9, 27, 36, 18, 0]。

但是,当数字变成分数时,这会出现问题。当您对数字进行四舍五入时(使用 round、floor 或 ceil),您最终会得到一个减去 1 或 2 的总和。我似乎找不到一个优雅的解决方案。我得到的一切都包括遍历所有值一次,然后返回修复偏移量。有什么想法吗?

编辑

为了澄清我想看到的行为,它的舍入方式对我来说并不重要,只要总和是正确的并且数字的比率大致相同。我不在乎总误差是最小的还是四舍五入的。

附加要求是数字只允许保持相等或变小,数字应 >= 0,并且生成的数字列表应为整数。

【问题讨论】:

  • 我不清楚您要做什么。您似乎希望对输入列表的数字进行一定比例,以使输出列表的总和等于给定数字。正如您所指出的,您并不总是得到完整的结果。但是你没有提到你在这种情况下的期望。另外,请包括您尝试过的内容。

标签: clojure


【解决方案1】:

我认为没有第二次通过列表来修复四舍五入就没有办法解决它。这是使用Largest Remainder Method 的一种解决方案:

(defn adj-seq
  [input ratio rounding]
  (let [;;
        ;; function to apply ratio to a number
        ;;
        mul-ratio    (partial * ratio)
        ;;
        ;; function to apply ratio and rounding to a number
        ;;
        mul-ratio-r  (comp rounding mul-ratio)
        ;;
        ;; sort oirignal input with largest remainder first
        ;; then applies ratio and rounding to each number
        ;;
        rounded-list (->> input
                          (sort-by #(- (mul-ratio-r %)
                                       (mul-ratio %)))
                          (map mul-ratio-r))
        ;;
        ;; sum of original numbers
        ;;
        sum-input    (reduce + input)
        ;;
        ;; calculate the delta between the expected sum and sum of all rounded numbers
        ;;
        delta        (- (mul-ratio-r sum-input) (reduce + rounded-list))]

    ;;
    ;; distribute delta to the rounded numbers in largest remainder order
    ;;
    (->> rounded-list
         (reductions (fn [[remain _] e]
                       ;; increment number by 1 if remaining delta is >1
                       (if (pos? remain)
                         [(dec remain) (inc e)]
                         ;; otherwise returns the rounded number as is
                         [0 e]))
                     ;; delta is the initial value to feed to the reducing function
                     [delta])
         ;;
         ;; ignore the first output from the reducing function - which is the original delta
         ;;
         rest
         ;;
         ;; get the adjusted number: ratio + rounding + delta-adj
         ;;
         (map last))))

还有一个示例运行:

(def input [0 10 30 40 20 0])
(def ratio 0.83)
(def rounding int)

(reduce + input)
;; => 100
(* ratio *1)
;; => 83.0
(adj-seq input ratio rounding)
;; => (25 17 8 33 0 0)
(reduce + *1)
;; => 83

【讨论】:

  • 谢谢,尤其是使用方法的链接,这是我需要的信息(我找不到开始的地方)。让我看看我是否能完全理解你的代码,遗憾的是它比我希望的要复杂,但我担心这就是问题的本质。
【解决方案2】:

我们可以使用 clojure.spec 指定函数的要求。如果我们希望函数支持任意精度的整数、总和为零的序列、空序列等,我们可以编写以下函数规范:

(s/def ::natural-integer (s/and integer? (comp not neg?)))
(s/fdef dec-sum-int
  :args (s/and (s/cat :new-sum ::natural-integer
                      :nums (s/coll-of ::natural-integer))
               #(<= (:new-sum %) (apply +' (:nums %))))
  :ret  (s/coll-of ::natural-integer)
  :fn   (fn [{:keys [args ret]}]
          (and (= (count (:nums args)) (count ret))
               ;; each output <= corresponding input
               (every? true? (map <= ret (:nums args)))
               (or (empty? ret)
                   (= (:new-sum args) (apply + ret))))))

然后st/check 下面的原始答案可以查看失败的示例,或者查看带有s/exercise-fn 的示例调用。

这是一个满足您更新要求的规范的版本。大多数复杂性是在调整舍入误差时确保每个输出

(defn dec-sum-int [new-sum nums]
  (let [sum   (apply +' nums)
        ratio (if (zero? sum) 1 (/ new-sum sum))
        nums' (map #(bigint (*' % ratio)) nums)
        err   (- new-sum (apply + nums'))]
    (loop [nums  nums
           nums' nums'
           out   []
           err   err]
      (cond
        (zero? err)
        (into out nums')

        (seq nums')
        (let [[num & more] nums
              [num' & more'] nums']
          (if (pos? num)
            (let [num'' (min num (+ num' err))]
              (recur more more'
                     (conj out num'')
                     (- err (- num'' num'))))
            (recur more more' (conj out num') err)))

        :else out))))

(st/summarize-results (st/check `dec-sum-int))
{:sym playground.so/dec-sum-int}
=> {:total 1, :check-passed 1}

原答案

这是一个函数,可以将集合中的每个数字乘以一个比率以达到某个所需的总和:

(defn adjust-sum [new-sum nums]
  (let [sum (apply + nums)]
    (map #(* % (/ new-sum sum))
         nums)))

(adjust-sum 90 [0 10 30 40 20 0])
=> (0N 9N 27N 36N 18N 0N)
(map int *1)
=> (0 9 27 36 18 0)

对于您的示例,结果自然会以大整数形式出现。这是唯一给出的例子,但这个问题很适合基于属性的生成测试。我们可以定义所有示例都应该持有的属性,并使用 test.check 来针对许多我们可能无法想象的随机示例测试该函数:

(tc/quick-check 10000
  (prop/for-all [new-sum gen/int
                 nums (->> (gen/vector gen/int)
                           ;; current approach fails for inputs that sum to zero
                           (gen/such-that #(not (zero? (apply + %)))))]
    (= new-sum (apply + (adjust-sum new-sum nums)))))
=> {:result true, :num-tests 10000, :seed 1552170880184}

有关处理舍入错误示例的更新,或处理负数的先前编辑,请参阅上面的更新。

【讨论】:

  • 太棒了,我喜欢你在其中包含一个规范。 adjust-sum-int 只是有一点问题。如果第一个值为 0,则调整将使其成为负数(这是不允许的),或者将增加值(这也是不允许的,所有数字应保持相等或下降)。我想最好更新第一个非零值,甚至更好,最高。
  • 我看到了,但是我仍然不确定我是否更喜欢这个或使用最大余数方法的答案。它看起来更干净。
【解决方案3】:

这是你需要的吗?

(defn scale-vector 
  "Given `s`, a sequence of numbers, and `t`, a target value for the sum of
  the sequence, return a sequence like `s` but with each number scaled 
  appropriately." 
  [s t]
  (let [ratio (/ (reduce + (filter number? s)) t)]
    (map #(if (number? %) (/ % ratio) %) s)))

(scale-vector [10 20 :foo 30 45.3 0 27/3] 21)
=> (1.837270341207349 3.674540682414698 :foo 5.511811023622047 8.32283464566929 0.0 1.6535433070866141)

(reduce + (filter number? (scale-vector [10 20 :foo 30 45.3 0 27/3] 21)))
=> 21.0

这里发生了什么:

  1. 我们假设s 是一个数字序列;但如果某些元素不是数字,则不一定是错误。过滤数字可以让我们优雅地处理一些元素是非数字的;我选择保留非数字元素,但您同样可以删除它们。
  2. 我没有做任何特别的事情来从输出中排除有理数,我不明白你为什么需要这样做;但如果你想这样做,你可以使用(map double [1 1/2 22/7]) =&gt; (1.0 0.5 3.142857142857143)
  3. 但习惯上,在 Clojure 中,数字只是数字。任何接受数字的函数都应该接受数字。有理数——你所说的“分数”——就像任何其他数字一样是数字。不用担心他们。

【讨论】:

  • 谢谢!我喜欢你检查非数字!但是,我希望结果列表由整数组成。我已经更新了原始问题以澄清这一点。
  • 啊!然后你想映射一些在结果上转换为整数的东西。例如(map int '(22/7 3.14 0.001))。当然,您同样可以使用(map #(int (Math/ceil %)) result)。没有方便的Math/round,但如果您需要使用(map #(int (+ 0.5 %)) result)
  • 还要注意一个问题:Clojure 中的基本 + - */ 函数仅适用于 64 位值。如果结果溢出 64 位值,则会出现异常,这很愚蠢。没有很好记录的是有替代函数+'-'*'/' 可以正确处理超过 64 位的值。这样做的性能损失非常小,我建议在任何地方都使用任意精度版本,除了需要优化性能的代码。
  • 是的,除了在转换为整数时,四舍五入可能会导致总和不正确。
猜你喜欢
  • 1970-01-01
  • 2018-11-11
  • 1970-01-01
  • 1970-01-01
  • 2018-10-29
  • 2020-03-25
  • 2021-09-10
  • 1970-01-01
  • 2016-03-17
相关资源
最近更新 更多