diff --git a/aoc2020/input/day16_provided.txt b/aoc2020/input/day16_provided1.txt similarity index 100% rename from aoc2020/input/day16_provided.txt rename to aoc2020/input/day16_provided1.txt diff --git a/aoc2020/input/day16_provided2.txt b/aoc2020/input/day16_provided2.txt new file mode 100644 index 0000000..b48bf37 --- /dev/null +++ b/aoc2020/input/day16_provided2.txt @@ -0,0 +1,11 @@ +class: 0-1 or 4-19 +row: 0-5 or 8-19 +seat: 0-13 or 16-19 + +your ticket: +11,12,13 + +nearby tickets: +3,9,18 +15,1,5 +5,14,9 diff --git a/aoc2020/src/day16.rs b/aoc2020/src/day16.rs index ab65d65..9d0dcdb 100644 --- a/aoc2020/src/day16.rs +++ b/aoc2020/src/day16.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::fmt::Write; use std::ops::RangeInclusive; @@ -9,6 +10,7 @@ pub fn run() -> Result { let mut res = String::with_capacity(128); writeln!(res, "part 1: {}", part1(INPUT)?)?; + writeln!(res, "part 2: {}", part2(INPUT)?)?; Ok(res) } @@ -16,14 +18,88 @@ pub fn run() -> Result { fn part1(input: &str) -> Result { let (fields, _, tickets) = parse_input(input)?; + let fields_vec = fields.values().collect::>(); + Ok(tickets .iter() - .map(|t| t.invalid_values(&fields)) + .map(|t| t.invalid_values(&fields_vec)) .flatten() .sum()) } -fn parse_input(input: &str) -> Result<(Vec, Ticket, Vec)> { +fn assign_field_positions( + fields: HashMap<&str, Field>, + tickets: Vec, +) -> HashMap { + let fields_vec = fields.values().collect::>(); + + let tickets: Vec = 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::>>(); + + let mut fields_to_assign: Vec<&str> = fields.keys().copied().collect(); + let mut field_indices: HashMap = 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 { + 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)> { let mut parts = input.split("\n\n"); let fields_part = parts.next().context("no fields specification found")?; @@ -32,7 +108,13 @@ fn parse_input(input: &str) -> Result<(Vec, Ticket, Vec)> { let fields = fields_part .lines() - .map(|line| line.parse()) + .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)) + }) .collect::>() .context("couldn't parse fields")?; let my_ticket = my_ticket_part @@ -57,12 +139,15 @@ struct Ticket { } impl Ticket { - fn invalid_values(&self, fields: &[Field]) -> Vec { + fn invalid_values<'a>(&'a self, fields: &'a [&Field]) -> impl Iterator + 'a { self.values .iter() - .filter(|val| !fields.iter().any(|f| f.contains(*val))) .copied() - .collect() + .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]) } } @@ -84,7 +169,6 @@ impl std::str::FromStr for Ticket { #[derive(Debug)] struct Field { - name: String, ranges: (RangeInclusive, RangeInclusive), } @@ -98,11 +182,7 @@ impl std::str::FromStr for Field { type Err = anyhow::Error; fn from_str(s: &str) -> Result { - let mut parts = s.split(": "); - - let name = parts.next().context("no name found")?.to_string(); - - let mut ranges = parts.next().context("no ranges found")?.split(" or "); + let mut ranges = s.split(" or "); let mut range1 = ranges.next().context("no first range found")?.split('-'); let range1_start = range1.next().context("no bound for range")?.parse()?; @@ -113,7 +193,6 @@ impl std::str::FromStr for Field { let range2_end = range2.next().context("no bound for range")?.parse()?; Ok(Field { - name, ranges: (range1_start..=range1_end, range2_start..=range2_end), }) } @@ -123,15 +202,36 @@ impl std::str::FromStr for Field { mod tests { use super::*; - const PROVIDED: &str = include_str!("../input/day16_provided.txt"); + const PROVIDED1: &str = include_str!("../input/day16_provided1.txt"); + const PROVIDED2: &str = include_str!("../input/day16_provided2.txt"); #[test] fn part1_provided() { - assert_eq!(part1(PROVIDED).unwrap(), 71); + assert_eq!(part1(PROVIDED1).unwrap(), 71); } #[test] fn part1_real() { assert_eq!(part1(INPUT).unwrap(), 20013); } + + #[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); + } }