1

I'm trying to figure out how to solve a problem that seems a tricky variation of a common algorithmic problem but require additional logic to handle specific requirements.

Given a list of coins and an amount, I need to count the total number of possible ways to extract the given amount using an unlimited supply of available coins (and this is a classical change making problem https://en.wikipedia.org/wiki/Change-making_problem easily solved using dynamic programming) that also satisfy some additional requirements:

  • extracted coins are splittable into two sets of equal size (but not necessarily of equal sum)
  • the order of elements inside the set doesn't matter but the order of set does.

Examples

Amount of 6 euros and coins [1, 2]: solutions are 4

[(1,1), (2,2)]
[(1,1,1), (1,1,1)]
[(2,2), (1,1)]
[(1,2), (1,2)]

Amount of 8 euros and coins [1, 2, 6]: solutions are 7

[(1,1,2), (1,1,2)]
[(1,2,2), (1,1,1)]
[(1,1,1,1), (1,1,1,1)]
[(2), (6)]
[(1,1,1), (1,2,2)]
[(2,2), (2,2)]
[(6), (2)]

By now I tried different approaches but the only way I found was to collect all the possible solution (using dynamic programming) and then filter non-splittable solution (with an odd number of coins) and duplicates. I'm quite sure there is a combinatorial way to calculate the total number of duplication but I can't figure out how.

2 Answers2

1

(The following method first enumerates partitions. My other answer generates the assignments in a bottom-up fashion.) If you'd like to count splits of the coin exchange according to coin count, and exclude redundant assignments of coins to each party (for example, where splitting 1 + 2 + 2 + 1 into two parts of equal cardinality is only either (1,1) | (2,2), (2,2) | (1,1) or (1,2) | (1,2) and element order in each part does not matter), we could rely on enumeration of partitions where order is disregarded.

However, we would need to know the multiset of elements in each partition (or an aggregate of similar ones) in order to count the possibilities of dividing them in two. For example, to count the ways to split 1 + 2 + 2 + 1, we would first count how many of each coin we have:

Python code:

def partitions_with_even_number_of_parts_as_multiset(n, coins):
  results = []

  def C(m, n, s, p):
    if n < 0 or m <= 0:
      return

    if n == 0:
      if not p:
        results.append(s)
      return

    C(m - 1, n, s, p)

    _s = s[:]
    _s[m - 1] += 1

    C(m, n - coins[m - 1], _s, not p)

  C(len(coins), n, [0] * len(coins), False)

  return results

Output:

=> partitions_with_even_number_of_parts_as_multiset(6, [1,2,6])
=> [[6, 0, 0], [2, 2, 0]]
                ^ ^ ^ ^ this one represents two 1's and two 2's

Now since we are counting the ways to choose half of these, we need to find the coefficient of x^2 in the polynomial multiplication

(x^2 + x + 1) * (x^2 + x + 1) = ... 3x^2 ...

which represents the three ways to choose two from the multiset count [2,2]:

2,0 => 1,1
0,2 => 2,2
1,1 => 1,2

In Python, we can use numpy.polymul to multiply polynomial coefficients. Then we lookup the appropriate coefficient in the result.

For example:

import numpy    

def count_split_partitions_by_multiset_count(multiset):
  coefficients = (multiset[0] + 1) * [1]

  for i in xrange(1, len(multiset)):
    coefficients = numpy.polymul(coefficients, (multiset[i] + 1) * [1])

  return coefficients[ sum(multiset) / 2 ]

Output:

=> count_split_partitions_by_multiset_count([2,2,0])
=> 3

(Posted a similar answer here.)

גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
  • Thanks a lot! Using this technique I was able to speed up the algorithm by an order of magnitude! Anyway, it's still slow for bigger input: with 7 coins [1,2,6,12,24,48,60] I'm able to compute up to an amount of 500 within an hour. I'm quite sure there is a way to count the ways to choose half of the coins or at least to cache part of the result set and reduce complexity again. – Andrea Mostosi Feb 23 '18 at 17:50
  • @AndreaMostosi I think we might be able to. A general algorithm might be: `for a,b, where a + b = n; count partitions of a with m parts + partitions of b with m parts, for m = 1 to min(a, b)` I'll see if I can come up with something later. Literature for counting partitions with a restricted number of parts is readily available. – גלעד ברקן Feb 23 '18 at 20:46
  • @AndreaMostosi added an answer implementing the idea just above. – גלעד ברקן Feb 24 '18 at 18:02
0

Here is a table implementation and a little elaboration on algrid's beautiful answer. This produces an answer for f(500, [1, 2, 6, 12, 24, 48, 60]) in about 2 seconds.

The simple declaration of C(n, k, S) = sum(C(n - s_i, k - 1, S[i:])) means adding all the ways to get to the current sum, n using k coins. Then if we split n into all ways it can be partitioned in two, we can just add all the ways each of those parts can be made from the same number, k, of coins.

The beauty of fixing the subset of coins we choose from to a diminishing list means that any arbitrary combination of coins will only be counted once - it will be counted in the calculation where the leftmost coin in the combination is the first coin in our diminishing subset (assuming we order them in the same way). For example, the arbitrary subset [6, 24, 48], taken from [1, 2, 6, 12, 24, 48, 60], would only be counted in the summation for the subset [6, 12, 24, 48, 60] since the next subset, [12, 24, 48, 60] would not include 6 and the previous subset [2, 6, 12, 24, 48, 60] has at least one 2 coin.

Python code (see it here; confirm here):

import time

def f(n, coins):
  t0 = time.time()

  min_coins = min(coins)

  m = [[[0] * len(coins) for k in xrange(n / min_coins + 1)] for _n in xrange(n + 1)]

  # Initialize base case
  for i in xrange(len(coins)):
    m[0][0][i] = 1

  for i in xrange(len(coins)):
    for _i in xrange(i + 1):
      for _n in xrange(coins[_i], n + 1):
        for k in xrange(1, _n / min_coins + 1):
          m[_n][k][i] += m[_n - coins[_i]][k - 1][_i]

  result = 0

  for a in xrange(1, n + 1):
    b = n - a

    for k in xrange(1, n / min_coins + 1):
      result = result + m[a][k][len(coins) - 1] * m[b][k][len(coins) - 1]

  total_time = time.time() - t0

  return (result, total_time)

print f(500, [1, 2, 6, 12, 24, 48, 60])
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
  • Thanks for the effort! :) This time is not clear to me how your approach should work. It seems quite similar than the previous in term of complexity. Is about 100 times faster but seems still unable to compute bigger amount with a minute. People who proposed me this problem say is possible to compute solution for 4800 within a minute (I still don't know how). Moreover, this specific implementation gives wrong answers but I'm quite sure it depends on some little problems with indexes. – Andrea Mostosi Feb 26 '18 at 18:42
  • @AndreaMostosi ah, interesting. Thank you for letting me know about larger possibility. If you have a relatively small example with a wrong answer, please let me know. It would help me debug the mistake. – גלעד ברקן Feb 26 '18 at 19:04
  • @AndreaMostosi I would recommend posting the question also on math.stackexchange.com where there may be more experts in combinatorics. – גלעד ברקן Feb 26 '18 at 19:07
  • @AndreaMostosi also, please let us know (maybe add this information to the question?) the coin configuration for your example with 4800, the expected result, and how long it should take to calculate. Are you sure those large examples are not for traditional partitions, where the set of sizes (coins) is restricted in known ways rather than arbitrary? – גלעד ברקן Feb 26 '18 at 19:10
  • Unfortunately, I have no information about the expected result for bigger input and I already posted on math.stackexchange.com but nobody answers yet https://math.stackexchange.com/questions/2661689/change-making-problem-with-split-into-two-sets – Andrea Mostosi Mar 01 '18 at 06:27
  • About the second implementation, results start to diverge with input 36: f(36, [60, 48, 24, 12, 6, 2, 1]) => 669 and should be 671. 37 is 738 instead of 742, 38 is 837 instead of 843 and so on. – Andrea Mostosi Mar 01 '18 at 06:35
  • @גלע- ברקן As far as I know, the problem is designed for 7 coins ([60, 48, 24, 12, 6, 2, 1]), take an amount as input and as output gives the total number of possible combinations splittable into 2 equally sized set. No more constraints. Can you suggest me anything in literature that introduce to this problem? I have no background about counting partitions except for dynamic programming approach. Many thanks! – Andrea Mostosi Mar 01 '18 at 07:33
  • @AndreaMostosi thanks, I see it's more complicated than I thought because of inclusion-exclusion. This article might be relevant http://www.math.drexel.edu/~phitczen/polypartprinted.pdf – גלעד ברקן Mar 02 '18 at 18:13