aoc/year2024/
day19.rs

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
//! # Linen Layout
//!
//! Solves both parts simultaneously. Part one is the number of designs with non-zero possible
//! combinations.
//!
//! An elegant approach to check if the design starts with any towel is to first build a
//! [trie](https://en.wikipedia.org/wiki/Trie). Each node in the trie stores a `bool` indicating
//! if it's a valid towel and links to the next node for each possible color.
//!
//! There are only 5 colors. A custom [perfect hash](https://en.wikipedia.org/wiki/Perfect_hash_function)
//! function maps indices between 0 and 7 so that they fit into a fixed size array. This is faster
//! than using a `HashSet`.
//!
//! Additionally we store the Trie in a flat `vec`. This is simpler and faster than creating
//! objects on the heap using [`Box`].
type Input = (usize, usize);

pub fn parse(input: &str) -> Input {
    let (prefix, suffix) = input.split_once("\n\n").unwrap();

    // Build Trie from all towels.
    let mut trie = Vec::with_capacity(1_000);
    trie.push(Node::new());

    for towel in prefix.split(", ") {
        let mut i = 0;

        for j in towel.bytes().map(perfect_hash) {
            if trie[i].next[j] == 0 {
                // This is a new prefix, so update the index to point to it then push new node.
                trie[i].next[j] = trie.len();
                i = trie.len();
                trie.push(Node::new());
            } else {
                // Follow existing prefix.
                i = trie[i].next[j];
            }
        }

        trie[i].set_towel();
    }

    let mut part_one = 0;
    let mut part_two = 0;
    let mut ways = Vec::with_capacity(100);

    for design in suffix.lines().map(str::as_bytes) {
        let size = design.len();

        // Reset state.
        ways.clear();
        ways.resize(size + 1, 0);

        // There's 1 way to create any possible first prefix.
        ways[0] = 1;

        for start in 0..size {
            // Only consider suffixes that have a valid prefix.
            if ways[start] > 0 {
                // Walk trie from root to leaf.
                let mut i = 0;

                for end in start..size {
                    // Get next link.
                    i = trie[i].next[perfect_hash(design[end])];

                    // This is not a valid prefix, stop the search.
                    if i == 0 {
                        break;
                    }

                    // Add the number of possible ways this prefix can be reached.
                    ways[end + 1] += trie[i].towels() * ways[start];
                }
            }
        }

        // Last element is the total possible combinations.
        let total = ways[size];
        part_one += (total > 0) as usize;
        part_two += total;
    }

    (part_one, part_two)
}

pub fn part1(input: &Input) -> usize {
    input.0
}

pub fn part2(input: &Input) -> usize {
    input.1
}

/// Hashes the five possible color values white (w), blue (u), black (b), red (r), or green (g)
/// to 0, 2, 4, 5 and 1 respectively. This compresses the range to fit into an array of 6 elements.
fn perfect_hash(b: u8) -> usize {
    let n = b as usize;
    (n ^ (n >> 4)) % 8
}

/// Simple Node object that uses indices to link to other nodes.
struct Node {
    next: [usize; 6],
}

impl Node {
    fn new() -> Self {
        Node { next: [0; 6] }
    }

    // Index 3 is not used by the hash, so we cheekily repurpose for the number of towels.
    fn set_towel(&mut self) {
        self.next[3] = 1;
    }

    fn towels(&self) -> usize {
        self.next[3]
    }
}