From 83c66682efe18b9a376cccabc33b5cd38f34f06f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Mon, 9 Jun 2025 18:28:16 +0200 Subject: [PATCH] Add solution for "Day 21: Keypad Conundrum", part 2 --- README.md | 10 +++ include/aoc/KeypadConundrum.hpp | 9 ++ .../aoc/extra/KeypadPatternTransformation.hpp | 28 ++++++ include/aoc/extra/KeypadRobot.hpp | 7 +- src/KeypadConundrum.cpp | 85 ++++++++++++++++--- src/extra/KeypadRobot.cpp | 44 ++++++---- tests/src/TestCases.cpp | 4 +- 7 files changed, 156 insertions(+), 31 deletions(-) create mode 100644 include/aoc/extra/KeypadPatternTransformation.hpp diff --git a/README.md b/README.md index fce89e1..c81cf60 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,16 @@ Initially for part 1, the solver was tracking the time index for each path posit So instead of tracking the path times, the algorithm checks, for each new path position, all previous positions whether they are in range for a cheat, and whether this cheat would cut enough time off to be valid. The number of positions to check can be reduced by only looking at positions that are far enough behind the current position, and by skipping several positions at once if the check for a cheat failed. +### Day 21: Keypad Conundrum + +:mag_right: Puzzle: , :white_check_mark: Solver: [`KeypadConundrum.cpp`](src/KeypadConundrum.cpp) + +It's trivial to see that it requires less instruction for the previous robot in the chain if a robot avoids switching directions as much as possible when the arm has to move more than once in one direction. For example if we want to move the arm from the `A` button to the `<` button, letting the previous robot push `v<A^>A`, it takes its parts `vA`, and `^>A`, and keeps iterating with those. That means the solver can iteratively calculate the length of each of these short patterns per robot to find the length of the original one at the end of the chain. + ## Thanks * [Alexander Brouwer](https://github.com/Bromvlieg) for getting the project set up with CMake. diff --git a/include/aoc/KeypadConundrum.hpp b/include/aoc/KeypadConundrum.hpp index 093495b..7ebd6ac 100644 --- a/include/aoc/KeypadConundrum.hpp +++ b/include/aoc/KeypadConundrum.hpp @@ -15,6 +15,9 @@ #pragma once +#include + +#include #include #include @@ -29,6 +32,12 @@ class KeypadConundrum virtual void finish() override; private: static constexpr char getStartPositionChar(); + static constexpr size_t getPart1NRobots(); + static constexpr size_t getPart2NRobots(); KeypadRobot numericKeyboardRobot_; KeypadRobot directionalKeyboardRobot_; + std::unordered_map transformations_; + void updateTransformationMap(KeypadPatternTransformation& targetTransformation); + void updateTransformationMapLengths(const std::vector& added); + int64_t calcAccumulatedLength(KeypadPatternTransformation& transformation, const size_t index); }; diff --git a/include/aoc/extra/KeypadPatternTransformation.hpp b/include/aoc/extra/KeypadPatternTransformation.hpp new file mode 100644 index 0000000..dee2f1a --- /dev/null +++ b/include/aoc/extra/KeypadPatternTransformation.hpp @@ -0,0 +1,28 @@ +// Solutions to the Advent Of Code 2024. +// Copyright (C) 2025 Stefan Müller +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . + +#pragma once + +#include +#include + +class KeypadPatternTransformation +{ +public: + int nDuplicates{ 0 }; + std::vector parts{}; + std::vector partPtrs{}; + std::vector accumulatedLengths{}; +}; diff --git a/include/aoc/extra/KeypadRobot.hpp b/include/aoc/extra/KeypadRobot.hpp index f7ad1e0..ddc4ced 100644 --- a/include/aoc/extra/KeypadRobot.hpp +++ b/include/aoc/extra/KeypadRobot.hpp @@ -18,15 +18,18 @@ #include #include +#include #include class KeypadRobot { public: KeypadRobot(const std::map&& keypad, const Point2&& forbidden); - std::string calcInputKeys(const std::string& targetOutputKeys) const; + KeypadPatternTransformation calcTransformation(const std::string& targetOutputKeys) const; private: const std::map keypad_; const Point2 forbidden_; - void move(std::ostringstream& stream, const int delta, const char positive, const char negative) const; + void move(std::ostringstream& stream, const int delta, const char positive, const char negative, + int& nDuplicates) const; + bool isForbidden(const int x, const int y) const; }; diff --git a/src/KeypadConundrum.cpp b/src/KeypadConundrum.cpp index a53d583..e18da3a 100644 --- a/src/KeypadConundrum.cpp +++ b/src/KeypadConundrum.cpp @@ -15,17 +15,16 @@ #include -#include #include - -#include +#include KeypadConundrum::KeypadConundrum() : numericKeyboardRobot_{ { { 'A', { 0, 0 } }, { '0', { -1, 0 } }, { '1', { -2, -1 } }, { '2', { -1, -1 } }, { '3', { 0, -1 } }, { '4', { -2, -2 } }, { '5', { -1, -2 } }, { '6', { 0, -2 } }, { '7', { -2, -3 } }, { '8', { -1, -3 } }, { '9', { 0, -3 } } }, { -2, 0 } }, directionalKeyboardRobot_{ { { 'A', { 0, 0 } }, { '<', { -2, 1 } }, { '>', { 0, 1 } }, { '^', { -1, 0 } }, - { 'v', { -1, 1 } } }, { -2, 0 } } + { 'v', { -1, 1 } } }, { -2, 0 } }, + transformations_{} { } @@ -41,15 +40,15 @@ const int KeypadConundrum::getPuzzleDay() const void KeypadConundrum::processDataLine(const std::string& line) { + KeypadPatternTransformation target{ numericKeyboardRobot_.calcTransformation(line) }; + updateTransformationMap(target); + std::istringstream stream{ line }; int64_t number; stream >> number; - std::string inputKeys{ numericKeyboardRobot_.calcInputKeys(line) }; - for (size_t i = 0; i < 2; i++) - { - inputKeys = directionalKeyboardRobot_.calcInputKeys(inputKeys); - } - part1 += number * static_cast(inputKeys.size()); + + part1 += number * calcAccumulatedLength(target, getPart1NRobots()); + part2 += number * calcAccumulatedLength(target, getPart2NRobots()); } void KeypadConundrum::finish() @@ -60,3 +59,69 @@ constexpr char KeypadConundrum::getStartPositionChar() { return 'A'; } + +constexpr size_t KeypadConundrum::getPart1NRobots() +{ + return 2; +} + +constexpr size_t KeypadConundrum::getPart2NRobots() +{ + return 25; +} + +void KeypadConundrum::updateTransformationMap(KeypadPatternTransformation& targetTransformation) +{ + std::stack stack{}; + stack.push(&targetTransformation); + + std::vector added{}; + + while (!stack.empty()) + { + auto transformation = stack.top(); + stack.pop(); + + for (const auto& part : transformation->parts) + { + auto it = transformations_.find(part); + if (it == transformations_.end()) + { + const auto emplaceResult = + transformations_.emplace(part, directionalKeyboardRobot_.calcTransformation(part)); + it = emplaceResult.first; + stack.push(&it->second); + added.push_back(&it->second); + } + transformation->partPtrs.push_back(&it->second); + } + } + + updateTransformationMapLengths(added); +} + +void KeypadConundrum::updateTransformationMapLengths(const std::vector& added) +{ + for (auto& transformation : added) + { + transformation->accumulatedLengths.reserve(getPart2NRobots()); + } + + for (size_t i = 0; i < getPart2NRobots(); i++) + { + for (auto& transformation : added) + { + transformation->accumulatedLengths.push_back(calcAccumulatedLength(*transformation, i)); + } + } +} + +int64_t KeypadConundrum::calcAccumulatedLength(KeypadPatternTransformation& transformation, const size_t index) +{ + int64_t n{ transformation.nDuplicates }; + for (const auto& part : transformation.partPtrs) + { + n += part->accumulatedLengths[index]; + } + return n; +} diff --git a/src/extra/KeypadRobot.cpp b/src/extra/KeypadRobot.cpp index 49a5c12..60aefa9 100644 --- a/src/extra/KeypadRobot.cpp +++ b/src/extra/KeypadRobot.cpp @@ -22,8 +22,11 @@ KeypadRobot::KeypadRobot(const std::map&& keypad, const Point2&& f { } -std::string KeypadRobot::calcInputKeys(const std::string& targetOutputKeys) const +KeypadPatternTransformation KeypadRobot::calcTransformation(const std::string& targetOutputKeys) const { + KeypadPatternTransformation result{}; + result.accumulatedLengths.push_back(targetOutputKeys.size()); + std::ostringstream stream{}; Point2 position{ 0, 0 }; for (const char c : targetOutputKeys) @@ -32,37 +35,44 @@ std::string KeypadRobot::calcInputKeys(const std::string& targetOutputKeys) cons // This specific order of robot arm movements aims to reduce resulting combinations of 'A' and '<' for the // second robot, which expand to more key presses starting with the third robot. - bool horizontalFirst{ (next.x < position.x && !(next.x == forbidden_.x && position.y == forbidden_.y)) || - (position.x == forbidden_.x && next.y == forbidden_.y) }; + bool horizontalFirst{ (next.x < position.x && !isForbidden(next.x, position.y)) || + isForbidden(position.x, next.y) }; if (horizontalFirst) { - move(stream, next.x - position.x, '>', '<'); + move(stream, next.x - position.x, '>', '<', result.nDuplicates); } - move(stream, next.y - position.y, 'v', '^'); + move(stream, next.y - position.y, 'v', '^', result.nDuplicates); if (!horizontalFirst) { - move(stream, next.x - position.x, '>', '<'); + move(stream, next.x - position.x, '>', '<', result.nDuplicates); } stream << 'A'; + + result.parts.push_back(stream.str()); + stream.str(""); + position = next; } - return stream.str(); + + return result; } -void KeypadRobot::move(std::ostringstream& stream, const int delta, const char positive, const char negative) const +void KeypadRobot::move(std::ostringstream& stream, const int delta, const char positive, const char negative, + int& nDuplicates) const { if (delta > 0) { - for (int i{ 0 }; i < delta; i++) - { - stream << positive; - } + stream << positive; + nDuplicates += delta - 1; } - else + else if (delta < 0) { - for (int i{ 0 }; i < -delta; i++) - { - stream << negative; - } + stream << negative; + nDuplicates -= delta + 1; } } + +bool KeypadRobot::isForbidden(const int x, const int y) const +{ + return x == forbidden_.x && y == forbidden_.y; +} diff --git a/tests/src/TestCases.cpp b/tests/src/TestCases.cpp index 5671c3c..07754fa 100644 --- a/tests/src/TestCases.cpp +++ b/tests/src/TestCases.cpp @@ -375,11 +375,11 @@ TEST_CASE("[KeypadConundrumTests]") TestContext test; SECTION("FullData") { - test.runFull(std::make_unique(), 136780, 0); + test.runFull(std::make_unique(), 136780, 167538833832712); } SECTION("ExampleData") { - test.runExample(std::make_unique(), 126384, 0); + test.runExamplePart1(std::make_unique(), 126384); } }