diff --git a/2023/day05/main.go b/2023/day05/main.go new file mode 100644 index 0000000..f6217f0 --- /dev/null +++ b/2023/day05/main.go @@ -0,0 +1,196 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "math" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + seeds, listOfMappingRanges := parseInput(input) + + lowestFinalNum := math.MaxInt64 + + for _, seed := range seeds { + finalMapping := getFinalMapping(seed, listOfMappingRanges) + lowestFinalNum = mathy.MinInt(lowestFinalNum, finalMapping) + } + + return lowestFinalNum +} + +func getFinalMapping(seed int, listOfMappingRanges [][]mappingRange) int { + currentVal := seed + + // jeez these are horrible variable names... + for _, mappingRanges := range listOfMappingRanges { + // if currentVal is between the sourceStart and sourceStart plus count, a mappingRange is found + // otherwise it maps to the same value and move on + // [sourceStart, sourceStart + count) <- not inclusive of sourceStart + count + for _, m := range mappingRanges { + if m.sourceStart <= currentVal && currentVal < m.sourceStart+m.count { + currentVal = m.destinationStart + (currentVal - m.sourceStart) + // then break so number is only mapped once per round of mappings + break + } + } + } + + return currentVal +} + +type mappingRange struct { + // seed to soil map: 50 98 2 + // means 2 mappings total: seed 98 maps to water 50 and seed 99 maps to soil 51 + // if a mappingRange does not exist, then it maps to the same number + sourceStart, destinationStart, count int +} + +func parseInput(input string) (seeds []int, listOfMappingRanges [][]mappingRange) { + parts := strings.Split(input, "\n\n") + + seedParts := strings.Split(parts[0], " ") + // skip first part which is "seeds: " + for i := 1; i < len(seedParts); i++ { + seeds = append(seeds, cast.ToInt(seedParts[i])) + } + + // parse mappings + for p := 1; p < len(parts); p++ { + // get separate lines and ignore throw away first line of text + mappingLines := strings.Split(parts[p], "\n")[1:] + var mappings []mappingRange + for _, l := range mappingLines { + lineParts := strings.Split(l, " ") + mappings = append(mappings, mappingRange{ + destinationStart: cast.ToInt(lineParts[0]), + sourceStart: cast.ToInt(lineParts[1]), + count: cast.ToInt(lineParts[2]), + }) + } + + listOfMappingRanges = append(listOfMappingRanges, mappings) + } + + return seeds, listOfMappingRanges +} + +func part2(input string) int { + seedRanges, listOfMappingRanges := parseInput(input) + + lowestFinalNum := math.MaxInt64 + + // store final mappings to save on duplicate calcs? + // not sure if that helped. brute force solution worked in a minute or two + // finalMappings := map[int]int{} + + for i := 0; i < len(seedRanges); i += 2 { + for count := 0; count < seedRanges[i+1]; count++ { + seed := seedRanges[i] + count + finalMapping := getFinalMapping(seed, listOfMappingRanges) + lowestFinalNum = mathy.MinInt(lowestFinalNum, finalMapping) + } + + // progress check... + fmt.Println(i, len(seedRanges)) + } + + return lowestFinalNum +} + +// //////////// +// naive solutions that are too slow with actual input +// + +func naivePart1(input string) int { + seeds, allMaps := naiveParseInput(input) + lowestFinalNum := math.MaxInt64 + + for _, s := range seeds { + finalLocation := naiveMapSeedToFinalLocation(s, allMaps) + lowestFinalNum = mathy.MinInt(lowestFinalNum, finalLocation) + } + + return lowestFinalNum +} + +func naiveMapSeedToFinalLocation(seed int, allMaps []map[int]int) int { + mappedNum := seed + + for _, m := range allMaps { + num, ok := m[mappedNum] + if ok { + mappedNum = num + } + } + + return mappedNum +} + +// assume that the mappings are in a logical order in the input +// seeds -> soil -> fertilizer -> water -> etc and not out of order... +// so a slice works fine for storing the mappings +func naiveParseInput(input string) (seeds []int, allMaps []map[int]int) { + // seed to soil map: 50 98 2 + // means 2 mappings total: seed 98 maps to water 50 and seed 99 maps to soil 51 + // if a mappingRange does not exist, then it maps to the same number + + parts := strings.Split(input, "\n\n") + + seedParts := strings.Split(parts[0], " ") + // skip first part which is "seeds: " + for i := 1; i < len(seedParts); i++ { + seeds = append(seeds, cast.ToInt(seedParts[i])) + } + + // mappings + for p := 1; p < len(parts); p++ { + // get separate lines and ignore throw away first line of text + mappingLines := strings.Split(parts[p], "\n")[1:] + m := map[int]int{} + for _, line := range mappingLines { + nums := strings.Split(line, " ") + destinationStart, sourceStart, count := cast.ToInt(nums[0]), cast.ToInt(nums[1]), cast.ToInt(nums[2]) + for c := 0; c < count; c++ { + m[sourceStart+c] = destinationStart + c + } + } + allMaps = append(allMaps, m) + } + + return seeds, allMaps +} diff --git a/2023/day05/main_test.go b/2023/day05/main_test.go new file mode 100644 index 0000000..2ee0cec --- /dev/null +++ b/2023/day05/main_test.go @@ -0,0 +1,91 @@ +package main + +import ( + "testing" +) + +var example = `seeds: 79 14 55 13 + +seed-to-soil map: +50 98 2 +52 50 48 + +soil-to-fertilizer map: +0 15 37 +37 52 2 +39 0 15 + +fertilizer-to-water map: +49 53 8 +0 11 42 +42 0 7 +57 7 4 + +water-to-light map: +88 18 7 +18 25 70 + +light-to-temperature map: +45 77 23 +81 45 19 +68 64 13 + +temperature-to-humidity map: +0 69 1 +1 0 69 + +humidity-to-location map: +60 56 37 +56 93 4` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 35, + }, + { + name: "actual", + input: input, + want: 88151870, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 46, + }, + { + name: "actual", + input: input, + want: 2008785, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +}