2023-12-05 13:22:38 +00:00
package main
import (
2023-12-05 22:38:41 +00:00
"context"
2023-12-05 13:22:38 +00:00
"errors"
"fmt"
"io"
"math"
"os"
"regexp"
2023-12-05 22:38:41 +00:00
"runtime"
"slices"
2023-12-05 13:22:38 +00:00
"strconv"
"strings"
2023-12-05 22:38:41 +00:00
"sync"
2023-12-05 13:22:38 +00:00
)
2023-12-05 22:38:41 +00:00
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 {
2023-12-05 13:22:38 +00:00
start int
size int
}
type ConversionMapEntry struct {
2023-12-05 22:38:41 +00:00
destRange Range
srcRange Range
2023-12-05 13:22:38 +00:00
}
type ConversionMap [ ] ConversionMapEntry
type ConvertsBetween struct {
from string
to string
}
2023-12-05 22:38:41 +00:00
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
2023-12-05 13:22:38 +00:00
}
2023-12-05 22:38:41 +00:00
// ConvertsTo executes the "conversion" of this step, as defined by the problem
2023-12-05 13:22:38 +00:00
func ( conversionMap ConversionMap ) ConvertsTo ( n int ) int {
for _ , entry := range conversionMap {
if entry . srcRange . Contains ( n ) {
2023-12-05 22:38:41 +00:00
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 ( )
2023-12-05 13:22:38 +00:00
}
}
return n
}
func main ( ) {
2023-12-05 22:38:41 +00:00
if len ( os . Args ) != 2 && len ( os . Args ) != 3 {
fmt . Fprintf ( os . Stderr , "Usage: %s inputfile [workSize]\n" , os . Args [ 0 ] )
2023-12-05 13:22:38 +00:00
os . Exit ( 1 )
}
2023-12-05 22:38:41 +00:00
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 )
}
}
2023-12-05 13:22:38 +00:00
inputFilename := os . Args [ 1 ]
inputFile , err := os . Open ( inputFilename )
if err != nil {
panic ( fmt . Sprintf ( "could not open input file: %s" , err ) )
}
defer inputFile . Close ( )
inputBytes , err := io . ReadAll ( inputFile )
if err != nil {
panic ( fmt . Sprintf ( "could not read input file: %s" , err ) )
}
input := strings . TrimSpace ( string ( inputBytes ) )
seeds , conversions , err := parseAlmanac ( input )
if err != nil {
panic ( fmt . Sprintf ( "failed to parse input: %s" , err ) )
}
2023-12-05 22:38:41 +00:00
// 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" )
2023-12-05 13:22:38 +00:00
fmt . Printf ( "Part 1: %d\n" , part1 ( seeds , conversions ) )
2023-12-05 22:38:41 +00:00
fmt . Printf ( "Part 2: %d\n" , part2 ( seeds , conversions , workSize ) )
2023-12-05 13:22:38 +00:00
}
func part1 ( seeds [ ] int , conversions map [ ConvertsBetween ] ConversionMap ) int {
min := math . MaxInt
for _ , seed := range seeds {
2023-12-05 22:38:41 +00:00
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
2023-12-05 13:22:38 +00:00
}
2023-12-05 22:38:41 +00:00
if canReverse {
select {
case <- ctx . Done ( ) :
return
case answerChan <- location :
}
}
2023-12-05 13:22:38 +00:00
}
2023-12-05 22:38:41 +00:00
}
}
2023-12-05 13:22:38 +00:00
2023-12-05 22:38:41 +00:00
// 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 )
2023-12-05 13:22:38 +00:00
}
2023-12-05 22:38:41 +00:00
currentItem = conversionMap . ReverseConversion ( currentItem )
2023-12-05 13:22:38 +00:00
}
2023-12-05 22:38:41 +00:00
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
2023-12-05 13:22:38 +00:00
}
func parseAlmanac ( input string ) ( [ ] int , map [ ConvertsBetween ] ConversionMap , error ) {
sections := strings . Split ( input , "\n\n" )
seeds , err := parseSeeds ( sections [ 0 ] )
if err != nil {
return nil , nil , fmt . Errorf ( "parse seeds: %w" , err )
}
conversions := map [ ConvertsBetween ] ConversionMap { }
for i , section := range sections [ 1 : ] {
convertsBetween , conversionMap , err := parseConversionSection ( section )
if err != nil {
return nil , nil , fmt . Errorf ( "parse section %d: %w" , i , err )
}
conversions [ convertsBetween ] = conversionMap
}
return seeds , conversions , nil
}
func parseSeeds ( seedSection string ) ( [ ] int , error ) {
stripped := strings . TrimPrefix ( seedSection , "seeds: " )
if stripped == seedSection {
return nil , errors . New ( "missing seeds prefix" )
}
rawSeedNumbers := strings . Split ( stripped , " " )
seedNumbers , err := tryParse ( rawSeedNumbers , strconv . Atoi )
if err != nil {
return nil , fmt . Errorf ( "invalid seed numbers: %w" , err )
}
return seedNumbers , nil
}
func parseConversionSection ( section string ) ( ConvertsBetween , ConversionMap , error ) {
sectionLines := strings . Split ( section , "\n" )
if len ( sectionLines ) < 2 {
return ConvertsBetween { } , nil , errors . New ( "not enough information in section" )
}
convertsBetween , err := parseSectionHeading ( sectionLines [ 0 ] )
if err != nil {
return ConvertsBetween { } , nil , fmt . Errorf ( "invalid section heading: %w" , err )
}
conversionMap , err := parseConversionMap ( sectionLines [ 1 : ] )
if err != nil {
return ConvertsBetween { } , nil , fmt . Errorf ( "invalid conversion: %w" , err )
}
return convertsBetween , conversionMap , nil
}
func parseSectionHeading ( heading string ) ( ConvertsBetween , error ) {
pattern := regexp . MustCompile ( ` ^(\w+)-to-(\w+) map: ` )
matches := pattern . FindStringSubmatch ( heading )
if matches == nil {
return ConvertsBetween { } , errors . New ( "malformed heading" )
}
return ConvertsBetween {
from : matches [ 1 ] ,
to : matches [ 2 ] ,
} , nil
}
func parseConversionMap ( lines [ ] string ) ( ConversionMap , error ) {
conversionMap := ConversionMap { }
for _ , line := range lines {
rawEntryNumbers := strings . Split ( line , " " )
entryNumbers , err := tryParse ( rawEntryNumbers , strconv . Atoi )
if err != nil {
return nil , fmt . Errorf ( "conversion map entry numbers: %w" , err )
} else if len ( entryNumbers ) != 3 {
return nil , fmt . Errorf ( "expected 3 numbers in a conversion section, got %d" , len ( rawEntryNumbers ) )
}
dest := entryNumbers [ 0 ]
src := entryNumbers [ 1 ]
size := entryNumbers [ 2 ]
entry := ConversionMapEntry {
2023-12-05 22:38:41 +00:00
destRange : Range { start : dest , size : size } ,
srcRange : Range { start : src , size : size } ,
2023-12-05 13:22:38 +00:00
}
conversionMap = append ( conversionMap , entry )
}
return conversionMap , nil
}
func tryParse [ T any ] ( items [ ] string , doParse func ( s string ) ( T , error ) ) ( [ ] T , error ) {
res := [ ] T { }
for i , line := range items {
parsedItem , err := doParse ( line )
if err != nil {
return nil , fmt . Errorf ( "malformed item at index %d: %w" , i , err )
}
res = append ( res , parsedItem )
}
return res , nil
}