mirror of
https://github.com/bertptrs/adventofcode.git
synced 2025-12-26 05:10:32 +01:00
499 lines
13 KiB
Rust
499 lines
13 KiB
Rust
use std::collections::HashMap;
|
|
use std::collections::HashSet;
|
|
use std::io::Read;
|
|
|
|
use regex::bytes::Regex;
|
|
|
|
use crate::common::Lines;
|
|
use crate::Solution;
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct Tile {
|
|
grid: Vec<Vec<bool>>,
|
|
id: u64,
|
|
// NESW order
|
|
sides: [Vec<bool>; 4],
|
|
rotations: u8,
|
|
flipped: bool,
|
|
}
|
|
|
|
impl Tile {
|
|
/// Rotate the slice once to the left. Affects
|
|
fn rotate(&mut self) {
|
|
self.rotations = (self.rotations + 1) % 4;
|
|
self.sides.rotate_right(1);
|
|
self.sides[0].reverse();
|
|
self.sides[2].reverse();
|
|
}
|
|
|
|
/// Horizontally flip the tile
|
|
fn flip(&mut self) {
|
|
debug_assert!(!self.flipped);
|
|
self.flipped = !self.flipped;
|
|
self.sides[0].reverse();
|
|
self.sides[2].reverse();
|
|
self.sides.swap(1, 3);
|
|
}
|
|
|
|
fn len(&self) -> usize {
|
|
self.grid.len() - 2
|
|
}
|
|
}
|
|
|
|
fn read_input(input: &mut dyn Read) -> HashMap<u64, Tile> {
|
|
let mut tiles = HashMap::new();
|
|
let mut lines = Lines::new(input);
|
|
|
|
while let Some(line) = lines.next() {
|
|
let id = line[5..(line.len() - 1)].parse().unwrap();
|
|
drop(line);
|
|
|
|
let mut grid: Vec<Vec<_>> = Vec::new();
|
|
|
|
// Clippy gets it wrong, this cannot be a for loop
|
|
#[allow(clippy::while_let_on_iterator)]
|
|
while let Some(line) = lines.next() {
|
|
if line.is_empty() {
|
|
break;
|
|
}
|
|
|
|
grid.push(line.chars().map(|c| c == '#').collect());
|
|
}
|
|
|
|
let sides = [
|
|
grid[0].clone(),
|
|
grid.iter().map(|v| *v.last().unwrap()).collect(),
|
|
grid.last().cloned().unwrap(),
|
|
grid.iter().map(|v| v[0]).collect(),
|
|
];
|
|
|
|
let tile = Tile {
|
|
grid,
|
|
sides,
|
|
id,
|
|
rotations: 0,
|
|
flipped: false,
|
|
};
|
|
|
|
tiles.insert(id, tile);
|
|
}
|
|
|
|
tiles
|
|
}
|
|
|
|
fn compute_matching<'a>(tiles: impl IntoIterator<Item = &'a Tile>) -> HashMap<Vec<bool>, Vec<u64>> {
|
|
let mut matching: HashMap<Vec<_>, Vec<u64>> = HashMap::new();
|
|
|
|
for tile in tiles {
|
|
for side in &tile.sides {
|
|
let neighbours = matching.entry(side.clone()).or_default();
|
|
|
|
if !neighbours.contains(&tile.id) {
|
|
neighbours.push(tile.id);
|
|
}
|
|
|
|
let mut rev = side.clone();
|
|
rev.reverse();
|
|
|
|
let neighbours = matching.entry(rev).or_default();
|
|
|
|
if !neighbours.contains(&tile.id) {
|
|
neighbours.push(tile.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if my tile assumption is violated
|
|
debug_assert!(matching.values().map(Vec::len).max().unwrap_or(0) <= 2);
|
|
|
|
matching
|
|
}
|
|
|
|
fn compute_neighbours<'a>(
|
|
cliques: impl IntoIterator<Item = &'a Vec<u64>>,
|
|
) -> HashMap<u64, Vec<u64>> {
|
|
let mut neighbours: HashMap<u64, Vec<u64>> = HashMap::new();
|
|
|
|
for clique in cliques {
|
|
for &a in clique {
|
|
let related = neighbours.entry(a).or_default();
|
|
|
|
for &b in clique {
|
|
if a != b && !related.contains(&b) {
|
|
related.push(b);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
neighbours
|
|
}
|
|
|
|
fn rev_eq(a: &[bool], b: &[bool]) -> bool {
|
|
a.iter().zip(b.iter().rev()).all(|(&a, &b)| a == b)
|
|
}
|
|
|
|
fn complete_row(
|
|
mut first: u64,
|
|
neighbours: &HashMap<u64, Vec<u64>>,
|
|
tiles: &mut HashMap<u64, Tile>,
|
|
used_tiles: &mut HashSet<u64>,
|
|
) -> Vec<u64> {
|
|
let mut row = vec![first];
|
|
used_tiles.insert(first);
|
|
|
|
loop {
|
|
let last = first;
|
|
let mut next = None;
|
|
|
|
for n in &neighbours[&last] {
|
|
if used_tiles.contains(n) {
|
|
continue;
|
|
}
|
|
|
|
for _ in 0..4 {
|
|
if tiles[&last].sides[1] == tiles[n].sides[3] {
|
|
break;
|
|
} else if rev_eq(&tiles[&last].sides[1], &tiles[n].sides[3]) {
|
|
let tile = tiles.get_mut(n).unwrap();
|
|
|
|
// Vertical flip == horizontal flip + 180
|
|
tile.rotate();
|
|
tile.rotate();
|
|
tile.flip();
|
|
break;
|
|
} else {
|
|
tiles.get_mut(&n).unwrap().rotate();
|
|
}
|
|
}
|
|
|
|
if tiles[&last].sides[1] == tiles[n].sides[3] {
|
|
// This tile matches, add it
|
|
next = Some(*n);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if let Some(next) = next {
|
|
row.push(next);
|
|
used_tiles.insert(next);
|
|
first = next;
|
|
} else {
|
|
return row;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn compute_image(
|
|
neighbours: &HashMap<u64, Vec<u64>>,
|
|
matching: HashMap<Vec<bool>, Vec<u64>>,
|
|
tiles: &mut HashMap<u64, Tile>,
|
|
) -> Vec<Vec<u64>> {
|
|
let mut corner = Vec::new();
|
|
|
|
for (&id, connected) in neighbours {
|
|
if connected.len() == 2 && !corner.contains(&id) {
|
|
corner.push(id);
|
|
}
|
|
}
|
|
|
|
// Randomly put down the first corner
|
|
let mut used_tiles = HashSet::new();
|
|
|
|
let corner_tile = tiles.get_mut(&corner[0]).unwrap();
|
|
|
|
// Rotate it until it fits
|
|
while matching[&corner_tile.sides[2]].len() != 2 {
|
|
corner_tile.rotate();
|
|
}
|
|
|
|
if matching[&corner_tile.sides[1]].len() != 2 {
|
|
corner_tile.flip();
|
|
}
|
|
|
|
debug_assert_eq!(matching[&corner_tile.sides[1]].len(), 2);
|
|
debug_assert_eq!(matching[&corner_tile.sides[2]].len(), 2);
|
|
|
|
let rotation = corner_tile.rotations;
|
|
let sides = corner_tile.sides.clone();
|
|
|
|
let mut rows = vec![complete_row(corner[0], &neighbours, tiles, &mut used_tiles)];
|
|
|
|
debug_assert_eq!(rotation, tiles[&rows[0][0]].rotations);
|
|
debug_assert_eq!(sides, tiles[&rows[0][0]].sides);
|
|
|
|
while used_tiles.len() < tiles.len() {
|
|
let prev = rows.last().unwrap()[0];
|
|
|
|
// Should be just one tile that can go there.
|
|
let next = neighbours[&prev]
|
|
.iter()
|
|
.find(|&n| !used_tiles.contains(n))
|
|
.unwrap();
|
|
|
|
for _ in 0..4 {
|
|
if tiles[&prev].sides[2] == tiles[next].sides[0] {
|
|
break;
|
|
} else if rev_eq(&tiles[&prev].sides[2], &tiles[next].sides[0]) {
|
|
tiles.get_mut(next).unwrap().flip();
|
|
break;
|
|
} else {
|
|
tiles.get_mut(next).unwrap().rotate();
|
|
}
|
|
}
|
|
|
|
debug_assert_eq!(&tiles[&prev].sides[2], &tiles[next].sides[0]);
|
|
|
|
rows.push(complete_row(*next, &neighbours, tiles, &mut used_tiles));
|
|
}
|
|
|
|
rows
|
|
}
|
|
|
|
fn combine_tiles(rows: &[Vec<u64>], tiles: &mut HashMap<u64, Tile>) -> Vec<Vec<u8>> {
|
|
// Fix orientation
|
|
for tile in tiles.values_mut() {
|
|
let to_rotate = tile.rotations;
|
|
|
|
for _ in 0..to_rotate {
|
|
tile.grid = rotate(&tile.grid);
|
|
}
|
|
|
|
if tile.flipped {
|
|
reverse(&mut tile.grid);
|
|
}
|
|
|
|
tile.rotations = 0;
|
|
tile.flipped = false;
|
|
}
|
|
|
|
rows.iter()
|
|
.flat_map(|row| {
|
|
let len = tiles[row.first().unwrap()].len();
|
|
|
|
let mut rows = vec![Vec::new(); len];
|
|
|
|
for id in row {
|
|
let tile = &tiles[id];
|
|
for (r, line) in tile.grid.iter().skip(1).take(len).enumerate() {
|
|
rows[r].extend(
|
|
line.iter()
|
|
.skip(1)
|
|
.take(len)
|
|
.map(|&b| if b { b'#' } else { b'.' }),
|
|
);
|
|
}
|
|
}
|
|
|
|
rows
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn monster_regex() -> [Regex; 3] {
|
|
[
|
|
Regex::new(&" # ".replace(' ', ".")).unwrap(),
|
|
Regex::new(&"# ## ## ###".replace(' ', ".")).unwrap(),
|
|
Regex::new(&" # # # # # # ".replace(' ', ".")).unwrap(),
|
|
]
|
|
}
|
|
|
|
fn rotate<T: Copy>(image: &[Vec<T>]) -> Vec<Vec<T>> {
|
|
let mut new = vec![Vec::new(); image[0].len()];
|
|
|
|
for (c, target) in new.iter_mut().enumerate() {
|
|
target.extend(image.iter().rev().map(|r| r[c]));
|
|
}
|
|
|
|
new
|
|
}
|
|
|
|
fn reverse<T>(image: &mut [Vec<T>]) {
|
|
for row in image {
|
|
row.reverse();
|
|
}
|
|
}
|
|
|
|
fn replace_monster(image: &mut [Vec<u8>]) {
|
|
let searchers = monster_regex();
|
|
|
|
for i in 1..(image.len() - 1) {
|
|
let mut start = 0;
|
|
|
|
while let Some(found) = searchers[1].find_at(&image[i], start) {
|
|
start = found.start() + 1;
|
|
let range = found.range();
|
|
|
|
if searchers[2].is_match(&image[i + 1][range.clone()])
|
|
&& searchers[0].is_match(&image[i - 1][range.clone()])
|
|
{
|
|
image[(i - 1)..=(i + 1)]
|
|
.iter_mut()
|
|
.zip(&searchers)
|
|
.for_each(|(line, expr)| {
|
|
line[range.clone()]
|
|
.iter_mut()
|
|
.zip(expr.as_str().as_bytes().iter())
|
|
.for_each(|(b, &r)| {
|
|
if *b == r {
|
|
*b = b'O';
|
|
}
|
|
})
|
|
});
|
|
} else if searchers[2].is_match(&image[i - 1][range.clone()])
|
|
&& searchers[0].is_match(&image[i + 1][range.clone()])
|
|
{
|
|
image[(i - 1)..=(i + 1)]
|
|
.iter_mut()
|
|
.rev()
|
|
.zip(&searchers)
|
|
.for_each(|(line, expr)| {
|
|
line[range.clone()]
|
|
.iter_mut()
|
|
.zip(expr.as_str().as_bytes().iter())
|
|
.for_each(|(b, &r)| {
|
|
if *b == r {
|
|
*b = b'O';
|
|
}
|
|
})
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct Day20;
|
|
|
|
impl Solution for Day20 {
|
|
fn part1(&mut self, input: &mut dyn Read) -> String {
|
|
let tiles = read_input(input);
|
|
let matching = compute_matching(tiles.values());
|
|
let neighbours = compute_neighbours(matching.values());
|
|
|
|
neighbours
|
|
.into_iter()
|
|
.filter_map(|(i, n)| if n.len() == 2 { Some(i) } else { None })
|
|
.product::<u64>()
|
|
.to_string()
|
|
}
|
|
|
|
fn part2(&mut self, input: &mut dyn Read) -> String {
|
|
let mut tiles = read_input(input);
|
|
let matching = compute_matching(tiles.values());
|
|
let neighbours = compute_neighbours(matching.values());
|
|
|
|
let rows = compute_image(&neighbours, matching, &mut tiles);
|
|
|
|
let mut image = combine_tiles(&rows, &mut tiles);
|
|
|
|
replace_monster(&mut image);
|
|
reverse(&mut image);
|
|
replace_monster(&mut image);
|
|
image = rotate(&image);
|
|
replace_monster(&mut image);
|
|
reverse(&mut image);
|
|
replace_monster(&mut image);
|
|
|
|
image
|
|
.iter()
|
|
.map(|b| bytecount::count(&b, b'#'))
|
|
.sum::<usize>()
|
|
.to_string()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::test_implementation;
|
|
|
|
use super::*;
|
|
|
|
const SAMPLE: &[u8] = include_bytes!("../samples/20.txt");
|
|
|
|
#[test]
|
|
fn sample_part1() {
|
|
test_implementation(Day20, 1, SAMPLE, 20899048083289u64);
|
|
}
|
|
|
|
#[test]
|
|
fn sample_part2() {
|
|
test_implementation(Day20, 2, SAMPLE, 273);
|
|
}
|
|
|
|
#[test]
|
|
fn test_tile_rotate() {
|
|
let mut tile = Tile {
|
|
grid: Vec::new(),
|
|
id: 1,
|
|
sides: [
|
|
vec![false, true],
|
|
vec![false, false, true],
|
|
vec![false, true],
|
|
vec![true, true, false],
|
|
],
|
|
rotations: 0,
|
|
flipped: false,
|
|
};
|
|
|
|
tile.rotate();
|
|
|
|
assert_eq!(
|
|
tile.sides,
|
|
[
|
|
vec![false, true, true],
|
|
vec![false, true],
|
|
vec![true, false, false],
|
|
vec![false, true],
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_tile_flip() {
|
|
let mut tile = Tile {
|
|
grid: Vec::new(),
|
|
id: 1,
|
|
sides: [
|
|
vec![false, true],
|
|
vec![false, false, true],
|
|
vec![false, true],
|
|
vec![true, true, false],
|
|
],
|
|
rotations: 0,
|
|
flipped: false,
|
|
};
|
|
|
|
tile.flip();
|
|
|
|
assert_eq!(
|
|
tile.sides,
|
|
[
|
|
vec![true, false],
|
|
vec![true, true, false],
|
|
vec![true, false],
|
|
vec![false, false, true],
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_rotate() {
|
|
let sample: Vec<Vec<u8>> = vec![
|
|
b"###".as_ref().to_owned(),
|
|
b" #".as_ref().to_owned(),
|
|
b"# #".as_ref().to_owned(),
|
|
];
|
|
|
|
let rotated = rotate(&sample);
|
|
|
|
let correct: Vec<Vec<u8>> = vec![
|
|
b"# #".as_ref().to_owned(),
|
|
b" #".as_ref().to_owned(),
|
|
b"###".as_ref().to_owned(),
|
|
];
|
|
|
|
assert_eq!(correct, rotated);
|
|
}
|
|
}
|