Solve day 5 part 2
parent
e9bc265b94
commit
3dcd1776ac
247
day5/main.go
247
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)
|
||||
|
|
Loading…
Reference in New Issue