The Coin Problem

Developed by Hardeep Singh
Copyright © Hardeep Singh, 2000 - 2008
All rights reserved.
The code may not be used commercially without permission.
The code does not come with any warranties, explicit or implied.
The code cannot be distributed without this header.

Let us say we have an amount we have to pay someone: say $97. The given currency has notes worth $1, $5, $10 and $50. Now, what is the minimum number of currency notes with which the amount can be made? In this case, its eight: two $1 notes, a $5 note, four $10 notes and a $50 notes - eight in all.

If a computer had to solve the same problem, how would it do it? Let us see below. ‘Denomination’ refers to the list of available currency notes: $1, $5, $10 and $50 in this case - each note is a denomination.

Problem: We are given a set of denominations d1,d2,d3,...,dn in increasing order. (Without loss of generality) we assume also that d1=1 (that is, there is always a $1 note, and that is the first denomination) so that it is always possible to produce all amounts from the given denominations. We are also given an amount of money amt. We must use a minimum number of coins (or currency notes) of the given denominations to produce amt.

Solution #1: The naive approach (Greedy algorithm)

The easiest way to approach the solution is to divide amt by the coin with the largest denomination (dn). Let us say the number is k1. Now, whatever remains must be divided again by dn-1 and so on. Thus k1+k2+...+kn will be the result we seek, the minimum number of coins with which the amount can be made. This approach is known as the Greedy approach to problem solving. The following procedure solves the problem in this fashion:

PROCEDURE findcoins1(d:denom; n,amt:integer; VAR c:counts);
  i:=n;                            (* start with the largest coin *)
  WHILE amt>0 DO
    c[i]:=trunc(amt/d[i]);         (* get the number of coins required *)
    amt:=amt-c[i]*d[i];            (* this amount remains *)
  WHILE i>=1 DO                    (* make the numbers of other coins 0 *)

Both denom and counts are declared to be arrays of integers [1..1000]. Although this method works for the Indian currency, consider the set of denominations {1,10,30,40}. Also consider an amount 60. Based on this approach, we will divide the amount 60, first by 40 (because that is the largest denomination) to get 1 coin, then the remainder 20 by 30, to get none. Again, 20 will be divided by 10 to get 2 coins. Thus the problem will be solved by using 2 denomination 20 coins and a denomination 40 coin. However, we can solve the problem by using only 2 coins of denomination 30. Hence the algorithm is not optimal.

This problem can arise only for certain denomination sets. If the set has at least one set of denominations (di,dj) (j>i) such that di<dj and 2*di>dj then it can have some values of amt for which the above algorithm fails.

Always dividing first by the largest coin gets us an optimal number of coins in most but not all cases. Had we divided first by the coin of denomination 30 first, we would have got the optimal result. However, how to decide which coin to use first in which case?

There is no uniform way to answer this question, hence this approach must be discarded. For the denomination set given above and an amount 70, dividing by 30 first solves the problem in 3 coins(30,30,10) whereas it can be solved only in 2 coins(40,30) if we divide by 40 first.

Solution #2: Recursive approach

The other way to solve the problem is by splitting the amount into smaller amounts and then producing those smaller amounts using the minimum number of coins. Let us consider amount 60 and denomination set {1,10,30,40}. The amount can be written as (59+1),(50+10),(30+30) or (20+40). Consider a function howmany(amt) that finds the minimum number of coins required to make an amount amt. If howmany(amt) is called, it finds the minimum of howmany(59), howmany(50), howmany(30) and howmany(20), adds 1 to the result and gives this as the value of the function. This function is given below:

FUNCTION howmany(d:denom; n,amt:integer; VAR w:integer):integer;
  flag:=false;                         (* atleast one denomination found *)
  out:=false;                          (* the sum has been constructed *)
  i:=n;                                (* start with the largest coin *)
    IF (amt-d[i]>0) THEN               (* can d[i] be used? *)
      t:=howmany(d,n,amt-d[i],v);      (* get the number of coins if d[i] is used *)
      IF (NOT(flag)) OR (t<min-1) THEN (* accept if smaller than earlier values *)
        value:=i;                      (* store the value and number of coins *)
        flag:=true;                    (* flag that a denomination is found *)
    ELSE IF (amt-d[i]=0) THEN          (* if the amount has been made *)
      out:=true;                       (* terminate looping *)
  UNTIL (i=0) OR (out);

All that the program has to do is to find the numbers of coins based on the results of howmany:

PROCEDURE findcoins2(d:denom; n,amt:integer; VAR c:counts);
  FOR i:=1 TO n DO                     (* initialise to zero *)
    j:=howmany(d,n,amt,m);             (* find out howmany coins & which *)
    c[m]:=c[m]+1;                      (* increment the number of used coin *)
    amt:=amt-d[m];                     (* calculate amount left *)
  UNTIL (amt=0);                       (* until amt=0 *)

This approach, although flawless, takes a lot of time. Consider {1,3,10,30,75} and amt=45. It took 20 seconds flat on my Pentium II 350 Mhz system.

The reason it took so much time is that howmany is calculated for the same amount more than once. For instance, in the above example, howmany(20) & howmany(30) are calculated once during calculation of howmany(60) and howmany(20) is calculated again during calculation of howmany(30). In short, the same calculation is being carried out more than once. Let us see how things can be improved.

Solution #2: Dynamic Programming

A way to speed things up is by storing values of howmany(amt) that have been calculated in a table and reusing them later. This method of programming is called Dynamic Programming (the word dynamic, as used here, has nothing to do with memory allocation).

Dynamic programming therefore is a top-down approach: split the larger problem into smaller ones, solve the smaller ones (putting away the solution for each smaller problem somewhere so that it can be used again in case needed later), and combine to produce the solution to the larger problem. We did the same in the previous solution, except that we did not put away solutions for reuse - we ‘recalculated’ on demand!

The following procedure demonstrates this:

PROCEDURE findcoins3(d:denom; n,amt:integer; VAR c:counts);
  minnum,coin:ARRAY[0..1000] of integer;
  minnum[0]:=0;   (* No coins are reqd to construct amount 0 *)
  FOR i:=1 TO amt DO
    min:=minnum[i-d[1]]; (* Consider 1st denomination as lowest coin making *)
    FOR j:=2 TO n DO     (* Go through other denominations *)
      IF (i-d[j]>=0) AND (minnum[i-d[j]]<min) THEN (* If fewer coins needed *)
        min:=minnum[i-d[j]]; (* Store this denomination *)
    minnum[i]:=min+1;        (* Store the minimum denomination *)
  FOR i:=1 TO n DO           (* Initialise c to zeros *)
  WHILE (amt>0) DO
    c[coin[amt]]:=c[coin[amt]]+1; (* Calculate & store the number of coins of each den. *)

The procedure uses two local arrays. The array minnum stores in each cell, the minimum number of coins required to construct the amount denoted by the index of the cell (that is minnum[i] stores the minimum number of coins required to make amount i). The array coin stores the coin of largest denomination that is used to construct the amount of the index. Thus, minnum[0] & coin[0] are initialised to zero. Thereafter, minnum[i] & coin[i] are calculated for each i from 1 to amt. The improvement in speed is considerable. Consider again {1,3,10,30,75} and amt=45. This procedure prints the results immediately. However, this procedure requires extra space proportional to the amount. This may not always be possible. However, this algorithm is linear in time whereas findcoins2 is exponential.

Dynamic programming is used for a lot of other problems, the popular ones among those being word wrapping (optimally wrapping words on a line to fit the margins) and ‘Duckworth-Lewis method’ used to decided interrupted Cricket matches, for example by rain.

Note to any implementation using procedures defined here: all procedures accept input denominations starting at 1 (so first denomination always 1) and in increasing order thereafter. The input needs to be validated prior to passing to the procedures, and is the responsibility of the calling program.