From 5fd80e56d877c2806f031aa1276eae846df67265 Mon Sep 17 00:00:00 2001 From: Andrew Ekstedt Date: Fri, 12 Dec 2025 06:26:51 +0000 Subject: [PATCH] day 10 part 2 progress --- day10/sol2.go | 138 ++++++++++++++++++++++++++++++++------------------ day10/sol2.py | 26 +++++++--- 2 files changed, 109 insertions(+), 55 deletions(-) diff --git a/day10/sol2.go b/day10/sol2.go index 44d496f..94adfd9 100644 --- a/day10/sol2.go +++ b/day10/sol2.go @@ -24,6 +24,14 @@ func main() { solve("input") } +// Brilliant insight from a redditor: +// suppose we only want to match the parity of the required joltages. +// we can use our solution for part 1 to find the minimum number of button presses +// to make that happen. now, consider what happens if we press a button twice: +// it won't change the parity at all, only add 2 to some joltages. ok. +// take the 2nd bit of each joltage. what's the minimum number of double-presses +// required to match that pattern? and so on. + func solve(filename string) { input, err := os.Open(filename) check(err) @@ -35,14 +43,19 @@ func solve(filename string) { line := scanner.Text() parts := strings.Fields(line) parts = parts[1:] - var jolts []Jolts + var buts []uint32 + var target Jolts for _, p := range parts { j, err := parseJolt(p) check(err) - jolts = append(jolts, j) + if p[0] == '{' { + target = j + } else { + buts = append(buts, j.uint32()) + } } - fmt.Printf("%v %v\n", jolts, parts) - n := best(jolts[len(jolts)-1], jolts[:len(jolts)-1]) + fmt.Printf("%v %b %v\n", target, buts, parts) + n := solveJolts(target, buts) fmt.Printf("%s = %v\n", line, n) total += n //fmt.Println(n) @@ -89,62 +102,89 @@ func parseJoltByValue(s string) (Jolts, error) { return j, nil } -// An CostHeap is a min-heap of ints. -type CostHeap struct { - heap []Jolts - cost map[Jolts]int +func (j *Jolts) uint32() (u uint32) { + for i, v := range j { + if v != 0 { + u |= uint32(1) << i + } + } + return u } -func (h *CostHeap) Len() int { return len(h.heap) } -func (h *CostHeap) Less(i, j int) bool { return h.cost[h.heap[i]] < h.cost[h.heap[j]] } -func (h *CostHeap) Swap(i, j int) { h.heap[i], h.heap[j] = h.heap[j], h.heap[i] } - -func (h *CostHeap) Push(x any) { - // Push and Pop use pointer receivers because they modify the slice's length, - // not just its contents. - h.heap = append(h.heap, x.(Jolts)) +func (j Jolts) Sub(m uint32, value int) Jolts { + for i := range j { + if m>>i&1 != 0 { + j[i] -= int16(value) + } + } + return j } -func (h *CostHeap) Pop() any { - old := h.heap - n := len(old) - x := old[n-1] - h.heap = old[0 : n-1] - return x +func (j Jolts) Valid() bool { + for i := range j { + if j[i] < 0 { + return false + } + } + return true } -// it doesn't matter what order we push the buttons in -- -// only how many times we push each one. +// TODO: this isn't quite enough. we need to know not only how many button presses +// a mask can be solved in, but also *the specific buttons* -- because even though +// each button is pressed at most once, two buttons can be connected to the same +// output so the joltages can be between like 0-10. this needs to be subtracted +// from the target, and two different button patterns can have different effects +// on the target. +// +// the good news is that we can cache and reuse the cost map computed in best(), +// since it depends only on the button wiring. all that's left after that is a +// graph search through the state space, using masks to prune each level. -func best(target Jolts, pool []Jolts) int { - var cost = make(map[Jolts]int) - var states []Jolts - states = append(states, Jolts{}) - cost[Jolts{}] = 0 - for pi, p := range pool { +func solveJolts(target Jolts, pool []uint32) int { + var bits uint32 + for _, x := range target { + bits |= uint32(x) + } + var answer int + for bit := uint(0); bits>>bit > 0; bit++ { + var mask uint32 + for i := range target { + mask |= uint32(target[i]) >> bit << i + } + + if mask == 0 { + continue + } + + n := best(mask, pool) + answer += n << bit + + //for i, p := range pool { + // if mask>>i&1 != 0 { + // target.Sub(p, 1) + // } + //} + } + return answer +} + +func best(target uint32, pool []uint32) int { + var cost = make(map[uint32]int) + var states = []uint32{0} + cost[0] = 0 + for _, p := range pool { // enumerate all the states that can be reached by toggling button p - queue := states - fmt.Print(pi, len(states), len(queue)) - for _, s := range queue { - here: - for t, c := s, cost[s]; ; { - for i, v := range p { - t[i] += v - if t[i] > target[i] { - // blown target, cut path - break here - } - } - c += 1 + for _, s := range states { + t := s ^ p + c := cost[t] + 1 - if cost_t, ok := cost[t]; ok { - if c < cost_t { - cost[t] = c - } - } else { + if cost_t, ok := cost[t]; ok { + if c < cost_t { cost[t] = c - states = append(states, t) } + } else { + cost[t] = c + states = append(states, t) } } } diff --git a/day10/sol2.py b/day10/sol2.py index d886ce6..c2a6ade 100644 --- a/day10/sol2.py +++ b/day10/sol2.py @@ -15,9 +15,27 @@ def solve(input): M = sympy.Matrix(matrix + [target]).T #print(repr(M)) S, pivots = M.rref() - if len(pivots) < BTNS: + if len(pivots) == BTNS: + # solved + presses = S.col(-1) + print("*", target, presses.T, sum(presses)) + part2 += sum(presses) + else: # not solved - print(repr(S)) + # the system of equations is underdetermined, either because + # we started with too few equations or because some of them were + # not linearly independent. + # the upshot is that we have one or more free variables + # (in practice 1-3 free variables) so if we just iterate + # through all legal values for those variables we should + # be able to find a solution? + # unfortunately i am still running into some unsolveable cases + # and i'm not sure why. + + #if BTNS - len(pivots) >= 3: + # print(repr(S)) + #continue + coords = [] limits = [] extra_rows = [] @@ -46,10 +64,6 @@ def solve(input): part2 += min(totals) else: print("uhoh", target) - else: - presses = S.col(-1) - print("*", target, presses.T, sum(presses)) - part2 += sum(presses) print(part2) #print(less, greater)