advent_of_code/year2019/
day20.rs1use crate::input::Input;
2use std::collections::hash_map::Entry;
3use std::collections::{HashMap, HashSet, VecDeque};
4
5const DIRECTIONS: [(i32, i32); 4] = [(0, 1), (0, -1), (-1, 0), (1, 0)];
6
7struct Maze {
8 cols: usize,
9 array: Vec<u8>,
10 portals: HashMap<(i32, i32), ((i32, i32), i32)>,
11 start_location: (i32, i32),
12 end_location: (i32, i32),
13}
14
15impl Maze {
16 fn tile_at(&self, x: i32, y: i32) -> u8 {
17 if x < 0 || y < 0 {
18 return b' ';
19 }
20 *self
21 .array
22 .get((x as usize) + self.cols * (y as usize))
23 .unwrap_or(&b' ')
24 }
25
26 fn set_tile(&mut self, x: usize, y: usize, tile: u8) {
27 self.array[x + self.cols * y] = tile;
28 }
29
30 fn parse(input: &str, part1: bool) -> Result<Self, String> {
31 let rows = input.lines().count();
32 let cols = input
33 .lines()
34 .map(str::len)
35 .max()
36 .ok_or("Internal error: No max line length")?;
37
38 if rows < 5 || cols < 5 {
39 return Err("Too small input - expected at least 5x5".to_string());
40 }
41
42 let array = vec![b' '; rows * cols];
43 let mut maze = Self {
44 cols,
45 array,
46 portals: HashMap::new(),
47 end_location: (0, 0),
48 start_location: (0, 0),
49 };
50 input.lines().enumerate().for_each(|(y, line)| {
51 line.bytes().enumerate().for_each(|(x, tile)| {
52 maze.set_tile(x, y, tile);
53 });
54 });
55
56 let mut current_string = String::new();
57 let mut coming_from_passage = false;
58 let mut portal_name_to_location: HashMap<String, (i32, i32)> = HashMap::new();
59
60 let mut on_tile = |x: i32, y: i32, x_direction| {
61 let tile = if x as usize >= cols || y as usize >= rows {
62 b' '
63 } else {
64 maze.tile_at(x, y)
65 };
66
67 if tile.is_ascii_uppercase() {
68 current_string.push(tile as char);
69 } else {
70 if current_string.len() == 2 {
71 let portal_x = if x_direction && coming_from_passage {
72 x - 3
73 } else {
74 x
75 };
76 let portal_y = if !x_direction && coming_from_passage {
77 y - 3
78 } else {
79 y
80 };
81
82 let current_location: (i32, i32) = (portal_x, portal_y);
83
84 match current_string.as_str() {
85 "AA" => {
86 maze.start_location = (portal_x, portal_y);
87 }
88 "ZZ" => {
89 maze.end_location = (portal_x, portal_y);
90 }
91 _ => {}
92 };
93
94 match portal_name_to_location.entry(current_string.clone()) {
95 Entry::Occupied(other_location) => {
96 let level_difference = if part1 {
97 0
98 } else if current_location.0 == 2
99 || current_location.0 == (cols - 3) as i32
100 || current_location.1 == 2
101 || current_location.1 == (rows - 3) as i32
102 {
103 -1
104 } else {
105 1
106 };
107 let other_location = *other_location.get();
108 maze.portals
109 .insert(current_location, (other_location, level_difference));
110 maze.portals
111 .insert(other_location, (current_location, -level_difference));
112 }
113 Entry::Vacant(vacant) => {
114 vacant.insert(current_location);
115 }
116 }
117 }
118
119 coming_from_passage = tile == b'.';
120 current_string.clear();
121 }
122 };
123
124 for x in 0..=cols {
125 for y in 0..=rows {
126 on_tile(x as i32, y as i32, false);
127 }
128 }
129 for y in 0..=rows {
130 for x in 0..=cols {
131 on_tile(x as i32, y as i32, true);
132 }
133 }
134
135 if maze.start_location == maze.end_location {
136 return Err("Start location not distinct from end location".to_string());
137 }
138
139 Ok(maze)
140 }
141}
142
143pub fn solve(input: &Input) -> Result<i32, String> {
144 let maze = Maze::parse(input.text, input.is_part_one())?;
145
146 let mut to_visit = VecDeque::new();
147 let mut visited = HashSet::new();
148 to_visit.push_back((maze.start_location, 0, 0));
150 visited.insert((maze.start_location, 0));
152
153 while let Some((visiting, distance, level)) = to_visit.pop_front() {
154 let new_distance = distance + 1;
155
156 for (new_location, level_difference) in DIRECTIONS
157 .iter()
158 .map(|&(dx, dy)| ((visiting.0 + dx, visiting.1 + dy), 0))
159 .chain(
160 if let Some(&(new_location, level_difference)) = maze.portals.get(&visiting) {
161 Some((new_location, level_difference)).into_iter()
162 } else {
163 None.into_iter()
164 },
165 )
166 {
167 let new_level = level + level_difference;
168 if new_level >= 0
169 && maze.tile_at(new_location.0, new_location.1) == b'.'
170 && visited.insert((new_location, new_level))
171 {
172 if new_location == maze.end_location && new_level == 0 {
173 return Ok(new_distance);
174 }
175 to_visit.push_back((new_location, new_distance, new_level));
176 }
177 }
178 }
179 Err("No path found".to_string())
180}
181
182#[test]
183pub fn tests() {
184 use crate::input::{test_part_one, test_part_two};
185
186 let example = include_str!("day20_example.txt");
187 test_part_one!(example => 23);
188
189 let input = include_str!("day20_input.txt");
190 test_part_one!(input => 580);
191 test_part_two!(input => 6362);
192
193 let other_input = include_str!("day20_input_ray.txt");
194 test_part_one!(other_input => 552);
195 test_part_two!(other_input => 6492);
196}