【问题标题】:Representing an amount of money with specific bills用特定票据表示金额
【发布时间】:2018-10-09 17:10:12
【问题描述】:

我想在 Racket 中编写一个函数,它接受一定数量的钱和一个特定账单值的列表,然后返回一个列表,其中包含每种类型的账单数量,以使总金额达到给定。例如(calc 415 (list 100 10 5 2 1)) 应该返回'(4 1 1 0 0)

我尝试过这种方式,但这不起作用:/ 老实说,我想我还没有完全理解在 Racket 中使用 set! 可以/不能做什么。

(define (calc n xs)
  (cond ((null? xs) (list))
        ((not (pair? xs)) 
          (define y n) 
          (begin (set! n (- n (* xs (floor (/ n xs)))))
                 (list (floor (/ y xs))) ))
        (else (append (calc n (car xs))
                      (calc n (cdr xs))))))

【问题讨论】:

  • 查看this question 对给定金额进行更改,这是完全相同的问题。 (它不是重复的)。看看my answer there 是否有帮助。
  • 您的意思是“set”,还是“set!”?
  • 设置! mb 我写错了
  • @WillNess 对不起,我还是不明白:/ 我觉得我的问题是我不能在坚持的同时减少我的 n,我曾经减少了多少账单。我的意思是也许我完全错过了这一点,这与他的实际解决方案相去甚远......
  • 如果您需要从蛋糕上咬一口,并且仍然握住它,也许您需要两只手,一只手握住每件东西。或变量。也就是两个变量。 :) 或更多,根据需要。你有目标总和,当前总和,剩余总和,可用账单列表......其中一些可能是多余的,但最好写正确,首先,仅优化稍后。俗话说“过早的优化是万恶之母”……也就是说,不要试图立即简洁-您可以随时优化!

标签: recursion scheme racket recursive-backtracking coin-change


【解决方案1】:

您的程序做的太多,并且您使用了不必要的突变。如果你把问题分开。

(define (calc-one-bill n bill)
  ...)

;; test 
(calc-one-bill 450 100) ; ==> 4
(calc-one-bill 450 50)  ; ==> 9

然后你可以制作:

(define (calc-new-n n bill amount)
  ...)

(calc-new-n 450 100 4) ; ==> 50
(calc-new-n 450 50 9)  ; ==> 0

然后你可以像这样减少你原来的实现:

(define (calc n bills)
  (if (null? bills)
      (if (zero? n)
          '()
          (error "The unit needs to be the last element in the bills list"))
      (let* ((bill (car bills))
             (amount (calc-one-bill n bill)))
        (cons amount
              (calc (calc-new-n n bill amount)
                    (cdr bills))))))

这将始终选择费用最少的解决方案,就像您的版本似乎所做的那样。两个版本都要求传递的bill 中的最后一个元素是单位1。对于更复杂的方法,它适用于(calc 406 (list 100 10 5 2)),并且可能找到所有解决方案的组合,请参阅Will's answer

【讨论】:

  • 您似乎指的是问题中的代码,但他们也发布了一个答案,我认为这正是您在此处提出的建议。也解决不了(calc 406 (list 100 10 5 2))的情况。
  • @WillNess 您的解决方案远比提供的示例所要求的复杂。对于初学者来说,从 OP 的努力中掌握和判断要困难得多,我说他们还没有准备好继续。您的解决方案与魔法无法区分:-p
  • 这里不是必须的,正如讨论的那样。复杂或简单,首先必须正确。失败总比返回错误的答案好......
  • @WillNess 我同意。我实际上做了一个改变,使它在你上次编辑的情况下失败。
【解决方案2】:

这个问题需要一些简单的递归非确定性编程。

  • 我们从给定的金额和给定的面额列表开始,显然每张钞票的数量不受限制(否则,这将是一个不同的问题)。
  • 在每个时间点,我们都可以使用最大的账单,也可以不使用。
  • 如果我们使用它,总金额会减少帐单的价值。
  • 如果总数为 0,我们就有了解决方案!
  • 如果总数为负数,则无效,所以我们应该放弃这条路。

此处的代码将遵循我的another answer,它找出解决方案的总数(对于您的示例, 不止一个)。我们只需要考虑解决方案本身,而上面提到的代码只计算了它们。

我们可以将此编码为 过程,调用一个回调,每个成功找到的解决方案都从最深级递归内部(相当于最深使用递归创建的嵌套循环结构中的嵌套循环,递归回溯的本质):

(define (change sum bills callback)
  (let loop ([sum sum] [sol '()] [bills bills])    ; "sol" for "solution"
    (cond
       ((zero? sum)   (callback sol))              ; process a solution found
       ((< sum 0)      #f)
       ((null? bills)   #f)
       (else
        (apply
          (lambda (b . bs)                         ; the "loop":
              ;; 1.                                ; either use the first
            (loop (- sum b) (cons b sol) bills)    ;   denomination,
              ;; 2.                                ;   or,
            (loop    sum            sol     bs))   ;   after backtracking, don't!
          bills)))))

它将通过例如调用之一

;; construct `the-callback` for `solve` and call 
;;       (solve ...params the-callback)
;; where `the-callback` is an exit continuation

(define (first-solution solve . params)
  (call/cc (lambda (return)
      (apply solve (append params         ; use `return` as
                     (list return))))))   ; the callback

(define (n-solutions n solve . params)    ; n assumed an integer
  (let ([res '()])                        ; n <= 0 gets ALL solutions
    (call/cc (lambda (break)
        (apply solve (append params
                       (list (lambda (sol)
                               (set! res (cons sol res))
                               (set! n (- n 1))
                               (cond ((zero? n) (break)))))))))
    (reverse res)))

测试,

> (first-solution change 406 (list 100 10 5 2))

'(2 2 2 100 100 100 100)

> (n-solutions 7 change 415 (list 100 10 5 2 1))

'((5 10 100 100 100 100)
  (1 2 2 10 100 100 100 100)
  (1 1 1 2 10 100 100 100 100)
  (1 1 1 1 1 10 100 100 100 100)
  (5 5 5 100 100 100 100)
  (1 2 2 5 5 100 100 100 100)
  (1 1 1 2 5 5 100 100 100 100))

关于此代码的结构,请参阅。 How to generate all the permutations of elements in a list one at a time in Lisp? 它创建嵌套循环,解决方案可以在最里面的循环体中访问。

关于如何以适当的功能方式编写非确定性算法(一次做出所有可能的选择),请参阅How to do a powerset in DrRacket?How to find partitions of a list in Scheme

【讨论】:

  • 非常感谢您的帮助!将来我肯定会仔细研究您的解决方案,但是我刚开始使用球拍,甚至不知道循环是如何工作的,所以这有点太令人困惑了:/。即使它给了我一些关于如何解决问题的想法,我现在有一个解决方案^^
  • "loop" 只是一个名字。也可以是“g”。查找“named let”,here on SO 也是如此,但它仅相当于内部的 define
【解决方案3】:

我现在就是这样解决的:)

(define (calc n xs)
  (define (calcAssist n xs usedBills)
    (cond ((null? xs) usedBills)
          ((pair? xs) 
            (calcAssist (- n (* (car xs) (floor (/ n (car xs))))) 
                        (cdr xs) 
                        (append usedBills 
                                (list (floor (/ n (car xs)))))))
          (else 
            (if ((= (- n (* xs (floor (/ n xs)))) 0)) 
               (append usedBills (list (floor (/ n xs))))
               (display "No solution")))))

  (calcAssist n xs (list)))

测试:

> (calc 415 (list 100 10 5 2 1))
'(4 1 1 0 0)

【讨论】:

  • 您的解决方案是"greedy"。这意味着即使有很多,它也不会总是找到解决方案。 理论上,就是这样。此外,cond 的第三个子句永远不会触发。
  • 例如,(calc 406 (list 100 10 5 2))。它应该会因您的代码而失败(但它错误地将'(4 0 1 0) 报告为解决方案)。正确的当然是'(4 0 0 3);还有'(3 10 0 3)'(3 0 20 3)等。
【解决方案4】:

我想这是我学习FORTRAN时写的第一个程序!这是一个版本,它毫不犹豫地使用了 Racket 提供的所有东西(或者,至少,我所知道的一切)。因此,它可能是一个糟糕的家庭作业解决方案,而且它肯定比我在 1984 年写的 FORTRAN 更漂亮。

请注意,此版本不搜索,因此即使不需要,它也会得到余数。当然,如果最小面额是 1,它永远不会得到余数。

(define/contract (denominations-of amount denominations)
  ;; split amount into units of denominations, returning the split
  ;; in descending order of denomination, and any remainder (if there is
  ;; no 1 denomination there will generally be a remainder).
  (-> natural-number/c (listof (integer-in 1 #f))
      (values (listof natural-number/c) natural-number/c))
  (let handle-one-denomination ([current amount]
                                [remaining-denominations (sort denominations >)]
                                [so-far '()])
    ;; handle a single denomination: current is the balance,
    ;; remaining-denominations is the denominations left (descending order)
    ;; so-far is the list of amounts of each denomination we've accumulated
    ;; so far, which is in ascending order of denomination
    (if (null? remaining-denominations)
        ;; we are done: return the reversed accumulator and anything left over
        (values (reverse so-far) current)
        (match-let ([(cons first-denomination rest-of-the-denominations)
                     remaining-denominations])
          (if (> first-denomination current)
              ;; if the first denomination is more than the balance, just
              ;; accumulate a 0 for it and loop on the rest
              (handle-one-denomination current rest-of-the-denominations
                                       (cons 0 so-far))
              ;; otherwise work out how much of it we need and how much is left
              (let-values ([(q r)
                            (quotient/remainder current first-denomination)])
                ;; and loop on the remainder accumulating the number of bills
                ;; we needed
                (handle-one-denomination r rest-of-the-denominations
                                         (cons q so-far))))))))

【讨论】:

  • 1.当我将它复制到#lang 球拍下的 DrRacket 窗口并加载它时,我得到了某种错误(我的球拍有点过时了,也许这就是为什么?...)。 2.这似乎也在做OP的answer正在做的事情......我认为它不能解决406 '(100 10 5 2)的情况。
  • 你在使用#lang racket吗?例如,我认为它不适用于racket/base,它没有define/contract。我正在使用 6.12,我认为它是最新的,但我认为这应该在最近的任何东西中都有效。不,它不会搜索,因此在您的情况下它会得到 1 的余数。我怀疑人们寻找的答案不会搜索,因为如果最低面额是 1,您永远不需要搜索,而且总是如此。但是搜索的版本显然更酷。我之所以写它,是因为它勾起了我的回忆。
  • #lang racket,但我的次要版本号远低于 12。是的,integer-in 期望我的版本有两个 exact-integer?s。过时了! --- 您是否以与此处相同的方式在 FORTRAN 中对其进行了编码?这是不正确的,你知道的。你必须能够回溯......所以这就是你所说的“得到一个余数”的意思,现在我明白了。 :)
  • 您可以使用exact-positive-integer? 代替(integer-in 1 #f)。仅当最小面额不是 1(通常以实际货币表示)时,它才是不正确的。如果最小面额不是 1,那么即使搜索也总是没有解决方案(即 1!)。我并不是要争辩说回溯不是一件好事,只是我非常怀疑最初的作业(我认为)问题需要它。我不记得我的 FORTRAN 是什么样子的,但它可能没有搜索过:在 F77 子集中让任何东西都能工作已经够难了。
  • 我只是用了 10000000 而不是#f。 :) --- 检测到不可能的解决方案本身也是一个有效的答案。回溯只是搜索的一种策略;您可以维护一个堆栈,并将其放入例如对于 406 / (100 10 5 2),不仅仅是 100:(4),而是 100:(4 3 2 1 0),然后弹出第一个并继续。这将在解决方案空间中执行 DFS 搜索。并且它总能找到解决方案,如果有的话。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-11-18
  • 1970-01-01
  • 2018-02-14
  • 1970-01-01
  • 1970-01-01
  • 2018-09-05
  • 1970-01-01
相关资源
最近更新 更多