312 lines
8.7 KiB
Python
312 lines
8.7 KiB
Python
|
#!/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)
|