diff --git a/day5/main.go b/day5/main.go index 4f6a5c0..c15fbed 100644 --- a/day5/main.go +++ b/day5/main.go @@ -1,24 +1,42 @@ +// 136110000 TOO HIGH +// 664041710 TOO HIGH +// 887862361 TOO HIGH + package main import ( + "context" "errors" "fmt" "io" "math" "os" "regexp" + "runtime" + "slices" "strconv" "strings" + "sync" ) -type MapRange struct { +var conversionSteps = []ConvertsBetween{ + {from: "seed", to: "soil"}, + {from: "soil", to: "fertilizer"}, + {from: "fertilizer", to: "water"}, + {from: "water", to: "light"}, + {from: "light", to: "temperature"}, + {from: "temperature", to: "humidity"}, + {from: "humidity", to: "location"}, +} + +type Range struct { start int size int } type ConversionMapEntry struct { - destRange MapRange - srcRange MapRange + destRange Range + srcRange Range } type ConversionMap []ConversionMapEntry @@ -28,15 +46,37 @@ type ConvertsBetween struct { to string } -func (mapRange MapRange) Contains(n int) bool { - return n >= mapRange.start && n < mapRange.start+mapRange.size +type WorkerData struct { + startLocation int + numToProcess int } +// Contains checks if the given value is contained in the range +func (r Range) Contains(n int) bool { + return n >= r.start && n < r.start+r.size +} + +// RangeDelta indicates how large the span of the range starts are for this entry +func (entry ConversionMapEntry) RangeDelta() int { + return entry.destRange.start - entry.srcRange.start +} + +// ConvertsTo executes the "conversion" of this step, as defined by the problem func (conversionMap ConversionMap) ConvertsTo(n int) int { for _, entry := range conversionMap { if entry.srcRange.Contains(n) { - delta := n - entry.srcRange.start - return entry.destRange.start + delta + return entry.RangeDelta() + n + } + } + + return n +} + +// ReverseConversion is the onverse of ConvertsTo +func (conversionMap ConversionMap) ReverseConversion(n int) int { + for _, entry := range conversionMap { + if entry.destRange.Contains(n) { + return n - entry.RangeDelta() } } @@ -44,11 +84,21 @@ func (conversionMap ConversionMap) ConvertsTo(n int) int { } func main() { - if len(os.Args) != 2 { - fmt.Fprintf(os.Stderr, "Usage: %s inputfile\n", os.Args[0]) + if len(os.Args) != 2 && len(os.Args) != 3 { + fmt.Fprintf(os.Stderr, "Usage: %s inputfile [workSize]\n", os.Args[0]) os.Exit(1) } + workSize := 1000 + if len(os.Args) == 3 { + var err error + workSize, err = strconv.Atoi(os.Args[2]) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid work size %s", os.Args[3]) + os.Exit(1) + } + } + inputFilename := os.Args[1] inputFile, err := os.Open(inputFilename) if err != nil { @@ -68,40 +118,171 @@ func main() { panic(fmt.Sprintf("failed to parse input: %s", err)) } + // I got lazy here + fmt.Fprintln(os.Stderr, "Warning: Part 2 does not halt in the absence of a solution, so it taking a long time does not mean it will eventually find it") fmt.Printf("Part 1: %d\n", part1(seeds, conversions)) + fmt.Printf("Part 2: %d\n", part2(seeds, conversions, workSize)) } func part1(seeds []int, conversions map[ConvertsBetween]ConversionMap) int { - conversionSteps := []ConvertsBetween{ - {from: "seed", to: "soil"}, - {from: "soil", to: "fertilizer"}, - {from: "fertilizer", to: "water"}, - {from: "water", to: "light"}, - {from: "light", to: "temperature"}, - {from: "temperature", to: "humidity"}, - {from: "humidity", to: "location"}, - } - min := math.MaxInt for _, seed := range seeds { - item := seed - for _, conversionStep := range conversionSteps { - conversionMap, ok := conversions[conversionStep] - if !ok { - panic(fmt.Sprintf("Missing conversion for %s-to-%s", conversionStep.from, conversionStep.to)) - } - - item = conversionMap.ConvertsTo(item) - } - - if item < min { - min = item + location := growPlant(seed, conversions) + if location < min { + min = location } } return min } +func part2(seeds []int, conversions map[ConvertsBetween]ConversionMap, workSize int) int { + seedRanges, err := makeSeedRanges(seeds) + if err != nil { + panic(fmt.Sprintf("failed to make seed ranges: %s", err)) + } + + answerChan := make(chan int) + workChan := make(chan WorkerData) + + ctx, cancel := context.WithCancel(context.Background()) + numWorkers := runtime.NumCPU() + wg := sync.WaitGroup{} + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go func() { + part2Worker(ctx, seedRanges, conversions, workChan, answerChan) + wg.Done() + }() + } + + dispatchCtx, cancelDispatch := context.WithCancel(ctx) + go func() { + dispatchPart2Work(dispatchCtx, workChan, workSize) + // Wait for all workers to finish, and then close the answer chan so that we know they're gone + wg.Wait() + close(answerChan) + }() + + bestAnswer := math.MaxInt + for answer := range answerChan { + // We may get multiple possible answers, but we only wanna take the min + if answer < bestAnswer { + bestAnswer = answer + // Once we get an answer, stop dispatching work + cancelDispatch() + } + } + + cancel() + // This isn't really needed because we cancel the parent ctx but it satisfies the linter + cancelDispatch() + + return bestAnswer +} + +// growPlant will grow a plant from a seed through all the stages until all the conversions are complete +func growPlant(seed int, conversions map[ConvertsBetween]ConversionMap) int { + item := seed + for _, conversionStep := range conversionSteps { + conversionMap, ok := conversions[conversionStep] + if !ok { + panic(fmt.Sprintf("Missing conversion for %s-to-%s", conversionStep.from, conversionStep.to)) + } + + item = conversionMap.ConvertsTo(item) + } + + return item +} + +// dispatchPart2Work will dispatch work to the given workChan. It will give workSize number of items +// for each piece of worker data, allowing fair distribution of work between goroutines +func dispatchPart2Work(ctx context.Context, workChan chan WorkerData, workSize int) { + for i := 0; ; i += workSize { + data := WorkerData{ + startLocation: i, + numToProcess: workSize, + } + + select { + case <-ctx.Done(): + close(workChan) + return + case workChan <- data: + } + } +} + +// part2Worker solves part 2 for a given +func part2Worker(ctx context.Context, seedRanges []Range, conversions map[ConvertsBetween]ConversionMap, workChan <-chan WorkerData, answerChan chan<- int) { + for workerData := range workChan { + // fmt.Printf("Got work: %+v\n", workerData) + for i := 0; i < workerData.numToProcess; i++ { + // Stop working if context says we're done + select { + case <-ctx.Done(): + return + default: + } + + location := workerData.startLocation + i + canReverse, err := canGrowLocation(seedRanges, conversions, location) + if err != nil { + // We can't do a ton here without a bunch of error plumbing that is too much work for an AoC problem. + // An operator will see the error and handle it :) + fmt.Fprintf(os.Stderr, "Cannot reverse growth for location %d\n", location) + continue + } + + if canReverse { + select { + case <-ctx.Done(): + return + case answerChan <- location: + } + } + } + } +} + +// canGrowLocation indicates whether or not the location can be grown with the given seedRanges +func canGrowLocation(seedRanges []Range, conversions map[ConvertsBetween]ConversionMap, location int) (bool, error) { + currentItem := location + for i := len(conversionSteps) - 1; i >= 0; i-- { + step := conversionSteps[i] + conversionMap, ok := conversions[step] + if !ok { + return false, fmt.Errorf("no conversion available for %v", step) + } + + currentItem = conversionMap.ReverseConversion(currentItem) + } + + idx := slices.IndexFunc(seedRanges, func(r Range) bool { + return r.Contains(currentItem) + }) + + return idx != -1, nil +} + +// makeSeedRanges will convert a set of seed input values to ranges (only needed for part 2) +func makeSeedRanges(seeds []int) ([]Range, error) { + if len(seeds)%2 != 0 { + return nil, errors.New("number of seed entries must be even") + } + + seedRanges := make([]Range, len(seeds)/2) + for i := 0; i < len(seeds); i += 2 { + rangeStart := seeds[i] + rangeSize := seeds[i+1] + + seedRanges = append(seedRanges, Range{start: rangeStart, size: rangeSize}) + } + + return seedRanges, nil +} + func parseAlmanac(input string) ([]int, map[ConvertsBetween]ConversionMap, error) { sections := strings.Split(input, "\n\n") seeds, err := parseSeeds(sections[0]) @@ -185,8 +366,8 @@ func parseConversionMap(lines []string) (ConversionMap, error) { size := entryNumbers[2] entry := ConversionMapEntry{ - destRange: MapRange{start: dest, size: size}, - srcRange: MapRange{start: src, size: size}, + destRange: Range{start: dest, size: size}, + srcRange: Range{start: src, size: size}, } conversionMap = append(conversionMap, entry)