Algoritmo per trovare denominazioni valutarie ottimali


8

Mark vive in un piccolo paese popolato da persone che tendono a pensare troppo alle cose. Un giorno, il re del paese decide di ridisegnare la valuta del paese per rendere il cambiamento più efficiente. Il re vuole ridurre al minimo il numero previsto di monete necessarie per pagare esattamente qualsiasi importo fino a (ma non incluso) l'importo della fattura cartacea più piccola.

Supponiamo che la più piccola unità di valuta sia la moneta. La banconota di carta più piccola del regno vale monete. Il re decide che non dovrebbero esserci più di monete diverse in circolazione. Il problema, quindi, è trovare un -set di numeri interi da che minimizza soggetto a .nmm{d1,d2,...,dm}{1,2,...,n-1}1n-1Σio=1n-1c1(io)+c2(io)+...+cm(io)c1(io)d1+c2(io)d2+...cm(io)dm=io

Ad esempio, prendi l'USD standard e le sue denominazioni in monete di . Qui, la banconota di carta più piccola vale 100 della moneta più piccola. Ci vogliono 4 monete per fare 46 cent usando questa valuta; abbiamo . Tuttavia, se avessimo un taglio in monete di , ci vorrebbero solo 3 monete: c_1 (46) = 1, c_2 (46) = 1, c_3 (46) = 1 . Quale di questi set di denominazioni minimizza il numero medio di monete per fare una somma fino a 99 centesimi inclusi?{1,5,10,25,50}c1(46)=1,c2(46)=0,c3(46)=2,c4(46)=1,c5(46)=0{1,15,30}c1(46)=1,c2(46)=1,c3(46)=1

Più in generale, dati e , come si può determinare algoritmicamente il set ottimale? Chiaramente, si potrebbero enumerare tutti i -subset vitali e calcolare il numero medio di monete necessarie per fare somme da 1 a , tenendo traccia di quello ottimale lungo il percorso. Dato che ci sono circa -subset (non tutti fattibili, ma comunque), questo non sarebbe terribilmente efficiente. Puoi fare di meglio?nmmn-1C(n-1,m) m


Se m <n - 1, la soluzione non avrà sempre esattamente m denominazioni? Se ho una soluzione con k monete per (k <m <n - 1) posso sempre ridurre un conteggio delle monete per un conteggio> 0 a 1 aggiungendo una denominazione solo per essa, riducendo così la media. Se ciò è vero, ciò riduce il tempo di esecuzione ingenuo?
Mike Samuel,

@MikeSamuel Certo. Tuttavia, se ci sono due soluzioni ugualmente buone, una con denominazioni e una con denominazioni, potrebbe essere qualcosa che il re è interessato a conoscere. Fare più tipi di monete costa soldi, dopo tutto. mK<m
Patrick87,

Non penso che ci possano essere due soluzioni ugualmente valide, come definito esclusivamente dalla sommatoria sopra quando m <n-1. Se esiste una moneta del valore di k dove 1 <= k <n, quell'elemento della somma è 1 e se non esiste una moneta del valore di k, allora quell'elemento della somma è> 1.
Mike Samuel,

@MikeSamuel Penso che sia probabilmente vero, ma poi di nuovo mi piacerebbe vederlo come parte di una risposta, forse con qualche motivazione. In realtà diventa un po 'complicato, dal momento che i set possono essere (principalmente) non sovrapposti.
Patrick87,

Ecco un altro fatto che restringe lo spazio della soluzione: una moneta del valore di 1 deve apparire in tutte le soluzioni.
Mike Samuel,

Risposte:


6

Ciò è legato al noto problema di modifica . Così ben studiato, infatti, che questa domanda è stata indagatam7[1] usando la forza bruta. A partire dal 2003, la durezza di trovare denominazioni ottimali sembra essere un problema aperto.

Se controlli gli articoli che citano Shallit, sembra che le denominazioni che consentono avide strategie di cambiamento siano di particolare interesse. È chiaro che tali denominazioni presentano in pratica vantaggi.


  1. What This Country Needs is a 18c Piece di Jeffrey Shallit (2003)

2

Ho indovinato (a torto, ma abbi pazienza con me) che la serie {Bio| B=n1/m,0io<m}di monete sarebbe l'ottimale, poiché le monete sarebbero spaziate esponenzialmente, riducendo così il valore rimanente il più possibile per ogni moneta aggiunta. Per il tuo esempio, questo sarebbe{1,3,9,27,81}.

Questa è una tacca migliore (390/99) rispetto alle denominazioni in USD (420/99), ma ciò non deve significare nulla.

Ho scritto una sceneggiatura Haskell per ottenere alcuni numeri con la forza bruta, dato che non sono sicuro in questo momento come affrontarlo analiticamente.
Si scopre che la distribuzione esponenziale non è sempre la migliore: ce ne sono a volte leggermente migliori, ad esempio, per(m,n)=(4,30) noi abbiamo 75/29 per {20,8,3,1} ma 87/29 per {27,9,3,1}. La mia macchina lenta non ci riesce(5,100), quindi dobbiamo usare numeri più piccoli, qui.

Tuttavia, ho notato che l'errore sembra essere piuttosto piccolo. La maggior parte delle volte, la divisione delle somme produce qualcosa che inizia con un 1.0 ..., quindi ho eseguito altri test.

Da un set di test con 3m5 e 6n40, otteniamo un errore medio della nostra crescita esponenziale rispetto alla migliore soluzione di 1.12 con una deviazione standard di 0,085.

Potresti sostenere che i parametri del test sono piuttosto piccoli, ma come fai notare, è solo una forza bruta se imposti n=100 (Probabilmente c'è una soluzione migliore, ma questa è stata un'ottima scusa per rilassarmi e fare un po 'di Haskell).


Ecco la mia suite di test, se vuoi provarlo:

getopt :: [Integer] -> Integer -> [Integer]
getopt _ 0 = []
getopt coins target = choice:(getopt viable $ target - choice)
                          where
                            viable = filter ((>=) target) coins
                            choice = maximum $ viable

getsum :: [Integer] -> Integer -> Int
getsum coins n = sum $ map length $ map (getopt coins) [1..(n-1)]

buildall :: Integer -> Integer -> [[Integer]]
buildall 1 _ = [[1]]
buildall m n = foldl1 (++) $ map (\am -> map (\x -> x:am) [((head am)+1) .. (n-1)]) $ buildall (m-1) n

buildguess :: Integer -> Integer -> [Integer]
buildguess m n = reverse $ map ((^) $ ceiling $ (fromInteger n)**(1.0/(fromInteger m))) [0..(m-1)]

findopt :: Integer -> Integer -> ([Integer],Int)
findopt m n = foldl1 (\(l@(_,lhs)) -> (\(r@(_,rhs)) -> (if (lhs < rhs) then l else r)))
            $ map (\arr -> (arr,getsum arr n)) $ buildall m n

intcast :: (Integral a,Num b) => a -> b
intcast = fromInteger.toInteger

esterror :: Integer -> Integer -> Double
esterror m n = (intcast $ getsum (buildguess m n) n) / (intcast best)
                 where (_,best) = findopt m n

Ho eseguito il test con

map (uncurry esterror) [(m,n) | m <- [3..5], n <- [6..40] ]
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.