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(Deck),
66 Player2(Deck),
67}
68
69#[derive(Clone, Copy)]
70pub struct Deck {
71 sum: usize,
72 score: usize,
73 start: usize,
74 end: usize,
75 cards: [u8; 64],
76}
77
78impl Deck {
79 fn new() -> Deck {
80 Deck { sum: 0, score: 0, start: 0, end: 0, cards: [0; 64] }
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 % 64] 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 % 64] = 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 % 64]).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 fn copy(&self, amount: usize) -> Deck {
113 let mut copy = Deck::new();
114 copy.end = amount;
115
116 for i in 0..amount {
117 let card = self.cards[(self.start + i) % 64];
118 copy.cards[i] = card;
119 copy.sum += card as usize;
120 copy.score += copy.sum;
121 }
122
123 copy
124 }
125}
126
127pub fn parse(input: &str) -> Input {
128 let (mut deck1, mut deck2) = (Deck::new(), Deck::new());
129 let (player1, player2) = input.split_once("\n\n").unwrap();
130
131 player1.iter_unsigned().skip(1).for_each(|c| deck1.push_back(c));
132 player2.iter_unsigned().skip(1).for_each(|c| deck2.push_back(c));
133
134 (deck1, deck2)
135}
136
137pub fn part1(input: &Input) -> usize {
138 let (mut deck1, mut deck2) = *input;
139
140 while deck1.non_empty() && deck2.non_empty() {
141 let (card1, card2) = (deck1.pop_front(), deck2.pop_front());
142
143 if card1 > card2 {
144 deck1.push_back(card1);
145 deck1.push_back(card2);
146 } else {
147 deck2.push_back(card2);
148 deck2.push_back(card1);
149 }
150 }
151
152 if deck1.non_empty() { deck1.score } else { deck2.score }
153}
154
155pub fn part2(input: &Input) -> usize {
156 let (deck1, deck2) = *input;
157
158 match combat(deck1, deck2, &mut Vec::new(), 0) {
159 Winner::Player1(deck) | Winner::Player2(deck) => deck.score,
160 }
161}
162
163fn combat(mut deck1: Deck, mut deck2: Deck, cache: &mut Cache, depth: usize) -> Winner {
164 // Player 1 always wins recursive games if they have the high card.
165 if depth > 0 && deck1.max() > deck2.max() {
166 return Winner::Player1(deck1);
167 }
168
169 // Speed things up by re-using previously created caches, avoiding slow extra heap allocations.
170 if cache.len() == depth {
171 cache.push(FastSet::with_capacity(1_000));
172 } else {
173 cache[depth].clear();
174 }
175
176 while deck1.non_empty() && deck2.non_empty() {
177 // This will *very probably* work! Not 100% deterministic.
178 if !cache[depth].insert((deck1.score, deck2.score)) {
179 return Winner::Player1(deck1);
180 }
181
182 let (card1, card2) = (deck1.pop_front(), deck2.pop_front());
183
184 if deck1.size() < card1 || deck2.size() < card2 {
185 if card1 > card2 {
186 deck1.push_back(card1);
187 deck1.push_back(card2);
188 } else {
189 deck2.push_back(card2);
190 deck2.push_back(card1);
191 }
192 } else {
193 match combat(deck1.copy(card1), deck2.copy(card2), cache, depth + 1) {
194 Winner::Player1(_) => {
195 deck1.push_back(card1);
196 deck1.push_back(card2);
197 }
198 Winner::Player2(_) => {
199 deck2.push_back(card2);
200 deck2.push_back(card1);
201 }
202 }
203 }
204 }
205
206 if deck1.non_empty() { Winner::Player1(deck1) } else { Winner::Player2(deck2) }
207}