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}