Skip to content

Commit aac749e

Browse files
committed
Year 2018 Day 9
1 parent 8983100 commit aac749e

File tree

7 files changed

+204
-0
lines changed

7 files changed

+204
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ Performance is reasonable even on older hardware, for example a 2011 MacBook Pro
245245
| 6 | [Chronal Coordinates](https://adventofcode.com/2018/day/6) | [Source](src/year2018/day06.rs) | 38 |
246246
| 7 | [The Sum of Its Parts](https://adventofcode.com/2018/day/7) | [Source](src/year2018/day07.rs) | 8 |
247247
| 8 | [Memory Maneuver](https://adventofcode.com/2018/day/8) | [Source](src/year2018/day08.rs) | 29 |
248+
| 9 | [Marble Mania](https://adventofcode.com/2018/day/9) | [Source](src/year2018/day09.rs) | 980 |
248249

249250
## 2017
250251

benches/benchmark.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ mod year2018 {
137137
benchmark!(year2018, day06);
138138
benchmark!(year2018, day07);
139139
benchmark!(year2018, day08);
140+
benchmark!(year2018, day09);
140141
}
141142

142143
mod year2019 {

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ pub mod year2018 {
120120
pub mod day06;
121121
pub mod day07;
122122
pub mod day08;
123+
pub mod day09;
123124
}
124125

125126
/// # Rescue Santa from deep space with a solar system adventure.

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ fn year2018() -> Vec<Solution> {
187187
solution!(year2018, day06),
188188
solution!(year2018, day07),
189189
solution!(year2018, day08),
190+
solution!(year2018, day09),
190191
]
191192
}
192193

src/year2018/day09.rs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
//! # Marble Mania
2+
//!
3+
//! Efficient solution using an append only `vec` and generating only the minimum number of marbles
4+
//! needed to play the game.
5+
//!
6+
//! First let's consider some other slower approaches:
7+
//!
8+
//! We could store marbles in a `vec`, inserting and removing elements to make room. Each of these
9+
//! operations takes `O(n)` complexity. For part two if number of marbles is 100,000 then the
10+
//! total complexity is `100,000 * 100 * 100,000 = 10¹²` which is infeasible.
11+
//!
12+
//! A better approach is a linked list. Insert and remove operations are now `O(1)` for a total
13+
//! part two complexity of `100,000 * 1 * 100 = 10⁷`. This is slow but practical. However linked
14+
//! lists have a number of drawbacks:
15+
//!
16+
//! 1. Poor cache locality
17+
//! 2. Allocation per element
18+
//! 3. Ownership issues complex enough to inspire an entire
19+
//! [blog post series](https://rust-unofficial.github.io/too-many-lists/).
20+
//!
21+
//! ## First optimization
22+
//!
23+
//! The first key insight is that we can generate the marble sequence by only appending to a `vec`.
24+
//! We keep track of the head `()` and tail `<>` of the circle. Each turn adds two marbles to the
25+
//! head and removes one from the tail, growing the circle by one each time.
26+
//! For example the first 4 marbles look like:
27+
//!
28+
//! ```none
29+
//! <0>
30+
//! 0 <0> (1)
31+
//! 0 0 <1> 0 (2)
32+
//! 0 0 1 <0> 2 1 (3)
33+
//! 0 0 1 0 <2> 1 3 0 (4)
34+
//! ```
35+
//!
36+
//! Things start to get interesting at the 19th marble. When we pick the 23rd marble this will
37+
//! be 7 places counter clockwise, so we can optimize by not adding it at all the the circle.
38+
//! Instead we save the value for later.
39+
//!
40+
//! ```none
41+
//! 18th marble
42+
//! ...<9> 2 10 5 11 1 12 6 13 3 14 7 15 0 16 8 17 4 (18)
43+
//!
44+
//! 19th marble, saving value of previous tail 9.
45+
//! ...<2> 10 5 11 1 12 6 13 3 14 7 15 0 16 8 17 4 18 (19)
46+
//! ```
47+
//!
48+
//! For the 20th, 21st and 22nd marbles we re-write the history of the tail then move it backwards.
49+
//!
50+
//! ```none
51+
//! 20th marble
52+
//! ... 2 20 9 <2> 10 5 11 1 12 6 13 3 14 7 15 0 16 8 17 4 18 (19)
53+
//! ^ ^^
54+
//!
55+
//! 21st marble
56+
//! ... 2 20 10 <21> 10 5 11 1 12 6 13 3 14 7 15 0 16 8 17 4 18 (19)
57+
//! ^^ ^^
58+
//!
59+
//! 22nd marble (move tail)
60+
//! ...<2> 20 10 21 5 22 11 1 12 6 13 3 14 7 15 0 16 8 17 4 18 (19)
61+
//! ^ ^^
62+
//! ```
63+
//!
64+
//! The 23rd marble is never added to the circle instead increasing the current player's score.
65+
//! The cycle then begins again, handling the next 18 marbles normally, then the next 19th to 22nd
66+
//! marbles specially.
67+
//!
68+
//! ## Second optimization
69+
//!
70+
//! It may seem that we need to generate `(last marble / 23)` blocks. However in each block we add
71+
//! 37 marbles (2 each for the first 18 marbles and 1 for the 19th) while the marble added to each
72+
//! player's score advances `23 - 7 = 16` marbles. This means we only need to generate about
73+
//! `16/37` or `44%` of the total blocks to solve the game deterministcally. This saves both
74+
//! processing time and memory storage proportionally.
75+
use crate::util::iter::*;
76+
use crate::util::parse::*;
77+
78+
type Input = [usize; 2];
79+
80+
pub fn parse(input: &str) -> Input {
81+
input.iter_unsigned().chunk::<2>().next().unwrap()
82+
}
83+
84+
pub fn part1(input: &Input) -> u64 {
85+
let [players, last] = *input;
86+
game(players, last)
87+
}
88+
89+
pub fn part2(input: &Input) -> u64 {
90+
let [players, last] = *input;
91+
game(players, last * 100)
92+
}
93+
94+
fn game(players: usize, last: usize) -> u64 {
95+
// Play the game in blocks of 23.
96+
let blocks = last / 23;
97+
// The number of marbles needed for scoring.
98+
let needed = 2 + 16 * blocks;
99+
// Each block adds 37 marbles, so allow a little extra capacity to prevent reallocation.
100+
let mut circle: Vec<u32> = Vec::with_capacity(needed + 37);
101+
// The score for each block is deterministic so the number of players only affects how scores
102+
// are distributed. Type is `u64` to prevent overflow during part two.
103+
let mut scores = vec![0; players];
104+
// The first marble picked up and removed by the player is 9.
105+
let mut pickup = 9;
106+
// The first block is pre-generated, so we start at marble 23.
107+
let mut head = 23;
108+
// Keep track of previous marbles to re-add to the start of the circle and for scoring.
109+
let mut tail = 0;
110+
// Add pre-generated marbles for first block.
111+
let start = [2, 20, 10, 21, 5, 22, 11, 1, 12, 6, 13, 3, 14, 7, 15, 0, 16, 8, 17, 4, 18, 19];
112+
circle.extend_from_slice(&start);
113+
114+
for _ in 0..blocks {
115+
// Score the previous block.
116+
scores[head as usize % players] += (head + pickup) as u64;
117+
// The next marble picked up is from the current block.
118+
pickup = circle[tail + 18];
119+
120+
// Generate the next block only until we have enough marbles to finish the game.
121+
if circle.len() <= needed {
122+
// Extending a vector from a slice is faster than adding elements one at a time.
123+
let slice = &[
124+
circle[tail],
125+
head + 1,
126+
circle[tail + 1],
127+
head + 2,
128+
circle[tail + 2],
129+
head + 3,
130+
circle[tail + 3],
131+
head + 4,
132+
circle[tail + 4],
133+
head + 5,
134+
circle[tail + 5],
135+
head + 6,
136+
circle[tail + 6],
137+
head + 7,
138+
circle[tail + 7],
139+
head + 8,
140+
circle[tail + 8],
141+
head + 9,
142+
circle[tail + 9],
143+
head + 10,
144+
circle[tail + 10],
145+
head + 11,
146+
circle[tail + 11],
147+
head + 12,
148+
circle[tail + 12],
149+
head + 13,
150+
circle[tail + 13],
151+
head + 14,
152+
circle[tail + 14],
153+
head + 15,
154+
circle[tail + 15],
155+
head + 16,
156+
circle[tail + 16],
157+
head + 17,
158+
circle[tail + 17],
159+
head + 18,
160+
// circle[tail + 18] 19th marble is picked up and removed.
161+
head + 19,
162+
];
163+
circle.extend_from_slice(slice);
164+
165+
// Overwrite the tail for the 20th, 21st and 22nd marbles.
166+
let slice = &[
167+
circle[tail + 19],
168+
head + 20,
169+
circle[tail + 20],
170+
head + 21,
171+
circle[tail + 21],
172+
head + 22,
173+
];
174+
circle[tail + 16..tail + 22].copy_from_slice(slice);
175+
}
176+
177+
// Marbles increase by 23 per block but the tail only by 16 as we reset by 7 marbles
178+
// according to the rules.
179+
head += 23;
180+
tail += 16;
181+
}
182+
183+
*scores.iter().max().unwrap()
184+
}

tests/test.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ mod year2018 {
121121
mod day06_test;
122122
mod day07_test;
123123
mod day08_test;
124+
mod day09_test;
124125
}
125126

126127
mod year2019 {

tests/year2018/day09_test.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use aoc::year2018::day09::*;
2+
3+
const EXAMPLE: &str = "10 players; last marble is worth 1618 points";
4+
5+
#[test]
6+
fn part1_test() {
7+
let input = parse(EXAMPLE);
8+
assert_eq!(part1(&input), 8317);
9+
}
10+
11+
#[test]
12+
fn part2_test() {
13+
let input = parse(EXAMPLE);
14+
assert_eq!(part2(&input), 74765078);
15+
}

0 commit comments

Comments
 (0)