chore: refactor for cargo-aoc / codspeed

This commit is contained in:
2024-12-11 15:45:52 -08:00
parent 3bfde9fd9b
commit 11b29a4d57
44 changed files with 850 additions and 1305 deletions

90
src/day1.rs Normal file
View File

@ -0,0 +1,90 @@
use aoc_runner_derive::{aoc, aoc_generator};
use std::collections::HashMap;
use std::io::{BufRead, Lines};
#[aoc_generator(day1)]
pub fn get_input(input: &[u8]) -> Locations {
Locations::from(input.lines())
}
#[derive(Clone)]
pub struct Locations {
left: Vec<u64>,
right: Vec<u64>,
}
impl<T: BufRead> From<Lines<T>> for Locations {
fn from(input: Lines<T>) -> Self {
let mut left = Vec::new();
let mut right = Vec::new();
for line in input.map(|i| i.unwrap()) {
let parts: Vec<&str> = line.split_ascii_whitespace().collect();
left.push(parts[0].parse::<u64>().unwrap());
right.push(parts[1].parse::<u64>().unwrap());
}
Self { left, right }
}
}
impl Locations {
fn sort(&mut self) {
self.left.sort();
self.right.sort();
}
fn right_count(&self) -> HashMap<u64, u64> {
let mut right_count: HashMap<u64, u64> = HashMap::new();
for rval in &self.right {
right_count.insert(*rval, *right_count.get(rval).unwrap_or(&0) + 1);
}
right_count
}
}
// PROBLEM 1 solution
#[aoc(day1, part1)]
pub fn part1(locations: &Locations) -> u64 {
let mut locations = locations.clone();
locations.sort();
locations
.left
.iter()
.zip(locations.right)
.map(|(l, r)| u64::abs_diff(*l, r))
.sum()
}
// PROBLEM 2 solution
#[aoc(day1, part2)]
pub fn part2(locations: &Locations) -> u64 {
let right_count = locations.right_count();
locations
.left
.iter()
.map(|l| l * right_count.get(l).unwrap_or(&0))
.sum::<u64>()
}
#[cfg(test)]
mod tests {
use crate::day1::*;
const EXAMPLE: &[u8] = b"3 4
4 3
2 5
1 3
3 9
3 3";
#[test]
fn part1_example() {
let input = get_input(EXAMPLE);
assert_eq!(part1(&input), 11);
}
#[test]
fn part2_example() {
let input = get_input(EXAMPLE);
assert_eq!(part2(&input), 31);
}
}

111
src/day10.rs Normal file
View File

@ -0,0 +1,111 @@
use aoc_runner_derive::{aoc, aoc_generator};
use grid::Grid;
use itertools::Itertools;
use std::io::{BufRead, Lines};
#[aoc_generator(day10)]
pub fn get_input(input: &[u8]) -> TrailMap {
TrailMap::from(input.lines())
}
pub struct TrailMap {
map: Grid<u8>,
}
impl<T: BufRead> From<Lines<T>> for TrailMap {
fn from(input: Lines<T>) -> Self {
Self { map: input.into() }
}
}
impl TrailMap {
fn trailheads(&self) -> Vec<(i64, i64)> {
self.map
.data
.iter()
.enumerate()
.filter(|(_, v)| **v == b'0')
.map(|(i, _v)| self.map.coord(i as i64).unwrap())
.collect_vec()
}
fn count_reachable_from(&self, pos: &(i64, i64), needle: u8, visited: &mut Grid<bool>) -> u64 {
if visited.get(pos.0, pos.1) == Some(true) {
return 0;
} else {
visited.set(pos.0, pos.1, true);
}
let our_val = self.map.get(pos.0, pos.1).unwrap();
if our_val == needle {
return 1;
}
// adjacents that are +1
[(-1, 0), (1, 0), (0, -1), (0, 1)] // left, right, up, down
.iter()
.map(|(x_ofs, y_ofs)| (pos.0 + x_ofs, pos.1 + y_ofs)) // get target position
.map(|(x, y)| ((x, y), self.map.get(x, y))) // get value at that position
.filter(|(_, val)| *val == Some(our_val + 1)) // only interested if it's our value + 1
.map(|(pos, _)| pos) // discard the value
.map(|pos| self.count_reachable_from(&pos, needle, visited))
.sum()
}
fn count_paths_to(&self, pos: &(i64, i64), needle: u8) -> u64 {
let our_val = self.map.get(pos.0, pos.1).unwrap();
if our_val == needle {
return 1;
}
[(-1, 0), (1, 0), (0, -1), (0, 1)] // left, right, up, down
.iter()
.map(|(x_ofs, y_ofs)| (pos.0 + x_ofs, pos.1 + y_ofs)) // get target position
.map(|(x, y)| ((x, y), self.map.get(x, y))) // get value at that position
.filter(|(_, val)| *val == Some(our_val + 1)) // only interested if it's our value + 1
.map(|(pos, _)| pos) // discard the value
.map(|mov| self.count_paths_to(&mov, needle))
.sum::<u64>()
}
}
// PROBLEM 1 solution
#[aoc(day10, part1)]
pub fn part1(map: &TrailMap) -> u64 {
map.trailheads()
.iter()
.map(|pos| {
let mut visited = Grid::with_shape(map.map.width(), map.map.height(), false);
map.count_reachable_from(pos, b'9', &mut visited)
})
.sum()
}
// PROBLEM 2 solution
#[aoc(day10, part2)]
pub fn part2(map: &TrailMap) -> u64 {
map.trailheads()
.iter()
.map(|pos| map.count_paths_to(pos, b'9'))
.sum::<u64>()
}
#[cfg(test)]
mod tests {
use crate::day10::*;
const EXAMPLE: &[u8] = b"89010123
78121874
87430965
96549874
45678903
32019012
01329801
10456732";
#[test]
fn part1_example() {
assert_eq!(part1(&get_input(EXAMPLE)), 36);
}
#[test]
fn part2_example() {
assert_eq!(part2(&get_input(EXAMPLE)), 81);
}
}

102
src/day11.rs Normal file
View File

@ -0,0 +1,102 @@
use aoc_runner_derive::{aoc, aoc_generator};
use itertools::Itertools;
use std::collections::HashMap;
use std::iter::repeat;
type IntType = u64;
type CacheType = HashMap<Stone, IntType>;
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
struct Stone(IntType);
struct Stones(Vec<Stone>);
enum BlinkResult {
One(Stone),
Two(Stone, Stone),
}
impl From<&str> for Stones {
fn from(input: &str) -> Self {
Stones(
input
.split_ascii_whitespace()
.map(|v| Stone(v.parse().unwrap()))
.collect_vec(),
)
}
}
#[aoc_generator(day11)]
fn parse(input: &str) -> Stones {
Stones::from(input)
}
impl Stone {
fn blink_once(&self) -> BlinkResult {
let n_digits = if self.0 == 0 { 1 } else { self.0.ilog10() + 1 };
if self.0 == 0 {
BlinkResult::One(Stone(1))
} else if n_digits % 2 == 0 {
let split_factor = (10 as IntType).pow(n_digits / 2);
let parts = (self.0 / split_factor, self.0 % split_factor);
BlinkResult::Two(Stone(parts.0), Stone(parts.1))
} else {
BlinkResult::One(Stone(&self.0 * 2024))
}
}
}
fn count_blinks(stone: &Stone, blink: usize, cache: &mut Vec<CacheType>) -> IntType {
if cache[blink].contains_key(&stone) {
return cache[blink][&stone].clone();
}
let stones = stone.blink_once();
let result = if blink == 0 {
match stones {
BlinkResult::One(_) => 1,
BlinkResult::Two(_, _) => 2,
}
} else {
match stones {
BlinkResult::One(s) => count_blinks(&s, blink - 1, cache),
BlinkResult::Two(s1, s2) => count_blinks(&s1, blink - 1, cache) + count_blinks(&s2, blink - 1, cache),
}
};
cache[blink].insert(stone.clone(), result);
cache[blink][&stone].clone()
}
fn blink_stones(stones: &Stones, blinks: usize) -> IntType {
let mut cache = Vec::from_iter(repeat(CacheType::new()).take(blinks));
stones
.0
.iter()
.map(|stone| count_blinks(stone, blinks - 1, &mut cache))
.sum()
}
#[aoc(day11, part1)]
fn part1(stones: &Stones) -> IntType {
blink_stones(stones, 25)
}
#[aoc(day11, part2)]
fn part2(stones: &Stones) -> IntType {
blink_stones(stones, 75)
}
#[cfg(test)]
mod tests {
use super::*;
pub const EXAMPLE: &str = &"125 17";
#[test]
fn part1_example() {
assert_eq!(part1(&parse(EXAMPLE)), 55312);
}
#[test]
fn part2_example() {
assert_eq!(part2(&parse(EXAMPLE)), 65601038650482);
}
}

105
src/day2.rs Normal file
View File

@ -0,0 +1,105 @@
use aoc_runner_derive::{aoc, aoc_generator};
use std::io::{BufRead, Lines};
#[aoc_generator(day2)]
pub fn get_input(input: &[u8]) -> Reports {
Reports::from(input.lines())
}
#[derive(Debug)]
pub struct Reports {
reports: Vec<Vec<u64>>,
}
impl<T: BufRead> From<Lines<T>> for Reports {
fn from(lines: Lines<T>) -> Self {
let mut reports = Vec::new();
for line in lines.map(|i| i.unwrap()) {
reports.push(
line.split_ascii_whitespace()
.map(|record| record.parse::<u64>().unwrap())
.collect(),
)
}
Reports { reports }
}
}
impl Reports {
fn is_safe(report: &[u64]) -> bool {
let mut ascending: bool = true;
let mut descending: bool = true;
for (a, b) in report.iter().zip(report.iter().skip(1)) {
if a > b {
ascending = false
}
if a < b {
descending = false;
}
let ad = a.abs_diff(*b);
if !(1..=3).contains(&ad) || (!ascending && !descending) {
return false;
};
}
true
}
fn count_safe(&self) -> u64 {
self.reports.iter().filter(|report| Self::is_safe(report)).count() as u64
}
fn is_dumb_dampened_safe(report: &[u64]) -> bool {
if Self::is_safe(report) {
return true;
}
for i in 0..report.len() {
let mut new_vec = report.to_owned();
new_vec.remove(i);
if Self::is_safe(&new_vec) {
return true;
}
}
false
}
fn dampened_count_safe(&self) -> u64 {
self.reports
.iter()
.filter(|report| Self::is_dumb_dampened_safe(report))
.count() as u64
}
}
// PROBLEM 1 solution
#[aoc(day2, part1)]
pub fn part1(input: &Reports) -> u64 {
input.count_safe()
}
// PROBLEM 2 solution
#[aoc(day2, part2)]
pub fn part2(input: &Reports) -> u64 {
input.dampened_count_safe()
}
#[cfg(test)]
mod tests {
use crate::day2::*;
const EXAMPLE: &[u8] = b"7 6 4 2 1
1 2 7 8 9
9 7 6 2 1
1 3 2 4 5
8 6 4 4 1
1 3 6 7 9";
#[test]
fn part1_example() {
let input = get_input(EXAMPLE);
println!("{:?}", input);
assert_eq!(part1(&input), 2);
}
#[test]
fn part2_example() {
let input = get_input(EXAMPLE);
assert_eq!(part2(&input), 4);
}
}

66
src/day3.rs Normal file
View File

@ -0,0 +1,66 @@
use aoc_runner_derive::{aoc, aoc_generator};
use regex::bytes::Regex;
use std::io::BufRead;
#[aoc_generator(day3)]
pub fn get_input(input: &[u8]) -> Vec<String> {
input.lines().map(|l| l.unwrap()).collect()
}
// PROBLEM 1 solution
#[aoc(day3, part1)]
pub fn part1(input: &Vec<String>) -> u64 {
let mut sum = 0u64;
let re = Regex::new(r"(?-u)mul\((\d+),(\d+)\)").unwrap();
for line in input {
let line = line.as_bytes();
for m in re.captures_iter(line) {
sum += std::str::from_utf8(&m[1]).unwrap().parse::<u64>().unwrap()
* std::str::from_utf8(&m[2]).unwrap().parse::<u64>().unwrap();
}
}
sum
}
// PROBLEM 2 solution
#[aoc(day3, part2)]
pub fn part2(input: &Vec<String>) -> u64 {
let mut sum = 0u64;
let mut do_mul = true;
let re = Regex::new(r"(?-u)(do\(\)|don't\(\)|mul\((\d+),(\d+)\))").unwrap();
for line in input {
let line = line.as_bytes();
for m in re.captures_iter(line) {
match std::str::from_utf8(&m[1]).unwrap() {
"do()" => do_mul = true,
"don't()" => do_mul = false,
_ if do_mul => {
sum += std::str::from_utf8(&m[2]).unwrap().parse::<u64>().unwrap()
* std::str::from_utf8(&m[3]).unwrap().parse::<u64>().unwrap()
}
_ => {}
}
}
}
sum
}
#[cfg(test)]
mod tests {
use crate::day3::*;
const EXAMPLE1: &[u8] = b"xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))";
const EXAMPLE2: &[u8] = b"xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))";
#[test]
fn part1_example() {
let input = get_input(EXAMPLE1);
assert_eq!(part1(&input), 161);
}
#[test]
fn part2_example() {
let input = get_input(EXAMPLE2);
assert_eq!(part2(&input), 48);
}
}

145
src/day4.rs Normal file
View File

@ -0,0 +1,145 @@
use aoc_runner_derive::{aoc, aoc_generator};
use std::io::{BufRead, Lines};
#[aoc_generator(day4)]
pub fn get_input(input: &[u8]) -> WordSearch {
WordSearch::from(input.lines())
}
pub struct WordSearch {
rows: Vec<String>,
}
impl<T: BufRead> From<Lines<T>> for WordSearch {
fn from(input: Lines<T>) -> Self {
let mut rows = Vec::new();
for line in input.map(|i| i.unwrap()) {
rows.push(line);
}
Self { rows }
}
}
impl WordSearch {
fn count_occurences(haystack: &str, needle: &str) -> u64 {
let mut count = 0;
for start in 0..haystack.len() - needle.len() + 1 {
if &haystack[start..start + needle.len()] == needle {
count += 1;
}
}
count
}
fn count_forward(&self, needle: &str) -> u64 {
let mut count = 0;
for row in &self.rows {
count += Self::count_occurences(row, needle)
}
count
}
fn count_vertical(&self, needle: &str) -> u64 {
let mut count = 0;
for col in 0..self.rows[0].len() {
let s: String = self.rows.iter().map(|row| row.as_bytes()[col] as char).collect();
count += Self::count_occurences(&s, needle)
}
count
}
fn count_diagonal(&self, needle: &str) -> u64 {
let width = self.rows[0].len();
let height = self.rows.len();
let mut count = 0;
for x in 0..width {
for y in 0..height {
// check down-right
if x <= width - needle.len() && y <= height - needle.len() && (0..needle.len()).all(|i| self.get(x + i, y + i) == needle.as_bytes()[i].into()) {
count += 1
}
// check down-left
if x >= needle.len() - 1 && y <= height - needle.len() && (0..needle.len()).all(|i| self.get(x - i, y + i) == needle.as_bytes()[i].into()) {
count += 1
}
}
}
count
}
fn get(&self, x: usize, y: usize) -> char {
self.rows[y].as_bytes()[x].into()
}
fn count_x_mas(&self) -> u64 {
// M.M M.S S.M S.S
// .A. .A. .A. .A.
// S.S M.S S.M M.M
let searches: [[char; 5]; 4] =
["MMASS", "MSAMS", "SMASM", "SSAMM"].map(|s| s.chars().collect::<Vec<char>>().try_into().unwrap());
let width = self.rows[0].len();
let height = self.rows.len();
let mut count = 0;
for x in 0..width - 2 {
for y in 0..height - 2 {
let s = [
self.get(x, y),
self.get(x + 2, y),
self.get(x + 1, y + 1),
self.get(x, y + 2),
self.get(x + 2, y + 2),
];
for needle in &searches {
if needle == &s {
count += 1
}
}
}
}
count
}
}
// PROBLEM 1 solution
#[aoc(day4, part1)]
pub fn part1(ws: &WordSearch) -> u64 {
let needle = "XMAS";
let needle_rev: String = needle.chars().rev().collect();
ws.count_forward(needle)
+ ws.count_forward(&needle_rev)
+ ws.count_vertical(needle)
+ ws.count_vertical(&needle_rev)
+ ws.count_diagonal(needle)
+ ws.count_diagonal(&needle_rev)
}
// PROBLEM 2 solution
#[aoc(day4, part2)]
pub fn part2(ws: &WordSearch) -> u64 {
ws.count_x_mas()
}
#[cfg(test)]
mod tests {
use crate::day4::*;
const EXAMPLE: &[u8] = b"MMMSXXMASM
MSAMXMSMSA
AMXSXMAAMM
MSAMASMSMX
XMASAMXAMM
XXAMMXXAMA
SMSMSASXSS
SAXAMASAAA
MAMMMXMMMM
MXMXAXMASX";
#[test]
fn part1_example() {
assert_eq!(part1(&get_input(EXAMPLE)), 18);
}
#[test]
fn part2_example() {
assert_eq!(part2(&get_input(EXAMPLE)), 9);
}
}

187
src/day5.rs Normal file
View File

@ -0,0 +1,187 @@
use aoc_runner_derive::{aoc, aoc_generator};
use std::fmt::Debug;
use std::io::BufRead;
#[aoc_generator(day5)]
pub fn get_input(input: &[u8]) -> (OrderingRules, Vec<Vec<u64>>) {
let mut lines = input.lines();
let rules = OrderingRules {
rules: lines
.by_ref()
.map_while(|l| match l {
Ok(line) if !line.is_empty() => Some(Box::new(BeforeRule::from(line)) as _),
_ => None,
})
.collect(),
};
let updates: Vec<Vec<u64>> = lines
.by_ref()
.map(|l| l.unwrap().split(',').map(|n| n.parse::<u64>().unwrap()).collect())
.collect();
(rules, updates)
}
trait Rule: Debug {
fn check(&self, pages: &[u64]) -> bool;
fn fail_pos(&self, pages: &[u64]) -> Option<(usize, usize)>;
}
#[derive(Debug)]
struct BeforeRule {
a: u64,
b: u64,
}
impl Rule for BeforeRule {
fn check(&self, pages: &[u64]) -> bool {
let mut seen_a = false;
let mut seen_b = false;
for page in pages {
if *page == self.a {
if seen_b {
return false;
}
seen_a = true;
} else if *page == self.b {
if seen_a {
return true;
}
seen_b = true
}
}
true
}
fn fail_pos(&self, pages: &[u64]) -> Option<(usize, usize)> {
let mut a_pos = None;
let mut b_pos = None;
for (pos, page) in pages.iter().enumerate() {
if *page == self.a {
if let Some(b_pos) = b_pos {
return Some((b_pos, pos));
}
a_pos = Some(pos);
} else if *page == self.b {
if a_pos.is_some() {
return None;
}
b_pos = Some(pos);
}
}
None
}
}
impl From<String> for BeforeRule {
fn from(line: String) -> BeforeRule {
let nums: Vec<_> = line.splitn(2, '|').map(|s| s.parse::<u64>().unwrap()).collect();
BeforeRule { a: nums[0], b: nums[1] }
}
}
#[derive(Debug)]
pub struct OrderingRules {
rules: Vec<Box<dyn Rule>>,
}
impl OrderingRules {
fn check(&self, pages: &[u64]) -> bool {
self.rules.iter().all(|p| p.check(pages))
}
fn fail_pos(&self, pages: &[u64]) -> Option<(usize, usize)> {
for rule in &self.rules {
let fail_pos = rule.fail_pos(pages);
if fail_pos.is_some() {
return fail_pos;
}
}
None
}
}
// impl<'a, T: Iterator<Item = &'a str>> From<&mut T> for OrderingRules {
// fn from(input: &mut T) -> Self {
// let mut rules = Vec::new();
// for line in input {
// rules.push(line.into())
// }
// Self { rules }
// }
// }
// PROBLEM 1 solution
#[aoc(day5, part1)]
pub fn part1((rules, updates): &(OrderingRules, Vec<Vec<u64>>)) -> u64 {
let mut res = 0;
for update in updates {
if rules.check(update) {
res += update[update.len() / 2]
}
}
res
}
// PROBLEM 2 solution
#[aoc(day5, part2)]
pub fn part2((rules, updates): &(OrderingRules, Vec<Vec<u64>>)) -> u64 {
let mut updates = updates.clone();
let mut res = 0;
for update in updates.as_mut_slice() {
let mut did_swaps = false;
while let Some((a, b)) = rules.fail_pos(update) {
did_swaps = true;
update.swap(a, b);
}
if did_swaps {
if !rules.check(update) {
panic!("update still fails after swaps")
}
res += update[update.len() / 2];
}
}
res
}
#[cfg(test)]
mod tests {
use crate::day5::*;
const EXAMPLE: &[u8] = b"47|53
97|13
97|61
97|47
75|29
61|13
75|53
29|13
97|29
53|29
61|53
97|53
61|29
47|13
75|47
97|75
47|61
75|61
47|29
75|13
53|13
75,47,61,53,29
97,61,53,29,13
75,29,13
75,97,47,61,53
61,13,29
97,13,75,29,47";
#[test]
fn part1_example() {
assert_eq!(part1(&get_input(EXAMPLE)), 143);
}
#[test]
fn part2_example() {
assert_eq!(part2(&get_input(EXAMPLE)), 123);
}
}

229
src/day6.rs Normal file
View File

@ -0,0 +1,229 @@
use aoc_runner_derive::{aoc, aoc_generator};
use bitflags::bitflags;
use std::fmt;
use std::io::{BufRead, Lines};
use std::ops::BitAnd;
use grid::Grid;
#[aoc_generator(day6)]
pub fn get_input(input: &[u8]) -> Map {
Map::from(input.lines())
}
#[repr(u8)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
enum FacingDirection {
Up = b'^',
Down = b'v',
Left = b'<',
Right = b'>',
}
impl FacingDirection {
fn next(&self) -> FacingDirection {
match self {
FacingDirection::Up => FacingDirection::Right,
FacingDirection::Down => FacingDirection::Left,
FacingDirection::Left => FacingDirection::Up,
FacingDirection::Right => FacingDirection::Down,
}
}
fn pos_ofs(&self, pos: (i64, i64)) -> (i64, i64) {
match self {
FacingDirection::Up => (pos.0, pos.1 + -1),
FacingDirection::Down => (pos.0, pos.1 + 1),
FacingDirection::Left => (pos.0 + -1, pos.1),
FacingDirection::Right => (pos.0 + 1, pos.1),
}
}
}
enum StepOutcome {
LeftMap,
LoopFound,
Continue,
}
enum RunOutcome {
LeftMap,
LoopFound,
}
bitflags! {
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub struct DirectionSet: u8 {
const Up = 1;
const Down = 2;
const Left = 4;
const Right = 8;
}
}
impl From<FacingDirection> for DirectionSet {
fn from(value: FacingDirection) -> Self {
match value {
FacingDirection::Up => DirectionSet::Up,
FacingDirection::Down => DirectionSet::Down,
FacingDirection::Left => DirectionSet::Left,
FacingDirection::Right => DirectionSet::Right,
}
}
}
impl fmt::Display for DirectionSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl BitAnd<FacingDirection> for DirectionSet {
type Output = DirectionSet;
fn bitand(self, rhs: FacingDirection) -> Self::Output {
self & DirectionSet::from(rhs)
}
}
#[derive(Clone)]
pub struct Map {
grid: Grid<u8>,
visited_from: Grid<DirectionSet>,
guard_facing: FacingDirection,
guard_pos: (i64, i64),
path: Vec<((i64, i64), FacingDirection)>,
}
impl<T: BufRead> From<Lines<T>> for Map {
fn from(input: Lines<T>) -> Self {
let grid = Grid::from(input);
let mut visited_from: Grid<DirectionSet> = Grid::new(grid.width() as i64);
visited_from.data.resize(grid.data.len(), DirectionSet::empty());
let guard_pos = grid.find(b'^').expect("Guard not found");
let guard_facing = FacingDirection::Up;
Self {
grid,
guard_pos,
guard_facing,
visited_from,
path: Vec::new(),
}
}
}
impl Map {
fn look(&self, dir: &FacingDirection) -> Option<u8> {
match dir {
FacingDirection::Up => self.grid.get(self.guard_pos.0, self.guard_pos.1 - 1),
FacingDirection::Down => self.grid.get(self.guard_pos.0, self.guard_pos.1 + 1),
FacingDirection::Left => self.grid.get(self.guard_pos.0 - 1, self.guard_pos.1),
FacingDirection::Right => self.grid.get(self.guard_pos.0 + 1, self.guard_pos.1),
}
}
/// Move one step in the facing direction, return if we are still inside the bounds
fn step_guard<const RECORD_PATH: bool>(&mut self) -> StepOutcome {
let new_pos = self.guard_facing.pos_ofs(self.guard_pos);
if self
.visited_from
.get(new_pos.0, new_pos.1)
.is_some_and(|dirs| dirs.contains(self.guard_facing.into()))
{
return StepOutcome::LoopFound;
}
if self.grid.set(new_pos.0, new_pos.1, b'X') {
if RECORD_PATH {
self.path.push((new_pos, self.guard_facing));
}
self.visited_from.set(
new_pos.0,
new_pos.1,
self.visited_from.get(new_pos.0, new_pos.1).unwrap() | self.guard_facing.into(),
);
self.guard_pos = new_pos;
StepOutcome::Continue
} else {
StepOutcome::LeftMap
}
}
fn run_guard<const RECORD_PATH: bool>(&mut self) -> RunOutcome {
while let Some(val) = self.look(&self.guard_facing) {
match val {
b'#' => {
// obstacle, turn right
self.guard_facing = self.guard_facing.next();
}
_ => match self.step_guard::<RECORD_PATH>() {
StepOutcome::LeftMap => return RunOutcome::LeftMap,
StepOutcome::LoopFound => return RunOutcome::LoopFound,
StepOutcome::Continue => {}
},
}
}
RunOutcome::LeftMap
}
}
// PROBLEM 1 solution
#[aoc(day6, part1)]
pub fn part1(map: &Map) -> u64 {
let mut map = map.clone();
map.run_guard::<false>();
(map.grid.count(b'X') + map.grid.count(b'-') + map.grid.count(b'|') + map.grid.count(b'^')) as u64
}
// PROBLEM 2 solution
#[aoc(day6, part2)]
pub fn part2(input_map: &Map) -> u64 {
// Use the solution from problem 1 to reduce the number of positions where obstacle placement will change the path
let mut path_map = input_map.clone();
path_map.run_guard::<true>();
let mut tested_position: grid::Grid<bool> = grid::Grid::new(path_map.grid.width() as i64);
tested_position.data.resize(path_map.grid.data.len(), false);
let mut loop_count = 0u64;
let mut last_posdir = (input_map.guard_pos, input_map.guard_facing);
for ((x, y), dir) in path_map.path.iter() {
if !tested_position.get(*x, *y).unwrap() {
tested_position.set(*x, *y, true);
let mut test_map = input_map.clone();
test_map.grid.set(*x, *y, b'#');
test_map.guard_pos = last_posdir.0;
test_map.guard_facing = last_posdir.1;
if let RunOutcome::LoopFound = test_map.run_guard::<false>() {
loop_count += 1
}
last_posdir = ((*x, *y), *dir);
}
}
loop_count
}
#[cfg(test)]
mod tests {
use crate::day6::*;
const EXAMPLE: &[u8] = b"....#.....
.........#
..........
..#.......
.......#..
..........
.#..^.....
........#.
#.........
......#...";
#[test]
fn part1_example() {
assert_eq!(part1(&get_input(EXAMPLE)), 41);
}
#[test]
fn part2_example() {
assert_eq!(part2(&get_input(EXAMPLE)), 6);
}
}

139
src/day7.rs Normal file
View File

@ -0,0 +1,139 @@
use aoc_runner_derive::{aoc, aoc_generator};
use itertools::Itertools;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use std::io::{BufRead, Lines};
use thread_local::ThreadLocal;
#[aoc_generator(day7)]
pub fn get_input(input: &[u8]) -> Calibrations {
Calibrations::from(input.lines())
}
#[derive(Debug, Clone)]
struct Calibration {
result: u64,
numbers: Vec<u64>,
}
impl From<&str> for Calibration {
fn from(value: &str) -> Self {
let (result, rest) = value.split_once(':').unwrap();
Self {
result: result.parse().unwrap(),
numbers: rest.split_ascii_whitespace().map(|s| s.parse().unwrap()).collect(),
}
}
}
#[derive(Debug)]
pub struct Calibrations {
cals: Vec<Calibration>,
longest_cal: usize,
}
#[derive(Debug, Copy, Clone)]
enum Operator {
Add,
Multiply,
Concatenate,
}
impl Operator {
fn exec(&self, a: u64, b: u64) -> u64 {
match self {
Operator::Add => a + b,
Operator::Multiply => a * b,
Operator::Concatenate => u64::pow(10, u64::ilog10(b) + 1) * a + b,
}
}
}
impl<T: BufRead> From<Lines<T>> for Calibrations {
fn from(input: Lines<T>) -> Self {
let mut cals = Vec::new();
let mut longest_cal = 0;
for line in input.map(|l| l.unwrap()) {
let cal: Calibration = line.as_str().into();
longest_cal = std::cmp::max(longest_cal, cal.numbers.len());
cals.push(cal);
}
Self { cals, longest_cal }
}
}
impl Calibrations {
fn make_operator_sets(operators: &[Operator], n_opers: usize) -> Vec<Vec<Vec<Operator>>> {
(0..n_opers)
.map(|k| {
std::iter::repeat_n(operators.iter().copied(), k)
.multi_cartesian_product()
.collect()
})
.collect()
}
fn check_oper_set(cal: &Calibration, oper_set: &[Operator]) -> bool {
let accum = oper_set
.iter()
.zip(cal.numbers.iter().skip(1))
.fold(cal.numbers[0], |accum, (oper, val)| oper.exec(accum, *val));
accum == cal.result
}
fn possible(&self, operators: &[Operator]) -> u64 {
let operator_sets = Calibrations::make_operator_sets(operators, self.longest_cal);
self.cals
.par_iter()
.map(|cal| {
let n_opers = cal.numbers.len() - 1;
let tl = ThreadLocal::new();
if operator_sets[n_opers]
.par_iter()
.find_any(|oper_set| Self::check_oper_set(cal, oper_set))
.is_some()
{
let cal_local = tl.get_or(|| cal.clone());
return cal_local.result;
}
0
})
.sum()
}
}
// PROBLEM 1 solution
#[aoc(day7, part1)]
pub fn part1(cals: &Calibrations) -> u64 {
let operators = [Operator::Add, Operator::Multiply];
cals.possible(&operators)
}
// PROBLEM 2 solution
#[aoc(day7, part2)]
pub fn part2(cals: &Calibrations) -> u64 {
let operators = [Operator::Add, Operator::Multiply, Operator::Concatenate];
cals.possible(&operators)
}
#[cfg(test)]
mod tests {
use crate::day7::*;
const EXAMPLE: &[u8] = b"190: 10 19
3267: 81 40 27
83: 17 5
156: 15 6
7290: 6 8 6 15
161011: 16 10 13
192: 17 8 14
21037: 9 7 18 13
292: 11 6 16 20";
#[test]
fn part1_example() {
assert_eq!(part1(&get_input(EXAMPLE)), 3749);
}
#[test]
fn part2_example() {
assert_eq!(part2(&get_input(EXAMPLE)), 11387);
}
}

97
src/day8.rs Normal file
View File

@ -0,0 +1,97 @@
use aoc_runner_derive::{aoc, aoc_generator};
use grid::Grid;
use itertools::Itertools;
use std::collections::HashSet;
use std::io::{BufRead, Lines};
#[aoc_generator(day8)]
pub fn get_input(input: &[u8]) -> AntennaMap {
AntennaMap::from(input.lines())
}
pub struct AntennaMap {
map: Grid<u8>,
}
impl<T: BufRead> From<Lines<T>> for AntennaMap {
fn from(input: Lines<T>) -> Self {
Self { map: Grid::from(input) }
}
}
impl AntennaMap {
fn find_antinodes(&self, start: usize, reps: Option<usize>) -> Grid<bool> {
let mut antinodes = Grid::with_shape(self.map.width(), self.map.height(), false);
// find the unique frequencies in a dumb way
// NOTE: The dumb way is faster than the slightly-smarter ways I tried
let freq_set: HashSet<&u8> = HashSet::from_iter(self.map.data.iter().filter(|c| **c != b'.'));
// for each unique frequency, get all the pairs' positions
for freq in freq_set {
for pair in self
.map
.data
.iter()
.enumerate()
.filter(|(_, c)| *c == freq)
.map(|(i, _)| self.map.coord(i as i64).unwrap())
.permutations(2)
{
// permutations generates both pairs, ie. ((1,2),(2,1)) and ((2,1),(1,2)) so we don't need
// to consider the 'negative' side of the line, which will be generated by the other pair
let (a, b) = (pair[0], pair[1]);
let offset = (a.0 - b.0, a.1 - b.1);
for i in (start..).map_while(|i| if Some(i - start) != reps { Some(i as i64) } else { None }) {
let node_pos = (a.0 + i * offset.0, a.1 + i * offset.1);
if !antinodes.set(node_pos.0, node_pos.1, true) {
// left the grid
break;
}
}
}
}
antinodes
}
}
// PROBLEM 1 solution
#[aoc(day8, part1)]
pub fn part1(map: &AntennaMap) -> u64 {
let antinodes = map.find_antinodes(1, Some(1));
antinodes.count(true) as u64
}
// PROBLEM 2 solution
#[aoc(day8, part2)]
pub fn part2(map: &AntennaMap) -> u64 {
let antinodes = map.find_antinodes(0, None);
antinodes.count(true) as u64
}
#[cfg(test)]
mod tests {
use crate::day8::*;
const EXAMPLE: &[u8] = b"............
........0...
.....0......
.......0....
....0.......
......A.....
............
............
........A...
.........A..
............
............";
#[test]
fn part1_example() {
assert_eq!(part1(&get_input(EXAMPLE)), 14);
}
#[test]
fn part2_example() {
assert_eq!(part2(&get_input(EXAMPLE)), 34);
}
}

166
src/day9.rs Normal file
View File

@ -0,0 +1,166 @@
use aoc_runner_derive::{aoc, aoc_generator};
use itertools::Itertools;
use std::fmt::{Display, Write};
use std::io::{BufRead, Lines};
#[aoc_generator(day9)]
pub fn get_input(input: &[u8]) -> DiskMap {
DiskMap::from(input.lines())
}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
enum Unit {
File(usize),
Free,
}
#[derive(Clone)]
struct Inode {
id: usize,
pos: usize,
len: u8,
}
#[derive(Clone)]
pub struct DiskMap {
map: Vec<Unit>,
files: Vec<Inode>,
frees: Vec<Inode>,
}
impl<T: BufRead> From<Lines<T>> for DiskMap {
fn from(mut input: Lines<T>) -> Self {
let line_s = input.next().unwrap().unwrap();
let line = line_s.as_bytes();
let mut file_id = 0;
let mut map = Vec::new();
let mut files = Vec::new();
let mut frees = Vec::new();
for (i, c) in line.iter().enumerate() {
let len = c - b'0';
if i % 2 == 0 {
// file
files.push(Inode {
id: file_id,
pos: map.len(),
len,
});
for _ in 0..len {
map.push(Unit::File(file_id))
}
file_id += 1;
} else {
// free
frees.push(Inode {
id: 0,
pos: map.len(),
len,
});
for _ in 0..len {
map.push(Unit::Free)
}
}
}
Self { map, files, frees }
}
}
impl Display for DiskMap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for unit in &self.map {
match unit {
Unit::File(i) => f.write_char((b'0' + (*i % 10) as u8) as char)?,
Unit::Free => f.write_char('.')?,
}
}
Ok(())
}
}
impl DiskMap {
fn checksum(&self) -> u64 {
self.map
.iter()
.enumerate()
.map(|(i, u)| match u {
Unit::File(id) => i * id,
Unit::Free => 0,
})
.sum::<usize>() as u64
}
}
// PROBLEM 1 solution
#[aoc(day9, part1)]
pub fn part1(map: &DiskMap) -> u64 {
let mut map = map.to_owned();
let mut last_free = 0;
for file in map.files.iter().rev() {
let frees = map
.map
.iter()
.enumerate()
.skip(last_free) // we greedy fill, so no need to check for free space before the last one we used
.take(file.pos + file.len as usize) // and we only need to search until the end of the current file
.filter(|(_i, u)| **u == Unit::Free || **u == Unit::File(file.id)) // look for free space or our existing space
.map(|(i, _u)| i)
.take(file.len as usize) // get the first file.len free blocks
.collect_vec();
// Note: no need to test for too small frees list here, since we are guaranteed at worst to find our current position
if frees[0] >= file.pos {
// if the first available free is > file.pos, it's fully packed, job done
break;
}
#[allow(clippy::needless_range_loop)]
for j in 0..file.len as usize {
map.map.swap(frees[j], file.pos + j);
}
last_free = frees[file.len as usize - 1]
}
map.checksum()
}
// PROBLEM 2 solution
#[aoc(day9, part2)]
pub fn part2(map: &DiskMap) -> u64 {
let mut map = map.to_owned();
for file in map.files.iter().rev() {
let free = map.frees.iter_mut().find(|inode| inode.len >= file.len); // find the first entry in the free space map large enough
if let Some(free) = free {
if free.pos >= file.pos {
// if it's past our position, continue, but can't break since there might be free space for future files
continue;
}
for j in 0..file.len {
map.map.swap(free.pos + j as usize, file.pos + j as usize);
}
// Note: It is slightly faster to keep these hanging around in the free map with size = 0 then to remove them from the vec
free.len -= file.len;
free.pos += file.len as usize;
map.frees.push(Inode {
id: 0,
pos: file.pos,
len: file.len,
});
}
}
map.checksum()
}
#[cfg(test)]
mod tests {
use crate::day9::*;
const EXAMPLE: &[u8] = b"2333133121414131402";
#[test]
fn part1_example() {
assert_eq!(part1(&get_input(EXAMPLE)), 1928);
}
#[test]
fn part2_example() {
assert_eq!(part2(&get_input(EXAMPLE)), 2858);
}
}

15
src/lib.rs Normal file
View File

@ -0,0 +1,15 @@
mod day11;
use aoc_runner_derive::aoc_lib;
mod day1;
mod day2;
mod day3;
mod day4;
mod day5;
mod day6;
mod day7;
mod day8;
mod day9;
mod day10;
aoc_lib! { year = 2024 }