Algoritma untuk menemukan denominasi mata uang yang optimal

8

Mark tinggal di negara kecil yang dihuni oleh orang-orang yang cenderung terlalu memikirkan banyak hal. Suatu hari, raja negara memutuskan untuk mendesain ulang mata uang negara untuk membuat perubahan memberikan lebih efisien. Raja ingin meminimalkan jumlah koin yang diperlukan untuk membayar jumlah yang tepat hingga (tetapi tidak termasuk) jumlah tagihan kertas terkecil.

Misalkan unit mata uang terkecil adalah Koin. Tagihan kertas terkecil di kerajaan bernilai Koin. Raja memutuskan bahwa seharusnya tidak ada lebih dari denominasi koin yang berbeda yang beredar. Masalahnya, kemudian, adalah untuk menemukan -set bilangan bulat dari yang meminimalkan tunduk pada .nmm{d1,d2,...,dm}{1,2,...,n1}1n1i=1n1c1(i)+c2(i)+...+cm(i)c1(i)d1+c2(i)d2+...cm(i)dm=i

Misalnya, ambil USD standar dan denominasi koinnya dari . Di sini, tagihan kertas terkecil bernilai 100 dari koin terkecil. Dibutuhkan 4 koin untuk menghasilkan 46 sen menggunakan mata uang ini; kami memiliki . Namun, jika kami memiliki denominasi koin , hanya dibutuhkan 3 koin: . Manakah dari set denominasi ini yang meminimalkan jumlah koin rata-rata untuk menghasilkan jumlah hingga dan termasuk 99 sen?{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

Lebih umum, mengingat dan , bagaimana mungkin secara algoritmik menentukan set optimal? Jelas, seseorang dapat menghitung semua sub-set layak- dan menghitung jumlah rata-rata koin yang diperlukan untuk membuat jumlah dari 1 hingga , melacak yang optimal sepanjang jalan. Karena ada sekitar subset (tidak semuanya layak, tetapi masih), ini tidak akan menjadi sangat efisien. Bisakah Anda melakukan lebih baik dari itu?nmmn1C(n1,m) m

Patrick87
sumber
Jika m <n - 1, bukankah solusinya akan selalu memiliki denominasi m persis? Jika saya memiliki solusi dengan k koin untuk (k <m <n - 1) saya selalu dapat mengurangi satu jumlah koin untuk hitungan> 0 hingga 1 dengan menambahkan denominasi hanya untuk itu, sehingga mengurangi rata-rata. Jika itu benar, lalu apakah itu mengurangi runtime naif?
Mike Samuel
@MikeSamuel Tentu. Namun, jika ada dua solusi yang sama baiknya, satu dengan denominasi dan satu dengan denominasi , itu mungkin sesuatu yang raja ingin ketahui. Lagi pula, membuat lebih banyak jenis koin membutuhkan uang. mk<m
Patrick87
Saya tidak berpikir ada dua solusi yang sama baiknya dengan yang didefinisikan semata-mata oleh penjumlahan di atas ketika m <n-1. Jika ada koin bernilai k di mana 1 <= k <n, maka unsur penjumlahan itu adalah 1, dan jika tidak ada koin yang bernilai k, maka unsur penjumlahan itu adalah> 1.
Mike Samuel
@ MikeSamuel Saya pikir itu mungkin benar, tapi sekali lagi, saya agak ingin melihatnya sebagai bagian dari jawaban, mungkin dengan beberapa motivasi. Ini sebenarnya menjadi sedikit rumit, karena set dapat (kebanyakan) tidak tumpang tindih.
Patrick87
Berikut fakta lain yang mempersempit ruang solusi: koin senilai 1 harus muncul di semua solusi.
Mike Samuel

Jawaban:

6

Ini terkait dengan masalah pembuatan perubahan yang terkenal . Sudah dipelajari, pada kenyataannya, bahwa pertanyaan ini telah diselidiki untuk [1] menggunakan kekuatan kasar. Pada tahun 2003, kekerasan untuk menemukan denominasi yang optimal tampaknya menjadi masalah terbuka.m7

Jika Anda memeriksa artikel yang mengutip Shallit, sepertinya denominasi yang memungkinkan strategi perubahan rakus menjadi perhatian khusus. Jelaslah bahwa denominasi semacam itu memiliki keunggulan dalam praktik.


  1. What This Country Needs is a 18c Piece oleh Jeffrey Shallit (2003)
Raphael
sumber
2

Saya menduga (salah, tapi tahan dengan saya) bahwa seri koin akan menjadi optimal, karena koin akan ditempatkan secara eksponensial, sehingga mengurangi nilai yang tersisa sebanyak mungkin per koin tambahan. Sebagai contoh Anda, ini akan menjadi .{bi| b=n1/m,0i<m}{1,3,9,27,81}

Ini kedudukan yang lebih baik ( ) daripada denominasi USD ( ), tetapi itu tidak harus berarti apa-apa.390/99420/99

Saya menulis skrip Haskell untuk mendapatkan beberapa angka dengan kekerasan, karena saya tidak yakin sekarang bagaimana cara pendekatan ini secara analitis.
Ternyata, distribusi eksponensial tidak selalu yang terbaik: kadang-kadang ada yang sedikit lebih baik, misalnya, untuk kita mendapatkan untuk tetapi untuk . Mesin lambat saya tidak bisa mencapai , jadi kami harus menggunakan angka yang lebih kecil, di sini.(m,n)=(4,30)75/29{20,8,3,1}87/29{27,9,3,1}(5,100)

Namun, saya perhatikan bahwa kesalahannya tampaknya cukup kecil. Sebagian besar waktu, pembagian jumlah menghasilkan sesuatu yang dimulai dengan 1,0 ..., jadi saya menjalankan beberapa tes lagi.

Dari set uji dengan dan , kami mendapatkan kesalahan rata-rata pertumbuhan eksponensial kami dibandingkan dengan solusi terbaik dengan standar deviasi .3m56n401.120.085

Anda mungkin berpendapat bahwa parameter pengujian agak kecil, tetapi seperti yang Anda tunjukkan, itu hanya banyak untuk memaksa jika Anda menetapkan (kemungkinan besar ada solusi yang lebih baik, tapi ini adalah alasan yang sangat baik untuk mengendur dan melakukan beberapa Haskell).n=100


Ini adalah ruang tes saya, jika Anda ingin mencobanya:

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

Saya menjalankan tes dengan

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