mirror of
https://github.com/Threnklyn/advent-of-code-go.git
synced 2026-06-07 12:45:10 +02:00
day 12 memo hurting my brain
This commit is contained in:
@@ -0,0 +1,261 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alexchao26/advent-of-code-go/cast"
|
||||||
|
"github.com/alexchao26/advent-of-code-go/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed input.txt
|
||||||
|
var input string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// do this in init (not main) so test file has same input
|
||||||
|
input = strings.TrimRight(input, "\n")
|
||||||
|
if len(input) == 0 {
|
||||||
|
panic("empty input.txt file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var part int
|
||||||
|
flag.IntVar(&part, "part", 1, "part 1 or 2")
|
||||||
|
flag.Parse()
|
||||||
|
fmt.Println("Running part", part)
|
||||||
|
|
||||||
|
if part == 1 {
|
||||||
|
ans := part1(input)
|
||||||
|
util.CopyToClipboard(fmt.Sprintf("%v", ans))
|
||||||
|
fmt.Println("Output:", ans)
|
||||||
|
} else {
|
||||||
|
ans := part2(input)
|
||||||
|
util.CopyToClipboard(fmt.Sprintf("%v", ans))
|
||||||
|
fmt.Println("Output:", ans)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func part1(input string) int {
|
||||||
|
stringConditions := parseInput(input)
|
||||||
|
|
||||||
|
ans := 0
|
||||||
|
// brute force creating all possible combination per line
|
||||||
|
// then check each possibility
|
||||||
|
// input is 1000 lines with 10k ?'s total, so approx 10/line
|
||||||
|
// 2^10 = 1024 options per line approx. * 1000 = 1_024_000 checks total... seems ok... for part 1...
|
||||||
|
for _, sc := range stringConditions {
|
||||||
|
possibilities := generatePossibilities(sc.record)
|
||||||
|
|
||||||
|
for _, p := range possibilities {
|
||||||
|
if checkIfSpringRecordFitsDamagedGroupCounts(p, sc.damagedGroupCounts) {
|
||||||
|
ans++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ans
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatePossibilities(record []string) [][]string {
|
||||||
|
var recurse func(record []string, index int) [][]string
|
||||||
|
recurse = func(record []string, index int) [][]string {
|
||||||
|
if index == len(record) {
|
||||||
|
cp := make([]string, len(record))
|
||||||
|
copy(cp, record)
|
||||||
|
return [][]string{cp}
|
||||||
|
}
|
||||||
|
|
||||||
|
if record[index] != "?" {
|
||||||
|
return recurse(record, index+1)
|
||||||
|
}
|
||||||
|
possibilities := [][]string{}
|
||||||
|
record[index] = "#"
|
||||||
|
possibilities = append(possibilities, recurse(record, index+1)...)
|
||||||
|
|
||||||
|
record[index] = "."
|
||||||
|
possibilities = append(possibilities, recurse(record, index+1)...)
|
||||||
|
|
||||||
|
record[index] = "?"
|
||||||
|
|
||||||
|
return possibilities
|
||||||
|
}
|
||||||
|
|
||||||
|
return recurse(record, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkIfSpringRecordFitsDamagedGroupCounts(condition []string, damagedGroupCounts []int) bool {
|
||||||
|
consecutiveDamagedCount := 0
|
||||||
|
foundDamageGroupCounts := []int{}
|
||||||
|
for _, cond := range condition {
|
||||||
|
if cond == "." {
|
||||||
|
if consecutiveDamagedCount != 0 {
|
||||||
|
foundDamageGroupCounts = append(foundDamageGroupCounts, consecutiveDamagedCount)
|
||||||
|
}
|
||||||
|
consecutiveDamagedCount = 0
|
||||||
|
} else {
|
||||||
|
consecutiveDamagedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if consecutiveDamagedCount != 0 {
|
||||||
|
foundDamageGroupCounts = append(foundDamageGroupCounts, consecutiveDamagedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(damagedGroupCounts) == len(foundDamageGroupCounts) {
|
||||||
|
for i := 0; i < len(damagedGroupCounts); i++ {
|
||||||
|
if damagedGroupCounts[i] != foundDamageGroupCounts[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func part2(input string) int {
|
||||||
|
// brute force will not work for part 2 presumably. 2^10 becomes 2^50 which is 1 trillion times larger?
|
||||||
|
|
||||||
|
stringConditions := parseInput(input)
|
||||||
|
|
||||||
|
// hacky hacky way to update string conditions...
|
||||||
|
for i, sc := range stringConditions {
|
||||||
|
for x := 0; x < 4; x++ {
|
||||||
|
stringConditions[i].record = append(stringConditions[i].record, "?")
|
||||||
|
stringConditions[i].record = append(stringConditions[i].record, sc.record...)
|
||||||
|
stringConditions[i].damagedGroupCounts = append(stringConditions[i].damagedGroupCounts, sc.damagedGroupCounts...)
|
||||||
|
}
|
||||||
|
// adding a "." at the end helps future logic ensure that the final damaged group will be ended
|
||||||
|
stringConditions[i].record = append(stringConditions[i].record, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
ans := 0
|
||||||
|
for _, sc := range stringConditions {
|
||||||
|
memoOfPossibilities := map[[3]int]int{}
|
||||||
|
ans += memo(sc, 0, 0, 0, memoOfPossibilities)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ans
|
||||||
|
}
|
||||||
|
|
||||||
|
func memo(sc springCondition, index, doneGroups, currentGroupSize int, memoOfPossibilities map[[3]int]int) int {
|
||||||
|
// key of 0, 0, 0 holds final answer of possible results
|
||||||
|
key := [3]int{index, doneGroups, currentGroupSize}
|
||||||
|
|
||||||
|
if ans, ok := memoOfPossibilities[key]; ok {
|
||||||
|
return ans
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the end of the record is reached, and all damaged groups are accounted for
|
||||||
|
// do not need to check for currentGroupSize because of the trailing "." that was added
|
||||||
|
if index == len(sc.record) && doneGroups == len(sc.damagedGroupCounts) {
|
||||||
|
memoOfPossibilities[key] = 1
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// any other scenario where we've reached the final index means this possibility is invalid
|
||||||
|
if index == len(sc.record) {
|
||||||
|
memoOfPossibilities[key] = 0
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// damaged spring groups are all accounted for but ran into an additional broken spring,
|
||||||
|
// this branch is not valid
|
||||||
|
if doneGroups == len(sc.damagedGroupCounts) && sc.record[index] == "#" {
|
||||||
|
memoOfPossibilities[key] = 0
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle ".", "#" or "?"
|
||||||
|
possibilities := 0
|
||||||
|
if sc.record[index] == "." {
|
||||||
|
// end the previous group
|
||||||
|
if index == 0 {
|
||||||
|
possibilities = memo(sc, index+1, 0, 0, memoOfPossibilities)
|
||||||
|
} else if currentGroupSize == 0 {
|
||||||
|
possibilities = memo(sc, index+1, doneGroups, 0, memoOfPossibilities)
|
||||||
|
} else if currentGroupSize != 0 {
|
||||||
|
// we have a non-zero current group size so if all damaged groups are accounted for,
|
||||||
|
// there are no possibilities left for this branch
|
||||||
|
if doneGroups == len(sc.damagedGroupCounts) {
|
||||||
|
possibilities = 0
|
||||||
|
} else {
|
||||||
|
// not all damaged groups are accounted for
|
||||||
|
// if the current group is the right size, recurse; if not, then zero possibilities remain
|
||||||
|
if currentGroupSize == sc.damagedGroupCounts[doneGroups] {
|
||||||
|
possibilities = memo(sc, index+1, doneGroups+1, 0, memoOfPossibilities)
|
||||||
|
} else if currentGroupSize != sc.damagedGroupCounts[doneGroups] {
|
||||||
|
// last group is the wrong size, zero possibilities for this branch
|
||||||
|
possibilities = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if sc.record[index] == "#" {
|
||||||
|
// build group
|
||||||
|
currentGroupSize++
|
||||||
|
// if current group size is too big, this branch has zero possibilities
|
||||||
|
if currentGroupSize > sc.damagedGroupCounts[doneGroups] {
|
||||||
|
possibilities = 0
|
||||||
|
} else {
|
||||||
|
possibilities = memo(sc, index+1, doneGroups, currentGroupSize, memoOfPossibilities)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if sc.record[index] == "?" {
|
||||||
|
// ?
|
||||||
|
// add two possibilities: a damaged spring or OK spring
|
||||||
|
|
||||||
|
// if it is a #
|
||||||
|
// do not need to account for if the group is too big here, it'll be handled by a future "#"
|
||||||
|
// check or a ".", again part of the reason why a trailing period was added
|
||||||
|
possibilities += memo(sc, index+1, doneGroups, currentGroupSize+1, memoOfPossibilities)
|
||||||
|
// currentGroupSize--
|
||||||
|
|
||||||
|
// take as .
|
||||||
|
// same code as above for if "." block, but possibilities is added to instead of just set
|
||||||
|
if index == 0 {
|
||||||
|
possibilities += memo(sc, index+1, 0, 0, memoOfPossibilities)
|
||||||
|
} else if currentGroupSize == 0 {
|
||||||
|
possibilities += memo(sc, index+1, doneGroups, currentGroupSize, memoOfPossibilities)
|
||||||
|
} else {
|
||||||
|
if doneGroups == len(sc.damagedGroupCounts) {
|
||||||
|
possibilities += 0
|
||||||
|
} else {
|
||||||
|
if currentGroupSize == sc.damagedGroupCounts[doneGroups] {
|
||||||
|
possibilities += memo(sc, index+1, doneGroups+1, 0, memoOfPossibilities)
|
||||||
|
} else if currentGroupSize != sc.damagedGroupCounts[doneGroups] {
|
||||||
|
possibilities += 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
panic("unexpected string condition record character: " + sc.record[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
memoOfPossibilities[key] = possibilities
|
||||||
|
return possibilities
|
||||||
|
}
|
||||||
|
|
||||||
|
type springCondition struct {
|
||||||
|
record []string
|
||||||
|
damagedGroupCounts []int
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseInput(input string) (ans []springCondition) {
|
||||||
|
for _, line := range strings.Split(input, "\n") {
|
||||||
|
parts := strings.Split(line, " ")
|
||||||
|
sc := springCondition{
|
||||||
|
record: strings.Split(parts[0], ""),
|
||||||
|
damagedGroupCounts: []int{},
|
||||||
|
}
|
||||||
|
for _, str := range strings.Split(parts[1], ",") {
|
||||||
|
sc.damagedGroupCounts = append(sc.damagedGroupCounts, cast.ToInt(str))
|
||||||
|
}
|
||||||
|
ans = append(ans, sc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ans
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var example = `???.### 1,1,3
|
||||||
|
.??..??...?##. 1,1,3
|
||||||
|
?#?#?#?#?#?#?#? 1,3,1,6
|
||||||
|
????.#...#... 4,1,1
|
||||||
|
????.######..#####. 1,6,5
|
||||||
|
?###???????? 3,2,1`
|
||||||
|
|
||||||
|
func Test_part1(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "example",
|
||||||
|
input: example,
|
||||||
|
want: 21,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "actual",
|
||||||
|
input: input,
|
||||||
|
want: 7792,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := part1(tt.input); got != tt.want {
|
||||||
|
t.Errorf("part1() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_part2(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "example",
|
||||||
|
input: example,
|
||||||
|
want: 525152,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "actual",
|
||||||
|
input: input,
|
||||||
|
want: 13012052341533,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := part2(tt.input); got != tt.want {
|
||||||
|
t.Errorf("part2() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user