1
+ //! # Dirac Dice
1
2
use crate :: util:: iter:: * ;
2
3
use crate :: util:: parse:: * ;
3
4
4
- type Pair = ( u64 , u64 ) ;
5
+ type Pair = ( usize , usize ) ;
5
6
type State = ( Pair , Pair ) ;
6
7
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.
7
12
const DIRAC : [ Pair ; 7 ] = [ ( 3 , 1 ) , ( 4 , 3 ) , ( 5 , 6 ) , ( 6 , 7 ) , ( 7 , 6 ) , ( 8 , 3 ) , ( 9 , 1 ) ] ;
8
13
14
+ /// Extract the starting position for both players converting to zero based indices.
9
15
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 ( ) ;
11
17
( ( one - 1 , 0 ) , ( two - 1 , 0 ) )
12
18
}
13
19
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 {
15
24
let mut state = * input;
16
25
let mut dice = 6 ;
17
26
let mut rolls = 0 ;
18
27
19
28
loop {
29
+ // Player position is 0 based from 0 to 9, but score is 1 based from 1 to 10
20
30
let ( ( player_position, player_score) , ( other_position, other_score) ) = state;
21
31
let next_position = ( player_position + dice) % 10 ;
22
32
let next_score = player_score + next_position + 1 ;
23
33
24
34
dice = ( dice + 9 ) % 10 ;
25
35
rolls += 3 ;
26
36
37
+ // Return the score of the losing player times the number of dice rolls.
27
38
if next_score >= 1000 {
28
39
break other_score * rolls;
29
40
}
30
41
42
+ // Swap the players so that they take alternating turns.
31
43
state = ( ( other_position, other_score) , ( next_position, next_score) ) ;
32
44
}
33
45
}
34
46
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 {
36
55
let mut cache = vec ! [ None ; 44100 ] ;
37
56
let ( win, lose) = dirac ( * input, & mut cache) ;
38
57
win. max ( lose)
@@ -41,9 +60,9 @@ pub fn part2(input: &State) -> u64 {
41
60
fn dirac ( state : State , cache : & mut [ Option < Pair > ] ) -> Pair {
42
61
let ( ( player_position, player_score) , ( other_position, other_score) ) = state;
43
62
44
- // 10, 10, 21 ,21
63
+ // Calculate the perfect hash of the state and lookup the cache in case we've seen this before.
45
64
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] {
47
66
return result;
48
67
}
49
68
@@ -54,13 +73,17 @@ fn dirac(state: State, cache: &mut [Option<Pair>]) -> Pair {
54
73
if next_score >= 21 {
55
74
( win + frequency, lose)
56
75
} 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).
57
79
let next_state = ( ( other_position, other_score) , ( next_position, next_score) ) ;
58
80
let ( next_lose, next_win) = dirac ( next_state, cache) ;
59
81
( win + frequency * next_win, lose + frequency * next_lose)
60
82
}
61
83
} ;
62
84
85
+ // Compute the number of wins and losses from this position and add to the cache.
63
86
let result = DIRAC . iter ( ) . fold ( ( 0 , 0 ) , helper) ;
64
- cache[ index as usize ] = Some ( result) ;
87
+ cache[ index] = Some ( result) ;
65
88
result
66
89
}
0 commit comments