diff --git a/README.md b/README.md index 896a44e..24bdbfd 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,14 @@ Dijkstra's algorithm solves part 1, with the weights of each edge being the sum Since Dijkstra's algorithm cheaply produces shortest distances from the exit to all other vertices in a single run, the solver can use these to quickly traverse the graph to find all minimum paths for part 2. In order to calculate the number of tiles for all these paths, duplicate path segments and crossings have to be detected. The solver attaches the number of tiles per path segment and the connected crossings to each vertex while constructing the graph to facilitate this. +### Day 17: Chronospatial Computer + +:mag_right: Puzzle: , :white_check_mark: Solver: [`ChronospatialComputer.cpp`](src/ChronospatialComputer.cpp) + +For part 1, the solver implements the eight defined instructions and a runtime containing the program, an instruction pointer, the three registers, and an output buffer, which allows the execution of the given program. The solver framework had to be updated to allow non-integer solutions, i.e. strings. + +For part 2, the algorithm appends iteratively 3-digit binary numbers in such a way, that the desired output is approached. The required search patterns and mappings were determined manually, and are predefined for the specific given program in code. It is not a generic solution. + ## Thanks * [Alexander Brouwer](https://github.com/Bromvlieg) for getting the project set up with CMake. diff --git a/include/aoc/ChronospatialComputer.hpp b/include/aoc/ChronospatialComputer.hpp index 86b7eb0..38ccaf4 100644 --- a/include/aoc/ChronospatialComputer.hpp +++ b/include/aoc/ChronospatialComputer.hpp @@ -37,4 +37,5 @@ class ChronospatialComputer std::array, 8> instructions_; std::vector program_; ChronospatialComputerState state_; + long long findQuineState(); }; diff --git a/include/aoc/extra/ChronospatialComputerInstruction.hpp b/include/aoc/extra/ChronospatialComputerInstruction.hpp index fa695bf..b6e0c16 100644 --- a/include/aoc/extra/ChronospatialComputerInstruction.hpp +++ b/include/aoc/extra/ChronospatialComputerInstruction.hpp @@ -30,7 +30,7 @@ class ChronospatialComputerInstruction protected: virtual void runValue(ChronospatialComputerState& state, const int operandValue) const = 0; private: - std::function&, const int)> operandFunctor_; + std::function&, const int)> operandFunctor_; }; #pragma region ChronospatialComputerDivisionInstruction diff --git a/include/aoc/extra/ChronospatialComputerState.hpp b/include/aoc/extra/ChronospatialComputerState.hpp index 5aef12c..eb5b1bf 100644 --- a/include/aoc/extra/ChronospatialComputerState.hpp +++ b/include/aoc/extra/ChronospatialComputerState.hpp @@ -22,6 +22,6 @@ class ChronospatialComputerState { public: size_t instructionPointer{ 0 }; - std::array registers{}; + std::array registers{}; std::ostringstream output{}; }; diff --git a/src/ChronospatialComputer.cpp b/src/ChronospatialComputer.cpp index 73b154f..d0e1fe3 100644 --- a/src/ChronospatialComputer.cpp +++ b/src/ChronospatialComputer.cpp @@ -73,6 +73,7 @@ void ChronospatialComputer::finish() { runProgram(program_, state_); part1 = state_.output.str(); + part2 = findQuineState(); } void ChronospatialComputer::runProgram(const std::vector& program, ChronospatialComputerState& state) const @@ -83,3 +84,80 @@ void ChronospatialComputer::runProgram(const std::vector& program, Chronosp instructions_[program[state.instructionPointer]]->run(state, program[state.instructionPointer + 1]); } } + +long long ChronospatialComputer::findQuineState() +{ + // The following masks and output patterns are specific to a single program and have been precalculated manually. + // The given Chronospatial Computer program essentially does the following calculations. + // + // A' = A >> 3 + // OUT = (((A mod 8) xor 4) xor (A shr ((A mod 8) xor 1))) mod 8, + // + // where A is the initial value of register A, A' is the value of register A when program flow reaches the jump + // instruction at the end of the program, and OUT is the single value written to the output when program flow reaches + // that same jump instruction. + // + // So given a desired output OUT, the number to be appended to the accumulated result can be found by matching the + // output patterns below against the masked trailing bits of the accumulated result, see the calculations below. + + // Array index corresponds to number to append. + std::array masks{ 0b1, 0b0, 0b111, 0b11, 0b11100, 0b1110, 0b1110000, 0b111000 }; + + // Array index is the output number. + // First pair element is the number appended to register A. + // Second pair element is the pattern to match against register A. + std::array>, 8> outputPatterns + { { + // output 0 + { { 0, 0b1 }, { 2, 0b110 }, { 4, 0b00000 }, { 5, 0b0010 }, { 6, 0b0100000 }, { 7, 0b0110000 } }, + // output 1 + { { 2, 0b111 }, { 3, 0b11 }, { 4, 0b00100 }, { 5, 0b0000 }, { 6, 0b0110000 }, { 7, 0b010000 } }, + // output 2 + { { 2, 0b100 }, { 4, 0b01000 }, { 5, 0b0110 }, { 6, 0b0000000 }, { 7, 0b001000 } }, + // output 3 + { { 2, 0b101 }, { 3, 0b10 }, { 4, 0b01100 }, { 5, 0b0100 }, { 6, 0b0010000 }, { 7, 0b000000 } }, + // output 4 + { { 0, 0b0 }, { 1, 0b0 }, { 2, 0b010 }, { 4, 0b10000 }, { 5, 0b1010 }, { 6, 0b1100000 }, { 7, 0b111000 } }, + // output 5 + { { 2, 0b011 }, { 3, 0b01 }, { 4, 0b10100 }, { 5, 0b1000 }, { 6, 0b1110000 }, { 7, 0b110000 } }, + // output 6 + { { 2, 0b000 }, { 4, 0b11000 }, { 5, 0b1110 }, { 6, 0b1000000 }, { 7, 0b101000 } }, + // output 7 + { { 2, 0b001 }, { 3, 0b00 }, { 4, 0b11100 }, { 5, 0b1100 }, { 6, 0b1010000 }, { 7, 0b100000 } } + } }; + + long long result{ 0 }; + size_t i{ program_.size() }; + int lastAppend{ -1 }; + + // Appends 3-digit binary numbers to the end of 'result' that will cause the program to output the next desired + // number (from 'program_'). The target output numbers are considered in reverse order. + while (i > 0) + { + auto patterns = outputPatterns[program_[i - 1]]; + auto it = std::find_if(patterns.begin(), patterns.end(), + [&lastAppend, &result, &masks](auto& x) { + return lastAppend < x.first && (result & masks[x.first]) == x.second; }); + if (it != patterns.end()) + { + // Appends the next number 'it->first' that will result in the desired output 'program_[i - 1]'. + lastAppend = -1; + result = (result << 3) + it->first; + i--; + } + else if (i < program_.size()) + { + // Backtrack because no 3-digit number can be appended to yield the desired output. + lastAppend = result & 0b111; + result >>= 3; + i++; + } + else + { + // This should not execute, it's a safeguard against an infinite loop in case of unexpected input. + return result; + } + } + + return result; +} diff --git a/src/extra/ChronospatialComputerInstruction.cpp b/src/extra/ChronospatialComputerInstruction.cpp index 1979d83..693bd63 100644 --- a/src/extra/ChronospatialComputerInstruction.cpp +++ b/src/extra/ChronospatialComputerInstruction.cpp @@ -22,10 +22,10 @@ ChronospatialComputerInstruction::ChronospatialComputerInstruction(const Chronos switch (type) { case ChronospatialComputerOperandType::Literal : - operandFunctor_ = [](const std::array& registers, const int operand) { return operand; }; + operandFunctor_ = [](const std::array& registers, const int operand) { return operand; }; break; case ChronospatialComputerOperandType::Combo : - operandFunctor_ = [](const std::array& registers, const int operand) + operandFunctor_ = [](const std::array& registers, const int operand) { return operand < 4 ? operand : registers[static_cast(operand - 4)]; }; break; } diff --git a/tests/include/aocTests/TestContext.hpp b/tests/include/aocTests/TestContext.hpp index 4ebf650..23f8aff 100644 --- a/tests/include/aocTests/TestContext.hpp +++ b/tests/include/aocTests/TestContext.hpp @@ -32,6 +32,8 @@ class TestContext const long long expected2, const std::vector& inputPaths); void runPart1(const std::unique_ptr>&& solver, const long long expected, const std::vector& inputPaths); + void runPart1(const std::unique_ptr>&& solver, const std::string expected, + const std::vector& inputPaths); void runPart2(const std::unique_ptr>&& solver, const long long expected, const std::vector& inputPaths); std::vector getInputPaths() const; diff --git a/tests/src/TestCases.cpp b/tests/src/TestCases.cpp index 6d49696..330d959 100644 --- a/tests/src/TestCases.cpp +++ b/tests/src/TestCases.cpp @@ -276,11 +276,11 @@ TEST_CASE("[ChronospatialComputerTests]") TestContext test; SECTION("FullData") { - test.run(std::make_unique(), "5,0,3,5,7,6,1,5,4", 0, test.getInputPaths()); + test.run(std::make_unique(), "5,0,3,5,7,6,1,5,4", 164516454365621, test.getInputPaths()); } SECTION("ExampleData") { - test.run(std::make_unique(), "4,6,3,5,6,3,5,2,1,0", 0, test.getExampleInputPaths()); + test.runPart1(std::make_unique(), "4,6,3,5,6,3,5,2,1,0", test.getExampleInputPaths()); } SECTION("ExampleInstruction1") { diff --git a/tests/src/TestContext.cpp b/tests/src/TestContext.cpp index 2170e71..22bbb76 100644 --- a/tests/src/TestContext.cpp +++ b/tests/src/TestContext.cpp @@ -58,6 +58,15 @@ void TestContext::runPart1(const std::unique_ptr>&& REQUIRE(expected == solver->getResultPart1()); } +void TestContext::runPart1(const std::unique_ptr>&& solver, const std::string expected, + const std::vector& inputPaths) +{ + SolverEngine solverEngine{ inputPaths }; + solverEngine.run(*solver); + + REQUIRE(expected == solver->getResultPart1()); +} + void TestContext::runPart2(const std::unique_ptr>&& solver, const long long expected, const std::vector& inputPaths) {