diff --git a/Cargo.lock b/Cargo.lock index 14e4a4c..0abe446 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,6 +187,34 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "d05-print-queue" +version = "0.1.0" +dependencies = [ + "aoc", +] + +[[package]] +name = "d06-guard-gallivant" +version = "0.1.0" +dependencies = [ + "aoc", +] + +[[package]] +name = "d09-disk-fragmenter" +version = "0.1.0" +dependencies = [ + "aoc", +] + +[[package]] +name = "d10-hoof-it" +version = "0.1.0" +dependencies = [ + "aoc", +] + [[package]] name = "darling" version = "0.14.4" @@ -833,11 +861,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radsort" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "019b4b213425016d7d84a153c4c73afb0946fbb4840e4eece7ba8848b9d6da22" + [[package]] name = "rayon" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -845,9 +879,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -1852,8 +1886,6 @@ name = "y23d12" version = "0.1.0" dependencies = [ "aoc", - "itertools 0.12.0", - "rayon", ] [[package]] @@ -1893,6 +1925,7 @@ name = "y23d17" version = "0.1.0" dependencies = [ "aoc", + "smallvec", ] [[package]] @@ -1950,3 +1983,12 @@ version = "0.1.0" dependencies = [ "aoc", ] + +[[package]] +name = "y24d01" +version = "0.1.0" +dependencies = [ + "aoc", + "radsort", + "rayon", +] diff --git a/Cargo.toml b/Cargo.toml index 48b9810..6f0ab92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ members = [ "y23/d04-scratchcards", "y23/d05-if-you-give-a-seed-a-fertilizer", "y23/d06-wait-for-it", - "y23/d07-camel-cards", + "y23/d07-camel-cards", "y23/d08-brute-forced", "y23/d08-haunted-wasteland", "y23/d09-mirage-maintenance", @@ -56,4 +56,10 @@ members = [ "y23/d23", "y23/d24", "y23/d25", + + "y24/d01-historian-hysteria", + "y24/d05-print-queue", + "y24/d06-guard-gallivant", + "y24/d09-disk-fragmenter", + "y24/d10-hoof-it", ] diff --git a/y24/d01-historian-hysteria/Cargo.toml b/y24/d01-historian-hysteria/Cargo.toml new file mode 100644 index 0000000..ea51845 --- /dev/null +++ b/y24/d01-historian-hysteria/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "y24d01" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +aoc = { path = "../../aoc" } +radsort = "0.1.1" +rayon = "1.10.0" diff --git a/y24/d01-historian-hysteria/src/main.rs b/y24/d01-historian-hysteria/src/main.rs new file mode 100644 index 0000000..f6eecbc --- /dev/null +++ b/y24/d01-historian-hysteria/src/main.rs @@ -0,0 +1,77 @@ +use std::collections::HashMap; + +use aoc::input_str; + +fn part1(input: &str) -> i32 { + let (mut lefts, mut rights) = input + .lines() + .map(|line| { + let mut iter = line.split_whitespace(); + ( + iter.next().unwrap().parse::().unwrap(), + iter.next().unwrap().parse::().unwrap(), + ) + }) + .fold((vec![], vec![]), |(mut lefts, mut rights), (l, r)| { + lefts.push(l); + rights.push(r); + (lefts, rights) + }); + + lefts.sort_unstable(); + rights.sort_unstable(); + + lefts + .iter() + .zip(rights.iter()) + .map(|(l, r)| (l - r).abs()) + .sum() +} + +fn part2(input: &str) -> usize { + let (lefts, rights): (HashMap, HashMap) = input + .lines() + .map(|line| { + let mut iter = line.split_whitespace(); + ( + iter.next().unwrap().parse::().unwrap(), + iter.next().unwrap().parse::().unwrap(), + ) + }) + .fold( + (Default::default(), Default::default()), + |(mut lefts, mut rights), (l, r)| { + *lefts.entry(l).or_insert(0) += 1; + *rights.entry(r).or_insert(0) += 1; + (lefts, rights) + }, + ); + + lefts + .into_iter() + .map(|(id, l_count)| id * l_count * rights.get(&id).unwrap_or(&0)) + .sum() +} + +fn main() { + let input = input_str!(2024, 1); + + let start = std::time::Instant::now(); + println!("Part 1: {}", part1(input)); + println!("Time: {:?}", start.elapsed()); + + let start = std::time::Instant::now(); + println!("Part 2: {}", part2(input)); + println!("Time: {:?}", start.elapsed()); +} + +#[cfg(test)] +mod tests { + use crate::part2; + + #[test] + fn test_part2() { + let input = "3 4\n4 3\n2 5\n1 3\n3 9\n3 3"; + assert_eq!(part2(input), 31); + } +} diff --git a/y24/d05-print-queue/Cargo.toml b/y24/d05-print-queue/Cargo.toml new file mode 100644 index 0000000..406b3a0 --- /dev/null +++ b/y24/d05-print-queue/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "d05-print-queue" +version = "0.1.0" +edition = "2021" + +[dependencies] +aoc = { path = "../../aoc" } diff --git a/y24/d05-print-queue/src/main.rs b/y24/d05-print-queue/src/main.rs new file mode 100644 index 0000000..2d37015 --- /dev/null +++ b/y24/d05-print-queue/src/main.rs @@ -0,0 +1,117 @@ +use std::cmp::Ordering; + +use aoc::input_str; + +/// Eh, every problem is a graph problem. +/// +/// If (x, y) is in the matrix then X | Y, X comes before Y +#[derive(Debug)] +pub struct Graph { + n: usize, + matrix: Vec, +} + +impl Graph { + pub fn new(n: usize) -> Self { + Self { + n, + matrix: vec![false; (n + 1) * (n + 1)], + } + } + + pub fn index(&self, x: usize, y: usize) -> usize { + y * self.n + x + } + + pub fn insert(&mut self, x: usize, y: usize) { + let idx = self.index(x, y); + self.matrix[idx] = true + } + + pub fn contains(&self, x: usize, y: usize) -> bool { + let idx = self.index(x, y); + self.matrix[idx] + } +} + +fn main() { + let input = input_str!(2024, 5); + + let start = std::time::Instant::now(); + + let mut lines = input.lines(); + + let mut counter = 0; + let mut mapping = [None; 100]; + let mut reverse = vec![]; + let mut constraints = vec![]; + + for line in lines.by_ref() { + if line.is_empty() { + break; + } + + let mut split = line.split('|'); + let x: usize = split.next().unwrap().parse().unwrap(); + let y: usize = split.next().unwrap().parse().unwrap(); + + if mapping[x].is_none() { + counter += 1; + mapping[x] = Some(counter); + reverse.push(x); + } + + if mapping[y].is_none() { + counter += 1; + mapping[y] = Some(counter); + reverse.push(y); + } + + constraints.push((mapping[x].unwrap(), mapping[y].unwrap())); + } + + debug_assert_eq!(mapping.iter().filter(|m| m.is_some()).count(), counter); + let mut graph = Graph::new(counter); + for (x, y) in constraints { + graph.insert(x, y); + } + + println!("Created graph: {:?}", start.elapsed()); + let start = std::time::Instant::now(); + + let (good, bad): (Vec<_>, Vec<_>) = lines + .map(|line| { + line.split(',') + .map(|i| i.parse::().unwrap()) + .map(|i| mapping[i].unwrap()) + .collect::>() + }) + .partition(|pages| pages.windows(2).all(|v| graph.contains(v[0], v[1]))); + + println!("Partitioned: {:?}", start.elapsed()); + let start = std::time::Instant::now(); + + let part1: usize = good + .into_iter() + .map(|pages| reverse[pages[pages.len() / 2] - 1]) + .sum(); + + println!("Part 1: {:?}", start.elapsed()); + println!("Part 1: {}", part1); + let start = std::time::Instant::now(); + + let part2: usize = bad + .into_iter() + .map(|mut page| { + page.sort_unstable_by(|x, y| match graph.contains(*x, *y) { + true => Ordering::Greater, + false => Ordering::Less, + }); + page + }) + .map(|pages| reverse[pages[pages.len() / 2] - 1]) + .sum(); + + println!("Part 2: {:?}", start.elapsed()); + println!("Part 2: {}", part2); +} diff --git a/y24/d06-guard-gallivant/Cargo.toml b/y24/d06-guard-gallivant/Cargo.toml new file mode 100644 index 0000000..6c53e19 --- /dev/null +++ b/y24/d06-guard-gallivant/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "d06-guard-gallivant" +version = "0.1.0" +edition = "2021" + +[dependencies] +aoc = { path = "../../aoc" } diff --git a/y24/d06-guard-gallivant/src/main.rs b/y24/d06-guard-gallivant/src/main.rs new file mode 100644 index 0000000..216ae58 --- /dev/null +++ b/y24/d06-guard-gallivant/src/main.rs @@ -0,0 +1,140 @@ +use std::collections::HashSet; + +use aoc::{input_str, time}; + +fn parse(input: &str) -> (HashSet<(usize, usize)>, (usize, usize)) { + let map = input + .lines() + .enumerate() + .flat_map(move |(y, line)| line.chars().enumerate().map(move |(x, c)| (x, y, c))) + .filter_map(|(x, y, c)| c.eq(&'#').then_some((x, y))) + .collect(); + + let start = input + .lines() + .enumerate() + .flat_map(move |(y, line)| line.chars().enumerate().map(move |(x, c)| (x, y, c))) + .find_map(|(x, y, c)| c.eq(&'^').then_some((x, y))) + .unwrap(); + + (map, start) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum Direction { + North, + East, + South, + West, +} + +impl Direction { + fn turn_right(self) -> Self { + match self { + Direction::North => Direction::East, + Direction::East => Direction::South, + Direction::South => Direction::West, + Direction::West => Direction::North, + } + } +} + +fn step_forward( + dir: Direction, + (x, y): (usize, usize), + bounds: (usize, usize), +) -> Option<(usize, usize)> { + match dir { + Direction::North => y.checked_sub(1).map(|y| (x, y)), + Direction::East => (x + 1).le(&bounds.0).then_some((x + 1, y)), + Direction::South => (y + 1).le(&bounds.1).then_some((x, y + 1)), + Direction::West => x.checked_sub(1).map(|x| (x, y)), + } +} + +fn part1(map: &HashSet<(usize, usize)>, start: (usize, usize)) -> usize { + // I only find the bounds here because I forgot to while parsing + let bounds = map + .iter() + .copied() + .reduce(|(mx, my), (x, y)| (mx.max(x), my.max(y))) + .unwrap(); + let mut direction = Direction::North; + let (mut x, mut y) = start; + let mut visited = HashSet::new(); + + while let Some(next) = step_forward(direction, (x, y), bounds) { + visited.insert((x, y)); + if map.contains(&next) { + direction = direction.turn_right(); + } else { + (x, y) = next; + } + } + + visited.insert((x, y)); + visited.len() +} + +fn part2(map: &HashSet<(usize, usize)>, start: (usize, usize)) -> usize { + let bounds = map + .iter() + .copied() + .reduce(|(mx, my), (x, y)| (mx.max(x), my.max(y))) + .unwrap(); + + let mut count = 0; + for x in 0..=bounds.0 { + for y in 0..=bounds.1 { + let mut map = map.clone(); + map.insert((x, y)); + + let mut visited = HashSet::new(); + let ((mut x, mut y), mut direction) = (start, Direction::North); + while let Some(next) = step_forward(direction, (x, y), bounds) { + if !visited.insert((x, y, direction)) { + count += 1; + break; + } + if map.contains(&next) { + direction = direction.turn_right(); + } else { + (x, y) = next + } + } + } + } + + count +} + +fn main() { + let input = input_str!(2024, 6); + + let (map, start) = time("Parsed", || parse(input)); + let part1 = time("Part 1", || part1(&map, start)); + println!("Part 1: {}", part1); + + // 674 low + let part2 = time("Part 2", || part2(&map, start)); + println!("Part 2: {}", part2); +} + +#[cfg(test)] +mod tests { + use crate::*; + + #[test] + fn part1_example() { + let input = "....#.....\n.........#\n..........\n..#.......\n.......#..\n..........\n.#..^.....\n........#.\n#.........\n......#..."; + let (map, start) = parse(input); + assert_eq!(part1(&map, start), 41) + } + + #[test] + fn part2_example() { + let input = "....#.....\n.........#\n..........\n..#.......\n.......#..\n..........\n.#..^.....\n........#.\n#.........\n......#..."; + let (map, start) = parse(input); + assert_eq!(part2(&map, start), 6) + } +} diff --git a/y24/d09-disk-fragmenter/Cargo.toml b/y24/d09-disk-fragmenter/Cargo.toml new file mode 100644 index 0000000..d8ae646 --- /dev/null +++ b/y24/d09-disk-fragmenter/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "d09-disk-fragmenter" +version = "0.1.0" +edition = "2021" + +[dependencies] +aoc = { path = "../../aoc" } diff --git a/y24/d09-disk-fragmenter/src/main.rs b/y24/d09-disk-fragmenter/src/main.rs new file mode 100644 index 0000000..f399257 --- /dev/null +++ b/y24/d09-disk-fragmenter/src/main.rs @@ -0,0 +1,191 @@ +use aoc::{input_str, stringstuff::CharExt, time}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Block { + Empty, + File(usize), +} + +impl Block { + pub fn is_empty(&self) -> bool { + matches!(self, Block::Empty) + } + + pub fn file_id(self) -> Option { + match self { + Block::File(id) => Some(id), + _ => None, + } + } +} + +fn part1(input: &str) -> usize { + let mut file_id = 0; + let mut is_file = true; + let mut blocks = vec![]; + + for len in input.trim().chars().map(|b| b.digit_to_num()) { + if is_file { + blocks.extend((0..len).map(|_| Block::File(file_id))); + file_id += 1 + } else { + blocks.extend((0..len).map(|_| Block::Empty)); + } + is_file = !is_file + } + + // 2 pointer solution + let mut start = 0; + let mut end = blocks.len() - 1; + + loop { + // move the end pointer to a block + while blocks[end].is_empty() { + end -= 1; + } + + // move the start point until it hits empty space + while !blocks[start].is_empty() { + start += 1; + } + + if start >= end { + break; + } + + blocks.swap(start, end); + } + + #[cfg(debug_assertions)] + { + let mut blocks = blocks.iter().copied(); + while let Some(Block::File(_)) = blocks.next() {} + debug_assert!(blocks.all(|b| b == Block::Empty)); + } + + // checksum + blocks + .into_iter() + .take_while(|b| !b.is_empty()) + .enumerate() + .map(|(idx, block)| block.file_id().unwrap() * idx) + .sum() +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum Page { + // length + Empty(usize), + // length, id + File(usize, usize), +} + +impl Page { + pub fn is_empty_page(&self) -> bool { + matches!(self, Page::Empty(_)) + } + + pub fn shrink_empty_space(&mut self, by: usize) { + match self { + Page::Empty(len) => *len -= by, + Page::File(_, _) => panic!("shrink_empty_space requires an empty page"), + } + } + + pub fn len(&self) -> usize { + match self { + Page::Empty(len) => *len, + Page::File(len, _) => *len, + } + } +} + +fn part2(input: &str) -> usize { + let mut file_id = 0; + let mut is_file = true; + let mut pages = vec![]; + + for len in input.trim().chars().map(|b| b.digit_to_num()) { + if is_file { + pages.push(Page::File(len, file_id)); + file_id += 1 + } else { + pages.push(Page::Empty(len)); + } + is_file = !is_file + } + + let mut end = pages.len() - 1; + + loop { + #[cfg(debug_assertions)] + { + let _debug = pages + .iter() + .copied() + .flat_map(|p| match p { + Page::Empty(len) => (0..len).map(|_| Block::Empty).collect::>(), + Page::File(len, id) => (0..len).map(|_| Block::File(id)).collect::>(), + }) + .map(|b| match b { + Block::Empty => String::from("."), + Block::File(id) => id.to_string(), + }) + .collect::(); + println!("{:?}", _debug); + } + + // move the end pointer to a block + while end > 0 && pages[end].is_empty_page() { + end -= 1; + } + if end == 0 { + break; + } + let end_page = pages[end]; + + for start in 0..end { + let start_page = pages[start]; + if start_page.is_empty_page() && start_page.len() >= end_page.len() { + pages.insert(start, end_page); + debug_assert_eq!(pages[start + 1], start_page); + pages[start + 1].shrink_empty_space(end_page.len()); + pages[end + 1] = Page::Empty(end_page.len()); + break; + } + } + end -= 1; + } + + pages + .into_iter() + .flat_map(|p| match p { + Page::Empty(len) => (0..len).map(|_| Block::Empty).collect::>(), + Page::File(len, id) => (0..len).map(|_| Block::File(id)).collect::>(), + }) + .enumerate() + .flat_map(|(idx, block)| block.file_id().map(|id| id * idx)) + .sum() +} + +fn main() { + let input = input_str!(2024, 9); + + let part1 = time("Part 1", || part1(input)); + println!("Part 1: {}", part1); + + let part2 = time("Part 2", || part2(input)); + println!("Part 2: {}", part2); +} + +#[cfg(test)] +mod test { + use crate::*; + + #[test] + fn example() { + let input = "2333133121414131402"; + assert_eq!(part1(input), 1928); + assert_eq!(part2(input), 2858); + } +} diff --git a/y24/d10-hoof-it/Cargo.toml b/y24/d10-hoof-it/Cargo.toml new file mode 100644 index 0000000..79eb913 --- /dev/null +++ b/y24/d10-hoof-it/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "d10-hoof-it" +version = "0.1.0" +edition = "2021" + +[dependencies] +aoc = { path = "../../aoc" } diff --git a/y24/d10-hoof-it/src/main.rs b/y24/d10-hoof-it/src/main.rs new file mode 100644 index 0000000..180623c --- /dev/null +++ b/y24/d10-hoof-it/src/main.rs @@ -0,0 +1,143 @@ +use std::collections::{HashMap, HashSet}; + +use aoc::{input_str, stringstuff::CharExt, time}; + +struct Map { + data: HashMap<(usize, usize), u8>, +} + +impl Map { + fn get(&self, x: usize, y: usize) -> Option { + self.data.get(&(x, y)).copied() + } + + fn diff(&self, (x1, y1): (usize, usize), (x2, y2): (usize, usize)) -> Option { + debug_assert_eq!(x1.abs_diff(x2) + y1.abs_diff(y2), 1); + self.get(x2, y2) + .and_then(|p2| self.get(x1, y1).map(|p1| (p1, p2))) + .map(|(p1, p2)| p2 as i8 - p1 as i8) + } + + pub fn part1_neighbors(&self, x: usize, y: usize) -> Vec<(usize, usize)> { + [ + x.checked_sub(1).map(|x| (x, y)), + y.checked_sub(1).map(|y| (x, y)), + Some((x + 1, y)), + Some((x, y + 1)), + ] + .into_iter() + .flatten() + .filter(|p2| self.diff((x, y), *p2) == Some(-1)) + .collect() + } +} + +fn part1(map: &Map) -> usize { + // BFS from the 9s figuring out how many peaks each tile can reach. After the fact check the scores for h=0 + let mut reachable_peaks: HashMap<_, _> = + map.data.keys().map(|k| (*k, HashSet::new())).collect(); + let mut layer: HashSet<_> = map + .data + .iter() + .filter_map(|(k, v)| (*v == 9).then_some(*k)) + .collect(); + + layer.iter().for_each(|&p| { + let set = reachable_peaks.get_mut(&p).unwrap(); + set.insert(p); + }); + + let mut next_layer = HashSet::new(); + while !layer.is_empty() { + debug_assert!(next_layer.is_empty()); + for (x, y) in layer.drain() { + let peaks = reachable_peaks.get(&(x, y)).unwrap().clone(); + map.part1_neighbors(x, y).into_iter().for_each(|n| { + let next_peaks = reachable_peaks.get_mut(&n).unwrap(); + next_peaks.extend(peaks.iter()); + next_layer.insert(n); + }); + } + + std::mem::swap(&mut layer, &mut next_layer); + } + + map.data + .iter() + .filter_map(|(k, height)| { + (*height == 0) + .then_some(k) + .and_then(|p| reachable_peaks.get(p)) + }) + .map(|peaks| peaks.len()) + .sum() +} + +fn part2(map: &Map) -> usize { + // BFS from the 9s figuring out how many peaks each tile can reach. After the fact check the scores for h=0 + let mut trails: HashMap<_, _> = map.data.keys().map(|k| (*k, 0)).collect(); + let mut layer: HashSet<_> = map + .data + .iter() + .filter_map(|(k, v)| (*v == 9).then_some(*k)) + .collect(); + + layer.iter().for_each(|&p| { + trails.insert(p, 1); + }); + + let mut next_layer = HashSet::new(); + while !layer.is_empty() { + debug_assert!(next_layer.is_empty()); + for (x, y) in layer.drain() { + let paths = *trails.get(&(x, y)).unwrap(); + map.part1_neighbors(x, y).into_iter().for_each(|n| { + *trails.get_mut(&n).unwrap() += paths; + next_layer.insert(n); + }); + } + + std::mem::swap(&mut layer, &mut next_layer); + } + + map.data + .iter() + .filter_map(|(k, height)| (*height == 0).then_some(k).and_then(|p| trails.get(p))) + .sum() +} + +fn parse(input: &str) -> Map { + let mut data = HashMap::new(); + for (y, line) in input.lines().enumerate() { + for (x, b) in line.chars().map(|b| b.digit_to_num::()).enumerate() { + data.insert((x, y), b); + } + } + Map { data } +} + +fn main() { + let input = input_str!(2024, 10); + + let map = time("Parsed", || parse(input)); + let part1 = time("Part 1", || part1(&map)); + println!("Part 1: {}", part1); + + let part2 = time("Part 2", || part2(&map)); + println!("Part 2: {}", part2); +} + +#[cfg(test)] +mod test { + use crate::*; + + #[test] + fn example() { + let input = + "89010123\n78121874\n87430965\n96549874\n45678903\n32019012\n01329801\n10456732"; + + let map = parse(input); + assert_eq!(part1(&map), 36); + assert_eq!(part2(&map), 81); + } +}