You caught the competitive programmer in me.
Using dynamic programming (memoization) I was able to get the runtime down to a reasonable level.
First, we define the kind of weights we have, and the targets we want to hit.
weights = [5, 10, 25, 35, 45]
targets = [30, 40, 45, 50, 55, 65, 70, 80]
Next up we have the main DP function. walk
takes an ordered list of used weights (initially empty) and a pos
telling us what weights we have already considered (initially zero). This lowers the number of calls to walk
from O(n!)
to O(2^n)
. walk
is also memoized, further lowering execution time from O(2^n)
to O(n)
.
There are a few base cases, some of which are dynamically modified for performance:
pos >= len(weights)
If pos is greater than the length of weights, we have checked all weights, and we are done recursing.
len(used) > max(targets) / min(weights)
This is a weak bound on the number of weights to use. If there's a way to just use the smallest kind of weight and still pass the biggest target, we know we have checked enough numbers, and that this branch is useless. Moving on.
len(used) > bwnum
where bwnum
is the number of weights used in the best answer so far. Since that's our primary criterion, we can stop recursing when we have picked more than bwnum
weights. This is a big optimization, assuming we quickly find any valid answer.
As for the two cases a
and b
, we can either pick another weight of the kind at pos
, or we can move pos
forward. The best one (shortest, then smallest sum) is memoized and returned. Since there are two cases, we have a branching factor of 2
.
mem = {}
bwnum = len(weights)+1
def walk(used, pos):
k = (used, pos)
global bwnum, weights, targets
if pos >= len(weights) or len(used) > bwnum or len(used) > max(targets) / min(weights):
return used if valid(used) else (1e9,)*(bwnum+1)
if k not in mem:
a = walk(used + (weights[pos],), pos)
b = walk(used, pos + 1)
mem[k] = a if len(a) < len(b) or (len(a) == len(b) and sum(a) < sum(b)) else b
if valid(mem[k]):
bwnum = min(bwnum, len(mem[k]))
return mem[k]
Then we need a validator function, to check if a given list of weights is enough to reach all targets. This can be optimized further, but it's pretty fast. I'm spending 80% of my execution time in this function.
from itertools import combinations
vmem = {}
def valid(used):
if used not in vmem:
tmap = {}
for t in targets:
tmap[t] = 0
for le in range(1, len(used) + 1):
for c in combinations(used, le):
if sum(c) in tmap:
del tmap[sum(c)]
vmem[used] = len(tmap) == 0
return vmem[used]
Finally, call walk
with empty arguments, and print the result.
r = walk((), 0)
print r, len(r), sum(r)
Which outputs:
(5, 5, 10, 25, 35) 5 80
Oh, by the way, your examples were correct. Thanks.