Представление суммы денег конкретными счетами

Я хочу написать функцию в Racket, которая принимает сумму денег и список конкретных значений счетов, а затем возвращает список с количеством счетов, использованных каждого типа для получения данной суммы в целом. Например, (calc 415 (list 100 10 5 2 1)) должен вернуть '(4 1 1 0 0).

Я пробовал вот так, но это не сработало: / Я думаю, я не совсем понял, что можно / нельзя делать с set! в Racket, если честно.

(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))))))

person SyntaxIsNotTheProblem    schedule 29.04.2018    source источник
comment
ознакомьтесь с этим вопросом об изменении заданной суммы, которая точно соответствует та же проблема. (это не дубликат). посмотрим, поможет ли мой ответ.   -  person Will Ness    schedule 29.04.2018
comment
Вы имели в виду set или имели ввиду set!?   -  person Will Ness    schedule 29.04.2018
comment
установленный! мб я неправильно написал   -  person SyntaxIsNotTheProblem    schedule 29.04.2018
comment
@WillNess sry, я до сих пор не понимаю: / Я чувствую, что моя проблема в том, что я не могу уменьшить свой n, все еще держась за то, сколько счетов я использовал, чтобы уменьшить его. Я имею в виду, что, возможно, я полностью упускаю суть, и это далеко от реального решения ...   -  person SyntaxIsNotTheProblem    schedule 29.04.2018
comment
Если вам нужно откусить от торта и при этом держаться за него, возможно, вам понадобятся две руки, чтобы держать каждую вещь в каждой руке. или переменная. то есть две переменные. :) Или больше, сколько нужно. у вас есть целевая сумма, текущая сумма, остаток суммы к пополнению, список доступных счетов .... некоторые из них могут быть избыточными, но лучше писать правильно, сначала, оптимизируйте только позже < / я>. Как говорится, преждевременная оптимизация - мать всех зол .... то есть не пытайтесь сразу говорить кратко - вы всегда сможете оптимизировать позже!   -  person Will Ness    schedule 29.04.2018


Ответы (4)


Ваша процедура делает слишком много, и вы используете ненужную мутацию. Если вы разделите проблему на части.

(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)) и потенциально может найти все комбинации решений, см. В ответе Уилла.

person Sylwester    schedule 29.04.2018
comment
вы, кажется, ссылаетесь на код в вопросе, но они также опубликовали ответ, который, я думаю, делает именно то, что вы предлагаете здесь. и он не может решить случай (calc 406 (list 100 10 5 2)). - person Will Ness; 30.04.2018
comment
@WillNess Ваше решение намного сложнее, чем требуется в приведенном примере. Новичку было бы гораздо труднее понять, и, судя по усилиям ОП, я говорю, что они не готовы к продолжению. Ваше решение было бы неотличимо от magic :-p - person Sylwester; 30.04.2018
comment
как уже говорилось, продолжения здесь не важны. сложный или простой, он должен быть в первую очередь правильным. лучше потерпеть неудачу, чем дать неправильный ответ ... - person Will Ness; 30.04.2018
comment
@WillNess Согласен. На самом деле я внес изменение, из-за которого он не работает в вашем случае при последнем редактировании. - person Sylwester; 30.04.2018

Эта проблема требует простого рекурсивного недетерминированного программирования.

  • Мы начинаем с заданной суммы и заданного списка банкнот достоинств, очевидно, с неограниченным количеством каждой банкноты (в противном случае это была бы другая проблема).
  • В каждый момент времени мы можем использовать либо самую большую купюру, либо нет.
  • Если мы его используем, общая сумма уменьшается на сумму счета.
  • Если сумма равна 0, мы получили решение!
  • Если сумма отрицательная, она недействительна, поэтому нам следует отказаться от этого пути.

Код здесь будет следовать за другим моим ответом, в котором из общего количества решений (которых больше одного, для вашего примера). Нам просто нужно будет помнить и о самих решениях, тогда как упомянутый выше код только их считал.

Мы можем закодировать это как recursive-backtracking, вызывая обратный вызов с каждым успешно найденным решением изнутри самого глубокого уровня рекурсии (равносильно самому глубоко вложенному циклу в структуре вложенных циклов, созданной с помощью рекурсии , что является сутью рекурсивного поиска с возвратом):

(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))

Относительно того, как структурирован этот код, см. Как сгенерировать все перестановки элементов в списке по одной в Лиспе? Он создает вложенные циклы с решением доступный в теле самого внутреннего цикла.

Относительно того, как кодировать недетерминированный алгоритм (делая все возможные варианты сразу) надлежащим функциональным способом, см. Как сделать powerset в DrRacket? и Как найти разделы списка в схеме.

person Will Ness    schedule 29.04.2018
comment
Большое спасибо за помощь! Я обязательно рассмотрю ваше решение в будущем, но я только начал с racket и даже не знаю, как работают петли, так что это немного сбивает с толку: /. Даже несмотря на то, что это дало мне некоторые идеи о том, как я могу решить проблему, и теперь у меня есть решение ^^ - person SyntaxIsNotTheProblem; 29.04.2018
comment
loop - это просто имя. это могло быть g точно так же. найдите имя let, и здесь, на SO, но оно просто эквивалентно внутреннему define. - person Will Ness; 29.04.2018

Я вот так решил сейчас :)

(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)
person SyntaxIsNotTheProblem    schedule 29.04.2018
comment
ваше решение жадное. это означает, что он не всегда найдет решение, даже если их много. теоретически, то есть. кроме того, 3-е предложение cond никогда не сработает. - person Will Ness; 29.04.2018
comment
например, (calc 406 (list 100 10 5 2)). он должен выйти из строя с вашим кодом (но он неверно сообщает '(4 0 1 0) как решение). правильный, конечно, '(4 0 0 3); а также '(3 10 0 3), '(3 0 20 3) и т. д. - person Will Ness; 29.04.2018

Думаю, это первая программа, которую я написал при изучении ФОРТРАНА! Вот версия, которая не скрывает использования всего, что может предложить Racket (или, по крайней мере, всего, о чем я знаю). Таким образом, это, вероятно, ужасное домашнее задание, и оно определенно красивее, чем ФОРТРАН, который я написал в 1984 году.

Обратите внимание, что эта версия не выполняет поиск, поэтому она будет получать остатки, даже если в этом нет необходимости. Конечно, он никогда не получит остаток, если наименьший номинал равен 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))))))))
person Community    schedule 29.04.2018
comment
1. когда я копирую его в окно DrRacket под #lang racket и загружаю, я получаю какую-то ошибку (моя Racket немного устарела, может поэтому? ...). 2. похоже, что это делает то же самое, что и answer OP ... Я не думаю, что это может решить случай 406 '(100 10 5 2). - person Will Ness; 30.04.2018
comment
Вы используете #lang racket? Я не думаю, что это сработает, например, в racket/base, где нет define/contract. Я использую 6.12, который, как мне кажется, является текущим, но я думаю, что все это должно работать во всех последних версиях. Нет, он не выполняет поиск, поэтому в вашем случае он получает остаток 1. Я подозреваю, что ответы, которые ищут люди, не ищут, так как вам никогда не нужно искать, если наименьший номинал равен 1, и это всегда так. Но версия, в которой выполняет поиск, явно круче. Я написал это просто потому, что это вызвало воспоминания. - person ; 30.04.2018
comment
#lang racket, но мой второстепенный номер версии намного ниже 12. да, integer-in ожидает два exact-integer? для моей версии. устарело! --- вы закодировали его на FORTRAN так же, как здесь? это неверно, знаете ли. вы должны иметь возможность отступить ... Теперь я понимаю, что вы имели в виду, говоря о получении остатка. :) - person Will Ness; 30.04.2018
comment
Вы можете использовать exact-positive-integer? вместо (integer-in 1 #f). Это неверно только в том случае, если наименьший номинал не равен 1 (что обычно бывает в реальных валютах). И если наименьший номинал не равен 1, всегда есть случай, когда нет решения даже с поиском (а именно 1!). Я не пытаюсь утверждать, что возврат - это нехорошая вещь, просто я очень сомневаюсь, что первоначальный вопрос домашнего задания (я полагаю) требовал этого. Я не помню, как выглядел мой FORTRAN, но он, вероятно, не искал: просто заставить что-нибудь работать в подмножестве F77 было достаточно сложно. - person ; 01.05.2018
comment
Я просто использовал 10000000 вместо #f. :) --- определение того, что решения невозможны, тоже является правильным ответом. поиск с возвратом - лишь одна из стратегий поиска; вы можете поддерживать стек и помещать в него, например, для 406 / (100 10 5 2), а не просто 100: (4), как вы делаете сейчас, а вместо этого 100: (4 3 2 1 0), затем вставьте первый и продолжайте. при этом будет выполнен поиск DFS в пространстве решений. и он всегда найдет решение, если оно есть. - person Will Ness; 01.05.2018
comment
Ничего страшного, я умею писать алгоритмы поиска: я просто не заморачивался в этом случае! - person ; 01.05.2018
comment
ну, я ничего не имел в виду, просто думал вслух. - person Will Ness; 01.05.2018
comment
Ничего страшного, извини, я не пытался вести себя враждебно. Решение, которое ищет, как ваше, явно лучше, я был просто ленив (также я всегда стараюсь делать это с продолжениями и часто путаюсь при этом, и когда меня нет, я беспокоюсь о том, использую ли ядерное оружие call / cc для забить гвоздь действительно правильно) - person ; 02.05.2018
comment
Преимущество написания кода для ответа SO в том, что это не настоящий код. :) но серьезно, когда он фактически используется как простое продолжение exit, я бы ожидал, что компилятор скомпилирует его до простого перехода. какой смысл в компиляторе, иначе. :) --- что мне понравилось в коде в моем ответе, когда я его писал, так это то, насколько простым он оказался. Сделай то, потом сделай то. Простой. В конце концов, кажется, что императивное мышление имеет свое место. - person Will Ness; 02.05.2018