1use crate::input::Input;
2use std::collections::HashMap;
3
4#[derive(Copy, Clone)]
5struct Edge {
6 bitmask: u16,
8 matching: Option<TileId>,
10}
11
12impl Edge {
13 const fn flipped(self) -> Self {
14 Self {
15 bitmask: self.bitmask.reverse_bits() >> 6,
17 matching: self.matching,
18 }
19 }
20}
21
22type TileId = u16;
23
24#[derive(Copy, Clone)]
25struct Tile {
26 id: TileId,
27 edges: [Edge; 4],
29 body: [u8; 8],
32}
33
34impl Tile {
35 fn parse(input: &str) -> Result<Vec<Self>, String> {
36 let mut tiles = Vec::new();
37 for tile_str in input.split("\n\n") {
38 let mut tile_id = 0;
39 let mut this_edges = [Edge {
40 bitmask: 0,
41 matching: None,
42 }; 4];
43 let mut body = [0_u8; 8];
44
45 for (line_idx, line) in tile_str.lines().enumerate() {
46 if line_idx == 0 {
47 if !(line.len() == 10 && line.starts_with("Tile ") && line.ends_with(':')) {
48 return Err("Invalid tile header".to_string());
49 }
50 tile_id = line[5..9]
51 .parse::<u16>()
52 .map_err(|_| "Invalid tile header - cannot parse tile id")?;
53 } else {
54 let bytes = line.as_bytes();
55 if !(bytes.len() == 10 && bytes.iter().all(|c| matches!(c, b'#' | b'.'))) {
56 return Err(
57 "Invalid tile line (not 10 in length and only '.' and '#'".to_string()
58 );
59 }
60
61 if line_idx > 1 && line_idx < 10 {
62 for i in 0..8 {
63 if bytes[i + 1] == b'#' {
64 body[line_idx - 2] |= 1 << (7 - i);
65 }
66 }
67 }
68
69 if line_idx == 1 {
70 for (i, &b) in bytes.iter().enumerate().take(10) {
72 if b == b'#' {
73 this_edges[0].bitmask |= 1 << (9 - i);
74 }
75 }
76 } else if line_idx == 10 {
77 for (i, &b) in bytes.iter().enumerate().take(10) {
79 if b == b'#' {
80 this_edges[2].bitmask |= 1 << (9 - i);
81 }
82 }
83 }
84
85 if bytes[0] == b'#' {
86 this_edges[3].bitmask |= 1 << (10 - line_idx);
88 }
89
90 if bytes[9] == b'#' {
91 this_edges[1].bitmask |= 1 << (10 - line_idx);
93 }
94 }
95 }
96 tiles.push(Self {
97 id: tile_id,
98 edges: this_edges,
99 body,
100 });
101 }
102
103 let mut edge_to_tile_idx = vec![Vec::new(); 1024];
105 for tile in tiles.iter() {
106 for &edge in tile.edges.iter() {
107 edge_to_tile_idx[edge.bitmask as usize].push(tile.id);
108 edge_to_tile_idx[edge.flipped().bitmask as usize].push(tile.id);
109 }
110 }
111 for tile in tiles.iter_mut() {
112 let tile_id = tile.id;
113 for edge in tile.edges.iter_mut() {
114 if let Some(&other_tile_id) = edge_to_tile_idx[edge.bitmask as usize]
115 .iter()
116 .find(|&&other_tile_id| other_tile_id != tile_id)
117 {
118 edge.matching = Some(other_tile_id);
119 }
120 }
121 }
122
123 Ok(tiles)
124 }
125
126 fn is_corner(&self) -> bool {
127 self.edges
128 .iter()
129 .filter(|edge| edge.matching.is_none())
130 .count()
131 == 2
132 }
133
134 fn transform_to_match(
135 &self,
136 x: u8,
137 y: u8,
138 composed_image: &HashMap<(u8, u8), Self>,
139 composed_image_width: u8,
140 ) -> Result<Self, String> {
141 let mut current = *self;
142
143 for _flip in 0..=1 {
144 for _rotation in 0..=3 {
145 if current.edges[0].matching.is_none() == (y == 0)
146 && current.edges[1].matching.is_none() == (x + 1 == composed_image_width)
147 && current.edges[2].matching.is_none() == (y + 1 == composed_image_width)
148 && current.edges[3].matching.is_none() == (x == 0)
149 {
150 let mut possible = true;
151 if x != 0 {
152 if let Some(tile_to_left) = composed_image.get(&(x - 1, y)) {
153 if Some(tile_to_left.id) != current.edges[3].matching {
154 possible = false;
155 }
156 }
157 }
158 if y != 0 {
159 if let Some(tile_above) = composed_image.get(&(x, y - 1)) {
160 if Some(tile_above.id) != current.edges[0].matching {
161 possible = false;
162 }
163 }
164 }
165 if let Some(tile_to_right) = composed_image.get(&(x + 1, y)) {
166 if Some(tile_to_right.id) != current.edges[1].matching {
167 possible = false;
168 }
169 }
170 if let Some(tile_below) = composed_image.get(&(x, y + 1)) {
171 if Some(tile_below.id) != current.edges[2].matching {
172 possible = false;
173 }
174 }
175 if possible {
176 return Ok(current);
177 }
178 }
179 current = current.rotate_clockwise();
180 }
181 current = current.flip_horizontal();
182 }
183 Err("transform_to_match not found".to_string())
184 }
185
186 fn rotate_clockwise(&self) -> Self {
187 let rotated_edges = [self.edges[3], self.edges[0], self.edges[1], self.edges[2]];
188 let mut rotated_body = [0_u8; 8];
189 for i in 0..8 {
196 for j in 0..8 {
197 rotated_body[7 - i] |= if self.body[j] & (1 << i) > 0 {
198 1 << j
199 } else {
200 0
201 };
202 }
203 }
204 Self {
205 id: self.id,
206 edges: rotated_edges,
207 body: rotated_body,
208 }
209 }
210
211 fn flip_horizontal(&self) -> Self {
212 let mut flipped_body = self.body;
213 for b in flipped_body.iter_mut() {
214 *b = b.reverse_bits();
215 }
216 Self {
217 id: self.id,
218 edges: [
219 self.edges[0].flipped(),
220 self.edges[3],
221 self.edges[2].flipped(),
222 self.edges[1],
223 ],
224 body: flipped_body,
225 }
226 }
227}
228
229pub fn solve(input: &Input) -> Result<u64, String> {
236 let tiles = Tile::parse(input.text)?;
237
238 if input.is_part_one() {
239 return Ok(tiles
240 .iter()
241 .filter_map(|tile| {
242 if tile.is_corner() {
243 Some(u64::from(tile.id))
244 } else {
245 None
246 }
247 })
248 .product());
249 }
250
251 let composed_image_tile_width = (tiles.len() as f64).sqrt() as u8;
252 let composed_image_pixel_width = composed_image_tile_width * 8;
253
254 let a_corner = *tiles
255 .iter()
256 .find(|tile| tile.is_corner())
257 .ok_or_else(|| "No corner found".to_string())?;
258
259 let starting_coordinates = match (
260 a_corner.edges[0].matching.is_none(),
261 a_corner.edges[1].matching.is_none(),
262 a_corner.edges[2].matching.is_none(),
263 a_corner.edges[3].matching.is_none(),
264 ) {
265 (true, false, false, true) => (0, 0),
266 (true, true, false, false) => (composed_image_tile_width - 1, 0),
267 (false, false, true, true) => (0, composed_image_tile_width - 1),
268 (false, true, true, false) => {
269 (composed_image_tile_width - 1, composed_image_tile_width - 1)
270 }
271 _ => {
272 return Err("Invalid input - tile with two matches is no".to_string());
273 }
274 };
275
276 let mut composed_image: HashMap<(u8, u8), Tile> = HashMap::new();
277 composed_image.insert(starting_coordinates, a_corner);
278
279 let mut stack = vec![(starting_coordinates.0, starting_coordinates.1, a_corner)];
280
281 while let Some((x, y, popped_tile)) = stack.pop() {
282 for (edge_idx, &edge) in popped_tile.edges.iter().enumerate() {
283 if let Some(matched_tile) = edge.matching {
284 let tile_with_matching_edge = tiles
285 .iter()
286 .find(|t| t.id == matched_tile)
287 .ok_or_else(|| "Internal error".to_string())?;
288
289 let (new_x, new_y) = match edge_idx {
290 0 if y > 0 => (x, y - 1),
291 1 => (x + 1, y),
292 2 => (x, y + 1),
293 3 if x > 0 => (x - 1, y),
294 _ => {
295 continue;
296 }
297 };
298 if new_x >= composed_image_tile_width
299 || new_y >= composed_image_tile_width
300 || composed_image.contains_key(&(new_x, new_y))
301 {
302 continue;
303 }
304
305 let transformed_tile = tile_with_matching_edge.transform_to_match(
306 new_x,
307 new_y,
308 &composed_image,
309 composed_image_tile_width,
310 )?;
311
312 composed_image.insert((new_x, new_y), transformed_tile);
313
314 stack.push((new_x, new_y, transformed_tile));
315 }
316 }
317 }
318
319 let is_hash_at = |direction: u8, sideway: u8, offset: u8| {
320 let (pixel_x, pixel_y) = match direction {
321 0 => (sideway, offset),
322 1 => (offset, sideway),
323 2 => (sideway, composed_image_pixel_width - 1 - offset),
324 _ => (composed_image_pixel_width - 1 - offset, sideway),
325 };
326
327 let tile_x = pixel_x / 8;
328 let tile_y = pixel_y / 8;
329 let bit = pixel_x % 8;
330 let row = pixel_y % 8;
331
332 composed_image
333 .get(&(tile_x, tile_y))
334 .map(|tile| tile.body[row as usize] & (1 << (7 - bit)) != 0)
335 .unwrap_or_default()
336 };
337
338 let monster_body_len = 20;
345 for direction in [0_u8, 1, 2, 3] {
346 for flip in [1_i8, -1] {
347 let mut monster_count = 0;
348 for offset in 0..(composed_image_pixel_width - monster_body_len + 1) {
349 for sideway in 1..(composed_image_pixel_width - 1) {
350 if is_hash_at(direction, sideway, offset)
351 && is_hash_at(direction, sideway, offset + 5)
352 && is_hash_at(direction, sideway, offset + 6)
353 && is_hash_at(direction, sideway, offset + 11)
354 && is_hash_at(direction, sideway, offset + 12)
355 && is_hash_at(direction, sideway, offset + 17)
356 && is_hash_at(direction, sideway, offset + 18)
357 && is_hash_at(direction, sideway, offset + 19)
358 && is_hash_at(direction, (sideway as i8 - flip) as u8, offset + 18)
359 && is_hash_at(direction, (sideway as i8 + flip) as u8, offset + 1)
360 && is_hash_at(direction, (sideway as i8 + flip) as u8, offset + 4)
361 && is_hash_at(direction, (sideway as i8 + flip) as u8, offset + 7)
362 && is_hash_at(direction, (sideway as i8 + flip) as u8, offset + 10)
363 && is_hash_at(direction, (sideway as i8 + flip) as u8, offset + 13)
364 && is_hash_at(direction, (sideway as i8 + flip) as u8, offset + 16)
365 {
366 monster_count += 1;
367 }
368 }
369 }
370
371 if monster_count != 0 {
372 return Ok(tiles
373 .iter()
374 .map(|t| {
375 t.body
376 .iter()
377 .map(|row| u64::from(row.count_ones()))
378 .sum::<u64>()
379 })
380 .sum::<u64>()
381 - monster_count * 15);
382 }
383 }
384 }
385
386 Err("No sea monster found".to_string())
387}
388
389#[cfg(test)]
390const fn edge(bitmask: u16) -> Edge {
391 Edge {
392 bitmask,
393 matching: None,
394 }
395}
396
397#[test]
398pub fn test_rotate() {
399 let tile = Tile {
400 id: 0,
401 edges: [edge(1), edge(2), edge(3), edge(4)],
402 body: [0b1010_0000, 0b0101_0000, 0, 0, 0, 0, 0, 0],
403 };
404
405 let rotated_tile = tile.rotate_clockwise();
406 assert_eq!(rotated_tile.id, tile.id);
407 assert_eq!(
408 rotated_tile
409 .edges
410 .iter()
411 .map(|e| e.bitmask)
412 .collect::<Vec<_>>(),
413 [4, 1, 2, 3]
414 );
415 assert_eq!(rotated_tile.body, [0b1, 0b10, 0b1, 0b10, 0, 0, 0, 0]);
427}
428
429#[test]
430pub fn test_flip() {
431 let tile = Tile {
432 id: 17,
433 edges: [edge(0b1), edge(0b10), edge(0b11), edge(0b100)],
434 body: [0b1010_0000, 0b0101_0000, 0, 0, 0, 0, 0, 0],
435 };
436
437 let horizontally_flipped = tile.flip_horizontal();
438 assert_eq!(17, horizontally_flipped.id);
439 assert_eq!(
440 horizontally_flipped
441 .edges
442 .iter()
443 .map(|e| e.bitmask)
444 .collect::<Vec<_>>(),
445 [0b10_0000_0000, 0b100, 0b11_0000_0000, 0b10]
446 );
447 assert_eq!(
448 horizontally_flipped.body,
449 [0b0000_0101, 0b0000_1010, 0, 0, 0, 0, 0, 0]
450 );
451}
452
453#[test]
454pub fn tests() {
455 use crate::input::{test_part_one, test_part_two};
456
457 let example = "Tile 2311:
458..##.#..#.
459##..#.....
460#...##..#.
461####.#...#
462##.##.###.
463##...#.###
464.#.#.#..##
465..#....#..
466###...#.#.
467..###..###
468
469Tile 1951:
470#.##...##.
471#.####...#
472.....#..##
473#...######
474.##.#....#
475.###.#####
476###.##.##.
477.###....#.
478..#.#..#.#
479#...##.#..
480
481Tile 1171:
482####...##.
483#..##.#..#
484##.#..#.#.
485.###.####.
486..###.####
487.##....##.
488.#...####.
489#.##.####.
490####..#...
491.....##...
492
493Tile 1427:
494###.##.#..
495.#..#.##..
496.#.##.#..#
497#.#.#.##.#
498....#...##
499...##..##.
500...#.#####
501.#.####.#.
502..#..###.#
503..##.#..#.
504
505Tile 1489:
506##.#.#....
507..##...#..
508.##..##...
509..#...#...
510#####...#.
511#..#.#.#.#
512...#.#.#..
513##.#...##.
514..##.##.##
515###.##.#..
516
517Tile 2473:
518#....####.
519#..#.##...
520#.##..#...
521######.#.#
522.#...#.#.#
523.#########
524.###.#..#.
525########.#
526##...##.#.
527..###.#.#.
528
529Tile 2971:
530..#.#....#
531#...###...
532#.#.###...
533##.##..#..
534.#####..##
535.#..####.#
536#..#.#..#.
537..####.###
538..#.#.###.
539...#.#.#.#
540
541Tile 2729:
542...#.#.#.#
543####.#....
544..#.#.....
545....#..#.#
546.##..##.#.
547.#.####...
548####.#.#..
549##.####...
550##..#.##..
551#.##...##.
552
553Tile 3079:
554#.#.#####.
555.#..######
556..#.......
557######....
558####.#..#.
559.#...#.##.
560#.#####.##
561..#.###...
562..#.......
563..#.###...";
564 test_part_one!(example=> 20_899_048_083_289);
565 test_part_two!(example => 273);
566
567 let real_input = include_str!("day20_input.txt");
568 test_part_one!(real_input => 21_599_955_909_991);
569 test_part_two!(real_input => 2495);
570}