192 lines
5.8 KiB
Python
192 lines
5.8 KiB
Python
import sys
|
|
import numpy
|
|
|
|
def read_map(input):
|
|
map = [x.strip() for x in input]
|
|
|
|
if len(map) < 80:
|
|
print(map)
|
|
|
|
Y = len(map)
|
|
X = len(map[0])
|
|
assert all(len(row) == X for row in map)
|
|
assert X == Y
|
|
|
|
return map
|
|
|
|
def draw(mask, fill):
|
|
if len(fill) < 50:
|
|
for i, row in enumerate(fill):
|
|
print("".join(".O#!"[f + 2*mask[i,j]] for j,f in enumerate(row)))
|
|
print()
|
|
|
|
|
|
def solve(map):
|
|
Y = len(map)
|
|
X = len(map[0])
|
|
|
|
fill = numpy.zeros((Y,X+1), dtype='uint8')
|
|
mask = numpy.zeros((Y,X+1), dtype='uint8')
|
|
for i, row in enumerate(map):
|
|
for j, c in enumerate(row):
|
|
if c == '#':
|
|
mask[i,j] = 1
|
|
if c == 'S':
|
|
start = (i,j)
|
|
|
|
# add a border on the side of the map (to prevent wraparound)
|
|
mask[:, -1] = 1
|
|
|
|
# start at the start
|
|
fill[start] = 1
|
|
|
|
for i in range(64):
|
|
fill = step(mask, fill)
|
|
#draw(mask, fill)
|
|
if i == 6-1: # sample
|
|
print(fill.sum())
|
|
|
|
print("part1 = ", fill.sum())
|
|
|
|
|
|
def solve2(map):
|
|
Y = len(map)
|
|
X = len(map[0])
|
|
|
|
fill = numpy.zeros((Y,X), dtype='uint8')
|
|
mask = numpy.zeros((Y,X), dtype='uint8')
|
|
for i, row in enumerate(map):
|
|
for j, c in enumerate(row):
|
|
if c == '#':
|
|
mask[i,j] = 1
|
|
if c == 'S':
|
|
start = (i,j)
|
|
|
|
print(fill.shape, mask.shape)
|
|
|
|
fill[start] = 1
|
|
values, period, d2 = simulate(fill, mask, max_steps=500)
|
|
|
|
for n in 6,10,50,100,500,1000, 5000, 26501365:
|
|
print(n, extrapolate(n, values, period, d2))
|
|
|
|
|
|
def simulate(fill, mask, max_steps):
|
|
Y,X = mask.shape
|
|
assert Y == X, "need a square matrix"
|
|
period = X
|
|
tiles = 1
|
|
|
|
original_mask = mask
|
|
prev = [0]
|
|
diffs = []
|
|
for i in range(max_steps):
|
|
# expand the map if necessary
|
|
if fill[0].any() or fill[-1].any() or fill[:,0].any() or fill[:,-1].any():
|
|
draw(mask,fill)
|
|
print("expanding...")
|
|
tiles += 2
|
|
mask = numpy.tile(original_mask, (tiles,tiles))
|
|
fill = numpy.pad(fill, [(Y,Y), (X,X)])
|
|
|
|
# take one more step and count the number of reachable squares
|
|
fill = step(mask, fill)
|
|
n = int(fill.sum())
|
|
|
|
# look for patterns
|
|
# although the number of squares is somewhat unpreditable from step
|
|
# to step (due to the maze-like structure of the mask), the fact that
|
|
# the mask repeats in tiles (and the fact that there are unobstructed
|
|
# pathways in the orthogonal and diagonal directions) means that, in
|
|
# the long run, the flood fill will even out and -importantly- it should
|
|
# have some recognizable pattern every N steps (where N is the period
|
|
# of the tiling - 11 in the sample, 131 in the input). this is because
|
|
# we enter new tiles every N steps, and although it's hard to predict
|
|
# exactly how many of the squares in each tile we'll have visited,
|
|
# it should be the same number in every tile (or rather, there should
|
|
# be some small set of repeated tile-states, and we should be able to
|
|
# predict how many of each tile-state there will be).
|
|
#
|
|
# SO the first step is to get the difference between the current
|
|
# number of reachable squares and the number N steps ago.
|
|
#
|
|
# d1(i) = f(i) - f(i - N)
|
|
#
|
|
# we then have to discover some pattern in that sequence.
|
|
# we know the number of reachable squares will grow roughly as the
|
|
# square of the number of steps (because the map is 2D) so
|
|
# we should be looking for a quadratic relation.
|
|
# we can use second-order differences (see day 9) to do that.
|
|
# d1 is already a first-order difference, so take the difference
|
|
# between d1s to get a second-order difference. if our assumption is
|
|
# correct, then d1 should be a linear sequence and d2 should be a
|
|
# constant.
|
|
#
|
|
# d2(i) = d1(i) - d1(i-N)
|
|
# = (f(i) - f(i-N)) - (f(i-N) - f(i-N-N))
|
|
#
|
|
#
|
|
# there may be some unstability at the beginning of the simulation
|
|
# so we need to wait until the d2 values for every step in the period
|
|
# all agree.
|
|
#
|
|
# once it settles down, this gives us a set of N (11, 131, whatever)
|
|
# equations we can use to predict the number of squares after any
|
|
# future number of steps
|
|
#
|
|
d2 = 0
|
|
if len(prev) >= 2*period:
|
|
# find the second differece
|
|
d2 = (n - prev[-period]) - (prev[-period] - prev[-2*period])
|
|
diffs.append(d2)
|
|
prev.append(n)
|
|
print(i, n, d2, sep="\t", flush=True)
|
|
if len(diffs) > period and all_same_value(diffs[-period:]):
|
|
print("gotcha!")
|
|
return prev, period, diffs[-1]
|
|
assert False, "failed to find a stable pattern"
|
|
return prev, period, None
|
|
|
|
def all_same_value(list):
|
|
if len(list) < 1:
|
|
return False
|
|
x = list[0]
|
|
return all(x == y for y in list)
|
|
|
|
def extrapolate(n, values, period, d2):
|
|
if n < len(values):
|
|
return values[n]
|
|
quo, rem = divmod(n-len(values)+period, period)
|
|
x = len(values)-period+rem
|
|
y = values[-period+rem]
|
|
d1 = y - values[-2*period+rem]
|
|
while x < n:
|
|
d1 += d2
|
|
y += d1
|
|
x += period
|
|
assert x == n
|
|
return y
|
|
|
|
def step(mask, old):
|
|
# flood fill
|
|
#draw(fill)
|
|
fill = numpy.zeros(old.shape, dtype='uint8')
|
|
y,x = fill.shape
|
|
for i in range(y):
|
|
f = (old[i] == 1)
|
|
f = numpy.roll(f, 1) | numpy.roll(f, -1)
|
|
if i > 0: f |= (old[i-1] == 1)
|
|
if i < y-1: f |= (old[i+1] == 1)
|
|
f &= (mask[i] == 0)
|
|
#print(old, f)
|
|
if f.any():
|
|
fill[i, f] = 1
|
|
|
|
assert fill.any()
|
|
return fill
|
|
|
|
|
|
map = read_map(sys.stdin)
|
|
solve(map)
|
|
solve2(map)
|