advent-of-code-2023/day12/main.go

242 lines
6.3 KiB
Go
Raw Normal View History

2023-12-13 13:45:54 +00:00
package main
import (
"errors"
"fmt"
"io"
"os"
"regexp"
"slices"
"strconv"
"strings"
)
type SpringState int
const (
SpringStateOperational SpringState = iota
SpringStateDamaged
SpringStateUnknown
)
type SequenceStatus int
const (
SequenceStatusDoesntMatch SequenceStatus = iota
SequenceStatusMatches
SequencesStatusCouldMatch
)
2023-12-13 13:45:54 +00:00
type SpringStates []SpringState
type Record struct {
states SpringStates
sequences []int
}
func (states SpringStates) Print() {
for _, state := range states {
switch state {
case SpringStateDamaged:
fmt.Print("#")
case SpringStateOperational:
fmt.Print(".")
case SpringStateUnknown:
fmt.Print("?")
default:
panic("invalid state")
}
}
fmt.Println()
}
func (r Record) EvaluateUnknownStates() []SpringStates {
return r.generateStates(r.states, 0, 0)
2023-12-13 13:45:54 +00:00
}
func (r Record) generateStates(states SpringStates, stateIdx int, sequenceIdx int) []SpringStates {
if slices.Index(states, SpringStateUnknown) == -1 && matchesSequence(states, r.sequences) == SequenceStatusMatches {
return []SpringStates{states}
} else if stateIdx > len(r.states)-1 {
return []SpringStates{}
} else if sequenceIdx == len(r.sequences) {
// Try the same thing, but with all the remaining items as operational (as this can still be a match)
allOperational := slices.Clone(states)
for i, state := range allOperational {
if state == SpringStateUnknown {
allOperational[i] = SpringStateOperational
}
2023-12-13 13:45:54 +00:00
}
if matchesSequence(allOperational, r.sequences) == SequenceStatusMatches {
return []SpringStates{allOperational}
} else {
return []SpringStates{}
2023-12-13 13:45:54 +00:00
}
} else if matchesSequence(states[:stateIdx+1], r.sequences[:sequenceIdx+1]) == SequenceStatusMatches {
return r.generateStates(states, stateIdx+1, sequenceIdx+1)
} else if r.states[stateIdx] != SpringStateUnknown {
return r.generateStates(states, stateIdx+1, sequenceIdx)
}
2023-12-13 13:45:54 +00:00
generatedStates := []SpringStates{}
ifOperational := slices.Clone(states)
ifOperational[stateIdx] = SpringStateOperational
operationalMatches := matchesSequence(ifOperational[:stateIdx+1], r.sequences[:sequenceIdx+1])
if operationalMatches == SequenceStatusMatches {
generatedStates = append(generatedStates, r.generateStates(ifOperational, stateIdx+1, sequenceIdx+1)...)
} else if operationalMatches == SequencesStatusCouldMatch {
generatedStates = append(generatedStates, r.generateStates(ifOperational, stateIdx+1, sequenceIdx)...)
}
2023-12-13 13:45:54 +00:00
ifDamaged := slices.Clone(states)
ifDamaged[stateIdx] = SpringStateDamaged
damagedMatches := matchesSequence(ifDamaged[:stateIdx+1], r.sequences[:sequenceIdx+1])
if damagedMatches == SequenceStatusMatches {
generatedStates = append(generatedStates, r.generateStates(ifDamaged, stateIdx+1, sequenceIdx+1)...)
} else if damagedMatches == SequencesStatusCouldMatch {
generatedStates = append(generatedStates, r.generateStates(ifDamaged, stateIdx+1, sequenceIdx)...)
2023-12-13 13:45:54 +00:00
}
return generatedStates
2023-12-13 13:45:54 +00:00
}
func main() {
if len(os.Args) != 2 && len(os.Args) != 3 {
fmt.Fprintf(os.Stderr, "Usage: %s inputfile\n", os.Args[0])
os.Exit(1)
}
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))
inputLines := strings.Split(input, "\n")
records, err := parseRecords(inputLines)
if err != nil {
panic(fmt.Sprintf("failed to parse input: %s", err))
}
fmt.Printf("Part 1: %d\n", part1(records))
}
func part1(records []Record) int {
total := 0
for _, record := range records {
possibilities := record.EvaluateUnknownStates()
total += len(possibilities)
}
return total
}
func matchesSequence(states SpringStates, sequences []int) SequenceStatus {
sequenceCountCursor := 0
damageCount := 0
needSpace := false
for _, state := range states {
if sequenceCountCursor > len(sequences)-1 && state == SpringStateDamaged {
return SequenceStatusDoesntMatch
} else if sequenceCountCursor > len(sequences)-1 {
continue
}
2023-12-13 13:45:54 +00:00
sequenceCount := sequences[sequenceCountCursor]
if needSpace && state != SpringStateOperational {
return SequenceStatusDoesntMatch
} else if needSpace {
needSpace = false
continue
} else if state == SpringStateOperational && damageCount > 0 && damageCount < sequenceCount {
return SequenceStatusDoesntMatch
}
2023-12-13 13:45:54 +00:00
if state == SpringStateDamaged {
damageCount++
} else if state == SpringStateOperational {
damageCount = 0
}
2023-12-13 13:45:54 +00:00
if damageCount == sequenceCount {
sequenceCountCursor++
damageCount = 0
needSpace = true
}
}
2023-12-13 13:45:54 +00:00
if sequenceCountCursor < len(sequences) && damageCount < sequences[sequenceCountCursor] {
return SequencesStatusCouldMatch
} else if sequenceCountCursor == len(sequences) {
return SequenceStatusMatches
} else {
return SequenceStatusDoesntMatch
}
2023-12-13 13:45:54 +00:00
}
func parseRecords(inputLines []string) ([]Record, error) {
return tryParse(inputLines, parseRecord)
}
func parseRecord(inputLine string) (Record, error) {
lineRegexp := regexp.MustCompile(`^([#.?]+) ((?:\d+,)*\d+)$`)
matches := lineRegexp.FindStringSubmatch(inputLine)
if matches == nil {
return Record{}, errors.New("malformed input line")
}
states, err := parseSpringStates(matches[1])
if err != nil {
return Record{}, fmt.Errorf("spring state: %w", err)
}
sequences, err := tryParse(strings.Split(matches[2], ","), strconv.Atoi)
if err != nil {
return Record{}, fmt.Errorf("sequences: %w", err)
}
return Record{states: states, sequences: sequences}, nil
}
func parseSpringStates(inputStates string) ([]SpringState, error) {
states := make([]SpringState, len(inputStates))
for i, char := range inputStates {
switch char {
case '.':
states[i] = SpringStateOperational
case '#':
states[i] = SpringStateDamaged
case '?':
states[i] = SpringStateUnknown
default:
return nil, fmt.Errorf("invalid spring state char, %c", char)
}
}
return states, nil
}
func tryParse[T any](items []string, parse func(string) (T, error)) ([]T, error) {
res := make([]T, 0, len(items))
for i, item := range items {
parsed, err := parse(item)
if err != nil {
return nil, fmt.Errorf("invalid item #%d: %w", i+1, err)
}
res = append(res, parsed)
}
return res, nil
}