···11+import fileinput
22+from collections import defaultdict, deque
33+44+from utils import Point, DIRS, N, S, E, W
55+66+77+def process_plot(garden, start, visited):
88+ """
99+ Walk through the garden using BFS from `start`, only
1010+ visiting the same plant as present in `garden[start]`.
1111+1212+ Write seen locations back to `visited` so that we can
1313+ keep track of which garden plots have already been seen.
1414+1515+ Returns the area, perimeter, and side count of the plot.
1616+ """
1717+ plot = set()
1818+ horizon = deque([start])
1919+ plant = garden.get(start)
2020+2121+ while horizon:
2222+ node = horizon.popleft()
2323+ if node in plot:
2424+ continue
2525+2626+ plot.add(node)
2727+ visited.add(node)
2828+2929+ for n in node.neighbours():
3030+ if n in garden and garden.get(n) == plant:
3131+ horizon.append(n)
3232+3333+ # The area of the plot is simply how many locations we visited.
3434+ area = len(plot)
3535+3636+ # Compute the perimeter by taking every plant location in the
3737+ # plot, and counting how many neighbouring locations are *not*
3838+ # also in the plot. Each of those locations contributes 1 unit
3939+ # length to the overall perimeter of the plot.
4040+ perimeter = sum(len(set(p.neighbours()) - plot) for p in plot)
4141+4242+ # Compute the number of sides. This one is a bit tricky.
4343+4444+ # Walk every row in the garden. For each position that is part
4545+ # of this plot, look to see if there is a non-plot location above
4646+ # and below it (similar to the perimeter calculation). Add this
4747+ # to a set of "potential sides". We will need to eliminate extra
4848+ # sides in a future step.
4949+ row_sides = {
5050+ N: defaultdict(set),
5151+ S: defaultdict(set),
5252+ }
5353+ for y in range(-2, 150):
5454+ for d in [N, S]:
5555+ for x in range(-2, 150):
5656+ p = Point(x, y)
5757+ if p in plot and p + d not in plot:
5858+ row_sides[d][y].add(p)
5959+6060+ # Do the same for every column in the garden, looking left/right.
6161+ col_sides = {
6262+ E: defaultdict(set),
6363+ W: defaultdict(set)
6464+ }
6565+6666+ for x in range(-2, 150):
6767+ for d in [E, W]:
6868+ for y in range(-2, 150):
6969+ p = Point(x, y)
7070+ if p in plot and p + d not in plot:
7171+ col_sides[d][x].add(p)
7272+7373+ # Do another horizonal/vertical sweep of the garden. When we
7474+ # "enter" a position that is part of a "side", increment the
7575+ # side count. Continue to walk until we are not in that side
7676+ # anymore. At this point, the polygon has turned or something,
7777+ # or we might be passing over a "break" (imagine walking down
7878+ # the horizontal legs of the letter "E" vertically). Don't start
7979+ # counting another side until we reach an in-plot location again.
8080+ sides = 0
8181+8282+ for d in [N, S]:
8383+ for y in range(-2, 150):
8484+ x = -2
8585+ inside = False
8686+ while x < 150:
8787+ p = Point(x, y)
8888+ if p in row_sides[d][y]:
8989+ if not inside:
9090+ sides += 1
9191+ inside = True
9292+ else:
9393+ if inside:
9494+ inside = False
9595+9696+ x += 1
9797+9898+ for d in [E, W]:
9999+ for x in range(-2, 150):
100100+ y = -2
101101+ inside = False
102102+ while y < 150:
103103+ p = Point(x, y)
104104+ if p in col_sides[d][x]:
105105+ if not inside:
106106+ sides += 1
107107+ inside = True
108108+ else:
109109+ if inside:
110110+ # print("exit inside")
111111+ inside = False
112112+113113+ y += 1
114114+115115+ return area, perimeter, sides
116116+117117+118118+# Parse problem input.
119119+GARDEN = {}
120120+for y, line in enumerate(fileinput.input()):
121121+ for x, c in enumerate(line.strip()):
122122+ GARDEN[Point(x, y)] = c
123123+124124+# Solve problem.
125125+part_1 = 0
126126+part_2 = 0
127127+visited = set()
128128+129129+for pos in GARDEN:
130130+ if pos not in visited:
131131+ area, perim, sides = process_plot(GARDEN, pos, visited)
132132+ part_1 += area * perim
133133+ part_2 += area * sides
134134+135135+print("Part 1:", part_1)
136136+print("Part 2:", part_2)