Skip to content

Commit 6df9899

Browse files
committed
2024 day 15: solve part 2 (2024 complete!)
1 parent 473e329 commit 6df9899

File tree

6 files changed

+155
-58
lines changed

6 files changed

+155
-58
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
Day 15:
22
2028
3+
1751
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
Day 15:
22
10092
3+
9021
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Day 15:
2+
905
3+
812

2024/input/day15/example3.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
######
2+
#....#
3+
#.O..#
4+
#O.O@#
5+
#.O..#
6+
#....#
7+
######
8+
9+
<<^<>vv<v<^^

2024/src/day15.cpp

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,8 @@
1010
#include <fstream> // for ifstream
1111
#include <iostream> // for cout
1212

13-
int main(int argc, char **argv) {
14-
std::ifstream infile = aoc::parse_args(argc, argv).infile;
15-
16-
auto [warehouse, moves] = aoc::day15::read_input(infile);
17-
13+
int simulate_robot(aoc::day15::Warehouse &warehouse,
14+
const std::vector<aoc::AbsDirection> &moves) {
1815
if constexpr (aoc::DEBUG) {
1916
std::cerr << "Initial state:\n" << warehouse << "\n";
2017
}
@@ -24,8 +21,18 @@ int main(int argc, char **argv) {
2421
std::cerr << "Move " << dir << ":\n" << warehouse << "\n";
2522
}
2623
}
24+
return warehouse.gps_sum();
25+
}
26+
27+
int main(int argc, char **argv) {
28+
std::ifstream infile = aoc::parse_args(argc, argv).infile;
29+
30+
auto [warehouse_1, moves] = aoc::day15::read_input(infile);
31+
// make a wider copy for part 2
32+
auto warehouse_2 = warehouse_1.widen();
2733

28-
std::cout << warehouse.gps_sum() << "\n";
34+
std::cout << simulate_robot(warehouse_1, moves) << "\n";
35+
std::cout << simulate_robot(warehouse_2, moves) << "\n";
2936

3037
return 0;
3138
}

2024/src/day15.hpp

Lines changed: 128 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,102 +8,178 @@
88
#ifndef DAY15_HPP_7IJC3NYE
99
#define DAY15_HPP_7IJC3NYE
1010

11-
#include "ds/grid.hpp" // for Grid
12-
#include "lib.hpp" // for Pos, Delta, AbsDirection, DEBUG, FAST
13-
#include <cassert> // for assert
14-
#include <iostream> // for istream
15-
#include <optional> // for optional
16-
#include <string> // for string, getline
17-
#include <utility> // for swap, move, pair
18-
#include <vector> // for vector
11+
#include "ds/grid.hpp" // for Grid
12+
#include "unit_test/pretty_print.hpp" // for repr
13+
14+
#include "lib.hpp" // for Pos, Delta, AbsDirection, DEBUG, FAST
15+
#include <cassert> // for assert
16+
#include <functional> // for hash (unordered_set)
17+
#include <iostream> // for istream
18+
#include <optional> // for optional
19+
#include <string> // for string, getline
20+
#include <unordered_set> // for unordered_set
21+
#include <utility> // for swap, move, pair
22+
#include <vector> // for vector
1923

2024
namespace aoc::day15 {
2125

2226
class Warehouse {
2327
aoc::ds::Grid<char> grid;
24-
Pos robot_pos;
28+
Pos robot_pos{-1, -1};
29+
30+
Warehouse(int width, int height) : grid(width, height) {}
2531

26-
std::optional<Pos> find_empty_tile(Pos pos, AbsDirection dir) const;
32+
std::optional<std::vector<Pos>> try_move(const Pos &pos, AbsDirection dir,
33+
bool wide_box = false) const;
2734

2835
public:
2936
explicit Warehouse(const std::vector<std::string> &lines);
37+
Warehouse widen() const;
38+
3039
void move_robot(AbsDirection dir);
3140
int gps_sum() const;
3241

3342
friend std::ostream &operator<<(std::ostream &, const Warehouse &);
3443
};
3544

36-
Warehouse::Warehouse(const std::vector<std::string> &lines)
37-
: grid(lines), robot_pos(-1, -1) {
45+
Warehouse::Warehouse(const std::vector<std::string> &lines) : grid(lines) {
3846
grid.for_each([this](char tile, const Pos &pos) {
3947
if (tile == '@') {
4048
this->robot_pos = pos;
4149
}
4250
});
4351
}
4452

53+
Warehouse Warehouse::widen() const {
54+
Warehouse wider(grid.width * 2, grid.height);
55+
grid.for_each([&wider](char tile, const Pos &pos) {
56+
char &wider_left = wider.grid.at_unchecked(pos.x * 2, pos.y);
57+
char &wider_right = wider.grid.at_unchecked(pos.x * 2 + 1, pos.y);
58+
switch (tile) {
59+
case '#':
60+
wider_left = '#';
61+
wider_right = '#';
62+
break;
63+
case 'O':
64+
wider_left = '[';
65+
wider_right = ']';
66+
break;
67+
case '.':
68+
wider_left = '.';
69+
wider_right = '.';
70+
break;
71+
case '@':
72+
wider_left = '@';
73+
wider_right = '.';
74+
wider.robot_pos = {pos.x * 2, pos.y};
75+
break;
76+
}
77+
});
78+
return wider;
79+
}
80+
4581
/**
46-
* Returns the position directly in front of the robot if it's unoccupied, the
47-
* new position for a pushed box if there's one or more in the way, or an empty
48-
* optional if the robot is blocked in that direction.
82+
* Returns a collection of box positions that need to be updated, in order from
83+
* furthest to nearest, or an empty optional if the robot is blocked in that
84+
* direction
4985
*/
50-
std::optional<Pos> Warehouse::find_empty_tile(Pos pos, AbsDirection dir) const {
86+
std::optional<std::vector<Pos>>
87+
Warehouse::try_move(const Pos &pos, AbsDirection dir, bool wide_box) const {
88+
using pretty_print::repr;
5189
Delta delta{dir, true};
52-
for (pos += delta; grid.in_bounds(pos); pos += delta) {
53-
switch (grid[pos]) {
54-
case '.':
55-
// found an empty tile
56-
return pos;
57-
case '#':
58-
// hit a wall
59-
return {};
90+
Pos new_pos = pos + delta;
91+
if (!grid.in_bounds(new_pos)) {
92+
// move is blocked
93+
return {};
94+
}
95+
char tile = grid[new_pos];
96+
if (tile == '#') {
97+
// move is blocked
98+
return {};
99+
}
100+
if (tile == '.') {
101+
// move is valid
102+
return {{}};
103+
}
104+
if (tile == '[' || tile == ']') {
105+
// treat a wide box as two connected small boxes when moving vertically,
106+
// and recurse on each, setting the wide_box flag so we only do it once
107+
if (delta.dy != 0 && !wide_box) {
108+
Pos left_side, right_side;
109+
if (tile == '[') {
110+
left_side = pos;
111+
right_side = pos + Delta{1, 0};
112+
} else {
113+
left_side = pos + Delta{-1, 0};
114+
right_side = pos;
115+
}
116+
auto positions_left = try_move(left_side, dir, true);
117+
auto positions_right = try_move(right_side, dir, true);
118+
if (positions_left && positions_right) {
119+
// append all values from positions_right into positions_left
120+
positions_left->insert(positions_left->end(),
121+
positions_right->begin(),
122+
positions_right->end());
123+
return positions_left;
124+
} else {
125+
// movement was blocked somewhere
126+
return {};
127+
}
128+
} else {
129+
// this will handle horizontal movements as two unconnected boxes
130+
tile = 'O';
131+
}
132+
}
133+
if (tile == 'O') {
134+
auto positions = try_move(new_pos, dir);
135+
if (positions.has_value()) {
136+
// move is valid, add this position to the update list
137+
positions->push_back(new_pos);
60138
}
139+
return positions;
61140
}
62-
// hit the edge of the map
63-
return {};
141+
std::cerr << "unhandled tile type " << pretty_print::repr(tile) << " at "
142+
<< new_pos << "\n";
143+
assert(false);
64144
}
65145

66146
void Warehouse::move_robot(AbsDirection dir) {
67-
if (auto empty_pos = find_empty_tile(robot_pos, dir)) {
68-
// new position for the robot
69-
Pos new_pos = robot_pos + Delta(dir, true);
147+
if (auto positions_to_move = try_move(robot_pos, dir);
148+
positions_to_move.has_value()) {
149+
Delta delta(dir, true);
70150
if constexpr (aoc::DEBUG) {
71151
std::cerr << "moving robot " << dir << " from " << robot_pos
72-
<< " to " << new_pos;
152+
<< " to " << robot_pos + delta << "; box positions: "
153+
<< pretty_print::repr(*positions_to_move) << "\n";
73154
}
74-
if (grid[new_pos] == 'O') {
75-
// need to move the box to the next empty position
76-
if constexpr (!aoc::FAST) {
77-
assert(grid[*empty_pos] == '.');
155+
// add the robot to the update list, after any boxes
156+
positions_to_move->push_back(robot_pos);
157+
// execute all movements, skipping duplicate positions
158+
std::unordered_set<Pos> already_moved;
159+
for (const Pos &pos : *positions_to_move) {
160+
Pos new_pos = pos + delta;
161+
bool inserted = already_moved.emplace(new_pos).second;
162+
if (inserted) {
163+
if constexpr (!aoc::FAST) {
164+
assert(grid[new_pos] == '.');
165+
}
166+
std::swap(grid[pos], grid[new_pos]);
78167
}
79-
if constexpr (aoc::DEBUG) {
80-
int box_count = (*empty_pos - new_pos).manhattan_distance();
81-
std::cerr << " and pushing " << box_count << " "
82-
<< (box_count > 1 ? "boxes" : "box") << " to "
83-
<< *empty_pos;
84-
}
85-
std::swap(grid[*empty_pos], grid[new_pos]);
86-
}
87-
// nothing in the way, just move the robot
88-
if constexpr (!aoc::FAST) {
89-
assert(grid[new_pos] == '.');
90168
}
91-
std::swap(grid[robot_pos], grid[new_pos]);
92-
robot_pos = new_pos;
169+
// update the robot position
170+
robot_pos += delta;
93171
} else {
94172
if constexpr (aoc::DEBUG) {
95-
std::cerr << "blocked from moving " << dir << " at " << robot_pos;
173+
std::cerr << "blocked from moving " << dir << " at " << robot_pos
174+
<< "\n";
96175
}
97176
}
98-
if constexpr (aoc::DEBUG) {
99-
std::cerr << "\n";
100-
}
101177
}
102178

103179
int Warehouse::gps_sum() const {
104180
int sum = 0;
105181
grid.for_each([&sum](char tile, const Pos &pos) {
106-
if (tile == 'O') {
182+
if (tile == 'O' || tile == '[') {
107183
sum += 100 * pos.y + pos.x;
108184
}
109185
});

0 commit comments

Comments
 (0)