Skip to content

Commit 4371240

Browse files
committed
Document Year 2021 Day 21
1 parent 12bda58 commit 4371240

File tree

1 file changed

+30
-7
lines changed

1 file changed

+30
-7
lines changed

src/year2021/day21.rs

+30-7
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,57 @@
1+
//! # Dirac Dice
12
use crate::util::iter::*;
23
use crate::util::parse::*;
34

4-
type Pair = (u64, u64);
5+
type Pair = (usize, usize);
56
type State = (Pair, Pair);
67

8+
/// Rolling the Dirac dice 3 times results in 27 quantum universes. However the dice total is
9+
/// one of only 7 possible values. Instead of handling 27 values, we encode the possible dice
10+
/// totals with the number of times that they occur. For example a score of 3 (1 + 1 + 1) only
11+
/// happens once in the 27 rolls, but a score of 6 happens a total of 7 times.
712
const DIRAC: [Pair; 7] = [(3, 1), (4, 3), (5, 6), (6, 7), (7, 6), (8, 3), (9, 1)];
813

14+
/// Extract the starting position for both players converting to zero based indices.
915
pub fn parse(input: &str) -> State {
10-
let [_, one, _, two]: [u64; 4] = input.iter_unsigned().chunk::<4>().next().unwrap();
16+
let [_, one, _, two]: [usize; 4] = input.iter_unsigned().chunk::<4>().next().unwrap();
1117
((one - 1, 0), (two - 1, 0))
1218
}
1319

14-
pub fn part1(input: &State) -> u64 {
20+
/// The initial deterministic dice roll total is 6 (1 + 2 + 3) and increases by 9 each turn.
21+
/// An interesting observation is that since the player's position is always modulo 10, we can
22+
/// also increase the dice total modulo 10, as (a + b) % 10 = (a % 10) + (b % 10).
23+
pub fn part1(input: &State) -> usize {
1524
let mut state = *input;
1625
let mut dice = 6;
1726
let mut rolls = 0;
1827

1928
loop {
29+
// Player position is 0 based from 0 to 9, but score is 1 based from 1 to 10
2030
let ((player_position, player_score), (other_position, other_score)) = state;
2131
let next_position = (player_position + dice) % 10;
2232
let next_score = player_score + next_position + 1;
2333

2434
dice = (dice + 9) % 10;
2535
rolls += 3;
2636

37+
// Return the score of the losing player times the number of dice rolls.
2738
if next_score >= 1000 {
2839
break other_score * rolls;
2940
}
3041

42+
// Swap the players so that they take alternating turns.
3143
state = ((other_position, other_score), (next_position, next_score));
3244
}
3345
}
3446

35-
pub fn part2(input: &State) -> u64 {
47+
/// [Memoization](https://en.wikipedia.org/wiki/Memoization) is the key to solving part two in a
48+
/// reasonable time. For each possible starting universe we record the number of winning and losing
49+
/// recursive universes so that we can re-use the result and avoid uneccessary calculations.
50+
///
51+
/// Each player can be in position 1 to 10 and can have a score from 0 to 20 (as a score of 21
52+
/// ends the game). This is a total of (10 * 21) ^ 2 = 44100 possible states. For speed this
53+
/// can fit in an array with perfect hashing, instead of using a slower `HashMap`.
54+
pub fn part2(input: &State) -> usize {
3655
let mut cache = vec![None; 44100];
3756
let (win, lose) = dirac(*input, &mut cache);
3857
win.max(lose)
@@ -41,9 +60,9 @@ pub fn part2(input: &State) -> u64 {
4160
fn dirac(state: State, cache: &mut [Option<Pair>]) -> Pair {
4261
let ((player_position, player_score), (other_position, other_score)) = state;
4362

44-
// 10, 10, 21 ,21
63+
// Calculate the perfect hash of the state and lookup the cache in case we've seen this before.
4564
let index = player_position + 10 * other_position + 100 * player_score + 2100 * other_score;
46-
if let Some(result) = cache[index as usize] {
65+
if let Some(result) = cache[index] {
4766
return result;
4867
}
4968

@@ -54,13 +73,17 @@ fn dirac(state: State, cache: &mut [Option<Pair>]) -> Pair {
5473
if next_score >= 21 {
5574
(win + frequency, lose)
5675
} else {
76+
// Sneaky trick here to handle both players with the same function.
77+
// We swap the order of players state each turn, so that turns alternate
78+
// and record the result as (lose, win) instead of (win, lose).
5779
let next_state = ((other_position, other_score), (next_position, next_score));
5880
let (next_lose, next_win) = dirac(next_state, cache);
5981
(win + frequency * next_win, lose + frequency * next_lose)
6082
}
6183
};
6284

85+
// Compute the number of wins and losses from this position and add to the cache.
6386
let result = DIRAC.iter().fold((0, 0), helper);
64-
cache[index as usize] = Some(result);
87+
cache[index] = Some(result);
6588
result
6689
}

0 commit comments

Comments
 (0)