241 lines
6.1 KiB
Rust
241 lines
6.1 KiB
Rust
use ndarray::*;
|
|
|
|
|
|
use std::fmt::Display;
|
|
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());
|
|
}
|
|
|
|
// PARSE
|
|
|
|
#[derive(Debug)]
|
|
struct Platform {
|
|
matrix: Array2<char>,
|
|
width: usize,
|
|
height: usize,
|
|
}
|
|
|
|
impl<T: BufRead> From<Lines<T>> for Platform {
|
|
fn from(lines: Lines<T>) -> Self {
|
|
let rows: Vec<Vec<char>> = lines.map(|line| line.unwrap().chars().collect()).collect();
|
|
let width = rows[0].len();
|
|
let height = rows.len();
|
|
Self {
|
|
matrix: Array2::from_shape_vec(
|
|
(height, width),
|
|
rows.iter().flat_map(|row| row.iter().map(|c| *c)).collect(),
|
|
)
|
|
.unwrap(),
|
|
width,
|
|
height,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
enum Axis {
|
|
Rows,
|
|
Columns,
|
|
}
|
|
|
|
struct North;
|
|
struct South;
|
|
struct East;
|
|
struct West;
|
|
|
|
trait Direction {
|
|
const REVERSED: bool;
|
|
const AXIS: Axis;
|
|
const ROTATIONS: usize;
|
|
}
|
|
|
|
impl Direction for North {
|
|
const REVERSED: bool = false;
|
|
const AXIS: Axis = Axis::Columns;
|
|
const ROTATIONS: usize = 0;
|
|
}
|
|
impl Direction for South {
|
|
const REVERSED: bool = true;
|
|
const AXIS: Axis = Axis::Columns;
|
|
const ROTATIONS: usize = 2; // 180
|
|
}
|
|
impl Direction for East {
|
|
const REVERSED: bool = true;
|
|
const AXIS: Axis = Axis::Rows;
|
|
const ROTATIONS: usize = 1; // CCW
|
|
}
|
|
impl Direction for West {
|
|
const REVERSED: bool = false;
|
|
const AXIS: Axis = Axis::Rows;
|
|
const ROTATIONS: usize = 3; // CW
|
|
}
|
|
|
|
impl<'a> Platform {
|
|
fn roll<T: Direction>(&mut self) {
|
|
// Get a view into the rotated matrix with the target direction to the north
|
|
let mut view = self.matrix.view_mut();
|
|
|
|
match T::ROTATIONS % 4 {
|
|
0 => {}
|
|
1 => {
|
|
view.invert_axis(Axis(1));
|
|
view.swap_axes(0, 1);
|
|
}
|
|
2 => {
|
|
view.invert_axis(Axis(0));
|
|
view.invert_axis(Axis(1));
|
|
}
|
|
3 => {
|
|
view.swap_axes(0, 1);
|
|
view.invert_axis(Axis(1));
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
Self::roll_rocks(view);
|
|
}
|
|
fn roll_rocks(mut view: ArrayBase<ViewRepr<&mut char>, Dim<[usize; 2]>>) {
|
|
let axis_len = view.len_of(Axis(1));
|
|
for mut col in view.columns_mut() {
|
|
for inner_idx in 1..axis_len {
|
|
if col[inner_idx] == 'O' {
|
|
let lower_limit = (0..inner_idx).rev().find(|i| col[*i] == '#').unwrap_or(0);
|
|
if let Some(empty_pos) = (lower_limit..inner_idx).find(|i| col[*i] == '.') {
|
|
col.swap(inner_idx, empty_pos);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
fn score<T: Direction>(&self) -> u64 {
|
|
match T::AXIS {
|
|
Axis::Columns => self.score_columns::<T>(),
|
|
Axis::Rows => self.score_rows::<T>(),
|
|
}
|
|
}
|
|
fn row_or_column_score<T: Direction>(&self, idx: usize) -> usize {
|
|
if T::REVERSED {
|
|
idx + 1
|
|
} else {
|
|
match T::AXIS {
|
|
Axis::Rows => self.width - idx,
|
|
Axis::Columns => self.height - idx,
|
|
}
|
|
}
|
|
}
|
|
fn score_columns<T: Direction>(&self) -> u64 {
|
|
self.matrix
|
|
.rows()
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(idx, row)| {
|
|
row.iter().filter(|c| **c == 'O').count() * self.row_or_column_score::<T>(idx)
|
|
})
|
|
.sum::<usize>() as u64
|
|
}
|
|
fn roll_cycle(&mut self) {
|
|
self.roll::<North>();
|
|
self.roll::<West>();
|
|
self.roll::<South>();
|
|
self.roll::<East>();
|
|
}
|
|
|
|
// find the first loop, return the iteration count when we first saw it and when we saw it again
|
|
fn find_loop(&mut self) -> (usize, usize) {
|
|
let mut first_seen = Vec::new();
|
|
first_seen.push((self.matrix.clone(), 0));
|
|
let mut i = 0;
|
|
loop {
|
|
self.roll_cycle();
|
|
i += 1;
|
|
if let Some((_, first_idx)) = first_seen.iter().find(|(val, _)| *val == self.matrix) {
|
|
return (*first_idx, i);
|
|
} else {
|
|
first_seen.push((self.matrix.clone(), i));
|
|
}
|
|
}
|
|
}
|
|
fn score_rows<T: Direction>(&self) -> u64 {
|
|
unimplemented!()
|
|
}
|
|
}
|
|
|
|
impl Display for Platform {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
self.matrix.fmt(f)
|
|
}
|
|
}
|
|
|
|
// PROBLEM 1 solution
|
|
|
|
fn problem1<T: BufRead>(input: Lines<T>) -> u64 {
|
|
let mut p = Platform::from(input);
|
|
p.roll::<North>();
|
|
p.score::<North>()
|
|
}
|
|
|
|
// PROBLEM 2 solution
|
|
fn problem2<T: BufRead>(input: Lines<T>) -> u64 {
|
|
const ITERATIONS: usize = 1000000000;
|
|
let mut p = Platform::from(input);
|
|
let first_loop = p.find_loop();
|
|
let loop_length = first_loop.1 - first_loop.0;
|
|
let cycles_to_skip = ((ITERATIONS - first_loop.0) / loop_length) * loop_length;
|
|
let iterations_remaining = ITERATIONS - first_loop.0 - cycles_to_skip;
|
|
|
|
for _ in 0..iterations_remaining {
|
|
p.roll_cycle();
|
|
}
|
|
|
|
p.score::<North>()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::*;
|
|
use std::io::Cursor;
|
|
|
|
const EXAMPLE: &str = &"O....#....
|
|
O.OO#....#
|
|
.....##...
|
|
OO.#O....O
|
|
.O.....O#.
|
|
O.#..O.#.#
|
|
..O..#O..O
|
|
.......O..
|
|
#....###..
|
|
#OO..#....";
|
|
|
|
#[test]
|
|
fn problem1_example() {
|
|
let c = Cursor::new(EXAMPLE);
|
|
assert_eq!(problem1(c.lines()), 136);
|
|
}
|
|
|
|
#[test]
|
|
fn problem2_example() {
|
|
let c = Cursor::new(EXAMPLE);
|
|
assert_eq!(problem2(c.lines()), 64);
|
|
}
|
|
}
|