aoc/year2023/
day23.rs

1use crate::util::grid::*;
2use crate::util::hash::*;
3use crate::util::point::*;
4use std::collections::VecDeque;
5
6pub struct Input {
7    extra: u32,
8    horizontal: [[u32; 6]; 6],
9    vertical: [[u32; 6]; 6],
10}
11
12struct State {
13    letter: u8,
14    skipped: bool,
15    grid: [[u8; 6]; 7],
16    convert: [u8; 32],
17    result: u32,
18}
19
20pub fn parse(input: &str) -> Input {
21    let mut grid = Grid::parse(input);
22    let width = grid.width;
23    let height = grid.height;
24
25    // Modify edge of grid to remove the need for boundary checks.
26    grid[Point::new(1, 0)] = b'#';
27    grid[Point::new(width - 2, height - 1)] = b'#';
28
29    // Move start and end away from edge.
30    let start = Point::new(1, 1);
31    let end = Point::new(width - 2, height - 2);
32
33    // Points of interest are start, end and junctions.
34    grid[start] = b'P';
35    grid[end] = b'P';
36
37    let mut poi = Vec::new();
38    poi.push(start);
39    poi.push(end);
40
41    for y in 1..height - 1 {
42        for x in 1..width - 1 {
43            let position = Point::new(x, y);
44
45            if grid[position] != b'#' {
46                let neighbors =
47                    ORTHOGONAL.iter().map(|&o| position + o).filter(|&n| grid[n] != b'#').count();
48                if neighbors > 2 {
49                    grid[position] = b'P';
50                    poi.push(position);
51                }
52            }
53        }
54    }
55
56    // BFS to find distances between POIs.
57    let mut todo = VecDeque::new();
58    let mut edges = FastMap::new();
59    let mut weight = FastMap::new();
60
61    for from in poi {
62        todo.push_back((from, 0));
63        grid[from] = b'#';
64        weight.insert((from, from), 0);
65
66        while let Some((position, cost)) = todo.pop_front() {
67            for direction in ORTHOGONAL {
68                let to = position + direction;
69
70                match grid[to] {
71                    b'#' => (),
72                    b'P' => {
73                        edges.entry(from).or_insert(FastSet::new()).insert(to);
74                        edges.entry(to).or_insert(FastSet::new()).insert(from);
75                        weight.insert((from, to), cost + 1);
76                        weight.insert((to, from), cost + 1);
77                    }
78                    _ => {
79                        todo.push_back((to, cost + 1));
80                        grid[to] = b'#';
81                    }
82                }
83            }
84        }
85    }
86
87    // Convert reduced graph to a 6x6 square grid.
88    graph_to_grid(start, end, &edges, &weight)
89}
90
91/// The graph is directed so the only allowed steps are down or to the right. The maximum value
92/// for any cell is the maximum of either the cell to the left or above.
93pub fn part1(input: &Input) -> u32 {
94    let mut total = [[0; 6]; 6];
95
96    for y in 0..6 {
97        for x in 0..6 {
98            let left = if x == 0 { 0 } else { total[y][x - 1] + input.horizontal[y][x - 1] };
99            let above = if y == 0 { 0 } else { total[y - 1][x] + input.vertical[y - 1][x] };
100            total[y][x] = left.max(above);
101        }
102    }
103
104    input.extra + total[5][5]
105}
106
107/// Graph is undirected so we can also move up or to the right.
108pub fn part2(input: &Input) -> u32 {
109    let mut state =
110        State { letter: 2, skipped: false, grid: [[0; 6]; 7], convert: [0; 32], result: 0 };
111
112    state.grid[0][0] = 1;
113
114    for i in 0..32 {
115        state.convert[i] = i as u8;
116    }
117
118    dfs(input, &mut state, 0, 0, 0);
119    input.extra + state.result
120}
121
122#[expect(clippy::needless_range_loop)]
123fn graph_to_grid(
124    start: Point,
125    end: Point,
126    edges: &FastMap<Point, FastSet<Point>>,
127    weight: &FastMap<(Point, Point), u32>,
128) -> Input {
129    let mut extra = 2;
130    extra += edges[&start].iter().map(|&e| weight[&(start, e)]).sum::<u32>();
131    extra += edges[&end].iter().map(|&e| weight[&(e, end)]).sum::<u32>();
132
133    let mut places = [[ORIGIN; 6]; 6];
134    let mut horizontal = [[0; 6]; 6];
135    let mut vertical = [[0; 6]; 6];
136
137    let mut point = *edges[&start].iter().next().unwrap();
138    let mut seen = FastSet::new();
139    let mut next_perimeter = |point: Point| {
140        seen.insert(point);
141        *edges[&point]
142            .iter()
143            .find(|&next| edges[next].len() == 3 && !seen.contains(next))
144            .unwrap_or(&ORIGIN)
145    };
146
147    for y in 0..5 {
148        places[y][0] = point;
149        point = next_perimeter(point);
150    }
151
152    for x in 1..6 {
153        places[5][x] = point;
154        point = next_perimeter(point);
155    }
156
157    for y in (1..5).rev() {
158        places[y][5] = point;
159        point = next_perimeter(point);
160    }
161
162    for x in (1..5).rev() {
163        places[0][x] = point;
164        point = next_perimeter(point);
165    }
166
167    for y in 1..5 {
168        for x in 1..5 {
169            let above = places[y - 1][x];
170            let left = places[y][x - 1];
171            let (&point, _) = edges
172                .iter()
173                .find(|(k, v)| !seen.contains(k) && v.contains(&above) && v.contains(&left))
174                .unwrap();
175
176            places[y][x] = point;
177            seen.insert(point);
178        }
179    }
180
181    places[0][5] = places[0][4];
182    places[5][0] = places[5][1];
183
184    for y in 0..6 {
185        for x in 0..5 {
186            let key = (places[y][x], places[y][x + 1]);
187            horizontal[y][x] = *weight.get(&key).unwrap_or(&0);
188        }
189    }
190
191    for y in 0..5 {
192        for x in 0..6 {
193            let key = (places[y][x], places[y + 1][x]);
194            vertical[y][x] = *weight.get(&key).unwrap_or(&0);
195        }
196    }
197
198    Input { extra, horizontal, vertical }
199}
200
201/// Modified depth first search that only allows paths that skip one node.
202///
203/// For a 6x6 grid there are 1262816 total possible rook walks
204/// (given by [OEIS A007764](https://oeis.org/A007764)).
205///
206/// However since we want the longest path it only makes sense to consider the paths that visit the
207/// most possible nodes. There are only 10180 of these paths making it much faster.
208fn dfs(input: &Input, state: &mut State, mut row: usize, mut col: usize, mut steps: u32) {
209    // Wrap around at end of each row.
210    if col == 6 {
211        // We've reached the bottom right corner.
212        if row == 5 {
213            state.result = state.result.max(steps);
214            return;
215        }
216        row += 1;
217        col = 0;
218    }
219
220    if state.grid[row][col] == 0 {
221        // Skip only 1 node in each path.
222        if !(state.skipped || (row == 5 && col == 5)) {
223            state.skipped = true;
224            state.grid[row + 1][col] = 0;
225            dfs(input, state, row, col + 1, steps);
226            state.skipped = false;
227        }
228
229        // Create new paths (except on the final row).
230        if row < 5 {
231            let id = state.letter;
232            steps += input.vertical[row][col];
233
234            for end in (col + 1)..6 {
235                state.grid[row + 1][end - 1] = 0;
236                steps += input.horizontal[row][end - 1];
237
238                if state.grid[row][end] == 0 {
239                    state.grid[row + 1][col] = id;
240                    state.grid[row + 1][end] = id;
241                    let extra = input.vertical[row][end];
242                    state.letter += 1;
243                    dfs(input, state, row, end + 1, steps + extra);
244                    state.letter -= 1;
245                } else {
246                    state.grid[row + 1][col] = state.convert[state.grid[row][end] as usize];
247                    state.grid[row + 1][end] = 0;
248                    dfs(input, state, row, end + 1, steps);
249                    break;
250                }
251            }
252        }
253    } else {
254        let index = state.grid[row][col] as usize;
255        let id = state.convert[index];
256
257        // Straight down
258        if row < 5 || col == 5 {
259            state.grid[row + 1][col] = id;
260            let extra = input.vertical[row][col];
261            dfs(input, state, row, col + 1, steps + extra);
262        }
263
264        for end in (col + 1)..6 {
265            state.grid[row + 1][end - 1] = 0;
266            steps += input.horizontal[row][end - 1];
267
268            if state.grid[row][end] == 0 {
269                // Move down only if not final row (except final corner).
270                if row < 5 || end == 5 {
271                    state.grid[row + 1][end] = id;
272                    let extra = input.vertical[row][end];
273                    dfs(input, state, row, end + 1, steps + extra);
274                }
275            } else {
276                // Join two path together as long as they are different.
277                // (prevent disjoint loops)
278                let other = state.convert[state.grid[row][end] as usize];
279
280                if id != other {
281                    state.grid[row + 1][end] = 0;
282                    state.convert[index] = other;
283                    dfs(input, state, row, end + 1, steps);
284                    state.convert[index] = id;
285                }
286
287                break;
288            }
289        }
290    }
291}