Performance improvements for 2024 Quest 16

This commit is contained in:
Chris Alge 2024-11-26 17:17:29 +01:00
parent 6d665a8e71
commit 5d1320bec9
10 changed files with 462 additions and 0 deletions

View file

@ -0,0 +1,6 @@
[package]
name = "day16_cat_grin_of_fortune"
version = "0.1.0"
edition = "2021"
[dependencies]

View file

@ -0,0 +1,105 @@
Part I
You find yourself strolling through the streets of the town buzzing with excitement for the Tournament. As you meander along the cobblestone paths, you can't help but notice the Byter Paelish, standing proudly in front of his Entertainment Hub with a peculiar contraption by his side that looks like a mix between a jukebox and a magical slot machine.
The machine, or as the Byter put it, the "Cat Grin of Fortune" features a window showcasing a line-up of whimsical cat faces: left eye, muzzle, and right eye, like a happy cat represented by ^_^. A hefty lever on the right side seems to control the fate of these cat codes.
The Byter pulls the right lever and cat faces scroll in the window, appearing to be engraved on wheels . This explains the machine's size compared to the window. After a suspenseful moment, the wheels halt one after another, revealing a new sequence of cats.
To the tune of lute strings and catchy lyrics, Paelish spills the beans on the rules of the game. For every trio of identical symbols in the whimsically random cat sequence, you'll be rewarded with one Byte Coin. But the fun doesn't stop there - every additional symbol matching the trio adds another coin to your prize!
The Byter explains there are no secrets in the Cat Grin of Fortune. The side of the machine has the operating instructions and the wheel configurations (your notes) engraved on it. Above the image screen, a counter displays the number of right lever pulls since the machine started operating.
As you delve into the instructions, it becomes apparent that this contraption isn't just a random assortment of items. It is a simple yet whimsical masterpiece, where each wheel spins a sequence of cat faces, represented for simplicity as a vertical strip. Before the right lever is pulled for the first time, the wheels are set to display the first symbol of each strip. The numbers above the strips show how many positions each wheel turns with a single pull of the right lever. You wonder if it is easy to predict the next sequence on the wheels. The counter currently shows number 99, so you need to predict the 100th sequence.
Example based on the following notes:
1,2,3
^_^ -.- ^,-
>.- ^_^ >.<
-_- -.- >.<
-.^ ^_^
>.>
The first line contains the number of positions each wheel turns with a single pull of the right lever. The rest of the input represents the sequence of symbols on each wheel as vertical strips. The machine starts by displaying the first trio of symbols: ^_^ -.- ^,-.
After the first pull, the first wheel turns by 1, the second by 2, and the third by 3 positions, resulting in the new sequence: >.- -.- ^_^ which is worth 1 Byte Coin for the - triple.
After the second pull, the wheels turn again in the same way, resulting in: -_- >.> >.< which is also worth 1 Byte Coin for the > triple. Below you can see the results of pulling the right lever several times, followed by the number of coins won.
Pull Result Byte Coins Won
0: ^_^ -.- ^,- -
1: >.- -.- ^_^ 1
2: -_- >.> >.< 1
3: ^_^ ^_^ >.< 2 (one extra for 4th ^ symbol)
4: >.- -.^ ^,- 1
5: -_- -.- ^_^ 2 (one extra for 4th - symbol)
...
21: ^_^ -.- ^_^ 2 (one extra for 4th ^ symbol)
...
33: ^_^ ^_^ ^_^ 5 (one coin for _ trio plus 4 coins for six ^ symbols)
...
100: >.- -.- ^,- 2
...
For this example, the 100th sequence of the Cat Grin of Fortune is >.- -.- ^,-.
What is the 100th sequence produced by the Byter's machine?
Part II
As soon as the next sequence appears in the machine's window, you hear horns announcing a gathering of tournament participants at Paelish's estate. The Knights of the Order decide to check the fairness of the owner's machines and at the same time play the next round of the tournament.
The knights enter the Entertainment Hub, where there are dozens of machines similar to the one at the entrance, but their wheel schemes are much more complex. Additionally, the game instructions are slightly different. The muzzles of the cats are ignored in the search for matching symbols. Only the eyes are interpreted, which likely reduces the chances of winning by a significant amount.
Each knight's task is to calculate the number of coins won so far on the machine they are analysing. You approach your machine (your notes). The right lever pull counter shows the value 202420242024.
Example based on the following notes:
1,2,3
^_^ -.- ^,-
>.- ^_^ >.<
-_- -.- >.<
-.^ ^_^
>.>
For this example, the total number of Byte Coins won after pulling the right lever several times is as follows:
Pull Total Byte Coins
0: -
1: 1
2: 2
3: 4
4: 5
5: 7
... ...
10: 15
100: 138
1000: 1383
10000: 13833
100000: 138333
1000000: 1383333
10000000: 13833333
100000000: 138333333
1000000000: 1383333333
10000000000: 13833333333
100000000000: 138333333333
202420242024: 280014668134
What is the total number of Byte Coins won so far on the machine you are verifying after 202420242024 pulls of the right lever?
Part III
Just as you expected, in the long run, it is impossible to win as much as you invest on any of the machines. Byter Paelish explains that anyone can check if they will win or not before pulling the right lever, and it is not his fault that people are simply lazy and prefer to rely on luck. However, the Knights of the Order are relentless and order the immediate shutdown of the machines.
But instead, Paelish instructs his employees to reset the wheels to their initial state, add a second lever to each machine on the left side and to hang an additional informational plaque outlining the rules of the game. With his signature smile, he then asks the Knights of the Order to re-evaluate the machines. It appears that the old fox was prepared for a knightly inspection.
The informational plaque introduces the following rule: when all wheels have stopped, you may pull the lever on the left side of the machine to move all wheels downwards by one step, or push it to move all wheels upwards by one step. You don't win any coins after this move alone, and you can do it only once or not at all before pulling the right lever, which triggers the spin.
The inspectors decide that to verify the legality of the machines, knights need to calculate the maximum and minimum number of Byte Coins that can be won with 256 pulls of the right lever.
Example based on the following notes:
1,2,3
^_^ -.- ^,-
>.- ^_^ >.<
-_- -.- ^.^
-.^ >.<
>.>
The machine starts by displaying the first trio of symbols: ^_^ -.- ^,-.
Before pulling the right lever, you may push or pull the left one, changing the sequence to
>.- ^_^ >.< - with a pull (the wheels move one step forward).
-_- >.> >.< - with a push (the wheels move one step backward).
So, with the first pull of the right lever, the result might be:
Before the pull After the pull Byte Coins Won
^_^ -.- ^,- >.- -.- >.< 1
>.- ^_^ >.< -_- -.^ ^,- 2
-_- >.> >.< ^_^ ^_^ ^.^ 4
With a single right lever pull, the maximum number of coins you can win is 4 and the minimum number is 1, so the answer would be 4 1.
The results for further right lever pulls stand as follows:
for 2 pulls: 6 1
for 3 pulls: 9 2
for 10 pulls: 26 5
for 100 pulls: 246 50
for 256 pulls: 627 128
for 1000 pulls: 2446 500
for 2024 pulls: 4948 1012
What is the maximum and the minimum number of Byte Coins that can be won on your new machine with 256 right lever pulls?

View file

@ -0,0 +1,210 @@
use core::fmt::Display;
use std::collections::HashMap;
#[derive(Debug, PartialEq, Eq)]
pub enum ParseError<'a> {
InputMalformed,
LineMalformed(&'a str),
ParseIntError(&'a str),
}
impl Display for ParseError<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InputMalformed => write!(f, "Input should consist of the spin rates, an empty line and the sequence of cat faces"),
Self::LineMalformed(e) => write!(f, "Unable to parse malformed line: {e}\nShould be of format:\n"),
Self::ParseIntError(e) => write!(f, "Unable to parse into a number: {e}"),
}
}
}
type Face = [u8; 3];
struct Configuration {
advance_by: Vec<usize>,
wheels: Vec<Vec<Face>>,
}
impl<'a> TryFrom<&'a str> for Configuration {
type Error = ParseError<'a>;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
if let Some((advance, faces)) = value.split_once("\n\n") {
let advance_by: Vec<usize> = advance
.split(',')
.map(|i| i.parse::<usize>().map_err(|_| Self::Error::ParseIntError(i)))
.collect::<Result<Vec<usize>, ParseError>>()?;
let wheel_count = advance_by.len();
let mut wheels = vec![Vec::new(); wheel_count];
for line in faces.lines() {
if line.len() > 4 * wheel_count || [1, 2].contains(&(line.len() % 4)) {
return Err(Self::Error::LineMalformed(line));
}
for wheel in 0..wheel_count.min((line.len() + 1) / 4) {
let face = &line[4*wheel..4*wheel+3];
if face == " " {
continue;
}
let face: [u8; 3] = face.as_bytes().try_into().unwrap();
wheels[wheel].push(face);
}
}
Ok(Self { advance_by, wheels, })
} else {
Err(Self::Error::InputMalformed)
}
}
}
impl Configuration {
/// Get the faces after `pull_count` pulls of the right lever, and `adjust` pulls of the left
/// lever. Pushes of the left lever are represented by negative `adjust` values.
///
/// The absolute value of `adjust` MUST be less than or equal to pull_count, as the calculation
/// may underflow otherwise. `panic()`s in debug mode otherwise.
fn at(&self, pull_count: usize, adjust: isize) -> Vec<[u8; 3]> {
debug_assert!(adjust.abs_diff(0) <= pull_count);
self.wheels
.iter()
.enumerate()
.map(|(idx, wheel)| wheel[((pull_count * self.advance_by[idx]) as isize + adjust) as usize % wheel.len()])
.collect()
}
fn print_at(&self, pull_count: usize) -> String {
self.at(pull_count, 0).iter().map(Self::face).collect::<Vec<_>>().join(" ")
}
fn face(bytes: &[u8; 3]) -> String {
std::str::from_utf8(bytes).unwrap().to_string()
}
fn score(faces: &[[u8; 3]]) -> usize {
// Since we never calculate the score in the way described in part 1 (including muzzles),
// we only need to worry about the "eyes", i. e. symbols 0 and 2 in our faces.
let mut symbols = HashMap::new();
faces.iter().for_each(|face| {
symbols.entry(face[0]).and_modify(|count| *count += 1).or_insert(1);
symbols.entry(face[2]).and_modify(|count| *count += 1).or_insert(1);
});
symbols.iter().filter(|(_s, count)| **count > 2).map(|(_s, count)| *count - 2).sum()
}
fn score_after(&self, pull_count: usize) -> usize {
// All symbols must repeat after a number of pulls equal to the least common multiple of
// all wheel sizes. If we surpass that number, we can extrapolate any future scores.
let cycle_len = self.wheels
.iter()
.map(|wheel| wheel.len())
.reduce(lcm)
.unwrap();
if cycle_len < pull_count {
let rest = (1..=pull_count % cycle_len)
.map(|pull| Self::score(&self.at(pull, 0)))
.sum::<usize>();
let per_cycle = rest + (((pull_count % cycle_len)+1)..=cycle_len)
.map(|pull| Self::score(&self.at(pull, 0)))
.sum::<usize>();
(pull_count / cycle_len) * per_cycle + rest
} else {
(1..=pull_count)
.map(|pull| Self::score(&self.at(pull, 0)))
.sum::<usize>()
}
}
fn min_max(&self, pull_count: usize) -> (usize, usize) {
// After each pull of the right lever, for each sum of pulls - pushes of the left lever,
// this Vec will represent the (min, max) coin values that can be won this way at index
// [pull_count - pulls + pushes].
// These values are added to the possible outcomes of pulling the right lever again (and
// possibly pushing/pulling the left one once more), to determine the new (min, max)s.
// Since the left lever can only be operated once per pull, the new values for each cell
// only depend on the (min, max)s of it and their two neighbouring cells (except for the
// edge cases of pushing/pulling every time, which only have one neighbour in the previous
// step), and the possible scores for this pull itself.
let mut res = vec![(0, 0); 2 * pull_count + 1];
(1..=pull_count as isize)
.for_each(|pull| {
let possible_outcomes = (-pull..=pull)
.map(|push_pull| {
let this = Self::score(&self.at(pull as usize, push_pull));
let pred = res
.iter()
.skip(0.max(pull_count as isize + push_pull - 1) as usize)
.take(3)
.filter(|range| **range != (0, 0) || pull == 1)
.copied()
.collect::<Vec<(usize, usize)>>();
let min = *pred.iter().map(|(min, _max)| min).min().unwrap();
let max = *pred.iter().map(|(_min, max)| max).max().unwrap();
(min + this, max + this)
}).collect::<Vec<(usize, usize)>>();
possible_outcomes.iter().enumerate().for_each(|(idx, outcome)|
res[pull_count - pull as usize + idx] = *outcome);
});
let min = *res.iter().map(|(min, _max)| min).min().unwrap();
let max = *res.iter().map(|(_min, max)| max).max().unwrap();
(min, max)
}
}
fn gcd(lhs: usize, rhs: usize) -> usize {
let mut a = lhs;
let mut b = rhs;
while b != 0 {
(a, b) = (b, a % b);
}
a
}
fn lcm(lhs: usize, rhs: usize) -> usize {
(lhs / gcd(lhs, rhs)).saturating_mul(rhs)
}
pub fn run(input: &str, part: usize) -> Result<String, ParseError> {
let config = Configuration::try_from(input)?;
match part {
1 => Ok(config.print_at(100)),
2 => Ok(format!("{}", config.score_after(202420242024))),
3 => {
let (min, max) = config.min_max(256);
Ok(format!("{max} {min}"))
},
_ => panic!("Illegal part number"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::read_to_string;
fn read_file(name: &str) -> String {
read_to_string(name).expect(&format!("Unable to read file: {name}")[..]).trim().to_string()
}
#[test]
fn test_sample() {
let expected = [">.- -.- ^,-", "280014668134", "627 128"];
for part in 1..=expected.len() {
let sample_input = read_file(&format!("tests/sample{part}"));
assert_eq!(run(&sample_input, part), Ok(expected[part-1].to_string()));
}
}
#[test]
fn test_challenge() {
let expected = [">_^ ^.> >_^ >,^", "105328965118", "619 80"];
for part in 1..=expected.len() {
let challenge_input = read_file(&format!("tests/challenge{part}"));
assert_eq!(run(&challenge_input, part), Ok(expected[part-1].to_string()));
}
}
}

View file

@ -0,0 +1,26 @@
10,3,11,20
>_^ >,^ >:> >,^
>:> ^.> <:* <:*
<_< >:> <:* <_<
<_< <,> ^,- <,>
>,^ >:> <:- <,>
*_< *_< >,^ >:>
^,- *_< ^,- ^,-
<:- >,^ <_< <:-
<:* ^.> ^.> <:-
>_^ >_^ <_< >_^
*_< >:>
<:* <,>
^,- ^.>
<:-
<:-
>:>
<:*
^,-
<,>
>_^
>_^
^,-
>_^
<_<

View file

@ -0,0 +1,42 @@
47,79,59,83,53,73,43,71,67,61
{_* -_" +.` o:o ~:T <,< ],> [.P );X $_"
>;x G.Q $_Y -_] =;` G.$ S,+ o;T <;| \,%
I,- {,@ ):T -,= /:* T_o Y_o >.X Q_= Q,P
*:+ <./ ~.I ^:) '.G /.U -:~ $,' &;^ =.P
#;< 0,G Q;- /," >;X T_G ].} *.I #_@ ),+
|.T {,> *;$ /_$ }.` >.0 U_| /,\ %,X G_$
X:/ },\ -;< `.} %.O I,O `,I (.~ ~.[ /;O
X:* `.| >,` }.= *_G o,% +;G >;& '_P [:x
P:' %:~ <;% -,Q S.0 /;P <,\ %./ =,* <;'
Q;' Q;} %_% @_[ (,= x_~ S;= ];\ >.' &:{
}_< ~_- &:/ +.I o_% `:> +.= ",I (,0 O.\
~_@ /_U +;+ P_` <;x T_] |.@ [;( T,` >,^
^:@ {:) \:- &.Q *.] }:^ `_$ <_{ ';# ~:0
`,P ';P @,$ X_T Q.Y P,x >.- `:) /,~ ):*
/:] [,x X_T ):X G:~ %:~ ^;U |,# -_{ ~:[
$_Y =;< @,O /;= U;% $_Q ~_) x_[ -_= O.I
P_x @;{ [.) }_~ \:# |.] %_< },~ Q,U ^,(
Y.% ',/ x,T #_| I;S #,P @_@ G.) ";} {_0
(:T T_S o_Q x:~ `.[ x_U #_S S_# |_+ \;/
Y_S Y.` &,@ *,( I:x I_= ].) *.0 -,Y ):}
T_T /_T #_+ @;% ^_& *;( I,) 0_x
T;- ];( ):o U,$ G:0 I_] -,+ <:"
-.$ ".] T:Q (:) (,T *:~ O.& P_~
-.< +_\ ~,U ):| @,| Y," $.+ `_}
"_( ':| o;Y S,T |,] |,S ':I -_/
+_` |;- @,& >;G ":} Q.%
=:X O_O >,o ):x T:| Y.$
<.T ^:/ \:{ x:^ };< =_+
X:| @;~ <_\ ";` ";- >.&
(,@ O_$ <,G +.^ Y_S ~:[
>,S <_" X:/ >_&
/;" ~;% (:Q (:U
\_] ];# (.[ &_/
U.U };T -,# ]_>
(_) $_> #.` >_{
Q.| ^:(
<.G S:x
$;X #;~
\:$ >.>
]_P

View file

@ -0,0 +1,52 @@
53,47,61,59,43
o.> =:< o:- ^.> o.-
$.o <.< $:> *:^ -:^
<.= *.< ^:> o:o -.o
*.= >.< =:* =:o o.=
^.< *:= *:$ -:< =:>
>:< <:- ^.< >.> <.^
^:o $:* o.= =.< o:=
>.< <.* >.* *.$ =:*
>.^ =:= -.^ >.$ o.o
o.$ $:= ^:$ $:- =.o
=:^ o:^ o.< *.* -:>
<.o $.- <:- *:- -.<
>.- >:$ ^:^ ^.< o:*
^.o ^.$ $.o >.= <.-
>.> *:$ >:> o.> >.^
-:* *.o >:* *.> -:o
=:^ <:* =.^ ^.* >:^
>:$ >:= *:- -:> -.$
-:< <.> >:> o.$ ^:*
*.= -.> =:> =.- *:*
=.$ -:> <.= -:$ ^.<
^:= $:$ -.^ o.o *.=
$.> o:> >:> o:$ *.^
-.^ *.* =.= o:* *.<
<.o o.- -:< $.$ <:=
-:^ >.- $:^ ^:^ $.*
o:= ^.> -:^ ^:^ *.^
o:^ -:= <:* ^.> ^:<
^:o <.$ <:- o.< *.<
-:= <.= *:* =:< o.>
-.$ -:- o:< *.=
*:= *.= <.* *:*
*.^ *:> -:^ o:>
>.$ >.o -:> =.-
o.^ >.- *:> >.<
^:> ^:= -.$
$.o -:* <.<
>:o *.* =.=
o:- o:o o.^
*.^ =:< ^.o
$.> <.<
-:* ^.^
$:o $:-
-.- <:-
>.= -.o
-:$
o.^
-.*
=.<
o:<

View file

@ -0,0 +1,7 @@
1,2,3
^_^ -.- ^,-
>.- ^_^ >.<
-_- -.- >.<
-.^ ^_^
>.>

View file

@ -0,0 +1,7 @@
1,2,3
^_^ -.- ^,-
>.- ^_^ >.<
-_- -.- >.<
-.^ ^_^
>.>

View file

@ -0,0 +1,7 @@
1,2,3
^_^ -.- ^,-
>.- ^_^ >.<
-_- -.- ^.^
-.^ >.<
>.>