Even though it wasn't remotely in my control, I love my nice even score this year.
I get your point but that graph labelling is atrocious
Unfortunately the article is paywalled, so I can't see them.
What qualifies this as data is beautiful?
feed her a stew that blinds her for 1 day
I struggled with optimizing this puzzle and had to go online to figure it out too, so I'm glad my hint helped you out.
I actually did a lot of optimizations before I had to give up and search what I was missing. I did have a look at my input data, but still wouldn't make the connection because in my mind, any solution had to work for the sample too.
Tap for spoiler
That's very interesting because that was one of my early approaches too, but it actually gave me the wrong answer for my input!
Python
You got me good, Eric.
hint
Think of the simplest check you can make for a region.
click to view code
# This does not work for the sample :D
def solve(data: str):
# split input
blocks = data.split("\n\n")
shape_blocks = blocks[:-1]
region_block = blocks[-1]
# for every shape, get the number of occupied cells
shape_area = []
for shape_block in shape_blocks:
shape_area.append(shape_block.count('#'))
fit_regions = 0
# for every region, check if the area is sufficient to fit all shapes
for region_data in region_block.splitlines():
size_data, shape_data = region_data.split(': ')
# get region size
m, n = [int(dim) for dim in size_data.split('x')]
# get area needed to fit all shapes, without considering arrangement
area_needed = 0
for id, freq in enumerate(map(int, shape_data.split(' '))):
area_needed += shape_area[id] * freq
# if the region area is sufficient, count it as a fit (!!!)
if m * n > area_needed:
fit_regions += 1
return fit_regions
Python
Part 1 can be done with simple dfs / backtracking, even without memoization.
Part 2 needed to have dp / memoization to finish in a reasonable time. Initially, I also checked for cycles in the path, but I removed the check once I realized there were none. Also, instead of adding booleans / int to my memoization function, I chose to compute both path arrangements (svr -> fft -> dac -> out AND svr -> dac -> fft -> out) and add them up.
click to view code
from collections import defaultdict
# build the adjacency graph from the input data
def build_graph(data: str):
rules_data = data.splitlines()
graph = defaultdict(list)
for rule_data in rules_data:
from_node, to_nodes = rule_data.split(": ")
graph[from_node].extend(to_nodes.split(' '))
return graph
# simply dfs every path from 'you' until we reach 'out'
# this is what I initially used for part 1
def count_paths_dfs(data: str, start = "you", end = "out"):
graph = build_graph(data)
paths_to_end = 0
stack = [start] # currently active paths
while stack:
node = stack.pop()
if node == end:
# we reached end, no need to check further
paths_to_end += 1
continue
# every node connected to current node is a potential path
stack.extend(graph[node])
return paths_to_end
# memoized version of counting paths from start to end
def count_paths_memo(graph: defaultdict[list], start = "you", end = "out"):
# I used to have some code to check for cycles, but thankfully it wasn't needed
# its pretty much same logic as count_paths_dfs, except recursive and with memoization
memo = {}
def backtrack(curr: str):
if curr in memo:
return memo[curr]
if curr == end:
ans = 1
else:
ans = sum(backtrack(conn) for conn in graph[curr])
memo[curr] = ans
return ans
return backtrack(start)
# Optimized part 1 using memoization (not necessary, but faster)
def part1_with_memoiz(data: str, start = "you", end = "out"):
graph = build_graph(data)
return count_paths_memo(graph, start, end)
# Part 2 requires counting paths from "svr" to "out",
# including only those paths that go through both "fft" and "dac"
# Instead of adding booleans / int to my memoization function,
# I chose to compute all path arrangements and add them up
def part2(data: str):
graph = build_graph(data)
# getting from start to one requirement
svr_to_fft = count_paths_memo(graph, "svr", "fft")
svr_to_dac = count_paths_memo(graph, "svr", "dac")
# getting from one requirement to the other
fft_to_dac = count_paths_memo(graph, "fft", "dac")
dac_to_fft = count_paths_memo(graph, "dac", "fft")
# the final push to out
fft_to_out = count_paths_memo(graph, "fft", "out")
dac_to_out = count_paths_memo(graph, "dac", "out")
# total paths = (start -> fft -> dac -> out) + (start -> dac -> fft -> out)
svr_to_out = (
svr_to_dac * dac_to_fft * fft_to_out +
svr_to_fft * fft_to_dac * dac_to_out
)
return svr_to_out
sample_p1 = """aaa: you hhh
you: bbb ccc
bbb: ddd eee
ccc: ddd eee fff
ddd: ggg
eee: out
fff: out
ggg: out
hhh: ccc fff iii
iii: out"""
assert (t := count_paths_dfs(sample_p1)) == 5, f"Expected 5, got {t}"
assert (t := part1_with_memoiz(sample_p1)) == 5, f"Expected 5, got {t}"
sample_p2 = """svr: aaa bbb
aaa: fft
fft: ccc
bbb: tty
tty: ccc
ccc: ddd eee
ddd: hub
hub: fff
eee: dac
dac: fff
fff: ggg hhh
ggg: out
hhh: out"""
assert (t := part2(sample_p2)) == 2, f"Expected 2, got {t}"
Python
Part 2 is only partially optimized (runs in ~7.5s).
The optimization I did do is to compress the grid coords by recording all red tiles and their adjacent cells only (thanks Everybody,Codes #15)
The part which needs further optimization is my approach to getting the largest rectangle in the polygon. I did it by flood-filling the area outside the polygon, then walking through all cells of every pair of red tiles to make sure their rectangle is within the polygon.
click to view code
# combinations helps us get all pairs of red tiles
# pairwise helps us get all adjacent pairs of red tiles
from itertools import combinations, pairwise
from collections import deque
def get_area(p1, p2):
x1, y1 = p1
x2, y2 = p2
return (abs(x2 - x1) + 1) * (abs(y2 - y1) + 1)
@timer()
def part1(data: str):
red_tiles = [tuple(map(int, line.split(','))) for line in data.splitlines()]
max_area = 0
for r1, r2 in combinations(red_tiles, 2):
max_area = max(max_area, get_area(r1, r2))
return max_area
def compress_coordinates(coords: list[tuple[int, int]]):
i_of_interest_set = set()
j_of_interest_set = set()
for i, j in coords:
i_of_interest_set.update([i-1, i, i+1])
j_of_interest_set.update([j-1, j, j+1])
sorted_is = sorted(list(i_of_interest_set))
sorted_js = sorted(list(j_of_interest_set))
m = len(sorted_is)
n = len(sorted_js)
compress_i_map = {val: id for id, val in enumerate(sorted_is)}
compress_j_map = {val: id for id, val in enumerate(sorted_js)}
return m, n, compress_i_map, compress_j_map
@timer()
def part2(data: str):
red_tiles = [tuple(map(int, line.split(','))) for line in data.splitlines()]
m, n, compress_i_map, compress_j_map = compress_coordinates(red_tiles)
def get_compressed_coords(og_coords: tuple[int, int]):
return compress_i_map[og_coords[0]], compress_j_map[og_coords[1]]
# make the grid
grid = [['.']*n for _ in range(m)]
for i, j in red_tiles:
ci, cj = get_compressed_coords((i, j))
grid[ci][cj] = '#'
# make the walls of the polygon enclosed by the red tiles
def make_line(r1, r2):
r1_i, r1_j = get_compressed_coords(r1)
r2_i, r2_j = get_compressed_coords(r2)
wall_char = '#'
if r1_i == r2_i:
# same row
row = r1_i
for col in range(min(r1_j, r2_j) + 1, max(r1_j, r2_j)):
grid[row][col] = wall_char
elif r1_j == r2_j:
# same col
col = r1_j
for row in range(min(r1_i, r2_i) + 1, max(r1_i, r2_i)):
grid[row][col] = wall_char
for r1, r2 in pairwise(red_tiles):
make_line(r1, r2)
make_line(red_tiles[-1], red_tiles[0])
# whiteout the exterior
DIRECTIONS = [(0, 1), (1, 0), (0, -1), (-1, 0)]
queue = deque([(0, 0)])
while queue:
i, j = queue.popleft()
if grid[i][j] == ' ':
continue
grid[i][j] = ' '
for di, dj in DIRECTIONS:
ni, nj = i+di, j+dj
if not (0 <= ni < m) or not (0 <= nj < n):
continue
if grid[ni][nj] != '.':
continue
queue.append((ni, nj))
# DEBUG: print the grid
# print()
# for row in grid:
# print(''.join(row))
# check if the rectangle formed by the corners r1, r2 is contained within the red-green polygon
def check_contained(r1, r2):
r1_i, r1_j = get_compressed_coords(r1)
r2_i, r2_j = get_compressed_coords(r2)
min_i, max_i = min(r1_i, r2_i), max(r1_i, r2_i)
min_j, max_j = min(r1_j, r2_j), max(r1_j, r2_j)
for i in range(min_i, max_i+1):
for j in range(min_j, max_j+1):
if grid[i][j] == ' ':
return False
return True
# get the max area of a rectangle that is contained within the red-green polygon
# and has red tiles at two of its corners
max_area = 0
for r1, r2 in combinations(red_tiles, 2):
area = get_area(r1, r2)
if area <= max_area:
# dont bother
continue
if not check_contained(r1, r2):
continue
max_area = area
return max_area
sample = """7,1
11,1
11,7
9,7
9,5
2,5
2,3
7,3"""
assert part1(sample) == 50
assert part2(sample) == 24
___