212 lines
4.7 KiB
Go
212 lines
4.7 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"os"
|
||
|
"regexp"
|
||
|
"slices"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
type SpringState int
|
||
|
|
||
|
const (
|
||
|
SpringStateOperational SpringState = iota
|
||
|
SpringStateDamaged
|
||
|
SpringStateUnknown
|
||
|
)
|
||
|
|
||
|
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 {
|
||
|
res := []SpringStates{}
|
||
|
for _, possibleStates := range getAllPossibleStates(r.states, 0) {
|
||
|
if r.matchesSequence(possibleStates) {
|
||
|
res = append(res, possibleStates)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return res
|
||
|
}
|
||
|
|
||
|
func (r Record) matchesSequence(states SpringStates) bool {
|
||
|
sequenceCountCursor := 0
|
||
|
// states.Print()
|
||
|
damageCount := 0
|
||
|
needSpace := false
|
||
|
for _, state := range states {
|
||
|
if sequenceCountCursor > len(r.sequences)-1 && state == SpringStateDamaged {
|
||
|
return false
|
||
|
} else if sequenceCountCursor > len(r.sequences)-1 {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
sequenceCount := r.sequences[sequenceCountCursor]
|
||
|
if needSpace && state != SpringStateOperational {
|
||
|
return false
|
||
|
} else if needSpace {
|
||
|
needSpace = false
|
||
|
continue
|
||
|
} else if state == SpringStateOperational && damageCount > 0 && damageCount < sequenceCount {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
if state == SpringStateDamaged {
|
||
|
damageCount++
|
||
|
} else if state == SpringStateOperational {
|
||
|
damageCount = 0
|
||
|
}
|
||
|
|
||
|
if damageCount == sequenceCount {
|
||
|
sequenceCountCursor++
|
||
|
damageCount = 0
|
||
|
needSpace = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return sequenceCountCursor == len(r.sequences)
|
||
|
}
|
||
|
|
||
|
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 getAllPossibleStates(states SpringStates, trackingIdx int) []SpringStates {
|
||
|
if trackingIdx > len(states)-1 {
|
||
|
return []SpringStates{}
|
||
|
} else if states[trackingIdx] != SpringStateUnknown {
|
||
|
return getAllPossibleStates(states, trackingIdx+1)
|
||
|
}
|
||
|
|
||
|
ifOperational := slices.Clone(states)
|
||
|
ifOperational[trackingIdx] = SpringStateOperational
|
||
|
ifDamaged := slices.Clone(states)
|
||
|
ifDamaged[trackingIdx] = SpringStateDamaged
|
||
|
|
||
|
if slices.Index(states[trackingIdx+1:], SpringStateUnknown) == -1 {
|
||
|
return []SpringStates{ifOperational, ifDamaged}
|
||
|
}
|
||
|
|
||
|
possibilities := []SpringStates{}
|
||
|
possibilities = append(possibilities, getAllPossibleStates(ifOperational, trackingIdx+1)...)
|
||
|
possibilities = append(possibilities, getAllPossibleStates(ifDamaged, trackingIdx+1)...)
|
||
|
|
||
|
return possibilities
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|