Day 17
This commit is contained in:
parent
fd76032ee1
commit
81cc189717
1 changed files with 334 additions and 0 deletions
334
day17.py
Normal file
334
day17.py
Normal file
|
@ -0,0 +1,334 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import re
|
||||
from random import randrange
|
||||
from array import array
|
||||
from copy import copy
|
||||
|
||||
class VM():
|
||||
def __init__(self, reg: array = array('q'), mem: list = [], pc: int = 0):
|
||||
self.reg = copy(reg)
|
||||
self.mem = copy(mem)
|
||||
self.pc = copy(pc)
|
||||
self.outbuf = []
|
||||
|
||||
def run(self, abort: int = 0):
|
||||
while self.pc < len(self.mem):
|
||||
if abort>0 and len(self.mem) > abort: return
|
||||
|
||||
op = self.mem[self.pc]
|
||||
self.pc += 1
|
||||
|
||||
if op == 0b000: # adv
|
||||
self.reg[0] = self.reg[0] // (2**self._combo(self.mem[self.pc]))
|
||||
self.pc += 1
|
||||
if op == 0b001: # bxl
|
||||
self.reg[1] = self.reg[1] ^ self.mem[self.pc]
|
||||
self.pc += 1
|
||||
if op == 0b010: # bst
|
||||
self.reg[1] = self._combo(self.mem[self.pc]) & 0b111
|
||||
self.pc += 1
|
||||
if op == 0b011: # jnz
|
||||
self.pc = self.mem[self.pc] if self.reg[0] != 0 else self.pc+1
|
||||
if op == 0b100: # bxc
|
||||
self.reg[1] = self.reg[1] ^ self.reg[2]
|
||||
self.pc += 1
|
||||
if op == 0b101: # out
|
||||
self.outbuf.append(self._combo(self.mem[self.pc]) & 0b111)
|
||||
self.pc += 1
|
||||
if op == 0b110: # bdv
|
||||
self.reg[1] = self.reg[0] // (2**self._combo(self.mem[self.pc]))
|
||||
self.pc += 1
|
||||
if op == 0b111: # cdv
|
||||
self.reg[2] = self.reg[0] // (2**self._combo(self.mem[self.pc]))
|
||||
self.pc += 1
|
||||
|
||||
def _combo(self, c) -> int:
|
||||
if c == 0: return 0
|
||||
if c == 1: return 1
|
||||
if c == 2: return 2
|
||||
if c == 3: return 3
|
||||
if c == 4: return self.reg[0]
|
||||
if c == 5: return self.reg[1]
|
||||
if c == 6: return self.reg[2]
|
||||
raise RuntimeError()
|
||||
|
||||
def flush(self, binfmt = False):
|
||||
if not binfmt:
|
||||
print(','.join(map(str, self.outbuf)))
|
||||
else:
|
||||
for o in self.outbuf:
|
||||
print(f"{o:03b},", end="")
|
||||
print()
|
||||
|
||||
def cmp(self, prog):
|
||||
return self.outbuf == prog
|
||||
|
||||
|
||||
reg: array = array('q')
|
||||
mem: list = []
|
||||
pc: int = 0
|
||||
|
||||
with open('day17') as f:
|
||||
reg_a = int(re.match('Register A: ([0-9]+)', f.readline()).groups()[0])
|
||||
reg_b = int(re.match('Register B: ([0-9]+)', f.readline()).groups()[0])
|
||||
reg_c = int(re.match('Register C: ([0-9]+)', f.readline()).groups()[0])
|
||||
|
||||
reg = array('q', [reg_a, reg_b, reg_c])
|
||||
|
||||
f.readline()
|
||||
|
||||
mem_str = re.match('Program: (.*)', f.readline()).groups()[0]
|
||||
|
||||
for c in mem_str:
|
||||
if c == ',': continue
|
||||
mem.append(int(c))
|
||||
|
||||
# day 1
|
||||
|
||||
vm = VM(reg, mem, pc)
|
||||
vm.run()
|
||||
vm.flush()
|
||||
|
||||
# day 2
|
||||
|
||||
# enumerates all 3 bit number, then increases size by one
|
||||
# enumerates them again
|
||||
|
||||
# -> we can find first boundaries by getting numbers where
|
||||
# output buffer length is too small or too large
|
||||
|
||||
# but first we need to find where to even start
|
||||
# we know the reg_a from the puzzle input is
|
||||
# too small, but start from here and increas rapidly:
|
||||
|
||||
reg_a = 17323786
|
||||
while True:
|
||||
reg = array('q', [reg_a, reg_b, reg_c])
|
||||
vm = VM(reg, mem, pc)
|
||||
vm.run(abort=len(mem))
|
||||
if vm.cmp(mem): break
|
||||
|
||||
if len(mem) == len(vm.outbuf): break
|
||||
|
||||
reg_a *= 10
|
||||
|
||||
|
||||
print(reg_a)
|
||||
vm.flush()
|
||||
|
||||
# we get: 173237860000000
|
||||
# producing a program: 4,4,4,4,1,6,0,0,0,2,1,0,0,1,3,0
|
||||
# that's exactly the length we want
|
||||
|
||||
# let's find our first set of boundaries so we can start a search
|
||||
known_bytes = vm.outbuf[-2:]
|
||||
init_reg_a = reg_a
|
||||
upper_bound = -1
|
||||
lower_bound = -1
|
||||
|
||||
reg_a = init_reg_a
|
||||
while True:
|
||||
reg = array('q', [reg_a, reg_b, reg_c])
|
||||
vm = VM(reg, mem, pc)
|
||||
vm.run(abort=len(mem))
|
||||
if vm.cmp(mem): break
|
||||
|
||||
if len(vm.outbuf) > len(mem):
|
||||
upper_bound = reg_a
|
||||
break
|
||||
|
||||
reg_a *= 10
|
||||
|
||||
|
||||
print(reg_a)
|
||||
vm.flush()
|
||||
|
||||
reg_a = init_reg_a
|
||||
while True:
|
||||
reg = array('q', [reg_a, reg_b, reg_c])
|
||||
vm = VM(reg, mem, pc)
|
||||
vm.run(abort=len(mem))
|
||||
if vm.cmp(mem): break
|
||||
|
||||
if len(vm.outbuf) < len(mem):
|
||||
lower_bound = reg_a
|
||||
break
|
||||
|
||||
reg_a //= 10
|
||||
|
||||
|
||||
print(reg_a)
|
||||
vm.flush()
|
||||
|
||||
|
||||
# that was fast again and did not produce very enlightening bounds,
|
||||
# nevertheless we have bounds:
|
||||
#
|
||||
# ub: 1732378600000000
|
||||
# out: 4,4,0,7,0,7,6,0,3,5,1,5,4,2,4,4,2
|
||||
#
|
||||
# mid: 173237860000000
|
||||
# out: 4,4,4,4,1,6,0,0,0,2,1,0,0,1,3,0
|
||||
#
|
||||
# lb: 17323786000000
|
||||
# out: 4,4,5,5,5,0,1,4,6,4,4,4,7,3,7
|
||||
|
||||
|
||||
# helper function
|
||||
def cmp_bytes(a,b):
|
||||
if len(a) != len(b): return []
|
||||
|
||||
same = []
|
||||
for i in range(1, len(a)):
|
||||
if a[-i] == b[-i]:
|
||||
same.append(a[-i])
|
||||
continue
|
||||
break
|
||||
|
||||
same.reverse()
|
||||
return same
|
||||
|
||||
# params for our algorithm to tune
|
||||
generation = 1
|
||||
population = 1
|
||||
mu = 173237860000000 # current best guess
|
||||
mu_hit = 2 # correct positions of mu
|
||||
mu_len = 16
|
||||
sd = 171505481400000 # standard deviation, (up-low)*0.1 - I pulled that out of my nose
|
||||
lo = 17323786000000 # lower bound
|
||||
lo_hit = 0
|
||||
lo_len = 15
|
||||
up = 1732378600000000 # upper bound
|
||||
up_hit = 0
|
||||
up_len = 17
|
||||
|
||||
|
||||
from day17_truncnorm import Truncnorm
|
||||
truncnorm = Truncnorm(mu, sd, lo, up)
|
||||
|
||||
import random
|
||||
|
||||
prodigy = mu
|
||||
prodigy_hit = mu_hit
|
||||
prodigy_len = mu_len
|
||||
|
||||
mus = [(mu, mu_hit, mu_len)]
|
||||
|
||||
evolve = True
|
||||
while evolve:
|
||||
# each generation we birth 10000 kin. The new generation's genes center
|
||||
# around the best produced offsprings so far. However, we allow for
|
||||
# mutations with decreasing likelihood the more deformed the kin would be.
|
||||
#
|
||||
# The mutations are bounded. Mutations outside these bounds lead to
|
||||
# a miscarriage.
|
||||
#
|
||||
# With each generation we re-asses and adapt our parameters.
|
||||
#
|
||||
# Occasionally a catastrophic event happens erasing the whole
|
||||
# popluation. This hopefully prevents us from being trapped in a
|
||||
# local optima.
|
||||
#
|
||||
# To preserve their legacy during these catastrophic events the mu
|
||||
# collect and freeze prodigies - the most promising genes they
|
||||
# know.
|
||||
#
|
||||
# All this allows us to search a search space probabilistically which
|
||||
# would be way too large to search exhaustively.
|
||||
|
||||
catastrophy = random.randint(0,100)
|
||||
if catastrophy % 10 == 0:
|
||||
mus.append((mu, mu_hit, mu_len))
|
||||
print(f"\033[31mA catastrophy erased the entire population. Not all is lost, a new {prodigy=} awakes.\033[0m")
|
||||
mu = prodigy
|
||||
mu_hit = prodigy_hit
|
||||
mu_len = prodigy_len
|
||||
print(f"\033[31mIn a last attempt to preserve their culture the best known mus are engraved in a stone wall:\033[0m {mus=}")
|
||||
sd = round(random.uniform(0.00001, 1.0)*(up-lo))
|
||||
print(f"\033[31mEvolution restarts with {sd=}\033[0m")
|
||||
population += 1
|
||||
generation = 1
|
||||
|
||||
print(f"\033[35mGeneration {generation} (Population {population}):\033[0m {mu=}, {sd=}, {lo=}, {up=}")
|
||||
|
||||
|
||||
for kin in truncnorm.draw(30000):
|
||||
kin = round(kin)
|
||||
|
||||
reg = array('q', [kin, reg_b, reg_c])
|
||||
vm = VM(reg, mem, pc)
|
||||
vm.run()
|
||||
|
||||
if len(cmp_bytes(vm.outbuf, mem))>mu_hit:
|
||||
mu = kin
|
||||
mu_hit = len(cmp_bytes(vm.outbuf, mem))
|
||||
mu_len = len(vm.outbuf)
|
||||
print(f"\033[32mFound new best kin: {mu=}, {mu_hit=}\033[0m", end=" ")
|
||||
vm.flush()
|
||||
continue
|
||||
|
||||
if len(cmp_bytes(vm.outbuf, mem))==mu_hit and kin<mu:
|
||||
mu = kin
|
||||
mu_hit = len(cmp_bytes(vm.outbuf, mem))
|
||||
mu_len = len(vm.outbuf)
|
||||
print(f"\033[32mFound new best kin: {mu=}, {mu_hit=}\033[0m", end=" ")
|
||||
vm.flush()
|
||||
continue
|
||||
|
||||
if kin < mu and kin < prodigy and len(cmp_bytes(vm.outbuf, mem)) > 3:
|
||||
prodigy = kin
|
||||
prodigy_hit = len(cmp_bytes(vm.outbuf, mem))
|
||||
prodigy_len = len(vm.outbuf)
|
||||
print(f"Found new prodigy: {prodigy=}, {prodigy_hit=}", end=" ")
|
||||
vm.flush()
|
||||
|
||||
if len(vm.outbuf) < mu_len and kin>lo:
|
||||
lo = kin
|
||||
lo_hit = len(cmp_bytes(vm.outbuf, mem))
|
||||
lo_len = len(vm.outbuf)
|
||||
print(f"Found new lower bound: {lo}, {lo_hit=}, {lo_len=}", end=" ")
|
||||
vm.flush()
|
||||
continue
|
||||
|
||||
if len(vm.outbuf) > mu_len and kin<up:
|
||||
up = kin
|
||||
up_hit = len(cmp_bytes(vm.outbuf, mem))
|
||||
up_len = len(vm.outbuf)
|
||||
print(f"Found new upper bound: {up}, {up_hit=}, {up_len=}", end=" ")
|
||||
vm.flush()
|
||||
continue
|
||||
|
||||
assert(lo < mu)
|
||||
assert(mu < up)
|
||||
|
||||
|
||||
sd = sd//generation
|
||||
if sd<100:
|
||||
sd = round(random.uniform(0.00001, 1.0)*(up-lo))
|
||||
print(f"\033[36mA fungus infected the generation and changed mutations. New mutation with {sd=}\033[0m")
|
||||
|
||||
print(f"{sd=}")
|
||||
truncnorm = Truncnorm(mu, sd, lo, up)
|
||||
generation += 1
|
||||
|
||||
# we evolved to a species capable of genetic engineering the very best
|
||||
# prodigy we found is 164278764924544. We even found that multiple
|
||||
# times. Now brute force ourselves to the top of the food chain.
|
||||
#
|
||||
# Our probabilistic algorithm is good a finding a a very good solution
|
||||
# (up to 15 elements correct) but it is actually bad at pinpointing
|
||||
# the exactly best solution.
|
||||
|
||||
mu = 164278764924544
|
||||
for breed in range(mu-(2**(5*3)), mu+(2**(5*3))):
|
||||
reg = array('q', [breed, reg_b, reg_c])
|
||||
vm = VM(reg, mem, pc)
|
||||
vm.run()
|
||||
|
||||
if vm.cmp(mem):
|
||||
print(f"Self replicating {breed=}")
|
||||
vm.flush()
|
||||
|
||||
print(reg_a)
|
||||
vm.flush()
|
Loading…
Add table
Reference in a new issue