aoc/year2020/day22.rs
1//! # Crab Combat
2//!
3//! We start things off with a pun, by implementing a `Deck` of cards that is also a `Deque` or
4//! [double-ended queue](https://en.wikipedia.org/wiki/Double-ended_queue) backed by a
5//! circular buffer. Why use our own implementation when there is a perfectly good [`VecDeque`]
6//! already available? The answer is speed. As there are at most fifty cards in the pack, the Deck
7//! can use a fix sized stack allocated array, avoiding the expensive heap allocations that
8//! [`VecDeque`] must use.
9//!
10//! `Deck` also keeps the score up to date as cards are added and removed, as this comes in useful
11//! during part two. To update the score when a card is removed we subtract the card's value
12//! multiplied by the size of the deck. For example if 5 is removed then the new
13//! score is 67 - 5 * 4 = 47.
14//!
15//! | Deck ↓ | Weight | Score | Sum |
16//! | - | - | -- | -- |
17//! | 5 | 4 | 67 | 29 |
18//! | 8 | 3 | | |
19//! | 7 | 2 | | |
20//! | 9 | 1 | | |
21//!
22//! When adding a card, it helps to have the sum of the existing cards. The new score is the old
23//! score added to the new sum. For example if 6 is added to the deck:
24//!
25//! | Old Score | New Score | Difference |
26//! | ----- | ----- | - |
27//! | 5 * 4 | 5 * 5 | 5 |
28//! | 8 * 3 | 8 * 4 | 8 |
29//! | 7 * 2 | 7 * 3 | 7 |
30//! | 9 * 1 | 9 * 2 | 9 |
31//! | - | 6 * 1 | 6 |
32//! | Total: 67 | Total: 102 | Total: 35 |
33//!
34//! ## Part One
35//!
36//! The winner will always eventually be the player that starts with card 50 as they can never
37//! lose this card in a round. However we need to play the full game in order to find out the
38//! score of the winner's deck.
39//!
40//! ## Part Two
41//!
42//! We use two tricks to speed things up, one deterministic and one probabilistic.
43//!
44//! The deterministic trick is an observation that if Player 1 holds the high card in a recursive
45//! game, then they will always eventually win. This comes from the fact that all cards are unique,
46//! so the highest card is always greater than the size of the remaining deck, so a further
47//! recursive game will never trigger when this card is played in a round. This means that Player 1
48//! will never lose this card, so will either win outright or by triggering the repetition rule.
49//!
50//! This observation does *not* hold for Player 2. Although they can never lose the high card, they
51//! could lose by the repetition rule, so the round needs to be played out in full.
52//!
53//! The probabilistic trick is an observation that the score makes a surprisingly good hash
54//! function. Instead of storing the entire deck in the previously seen cache, we can store only
55//! the combined hash of both decks, with a very good probability of no collisions.
56//!
57//! [`VecDeque`]: std::collections::VecDeque
58use crate::util::hash::*;
59use crate::util::parse::*;
60
61type Input = (Deck, Deck);
62type Cache = Vec<FastSet<(usize, usize)>>;
63
64enum Winner {
65 Player1,
66 Player2,
67}
68
69#[derive(Clone, Copy)]
70pub struct Deck {
71 sum: usize,
72 score: usize,
73 start: usize,
74 end: usize,
75 cards: [u8; 50],
76}
77
78impl Deck {
79 fn new() -> Deck {
80 Deck { sum: 0, score: 0, start: 0, end: 0, cards: [0; 50] }
81 }
82
83 // To make things easier, `start` and `end` never wrap around, so that `end` is always
84 // greater than or equal to `start`.
85 fn pop_front(&mut self) -> usize {
86 let card = self.cards[self.start % 50] as usize;
87 self.sum -= card;
88 self.score -= self.size() * card;
89 self.start += 1;
90 card
91 }
92
93 fn push_back(&mut self, card: usize) {
94 self.cards[self.end % 50] = card as u8;
95 self.sum += card;
96 self.score += self.sum;
97 self.end += 1;
98 }
99
100 fn max(&self) -> u8 {
101 (self.start..self.end).map(|i| self.cards[i % 50]).max().unwrap()
102 }
103
104 fn non_empty(&self) -> bool {
105 self.end > self.start
106 }
107
108 fn size(&self) -> usize {
109 self.end - self.start
110 }
111
112 // Sneaky trick here to speed things up a little. We don't recalculate the score properly,
113 // so it will be too high by a constant amount. This doesn't matter for recursive games as
114 // we only need the winner, not the exact score.
115 fn copy(&self, amount: usize) -> Deck {
116 let mut copy = *self;
117 copy.end = copy.start + amount;
118 copy.sum = 0;
119
120 for i in 0..amount {
121 let card = copy.cards[(copy.start + i) % 50] as usize;
122 copy.sum += card;
123 }
124
125 copy
126 }
127}
128
129pub fn parse(input: &str) -> Input {
130 let (mut deck1, mut deck2) = (Deck::new(), Deck::new());
131 let (player1, player2) = input.split_once("\n\n").unwrap();
132
133 player1.iter_unsigned().skip(1).for_each(|c| deck1.push_back(c));
134 player2.iter_unsigned().skip(1).for_each(|c| deck2.push_back(c));
135
136 (deck1, deck2)
137}
138
139pub fn part1(input: &Input) -> usize {
140 let (mut deck1, mut deck2) = *input;
141
142 while deck1.non_empty() && deck2.non_empty() {
143 let (card1, card2) = (deck1.pop_front(), deck2.pop_front());
144
145 if card1 > card2 {
146 deck1.push_back(card1);
147 deck1.push_back(card2);
148 } else {
149 deck2.push_back(card2);
150 deck2.push_back(card1);
151 }
152 }
153
154 if deck1.non_empty() { deck1.score } else { deck2.score }
155}
156
157pub fn part2(input: &Input) -> usize {
158 let (mut deck1, mut deck2) = *input;
159
160 match combat(&mut deck1, &mut deck2, &mut Vec::new(), 0) {
161 Winner::Player1 => deck1.score,
162 Winner::Player2 => deck2.score,
163 }
164}
165
166fn combat(deck1: &mut Deck, deck2: &mut Deck, cache: &mut Cache, depth: usize) -> Winner {
167 // Player 1 always wins recursive games if they have the high card.
168 if depth > 0 && deck1.max() > deck2.max() {
169 return Winner::Player1;
170 }
171
172 // Speed things up by re-using previously created caches, avoiding slow extra heap allocations.
173 if cache.len() == depth {
174 cache.push(FastSet::with_capacity(1_000));
175 } else {
176 cache[depth].clear();
177 }
178
179 while deck1.non_empty() && deck2.non_empty() {
180 // This will *very probably* work! Not 100% deterministic.
181 if !cache[depth].insert((deck1.score, deck2.score)) {
182 return Winner::Player1;
183 }
184
185 let (card1, card2) = (deck1.pop_front(), deck2.pop_front());
186
187 if deck1.size() < card1 || deck2.size() < card2 {
188 if card1 > card2 {
189 deck1.push_back(card1);
190 deck1.push_back(card2);
191 } else {
192 deck2.push_back(card2);
193 deck2.push_back(card1);
194 }
195 } else {
196 match combat(&mut deck1.copy(card1), &mut deck2.copy(card2), cache, depth + 1) {
197 Winner::Player1 => {
198 deck1.push_back(card1);
199 deck1.push_back(card2);
200 }
201 Winner::Player2 => {
202 deck2.push_back(card2);
203 deck2.push_back(card1);
204 }
205 }
206 }
207 }
208
209 if deck1.non_empty() { Winner::Player1 } else { Winner::Player2 }
210}