aoc/year2015/
day17.rs

1//! # No Such Thing as Too Much
2//!
3//! Given `n` items the number of possible subsets is `2ⁿ`. We could brute force through each
4//! subset by iterating from 0 to 2ⁿ using the binary bits to indicate if a container is present.
5//! This will work but is a little slow as there are 20 containers, giving 2²⁰ = 1048576
6//! combinations to check.
7//!
8//! We speed things up by noticing that some containers are the same size, so the total number
9//! of combinations is fewer. For example if there are 3 containers of size `x` that is only
10//! 4 possibilities rather than 8.
11//!
12//! We have to multiply the result by [nCr](https://en.wikipedia.org/wiki/Combination)
13//! or the number of ways of choosing `r` items from `n`. For example if 2 containers out of 3
14//! are used in a solution then there are 3 ways of selecting the 2 containers. This solution
15//! hardcodes nCr tables for `n` up to 4.
16//!
17//! As an optimization containers are ordered from largest to smallest so that the
18//! recursive checking can exit as early as possible if we exceed 150 litres.
19use crate::util::parse::*;
20use std::collections::BTreeMap;
21
22/// nCr for `n` from 0 to 4 inclusive.
23const NCR: [[u32; 5]; 5] =
24    [[1, 0, 0, 0, 0], [1, 1, 0, 0, 0], [1, 2, 1, 0, 0], [1, 3, 3, 1, 0], [1, 4, 6, 4, 1]];
25
26struct State {
27    size: Vec<u32>,
28    freq: Vec<usize>,
29    result: Vec<u32>,
30}
31
32pub fn parse(input: &str) -> Vec<u32> {
33    // Collect size and frequency of each container.
34    let mut containers = BTreeMap::new();
35
36    for size in input.iter_unsigned() {
37        containers.entry(size).and_modify(|e| *e += 1).or_insert(1);
38    }
39
40    // Convenience struct to group size and frequency of each container, plus the number of
41    // combinations grouped by total number of containers.
42    let mut state = State {
43        size: containers.keys().copied().collect(),
44        freq: containers.values().copied().collect(),
45        result: vec![0; containers.len()],
46    };
47
48    // As an optimization order largest containers first so that we can exit early if the total
49    // size is greater than 150.
50    state.size.reverse();
51    state.freq.reverse();
52
53    combinations(&mut state, 0, 0, 0, 1);
54    state.result
55}
56
57/// We only care about the total combinations, so sum the entire vec.
58pub fn part1(input: &[u32]) -> u32 {
59    input.iter().sum()
60}
61
62/// We want the number of combination with the fewest containers, so find first non-zero value.
63pub fn part2(input: &[u32]) -> u32 {
64    *input.iter().find(|&&n| n > 0).unwrap()
65}
66
67/// Recursively try every possible combination, returning early if the size exceeds 150 litres.
68///
69/// `state`: Convenience struct to reduce parameters
70/// `index`: Current container
71/// `containers`: Number of containers used so far
72/// `litres`: How many litres of eggnog stored so far
73/// `factor`: The total different number of ways of selecting previous containers
74fn combinations(state: &mut State, index: usize, containers: usize, litres: u32, factor: u32) {
75    let n = state.freq[index];
76    let mut next = litres;
77
78    for r in 0..(n + 1) {
79        if next < 150 {
80            if index < state.size.len() - 1 {
81                combinations(state, index + 1, containers + r, next, factor * NCR[n][r]);
82            }
83        } else {
84            if next == 150 {
85                state.result[containers + r] += factor * NCR[n][r];
86            }
87            break;
88        }
89        next += state.size[index];
90    }
91}