1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
//! # Clumsy Crucible
//!
//! Our high level approach is an [A*](https://en.wikipedia.org/wiki/A*_search_algorithm) search.
//! This [fantastic blog](https://www.redblobgames.com/pathfinding/a-star/introduction.html)
//! is a great introduction to this algorithm.
//!
//! The heuristic is the [Manhattan distance](https://en.wikipedia.org/wiki/Taxicab_geometry)
//! to the bottom right corner. This will never overestimate the actual distance which is an
//! essential characteristic in the heuristic.
//!
//! A crucial insight speeds things up. We only needs to store `(position, direction)` pairs in
//! the map of previously seen costs and do not also need to store the number of steps.
//! The reason is that each time we generate new states from the current state we loop over all
//! possible forward states. This implicitly means that every new state will always make a left or
//! right turn, alternating between horizontal and vertical movements.
//!
//! It's a little more subtle but we also don't need to store 4 directions but only 2, horizontal
//! and vertical. The reason is similar to not encoding the number of steps. As we are always
//! implictly going to make a left or right turn immediately, entering a square from the opposite
//! direction is equivalent. This reduces the storage space and time by half.
//!
//! To speed things up even further we use a trick. Classic A* uses a generic priority queue that
//! can be implemented in Rust using a [`BinaryHeap`]. However the total cost follows a strictly
//! increasing order in a constrained range of values, so we can use a much faster
//! [bucket queue](https://en.wikipedia.org/wiki/Bucket_queue). The maximum possible increase in
//! heuristic is 10 * 9 from heat plus 10 for the distance change for a total of 100 buckets.
//!
//! [`BinaryHeap`]: std::collections::BinaryHeap
use crate::util::grid::*;
use crate::util::parse::*;

/// Parse the input into a 2D grid of `u8` then convert to `u32` for convenience.
pub fn parse(input: &str) -> Grid<i32> {
    let Grid { width, height, bytes } = Grid::parse(input);
    let bytes = bytes.iter().map(|b| b.to_decimal() as i32).collect();
    Grid { width, height, bytes }
}

/// Search with a maximum of 3 steps in any direction.
pub fn part1(grid: &Grid<i32>) -> i32 {
    astar::<1, 3>(grid)
}

/// Search with a minimum of 4 and maximum of 10 steps in any direction. Using const generics
/// to specify the limits allows the compiler to optimize and unroll loops, speeding things
/// up by about 25%, versus specifying the loop limits as regular parameters.
pub fn part2(grid: &Grid<i32>) -> i32 {
    astar::<4, 10>(grid)
}

/// Optimized A* search.
fn astar<const L: i32, const U: i32>(grid: &Grid<i32>) -> i32 {
    let size = grid.width;
    let stride = size as usize;
    let heat = &grid.bytes;

    let mut index = 0;
    let mut todo = (0..100).map(|_| Vec::with_capacity(1000)).collect::<Vec<_>>();
    let mut cost = vec![[i32::MAX; 2]; heat.len()];

    // Start from the top left corner checking both vertical and horizontal directions.
    todo[0].push((0, 0, 0));
    todo[0].push((0, 0, 1));

    cost[0][0] = 0;
    cost[0][1] = 0;

    loop {
        // All items in the same bucket have the same priority.
        while let Some((x, y, direction)) = todo[index % 100].pop() {
            // Retrieve cost for our current location and direction.
            let index = (size * y + x) as usize;
            let steps = cost[index][direction];

            // The heuristic is used as an index into the bucket priority queue.
            // Prefer heading towards the bottom right corner, except if we're in the top left
            // quadrant where all directions are considered equally. This prevents a pathological
            // dual frontier on some inputs that takes twice the time.
            let heuristic = |x: i32, y: i32, cost: i32| {
                let priority = (2 * size - x - y).min(size + size / 2);
                ((cost + priority) % 100) as usize
            };

            // Check if we've reached the end.
            if x == size - 1 && y == size - 1 {
                return steps;
            }

            // Alternate directions each turn. We arbitrarily pick `0` to mean vertical and `1` to
            // mean horizontal. These constants are used as offsets into the `cost` array.
            if direction == 0 {
                // We just moved vertically so now check both left and right directions.

                // Each direction loop is the same:
                // * Check to see if we gone out of bounds
                // * Increase the cost by the "heat" of the square we've just moved into.
                // * Check if we've already been to this location with a lower cost.
                // * Add new state to priority queue.

                // Right
                let mut next = index;
                let mut extra = steps;

                for i in 1..=U {
                    if x + i >= size {
                        break;
                    }

                    next += 1;
                    extra += heat[next];

                    if i >= L && extra < cost[next][1] {
                        todo[heuristic(x + i, y, extra)].push((x + i, y, 1));
                        cost[next][1] = extra;
                    }
                }

                // Left
                let mut next = index;
                let mut extra = steps;

                for i in 1..=U {
                    if i > x {
                        break;
                    }

                    next -= 1;
                    extra += heat[next];

                    if i >= L && extra < cost[next][1] {
                        todo[heuristic(x - i, y, extra)].push((x - i, y, 1));
                        cost[next][1] = extra;
                    }
                }
            } else {
                // We just moved horizontally so now check both up and down directions.

                // Down
                let mut next = index;
                let mut extra = steps;

                for i in 1..=U {
                    if y + i >= size {
                        break;
                    }

                    next += stride;
                    extra += heat[next];

                    if i >= L && extra < cost[next][0] {
                        todo[heuristic(x, y + i, extra)].push((x, y + i, 0));
                        cost[next][0] = extra;
                    }
                }

                // Up
                let mut next = index;
                let mut extra = steps;

                for i in 1..=U {
                    if i > y {
                        break;
                    }

                    next -= stride;
                    extra += heat[next];

                    if i >= L && extra < cost[next][0] {
                        todo[heuristic(x, y - i, extra)].push((x, y - i, 0));
                        cost[next][0] = extra;
                    }
                }
            }
        }

        // Bump priority by one to check the next bucket.
        index += 1;
    }
}