我有一个正在尝试在Clojure中解决的编程问题。
说,我有一个带有整数值的列表(它们也包括零)。这些值有一个总和,我想减少一定值。为了达到较低的总和,我想按比例减小列表中的值。
说,我有以下列表:[0,10,30,40,20,0]。总和为100,我想将总和减少为90。我想按比例减少值,因此新列表将为[0,9,27,36,18,0]。
但是,当数字变成小数时,这将成为问题。当您对数字进行四舍五入时(无论是四舍五入,最小或最小),最终的总和都将减少1或2。我似乎找不到一个优雅的解决方案。我得到的一切都包括一次遍历所有值,然后返回以修复偏移量。有什么想法吗?
修改
要弄清楚我想看到的行为,四舍五入的方式对我来说并不重要,只要总和正确且数字的比率大致相同即可。我不在乎总误差是最小还是舍去最大。
其他要求是仅允许数字保持相等或降低,数字应> = 0,并且结果数字列表应为整数。
答案 0 :(得分:5)
我们可以使用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}
有关处理四舍五入错误的示例,请参见以上更新;有关处理负数的示例,请参见上述更新。
答案 1 :(得分: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 :(得分:0)
这是您需要的吗?
(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
这是怎么回事:
s
是一个数字序列;但是如果某些元素不是数字,则不一定是错误。过滤数字可以让我们优雅地应对某些非数字元素;我选择保留非数字元素,但您也可以删除它们。(map double [1 1/2 22/7]) => (1.0 0.5 3.142857142857143)
。