use std::{io::stdin, ops::Range}; use anyhow::{Result, anyhow, bail, Context}; use itertools::Itertools; mod parse; #[derive(Debug, Clone, Copy)] struct MapRange { base: usize, target: usize, } /// Stores a map as a series of contiguous offset ranges. #[derive(Debug, Clone)] struct Map { from: String, to: String, ranges: Vec, } impl Map { fn identity(from: String) -> Self{ let to = from.clone(); Self { from, to, ranges: vec![MapRange { base: 0, target: 0 }] } } /// Build a compact segment map from a list of (target, source, length) tuples fn from_ranges(from: String, to: String, mut v: Vec<(usize,usize,usize)>) -> Self { v.sort_by_key(|r| r.1); let mut ranges = Vec::with_capacity(2*v.len() + 1); ranges.push(MapRange {base: 0, target: 0 } ); for (target, base, len) in v { if ranges.last().unwrap().base == base { ranges.pop(); } ranges.push(MapRange { base, target }); ranges.push(MapRange { base: base+len, target: base+len }); } Self { from, to, ranges } } /// Gives the segment index relevant for a given source location in the map fn range_idx(&self, target: usize) -> usize { self.ranges.binary_search_by_key(&target, |r| r.base) .unwrap_or_else(|i| i-1) //INVARIANT: `i`` can never be 0 because we always have a range starting at 0 } /// Computes the image of a location over the map fn lookup(&self, src: usize) -> usize { let range = &self.ranges[self.range_idx(src)]; (src + range.target) - range.base } fn segments(&self) -> impl Iterator, Range)> + '_ { self.ranges.iter() .zip(self.ranges.iter().dropping(1)) .map(|(a,b)| (a.base..b.base, a.target..(b.base-a.base))) } /// Composes two maps fn compose(&self, rhs: &Map) -> Result { if self.to != rhs.from { bail!("incompatible maps {self:?} and {rhs:?}")}; let mut ranges = Vec::with_capacity(self.ranges.len() + rhs.ranges.len()); for i in 0..self.ranges.len() { let mine = &self.ranges[i]; // because we don't store ends of ranges, we have to check the next one let stop = self.ranges.get(i+1) .map(|next| next.base + mine.target - mine.base); // watch for order of // operations because // unsigned let start_index = rhs.range_idx(mine.target); // stop index must be inclusive because our stop point may not match rhs boundary let stop_index = stop.map(|s| rhs.range_idx(s-1)).unwrap_or(rhs.ranges.len()-1); // The first target range begins before our own start, treat it specially let first_range = &rhs.ranges[start_index]; ranges.push(MapRange { base: mine.base, target: first_range.target + mine.target - first_range.base }); for other in &rhs.ranges[start_index+1 ..= stop_index] { ranges.push(MapRange { base: mine.base + other.base - mine.target, target: other.target }); } } Ok(Map { from: self.from.to_owned(), to: rhs.to.to_owned(), ranges, }) } } fn intersect(a: &Range, b: &Range) -> Range { a.start.max(b.start) .. a.end.min(b.end) } fn vec_to_ranges(s: &[usize]) -> Result>> { let mut i = s.iter(); let mut rs = Vec::with_capacity(s.len() / 2); loop { let Some(&start) = i.next() else { break }; let len = i.next().ok_or(anyhow!("odd number of values"))?; rs.push(start .. start+len); } Ok(rs) } fn main() -> Result<()> { let (seeds, maps) = parse::input(stdin().lock())?; eprintln!("seeds: {seeds:?}"); let map = maps.iter() .try_fold(Map::identity("seed".to_owned()), |lhs, rhs| lhs.compose(rhs)) .context(anyhow!("Building combined map"))?; eprintln!("built combined map from {} to {} ({} segments)", map.from, map.to, map.ranges.len()); let best = seeds.iter().copied() .map(|v| map.lookup(v)) .min(); println!("Part 1: lowest location: {best:?}"); let part2_ranges = vec_to_ranges(&seeds)?; let mut segment_index: Vec<_> = map.segments().collect(); segment_index.sort_by_key(|s| s.1.start); let mut best2: Option = None; for (src_range, dst_range) in &segment_index { if let Some(b) = best2 { if b <= dst_range.start { break } } for seeds in &part2_ranges { let inter = intersect(seeds, src_range); if !inter.is_empty() { let candidate = inter.start + dst_range.start - src_range.start; if let Some(b) = best2 { if b < candidate { continue } } best2 = Some(candidate); } } } let best2 = best2.ok_or(anyhow!("No intersect inside known range"))?; println!("Part 2: lowest location {best2}"); Ok(()) }