2020-12-17 00:31:36 +01:00
|
|
|
use std::collections::HashMap;
|
2020-12-16 09:28:45 +01:00
|
|
|
use std::fmt::Write;
|
|
|
|
use std::ops::RangeInclusive;
|
|
|
|
|
|
|
|
use anyhow::{Context, Result};
|
|
|
|
|
|
|
|
const INPUT: &str = include_str!("../input/day16.txt");
|
|
|
|
|
|
|
|
pub fn run() -> Result<String> {
|
|
|
|
let mut res = String::with_capacity(128);
|
|
|
|
|
|
|
|
writeln!(res, "part 1: {}", part1(INPUT)?)?;
|
2020-12-17 00:31:36 +01:00
|
|
|
writeln!(res, "part 2: {}", part2(INPUT)?)?;
|
2020-12-16 09:28:45 +01:00
|
|
|
|
|
|
|
Ok(res)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn part1(input: &str) -> Result<u64> {
|
|
|
|
let (fields, _, tickets) = parse_input(input)?;
|
|
|
|
|
2020-12-17 00:31:36 +01:00
|
|
|
let fields_vec = fields.values().collect::<Vec<_>>();
|
|
|
|
|
2020-12-16 09:28:45 +01:00
|
|
|
Ok(tickets
|
|
|
|
.iter()
|
2020-12-17 00:31:36 +01:00
|
|
|
.map(|t| t.invalid_values(&fields_vec))
|
2020-12-16 09:28:45 +01:00
|
|
|
.flatten()
|
|
|
|
.sum())
|
|
|
|
}
|
|
|
|
|
2020-12-17 00:31:36 +01:00
|
|
|
fn assign_field_positions(
|
|
|
|
fields: HashMap<&str, Field>,
|
|
|
|
tickets: Vec<Ticket>,
|
|
|
|
) -> HashMap<usize, &str> {
|
|
|
|
let fields_vec = fields.values().collect::<Vec<_>>();
|
|
|
|
|
|
|
|
let tickets: Vec<Ticket> = tickets
|
|
|
|
.into_iter()
|
|
|
|
.filter(|t| t.invalid_values(&fields_vec).count() == 0)
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
let num_values = tickets[0].values.len();
|
|
|
|
|
|
|
|
// build list of all possibilities for each field
|
|
|
|
let mut possibilities = fields
|
|
|
|
.iter()
|
|
|
|
.map(|(&name, field)| {
|
|
|
|
let possibilities = (0..num_values)
|
|
|
|
.into_iter()
|
|
|
|
.filter(|i| tickets.iter().all(|t| t.valid_field(field, *i)))
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
(name, possibilities)
|
|
|
|
})
|
|
|
|
.collect::<HashMap<&str, Vec<usize>>>();
|
|
|
|
|
|
|
|
let mut fields_to_assign: Vec<&str> = fields.keys().copied().collect();
|
|
|
|
let mut field_indices: HashMap<usize, &str> = HashMap::new();
|
|
|
|
|
|
|
|
for _ in 0..fields.len() {
|
|
|
|
// get field with only one possibility, and assign it
|
|
|
|
let field = fields_to_assign
|
|
|
|
.iter()
|
|
|
|
.min_by_key(|&name| possibilities[name].len())
|
|
|
|
.copied()
|
|
|
|
.expect("fields_to_assign should never be empty in this loop");
|
|
|
|
|
|
|
|
assert_eq!(possibilities[field].len(), 1);
|
|
|
|
|
|
|
|
let possibility = possibilities[field][0];
|
|
|
|
|
|
|
|
// remove position from other fields' possibilities
|
|
|
|
possibilities.values_mut().for_each(|list| {
|
|
|
|
if let Some(pos) = list.iter().position(|idx| *idx == possibility) {
|
|
|
|
list.swap_remove(pos);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
field_indices.insert(possibility, field);
|
|
|
|
fields_to_assign.swap_remove(fields_to_assign.iter().position(|f| *f == field).unwrap());
|
|
|
|
}
|
|
|
|
|
|
|
|
field_indices
|
|
|
|
}
|
|
|
|
|
|
|
|
fn part2(input: &str) -> Result<u64> {
|
|
|
|
let (fields, my_ticket, tickets) = parse_input(input)?;
|
|
|
|
|
|
|
|
let field_pos_matches = assign_field_positions(fields, tickets);
|
|
|
|
|
|
|
|
Ok(my_ticket
|
|
|
|
.values
|
|
|
|
.iter()
|
|
|
|
.enumerate()
|
|
|
|
.filter(|(idx, _)| {
|
|
|
|
let field = field_pos_matches[idx];
|
|
|
|
field.starts_with("departure")
|
|
|
|
})
|
|
|
|
.map(|(_, val)| val)
|
|
|
|
.product())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_input(input: &str) -> Result<(HashMap<&str, Field>, Ticket, Vec<Ticket>)> {
|
2020-12-16 09:28:45 +01:00
|
|
|
let mut parts = input.split("\n\n");
|
|
|
|
|
|
|
|
let fields_part = parts.next().context("no fields specification found")?;
|
|
|
|
let my_ticket_part = parts.next().context("no personal ticket found")?;
|
|
|
|
let tickets_part = parts.next().context("no list of tickets found")?;
|
|
|
|
|
|
|
|
let fields = fields_part
|
|
|
|
.lines()
|
2020-12-17 00:31:36 +01:00
|
|
|
.map(|line| {
|
|
|
|
let mut parts = line.split(": ");
|
|
|
|
let name = parts.next().context("no name found")?;
|
|
|
|
let field = parts.next().context("no ranges found")?.parse()?;
|
|
|
|
|
|
|
|
Ok((name, field))
|
|
|
|
})
|
2020-12-16 09:28:45 +01:00
|
|
|
.collect::<Result<_>>()
|
|
|
|
.context("couldn't parse fields")?;
|
|
|
|
let my_ticket = my_ticket_part
|
|
|
|
.lines()
|
|
|
|
.skip(1)
|
|
|
|
.map(|line| line.parse())
|
|
|
|
.next()
|
|
|
|
.context("no second line for ticket")??;
|
|
|
|
let tickets = tickets_part
|
|
|
|
.lines()
|
|
|
|
.skip(1)
|
|
|
|
.map(|line| line.parse())
|
|
|
|
.collect::<Result<_>>()
|
|
|
|
.context("couldn't parse tickets")?;
|
|
|
|
|
|
|
|
Ok((fields, my_ticket, tickets))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
struct Ticket {
|
|
|
|
values: Vec<u64>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Ticket {
|
2020-12-17 00:31:36 +01:00
|
|
|
fn invalid_values<'a>(&'a self, fields: &'a [&Field]) -> impl Iterator<Item = u64> + 'a {
|
2020-12-16 09:28:45 +01:00
|
|
|
self.values
|
|
|
|
.iter()
|
|
|
|
.copied()
|
2020-12-17 00:31:36 +01:00
|
|
|
.filter(move |val| !fields.iter().any(|f| f.contains(val)))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn valid_field(&self, field: &Field, val_idx: usize) -> bool {
|
|
|
|
field.contains(&self.values[val_idx])
|
2020-12-16 09:28:45 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl std::str::FromStr for Ticket {
|
|
|
|
type Err = anyhow::Error;
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self> {
|
|
|
|
let values = s
|
|
|
|
.split(',')
|
|
|
|
.map(|num| {
|
|
|
|
num.parse()
|
|
|
|
.with_context(|| format!("couldn't parse `{}`", num))
|
|
|
|
})
|
|
|
|
.collect::<Result<_>>()?;
|
|
|
|
|
|
|
|
Ok(Ticket { values })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
struct Field {
|
|
|
|
ranges: (RangeInclusive<u64>, RangeInclusive<u64>),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Field {
|
|
|
|
fn contains(&self, val: &u64) -> bool {
|
|
|
|
self.ranges.0.contains(val) || self.ranges.1.contains(val)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl std::str::FromStr for Field {
|
|
|
|
type Err = anyhow::Error;
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self> {
|
2020-12-17 00:31:36 +01:00
|
|
|
let mut ranges = s.split(" or ");
|
2020-12-16 09:28:45 +01:00
|
|
|
|
|
|
|
let mut range1 = ranges.next().context("no first range found")?.split('-');
|
|
|
|
let range1_start = range1.next().context("no bound for range")?.parse()?;
|
|
|
|
let range1_end = range1.next().context("no bound for range")?.parse()?;
|
|
|
|
|
|
|
|
let mut range2 = ranges.next().context("no second range found")?.split('-');
|
|
|
|
let range2_start = range2.next().context("no bound for range")?.parse()?;
|
|
|
|
let range2_end = range2.next().context("no bound for range")?.parse()?;
|
|
|
|
|
|
|
|
Ok(Field {
|
|
|
|
ranges: (range1_start..=range1_end, range2_start..=range2_end),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
2020-12-17 00:31:36 +01:00
|
|
|
const PROVIDED1: &str = include_str!("../input/day16_provided1.txt");
|
|
|
|
const PROVIDED2: &str = include_str!("../input/day16_provided2.txt");
|
2020-12-16 09:28:45 +01:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn part1_provided() {
|
2020-12-17 00:31:36 +01:00
|
|
|
assert_eq!(part1(PROVIDED1).unwrap(), 71);
|
2020-12-16 09:28:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn part1_real() {
|
|
|
|
assert_eq!(part1(INPUT).unwrap(), 20013);
|
|
|
|
}
|
2020-12-17 00:31:36 +01:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn part2_provided() {
|
|
|
|
let (fields, _, tickets) = parse_input(PROVIDED2).unwrap();
|
|
|
|
|
|
|
|
let matches = assign_field_positions(fields, tickets);
|
|
|
|
|
|
|
|
let expected = (&["row", "class", "seat"])
|
|
|
|
.iter()
|
|
|
|
.copied()
|
|
|
|
.enumerate()
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
assert_eq!(matches, expected);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn part2_real() {
|
|
|
|
assert_eq!(part2(INPUT).unwrap(), 5977293343129);
|
|
|
|
}
|
2020-12-16 09:28:45 +01:00
|
|
|
}
|