Add solution for "Day 21: Keypad Conundrum", part 2

This commit is contained in:
Stefan Müller 2025-06-09 18:28:16 +02:00
parent ab26563e34
commit 83c66682ef
7 changed files with 156 additions and 31 deletions

View File

@ -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: <https://adventofcode.com/2024/day/21>, :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` is of course better than `<v<A`.
However, it was not immediately obvious to me, that the order of directions in a diagonal movement matters, too. For example `<vA` is better than `v<A`, which becomes only apparent when looking at the input of the robot before the previous robot in the chain. The reason for this is the need to minimize presses of `A` and `<` buttons immediately following each other, since they are the furthest apart. And of course, we have to restrict certain movements, for example `<vA`, if they would move over the gap on the keypad.
Since the length of the target button pattern roughly doubles with each robot along the chain, the algorithm cannot calculate the list of instructions for each robot explicitly. Instead, it considers only the twelve patterns without duplicate buttons that start at the `A` button and end there again. For example, instead of `v<<A>A^>A`, it takes its parts `v<A`, `>A`, 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.

View File

@ -15,6 +15,9 @@
#pragma once
#include <unordered_map>
#include <aoc/extra/KeypadPatternTransformation.hpp>
#include <aoc/extra/KeypadRobot.hpp>
#include <aoc/framework/Solver-types.hpp>
@ -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<std::string, KeypadPatternTransformation> transformations_;
void updateTransformationMap(KeypadPatternTransformation& targetTransformation);
void updateTransformationMapLengths(const std::vector<KeypadPatternTransformation*>& added);
int64_t calcAccumulatedLength(KeypadPatternTransformation& transformation, const size_t index);
};

View File

@ -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 <http://www.gnu.org/licenses/>.
#pragma once
#include <string>
#include <vector>
class KeypadPatternTransformation
{
public:
int nDuplicates{ 0 };
std::vector<std::string> parts{};
std::vector<KeypadPatternTransformation*> partPtrs{};
std::vector<int64_t> accumulatedLengths{};
};

View File

@ -18,15 +18,18 @@
#include <map>
#include <string>
#include <aoc/extra/KeypadPatternTransformation.hpp>
#include <aoc/common/Point2.hpp>
class KeypadRobot
{
public:
KeypadRobot(const std::map<char, Point2>&& keypad, const Point2&& forbidden);
std::string calcInputKeys(const std::string& targetOutputKeys) const;
KeypadPatternTransformation calcTransformation(const std::string& targetOutputKeys) const;
private:
const std::map<char, Point2> 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;
};

View File

@ -15,17 +15,16 @@
#include <aoc/KeypadConundrum.hpp>
#include <map>
#include <sstream>
#include <aoc/common/Point2.hpp>
#include <stack>
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<int64_t>(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<KeypadPatternTransformation*> stack{};
stack.push(&targetTransformation);
std::vector<KeypadPatternTransformation*> 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<KeypadPatternTransformation*>& 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;
}

View File

@ -22,8 +22,11 @@ KeypadRobot::KeypadRobot(const std::map<char, Point2>&& 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;
}

View File

@ -375,11 +375,11 @@ TEST_CASE("[KeypadConundrumTests]")
TestContext test;
SECTION("FullData")
{
test.runFull(std::make_unique<KeypadConundrum>(), 136780, 0);
test.runFull(std::make_unique<KeypadConundrum>(), 136780, 167538833832712);
}
SECTION("ExampleData")
{
test.runExample(std::make_unique<KeypadConundrum>(), 126384, 0);
test.runExamplePart1(std::make_unique<KeypadConundrum>(), 126384);
}
}