1 | """ |
||
2 | Project Euler Problem 31: Coin Sums |
||
3 | =================================== |
||
4 | |||
5 | .. module:: solutions.problem31 |
||
6 | :synopsis: My solution to problem #31. |
||
7 | |||
8 | The source code for this problem can be |
||
9 | `found here <https://bitbucket.org/nekedome/project-euler/src/master/solutions/problem31.py>`_. |
||
10 | |||
11 | Problem Statement |
||
12 | ################# |
||
13 | |||
14 | In England the currency is made up of pound, :raw-html:`£`, and pence, p, and there are eight coins in general |
||
0 ignored issues
–
show
|
|||
15 | circulation: |
||
16 | |||
17 | :math:`1p, 2p, 5p, 10p, 20p, 50p,` :raw-html:`£1` :math:`(100p)` and :raw-html:`£2` :math:`(200p)`. |
||
0 ignored issues
–
show
|
|||
18 | |||
19 | It is possible to make :raw-html:`£2` in the following way: |
||
20 | |||
21 | :math:`1 \\times` :raw-html:`£1` :math:`+ 1 \\times 50p + 2 \\times 20p + 1 \\times 5p + 1 \\times 2p +` |
||
0 ignored issues
–
show
|
|||
22 | :math:`3 \\times 1p` |
||
23 | |||
24 | How many different ways can :raw-html:`£2` be made using any number of coins? |
||
25 | |||
26 | Solution Discussion |
||
27 | ################### |
||
28 | |||
29 | This solution is inspired by a `meet-in-the-middle approach <https://en.wikipedia.org/wiki/Meet-in-the-middle_attack>`_. |
||
0 ignored issues
–
show
|
|||
30 | |||
31 | We will separate the denominations into three mutually exclusive subsets. Two subsets representing similarly sized |
||
0 ignored issues
–
show
|
|||
32 | subspaces and a final subset consisting of the one pence coin. More specifically: |
||
33 | |||
34 | * :math:`\\lbrace 2p, 20p, 50p \\rbrace` |
||
35 | * :math:`\\lbrace 5p, 10p, 100p, 200p \\rbrace` |
||
36 | * :math:`\\lbrace 1p \\rbrace` |
||
37 | |||
38 | Consider the possible range of coins of a given denomination :math:`x` s.t. the sum-total doesn't exceed |
||
0 ignored issues
–
show
|
|||
39 | :raw-html:`£2`. |
||
40 | |||
41 | .. math:: |
||
42 | |||
43 | 0 \\le \\# \\mbox{ coins of denomination } x \\le \\left \\lfloor \\frac{200}{x} \\right \\rfloor |
||
0 ignored issues
–
show
|
|||
44 | |||
45 | These bounds give the size of each search space associated with the denomination :math:`x`. |
||
46 | |||
47 | The size of the search space associated with each subset is the size of the cross product of the respective |
||
0 ignored issues
–
show
|
|||
48 | denominations. That is: |
||
49 | |||
50 | * :math:`|| \\lbrace 1p \\rbrace || = (200+1) = 201` |
||
51 | * :math:`|| \\lbrace 2p, 20p, 50p \\rbrace || =` |
||
52 | :math:`(100 + 1) \\times (10 + 1) \\times (4 + 1) = 101 \\times 11 \\times 5 = 5555` |
||
53 | * :math:`|| \\lbrace 5p, 10p, 100p, 200p \\rbrace || =` |
||
54 | :math:`(40 + 1) \\times (20 + 1) \\times (2 + 1) \\times (1 + 1) = 41 \\times 21 \\times 3 \\times 2 = 5166` |
||
0 ignored issues
–
show
|
|||
55 | |||
56 | .. note:: the one pence coin is considered separately for a very specific reason. As long as the total of all other |
||
0 ignored issues
–
show
|
|||
57 | denominations is no greater than two pounds then a solution is possible by adding the required number of one |
||
0 ignored issues
–
show
|
|||
58 | pence coins until two pounds is achieved. |
||
59 | |||
60 | Now, the algorithm is simple. Find all combinations of coins from each of the two non-trivial subsets that do not |
||
0 ignored issues
–
show
|
|||
61 | exceed two pounds. Search through the cross product of these two spaces and every total combination not exceeding |
||
0 ignored issues
–
show
|
|||
62 | two pounds is a valid answer. The difference can simply be padded with one pence coins until two pounds is reached. |
||
0 ignored issues
–
show
|
|||
63 | |||
64 | Solution Implementation |
||
65 | ####################### |
||
66 | |||
67 | .. literalinclude:: ../../solutions/problem31.py |
||
68 | :language: python |
||
69 | :lines: 72- |
||
70 | """ |
||
71 | |||
72 | from itertools import product |
||
73 | from typing import Dict, List |
||
74 | |||
75 | |||
76 | def partial_solutions(bounds: Dict[int, int], threshold: int) -> List[int]: |
||
77 | """ Produce the set of combinations of coins that doesn't exceed `threshold` pence |
||
78 | |||
79 | The coins that may be used are specified in `bounds` which is a dictionary mapping denominations to the upper bound |
||
0 ignored issues
–
show
|
|||
80 | in the search space for that denomination. |
||
81 | |||
82 | :param bounds: dictionary mapping denominations to search space upper bounds |
||
83 | :param threshold: the sum-total threshold |
||
84 | :return: the list of totals given by combinations of coins not exceeding `threshold` |
||
85 | """ |
||
86 | |||
87 | rv = [] |
||
0 ignored issues
–
show
The name
rv does not conform to the variable naming conventions ((([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ ).
This check looks for invalid names for a range of different identifiers. You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements. If your project includes a Pylint configuration file, the settings contained in that file take precedence. To find out more about Pylint, please refer to their site. ![]() |
|||
88 | for x in product(*(range(bounds[y] + 1) for y in bounds.keys())): |
||
0 ignored issues
–
show
The name
x does not conform to the variable naming conventions ((([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ ).
This check looks for invalid names for a range of different identifiers. You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements. If your project includes a Pylint configuration file, the settings contained in that file take precedence. To find out more about Pylint, please refer to their site. ![]() |
|||
89 | subtotal = sum([ai * bi for ai, bi in zip(x, bounds.keys())]) |
||
90 | if subtotal <= threshold: |
||
91 | rv.append(subtotal) |
||
92 | rv.sort() |
||
93 | return rv |
||
94 | |||
95 | |||
96 | def solve(): |
||
97 | """ Compute the answer to Project Euler's problem #31 """ |
||
98 | |||
99 | target = 200 # in pence |
||
100 | subset1, subset2 = [2, 20, 50], [5, 10, 100, 200] # 1p is not included |
||
101 | bounds1 = {pence: target // pence for pence in subset1} |
||
102 | bounds2 = {pence: target // pence for pence in subset2} |
||
103 | |||
104 | # Independently compute partial solutions based on each non-trivial subset |
||
105 | partial_solutions1 = partial_solutions(bounds1, target) |
||
106 | partial_solutions2 = partial_solutions(bounds2, target) |
||
107 | |||
108 | # Consider the cross-product of partial solutions and identify any combination not exceeding two points |
||
0 ignored issues
–
show
|
|||
109 | answer = 0 |
||
110 | for ps1 in partial_solutions1: |
||
111 | for ps2 in partial_solutions2: |
||
112 | if ps1 + ps2 <= target: |
||
113 | answer += 1 # 1p coins can increase this to target as required |
||
114 | else: |
||
115 | break # ps1 + ps2 for all further ps2 will now exceed target due to sorting |
||
116 | return answer |
||
117 | |||
118 | |||
119 | expected_answer = 73682 |
||
0 ignored issues
–
show
The name
expected_answer does not conform to the constant naming conventions ((([A-Z_][A-Z0-9_]*)|(__.*__))$ ).
This check looks for invalid names for a range of different identifiers. You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements. If your project includes a Pylint configuration file, the settings contained in that file take precedence. To find out more about Pylint, please refer to their site. ![]() |
|||
120 |
This check looks for lines that are too long. You can specify the maximum line length.