Skip to content

Commit 82d0d72

Browse files
committed
Much faster approach expressing the problem as a maximum flow problem
1 parent 56a75de commit 82d0d72

File tree

3 files changed

+216
-62
lines changed

3 files changed

+216
-62
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ pie
105105
| 22 | [Sand Slabs](https://adventofcode.com/2023/day/22) | [Source](src/year2023/day22.rs) | 486 |
106106
| 23 | [A Long Walk](https://adventofcode.com/2023/day/23) | [Source](src/year2023/day23.rs) | - |
107107
| 24 | [Never Tell Me The Odds](https://adventofcode.com/2023/day/24) | [Source](src/year2023/day24.rs) | 116 |
108-
| 25 | [Snowverload](https://adventofcode.com/2023/day/25) | [Source](src/year2023/day25.rs) | - |
108+
| 25 | [Snowverload](https://adventofcode.com/2023/day/25) | [Source](src/year2023/day25.rs) | 157 |
109109

110110
## 2022
111111

src/year2023/day25.rs

Lines changed: 194 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,223 @@
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.
253
use std::collections::VecDeque;
354

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+
}
1462

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+
}
1871

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);
2290
}
2391
}
2492

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());
3296

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+
}
36103

37-
let mut seen = FastSet::new();
38-
seen.insert(start);
104+
Input { edges, nodes }
105+
}
39106

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+
}
44117

45-
let entry = freq.entry(key).or_insert(0);
46-
*entry += 1;
118+
pub fn part2(_input: &Input) -> &'static str {
119+
"n/a"
120+
}
47121

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));
52136
}
53137

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+
}
61140

141+
/// BFS across the graph to find the furthest nodes from start.
142+
fn furthest(input: &Input, start: usize) -> usize {
62143
let mut todo = VecDeque::new();
63144
todo.push_back(start);
64145

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;
67149

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;
71151

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;
75155

76-
if seen.insert(next) {
77-
size += 1;
156+
for (_, next) in input.neighbours(current) {
157+
if !seen[next] {
78158
todo.push_back(next);
159+
seen[next] = true;
79160
}
80161
}
81162
}
82163

83-
size * (edges.len() - size)
164+
result
84165
}
85166

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
88223
}

tests/year2023/day25_test.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
1+
use aoc::year2023::day25::*;
2+
3+
const EXAMPLE: &str = "\
4+
jqt: rhn xhk nvd
5+
rsh: frs pzl lsr
6+
xhk: hfx
7+
cmg: qnr nvd lhk bvb
8+
rhn: xhk bvb hfx
9+
bvb: xhk hfx
10+
pzl: lsr hfx nvd
11+
qnr: nvd
12+
ntq: jqt hfx bvb xhk
13+
nvd: lhk
14+
lsr: lhk
15+
rzs: qnr cmg lsr rsh
16+
frs: qnr lhk lsr";
17+
118
#[test]
219
fn part1_test() {
3-
// No example data
20+
let input = parse(EXAMPLE);
21+
assert_eq!(part1(&input), 54);
422
}
523

624
#[test]
725
fn part2_test() {
8-
// No example data
26+
let input = parse(EXAMPLE);
27+
assert_eq!(part2(&input), "n/a");
928
}

0 commit comments

Comments
 (0)