-1

I just wanted to know if there is any efficient and optimal algorithm for the classical problem of finding the minimum number of coins that sum up to a number S where S can be very large (up to 10^16). In this case, the coins are (2, 5, 10). The DP solution is not efficient enough in this case, so I wondered if a greedy approach may work on this specific set of coins but I am not sure.

Thank you!

kaya3
  • 47,440
  • 4
  • 68
  • 97
ahmedgu
  • 51
  • 1
  • 6
  • The provided ad'hoc solution is optimal, for this particular case. However, a well implemented backtracking solution will be rather fast, even for 10^16 input. – Damien Dec 18 '19 at 20:04

2 Answers2

4

In order to minimize the number of coins we trivially notice that to get to 20, we need two 10-coins. (and no 2-coins nor 5-coins).

More generally to get close to 10k (with k a multiple of 10), we just need 10-coins.

Now for sums which are not multiple of 10, we may want to use a maximum of 10-coins. Say S = 10k + 2. The minimum of coins is k+1. (k 10-coins, and one 2-coin).

So the goal is to get find (k,r), such that S = 10k +r, (r < 10).

We trivially do so by usage of % operator.

r = S % 10
k = S - S % 10

Now find all combination needed for 2-coins and 5-coins for every r

2=2
4=2+2
5=5
6=2+2+2
7=5+2
8=2+2+2+2
9=5+2+2
1=5+2+2+2 (%10)
3=5+2+2+2+2 (%10)

I put 1 and 3 cases at the bottom because they are special cases.

To reach 21, we need to go up to 10, then make 11 (5+2+2+2)

Same holds for 23 we can't go to 20, we need to go to 10, then make 13 (with 5+2+2+2).

The key point being to make a sum with a combination of a 2-coins and b 5-coins such that 2a + 5b = r % 10

Finally

  • for S%10=r not in {1, 3}, get to (S - (S%10))=10k with k 10-coins and complete
  • Otherwise to (S - 10 - S%10)=10(k-1) with k-1 10-coins and complete

Final note as noticed by @Iłya Bursov we can't make it for S=1 or S=3.

All others S can be reached.

grodzi
  • 5,633
  • 1
  • 15
  • 15
  • I respect your time, but can you kindly explain this in more detail as this looks me a very interesting solution. – Deepak Tatyaji Ahire Dec 18 '19 at 19:16
  • I think, everything and why we are doing so. – Deepak Tatyaji Ahire Dec 18 '19 at 19:19
  • Kindly, it's a request – Deepak Tatyaji Ahire Dec 18 '19 at 19:20
  • I don't understand the downvote. Post is correct and gives optimal solution. I think post was pretty clear for _anyone_ who looked a bit a solution. @DeepakTatyajiAhire even if you downvoted, I hope that at least you understood the solution – grodzi Dec 18 '19 at 19:29
  • @grodzi modulo approach will not work here because we don't have coin `1`, try to think what result should be for `11` for example – Iłya Bursov Dec 18 '19 at 19:31
  • @IłyaBursov The result for 11 is *literally in the answer:* it is 5+2+2+2. The modulo approach is perfectly correct, with some additional details which aren't missing from this answer. The answer is correct. – kaya3 Dec 18 '19 at 19:32
  • Thanks for reading my post and noticing failing cases @IłyaBursov. However, it is specified for big S in problem . I believe though that for S >= 10, every S can be reached – grodzi Dec 18 '19 at 19:33
  • np @DeepakTatyajiAhire what's important is that solution is clear enough to be understood to you and others – grodzi Dec 18 '19 at 19:34
  • @kaya3 proposed solution first will use `%10`, which gives 1 left – Iłya Bursov Dec 18 '19 at 19:35
  • The proposed solution treats `S%10` being 1 or 3 as a special case. – kaya3 Dec 18 '19 at 19:36
  • @kaya3 ok, now I see this – Iłya Bursov Dec 18 '19 at 19:37
  • I have edited post @IłyaBursov to make it clearer hopefully it is now clear enough – grodzi Dec 18 '19 at 19:43
  • @grodzi Thank you very much for your answer, it's clear and responds to what I am looking for. I had the same approach in my mind, I just didn't know if it was always correct (thanks for the two special cases of r = {1, 3}, that's what I was missing). – ahmedgu Dec 18 '19 at 21:04
4

Here's a solution generalizing @grodzi's, in Python. The solution depends on the coin denominations not having a common factor; if they do, you adjust the solution by dividing everything by the highest common factor, and rejecting inputs where that division has a remainder.

For sufficiently large inputs, every sum is possible. "Sufficiently large" in this case means after the first run of c sums that are possible, where c is the largest coin denomination. We can do dynamic programming to compute solutions up until a run of length c is found, and then every sum can be solved by taking the appropriate number of coins of denomination c away, reducing the sum to within this range.

The time complexity of the initialisation stage is at most O(f(coins) * len(coins)) where the function f gives the Frobenius number of the set of coin denominations. The time complexity of the make_change method is O(len(coins)) plus the complexity of doing the integer division and remainder operations, which would be O(1) in a language with bounded integers.

from collections import Counter

class ChangeMaker:
    def __init__(self, *denominations):
        denominations = sorted(denominations, reverse=True)
        self.c = denominations[0]
        self.cache = [Counter()]

        def solve(n):
            for d in denominations:
                if d <= n and self.cache[n - d] is not None:
                    return Counter({ d: 1 }) + self.cache[n - d]
            return None

        run_length = 0
        while run_length < self.c:
            r = solve(len(self.cache))
            self.cache.append(r)
            if r is not None:
                run_length += 1
            else:
                run_length = 0

    def make_change(self, n):
        if n < len(self.cache):
            return self.cache[n]
        else:
            diff = n - len(self.cache) + self.c
            div = diff // self.c
            rem = diff % self.c
            cached = self.cache[len(self.cache) - self.c + rem]
            return Counter({ self.c: div }) + cached

Example:

>>> c = ChangeMaker(2, 5, 10)
>>> c.cache
[Counter(),
 None,
 Counter({2: 1}),
 None,
 Counter({2: 2}),
 Counter({5: 1}),
 Counter({2: 3}),
 Counter({5: 1, 2: 1}),
 Counter({2: 4}),
 Counter({2: 2, 5: 1}),
 Counter({10: 1}),
 Counter({2: 3, 5: 1}),
 Counter({10: 1, 2: 1}),
 Counter({2: 4, 5: 1})]
>>> c.make_change(123456789011)
Counter({10: 12345678900, 2: 3, 5: 1})
>>> c.make_change(123456789013)
Counter({10: 12345678900, 2: 4, 5: 1})
kaya3
  • 47,440
  • 4
  • 68
  • 97
  • 1
    I don't know either but that's neat. Likely someone did not like you gave an already coded solution – grodzi Dec 18 '19 at 20:20
  • 1
    I thought it would be useful to give a coded solution, since there was debate over whether your solution is correct, and this irrefutably demonstrates that it is. – kaya3 Dec 18 '19 at 20:42
  • @kaya3 Thank you for your explanation and I appreciate your effort for providing a code sample :D – ahmedgu Dec 18 '19 at 21:11