{"id":71,"date":"2014-01-19T00:46:22","date_gmt":"2014-01-18T23:46:22","guid":{"rendered":"https:\/\/balanceofcowards.net\/boc_blog\/?p=71"},"modified":"2015-01-12T20:48:00","modified_gmt":"2015-01-12T19:48:00","slug":"muenzspaltereien","status":"publish","type":"post","link":"https:\/\/balanceofcowards.net\/boc_blog\/2014\/01\/muenzspaltereien\/","title":{"rendered":"[:de]M\u00fcnzspaltereien[:en]Coinage[:]"},"content":{"rendered":"<p>[:de]<\/p>\n<p style=\"text-align: justify;\">Ein interessantes Problem \u00fcber das ich heute gestolpert bin: F\u00fcr einen beliebigen Geldbetrag n &#8211; was ist die minimale Anzahl an M\u00fcnzen, mit der man diesen Geldbetrag erreichen kann? Also zum Beispiel kann man ja 10 Cent als zehn einzelne Cent-St\u00fccke, oder aber als zwei F\u00fcnf-Cent St\u00fccke, oder als ein F\u00fcnf-Cent St\u00fcck und f\u00fcnf einzelne Cent-St\u00fccke darstellen, und so weiter. F\u00fcr zehn Cent ist nat\u00fcrlich die minimale Anzahl an M\u00fcnzen genau eine Zehn-Cent M\u00fcnze. Aber wie l\u00f6st man das Problem allgemein?<\/p>\n<p>[:]<br \/>\n[:en]<br \/>\nThis article is only available in <a href=\"https:\/\/balanceofcowards.net\/boc_blog\/?p=71&#038;lang=de\">German<\/a>, unfortunately.<br \/>\n[:]<br \/>\n<!--more--><\/p>\n<p>[:de]Die Anzahl an Kombinationen steigt ziemlich schnell, wie folgendes Bild f\u00fcr sieben Cent veranschaulicht:<\/p>\n<p style=\"text-align: justify;\"><a href=\"https:\/\/balanceofcowards.net\/wp-uploads\/balanceofcowards.net\/2014\/01\/sieben.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-medium wp-image-72 aligncenter\" src=\"https:\/\/balanceofcowards.net\/wp-uploads\/balanceofcowards.net\/2014\/01\/sieben-300x125.png\" alt=\"sieben\" width=\"300\" height=\"125\" srcset=\"https:\/\/balanceofcowards.net\/wp-uploads\/balanceofcowards.net\/2014\/01\/sieben-300x125.png 300w, https:\/\/balanceofcowards.net\/wp-uploads\/balanceofcowards.net\/2014\/01\/sieben-1024x429.png 1024w, https:\/\/balanceofcowards.net\/wp-uploads\/balanceofcowards.net\/2014\/01\/sieben.png 1310w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a>Hier entspricht jeder Pfeil einer Reduktion um einen bestimmten Betrag (1, 2, oder 5 Cent). Gesucht ist anschaulich die minimale Anzahl an Pfeilen, denen man folgen muss, bis man eine 0 erreicht. Im Bild wird schnell klar, dass das der rechteste Pfad ist &#8211; der Pfad, der einem 5-Cent St\u00fcck und einem 2-Cent St\u00fcck entspricht (wer genau hinsieht, erkennt, dass ein \u00e4quivalenter Pfad im Bild noch ein zweites Mal vorhanden ist).<\/p>\n<p style=\"text-align: justify;\">F\u00fcr das Euro-M\u00fcnzsystem, in dem jede gr\u00f6\u00dfere M\u00fcnze ein vielfaches aller kleineren M\u00fcnzen darstellt, l\u00e4sst sich das Problem recht einfach l\u00f6sen &#8211; n\u00e4mlich genau so wie das typischerweise eine Kassiererin machen w\u00fcrde (zumindest solange ihr das Kleingeld nicht ausgeht). Man nimmt einfach m\u00f6glichst gro\u00dfe M\u00fcnzen &#8211; also ein Greedy Algorithmus. Der w\u00fcrde dann in Python etwa so aussehen:<\/p>\n<pre class=\"lang:python decode:true\"># Example uses European currency - Euros and Cents (coins only)\r\ncoins = [1, 2, 5, 10, 20, 50, 100, 200]\r\n\r\ndef coinage1(value):\r\n    \"\"\"\r\n        Greedy search - try to minimize value as fast as possible. This is what\r\n        ordinary people would usually do. May miss more optimal solutions in\r\n        specific cases.\r\n    \"\"\"\r\n    if value == 0:\r\n        return 0\r\n    fits = filter( (lambda n: n &lt;= value), coins)\r\n    return 1 + coinage1(value - max(fits))<\/pre>\n<p style=\"text-align: justify;\">Das funktioniert, wie gesagt, bei unserem M\u00fcnzsystem ganz gut &#8211; kann aber in anderen Situationen schiefgehen. Ein einfaches Beispiel w\u00e4re ein M\u00fcnzsystem in dem als M\u00fcnzwerte nur 1 Cent, 4 Cent, 199 Cent und 200 Cent vorkommen. Hier w\u00fcrde der Algorithmus f\u00fcr 203 Cent vier M\u00fcnzen verlangen (eine 200 Cent \/ 2 Euro M\u00fcnze und drei 1 Cent M\u00fcnzen) obwohl offensichtlich bereits zwei M\u00fcnzen reichen (eine 199 Cent M\u00fcnze und eine 4 Cent M\u00fcnze). Die Frage ist also &#8211; gibt es einen Algorithmus der auch mit derart seltsamen M\u00fcnzsystemen zurechtkommt? Wenn ich mir das M\u00fcnzsystem anschaue, welches in Gro\u00dfbritannien bis 1971 <a href=\"http:\/\/de.wikipedia.org\/wiki\/Pfund_Sterling#Bis_1971\">\u00fcblich war<\/a>, k\u00f6nnte das Problem durchaus ein realistisches sein :)<\/p>\n<p style=\"text-align: justify;\">Also geht es zuverl\u00e4ssiger? Naja, man kann den Baum einfach mal rekursiv komplett durchgehen und mitz\u00e4hlen, wieviele Pfeile man jeweils gelaufen ist. In Code gegossen w\u00fcrde das etwa so aussehen (die Definition der M\u00fcnzen bleibt wie oben):<\/p>\n<pre class=\"lang:python decode:true\" title=\"Naive rekursive Suche\">def coinage2(value):\r\n    \"\"\"\r\n        Naive, recursive search through the entire solution space\r\n    \"\"\"\r\n    if value == 0:\r\n        return 0\r\n    nvalues = filter( (lambda n: n &gt;= 0), map( (lambda n: value - n), coins ) )\r\n    return 1 + min( map( coinage1, nvalues) )<\/pre>\n<p style=\"text-align: justify;\">\u00a0Wie oben ist die Abbruchbedingung, dass die 0 erreicht wurde. Falls wir noch nicht am Ende sind, berechnen wir f\u00fcr jede M\u00fcnze einen neuen Wert aus dem aktuellen Wert minus dem Wert der M\u00fcnze. Das erzeugt quasi f\u00fcr einen Knoten im Baum die jeweiligen Kindknoten. Negative Ergebnisse werden weggeworfen &#8211; dann war die M\u00fcnze zu gro\u00df f\u00fcr den Restbetrag. Anschlie\u00dfend suchen wir rekursiv weiter und geben am Ende das Minimum aus.<\/p>\n<p style=\"text-align: justify;\">Das Problem bei diesem Ansatz ist, dass er zwar elegant hingeschrieben, aber unglaublich ineffizient ist. Tats\u00e4chlich f\u00fchrt der Ansatz schon bei kleinen Betr\u00e4gen zu exorbitant langen Wartezeiten. Auf meinem Rechner habe ich die Berechnung des Betrages 202 (also zwei Euro und zwei Cent) nach f\u00fcnf Minuten abgebrochen. Tats\u00e4chlich kann man sich schnell klar machen, dass dieser Algorithmus sehr viele Varianten v\u00f6llig unn\u00f6tigerweise untersucht. Eine erste M\u00f6glichkeit dieses Verhalten zu verbessern, ist es, die Suche abzubrechen, falls man noch nicht am Ende angelangt ist, aber bei einer vorherigen Variante mit derselben Anzahl an Schritten bereits am Ende angelangt w\u00e4re. Im Bild von oben: Wenn man zun\u00e4chst den rechten Pfad gew\u00e4hlt hat (also die 5 Cent und dann die 2 Cent), dann wei\u00df man bereits, dass es eine L\u00f6sung mit zwei Schritten gibt. Es ergibt dann f\u00fcr alle weiteren potentiellen L\u00f6sungen keinen Sinn mehr, weiter als zwei Schritte zu suchen. Der folgende Algorithmus verwendet diesen Ansatz um schneller ans Ziel zu kommen. Um das noch etwas zu beschleunigen, adaptiert dieser Algorithmus noch den Greedy-Ansatz, um so schnell wie m\u00f6glich eine erste (hoffentlich kurze) L\u00f6sung zu finden. Das sieht dann wie folgt aus:<\/p>\n<pre class=\"lang:python decode:true\" title=\"Greedy-Suche, welche nicht unn\u00f6tig weit rekursiert\">def coinage3(value, limit=None):\r\n    \"\"\"\r\n        Recursive search that will prune those parts of the solution space where\r\n        a better solution has been found already.\r\n    \"\"\"\r\n    if limit is None:\r\n        limit = value\r\n\r\n    if value == 0:\r\n        return 0\r\n\r\n    scoins = sorted(coins, reverse=True)\r\n    solution = limit\r\n    for coin in scoins:\r\n        newval = value - coin\r\n        if newval &gt;= 0 and solution &gt;= 0:\r\n            newlim = 1 + coinage3(newval, solution - 1)\r\n            if newlim &lt; solution:\r\n                solution = newlim\r\n    return solution<\/pre>\n<p style=\"text-align: justify;\">\u00a0Das funktioniert schon deutlich besser. Werte im niedrigen Eurobereich werden hier halbwegs schnell gefunden. Trotzdem ist auch diese Variante noch zu langsam. Der Grund daf\u00fcr ist, dass immer noch viel zu viel berechnet wird. Statt sich Zwischenergebnisse zu merken und f\u00fcr andere Pfade weiterzuverwenden, verwirft der Algorithmus alle Ergebnisse und berechnet alles wieder neu. Dabei k\u00f6nnten einige Ergebnisse sehr hilfreich sein. Im Bild ganz oben sieht man zum Beispiel, dass die Kombination <span class=\"theme:classic lang:default decode:true  crayon-inline \">0 0<\/span> (also die M\u00f6glichkeiten zwei Cent aufzuteilen) sehr oft vorkommt. Statt dies immer wieder neu zu berechnen, w\u00e4re es viel sinnvoller, sich die Ergebnisse zu merken und in anderen Zweigen dann einfach einzusetzen.<\/p>\n<p style=\"text-align: justify;\">Hier kommt Dynamic Programming ins Spiel. Man ben\u00f6tigt eine Datenstruktur, die durch die Rekursion mitgezogen wird und dazu dient, Zwischenergebnisse zu speichern. Das dahinterliegende Konzept ist <a href=\"http:\/\/community.topcoder.com\/tc?module=Static&amp;d1=tutorials&amp;d2=dynProg\">hier<\/a> gut erkl\u00e4rt. Wenn wir das versuchen, sieht der Algorithmus etwa wie folgt aus:<\/p>\n<pre class=\"lang:default decode:true\" title=\"Recursive search with Dynamic Programming\">def coinage4(value, solutions=None):\r\n    \"\"\"\r\n        Recursive search with Dynamic Programming. This will avoid working on\r\n        the same subproblem again and again.\r\n    \"\"\"\r\n    if solutions is None:\r\n        solutions = {0: 0}\r\n\r\n    if value not in solutions:\r\n        nvalues = filter((lambda n: n &gt;= 0), map((lambda n: value - n), coins))\r\n        cfunc = lambda n: coinage4(n, solutions=solutions)\r\n        solutions[value] = 1 + min(map( cfunc, nvalues ))\r\n    return solutions[value]<\/pre>\n<p style=\"text-align: justify;\">Hier merkt sich der Algorithmus die bereits erarbeiteten L\u00f6sungen in einem Dictionary. Nur wenn das aktuelle Teilproblem noch nicht gel\u00f6st wurde, wird es tats\u00e4chlich berechnet. Ansonsten schl\u00e4gt man einfach im Dictionary nach. Diese L\u00f6sung bringt tats\u00e4chlich einen erheblichen Geschwindigkeitsschub mit sich. F\u00fcr eine finale Verbesserung k\u00f6nnen wir noch die letzten beiden Varianten kombinieren, und zus\u00e4tzlich zur Weiterverwendung bisheriger Rechenergebnisse auch die Suche abbrechen, falls wir schon einen k\u00fcrzeren Pfad kennen. Das sieht dann wie folgt aus:<\/p>\n<pre class=\"lang:python decode:true\" title=\"Kombination der beiden vorhergehenden Ans\u00e4tze\">def coinage5(value, limit=None, solutions=None):\r\n    \"\"\"\r\n        A combination of coinage4 and coinage3 - avoid searching unnecessary\r\n        parts of the solution space.\r\n    \"\"\"\r\n    if limit is None:\r\n        limit = value\r\n\r\n    if solutions is None:\r\n        solutions = {0: 0}\r\n\r\n    if value == 0:\r\n        return 0\r\n\r\n    scoins = sorted(coins, reverse=True)\r\n\r\n    if value not in solutions:\r\n        solution = limit\r\n        for coin in scoins:\r\n            newval = value - coin\r\n            if newval &gt;= 0 and solution &gt;= 0:\r\n                newlim = 1 + coinage5(newval, solution - 1, solutions)\r\n                if newlim &lt; solution:\r\n                    solution = newlim\r\n        solutions[value] = solution\r\n    return solutions[value]<\/pre>\n<p style=\"text-align: justify;\">\u00a0Diese Variante arbeitet richtig schnell. Hier treten Probleme dann vorrangig nicht mehr mit der Laufzeit, sondern mit der Rekursionstiefe auf. Jenseits der 1000 Euro wird die vorgegebene Rekursionstiefe \u00fcberschritten und der Interpreter bricht die Berechnung mit einem Fehler ab. Man kann zwar die erlaubte Rekursionstiefe erh\u00f6hen, aber letztendlich w\u00fcrde dann doch nur ein iterativer Algorithms dieses Problem l\u00f6sen.<\/p>\n<p style=\"text-align: justify;\">Interessante Tatsache am Rande: Das Problem ist eine Variante des bekannten <a href=\"http:\/\/de.wikipedia.org\/wiki\/Rucksackproblem\">Rucksack-Problems<\/a> und damit im allgemeinen Fall NP-vollst\u00e4ndig. Jedenfalls eine nette Abwechslung das mal zu l\u00f6sen. Den Code so zu erweitern, dass er rausfindet, <em>welche<\/em> M\u00fcnzkombination denn jetzt die minimale war, bleibt dann zum selber l\u00f6sen :)<\/p>\n<p>[:]<\/p>\n","protected":false},"excerpt":{"rendered":"<p>[:de] Ein interessantes Problem \u00fcber das ich heute gestolpert bin: F\u00fcr einen beliebigen Geldbetrag n &#8211; was ist die minimale Anzahl an M\u00fcnzen, mit der man diesen Geldbetrag erreichen kann? Also zum Beispiel kann man ja 10 Cent als zehn einzelne Cent-St\u00fccke, oder aber als zwei F\u00fcnf-Cent St\u00fccke, oder als ein F\u00fcnf-Cent St\u00fcck und f\u00fcnf [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[8,23,7,9,6],"class_list":["post-71","post","type-post","status-publish","format-standard","hentry","category-code","tag-algorithm","tag-code","tag-dynamic-programming","tag-np-complete","tag-python"],"_links":{"self":[{"href":"https:\/\/balanceofcowards.net\/boc_blog\/wp-json\/wp\/v2\/posts\/71","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/balanceofcowards.net\/boc_blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/balanceofcowards.net\/boc_blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/balanceofcowards.net\/boc_blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/balanceofcowards.net\/boc_blog\/wp-json\/wp\/v2\/comments?post=71"}],"version-history":[{"count":15,"href":"https:\/\/balanceofcowards.net\/boc_blog\/wp-json\/wp\/v2\/posts\/71\/revisions"}],"predecessor-version":[{"id":188,"href":"https:\/\/balanceofcowards.net\/boc_blog\/wp-json\/wp\/v2\/posts\/71\/revisions\/188"}],"wp:attachment":[{"href":"https:\/\/balanceofcowards.net\/boc_blog\/wp-json\/wp\/v2\/media?parent=71"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/balanceofcowards.net\/boc_blog\/wp-json\/wp\/v2\/categories?post=71"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/balanceofcowards.net\/boc_blog\/wp-json\/wp\/v2\/tags?post=71"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}