diff --git a/README.md b/README.md index 63b10e5..fce89e1 100644 --- a/README.md +++ b/README.md @@ -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. +### Day 20: Race Condition + +:mag_right: Puzzle: , :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 * [Alexander Brouwer](https://github.com/Bromvlieg) for getting the project set up with CMake. diff --git a/include/aoc/RaceCondition.hpp b/include/aoc/RaceCondition.hpp index cca4cd2..77e4445 100644 --- a/include/aoc/RaceCondition.hpp +++ b/include/aoc/RaceCondition.hpp @@ -15,7 +15,8 @@ #pragma once -#include +#include + #include class RaceCondition @@ -29,9 +30,9 @@ class RaceCondition private: static constexpr char getStartChar(); static constexpr char getWallChar(); - static constexpr int getCheatLength(); - const std::array doubleSteps_{ Point2::down * 2, Point2::downRight, Point2::right * 2, - Point2::upRight, Point2::up * 2, Point2::upLeft, Point2::left * 2, Point2::downLeft }; + static constexpr int getPart1CheatLength(); + static constexpr int getPart2CheatLength(); int threshold_; - void checkCheat(const Point2& position, Grid& times); + bool tryFindNextPathPosition(std::vector& path); + void checkCheat(const std::vector& path); }; diff --git a/include/aoc/common/Point2.hpp b/include/aoc/common/Point2.hpp index d5ad5bb..b364bce 100644 --- a/include/aoc/common/Point2.hpp +++ b/include/aoc/common/Point2.hpp @@ -36,6 +36,7 @@ class Point2 int x, y; Point2(); 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; diff --git a/src/RaceCondition.cpp b/src/RaceCondition.cpp index dcb26cd..dab9e5c 100644 --- a/src/RaceCondition.cpp +++ b/src/RaceCondition.cpp @@ -32,33 +32,19 @@ const int RaceCondition::getPuzzleDay() const void RaceCondition::finish() { - int time{ 0 }; - Grid times{ lines.size(), lines[0].size() }; - // Fills the grid with a number that is guaranteed to be greater than the length of the path. - times.fill(static_cast(times.getNColumns() * times.getNRows())); + // Vector of positions that form the path. The index of an element is also the time at which the position is passed. + std::vector path{}; + path.reserve(static_cast(threshold_) * 2); - Point2 position{ findChar(getStartChar()) }; - Point2 previous{ -1, -1 }; - while (position != previous) + path.push_back(findChar(getStartChar())); + bool isMoving{ true }; + while (isMoving) { - // Tracks time for current position. - times.cell(position) = time++; - // Checks if there is a cheat leading to the current position. - checkCheat(position, times); + checkCheat(path); // Progresses the race path. - auto oldPosition = position; - for (const auto& direction : Point2::cardinalDirections) - { - auto next = position + direction; - if (next != previous && getCharAt(next) != getWallChar()) - { - position = next; - break; - } - } - previous = oldPosition; + isMoving = tryFindNextPathPosition(path); } } @@ -72,20 +58,60 @@ constexpr char RaceCondition::getWallChar() return '#'; } -constexpr int RaceCondition::getCheatLength() +constexpr int RaceCondition::getPart1CheatLength() { return 2; } -void RaceCondition::checkCheat(const Point2& position, Grid& times) +constexpr int RaceCondition::getPart2CheatLength() { - auto time = times.cell(position); - for (auto& direction : doubleSteps_) + return 20; +} + +bool RaceCondition::tryFindNextPathPosition(std::vector& path) +{ + auto previous = path.size() <= 1 ? Point2{ -1, -1 } : path[path.size() - 2]; + for (const auto& direction : Point2::cardinalDirections) { - auto other = position + direction; - if (isInBounds(other) && time >= threshold_ + times.cell(other) + getCheatLength()) + auto next = path.back() + direction; + if (next != previous && getCharAt(next) != getWallChar()) { - part1++; + path.push_back(next); + return true; + } + } + return false; +} + +void RaceCondition::checkCheat(const std::vector& 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(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(path.size()) + i + distance }; + int64_t cheatLengthDiff{ distance - getPart2CheatLength() }; + if (thresholdMinusTimeSaved < 0 && cheatLengthDiff <= 0) + { + part2++; + if (distance <= getPart1CheatLength()) + { + 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(std::max(thresholdMinusTimeSaved >> 1, cheatLengthDiff), 1); } } } diff --git a/src/common/Point2.cpp b/src/common/Point2.cpp index 40f2330..e01d31b 100644 --- a/src/common/Point2.cpp +++ b/src/common/Point2.cpp @@ -15,6 +15,8 @@ #include +#include + const Point2 Point2::left{ -1, 0 }; const Point2 Point2::right{ 1, 0 }; const Point2 Point2::up{ 0, -1 }; @@ -44,7 +46,6 @@ Point2 Point2::getCardinalDirection(const char directionChar) } } - Point2::Point2() : 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 { return x == rhs.x && y == rhs.y; diff --git a/tests/src/TestCases.cpp b/tests/src/TestCases.cpp index 322d5ed..a83b57d 100644 --- a/tests/src/TestCases.cpp +++ b/tests/src/TestCases.cpp @@ -360,11 +360,12 @@ TEST_CASE("[RaceConditionTests]") TestContext test; SECTION("FullData") { - test.runFull(std::make_unique(), 1448, 0); + test.runFull(std::make_unique(), 1448, 1017615); } SECTION("ExampleData") { - test.runExample(std::make_unique(2), 44, 0); + test.runExamplePart1(std::make_unique(2), 44); + test.runExamplePart2(std::make_unique(50), 285); } }