Algoritmo para encontrar denominações de moeda ideais

8

Mark mora em um pequeno país povoado por pessoas que tendem a pensar demais. Um dia, o rei do país decide redesenhar a moeda do país para tornar as trocas mais eficientes. O rei quer minimizar o número esperado de moedas necessárias para pagar exatamente qualquer quantia até (mas não incluindo) a quantia da menor nota de papel.

Suponha que a menor unidade de moeda seja a moeda. A menor nota de papel no reino vale a penanMoedas. O rei decide que não deve haver mais do quemdiferentes denominações de moedas em circulação. O problema, então, é encontrar umm-conjunto {d1,d2,...,dm} de números inteiros de {1,2,...,n1} o que minimiza 1n1i=1n1c1(i)+c2(i)+...+cm(i) sujeito a c1(i)d1+c2(i)d2+...cm(i)dm=i.

Por exemplo, considere o dólar padrão e suas denominações de moedas de {1,5,10,25,50}. Aqui, a menor conta de papel vale 100 da menor moeda. Leva 4 moedas para fazer 46 centavos usando esta moeda; temosc1(46)=1,c2(46)=0,c3(46)=2,c4(46)=1,c5(46)=0. No entanto, se tivéssemos denominações de moedas de{1,15,30}, seriam necessárias apenas 3 moedas: c1(46)=1,c2(46)=1,c3(46)=1. Qual desses conjuntos de denominações minimiza o número médio de moedas para fazer qualquer soma até 99 centavos?

Mais geralmente, dado n e m, como alguém pode determinar algoritmicamente o conjunto ideal? Claramente, pode-se enumerar todos osm-subsets e calcule o número médio de moedas necessárias para fazer somas de 1 a n1, acompanhando o melhor ao longo do caminho. Como existem cerca deC(n1,m) m-subsets (nem todos viáveis, mas ainda assim), isso não seria muito eficiente. Você pode fazer melhor que isso?

Patrick87
fonte
Se m <n - 1, então a solução sempre não terá exatamente m denominações? Se eu tiver uma solução com k moedas para (k <m <n - 1), sempre posso reduzir uma contagem de moedas para uma contagem> 0 a 1 adicionando uma denominação apenas para ela, reduzindo assim a média. Se isso for verdade, isso reduz o tempo de execução ingênuo?
Mike Samuel
@MikeSamuel Sure. No entanto, se houver duas soluções igualmente boas, uma comm denominações e uma com k<mdenominações, isso pode ser algo que o rei está interessado em saber. Afinal, fazer mais tipos de moedas custa dinheiro.
22812 Patrick87
Eu não acho que possa haver duas soluções igualmente boas, definidas apenas pelo somatório acima, quando m <n-1. Se existe uma moeda que vale k, onde 1 <= k <n, esse elemento do somatório é 1, e se não existe uma moeda que vale k, esse elemento do somatório é> 1.
Mike Samuel
@ MikeSamuel Eu acho que isso provavelmente é verdade, mas, novamente, eu meio que gostaria de ver isso como parte de uma resposta, possivelmente com alguma motivação. Na verdade, fica um pouco complicado, pois os conjuntos podem ser (principalmente) não sobrepostos.
22812 Patrick87
Aqui está outro fato que restringe o espaço da solução: uma moeda no valor de 1 deve aparecer em todas as soluções.
9608 Mike Samuel Samuel

Respostas:

6

Isso está relacionado ao conhecido problema de mudança . Tão bem estudado, de fato, que esta questão foi investigada porm7[1] usando força bruta. A partir de 2003, a dureza de encontrar denominações ótimas parece ser um problema em aberto.

Se você verificar os artigos que citam Shallit, parece que denominações que permitem estratégias gananciosas de mudança foram de particular interesse. É claro que essas denominações têm vantagens na prática.


  1. O que este país precisa é de uma peça de 18 c por Jeffrey Shallit (2003)
Rafael
fonte
2

Eu imaginei (erroneamente, mas tenha paciência comigo) que a série {bi| b=n1/m,0i<m}de moedas seria o ideal, pois as moedas seriam espaçadas exponencialmente, reduzindo assim o valor restante o máximo possível por moeda adicionada. Para o seu exemplo, isso seria{1,3,9,27,81}.

Este é um ponto melhor (390/99) que as denominações em USD (420/99), mas isso não precisa significar nada.

Escrevi um script hackeado de Haskell para obter alguns números por força bruta, pois não tenho certeza no momento de como abordar isso analiticamente.
Acontece que a distribuição exponencial nem sempre é a melhor: às vezes há outras um pouco melhores, por exemplo, para(m,n)=(4,30) Nós temos 75/29 para {20,8,3,1} mas 87/29 para {27,9,3,1}. Minha máquina lenta não consegue(5,100), então temos que usar números menores aqui.

No entanto, notei que o erro parece ser bastante pequeno. Na maioria das vezes, a divisão das somas produz algo começando com um 1,0 ..., então eu fiz mais alguns testes.

De um conjunto de teste com 3m5 e 6n40, obtemos um erro médio de nosso crescimento exponencial em comparação com a melhor solução de 1.12 com um desvio padrão de 0.085.

Você pode argumentar que os parâmetros de teste são bastante pequenos, mas, como você ressalta, é muita força bruta se você definir n=100 (provavelmente existe uma solução melhor, mas essa foi uma excelente desculpa para relaxar e fazer um pouco de Haskell).


Aqui está o meu conjunto de testes, se você quiser experimentá-lo:

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

Eu fiz o teste com

map (uncurry esterror) [(m,n) | m <- [3..5], n <- [6..40] ]
bitmask
fonte