refactor for yearly

This commit is contained in:
RingOfStorms (Josh) 2024-09-28 21:52:31 -05:00
parent da00eb3606
commit dddda24957
57 changed files with 3 additions and 0 deletions

104
2023/src/bin/day01.rs Normal file
View file

@ -0,0 +1,104 @@
use aoc23::prelude::*;
fn part1(input: String) -> Result<usize> {
let mut sum = 0;
for line in input.lines().into_iter() {
let chars = line.chars();
let first_num: usize = chars
.clone()
.into_iter()
.skip_while(|c| !c.is_digit(10))
.take(1)
// This would get continuous digits: .take_while(|c| c.is_digit(10))
.collect::<String>()
.parse()?;
let last_num: usize = chars
.into_iter()
.rev()
.skip_while(|c| !c.is_digit(10))
.take(1)
.collect::<String>()
.parse()?;
let value = first_num * 10 + last_num;
sum += value;
}
// println!("Answer: {}", sum);
Ok(sum)
}
fn num_to_word(num: u32) -> Option<&'static str> {
match num {
1 => Some("one"),
2 => Some("two"),
3 => Some("three"),
4 => Some("four"),
5 => Some("five"),
6 => Some("six"),
7 => Some("seven"),
8 => Some("eight"),
9 => Some("nine"),
_ => None,
}
}
fn first_occurrence(str: &str, reversed: bool) -> Option<u32> {
if reversed {
for (i, char) in str.chars().rev().enumerate() {
let digit = char.to_digit(10).or_else(|| {
for num in 1..=9 {
if let Some(num_word) = num_to_word(num) {
if i + 1 >= num_word.len()
&& &str[str.len() - i - 1..str.len()][..num_word.len()] == num_word
{
return Some(num);
}
}
}
None
});
if digit.is_some() {
return digit;
}
}
} else {
for (i, char) in str.chars().enumerate() {
let digit = char.to_digit(10).or_else(|| {
for num in 1..=9 {
if let Some(num_word) = num_to_word(num) {
if i + 1 >= num_word.len() && &str[i + 1 - num_word.len()..=i] == num_word {
return Some(num);
}
}
}
None
});
if digit.is_some() {
return digit;
}
}
}
None
}
fn part_2(input: String) -> Result<u32> {
let mut sum = 0;
for line in input.lines().into_iter() {
let first_num = first_occurrence(line, false).ok_or("No number found in line")?;
let last_num = first_occurrence(line, true).ok_or("No number found in line reversed")?;
let value = first_num * 10 + last_num;
sum += value;
println!("Line {line}: a:{first_num}|b:{last_num} = {value} ++ {sum}");
}
Ok(sum)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("Day 1");
println!("=====");
let input = utils::aoc::get_puzzle_input(1).await?;
println!("part 1: {}", part1(input.clone())?);
println!("part 2: {}", part_2(input.clone())?);
Ok(())
}

View file

@ -0,0 +1,144 @@
use aoc23::prelude::*;
fn part_1(input: String) -> Result<usize> {
let mut sum = 0;
for line in input.lines().into_iter() {
let chars = line.chars();
let first_num: usize = chars
.clone()
.into_iter()
.skip_while(|c| !c.is_digit(10))
.take(1)
// This would get continuous digits: .take_while(|c| c.is_digit(10))
.collect::<String>()
.parse()?;
let last_num: usize = chars
.into_iter()
.rev()
.skip_while(|c| !c.is_digit(10))
.take(1)
.collect::<String>()
.parse()?;
let value = first_num * 10 + last_num;
sum += value;
}
// println!("Answer: {}", sum);
Ok(sum)
}
fn part_2(input: String) -> Result<u32> {
let numbers: [(&'static str, u32); 9] = [
("one", 1),
("two", 2),
("three", 3),
("four", 4),
("five", 5),
("six", 6),
("seven", 7),
("eight", 8),
("nine", 9),
];
let mut sum = 0;
for line in input.lines().into_iter() {
let mut chars = line.chars();
let mut peek_index = 0;
let first_num = loop {
// I wanted this to work but peek advances the iterator anyways despite saying it
// doesn't. I don't understand
//
// ```
// if let Some(Some(peek_digit)) = chars.by_ref().peekable().peek().map(|c| c.to_digit(10))
// ```
if let Some(Some(peek_digit)) = line.chars().nth(peek_index).map(|c| c.to_digit(10)) {
break peek_digit;
}
if let Some((_, digit)) = numbers.iter().find(|num| chars.as_str().starts_with(num.0)) {
break *digit;
}
if chars.next().is_none() {
break 0;
}
peek_index += 1;
};
let last_num = loop {
if let Some((_, digit)) = numbers.iter().find(|num| chars.as_str().ends_with(num.0)) {
break *digit;
}
if let Some(last_char) = chars.next_back() {
if let Some(digit) = last_char.to_digit(10) {
break digit;
}
} else {
break first_num;
}
};
let value = first_num * 10 + last_num;
sum += value;
// println!("Line {line}: a:{first_num}|b:{last_num} = {value} ++ {sum}");
}
Ok(sum)
}
fn part_2_no_peek(input: String) -> Result<u32> {
// We can just add the text versions of the number as well and this removes the
// need to peek at the digit like in the above method. Idk what I like better...
let numbers: [(&'static str, u32); 18] = [
("one", 1),
("1", 1),
("two", 2),
("2", 2),
("three", 3),
("3", 3),
("four", 4),
("4", 4),
("five", 5),
("5", 5),
("six", 6),
("6", 6),
("seven", 7),
("7", 7),
("eight", 8),
("8", 8),
("nine", 9),
("9", 9),
];
let mut sum = 0;
for line in input.lines().into_iter() {
let mut chars = line.chars();
let first_num = loop {
if let Some((_, digit)) = numbers.iter().find(|num| chars.as_str().starts_with(num.0)) {
break *digit;
}
if chars.next().is_none() {
break 0;
}
};
let last_num = loop {
if let Some((_, digit)) = numbers.iter().find(|num| chars.as_str().ends_with(num.0)) {
break *digit;
}
if chars.next_back().is_none() {
break first_num;
}
};
let value = first_num * 10 + last_num;
sum += value;
// println!("Line {line}: a:{first_num}|b:{last_num} = {value} ++ {sum}");
}
Ok(sum)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("Day 1");
println!("=====");
let input = utils::aoc::get_puzzle_input(1).await?;
println!("part 1: {}", part_1(input.clone())?);
println!("part 2: {}\t[peek method]", part_2(input.clone())?);
println!(
"part 2: {}\t[no peek method]",
part_2_no_peek(input.clone())?
);
Ok(())
}

134
2023/src/bin/day02.rs Normal file
View file

@ -0,0 +1,134 @@
use aoc23::prelude::*;
use std::{collections::HashMap, fmt::Display, str::FromStr};
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
enum Cube {
Blue,
Red,
Green,
}
impl FromStr for Cube {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"blue" => Ok(Cube::Blue),
"red" => Ok(Cube::Red),
"green" => Ok(Cube::Green),
_ => Err("asd".into()),
}
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
struct Game {
id: u32,
sets: Vec<HashMap<Cube, u32>>,
}
impl FromStr for Game {
type Err = String;
// Game 1: 7 green, 4 blue, 3 red; 4 blue, 10 red, 1 green; 1 blue, 9 red Game 2:
// 2 red, 4 blue, 3 green; 5 green, 3 red, 1 blue; 3 green, 5 blue, 3 red Game 3:
// 12 red, 1 blue; 6 red, 2 green, 3 blue; 2 blue, 5 red, 3 green
fn from_str(line: &str) -> std::result::Result<Self, Self::Err> {
let parts: Vec<&str> = line.split(':').collect();
let id = parts[0]
.trim()
.split_whitespace()
.last()
.ok_or("no id")?
.parse::<u32>()
.ok()
.ok_or("not a number")?;
let sets = parts[1]
.split(';')
.map(|set| {
set.split(',')
.filter_map(|part| {
let mut iter = part.trim().split_whitespace();
let count = iter.next().unwrap().parse::<u32>().unwrap();
let color = iter.next().unwrap().parse::<Cube>().ok()?;
Some((color, count))
})
.collect()
})
.collect();
Ok(Game { id, sets })
}
}
fn part1(input: String) -> Result<impl Display> {
let games: Vec<Game> = input
.lines()
.into_iter()
.map(|l| l.parse().expect("failed to parse line"))
.collect();
let mut possible_game_id_sum = 0;
let limit_red = 12;
let limit_green = 13;
let limit_blue = 14;
for game in games {
let out_of_bounds = game.sets.iter().any(|set| match set.get(&Cube::Red) {
Some(red) => red > &limit_red,
None => false,
} || match set.get(&Cube::Green) {
Some(green) => green > &limit_green,
None => false,
} || match set.get(&Cube::Blue) {
Some(blue) => blue > &limit_blue,
None => false,
});
if !out_of_bounds {
// println!("Valid game id, {:?}", game);
possible_game_id_sum += game.id;
} else {
// println!("INVALID game {:?}", game);
}
}
Ok(possible_game_id_sum)
}
fn part2(input: String) -> Result<impl Display> {
let games: Vec<Game> = input
.lines()
.into_iter()
.map(|l| l.parse().expect("failed to parse line"))
.collect();
let mut power_sum = 0;
for game in games {
let mut maxes: HashMap<Cube, u32> = [(Cube::Red, 1), (Cube::Green, 1), (Cube::Blue, 1)]
.into_iter()
.collect();
for set in game.sets.iter() {
for (cube, &count) in set {
let max_count = maxes.get_mut(cube).ok_or("default missing")?;
*max_count = u32::max(*max_count, count);
}
}
let power = maxes.values().into_iter().fold(1u32, |acc, v| acc * v);
power_sum += power;
}
Ok(power_sum)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("Day 1");
println!("=====");
let input = utils::aoc::get_puzzle_input(2).await?;
// ```
// let input = "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
// Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
// Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
// Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
// Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green"
// .to_owned();
// ```
println!("part 1: {}", part1(input.clone())?);
println!("part 2: {}", part2(input.clone())?);
Ok(())
}

189
2023/src/bin/day03.rs Normal file
View file

@ -0,0 +1,189 @@
use aoc23::prelude::*;
use std::str::FromStr;
#[derive(Debug)]
enum Item {
Digit(u32),
Symbol(bool),
None,
}
#[derive(Debug)]
struct Number {
pub value: usize,
pub row_index: usize,
pub char_start: usize,
pub char_end: usize,
}
struct Grid {
pub rows: Vec<Vec<Item>>,
pub numbers: Vec<Number>,
}
impl Grid {
fn get_item(&self, row: usize, col: usize) -> Option<&Item> {
match self.rows.get(row) {
Some(row) => row.get(col),
None => None,
}
}
fn get_adjacent_items(
&self,
row: usize,
start: usize,
end: usize,
) -> Vec<(&Item, usize, usize)> {
let row_start = if row > 0 { row - 1 } else { 0 };
let col_start = if start > 0 { start - 1 } else { 0 };
let mut items: Vec<(&Item, usize, usize)> = vec![];
for row_i in row_start..=row + 1 {
for col_i in col_start..=end + 1 {
if row_i != row || col_i < start || col_i > end {
let item = self.get_item(row_i, col_i);
if let Some(item) = item {
items.push((item, row_i, col_i));
}
}
}
}
return items;
}
fn get_adjacent_numbers(&self, row: usize, col: usize) -> Vec<&Number> {
let mut unique_numbers: Vec<&Number> = vec![];
for item in self.get_adjacent_items(row, col, col) {
match item {
(Item::Digit(_), r, c) => {
if let Some(number_at) = self
.numbers
.iter()
.find(|n| n.row_index == r && c >= n.char_start && c <= n.char_end)
{
if unique_numbers.iter().all(|n| n.value != number_at.value) {
unique_numbers.push(number_at);
}
}
}
_ => {}
}
}
unique_numbers
}
}
impl FromStr for Grid {
type Err = BoxE;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut grid = Grid {
rows: vec![],
numbers: vec![],
};
for (row_index, line) in s.lines().enumerate() {
let mut row: Vec<Item> = vec![];
let mut num: Option<usize> = None;
let mut char_start = 0;
for (char_index, char) in line.trim().chars().enumerate() {
if let Some(digit) = char.to_digit(10) {
row.push(Item::Digit(digit));
num = if let Some(existing_num) = num {
Some(existing_num * 10 + digit as usize)
} else {
char_start = char_index;
Some(digit as usize)
};
} else {
if char == '.' {
row.push(Item::None);
} else {
row.push(Item::Symbol(char == '*'))
}
if let Some(value) = num {
grid.numbers.push(Number {
value,
row_index,
char_start,
char_end: char_index - 1,
});
num = None;
char_start = 0;
}
}
}
// This killed me, I forgot to add a check after the end of the line to see if I
// was still "scanning" for a number and adding it. There was 1 single number not
// getting parsed that was on the right edge of the grid and I could not figure it
// out for a while.
if let Some(value) = num {
grid.numbers.push(Number {
value,
row_index,
char_start,
char_end: line.len() - 1,
});
}
grid.rows.push(row);
}
Ok(grid)
}
}
fn part1(input: String) -> Result<usize> {
let grid: Grid = input.parse()?;
let mut sum = 0;
for number in grid.numbers.iter() {
let adjacent =
grid.get_adjacent_items(number.row_index, number.char_start, number.char_end);
let is_part_no = adjacent
.iter()
.any(|item| matches!(item, (Item::Symbol(_), _, _)));
if is_part_no {
sum += number.value;
}
}
Ok(sum)
}
fn part2(input: String) -> Result<usize> {
let grid: Grid = input.parse()?;
let mut sum = 0;
let mut used: Vec<(usize, usize)> = vec![];
for number in grid.numbers.iter() {
let is_used = used
.iter()
.any(|(row, char_start)| number.row_index == *row && number.char_start == *char_start);
if !is_used {
let adjacent =
grid.get_adjacent_items(number.row_index, number.char_start, number.char_end);
if let Some((_, row, col)) = adjacent
.iter()
.find(|item| matches!(item.0, Item::Symbol(true)))
{
let gear_nums = grid.get_adjacent_numbers(*row, *col);
if gear_nums.len() >= 2 {
// this is lazy.. adds duplicates but w/e
let power = gear_nums.iter().fold(1, |pow, gear| pow * gear.value);
gear_nums
.iter()
.for_each(|g| used.push((g.row_index, g.char_start)));
sum += power;
}
}
}
}
Ok(sum)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("Day 3");
println!("=====");
let input = utils::aoc::get_puzzle_input(3).await?;
let part_1 = part1(input.clone())?;
println!("part 1: {}", part_1);
println!("part 2: {}", part2(input.clone())?);
Ok(())
}

124
2023/src/bin/day04.rs Normal file
View file

@ -0,0 +1,124 @@
use aoc23::prelude::*;
use itertools::Itertools;
use std::{collections::VecDeque, str::FromStr, time::Instant};
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
struct Card {
id: usize,
part_1_score: usize,
part_2_matches: usize,
part_2_count: usize,
}
impl FromStr for Card {
type Err = BoxE;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut id: Option<usize> = None;
let mut winning_numbers: Vec<usize> = vec![];
let mut score = 0;
let mut matches = 0;
for part in s.split(':') {
if id.is_some() {
for (num_mode, wins_or_nums) in part.split('|').enumerate() {
wins_or_nums
.trim()
.split_whitespace()
.into_iter()
.map(|num| {
num.parse::<usize>()
.expect(&format!("could not parse number: {}", num))
})
.for_each(|num| {
if num_mode == 0 {
winning_numbers.push(num);
} else {
if winning_numbers.iter().any(|winner| winner == &num) {
matches += 1;
score = if score == 0 { 1 } else { score * 2 };
}
}
});
}
} else {
id = Some(
part.split_whitespace()
.last()
.ok_or("Failed to get last item")?
.parse()?,
)
}
}
Ok(Card {
id: id.ok_or("no id found")?,
part_1_score: score,
part_2_matches: matches,
part_2_count: 1,
})
}
}
fn part1(input: String) -> Result<usize> {
Ok(input
.lines()
.map(|line| line.parse::<Card>())
.filter_map(|card| card.ok())
.fold(0, |sum, card| sum + card.part_1_score))
}
fn part2(input: String) -> Result<usize> {
let mut sum = 0;
let cards = input
.lines()
.map(|line| line.parse::<Card>())
.filter_map(|card| card.ok())
.collect_vec();
let mut queue: VecDeque<&Card> = cards.iter().collect();
while let Some(card) = queue.pop_front() {
sum += 1;
for card_index in card.id..card.id + card.part_2_matches {
cards.get(card_index).map(|c| queue.push_back(&c));
}
}
Ok(sum)
}
fn part2_revised(input: String) -> Result<usize> {
let mut sum = 0;
let mut cards = input
.lines()
.map(|line| line.parse::<Card>())
.filter_map(|card| card.ok())
.collect_vec();
for i in 0..cards.len() {
let (count, matches) = {
let card = cards.get(i).ok_or("card not found")?;
sum += card.part_2_count;
(card.part_2_count, card.part_2_matches)
};
for card_index in 1..=matches {
cards.get_mut(i + card_index).map(|card_below| {
card_below.part_2_count += count;
});
}
}
Ok(sum)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("Day 4");
println!("=====");
let input = utils::aoc::get_puzzle_input(4).await?;
let start = Instant::now();
println!("part 1: {}\t[{:?}]", part1(input.clone())?, start.elapsed());
let start = Instant::now();
println!("part 2: {}\t[{:?}]", part2(input.clone())?, start.elapsed());
let start = Instant::now();
println!(
"part 2: {}\t[{:?}]\t[revised]",
part2_revised(input.clone())?,
start.elapsed()
);
Ok(())
}

253
2023/src/bin/day05.rs Normal file
View file

@ -0,0 +1,253 @@
use aoc23::prelude::*;
use derive_builder::Builder;
use itertools::Itertools;
use rayon::prelude::*;
use std::{str::FromStr, time::Instant};
#[derive(Debug, Builder, Clone)]
struct Map {
destination_start: usize,
source_start: usize,
length: usize,
}
impl FromStr for Map {
type Err = BoxE;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut parts = s
.trim()
.split_whitespace()
.map(|s| s.parse::<usize>().expect("failed to parse"));
Ok(MapBuilder::default()
.destination_start(parts.next().expect("no dest"))
.source_start(parts.next().expect("no start"))
.length(parts.next().expect("no length"))
.build()?)
}
}
impl Map {
fn includes_source(&self, source: usize) -> bool {
source >= self.source_start && source < self.source_start + self.length
}
fn apply(&self, source: usize) -> Option<usize> {
if self.includes_source(source) {
let diff = source - self.source_start;
Some(self.destination_start + diff)
} else {
None
}
}
}
#[derive(Debug, Builder, Clone)]
struct Mapper {
source: String,
destination: String,
maps: Vec<Map>,
}
impl FromStr for Mapper {
type Err = BoxE;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut mapper = MapperBuilder::default();
mapper.maps(vec![]);
for line in s.trim().lines() {
if mapper.source.is_none() {
let mut keys = line
.split_whitespace()
.next()
.expect("no source-dest")
.split("-to-");
mapper.source(keys.next().expect("no source key").to_owned());
mapper.destination(keys.next().expect("no destination key").to_owned());
} else if let Some(maps) = &mut mapper.maps {
maps.push(line.parse()?);
}
}
Ok(mapper.build()?)
}
}
impl Mapper {
fn apply(&self, source: usize) -> usize {
self.maps
.iter()
.find_map(|map| map.apply(source))
.unwrap_or(source)
}
}
#[derive(Debug, Builder, Clone)]
struct Seeds {
source: String,
}
#[derive(Debug, Builder)]
struct Almanac {
seeds: Seeds,
mappers: Vec<Mapper>,
}
impl Almanac {
fn map_source(&self, source: usize, start_key: &str, end_key: &str) -> usize {
let mut current_key = start_key;
let mut current_value = source;
while current_key != end_key {
let mapper = self
.mappers
.iter()
.find(|mapper| mapper.source == current_key)
.expect("could not find mapper");
current_value = mapper.apply(current_value);
current_key = &mapper.destination;
}
current_value
}
}
impl FromStr for Almanac {
type Err = BoxE;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut almanac = AlmanacBuilder::default();
let mut parts = s.trim().split("\n\n");
almanac.seeds(Seeds {
source: parts.next().expect("seed line missing").to_string(),
});
almanac.mappers(vec![]);
while let Some(mapper_section) = parts.next() {
if let Some(mappers) = &mut almanac.mappers {
mappers.push(mapper_section.parse()?);
}
}
Ok(almanac.build()?)
}
}
fn part1(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let almanac: Almanac = input.parse()?;
let parsed_time = start.elapsed();
// algo
let start = Instant::now();
let answer = almanac
.seeds
.source
.split(":")
.nth(1)
.expect("seeds missing")
.trim()
.split_whitespace()
.map(|s| s.parse::<usize>().expect("failed to parse seed as number"))
.map(|source| almanac.map_source(source, "seed", "location"))
.min()
.ok_or("failed to get min location")?;
let algo_time = start.elapsed();
// output
println!("Day 5, part 1: {answer}");
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
fn part2(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let almanac: Almanac = input.parse()?;
let parsed_time = start.elapsed();
// algo
let start = Instant::now();
let answer = almanac
.seeds
.source
.split(":")
.nth(1)
.expect("seeds missing")
.trim()
.split_whitespace()
.map(|s| s.parse::<usize>().expect("failed to parse seed as number"))
.tuples()
.flat_map(|(start, length)| start..start + length)
.take(3000)
// Squeeze with rayon for brute force approach
.par_bridge()
.map(|source| almanac.map_source(source, "seed", "location"))
.min()
.ok_or("failed to get min location")?;
let algo_time = start.elapsed();
// output
println!("Day 5, part 2: {answer}");
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
// TODO come back and revise for a faster solution
#[tokio::main]
async fn main() -> Result<()> {
let input = utils::aoc::get_puzzle_input(5).await?;
part1(input.clone())?;
part2(input.clone())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
static DATA: &'static str = "seeds: 79 14 55 13
seed-to-soil map:
50 98 2
52 50 48
soil-to-fertilizer map:
0 15 37
37 52 2
39 0 15
fertilizer-to-water map:
49 53 8
0 11 42
42 0 7
57 7 4
water-to-light map:
88 18 7
18 25 70
light-to-temperature map:
45 77 23
81 45 19
68 64 13
temperature-to-humidity map:
0 69 1
1 0 69
humidity-to-location map:
60 56 37
56 93 4
";
#[test]
fn test_part_1() -> Result<()> {
assert_eq!(part1(DATA.to_owned())?, 35);
Ok(())
}
#[test]
fn test_part_2() -> Result<()> {
assert_eq!(part2(DATA.to_owned())?, 46);
Ok(())
}
}

109
2023/src/bin/day06.rs Normal file
View file

@ -0,0 +1,109 @@
use aoc23::prelude::*;
use derive_builder::Builder;
static DAY: u8 = 6;
#[derive(Debug, Builder, Clone)]
struct Race {
time: usize,
record_distance: usize,
}
impl Race {
fn get_roots(&self) -> (usize, usize) {
let a = -1.0;
let b = self.time as f64;
let c = -(self.record_distance as f64);
let n1 = (-b + f64::sqrt(b * b - (4.0 * a * c))) / (2.0 * a);
let n2 = (-b - f64::sqrt(b * b - (4.0 * a * c))) / (2.0 * a);
if n1 < n2 {
// floor and round up to ensure we win
let n1 = f64::floor(n1) as usize + 1;
// ceil and round down to ensure we don't exceed time cap
let n2 = f64::ceil(n2) as usize - 1;
(n1, n2)
} else {
let n1 = f64::ceil(n1) as usize - 1;
let n2 = f64::floor(n2) as usize + 1;
(n2, n1)
}
}
fn get_ways_to_beat_count(&self) -> usize {
let (n1, n2) = self.get_roots();
if self.time < n1 {
0
} else {
let n2 = usize::min(self.time, n2);
n2 - n1 + 1
}
}
}
fn get_ways_to_beat_in_all_races(races: &[Race]) -> Result<usize> {
let mut answer = 1;
for race in races {
let ways_to_beat = race.get_ways_to_beat_count();
answer *= ways_to_beat;
}
Ok(answer)
}
#[tokio::main]
async fn main() -> Result<()> {
// let input = utils::aoc::get_puzzle_input(DAY).await?;
println!("Day {DAY}");
println!("=====");
let races: [Race; 3] = [
Race {
time: 7,
record_distance: 9,
},
Race {
time: 15,
record_distance: 40,
},
Race {
time: 30,
record_distance: 200,
},
];
println!(
"part 1, example: {}",
get_ways_to_beat_in_all_races(&races)?
);
let races: [Race; 4] = [
Race {
time: 53,
record_distance: 250,
},
Race {
time: 91,
record_distance: 1330,
},
Race {
time: 67,
record_distance: 1081,
},
Race {
time: 68,
record_distance: 1025,
},
];
println!("part 1, real: {}", get_ways_to_beat_in_all_races(&races)?);
let races: [Race; 1] = [Race {
time: 71530,
record_distance: 940200,
}];
println!(
"part 2, example: {}",
get_ways_to_beat_in_all_races(&races)?
);
let races: [Race; 1] = [Race {
time: 53916768,
record_distance: 250133010811025,
}];
println!("part 2, real: {}", get_ways_to_beat_in_all_races(&races)?);
Ok(())
}

207
2023/src/bin/day07.rs Normal file
View file

@ -0,0 +1,207 @@
use aoc23::prelude::*;
use derive_builder::Builder;
use itertools::Itertools;
use std::{cmp::Ordering, str::FromStr, time::Instant};
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)]
enum Strength {
HighCard,
OnePair,
TwoPair,
ThreeOfAKind,
FullHouse,
FourOfAKind,
FiveOfAKind,
}
impl Strength {
#[cfg(feature = "part2")]
fn for_hand(hand: &Hand) -> Self {
let mut counts = [0; 15];
let mut jokers = 0;
for card in hand.cards.iter() {
if card == &1 {
jokers += 1;
} else {
counts[*card as usize - 2] += 1;
}
}
let mut strength = counts
.iter()
.fold(Strength::HighCard, |strength, &count| match count {
5 => Strength::FiveOfAKind,
4 => Strength::FourOfAKind,
3 => match strength {
Strength::TwoPair => Strength::FullHouse,
Strength::OnePair => Strength::FullHouse,
_ => Strength::ThreeOfAKind,
},
2 => match strength {
Strength::ThreeOfAKind => Strength::FullHouse,
Strength::OnePair => Strength::TwoPair,
_ => Strength::OnePair,
},
_ => strength,
});
while jokers > 0 {
strength = match strength {
Strength::HighCard => Strength::OnePair,
Strength::OnePair => Strength::ThreeOfAKind,
Strength::TwoPair => Strength::FullHouse,
Strength::ThreeOfAKind => Strength::FourOfAKind,
Strength::FullHouse => Strength::FourOfAKind,
Strength::FourOfAKind => Strength::FiveOfAKind,
Strength::FiveOfAKind => Strength::FiveOfAKind,
};
jokers -= 1;
}
strength
}
#[cfg(feature = "part1")]
fn for_hand(hand: &Hand) -> Self {
let mut counts = [0; 15];
for card in hand.cards.iter() {
counts[*card as usize - 2] += 1;
}
counts
.iter()
.fold(Strength::HighCard, |strength, &count| match count {
5 => Strength::FiveOfAKind,
4 => Strength::FourOfAKind,
3 => match strength {
Strength::TwoPair => Strength::FullHouse,
Strength::OnePair => Strength::FullHouse,
_ => Strength::ThreeOfAKind,
},
2 => match strength {
Strength::ThreeOfAKind => Strength::FullHouse,
Strength::OnePair => Strength::TwoPair,
_ => Strength::OnePair,
},
_ => strength,
})
}
}
#[derive(Debug, Builder, Clone, PartialEq, Eq)]
struct Hand {
cards: Vec<u32>,
bid: usize,
}
impl Ord for Hand {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
let self_strength = Strength::for_hand(self);
let other_strength = Strength::for_hand(other);
match self_strength.cmp(&other_strength) {
Ordering::Equal => self
.cards
.iter()
.zip(other.cards.iter())
.map(|(s, o)| s.cmp(o))
.skip_while(|ord| matches!(ord, Ordering::Equal))
.next()
.unwrap_or(Ordering::Equal),
Ordering::Less => Ordering::Less,
Ordering::Greater => Ordering::Greater,
}
}
}
impl PartialOrd for Hand {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl FromStr for Hand {
type Err = BoxE;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut splits = s.trim().split_whitespace();
Ok(HandBuilder::default()
.cards(
splits
.next()
.unwrap()
.chars()
.map(|c| {
if let Some(digit) = c.to_digit(10) {
digit
} else {
match c {
#[cfg(feature = "part1")]
'A' => 14,
#[cfg(feature = "part2")]
'A' => 13,
#[cfg(feature = "part1")]
'K' => 13,
#[cfg(feature = "part2")]
'K' => 12,
#[cfg(feature = "part1")]
'Q' => 12,
#[cfg(feature = "part2")]
'Q' => 11,
#[cfg(feature = "part1")]
'J' => 11,
#[cfg(feature = "part2")]
'J' => 1,
'T' => 10,
_ => panic!("invalid card: {}", c),
}
}
})
.collect(),
)
.bid(splits.next().unwrap().parse()?)
.build()?)
}
}
fn calculate(input: String) -> Result<usize> {
let start = Instant::now();
let answer = input
.lines()
.map(|line| line.parse::<Hand>().unwrap())
.sorted()
.enumerate()
.map(|(idx, hand)| hand.bid * (idx + 1))
.sum();
let algo_time = start.elapsed();
println!("Day {DAY}: {answer}");
println!("\t{algo_time:?}");
Ok(answer)
}
// TODO come back and revise for a faster solution
#[tokio::main]
async fn main() -> Result<()> {
println!("Day {DAY}");
println!("=====");
let input = utils::aoc::get_puzzle_input(DAY).await?;
calculate(input.clone())?;
Ok(())
}
static DAY: u8 = 7;
#[cfg(test)]
mod tests {
use super::*;
static DATA: &'static str = "32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483";
#[test]
fn test() -> Result<()> {
#[cfg(feature = "part1")]
assert_eq!(calculate(DATA.to_owned())?, 6440);
#[cfg(feature = "part2")]
assert_eq!(calculate(DATA.to_owned())?, 5905);
Ok(())
}
}

212
2023/src/bin/day08.rs Normal file
View file

@ -0,0 +1,212 @@
use aoc23::prelude::*;
use derive_builder::Builder;
use itertools::Itertools;
use rayon::prelude::*;
use std::{collections::HashMap, time::Instant};
extern crate regex;
use regex::Regex;
use static_init::dynamic;
// Regex is slow to compile, don't do it in the parse loop, pull it out here and
// compile it once and reuse below.
#[dynamic]
static RE_PARSE_NODE: Regex =
Regex::new(r"(?<id>\w{3}).*?(?<left>\w{3}).*?(?<right>\w{3})").expect("re_parse_node invalid");
#[derive(Debug, Builder, Clone)]
struct Node<'a> {
id: &'a str,
left: &'a str,
right: &'a str,
}
impl<'a> Node<'a> {
fn new(s: &'a str) -> Result<Self> {
let re = RE_PARSE_NODE.captures(s).expect("No match for regex");
Ok(NodeBuilder::default()
.id(re.name("id").expect("no id").as_str())
.left(re.name("left").expect("no left").as_str())
.right(re.name("right").expect("no right").as_str())
.build()?)
}
}
#[derive(Debug, Clone)]
enum Dir {
Left,
Right,
}
impl Dir {
fn from_char(c: &char) -> Self {
match c {
'L' => Self::Left,
'R' => Self::Right,
_ => panic!("Not valid dir"),
}
}
}
#[derive(Debug, Builder, Clone)]
struct Map<'a> {
dirs: Vec<Dir>,
nodes: HashMap<&'a str, Node<'a>>,
}
impl<'a> Map<'a> {
fn new(s: &'a str) -> Result<Self> {
let mut splits = s.trim().split("\n\n");
Ok(MapBuilder::default()
.dirs(
splits
.next()
.unwrap()
.trim()
.chars()
.map(|c| Dir::from_char(&c))
.collect(),
)
.nodes(
splits
.next()
.unwrap()
.trim()
.lines()
.map(|a| Node::new(a.trim()).map(|n| (n.id, n)))
.try_collect()?,
)
.build()?)
}
}
fn part1(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let map: Map = Map::new(&input)?;
let parsed_time = start.elapsed();
// algo
let start = Instant::now();
let mut answer = 0;
let mut current = map.nodes.get("AAA").expect("no start node");
let mut dir = map.dirs.iter().cycle();
while !current.id.eq("ZZZ") {
match dir.next().unwrap() {
Dir::Left => current = map.nodes.get(current.left).expect("no left node"),
Dir::Right => current = map.nodes.get(current.right).expect("no right node"),
}
answer += 1;
}
let algo_time = start.elapsed();
// output
println!("Day {DAY}, part 1: {answer}");
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
fn part2(input: String) -> Result<u64> {
// parse
let start = Instant::now();
let map: Map = Map::new(&input)?;
let parsed_time = start.elapsed();
// algo
let start = Instant::now();
let currents: Vec<&Node> = map.nodes.values().filter(|n| n.id.ends_with("A")).collect();
let steps: Vec<u64> = currents
.par_iter()
.map(|node| {
let mut dir = map.dirs.iter().cycle();
let mut steps = 0;
let mut current = *node;
while !current.id.ends_with("Z") {
match dir.next().unwrap() {
Dir::Left => current = map.nodes.get(current.left).expect("no left node"),
Dir::Right => current = map.nodes.get(current.right).expect("no right node"),
}
steps += 1;
// println!("Step: {answer}"); let _ = std::io::stdout().flush();
}
steps
})
.collect();
let answer = utils::math::find_lcm(&steps[..]);
let algo_time = start.elapsed();
// output
println!("Day {DAY}, part 2: {answer}");
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
// TODO come back and revise for a faster solution
#[tokio::main]
async fn main() -> Result<()> {
println!("Day {DAY}");
println!("=====");
let input = utils::aoc::get_puzzle_input(DAY).await?;
part1(input.clone())?;
part2(input.clone())?;
Ok(())
}
static DAY: u8 = 8;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part_1() -> Result<()> {
assert_eq!(
part1(
"RL
AAA = (BBB, CCC)
BBB = (DDD, EEE)
CCC = (ZZZ, GGG)
DDD = (DDD, DDD)
EEE = (EEE, EEE)
GGG = (GGG, GGG)
ZZZ = (ZZZ, ZZZ)"
.to_owned(),
)?,
2
);
assert_eq!(
part1(
"LLR
AAA = (BBB, BBB)
BBB = (AAA, ZZZ)
ZZZ = (ZZZ, ZZZ)"
.to_owned()
)?,
6
);
Ok(())
}
#[test]
fn test_part_2() -> Result<()> {
part2(
"LR
11A = (11B, XXX)
11B = (XXX, 11Z)
11Z = (11B, XXX)
22A = (22B, XXX)
22B = (22C, 22C)
22C = (22Z, 22Z)
22Z = (22B, 22B)
XXX = (XXX, XXX)"
.to_owned(),
);
Ok(())
}
}

131
2023/src/bin/day09.rs Normal file
View file

@ -0,0 +1,131 @@
use aoc23::prelude::*;
use derive_builder::Builder;
use itertools::Itertools;
use rayon::prelude::*;
use std::{arch::x86_64::_MM_FROUND_CUR_DIRECTION, str::FromStr, time::Instant};
static DAY: u8 = 9;
fn part1(input: String) -> Result<i64> {
let start = Instant::now();
let answer: i64 = input
.lines()
.par_bridge()
.map(|line| {
let mut sequences: Vec<Vec<i64>> = vec![];
line.trim()
.split_whitespace()
.map(|s| s.parse::<i64>().expect("Failed to parse number in input"))
.enumerate()
.for_each(|(idx, num)| {
// every new number of primary history can result in a new row of depth
sequences.push(vec![]);
for depth in 0..sequences.len() {
if depth == 0 {
sequences
.get_mut(depth)
.expect("top history expected")
.push(num);
} else {
let len = sequences[depth].len();
let above = &sequences[depth - 1];
let left = *above.get(len).expect("expected value left");
let right = *above.get(len + 1).expect("expected value right");
sequences
.get_mut(depth)
.expect("seq current depth expect")
.push(right - left);
}
}
});
sequences
.iter()
.rev()
.skip_while(|seq| seq.iter().all(|n| n == &0))
.fold(0, |acc, seq| seq.last().expect("expected last value") + acc)
})
.sum();
// output
println!("part 1: {answer}\t[total: {:?}]", start.elapsed());
Ok(answer)
}
// algo
fn part2(input: String) -> Result<i64> {
let start = Instant::now();
let answer: i64 = input
.lines()
.par_bridge()
.map(|line| {
let mut sequences: Vec<Vec<i64>> = vec![];
line.trim()
.split_whitespace()
.map(|s| s.parse::<i64>().expect("Failed to parse number in input"))
.enumerate()
.for_each(|(idx, num)| {
// every new number of primary history can result in a new row of depth
sequences.push(vec![]);
for depth in 0..sequences.len() {
if depth == 0 {
sequences
.get_mut(depth)
.expect("top history expected")
.push(num);
} else {
let len = sequences[depth].len();
let above = &sequences[depth - 1];
let left = *above.get(len).expect("expected value left");
let right = *above.get(len + 1).expect("expected value right");
sequences
.get_mut(depth)
.expect("seq current depth expect")
.push(right - left);
}
}
});
sequences
.iter()
.rev()
.skip_while(|seq| seq.iter().all(|n| n == &0))
.fold(0, |acc, seq| {
seq.first().expect("expected last value") - acc
})
})
.sum();
// output
println!("part 2: {answer}\t[total: {:?}]", start.elapsed());
Ok(answer)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("Day {DAY}");
println!("=====");
let input = utils::aoc::get_puzzle_input(DAY).await?;
part1(input.clone())?;
part2(input.clone())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
static DATA: &'static str = "0 3 6 9 12 15
1 3 6 10 15 21
10 13 16 21 30 45";
#[test]
fn test_part_1() -> Result<()> {
assert_eq!(part1(DATA.to_owned())?, 114);
Ok(())
}
#[test]
fn test_part_2() -> Result<()> {
assert_eq!(part2(DATA.to_owned())?, 2);
Ok(())
}
}

428
2023/src/bin/day10.rs Normal file
View file

@ -0,0 +1,428 @@
use aoc23::prelude::*;
use derive_builder::Builder;
use itertools::Itertools;
use rayon::prelude::*;
use std::{collections::HashSet, str::FromStr, time::Instant};
static DAY: u8 = 10;
#[derive(Debug, Clone)]
enum PipeDirection {
Vertical,
Horizontal,
NorthEast,
NorthWest,
SouthWest,
SouthEast,
Start,
Ground,
}
impl PipeDirection {
fn is_north(&self) -> bool {
matches!(self, PipeDirection::NorthEast | PipeDirection::NorthWest)
}
}
#[derive(Debug, Clone)]
struct Pipe {
position: (usize, usize),
direction: PipeDirection,
}
impl Eq for Pipe {}
impl PartialEq for Pipe {
fn eq(&self, other: &Self) -> bool {
self.position == other.position
}
}
impl Pipe {
fn for_char(c: char, position: (usize, usize)) -> Self {
match c {
'|' => Self {
position,
direction: PipeDirection::Vertical,
},
'-' => Self {
position,
direction: PipeDirection::Horizontal,
},
'L' => Self {
position,
direction: PipeDirection::NorthEast,
},
'J' => Self {
position,
direction: PipeDirection::NorthWest,
},
'7' => Self {
position,
direction: PipeDirection::SouthWest,
},
'F' => Self {
position,
direction: PipeDirection::SouthEast,
},
'.' => Self {
position,
direction: PipeDirection::Ground,
},
'S' => Self {
position,
direction: PipeDirection::Start,
},
_ => panic!("invalid pipe char"),
}
}
fn connects_to(&self) -> Vec<(usize, usize)> {
let deltas = match self.direction {
PipeDirection::Vertical => Some([(-1i64, 0i64), (1, 0)]),
PipeDirection::Horizontal => Some([(0, -1), (0, 1)]),
PipeDirection::NorthEast => Some([(-1, 0), (0, 1)]),
PipeDirection::NorthWest => Some([(-1, 0), (0, -1)]),
PipeDirection::SouthWest => Some([(1, 0), (0, -1)]),
PipeDirection::SouthEast => Some([(1, 0), (0, 1)]),
_ => None,
};
match deltas {
Some(deltas) => {
let mut to = vec![];
for delta in deltas {
let (row_delta, col_delta) = delta;
let (row, col) = self.position;
if row == 0 && row_delta < 0 {
continue;
}
if col == 0 && col_delta < 0 {
continue;
}
to.push((
(row as i64 + row_delta).try_into().unwrap(),
(col as i64 + col_delta).try_into().unwrap(),
));
}
to
}
None => vec![],
}
}
}
#[derive(Debug, Clone)]
struct Pipes {
grid: Vec<Vec<Pipe>>,
start: Pipe,
}
impl Pipes {
fn new(s: &str) -> Result<Self> {
let mut start = None;
let mut grid = vec![];
for (row_index, line) in s.trim().lines().enumerate() {
let mut row = vec![];
for (col_index, char) in line.trim().chars().enumerate() {
row.push(Pipe::for_char(char, (row_index, col_index)));
if char == 'S' {
start = Some((row_index, col_index))
}
}
grid.push(row);
}
// Get actual start pipe variant for easier code on the rest of the walking code
// later
let start = start
.map(|position| {
let mut up = true;
let mut left = true;
// We assume start always has two connections by definition of the problem. Will
// assume it is up and left and swap to the opposite after checking those.
let (row, col) = position;
if let Some(down) = grid.get(row + 1) {
match down[col].direction {
PipeDirection::Vertical
| PipeDirection::NorthEast
| PipeDirection::NorthWest => {
up = false;
}
_ => {}
}
}
if let Some(right) = grid[row].get(col + 1) {
match right.direction {
PipeDirection::Horizontal
| PipeDirection::NorthWest
| PipeDirection::SouthWest => {
left = false;
}
_ => {}
}
}
if up && left {
Pipe {
position,
direction: PipeDirection::NorthWest,
}
} else if up && !left {
Pipe {
position,
direction: PipeDirection::NorthEast,
}
} else if !up && left {
Pipe {
position,
direction: PipeDirection::SouthWest,
}
} else {
Pipe {
position,
direction: PipeDirection::SouthEast,
}
}
})
.expect("start");
Ok(Self { grid, start })
}
fn get(&self, position: &(usize, usize)) -> &Pipe {
&self.grid[position.0][position.1]
}
}
fn part1(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let pipes = Pipes::new(&input)?;
let parsed_time = start.elapsed();
// algo
let a_start = Instant::now();
let mut answer = 1;
let mut starters = pipes.start.connects_to().into_iter();
let mut paths: [(&Pipe, &Pipe); 2] = [
(&pipes.start, pipes.get(&starters.next().unwrap())),
(&pipes.start, pipes.get(&starters.next().unwrap())),
];
loop {
// reached end
if paths[0].1.position.0 == paths[1].1.position.0
&& paths[0].1.position.1 == paths[1].1.position.1
{
break;
}
answer += 1;
for path in paths.iter_mut() {
let next = path
.1
.connects_to()
.into_iter()
.filter(|pipe| pipe.0 != path.0.position.0 || pipe.1 != path.0.position.1)
.next()
.map(|pos| pipes.get(&pos))
.unwrap();
*path = (path.1, next);
}
}
let algo_time = a_start.elapsed();
// output
println!("part 1: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
fn part2(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let pipes = Pipes::new(&input)?;
let parsed_time = start.elapsed();
// algo
let a_start = Instant::now();
let mut path = vec![
&pipes.start,
pipes
.start
.connects_to()
.into_iter()
.next()
.map(|pos| pipes.get(&pos))
.unwrap(),
];
loop {
let previous = path.get(path.len() - 2).unwrap();
let next = path
.last()
.unwrap()
.connects_to()
.into_iter()
.filter(|pipe| pipe.0 != previous.position.0 || pipe.1 != previous.position.1)
.next()
.map(|pos| pipes.get(&pos))
.unwrap();
match next.direction {
PipeDirection::Start => {
break;
}
_ => {
path.push(next);
}
};
}
let mut answer = 0;
for (ri, row) in pipes.grid.iter().enumerate() {
for (idx, pipe) in row.iter().enumerate() {
if !path.contains(&pipe) {
// println!("Pipe check, {}, {}", ri, idx);
let (intersections, _) = row
.iter()
// Start at current non path pipe we are checking
.skip(idx + 1)
// Add fake pipe at the end so we can match on pipes on the right edge of the Grid
// .chain(std::iter::once(&Pipe { position: (0, 0), direction:
// PipeDirection::Ground, }))
.fold(
(0, None),
|(i_count, intersecting_dir): (usize, Option<&PipeDirection>), pipe| {
if path.contains(&pipe) {
return match pipe.direction {
PipeDirection::Vertical => (i_count + 1, None),
PipeDirection::NorthWest
| PipeDirection::NorthEast
| PipeDirection::SouthEast
| PipeDirection::SouthWest => match intersecting_dir {
Some(last_direction) => {
match (
last_direction.is_north(),
pipe.direction.is_north(),
) {
(true, true) | (false, false) => (i_count, None),
(true, false) | (false, true) => {
(i_count + 1, None)
}
}
}
None => (i_count, Some(&pipe.direction)),
},
PipeDirection::Horizontal => (i_count, intersecting_dir),
_ => (i_count, None),
};
}
return (i_count, None);
},
);
// println!( "\t\tPipe check, {}, {} intersections: {} | {}", ri, idx,
// intersections, (intersections > 0 && intersections % 2 == 1) );
if intersections > 0 && intersections % 2 == 1 {
answer += 1;
}
}
}
}
let algo_time = a_start.elapsed();
// output
println!("part 2: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("Day {DAY}");
println!("=====");
let input = utils::aoc::get_puzzle_input(DAY).await?;
part1(input.clone())?;
part2(input.clone())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part_1() -> Result<()> {
assert_eq!(
part1(
"..F7.
.FJ|.
SJ.L7
|F--J
LJ..."
.to_owned()
)?,
8
);
Ok(())
}
#[test]
fn test_part_2_small() -> Result<()> {
assert_eq!(
part2(
"...........
.S-------7.
.|F-----7|.
.||.....||.
.||.....||.
.|L-7.F-J|.
.|..|.|..|.
.L--J.L--J.
..........."
.to_owned(),
)?,
4
);
Ok(())
}
#[test]
fn test_part_2_med() -> Result<()> {
assert_eq!(
part2(
".F----7F7F7F7F-7....
.|F--7||||||||FJ....
.||.FJ||||||||L7....
FJL7L7LJLJ||LJ.L-7..
L--J.L7...LJS7F-7L7.
....F-J..F7FJ|L7L7L7
....L7.F7||L7|.L7L7|
.....|FJLJ|FJ|F7|.LJ
....FJL-7.||.||||...
....L---J.LJ.LJLJ..."
.to_owned(),
)?,
8
);
Ok(())
}
#[test]
fn test_part_2_hard() -> Result<()> {
assert_eq!(
part2(
"FF7FSF7F7F7F7F7F---7
L|LJ||||||||||||F--J
FL-7LJLJ||||||LJL-77
F--JF--7||LJLJ7F7FJ-
L---JF-JLJ.||-FJLJJ7
|F|F-JF---7F7-L7L|7|
|FFJF7L7F-JF7|JL---7
7-L-JL7||F7|L7F-7F7|
L.L7LFJ|||||FJL7||LJ
L7JLJL-JLJLJL--JLJ.L"
.to_owned(),
)?,
10
);
Ok(())
}
}

173
2023/src/bin/day11.rs Normal file
View file

@ -0,0 +1,173 @@
use aoc23::prelude::*;
use derive_builder::Builder;
use itertools::Itertools;
use rayon::prelude::*;
use std::{str::FromStr, time::Instant};
static DAY: u8 = 11;
#[derive(Debug, Clone, Eq, PartialEq)]
struct Vec2D {
x: usize,
y: usize,
}
impl From<(usize, usize)> for Vec2D {
fn from(value: (usize, usize)) -> Self {
Self {
x: value.0,
y: value.1,
}
}
}
#[derive(Debug, Clone)]
struct Space {
galaxies: Vec<Vec2D>,
x_max: usize,
y_max: usize,
}
impl Space {
fn new(s: &str) -> Result<Self> {
let mut x_max = 0;
let mut y_max = 0;
let mut galaxies = vec![];
for (y, row) in s.trim().lines().enumerate() {
for (x, col) in row.chars().enumerate() {
if col == '#' {
galaxies.push((x, y).into());
}
x_max = x;
}
y_max = y;
}
Ok(Space {
galaxies,
x_max,
y_max,
})
}
fn expand(&mut self, by: usize) {
for y in (0..=self.y_max).rev() {
if self.galaxies.iter().filter(|g| g.y == y).count() == 0 {
self.galaxies
.iter_mut()
.filter(|g| g.y > y)
.for_each(|g| g.y += by);
}
}
for x in (0..=self.x_max).rev() {
if self.galaxies.iter().filter(|g| g.x == x).count() == 0 {
self.galaxies
.iter_mut()
.filter(|g| g.x > x)
.for_each(|g| g.x += by);
}
}
}
fn galaxy_pairs(&self) -> Vec<(&Vec2D, &Vec2D)> {
let mut galaxy_pairs = vec![];
for i in 0..self.galaxies.len() {
for i2 in i + 1..self.galaxies.len() {
galaxy_pairs.push((&self.galaxies[i], &self.galaxies[i2]));
}
}
galaxy_pairs
}
}
fn part1(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let mut space = Space::new(&input)?;
let parsed_time = start.elapsed();
let step_start = Instant::now();
space.expand(1);
let expansion_time = step_start.elapsed();
// algo
let step_start = Instant::now();
let mut answer = 0;
for pair in space.galaxy_pairs() {
let distance = pair.0.y.abs_diff(pair.1.y) + pair.0.x.abs_diff(pair.1.x);
answer += distance;
}
let algo_time = step_start.elapsed();
// output
println!("part 1: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\texpansion: {expansion_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
fn part2(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let mut space = Space::new(&input)?;
let parsed_time = start.elapsed();
let step_start = Instant::now();
space.expand(1000000 - 1);
let expansion_time = step_start.elapsed();
// algo
let step_start = Instant::now();
let mut answer = 0;
for pair in space.galaxy_pairs() {
let distance = pair.0.y.abs_diff(pair.1.y) + pair.0.x.abs_diff(pair.1.x);
answer += distance;
}
let algo_time = step_start.elapsed();
// output
println!("part 2: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\texpansion: {expansion_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("Day {DAY}");
println!("=====");
let input = utils::aoc::get_puzzle_input(DAY).await?;
part1(input.clone())?;
part2(input.clone())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
static DATA: &'static str = "...#......
.......#..
#.........
..........
......#...
.#........
.........#
..........
.......#..
#...#.....
";
#[test]
fn test_part_1() -> Result<()> {
assert_eq!(part1(DATA.to_owned())?, 374);
Ok(())
}
#[test]
// Must set the space.expand(1000000 - 1); to (100 - 1) instead for this test
#[ignore]
fn test_part_2() -> Result<()> {
assert_eq!(part2(DATA.to_owned())?, 8410);
Ok(())
}
}

164
2023/src/bin/day12.rs Normal file
View file

@ -0,0 +1,164 @@
use aoc23::prelude::*;
use derive_builder::Builder;
use itertools::Itertools;
use rayon::prelude::*;
use std::{collections::HashMap, str::FromStr, time::Instant};
static DAY: u8 = 12;
fn count_possibilities(
layout: &str,
contiguous: &[usize],
cache: &mut HashMap<(String, Vec<usize>), usize>,
) -> usize {
if layout.chars().count() == 0 {
return if contiguous.len() == 0 {
// println!("\tLine is variant: {layout}");
1
} else {
0
};
}
let cache_key = (layout.to_owned(), contiguous.to_vec());
if let Some(&result) = cache.get(&cache_key) {
return result;
}
// Remove leading dots
let result = if layout.starts_with('.') {
count_possibilities(layout.to_owned().trim_matches('.'), contiguous, cache)
// Try both options for ?
} else if layout.starts_with('?') {
count_possibilities(&layout.to_owned().replacen('?', &".", 1), contiguous, cache)
+ count_possibilities(&layout.to_owned().replacen('?', &"#", 1), contiguous, cache)
// If group, check if it matches
} else if layout.starts_with("#") {
// no groups left to match || not enough # to make the group || not all of this
// group are #
if contiguous.len() == 0
|| layout.chars().count() < contiguous[0]
|| layout.chars().take(contiguous[0]).any(|c| c == '.')
{
0
} else if contiguous.len() > 1 {
if layout.len() < contiguous[0] + 1
|| layout.chars().skip(contiguous[0]).next().unwrap() == '#'
{
0
} else {
count_possibilities(
&layout.chars().skip(contiguous[0] + 1).collect::<String>(),
&contiguous[1..],
cache,
)
}
} else {
count_possibilities(
&layout.chars().skip(contiguous[0]).collect::<String>(),
&contiguous[1..],
cache,
)
}
} else {
panic!("Should not get here.")
};
cache.insert(cache_key, result);
result
}
fn part1(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let answer = input
.trim()
.lines()
.enumerate()
.par_bridge()
.map(|(idx, line)| {
let mut split = line.trim().split_whitespace();
let layout = split.next().unwrap();
let contiguous = split
.next()
.unwrap()
.split(',')
.map(|s| s.parse::<usize>().unwrap())
.collect_vec();
let count = count_possibilities(&layout, &contiguous, &mut HashMap::new());
// println!("Line {idx}: {line} = {count}");
count
})
.sum();
// output
println!("part 1: {answer}\t[total: {:?}]", start.elapsed());
Ok(answer)
}
fn part2(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let answer = input
.trim()
.lines()
.enumerate()
.par_bridge()
.map(|(idx, line)| {
let mut split = line.trim().split_whitespace();
let layout = split.next().unwrap();
let contiguous = split
.next()
.unwrap()
.split(',')
.map(|s| s.parse::<usize>().unwrap())
.collect_vec();
let layout = std::iter::repeat(layout).take(5).join("?");
let contiguous = std::iter::repeat(contiguous)
.take(5)
.flatten()
.collect_vec();
let count = count_possibilities(&layout, &contiguous, &mut HashMap::new());
// println!("Line {idx}: {line} = {count}");
count
})
.sum();
// output
println!("part 2: {answer}\t[total: {:?}]", start.elapsed());
Ok(answer)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("Day {DAY}");
println!("=====");
let input = utils::aoc::get_puzzle_input(DAY).await?;
part1(input.clone())?;
part2(input.clone())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
static DATA: &'static str = "???.### 1,1,3
.??..??...?##. 1,1,3
?#?#?#?#?#?#?#? 1,3,1,6
????.#...#... 4,1,1
????.######..#####. 1,6,5
?###???????? 3,2,1";
#[test]
fn test_part_1() -> Result<()> {
assert_eq!(part1(DATA.to_owned())?, 21);
Ok(())
}
#[test]
fn test_part_2() -> Result<()> {
assert_eq!(part2(DATA.to_owned())?, 525152);
Ok(())
}
}

249
2023/src/bin/day13.rs Normal file
View file

@ -0,0 +1,249 @@
use aoc23::prelude::*;
use derive_builder::Builder;
use itertools::Itertools;
use rayon::prelude::*;
use std::{fmt::Debug, str::FromStr, time::Instant};
static DAY: u8 = 13;
#[derive(Clone)]
struct Grid {
cells: Vec<Vec<bool>>,
}
impl Debug for Grid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for row in &self.cells {
f.write_str("\t");
for b in row {
f.write_str(if *b { "#" } else { "." });
}
f.write_str("\n");
}
Ok(())
}
}
type Split<'a> = (&'a [Vec<bool>], &'a [Vec<bool>]);
impl Grid {
fn new(s: &str) -> Result<Self> {
Ok(Self {
cells: s
.trim()
.lines()
.map(|l| l.chars().map(|c| c == '#').collect_vec())
.collect(),
})
}
fn all_vertical_splits(&self) -> Vec<Split> {
let mut splits = vec![];
for i in 1..self.cells.len() {
splits.push(self.vertical_split(i));
}
splits
}
fn vertical_split(&self, index: usize) -> Split {
(&self.cells[0..index], &self.cells[index..])
}
fn is_vertical_mirror(top: &[Vec<bool>], bot: &[Vec<bool>]) -> bool {
let tl = top.len();
let bl = bot.len();
for i in 0..usize::min(tl, bl) {
let t = &top[tl - 1 - i];
let b = &bot[i];
// println!("\tChecking {i}: {t:?} <=> {b:?} || {}", t == b);
if t != b {
return false;
}
}
true
}
fn is_vertical_mirror_with_smudge(top: &[Vec<bool>], bot: &[Vec<bool>]) -> bool {
let tl = top.len();
let bl = bot.len();
let mut claimed_smudge = false;
for i in 0..usize::min(tl, bl) {
let t = &top[tl - 1 - i];
let b = &bot[i];
// println!( "\tChecking {i}: {t:?} <=> {b:?} || {} || {}", t == b, claimed_smudge
// );
if t != b {
if !claimed_smudge && t.iter().zip(b.iter()).filter(|(z, x)| z != x).count() == 1 {
claimed_smudge = true
} else {
return false;
}
}
}
// FFS I had `true` here but all mirrors have EXACTLY ONE smudge, not at most
// one...
claimed_smudge
}
fn clone_and_rotate(&self) -> Self {
Self {
cells: (0..self.cells[0].len())
.map(|i| self.cells.iter().map(|row| row[i]).collect())
.collect(),
}
}
}
fn part1(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let grids = input
.split("\n\n")
.map(|g| Grid::new(g).unwrap())
.collect_vec();
let parsed_time = start.elapsed();
// algo
let a_start = Instant::now();
let answer: usize = grids
.iter()
.map(|grid| {
let mut vertical_mirror_at = None;
for (idx, (top, bot)) in grid.all_vertical_splits().iter().enumerate() {
if Grid::is_vertical_mirror(top, bot) {
vertical_mirror_at = Some(idx);
break;
}
}
let mut horizontal_mirror_at = None;
if vertical_mirror_at.is_none() {
let rotated = grid.clone_and_rotate();
for (idx, (top, bot)) in rotated.all_vertical_splits().iter().enumerate() {
if Grid::is_vertical_mirror(top, bot) {
horizontal_mirror_at = Some(idx);
break;
}
}
}
// index + 1 = number of rows to the left or above the mirror index
let value = horizontal_mirror_at
.map(|h| h + 1)
.or(vertical_mirror_at.map(|v| (v + 1) * 100))
.unwrap_or(0);
// println!( "Grid: vertical mirror at: {vertical_mirror_at:?}\thorizontal at:
// {horizontal_mirror_at:?} == {value}" );
value
})
.sum();
let algo_time = a_start.elapsed();
// output
println!("part 1: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
fn part2(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let grids = input
.split("\n\n")
.map(|g| Grid::new(g).unwrap())
.collect_vec();
let parsed_time = start.elapsed();
// algo
let a_start = Instant::now();
let answer: usize = grids
.iter()
.map(|grid| {
// println!("GRID: {grid:?}");
let mut vertical_mirror_at = None;
for (idx, (top, bot)) in grid.all_vertical_splits().iter().enumerate() {
if Grid::is_vertical_mirror_with_smudge(top, bot) {
// println!("GOT VERT: {idx}");
vertical_mirror_at = Some(idx);
break;
}
}
let mut horizontal_mirror_at = None;
if vertical_mirror_at.is_none() {
let rotated = grid.clone_and_rotate();
// println!("ROTATED: {grid:?}");
for (idx, (top, bot)) in rotated.all_vertical_splits().iter().enumerate() {
if Grid::is_vertical_mirror_with_smudge(top, bot) {
horizontal_mirror_at = Some(idx);
break;
}
}
}
// index + 1 = number of rows to the left or above the mirror index
let value = horizontal_mirror_at
.map(|h| h + 1)
.or(vertical_mirror_at.map(|v| (v + 1) * 100))
.unwrap_or(0);
// println!( "Grid: vertical mirror at: {vertical_mirror_at:?}\thorizontal at:
// {horizontal_mirror_at:?} == {value}" );
value
})
.sum();
let algo_time = a_start.elapsed();
// output
println!("part 2: {answer} == 31836\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("Day {DAY}");
println!("=====");
let input = utils::aoc::get_puzzle_input(DAY).await?;
part1(input.clone())?;
part2(input.clone())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
static DATA: &'static str = "#.##..##.
..#.##.#.
##......#
##......#
..#.##.#.
..##..##.
#.#.##.#.
#...##..#
#....#..#
..##..###
#####.##.
#####.##.
..##..###
#....#..#";
#[test]
fn test_part_1() -> Result<()> {
assert_eq!(part1(DATA.to_owned())?, 405);
Ok(())
}
#[test]
fn test_part_2() -> Result<()> {
assert_eq!(part2(DATA.to_owned())?, 400);
Ok(())
}
}

234
2023/src/bin/day14.rs Normal file
View file

@ -0,0 +1,234 @@
use aoc23::prelude::*;
use grid::Grid;
use itertools::Itertools;
use std::{collections::HashMap, fmt::Display, time::Instant};
static DAY: u8 = 14;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
enum Rock {
Round,
Square,
None,
}
impl Rock {
fn from_char(char: char) -> Self {
match char {
'O' => Rock::Round,
'#' => Rock::Square,
'.' => Rock::None,
_ => panic!("unknown rock type"),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
struct Dish {
grid: Grid<Rock>,
}
impl std::hash::Hash for Dish {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
for r in self.grid.iter() {
r.hash(state);
}
}
}
impl Dish {
fn new(s: &str) -> Result<Self> {
let mut grid: Option<Grid<Rock>> = None;
for line in s.trim().lines() {
let rocks = line.chars().map(Rock::from_char).collect_vec();
let len = rocks.len();
match grid {
Some(ref mut grid) => {
grid.push_row(rocks);
}
None => {
grid = Some(Grid::from_vec(rocks, len));
}
}
}
let grid = grid.unwrap();
Ok(Dish { grid })
}
fn tilt_north(mut self) -> Self {
for col_idx in 0..self.grid.cols() {
let column = self.grid.iter_col(col_idx);
let column_length = column.len();
// start,end,round_count ranges for rolling zones
let mut rolling_ranges: Vec<(usize, usize, usize)> = vec![];
let mut in_range: Option<(usize, usize)> = None;
for (row_idx, rock) in column
.enumerate()
// Add one at the end so we close out our range, it should be square so it is
// treated as a wall
.chain(std::iter::once((column_length, &Rock::Square)))
{
if let Rock::Square = rock {
if let Some((start, round_count)) = in_range {
// rolling range it only including round or none, not this wall.
rolling_ranges.push((start, row_idx - 1, round_count));
in_range = None;
}
} else {
let is_round = matches!(rock, Rock::Round);
if let Some((_, ref mut round_count)) = in_range {
if is_round {
*round_count += 1;
}
} else {
in_range = Some((row_idx, if is_round { 1 } else { 0 }));
}
}
}
// Go through ranges and set them to tilt north.
rolling_ranges.iter().for_each(|(start, end, round_count)| {
let mut round_remaining = *round_count;
for i in *start..=*end {
if let Some(rock) = self.grid.get_mut(i, col_idx) {
if round_remaining > 0 {
*rock = Rock::Round;
round_remaining -= 1;
} else {
*rock = Rock::None;
}
} else {
panic!("no rock");
}
}
});
}
self
}
fn calculate_north_load(&self) -> usize {
let len = self.grid.rows();
self.grid
.iter_rows()
.enumerate()
.map(|(row_idx, row)| {
let multiplier = len - row_idx;
multiplier * row.filter(|rock| matches!(rock, Rock::Round)).count()
})
.sum()
}
}
impl Display for Dish {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for row in self.grid.iter_rows() {
for rock in row {
match rock {
Rock::Round => f.write_str("O"),
Rock::Square => f.write_str("#"),
Rock::None => f.write_str("."),
}?;
}
f.write_str("\n")?;
}
Ok(())
}
}
fn part1(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let mut dish = Dish::new(&input)?;
dish = dish.tilt_north();
let parsed_time = start.elapsed();
// algo
let a_start = Instant::now();
let answer = dish.calculate_north_load();
let algo_time = a_start.elapsed();
// output
println!("part 1: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
fn part2(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let mut dish = Dish::new(&input)?;
let parsed_time = start.elapsed();
// algo
let a_start = Instant::now();
let cycles = 1000000000;
let mut seen = HashMap::new();
let mut scores = vec![];
let mut answer = 0;
for i in 0..cycles {
for _ in 0..4 {
dish = dish.tilt_north();
dish.grid.rotate_right();
}
scores.push(dish.calculate_north_load());
if let Some(repeated_index) = seen.get(&dish) {
// Figure out final score based on sliding window of known scores within the
// repeated pattern:
//
// (cycles - repeated index) gives us the remaining times we need to run through.
// We then get the remainder of that divided by the difference of the current
// index minus all seen (total repeating count).
answer = scores
[repeated_index - 1 + (cycles - repeated_index) % (seen.len() - repeated_index)];
break;
}
seen.insert(dish.clone(), i);
}
let algo_time = a_start.elapsed();
// output
println!("part 2: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("Day {DAY}");
println!("=====");
let input = utils::aoc::get_puzzle_input(DAY).await?;
part1(input.clone())?;
part2(input.clone())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
static DATA: &'static str = "O....#....
O.OO#....#
.....##...
OO.#O....O
.O.....O#.
O.#..O.#.#
..O..#O..O
.......O..
#....###..
#OO..#....";
#[test]
fn test_part_1() -> Result<()> {
assert_eq!(part1(DATA.to_owned())?, 136);
Ok(())
}
#[test]
fn test_part_2() -> Result<()> {
assert_eq!(part2(DATA.to_owned())?, 64);
Ok(())
}
}

190
2023/src/bin/day15.rs Normal file
View file

@ -0,0 +1,190 @@
use aoc23::prelude::*;
use derive_builder::Builder;
use itertools::Itertools;
use rayon::prelude::*;
use std::{str::FromStr, time::Instant};
static DAY: u8 = 15;
#[derive(Debug, Clone, Default, Eq, PartialEq)]
struct Seq {
chars: Vec<char>,
}
impl Seq {
fn new(s: &str) -> Result<Self> {
Ok(Self {
chars: s.trim().chars().filter(|c| !c.is_whitespace()).collect(),
})
}
fn hash(&self) -> usize {
self.chars.iter().fold(0, |acc, char| {
let mut value = acc;
value += *char as usize;
value *= 17;
value = value % 256;
value
})
}
}
#[derive(Debug, Clone)]
enum Operation {
Remove,
// focal length
Insert(usize),
}
#[derive(Debug, Clone)]
struct LensOp {
label: Seq,
box_index: usize,
operation: Operation,
}
impl LensOp {
fn new(value: &Seq) -> Self {
let label = Seq {
chars: value
.chars
.iter()
.take_while(|c| c != &&'=' && c != &&'-')
.map(|c| *c)
.collect(),
};
let box_index = label.hash();
let mut op = value.chars.iter().skip(label.chars.len());
Self {
label,
box_index,
operation: match op.next().unwrap() {
'-' => Operation::Remove,
'=' => Operation::Insert(op.next().unwrap().to_digit(10).unwrap() as usize),
o => panic!("Unknown op {o}"),
},
}
}
}
#[derive(Debug, Clone, Default)]
struct LensBox {
lenses: Vec<LensOp>,
}
fn part1(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let sequences: Vec<Seq> = input.split(',').map(|seq| Seq::new(seq)).try_collect()?;
let parsed_time = start.elapsed();
// algo
let a_start = Instant::now();
let answer = sequences.par_iter().map(Seq::hash).sum();
let algo_time = a_start.elapsed();
// output
println!("part 1: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
fn part2(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let lens_ops: Vec<LensOp> = input
.split(',')
.map(|seq| Seq::new(seq).unwrap())
.map(|s| LensOp::new(&s))
.collect();
let mut lens_boxes: Vec<LensBox> = std::iter::repeat_with(|| LensBox::default())
.take(256)
.collect();
let parsed_time = start.elapsed();
// algo
let a_start = Instant::now();
for lens_op in lens_ops {
if let Some(lens_box) = lens_boxes.get_mut(lens_op.box_index) {
match lens_op.operation {
Operation::Remove => lens_box.lenses.retain(|lop| lop.label != lens_op.label),
Operation::Insert(_) => {
if let Some(existing) = lens_box
.lenses
.iter_mut()
.find(|lop| lop.label == lens_op.label)
{
*existing = lens_op;
} else {
lens_box.lenses.push(lens_op);
}
}
}
}
}
let answer = lens_boxes
.iter()
.enumerate()
.map(|(box_idx, lens_box)| {
lens_box
.lenses
.iter()
.enumerate()
.map(|(lens_idx, lens)| {
if let Operation::Insert(focal_length) = lens.operation {
(box_idx + 1) * (lens_idx + 1) * focal_length
} else {
panic!("How did a removal lens get in there?");
}
})
.sum::<usize>()
})
.sum();
let algo_time = a_start.elapsed();
// output
println!("part 2: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("Day {DAY}");
println!("=====");
let input = utils::aoc::get_puzzle_input(DAY).await?;
part1(input.clone())?;
part2(input.clone())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part_1_a() -> Result<()> {
assert_eq!(part1("HASH".to_owned())?, 52);
Ok(())
}
#[test]
fn test_part_1_b() -> Result<()> {
assert_eq!(
part1("rn=1,cm-,qp=3,cm=2,qp-,pc=4,ot=9,ab=5,pc-,pc=6,ot=7".to_owned())?,
1320
);
Ok(())
}
#[test]
fn test_part_2() -> Result<()> {
assert_eq!(
part2("rn=1,cm-,qp=3,cm=2,qp-,pc=4,ot=9,ab=5,pc-,pc=6,ot=7".to_owned())?,
145
);
Ok(())
}
}

256
2023/src/bin/day16.rs Normal file
View file

@ -0,0 +1,256 @@
use aoc23::prelude::*;
use derive_builder::Builder;
use grid::Grid;
use itertools::Itertools;
use rayon::prelude::*;
use std::{collections::HashSet, ops::Add, str::FromStr, time::Instant};
static DAY: u8 = 16;
#[derive(Debug, Clone, Default)]
enum Tile {
#[default]
Empty,
VerticalSplit,
HorizontalSplit,
RMirror,
LMirror,
}
impl Tile {
fn new_tile_grid(input: &str) -> Grid<Self> {
let mut grid = Grid::new(0, 0);
for line in input.lines() {
let tiles = line
.trim()
.chars()
.map(|c| match c {
'.' => Self::Empty,
'|' => Self::VerticalSplit,
'-' => Self::HorizontalSplit,
'/' => Self::RMirror,
'\\' => Self::LMirror,
unknown => panic!("unknown tile: {unknown}"),
})
.collect_vec();
grid.push_row(tiles);
}
grid
}
fn direction_to(&self, direction: &Dir) -> Vec<Dir> {
match self {
Tile::Empty => vec![direction.clone()],
Tile::VerticalSplit => match direction {
Dir::Up | Dir::Down => vec![direction.clone()],
Dir::Right | Dir::Left => vec![Dir::Up, Dir::Down],
},
Tile::HorizontalSplit => match direction {
Dir::Right | Dir::Left => vec![direction.clone()],
Dir::Up | Dir::Down => vec![Dir::Right, Dir::Left],
},
Tile::RMirror => vec![match direction {
Dir::Up => Dir::Right,
Dir::Right => Dir::Up,
Dir::Down => Dir::Left,
Dir::Left => Dir::Down,
}],
Tile::LMirror => vec![match direction {
Dir::Up => Dir::Left,
Dir::Right => Dir::Down,
Dir::Down => Dir::Right,
Dir::Left => Dir::Up,
}],
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
enum Dir {
Up,
Right,
Down,
Left,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
struct Beam(usize, usize, Dir);
impl Beam {
fn add_dir(&self, dir: &Dir) -> Self {
Self(
match dir {
Dir::Up => self.0.saturating_sub(1),
Dir::Down => self.0.saturating_add(1),
_ => self.0,
},
match dir {
Dir::Left => self.1.saturating_sub(1),
Dir::Right => self.1.saturating_add(1),
_ => self.1,
},
dir.clone(),
)
}
fn next_path(&self, tiles: &Grid<Tile>) -> Vec<Beam> {
tiles
.get(self.0, self.1)
.map(|tile| tile.direction_to(&self.2))
.map(|dirs| {
dirs.iter()
.map(|dir| self.add_dir(dir))
.filter(|beam| tiles.get(beam.0, beam.1).is_some())
.collect_vec()
})
.unwrap_or(vec![])
}
fn part2_starts(grid: &Grid<Tile>) -> Vec<Self> {
let mut starts = vec![];
let rows = grid.rows();
let cols = grid.cols();
starts.push(Beam(0, 0, Dir::Right));
starts.push(Beam(0, 0, Dir::Down));
starts.push(Beam(0, cols - 1, Dir::Down));
starts.push(Beam(0, cols - 1, Dir::Left));
starts.push(Beam(rows - 1, cols - 1, Dir::Left));
starts.push(Beam(rows - 1, cols - 1, Dir::Up));
starts.push(Beam(rows - 1, 0, Dir::Up));
starts.push(Beam(rows - 1, 0, Dir::Right));
for row in 1..rows - 2 {
starts.push(Beam(row, 0, Dir::Right));
starts.push(Beam(row, cols - 1, Dir::Left));
}
for col in 1..cols - 2 {
starts.push(Beam(0, col, Dir::Down));
starts.push(Beam(rows - 1, col, Dir::Up));
}
starts
}
}
fn part1(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let grid = Tile::new_tile_grid(&input);
let parsed_time = start.elapsed();
// algo
let a_start = Instant::now();
let mut energized = HashSet::new();
let mut full_beam = HashSet::new();
let mut beam_path = vec![Beam(0, 0, Dir::Right)];
while let Some(beam) = beam_path.pop() {
energized.insert((beam.0, beam.1));
full_beam.insert(beam.clone());
beam.next_path(&grid)
.into_iter()
.filter(|b| !full_beam.contains(b))
.for_each(|new_beam| {
beam_path.push(new_beam);
});
}
let answer = energized.len();
let algo_time = a_start.elapsed();
// output
println!("part 1: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
fn part2(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let grid = Tile::new_tile_grid(&input);
let parsed_time = start.elapsed();
// algo
let a_start = Instant::now();
let answer = Beam::part2_starts(&grid)
.into_iter()
.map(|start| {
let mut energized = HashSet::new();
let mut full_beam = HashSet::new();
let mut beam_path = vec![start];
while let Some(beam) = beam_path.pop() {
energized.insert((beam.0, beam.1));
full_beam.insert(beam.clone());
beam.next_path(&grid)
.into_iter()
.filter(|b| !full_beam.contains(b))
.for_each(|new_beam| {
beam_path.push(new_beam);
});
}
energized.len()
})
.max()
.unwrap();
let algo_time = a_start.elapsed();
// output
println!("part 2: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("Day {DAY}");
println!("=====");
let input = utils::aoc::get_puzzle_input(DAY).await?;
part1(input.clone())?;
part2(input.clone())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part_1() -> Result<()> {
assert_eq!(
part1(
r#".|...\....
|.-.\.....
.....|-...
........|.
..........
.........\
..../.\\..
.-.-/..|..
.|....-|.\
..//.|...."#
.to_owned(),
)?,
46
);
Ok(())
}
#[test]
fn test_part_2() -> Result<()> {
assert_eq!(
part2(
r#".|...\....
|.-.\.....
.....|-...
........|.
..........
.........\
..../.\\..
.-.-/..|..
.|....-|.\
..//.|...."#
.to_owned(),
)?,
51
);
Ok(())
}
}

259
2023/src/bin/day17.rs Normal file
View file

@ -0,0 +1,259 @@
use aoc23::prelude::*;
use derive_builder::Builder;
use grid::Grid;
use itertools::Itertools;
use pathfinding::prelude::dijkstra;
use rayon::prelude::*;
use std::{
collections::{HashMap, HashSet},
str::FromStr,
time::Instant,
};
static DAY: u8 = 17;
type Pos = (usize, usize);
fn delta_pos(from: &Pos, to: &Pos) -> (isize, isize) {
return (
to.0 as isize - from.0 as isize,
to.1 as isize - from.1 as isize,
);
}
fn grid(input: &str) -> Grid<usize> {
let mut grid = Grid::new(0, 0);
for line in input.lines() {
let weights = line
.trim()
.chars()
.map(|c| c.to_digit(10).expect("invalid weight") as usize)
.collect_vec();
grid.push_row(weights);
}
grid
}
struct Graph {
edges: HashMap<Pos, HashMap<Pos, usize>>,
}
impl Graph {
fn new_from_weighted_grid(weights: &Grid<usize>) -> Self {
let mut edges = HashMap::new();
for r in 0..weights.rows() {
for c in 0..weights.cols() {
let pos = (r, c);
for (n_pos, n_weight) in weights.neighbors_orthogonal(pos) {
edges
.entry(pos)
.or_insert_with(|| HashMap::new())
.insert(n_pos, *n_weight);
}
}
}
Self { edges }
}
fn shortest_path_with_movement_limit(
&self,
start: Pos,
end: Pos,
move_limit_min: usize,
move_limit_max: usize,
) -> usize {
// TODO min move limit?
let valid_move_in_limit =
|delta: (isize, isize), since: &Pos, predecessors: &HashMap<Pos, Option<Pos>>| {
let mut current = since;
let mut previous = vec![];
for i in 0..move_limit_max {
if let Some(Some(prev)) = predecessors.get(current) {
let previous_movement = delta_pos(prev, current);
if previous_movement != delta {
// movement is different we are okay
break;
} else if i == (move_limit_max - 1) {
// movement is the same and we are not at the movement limit, break out of this
// path, not allowed
previous.push(*current);
return (false, previous);
} else {
// check the prior element
previous.push(*current);
current = prev;
}
} else {
// no path we are good
break;
}
}
(true, previous)
};
// based on https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm
let mut queue: HashMap<Pos, usize> = HashMap::new();
let mut distances: HashMap<Pos, usize> = HashMap::new();
let mut predecessors: HashMap<Pos, Option<Pos>> = HashMap::new();
self.edges.iter().for_each(|(pos, _)| {
distances.insert(*pos, usize::MAX);
predecessors.insert(*pos, None);
queue.insert(*pos, 0);
});
distances.insert(start, 0);
// go from start node and check all other nodes
while queue.len() > 0 {
let ((current, backtrack), distance) = queue
.iter()
.map(|item| (item, distances.get(item.0).unwrap()))
.sorted_by(|(_, distance_a), (_, distance_b)| distance_a.cmp(distance_b))
.next()
.unwrap();
let current = *current;
let distance = *distance;
queue.remove(&current);
// Can terminate dijkstra early if we hit our end value.
if current == end {
break;
}
self.edges
.get(&current)
.unwrap()
.iter()
.filter(|(neighbor, n_dist)| {
let movement = delta_pos(&current, neighbor);
let in_queue = queue.contains_key(neighbor);
let is_not_backwards = {
let pred = predecessors.get(&current).unwrap().unwrap();
let pred_movement = delta_pos(&pred, &current);
// not 180 degree turn
!(pred_movement.0 * -1 == movement.0 && pred_movement.1 * -1 == movement.1)
};
let (valid_movement, _) =
valid_move_in_limit(movement, &current, &predecessors);
if in_queue && is_not_backwards && !valid_movement {
// if not valid movement in limits then backtrack and try those again ... todo
// TODO Do this thing?
}
return in_queue && valid_movement;
})
.for_each(|(neighbor, n_dist)| {
let current_n_dist = *distances.get(neighbor).unwrap();
let alt = distance + n_dist;
if alt < current_n_dist {
distances.insert(*neighbor, alt);
predecessors.insert(*neighbor, Some(current.clone()));
}
});
}
*distances.get(&end).unwrap()
}
}
fn part1(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let weights = grid(&input);
let graph = Graph::new_from_weighted_grid(&weights);
let root = (0, 0);
let goal = (weights.rows() - 1, weights.cols() - 1);
let parsed_time = start.elapsed();
// algo
let a_start = Instant::now();
let answer = graph.shortest_path_with_movement_limit(root, goal, 1, 3);
let algo_time = a_start.elapsed();
// output
println!("part 1: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
fn part2(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let weights = grid(&input);
let graph = Graph::new_from_weighted_grid(&weights);
let root = (0, 0);
let goal = (weights.rows() - 1, weights.cols() - 1);
let parsed_time = start.elapsed();
// algo
let a_start = Instant::now();
let mut answer = 0;
let algo_time = a_start.elapsed();
// output
println!("part 2: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("Day {DAY}");
println!("=====");
let input = utils::aoc::get_puzzle_input(DAY).await?;
part1(input.clone())?;
part2(input.clone())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part_1() -> Result<()> {
assert_eq!(
part1(
"2413432311323
3215453535623
3255245654254
3446585845452
4546657867536
1438598798454
4457876987766
3637877979653
4654967986887
4564679986453
1224686865563
2546548887735
4322674655533"
.to_owned(),
)?,
102
);
Ok(())
}
#[test]
#[ignore]
fn test_part_2() -> Result<()> {
assert_eq!(
part2(
"2413432311323
3215453535623
3255245654254
3446585845452
4546657867536
1438598798454
4457876987766
3637877979653
4654967986887
4564679986453
1224686865563
2546548887735
4322674655533"
.to_owned(),
)?,
0
);
Ok(())
}
}

180
2023/src/bin/day18.rs Normal file
View file

@ -0,0 +1,180 @@
use aoc23::prelude::*;
use derive_builder::Builder;
use geo::{Area, Coord, Coordinate, LineString, Polygon};
use itertools::Itertools;
use pathfinding::num_traits::{Float, Signed};
use rayon::prelude::*;
use std::{collections::HashSet, str::FromStr, time::Instant};
static DAY: u8 = 18;
#[derive(Debug, Clone)]
struct DigCommand {
direction: char,
distance: i32,
color: String,
}
#[derive(Debug, Clone)]
struct DigPlan {
commands: Vec<DigCommand>,
}
impl DigPlan {
fn new(s: &str) -> Result<Self> {
let commands = s
.lines()
.map(|line| {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() != 3 {
return Err("Invalid command format".into());
}
Ok(DigCommand {
direction: parts[0].chars().next().ok_or("Invalid direction")?,
distance: parts[1].parse()?,
color: parts[2].to_string(),
})
})
.collect::<Result<Vec<DigCommand>>>()?;
Ok(Self { commands })
}
fn calculate_poly(&self) -> geo::Polygon {
let mut position = (0, 0);
let mut visited: Vec<(i32, i32)> = vec![position];
for command in &self.commands {
let (dx, dy) = match command.direction {
'U' => (0, -1),
'D' => (0, 1),
'L' => (-1, 0),
'R' => (1, 0),
_ => unreachable!(),
};
for _ in 0..command.distance {
position.0 += dx;
position.1 += dy;
visited.push(position);
}
}
if *visited.last().unwrap() != *visited.first().unwrap() {
panic!("expected a closed loop")
}
if visited.iter().sorted_unstable().dedup().count() != visited.len() - 1 {
panic!("Loop intersected with itself more than the 1 allowed time for the start node");
}
println!("visited {}: {visited:?}", visited.len());
let coordinates: Vec<Coordinate<f64>> = visited
.into_iter()
.map(|(x, y)| Coordinate {
x: x as f64,
y: y as f64,
})
.collect_vec();
Polygon::new(LineString::from(coordinates), vec![])
// visited.len()
}
}
fn part1(input: String) -> Result<usize> {
println!("Input\n====\n{input}\n\n");
// parse
let start = Instant::now();
let dig_plan = DigPlan::new(&input)?;
let parsed_time = start.elapsed();
// algo
let a_start = Instant::now();
let polygon = dig_plan.calculate_poly();
let answer = polygon.signed_area().abs();
let algo_time = a_start.elapsed();
// output
println!("part 1: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer as usize)
}
fn part2(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let dig_plan = DigPlan::new(&input)?;
let parsed_time = start.elapsed();
// algo
let a_start = Instant::now();
let mut answer = 0;
let algo_time = a_start.elapsed();
// output
println!("part 2: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("Day {DAY}");
println!("=====");
let input = utils::aoc::get_puzzle_input(DAY).await?;
part1(input.clone())?;
part2(input.clone())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part_1() -> Result<()> {
assert_eq!(
part1(
"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)"
.to_owned(),
)?,
62
);
Ok(())
}
#[test]
fn test_part_2() -> Result<()> {
assert_eq!(
part2(
"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)"
.to_owned(),
)?,
0
);
Ok(())
}
}

425
2023/src/bin/day19.rs Normal file
View file

@ -0,0 +1,425 @@
use aoc23::prelude::*;
use itertools::Itertools;
use regex::Regex;
use static_init::dynamic;
use std::{collections::HashMap, time::Instant};
static DAY: u8 = 19;
#[dynamic]
static RE_PARSE_PART: Regex = Regex::new(r"x=(?<x>\d+).*m=(?<m>\w+).*a=(?<a>\d+).*s=(?<s>\d+)")
.expect("re_parse_part invalid");
#[dynamic]
static RE_PARSE_WORKFLOW: Regex =
Regex::new(r"^(?<name>\w+)\{(?<ops>.*)}").expect("re_parse_workflow invalid");
#[derive(Debug, Clone)]
struct Part {
x: usize,
m: usize,
a: usize,
s: usize,
}
impl Part {
fn new_from_line(line: &str) -> Self {
let re = RE_PARSE_PART.captures(line).expect("part does not match");
Self {
x: re.name("x").unwrap().as_str().parse().unwrap(),
m: re.name("m").unwrap().as_str().parse().unwrap(),
a: re.name("a").unwrap().as_str().parse().unwrap(),
s: re.name("s").unwrap().as_str().parse().unwrap(),
}
}
}
#[derive(Debug, Clone)]
enum PartCategory {
X,
M,
A,
S,
}
impl PartCategory {
fn new_from_char(c: &char) -> Self {
match c {
'x' => Self::X,
'm' => Self::M,
'a' => Self::A,
's' => Self::S,
_ => panic!("wee woo"),
}
}
fn value_of(&self, part: &Part) -> usize {
match self {
PartCategory::X => part.x,
PartCategory::M => part.m,
PartCategory::A => part.a,
PartCategory::S => part.s,
}
}
}
#[derive(Debug, Clone)]
enum BasicFlowOp {
Accept,
Reject,
Route(String),
}
impl BasicFlowOp {
fn new_from_line(line: &str) -> Self {
match line {
"A" => Self::Accept,
"R" => Self::Reject,
route => Self::Route(route.to_owned()),
}
}
fn as_flow_op(&self) -> FlowOp {
match self {
BasicFlowOp::Accept => FlowOp::Accept,
BasicFlowOp::Reject => FlowOp::Reject,
BasicFlowOp::Route(to) => FlowOp::Route(to.to_owned()),
}
}
fn part2_to(&self) -> String {
match self {
BasicFlowOp::Accept => "A".to_owned(),
BasicFlowOp::Reject => "R".to_owned(),
BasicFlowOp::Route(to) => to.to_owned(),
}
}
}
#[derive(Debug, Clone)]
enum FlowOp {
LessThan(PartCategory, usize, BasicFlowOp),
GreaterThan(PartCategory, usize, BasicFlowOp),
Accept,
Reject,
Route(String),
}
impl FlowOp {
fn new_from_line(line: &str) -> Self {
let split = line.split([':', ',', '>', '<']).collect_vec();
if split.len() == 1 {
BasicFlowOp::new_from_line(split.get(0).unwrap()).as_flow_op()
} else {
if line.contains('>') {
Self::GreaterThan(
PartCategory::new_from_char(&split.get(0).unwrap().chars().next().unwrap()),
split.get(1).unwrap().parse().unwrap(),
BasicFlowOp::new_from_line(split.get(2).unwrap()),
)
} else {
Self::LessThan(
PartCategory::new_from_char(&split.get(0).unwrap().chars().next().unwrap()),
split.get(1).unwrap().parse().unwrap(),
BasicFlowOp::new_from_line(split.get(2).unwrap()),
)
}
}
}
}
#[derive(Debug, Clone)]
struct Workflow {
name: String,
flow: Vec<FlowOp>,
}
impl Workflow {
fn new_from_line(line: &str) -> Self {
let re = RE_PARSE_WORKFLOW
.captures(line)
.expect("workflow does not match");
Self {
name: re.name("name").unwrap().as_str().to_owned(),
flow: re
.name("ops")
.unwrap()
.as_str()
.split(',')
.map(FlowOp::new_from_line)
.collect(),
}
}
}
fn parse_system(input: &str) -> (HashMap<String, Workflow>, Vec<Part>) {
let split = input.split("\n\n").collect_vec();
(
split
.get(0)
.unwrap()
.lines()
.map(Workflow::new_from_line)
.map(|w| (w.name.clone(), w))
.collect(),
split
.get(1)
.unwrap()
.lines()
.map(Part::new_from_line)
.collect(),
)
}
fn check_part_accepted(part: &Part, workflows: &HashMap<String, Workflow>) -> bool {
let mut current_workflow = workflows.get("in");
'flows: while let Some(workflow) = current_workflow {
current_workflow = None;
for flow_op in workflow.flow.iter() {
let basic_op = match flow_op {
FlowOp::LessThan(cat, num, bop) => {
if cat.value_of(part) < *num {
bop.as_flow_op()
} else {
continue;
}
}
FlowOp::GreaterThan(cat, num, bop) => {
if cat.value_of(part) > *num {
bop.as_flow_op()
} else {
continue;
}
}
op => op.clone(),
};
match basic_op {
FlowOp::Accept => return true,
FlowOp::Reject => return false,
FlowOp::Route(to) => {
current_workflow = workflows.get(&to);
continue 'flows;
}
_ => panic!("not basic"),
}
}
}
panic!("why are we here?")
}
fn part1(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let (workflows, parts) = parse_system(&input);
let parsed_time = start.elapsed();
// algo
let a_start = Instant::now();
let answer = parts
.iter()
.filter_map(|p| check_part_accepted(p, &workflows).then(|| p.x + p.m + p.a + p.s))
.sum();
let algo_time = a_start.elapsed();
// output
println!("part 1: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
fn part2(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let (workflows, _) = parse_system(&input);
let parsed_time = start.elapsed();
// algo
let a_start = Instant::now();
// LOL yeah right... no no no to brute force on this one
//
// ```
// let bar = ProgressBar::new(4000 * 4000 * 4000 * 4000);
// let answer = iproduct!(1..=4000usize, 1..=4000usize, 1..=4000usize, 1..=4000usize)
// .par_bridge()
// .map(|(x, m, a, s)| {
// bar.inc(1);
// Part { x, m, a, s }
// })
// .filter(|part| check_part_accepted(part, &workflows))
// .count();
// bar.finish();
// ```
//
// Honestly copied someone else's approach for this one...
// https://github.com/klimesf/advent-of-code/blob/master/src/y2023/day19.rs
let mut stack: Vec<(
(usize, usize),
(usize, usize),
(usize, usize),
(usize, usize),
String,
usize,
)> = vec![(
(1, 4000),
(1, 4000),
(1, 4000),
(1, 4000),
"in".to_owned(),
0,
)];
let mut accepted: Vec<(
(usize, usize),
(usize, usize),
(usize, usize),
(usize, usize),
)> = vec![];
while let Some(range) = stack.pop() {
let (x, m, a, s, wf_key, rule_key) = range;
if wf_key == "A" {
accepted.push((x, m, a, s));
continue;
} else if wf_key == "R" {
continue;
}
// Invalid bounds check
if x.0 > x.1 || m.0 > m.1 || a.0 > a.1 || s.0 > s.1 {
continue;
}
let workflow = workflows.get(&wf_key).unwrap();
let flow_op = &workflow.flow[rule_key];
match flow_op {
FlowOp::Accept => {
accepted.push((x, m, a, s));
continue;
}
FlowOp::Reject => {
continue;
}
FlowOp::Route(new_wf_key) => {
stack.push((x, m, a, s, new_wf_key.to_owned(), 0));
continue;
}
FlowOp::GreaterThan(category, number, flow_op) => match category {
PartCategory::X => {
stack.push(((number + 1, x.1), m, a, s, flow_op.part2_to(), 0));
stack.push(((x.0, *number), m, a, s, wf_key, rule_key + 1));
}
PartCategory::M => {
stack.push((x, (number + 1, m.1), a, s, flow_op.part2_to(), 0));
stack.push((x, (m.0, *number), a, s, wf_key, rule_key + 1));
}
PartCategory::A => {
stack.push((x, m, (number + 1, a.1), s, flow_op.part2_to(), 0));
stack.push((x, m, (a.0, *number), s, wf_key, rule_key + 1));
}
PartCategory::S => {
stack.push((x, m, a, (number + 1, s.1), flow_op.part2_to(), 0));
stack.push((x, m, a, (s.0, *number), wf_key, rule_key + 1));
}
},
FlowOp::LessThan(category, number, flow_op) => match category {
PartCategory::X => {
stack.push(((x.0, number - 1), m, a, s, flow_op.part2_to(), 0));
stack.push(((*number, x.1), m, a, s, wf_key, rule_key + 1));
}
PartCategory::M => {
stack.push((x, (m.0, number - 1), a, s, flow_op.part2_to(), 0));
stack.push((x, (*number, m.1), a, s, wf_key, rule_key + 1));
}
PartCategory::A => {
stack.push((x, m, (a.0, number - 1), s, flow_op.part2_to(), 0));
stack.push((x, m, (*number, a.1), s, wf_key, rule_key + 1));
}
PartCategory::S => {
stack.push((x, m, a, (s.0, number - 1), flow_op.part2_to(), 0));
stack.push((x, m, a, (*number, s.1), wf_key, rule_key + 1));
}
},
}
}
let answer = accepted
.iter()
.map(|(x, m, a, s)| (x.1 - x.0 + 1) * (m.1 - m.0 + 1) * (a.1 - a.0 + 1) * (s.1 - s.0 + 1))
.sum();
let algo_time = a_start.elapsed();
// output
println!("part 2: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("Day {DAY}");
println!("=====");
let input = utils::aoc::get_puzzle_input(DAY).await?;
part1(input.clone())?;
part2(input.clone())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part_1() -> Result<()> {
assert_eq!(
part1(
"px{a<2006:qkq,m>2090:A,rfg}
pv{a>1716:R,A}
lnx{m>1548:A,A}
rfg{s<537:gd,x>2440:R,A}
qs{s>3448:A,lnx}
qkq{x<1416:A,crn}
crn{x>2662:A,R}
in{s<1351:px,qqz}
qqz{s>2770:qs,m<1801:hdj,R}
gd{a>3333:R,R}
hdj{m>838:A,pv}
{x=787,m=2655,a=1222,s=2876}
{x=1679,m=44,a=2067,s=496}
{x=2036,m=264,a=79,s=2244}
{x=2461,m=1339,a=466,s=291}
{x=2127,m=1623,a=2188,s=1013}
"
.to_owned(),
)?,
19114
);
Ok(())
}
#[test]
fn test_part_2() -> Result<()> {
assert_eq!(
part2(
"px{a<2006:qkq,m>2090:A,rfg}
pv{a>1716:R,A}
lnx{m>1548:A,A}
rfg{s<537:gd,x>2440:R,A}
qs{s>3448:A,lnx}
qkq{x<1416:A,crn}
crn{x>2662:A,R}
in{s<1351:px,qqz}
qqz{s>2770:qs,m<1801:hdj,R}
gd{a>3333:R,R}
hdj{m>838:A,pv}
{x=787,m=2655,a=1222,s=2876}
{x=1679,m=44,a=2067,s=496}
{x=2036,m=264,a=79,s=2244}
{x=2461,m=1339,a=466,s=291}
{x=2127,m=1623,a=2188,s=1013}
"
.to_owned(),
)?,
167409079868000
);
Ok(())
}
}

81
2023/src/bin/template.rs Normal file
View file

@ -0,0 +1,81 @@
use aoc23::prelude::*;
use derive_builder::Builder;
use itertools::Itertools;
use rayon::prelude::*;
use std::{str::FromStr, time::Instant};
static DAY: u8 = TODO;
#[derive(Debug, Clone)]
struct Todo {}
impl Todo {
fn new(s: &str) -> Result<Self> {
Ok(Self {})
}
}
fn part1(input: String) -> Result<usize> {
println!("Input\n====\n{input}\n\n");
// parse
let start = Instant::now();
let todo = Todo::new(&input)?;
let parsed_time = start.elapsed();
// algo
let a_start = Instant::now();
let mut answer = 0;
let algo_time = a_start.elapsed();
// output
println!("part 1: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
fn part2(input: String) -> Result<usize> {
// parse
let start = Instant::now();
let todo = Todo::new(&input)?;
let parsed_time = start.elapsed();
// algo
let a_start = Instant::now();
let mut answer = 0;
let algo_time = a_start.elapsed();
// output
println!("part 2: {answer}\t[total: {:?}]", start.elapsed());
println!("\tparse: {parsed_time:?}");
println!("\talgo: {algo_time:?}");
Ok(answer)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("Day {DAY}");
println!("=====");
let input = utils::aoc::get_puzzle_input(DAY).await?;
part1(input.clone())?;
part2(input.clone())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part_1() -> Result<()> {
assert_eq!(part1("REPLACE".to_owned())?, 0);
Ok(())
}
#[test]
fn test_part_2() -> Result<()> {
assert_eq!(part2("REPLACE".to_owned())?, 0);
Ok(())
}
}

66
2023/src/bin/test.rs Normal file
View file

@ -0,0 +1,66 @@
use std::io::{self, Write};
use std::thread;
use std::time::Duration;
fn main() {
let mut food = (0, 0);
let mut snake = vec![(1, 2), (1, 3)];
let mut direction = (0, 1);
print_board(&snake, &food);
loop {
let command: String = io::stdin().lock().read_line().unwrap();
match command.as_str() {
"\n" | "" => {}
"w" | "W" => direction = (0, -1),
"a" | "A" => direction = (-1, 0),
"s" | "S" => direction = (0, 1),
"d" | "D" => direction = (1, 0),
_ => println!("Invalid command!"),
}
let new_head = get_new_position(&snake.last().unwrap(), &direction);
if snake.contains(&new_head)
|| new_head.0 < 0
|| new_head.0 >= 20
|| new_head.1 < 0
|| new_head.1 >= 20
{
println!("Game over!");
break;
}
snake.push(new_head);
if new_head == food {
food = get_new_food();
} else {
snake.remove(0);
}
print_board(&snake, &food);
thread::sleep(Duration::from_millis(50));
}
}
fn get_new_position(head: &(i32, i32), direction: &(i32, i32)) -> (i32, i32) {
let x = head.0 + direction.0;
let y = head.1 + direction.1;
(x, y)
}
fn get_new_food() -> (i32, i32) {
(rand::random::<i32>() % 20, rand::random::<i32>() % 20)
}
fn print_board(snake: &Vec<(i32, i32)>, food: &(i32, i32)) {
// Clear screen
print!("\x1b[H\x1b[J");
for y in 0..20 {
for x in 0..20 {
if snake.contains(&(x, y)) {
print!("");
} else if (x, y) == *food {
print!("*");
} else {
print!(".");
}
}
println!();
}
}

12
2023/src/lib.rs Normal file
View file

@ -0,0 +1,12 @@
#[cfg(all(feature = "part1", feature = "part2"))]
compile_error!("Part 1 and Part 2 are mutually exclusive and cannot be enabled together");
pub mod utils;
pub mod prelude {
pub use super::utils::{
self,
common::{BoxE, Result, SError, SResult},
config::get_config,
grid::GridExtras,
};
}

61
2023/src/utils/aoc.rs Normal file
View file

@ -0,0 +1,61 @@
use crate::prelude::*;
use reqwest::{
header::{self, COOKIE},
Client,
};
use reqwest_middleware::{ClientBuilder as MiddlewareClientBuilder, ClientWithMiddleware};
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::PathBuf;
static AOC_PUZZLE_INPUT_CACHE: &str = "aoc_puzzle_cache";
pub async fn get_puzzle_input(day: u8) -> Result<String> {
let file_name = format!("day_{:02}", day);
let cache_path = PathBuf::from(AOC_PUZZLE_INPUT_CACHE).join(file_name);
if cache_path.exists() {
// Read from the cache file
let mut cache_file = File::open(cache_path)?;
let mut contents = String::new();
cache_file.read_to_string(&mut contents)?;
Ok(contents)
} else {
// Fetch the content from the URL
let response = aoc_client()
.await?
.get(format!("https://adventofcode.com/2023/day/{}/input", day))
.send()
.await?
.text()
.await?;
// Cache the content to a file Ensure the cache directory exists
fs::create_dir_all(AOC_PUZZLE_INPUT_CACHE)?;
let mut cache_file = File::create(cache_path)?;
cache_file.write_all(response.as_bytes())?;
Ok(response)
}
}
pub async fn aoc_client() -> Result<ClientWithMiddleware> {
let session = &get_config().aoc_session;
let mut session_cookie = header::HeaderValue::from_str(&format!("session={}", session))
.expect("failed to create header with api_key.");
session_cookie.set_sensitive(true);
let mut headers = header::HeaderMap::new();
headers.insert(COOKIE, session_cookie);
let client = Client::builder()
.default_headers(headers)
.gzip(true)
.brotli(true)
.deflate(true)
.https_only(false)
.build()?;
let client = MiddlewareClientBuilder::new(client)
.with(RetryTransientMiddleware::new_with_policy(
ExponentialBackoff::builder().build_with_max_retries(2),
))
.build();
Ok(client)
}

4
2023/src/utils/common.rs Normal file
View file

@ -0,0 +1,4 @@
pub type SResult<T, R> = std::result::Result<T, R>;
pub type SError = dyn std::error::Error;
pub type BoxE = Box<SError>;
pub type Result<T> = SResult<T, BoxE>;

20
2023/src/utils/config.rs Normal file
View file

@ -0,0 +1,20 @@
use std::{env::var, sync::OnceLock};
static CONFIG: OnceLock<Config> = OnceLock::new();
pub struct Config {
pub aoc_session: String,
}
impl Config {}
fn get_var(var_name: &str) -> String {
var(var_name).unwrap_or("".to_owned())
}
pub fn get_config() -> &'static Config {
let config = CONFIG.get_or_init(|| Config {
aoc_session: get_var("AOC_SESSION"),
});
config
}

36
2023/src/utils/grid.rs Normal file
View file

@ -0,0 +1,36 @@
use super::math::orthogonal_u_bounded;
use grid::Grid;
use std::collections::HashMap;
pub trait GridExtras<T> {
fn neighbors_all(&self, pos: (usize, usize)) -> HashMap<(usize, usize), &T>;
fn neighbors_orthogonal(&self, pos: (usize, usize)) -> HashMap<(usize, usize), &T>;
}
impl<T> GridExtras<T> for Grid<T> {
fn neighbors_all(&self, pos: (usize, usize)) -> HashMap<(usize, usize), &T> {
let mut n = HashMap::new();
for r in -1..=1 {
for c in -1..=1 {
// skip self
if r == 0 && c == 0 {
continue;
}
if let Some(neighbor) = pos.0.checked_add_signed(r).zip(pos.1.checked_add_signed(c))
{
if let Some(t) = self.get(neighbor.0, neighbor.1) {
n.insert(neighbor, t);
}
}
}
}
n
}
fn neighbors_orthogonal(&self, pos: (usize, usize)) -> HashMap<(usize, usize), &T> {
orthogonal_u_bounded(pos)
.into_iter()
.filter_map(|(pos, _delta)| self.get(pos.0, pos.1).map(|t| (pos, t)))
.collect()
}
}

27
2023/src/utils/math.rs Normal file
View file

@ -0,0 +1,27 @@
fn gcd(a: u64, b: u64) -> u64 {
if b == 0 {
a
} else {
gcd(b, a % b)
}
}
fn lcm(a: u64, b: u64) -> u64 {
a / gcd(a, b) * b
}
pub fn find_lcm(numbers: &[u64]) -> u64 {
numbers.iter().cloned().fold(1, |acc, num| lcm(acc, num))
}
pub fn orthogonal_u_bounded(root: (usize, usize)) -> Vec<((usize, usize), (isize, isize))> {
[(0, 1), (0, -1), (1, 0), (-1, 0)]
.into_iter()
.filter_map(|delta| {
root.0
.checked_add_signed(delta.0)
.zip(root.1.checked_add_signed(delta.1))
.map(|new_pos| (new_pos, delta))
})
.collect()
}

5
2023/src/utils/mod.rs Normal file
View file

@ -0,0 +1,5 @@
pub mod aoc;
pub mod common;
pub mod config;
pub mod grid;
pub mod math;