Solve day 5 part 2

master
Nick Krichevsky 2023-12-05 17:38:41 -05:00
parent e9bc265b94
commit 3dcd1776ac
1 changed files with 214 additions and 33 deletions

View File

@ -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)