mirror of
https://github.com/Threnklyn/advent-of-code-go.git
synced 2026-05-18 19:13:27 +02:00
2021 day 18, ROUGH digging out small doubly linked list bugs :/
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
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 {
|
||||
nums := parseInput(input)
|
||||
|
||||
base := nums[0]
|
||||
for i := 1; i < len(nums); i++ {
|
||||
base = addNodes(base, nums[i])
|
||||
}
|
||||
|
||||
return base.magnitude()
|
||||
}
|
||||
|
||||
func part2(input string) int {
|
||||
nums := parseInput(input)
|
||||
|
||||
var best int
|
||||
for i, n1 := range nums {
|
||||
for j, n2 := range nums {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
// these operations are destructive to the lists so make copies every time
|
||||
cp1, cp2 := copyList(n1), copyList(n2)
|
||||
|
||||
n1PlusN2 := addNodes(cp1, cp2)
|
||||
mag := n1PlusN2.magnitude()
|
||||
if mag > best {
|
||||
best = mag
|
||||
}
|
||||
cp1, cp2 = copyList(n1), copyList(n2)
|
||||
|
||||
n2PlusN1 := addNodes(cp2, cp1)
|
||||
mag = n2PlusN1.magnitude()
|
||||
if mag > best {
|
||||
best = mag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return best
|
||||
}
|
||||
|
||||
// doubly linked list node
|
||||
type node struct {
|
||||
val int
|
||||
prev, next *node
|
||||
depth int
|
||||
}
|
||||
|
||||
func addNodes(n1, n2 *node) *node {
|
||||
// increase the depth on all nodes in n1 and n2 bc they will form a pair
|
||||
n := n1
|
||||
for n != nil {
|
||||
n.depth++
|
||||
n = n.next
|
||||
}
|
||||
n = n2
|
||||
for n != nil {
|
||||
n.depth++
|
||||
n = n.next
|
||||
}
|
||||
|
||||
// point last of n1 to first of n2 AND in reverse
|
||||
lastNode1 := n1
|
||||
for lastNode1.next != nil {
|
||||
lastNode1 = lastNode1.next
|
||||
}
|
||||
lastNode1.next = n2
|
||||
n2.prev = lastNode1 // reverse
|
||||
|
||||
n1 = n1.reduce()
|
||||
return n1
|
||||
}
|
||||
|
||||
func copyList(n *node) *node {
|
||||
var head, last *node
|
||||
|
||||
for p := n; p != nil; p = p.next {
|
||||
cp := &node{
|
||||
val: p.val,
|
||||
prev: last,
|
||||
next: nil,
|
||||
depth: p.depth,
|
||||
}
|
||||
if head == nil {
|
||||
head = cp
|
||||
last = cp
|
||||
} else {
|
||||
last.next = cp
|
||||
last = cp
|
||||
}
|
||||
}
|
||||
|
||||
return head
|
||||
}
|
||||
|
||||
func (n *node) reduce() (head *node) {
|
||||
for pointer := n; pointer != nil; pointer = pointer.next {
|
||||
// nested inside 4 pairs means depth is 5 or more
|
||||
if pointer.depth >= 5 {
|
||||
// should explode
|
||||
|
||||
pairRight := pointer.next // pointer to right node of pair
|
||||
if pairRight.depth != pointer.depth {
|
||||
panic(fmt.Sprintf("exploding pair should have same depth, got %d and %d", pointer.depth, pairRight.depth))
|
||||
}
|
||||
|
||||
// pair will become a node with zero value
|
||||
replacement := &node{
|
||||
val: 0,
|
||||
depth: pointer.depth - 1,
|
||||
prev: pointer.prev, // may be nil
|
||||
next: pairRight.next, // may be nil
|
||||
}
|
||||
|
||||
if pointer.prev != nil {
|
||||
pointer.prev.val += pointer.val
|
||||
// reassignments to remove old pair
|
||||
pointer.prev.next = replacement
|
||||
}
|
||||
if pairRight.next != nil {
|
||||
pairRight.next.val += pairRight.val
|
||||
// reassignments to remove old pair
|
||||
pairRight.next.prev = replacement
|
||||
}
|
||||
|
||||
// recursively call reduce on n again and RETURN to stop further reductions (reset logic)
|
||||
|
||||
// edge case for head is exploding
|
||||
if n == pointer {
|
||||
return replacement.reduce()
|
||||
}
|
||||
|
||||
return n.reduce()
|
||||
}
|
||||
}
|
||||
|
||||
for pointer := n; pointer != nil; pointer = pointer.next {
|
||||
if pointer.val >= 10 {
|
||||
// should split
|
||||
|
||||
replacementLeft := &node{
|
||||
val: pointer.val / 2, // integer division will round down
|
||||
prev: pointer.prev,
|
||||
next: nil, // will be replacementRight
|
||||
depth: pointer.depth + 1,
|
||||
}
|
||||
replacementRight := &node{
|
||||
val: pointer.val / 2, // need to round up
|
||||
prev: replacementLeft,
|
||||
next: pointer.next,
|
||||
depth: pointer.depth + 1,
|
||||
}
|
||||
|
||||
// adjustments to inits
|
||||
replacementLeft.next = replacementRight
|
||||
if pointer.val%2 == 1 {
|
||||
replacementRight.val++
|
||||
}
|
||||
|
||||
toLeft, toRight := pointer.prev, pointer.next
|
||||
if toLeft != nil {
|
||||
toLeft.next = replacementLeft
|
||||
}
|
||||
if toRight != nil {
|
||||
toRight.prev = replacementRight
|
||||
}
|
||||
|
||||
// recursively call reduce on n again and RETURN to stop further reductions (reset logic)
|
||||
|
||||
// edge case for head is splitting
|
||||
if n == pointer {
|
||||
return replacementLeft.reduce()
|
||||
}
|
||||
|
||||
return n.reduce()
|
||||
}
|
||||
}
|
||||
|
||||
// just return self if none of the reduce actions occurred
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *node) String() string {
|
||||
var sb strings.Builder
|
||||
|
||||
for p := n; p != nil; p = p.next {
|
||||
sb.WriteString(fmt.Sprintf("v: %d, depth: %d -> ", p.val, p.depth))
|
||||
}
|
||||
|
||||
// this would be prettier, but my brain hurts
|
||||
// for p := n; p != nil; p = p.next {
|
||||
// if p == n {
|
||||
// sb.WriteString(strings.Repeat("[", p.depth))
|
||||
// sb.WriteString(cast.ToString(p.val))
|
||||
// sb.WriteString(",")
|
||||
// } else {
|
||||
// if p.depth == p.prev.depth {
|
||||
// // right of a pair
|
||||
// sb.WriteString(cast.ToString(p.val))
|
||||
// sb.WriteString("]")
|
||||
// } else {
|
||||
|
||||
// }
|
||||
|
||||
// if p.next == nil {
|
||||
// sb.WriteString(strings.Repeat("]", p.prev.depth - p.depth))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
func (n *node) magnitude() int {
|
||||
// recursive calculation, 3*left + 2*right
|
||||
// regular numbers are just their number so [2,5] -> 3*2 + 2*5 = 16
|
||||
|
||||
// make copy because calculation works by collapsing the list's pairs
|
||||
cp := copyList(n)
|
||||
for depth := 4; depth > 0; depth-- {
|
||||
|
||||
for p := cp; p != nil; p = p.next {
|
||||
if p.depth == depth && p.next != nil && p.next.depth == depth {
|
||||
left, right := p, p.next
|
||||
newNode := node{
|
||||
val: 3*left.val + 2*right.val,
|
||||
prev: left.prev,
|
||||
next: right.next,
|
||||
depth: depth - 1,
|
||||
}
|
||||
if left == cp {
|
||||
cp = &newNode
|
||||
} else {
|
||||
left.prev.next = &newNode
|
||||
}
|
||||
if right.next != nil {
|
||||
right.next.prev = &newNode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cp.val
|
||||
}
|
||||
|
||||
func parseInput(input string) []*node {
|
||||
var snailfishNums []*node
|
||||
for _, line := range strings.Split(input, "\n") {
|
||||
var depth int
|
||||
|
||||
var pointer, head *node
|
||||
|
||||
for _, r := range line {
|
||||
switch r {
|
||||
case '[':
|
||||
depth++
|
||||
case ']':
|
||||
depth--
|
||||
case ',': // do nothing
|
||||
|
||||
default: // all single digit numbers
|
||||
newNode := node{
|
||||
val: cast.ToInt(string(r)),
|
||||
prev: pointer,
|
||||
next: nil,
|
||||
depth: depth,
|
||||
}
|
||||
// assign head and pointer if none already
|
||||
if pointer == nil {
|
||||
head = &newNode
|
||||
pointer = &newNode
|
||||
} else {
|
||||
// otherwise assign pointer's next to new node, reassign pointer to new node
|
||||
pointer.next = &newNode
|
||||
pointer = &newNode
|
||||
}
|
||||
}
|
||||
}
|
||||
snailfishNums = append(snailfishNums, head)
|
||||
}
|
||||
return snailfishNums
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var shortExample = `[1,1]
|
||||
[2,2]
|
||||
[3,3]
|
||||
[4,4]
|
||||
[5,5]`
|
||||
|
||||
/*
|
||||
[[[[[1,1],[2,2]],[3,3]],[4,4]],[5,5]]
|
||||
[[[[ 0, [3,2]],[3,3]],[4,4]],[5,5]]
|
||||
[[[[ 3, 0 ], [5,3]],[4,4]],[5,5]]
|
||||
|
||||
// reduces to [[[[3,0],[5,3]],[4,4]],[5,5]]
|
||||
|
||||
*/
|
||||
|
||||
var bigExample = `[[[0,[5,8]],[[1,7],[9,6]]],[[4,[1,2]],[[1,4],2]]]
|
||||
[[[5,[2,8]],4],[5,[[9,9],0]]]
|
||||
[6,[[[6,2],[5,6]],[[7,6],[4,7]]]]
|
||||
[[[6,[0,7]],[0,9]],[4,[9,[9,0]]]]
|
||||
[[[7,[6,4]],[3,[1,3]]],[[[5,5],1],9]]
|
||||
[[6,[[7,3],[3,2]]],[[[3,8],[5,7]],4]]
|
||||
[[[[5,4],[7,7]],8],[[8,3],8]]
|
||||
[[9,3],[[9,9],[6,[4,9]]]]
|
||||
[[2,[[7,7],7]],[[5,8],[[9,3],[0,2]]]]
|
||||
[[[[5,2],5],[8,[3,7]]],[[5,[7,5]],[4,4]]]`
|
||||
|
||||
var example1 = `[[[0,[4,5]],[0,0]],[[[4,5],[2,6]],[9,5]]]
|
||||
[7,[[[3,7],[4,3]],[[6,3],[8,8]]]]
|
||||
[[2,[[0,8],[3,4]]],[[[6,7],1],[7,[1,6]]]]
|
||||
[[[[2,4],7],[6,[0,5]]],[[[6,8],[2,8]],[[2,1],[4,5]]]]
|
||||
[7,[5,[[3,8],[1,4]]]]
|
||||
[[2,[2,2]],[8,[8,1]]]
|
||||
[2,9]
|
||||
[1,[[[9,3],9],[[9,0],[0,7]]]]
|
||||
[[[5,[7,4]],7],1]
|
||||
[[[[4,2],2],6],[8,7]]`
|
||||
|
||||
/*
|
||||
|
||||
[[[[7,7],[7,7]],[[8,7],[8,7]]],[[[7,0],[7,7]],9]]
|
||||
+ [[[[4,2],2],6],[8,7]]
|
||||
[[[[[7,7],[7,7]],[[8,7],[8,7]]],[[[7,0],[7,7]],9]],[[[[4,2],2],6],[8,7]]]
|
||||
[[[[0,[14,7]],[[8,7],[8,7]]],[[[7,0],[7,7]],9]],[[[[4,2],2],6],[8,7]]]
|
||||
[[[[14,0],[[15,7],[8,7]]],[[[7,0],[7,7]],9]],[[[[4,2],2],6],[8,7]]]
|
||||
[[[[14,15],[0,[15,7]]],[[[7,0],[7,7]],9]],[[[[4,2],2],6],[8,7]]]
|
||||
[[[[14,15],[15,0]],[[[14,0],[7,7]],9]],[[[[4,2],2],6],[8,7]]]
|
||||
[[[[14,15],[15,14]],[[0,[7,7]],9]],[[[[4,2],2],6],[8,7]]]
|
||||
[[ [[14,15],[15,14]] , [[7,0],16]],[[[[4,2],2],6],[8,7]]]
|
||||
^ this 16 was not getting added to
|
||||
indicating that the prev pointers were broken
|
||||
the bug was in the addNodes function to set n2.prev to lastN1 😭
|
||||
[[ [[14,15],[15,14]] , [[7,0],20] ],[[[0,4],6],[8,7]]]
|
||||
|
||||
|
||||
*/
|
||||
|
||||
var splitExample = `[[[[4,3],4],4],[7,[[8,4],9]]]
|
||||
[1,1]`
|
||||
|
||||
func Test_part1(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "example",
|
||||
input: "[[1,2],[[3,4],5]]",
|
||||
want: 143,
|
||||
},
|
||||
{
|
||||
name: "short",
|
||||
input: shortExample,
|
||||
// reduces to [[[[3,0],[5,3]],[4,4]],[5,5]]
|
||||
want: 791,
|
||||
},
|
||||
{
|
||||
name: "short plus 6s",
|
||||
input: shortExample + "\n[6,6]",
|
||||
// reduces to [[[[5,0],[7,4]],[5,5]],[6,6]]
|
||||
want: 1137,
|
||||
},
|
||||
{
|
||||
name: "split example",
|
||||
input: splitExample,
|
||||
// reduces to [[[[0,7],4],[[7,8],[6,0]]],[8,1]]
|
||||
want: 1384,
|
||||
},
|
||||
|
||||
{
|
||||
name: "example1",
|
||||
input: example1,
|
||||
want: 3488,
|
||||
},
|
||||
|
||||
{
|
||||
name: "bigExample",
|
||||
input: bigExample,
|
||||
want: 4140,
|
||||
},
|
||||
|
||||
{
|
||||
name: "actual",
|
||||
input: input,
|
||||
want: 4202,
|
||||
},
|
||||
}
|
||||
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: bigExample,
|
||||
want: 3993,
|
||||
},
|
||||
{
|
||||
name: "actual",
|
||||
input: input,
|
||||
want: 4779,
|
||||
},
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_node_reduce(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantString string
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
input: "[[[[[9,8],1],2],3],4]",
|
||||
wantString: "v: 0, depth: 4 -> v: 9, depth: 4 -> v: 2, depth: 3 -> v: 3, depth: 2 -> v: 4, depth: 1 -> ",
|
||||
// wantString: "[[[[0,9],2],3],4]",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
nums := parseInput(tt.input)
|
||||
head := nums[0]
|
||||
// fmt.Println(head)
|
||||
gotHead := head.reduce()
|
||||
if gotHead.String() != tt.wantString {
|
||||
t.Errorf("node.reduce() = %q, want %q", gotHead.String(), tt.wantString)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_node_magnitude(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want int
|
||||
}{
|
||||
{input: `[[1,2],[[3,4],5]]`, want: 143},
|
||||
{input: `[[[[0,7],4],[[7,8],[6,0]]],[8,1]]`, want: 1384},
|
||||
{input: `[[[[1,1],[2,2]],[3,3]],[4,4]]`, want: 445},
|
||||
{input: `[[[[3,0],[5,3]],[4,4]],[5,5]]`, want: 791},
|
||||
{input: `[[[[5,0],[7,4]],[5,5]],[6,6]]`, want: 1137},
|
||||
{input: `[[[[8,7],[7,7]],[[8,6],[7,7]]],[[[0,7],[6,6]],[8,7]]]`, want: 3488},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
n := parseInput(tt.input)[0]
|
||||
if got := n.magnitude(); got != tt.want {
|
||||
t.Errorf("node.magnitude() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user