advent_of_code_2021

My attempts to work through the 2021 Advent of Code problems.
git clone https://git.eamoncaddigan.net/advent_of_code_2021.git
Log | Files | Refs | README | LICENSE

commit 069ee1e78513394cbe798aafb7b643100bf93556
parent d838a5e4566e7bb71a52e17946fd296537608752
Author: Eamon Caddigan <eamon.caddigan@gmail.com>
Date:   Sat, 25 Dec 2021 17:50:52 -0500

Solution to day 24, part 1

Diffstat:
Aday24_input.txt | 252+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aday24_part1.py | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 403 insertions(+), 0 deletions(-)

diff --git a/day24_input.txt b/day24_input.txt @@ -0,0 +1,252 @@ +inp w +mul x 0 +add x z +mod x 26 +div z 1 +add x 15 +eql x w +eql x 0 +mul y 0 +add y 25 +mul y x +add y 1 +mul z y +mul y 0 +add y w +add y 15 +mul y x +add z y +inp w +mul x 0 +add x z +mod x 26 +div z 1 +add x 15 +eql x w +eql x 0 +mul y 0 +add y 25 +mul y x +add y 1 +mul z y +mul y 0 +add y w +add y 10 +mul y x +add z y +inp w +mul x 0 +add x z +mod x 26 +div z 1 +add x 12 +eql x w +eql x 0 +mul y 0 +add y 25 +mul y x +add y 1 +mul z y +mul y 0 +add y w +add y 2 +mul y x +add z y +inp w +mul x 0 +add x z +mod x 26 +div z 1 +add x 13 +eql x w +eql x 0 +mul y 0 +add y 25 +mul y x +add y 1 +mul z y +mul y 0 +add y w +add y 16 +mul y x +add z y +inp w +mul x 0 +add x z +mod x 26 +div z 26 +add x -12 +eql x w +eql x 0 +mul y 0 +add y 25 +mul y x +add y 1 +mul z y +mul y 0 +add y w +add y 12 +mul y x +add z y +inp w +mul x 0 +add x z +mod x 26 +div z 1 +add x 10 +eql x w +eql x 0 +mul y 0 +add y 25 +mul y x +add y 1 +mul z y +mul y 0 +add y w +add y 11 +mul y x +add z y +inp w +mul x 0 +add x z +mod x 26 +div z 26 +add x -9 +eql x w +eql x 0 +mul y 0 +add y 25 +mul y x +add y 1 +mul z y +mul y 0 +add y w +add y 5 +mul y x +add z y +inp w +mul x 0 +add x z +mod x 26 +div z 1 +add x 14 +eql x w +eql x 0 +mul y 0 +add y 25 +mul y x +add y 1 +mul z y +mul y 0 +add y w +add y 16 +mul y x +add z y +inp w +mul x 0 +add x z +mod x 26 +div z 1 +add x 13 +eql x w +eql x 0 +mul y 0 +add y 25 +mul y x +add y 1 +mul z y +mul y 0 +add y w +add y 6 +mul y x +add z y +inp w +mul x 0 +add x z +mod x 26 +div z 26 +add x -14 +eql x w +eql x 0 +mul y 0 +add y 25 +mul y x +add y 1 +mul z y +mul y 0 +add y w +add y 15 +mul y x +add z y +inp w +mul x 0 +add x z +mod x 26 +div z 26 +add x -11 +eql x w +eql x 0 +mul y 0 +add y 25 +mul y x +add y 1 +mul z y +mul y 0 +add y w +add y 3 +mul y x +add z y +inp w +mul x 0 +add x z +mod x 26 +div z 26 +add x -2 +eql x w +eql x 0 +mul y 0 +add y 25 +mul y x +add y 1 +mul z y +mul y 0 +add y w +add y 12 +mul y x +add z y +inp w +mul x 0 +add x z +mod x 26 +div z 26 +add x -16 +eql x w +eql x 0 +mul y 0 +add y 25 +mul y x +add y 1 +mul z y +mul y 0 +add y w +add y 10 +mul y x +add z y +inp w +mul x 0 +add x z +mod x 26 +div z 26 +add x -14 +eql x w +eql x 0 +mul y 0 +add y 25 +mul y x +add y 1 +mul z y +mul y 0 +add y w +add y 13 +mul y x +add z y diff --git a/day24_part1.py b/day24_part1.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python +"""Advent of Code 2021, day 24 (part 1): Arithmetic Logic Unit +Emulate and validate input for a simple ALU""" + +# This one is tricky. Emulating the ALU is straightforward, but validating the +# input could blow up. Since there are 9^14 (~2^44) different model numbers, +# running through these is infeasible. The approach I'm taking right now is +# breaking the program into 14 different sections, divided by `inp` +# instruction. At the end of each section, we'll determine for preceding +# inputted values: +# * Which inputs are invalid (due to dividing by or taking modulus of 0) +# * The mapping from each _valid_ input to register values + +from typing import Tuple, List, Union, Dict, NewType, cast +from functools import reduce +import re + +from utils import get_puzzle_input + +REGISTER_TO_INDEX = {'w': 0, 'x': 1, 'y': 2, 'z': 3} + +Expression = NewType( + 'Expression', + Tuple[str, str, Union[None, int, str]] +) + +# Only four registers in the ALU +RegisterState = NewType( + 'RegisterState', + Tuple[int, int, int, int] +) + +def parse_input(input_string: str) -> List[Expression]: + """Honestly this just splits the string into a bunch of tuples, the only + fancy thing it does is deal with the third section smartly""" + instructions = [] + for expression in (l.split(' ') for l in input_string.rstrip('\n').split('\n')): + if len(expression) == 2: + instructions.append(Expression((expression[0], expression[1], None))) + elif re.match(r'[-+]?\d+', expression[2]) is not None: + instructions.append(Expression((expression[0], expression[1], + int(expression[2])))) + else: + instructions.append(cast(Expression, tuple(expression))) + return instructions + +def split_instructions(instructions: List[Expression]) -> List[List[Expression]]: + """Split the instructions into sections, each beginning with an `inp` + command""" + instructions_list: List[List[Expression]] = [] + for expression in instructions: + if expression[0] == 'inp': + instructions_list.append([]) + instructions_list[-1].append(expression) + return instructions_list + +def run_instruction(expression: Expression, register_state: RegisterState, + instruction_input: Union[int, None] = None) -> RegisterState: + """Run an instruction and update the registers""" + instruction, target, operand = expression + target_idx = REGISTER_TO_INDEX[target] + register_list = list(register_state) + + operand_value: Union[int, None] + if isinstance(operand, str): + operand_value = register_list[REGISTER_TO_INDEX[operand]] + else: + operand_value = operand + + # Note that if the operand is 0 and mod or div, we'll get a + # ZeroDivisionError, which is fine! + assert instruction == 'inp' or operand_value is not None + if instruction == 'inp': + if instruction_input is None: + raise RuntimeError("`inp` command with no input") + register_list[target_idx] = instruction_input + elif instruction == 'add': + register_list[target_idx] += cast(int, operand_value) + elif instruction == 'mul': + register_list[target_idx] *= cast(int, operand_value) + elif instruction == 'div': + register_list[target_idx] //= cast(int, operand_value) + elif instruction == 'mod': + if register_list[target_idx] < 0 or cast(int, operand_value) < 0: + raise ZeroDivisionError('ALU mod involving a negative number') + register_list[target_idx] %= cast(int, operand_value) + elif instruction == 'eql': + register_list[target_idx] = int( + register_list[target_idx] == cast(int, operand_value) + ) + else: + raise RuntimeError('Unimplemented instruction') + + return cast(RegisterState, tuple(register_list)) + +def run_routine(routine: List[Expression], register_state: RegisterState, + instruction_input: Union[int, None] = None) -> RegisterState: + """Run a list of instructions (typically beginning with an `inp` + instruction and ending before another)""" + return reduce(lambda r, e: run_instruction(e, r, instruction_input), + routine, + register_state) + +def run_routine_fast(routine: List[Expression], register_state: RegisterState, + instruction_input: int) -> RegisterState: + """Run puzzle input routines--note that this doesn't read the actual + instructions and doesn't work in the general case (as `run_rouine` does)""" + # It is, however, almost 22 times faster though + w_register = instruction_input + z_register = register_state[3] + x_register = int(w_register != z_register % 26 + cast(int, routine[5][2])) + z_register //= cast(int, routine[4][2]) # 1 or 26 + z_register *= 25 * x_register + 1 # Also 1 or 26 + y_register = (w_register + cast(int, routine[15][2])) * x_register + z_register += y_register + return RegisterState((w_register, x_register, y_register, z_register)) + +def test_inputs(routines: List[List[Expression]]) -> int: + """Return the highest value that results in a valid state of the ALU""" + # This operates on the assumption (gleaned through inspection) that only + # the z register actually gets carried forward from one block of + # instructions to another. So this finds the z value associated with each + # input at every step of the way, pruning input combinations that result in + # a z value that is obtained with a higher input combination + z_to_input = {0: 0} + for routine in routines: + new_z_to_input: Dict[int, int] = {} + for z_in, previous_inputs in z_to_input.items(): + for instruction_input in range(1, 10): + registers = run_routine_fast(routine, + RegisterState((0, 0, 0, z_in)), + instruction_input) + z_out = registers[3] + input_sequence = previous_inputs * 10 + instruction_input + if z_out not in new_z_to_input \ + or new_z_to_input[z_out] < input_sequence: + new_z_to_input[z_out] = input_sequence + z_to_input = new_z_to_input + return z_to_input[0] + +def solve_puzzle(input_string: str) -> int: + """Return the numeric solution to the puzzle""" + return test_inputs(split_instructions(parse_input(input_string))) + +def main() -> None: + """Run when the file is called as a script""" + print("Largest possible model number:", + solve_puzzle(get_puzzle_input(24))) + +if __name__ == "__main__": + main()