aoc/year2024/
day23.rs

1//! # LAN Party
2//!
3//! This is the [Clique problem](https://en.wikipedia.org/wiki/Clique_problem). For part one we
4//! find triangles (cliques of size 3) for each node by checking if there's an edge between any
5//! distinct pair of neighbouring nodes.
6//!
7//! Part two asks to find the maximum clique, for which we could use the
8//! [Bron–Kerbosch algorithm](https://en.wikipedia.org/wiki/Bron%E2%80%93Kerbosch_algorithm).
9//! However the input has a specific structure that enables a simpler approach of finding the
10//! largest *maximal* clique using a greedy algorithm. Nodes are arranged in clusters of 13 and
11//! the maximum clique is size 14. This approach will not necessarily work for any general graph,
12//! but will work for the inputs provided.
13use crate::util::hash::*;
14
15type Input = (FastMap<usize, Vec<usize>>, Vec<[bool; 676]>);
16
17/// Convert each character pair `xy` to an index from 0..676 so that we can use much faster array
18/// lookup instead of a `HashMap`.
19pub fn parse(input: &str) -> Input {
20    let mut nodes = FastMap::with_capacity(1_000);
21    let mut edges = vec![[false; 676]; 676];
22
23    let to_index = |b: &[u8]| 26 * to_usize(b[0]) + to_usize(b[1]);
24    let empty = || Vec::with_capacity(16);
25
26    for edge in input.as_bytes().chunks(6) {
27        let from = to_index(&edge[..2]);
28        let to = to_index(&edge[3..]);
29
30        // Graph is undirected so add edges to both nodes.
31        nodes.entry(from).or_insert_with(empty).push(to);
32        nodes.entry(to).or_insert_with(empty).push(from);
33
34        // https://en.wikipedia.org/wiki/Adjacency_matrix
35        edges[from][to] = true;
36        edges[to][from] = true;
37    }
38
39    (nodes, edges)
40}
41
42pub fn part1(input: &Input) -> usize {
43    let (nodes, edges) = input;
44    let mut seen = [false; 676];
45    let mut triangles = 0;
46
47    // Only consider nodes starting with `t`.
48    for n1 in 494..520 {
49        if let Some(neighbours) = nodes.get(&n1) {
50            seen[n1] = true;
51
52            for (i, &n2) in neighbours.iter().enumerate() {
53                for &n3 in neighbours.iter().skip(i) {
54                    // Skip nodes if we've already seen them.
55                    if !seen[n2] && !seen[n3] && edges[n2][n3] {
56                        triangles += 1;
57                    }
58                }
59            }
60        }
61    }
62
63    triangles
64}
65
66pub fn part2(input: &Input) -> String {
67    let (nodes, edges) = input;
68    let mut seen = [false; 676];
69    let mut clique = Vec::new();
70    let mut largest = Vec::new();
71
72    // Greedy algorithm to find *maximal* (not maximum) cliques.
73    for (&n1, neighbours) in nodes {
74        if !seen[n1] {
75            clique.clear();
76            clique.push(n1);
77
78            // Add nodes if they're connected to every node already in the clique.
79            for &n2 in neighbours {
80                if clique.iter().all(|&c| edges[n2][c]) {
81                    seen[n2] = true;
82                    clique.push(n2);
83                }
84            }
85
86            // For the specific graphs given in the input
87            // finding the largest maximal clique will work.
88            if clique.len() > largest.len() {
89                largest.clone_from(&clique);
90            }
91        }
92    }
93
94    // Convert each index back into 2 character identifiers sorted alphabetically.
95    let mut result = String::new();
96    largest.sort_unstable();
97
98    for n in largest {
99        result.push(to_char(n / 26));
100        result.push(to_char(n % 26));
101        result.push(',');
102    }
103
104    result.pop();
105    result
106}
107
108fn to_usize(b: u8) -> usize {
109    (b - b'a') as usize
110}
111
112fn to_char(u: usize) -> char {
113    ((u as u8) + b'a') as char
114}