From 9b868d8553951e0d0a4b72d921634157a7168b78 Mon Sep 17 00:00:00 2001 From: Felix Martin Date: Fri, 24 Jul 2026 14:44:09 -0400 Subject: [PATCH] Solve problem 351 --- python/e351.py | 108 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 python/e351.py diff --git a/python/e351.py b/python/e351.py new file mode 100644 index 0000000..e12898a --- /dev/null +++ b/python/e351.py @@ -0,0 +1,108 @@ +from fractions import Fraction + + +def plot(n): + """ + First iteration with plotting so that I could understand + what's going on. Worked well to accomplish this plan. + + 1. Visualize till H(10) naively + 2. See pattern + 3. Generalize and compute solution + """ + import numpy as np + import matplotlib + import matplotlib.pyplot as plt + from matplotlib.ticker import MultipleLocator + + matplotlib.use("QtAgg") + + _, ax = plt.subplots() + ax.set_aspect("equal") + dx = np.cos(np.pi / 3) + dy = np.sin(np.pi / 3) + + c = 0 + seen_set = set() + seen = [] + for i in range(n + 1): + x, y = 2 * i, 0 + for _ in range(i): + for xs, ys in seen: + if x % xs == 0 and x * ys == y * xs: + assert Fraction(y, x) in seen_set + ax.plot(x * dx, y * dy, "x", color="green") + c += 1 + break + else: + ax.plot(x * dx, y * dy, "o", color="blue") + if x != 0 or y != 0: + seen_set.add(Fraction(y, x)) + seen.append((x, y)) + x -= 1 + y += 1 + + ax.xaxis.set_major_locator(MultipleLocator(1)) + ax.yaxis.set_major_locator(MultipleLocator(1)) + ax.grid(True, alpha=0.2) + print(f"h({n}) = {c * 6}") + plt.show() + + +def quick(n): + """ + Improved version that uses slopes to identify + if a point is visible. Still way too slow for 10**9, though. + """ + c = 0 + seen_set = set() + for i in range(n + 1): + x, y = 2 * i, 0 + for _ in range(i): + if Fraction(y, x) in seen_set: + c += 1 + else: + if x != 0 or y != 0: + seen_set.add(Fraction(y, x)) + x -= 1 + y += 1 + return c * 6 + + +def h(n: int) -> int: + """Optimized version based on realization that the + totient, i.e., how many numbers don't share a non-one + divisor with the respective row can be used to calculate + how many points in that row are visible.""" + totients = [i for i in range(n + 1)] + + r = (n * (n + 1)) // 2 # All points in 1/6 of the hexagon + r -= 1 # totient of 1 is 1 + for i in range(2, n + 1): + # Number had no factor so far and is therefore a prime. + if totients[i] == i: + # That phi(p) = p - 1: + totients[i] -= 1 + # Update totients of other numbers of which i is a factor. + # This is the same as * (1 - 1 / i); since i is a factor + # //= i will always be accurate. + for j in range(i + i, n + 1, i): + totients[j] *= i - 1 + totients[j] //= i + r -= totients[i] + # We only calculate the points for 1/6 of the grid. + return r * 6 + + +def euler_351(): + assert h(5) == 30 + assert h(10) == 138 + assert h(1000) == 1177848 + assert quick(1000) == 1177848 + return h(100000000) + + +if __name__ == "__main__": + solution = euler_351() + print(f"e351.py: {solution}") + assert solution == 11762187201804552