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
|
|
|
|
)
|
|
|
|
|
2023-12-16 14:51:43 +00:00
|
|
|
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 {
|
2023-12-16 14:51:43 +00:00
|
|
|
return r.generateStates(r.states, 0, 0)
|
2023-12-13 13:45:54 +00:00
|
|
|
}
|
|
|
|
|
2023-12-16 14:51:43 +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
|
|
|
}
|
|
|
|
|
2023-12-16 14:51:43 +00:00
|
|
|
if matchesSequence(allOperational, r.sequences) == SequenceStatusMatches {
|
|
|
|
return []SpringStates{allOperational}
|
|
|
|
} else {
|
|
|
|
return []SpringStates{}
|
2023-12-13 13:45:54 +00:00
|
|
|
}
|
2023-12-16 14:51:43 +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
|
|
|
|
2023-12-16 14:51:43 +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
|
|
|
|
2023-12-16 14:51:43 +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
|
|
|
}
|
|
|
|
|
2023-12-16 14:51:43 +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
|
|
|
|
}
|
|
|
|
|
2023-12-16 14:51:43 +00:00
|
|
|
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
|
|
|
|
2023-12-16 14:51:43 +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
|
|
|
|
2023-12-16 14:51:43 +00:00
|
|
|
if state == SpringStateDamaged {
|
|
|
|
damageCount++
|
|
|
|
} else if state == SpringStateOperational {
|
|
|
|
damageCount = 0
|
|
|
|
}
|
2023-12-13 13:45:54 +00:00
|
|
|
|
2023-12-16 14:51:43 +00:00
|
|
|
if damageCount == sequenceCount {
|
|
|
|
sequenceCountCursor++
|
|
|
|
damageCount = 0
|
|
|
|
needSpace = true
|
|
|
|
}
|
|
|
|
}
|
2023-12-13 13:45:54 +00:00
|
|
|
|
2023-12-16 14:51:43 +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
|
|
|
|
}
|