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
//! This is my solution for [Advent of Code - Day 7 - _The Treachery of Whales_](https://adventofcode.com/2021/day/7)
//!
//! Today's input was a bunch of 🦀 crabs 🦀 (in submarines ) who can only move sideways (despite
//! being in submarines 🤷.) The challenge was to find the most efficient position to align them to,
//! given a particular cost function for a crab submarine to move a certain distance.
//!
//! For part one this was a linear cost, which reduces to finding the median, and calculating the
//! cost for each crab to move to that median. This is implemented by [`find_distance_to_median`].
//! As an informal proof for this method consider: for any given position value, incrementing that
//! position by one increases the overall cost by one per crab below that value, and decreases it by
//! one per crab above that value. Decrementing the position has the opposite effect. For any crab
//! other than the median, if you move the value towards the median, more crabs are on the side that
//! decreases the cost (by definition), and so the overall cost decreases. An interesting note in
//! the case of an even number of crabs, the value of either crab in the middle pair, and any values
//! between them will produce the same optimal cost, so in that case we can just pick one
//! arbitrarily.
//!
//! For part two the cost function was the triangular number of the distance, `1 => 1`, `2 => 3`,
//! `3 => 6`, ... or `(n * (n+1) ) / 2`. This is implemented by [`find_triangular_distance_to_mean`].
//! I have to admit I guessed the mean here. It seems reasonable - if the cost increases as you move
//! further you want a lot of crabs to move the shortest distance they can. Given there was an issue
//! where the rounding was slightly off (rounding the calculated mean worked for the sample, but
//! floor was needed for the puzzle input) I suspect the mean is the limit as the steepness of the
//! cost function increases, and it's close enough (±1) for triangular distance. But equally that
//! may just be a weirdness of integer maths. If anyone has information on more concrete theory
//! about this I'd be interested in a link.

use std::cmp::min;
use std::fs;

/// The entry point for running the solutions with the 'real' puzzle input.
///
/// - The puzzle input is expected to be at `<project_root>/res/day-7-input`
/// - It is expected this will be called by [`super::main()`] when the user elects to run day 7.
pub fn run() {
    let contents = fs::read_to_string("res/day-7-input").expect("Failed to read file");
    let positions: Vec<usize> = contents
        .trim()
        .split(',')
        .flat_map(|pos| pos.parse())
        .collect();

    let total_fuel = find_distance_to_median(&positions);
    println!("Total fuel to align - linear: {}", total_fuel);

    let total_fuel = find_triangular_distance_to_mean(&positions);
    println!("Total fuel to align - triangular: {}", total_fuel);
}

/// First find the median by sorting the list and taking the value at the midpoint. As discussed in
/// the summary, either midpoint is fine in the case of an even length list, so just use the default
/// rounding. Secondly iterate through the list to total the distance to the median and sum those
/// values.
fn find_distance_to_median(positions: &Vec<usize>) -> usize {
    let mut sorted = positions.to_vec();
    sorted.sort();
    let mid = sorted.len() / 2;
    let &median = sorted.get(mid).unwrap();

    positions
        .iter()
        .map(|&pos| (pos as isize - median as isize).abs() as usize)
        .sum()
}

/// Very similar to [`find_distance_to_median`] with three differences:
/// - Calculate mean instead of median for the target position
/// - Map the resulting fuel cost using the triangular number distance
/// - Calculate the total for the integer values both sides of the mean and take the lowest (see
///   main description)
fn find_triangular_distance_to_mean(positions: &Vec<usize>) -> usize {
    let mean = (positions.iter().sum::<usize>() as f64 / positions.len() as f64).floor() as usize;

    min(
        positions
            .iter()
            .map(|&pos| (pos as isize - mean as isize).abs() as usize)
            .map(|distance| (distance * (distance + 1)) / 2)
            .sum(),
        positions
            .iter()
            .map(|&pos| (pos as isize - (mean as isize + 1)).abs() as usize)
            .map(|distance| (distance * (distance + 1)) / 2)
            .sum(),
    )
}

#[cfg(test)]
mod tests {
    use crate::day_7::{find_distance_to_median, find_triangular_distance_to_mean};

    #[test]
    fn can_find_distance_to_median() {
        assert_eq!(
            find_distance_to_median(&vec![16, 1, 2, 0, 4, 2, 7, 1, 2, 14]),
            37
        )
    }

    #[test]
    fn can_find_triangular_distance_to_mean() {
        assert_eq!(
            find_triangular_distance_to_mean(&vec![16, 1, 2, 0, 4, 2, 7, 1, 2, 14]),
            168
        )
    }
}