aoc/22/day17.py
2023-12-04 10:25:33 +01:00

311 lines
8.7 KiB
Python
Executable file

#!/bin/python3
from enum import Enum
from collections import defaultdict
from itertools import pairwise, combinations
class Move(Enum):
LEFT = 1
RIGHT = 2
DOWN = 3
moves = list()
with open('inputs/day17.txt', 'r') as f:
for line in f:
for char in line.strip():
if char == '<':
moves.append(Move.LEFT)
elif char == '>':
moves.append(Move.RIGHT)
def move_stream():
while True:
for i, d in enumerate(moves):
yield (i, d)
yield (i, Move.DOWN)
move_map = {Move.LEFT: (-1, 0), Move.RIGHT: (1, 0), Move.DOWN: (0, -1)}
class Rock():
def __init__(self, shape):
self.blocks = shape
def init_position(self, y_top):
self.blocks = list(map(lambda pos: (pos[0] + 2, pos[1] + y_top + 4), self.blocks))
def move(self, chamber, move):
new_blocks = self.move_blocks(move)
if self.check_valid(chamber, new_blocks):
self.blocks = new_blocks
return True
else:
return False
def move_blocks(self, move):
(movex, movey) = move_map[move]
return list(map(lambda block: (block[0] + movex, block[1] + movey), self.blocks))
def check_valid(self, chamber, blocks):
for (x, y) in blocks:
if x > 6 or x < 0 or chamber[y][x]:
return False
return True
shapes = [
[(0, 0), (1, 0), (2, 0), (3, 0)],
[(0, 1), (1, 1), (2, 1), (1, 0), (1, 2)],
[(0, 0), (1, 0), (2, 0), (2, 1), (2, 2)],
[(0, 0), (0, 1), (0, 2), (0, 3)],
[(0, 0), (1, 0), (0, 1), (1, 1)]
]
def rock_stream():
while True:
for i, shape in enumerate(shapes):
yield (i, Rock(shape.copy()))
chamber = defaultdict(lambda: defaultdict(lambda: False))
for i in range(7):
chamber[-1][i] = True
def print_chamber(chamber, rock, print_index):
keys = list(reversed(sorted(chamber.keys())))
for key in keys:
if print_index:
print(f'{key}\t', end='')
for i in range(7):
if chamber[key][i]:
print('#', end='')
elif rock and (i, key) in rock.blocks:
print('@', end='')
else:
print('.', end='')
print('')
def prune_chamber(chamber):
ys = list(reversed(sorted(chamber.keys())))
prune_height = None
for y_high, y_low in pairwise(ys):
prunable = True
for x in range(7):
if not chamber[y_high][x] and not chamber[y_low][x]:
prunable = False
break
if prunable:
prune_height = y_low
break
if prune_height:
for y in ys:
if y < prune_height:
del chamber[y]
return prune_height != None
def row_to_mask(row):
mask = 0
bit = 1
for x in range(6, -1, -1):
if row[x]:
mask = mask | bit
bit = bit << 1
return mask
barrier_fingerprints = defaultdict(lambda: list())
rock_count = 20220
prune_freq = 100
y_top = -1
moves_gen = move_stream()
rocks_gen = rock_stream()
rocks_fallen = 0
rock_idx = None
for rock_idx, rock in rocks_gen:
rock.init_position(y_top)
for move_idx, move in moves_gen:
moved = rock.move(chamber, move)
if not moved and move == Move.DOWN:
for (x, y) in rock.blocks:
if y > y_top:
y_top = y
chamber[y][x] = True
rocks_fallen += 1
break
ys = set(map(lambda x: x[1], rock.blocks))
for y in ys:
all_filled = True
for x in range(7):
if not chamber[y][x] and not chamber[y-1][x]:
all_filled = False
break
if all_filled:
mask_high = row_to_mask(chamber[y])
mask_low = row_to_mask(chamber[y-1])
fingerprint = (rock_idx, move_idx, mask_high, mask_low)
barrier_fingerprints[fingerprint].append((rocks_fallen, y))
if rocks_fallen == rock_count:
break
print('part1', y_top + 1)
candidate_fp = None
candidate_ys_count = 0
for fp, ys in barrier_fingerprints.items():
l = len(ys)
if l > candidate_ys_count:
candidate_ys_count = l
candidate_fp = fp
# print('candidate FP:', candidate_fp)
first_occ = barrier_fingerprints[candidate_fp][0]
second_occ = barrier_fingerprints[candidate_fp][1]
height_cycle = second_occ[1] - first_occ[1]
rock_cycle = second_occ[0] - first_occ[0]
# print('first occurance:', 'height', first_occ[1], 'fallen', first_occ[0])
# print('second occurance:', 'height', second_occ[1], 'fallen', second_occ[0])
# print('height cycle:', second_occ[1] - first_occ[1])
# print('rock cycle:', second_occ[0] - first_occ[0])
rocks_left = 1000000000000 - first_occ[0]
superheight = first_occ[1]
superheight += (rocks_left // rock_cycle) * height_cycle
rocks_left %= rock_cycle
print(rocks_left, superheight)
# We just need $rocks_left rocks more!
# Move to the next cycle point
# Then simulate $rocks_left movements to get the height this generates.
# Add this to superheight then subtract one height_cycle.
for rock_idx, rock in rocks_gen:
rock.init_position(y_top)
for move_idx, move in moves_gen:
moved = rock.move(chamber, move)
if not moved and move == Move.DOWN:
for (x, y) in rock.blocks:
if y > y_top:
y_top = y
chamber[y][x] = True
rocks_fallen += 1
break
ys = set(map(lambda x: x[1], rock.blocks))
cycle_start = False
for y in ys:
all_filled = True
for x in range(7):
if not chamber[y][x] and not chamber[y-1][x]:
all_filled = False
break
if all_filled:
mask_high = row_to_mask(chamber[y])
mask_low = row_to_mask(chamber[y-1])
fingerprint = (rock_idx, move_idx, mask_high, mask_low)
if fingerprint == candidate_fp:
cycle_start = True
break
if cycle_start:
break
start_height = y_top
for rock_idx, rock in rocks_gen:
rock.init_position(y_top)
for move_idx, move in moves_gen:
moved = rock.move(chamber, move)
if not moved and move == Move.DOWN:
for (x, y) in rock.blocks:
if y > y_top:
y_top = y
chamber[y][x] = True
rocks_left -= 1
break
if rocks_left == 0:
break
end_height = y_top
superheight += end_height - start_height
print('part2', superheight + 3)
# for fp, ys in barrier_fingerprints.items():
# print('fingerprint:', fp)
# print('count:', len(ys))
# NEW IDEA
# Find a cycle.
# A cycle can be characterized by: rock rotation, direction rotation and rock formation.
# Problem is, we need to save a lot of the rock formation, because new rocks can fall down a long while.
# Additionally, we cannot just save the height of each column, because rocks can move underneath hanging arches.
# Possible solution: find lines that block this.
# Possibly in one line, definitely in two lines.
# IDEA:
# Create bitmap.
# Easily compare bitmaps with eachother to find cycle
# def print_mask(mask):
# bit = 1 << 6
# for i in range(7):
# if mask & (bit >> i):
# print('1', end='')
# else:
# print('0', end='')
# print('')
# def print_bitmap(bitmap):
# for row in bitmap:
# print_mask(row)
# bitmap = []
# ys = list(reversed(sorted(chamber.keys())))
# y_min = min(ys)
# y_max = max(ys)
# for y in range(y_min + 1, y_max + 1):
# mask = 0
# bit = 1
# for x in range(6, -1, -1):
# if chamber[y][x]:
# mask = mask | bit
# bit = bit << 1
# print_mask(mask)
# bitmap.append(mask)
# def in_order(l):
# last = None
# for x in l:
# if last == None or x > last:
# last = x
# else:
# return False
# return True
# def same_distance(l):
# [y1, y2, y3] = l
# return y2 - y1 == y3 - y2
# def is_cycle(bitmap, l):
# [y1, y2, y3] = l
# distance = y3 - y2
# for i in range(distance + 1):
# if bitmap[y1+i] != bitmap[y2]+i:
# return False
# return True
# Now try to find the cycle!
# We loop over every row.
# We find all indices of this row in the whole chamber
# checked = []
# print('finding cycle')
# for row in bitmap:
# print('row:', row)
# if row not in checked:
# checked.append(row)
# indices = [i for i, x in enumerate(bitmap) if x == row]
# if len(indices) >= 3:
# for maybe_cycle in combinations(indices, 3):
# if in_order(maybe_cycle) and same_distance(maybe_cycle):
# if is_cycle(bitmap, maybe_cycle):
# print('FOUND CYCLE:', maybe_cycle)
# # print_bitmap(bitmap)