aoc2023/18/src/main.rs

305 lines
8.2 KiB
Rust

use itertools::Itertools;
use std::collections::LinkedList;
use std::fs::File;
use std::io::{BufRead, BufReader, Lines};
use std::time::Instant;
// BOILERPLATE
type InputIter = Lines<BufReader<File>>;
fn get_input() -> InputIter {
let f = File::open("input").unwrap();
let br = BufReader::new(f);
br.lines()
}
fn main() {
let start = Instant::now();
let ans1 = problem1(get_input());
let duration = start.elapsed();
println!("Problem 1 solution: {} [{}s]", ans1, duration.as_secs_f64());
let start = Instant::now();
let ans2 = problem2(get_input());
let duration = start.elapsed();
println!("Problem 2 solution: {} [{}s]", ans2, duration.as_secs_f64());
}
// DATA
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
enum Direction {
Left,
Right,
Up,
Down,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
enum Turn {
LeftNinety,
RightNinety,
OneEighty,
None,
}
impl Direction {
const fn all() -> &'static [Self; 4] {
&[Direction::Left, Direction::Right, Direction::Up, Direction::Down]
}
const fn opposite(&self) -> Self {
match self {
Direction::Left => Direction::Right,
Direction::Right => Direction::Left,
Direction::Up => Direction::Down,
Direction::Down => Direction::Up,
}
}
const fn offset(&self) -> (isize, isize) {
match self {
Direction::Left => (-1, 0),
Direction::Right => (1, 0),
Direction::Up => (0, -1),
Direction::Down => (0, 1),
}
}
fn turn_kind(&self, next_dir: Direction) -> Turn {
if *self == next_dir {
Turn::None
} else if self.opposite() == next_dir {
Turn::OneEighty
} else {
match self {
Direction::Left if next_dir == Direction::Up => Turn::RightNinety,
Direction::Left if next_dir == Direction::Down => Turn::LeftNinety,
Direction::Right if next_dir == Direction::Up => Turn::LeftNinety,
Direction::Right if next_dir == Direction::Down => Turn::RightNinety,
Direction::Up if next_dir == Direction::Left => Turn::LeftNinety,
Direction::Up if next_dir == Direction::Right => Turn::RightNinety,
Direction::Down if next_dir == Direction::Right => Turn::LeftNinety,
Direction::Down if next_dir == Direction::Left => Turn::RightNinety,
_ => unreachable!(),
}
}
}
}
impl From<&str> for Direction {
fn from(s: &str) -> Self {
match s {
"L" => Direction::Left,
"R" => Direction::Right,
"U" => Direction::Up,
"D" => Direction::Down,
s => panic!("{} is not a valid direction", s),
}
}
}
impl From<&Direction> for char {
fn from(dir: &Direction) -> Self {
match dir {
Direction::Left => '←',
Direction::Right => '→',
Direction::Up => '↑',
Direction::Down => '↓',
}
}
}
#[derive(Debug, Clone)]
struct DigInstruction {
dir: Direction,
count: usize,
color: String,
}
impl From<&str> for DigInstruction {
fn from(s: &str) -> Self {
let mut parts = s.split_ascii_whitespace();
let (dir, count, color) = (
parts.next().unwrap(),
parts.next().unwrap(),
parts.next().unwrap().chars().skip(2).take(6).collect::<String>(),
);
Self {
dir: dir.into(),
count: count.parse().unwrap(),
color: color.into(),
}
}
}
impl DigInstruction {
fn part2_transform(&mut self) {
let (distance_s, direction_s) = self.color.split_at(5);
self.count = usize::from_str_radix(distance_s, 16).unwrap();
self.dir = match direction_s {
"0" => Direction::Right,
"1" => Direction::Down,
"2" => Direction::Left,
"3" => Direction::Up,
s => panic!("`{}` is not a valid direction code", s),
};
}
}
#[derive(Debug)]
struct DigPlan {
instructions: Vec<DigInstruction>,
}
impl DigPlan {
fn part2_transform(&mut self) {
for i in &mut self.instructions {
i.part2_transform();
}
}
}
#[derive(Debug, Clone)]
struct DigTile {
position: Position,
}
impl Default for DigTile {
fn default() -> Self {
Self { position: (0, 0) }
}
}
type Position = (isize, isize);
#[derive(Debug)]
struct DigHole {
tiles_loop: LinkedList<DigTile>,
area: u64,
}
// determinant of positions p1 and p2
fn det(p1: Position, p2: Position) -> i64 {
((p1.0 * p2.1) - (p1.1 * p2.0)) as i64
}
impl DigHole {
fn new() -> Self {
DigHole {
tiles_loop: LinkedList::new(),
area: 0,
}
}
fn pos_offset_n(&self, pos: Position, offset: (isize, isize), n: usize) -> Position {
(pos.0 + offset.0 * n as isize, pos.1 + offset.1 * n as isize)
}
fn run_plan(&mut self, plan: &DigPlan) {
let mut cur_pos = (0, 0);
self.tiles_loop.push_back(DigTile { position: cur_pos });
let mut move_offset;
for (idx, i) in plan.instructions.iter().enumerate() {
let prev_instruction = if idx > 0 {
&plan.instructions[idx - 1]
} else {
&plan.instructions[plan.instructions.len() - 1]
};
let Some(next_instruction) = plan.instructions.get(idx + 1).or(Some(&plan.instructions[0])) else {
panic!()
};
let cur_turn = prev_instruction.dir.turn_kind(i.dir);
let next_turn = i.dir.turn_kind(next_instruction.dir);
// Point needs to live on the 'outside' corner of the character. to achieve this we need to offset the move
// by the following. Found this empirically but there's probably some mathematical principle behind it...
move_offset = match (cur_turn, next_turn) {
(Turn::RightNinety, Turn::RightNinety) => 1,
(Turn::RightNinety, Turn::LeftNinety) => 0,
(Turn::LeftNinety, Turn::LeftNinety) => -1,
(Turn::LeftNinety, Turn::RightNinety) => 0,
t => panic!("turn {:?} not allowed here", t),
};
cur_pos = self.pos_offset_n(cur_pos, i.dir.offset(), (i.count as isize + move_offset) as usize);
self.tiles_loop.push_back(DigTile { position: cur_pos });
}
// Shoelace formula
// https://en.wikipedia.org/wiki/Shoelace_formula
let double_area: i64 = self
.tiles_loop
.iter()
.tuple_windows()
.map(|(a, b)| det(a.position, b.position))
.sum();
self.area = (double_area / 2).abs() as u64;
}
}
impl<T: BufRead> From<Lines<T>> for DigPlan {
fn from(lines: Lines<T>) -> Self {
Self {
instructions: lines.map(|line| DigInstruction::from(line.unwrap().as_str())).collect(),
}
}
}
// PROBLEM 1 solution
fn problem1<T: BufRead>(input: Lines<T>) -> u64 {
let plan = DigPlan::from(input);
let mut dig = DigHole::new();
dig.run_plan(&plan);
dig.area
}
// PROBLEM 2 solution
fn problem2<T: BufRead>(input: Lines<T>) -> u64 {
let mut plan = DigPlan::from(input);
let mut dig = DigHole::new();
plan.part2_transform();
dig.run_plan(&plan);
dig.area
}
#[cfg(test)]
mod tests {
use crate::*;
use std::io::Cursor;
const EXAMPLE: &str = &"R 6 (#70c710)
D 5 (#0dc571)
L 2 (#5713f0)
D 2 (#d2c081)
R 2 (#59c680)
D 2 (#411b91)
L 5 (#8ceee2)
U 2 (#caa173)
L 1 (#1b58a2)
U 2 (#caa171)
R 2 (#7807d2)
U 3 (#a77fa3)
L 2 (#015232)
U 2 (#7a21e3)";
const AREA16_SQUARE: &str = &"R 3 (#000000)
D 3 (#000000)
L 3 (#000000)
U 4 (#000000";
#[test]
fn area16_square() {
let c = Cursor::new(AREA16_SQUARE);
assert_eq!(problem1(c.lines()), 16);
}
#[test]
fn problem1_example() {
let c = Cursor::new(EXAMPLE);
assert_eq!(problem1(c.lines()), 62);
}
#[test]
fn problem2_example() {
let c = Cursor::new(EXAMPLE);
assert_eq!(problem2(c.lines()), 952408144115);
}
}