Skip to content

Commit

Permalink
add day 4
Browse files Browse the repository at this point in the history
  • Loading branch information
wowkster committed Dec 4, 2023
1 parent 703069a commit cc5a088
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ default-run = "advent_of_code_2023"

[dependencies]
aoc = "0.4.0"
lazy_static = "1.4.0"
nom = "7.1.3"
rayon = "1.8.0"

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This is my implementation of the [Advent of Code](https://adventofcode.com/) pro
| [Day 1](https://adventofcode.com/2023/day/1) | :star: | :star: |
| [Day 2](https://adventofcode.com/2023/day/2) | :star: | :star: |
| [Day 3](https://adventofcode.com/2023/day/3) | :star: | :star: |
| [Day 4](https://adventofcode.com/2023/day/4) | :star: | :star: |

## Benchmarks

Expand All @@ -26,6 +27,7 @@ The following benchmarks were created on a Macbook Pro with an M2 Pro processor:
| [Day 1](https://github.com/wowkster/advent-of-code-2023/blob/main/src/bin/01.rs) | `24.2µs` | `120.8µs` |
| [Day 2](https://github.com/wowkster/advent-of-code-2023/blob/main/src/bin/02.rs) | `40.9µs` | `40.8µs` |
| [Day 3](https://github.com/wowkster/advent-of-code-2023/blob/main/src/bin/03.rs) | `260.7µs` | `116.3µs` |
| [Day 4](https://github.com/wowkster/advent-of-code-2023/blob/main/src/bin/04.rs) | `86.4µs` | `368.9µs` |

## Project Structure

Expand Down
8 changes: 8 additions & 0 deletions data/examples/04/part-1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
13
---
Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1
Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11
8 changes: 8 additions & 0 deletions data/examples/04/part-2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
30
---
Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1
Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11
138 changes: 138 additions & 0 deletions src/bin/04.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#![feature(test)]

advent_of_code_2023::solution!(4);

use std::{collections::HashMap, sync::Mutex};

use nom::{
bytes::complete::{tag, take_while_m_n},
character::complete::space1,
combinator::map_res,
multi::separated_list0,
sequence::tuple,
IResult,
};
use rayon::prelude::*;

pub fn part_1(input: &str) -> Option<u32> {
let cards = parse_cards(input);

let sum = cards.iter().map(Card::score).sum();

Some(sum)
}

#[inline]
pub fn part_2(input: &str) -> Option<u32> {
// UNOPTIMIZATION: For the benchmark to be fair, we must ensure that the cache
// is cleared between iterations
COPIES_CACHE.lock().unwrap().clear();

let original_cards = parse_cards(input);

let total_cards = original_cards
.par_iter()
.map(|c| c.count_copies(&original_cards))
.sum::<u32>();

Some(total_cards)
}

#[derive(Debug, Clone)]
struct Card {
id: u32,
matches: u32,
}

lazy_static::lazy_static! {
static ref COPIES_CACHE: Mutex<HashMap<u32, u32>> = Mutex::default();
}

impl Card {
pub fn new(id: u32, winning_numbers: Vec<u32>, my_numbers: Vec<u32>) -> Self {
// OPTIMIZATION: Cache matches in constructor
let matches = winning_numbers
.iter()
.filter(|w| my_numbers.contains(w))
.count() as u32;

Self { id, matches }
}

pub fn score(&self) -> u32 {
if self.matches == 0 {
return 0;
}

2u32.pow(self.matches - 1)
}

pub fn count_copies(&self, original_cards: &[Card]) -> u32 {
// OPTIMIZATION: Caches the copies internally because the same card
// will always return the same number of new created cards

let cached_count = COPIES_CACHE.lock().unwrap().get(&self.id).copied();

if let Some(res) = cached_count {
res
} else {
let res = self.count_copies_uncached(original_cards);

COPIES_CACHE.lock().unwrap().insert(self.id, res);

res
}
}

#[inline]
fn count_copies_uncached(&self, original_cards: &[Card]) -> u32 {
if self.matches == 0 {
return 1;
}

(0..self.matches)
.into_par_iter()
.map(|i| self.id + i)
.map(|c| original_cards[c as usize].count_copies(original_cards))
.sum::<u32>()
+ 1
}
}

fn parse_cards(input: &str) -> Vec<Card> {
// OPTIMIZATION: Instead of using nom to parse out the newline separators,
// we can parse in parallel which spreads out card `matches` compute
input
.par_lines()
.map(|l| {
let (_, card) = parse_card(l).unwrap();
card
})
.collect()
}

fn parse_card(input: &str) -> IResult<&str, Card> {
let (input, _) = tag("Card")(input)?;
let (input, _) = space1(input)?;
let (input, id) = parse_int(input)?;
let (input, _) = tag(":")(input)?;
let (input, _) = space1(input)?;

let (input, (winning_numbers, _, _, _, my_numbers)) =
tuple((parse_int_list, space1, tag("|"), space1, parse_int_list))(input)?;

Ok((input, Card::new(id, winning_numbers, my_numbers)))
}

fn parse_int_list(input: &str) -> IResult<&str, Vec<u32>> {
let (input, ints) = separated_list0(space1, parse_int)(input)?;

Ok((input, ints))
}

fn parse_int(input: &str) -> IResult<&str, u32> {
map_res(
take_while_m_n(1, 8, |c: char| c.is_ascii_digit()),
str::parse,
)(input)
}

0 comments on commit cc5a088

Please sign in to comment.