-
Notifications
You must be signed in to change notification settings - Fork 0
/
day20.py
184 lines (153 loc) · 4.84 KB
/
day20.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# vi: set shiftwidth=4 tabstop=4 expandtab:
import datetime
import os
import math
import itertools
import collections
import functools
import operator
top_dir = os.path.dirname(os.path.abspath(__file__)) + "/../../"
def get_input_value_from_file(file_path=top_dir + "resources/year2015_day20_input.txt"):
with open(file_path) as f:
for l in f:
return int(l.strip())
# Various functions based on divisors and primes
def mult(iterable, start=1):
"""Returns the product of an iterable - like the sum builtin."""
return functools.reduce(operator.mul, iterable, start)
def yield_divisors_using_divisions(n):
"""Yields distinct divisors of n.
This uses sucessive divisions so it can be slower than
yield_divisors_using_primes_factorisation on big inputs but it is easier
to understand, the upper bound of O(sqrt(n)) is guarantee and faster on
small inputs."""
assert n > 0
yield 1
if n > 1:
yield n
for i in range(2, int(math.sqrt(n)) + 1):
if n % i == 0:
j = n // i
yield i
if i != j:
yield j
def prime_factors(n):
"""Yields prime factors of a positive number."""
assert n > 0
d = 2
while d * d <= n:
while n % d == 0:
n //= d
yield d
d += 1
if n > 1: # to avoid 1 as a factor
assert d <= n
yield n
def yield_divisors_using_primes_factorisation(n):
"""Yields distinct divisors of n.
This uses the prime factorisation to generate the different divisors.
It is faster than yield_divisors_using_divisions on most big inputs
but the complexity is hard to guarantee on all inputs and it is slower
on small inputs."""
elements = (
[p**power for power in range(c + 1)]
for p, c in collections.Counter(prime_factors(n)).items()
)
return (mult(it) for it in itertools.product(*elements))
# https://en.wikipedia.org/wiki/Divisor_function
def sum_of_divisors_naive(n):
return sum(yield_divisors_using_divisions(n))
def sum_of_divisors_optimised(n):
return mult(
((p ** (c + 1) - 1) / (p - 1))
for p, c in collections.Counter(prime_factors(n)).items()
)
# https://en.wikipedia.org/wiki/Highly_abundant_number
def get_naive_sum_of_divisors_bigger_than(n):
for i in itertools.count(start=1):
if sum_of_divisors_optimised(i) >= n:
return i
def get_optimised_sum_of_divisors_bigger_than(n):
# Get numbers of divisors using the prime distinct factors formula
# sigma(n) > X
# _____ ai + 1
# | | pi - 1
# | | ------------ > X
# | | pi - 1
if n < 2:
return 1
candidate = None
values = [(1, 1)] # (i, sigma(i)) with sigma(i) < n
# TODO: We are missing a condition to stop checking more primes
for p in [
2,
3,
5,
7,
11,
13,
17,
19,
23,
29,
31,
37,
41,
43,
47,
53,
59,
61,
67,
]: # TODO
new_values = []
for power in itertools.count(start=1):
new_value_added = False
p_pow = p**power
coef, remaining = divmod(p_pow * p - 1, p - 1)
assert remaining == 0
for (prod, sum_div) in values:
prod2, sum_div2 = prod * p_pow, sum_div * coef
cand2 = prod2, sum_div2
if candidate is None or prod2 <= candidate:
if sum_div2 >= n:
candidate = prod2
else:
new_values.append(cand2)
new_value_added = True
if not new_value_added:
break
values.extend(new_values)
return candidate
def get_lowest_house_with_presents(nb):
return get_optimised_sum_of_divisors_bigger_than(nb // 10)
def run_tests():
tests = [
(1, 1),
(2, 3),
(3, 4),
(4, 7),
(5, 6),
(6, 12),
(7, 8),
(8, 15),
(9, 13),
]
for n, s in tests:
assert sum_of_divisors_naive(n) == s
assert sum_of_divisors_optimised(n) == s
assert get_lowest_house_with_presents(140) == 8
assert get_lowest_house_with_presents(120) == 6
for val in list(range(0, 10)) + list(range(100, 110)) + list(range(1000, 1100)):
naive = get_naive_sum_of_divisors_bigger_than(val)
optimised = get_optimised_sum_of_divisors_bigger_than(val)
assert naive == optimised
def get_solutions():
input_value = get_input_value_from_file()
print(get_lowest_house_with_presents(input_value) == 665280)
if __name__ == "__main__":
begin = datetime.datetime.now()
run_tests()
get_solutions()
end = datetime.datetime.now()
print(end - begin)