aoc/year2023/
day25.rs

1//! # Snowverload
2//!
3//! We need to find a [minimum cut](https://en.wikipedia.org/wiki/Minimum_cut) of 3 edges that
4//! divides the graph into 2 parts. Several general purpose algorithms exist:
5//!
6//! * Deterministic [Stoer–Wagner algorithm](https://en.wikipedia.org/wiki/Stoer%E2%80%93Wagner_algorithm)
7//! * Probabilistic [Karger's algorithm](https://en.wikipedia.org/wiki/Karger%27s_algorithm)
8//!
9//! The [max-flow min-cut theorem](https://en.wikipedia.org/wiki/Max-flow_min-cut_theorem) also
10//! allows the minimum cut to be expressed as a [maximum flow problem](https://en.wikipedia.org/wiki/Maximum_flow_problem).
11//! There are several general purpose algorithms:
12//!
13//! * [Ford–Fulkerson algorithm](https://en.wikipedia.org/wiki/Ford%E2%80%93Fulkerson_algorithm)
14//! * [Edmonds–Karp algorithm](https://en.wikipedia.org/wiki/Edmonds%E2%80%93Karp_algorithm)
15//!
16//! We can use a simplified version of the Edmonds–Karp algorithm taking advantage of two pieces of
17//! information and a special property of the input graph structure:
18//!
19//! * The minimum cut size is already known to be 3.
20//! * All edge weights (or flow capacity) are 1.
21//! * The 3 edges to be cut are in the "middle" of the graph, that is the graph looks something
22//!   like:
23//!     ```none
24//!           * *       * *
25//!         * * * * - * * * *
26//!       * * * * * - * * * * *
27//!         * * * * - * * * *
28//!           * *       * *
29//!     ```
30//!
31//! Our high level approach is as follows:
32//! * Pick any arbitrary node
33//! * Find a start node furthest from it.
34//! * Find a end node furthest from the start node.
35//!
36//! The key insight is that the start and end nodes must be on opposite sides of the cut.
37//! We then BFS 3 times from the start to the end to find 3 different shortest paths.
38//! We keep track of the edges each time and only allow each edge to be used once so that each
39//! path has no common edges.
40//!
41//! This will "saturate" the 3 edges across the middle. Finally we BFS from the start node
42//! a fourth time. As the middle links are already used, this will only be able to reach the nodes
43//! on start's side of the graph and will find our answer.
44//!
45//! The complexity of each BFS is `O(V + E)` and we perform a total of 6. To speed things up even
46//! further some low level optimizations are used:
47//!
48//! * Numeric node and edge identifiers to allow `vec` to store previously seen values instead
49//!   of `HashMap`.
50//! * Linked list of path from start to end, stored in a `vec` using indices for simplicity and
51//!   cache locality. This [blog post series](https://rust-unofficial.github.io/too-many-lists/)
52//!   describes the complexity of using actual references.
53use std::collections::VecDeque;
54
55/// Store the graph as an [adjacency list](https://en.wikipedia.org/wiki/Adjacency_list).
56/// Each node has a unique index in the `nodes` vec.
57/// Each directed edge has a unique index in the `edges` vec.
58pub struct Input {
59    edges: Vec<usize>,
60    nodes: Vec<(usize, usize)>,
61}
62
63impl Input {
64    /// Convenience function to return an iterator of `(edge, node)` pairs.
65    #[inline]
66    fn neighbours(&self, node: usize) -> impl Iterator<Item = (usize, usize)> + '_ {
67        let (start, end) = self.nodes[node];
68        (start..end).map(|edge| (edge, self.edges[edge]))
69    }
70}
71
72/// Convert the input to use numeric indices instead of string keys for speed.
73/// Each node is assigned a unique index on a first come first served basis.
74/// Then the edges are gathered into a single vec so that each edge also has a unique index.
75///
76/// As both node and edge indices are contigous this allows us to use a vec to store previously
77/// seen values which is must faster than using a `HashMap`.
78pub fn parse(input: &str) -> Input {
79    let mut lookup = vec![usize::MAX; 26 * 26 * 26];
80    let mut neighbours = Vec::with_capacity(2_000);
81
82    for line in input.lines().map(str::as_bytes) {
83        let first = perfect_minimal_hash(&mut lookup, &mut neighbours, line);
84
85        // The graph is undirected so each link is bidirectional.
86        for chunk in line[5..].chunks(4) {
87            let second = perfect_minimal_hash(&mut lookup, &mut neighbours, chunk);
88            neighbours[first].push(second);
89            neighbours[second].push(first);
90        }
91    }
92
93    // Assign each edge a unique index. Each node then specifies a range into the edges vec.
94    let mut edges = Vec::with_capacity(5_000);
95    let mut nodes = Vec::with_capacity(neighbours.len());
96
97    for list in neighbours {
98        let start = edges.len();
99        let end = edges.len() + list.len();
100        edges.extend(list);
101        nodes.push((start, end));
102    }
103
104    Input { edges, nodes }
105}
106
107pub fn part1(input: &Input) -> usize {
108    // Arbitrarily pick the first node then find the furthest node from it.
109    let start = furthest(input, 0);
110    // Find the furthest node from start. The graph is constructed so that the minimum cut is
111    // in the center of the graph, so start and end will be on opposite sides of the cut.
112    let end = furthest(input, start);
113    // Find the size of the graph still connected to start after the cut.
114    let size = flow(input, start, end);
115    size * (input.nodes.len() - size)
116}
117
118pub fn part2(_input: &Input) -> &'static str {
119    "n/a"
120}
121
122/// Each node's name is exactly 3 lowercase ASCII letters. First we calculate a
123/// [perfect hash](https://en.wikipedia.org/wiki/Perfect_hash_function) by converting to a base 26
124/// number. Then we construct a perfect *minimal* hash by using the first index to lookup a
125/// contigous index into the nodes vec.
126fn perfect_minimal_hash(lookup: &mut [usize], nodes: &mut Vec<Vec<usize>>, slice: &[u8]) -> usize {
127    // Base 26 index.
128    let hash = slice[..3].iter().fold(0, |acc, b| 26 * acc + ((b - b'a') as usize));
129    let mut index = lookup[hash];
130
131    // First time seeing this key so push a new node and return its index.
132    if index == usize::MAX {
133        index = nodes.len();
134        lookup[hash] = index;
135        nodes.push(Vec::with_capacity(10));
136    }
137
138    index
139}
140
141/// BFS across the graph to find the furthest nodes from start.
142fn furthest(input: &Input, start: usize) -> usize {
143    let mut todo = VecDeque::new();
144    todo.push_back(start);
145
146    // The node indices are also their key so we can use a vec instead of a HashSet for speed.
147    let mut seen = vec![false; input.nodes.len()];
148    seen[start] = true;
149
150    let mut result = start;
151
152    while let Some(current) = todo.pop_front() {
153        // The last node visited will be the furthest.
154        result = current;
155
156        for (_, next) in input.neighbours(current) {
157            if !seen[next] {
158                todo.push_back(next);
159                seen[next] = true;
160            }
161        }
162    }
163
164    result
165}
166
167/// Simplified approach based on Edmonds–Karp algorithm.
168fn flow(input: &Input, start: usize, end: usize) -> usize {
169    let mut todo = VecDeque::new();
170    // The path forms a linked list. During the BFS each path shares most nodes, so it's
171    // more efficient both in space and speed to store the path as a linked list instead
172    // of multiple copies of `vec`s.
173    let mut path = Vec::new();
174    // The capacity of each edge is 1 so only allow each edge to be used once.
175    let mut used = vec![false; input.edges.len()];
176    // The number of nodes from the 4th BFS is the size of one part of the cut graph.
177    let mut result = 0;
178
179    // We know the minimum cut is 3, so the 4th iteration will only be able to reach nodes
180    // on start's side.
181    for _ in 0..4 {
182        todo.push_back((start, usize::MAX));
183        result = 0;
184
185        let mut seen = vec![false; input.nodes.len()];
186        seen[start] = true;
187
188        while let Some((current, head)) = todo.pop_front() {
189            // Count how many nodes we visit.
190            result += 1;
191
192            // If we reached the end then add each edge of the path to `used`
193            // so that it can be used only once.
194            if current == end {
195                let mut index = head;
196
197                // Traverse the linked list.
198                while index != usize::MAX {
199                    let (edge, next) = path[index];
200                    used[edge] = true;
201                    index = next;
202                }
203
204                break;
205            }
206
207            // Find neighbouring nodes to explore, only allowing each edge to be used once.
208            for (edge, next) in input.neighbours(current) {
209                if !used[edge] && !seen[next] {
210                    seen[next] = true;
211                    todo.push_back((next, path.len()));
212                    path.push((edge, head));
213                }
214            }
215        }
216
217        // Re-use for each iteration as a minor optimization.
218        todo.clear();
219        path.clear();
220    }
221
222    result
223}