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