Skip to content

Commit 98b91c5

Browse files
committed
Day 10, part 2: Theory was pretty good, and writing Rust is getting easier.
1 parent 3510125 commit 98b91c5

File tree

1 file changed

+179
-8
lines changed

1 file changed

+179
-8
lines changed

src/day10.rs

+179-8
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,14 @@ impl Tile {
5656

5757
// Return whether the current tile connects to the other tile in the given direction.
5858
fn connects_with(&self, other: &Tile, direction: &Direction) -> bool {
59-
let self_connects_to_other = self.connections[*direction as usize];
60-
let other_connects_to_self = other.connections[direction.opposite() as usize];
59+
let self_connects_to_other = self.connects_direction(direction);
60+
let other_connects_to_self = other.connects_direction(&direction.opposite());
6161
self_connects_to_other && other_connects_to_self
6262
}
63+
64+
fn connects_direction(&self, direction: &Direction) -> bool {
65+
self.connections[*direction as usize]
66+
}
6367
}
6468

6569
impl std::str::FromStr for Tile {
@@ -88,15 +92,19 @@ struct Maze {
8892

8993
impl Maze {
9094
pub fn find_loop_length(&self) -> usize {
91-
let mut result = 0;
95+
self.find_loop().count()
96+
}
97+
98+
fn find_loop(&self) -> impl Iterator<Item = (usize, usize)> {
99+
let mut result = vec![];
92100
let mut current = self.start;
93101
let mut previous = None;
94102
loop {
95103
//self.print_maze(Some(current));
96104

97105
// Find a connected tile that is not going back
98106
let mut next = None;
99-
for (x, y, d) in self.connected_tiles(current) {
107+
for (x, y, _) in self.connected_tiles(current) {
100108
match previous {
101109
Some((px, py)) => {
102110
if (x, y) != (px, py) {
@@ -113,13 +121,114 @@ impl Maze {
113121
// Walk it
114122
previous = Some(current);
115123
current = next.unwrap();
116-
result += 1;
124+
result.push(current);
117125

118126
if current == self.start {
119127
break;
120128
}
121129
}
122-
result
130+
result.into_iter()
131+
}
132+
133+
pub fn find_num_enclosed_tiles(&self) -> usize {
134+
// Step 1: Mark the loop: Sort the loop tiles by y and x coordinate, so we can quickly look up whether a tile is in the loop.
135+
// The start tile is a bit annoying here, replace it with what it represents by finding the two tiles that connect to it in the loop.
136+
let start_connected_tiles = self.connected_tiles(self.start).collect::<Vec<_>>();
137+
assert!(start_connected_tiles.len() == 2);
138+
let mut start_real_tile = None;
139+
for t in Tile::all() {
140+
if *t == Tile::START {
141+
continue
142+
}
143+
let (x1, y1, d1) = start_connected_tiles[0];
144+
let (x2, y2, d2) = start_connected_tiles[1];
145+
if t.connects_with(&self.tiles[y1][x1], &d1) && t.connects_with(&self.tiles[y2][x2], &d2) {
146+
start_real_tile = Some(t);
147+
break;
148+
}
149+
}
150+
assert!(start_real_tile.is_some());
151+
152+
let mut marked_tiles = self.find_loop().collect::<Vec<_>>();
153+
marked_tiles.sort_by(|(x1, y1), (x2, y2)| {
154+
if y1 < y2 {
155+
return std::cmp::Ordering::Less
156+
}
157+
if y1 > y2 {
158+
return std::cmp::Ordering::Greater
159+
}
160+
if x1 < x2 {
161+
return std::cmp::Ordering::Less
162+
}
163+
if x1 > x2 {
164+
return std::cmp::Ordering::Greater
165+
}
166+
std::cmp::Ordering::Equal
167+
});
168+
let mut marked_tiles_iter = marked_tiles.iter().peekable();
169+
170+
// Step 2: Scan over the whole maze, and track which tile is "outside" and "inside" the loop.
171+
// We assume the tile at -1,y is "outside", and then by scanning line per line change our understanding
172+
// of "outside": When crossing a `|` tile we are now "inside", and when crossing the next one we're back "outside."
173+
let mut inside_tiles = vec![];
174+
175+
for (y, row) in self.tiles.iter().enumerate() {
176+
// NB: We could probably keep the previous value: The loop is fully enclosed in the maze, so at the end of a line
177+
// we must hit "outside == true".
178+
let mut outside = true;
179+
let mut first_on_loop_tile: Option<&Tile> = None;
180+
for (x, mut tile) in row.iter().enumerate() {
181+
if *tile == Tile::START {
182+
println!("replacing start tile with {:?}", start_real_tile);
183+
tile = start_real_tile.unwrap();
184+
}
185+
if let Some((lx, ly)) = marked_tiles_iter.peek() {
186+
if *ly == y && *lx == x {
187+
// Tile is part of the loop, consume this tile.
188+
marked_tiles_iter.next();
189+
// See whether we cross the loop boundary:
190+
// - | is trivially crossing
191+
// - L-*7 is crossing (L has north, 7 has south)
192+
// - F-*J is crossing (F has south, J has north)
193+
// We don't need to check for horizontal tiles, because we're scanning line per line,
194+
// and we do need to look at 7/J tiles to start at pattern as we're going west to east.
195+
if *tile == Tile::VERTICAL {
196+
// println!("crossed the loop");
197+
outside = !outside;
198+
} else if let Some(folt) = first_on_loop_tile {
199+
// We had a previous loop tile, check whether the current tile completes or cancels the crossing.
200+
// The next tile is expected to be _not_ on the loop anymore, or will start another crossing.
201+
if (folt.connects_direction(&Direction::North) && tile.connects_direction(&Direction::South)) || (folt.connects_direction(&Direction::South) && tile.connects_direction(&Direction::North)) {
202+
// println!("finished crossing the loop");
203+
outside = !outside;
204+
first_on_loop_tile = None;
205+
} else if (folt.connects_direction(&Direction::North) && tile.connects_direction(&Direction::North)) || (folt.connects_direction(&Direction::South) && tile.connects_direction(&Direction::South)) {
206+
// println!("canceled crossing the loop");
207+
first_on_loop_tile = None;
208+
} else {
209+
// println!("still on the loop");
210+
}
211+
} else if tile.connections[Direction::East as usize] {
212+
// This cannot be a `-`, as we would have already had another first_on_loop_tile.
213+
// println!("started crossing the loop");
214+
first_on_loop_tile = Some(tile);
215+
}
216+
} else {
217+
// Tile is not part of the loop, count it if we're "inside" right now.
218+
assert!(first_on_loop_tile.is_none(), "Must not have a pending crossing when leaving the loop");
219+
if !outside {
220+
inside_tiles.push((x, y));
221+
}
222+
}
223+
}
224+
// self.print_maze(Some((x, y)), Some(&inside_tiles));
225+
}
226+
assert!(outside);
227+
}
228+
println!("Final maze:");
229+
self.print_maze(None, Some(&inside_tiles));
230+
231+
inside_tiles.len()
123232
}
124233

125234
fn connected_tiles(&self, node: (usize, usize)) -> impl Iterator<Item = (usize, usize, Direction)> {
@@ -135,7 +244,7 @@ impl Maze {
135244
}
136245
let other = &self.tiles[ny as usize][nx as usize];
137246
if tile.connects_with(other, d) {
138-
println!("tile {:?} connects to {:?} in {:?} direction", tile.symbol, other.symbol, d);
247+
// println!("tile {:?} connects to {:?} in {:?} direction", tile.symbol, other.symbol, d);
139248
result.push((nx as usize, ny as usize, *d));
140249
}
141250
}
@@ -144,12 +253,14 @@ impl Maze {
144253
}
145254

146255
#[allow(dead_code)]
147-
fn print_maze(&self, current: Option<(usize, usize)>) {
256+
fn print_maze(&self, current: Option<(usize, usize)>, inside_tiles: Option<&Vec<(usize, usize)>>) {
148257
for (y, row) in self.tiles.iter().enumerate() {
149258
print!("{:04} ", y);
150259
for (x, tile) in row.iter().enumerate() {
151260
if Some((x, y)) == current {
152261
print!("*");
262+
} else if inside_tiles.is_some() && inside_tiles.unwrap().contains(&(x, y)) {
263+
print!("I");
153264
} else {
154265
print!("{}", tile.symbol);
155266
}
@@ -186,6 +297,7 @@ pub fn main() {
186297
let maze = input.parse::<Maze>().unwrap();
187298
let loop_length = maze.find_loop_length();
188299
println!("loop length = {:?}, farthest distance = {:?}", loop_length, loop_length / 2);
300+
println!("enclosed tiles = {:?}", maze.find_num_enclosed_tiles());
189301
},
190302
Err(reason) => println!("error = {}", reason)
191303
}
@@ -255,4 +367,63 @@ SJLL7
255367
LJ.LJ";
256368
assert_eq!(MAZE.parse::<Maze>().ok().unwrap().find_loop_length(), 16);
257369
}
370+
371+
372+
#[test]
373+
fn part2_example1() {
374+
static MAZE: &str = "...........
375+
.S-------7.
376+
.|F-----7|.
377+
.||.....||.
378+
.||.....||.
379+
.|L-7.F-J|.
380+
.|..|.|..|.
381+
.L--J.L--J.
382+
...........";
383+
assert_eq!(MAZE.parse::<Maze>().ok().unwrap().find_num_enclosed_tiles(), 4);
384+
}
385+
386+
#[test]
387+
fn part2_example2() {
388+
static MAZE: &str = "..........
389+
.S------7.
390+
.|F----7|.
391+
.||....||.
392+
.||....||.
393+
.|L-7F-J|.
394+
.|..||..|.
395+
.L--JL--J.
396+
..........";
397+
assert_eq!(MAZE.parse::<Maze>().ok().unwrap().find_num_enclosed_tiles(), 4);
398+
}
399+
400+
#[test]
401+
fn part2_example3() {
402+
static MAZE: &str = ".F----7F7F7F7F-7....
403+
.|F--7||||||||FJ....
404+
.||.FJ||||||||L7....
405+
FJL7L7LJLJ||LJ.L-7..
406+
L--J.L7...LJS7F-7L7.
407+
....F-J..F7FJ|L7L7L7
408+
....L7.F7||L7|.L7L7|
409+
.....|FJLJ|FJ|F7|.LJ
410+
....FJL-7.||.||||...
411+
....L---J.LJ.LJLJ...";
412+
assert_eq!(MAZE.parse::<Maze>().ok().unwrap().find_num_enclosed_tiles(), 8);
413+
}
414+
415+
#[test]
416+
fn part2_example4() {
417+
static MAZE: &str = "FF7FSF7F7F7F7F7F---7
418+
L|LJ||||||||||||F--J
419+
FL-7LJLJ||||||LJL-77
420+
F--JF--7||LJLJ7F7FJ-
421+
L---JF-JLJ.||-FJLJJ7
422+
|F|F-JF---7F7-L7L|7|
423+
|FFJF7L7F-JF7|JL---7
424+
7-L-JL7||F7|L7F-7F7|
425+
L.L7LFJ|||||FJL7||LJ
426+
L7JLJL-JLJLJL--JLJ.L";
427+
assert_eq!(MAZE.parse::<Maze>().ok().unwrap().find_num_enclosed_tiles(), 10);
428+
}
258429
}

0 commit comments

Comments
 (0)