diff --git a/2024/answer_tests/day15/example1.txt b/2024/answer_tests/day15/example1.txt index c74a7ff..8690b77 100644 --- a/2024/answer_tests/day15/example1.txt +++ b/2024/answer_tests/day15/example1.txt @@ -1,2 +1,3 @@ Day 15: 2028 +1751 diff --git a/2024/answer_tests/day15/example2.txt b/2024/answer_tests/day15/example2.txt index 46d5172..eafe926 100644 --- a/2024/answer_tests/day15/example2.txt +++ b/2024/answer_tests/day15/example2.txt @@ -1,2 +1,3 @@ Day 15: 10092 +9021 diff --git a/2024/answer_tests/day15/example3.txt b/2024/answer_tests/day15/example3.txt new file mode 100644 index 0000000..d260777 --- /dev/null +++ b/2024/answer_tests/day15/example3.txt @@ -0,0 +1,3 @@ +Day 15: +905 +812 diff --git a/2024/input/day15/example3.txt b/2024/input/day15/example3.txt new file mode 100644 index 0000000..bbb8e44 --- /dev/null +++ b/2024/input/day15/example3.txt @@ -0,0 +1,9 @@ +###### +#....# +#.O..# +#O.O@# +#.O..# +#....# +###### + +<<^<>vv // for ifstream #include // for cout -int main(int argc, char **argv) { - std::ifstream infile = aoc::parse_args(argc, argv).infile; - - auto [warehouse, moves] = aoc::day15::read_input(infile); - +int simulate_robot(aoc::day15::Warehouse &warehouse, + const std::vector &moves) { if constexpr (aoc::DEBUG) { std::cerr << "Initial state:\n" << warehouse << "\n"; } @@ -24,8 +21,18 @@ int main(int argc, char **argv) { std::cerr << "Move " << dir << ":\n" << warehouse << "\n"; } } + return warehouse.gps_sum(); +} + +int main(int argc, char **argv) { + std::ifstream infile = aoc::parse_args(argc, argv).infile; + + auto [warehouse_1, moves] = aoc::day15::read_input(infile); + // make a wider copy for part 2 + auto warehouse_2 = warehouse_1.widen(); - std::cout << warehouse.gps_sum() << "\n"; + std::cout << simulate_robot(warehouse_1, moves) << "\n"; + std::cout << simulate_robot(warehouse_2, moves) << "\n"; return 0; } diff --git a/2024/src/day15.hpp b/2024/src/day15.hpp index 7bb368e..3cfe183 100644 --- a/2024/src/day15.hpp +++ b/2024/src/day15.hpp @@ -8,33 +8,41 @@ #ifndef DAY15_HPP_7IJC3NYE #define DAY15_HPP_7IJC3NYE -#include "ds/grid.hpp" // for Grid -#include "lib.hpp" // for Pos, Delta, AbsDirection, DEBUG, FAST -#include // for assert -#include // for istream -#include // for optional -#include // for string, getline -#include // for swap, move, pair -#include // for vector +#include "ds/grid.hpp" // for Grid +#include "unit_test/pretty_print.hpp" // for repr + +#include "lib.hpp" // for Pos, Delta, AbsDirection, DEBUG, FAST +#include // for assert +#include // for hash (unordered_set) +#include // for istream +#include // for optional +#include // for string, getline +#include // for unordered_set +#include // for swap, move, pair +#include // for vector namespace aoc::day15 { class Warehouse { aoc::ds::Grid grid; - Pos robot_pos; + Pos robot_pos{-1, -1}; + + Warehouse(int width, int height) : grid(width, height) {} - std::optional find_empty_tile(Pos pos, AbsDirection dir) const; + std::optional> try_move(const Pos &pos, AbsDirection dir, + bool wide_box = false) const; public: explicit Warehouse(const std::vector &lines); + Warehouse widen() const; + void move_robot(AbsDirection dir); int gps_sum() const; friend std::ostream &operator<<(std::ostream &, const Warehouse &); }; -Warehouse::Warehouse(const std::vector &lines) - : grid(lines), robot_pos(-1, -1) { +Warehouse::Warehouse(const std::vector &lines) : grid(lines) { grid.for_each([this](char tile, const Pos &pos) { if (tile == '@') { this->robot_pos = pos; @@ -42,68 +50,136 @@ Warehouse::Warehouse(const std::vector &lines) }); } +Warehouse Warehouse::widen() const { + Warehouse wider(grid.width * 2, grid.height); + grid.for_each([&wider](char tile, const Pos &pos) { + char &wider_left = wider.grid.at_unchecked(pos.x * 2, pos.y); + char &wider_right = wider.grid.at_unchecked(pos.x * 2 + 1, pos.y); + switch (tile) { + case '#': + wider_left = '#'; + wider_right = '#'; + break; + case 'O': + wider_left = '['; + wider_right = ']'; + break; + case '.': + wider_left = '.'; + wider_right = '.'; + break; + case '@': + wider_left = '@'; + wider_right = '.'; + wider.robot_pos = {pos.x * 2, pos.y}; + break; + } + }); + return wider; +} + /** - * Returns the position directly in front of the robot if it's unoccupied, the - * new position for a pushed box if there's one or more in the way, or an empty - * optional if the robot is blocked in that direction. + * Returns a collection of box positions that need to be updated, in order from + * furthest to nearest, or an empty optional if the robot is blocked in that + * direction */ -std::optional Warehouse::find_empty_tile(Pos pos, AbsDirection dir) const { +std::optional> +Warehouse::try_move(const Pos &pos, AbsDirection dir, bool wide_box) const { + using pretty_print::repr; Delta delta{dir, true}; - for (pos += delta; grid.in_bounds(pos); pos += delta) { - switch (grid[pos]) { - case '.': - // found an empty tile - return pos; - case '#': - // hit a wall - return {}; + Pos new_pos = pos + delta; + if (!grid.in_bounds(new_pos)) { + // move is blocked + return {}; + } + char tile = grid[new_pos]; + if (tile == '#') { + // move is blocked + return {}; + } + if (tile == '.') { + // move is valid + return {{}}; + } + if (tile == '[' || tile == ']') { + // treat a wide box as two connected small boxes when moving vertically, + // and recurse on each, setting the wide_box flag so we only do it once + if (delta.dy != 0 && !wide_box) { + Pos left_side, right_side; + if (tile == '[') { + left_side = pos; + right_side = pos + Delta{1, 0}; + } else { + left_side = pos + Delta{-1, 0}; + right_side = pos; + } + auto positions_left = try_move(left_side, dir, true); + auto positions_right = try_move(right_side, dir, true); + if (positions_left && positions_right) { + // append all values from positions_right into positions_left + positions_left->insert(positions_left->end(), + positions_right->begin(), + positions_right->end()); + return positions_left; + } else { + // movement was blocked somewhere + return {}; + } + } else { + // this will handle horizontal movements as two unconnected boxes + tile = 'O'; + } + } + if (tile == 'O') { + auto positions = try_move(new_pos, dir); + if (positions.has_value()) { + // move is valid, add this position to the update list + positions->push_back(new_pos); } + return positions; } - // hit the edge of the map - return {}; + std::cerr << "unhandled tile type " << pretty_print::repr(tile) << " at " + << new_pos << "\n"; + assert(false); } void Warehouse::move_robot(AbsDirection dir) { - if (auto empty_pos = find_empty_tile(robot_pos, dir)) { - // new position for the robot - Pos new_pos = robot_pos + Delta(dir, true); + if (auto positions_to_move = try_move(robot_pos, dir); + positions_to_move.has_value()) { + Delta delta(dir, true); if constexpr (aoc::DEBUG) { std::cerr << "moving robot " << dir << " from " << robot_pos - << " to " << new_pos; + << " to " << robot_pos + delta << "; box positions: " + << pretty_print::repr(*positions_to_move) << "\n"; } - if (grid[new_pos] == 'O') { - // need to move the box to the next empty position - if constexpr (!aoc::FAST) { - assert(grid[*empty_pos] == '.'); + // add the robot to the update list, after any boxes + positions_to_move->push_back(robot_pos); + // execute all movements, skipping duplicate positions + std::unordered_set already_moved; + for (const Pos &pos : *positions_to_move) { + Pos new_pos = pos + delta; + bool inserted = already_moved.emplace(new_pos).second; + if (inserted) { + if constexpr (!aoc::FAST) { + assert(grid[new_pos] == '.'); + } + std::swap(grid[pos], grid[new_pos]); } - if constexpr (aoc::DEBUG) { - int box_count = (*empty_pos - new_pos).manhattan_distance(); - std::cerr << " and pushing " << box_count << " " - << (box_count > 1 ? "boxes" : "box") << " to " - << *empty_pos; - } - std::swap(grid[*empty_pos], grid[new_pos]); - } - // nothing in the way, just move the robot - if constexpr (!aoc::FAST) { - assert(grid[new_pos] == '.'); } - std::swap(grid[robot_pos], grid[new_pos]); - robot_pos = new_pos; + // update the robot position + robot_pos += delta; } else { if constexpr (aoc::DEBUG) { - std::cerr << "blocked from moving " << dir << " at " << robot_pos; + std::cerr << "blocked from moving " << dir << " at " << robot_pos + << "\n"; } } - if constexpr (aoc::DEBUG) { - std::cerr << "\n"; - } } int Warehouse::gps_sum() const { int sum = 0; grid.for_each([&sum](char tile, const Pos &pos) { - if (tile == 'O') { + if (tile == 'O' || tile == '[') { sum += 100 * pos.y + pos.x; } });