···11+import fileinput
22+from collections import defaultdict, deque, Counter
33+44+from utils import Point
55+66+77+PART_1_STEPS = 64
88+PART_2_STEPS = 26501365
99+1010+1111+def search(start, max_dist):
1212+ horizon = deque([(start, 0)])
1313+ cost_so_far = {}
1414+1515+ while horizon:
1616+ curr, dist = horizon.pop()
1717+1818+ if dist > max_dist:
1919+ continue
2020+2121+ if curr in cost_so_far:
2222+ continue
2323+2424+ cost_so_far[curr] = dist
2525+2626+ for n in curr.neighbours_4():
2727+ if GRAPH.get(n) != '.':
2828+ continue
2929+3030+ horizon.appendleft((n, dist + 1))
3131+3232+ return sum(1 for c, v in cost_so_far.items() if v % 2 == ((max_dist % 2)))
3333+3434+3535+# Read problem input.
3636+GRAPH = {}
3737+START = None
3838+for y, line in enumerate(fileinput.input()):
3939+ for x, c in enumerate(line.strip()):
4040+ p = Point(x, y)
4141+ if c == 'S':
4242+ START = p
4343+ GRAPH[p] = '.'
4444+ else:
4545+ GRAPH[p] = c
4646+4747+# Solve part 1.
4848+print("Part 1:", search(START, 64))
4949+5050+# Solve part 2.
5151+HEIGHT = y + 1
5252+WIDTH = x + 1
5353+assert HEIGHT == WIDTH
5454+5555+assert START.x == START.y == (HEIGHT // 2) == (WIDTH // 2)
5656+RADIUS = START.x
5757+assert (PART_2_STEPS % WIDTH) == RADIUS
5858+5959+# Define the key search-start points for blocks within the infinite diamond.
6060+EDGE_STARTS = [Point(x, y) for x, y in [(RADIUS, 0), (WIDTH - 1, RADIUS), (0, RADIUS), (RADIUS, HEIGHT - 1)]]
6161+CORNER_STARTS = [Point(x, y) for x, y in [(0, 0), (0, HEIGHT - 1), (WIDTH - 1, 0), (WIDTH - 1, HEIGHT - 1)]]
6262+6363+# Start computing the final answer to part 2. We leverage the following properties of the problem input:
6464+# - There is a straight path from the central starting point to the outer edges of the block.
6565+# - The step length is equal to the block radius mod block length, meaning there is perfect
6666+# symmetry even on the outside edges of the diamond that gets formed.
6767+6868+# First, add in the centre piece of the diamond.
6969+part_2 = search(START, PART_2_STEPS)
7070+7171+# Compute how many times we need to add "full edge pieces" in with parity.
7272+num_edge_pieces = Counter()
7373+for i, x in enumerate(range(WIDTH, PART_2_STEPS - RADIUS, WIDTH), start=1):
7474+ num_edge_pieces[i % 2] += 1
7575+7676+edge_addition = 0
7777+for s in EDGE_STARTS:
7878+ # Add "fully searched" edge blocks.
7979+ edge_addition += num_edge_pieces[PART_2_STEPS % 2] * search(s, PART_2_STEPS)
8080+ edge_addition += num_edge_pieces[(PART_2_STEPS + 1) % 2] * search(s, PART_2_STEPS + 1)
8181+8282+ # Accomodate the outer tips of the infinite diamond.
8383+ edge_addition += search(s, WIDTH - 1)
8484+8585+part_2 += edge_addition
8686+8787+# Compute how many "corner pieces" are scattered on the edge of the diamond with parity.
8888+num_corner_pieces = Counter() # track parity
8989+for i, _ in enumerate(range(WIDTH, PART_2_STEPS - WIDTH * 1, WIDTH), start=0):
9090+ num_corner_pieces[i % 2] += i
9191+9292+corner_addition = 0
9393+for s in CORNER_STARTS:
9494+ # Add "fully searched" corner blocks.
9595+ corner_addition += search(s, PART_2_STEPS) * num_corner_pieces[1]
9696+ corner_addition += search(s, PART_2_STEPS + 1) * num_corner_pieces[0]
9797+9898+ # Accomodate the outer edges of the infinite diamond.
9999+ corner_addition += search(s, WIDTH + RADIUS - 1) * ((PART_2_STEPS // WIDTH) - 1)
100100+ corner_addition += search(s, RADIUS - 1) * ((PART_2_STEPS // WIDTH))
101101+102102+part_2 += corner_addition
103103+104104+print("Part 2:", part_2)