Advent of Code Day 9¶

In day 9, we calculate areas in a very large 2-D space

Startup Code¶

In [80]:
import sys
from collections import deque
from queue import PriorityQueue
import copy
import math
import itertools

def getPoints(stry: str) -> list[str]:
    with open(stry) as f:
        data = f.read().splitlines()

    points=[]
    for l in data:
        l = l.split(",")
        x = int(l[0])
        y = int(l[1])
        points.append((x,y))
    return points
    
points = getPoints("9.input")
small_points = getPoints("9.small")

Part 1¶

Part 1 is straight forward enough: find all the pairs of points, calculate their areas, find the max area. Note becareful of calculating area

In [81]:
def area(pair):
    p1, p2 = pair
    if p1[0] == p2[0]:
        #Case 1 same row
        return abs(p1[0]-p2[0])+1
    elif p1[1] == p2[1]:
        #Case 2 same column:
        return abs(p1[1]-p2[1])+1
    else:
        #Case 3 NW SE
        #Case 4 NE SW
        return (abs(p1[1]-p2[1])+1)*(abs(p1[0]-p2[0])+1)
print(f"The answer for example in part one is {max([ area(pr) for pr in list(
itertools.combinations(small_points, 2))])}")
print(f"The answer for part one is {max([ area(pr) for pr in list(itertools.combinations(points, 2))])}")
The answer for example in part one is 50
The answer for part one is 4763932976

Part 2¶

Part 2 is significantly more difficult. As I did on Day 10, I borrow heavily from HyperNeutrino’s approach. He introduces two key optimizations: grid compression and a 2-D prefix sum array.

His basic procedure, as outlined in the following video, is:

  1. Compress the grid.
  2. Use a flood fill to toggle tiles that are considered green or red.
  3. Precompute the number of red and green tiles within any given area.
  4. For every pair, calculate the area and validate it against the precomputed red and green tile counts.

Compress the grid¶

In the code below, HyperNeutrino uses grid compression to keep the memory usage manageable.

In [82]:
def compressGrid(points: list[(int,int)]) -> list[list[int]]:
    xs = sorted({ x for x,_ in points })
    ys = sorted({ y for _,y in points })
    grid = [[0]*(len(xs) * 2 - 1) for _ in range(len(ys)*2-1)]
    for (x1,y1), (x2,y2) in zip(points, points[1:]+points[:1]):
        cx1,cx2  = sorted([xs.index(x1) * 2, xs.index(x2) * 2])
        cy1,cy2  = sorted([ys.index(y1) * 2, ys.index(y2) * 2])
        for cx in range(cx1,cx2+1):
            for cy in range(cy1,cy2+1):
                grid[cy][cx]=1
    return grid

grid = compressGrid(points)
small_grid = compressGrid(small_points)
            
for row in small_grid:
    print("".join([str(n) for n in row]))
0011111
0010001
1110001
1000001
1111101
0000101
0000111

Flood fill¶

Flood fill is used to mark areas enclosed by red and green tiles.

In [83]:
def flood(grid: list[list[int]])-> list[list[int]]:
    outside={(-1,-1)}

    q = deque(outside)

    while q :
        tx, ty = q.popleft()
        for nx,ny in [(tx-1,ty), (tx+1,ty), (tx,ty-1),(tx,ty+1)]:
            if nx < -1 or ny < -1 or nx > len(grid[0]) or ny > len(grid): continue
            if 0 <= nx < len(grid[0]) and 0 <= ny < len(grid) and grid[ny][nx]==1: continue
            if (nx,ny) in outside: continue
            outside.add((nx,ny))
            q.append((nx,ny))

    for x in range(len(grid[0])):
        for y in range(len(grid)):
            if (x,y) not in outside:
                grid[y][x]=1
    return grid

grid=flood(grid)
small_grid=flood(small_grid)

for row in small_grid:
    print("".join([str(n) for n in row]))
0011111
0011111
1111111
1111111
1111111
0000111
0000111

Build prefix summary array¶

The prefix summary array is an efficient way to calculate the value of red and green tiles without having to iterate over all the elements repetitively.

In [84]:
def buildPSA(grid: list[list[int]]) -> list[list[int]]:

    psa = [[0] * len(row) for row in grid]

    for y in range(len(psa)):
        for x in range(len(psa[0])):
            left = psa[y][x- 1] if x > 0 else 0
            top = psa[y-1][x] if y > 0 else 0
            topleft = psa[y - 1][x - 1] if x > 0 < y else 0
            psa[y][x] = left + top - topleft + grid[y][x]
    
    return psa
    
psa = buildPSA(grid)
s_psa = buildPSA(small_grid)

for row in s_psa:
    print("".join([f"{n:02d}|" for n in row]))
00|00|01|02|03|04|05|
00|00|02|04|06|08|10|
01|02|05|08|11|14|17|
02|04|08|12|16|20|24|
03|06|11|16|21|26|31|
03|06|11|16|22|28|34|
03|06|11|16|23|30|37|
In [85]:
def valid(psa, x1, y1, x2, y2,xs,ys):
    cx1, cx2 = sorted([xs.index(x1)*2, xs.index(x2)*2])
    cy1, cy2 = sorted([ys.index(y1)*2, ys.index(y2)*2])
    left    = psa[cy2][cx1 - 1]   if cx1 > 0 else 0
    top     = psa[cy1-1][cx2]     if cy1 > 0 else 0
    topleft = psa[cy1-1][cx1 - 1] if (cx1 > 0 < cy1) else 0
    count = psa[cy2][cx2] - left - top + topleft  
    return count == (abs(cx2 - cx1) + 1) * (abs(cy2 - cy1) + 1)

sxs = sorted({ x for x,_ in small_points })
sys = sorted({ y for _,y in small_points })
areas = [((x1,y1,x2,y2),(abs(x2 - x1) + 1) * (abs(y2 - y1) + 1)) for i, (x1, y1) in enumerate(small_points) for x2, y2 in small_points[:i]]
valid_areas = [((x1,y1,x2,y2),(abs(x2 - x1) + 1) * (abs(y2 - y1) + 1)) for i, (x1, y1) in enumerate(small_points) for x2, y2 in small_points[:i] if valid(s_psa,x1,y1,x2,y2,sxs,sys)]
invalid_areas = [((x1,y1,x2,y2),(abs(x2 - x1) + 1) * (abs(y2 - y1) + 1)) for i, (x1, y1) in enumerate(small_points) for x2, y2 in small_points[:i] if not valid(s_psa,x1,y1,x2,y2,sxs,sys)]
small_ans1 = max((abs(x2 - x1) + 1) * (abs(y2 - y1) + 1) for i, (x1, y1) in enumerate(small_points) for x2, y2 in small_points[:i] if valid(s_psa,x1,y1,x2,y2,sxs,sys))

print(f"The areas are \n{areas}")
print(f"The valid areas are \n{valid_areas}")
print(f"The invalid areas are \n{invalid_areas}")
print(f"The answer for part two example prompt is {small_ans1}")

xs = sorted({ x for x,_ in points })
ys = sorted({ y for _,y in points })
ans1 = max((abs(x2 - x1) + 1) * (abs(y2 - y1) + 1) for i, (x1, y1) in enumerate(points) for x2, y2 in points[:i] if valid(psa,x1, y1, x2, y2,xs,ys))
print(f"The answer for part two is {ans1}")
The areas are 
[((11, 1, 7, 1), 5), ((11, 7, 7, 1), 35), ((11, 7, 11, 1), 7), ((9, 7, 7, 1), 21), ((9, 7, 11, 1), 21), ((9, 7, 11, 7), 3), ((9, 5, 7, 1), 15), ((9, 5, 11, 1), 15), ((9, 5, 11, 7), 9), ((9, 5, 9, 7), 3), ((2, 5, 7, 1), 30), ((2, 5, 11, 1), 50), ((2, 5, 11, 7), 30), ((2, 5, 9, 7), 24), ((2, 5, 9, 5), 8), ((2, 3, 7, 1), 18), ((2, 3, 11, 1), 30), ((2, 3, 11, 7), 50), ((2, 3, 9, 7), 40), ((2, 3, 9, 5), 24), ((2, 3, 2, 5), 3), ((7, 3, 7, 1), 3), ((7, 3, 11, 1), 15), ((7, 3, 11, 7), 25), ((7, 3, 9, 7), 15), ((7, 3, 9, 5), 9), ((7, 3, 2, 5), 18), ((7, 3, 2, 3), 6)]
The valid areas are 
[((11, 1, 7, 1), 5), ((11, 7, 11, 1), 7), ((9, 7, 11, 1), 21), ((9, 7, 11, 7), 3), ((9, 5, 7, 1), 15), ((9, 5, 11, 1), 15), ((9, 5, 11, 7), 9), ((9, 5, 9, 7), 3), ((2, 5, 9, 5), 8), ((2, 3, 9, 5), 24), ((2, 3, 2, 5), 3), ((7, 3, 7, 1), 3), ((7, 3, 11, 1), 15), ((7, 3, 9, 5), 9), ((7, 3, 2, 5), 18), ((7, 3, 2, 3), 6)]
The invalid areas are 
[((11, 7, 7, 1), 35), ((9, 7, 7, 1), 21), ((2, 5, 7, 1), 30), ((2, 5, 11, 1), 50), ((2, 5, 11, 7), 30), ((2, 5, 9, 7), 24), ((2, 3, 7, 1), 18), ((2, 3, 11, 1), 30), ((2, 3, 11, 7), 50), ((2, 3, 9, 7), 40), ((7, 3, 11, 7), 25), ((7, 3, 9, 7), 15)]
The answer for part two example prompt is 24
The answer for part two is 1501292304

Please comment on this solution