Add solution for "Day 20: Race Condition", part 2

This commit is contained in:
Stefan Müller 2025-06-04 23:29:27 +02:00
parent 43f1798343
commit e1eb8fbe2b
6 changed files with 80 additions and 37 deletions

View File

@ -148,6 +148,14 @@ This solver uses a flat tree data structure, implemented in [`LinenTowelPatterns
Since a valid placement of a towel in a design is independent of other towel placements, this puzzle can be solved by iteratively finding all possible start positions of towel patterns, and which towels can be placed starting at those positions. While calculating these, the number of possible arrangements can be tracked per start position, by adding up the possible arrangements from previous start positions that lead to this position. Since a valid placement of a towel in a design is independent of other towel placements, this puzzle can be solved by iteratively finding all possible start positions of towel patterns, and which towels can be placed starting at those positions. While calculating these, the number of possible arrangements can be tracked per start position, by adding up the possible arrangements from previous start positions that lead to this position.
### Day 20: Race Condition
:mag_right: Puzzle: <https://adventofcode.com/2024/day/20>, :white_check_mark: Solver: [`RaceCondition.cpp`](src/RaceCondition.cpp)
Initially for part 1, the solver was tracking the time index for each path position, and checking the eight possible, pre-defined offsets for each new position for a potential backwards cheat shortcut. However, this would not transfer well to part 2.
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.
## Thanks ## Thanks
* [Alexander Brouwer](https://github.com/Bromvlieg) for getting the project set up with CMake. * [Alexander Brouwer](https://github.com/Bromvlieg) for getting the project set up with CMake.

View File

@ -15,7 +15,8 @@
#pragma once #pragma once
#include <aoc/common/Grid.hpp> #include <vector>
#include <aoc/framework/LinesSolver.hpp> #include <aoc/framework/LinesSolver.hpp>
class RaceCondition class RaceCondition
@ -29,9 +30,9 @@ class RaceCondition
private: private:
static constexpr char getStartChar(); static constexpr char getStartChar();
static constexpr char getWallChar(); static constexpr char getWallChar();
static constexpr int getCheatLength(); static constexpr int getPart1CheatLength();
const std::array<Point2, 8> doubleSteps_{ Point2::down * 2, Point2::downRight, Point2::right * 2, static constexpr int getPart2CheatLength();
Point2::upRight, Point2::up * 2, Point2::upLeft, Point2::left * 2, Point2::downLeft };
int threshold_; int threshold_;
void checkCheat(const Point2& position, Grid<int>& times); bool tryFindNextPathPosition(std::vector<Point2>& path);
void checkCheat(const std::vector<Point2>& path);
}; };

View File

@ -36,6 +36,7 @@ class Point2
int x, y; int x, y;
Point2(); Point2();
Point2(const int x, const int y); Point2(const int x, const int y);
int calcManhattanDistance(const Point2& other) const;
bool operator==(const Point2& rhs) const; bool operator==(const Point2& rhs) const;
bool operator!=(const Point2& rhs) const; bool operator!=(const Point2& rhs) const;
bool operator<(const Point2& rhs) const; bool operator<(const Point2& rhs) const;

View File

@ -32,33 +32,19 @@ const int RaceCondition::getPuzzleDay() const
void RaceCondition::finish() void RaceCondition::finish()
{ {
int time{ 0 }; // Vector of positions that form the path. The index of an element is also the time at which the position is passed.
Grid<int> times{ lines.size(), lines[0].size() }; std::vector<Point2> path{};
// Fills the grid with a number that is guaranteed to be greater than the length of the path. path.reserve(static_cast<size_t>(threshold_) * 2);
times.fill(static_cast<int>(times.getNColumns() * times.getNRows()));
Point2 position{ findChar(getStartChar()) }; path.push_back(findChar(getStartChar()));
Point2 previous{ -1, -1 }; bool isMoving{ true };
while (position != previous) while (isMoving)
{ {
// Tracks time for current position.
times.cell(position) = time++;
// Checks if there is a cheat leading to the current position. // Checks if there is a cheat leading to the current position.
checkCheat(position, times); checkCheat(path);
// Progresses the race path. // Progresses the race path.
auto oldPosition = position; isMoving = tryFindNextPathPosition(path);
for (const auto& direction : Point2::cardinalDirections)
{
auto next = position + direction;
if (next != previous && getCharAt(next) != getWallChar())
{
position = next;
break;
}
}
previous = oldPosition;
} }
} }
@ -72,20 +58,60 @@ constexpr char RaceCondition::getWallChar()
return '#'; return '#';
} }
constexpr int RaceCondition::getCheatLength() constexpr int RaceCondition::getPart1CheatLength()
{ {
return 2; return 2;
} }
void RaceCondition::checkCheat(const Point2& position, Grid<int>& times) constexpr int RaceCondition::getPart2CheatLength()
{ {
auto time = times.cell(position); return 20;
for (auto& direction : doubleSteps_) }
bool RaceCondition::tryFindNextPathPosition(std::vector<Point2>& path)
{ {
auto other = position + direction; auto previous = path.size() <= 1 ? Point2{ -1, -1 } : path[path.size() - 2];
if (isInBounds(other) && time >= threshold_ + times.cell(other) + getCheatLength()) for (const auto& direction : Point2::cardinalDirections)
{
auto next = path.back() + direction;
if (next != previous && getCharAt(next) != getWallChar())
{
path.push_back(next);
return true;
}
}
return false;
}
void RaceCondition::checkCheat(const std::vector<Point2>& path)
{
// Checks previously encountered path positions that are at least the threshold away from the current position in
// reverse order for valid cheat opportunities.
int64_t i{ static_cast<int64_t>(path.size()) - threshold_ - 2 };
while (i >= 0)
{
int64_t distance{ path.back().calcManhattanDistance(path[i]) };
// Checks if the time saved by the cheat reaches at least the threshold, and if the cheat is not longer than
// permitted. The permitted cheat time is longer for part 2 than for part 1.
int64_t thresholdMinusTimeSaved{ threshold_ - static_cast<int64_t>(path.size()) + i + distance };
int64_t cheatLengthDiff{ distance - getPart2CheatLength() };
if (thresholdMinusTimeSaved < 0 && cheatLengthDiff <= 0)
{
part2++;
if (distance <= getPart1CheatLength())
{ {
part1++; part1++;
} }
i--;
}
else
{
// Backtracks the path as much as possible for the next potential position. This is possible because we know
// that the 'distance' between 'path.back()' and 'path[i]' cannot change by more than the change of 'i',
// since the positions in 'path' are contiguous. In other words, from this iteration to the next, both
// 'cheatLengthDiff' and 'thresholdMinusTimeSaved / 2' cannot change more than 'i'. We use this to skip the
// positions that cannot fulfill the condition above.
i -= std::max<int64_t>(std::max(thresholdMinusTimeSaved >> 1, cheatLengthDiff), 1);
}
} }
} }

View File

@ -15,6 +15,8 @@
#include <aoc/common/Point2.hpp> #include <aoc/common/Point2.hpp>
#include <math.h>
const Point2 Point2::left{ -1, 0 }; const Point2 Point2::left{ -1, 0 };
const Point2 Point2::right{ 1, 0 }; const Point2 Point2::right{ 1, 0 };
const Point2 Point2::up{ 0, -1 }; const Point2 Point2::up{ 0, -1 };
@ -44,7 +46,6 @@ Point2 Point2::getCardinalDirection(const char directionChar)
} }
} }
Point2::Point2() Point2::Point2()
: Point2{ 0, 0 } : Point2{ 0, 0 }
{ {
@ -55,6 +56,11 @@ Point2::Point2(const int x, const int y)
{ {
} }
int Point2::calcManhattanDistance(const Point2& other) const
{
return std::abs(x - other.x) + std::abs(y - other.y);
}
bool Point2::operator==(const Point2& rhs) const bool Point2::operator==(const Point2& rhs) const
{ {
return x == rhs.x && y == rhs.y; return x == rhs.x && y == rhs.y;

View File

@ -360,11 +360,12 @@ TEST_CASE("[RaceConditionTests]")
TestContext test; TestContext test;
SECTION("FullData") SECTION("FullData")
{ {
test.runFull(std::make_unique<RaceCondition>(), 1448, 0); test.runFull(std::make_unique<RaceCondition>(), 1448, 1017615);
} }
SECTION("ExampleData") SECTION("ExampleData")
{ {
test.runExample(std::make_unique<RaceCondition>(2), 44, 0); test.runExamplePart1(std::make_unique<RaceCondition>(2), 44);
test.runExamplePart2(std::make_unique<RaceCondition>(50), 285);
} }
} }