advent_of_code/year2020/
day20.rs

1use crate::input::Input;
2use std::collections::HashMap;
3
4#[derive(Copy, Clone)]
5struct Edge {
6    /// Bitmask where '#' is set bit, '.' is unset. Only 10 first bits used.
7    bitmask: u16,
8    /// The tile that matches on the other end.
9    matching: Option<TileId>,
10}
11
12impl Edge {
13    const fn flipped(self) -> Self {
14        Self {
15            // Only the first 10 bits of the edge bitmask is used:
16            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    /// Indexed by 0,1,3,4 = Top,Right,Bottom,Left.
28    edges: [Edge; 4],
29    /// Indexed by row. Lowest bit to the right.
30    /// Example: The row "#..#...." is stored as 0b10010000.
31    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                        // Top edge:
71                        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                        // Bottom edge:
78                        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                        // Left edge:
87                        this_edges[3].bitmask |= 1 << (10 - line_idx);
88                    }
89
90                    if bytes[9] == b'#' {
91                        // Right edge.
92                        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        // Mapped from edge bitmask to list of tile_id:s.
104        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        // abcdefgh
190        // ABCDEFGH
191        // =>
192        // ......Aa
193        // ......Bb
194        // [..]
195        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
229/// Key properties from the problem description:
230///
231/// - Each tile is a 8x8 grid.
232/// - Each tile edge is 10 bits (so max 1024 distinct values).
233/// - The composed image is square.
234/// - The outermost edges tile edges won't line up with any other tiles.
235pub 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    // Search for the main body center streak "#    ##    ##    ###"
339    // of length 20, and then look at the sides for the full shape:
340    //
341    // "                  # "
342    // "#    ##    ##    ###"
343    // " #  #  #  #  #  #   "
344    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    // #.#.....
416    // .#.#....
417    // [6 empty rows]
418    //
419    // =>
420    //
421    // .......#
422    // ......#.
423    // .......#
424    // ......#.
425    // [4 empty rows]
426    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}