Skip to main content

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-Wagner_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-Fulkerson_algorithm)
14//! * [Edmonds-Karp algorithm](https://en.wikipedia.org/wiki/Edmonds-Karp_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 an 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 of
49//!   `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 neighbors(&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 contiguous this allows us to use a vec to store previously
77/// seen values which is much faster than using a `HashMap`.
78pub fn parse(input: &str) -> Input {
79    let mut lookup = vec![usize::MAX; 26 * 26 * 26];
80    let mut neighbors = 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 neighbors, 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 neighbors, chunk);
88            neighbors[first].push(second);
89            neighbors[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(neighbors.len());
96
97    for list in neighbors {
98        let start = edges.len();
99        edges.extend(list);
100        nodes.push((start, edges.len()));
101    }
102
103    Input { edges, nodes }
104}
105
106pub fn part1(input: &Input) -> usize {
107    // Arbitrarily pick the first node then find the furthest node from it.
108    let start = furthest(input, 0);
109    // Find the furthest node from start. The graph is constructed so that the minimum cut is
110    // in the center of the graph, so start and end will be on opposite sides of the cut.
111    let end = furthest(input, start);
112    // Find the size of the graph still connected to start after the cut.
113    let size = flow(input, start, end);
114    size * (input.nodes.len() - size)
115}
116
117pub fn part2(_input: &Input) -> &'static str {
118    "n/a"
119}
120
121/// Each node's name is exactly 3 lowercase ASCII letters. First we calculate a
122/// [perfect hash](https://en.wikipedia.org/wiki/Perfect_hash_function) by converting to a base 26
123/// number. Then we construct a perfect *minimal* hash by using the first index to lookup a
124/// contiguous index into the nodes vec.
125fn perfect_minimal_hash(lookup: &mut [usize], nodes: &mut Vec<Vec<usize>>, slice: &[u8]) -> usize {
126    // Base 26 index.
127    let hash = slice[..3].iter().fold(0, |acc, b| 26 * acc + usize::from(b - b'a'));
128    let mut index = lookup[hash];
129
130    // First time seeing this key so push a new node and return its index.
131    if index == usize::MAX {
132        index = nodes.len();
133        lookup[hash] = index;
134        nodes.push(Vec::with_capacity(10));
135    }
136
137    index
138}
139
140/// BFS across the graph to find the furthest nodes from start.
141fn furthest(input: &Input, start: usize) -> usize {
142    let mut todo = VecDeque::from([start]);
143
144    // The node indices are also their key so we can use a vec instead of a HashSet for speed.
145    let mut seen = vec![false; input.nodes.len()];
146    seen[start] = true;
147
148    let mut result = start;
149
150    while let Some(current) = todo.pop_front() {
151        // The last node visited will be the furthest.
152        result = current;
153
154        for (_, next) in input.neighbors(current) {
155            if !seen[next] {
156                todo.push_back(next);
157                seen[next] = true;
158            }
159        }
160    }
161
162    result
163}
164
165/// Simplified approach based on Edmonds-Karp algorithm.
166fn flow(input: &Input, start: usize, end: usize) -> usize {
167    let mut todo = VecDeque::new();
168    // The path forms a linked list. During the BFS each path shares most nodes, so it's
169    // more efficient both in space and speed to store the path as a linked list instead
170    // of multiple copies of `vec`s.
171    let mut path = Vec::new();
172    // The capacity of each edge is 1 so only allow each edge to be used once.
173    let mut used = vec![false; input.edges.len()];
174    // The number of nodes from the 4th BFS is the size of one part of the cut graph.
175    let mut result = 0;
176
177    // We know the minimum cut is 3, so the 4th iteration will only be able to reach nodes
178    // on start's side.
179    for _ in 0..4 {
180        todo.push_back((start, usize::MAX));
181        result = 0;
182
183        let mut seen = vec![false; input.nodes.len()];
184        seen[start] = true;
185
186        while let Some((current, head)) = todo.pop_front() {
187            // Count how many nodes we visit.
188            result += 1;
189
190            // If we reached the end then add each edge of the path to `used`
191            // so that it can be used only once.
192            if current == end {
193                let mut index = head;
194
195                // Traverse the linked list.
196                while index != usize::MAX {
197                    let (edge, next) = path[index];
198                    used[edge] = true;
199                    index = next;
200                }
201
202                break;
203            }
204
205            // Find neighboring nodes to explore, only allowing each edge to be used once.
206            for (edge, next) in input.neighbors(current) {
207                if !used[edge] && !seen[next] {
208                    seen[next] = true;
209                    todo.push_back((next, path.len()));
210                    path.push((edge, head));
211                }
212            }
213        }
214
215        // Reuse for each iteration as a minor optimization.
216        todo.clear();
217        path.clear();
218    }
219
220    result
221}