Related
How can we change this recursive function to "for iteration"?.....
Note: Greed algorithms should not be used. Greedy algorithms are less accurate.
Reason to change : To increase the efficiency of a function by changing to a dynamic algorithm
What I've tried:
# this code is trash
for i in range(1,m+1):
n1=m-coins[i]
for i in range(1,m+1):
n2=m-coins[i]
return min(n1,n2)
while n1==0:
i=i+1
n1=m-coins[i]
return n1
Besides this, I tried using dictionary and combination, but I forgot because I thought it wouldn't work.
How the function behaves : It is a function that takes a list of coins and a value of m and makes m==0.
n1 means moving on without using coins[0].
n2 means using coins[0] as a necessity and moving on to the next level.
If I had a list of [5,4,2,1] and a value of 10 m, I would recurs the first n1=[4,2,1],10 and the first n2=[5,4,2,1],10.
coins =[50,40,20,10,5,4,2,1]
m = 80
def coin_count(coins, m):
if m > 0:
while len(coins) > 0 and m < coins[0]:
coins = coins[1:]
#Below is the part I want to change.
if coins[0] > 1:
n1=coin_count(coins[1:],m)
n2=coin_count(coins,m-coins[0])+1
return min(n1,n2)
else: # coins[0] == 1
return m
#Above is the part I want to change
else: # m == 0
return 0
Here is my solution to your function using a while loop. It returns the smallest possible list of coins that sums up to m - I suppose this is what you were looking for?
How it works is that in each while loop iteration, it takes the current largest coin and checks if the remainder of m is divisible by that coin, adding the number of coins needed and setting m to the remainder
def coin_count(coins, m):
coins = sorted(coins, reverse=True)
out = []
while m > 0:
cur_coin = coins.pop(0)
out.extend([cur_coin] * (m // cur_coin))
m %= cur_coin
return out
NB: If the input coins are always sorted in descending order, you can remove the first line.
Example:
coins = [50, 40, 20, 10, 5, 4, 2, 1]
m = 128
print(coin_count(coins, m))
[50, 50, 20, 5, 2, 1]
I am new to python, and I was wondering if I could generate the fibonacci series using python's list comprehension feature. I don't know how list comprehensions are implemented.
I tried the following (the intention was to generate the first five fibonacci numbers):
series=[]
series.append(1)
series.append(1)
series += [series[k-1]+series[k-2] for k in range(2,5)]
This piece of code throws the error: IndexError: list index out of range.
Let me know if it is even possible to generate such a series using a list comprehension.
You cannot do it like that: the list comprehension is evaluated first, and then that list is added to series. So basically it would be like you would have written:
series=[]
series.append(1)
series.append(1)
temp = [series[k-1]+series[k-2] for k in range(2,5)]
series += temp
You can however solve this by using list comprehension as a way to force side effects, like for instance:
series=[]
series.append(1)
series.append(1)
[series.append(series[k-1]+series[k-2]) for k in range(2,5)]
Note that we here do not add the result to series. The list comprehension is only used such that .append is called on series. However some consider list comprehensions with side effects rather error prone: it is not very declarative and tends to introduce bugs if not done carefully.
We could write it as a clean Python list comprehension (or generator) using it's relationship to the golden ratio:
>>> series = [int((((1 + 5**0.5) / 2)**n - ((1 - 5**0.5) / 2)**n) / 5**0.5) for n in range(1, 21)]
>>> series
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]
>>>
or a little more nicely as:
>>> square_root_of_five = 5**0.5
>>> Phi = (1 + square_root_of_five) / 2
>>> phi = (1 - square_root_of_five) / 2
>>>
>>> series = [int((Phi**n - phi**n) / square_root_of_five) for n in range(1, 21)]
>>> series
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]
If you know how many terms of the series you will need then you can write the code compactly without a list comprehension like this.
def Fibonacci(n):
f0, f1 = 1, 1
for _ in range(n):
yield f0
f0, f1 = f1, f0+f1
fibs = list(Fibonacci(10))
print (fibs)
If you want some indefinite number of terms then you could use this, which is very similar.
def Fibonacci():
f0, f1 = 1, 1
while True:
yield f0
f0, f1 = f1, f0+f1
fibs = []
for f in Fibonacci():
fibs.append(f)
if f>100:
break
print (fibs)
When you need a potentially infinite collection of items you should perhaps consider either a function with one or more yield statements or a generator expression. I'd love to be able to make Fibonacci numbers with a generator expression but apparently one can't.
Using Assignment Expression (python >= 3.8):
s = [0, 1]
s += [(s := [s[1], s[0] + s[1]]) and s[1] for k in range(10)]
print (s)
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
To build on what Willem van Onsem said:
The conventional way to calculate the nth term of the fibonacci sequence is to sum the n-1 and n-2 terms, as you're aware. A list comprehension is designed to create a list with no side effects during the comprehension (apart from the creation of the single list). Storing the last 2 terms of the sequence during calculation of the sequence is a side-effect, therefore a list comprehension is ill-suited to the task on its own.
A safe way around this would be to make a closure generator (essentially a generator with some associated private state) that can be passed to the list comprehension such that the list comprehension does not have to worry about the details of what's being stored:
def fib_generator(n):
def fib_n_generator():
last = 1
curr = 1
if n == 0:
return
yield last
if n == 1:
return
yield curr
if n == 2:
return
ii = 2
while ii < n:
next = curr + last
yield next
last = curr
curr = next
ii += 1
return fib_n_generator()
fib = [xx for xx in fib_generator(10)]
print(fib)
Here's a one-line list comprehension solution that avoids the separate initialization step with nested ternary operators and the walrus operator (so needs Python 3.8), and also avoids the rapid onset of overflow problems that the explicit form can give you (with its **n component):
[
0 if not i else
(x := [0, 1]) and 1 if i == 1 else
not x.append(x[-2] + x[-1]) and x[-1]
for i in range(10)
]
Gives:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
This is faster than the explicit form for generating all of the values up to N. If, however, you don't want all of the values then the explicit form could be much faster, but it does suffer from overflow for some N between 1000 and 2000:
n = 2000
int((((1 + 5**0.5) / 2)**n - ((1 - 5**0.5) / 2)**n) / 5**0.5)
gives for me:
OverflowError: (34, 'Numerical result out of range')
whereas the "adding the last two values" approach can generate higher values for larger N. On my machine, I can keep going until some N between 300000 and 400000 before I run out of memory.
Thanks to Jonathan Gregory for leading me most of the way to this approach.
List comprehension of the fibonacci serie, based on the explicit formula 1:
[int((0.5+5**0.5/2)**n/5**0.5+0.5) for n in range(21)]
From Python One-Liners by Christian Mayer.
n = 10
x = [0,1]
fibs = x[0:2] + [x.append(x[-1] + x[-2]) or x[-1] for i in range(n-2)]
print(fibs)
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
The answer is you can do this with a list comprehension without the assignment operator (works even in Python 2).
I did it this way:
def Phi(number:int):
n = [1,1]
[n.append(n[i-2]+n[i-1])for i in range(2,number)]
return n
Simplification of #dhassel version (requires python 3.8 or later)
series = [i0 := 0, i1 := 1]+[i1 := i0 + (i0 := i1) for j in range(2, 5)]
One can also be written as a generator expression, but it's a bit tricky because for some reason, the obvious answer: fibo = (v for g in ((i0 := 0, i1 := 1), (i1 := i0 + (i0 := i1) for j in range(2,10))) for v in g) doesn't work (I do not exclude a bug). However, it is OK if you get the subgenerators list outside :
glist = ((i0 := 0, i1 := 1), (i1 := i0 + (i0 := i1) for j in range(2, 5)))
fibo = (v for g in glist for v in g)
# Get a number from the user.
number = int(input("enter a number"))
# Create a empty list
mylist=[]
# create list comprehension following fibonaci series
[mylist.append(0) if n==0 else mylist.append(1) if n==1 else mylist.append(mylist[-2]+mylist[-1]) for n in range(number+1)]
print(mylist)
Using List comprehension :
n = int(input())
fibonacci_list = [0,1]
[fibonacci_list.append(fibonacci_list[k-1]+fibonacci_list[k-2]) for k in range(2,n)]
if n<=0:
print('+ve numbers only')
elif n == 1:
fibonacci_list = [fibonacci_list[0]]
print(fibonacci_list)
else:
print(fibonacci_list)
maybe it's a feasible solution for this problem...
I'm doing the Project Euler problems, and I'm on number two. The question is:
Each new term in the Fibonacci sequence is generated by adding the previous two terms. By starting with 1 and 2, the first 10 terms will be:
1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...
By considering the terms in the Fibonacci sequence whose values do not exceed four million, find the sum of the even-valued terms.
I'm trying to solve this in python. I think I have the correct code, but for some reason When I run it with n being anything greater than or equal to 27, it will wait like a minute and just return 0. However, for anything 26 or lower, it runs fine. Here's my code:
def fib_seq(n):
if n == 0:
return n
elif n == 1:
return n
else:
return fib_seq(n-1) + fib_seq(n-2)
def get_fib_sum(n):
x = n
sum = 0
for i in range(n):
if fib_seq(x) > 4000000:
pass
elif fib_seq(x) % 2 == 0:
pass
else:
sum += fib_seq(x)
x = i
return sum
print get_fib_sum(27)
Is there anyway to fix this or at least get it to work? If it makes a difference, I'm using Wing IDE 101 Student Edition.
In your loop, you are using fib_seq(x) and it should be fib_seq(i)
Also, if you want to reduce time a bit more, you can use memoization technique
def fib_seq(n):
if n == 0:
return n
elif n == 1:
return n
else:
return fib_seq(n-1) + fib_seq(n-2)
def memoize(fn, arg):
memo = {}
if arg not in memo:
memo[arg] = fn(arg)
return memo[arg]
fibm = memoize(fib_seq,27)
print fibm
Why are you using recursion? your code is recalculating the ENTIRE fibonnaci sequence over and over and over and over and over... The code just wants the sum of the even terms. There is NO need for recursion. In pseudo-code:
t1 = 1
t2 = 2;
sum = 2;
do {
t3 = t1 + t2;
if (t3 is even) {
sum += t3;
}
t1 = t2;
t2 = t3;
} while (t2 <= 4000000)
Fibonacci sequence is often used as an example of how to write recursive code, which is ridiculous because it has a very straight-forward iterative solution:
def fib(n):
if n < 2:
return n
else:
a, b = 1, 1
for _ in range(2, n): # O(n)
a, b = b, a+b
return b
What is less obvious is that it also has a matrix representation,
F = [[0, 1]] # initial state
T = [[0, 1], # transition matrix
[1, 1]]
fib(n) = (F * T**n)[0][0]
which is extremely useful because T**n can be computed in O(log(n)) steps.
(As an aside, the eigenvector of the log of the transition matrix leads to the analytic solution,
phi = (1 + 5**0.5) / 2 # golden ratio
fib(n) = round(phi**n / 5**0.5, 0)
but that's not where I'm going with this.)
Looking at the terms produced in terms of odd-or-even, you see
n: 0, 1, 2, 3, 4, 5, 6, 7, 8, ...
f(n): 0, 1, 1, 2, 3, 5, 8, 13, 21, ...
e/o: even, odd, odd, even, odd, odd, even, odd, odd, ...
so what you need is fib(0) + fib(3) + fib(6) + ... and computing T**3 gives you the coefficients needed to step directly from term to term.
The rest is left as an exercise for the reader ;-)
It does a lot of recursion, that's why it's taking so long.
The get_fib_sum() will evaluate fib_seq(27) in a loop, which does a lot of recursion and takes a while. Since the result of fib_seq(27) is greater then 4000000 it will then will never add anything to sum, returning 0 in the end.
I want to split the value into number of spits provided. so for example if I have a value = 165340
and split = 5 then the list should become ['0-33068', '33069-66137', '66138-99204', '99205-132272', '132273-165340']...
so far I have just come up with something like this but this is not dynamic...
so thinking how can I build a list of strings like of numbers split with the difference val/split
for i in range(split):
if i==0:
lst.append('%s-%s' % (i, val/split))
elif i==1:
lst.append('%s-%s' % (val/split+i, val/split*2+1))
elif i == 2:
lst.append('%s-%s' % (val/split*i+2, val/split*3))
elif i == 3:
lst.append('%s-%s' % (val/split*i+1, val/split*4))
elif i == 4:
lst.append('%s-%s' % (val/split*i+1, val/split*5))
else:
pass
FINAL:
I made a bunch of attempts here, especially in using remainder = value % numsplits, then int(i * remainder // numsplits) to try and keep things close. Eventually, though, I had to give up and go back to floating point which seems to give the closest results. The usual floating point concerns apply.
def segment(value, numsplits):
return ["{}-{}".format(
int(round(1 + i * value/(numsplits*1.0),0)),
int(round(1 + i * value/(numsplits*1.0) +
value/(numsplits*1.0)-1, 0))) for
i in range(numsplits)]
>>> segment(165340, 5)
['1-33068', '33069-66136', '66137-99204', '99205-132272', '132273-165340']
>>> segment(7, 4)
['1-2', '3-4', '4-5', '6-7']
I don't see a huge issue with this one. I did start at 1 instead of 0, but that's not necessary (change both the int(round(1 + i * ... to int(round(i * ... to change that). Old results follow.
value = 165340
numsplits = 5
result = ["{}-{}".format(i + value//numsplits*i, i + value//numsplits*i + value//numsplits) for i in range(numsplits)]
Probably worth tossing in a function
def segment(value,numsplits):
return ["{}-{}".format(value*i//numsplits, 1 + value//numsplits*i + value//numsplits) for i in range(numsplits)]
The following will cut it off at your value
def segment(value, numsplits):
return ["{}-{}".format(max(0,i + value*i//numsplits), min(value,i + value*i//numsplits + value//numsplits)) for i in range(numsplits)]
To answer this question, it's important to know exactly how we should treat 0 - but it doesn't seem like you've asked yourself this question. The intervals in your example output are inconsistent; you're starting with 0 in the first interval and the first two intervals both have 33,069 elements (counting 0) in them, but you're also ending your last interval at 165340. If 0 and 165340 are both counted in the number of elements, then 165340 is not divisible into five even intervals.
Here are a few different solutions that might help you understand the problem.
Even intervals, counting from zero
Let's start with the assumption that you really do want both 0 and the "top" value counted as elements and displayed in the result. In other words, the value 11 would actually indicate the following 12-element range:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
And be evenly split into the following non-negative intervals:
['0-3', '4-7', '8-11']
If we're only concerned with evenly-divisible cases, we can use a fairly short function (NOTE: These solutions are valid for Python 3.x, or for Python 2.x with from __future__ import division):
>>> def evenintervals(value, n):
... binsize = (value + 1) // n
... intervals = ((x * binsize, (x + 1) * binsize - 1) for x in range(n))
... return ['{}-{}'.format(x, y) for x, y in intervals]
...
>>> evenintervals(11, 3)
['0-3', '4-7', '8-11']
>>> evenintervals(17, 2)
['0-8', '9-17']
However, this function deals with 165340 (and any other not-evenly-divisible case) by dropping some numbers off the end:
>>> evenintervals(165340, 5)
['0-33067', '33068-66135', '66136-99203', '99204-132271', '132272-165339']
From a purely mathematical perspective, this just doesn't work. However, we could fudge it a bit if for some reason you want to display 0, but not actually count it as an element of the first interval.
Even intervals, counting from one
Here's a function that doesn't count 0 as an element of the list, but does give you the option of displaying it, if you're just that zany:
>>> def evenintervals1(value, n, show_zero=False):
... binsize = value // n
... intervals = [[x * binsize + 1, (x + 1) * binsize] for x in range(n)]
... if show_zero:
... intervals[0][0] = 0
... return ['{}-{}'.format(x, y) for x, y in intervals]
...
>>> evenintervals1(20, 4)
['1-5', '6-10', '11-15', '16-20']
>>> evenintervals1(20, 5, show_zero=True)
['0-5', '6-10', '11-15', '16-20']
This version of the function might be the closest thing to what you asked for in your question, even though it doesn't show the exact values you gave in your example output:
>>> evenintervals1(165340, 5, show_zero=True)
['0-33068', '33069-66136', '66137-99204', '99205-132272', '132273-165340']
But we still have problems with inputs that aren't evenly divisible. What if we wanted a more general solution?
Uneven intervals
Let's think about how to deal with a wider range of inputs. We should be able to produce, from any positive integer n, anywhere from 1 to n non-overlapping ranges of positive integers. In other words, if our integer is 5, we want to be able to produce a list with as many as five ranges. But how should we distribute "extra" elements, in order to make the ranges as even as possible?
We probably don't want to distribute them randomly. We could just lengthen or shorten the last range in the list, but that has the potential to be very lop-sided:
# 40 split 7 times, adding remainder to last item
['1-5', '6-10', '11-15', '16-20', '21-25', '26-30', '31-40']
# 40 split 7 times, subtracting excess from last item
['1-6', '7-12', '13-18', '19-24', '25-30', '31-36', '37-40']
In the former case the last element is 100% larger than the others and in the latter case it's 33% smaller. If you're splitting a very large value into a much smaller number of intervals, this may not be as much of a problem.
More likely, we want a function that produces the most even set of ranges possible. I'm going to do this by spreading the remainder of the division out among the first elements of the list, with a little help from itertools:
>>> from itertools import zip_longest # izip_longest for Python 2.7
>>> def anyintervals(value, n):
... binsize, extras = value // n, value % n
... intervals = []
... lower = 0
... upper = 0
... for newbinsize in map(sum, zip_longest([binsize] * n, [1] * extras, fillvalue=0)):
... lower, upper = upper + 1, upper + newbinsize
... intervals.append((lower, upper))
... return ['{}-{}'.format(x, y) for x, y in intervals]
...
>>> anyintervals(11, 3)
['1-4', '5-8', '9-11']
>>> anyintervals(17, 2)
['1-9', 10-17']
Finally, with the example inputs given in the OP:
>>> anyintervals(165340, 5)
['1-33068', '33069-66136', '66137-99204', '99205-132272', '132273-165340']
If it were really important to show the first interval starting at zero, we could apply the same logic here that was used in evenintervals1 to modify the very first integer in intervals before returning, or write a similar function to this one that started counting at zero.
I did implement another version that distributes the "extras" among the last ranges rather than the first, and there are certainly many other implementations that you might be interested in fiddling around with, but those solutions are left as an exercise to the reader. ;)
One possibility using numpy:
from numpy import arange
v = 165340
s = 5
splits = arange(s + 1) * (v / s)
lst = ['%d-%d' % (splits[idx], splits[idx+1]) for idx in range(s)]
print '\n'.join(lst)
output:
0-33068
33068-66136
66136-99204
99204-132272
132272-165340
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?
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]
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!
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]
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.
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).