diff --git a/AdventOfCode/_2023/Day5.cs b/AdventOfCode/_2023/Day5.cs new file mode 100644 index 0000000..b874e53 --- /dev/null +++ b/AdventOfCode/_2023/Day5.cs @@ -0,0 +1,251 @@ +using AdventOfCode.Common; +using AdventOfCode.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace AdventOfCode._2023 +{ + public class Day5 : AOCDay + { + + private static MapType[] MAP_ORDER = new MapType[] { MapType.SOIL, MapType.FERTILIZER, MapType.WATER, MapType.LIGHT, MapType.TEMPERATURE, MapType.HUMIDITY, MapType.LOCATION }; + protected override AOCResponse ExecutePartA() + { + var inputs = this.SplitInputOnEmptyLine(); + + //Convert our seeds into a long list + var seeds = inputs.First().First().Split(":")[1].Trim().Split(" ").Select(x => long.Parse(x)).ToDictionary(key => key, value => value); + + //Create our map objects + var maps = new List(); + foreach (var map in inputs.Skip(1)) + { + maps.Add(new AlmanacMap(map)); + } + + var prevType = MapType.SEED; + foreach (var mapType in MAP_ORDER) + { + Log($"Mapping from {prevType} to {mapType}"); + var activeMap = maps.First(x => x.From == prevType && x.To == mapType); + foreach (var key in seeds.Keys) + { + seeds[key] = activeMap.Transform(seeds[key]); + Log($"Seed {key} has a value of {seeds[key]} for {mapType}"); + } + prevType = mapType; //Denote our next mapping + } + this.Answer = seeds.Values.Min(); + return this._response; + } + + protected override AOCResponse ExecutePartB() + { + var inputs = this.SplitInputOnEmptyLine(); + + //Create our map objects + var maps = new List(); + foreach (var map in inputs.Skip(1)) + { + maps.Add(new AlmanacMap(map)); + } + + var finalizedSeedRanges = new List(); + + //Get our seed ranges + var values = inputs.First().First().Split(":")[1].Trim().Split(" ").Select(x => long.Parse(x)).ToArray(); + for (int i = 0; i < values.Length; i += 2) + { + var startSeed = values[i]; + var endSeed = values[i] + values[i + 1]-1; //Minus one because it counts i + + var seed = new SeedRange() { Start = startSeed, End = endSeed }; + Log($"Transforming seed range {seed.Start} - {seed.End}"); + + var prevType = MapType.SEED; + var seedRanges = new List() { seed }; + foreach (var mapType in MAP_ORDER) + { + Log($"Mapping from {prevType} to {mapType}"); + var activeMap = maps.First(x => x.From == prevType && x.To == mapType); + seedRanges = activeMap.GetSeedRanges(seedRanges); + seedRanges.ForEach(x => Log($"Range: {x.Start} - {x.End}")); + prevType = mapType; //Denote our next mapping + } + + //Add the output ranges to our total list to be minimized later + finalizedSeedRanges = finalizedSeedRanges.Concat(seedRanges).ToList(); + } + this.Answer = finalizedSeedRanges.Min(x => x.Start); + return this._response; + } + } + + enum MapType + { + SEED, SOIL, FERTILIZER, WATER, LIGHT,TEMPERATURE, HUMIDITY, LOCATION + } + + class AlmanacMap + { + public MapType From { get; set; } + public MapType To { get; set; } + private readonly List _mapData; + public AlmanacMap(string[] maps) + { + var types = maps.First().Split(" ")[0].Split("-to-"); + From = Enum.Parse(types[0].ToUpper()); + To = Enum.Parse(types[1].ToUpper()); + + var mapDataList = new List(); + foreach (var mapData in maps.Skip(1)) + { + var splitMap = mapData.Split(" ").Select(x => long.Parse(x)).ToArray(); + var destinationRange = splitMap[0]; + var sourceRange = splitMap[1]; + var rangeLength = splitMap[2]; + mapDataList.Add(new MapData() + { + Destination = destinationRange, + Source = sourceRange, + Range = rangeLength + }); + } + _mapData = mapDataList.OrderBy(x => x.Source).ToList(); + } + + public long Transform(long value) + { + var result = value; + foreach (var mapData in _mapData) + { + if (value >= mapData.Source && value < (mapData.Source + mapData.Range)) + { + var rangeToBeAdded = value - mapData.Source; + result = mapData.Destination + rangeToBeAdded; + break; + } + } + return result; + } + + /// + /// Generate the given seed ranges for Part B with the large dataset + /// + /// + /// + public List GetSeedRanges(List ranges) + { + //All valid return ranges for the given input seed range + var totalRanges = new List(); + + foreach (var range in ranges) + { + var seedRanges = new List(); + var originalSeedRanges = new List(); + + foreach (var mapData in _mapData) + { + // Check if any part of rangeA is within rangeB, or vice versa + bool isRangeWithinMapData = (range.Start >= mapData.Source && range.Start <= mapData.SourceEnd) || + (range.End >= mapData.Source && range.End <= mapData.SourceEnd) || + (mapData.Source >= range.Start && mapData.Source <= range.End) || + (mapData.SourceEnd >= range.Start && mapData.SourceEnd <= range.End); + if (isRangeWithinMapData) + { + //Calculate the localized range in relation to the given range and the given map data + //Math.Max(0, range.Start - mapData.Source) - In the event the range start is below the start of the mapData, take 0 as that is within the range + // Math.Min(mapData.Range - 1, range.End - mapData.Source) - if the range extends past the bounds of mapData, take mapData -1. -1 is because we count the initial starting source in range + var localizedRange = new SeedRange(Math.Max(0, range.Start - mapData.Source), Math.Min(mapData.Range - 1, range.End - mapData.Source) ); + + //Calculate the Source values (we need this to track any values that aren't within our output bounds) + var inputStart = mapData.Source + localizedRange.Start; + var inputEnd = mapData.Source + localizedRange.End; + originalSeedRanges.Add(new SeedRange() + { + Start = inputStart, + End = inputEnd + }); + + //Calculate the valid output ranges for the given mapData (this is the actual transformation for the given ranges) + var outputStart = mapData.Destination + localizedRange.Start; + var outputEnd = mapData.Destination + localizedRange.End; + + seedRanges.Add(new SeedRange() + { + Start = outputStart, + End = outputEnd + }); + } + } + + //FOr all the ranges NOT covered by the original seed range, create a 1-1 mapping. + var validRangesNotFound = GetNonOverlappingRanges(range, originalSeedRanges.OrderBy(x => x.Start).ToList()); + + //Concat all of the seed ranges together + totalRanges = totalRanges.Concat(seedRanges.Concat(validRangesNotFound)).OrderBy(x => x.Start).ToList(); + } + return totalRanges; + } + + private List GetNonOverlappingRanges(SeedRange rangeA, List exclusions) + { + var result = new List(); + + // Add the range before the first exclusion, if any + if (exclusions.Count > 0 && rangeA.Start < exclusions[0].Start) + { + result.Add(new SeedRange(rangeA.Start, Math.Min(rangeA.End, exclusions[0].Start - 1))); + } + + // Add non-overlapping ranges between exclusions + for (int i = 0; i < exclusions.Count - 1; i++) + { + long start = exclusions[i].End + 1; + long end = exclusions[i + 1].Start - 1; + + if (start <= end) + { + result.Add(new SeedRange(start, end)); + } + } + + // Add the range after the last exclusion, if any + if (exclusions.Count > 0 && rangeA.End > exclusions[exclusions.Count - 1].End) + { + result.Add(new SeedRange(Math.Max(rangeA.Start, exclusions[exclusions.Count - 1].End + 1), rangeA.End)); + } + + if (exclusions.Count == 0) + { + result.Add(rangeA); + } + + return result; + } + } + + class SeedRange + { + public SeedRange() { } + public SeedRange(long Start, long End) + { + this.Start = Start; + this.End = End; + } + public long Start { get; set; } + public long End { get; set; } + } + + class MapData + { + public long Destination { get; set; } + public long Source { get; set; } + public long SourceEnd { get { return Source + Range; } } + public long Range { get; set; } + } + + +}