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}