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}