From d7c815f0c0307bda40fff1554055ffbb197f3b44 Mon Sep 17 00:00:00 2001 From: Bert Peters Date: Fri, 18 Dec 2015 13:00:52 +0100 Subject: [PATCH] Improve solution for day 17. --- day-17/README.md | 26 ++++++++++++++++++++ day-17/solution.py | 59 +++++++++++++++++++++++++++++++++------------- 2 files changed, 68 insertions(+), 17 deletions(-) create mode 100644 day-17/README.md diff --git a/day-17/README.md b/day-17/README.md new file mode 100644 index 0000000..a4ff566 --- /dev/null +++ b/day-17/README.md @@ -0,0 +1,26 @@ +# Day 17 efficient solution + +Day 17 is an instance of the subset sum problem. This problem asks whether for +a (multi)set of integers *V*, there is a non-empty subset of integers summing up to +exactly *s*. This problem is NP-complete. + +The brute force approach of this is trying every possible set in the powerset +of *V* to see if they match. This is inpractical however, because a powerset of +a set of size *n* contains 22 sets. + +In the exercise, we have 20 buckets, and 220 is still +brute-forcable. There is a smarter approach. + +We split the list of buckets in two lists of (approximately) the same size. We +then take the powersets of those two lists and compute the sum for each entry. +This leaves us with a total of 2n / 2 + 1 entries. We then sort both +sublists on the total value of each entry. + +Finally, we iterate of the first list, and use binary search to see whether +there is an appropriately sized entry in the second list. This gives us a final +complexity of *n* times 2*n*/2, allowing the solution to be computed +instantly. + +The algorithm above can be modified to find all combinations, not just one, in +time proportional to the number of solutions. This is implemented in the final +program. diff --git a/day-17/solution.py b/day-17/solution.py index cda8055..88bafff 100644 --- a/day-17/solution.py +++ b/day-17/solution.py @@ -1,21 +1,17 @@ from __future__ import print_function, division import fileinput from collections import defaultdict +import bisect -buckets = [] +def value(buckets, choice): + total = 0 + for value in buckets: + if choice % 2 == 1: + total += value -for line in fileinput.input(): - buckets.append(int(line)) + choice //= 2 -def works(bucketCombination, target): - for idx, value in enumerate(buckets): - - if bucketCombination % 2 == 1: - target -= value - - bucketCombination = bucketCombination // 2 - - return target == 0 + return total def ones(x): n = 0 @@ -27,11 +23,40 @@ def ones(x): return n +def partition(a_list): + pivot = len(a_list) // 2 + + return a_list[:pivot], a_list[pivot:] + +def partitionList(buckets): + result = [(value(buckets, x), ones(x)) for x in range(1 << len(buckets))] + result.sort() + return result + +buckets = [] + +for line in fileinput.input(): + buckets.append(int(line)) + +partition1, partition2 = partition(buckets) + +values1 = partitionList(partition1) +values2 = partitionList(partition2) + possible = defaultdict(lambda: 0) -for x in range(1 << len(buckets)): - if works(x, 150): - n = ones(x) - possible[n] += 1 +i = 0 + +target = 150 + +for entry in values1: + + i = bisect.bisect_left(values2, (target - entry[0], 0)) + + while i < len(values2) and entry[0] + values2[i][0] == target: + possible[entry[1] + values2[i][1]] += 1 + i += 1 + +print("Total possibilities:", sum(possible.values())) +print("Minimal possibilities:", possible[min(possible.keys())]) -print(sum(possible[x] for x in possible), possible[min(possible.keys())])