Skip to content

Commit 03ca6d6

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. For a given part, this results in slightly more calls to .max or .min (3584 instead of 2540), but less overhead than what permutations required for shuffling data around. Doing both parts at once requires a bit more work than day 9; part 2 is a longest path that requires 255 iterations from an implicit ninth node with distance zero, at which point the part one details can still be read off the table to manually bypass the ninth node if we can determine what the first node was. But that requires storing a bit more information in the table. On my laptop, performance improves from about 33us to 18us.
1 parent b524669 commit 03ca6d6

1 file changed

Lines changed: 60 additions & 35 deletions

File tree

src/year2015/day13.rs

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
//! # Knights of the Dinner Table
22
//!
33
//! This problem is very similar to [`Day 9`] and we solve it in almost exactly the same way by
4-
//! computing an adjacency matrix of happiness then permuting the order of the diners.
4+
//! computing an adjacency matrix of happiness then running [Held-Karp] to find the longest
5+
//! cycle. If part one were the only problem at hand, the answer would be possible by iterating
6+
//! over 127 sets and then selecting among seven candidates to close the loop back to whichever
7+
//! abritrary point we pinned as the start.
58
//!
6-
//! For part one we reduce the permutations from 8! = 40,320 permutations to 7!/2 = 2,520
7-
//! permutations by arbitrarily choosing one of the diners as the start, and skipping lexically
8-
//! reversed forms (seating a->b->c->a gives the same happiness as seating c->b->a->c).
9-
//!
10-
//! We solve part two at the same time by noticing that by inserting yourself between two diners
11-
//! you set the value of their mutual link to zero. Keeping track of the weakest link
12-
//! then subtracting that from the value for part one gives the result for part two at almost
13-
//! no additional cost.
9+
//! However, we are more interested in solving part two at the same time. Do this by noticing that
10+
//! when you insert yourself between two diners, you set the value of their mutual link to zero.
11+
//! This is effectively the same as inserting a ninth node into the algorithm, which we pin as the
12+
//! start node before iterating over 255 sets. Meanwhile, the results for part one can still be
13+
//! found from the table, if we also have an easy way to determine which diner was used to start
14+
//! the path represented by any given g(set,k). We can then manually close the loop of 8 diners
15+
//! by using all 8 g(255,k) plus the distance from k to the start node of that path, while the
16+
//! loop of 9 diners uses g(255,k) with no additional distance.
1417
//!
1518
//! [`Day 9`]: crate::year2015::day09
19+
//! [Held-Karp]: https://en.wikipedia.org/wiki/Held%E2%80%93Karp_algorithm
20+
use crate::util::bitset::*;
1621
use crate::util::hash::*;
1722
use crate::util::iter::*;
1823
use crate::util::parse::*;
19-
use crate::util::slice::*;
2024

21-
type Input = (i32, i32);
25+
type Input = (i16, i16);
2226

2327
pub fn parse(input: &str) -> Input {
2428
// Assign each diner an index on a first come first served basis.
@@ -35,52 +39,73 @@ pub fn parse(input: &str) -> Input {
3539

3640
// Calculate the happiness values. Note that the values are not reciprocal a => b != b => a.
3741
let stride = indices.len();
38-
let mut happiness = vec![0; stride * stride];
42+
let mut happiness = vec![0_i16; stride * stride];
3943

4044
for &[from, _, gain_lose, value, .., to, _] in &tokens {
4145
let start = indices[from];
4246
let end = indices[to];
4347
let sign = if gain_lose == "gain" { 1 } else { -1 };
44-
let value: i32 = value.signed();
48+
let value: i16 = value.signed();
4549

4650
// Add the values together to make the mutual link reciprocal.
4751
happiness[stride * start + end] += sign * value;
4852
happiness[stride * end + start] += sign * value;
4953
}
5054

5155
// Solve both parts simultaneously.
52-
let mut part_one = 0;
53-
let mut part_two = 0;
54-
let mut indices: Vec<_> = (1..stride).collect();
55-
56-
indices.half_permutations(|slice| {
57-
let mut sum = 0;
58-
let mut weakest_link = i32::MAX;
59-
60-
let mut link = |from, to| {
61-
let value = happiness[stride * from + to];
62-
sum += value;
63-
weakest_link = weakest_link.min(value);
64-
};
56+
// Initialize a shared table for both parts: 2ⁿ sets with n distances per set. Default 0 matches
57+
// g({k},k) for all singleton sets of zero distance from yourself, but tracking k as the start
58+
// of the path. The initial value of other sets does not matter.
59+
let zero = (0_i16, 0_u8);
60+
let mut table = vec![zero; stride * (1 << stride)];
61+
for k in 0..stride {
62+
table[(1 << k) * stride + k].1 = k as u8;
63+
}
6564

66-
link(0, slice[0]);
67-
link(0, slice[slice.len() - 1]);
65+
// Visit each non-empty set in order, with no work to do for singleton sets.
66+
for set in 3_usize..(1 << stride) {
67+
if set & !(set - 1) == set {
68+
continue;
69+
}
6870

69-
for i in 1..slice.len() {
70-
link(slice[i], slice[i - 1]);
71+
// For a given set, compute each g(set,k) for all k in the set.
72+
for k in set.biterator() {
73+
let subset = set ^ (1 << k);
74+
let mut longest = i16::MIN;
75+
let mut start = u8::MAX;
76+
77+
// For a given destination k, find which other bit m gives the best path from the
78+
// subset to m, and then m to k. All table[subset] references were filled in prior
79+
// iterations of the outer loop or the singleton base cases.
80+
for m in subset.biterator() {
81+
let prior = table[subset * stride + m];
82+
let distance = prior.0 + happiness[m * stride + k];
83+
if distance > longest {
84+
longest = distance;
85+
start = prior.1;
86+
}
87+
}
88+
table[set * stride + k] = (longest, start);
7189
}
90+
}
7291

73-
part_one = part_one.max(sum);
74-
part_two = part_two.max(sum - weakest_link);
75-
});
92+
// With the sets now built, we have stride candidates for each answer.
93+
// Part one requires completing the cycle back to the stashed first element.
94+
// Part two can be directly read off the table.
95+
let mut part_one = i16::MIN;
96+
let mut part_two = i16::MIN;
97+
for (k, &prior) in table[table.len() - stride..].iter().enumerate() {
98+
part_one = part_one.max(prior.0 + happiness[prior.1 as usize * stride + k]);
99+
part_two = part_two.max(prior.0);
100+
}
76101

77102
(part_one, part_two)
78103
}
79104

80-
pub fn part1(input: &Input) -> i32 {
105+
pub fn part1(input: &Input) -> i16 {
81106
input.0
82107
}
83108

84-
pub fn part2(input: &Input) -> i32 {
109+
pub fn part2(input: &Input) -> i16 {
85110
input.1
86111
}

0 commit comments

Comments
 (0)