diff --git a/cmd/10/button/button.go b/cmd/10/button/button.go index 29f17c6..416c05f 100644 --- a/cmd/10/button/button.go +++ b/cmd/10/button/button.go @@ -19,7 +19,7 @@ func (b button) String() string { return fmt.Sprintf("%v", b.switches) } -func From(line string) button { +func From(line string) Button { buttonRegex := regexp.MustCompile(`\d+`) matches := buttonRegex.FindAllString(line, -1) diff --git a/cmd/10/machine/machine.go b/cmd/10/machine/machine.go index f2d21fa..a0ecced 100644 --- a/cmd/10/machine/machine.go +++ b/cmd/10/machine/machine.go @@ -4,15 +4,17 @@ package machine import ( "math" "regexp" - "slices" + "sort" "strconv" "strings" "github.com/StevanFreeborn/advent-of-code-2025/cmd/10/button" + "github.com/StevanFreeborn/advent-of-code-2025/internal/stack" ) type Machine interface { ConfigureLights() int + ConfigureJoltages() int } type machine struct { @@ -65,53 +67,210 @@ func From(line string) Machine { } func (m machine) ConfigureLights() int { - combinations := [][]bool{} + combs := m.generateCombinations(len(m.desiredLightState)) + minPresses := math.MaxInt - numberOfButtons := len(m.buttons) - numberOfCombinations := int(math.Pow(2, float64(numberOfButtons))) - currentCombination := make([]bool, numberOfButtons) + found := false - for range numberOfCombinations { - temp := make([]bool, numberOfButtons) - copy(temp, currentCombination) + for _, comb := range combs { + matches := true - combinations = append(combinations, temp) + for i, count := range comb.deltas { + isLightOn := count%2 != 0 - for j := range numberOfButtons { - if currentCombination[j] == false { - currentCombination[j] = true + if isLightOn != m.desiredLightState[i] { + matches = false break - } else { - currentCombination[j] = false + } + } + + if matches { + if comb.numPresses < minPresses { + minPresses = comb.numPresses + found = true } } } - for _, currentCombination := range combinations { - currentPresses := 0 - initialLightState := make([]bool, len(m.desiredLightState)) - - for bi, bs := range currentCombination { - if bs == false { - continue - } - - currentPresses++ - switchesToToggle := m.buttons[bi].Switches() - - for _, switchToToggle := range switchesToToggle { - initialLightState[switchToToggle] = !initialLightState[switchToToggle] - } - } - - if slices.Equal(initialLightState, m.desiredLightState) == false { - continue - } - - if currentPresses < minPresses { - minPresses = currentPresses - } + if found == false { + return 0 } return minPresses } + +type combination struct { + deltas []int + numPresses int +} + +type searchState struct { + goal []int + currentCost int + weight int +} + +// If a target is odd I must press a combination of +// buttons that contributes an odd value to the target. +// This means I can pre-compute what all combinations +// of buttons do when pressed exactly once. +// I then can look for a combination that matches the +// odd/even pattern of the target +// When I find a match I can subtract it from the target +// and then divide the target by 2 to get a new target +// I repeat this until I reach a target of all zeros + +// i.e. Goal: [13, 7] +// Button A: [1, 0] +// Button B: [1, 1] +// +// Combinations: +// 0 presses: [0, 0] +// 1 press: [1, 0] (A) +// 1 press: [1, 1] (B) +// 1 press: [2, 1] (A, B) +// +// 1st iteration: +// Target: [13, 7] (odd, odd) +// Match: [1, 1] (B) +// New Target: [(13-1)/2, (7-1)/2] = [6, 3] +// Presses: 1 * weight 1 = 1 +// +// Second iteration: +// Target: [6, 3] (even, odd) +// Match: [2, 1] (A, B) +// New Target: [(6-2)/2, (3-1)/2] = [2, 1] +// Presses: 2 * weight 2 = 4 +// +// Third iteration: +// Target: [2, 1] (even, odd) +// Match: [2, 1] (A, B) +// New Target: [(2-1)/2, (1-0)/2] = [0, 0] +// Presses: 2 * weight 4 = 8 +// +// Total presses: 1 + 4 + 8 = 13 +func (m machine) ConfigureJoltages() int { + combinations := m.generateCombinations(len(m.desiredJoltages)) + + sort.Slice(combinations, func(i, j int) bool { + return combinations[i].numPresses < combinations[j].numPresses + }) + + stack := stack.New[searchState]() + stack.Push(searchState{ + goal: m.desiredJoltages, + currentCost: 0, + weight: 1, + }) + + minTotalCost := math.MaxInt + foundSolution := false + + for stack.IsEmpty() == false { + curr, _ := stack.Pop() + + if curr.currentCost >= minTotalCost { + continue + } + + if isZero(curr.goal) { + if curr.currentCost < minTotalCost { + minTotalCost = curr.currentCost + foundSolution = true + } + + continue + } + + for _, combination := range combinations { + if smallerOrEqual(combination.deltas, curr.goal) == false { + continue + } + + if hasSameParity(combination.deltas, curr.goal) == false { + continue + } + + nextGoal := make([]int, len(curr.goal)) + + for i := 0; i < len(curr.goal); i++ { + nextGoal[i] = (curr.goal[i] - combination.deltas[i]) / 2 + } + + stepCost := combination.numPresses * curr.weight + + stack.Push(searchState{ + goal: nextGoal, + currentCost: curr.currentCost + stepCost, + weight: curr.weight * 2, + }) + } + } + + if foundSolution == false { + return 0 + } + + return minTotalCost +} + +func (m machine) generateCombinations(size int) []combination { + res := []combination{{ + deltas: make([]int, size), + numPresses: 0, + }} + + for _, btn := range m.buttons { + currentCount := len(res) + + for i := range currentCount { + existing := res[i] + + newDeltas := make([]int, size) + copy(newDeltas, existing.deltas) + + for _, switchIdx := range btn.Switches() { + if switchIdx < size { + newDeltas[switchIdx]++ + } + } + + res = append(res, combination{ + deltas: newDeltas, + numPresses: existing.numPresses + 1, + }) + } + } + + return res +} + +func isZero(arr []int) bool { + for _, v := range arr { + if v != 0 { + return false + } + } + + return true +} + +func smallerOrEqual(a []int, b []int) bool { + for i := range a { + if a[i] > b[i] { + return false + } + } + + return true +} + +func hasSameParity(a []int, b []int) bool { + for i := range a { + if a[i]%2 != b[i]%2 { + return false + } + } + + return true +} diff --git a/cmd/10/main.go b/cmd/10/main.go index 8f45ea2..37c1a5b 100644 --- a/cmd/10/main.go +++ b/cmd/10/main.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "github.com/StevanFreeborn/advent-of-code-2025/cmd/10/machine" "github.com/StevanFreeborn/advent-of-code-2025/internal/file" ) @@ -16,3 +18,20 @@ func SolvePartOne(filePath string) int { return total } + +func SolvePartTwo(filePath string) int { + total := 0 + + linesRead := 0 + for line := range file.ReadLines(filePath) { + linesRead++ + machine := machine.From(line) + buttonsPressed := machine.ConfigureJoltages() + if buttonsPressed == 0 { + fmt.Printf("No solution found for line %d: %s\n", linesRead, line) + } + total += buttonsPressed + } + + return total +} diff --git a/cmd/10/main_test.go b/cmd/10/main_test.go index 63a1fa5..d92fcfb 100644 --- a/cmd/10/main_test.go +++ b/cmd/10/main_test.go @@ -25,3 +25,23 @@ func TestSolvePartOneWithInput(t *testing.T) { t.Errorf("got %d but wanted %d", result, expected) } } + +func TestSolvePartTwoWithExampleInput(t *testing.T) { + expected := 33 + + result := solution.SolvePartTwo("EXAMPLE.txt") + + if result != expected { + t.Errorf("got %d but wanted %d", result, expected) + } +} + +func TestSolvePartTwoWithInput(t *testing.T) { + expected := 19810 + + result := solution.SolvePartTwo("INPUT.txt") + + if result != expected { + t.Errorf("got %d but wanted %d", result, expected) + } +}