Finding shortest combinations in array/sequence that equals sum
Asked Answered
O

5

6

I'm totally stuck and have no idea how to go about solving this. Let's say I've an array

arr = [1, 4, 5, 10]

and a number

n = 8

I need shortest sequence from within arr which equals n. So for example following sequences within arr equals n

c1 = 5,1,1,1
c2 = 4,4
c3= 1,1,1,1,1,1,1,1

So in above case, our answer is c2 because it's shortest sequences in arr that equals sum.

I'm not sure what's the simplest way of finding a solution to above? Any ideas, or help will be really appreciated.

Thanks!

Edited:

  • Fixed the array
  • Array will possibly have postive values only.
  • I'm not sure how subset problem fixes this, probably due to my own ignorance. Does sub-set algorithm always give the shortest sequence that equals sum? For example, will subset problem identify c2 as the answer in above scenario?
Ogren answered 1/4, 2012 at 12:13 Comment(6)
en.wikipedia.org/wiki/Subset_sum_problemResponsible
What are the characteristics of the array? Can it contain negative numbers?Houdini
Duplicate of https://mcmap.net/q/669976/-subset-sum-problem and https://mcmap.net/q/656187/-fast-solution-to-subset-sum and https://mcmap.net/q/656187/-fast-solution-to-subset-sum and https://mcmap.net/q/333225/-subset-sum-algorithm and many more but I'm out of votes.Frostbitten
Hey Eli, I'm not sure how subset problem fixes this, probably due to my own ignorance. Does sub-set algorithm always give the shortest sequence that equals sum? For example, will subset problem identify c2 as the answer in above scenario?Ogren
@Ogren Does the sequence have to be continuous? Given n=8 and arr=[3,2,5,1,2] can I pick [3,5] (skipping 2) or is only [5,1,2] admissible here?Ziguard
Hey Rafal, in your example you can pick [3,5]. Any shortest sequence that whose sum equals the 'n'. So, yes, in your example you can pick [3,5]Ogren
T
3

As has been pointed before this is the minimum change coin problem, typically solved with dynamic programming. Here's a Python implementation solved in time complexity O(nC) and space complexity O(C), where n is the number of coins and C the required amount of money:

def min_change(V, C):
    table, solution = min_change_table(V, C)
    num_coins, coins = table[-1], []
    if num_coins == float('inf'):
        return []
    while C > 0:
        coins.append(V[solution[C]])
        C -= V[solution[C]]
    return coins

def min_change_table(V, C):
    m, n = C+1, len(V)
    table, solution = [0] * m, [0] * m
    for i in xrange(1, m):
        minNum, minIdx = float('inf'), -1
        for j in xrange(n):
            if V[j] <= i and 1 + table[i - V[j]] < minNum:
                minNum = 1 + table[i - V[j]]
                minIdx = j
        table[i] = minNum
        solution[i] = minIdx
    return (table, solution)

In the above functions V is the list of possible coins and C the required amount of money. Now when you call the min_change function the output is as expected:

min_change([1,4,5,10], 8)
> [4, 4]
Thallus answered 1/4, 2012 at 17:26 Comment(0)
C
3

For the benefit of people who find this question in future -

As Oscar Lopez and Priyank Bhatnagar, have pointed out, this is the coin change (change-giving, change-making) problem.

In general, the dynamic programming solution they have proposed is the optimal solution - both in terms of (provably!) always producing the required sum using the fewest items, and in terms of execution speed. If your basis numbers are arbitrary, then use the dynamic programming solution.

If your basis numbers are "nice", however, a simpler greedy algorithm will do.

For example, the Australian currency system uses denominations of $100, $50, $20, $10, $5, $2, $1, $0.50, $0.20, $0.10, $0.05. Optimal change can be given for any amount by repeatedly giving the largest unit of change possible until the remaining amount is zero (or less than five cents.)

Here's an instructive implementation of the greedy algorithm, illustrating the concept.

def greedy_give_change (denominations, amount):        
    # Sort from largest to smallest
    denominations = sorted(denominations, reverse=True)

    # number of each note/coin given
    change_given = list()

    for d in denominations:
        while amount > d:
            change_given.append(d)
            amount -= d

    return change_given

australian_coins = [100, 50, 20, 10, 5, 2, 1, 0.50, 0.20, 0.10, 0.05]
change = greedy_give_change(australian_coins, 313.37)
print (change)           # [100, 100, 100, 10, 2, 1, 0.2, 0.1, 0.05]
print (sum(change))      # 313.35

For the specific example in the original post (denominations = [1, 4, 5, 10] and amount = 8) the greedy solution is not optimal - it will give [5, 1, 1, 1]. But the greedy solution is much faster and simpler than the dynamic programming solution, so if you can use it, you should!

Condign answered 1/4, 2012 at 17:46 Comment(9)
I think you can combine these: if the amount is larger than the LCM of the coins you can greedily use the largest coin, and after that use the DP solution. that's both fast and gives optimal solution.Marble
@KarolyHorvath: I'm actually not sure what the precise criteria are for the greedy solution to be optimal, which is why I didn't suggest this. I believe it may have something to do with the ratio between coin sizes - note how in the Australian currency system each coin is at least twice as large as the previous one - but I haven't thought of a proof or refutation of this theory.Condign
it's precisely the LCM.. the LCM of the n smallest coins is either the nth coin itself or the n+1th one.Marble
@KarolyHorvath: I'm not sure about that - 50 isn't a LCM of [20,10,5]. (The lowest common multipliers are [20,40,60,...]) Yet the greedy method works for the Australian currency system just fine.Condign
sigh LCM of [20,10,5] is 20 (nth). that's just what I said.Marble
@KarolyHorvath : I misphrased that slightly. Your heuristic doesn't work because it would invalidate [50,20,10,5] (the LCM of that set is 100.)Condign
It turns out that this problem is surprisingly hard.. Given a set of denominations {d}, there is no known way to determine if they admit a greedy solution to the coin-changing problem.Condign
and that's the n+1th... and NO, nobody said anything like that in that question.... read it again.Marble
@KarolyHorvath: Ok, I'm wrong on the LCM (maybe I need to go to sleep.) I will admit that your LCM criteria may be a sufficient condition to determine whether a coin system admits a greedy solution. However it's not a sufficient and necessary condition , i.e. doesn't identify all coin systems which admit the greedy solution - see the link. So a good optimisation if provably correct, but not the be-all end-all. [And no, this no longer has anything to do with the original question; but that shouldn't stand in the way of science!]Condign
E
2

This is problem is known as Minimum coin change problem.

You can solve it by using dynamic programming. Here is the pseudo code :

Set MinCoin[i] equal to Infinity for all of i
MinCoin[0] = 0

For i = 1 to N // The number N
For j = 0 to M - 1 // M denominations given
// Number i is broken into i-Value[j] for which we already know the answer
// And we update if it gives us lesser value than previous known.
   If (Value[j] <= i and MinCoin[i-Value[j]]+1 < MinCoin[i])
       MinCoin[i] = MinCoin[i-Value[j]]+1

Output MinCoin[N]
Erleena answered 1/4, 2012 at 17:6 Comment(0)
V
1

This is an variant of subset-sum problem. In your problem, you can pick an item several times. You still can use a similar idea to solve this problem by using the dynamic prorgamming technique. The basic idea is to design a function F(k, j), such that F(k, j) = 1 means that there is a sequence from arr whose sum is j and length is k.

Formally, the base case is that F(k, 1) = 1, if there exists an i, such that arr[i] = k. For inductive case, F(k, j) = 1, if there exists an i, such that arr[i] = m, and F(k-1, j-m) = 1.

The smallest k with F(k, n) = 1 is the length of the shortest sequence you want.

By using the dynamic programming technique, you can compute function F without using recursion. By tracking additional information for every F(k, j), you also can reconstruct the shortest sequence.

Viipuri answered 1/4, 2012 at 13:5 Comment(0)
S
1

What you're trying to solve is a variant of the coin change problem. Here you're looking for smallest amount of change, or the minimum amount of coins that sum up to a given amount.

Consider a simple case where your array is

c = [1, 2, 3]

you write 5 as a combination of elements from C and want to know what is the shortest such combination. Here C is the set of coin values and 5 is the amount for which you want to get change.

Let's write down all possible combinations:

1 + 1 + 1 + 1 + 1
1 + 1 + 1 + 2
1 + 2 + 2
1 + 1 + 3
2 + 3

Note that two combinations are the same up to re-ordering, so for instance 2 + 3 = 3 + 2.

Here there is an awesome result that's not obvious at first sight but it's very easy to prove. If you have any sequence of coins/values that is a sequence of minimum length that sums up to a given amount, no matter how you split this sequence the two parts will also be sequences of minimum length for the respective amounts.

For instance if c[3] + c[1] + c[2] + c[7] + c[2] + c[3] add up to S and we know that 6 is the minimal length of any sequence of elements from c that add up to S then if you split

                              |
S = c[3] + c[1] + c[2] + c[7] | + c[2] + c[3]
                              |

you have that 4 is the minimal length for sequences that add up to c[3] + c[1] + c[2] + c[7] and 2 the minimal length for sequences that add up to c[2] + c[3].

                              |
S = c[3] + c[1] + c[2] + c[7] | + c[2] + c[3] 
                              |
  =        S_left             +     S_right

How to prove this? By contradiction, assume that the length of S_left is not optimal, that is there's a shorter sequence that adds up to S_left. But then we could write S as a sum of this shorter sequence and S_right, thus contradicting the fact that the length of S is minimal. □

Since this is true no matter how you split the sequence, you can use this result to build a recursive algorithm that follows the principles of dynamic programming paradigm (solving smaller problems while possibly skipping computations that won't be used, memoization or keeping track of computed values, and finally combining the results).

Because of this property of maintaining optimality for subproblems, the coins problem is also said to "exhibit optimal substructure".

OK, so in the small example above this is how we would go about solving the problem with a dynamic programming approach: assume we want to find the shortest sequence of elements from c = [1, 2, 3] for writing the sum 5. We solve the subproblems obtained by subtracting one coin: 5 - 1, 5 - 2, and 5 - 3, we take the smallest solution of these subproblems and add 1 (the missing coin).

So we can write something like

shortest_seq_length([1, 2, 3], 5) = 
    min( shortest_seq_length([1, 2, 3], 5-1),
         shortest_seq_length([1, 2, 3], 5-2),
         shortest_seq_length([1, 2, 3], 5-3)
        ) + 1

It is convenient to write the algorithm bottom-up, starting from smaller values of the sums that can be saved and used to form bigger sums. We just solve the problem for all possible values starting from 1 and going up to the desired sum.

Here's the code in Python:

def shortest_seq_length(c, S):
    res = {0: 0} # res contains computed results res[i] = shortest_seq_length(c, i)
    for i in range(1, S+1):
        res[i] = min([res[i-x] for x in c if x<=i]) + 1 
    return res[S]

Now this works except for the cases when we cannot fill the memoization structure for all values of i. This is the case when we don't have the value 1 in c, so for instance we cannot form the sum 1 if c = [2, 5] and with the above function we get

shortest_seq_length([2, 3], 5)
# ValueError: min() arg is an empty sequence

So to take care of this issue one could for instance use a try/catch:

def shortest_seq_length(c, S):
    res = {0: 0} # res contains results for each sum res[i] = shortest_seq_length(c, i)
    for i in range(1, S+1):
        try:
            res[i] = min([res[i-x] for x in c if x<=i and res[i-x] is not None]) +1
        except:
            res[i] = None # takes care of error when [res[i-x] for x in c if x<=i] is empty
    return res[S]

Or without try/catch:

def shortest_seq_length(c, S):
    res = {0: 0} # res[i] = shortest_seq_length(c, i)
    for i in range(1, S+1):
        prev = [res[i-x] for x in c if x<=i and res[i-x] is not None]
        if len(prev)>0:
            res[i] = min(prev) +1
        else:
            res[i] = None # takes care of error when [res[i-x] for x in c if x<=i] is empty
    return res[S]

Try it out:

print(shortest_seq_length([2, 3], 5))
# 2
print(shortest_seq_length([1, 5, 10, 25], 37))
# 4
print(shortest_seq_length([1, 5, 10], 30))
# 3
print(shortest_seq_length([1, 5, 10], 25))
# 3
print(shortest_seq_length([1, 5, 10], 29))
# 7
print(shortest_seq_length([5, 10], 9))
# None

To show not only the length but also the combinations of coins of minimal length:

from collections import defaultdict
def shortest_seq_length(coins, sum):
    combos = defaultdict(list)
    combos[0] = [[]]
    for i in range(1, sum+1):
        for x in coins:
            if x<=i and combos[i-x] is not None:
                for p in combos[i-x]:
                    comb = sorted(p + [x])
                    if comb not in combos[i]:
                        combos[i].append(comb)
        if len(combos[i])>0:
            m = (min(map(len,combos[i])))
            combos[i] = [combo for i, combo in enumerate(combos[i]) if len(combo) == m]
        else:
            combos[i] = None
    return combos[sum]

total = 9
coin_sizes = [10, 8, 5, 4, 1]
shortest_seq_length(coin_sizes, total)
# [[1, 8], [4, 5]]

To show all sequences remove the minumum computation:

from collections import defaultdict
def all_seq_length(coins, sum):
    combos = defaultdict(list)
    combos[0] = [[]]
    for i in range(1, sum+1):
        for x in coins:
            if x<=i and combos[i-x] is not None:
                for p in combos[i-x]:
                    comb = sorted(p + [x])
                    if comb not in combos[i]:
                        combos[i].append(comb)
        if len(combos[i])==0:
            combos[i] = None
    return combos[sum]

total = 9
coin_sizes = [10, 5, 4, 8, 1]
all_seq_length(coin_sizes, total)
# [[4, 5],
#  [1, 1, 1, 1, 5],
#  [1, 4, 4],
#  [1, 1, 1, 1, 1, 4],
#  [1, 8],
#  [1, 1, 1, 1, 1, 1, 1, 1, 1]]

One small improvement to the algorithm is to skip the step of computing the minimum when the sum is equal to one of the values/coins, but this can be done better if we write a loop to compute the minimum. This however doesn't improve the overall complexity that's O(mS) where m = len(c).

Setscrew answered 3/3, 2018 at 11:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.