|
1 |
| -use crate::util::hash::*; |
| 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. |
2 | 53 | use std::collections::VecDeque;
|
3 | 54 |
|
4 |
| -type Input<'a> = FastMap<&'a str, FastSet<&'a str>>; |
5 |
| - |
6 |
| -pub fn parse(input: &str) -> Input<'_> { |
7 |
| - let mut edges = FastMap::new(); |
8 |
| - |
9 |
| - for line in input.lines() { |
10 |
| - let tokens: Vec<_> = line.split_ascii_whitespace().collect(); |
11 |
| - |
12 |
| - let key = &tokens[0][..3]; |
13 |
| - let parent = edges.entry(key).or_insert(FastSet::new()); |
| 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. |
| 58 | +pub struct Input { |
| 59 | + edges: Vec<usize>, |
| 60 | + nodes: Vec<(usize, usize)>, |
| 61 | +} |
14 | 62 |
|
15 |
| - for &child in &tokens[1..] { |
16 |
| - parent.insert(child); |
17 |
| - } |
| 63 | +impl 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 | +} |
18 | 71 |
|
19 |
| - for &child in &tokens[1..] { |
20 |
| - let entry = edges.entry(child).or_insert(FastSet::new()); |
21 |
| - entry.insert(key); |
| 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`. |
| 78 | +pub 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); |
22 | 90 | }
|
23 | 91 | }
|
24 | 92 |
|
25 |
| - edges |
26 |
| -} |
27 |
| - |
28 |
| -pub fn part1(edges: &Input<'_>) -> usize { |
29 |
| - // Find the 3 connections with the highest count. |
30 |
| - // Assume these will be the links. |
31 |
| - let mut freq = FastMap::new(); |
| 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()); |
32 | 96 |
|
33 |
| - for &start in edges.keys() { |
34 |
| - let mut todo = VecDeque::new(); |
35 |
| - todo.push_back(start); |
| 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 | + } |
36 | 103 |
|
37 |
| - let mut seen = FastSet::new(); |
38 |
| - seen.insert(start); |
| 104 | + Input { edges, nodes } |
| 105 | +} |
39 | 106 |
|
40 |
| - while let Some(pos) = todo.pop_front() { |
41 |
| - for &next in &edges[pos] { |
42 |
| - if seen.insert(next) { |
43 |
| - let key = if pos < next { [pos, next] } else { [next, pos] }; |
| 107 | +pub 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 | +} |
44 | 117 |
|
45 |
| - let entry = freq.entry(key).or_insert(0); |
46 |
| - *entry += 1; |
| 118 | +pub fn part2(_input: &Input) -> &'static str { |
| 119 | + "n/a" |
| 120 | +} |
47 | 121 |
|
48 |
| - todo.push_back(next); |
49 |
| - } |
50 |
| - } |
51 |
| - } |
| 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. |
| 126 | +fn 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)); |
52 | 136 | }
|
53 | 137 |
|
54 |
| - let mut order: Vec<_> = freq.iter().collect(); |
55 |
| - order.sort_unstable_by_key(|e| e.1); |
56 |
| - order.reverse(); |
57 |
| - |
58 |
| - let cut: Vec<_> = order.iter().take(3).map(|p| *p.0).collect(); |
59 |
| - let start = *edges.keys().next().unwrap(); |
60 |
| - let mut size = 1; |
| 138 | + index |
| 139 | +} |
61 | 140 |
|
| 141 | +/// BFS across the graph to find the furthest nodes from start. |
| 142 | +fn furthest(input: &Input, start: usize) -> usize { |
62 | 143 | let mut todo = VecDeque::new();
|
63 | 144 | todo.push_back(start);
|
64 | 145 |
|
65 |
| - let mut seen = FastSet::new(); |
66 |
| - seen.insert(start); |
| 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; |
67 | 149 |
|
68 |
| - while let Some(pos) = todo.pop_front() { |
69 |
| - for &next in &edges[pos] { |
70 |
| - let key = if pos < next { [pos, next] } else { [next, pos] }; |
| 150 | + let mut result = start; |
71 | 151 |
|
72 |
| - if cut.contains(&key) { |
73 |
| - continue; |
74 |
| - } |
| 152 | + while let Some(current) = todo.pop_front() { |
| 153 | + // The last node visited will be the furthest. |
| 154 | + result = current; |
75 | 155 |
|
76 |
| - if seen.insert(next) { |
77 |
| - size += 1; |
| 156 | + for (_, next) in input.neighbours(current) { |
| 157 | + if !seen[next] { |
78 | 158 | todo.push_back(next);
|
| 159 | + seen[next] = true; |
79 | 160 | }
|
80 | 161 | }
|
81 | 162 | }
|
82 | 163 |
|
83 |
| - size * (edges.len() - size) |
| 164 | + result |
84 | 165 | }
|
85 | 166 |
|
86 |
| -pub fn part2(_input: &Input<'_>) -> &'static str { |
87 |
| - "n/a" |
| 167 | +/// Simplified approach based on Edmonds–Karp algorithm. |
| 168 | +fn 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 |
88 | 223 | }
|
0 commit comments