Skip to content

Commit f68ee37

Browse files
committed
Use Held-Karp dynamic programming rather than permutations
Switch to a different algorithm for finding the shortest path, using O(n^2*2^n) rather than O(n!) work. 2016 day 24 is nicer than 2015 days 9 and 13, in that the problem at hand really is asking about a path or cycle pinned to node 0, so we can drop to n=7 for a mere 7*6/2*128/2 = 1344 comparisons, outright beating the 2520 comparisons of the prior 7!/2 approach. On my laptop, performance improves from about 260us to 240us.
1 parent 03ca6d6 commit f68ee37

1 file changed

Lines changed: 50 additions & 25 deletions

File tree

src/year2016/day24.rs

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
11
//! # Air Duct Spelunking
22
//!
3-
//! This is a variant of the classic
4-
//! [Travelling Salesman Problem](https://en.wikipedia.org/wiki/Travelling_salesman_problem) and
5-
//! is similar to [`Year 2015 Day 13`].
3+
//! This is a variant of the classic [Travelling Salesman Problem] and is similar to
4+
//! [`Year 2015 Day 13`].
65
//!
76
//! We first simplify the problem by finding the distance between all locations using multiple
87
//! [BFS](https://en.wikipedia.org/wiki/Breadth-first_search)
98
//! searches starting from each location.
109
//!
11-
//! For speed we convert each location into an index, then store the distances between
12-
//! every pair of locations in a vec for fast lookup. Our utility [`half_permutations`] method uses
13-
//! [Steinhaus-Johnson-Trotter's algorithm](https://en.wikipedia.org/wiki/Steinhaus-Johnson-Trotter_algorithm) for efficiency,
14-
//! modifying the slice in place.
10+
//! Then we can use [Held-Karp's dynamic programming][Held-Karp] algorithm to determine the
11+
//! shortest cycle. The problem asks us to start from node 0, which conveniently means that the
12+
//! value g(127, k) is the shortest path to k, and adding the distance from k back to 0 for part 2
13+
//! is also trivial. Thus, this day completes with only `7*6/2*128/2` or 1,344 comparisons, quite a
14+
//! bit better than the 2,520 comparisons needed for an approach with 7!/2 permutations. A
15+
//! slight complication is that set bit 0 maps to node 1.
1516
//!
16-
//! There are 8 locations, however since we always start at `0` this requires checking only
17-
//! 7!/2 = 2,520 permutations. We find the answer to both part one and two simultaneously.
18-
//!
19-
//! [`half_permutations`]: crate::util::slice
2017
//! [`Year 2015 Day 13`]: crate::year2015::day13
18+
//! [Travelling Salesman Problem]: https://en.wikipedia.org/wiki/Travelling_salesman_problem
19+
//! [Held-Karp]: https://en.wikipedia.org/wiki/Held%E2%80%93Karp_algorithm
20+
use crate::util::bitset::*;
2121
use crate::util::grid::*;
2222
use crate::util::parse::*;
23-
use crate::util::slice::*;
2423
use std::collections::VecDeque;
2524

26-
type Input = (u32, u32);
25+
type Input = (u16, u16);
2726

2827
pub fn parse(input: &str) -> Input {
2928
let grid = Grid::parse(input);
@@ -77,26 +76,52 @@ pub fn parse(input: &str) -> Input {
7776
}
7877

7978
// Solve both parts simultaneously.
80-
let mut part_one = u32::MAX;
81-
let mut part_two = u32::MAX;
82-
let mut indices: Vec<_> = (1..found.len()).collect();
79+
// Initialize a table for each part: 2ⁿ⁻¹ sets with n-1 distances per set. Default each g({k},k)
80+
// singleton to distance[0][k+1] (since bit 0 maps to node 1), while the initial value of other
81+
// sets does not matter.
82+
let mut table = [[0_u16; 7]; 1 << 7];
83+
for k in 0..found.len() - 1 {
84+
table[1 << k][k] = distance[0][k + 1];
85+
}
8386

84-
indices.half_permutations(|slice| {
85-
let first = distance[0][slice[0]];
86-
let middle = slice.array_windows().map(|&[from, to]| distance[from][to]).sum::<u32>();
87-
let last = distance[slice[slice.len() - 1]][0];
87+
// Visit each non-empty set in order, with no work to do for singleton sets.
88+
for set in 3_usize..(1 << (found.len() - 1)) {
89+
if set & !(set - 1) == set {
90+
continue;
91+
}
8892

89-
part_one = part_one.min(first + middle).min(middle + last);
90-
part_two = part_two.min(first + middle + last);
91-
});
93+
// For a given set, compute each g(set,k) for all k in the set.
94+
for k in set.biterator() {
95+
let subset = set ^ (1 << k);
96+
let mut shortest = u16::MAX;
97+
98+
// For a given destination k, find which other bit m gives the best path from the
99+
// subset to m, and then m to k. All table[subset] references were filled in prior
100+
// iterations of the outer loop or the singleton base cases.
101+
for m in subset.biterator() {
102+
shortest = shortest.min(table[subset][m] + distance[m + 1][k + 1]);
103+
}
104+
table[set][k] = shortest;
105+
}
106+
}
107+
108+
// With the sets now built, we have 7 candidates for each answer.
109+
let mut part_one = u16::MAX;
110+
let mut part_two = u16::MAX;
111+
for (k, &path_len) in
112+
table[(1 << (found.len() - 1)) - 1].iter().take(found.len() - 1).enumerate()
113+
{
114+
part_one = part_one.min(path_len);
115+
part_two = part_two.min(path_len + distance[k + 1][0]);
116+
}
92117

93118
(part_one, part_two)
94119
}
95120

96-
pub fn part1(input: &Input) -> u32 {
121+
pub fn part1(input: &Input) -> u16 {
97122
input.0
98123
}
99124

100-
pub fn part2(input: &Input) -> u32 {
125+
pub fn part2(input: &Input) -> u16 {
101126
input.1
102127
}

0 commit comments

Comments
 (0)