Merge branch 'alexchao26:main' into main

This commit is contained in:
2025-12-03 09:52:53 +01:00
committed by GitHub
134 changed files with 16535 additions and 4 deletions
+126
View File
@@ -0,0 +1,126 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"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) string {
stacks, steps := parseInput(input)
for _, step := range steps {
// move crates ONE AT A TIME
for q := 0; q < step.qty; q++ {
top := stacks[step.from][len(stacks[step.from])-1]
stacks[step.to] = append(stacks[step.to], top)
stacks[step.from] = stacks[step.from][:len(stacks[step.from])-1]
}
}
ans := ""
for _, stack := range stacks {
ans += stack[len(stack)-1]
}
return ans
}
func part2(input string) string {
stacks, steps := parseInput(input)
for _, step := range steps {
// move crates ONCE
fromIndex := len(stacks[step.from]) - step.qty
stacks[step.to] = append(stacks[step.to], stacks[step.from][fromIndex:]...)
stacks[step.from] = stacks[step.from][:fromIndex]
}
ans := ""
for _, stack := range stacks {
ans += stack[len(stack)-1]
}
return ans
}
// move 4 from 3 to 1
type step struct {
qty, from, to int
}
func (s step) String() string {
return fmt.Sprintf("move %d from %d to %d", s.qty, s.from, s.to)
}
func parseInput(input string) ([][]string, []step) {
parts := strings.Split(input, "\n\n")
state := parts[0]
oversized := [][]string{}
for _, row := range strings.Split(state, "\n") {
oversized = append(oversized, strings.Split(row, ""))
}
oRows, oCols := len(oversized), len(oversized[0])
actual := [][]string{}
for c := 0; c < oCols-1; c++ {
if oversized[oRows-1][c] != " " {
// hit a column with values... move up from here
stack := []string{}
for r := oRows - 2; r >= 0; r-- {
char := oversized[r][c]
if char != " " {
stack = append(stack, char)
}
}
actual = append(actual, stack)
}
}
stepsRaw := parts[1]
steps := []step{}
for _, row := range strings.Split(stepsRaw, "\n") {
inst := step{}
_, err := fmt.Sscanf(row, "move %d from %d to %d", &inst.qty, &inst.from, &inst.to)
if err != nil {
panic(err)
}
// subtract one so they're zero indexed...
inst.from--
inst.to--
steps = append(steps, inst)
}
return actual, steps
}
+67
View File
@@ -0,0 +1,67 @@
package main
import (
"testing"
)
var example = ` [D]
[N] [C]
[Z] [M] [P]
1 2 3
move 1 from 2 to 1
move 3 from 1 to 3
move 2 from 2 to 1
move 1 from 1 to 2`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "example",
input: example,
want: "CMZ",
},
{
name: "actual",
input: input,
want: "QNHWJVJZW",
},
}
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 string
}{
{
name: "example",
input: example,
want: "MCD",
},
{
name: "actual",
input: input,
want: "BPCZJLFJW",
},
}
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)
}
})
}
}
+75
View File
@@ -0,0 +1,75 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"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 {
// packet starts w/ 4 characters that are all different
for i := 0; i+4 <= len(input); i++ {
if allDifferentLetters(input[i : i+4]) {
return i + 4
}
}
return -1
}
// lazy but easier than sliding window...
func allDifferentLetters(str string) bool {
// if len(str) != 4 {
// panic(fmt.Sprintf("invalid length %q", str))
// }
for i := 0; i < len(str); i++ {
for j := i + 1; j < len(str); j++ {
if str[i] == str[j] {
return false
}
}
}
return true
}
func part2(input string) int {
// wow super lazy but fast to write... ok
for i := 0; i+14 <= len(input); i++ {
if allDifferentLetters(input[i : i+14]) {
return i + 14
}
}
return -1
}
+59
View File
@@ -0,0 +1,59 @@
package main
import (
"testing"
)
var example = `mjqjpqmgbljsphdztnvjfqwrcgsmlb`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 7,
},
{
name: "actual",
input: input,
want: 1109,
},
}
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: 19,
},
{
name: "actual",
input: input,
want: 3965,
},
}
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)
}
})
}
}
+168
View File
@@ -0,0 +1,168 @@
package main
import (
_ "embed"
"flag"
"fmt"
"math"
"strings"
"github.com/alexchao26/advent-of-code-go/cast"
"github.com/alexchao26/advent-of-code-go/mathy"
"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 {
root := parseInput(input)
return sumDirsUnder100000(root)
}
func sumDirsUnder100000(itr *dir) int {
SizeLimit := 100000
sum := 0
if itr.totalSize <= SizeLimit {
sum += itr.totalSize
}
for _, child := range itr.childDirs {
sum += sumDirsUnder100000(child)
}
return sum
}
func part2(input string) int {
root := parseInput(input)
totalSapceAvailable := 70000000
spaceNeeded := 30000000
// find smallest directory to be deleted that would free up enough space...
directoryMinSize := spaceNeeded - (totalSapceAvailable - root.totalSize)
return findSmallestDirToDelete(root, directoryMinSize)
}
func findSmallestDirToDelete(itr *dir, directoryMinSize int) int {
smallest := math.MaxInt64
if itr.totalSize >= directoryMinSize {
smallest = mathy.MinInt(smallest, itr.totalSize)
}
for _, childDirs := range itr.childDirs {
smallest = mathy.MinInt(smallest, findSmallestDirToDelete(childDirs, directoryMinSize))
}
return smallest
}
type dir struct {
name string
parentDir *dir
childDirs map[string]*dir
files map[string]int
totalSize int
}
func parseInput(input string) *dir {
root := &dir{
name: "root",
childDirs: map[string]*dir{},
}
itr := root
cmds := strings.Split(input, "\n")
c := 0
for c < len(cmds) {
switch cmd := cmds[c]; cmd[0:1] {
case "$":
if cmd == "$ ls" {
// just move on, we will assume we're always in an listing state
c++
} else {
changeDir := strings.Split(cmd, "cd ")[1]
changeDir = strings.TrimSpace(changeDir)
if changeDir == ".." {
itr = itr.parentDir
} else {
// if changeDir doesn't exist..
if _, ok := itr.childDirs[changeDir]; !ok {
itr.childDirs[changeDir] = &dir{
name: changeDir,
parentDir: itr,
childDirs: map[string]*dir{},
files: map[string]int{}}
}
itr = itr.childDirs[changeDir]
}
c++
}
default:
// assume we're listing a dir's contents... add it
if strings.HasPrefix(cmd, "dir") {
childDirName := cmd[4:]
if _, ok := itr.childDirs[childDirName]; !ok {
itr.childDirs[childDirName] = &dir{
name: childDirName,
parentDir: itr,
childDirs: map[string]*dir{},
files: map[string]int{},
}
}
} else {
// file name
parts := strings.Split(cmd, " ")
itr.files[parts[0]] = cast.ToInt(parts[0])
}
c++
}
}
populateFileSizes(root)
return root
}
func populateFileSizes(itr *dir) int {
totalSize := 0
for _, childItr := range itr.childDirs {
totalSize += populateFileSizes(childItr)
}
for _, sz := range itr.files {
totalSize += sz
}
itr.totalSize = totalSize
return totalSize
}
+81
View File
@@ -0,0 +1,81 @@
package main
import (
"testing"
)
var example = `$ cd /
$ ls
dir a
14848514 b.txt
8504156 c.dat
dir d
$ cd a
$ ls
dir e
29116 f
2557 g
62596 h.lst
$ cd e
$ ls
584 i
$ cd ..
$ cd ..
$ cd d
$ ls
4060174 j
8033020 d.log
5626152 d.ext
7214296 k`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 95437,
},
{
name: "actual",
input: input,
want: 1423358,
},
}
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: 24933642,
},
{
name: "actual",
input: input,
want: 545729,
},
}
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)
}
})
}
}
+144
View File
@@ -0,0 +1,144 @@
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 {
grid := parseInput(input)
// may be visible from multiple angles
visibleCoords := map[[2]int]string{}
for r := 1; r < len(grid)-1; r++ {
// from left
highestFromLeft := -1
for c := 0; c < len(grid[0])-1; c++ {
height := grid[r][c]
if height > highestFromLeft {
visibleCoords[[2]int{r, c}] = "L"
highestFromLeft = height
}
}
// from right
highestFromRight := -1
for c := len(grid[0]) - 1; c > 0; c-- {
height := grid[r][c]
if height > highestFromRight {
visibleCoords[[2]int{r, c}] = "R"
highestFromRight = height
}
}
}
for c := 1; c < len(grid[0])-1; c++ {
// from top
highestFromTop := -1
for r := 0; r < len(grid)-1; r++ {
height := grid[r][c]
if height > highestFromTop {
visibleCoords[[2]int{r, c}] = "T"
highestFromTop = height
}
}
// from bottom
highestFromBottom := -1
for r := len(grid) - 1; r > 0; r-- {
height := grid[r][c]
if height > highestFromBottom {
visibleCoords[[2]int{r, c}] = "B"
highestFromBottom = height
}
}
}
return len(visibleCoords) + 4 // plus 4 for corners
}
func part2(input string) int {
// multiply the four scores together... score = how many trees any tree can see
// because trees on the edge will have a zero, just ignore them
grid := parseInput(input)
bestScore := 0
// iterate through every eligible tree
for r := 1; r < len(grid)-1; r++ {
for c := 1; c < len(grid[0])-1; c++ {
score := visible(grid, r, c, -1, 0)
score *= visible(grid, r, c, 1, 0)
score *= visible(grid, r, c, 0, -1)
score *= visible(grid, r, c, 0, 1)
if score > bestScore {
bestScore = score
}
}
}
return bestScore
}
func visible(grid [][]int, r, c, dr, dc int) int {
count := 0
startingHeight := grid[r][c]
r += dr
c += dc
for r >= 0 && r < len(grid) && c >= 0 && c < len(grid[0]) {
height := grid[r][c]
if height < startingHeight {
count++
} else {
count++
break
}
r += dr
c += dc
}
return count
}
func parseInput(input string) (ans [][]int) {
for _, line := range strings.Split(input, "\n") {
var row []int
for _, n := range strings.Split(line, "") {
row = append(row, cast.ToInt(n))
}
ans = append(ans, row)
}
return ans
}
+63
View File
@@ -0,0 +1,63 @@
package main
import (
"testing"
)
var example = `30373
25512
65332
33549
35390`
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: 1690,
},
}
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: 8,
},
{
name: "actual",
input: input,
want: 535680,
},
}
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)
}
})
}
}
+253
View File
@@ -0,0 +1,253 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"github.com/alexchao26/advent-of-code-go/cast"
"github.com/alexchao26/advent-of-code-go/mathy"
"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 {
// tail follows head logically if in the same row or column
// if not in same row or column, always moves diagonally
insts := parseInput(input)
// start stacked at 0,0
var head, tail [2]int
// "normal" grid mapping...
diffs := map[string][2]int{
"U": {1, 0},
"D": {-1, 0},
"L": {0, -1},
"R": {0, 1},
}
visited := map[[2]int]bool{
{0, 0}: true,
}
for _, inst := range insts {
for inst.val > 0 {
// move head
diff := diffs[inst.dir]
head[0] += diff[0] // row
head[1] += diff[1] // col
// update tail
// if diff to row or col is > 1
rowDiff := head[0] - tail[0]
colDiff := head[1] - tail[1]
// if either row or col diff is > 1, then that dimension HAS to move
// additionally, if the other diff is not zero, it needs to be
// adjusted to move diagonally
// note: the nested if blocks screwed me in part 2 because a longer
// rope can make coordinates off by 2 rows AND 2 cols
if mathy.AbsInt(rowDiff) > 1 {
/* 0 1 2
H . T
diff = head - tail = -2
want to make tail (2) to (1), so add diff / 2
T . H
diff = 2 - 0 = 2
tail (0) + 2/2 = 1, checks out still
*/
tail[0] += rowDiff / 2
// account for diagonal adjustment, same math... add col diff
if colDiff != 0 {
tail[1] += colDiff
}
} else if mathy.AbsInt(colDiff) > 1 {
tail[1] += colDiff / 2
// account for diagonal adjustment, same math... add col diff
if rowDiff != 0 {
tail[0] += rowDiff
}
}
// update where the tail has been...
visited[tail] = true
inst.val-- // one step at a time
}
}
// return spots TAIL visited at least once, map[[2]int]bool
return len(visited)
}
type inst struct {
dir string
val int
}
func parseInput(input string) (ans []inst) {
for _, line := range strings.Split(input, "\n") {
ans = append(ans, inst{
dir: line[:1],
val: cast.ToInt(line[2:]),
})
}
return ans
}
func part2(input string) int {
// oof, quite the refactor...
insts := parseInput(input)
rope := initRope(10)
visited := map[[2]int]bool{}
for _, inst := range insts {
for inst.val > 0 {
rope.moveOneSpace(inst.dir)
// update where the tail has been...
visited[rope.tail.coords] = true
inst.val-- // one step at a time
fmt.Println(inst, rope, len(visited))
}
}
return len(visited)
}
type node struct {
coords [2]int // row, col still
next *node
}
type rope struct {
head, tail *node
}
func initRope(length int) rope {
head := &node{}
itr := head
// start at 1 to account for head already being created
for i := 1; i < length; i++ {
itr.next = &node{}
itr = itr.next
}
return rope{
head: head,
tail: itr,
}
}
func (r rope) moveOneSpace(dir string) {
// "normal" grid mapping...
diffs := map[string][2]int{
"U": {1, 0},
"D": {-1, 0},
"L": {0, -1},
"R": {0, 1},
}
diff := diffs[dir]
r.head.coords[0] += diff[0]
r.head.coords[1] += diff[1]
// update rest of rope too
r.head.updateTrailer()
}
func (r rope) String() string {
str := ""
i := 0
for itr := r.head; itr != nil; itr = itr.next {
str += fmt.Sprintf("%d:[%d,%d]->", i, itr.coords[0], itr.coords[1])
i++
}
return str
}
// recursively updates the node behind itself as it follows
func (n *node) updateTrailer() {
if n.next == nil {
return
}
rowDiff := n.coords[0] - n.next.coords[0]
colDiff := n.coords[1] - n.next.coords[1]
// if either row or col diff is > 1, then that dimension HAS to move
// additionally, if the other diff is not zero, it needs to be
// adjusted to move diagonally
if mathy.AbsInt(rowDiff) > 1 && mathy.AbsInt(colDiff) > 1 {
n.next.coords[0] += rowDiff / 2
n.next.coords[1] += colDiff / 2
} else if mathy.AbsInt(rowDiff) > 1 {
// see part1 for math logic
n.next.coords[0] += rowDiff / 2
n.next.coords[1] += colDiff
} else if mathy.AbsInt(colDiff) > 1 {
n.next.coords[1] += colDiff / 2
n.next.coords[0] += rowDiff
} else {
// no need to continue updating children if movement is over
return
}
// go to next node
n.next.updateTrailer()
}
func reImplPart1(input string) int {
// oof, quite the refactor...
insts := parseInput(input)
rope := initRope(2)
visited := map[[2]int]bool{}
for _, inst := range insts {
for inst.val > 0 {
rope.moveOneSpace(inst.dir)
// update where the tail has been...
visited[rope.tail.coords] = true
inst.val-- // one step at a time
}
}
// return spots TAIL visited at least once, map[[2]int]bool
return len(visited)
}
+85
View File
@@ -0,0 +1,85 @@
package main
import (
"testing"
)
var example = `R 4
U 4
L 3
D 1
R 4
D 1
L 5
R 2`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 13,
},
{
name: "actual",
input: input,
want: 6236,
},
}
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)
}
})
t.Run("reimplementation_"+tt.name, func(t *testing.T) {
if got := reImplPart1(tt.input); got != tt.want {
t.Errorf("reImplPart1() = %v, want %v", got, tt.want)
}
})
}
}
var largerExample = `R 5
U 8
L 8
D 3
R 17
D 10
L 25
U 20`
func Test_part2(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 1,
},
{
name: "larger_example",
input: largerExample,
want: 36,
},
{
name: "actual",
input: input,
want: 2449,
},
}
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)
}
})
}
}
+167
View File
@@ -0,0 +1,167 @@
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 {
instructions := parseInput(input)
X := 1
sum := 0
i := 0 // what the current instruction is
for cycle := 1; cycle <= 220; cycle++ {
// "during" equates to the start of the cycle...
if (cycle-20)%40 == 0 {
sum += X * cycle
}
switch instructions[i].name {
case "addx":
// decrement cycles on that instruction
// IF it hits zero add V
// AND move to next step
instructions[i].cycles--
if instructions[i].cycles == 0 {
X += instructions[i].val
i++
}
case "noop":
// just increment to next instruction
i++
}
}
return sum
}
func part2(input string) string {
instructions := parseInput(input)
X := 1 // doubles as sprite's center coordinate
// 6 rows by 40 wide screen, starts all off
CRT := [6][40]string{}
for i, rows := range CRT {
for j := range rows {
CRT[i][j] = "."
}
}
i := 0 // what the current instruction is
for cycle := 1; i < len(instructions); cycle++ {
// if (cycle-20)%40 == 0 {
// sum += X * cycle
// }
/*
X = horizontal position of middle of (3 pixel wide) sprite
axis draws left to right, top to bottom, 40 wide x 6 high
1---40
41---80
...
201---240
draws 1 pixel per cycle
light up pixels IF the pixel being drawn is the same as one of the sprite's 3 pixels
*/
// calculate which pixel is being drawn... ZERO INDEXED
pixelRow := (cycle - 1) / 40
pixelCol := (cycle - 1) % 40
// see if the spite's horizontal location overlaps that pixelCol
spriteLeft, spriteRight := X-1, X+1
if spriteLeft <= pixelCol && spriteRight >= pixelCol {
CRT[pixelRow][pixelCol] = "#"
}
switch instructions[i].name {
case "addx":
// decrement cycles on that instruction
// IF it hits zero add V
// AND move to next step
instructions[i].cycles--
if instructions[i].cycles == 0 {
X += instructions[i].val
i++
}
case "noop":
// just increment to next instruction
i++
}
}
log := ""
for _, rows := range CRT {
for _, cell := range rows {
log += cell
}
log += "\n"
}
fmt.Println(log)
return log
}
type instruction struct {
name string
val int
cycles int
}
func parseInput(input string) (ans []instruction) {
for _, l := range strings.Split(input, "\n") {
switch l[:4] {
case "addx":
ans = append(ans, instruction{
name: "addx",
val: cast.ToInt(l[5:]),
cycles: 2,
})
case "noop":
ans = append(ans, instruction{
name: "noop",
cycles: 1,
})
default:
panic("input line: " + l)
}
}
return ans
}
+216
View File
@@ -0,0 +1,216 @@
package main
import (
"testing"
)
var example = `addx 15
addx -11
addx 6
addx -3
addx 5
addx -1
addx -8
addx 13
addx 4
noop
addx -1
addx 5
addx -1
addx 5
addx -1
addx 5
addx -1
addx 5
addx -1
addx -35
addx 1
addx 24
addx -19
addx 1
addx 16
addx -11
noop
noop
addx 21
addx -15
noop
noop
addx -3
addx 9
addx 1
addx -3
addx 8
addx 1
addx 5
noop
noop
noop
noop
noop
addx -36
noop
addx 1
addx 7
noop
noop
noop
addx 2
addx 6
noop
noop
noop
noop
noop
addx 1
noop
noop
addx 7
addx 1
noop
addx -13
addx 13
addx 7
noop
addx 1
addx -33
noop
noop
noop
addx 2
noop
noop
noop
addx 8
noop
addx -1
addx 2
addx 1
noop
addx 17
addx -9
addx 1
addx 1
addx -3
addx 11
noop
noop
addx 1
noop
addx 1
noop
noop
addx -13
addx -19
addx 1
addx 3
addx 26
addx -30
addx 12
addx -1
addx 3
addx 1
noop
noop
noop
addx -9
addx 18
addx 1
addx 2
noop
noop
addx 9
noop
noop
noop
addx -1
addx 2
addx -37
addx 1
addx 3
noop
addx 15
addx -21
addx 22
addx -6
addx 1
noop
addx 2
addx 1
noop
addx -10
noop
noop
addx 20
addx 1
addx 2
addx 2
addx -6
addx -11
noop
noop
noop`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 13140,
},
{
name: "actual",
input: input,
want: 15880,
},
}
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 string
}{
{
name: "example",
input: example,
want: `##..##..##..##..##..##..##..##..##..##..
###...###...###...###...###...###...###.
####....####....####....####....####....
#####.....#####.....#####.....#####.....
######......######......######......####
#######.......#######.......#######.....
`,
},
{
name: "actual",
input: input,
want: `###..#.....##..####.#..#..##..####..##..
#..#.#....#..#.#....#.#..#..#....#.#..#.
#..#.#....#....###..##...#..#...#..#....
###..#....#.##.#....#.#..####..#...#.##.
#....#....#..#.#....#.#..#..#.#....#..#.
#....####..###.#....#..#.#..#.####..###.
`,
},
}
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)
}
})
}
}
+243
View File
@@ -0,0 +1,243 @@
package main
import (
_ "embed"
"flag"
"fmt"
"sort"
"strings"
"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(true)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
} else {
ans := part2(true)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
}
}
func part1(useRealInput bool) int {
monkeys := initInput()
if !useRealInput {
monkeys = initExample()
}
inspectedCounts := make([]int, len(monkeys))
for round := 0; round < 20; round++ {
for i, monkey := range monkeys {
for _, item := range monkey.items {
newItemVal := monkey.operation(item) / 3
if newItemVal%monkey.testDivisibleBy == 0 {
monkeys[monkey.trueMonkey].items = append(
monkeys[monkey.trueMonkey].items, newItemVal)
} else {
monkeys[monkey.falseMonkey].items = append(
monkeys[monkey.falseMonkey].items, newItemVal)
}
}
inspectedCounts[i] += len(monkey.items)
// empty out this monkey's items
monkeys[i].items = []int{}
}
}
sort.Ints(inspectedCounts)
return inspectedCounts[len(inspectedCounts)-1] * inspectedCounts[len(inspectedCounts)-2]
}
// oh my god i figured out a math-y remainder theorem-y thing myself!
func part2(useRealInput bool) int {
monkeys := initInput()
if !useRealInput {
monkeys = initExample()
}
// the worry levels will always increase now that they're not being divided
// by 3, and we care about remainders because that's what all the tests are
// BUT we can't just mod by any monkey's testBy number, because they're all
// throwing the items around,
// so find a shared common denominator that can be used to keep the numbers
// under overflow
bigMod := 1
for _, m := range monkeys {
bigMod *= m.testDivisibleBy
}
inspectedCounts := make([]int, len(monkeys))
for round := 0; round < 10000; round++ {
for i, monkey := range monkeys {
for _, item := range monkey.items {
newItemVal := monkey.operation(item)
newItemVal %= bigMod
if newItemVal%monkey.testDivisibleBy == 0 {
monkeys[monkey.trueMonkey].items = append(
monkeys[monkey.trueMonkey].items, newItemVal)
} else {
monkeys[monkey.falseMonkey].items = append(
monkeys[monkey.falseMonkey].items, newItemVal)
}
}
inspectedCounts[i] += len(monkey.items)
// empty out this monkey's items
monkeys[i].items = []int{}
}
}
sort.Ints(inspectedCounts)
return inspectedCounts[len(inspectedCounts)-1] * inspectedCounts[len(inspectedCounts)-2]
}
type monkey struct {
items []int
operation func(int) int
testDivisibleBy int
trueMonkey, falseMonkey int // indices
}
// faster to manually type this than write a parser (and potentially debug)
func initInput() []monkey {
return []monkey{
{
items: []int{50, 70, 89, 75, 66, 66},
operation: func(old int) int {
return old * 5
},
testDivisibleBy: 2,
trueMonkey: 2,
falseMonkey: 1,
},
{
items: []int{85},
operation: func(old int) int {
return old * old
},
testDivisibleBy: 7,
trueMonkey: 3,
falseMonkey: 6,
},
{
items: []int{66, 51, 71, 76, 58, 55, 58, 60},
operation: func(old int) int {
return old + 1
},
testDivisibleBy: 13,
trueMonkey: 1,
falseMonkey: 3,
},
{
items: []int{79, 52, 55, 51},
operation: func(old int) int {
return old + 6
},
testDivisibleBy: 3,
trueMonkey: 6,
falseMonkey: 4,
},
{
items: []int{69, 92},
operation: func(old int) int {
return old * 17
},
testDivisibleBy: 19,
trueMonkey: 7,
falseMonkey: 5,
},
{
items: []int{71, 76, 73, 98, 67, 79, 99},
operation: func(old int) int {
return old + 8
},
testDivisibleBy: 5,
trueMonkey: 0,
falseMonkey: 2,
},
{
items: []int{82, 76, 69, 69, 57},
operation: func(old int) int {
return old + 7
},
testDivisibleBy: 11,
trueMonkey: 7,
falseMonkey: 4,
},
{
items: []int{65, 79, 86},
operation: func(old int) int {
return old + 5
},
testDivisibleBy: 17,
trueMonkey: 5,
falseMonkey: 0,
},
}
}
func initExample() []monkey {
return []monkey{
{
items: []int{79, 98},
operation: func(num int) int {
return num * 19
},
testDivisibleBy: 23,
trueMonkey: 2,
falseMonkey: 3,
},
{
items: []int{54, 65, 75, 74},
operation: func(num int) int {
return num + 6
},
testDivisibleBy: 19,
trueMonkey: 2,
falseMonkey: 0,
},
{
items: []int{79, 60, 97},
operation: func(num int) int {
return num * num
},
testDivisibleBy: 13,
trueMonkey: 1,
falseMonkey: 3,
},
{
items: []int{74},
operation: func(num int) int {
return num + 3
},
testDivisibleBy: 17,
trueMonkey: 0,
falseMonkey: 1,
},
}
}
+59
View File
@@ -0,0 +1,59 @@
package main
import (
"testing"
)
var example = ``
func Test_part1(t *testing.T) {
tests := []struct {
name string
useRealInput bool
want int
}{
{
name: "example",
useRealInput: false,
want: 10605,
},
{
name: "actual",
useRealInput: true,
want: 151312,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := part1(tt.useRealInput); got != tt.want {
t.Errorf("part1() = %v, want %v", got, tt.want)
}
})
}
}
func Test_part2(t *testing.T) {
tests := []struct {
name string
useRealInput bool
want int
}{
{
name: "example",
useRealInput: false,
want: 2713310158,
},
{
name: "actual",
useRealInput: true,
want: 51382025916,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := part2(tt.useRealInput); got != tt.want {
t.Errorf("part2() = %v, want %v", got, tt.want)
}
})
}
}
+152
View File
@@ -0,0 +1,152 @@
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)
}
}
var diffs = [4][2]int{
{0, -1},
{0, 1},
{-1, 0},
{1, 0},
}
func part1(input string) int {
grid := parseInput(input)
queue := [][3]int{}
label:
for r, rows := range grid {
for c, cell := range rows {
if cell == "S" {
queue = append(queue, [3]int{r, c, 0})
break label
}
}
}
seen := map[[2]int]bool{}
for len(queue) > 0 {
front := queue[0]
queue = queue[1:]
if seen[[2]int{front[0], front[1]}] {
continue
}
seen[[2]int{front[0], front[1]}] = true
if grid[front[0]][front[1]] == "E" {
return front[2]
}
for _, d := range diffs {
nextR, nextC := front[0]+d[0], front[1]+d[1]
if nextR >= 0 && nextR < len(grid) && nextC >= 0 && nextC < len(grid[0]) {
letterDiff := distanceBetweenLetters(grid[front[0]][front[1]], grid[nextR][nextC])
if letterDiff <= 1 {
queue = append(queue, [3]int{nextR, nextC, front[2] + 1})
}
}
}
}
return -1
}
func part2(input string) int {
grid := parseInput(input)
queue := [][3]int{}
label:
for r, rows := range grid {
for c, cell := range rows {
if cell == "E" {
queue = append(queue, [3]int{r, c, 0})
break label
}
}
}
seen := map[[2]int]bool{}
for len(queue) > 0 {
front := queue[0]
queue = queue[1:]
if seen[[2]int{front[0], front[1]}] {
continue
}
seen[[2]int{front[0], front[1]}] = true
if grid[front[0]][front[1]] == "a" {
return front[2]
}
for _, d := range diffs {
nextR, nextC := front[0]+d[0], front[1]+d[1]
if nextR >= 0 && nextR < len(grid) && nextC >= 0 && nextC < len(grid[0]) {
letterDiff := distanceBetweenLetters(grid[front[0]][front[1]], grid[nextR][nextC])
if letterDiff >= -1 {
queue = append(queue, [3]int{nextR, nextC, front[2] + 1})
}
}
}
}
return -1
}
func distanceBetweenLetters(x, y string) int {
if x == "S" {
x = "a"
}
if y == "S" {
y = "a"
}
if y == "E" {
y = "z"
}
if x == "E" {
x = "z"
}
return cast.ToASCIICode(y) - cast.ToASCIICode(x)
}
func parseInput(input string) (ans [][]string) {
for _, line := range strings.Split(input, "\n") {
ans = append(ans, strings.Split(line, ""))
}
return ans
}
+63
View File
@@ -0,0 +1,63 @@
package main
import (
"testing"
)
var example = `Sabqponm
abcryxxl
accszExk
acctuvwj
abdefghi`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 31,
},
{
name: "actual",
input: input,
want: 520,
},
}
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: 29,
},
{
name: "actual",
input: input,
want: 508,
},
}
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)
}
})
}
}
+139
View File
@@ -0,0 +1,139 @@
package main
import (
_ "embed"
"encoding/json"
"flag"
"fmt"
"sort"
"strings"
"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 {
pairs := parseInput(input)
// sum all the indexes that are in the right order
// ONE INDEXED NOT ZERO
goodIndexSum := 0
for i, pair := range pairs {
left, right := pair[0], pair[1]
if isInOrder(left, right) {
goodIndexSum += i + 1
}
}
return goodIndexSum
}
func part2(input string) int {
pairs := parseInput(input)
allPackets := [][]interface{}{
// good reminder that json.Unmarshal will convert numbers to float64...
// so need this to match for the way to isInOrder() function works..
{[]interface{}{float64(2)}},
{[]interface{}{float64(6)}},
}
for _, pair := range pairs {
allPackets = append(allPackets, pair[0])
allPackets = append(allPackets, pair[1])
}
sort.Slice(allPackets, func(i, j int) bool {
left, right := allPackets[i], allPackets[j]
return isInOrder(left, right)
})
ans := 1
for i, p := range allPackets {
if fmt.Sprint(p) == "[[2]]" || fmt.Sprint(p) == "[[6]]" {
ans *= i + 1
}
}
return ans
}
func parseInput(input string) (ans [][2][]interface{}) {
for _, packetPairs := range strings.Split(input, "\n\n") {
pairs := strings.Split(packetPairs, "\n")
ans = append(ans, [2][]interface{}{
parseRawString(pairs[0]),
parseRawString(pairs[1]),
})
}
return ans
}
// will parse as JSON with elements as either int or []int...
func parseRawString(raw string) []interface{} {
ans := []interface{}{}
json.Unmarshal([]byte(raw), &ans)
return ans
}
func isInOrder(left, right []interface{}) bool {
for l := 0; l < len(left); l++ {
if l > len(right)-1 {
return false
}
// attempt to convert both to ints...
leftNum, isLeftNum := left[l].(float64)
rightNum, isRightNum := right[l].(float64)
leftList, isLeftList := left[l].([]interface{})
rightList, isRightList := right[l].([]interface{})
if isLeftNum && isRightNum {
if leftNum != rightNum {
return leftNum < rightNum
}
} else if isLeftNum || isRightNum {
if isLeftNum {
leftList = []interface{}{leftNum}
} else if isRightNum {
rightList = []interface{}{rightNum}
} else {
panic(fmt.Sprintf("expected one num %T:%v, %T:%v", left[l],
left[l], right[l], right[l]))
}
return isInOrder(leftList, rightList)
} else {
// both lists
if !isLeftList || !isRightList {
panic(fmt.Sprintf("expected two lists %T:%v, %T:%v", left[l],
left[l], right[l], right[l]))
}
return isInOrder(leftList, rightList)
}
}
return true
}
+81
View File
@@ -0,0 +1,81 @@
package main
import (
"testing"
)
var example = `[1,1,3,1,1]
[1,1,5,1,1]
[[1],[2,3,4]]
[[1],4]
[9]
[[8,7,6]]
[[4,4],4,4]
[[4,4],4,4,4]
[7,7,7,7]
[7,7,7]
[]
[3]
[[[]]]
[[]]
[1,[2,[3,[4,[5,6,7]]]],8,9]
[1,[2,[3,[4,[5,6,0]]]],8,9]`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 13,
},
{
name: "actual",
input: input,
want: 5760,
},
}
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: 140,
},
{
name: "actual",
input: input,
want: 26670,
},
}
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)
}
})
}
}
+189
View File
@@ -0,0 +1,189 @@
package main
import (
_ "embed"
"flag"
"fmt"
"math"
"sort"
"strings"
"github.com/alexchao26/advent-of-code-go/cast"
"github.com/alexchao26/advent-of-code-go/mathy"
"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 {
matrix := parseInput(input)
originCol := 0
for i, c := range matrix[0] {
if c == "+" {
originCol = i
}
}
ans := 0
for !dropSand(matrix, originCol) {
ans++
}
return ans
}
func part2(input string) int {
matrix := parseInput(input)
originCol := 0
for i, c := range matrix[0] {
if c == "+" {
originCol = i
}
matrix[len(matrix)-1][i] = "#"
}
ans := 0
for !dropSand(matrix, originCol) {
ans++
// COULD incorporate this into the for loop conditional but then the
// ordering is important... must check origin cell BEFORE running
// dropSand... it's easier to read here...
if matrix[0][originCol] == "o" {
break
}
}
return ans
}
func parseInput(input string) (matrix [][]string) {
coordSets := [][][2]int{}
lowestCol := math.MaxInt64
highestRow := 0
for _, line := range strings.Split(input, "\n") {
rawCoords := strings.Split(line, " -> ")
coords := [][2]int{}
for _, rawCoord := range rawCoords {
rawNums := strings.Split(rawCoord, ",")
col, row := cast.ToInt(rawNums[0]), cast.ToInt(rawNums[1])
coord := [2]int{
col, row,
}
coords = append(coords, coord)
lowestCol = mathy.MinInt(lowestCol, col)
highestRow = mathy.MaxInt(highestRow, row)
}
coordSets = append(coordSets, coords)
}
// lowering this number to 1 makes it easier to print the matrix, which I
// used for part 1... but then needed to up it for part 2... or just have a
// massive screen and make the terminal text tiny...
ExtraLeftSpace := 200
highestCol := 0
for s, set := range coordSets {
for i := range set {
coordSets[s][i][0] -= lowestCol - ExtraLeftSpace
highestCol = mathy.MaxInt(highestCol, coordSets[s][i][0])
}
}
matrix = make([][]string, highestRow+3)
for r := range matrix {
matrix[r] = make([]string, highestCol+ExtraLeftSpace*2)
}
for _, set := range coordSets {
for i := 1; i < len(set); i++ {
cols := []int{set[i-1][0], set[i][0]}
rows := []int{set[i-1][1], set[i][1]}
sort.Ints(cols)
sort.Ints(rows)
if cols[0] == cols[1] {
for r := rows[0]; r <= rows[1]; r++ {
matrix[r][cols[0]] = "#"
}
} else if rows[0] == rows[1] {
for c := cols[0]; c <= cols[1]; c++ {
matrix[rows[0]][c] = "#"
}
}
}
}
originCol := 500 - lowestCol + ExtraLeftSpace
// make it a plus so it's searchable in the next step... or could just
// return this value too...
matrix[0][originCol] = "+"
for i, r := range matrix {
for j := range r {
if matrix[i][j] == "" {
matrix[i][j] = "."
}
}
}
// printMatrix(matrix)
return matrix
}
func printMatrix(matrix [][]string) {
for _, r := range matrix {
fmt.Println(r)
}
}
func dropSand(matrix [][]string, originCol int) (fallsIntoAbyss bool) {
r, c := 0, originCol
for r < len(matrix)-1 {
below := matrix[r+1][c]
diagonallyLeft := matrix[r+1][c-1]
diagonallyRight := matrix[r+1][c+1]
if below == "." {
r++
} else if diagonallyLeft == "." {
r++
c--
} else if diagonallyRight == "." {
r++
c++
} else {
matrix[r][c] = "o"
return false
}
}
return true
}
+60
View File
@@ -0,0 +1,60 @@
package main
import (
"testing"
)
var example = `498,4 -> 498,6 -> 496,6
503,4 -> 502,4 -> 502,9 -> 494,9`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 24,
},
{
name: "actual",
input: input,
want: 961,
},
}
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: 93,
},
{
name: "actual",
input: input,
want: 26375,
},
}
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)
}
})
}
}
+174
View File
@@ -0,0 +1,174 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"github.com/alexchao26/advent-of-code-go/mathy"
"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, 2000000)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
} else {
ans := part2(input, 4000000)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
}
}
// x is col, y is row
// unbounded grid...
// In the row where y=2000000, how many positions canNOT contain a beacon?
//
// very naive approach of marking each coord that is visible from some sensor
// then remove all beacons that are on the target row
// then just return the length of the map containing "seen" cells on target row
//
// this is brutally slow, no way this approach works for part 2
func part1(input string, targetRow int) int {
pairs := parseInput(input)
blockedCoords := map[[2]int]bool{}
for _, p := range pairs {
manhattanDist := mathy.ManhattanDistance(p.beaconRow, p.beaconCol,
p.sensorRow, p.sensorCol)
// if target row is reachable, block coords on it...
blockable := manhattanDist - mathy.AbsInt(p.sensorRow-targetRow)
if blockable > 0 {
for i := 0; i <= blockable; i++ {
// add blocks to map in both left and right directions
blockedCoords[[2]int{
targetRow, p.sensorCol - i,
}] = true
blockedCoords[[2]int{
targetRow, p.sensorCol + i,
}] = true
}
}
}
// remove any beacons that are present in the input?
for _, p := range pairs {
delete(blockedCoords, [2]int{p.beaconRow, p.beaconCol})
}
return len(blockedCoords)
}
func part2(input string, coordLimit int) int {
pairs := parseInput(input)
sensors := []parsedSensor{}
for _, p := range pairs {
sensors = append(sensors, parsedSensor{
sensorRow: p.sensorRow,
sensorCol: p.sensorCol,
manhattanDist: mathy.ManhattanDistance(p.sensorCol, p.sensorRow,
p.beaconCol, p.beaconRow),
})
}
// search space is too large to iterate over the entire thing and check if
// SOME sensor can see that location...
//
// we can assume that the final resting point will be 1 cell away from the
// border of a (actually multiple) sensor. this runs under the assumption
// that there is only one answer
for _, sensor := range sensors {
distPlusOne := sensor.manhattanDist + 1
// checking in this pattern w/ manhattan distance of 1
// 1
// 2 3
// 4 S 5
// 6B7
// 8
for r := -distPlusOne; r <= distPlusOne; r++ {
targetRow := sensor.sensorRow + r
if targetRow < 0 {
continue
}
if targetRow > coordLimit {
break
}
// check left and right on the target row
// zero for first and last r's... then subtract or add it from the
// sensor's col
colOffset := distPlusOne - mathy.AbsInt(r)
colLeft := sensor.sensorCol - colOffset
colRight := sensor.sensorCol + colOffset
if colLeft >= 0 && colLeft <= coordLimit &&
!isReachable(sensors, colLeft, targetRow) {
return colLeft*4000000 + targetRow
}
if colRight >= 0 && colRight <= coordLimit &&
!isReachable(sensors, colRight, targetRow) {
return colRight*4000000 + targetRow
}
}
}
panic("unreachable")
}
type pair struct {
sensorRow, sensorCol int
beaconRow, beaconCol int
}
func parseInput(input string) (ans []pair) {
// Sensor at x=2150774, y=3136587: closest beacon is at x=2561642, y=2914773
for _, line := range strings.Split(input, "\n") {
p := pair{}
_, err := fmt.Sscanf(line,
"Sensor at x=%d, y=%d: closest beacon is at x=%d, y=%d",
&p.sensorCol, &p.sensorRow, &p.beaconCol, &p.beaconRow)
if err != nil {
panic("parsing: " + err.Error())
}
ans = append(ans, p)
}
return ans
}
type parsedSensor struct {
sensorRow, sensorCol int
manhattanDist int
}
func isReachable(sensors []parsedSensor, c, r int) bool {
for _, sensor := range sensors {
// if reachable, break
if sensor.manhattanDist >= mathy.ManhattanDistance(c, r,
sensor.sensorCol, sensor.sensorRow) {
return true
}
}
return false
}
+78
View File
@@ -0,0 +1,78 @@
package main
import (
"testing"
)
var example = `Sensor at x=2, y=18: closest beacon is at x=-2, y=15
Sensor at x=9, y=16: closest beacon is at x=10, y=16
Sensor at x=13, y=2: closest beacon is at x=15, y=3
Sensor at x=12, y=14: closest beacon is at x=10, y=16
Sensor at x=10, y=20: closest beacon is at x=10, y=16
Sensor at x=14, y=17: closest beacon is at x=10, y=16
Sensor at x=8, y=7: closest beacon is at x=2, y=10
Sensor at x=2, y=0: closest beacon is at x=2, y=10
Sensor at x=0, y=11: closest beacon is at x=2, y=10
Sensor at x=20, y=14: closest beacon is at x=25, y=17
Sensor at x=17, y=20: closest beacon is at x=21, y=22
Sensor at x=16, y=7: closest beacon is at x=15, y=3
Sensor at x=14, y=3: closest beacon is at x=15, y=3
Sensor at x=20, y=1: closest beacon is at x=15, y=3`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
targetRow int
want int
}{
{
name: "example",
input: example,
targetRow: 10,
want: 26,
},
{
name: "actual",
input: input,
targetRow: 2000000,
want: 4560025,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := part1(tt.input, tt.targetRow); got != tt.want {
t.Errorf("part1() = %v, want %v", got, tt.want)
}
})
}
}
func Test_part2(t *testing.T) {
tests := []struct {
name string
input string
coordLimit int
want int
}{
{
name: "example",
input: example,
coordLimit: 20,
want: 56000011,
},
{
name: "actual",
input: input,
coordLimit: 4000000,
want: 12480406634249,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := part2(tt.input, tt.coordLimit); got != tt.want {
t.Errorf("part2() = %v, want %v", got, tt.want)
}
})
}
}
+393
View File
@@ -0,0 +1,393 @@
package main
import (
_ "embed"
"flag"
"fmt"
"sort"
"strings"
"github.com/alexchao26/advent-of-code-go/mathy"
"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)
}
}
/*
PART 1 and PART 2 ARE IMPLEMENTED COMPLETELY SEPARATELY.
If you're looking for part 2 inspiration you might want to not even look at my part1 code...
*/
func part1(input string) int {
// 30 min until cave erupts
// start in room AA
// creates graph to other rooms...
// flow rate is zero to start
// could spend 1 minute moving to BB and 1 more minute opening it. will release pressure of during remaining 28 of 30 minutes. 13 flow * 28 minutes = 364 total pressure lol
// then can move to CC in min 3, open in min 4, etc etc
// release max pressure in 30 min?
graph := makeGraph(input)
open := map[string]bool{"AA": true}
for name, rm := range graph {
if rm.flowRate == 0 {
open[name] = true
}
}
// "AA" open will not matter really...
return bfs(graph, "AA", 30, 0, open, map[string]int{})
}
type room struct {
name string
flowRate int
connectedTo []string
}
func makeGraph(input string) map[string]room {
graph := map[string]room{}
for _, line := range strings.Split(input, "\n") {
// Valve BB has flow rate=13; tunnels lead to valves CC, AA
parts := strings.Split(line, "; ")
rm := room{}
_, err := fmt.Sscanf(parts[0], "Valve %s has flow rate=%d", &rm.name, &rm.flowRate)
if err != nil {
panic("parsing valve name and flow rate" + err.Error())
}
connections := strings.Split(parts[1], ", ")
// update first entry to remove leading string
connections[0] = connections[0][len(connections[0])-2:]
rm.connectedTo = connections
graph[rm.name] = rm
}
return graph
}
func bfs(graph map[string]room, currentRoom string, minutesLeft, currentPressure int, open map[string]bool, memo map[string]int) int {
if minutesLeft == 0 {
return 0
}
key := hash(currentRoom, minutesLeft, open, currentPressure)
if v, ok := memo[key]; ok {
return v
}
// recursive calls will update this if it is better, then return it
bestFlow := 0
// there are two paths to take at a room
// 1. stay and open the valve
// this is only worth doing if the valve is not already on
// 2. move to a neighboring
// 1. open current room's valve
if !open[currentRoom] {
open[currentRoom] = true
// totalPressureContribution := (minutesLeft - 1) * graph[currentRoom].flowRate
newPressure := currentPressure + graph[currentRoom].flowRate
maybeBest := currentPressure + bfs(graph, currentRoom, minutesLeft-1, newPressure, open, memo)
bestFlow = mathy.MaxInt(bestFlow, maybeBest)
// backtrack
open[currentRoom] = false
}
// 2. move to neighbors
for _, neighbor := range graph[currentRoom].connectedTo {
maybeBest := currentPressure + bfs(graph, neighbor, minutesLeft-1, currentPressure, open, memo)
bestFlow = mathy.MaxInt(bestFlow, maybeBest)
}
memo[key] = bestFlow
return bestFlow
}
func hash(currentRoom string, minutesLeft int, open map[string]bool, currentPressure int) string {
rms := []string{}
for k := range open {
rms = append(rms, k)
}
sort.Strings(rms)
return fmt.Sprint(currentRoom, minutesLeft, rms, currentPressure)
}
// PART 2, basically restarting because my part1 seems too far in the opposite direction to reuse
func part2(input string) int {
// index within [16]int arrays which will be used to track which rooms have been visited
// from analysis i know that my input has 15 non-zero pressure rooms
// every time we visit a room we'll open the valve
graph := makeGraph(input)
roomToFlowRate := map[string]int{}
highestFlowRatePossible := 0 // might be useful for an optimization later
// sort room names so the arrays/slices later will be in a repeatable order (easier debugging)
// include starting room "AA" in this list even though it has zero flow rate
roomNames := []string{"AA"}
for name, room := range graph {
if room.flowRate != 0 {
roomNames = append(roomNames, name)
}
}
// reformat into arrays/slices so we don't have to do room name lookups
flowRates := make([]int, len(roomNames))
sort.Strings(roomNames)
for i, name := range roomNames {
roomToFlowRate[name] = graph[name].flowRate
flowRates[i] = roomToFlowRate[name]
highestFlowRatePossible += graph[name].flowRate
}
weightedGraph := makeWeightedGraph(graph, roomNames)
visitedArrayToHighestPressureTotals := map[[16]bool]int{}
dfsGetHighestPressureTotalsForEveryVisitedState(26, [16]bool{}, weightedGraph, flowRates, 0, 0, 0, visitedArrayToHighestPressureTotals)
return highestDisjointPair(visitedArrayToHighestPressureTotals)
}
func makeWeightedGraph(graph map[string]room, roomNames []string) [][]int {
ans := make([][]int, len(roomNames))
for i := range ans {
ans[i] = make([]int, len(roomNames))
}
// bfs between every node to make graph
for startIndex, startName := range roomNames {
for endIndex, endName := range roomNames {
if startName == endName {
continue
}
stepsBetweenRooms := bfsDistanceBetweenRooms(graph, startName, endName)
ans[startIndex][endIndex] = stepsBetweenRooms
ans[endIndex][startIndex] = stepsBetweenRooms
}
}
return ans
}
func bfsDistanceBetweenRooms(graph map[string]room, startName, endName string) int {
type node struct {
name string
steps int
}
queue := []node{
{
name: startName,
steps: 0,
},
}
seen := map[string]bool{}
for len(queue) > 0 {
pop := queue[0]
queue = queue[1:]
if seen[pop.name] {
continue
}
seen[pop.name] = true
if pop.name == endName {
return pop.steps
}
for _, neighbor := range graph[pop.name].connectedTo {
queue = append(queue, node{
name: neighbor,
steps: pop.steps + 1,
})
}
}
// assume all rooms are reachable
panic("should return from loop")
}
// populates ansArray with the best possible values for visiting a particular set of rooms
func dfsGetHighestPressureTotalsForEveryVisitedState(timeLeft int, visited [16]bool,
graph [][]int, flowRates []int, currentRoom int, flowRate, totalPressure int,
ansArray map[[16]bool]int) {
if timeLeft < 0 {
panic("negative timeLeft")
}
if timeLeft == 0 {
ansArray[visited] = mathy.MaxInt(ansArray[visited], totalPressure)
return
}
// branch 1: just not moving at all
dfsGetHighestPressureTotalsForEveryVisitedState(0, visited, graph, flowRates, currentRoom,
flowRate, totalPressure+flowRate*timeLeft, ansArray)
// rest of branches: attempt to visit every possible non-visited node
for roomIndex := range graph {
hasBeenVisited := visited[roomIndex]
if hasBeenVisited {
continue
}
// get to room, one more to open valve
timeToOpenNextValve := graph[currentRoom][roomIndex] + 1
// not worth visiting if the valve can't be opened in time
if timeLeft < timeToOpenNextValve {
continue
}
// in Go this makes a full copy of the array, so &nextVisited != &visited
// this is NOT true for slices ([]bool)
nextVisited := visited
nextVisited[roomIndex] = true
dfsGetHighestPressureTotalsForEveryVisitedState(
timeLeft-timeToOpenNextValve,
nextVisited,
graph,
flowRates,
roomIndex,
flowRate+flowRates[roomIndex],
totalPressure+flowRate*timeToOpenNextValve,
ansArray)
}
}
func highestDisjointPair(visitedArrayToHighestPressureTotals map[[16]bool]int) int {
type finishingState struct {
bitmap int
// leaving this here to explain a simpler solution (without bitmap overkill)
// visited [16]bool
totalPressure int
}
allFinishingStates := []finishingState{}
for visited, totalPressure := range visitedArrayToHighestPressureTotals {
allFinishingStates = append(allFinishingStates, finishingState{
bitmap: convertToBitmap(visited),
totalPressure: totalPressure,
})
}
// sort in decreasing order
sort.Slice(allFinishingStates, func(i, j int) bool {
return allFinishingStates[i].totalPressure > allFinishingStates[j].totalPressure
})
bestCombo := -1
for _, baseFinishingState := range allFinishingStates {
for _, maybeDisjointFinishingState := range allFinishingStates {
pressureSum := baseFinishingState.totalPressure + maybeDisjointFinishingState.totalPressure
// allFinishingStates is sorted in decreasing order so at this point there is no reason
// to continue checking sums
if pressureSum < bestCombo {
break
}
// only update baseFinishing state if sets are disjointed (human and elephant can't
// open the same room's valve)
//
// using bit logic is overkill, this could've been replaced with this single for loop:
// isDisjoint := true
// for i, wasVisited := range baseFinishingState.visited {
// if wasVisited && maybeDisjointFinishingState.visited[i] {
// isDisjoint = false
// break
// }
// }
if baseFinishingState.bitmap&maybeDisjointFinishingState.bitmap == 0 {
bestCombo = mathy.MaxInt(bestCombo, pressureSum)
}
}
}
return bestCombo
}
func convertToBitmap(visited [16]bool) int {
var bitmap int
for i, wasVisited := range visited {
if wasVisited {
bitmap |= 1 << i
}
}
return bitmap
}
// recheck part1 using part2 logic
func part1ViaPart2(input string) int {
graph := makeGraph(input)
roomToFlowRate := map[string]int{}
highestFlowRatePossible := 0 // might be useful for an optimization later
// sort room names so the arrays/slices later will be in a repeatable order (easier debugging)
// include starting room "AA" in this list even though it has zero flow rate
roomNames := []string{"AA"}
for name, room := range graph {
if room.flowRate != 0 {
roomNames = append(roomNames, name)
}
}
// reformat into arrays/slices so we don't have to do room name lookups
flowRates := make([]int, len(roomNames))
sort.Strings(roomNames)
for i, name := range roomNames {
roomToFlowRate[name] = graph[name].flowRate
flowRates[i] = roomToFlowRate[name]
highestFlowRatePossible += graph[name].flowRate
}
weightedGraph := makeWeightedGraph(graph, roomNames)
visitedArrayToHighestPressureTotals := map[[16]bool]int{}
dfsGetHighestPressureTotalsForEveryVisitedState(30, [16]bool{}, weightedGraph, flowRates, 0, 0, 0, visitedArrayToHighestPressureTotals)
highest := 0
for _, val := range visitedArrayToHighestPressureTotals {
highest = mathy.MaxInt(highest, val)
}
return highest
}
+73
View File
@@ -0,0 +1,73 @@
package main
import (
"testing"
)
var example = `Valve AA has flow rate=0; tunnels lead to valves DD, II, BB
Valve BB has flow rate=13; tunnels lead to valves CC, AA
Valve CC has flow rate=2; tunnels lead to valves DD, BB
Valve DD has flow rate=20; tunnels lead to valves CC, AA, EE
Valve EE has flow rate=3; tunnels lead to valves FF, DD
Valve FF has flow rate=0; tunnels lead to valves EE, GG
Valve GG has flow rate=0; tunnels lead to valves FF, HH
Valve HH has flow rate=22; tunnel leads to valve GG
Valve II has flow rate=0; tunnels lead to valves AA, JJ
Valve JJ has flow rate=21; tunnel leads to valve II`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 1651,
},
{
name: "actual",
input: input,
want: 1828,
},
}
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)
}
})
t.Run(tt.name+"with part 2 logic", func(t *testing.T) {
if got := part1ViaPart2(tt.input); got != tt.want {
t.Errorf("part1ViaPart2() = %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: 1707,
},
{
name: "actual",
input: input,
want: 2292,
},
}
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)
}
})
}
}
+312
View File
@@ -0,0 +1,312 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"github.com/alexchao26/advent-of-code-go/cast"
"github.com/alexchao26/advent-of-code-go/mathy"
"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, 1000000000000)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
}
}
func part1(input string) int {
// ####
// .#.
// ###
// .#.
// ..#
// ..#
// ###
// #
// #
// #
// #
// ##
// ##
// chamber is 7 units wide
// rocks fall w/ 2 spaces to left wall, 3 spaces to next rock or floor
// rocks pushed by wind, then fall down 1 space
// WHEN rock touches something below (ground or another settled rock), rock is "done" and next rock appears
// this also means that rocks will be pushed once L/R before fully settling (unless blocked L/R)
s := newState(input)
for i := 0; i < 2022; i++ {
s.dropRock()
}
// height after 2022 rocks fall
return s.highestSettledRow + 1 // 1 indexed
}
func part2(input string, wantedRocks int) int {
s := newState(input)
// obviously can't do the calculations literally anymore...
// need some kind of "last time this state was seen" check to skip steps
// need to hash states
// - the top 10ish rows should be plenty
// - rock index
// - value = steps since last seen AND highestRow to calc skips
// [2]int{steps when last seen, rocks dropped, highestRow to calc diff}
pastStates := map[string][3]int{}
// will keep track of rows that are mathematically skipped otherwise it'll
// mess with state and the settled matrix
dupeRows := 0
rocksDropped := 0
for rocksDropped < wantedRocks {
s.dropRock()
rocksDropped++
h := s.hash(20)
if past, ok := pastStates[h]; ok {
pastSteps, pastRocksDropped, pastHighRow := past[0], past[1], past[2]
stepsToSkip := s.stepIndex - pastSteps
rocksToSkip := rocksDropped - pastRocksDropped
rowsToAdd := s.highestSettledRow - pastHighRow
iterationsToSkip := (wantedRocks - rocksDropped) / rocksToSkip
dupeRows += rowsToAdd * iterationsToSkip
s.stepIndex += stepsToSkip * iterationsToSkip
rocksDropped += rocksToSkip * iterationsToSkip
} else {
pastStates[h] = [3]int{
s.stepIndex, rocksDropped, s.highestSettledRow,
}
}
}
// height after 2022 rocks fall
return s.highestSettledRow + 1 + dupeRows // 1 indexed
}
type state struct {
settled [][]string
highestSettledRow int
fallingCoords [][2]int
nextRockIndex int
steps []string
stepIndex int
}
func newState(input string) state {
s := state{
settled: [][]string{},
highestSettledRow: -1,
fallingCoords: nil,
nextRockIndex: 0,
steps: strings.Split(input, ""),
stepIndex: 0,
}
return s
}
// knew I'd need this for debugging...
func (s state) printState() {
copySettled := [][]string{}
for _, row := range s.settled {
copyRow := make([]string, len(row))
copy(copyRow, row)
copySettled = append(copySettled, copyRow)
}
for _, coord := range s.fallingCoords {
copySettled[coord[0]][coord[1]] = "@"
}
var sb strings.Builder
for r := len(copySettled) - 1; r >= 0; r-- {
sb.WriteString(strings.Join(copySettled[r], "") + cast.ToString(r) + "\n")
}
fmt.Println(sb.String())
}
func (s *state) dropRock() {
s.populateNextBaseCoords()
highestRow := 0
for _, c := range s.fallingCoords {
highestRow = mathy.MaxInt(highestRow, c[0])
}
for len(s.settled) <= highestRow {
s.settled = append(s.settled, newEmptyRow())
}
// will be set back to nil when settled
for s.fallingCoords != nil {
switch s.steps[s.stepIndex%len(s.steps)] {
case ">":
// check if can move right
canMoveRight := true
for _, c := range s.fallingCoords {
if c[1] == 6 || s.settled[c[0]][c[1]+1] != "." {
canMoveRight = false
}
}
if canMoveRight {
for i := range s.fallingCoords {
s.fallingCoords[i][1]++
}
}
case "<":
// check if can move left
canMoveLeft := true
for _, c := range s.fallingCoords {
if c[1] == 0 || s.settled[c[0]][c[1]-1] != "." {
canMoveLeft = false
}
}
if canMoveLeft {
for i := range s.fallingCoords {
s.fallingCoords[i][1]--
}
}
default:
panic(s.steps[s.stepIndex])
}
s.stepIndex++
// move down
canMoveDown := true
for _, c := range s.fallingCoords {
if c[0] == 0 || s.settled[c[0]-1][c[1]] != "." {
canMoveDown = false
}
}
// is blocked, draw onto settled then make nil
if !canMoveDown {
for _, c := range s.fallingCoords {
s.settled[c[0]][c[1]] = "#"
}
s.fallingCoords = nil
for r := len(s.settled) - 1; r >= 0; r-- {
if strings.Join(s.settled[r], "") != "......." {
s.highestSettledRow = r
break
}
}
} else {
for i := range s.fallingCoords {
s.fallingCoords[i][0]--
}
}
}
}
func newEmptyRow() []string {
row := make([]string, 7)
for i := range row {
row[i] = "."
}
return row
}
var baseCoords = [][][2]int{
{
// line ####
{0, 0},
{0, 1},
{0, 2},
{0, 3},
}, {
// plus
{0, 1},
{1, 0},
{1, 1},
{1, 2},
{2, 1},
}, {
// flipped L
{0, 0},
{0, 1},
{0, 2},
{1, 2},
{2, 2},
}, {
// vert line
{0, 0},
{1, 0},
{2, 0},
{3, 0},
}, {
// square
{0, 0},
{0, 1},
{1, 0},
{1, 1},
},
}
func init() {
// add 2 cols to all baseCoords because they fall 2 off of left wall
for i := range baseCoords {
for j := range baseCoords[i] {
baseCoords[i][j][1] += 2
}
}
}
func (s *state) populateNextBaseCoords() {
copyCoords := make([][2]int, len(baseCoords[s.nextRockIndex]))
copy(copyCoords, baseCoords[s.nextRockIndex])
s.nextRockIndex++
s.nextRockIndex %= 5
// lowest row of baseCoords...
for i := range copyCoords {
copyCoords[i][0] += s.highestSettledRow + 1 + 3
}
s.fallingCoords = copyCoords
}
// for part 2 to find return states
// NOTE: had to play with the number of rows to be hashed... 20 seems to
// work on the example input
func (s *state) hash(topRowsToHash int) string {
var sb strings.Builder
sb.WriteString(cast.ToString(s.nextRockIndex))
for r := s.highestSettledRow; r >= 0 && r > s.highestSettledRow-topRowsToHash; r-- {
sb.WriteString("\n" + strings.Join(s.settled[r], ""))
}
return sb.String()
}
+75
View File
@@ -0,0 +1,75 @@
package main
import (
"testing"
)
var example = `>>><<><>><<<>><>>><<<>>><<<><<<>><>><<>>`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 3068,
},
{
name: "actual",
input: input,
want: 3219,
},
}
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
wantedRocks int
want int
}{
// part 1 values should work as well, so using them for extra testing
{
name: "example",
input: example,
wantedRocks: 2022,
want: 3068,
},
{
name: "actual",
input: input,
wantedRocks: 2022,
want: 3219,
},
{
name: "example",
input: example,
wantedRocks: 1000000000000,
want: 1514285714288,
},
{
name: "actual",
input: input,
wantedRocks: 1000000000000,
want: 1582758620701,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := part2(tt.input, tt.wantedRocks); got != tt.want {
t.Errorf("part2() = %v, want %v", got, tt.want)
}
})
}
}
+171
View File
@@ -0,0 +1,171 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"github.com/alexchao26/advent-of-code-go/cast"
"github.com/alexchao26/advent-of-code-go/mathy"
"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)
}
}
// to six adjacent face...
var diffs = [][3]int{
{0, 0, 1},
{0, 1, 0},
{1, 0, 0},
{0, 0, -1},
{0, -1, 0},
{-1, 0, 0},
}
func part1(input string) int {
rawCoords := parseInput(input)
mapCoords := convertRawCoordsToMap(rawCoords)
totalSurfaceArea := 0
for _, coord := range rawCoords {
neighbors := 6
for _, d := range diffs {
if mapCoords[[3]int{
coord[0] - d[0],
coord[1] - d[1],
coord[2] - d[2],
}] {
neighbors--
}
}
totalSurfaceArea += neighbors
}
return totalSurfaceArea
}
func part2(input string) int {
rawCoords := parseInput(input)
mapCoords := convertRawCoordsToMap(rawCoords)
// get bounds
var limitX, limitY, limitZ int
for c := range mapCoords {
limitX = mathy.MaxInt(limitX, c[0])
limitY = mathy.MaxInt(limitY, c[1])
limitZ = mathy.MaxInt(limitZ, c[2])
}
// bfs to see if an edge can be reached
// delete if not useful
totalExternalSurfaceArea := 0
for coord := range mapCoords {
totalExternalSurfaceArea += facesThatCanReachEdge(coord, mapCoords,
limitX, limitY, limitZ)
}
// too low: 1036
return totalExternalSurfaceArea
}
func parseInput(input string) (ans [][3]int) {
for _, line := range strings.Split(input, "\n") {
parts := strings.Split(line, ",")
ans = append(ans, [3]int{
cast.ToInt(parts[0]),
cast.ToInt(parts[1]),
cast.ToInt(parts[2]),
})
}
return ans
}
func convertRawCoordsToMap(rawCoords [][3]int) map[[3]int]bool {
set := map[[3]int]bool{}
for _, coord := range rawCoords {
set[coord] = true
}
return set
}
// there would be a big optimization here to keep track of all coords that have
// a known path to an edge, that would eliminate a lot of duplicate work... but
// i think this is a small enough problem space to ignore that...
func facesThatCanReachEdge(coord [3]int, set map[[3]int]bool, limitX, limitY, limitZ int) int {
ans := 0
for _, d := range diffs {
next := [3]int{
coord[0] + d[0],
coord[1] + d[1],
coord[2] + d[2],
}
reachResult := canReachEdge(next, set, limitX, limitY, limitZ)
if reachResult {
ans++
}
}
return ans
}
func canReachEdge(coord [3]int, set map[[3]int]bool, limitX, limitY, limitZ int,
) bool {
queue := [][3]int{coord}
seen := map[[3]int]bool{}
for len(queue) > 0 {
front := queue[0]
queue = queue[1:]
// seen already or hit some other droplet, skip
if seen[front] || set[front] {
continue
}
seen[front] = true
// edge reached
if front[0] <= 0 || front[0] >= limitX ||
front[1] <= 0 || front[1] >= limitY ||
front[2] <= 0 || front[2] >= limitZ {
return true
}
for _, d := range diffs {
next := [3]int{
front[0] + d[0],
front[1] + d[1],
front[2] + d[2],
}
queue = append(queue, next)
}
}
return false
}
+99
View File
@@ -0,0 +1,99 @@
package main
import (
"testing"
)
var example = `2,2,2
1,2,2
3,2,2
2,1,2
2,3,2
2,2,1
2,2,3
2,2,4
2,2,6
1,2,5
3,2,5
2,1,5
2,3,5`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 64,
},
{
name: "actual",
input: input,
want: 4636,
},
}
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)
}
})
}
}
var flatDisc = `5,5,5
5,5,6
5,5,7
5,6,5
5,6,6
5,6,7
5,7,5
5,7,6
5,7,7`
// 3x3x1 disc...
func Test_part2(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "dumb simple",
input: "1,1,1\n2,1,1",
want: 10,
},
{
name: "dumber simpleer",
input: "2,1,1",
want: 6,
},
{
name: "example",
input: example,
want: 58,
},
{
name: "flatDisc",
input: flatDisc,
// 9 + 9 + 3 * 4 = 30
want: 30,
},
{
name: "actual",
input: input,
// PAIN, used coord instead of front in the bfs check :/
want: 2572,
},
}
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)
}
})
}
}
+211
View File
@@ -0,0 +1,211 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"github.com/alexchao26/advent-of-code-go/mathy"
"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 {
blueprints := parseInput(input)
// how many geodes can be opened in 24 minutes?
sum := 0
for _, bp := range blueprints {
st := newState(bp)
geodesMade := st.calcMostGeodes(0, map[string]int{}, 24, 24)
// fmt.Println("ID:", bp.id, geodesMade)
sum += st.blueprint.id * geodesMade
}
// total quality of all blueprints, quality = id * (# geodes in 24 min)
return sum
}
func part2(input string) int {
blueprints := parseInput(input)
if len(blueprints) > 3 {
blueprints = blueprints[:3]
}
prod := 1
for _, bp := range blueprints {
st := newState(bp)
geodesMade := st.calcMostGeodes(0, map[string]int{}, 32, 32)
// fmt.Println(bp.id, geodesMade)
prod *= geodesMade
}
// total quality of all blueprints, quality = id * (# geodes in 24 min)
return prod
}
type blueprint struct {
id int
oreForOreRobot int
oreForClayRobot int
oreForObsidianRobot, clayForObsidianRobot int
oreForGeodeRobot, obsidianForGeodeRobot int
}
type state struct {
blueprint
ore, clay, obsidian, geode int
oreRobots, clayRobots, obsidianRobots, geodeRobots int
}
func newState(blueprint blueprint) state {
return state{
blueprint: blueprint,
oreRobots: 1,
}
}
func (s *state) farm() {
s.ore += s.oreRobots
s.clay += s.clayRobots
s.obsidian += s.obsidianRobots
s.geode += s.geodeRobots
}
func (s *state) hash(time int) string {
return fmt.Sprint(time, s.ore, s.clay, s.obsidian,
s.geode, s.oreRobots, s.clayRobots, s.obsidianRobots, s.geodeRobots)
}
// NOT A POINTER METHOD SO A COPY CAN BE MADE
// this is some cheeky Go struct copying, it'd be easier to read if it was just
// directly recreating all the fields
func (s state) copy() state {
return s
}
func (s *state) calcMostGeodes(time int, memo map[string]int, totalTime int, earliestGeode int) int {
if time == totalTime {
return s.geode
}
h := s.hash(time)
if v, ok := memo[h]; ok {
return v
}
if s.geode == 0 && time > earliestGeode {
return 0
}
// factory can try to make any possible robot, will backtrack if necessary
mostGeodes := s.geode
// always make geode robots
if s.ore >= s.oreForGeodeRobot &&
s.obsidian >= s.obsidianForGeodeRobot {
cp := s.copy()
cp.farm()
cp.ore -= cp.oreForGeodeRobot
cp.obsidian -= cp.obsidianForGeodeRobot
cp.geodeRobots++
if cp.geodeRobots == 1 {
earliestGeode = mathy.MinInt(earliestGeode, time+1)
}
mostGeodes = mathy.MaxInt(mostGeodes, cp.calcMostGeodes(time+1, memo, totalTime, earliestGeode))
memo[h] = mostGeodes
return mostGeodes
}
if time <= totalTime-16 &&
s.oreRobots < s.oreForObsidianRobot*2 &&
s.ore >= s.oreForOreRobot {
cp := s.copy()
cp.ore -= cp.oreForOreRobot
cp.farm()
cp.oreRobots++
mostGeodes = mathy.MaxInt(mostGeodes, cp.calcMostGeodes(time+1, memo, totalTime, earliestGeode))
}
if time <= totalTime-8 &&
s.clayRobots < s.clayForObsidianRobot &&
s.ore >= s.oreForClayRobot {
cp := s.copy()
cp.ore -= cp.oreForClayRobot
cp.farm()
cp.clayRobots++
mostGeodes = mathy.MaxInt(mostGeodes, cp.calcMostGeodes(time+1, memo, totalTime, earliestGeode))
}
if time <= totalTime-4 &&
s.obsidianRobots < s.obsidianForGeodeRobot &&
s.ore >= s.oreForObsidianRobot && s.clay >= s.clayForObsidianRobot {
cp := s.copy()
cp.ore -= cp.oreForObsidianRobot
cp.clay -= cp.clayForObsidianRobot
cp.farm()
cp.obsidianRobots++
mostGeodes = mathy.MaxInt(mostGeodes, cp.calcMostGeodes(time+1, memo, totalTime, earliestGeode))
}
// or no factory production this minute
cp := s.copy()
cp.ore += cp.oreRobots
cp.clay += cp.clayRobots
cp.obsidian += cp.obsidianRobots
cp.geode += cp.geodeRobots
mostGeodes = mathy.MaxInt(mostGeodes, cp.calcMostGeodes(time+1, memo, totalTime, earliestGeode))
memo[h] = mostGeodes
return mostGeodes
}
func parseInput(input string) (ans []blueprint) {
// Blueprint 1: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 20 clay. Each geode robot costs 2 ore and 12 obsidian.
for _, line := range strings.Split(input, "\n") {
bp := blueprint{}
_, err := fmt.Sscanf(line, "Blueprint %d: Each ore robot costs %d ore. Each clay robot costs %d ore. Each obsidian robot costs %d ore and %d clay. Each geode robot costs %d ore and %d obsidian.",
&bp.id, &bp.oreForOreRobot, &bp.oreForClayRobot, &bp.oreForObsidianRobot,
&bp.clayForObsidianRobot, &bp.oreForGeodeRobot, &bp.obsidianForGeodeRobot)
if err != nil {
panic("parsing: " + err.Error())
}
ans = append(ans, bp)
}
return ans
}
+62
View File
@@ -0,0 +1,62 @@
package main
import (
"testing"
)
var example = `Blueprint 1: Each ore robot costs 4 ore. Each clay robot costs 2 ore. Each obsidian robot costs 3 ore and 14 clay. Each geode robot costs 2 ore and 7 obsidian.
Blueprint 2: Each ore robot costs 2 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 8 clay. Each geode robot costs 3 ore and 12 obsidian.`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 33,
},
{
name: "actual",
input: input,
want: 1127,
},
}
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: 56 * 62,
},
// I actually have zero idea how long this took to run, I ran it overnight
// 21 27 38
// {
// name: "actual",
// input: input,
// want: 21546,
// },
}
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)
}
})
}
}
+165
View File
@@ -0,0 +1,165 @@
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")
}
}
const part2DecryptionKey = 811589153
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 := mixList(input, 1, 1)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
} else {
ans := mixList(input, 811589153, 10)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
}
}
func mixList(input string, decryptionKey, mixes int) int {
zeroNode, originalOrder := parseInput(input)
for i := 0; i < len(originalOrder); i++ {
originalOrder[i].val *= decryptionKey
}
for i := 0; i < mixes; i++ {
for _, node := range originalOrder {
node.move(len(originalOrder))
}
}
// sum 3 vals from value zero, 1000th, 2000th, 3000th away
return getNodeXStepsAway(zeroNode, 1000, len(originalOrder)).val +
getNodeXStepsAway(zeroNode, 2000, len(originalOrder)).val +
getNodeXStepsAway(zeroNode, 3000, len(originalOrder)).val
}
func getNodeXStepsAway(node *llNode, steps int, listLength int) *llNode {
if steps < 0 {
panic("negative steps")
}
iter := node
for steps > 0 {
iter = iter.next
steps--
}
return iter
}
type llNode struct {
val int
prev, next *llNode
}
func (n *llNode) move(totalLength int) {
steps := n.val
// fmt.Println("before steps", steps, "total", totalLength)
steps %= totalLength - 1
if steps == 0 {
// fmt.Println("zero steps")
return
}
// find slot to fit into
if steps < 0 {
steps += (totalLength - 1)
}
// fmt.Println("modded steps", steps)
oldPrev, oldNext := n.prev, n.next
oldPrev.next = oldNext
oldNext.prev = oldPrev
iter := n
for steps > 0 {
// fmt.Println("steps left", steps, "iter", iter)
iter = iter.next
if iter == n {
panic("repeat")
}
steps--
}
nextPrev, nextNext := iter, iter.next
// fmt.Println("nextPrev & nextNext", nextPrev, nextNext)
nextPrev.next = n
n.prev = nextPrev
nextNext.prev = n
n.next = nextNext
}
func parseInput(input string) (zeroNode *llNode, originalOrder []*llNode) {
nums := []int{}
for _, line := range strings.Split(input, "\n") {
nums = append(nums, cast.ToInt(line))
}
var head, iter *llNode
for _, n := range nums {
node := &llNode{
val: n,
prev: iter,
}
if head == nil {
head = node
iter = node
} else {
iter.next = node
iter = iter.next
}
if iter.val == 0 {
zeroNode = iter
}
originalOrder = append(originalOrder, node)
}
head.prev = iter
iter.next = head
return zeroNode, originalOrder
}
// for debugging
func listToString(head *llNode, listLength int) string {
var sb strings.Builder
for listLength > 0 {
sb.WriteString(cast.ToString(head.val) + ",")
head = head.next
listLength--
}
return sb.String()
}
func printList(head *llNode, listLength int) {
fmt.Println(listToString(head, listLength))
}
+115
View File
@@ -0,0 +1,115 @@
package main
import (
_ "embed"
"testing"
)
var example = `1
2
-3
3
-2
0
4`
var example2 = `1
2
-3
9
-2
0
4`
var example3 = `1
2
-3
3
-8
0
4`
func Test_mixList(t *testing.T) {
tests := []struct {
name string
input string
decryptionKey, mixes int // for part 2 mostly
want int
}{
{
name: "example",
input: example,
decryptionKey: 1,
mixes: 1,
want: 3,
},
{
name: "example2",
input: example2,
decryptionKey: 1,
mixes: 1,
want: 3,
},
{
name: "example3",
input: example3,
decryptionKey: 1,
mixes: 1,
want: 3,
},
{
name: "actual",
input: input,
decryptionKey: 1,
mixes: 1,
want: 9945,
},
{
name: "example",
input: example,
decryptionKey: part2DecryptionKey,
mixes: 10,
want: 1623178306,
},
{
name: "actual",
input: input,
decryptionKey: part2DecryptionKey,
mixes: 10,
want: 3338877775442,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := mixList(tt.input, tt.decryptionKey, tt.mixes); got != tt.want {
t.Errorf("mixList() = %v, want %v", got, tt.want)
}
})
}
}
func Test_llNode_move(t *testing.T) {
zeroNode, nodeSlice := parseInput(`0
1
2
3
4
5`)
zeroNode.move(len(nodeSlice))
originalString := "0,1,2,3,4,5,"
// should be the same
if got := listToString(zeroNode, len(nodeSlice)); got != originalString {
t.Errorf("moving zero, want no change %q, got %q", originalString, got)
}
zeroNode.prev.move(len(nodeSlice))
if got := listToString(zeroNode, len(nodeSlice)); got != originalString {
t.Errorf("moving 5, want no change %q, got %q", originalString, got)
}
oneNode := zeroNode.next
oneNode.move(len(nodeSlice))
want := "0,2,1,3,4,5,"
if got := listToString(zeroNode, len(nodeSlice)); got != want {
t.Errorf("moving 1, want %q got %q", want, got)
}
}
+190
View File
@@ -0,0 +1,190 @@
package main
import (
_ "embed"
"flag"
"fmt"
"regexp"
"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 {
parsed := parseInput(input)
// what will monkey 'root' yell?
v, _ := bfs("root", parsed, map[string]int{})
return v
}
var numRegexp = regexp.MustCompile("^[0-9]+$")
func bfs(key string, raw map[string]string, solved map[string]int) (int, error) {
if v, ok := solved[key]; ok {
return v, nil
}
if numRegexp.MatchString(raw[key]) {
solved[key] = cast.ToInt(raw[key])
return solved[key], nil
}
equation := raw[key]
parts := strings.Split(equation, " ")
if len(parts) != 3 {
return 0, fmt.Errorf("expected 3 parts for %q, got %q", key, equation)
}
left, err := bfs(parts[0], raw, solved)
if err != nil {
return 0, err
}
right, err := bfs(parts[2], raw, solved)
if err != nil {
return 0, err
}
switch parts[1] {
case "+":
solved[key] = left + right
case "-":
solved[key] = left - right
case "*":
solved[key] = left * right
case "/":
solved[key] = left / right
default:
panic("error with key: " + key + " string: " + equation)
}
return solved[key], nil
}
func part2(input string) int {
raw := parseInput(input)
if len(strings.Split(raw["root"], " ")) != 3 {
panic(fmt.Sprintf("expected 3 parts to %q", raw["root"]))
}
// change humn to something that will error in bfs so we know which branch
// of the equations is fully solvable
raw["humn"] = "humn_will_error_in_bfs"
// basically making the root equation leftSymbol / rightSymbol = 1 in the
// inverted graph
invertedGraph := map[string]string{"root": "1"}
rootParts := strings.Split(raw["root"], " ")
rootParts[1] = "/"
raw["root"] = strings.Join(rootParts, " ")
keyToInvert := "root"
solvedMap := map[string]int{}
for keyToInvert != "humn" {
// find the equation, determine which side is easily solvable, and which
// is not, reverse the equation for the unsolvable variable (aka the one
// that needs to know what value humn shouts)
// end at humn
eq := raw[keyToInvert]
parts := strings.Split(eq, " ")
leftRaw, rightRaw := parts[0], parts[2]
leftVal, errLeft := bfs(leftRaw, raw, solvedMap)
if errLeft == nil {
invertedGraph[leftRaw] = cast.ToString(leftVal)
}
rightVal, errRight := bfs(rightRaw, raw, solvedMap)
if errRight == nil {
invertedGraph[rightRaw] = cast.ToString(rightVal)
}
switch parts[1] {
case "+":
if errLeft != nil {
invertedGraph[leftRaw] = fmt.Sprintf("%s - %s", keyToInvert, rightRaw)
keyToInvert = leftRaw
} else if errRight != nil {
invertedGraph[rightRaw] = fmt.Sprintf("%s - %s", keyToInvert, leftRaw)
keyToInvert = rightRaw
} else {
panic(fmt.Sprintf("both vals did not error '+' %q: %q", keyToInvert, eq))
}
case "-":
if errLeft != nil {
invertedGraph[leftRaw] = fmt.Sprintf("%s + %s", keyToInvert, rightRaw)
keyToInvert = leftRaw
} else if errRight != nil {
invertedGraph[rightRaw] = fmt.Sprintf("%s - %s", leftRaw, keyToInvert)
keyToInvert = rightRaw
} else {
panic(fmt.Sprintf("both vals did not error '-' %q: %q", keyToInvert, eq))
}
case "*":
if errLeft != nil {
invertedGraph[leftRaw] = fmt.Sprintf("%s / %s", keyToInvert, rightRaw)
keyToInvert = leftRaw
} else if errRight != nil {
invertedGraph[rightRaw] = fmt.Sprintf("%s / %s", keyToInvert, leftRaw)
keyToInvert = rightRaw
} else {
panic(fmt.Sprintf("both vals did not error '/' %q: %q", keyToInvert, eq))
}
case "/":
if errLeft != nil {
invertedGraph[leftRaw] = fmt.Sprintf("%s * %s", keyToInvert, rightRaw)
keyToInvert = leftRaw
} else if errRight != nil {
invertedGraph[rightRaw] = fmt.Sprintf("%s / %s", leftRaw, keyToInvert)
keyToInvert = rightRaw
} else {
panic(fmt.Sprintf("both vals did not error '*' %q: %q", keyToInvert, eq))
}
default:
panic(fmt.Sprintf("inverting graph: key: %q, eq: %q", keyToInvert, eq))
}
}
v, _ := bfs("humn", invertedGraph, map[string]int{})
return v
}
func parseInput(input string) map[string]string {
ans := map[string]string{}
for _, line := range strings.Split(input, "\n") {
parts := strings.Split(line, ": ")
ans[parts[0]] = parts[1]
}
return ans
}
+73
View File
@@ -0,0 +1,73 @@
package main
import (
"testing"
)
var example = `root: pppw + sjmn
dbpl: 5
cczh: sllz + lgvd
zczc: 2
ptdq: humn - dvpt
dvpt: 3
lfqf: 4
humn: 5
ljgn: 2
sjmn: drzm * dbpl
sllz: 4
pppw: cczh / lfqf
lgvd: ljgn * ptdq
drzm: hmdt - zczc
hmdt: 32`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 152,
},
{
name: "actual",
input: input,
want: 194501589693264,
},
}
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: 301,
},
{
name: "actual",
input: input,
want: 3887609741189,
},
}
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)
}
})
}
}
+464
View File
@@ -0,0 +1,464 @@
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 {
matrix, path := parseInput(input)
var row, col int
for c := 0; c < len(matrix[0]); c++ {
if matrix[0][c] == "." {
col = c
break
}
}
diffIndex := 0
diffs := [][2]int{
{0, 1}, // start facing right
{1, 0}, // turning right will point you down
{0, -1}, // left
{-1, 0}, // turning left from index 0 makes you face up
}
// walking over the edge wraps you around within the same direction...
// unless if it is a wall, then you're stuck
p := 0
for p < len(path) {
// get next direction...
indexOfLorR := p
for indexOfLorR < len(path) &&
path[indexOfLorR] != 'L' && path[indexOfLorR] != 'R' {
indexOfLorR++
}
steps := cast.ToInt(path[p:indexOfLorR])
// try to move that many steps
for s := 0; s < steps; s++ {
diff := diffs[diffIndex]
nextRow, nextCol := row+diff[0], col+diff[1]
// mod them so they wrap if necessary
nextRow += len(matrix)
nextCol += len(matrix[0])
nextRow %= len(matrix)
nextCol %= len(matrix[0])
// if it's an empty space you need to keep looping around...
for matrix[nextRow][nextCol] == " " || matrix[nextRow][nextCol] == "" {
nextRow += diff[0]
nextCol += diff[1]
// wrapping math...
nextRow += len(matrix)
nextCol += len(matrix[0])
nextRow %= len(matrix)
nextCol %= len(matrix[0])
}
// wall: break
if matrix[nextRow][nextCol] == "#" {
break
}
row = nextRow
col = nextCol
}
if indexOfLorR == len(path) {
break
}
// handle turn if indexOfLorR is still in bounds
switch path[indexOfLorR] {
case 'L':
diffIndex--
case 'R':
diffIndex++
}
diffIndex += 4
diffIndex %= 4
p = indexOfLorR + 1
}
// final row, col, facing
// row & col are 1 indexed
// facing is indexed same as diffs slice
// 1000 * row + 4 * col + facing_index
return 1000*(row+1) + 4*(col+1) + diffIndex
}
func parseInput(input string) ([][]string, string) {
parts := strings.Split(input, "\n\n")
matrix := [][]string{}
topRowLen := len(strings.Split(parts[0], "\n")[0])
for _, line := range strings.Split(parts[0], "\n") {
matrix = append(matrix, make([]string, topRowLen))
split := strings.Split(line, "")
copy(matrix[len(matrix)-1], split)
}
return matrix, parts[1]
}
func part2(input string) int {
matrix, path := parseInput(input)
var row, col int
for c := 0; c < len(matrix[0]); c++ {
if matrix[0][c] == "." {
col = c
break
}
}
diffIndex := 0
diffs := [][2]int{
{0, 1}, // start facing right
{1, 0}, // turning right will point you DOWN
{0, -1}, // left
{-1, 0}, // turning left from index 0 makes you face up
}
// shape of example
// #
// ###
// ##
//
//
// shape of my input
// ##
// #
// ##
// #
// a lot of (hah) edge case handling to determine where to "teleport" to
// pen and paper math... might not be worth doing the example input
// because i'm going to make a literal calculation for my input shape...
p := 0
for p < len(path) {
// get next direction...
indexOfLorR := p
for indexOfLorR < len(path) &&
path[indexOfLorR] != 'L' && path[indexOfLorR] != 'R' {
indexOfLorR++
}
steps := cast.ToInt(path[p:indexOfLorR])
// try to move that many steps
for s := 0; s < steps; s++ {
diff := diffs[diffIndex]
nextRow, nextCol := row+diff[0], col+diff[1]
// DO NOT UPDATE diffIndex here because if it's a wall we DON'T want
// to change directions
nextRow, nextCol, nextDiffIndex := handleWrap(row, col, nextRow, nextCol, diffIndex)
// we'll never see empty spaces now because we're handling wrapping above
// wall: break
if matrix[nextRow][nextCol] == "#" {
break
}
// only update if we didn't hit a wall
row = nextRow
col = nextCol
diffIndex = nextDiffIndex
}
if indexOfLorR == len(path) {
break
}
// handle turn if indexOfLorR is still in bounds
switch path[indexOfLorR] {
case 'L':
diffIndex--
case 'R':
diffIndex++
}
diffIndex += 4
diffIndex %= 4
p = indexOfLorR + 1
}
// final answer calculated from flattened map coords
// 1000 * row + 4 * col + facing_index
// too low: 111043
return 1000*(row+1) + 4*(col+1) + diffIndex
}
// handles edge cases ;)
// how i'll number my boxes...
// 21
// 3
// 54
// 6
const (
RightIndex = 0
DownIndex = 1
LeftIndex = 2
UpIndex = 3
)
// handleWrap checks if the movement from r,c to nextRow, nextCol is off the
// edge of the matrix, if so it does the maths and direction change to wrap
// around the edge of the cube, this is very manual and based upon a drawing i
// made of my input (i'll upload it when i remember...)
//
// got a little carried away with assertions in here trying to debug...
func handleWrap(r, c, nextRow, nextCol, diffIndex int) (newRow, newCol, newDiffIndex int) {
// there will be roughly 14 checks in here... this is gonna get ugly, esp
// b/c i'm too lazy to dry this up
// 2 -> 5 conversion
if getBoxNumber(r, c) == 2 &&
0 <= nextRow && nextRow < 50 && nextCol == 49 {
if diffIndex != LeftIndex {
panic(fmt.Sprintf("expected LeftIndex, got %d", diffIndex))
}
newCol = 0
newRow = 149 - nextRow
if getBoxNumber(newRow, newCol) != 5 {
panic(fmt.Sprintf("expected to move to box 5, got %d", getBoxNumber(newRow, newCol)))
}
return newRow, newCol, RightIndex
}
// 5 -> 2
if getBoxNumber(r, c) == 5 &&
nextCol == -1 && 100 <= nextRow && nextRow < 150 {
if diffIndex != LeftIndex {
panic(fmt.Sprintf("expected LeftIndex got %d", diffIndex))
}
newCol = 50
newRow = 149 - nextRow
if getBoxNumber(newRow, newCol) != 2 {
panic(fmt.Sprintf("expected to move to box 2, got %d", getBoxNumber(newRow, newCol)))
}
return newRow, newCol, RightIndex
}
// 3 -> 5
if getBoxNumber(r, c) == 3 &&
nextCol == 49 && 50 <= nextRow && nextRow < 100 {
if diffIndex != LeftIndex {
panic(fmt.Sprintf("expected LeftIndex, got %d", diffIndex))
}
newRow = 100
newCol = nextRow - 50
if getBoxNumber(newRow, newCol) != 5 {
panic(fmt.Sprintf("expected to move to box 5, got %d", getBoxNumber(newRow, newCol)))
}
return newRow, newCol, DownIndex
}
// 5 -> 3
if getBoxNumber(r, c) == 5 &&
nextRow == 99 && 0 <= nextCol && nextCol < 50 {
if diffIndex != UpIndex {
panic(fmt.Sprintf("expected UpIndex, got %d", diffIndex))
}
newRow = nextCol + 50
newCol = 50
if getBoxNumber(newRow, newCol) != 3 {
panic(fmt.Sprintf("expected to move to box 3, got %d", getBoxNumber(newRow, newCol)))
}
return newRow, newCol, RightIndex
}
// 2 -> 6
if getBoxNumber(r, c) == 2 &&
nextRow == -1 && 50 <= nextCol && nextCol < 100 {
if diffIndex != UpIndex {
panic(fmt.Sprintf("expected UpIndex, got %d", diffIndex))
}
newRow = nextCol + 100
newCol = 0
if getBoxNumber(newRow, newCol) != 6 {
panic(fmt.Sprintf("expected to move to box 6, got %d", getBoxNumber(newRow, newCol)))
}
return newRow, newCol, RightIndex
}
// 6 -> 2
if getBoxNumber(r, c) == 6 &&
nextCol == -1 && 150 <= nextRow && nextRow < 200 {
if diffIndex != LeftIndex {
panic(fmt.Sprintf("expected LeftIndex, got %d", diffIndex))
}
newRow = 0
newCol = nextRow - 100
if getBoxNumber(newRow, newCol) != 2 {
panic(fmt.Sprintf("expected to move to box 2, got %d", getBoxNumber(newRow, newCol)))
}
return newRow, newCol, DownIndex
}
// 1 -> 6
if getBoxNumber(r, c) == 1 &&
nextRow == -1 && 100 <= nextCol && nextCol < 150 {
if diffIndex != UpIndex {
panic(fmt.Sprintf("expected UpIndex, got %d", diffIndex))
}
newRow = 199
newCol = nextCol - 100
if getBoxNumber(newRow, newCol) != 6 {
panic(fmt.Sprintf("expected to move to box 6, got %d", getBoxNumber(newRow, newCol)))
}
return newRow, newCol, UpIndex
}
// 6 -> 1
if getBoxNumber(r, c) == 6 &&
nextRow == 200 && 0 <= nextCol && nextCol < 50 {
if diffIndex != DownIndex {
panic(fmt.Sprintf("expected DownIndex, got %d", diffIndex))
}
newRow = 0
newCol = nextCol + 100
if getBoxNumber(newRow, newCol) != 1 {
panic(fmt.Sprintf("expected to move to box 1, got %d", getBoxNumber(newRow, newCol)))
}
return newRow, newCol, DownIndex
}
// 4 -> 6
if getBoxNumber(r, c) == 4 &&
nextRow == 150 && 50 <= nextCol && nextCol < 100 {
if diffIndex != DownIndex {
panic(fmt.Sprintf("expected DownIndex, got %d", diffIndex))
}
newRow = nextCol + 100
newCol = 49
if getBoxNumber(newRow, newCol) != 6 {
panic(fmt.Sprintf("expected to move to box 6, got %d", getBoxNumber(newRow, newCol)))
}
return newRow, newCol, LeftIndex
}
// 6 -> 4
if getBoxNumber(r, c) == 6 &&
nextCol == 50 && 150 <= nextRow && nextRow < 200 {
if diffIndex != RightIndex {
panic(fmt.Sprintf("expected RightIndex, got %d", diffIndex))
}
newRow = 149
newCol = nextRow - 100
if getBoxNumber(newRow, newCol) != 4 {
panic(fmt.Sprintf("expected to move to box 4, got %d", getBoxNumber(newRow, newCol)))
}
return newRow, newCol, UpIndex
}
// 4 -> 1
if getBoxNumber(r, c) == 4 &&
nextCol == 100 && 100 <= nextRow && nextRow < 150 {
if diffIndex != RightIndex {
panic(fmt.Sprintf("expected RightIndex, got %d", diffIndex))
}
newRow = 149 - nextRow
newCol = 149
if getBoxNumber(newRow, newCol) != 1 {
panic(fmt.Sprintf("expected to move to box 1, got %d", getBoxNumber(newRow, newCol)))
}
return newRow, newCol, LeftIndex
}
// 1 -> 4
if getBoxNumber(r, c) == 1 &&
nextCol == 150 && 0 <= nextRow && nextRow < 50 {
if diffIndex != RightIndex {
panic(fmt.Sprintf("expected RightIndex, got %d", diffIndex))
}
newRow = 149 - nextRow
newCol = 99
if getBoxNumber(newRow, newCol) != 4 {
panic(fmt.Sprintf("expected to move to box 4, got %d", getBoxNumber(newRow, newCol)))
}
return newRow, newCol, LeftIndex
}
// 3 -> 1
if getBoxNumber(r, c) == 3 &&
nextCol == 100 && 50 <= nextRow && nextRow < 100 {
if diffIndex != RightIndex {
panic(fmt.Sprintf("expected RightIndex, got %d", diffIndex))
}
newRow = 49
newCol = nextRow + 50
if getBoxNumber(newRow, newCol) != 1 {
panic(fmt.Sprintf("expected to move to box 1, got %d", getBoxNumber(newRow, newCol)))
}
return newRow, newCol, UpIndex
}
// 1 -> 3
if getBoxNumber(r, c) == 1 &&
nextRow == 50 && 100 <= nextCol && nextCol < 150 {
if diffIndex != DownIndex {
panic(fmt.Sprintf("expected DownIndex, got %d", diffIndex))
}
newRow = nextCol - 50
newCol = 99
if getBoxNumber(newRow, newCol) != 3 {
panic(fmt.Sprintf("expected to move to box 3, got %d", getBoxNumber(newRow, newCol)))
}
return newRow, newCol, LeftIndex
}
// no edge conversion required, just pass through
return nextRow, nextCol, diffIndex
}
func getBoxNumber(r, c int) int {
if 0 <= r && r < 50 && 100 <= c && c < 150 {
return 1
}
if 0 <= r && r < 50 && 50 <= c && c < 100 {
return 2
}
if 50 <= r && r < 100 && 50 <= c && c < 100 {
return 3
}
if 100 <= r && r < 150 && 50 <= c && c < 100 {
return 4
}
if 100 <= r && r < 150 && 0 <= c && c < 50 {
return 5
}
if 150 <= r && r < 200 && 0 <= c && c < 50 {
return 6
}
panic(fmt.Sprintf("bad row %d and col %d", r, c))
}
+75
View File
@@ -0,0 +1,75 @@
package main
import (
"testing"
)
var example = ` ...#
.#..
#...
....
...#.......#
........#...
..#....#....
..........#.
...#....
.....#..
.#......
......#.
10R5L5R10L4R5L5`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 6032,
},
{
name: "actual",
input: input,
want: 144244,
},
}
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
}{
// NOTE: did not account for the example case because I manually coded
// for the shape of my exact input... which differed from the
// input shape
// {
// name: "example",
// input: example,
// want: 5031,
// },
{
name: "actual",
input: input,
want: 138131,
},
}
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)
}
})
}
}
+207
View File
@@ -0,0 +1,207 @@
package main
import (
_ "embed"
"flag"
"fmt"
"math"
"sort"
"strings"
"github.com/alexchao26/advent-of-code-go/mathy"
"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)
ans := unstableDiffusion(input, part)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
}
var diffsSlice = [][][2]int{
// N
{
{-1, -1},
{-1, 0},
{-1, 1},
},
// S
{
{1, -1},
{1, 0},
{1, 1},
},
// W
{
{-1, -1},
{0, -1},
{1, -1},
},
// E
{
{-1, 1},
{0, 1},
{1, 1},
},
}
var targetDiff = [][2]int{
{-1, 0}, // N
{1, 0}, // S
{0, -1}, // W
{0, 1}, // E
}
func unstableDiffusion(input string, part int) int {
elfCoords := parseInput(input)
diffStartIndex := 0
// part 2
var lastState string
round := 1
for {
if part == 1 && round == 11 {
break
}
elfPlannedMoves := [][][2]int{}
elvesTargetingCoord := map[[2]int]int{}
for coords, val := range elfCoords {
if val == "#" {
nonZeroNeighbors := 0
for _, diffSlice := range diffsSlice {
for _, d := range diffSlice {
if elfCoords[[2]int{
coords[0] + d[0],
coords[1] + d[1],
}] == "#" {
nonZeroNeighbors++
}
}
}
if nonZeroNeighbors == 0 {
elfPlannedMoves = append(elfPlannedMoves, [][2]int{
coords, coords,
})
elvesTargetingCoord[coords]++
} else {
foundAMove := false
for i := 0; i < 4; i++ {
diffSliceIndex := i + diffStartIndex
diffSlice := diffsSlice[diffSliceIndex%4]
neighbors := 0
for _, d := range diffSlice {
if elfCoords[[2]int{
coords[0] + d[0],
coords[1] + d[1],
}] == "#" {
neighbors++
}
}
if neighbors == 0 {
nextCoords := coords
nextCoords[0] += targetDiff[diffSliceIndex%4][0]
nextCoords[1] += targetDiff[diffSliceIndex%4][1]
elfPlannedMoves = append(elfPlannedMoves, [][2]int{
coords, nextCoords,
})
elvesTargetingCoord[nextCoords]++
foundAMove = true
break
}
}
if !foundAMove {
elfPlannedMoves = append(elfPlannedMoves, [][2]int{
coords, coords,
})
elvesTargetingCoord[coords]++
}
}
}
}
// reset coords, but only if elves are not blocked...
elfCoords = map[[2]int]string{}
for _, plannedMove := range elfPlannedMoves {
if elvesTargetingCoord[plannedMove[1]] > 1 {
// stay
elfCoords[plannedMove[0]] = "#"
} else {
// move
elfCoords[plannedMove[1]] = "#"
}
}
// rotate directions that are checked
diffStartIndex++
if part == 2 { // hash the state
allCoords := []string{}
for c := range elfCoords {
allCoords = append(allCoords, fmt.Sprint(c))
}
sort.Strings(allCoords)
thisState := fmt.Sprint(allCoords)
if lastState == thisState {
return round
}
lastState = thisState
}
round++
}
lowRow, highRow, lowCol, highCol := math.MaxInt16, math.MinInt16, math.MaxInt16, math.MinInt16
for coords := range elfCoords {
lowRow = mathy.MinInt(lowRow, coords[0])
highRow = mathy.MaxInt(highRow, coords[0])
lowCol = mathy.MinInt(lowCol, coords[1])
highCol = mathy.MaxInt(highCol, coords[1])
}
ans := 0
for r := lowRow; r <= highRow; r++ {
for c := lowCol; c <= highCol; c++ {
if elfCoords[[2]int{r, c}] != "#" {
ans++
}
}
}
return ans
}
func parseInput(input string) map[[2]int]string {
ans := map[[2]int]string{}
for r, line := range strings.Split(input, "\n") {
for c, v := range strings.Split(line, "") {
if v == "#" {
ans[[2]int{r, c}] = "#"
}
}
}
return ans
}
+67
View File
@@ -0,0 +1,67 @@
package main
import (
"testing"
)
var example = `....#..
..###.#
#...#.#
.#...##
#.###..
##.#.##
.#..#..`
func Test_unstableDiffusion(t *testing.T) {
tests := []struct {
name string
input string
part int
want int
}{
{
name: "small example",
input: `.....
..##.
..#..
.....
..##.
.....`,
part: 1,
want: 30 - 5,
},
{
name: "example",
input: example,
part: 1,
want: 110,
},
{
name: "actual",
input: input,
part: 1,
want: 4116,
},
//
{
name: "example",
input: example,
part: 2,
want: 20,
},
{
name: "actual",
input: input,
part: 2,
want: 984,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := unstableDiffusion(tt.input, tt.part); got != tt.want {
t.Errorf("part1() = %v, want %v", got, tt.want)
}
})
}
}
+286
View File
@@ -0,0 +1,286 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"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 := blizzardJourney(input, 1)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
} else {
ans := blizzardJourney(input, 2)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
}
}
func blizzardJourney(input string, part int) int {
start, end, blizzards, totalRows, totalCols := parseInput(input)
// some form of BFS seems to be the obvious answer for "least steps"
//
// if maps are used for all blizzard coords... then the map will have to be
// constantly copied for new states
// want something... less involved
//
// could represent all blizzards fairly easily via maths
// < and > stay in same row, col = (starting +/- time) % (# of cols)
// then it's just math for "is there a blizzard here" but that does require
// iterating through every blizzard, but that's a known amount so maybe that's fine... aka close enough to constant
//
// or just calculate state at time t and store that matrix of "occupied" spaces, and then no need to recalculate
// per blizzard per time
stepsForFirstLeg := bfs(blizzards, start, end, totalRows, totalCols, 0)
if part == 1 {
return stepsForFirstLeg
}
stepsBackToStart := bfs(blizzards, end, start, totalRows, totalCols, stepsForFirstLeg)
return bfs(blizzards, start, end, totalRows, totalCols, stepsBackToStart)
}
func bfs(blizzards []blizzard, start, end [2]int, totalRows, totalCols, stepsElapsedAlready int) int {
cacheRoomStates := map[int][][]string{}
type node struct {
coords [2]int
steps int
debugPath string
}
queue := []node{}
queue = append(queue, node{
coords: start,
steps: stepsElapsedAlready,
debugPath: fmt.Sprint(0, start),
})
seenCoordsSteps := map[[3]int]bool{}
for len(queue) > 0 {
popped := queue[0]
queue = queue[1:]
roomState := calcOrGetRoomState(blizzards, popped.steps+1, totalRows, totalCols, cacheRoomStates)
for _, diff := range [][2]int{
{1, 0},
{0, 1},
{0, -1},
{-1, 0},
} {
nextCoords := [2]int{
popped.coords[0] + diff[0],
popped.coords[1] + diff[1],
}
if nextCoords == start {
continue
}
if nextCoords != start && nextCoords != end {
if nextCoords[0] < 0 || nextCoords[0] >= totalRows ||
nextCoords[1] < 0 || nextCoords[1] >= totalCols {
continue
}
}
// no point in processing a coordinate & steps pair that has already been seen
hash := [3]int{nextCoords[0], nextCoords[1], popped.steps + 1}
if seenCoordsSteps[hash] {
continue
}
seenCoordsSteps[hash] = true
// because of how i indexed the room, need to do literal checks to see if we're in start
// or end coords
// if blocked, continue
if nextCoords != start && nextCoords != end &&
roomState[nextCoords[0]][nextCoords[1]] != "." {
continue
}
// if out of bounds, continue
if nextCoords != start && nextCoords != end {
if nextCoords[0] < 0 || nextCoords[0] >= totalRows ||
nextCoords[1] < 0 || nextCoords[1] >= totalCols {
continue
}
}
// done
if nextCoords == end {
return popped.steps + 1
}
queue = append(queue, node{
coords: nextCoords,
steps: popped.steps + 1,
debugPath: popped.debugPath + fmt.Sprint(popped.steps+1, nextCoords),
})
}
// if possible to stay still, add "wait" move
if popped.coords == start ||
roomState[popped.coords[0]][popped.coords[1]] == "." {
queue = append(queue, node{
coords: popped.coords,
steps: popped.steps + 1,
debugPath: popped.debugPath + fmt.Sprint(popped.steps+1, popped.coords),
})
}
}
panic("should return from loop")
}
type blizzard struct {
startRow, startCol int
rowSlope, colSlope int
totalRows, totalCols int
char string
}
func (b blizzard) calculateCoords(steps int) [2]int {
row := (b.startRow + b.rowSlope*steps) % b.totalRows
col := (b.startCol + b.colSlope*steps) % b.totalCols
row += b.totalRows
col += b.totalCols
row %= b.totalRows
col %= b.totalCols
return [2]int{
row, col,
}
}
// occupied coordinates are easy to calculate based on each blizzard's movement
// and steps/time elapsed, return a matrix that represents occupied cells
// and store the result in a map to reduce future calcs
func calcOrGetRoomState(blizzards []blizzard, steps, totalRows, totalCols int, memo map[int][][]string) [][]string {
if m, ok := memo[steps]; ok {
return m
}
matrix := make([][]string, totalRows)
for r := range matrix {
matrix[r] = make([]string, totalCols)
}
for _, b := range blizzards {
coords := b.calculateCoords(steps)
matrix[coords[0]][coords[1]] = b.char
}
for r := 0; r < len(matrix); r++ {
for c := 0; c < len(matrix[0]); c++ {
if matrix[r][c] == "" {
matrix[r][c] = "."
}
}
}
memo[steps] = matrix
return matrix
}
func parseInput(input string) ([2]int, [2]int, []blizzard, int, int) {
var start, end [2]int
blizzards := []blizzard{}
lines := strings.Split(input, "\n")
for c := 0; c < len(lines); c++ {
if lines[0][c] == '.' {
start = [2]int{-1, c - 1}
break
}
}
// 0,0 will be within the BOX we start in
// start and end will be off the bounds of that box
totalRows := len(lines) - 2
totalCols := len(lines[0]) - 2
for c := 0; c < len(lines[0]); c++ {
if lines[len(lines)-1][c] == '.' {
end = [2]int{totalRows, c - 1}
break
}
}
for l := 1; l < len(lines)-1; l++ {
chars := strings.Split(lines[l], "")
for c := 1; c < len(chars)-1; c++ {
switch chars[c] {
case ">":
blizzards = append(blizzards, blizzard{
startRow: l - 1,
startCol: c - 1,
rowSlope: 0,
colSlope: 1,
totalRows: totalRows,
totalCols: totalCols,
char: ">",
})
case "<":
blizzards = append(blizzards, blizzard{
startRow: l - 1,
startCol: c - 1,
rowSlope: 0,
colSlope: -1,
totalRows: totalRows,
totalCols: totalCols,
char: "<",
})
case "^":
blizzards = append(blizzards, blizzard{
startRow: l - 1,
startCol: c - 1,
rowSlope: -1,
colSlope: 0,
totalRows: totalRows,
totalCols: totalCols,
char: "^",
})
case "v":
blizzards = append(blizzards, blizzard{
startRow: l - 1,
startCol: c - 1,
rowSlope: 1,
colSlope: 0,
totalRows: totalRows,
totalCols: totalCols,
char: "v",
})
case ".", "#":
default:
panic("unhandled char")
}
}
}
return start, end, blizzards, totalRows, totalCols
}
+53
View File
@@ -0,0 +1,53 @@
package main
import (
"testing"
)
var example = `#.######
#>>.<^<#
#.<..<<#
#>v.><>#
#<^v^^>#
######.#`
func Test_blizzardJourney(t *testing.T) {
tests := []struct {
name string
input string
part int
want int
}{
{
name: "example",
input: example,
part: 1,
want: 18,
},
{
name: "actual",
input: input,
part: 1,
want: 240,
},
{
name: "example",
input: example,
part: 2,
want: 54,
},
{
name: "actual",
input: input,
part: 2,
want: 717,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := blizzardJourney(tt.input, tt.part); got != tt.want {
t.Errorf("part1() = %v, want %v", got, tt.want)
}
})
}
}
+117
View File
@@ -0,0 +1,117 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"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) string {
snafuNums := strings.Split(input, "\n")
// sum of the fuel requirements
// power of 5...
// 2 is 2
// 1 is 1
// 0 is 0
// -1 is -
// -2 is =
sum := ""
for _, n := range snafuNums {
sum = addSnafu(sum, n)
}
return sum
}
func addSnafu(one, two string) string {
// reversed...
split1, split2 := strings.Split(one, ""), strings.Split(two, "")
var reversed1, reversed2 []string
for i := len(split1) - 1; i >= 0; i-- {
reversed1 = append(reversed1, split1[i])
}
for i := len(split2) - 1; i >= 0; i-- {
reversed2 = append(reversed2, split2[i])
}
longer, shorter := reversed1, reversed2
if len(longer) < len(shorter) {
longer, shorter = shorter, longer
}
charToVal := map[string]int{
"=": -2,
"-": -1,
"0": 0,
"1": 1,
"2": 2,
}
valToChar := map[int]string{
-2: "=",
-1: "-",
0: "0",
1: "1",
2: "2",
}
ans := make([]int, len(longer)+1)
for i := 0; i < len(longer); i++ {
sum := charToVal[longer[i]]
if i < len(shorter) {
sum += charToVal[shorter[i]]
}
ans[i] += sum
if ans[i] > 2 {
ans[i] -= 5
ans[i+1]++
} else if ans[i] < -2 {
ans[i] += 5
ans[i+1]--
}
}
for ans[len(ans)-1] == 0 {
ans = ans[:len(ans)-1]
}
snafu := ""
for _, a := range ans {
snafu = valToChar[a] + snafu
}
return snafu
}
func part2(input string) string {
return ":)"
}
+67
View File
@@ -0,0 +1,67 @@
package main
import (
_ "embed"
"testing"
)
var example = `1=-0-2
12111
2=0=
21
2=01
111
20012
112
1=-1=
1-12
12
1=
122`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "example",
input: example,
want: "2=-1=0",
},
{
name: "actual",
input: input,
want: "2----0=--1122=0=0021",
},
}
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 string
}{
{
name: "example",
input: example,
want: ":)",
},
}
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)
}
})
}
}
+110
View File
@@ -0,0 +1,110 @@
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 {
var sum int
for _, line := range strings.Split(input, "\n") {
var tens, ones int
for i := 0; i < len(line); i++ {
if strings.ContainsAny(line[i:i+1], "0123456789") {
tens = cast.ToInt(line[i : i+1])
break
}
}
for i := len(line) - 1; i >= 0; i-- {
if strings.ContainsAny(line[i:i+1], "0123456789") {
ones = cast.ToInt(line[i : i+1])
break
}
}
sum += tens*10 + ones
}
return sum
}
func part2(input string) int {
prefixes := map[string]int{
"one": 1,
"two": 2,
"three": 3,
"four": 4,
"five": 5,
"six": 6,
"seven": 7,
"eight": 8,
"nine": 9,
"zero": 0,
}
for i := 0; i <= 9; i++ {
prefixes[cast.ToString(i)] = i
}
var sum int
for _, line := range strings.Split(input, "\n") {
var first, last int
for len(line) > 0 {
for prefix, val := range prefixes {
if doesStringHavePrefix(line, prefix) {
if first == 0 {
first = val
}
last = val
break
}
}
// shorten line
line = line[1:]
}
sum += first*10 + last
}
return sum
}
func doesStringHavePrefix(str string, prefix string) bool {
if len(str) < len(prefix) {
return false
}
return str[:len(prefix)] == prefix
}
+70
View File
@@ -0,0 +1,70 @@
package main
import (
"testing"
)
var example = `1abc2
pqr3stu8vwx
a1b2c3d4e5f
treb7uchet`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 142,
},
{
name: "actual",
input: input,
want: 55488,
},
}
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)
}
})
}
}
var example2 = `two1nine
eightwothree
abcone2threexyz
xtwone3four
4nineeightseven2
zoneight234
7pqrstsixteen`
func Test_part2(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example2,
want: 281,
},
{
name: "actual",
input: input,
want: 55614,
},
}
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)
}
})
}
}
+112
View File
@@ -0,0 +1,112 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"github.com/alexchao26/advent-of-code-go/cast"
"github.com/alexchao26/advent-of-code-go/mathy"
"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 {
games := parseInput(input)
var possibleIDSum int
for _, g := range games {
isPossible := true
for _, step := range g.steps {
// only 12 red cubes, 13 green cubes, and 14 blue cubes
if step["red"] > 12 || step["green"] > 13 || step["blue"] > 14 {
isPossible = false
break
}
}
if isPossible {
possibleIDSum += g.id
}
}
return possibleIDSum
}
func part2(input string) int {
games := parseInput(input)
var sum int
for _, g := range games {
lowestPossibleCount := map[string]int{}
for _, step := range g.steps {
lowestPossibleCount["red"] = mathy.MaxInt(lowestPossibleCount["red"], step["red"])
lowestPossibleCount["green"] = mathy.MaxInt(lowestPossibleCount["green"], step["green"])
lowestPossibleCount["blue"] = mathy.MaxInt(lowestPossibleCount["blue"], step["blue"])
}
sum += lowestPossibleCount["red"] * lowestPossibleCount["blue"] * lowestPossibleCount["green"]
}
return sum
}
type game struct {
id int
steps []map[string]int
}
func parseInput(input string) (ans []game) {
for i, line := range strings.Split(input, "\n") {
parts := strings.Split(line, ": ")
g := game{
id: i + 1,
}
for _, p := range strings.Split(parts[1], "; ") {
step := map[string]int{}
for _, group := range strings.Split(p, ", ") {
numberColor := strings.Split(group, " ")
if len(numberColor) != 2 {
panic(fmt.Sprintf("group not in two pieces %q", group))
}
step[numberColor[1]] = cast.ToInt(numberColor[0])
}
g.steps = append(g.steps, step)
}
ans = append(ans, g)
}
return ans
}
+63
View File
@@ -0,0 +1,63 @@
package main
import (
"testing"
)
var example = `Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 8,
},
{
name: "actual",
input: input,
want: 2632,
},
}
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: 2286,
},
// {
// name: "actual",
// input: input,
// want: 0,
// },
}
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)
}
})
}
}
+198
View File
@@ -0,0 +1,198 @@
package main
import (
_ "embed"
"flag"
"fmt"
"regexp"
"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)
}
}
var numReg = regexp.MustCompile("[0-9]")
func part1(input string) int {
matrix := [][]string{}
// collect special characters
specialChars := map[string]bool{}
for _, row := range strings.Split(input, "\n") {
matrix = append(matrix, strings.Split(row, ""))
for _, val := range strings.Split(row, "") {
if !numReg.MatchString(val) && val != "." {
specialChars[val] = true
}
}
}
specialCharsString := ""
for k := range specialChars {
specialCharsString += k
}
var sum int
diffs := [8][2]int{
{-1, -1},
{-1, 0},
{-1, 1},
{0, -1},
{0, 1},
{1, -1},
{1, 0},
{1, 1},
}
seen := map[[2]int]bool{}
for r, row := range matrix {
for c, val := range row {
coords := [2]int{r, c}
if seen[coords] {
continue
}
seen[coords] = true
// if we hit a number, collect the entire number and check along the way if it's
// adjacent to a special char
if numReg.MatchString(val) {
hasAdjacentSpecialChar := false
numStr := ""
for j := 0; j+c < len(matrix[0]); j++ {
char := row[c+j]
// breaks on period or special character, loop itself breaks on out of range
if !numReg.MatchString(char) {
break
}
// keep collecting number
numStr += char
// check all 8 directions for special char
for _, d := range diffs {
dr, dc := r+d[0], c+j+d[1]
if dr >= 0 && dr < len(matrix) && dc >= 0 && dc < len(matrix[0]) {
if strings.ContainsAny(matrix[dr][dc], specialCharsString) {
hasAdjacentSpecialChar = true
}
seen[[2]int{r, c + j}] = true
}
}
}
if hasAdjacentSpecialChar {
sum += cast.ToInt(numStr)
}
}
}
}
return sum
}
// getNumber returns -1 if a number is "not found" which could include the number
// already being seen
func getNumber(matrix [][]string, coord [2]int, seen map[[2]int]bool) int {
if !numReg.MatchString(matrix[coord[0]][coord[1]]) {
return -1
}
if seen[coord] {
return -1
}
// go to the left most digit
r, c := coord[0], coord[1]
for c-1 >= 0 {
if numReg.MatchString(matrix[r][c-1]) {
c--
} else {
break
}
}
numStr := ""
for c < len(matrix[0]) && numReg.MatchString(matrix[r][c]) {
numStr += matrix[r][c]
seen[[2]int{r, c}] = true
c++
}
return cast.ToInt(numStr)
}
func part2(input string) int {
// lucky edge case that there are not multiple gears that share the same number such as
// ....*....
// .123.456. OR ...123*456*789...
// ....*....
// with the current useage of "seen", this would only get counted once
seen := map[[2]int]bool{}
matrix := [][]string{}
for _, row := range strings.Split(input, "\n") {
matrix = append(matrix, strings.Split(row, ""))
}
diffs := [8][2]int{
{-1, -1},
{-1, 0},
{-1, 1},
{0, -1},
{0, 1},
{1, -1},
{1, 0},
{1, 1},
}
sum := 0
for r, rows := range matrix {
for c, val := range rows {
if val == "*" {
nums := []int{}
for _, diff := range diffs {
dr, dc := r+diff[0], c+diff[1]
if dr >= 0 && dr < len(matrix) && dc >= 0 && dc < len(matrix[0]) {
foundNum := getNumber(matrix, [2]int{dr, dc}, seen)
if foundNum != -1 {
nums = append(nums, foundNum)
}
}
}
if len(nums) == 2 {
sum += nums[0] * nums[1]
}
}
}
}
return sum
}
+68
View File
@@ -0,0 +1,68 @@
package main
import (
"testing"
)
var example = `467..114..
...*......
..35..633.
......#...
617*......
.....+.58.
..592.....
......755.
...$.*....
.664.598..`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 4361,
},
{
name: "actual",
input: input,
want: 536576,
},
}
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: 467835,
},
// {
// name: "actual",
// input: input,
// want: 0,
// },
}
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)
}
})
}
}
+138
View File
@@ -0,0 +1,138 @@
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 {
parsed := parseInput(input)
var ans int
for _, card := range parsed {
ans += scoreCard(card)
}
return ans
}
func scoreCard(c card) int {
var ans int
for _, num := range c.hand {
if c.winning[num] {
if ans == 0 {
ans = 1
} else {
ans *= 2
}
}
}
return ans
}
func part2(input string) int {
cards := parseInput(input)
// tracks the number of cards won, starts with 1 of each card
numCards := make([]int, len(cards))
for i := range numCards {
numCards[i] = 1
}
for index, c := range cards {
cardsWon := countWinningNumbers(c)
for i := 1; i <= cardsWon; i++ {
// add number of current card to account for previous wins
numCards[index+i] += numCards[index]
}
}
// add up total number of cards
var cardCount int
for _, n := range numCards {
cardCount += n
}
return cardCount
}
func countWinningNumbers(c card) int {
var ans int
for _, num := range c.hand {
if c.winning[num] {
ans++
}
}
return ans
}
type card struct {
// index int // unused
winning map[int]bool
hand []int
}
func parseInput(input string) (ans []card) {
for _, line := range strings.Split(input, "\n") {
c := card{
// index: len(ans) + 1,
winning: map[int]bool{},
}
half := strings.Split(line, ": ")
numParts := strings.Split(half[1], " | ")
for _, winningNum := range strings.Split(numParts[0], " ") {
// handles single digits that have an extra empty string between nums
if winningNum == "" {
continue
}
c.winning[cast.ToInt(winningNum)] = true
}
for _, handNum := range strings.Split(numParts[1], " ") {
if handNum == "" {
continue
}
c.hand = append(c.hand, cast.ToInt(handNum))
}
ans = append(ans, c)
}
return ans
}
+64
View File
@@ -0,0 +1,64 @@
package main
import (
"testing"
)
var example = `Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1
Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 13,
},
{
name: "actual",
input: input,
want: 19135,
},
}
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: 30,
},
{
name: "actual",
input: input,
want: 5704953,
},
}
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)
}
})
}
}
+196
View File
@@ -0,0 +1,196 @@
package main
import (
_ "embed"
"flag"
"fmt"
"math"
"strings"
"github.com/alexchao26/advent-of-code-go/cast"
"github.com/alexchao26/advent-of-code-go/mathy"
"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 {
seeds, listOfMappingRanges := parseInput(input)
lowestFinalNum := math.MaxInt64
for _, seed := range seeds {
finalMapping := getFinalMapping(seed, listOfMappingRanges)
lowestFinalNum = mathy.MinInt(lowestFinalNum, finalMapping)
}
return lowestFinalNum
}
func getFinalMapping(seed int, listOfMappingRanges [][]mappingRange) int {
currentVal := seed
// jeez these are horrible variable names...
for _, mappingRanges := range listOfMappingRanges {
// if currentVal is between the sourceStart and sourceStart plus count, a mappingRange is found
// otherwise it maps to the same value and move on
// [sourceStart, sourceStart + count) <- not inclusive of sourceStart + count
for _, m := range mappingRanges {
if m.sourceStart <= currentVal && currentVal < m.sourceStart+m.count {
currentVal = m.destinationStart + (currentVal - m.sourceStart)
// then break so number is only mapped once per round of mappings
break
}
}
}
return currentVal
}
type mappingRange struct {
// seed to soil map: 50 98 2
// means 2 mappings total: seed 98 maps to water 50 and seed 99 maps to soil 51
// if a mappingRange does not exist, then it maps to the same number
sourceStart, destinationStart, count int
}
func parseInput(input string) (seeds []int, listOfMappingRanges [][]mappingRange) {
parts := strings.Split(input, "\n\n")
seedParts := strings.Split(parts[0], " ")
// skip first part which is "seeds: "
for i := 1; i < len(seedParts); i++ {
seeds = append(seeds, cast.ToInt(seedParts[i]))
}
// parse mappings
for p := 1; p < len(parts); p++ {
// get separate lines and ignore throw away first line of text
mappingLines := strings.Split(parts[p], "\n")[1:]
var mappings []mappingRange
for _, l := range mappingLines {
lineParts := strings.Split(l, " ")
mappings = append(mappings, mappingRange{
destinationStart: cast.ToInt(lineParts[0]),
sourceStart: cast.ToInt(lineParts[1]),
count: cast.ToInt(lineParts[2]),
})
}
listOfMappingRanges = append(listOfMappingRanges, mappings)
}
return seeds, listOfMappingRanges
}
func part2(input string) int {
seedRanges, listOfMappingRanges := parseInput(input)
lowestFinalNum := math.MaxInt64
// store final mappings to save on duplicate calcs?
// not sure if that helped. brute force solution worked in a minute or two
// finalMappings := map[int]int{}
for i := 0; i < len(seedRanges); i += 2 {
for count := 0; count < seedRanges[i+1]; count++ {
seed := seedRanges[i] + count
finalMapping := getFinalMapping(seed, listOfMappingRanges)
lowestFinalNum = mathy.MinInt(lowestFinalNum, finalMapping)
}
// progress check...
fmt.Println(i, len(seedRanges))
}
return lowestFinalNum
}
// ////////////
// naive solutions that are too slow with actual input
//
func naivePart1(input string) int {
seeds, allMaps := naiveParseInput(input)
lowestFinalNum := math.MaxInt64
for _, s := range seeds {
finalLocation := naiveMapSeedToFinalLocation(s, allMaps)
lowestFinalNum = mathy.MinInt(lowestFinalNum, finalLocation)
}
return lowestFinalNum
}
func naiveMapSeedToFinalLocation(seed int, allMaps []map[int]int) int {
mappedNum := seed
for _, m := range allMaps {
num, ok := m[mappedNum]
if ok {
mappedNum = num
}
}
return mappedNum
}
// assume that the mappings are in a logical order in the input
// seeds -> soil -> fertilizer -> water -> etc and not out of order...
// so a slice works fine for storing the mappings
func naiveParseInput(input string) (seeds []int, allMaps []map[int]int) {
// seed to soil map: 50 98 2
// means 2 mappings total: seed 98 maps to water 50 and seed 99 maps to soil 51
// if a mappingRange does not exist, then it maps to the same number
parts := strings.Split(input, "\n\n")
seedParts := strings.Split(parts[0], " ")
// skip first part which is "seeds: "
for i := 1; i < len(seedParts); i++ {
seeds = append(seeds, cast.ToInt(seedParts[i]))
}
// mappings
for p := 1; p < len(parts); p++ {
// get separate lines and ignore throw away first line of text
mappingLines := strings.Split(parts[p], "\n")[1:]
m := map[int]int{}
for _, line := range mappingLines {
nums := strings.Split(line, " ")
destinationStart, sourceStart, count := cast.ToInt(nums[0]), cast.ToInt(nums[1]), cast.ToInt(nums[2])
for c := 0; c < count; c++ {
m[sourceStart+c] = destinationStart + c
}
}
allMaps = append(allMaps, m)
}
return seeds, allMaps
}
+91
View File
@@ -0,0 +1,91 @@
package main
import (
"testing"
)
var example = `seeds: 79 14 55 13
seed-to-soil map:
50 98 2
52 50 48
soil-to-fertilizer map:
0 15 37
37 52 2
39 0 15
fertilizer-to-water map:
49 53 8
0 11 42
42 0 7
57 7 4
water-to-light map:
88 18 7
18 25 70
light-to-temperature map:
45 77 23
81 45 19
68 64 13
temperature-to-humidity map:
0 69 1
1 0 69
humidity-to-location map:
60 56 37
56 93 4`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 35,
},
{
name: "actual",
input: input,
want: 88151870,
},
}
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: 46,
},
{
name: "actual",
input: input,
want: 2008785,
},
}
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)
}
})
}
}
+141
View File
@@ -0,0 +1,141 @@
package main
import (
_ "embed"
"flag"
"fmt"
"regexp"
"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 {
races := parseInputPart1(input)
ans := 1
for _, r := range races {
ans *= wayToWinRace(r)
}
return ans
}
func wayToWinRace(r race) int {
// probably fast enough to do this naively for part 1...
// Time: 40 92 97 90
// total: 319
// but could binary search this
var ans int
for chargeTime := 1; chargeTime < r.time; chargeTime++ {
// velocity (chargeTime) * remaining time
dist := chargeTime * (r.time - chargeTime)
if dist > r.distance {
ans++
}
}
return ans
}
func part2(input string) int {
combinedRace := parseInputPart2(input)
// binary search this?
// but the left and right and middle could all fail if the distribution is
// F F P P P P F F F F F F F F F F F F
// F = Fail, P = Pass
// and it is some kind of (UN-CENTERED) bell curve distribution
// could test 1 by 1 from left and right of time bound...
// unfortunately you can just brute force this.
return wayToWinRace(combinedRace)
// all the optimizations might fall apart given unkind inputs, the example
// has a distribution with many passes in the middle:
// F P P P P P P P P P P P P P P P P P P P P P P P P P P P P P P P P P P F
// but an unkind one could have F P F F F F F F F F F F F F F F F which could
// create a linear time complexity anyways...
// maybe there's some kind of search where you divide the input times until
// you find a passing time, so in half, then quarters, then eights, etc
// once you find a passing time you have a reduced window to search
// but if the entire entry is 0 or 1 pass and the rest fails, then it
// degrades to linear in the worst case scenario anyways, so sanitized or
// expected inputs do go a long way
}
type race struct {
time, distance int
}
func parseInputPart1(input string) (ans []race) {
parts := strings.Split(input, "\n")
timeParts := strings.Split(parts[0], " ")
distParts := strings.Split(parts[1], " ")
numRegexp := regexp.MustCompile("[0-9]+")
var t, d int
for t < len(timeParts) && d < len(distParts) {
for !numRegexp.MatchString(timeParts[t]) {
t++
}
for !numRegexp.MatchString(distParts[d]) {
d++
}
ans = append(ans, race{
time: cast.ToInt(timeParts[t]),
distance: cast.ToInt(distParts[d]),
})
t++
d++
}
return ans
}
func parseInputPart2(input string) race {
parts := strings.Split(input, "\n")
timeLine := parts[0]
distLine := parts[1]
nonNums := regexp.MustCompile("[^0-9]+")
timeLine = nonNums.ReplaceAllString(timeLine, "")
distLine = nonNums.ReplaceAllString(distLine, "")
return race{
time: cast.ToInt(timeLine),
distance: cast.ToInt(distLine),
}
}
+60
View File
@@ -0,0 +1,60 @@
package main
import (
"testing"
)
var example = `Time: 7 15 30
Distance: 9 40 200`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 288,
},
{
name: "actual",
input: input,
want: 6209190,
},
}
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: 71503,
},
// {
// name: "actual",
// input: input,
// want: 0,
// },
}
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)
}
})
}
}
+168
View File
@@ -0,0 +1,168 @@
package main
import (
_ "embed"
"flag"
"fmt"
"sort"
"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)
ans := part1(input)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
}
func part1(input string) int {
allPlayers := parseInput(input)
sort.Sort(allPlayers)
var ans int
for i, player := range allPlayers {
ans += int(player.bid) * int(i+1)
}
return ans
}
type player struct {
hand string
bid int
handTypeScore int
}
func parseInput(input string) (ans SortablePlayers) {
for _, line := range strings.Split(input, "\n") {
handAndBid := strings.Split(line, " ")
pl := player{
hand: handAndBid[0],
bid: cast.ToInt(handAndBid[1]),
}
pl.handTypeScore = scoreHandType(pl.hand)
ans = append(ans, pl)
}
return ans
}
/* point assignments:
five of a kind -> 7
four of a kind -> 6
full house -> 5
three of a kind -> 4
two pair -> 3
one pair -> 2
high card -> 1
*/
func scoreHandType(hand string) int {
counts := map[string]int{}
for _, card := range strings.Split(hand, "") {
counts[card]++
}
// high card
if len(counts) == 5 {
return 1
}
// one pair
if len(counts) == 4 {
return 2
}
if len(counts) == 3 {
// either two pair or three of a kind
for _, ct := range counts {
if ct == 3 {
return 4 // 3 of a kind, 3 1 1
}
}
return 3 // two pair, 2 2 1
}
if len(counts) == 2 {
// full house (3 2) or four of a kind (4 1)
for _, ct := range counts {
if ct == 3 {
return 5
}
}
return 6
}
if len(counts) == 1 {
return 7
}
panic(fmt.Sprintf("error scoring hand: %+v", hand))
}
type SortablePlayers []player
func (ps SortablePlayers) Len() int { return len(ps) }
func (ps SortablePlayers) Swap(i, j int) {
ps[i], ps[j] = ps[j], ps[i]
}
func (ps SortablePlayers) Less(i, j int) bool {
iTypeScore := ps[i].handTypeScore
jTypeScore := ps[j].handTypeScore
if iTypeScore == jTypeScore {
// higher score goes to end of ps slice
return !doesPlayer1WinTiebreak(ps[i], ps[j])
}
return iTypeScore < jTypeScore
}
var cardValuesMap = map[string]int{
"A": 14,
"K": 13,
"Q": 12,
"J": 11,
"T": 10,
"9": 9,
"8": 8,
"7": 7,
"6": 6,
"5": 5,
"4": 4,
"3": 3,
"2": 2,
}
// returns true if player1 wins the tie break
func doesPlayer1WinTiebreak(p1, p2 player) bool {
if p1.handTypeScore != p2.handTypeScore {
panic("p1 and p2 scores do not have the same level hand")
}
p1Cards := strings.Split(p1.hand, "")
p2Cards := strings.Split(p2.hand, "")
for i := 0; i < len(p1Cards); i++ {
if cardValuesMap[p1Cards[i]] > cardValuesMap[p2Cards[i]] {
return true
} else if cardValuesMap[p1Cards[i]] < cardValuesMap[p2Cards[i]] {
return false
}
}
panic("not expecting to have two matching hands")
}
+37
View File
@@ -0,0 +1,37 @@
package main
import (
"testing"
)
var example = `32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 6440,
},
{
name: "actual",
input: input,
want: 248569531,
},
}
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)
}
})
}
}
+196
View File
@@ -0,0 +1,196 @@
package main
import (
_ "embed"
"flag"
"fmt"
"sort"
"strings"
"github.com/alexchao26/advent-of-code-go/cast"
"github.com/alexchao26/advent-of-code-go/util"
)
/**
This implementation is too tangled to cover part 1 and 2 because the sort
logic is encapsulated in the interface which cannot (nicely) be made aware
of which part is being run...
There are two main differences highlighted by "// NOTE: diff to part 1 here"
*/
//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)
ans := part2(input)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
}
func part2(input string) int {
allPlayers := parseInput(input, 1)
sort.Sort(allPlayers)
var ans int
for i, player := range allPlayers {
ans += int(player.bid) * int(i+1)
}
return ans
}
type player struct {
hand string
bid int
handTypeScore int
}
func parseInput(input string, part int) (ans SortablePlayers) {
for _, line := range strings.Split(input, "\n") {
handAndBid := strings.Split(line, " ")
pl := player{
hand: handAndBid[0],
bid: cast.ToInt(handAndBid[1]),
}
pl.handTypeScore = scoreHandType(pl, part)
ans = append(ans, pl)
}
return ans
}
/* point assignments:
five of a kind -> 7
four of a kind -> 6
full house -> 5
three of a kind -> 4
two pair -> 3
one pair -> 2
high card -> 1
*/
func scoreHandType(p player, part int) int {
counts := map[string]int{}
for _, card := range strings.Split(p.hand, "") {
counts[card]++
}
// NOTE: diff to part 1 here
if counts["J"] != 0 {
// add the J counts to the highest card's count as that will always result in the best hand?
jCount := counts["J"]
// remove "J" from map in case if it's the highest count
delete(counts, "J")
var highestCard string
var highestCount int
for card, ct := range counts {
if ct > highestCount {
highestCount = ct
highestCard = card
}
}
counts[highestCard] += jCount
}
// high card
if len(counts) == 5 {
return 1
}
// one pair
if len(counts) == 4 {
return 2
}
if len(counts) == 3 {
// either two pair or three of a kind
for _, ct := range counts {
if ct == 3 {
return 4 // 3 of a kind, 3 1 1
}
}
return 3 // two pair, 2 2 1
}
if len(counts) == 2 {
// full house (3 2) or four of a kind (4 1)
for _, ct := range counts {
if ct == 3 {
return 5
}
}
return 6
}
if len(counts) == 1 {
return 7
}
panic(fmt.Sprintf("error scoring hand: %+v", p.hand))
}
type SortablePlayers []player
func (ps SortablePlayers) Len() int { return len(ps) }
func (ps SortablePlayers) Swap(i, j int) {
ps[i], ps[j] = ps[j], ps[i]
}
func (ps SortablePlayers) Less(i, j int) bool {
iTypeScore := ps[i].handTypeScore
jTypeScore := ps[j].handTypeScore
if iTypeScore == jTypeScore {
// higher score goes to end of ps slice
return !doesPlayer1WinTiebreak(ps[i], ps[j])
}
return iTypeScore < jTypeScore
}
var cardValuesMap = map[string]int{
"A": 14,
"K": 13,
"Q": 12,
// can just leave the same values though, the heiarchy is what matters
"T": 10,
"9": 9,
"8": 8,
"7": 7,
"6": 6,
"5": 5,
"4": 4,
"3": 3,
"2": 2,
// NOTE: diff to part 1 here: J is the worst card now
"J": 0,
}
// returns true if player1 wins the tie break
func doesPlayer1WinTiebreak(p1, p2 player) bool {
if p1.handTypeScore != p2.handTypeScore {
panic("p1 and p2 scores do not have the same level hand")
}
p1Cards := strings.Split(p1.hand, "")
p2Cards := strings.Split(p2.hand, "")
for i := 0; i < len(p1Cards); i++ {
if cardValuesMap[p1Cards[i]] > cardValuesMap[p2Cards[i]] {
return true
} else if cardValuesMap[p1Cards[i]] < cardValuesMap[p2Cards[i]] {
return false
}
}
panic("not expecting to have two matching hands")
}
+37
View File
@@ -0,0 +1,37 @@
package main
import (
"testing"
)
var example = `32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483`
func Test_part2(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 5905,
},
{
name: "actual",
input: input,
want: 250382098,
},
}
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)
}
})
}
}
+145
View File
@@ -0,0 +1,145 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"github.com/alexchao26/advent-of-code-go/mathy"
"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 {
steps, graph := parseInput(input)
location := "AAA"
numOfSteps := 0
for location != "ZZZ" {
dir := steps[numOfSteps%len(steps)]
if dir == "L" {
location = graph[location][0]
} else {
location = graph[location][1]
}
numOfSteps++
}
return numOfSteps
}
func part2(input string) int {
steps, graph := parseInput(input)
locations := []string{}
for loc := range graph {
if loc[2:3] == "A" {
locations = append(locations, loc)
}
}
/**
brute force doesn't work... need to figure out cycle times of each starting location
but they won't cycle just based on number of steps because of the weird L-R randomness
so we can only rely on the "full cycle" of all steps before it loops
- there are six starting locations
NOTE: BIG assumptions based on KIND inputs
- assume that the Z-end locations will sync EXACTLY at the end of a cycle of steps
- after further analyzing logs of the end of each cycle, the entry point VERY kindly deposits us
at the very start of a cycle that will eventually end in a Z-end location
AAA -> MLM -> ... -> XKZ -> MLM -> ... -> XKZ -> MLM -> ... -> XKZ -> MLM
and this holds true for all six locations in my input
Therefore the cycles are not offset by a particular number of steps at the start to get to the cycle
such as START --> LOC1 --> LOC2 --> Start -> A
^ |
| v
D <-- C
this makes the maths fairly straight forward with just having to find the LCM (least common multiple)
of all the cycle periods because that is when they will all sync up and land on a Z
*/
numOfSteps := 0
locationCyclePeriods := []int{}
for cycle := 0; len(locations) > 0; cycle++ {
for _, dir := range steps {
for i, loc := range locations {
if dir == "L" {
locations[i] = graph[loc][0]
} else {
locations[i] = graph[loc][1]
}
}
numOfSteps++
}
// if any location is at a z-end at the end of a cycle, record the cycle time
// to do the final maths at the end
newLocations := []string{}
for _, loc := range locations {
if loc[2:3] == "Z" {
locationCyclePeriods = append(locationCyclePeriods, numOfSteps)
} else {
newLocations = append(newLocations, loc)
}
}
locations = newLocations
}
// combine all into an LCM (helper function added to mathy package)
lcm := locationCyclePeriods[0]
for i := 1; i < len(locationCyclePeriods); i++ {
lcm = mathy.LeastCommonMultiple(lcm, locationCyclePeriods[i])
}
return lcm
}
func parseInput(input string) (steps []string, graph map[string][]string) {
graph = map[string][]string{}
parts := strings.Split(input, "\n\n")
steps = strings.Split(parts[0], "")
for _, line := range strings.Split(parts[1], "\n") {
graph[line[0:3]] = []string{
line[7:10],
line[12:15],
}
}
return steps, graph
}
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"testing"
)
var example = `LLR
AAA = (BBB, BBB)
BBB = (AAA, ZZZ)
ZZZ = (ZZZ, ZZZ)`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 6,
},
{
name: "actual",
input: input,
want: 18673,
},
}
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)
}
})
}
}
var examplePart2 = `LR
11A = (11B, XXX)
11B = (XXX, 11Z)
11Z = (11B, XXX)
22A = (22B, XXX)
22B = (22C, 22C)
22C = (22Z, 22Z)
22Z = (22B, 22B)
XXX = (XXX, XXX)`
func Test_part2(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: examplePart2,
want: 6,
},
{
name: "actual",
input: input,
want: 17972669116327,
},
}
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)
}
})
}
}
+128
View File
@@ -0,0 +1,128 @@
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 {
parsed := parseInput(input)
ans := 0
for _, hist := range parsed {
ans += findNextValue(hist)
}
return ans
}
func findNextValue(history []int) int {
matrix := [][]int{
history,
}
nonZeroFound := true
for nonZeroFound {
nonZeroFound = false
next := []int{}
matrixHistoryRow := matrix[len(matrix)-1]
for i := 1; i < len(matrixHistoryRow); i++ {
prev := matrixHistoryRow[i-1]
curr := matrixHistoryRow[i]
next = append(next, curr-prev)
if next[len(next)-1] != 0 {
nonZeroFound = true
}
}
matrix = append(matrix, next)
}
ans := 0
for _, row := range matrix {
ans += row[len(row)-1]
}
return ans
}
func part2(input string) int {
parsed := parseInput(input)
ans := 0
for _, hist := range parsed {
ans += findPrevValue(hist)
}
return ans
}
func findPrevValue(history []int) int {
matrix := [][]int{
history,
}
nonZeroFound := true
for nonZeroFound {
nonZeroFound = false
next := []int{}
matrixHistoryRow := matrix[len(matrix)-1]
for i := 1; i < len(matrixHistoryRow); i++ {
prev := matrixHistoryRow[i-1]
curr := matrixHistoryRow[i]
next = append(next, curr-prev)
if next[len(next)-1] != 0 {
nonZeroFound = true
}
}
matrix = append(matrix, next)
}
ans := 0
for r := len(matrix) - 1; r >= 0; r-- {
ans = matrix[r][0] - ans
}
return ans
}
func parseInput(input string) (ans [][]int) {
for _, line := range strings.Split(input, "\n") {
nums := []int{}
for _, str := range strings.Split(line, " ") {
nums = append(nums, cast.ToInt(str))
}
ans = append(ans, nums)
}
return ans
}
+61
View File
@@ -0,0 +1,61 @@
package main
import (
"testing"
)
var example = `0 3 6 9 12 15
1 3 6 10 15 21
10 13 16 21 30 45`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 114,
},
{
name: "actual",
input: input,
want: 1782868781,
},
}
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: 2,
},
{
name: "actual",
input: input,
want: 1057,
},
}
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)
}
})
}
}
+260
View File
@@ -0,0 +1,260 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"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)
ans := pipeMaze(input, part)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
}
var pipes = map[string][2][2]int{
"|": {
{-1, 0}, // up
{1, 0}, // down
},
"-": {
{0, -1}, // left
{0, 1}, //right
},
"L": {
{-1, 0}, // up
{0, 1}, // right
},
"J": {
{-1, 0}, // up
{0, -1}, // left
},
"7": {
{0, -1}, // left
{1, 0}, // down
},
"F": {
{0, 1}, // right
{1, 0}, // down
},
}
func pipeMaze(input string, part int) int {
grid := parseInput(input)
var r, c int
for i, row := range grid {
for j, val := range row {
if val == "S" {
r = i
c = j
}
}
}
fillGridLocation(grid, r, c)
// traverse entire loop to determine length
loopCoords := map[[2]int]bool{}
toVisit := [][2]int{
{r, c},
}
for len(toVisit) > 0 {
coords := toVisit[0]
if loopCoords[coords] {
break
}
loopCoords[coords] = true
toVisit = toVisit[1:]
// assumes loop is well formed, will cause a panic if not
diffs := pipes[grid[coords[0]][coords[1]]]
for _, diff := range diffs {
nextRow, nextCol := coords[0]+diff[0], coords[1]+diff[1]
if isInRange(grid, nextRow, nextCol) && !loopCoords[[2]int{nextRow, nextCol}] {
toVisit = append(toVisit, [2]int{nextRow, nextCol})
}
}
}
if part == 1 {
return len(loopCoords) / 2
}
// part 2
// create copy of grid with all non-loop spots replaced with a period
reducedGrid := make([][]string, len(grid))
for i := 0; i < len(grid); i++ {
for j := 0; j < len(grid[0]); j++ {
if loopCoords[[2]int{i, j}] {
reducedGrid[i] = append(reducedGrid[i], grid[i][j])
} else {
reducedGrid[i] = append(reducedGrid[i], ".")
}
}
}
// expand grid to double plus 2 in both dimensions to account for squeezing between pipes
// the plus two is to add an empty row/column on each side for easier traversing from the outside
expandedGrid := [][]string{}
expandedGrid = append(expandedGrid, make([]string, len(reducedGrid[0])*2+2))
for r, rows := range reducedGrid {
expandedGrid = append(expandedGrid, make([]string, len(reducedGrid[0])*2+2))
for c, val := range rows {
expandedGrid[r*2+1][c*2+1] = val
}
// empty row
expandedGrid = append(expandedGrid, make([]string, len(reducedGrid[0])*2+2))
}
// fill gaps between loop coords so we have an encased area again
// we can naively try to fill in every empty spot because only ones with two valid connecting
// pipes will be filled
for r, rows := range expandedGrid {
for c, val := range rows {
if val == "" {
fillGridLocation(expandedGrid, r, c)
}
}
}
// replacing empty strings with spaces makes the printout human readable
for r, rows := range expandedGrid {
for c, val := range rows {
if val == "" {
expandedGrid[r][c] = " "
}
}
}
toVisit = [][2]int{
{0, 0},
}
seen := map[[2]int]bool{}
for len(toVisit) > 0 {
coords := toVisit[0]
toVisit = toVisit[1:]
if seen[coords] {
continue
}
seen[coords] = true
// delete reachable dots
if expandedGrid[coords[0]][coords[1]] == "." {
expandedGrid[coords[0]][coords[1]] = " "
}
for _, diff := range [][2]int{
{-1, 0},
{1, 0},
{0, -1},
{0, 1},
} {
nextRow := coords[0] + diff[0]
nextCol := coords[1] + diff[1]
if isInRange(expandedGrid, nextRow, nextCol) {
if expandedGrid[nextRow][nextCol] == "." || expandedGrid[nextRow][nextCol] == " " {
toVisit = append(toVisit, [2]int{nextRow, nextCol})
}
}
}
}
// count remaining dots
var ans int
for _, rows := range expandedGrid {
for _, val := range rows {
if val == "." {
ans++
}
}
}
// for _, rows := range expandedGrid {
// fmt.Println(rows)
// }
return ans
}
func fillGridLocation(grid [][]string, r, c int) {
// inputs are nice and exactly two adjacent cells that connect to S
// check four directions from start
leftCol := c - 1
rightCol := c + 1
upRow := r - 1
downRow := r + 1
var combinedString string
// check left for inRange and possible valid pipe types
if isInRange(grid, r, leftCol) &&
(grid[r][leftCol] == "-" || grid[r][leftCol] == "L" || grid[r][leftCol] == "F") {
combinedString += "left"
}
// right
if isInRange(grid, r, rightCol) &&
(grid[r][rightCol] == "-" || grid[r][rightCol] == "J" || grid[r][rightCol] == "7") {
combinedString += "right"
}
// up
if isInRange(grid, upRow, c) &&
(grid[upRow][c] == "|" || grid[upRow][c] == "7" || grid[upRow][c] == "F") {
combinedString += "up"
}
if isInRange(grid, downRow, c) &&
(grid[downRow][c] == "|" || grid[downRow][c] == "J" || grid[downRow][c] == "L") {
combinedString += "down"
}
switch combinedString {
case "leftup":
grid[r][c] = "J"
case "leftdown":
grid[r][c] = "7"
case "rightup":
grid[r][c] = "L"
case "rightdown":
grid[r][c] = "F"
case "leftright":
grid[r][c] = "-"
case "updown":
grid[r][c] = "|"
// default:
// do not panic so we can use this function more naively for the expanded grid
// could return an error instead and choose to check it for part1 where we NEED it to find a result
// panic("ineligible configuration: " + combinedString)
}
}
func isInRange(grid [][]string, row, col int) bool {
return row >= 0 && row < len(grid) && col >= 0 && col < len(grid[0])
}
func parseInput(input string) (ans [][]string) {
for _, line := range strings.Split(input, "\n") {
ans = append(ans, strings.Split(line, ""))
}
return ans
}
+123
View File
@@ -0,0 +1,123 @@
package main
import (
"testing"
)
var example = `.....
.S-7.
.|.|.
.L-J.
.....`
var complexExample = `..F7.
.FJ|.
SJ.L7
|F--J
LJ...`
var examplePart2 = `...........
.S-------7.
.|F-----7|.
.||.....||.
.||.....||.
.|L-7.F-J|.
.|..|.|..|.
.L--J.L--J.
...........`
var examplePart2_2 = `..........
.S------7.
.|F----7|.
.||OOOO||.
.||OOOO||.
.|L-7F-J|.
.|II||II|.
.L--JL--J.
..........`
var examplePart2_large = `.F----7F7F7F7F-7....
.|F--7||||||||FJ....
.||.FJ||||||||L7....
FJL7L7LJLJ||LJ.L-7..
L--J.L7...LJS7F-7L7.
....F-J..F7FJ|L7L7L7
....L7.F7||L7|.L7L7|
.....|FJLJ|FJ|F7|.LJ
....FJL-7.||.||||...
....L---J.LJ.LJLJ...`
var examplePart2_larger = `FF7FSF7F7F7F7F7F---7
L|LJ||||||||||||F--J
FL-7LJLJ||||||LJL-77
F--JF--7||LJLJ7F7FJ-
L---JF-JLJ.||-FJLJJ7
|F|F-JF---7F7-L7L|7|
|FFJF7L7F-JF7|JL---7
7-L-JL7||F7|L7F-7F7|
L.L7LFJ|||||FJL7||LJ
L7JLJL-JLJLJL--JLJ.L`
func Test_pipeMaze(t *testing.T) {
tests := []struct {
name string
input string
part int
want int
}{
{
name: "example",
input: example,
part: 1,
want: 4,
},
{
name: "complexExample",
input: complexExample,
part: 1,
want: 8,
},
{
name: "actual part 1",
input: input,
part: 1,
want: 6773,
},
// part 2
{
name: "examplePart2",
input: examplePart2,
part: 2,
want: 4,
},
{
name: "examplePart2_2",
input: examplePart2_2,
part: 2,
want: 4,
},
{
name: "examplePart2_large",
input: examplePart2_large,
part: 2,
want: 8,
},
{
name: "examplePart2_larger",
input: examplePart2_larger,
part: 2,
want: 10,
},
{
name: "actual part 2",
input: input,
part: 2,
want: 493,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := pipeMaze(tt.input, tt.part); got != tt.want {
t.Errorf("pipeMaze() = %v, want %v", got, tt.want)
}
})
}
}
+116
View File
@@ -0,0 +1,116 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"github.com/alexchao26/advent-of-code-go/mathy"
"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, 2)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
} else {
ans := part1(input, 1000000)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
}
}
func part1(input string, expansionFactor int) int {
grid := parseInput(input)
// record which rows and cols are empty first
emptyRows := map[int]bool{}
emptyCols := map[int]bool{}
for r := 0; r < len(grid); r++ {
galaxyFound := false
for c := 0; c < len(grid[0]); c++ {
if grid[r][c] != "." {
galaxyFound = true
break
}
}
if !galaxyFound {
emptyRows[r] = true
}
}
for c := 0; c < len(grid[0]); c++ {
galaxyFound := false
for r := 0; r < len(grid); r++ {
if grid[r][c] != "." {
galaxyFound = true
break
}
}
if !galaxyFound {
emptyCols[c] = true
}
}
// traverse grid and calculate coordinates of each galaxy while accumulating the expanded rows/cols
// that can be added into the galaxy's coordinates as they're found
galaxyCoords := map[int][2]int{}
expandedRowsToAdd := 0
for r := 0; r < len(grid); r++ {
if emptyRows[r] {
expandedRowsToAdd += expansionFactor - 1
continue
}
expendedColsToAdd := 0
for c := 0; c < len(grid[0]); c++ {
if emptyCols[c] {
expendedColsToAdd += expansionFactor - 1
continue
}
if grid[r][c] == "#" {
galaxyCoords[len(galaxyCoords)] = [2]int{
r + expandedRowsToAdd,
c + expendedColsToAdd,
}
}
}
}
// shortest distance is basically manhattan distance, helper function handles absolute values
totalDistance := 0
for i := 0; i < len(galaxyCoords); i++ {
for j := i + 1; j < len(galaxyCoords); j++ {
g1, g2 := galaxyCoords[i], galaxyCoords[j]
totalDistance += mathy.ManhattanDistance(g1[0], g1[1], g2[0], g2[1])
}
}
return totalDistance
}
func parseInput(input string) (ans [][]string) {
for _, line := range strings.Split(input, "\n") {
ans = append(ans, strings.Split(line, ""))
}
return ans
}
+65
View File
@@ -0,0 +1,65 @@
package main
import (
"testing"
)
var example = `...#......
.......#..
#.........
..........
......#...
.#........
.........#
..........
.......#..
#...#.....`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
expansionFactor int
want int
}{
{
name: "example",
input: example,
expansionFactor: 2,
want: 374,
},
{
name: "actual",
input: input,
expansionFactor: 2,
want: 9734203,
},
// part 2
{
name: "example",
input: example,
expansionFactor: 10,
want: 1030,
},
{
name: "example",
input: example,
expansionFactor: 100,
want: 8410,
},
{
name: "actual",
input: input,
expansionFactor: 1000000,
want: 568914596391,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := part1(tt.input, tt.expansionFactor); got != tt.want {
t.Errorf("part1() = %v, want %v", got, tt.want)
}
})
}
}
+261
View File
@@ -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
}
+64
View File
@@ -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)
}
})
}
}
+165
View File
@@ -0,0 +1,165 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"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 {
patterns := parseInput(input)
ans := 0
for _, pattern := range patterns {
maybeMirrorRow := findMirrorRow(pattern, -1)
if maybeMirrorRow != -1 {
ans += 100 * maybeMirrorRow
} else {
maybeMirrorCol := findMirrorCol(pattern, -1)
if maybeMirrorCol == -1 {
panic("did not find mirror row or col")
}
ans += maybeMirrorCol
}
}
return ans
}
// returns the zero-index of the line, if the line is between index 3 and 4, it returns 4
// ignoreRow is for part2 where we want to ignore the original mirrored row because it may still be
// valid in the un-smudged pattern
func findMirrorRow(pattern [][]string, ignoreRow int) int {
// combine the string slices into a string so they're easier to compare
combinedRows := []string{}
for _, row := range pattern {
combinedRows = append(combinedRows, strings.Join(row, ""))
}
for i := 1; i < len(combinedRows); i++ {
mismatchFound := false
for offset := 1; i-offset >= 0 && i+offset-1 < len(combinedRows); offset++ {
// fmt.Println("combined row indexes", i-offset, i+offset-1)
// fmt.Println(combinedRows[i-offset], "\n", combinedRows[i+offset-1])
if combinedRows[i-offset] != combinedRows[i+offset-1] {
mismatchFound = true
// fmt.Println("mismatch found")
break
}
}
if !mismatchFound {
if i != ignoreRow {
return i
}
}
}
// none found
return -1
}
func findMirrorCol(pattern [][]string, ignoreCol int) int {
// rotate the grid, maintaining the indices for easier maths later
// then just pass it into the findMirrorRow func
rotatedGrid := [][]string{}
for c := 0; c < len(pattern[0]); c++ {
newRow := []string{}
for r := 0; r < len(pattern); r++ {
newRow = append(newRow, pattern[r][c])
}
rotatedGrid = append(rotatedGrid, newRow)
}
return findMirrorRow(rotatedGrid, ignoreCol)
}
func part2(input string) int {
patterns := parseInput(input)
ans := 0
for _, pattern := range patterns {
// store the original row and col so they can be ignored in the find mirror row func
originalMirrorRow := findMirrorRow(pattern, -1)
originalMirrorCol := findMirrorCol(pattern, -1)
traverse:
// labels suck but without the breaks this all has to go into a separate function which is
// arguably less readable. and the break is necessary to not double count reflections
for r, row := range pattern {
for c, val := range row {
if val == "." {
pattern[r][c] = "#"
if maybeMirrorRow := findMirrorRow(pattern, originalMirrorRow); maybeMirrorRow != -1 {
ans += 100 * maybeMirrorRow
break traverse
}
if maybeMirrorCol := findMirrorCol(pattern, originalMirrorCol); maybeMirrorCol != -1 {
ans += maybeMirrorCol
break traverse
}
pattern[r][c] = "."
} else if val == "#" {
pattern[r][c] = "."
if maybeMirrorRow := findMirrorRow(pattern, originalMirrorRow); maybeMirrorRow != -1 {
ans += 100 * maybeMirrorRow
break traverse
}
if maybeMirrorCol := findMirrorCol(pattern, originalMirrorCol); maybeMirrorCol != -1 {
ans += maybeMirrorCol
break traverse
}
pattern[r][c] = "#"
} else {
panic("expected input: " + val)
}
}
}
}
return ans
}
func parseInput(input string) (ans [][][]string) {
for _, section := range strings.Split(input, "\n\n") {
grid := [][]string{}
for _, line := range strings.Split(section, "\n") {
grid = append(grid, strings.Split(line, ""))
}
ans = append(ans, grid)
}
return ans
}
+73
View File
@@ -0,0 +1,73 @@
package main
import (
"testing"
)
var example = `#.##..##.
..#.##.#.
##......#
##......#
..#.##.#.
..##..##.
#.#.##.#.
#...##..#
#....#..#
..##..###
#####.##.
#####.##.
..##..###
#....#..#`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 405,
},
{
name: "actual",
input: input,
want: 30575,
},
}
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: 400,
},
{
name: "actual",
input: input,
want: 37478,
},
}
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)
}
})
}
}
+185
View File
@@ -0,0 +1,185 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"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 {
grid := parseInput(input)
// tilt north, all O's roll to the top or to the next #
tiltNorth(grid)
// then calculate total load (total rows) - (row index) per rock
ans := 0
for r, row := range grid {
for _, val := range row {
if val == "O" {
ans += len(grid) - r
}
}
}
return ans
}
func part2(input string) int {
grid := parseInput(input)
seenStates := map[string]int{}
cycles := 1000000000
for c := 0; c < cycles; c++ {
key := stringifyStringGrid(grid)
if lastIndex, ok := seenStates[key]; ok {
cyclePeriod := c - lastIndex
for c+cyclePeriod < cycles {
c += cyclePeriod
}
}
seenStates[key] = c
// 1 cycle = tilt N, W, S, E
tiltNorth(grid)
tiltWest(grid)
tiltSouth(grid)
tiltEast(grid)
}
ans := 0
for r, row := range grid {
for _, val := range row {
if val == "O" {
ans += len(grid) - r
}
}
}
// 99841 too low
return ans
}
func tiltNorth(grid [][]string) {
for r, row := range grid {
for c, val := range row {
if val == "O" {
for nextRow := r - 1; nextRow >= 0; nextRow-- {
// can only fall north if nextRow is an empty space
if grid[nextRow][c] == "." {
grid[nextRow][c] = "O"
grid[nextRow+1][c] = "."
} else {
break
}
}
}
}
}
}
func tiltSouth(grid [][]string) {
for r := len(grid) - 1; r >= 0; r-- {
for c := range len(grid[0]) {
val := grid[r][c]
if val == "O" {
for nextRow := r + 1; nextRow < len(grid); nextRow++ {
// can only fall north if nextRow is an empty space
if grid[nextRow][c] == "." {
grid[nextRow][c] = "O"
grid[nextRow-1][c] = "."
} else {
break
}
}
}
}
}
}
func tiltEast(grid [][]string) {
for c := len(grid[0]) - 1; c >= 0; c-- {
for r := range grid {
val := grid[r][c]
if val == "O" {
for nextCol := c + 1; nextCol < len(grid[0]); nextCol++ {
// can only fall north if nextCol is an empty space
if grid[r][nextCol] == "." {
grid[r][nextCol] = "O"
grid[r][nextCol-1] = "."
} else {
break
}
}
}
}
}
}
func tiltWest(grid [][]string) {
for c := range len(grid[0]) {
for r := range grid {
val := grid[r][c]
if val == "O" {
for nextCol := c - 1; nextCol >= 0; nextCol-- {
// can only fall north if nextCol is an empty space
if grid[r][nextCol] == "." {
grid[r][nextCol] = "O"
grid[r][nextCol+1] = "."
} else {
break
}
}
}
}
}
}
func stringifyStringGrid(grid [][]string) string {
ans := ""
for _, row := range grid {
ans += strings.Join(row, "")
}
return ans
}
func parseInput(input string) (ans [][]string) {
for _, line := range strings.Split(input, "\n") {
ans = append(ans, strings.Split(line, ""))
}
return ans
}
+68
View File
@@ -0,0 +1,68 @@
package main
import (
"testing"
)
var example = `O....#....
O.OO#....#
.....##...
OO.#O....O
.O.....O#.
O.#..O.#.#
..O..#O..O
.......O..
#....###..
#OO..#....`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 136,
},
{
name: "actual",
input: input,
want: 108840,
},
}
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: 64,
},
{
name: "actual",
input: input,
want: 103445,
},
}
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)
}
})
}
}
+132
View File
@@ -0,0 +1,132 @@
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 {
parsed := parseInput(input)
ans := 0
for _, step := range parsed {
ans += hash(step)
}
return ans
}
func hash(step string) int {
ans := 0
for _, char := range strings.Split(step, "") {
asciiVal := cast.ToASCIICode(char)
ans += asciiVal
ans *= 17
ans %= 256
}
return ans
}
func part2(input string) int {
boxes := make([]box, 256)
// optimization to keep a linked list within in box, but likely not necessary...
labelToBoxIndex := map[string]int{}
steps := parseInput(input)
for _, step := range steps {
if strings.Contains(step, "=") {
parts := strings.Split(step, "=")
label := parts[0]
focalLength := cast.ToInt(parts[1])
boxIndex := hash(label)
if oldBoxIndex, ok := labelToBoxIndex[label]; ok {
if oldBoxIndex != boxIndex {
panic("hashes should be the same...")
}
// iterate and update focalLength of found box
for i := range len(boxes[boxIndex]) {
if boxes[boxIndex][i].label == label {
boxes[boxIndex][i].focalLength = focalLength
}
}
} else {
boxes[boxIndex] = append(boxes[boxIndex], lense{
label: label,
focalLength: focalLength,
})
labelToBoxIndex[label] = boxIndex
}
} else if strings.Contains(step, "-") {
label := step[:len(step)-1]
if boxIndex, ok := labelToBoxIndex[label]; ok {
// switch it all the way to the end
for i := range len(boxes[boxIndex]) - 1 {
if boxes[boxIndex][i].label == label {
boxes[boxIndex][i], boxes[boxIndex][i+1] = boxes[boxIndex][i+1], boxes[boxIndex][i]
}
}
// cut off end, remove from map
boxes[boxIndex] = boxes[boxIndex][:len(boxes[boxIndex])-1]
delete(labelToBoxIndex, label)
}
} else {
panic("unexpected step format: " + step)
}
}
ans := 0
for boxIndex, box := range boxes {
for lenseIndex, lense := range box {
ans += (boxIndex + 1) * (lenseIndex + 1) * lense.focalLength
}
}
return ans
}
type lense struct {
label string
focalLength int
}
type box []lense
func parseInput(input string) (ans []string) {
return strings.Split(input, ",")
}
+59
View File
@@ -0,0 +1,59 @@
package main
import (
"testing"
)
var example = `rn=1,cm-,qp=3,cm=2,qp-,pc=4,ot=9,ab=5,pc-,pc=6,ot=7`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 1320,
},
{
name: "actual",
input: input,
want: 507666,
},
}
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: 145,
},
{
name: "actual",
input: input,
want: 233537,
},
}
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)
}
})
}
}
+236
View File
@@ -0,0 +1,236 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"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 {
grid := parseInput(input)
return calcEnergizedTiles(grid, beamHead{
velocity: right,
row: 0,
col: 0,
})
}
func part2(input string) int {
grid := parseInput(input)
best := 0
for r := range len(grid) {
// starting from left of grid, headed right
startBeam := beamHead{
velocity: right,
row: r,
col: 0,
}
// max is a go 1.21 addition: https://pkg.go.dev/builtin#max
best = max(best, calcEnergizedTiles(grid, startBeam))
// starting from right of grid, headed left
startBeam = beamHead{
velocity: left,
row: r,
col: len(grid[0]) - 1,
}
best = max(best, calcEnergizedTiles(grid, startBeam))
}
for c := range len(grid[0]) {
// starting from top of grid, headed down
startBeam := beamHead{
velocity: down,
row: 0,
col: c,
}
best = max(best, calcEnergizedTiles(grid, startBeam))
// starting from bottom of grid, headed up
startBeam = beamHead{
velocity: up,
row: len(grid) - 1,
col: c,
}
best = max(best, calcEnergizedTiles(grid, startBeam))
}
return best
}
func calcEnergizedTiles(grid [][]string, beam beamHead) int {
// need to track multiple beams because they can split and generate multiple
beams := []beamHead{beam}
// [4]bool represents being hit from left, right, up and down respectively
// need to track direction so that we can terminate beams that are cyclical
hitGrid := [][][4]bool{}
for range grid {
hitGrid = append(hitGrid, make([][4]bool, len(grid[0])))
}
for len(beams) > 0 {
b := beams[0]
beams = beams[1:]
skip := false
for vel, hitGridIndex := range velToHitGridIndex {
if b.velocity == vel && hitGrid[b.row][b.col][hitGridIndex] {
skip = true
}
}
if skip {
continue
}
// record direction hit in hit grid
hitGrid[b.row][b.col][velToHitGridIndex[b.velocity]] = true
cell := grid[b.row][b.col]
nextVelocities, ok := mirrorToNextVelocities[cell][b.velocity]
if !ok {
panic("no nextVelocities found for cell type: " + cell)
}
for _, nextVelocity := range nextVelocities {
nextRow := b.row + nextVelocity[0]
nextCol := b.col + nextVelocity[1]
if nextRow < 0 || nextRow >= len(grid) ||
nextCol < 0 || nextCol >= len(grid[0]) {
continue
}
beams = append(beams, beamHead{
velocity: nextVelocity,
row: nextRow,
col: nextCol,
})
}
}
energizedTiles := 0
for r := range len(hitGrid) {
for c := range len(hitGrid[0]) {
for _, dir := range hitGrid[r][c] {
if dir {
energizedTiles++
break
}
}
}
}
return energizedTiles
}
var left = [2]int{0, -1}
var right = [2]int{0, 1}
var up = [2]int{-1, 0}
var down = [2]int{1, 0}
var velToHitGridIndex = map[[2]int]int{
left: 0,
right: 1,
up: 2,
down: 3,
}
var mirrorToNextVelocities = map[string]map[[2]int][][2]int{
".": {
left: {left},
right: {right},
up: {up},
down: {down},
},
"/": {
left: {down},
down: {left},
up: {right},
right: {up},
},
"\\": {
left: {up},
up: {left},
down: {right},
right: {down},
},
"|": {
left: {up, down},
right: {up, down},
up: {up},
down: {down},
},
"-": {
left: {left},
right: {right},
up: {left, right},
down: {left, right},
},
}
type beamHead struct {
velocity [2]int
row, col int
}
func (b beamHead) String() string {
return fmt.Sprintf("vel: %v, coords: %d, %d", b.velocity, b.row, b.col)
}
// for debugging
func condenseHitGrid(grid [][][4]bool) [][]int {
ans := [][]int{}
for range grid {
ans = append(ans, make([]int, len(grid[0])))
}
for r, row := range grid {
for c, sli := range row {
for _, val := range sli {
if val {
ans[r][c]++
}
}
}
}
return ans
}
func parseInput(input string) (ans [][]string) {
for _, line := range strings.Split(input, "\n") {
ans = append(ans, strings.Split(line, ""))
}
return ans
}
+68
View File
@@ -0,0 +1,68 @@
package main
import (
"testing"
)
var example = `.|...\....
|.-.\.....
.....|-...
........|.
..........
.........\
..../.\\..
.-.-/..|..
.|....-|.\
..//.|....`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 46,
},
{
name: "actual",
input: input,
want: 7046,
},
}
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: 51,
},
{
name: "actual",
input: input,
want: 7313,
},
}
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)
}
})
}
}
+153
View File
@@ -0,0 +1,153 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"github.com/alexchao26/advent-of-code-go/cast"
"github.com/alexchao26/advent-of-code-go/data-structures/heap"
"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 := clumsyCart(input, 1, 3)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
} else {
ans := clumsyCart(input, 4, 10)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
}
}
func clumsyCart(input string, minMoves, maxMoves int) int {
grid := parseInput(input)
minHeap := heap.NewMinHeap()
minHeap.Add(bfsNode{
heatLoss: 0,
row: 0,
col: 0,
lastDir: right,
debugPath: "",
})
minHeap.Add(bfsNode{
heatLoss: 0,
row: 0,
col: 0,
lastDir: down,
debugPath: "",
})
// store lowest heat loss for each coordinate for each direction, slightly suboptimal
// because it can be divided into vertical and horizontal
// instead of L, R, U, D. But it's a constant time optimization but it runs fast enough
cache := map[string]int{}
for minHeap.Length() > 0 {
node := minHeap.Remove().(bfsNode)
key := fmt.Sprintf("%d %d - %v", node.row, node.col, node.lastDir)
if val, ok := cache[key]; ok {
if node.heatLoss >= val {
// exit if the current heatLoss isn't better
continue
} else {
cache[key] = node.heatLoss
}
} else {
cache[key] = node.heatLoss
}
if node.row == len(grid)-1 && node.col == len(grid[0])-1 {
return node.heatLoss
}
// just add a node for each vertical direction, then those will move vertically as well
// which covers all possibilities
for _, nextDir := range verticalTurns[node.lastDir] {
summedHeatLoss := 0
for i := 1; i <= maxMoves; i++ {
nextRow := node.row + nextDir[0]*i
nextCol := node.col + nextDir[1]*i
// skip if out of range
if nextRow < 0 || nextRow >= len(grid) || nextCol < 0 || nextCol >= len(grid[0]) {
continue
}
summedHeatLoss += grid[nextRow][nextCol]
// do not add to heap if the cart has moved less than the minimum required moves (part 2)
if i < minMoves {
continue
}
minHeap.Add(bfsNode{
heatLoss: node.heatLoss + summedHeatLoss,
row: nextRow,
col: nextCol,
lastDir: nextDir,
debugPath: node.debugPath + fmt.Sprintf("%d,%d ", nextRow, nextCol),
})
}
}
}
panic("should return from heap processing")
}
type bfsNode struct {
heatLoss int
row, col int
lastDir direction
debugPath string
}
func (b bfsNode) Value() int {
return b.heatLoss
}
type direction [2]int
var up = direction{-1, 0}
var down = direction{1, 0}
var left = direction{0, -1}
var right = direction{0, 1}
var verticalTurns = map[direction][2]direction{
up: {left, right},
down: {left, right},
left: {up, down},
right: {up, down},
}
func parseInput(input string) (ans [][]int) {
for _, line := range strings.Split(input, "\n") {
row := []int{}
for _, str := range strings.Split(line, "") {
row = append(row, cast.ToInt(str))
}
ans = append(ans, row)
}
return ans
}
+65
View File
@@ -0,0 +1,65 @@
package main
import (
"testing"
)
var example = `2413432311323
3215453535623
3255245654254
3446585845452
4546657867536
1438598798454
4457876987766
3637877979653
4654967986887
4564679986453
1224686865563
2546548887735
4322674655533`
func Test_clumsyCart(t *testing.T) {
tests := []struct {
name string
input string
minMoves int
maxMoves int
want int
}{
{
name: "example",
input: example,
minMoves: 1,
maxMoves: 3,
want: 102,
},
{
name: "actual",
input: input,
minMoves: 1,
maxMoves: 3,
want: 1001,
},
{
name: "example_part2",
input: example,
minMoves: 4,
maxMoves: 10,
want: 94,
},
{
name: "actual",
input: input,
minMoves: 1,
maxMoves: 3,
want: 1197,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := clumsyCart(tt.input, tt.minMoves, tt.maxMoves); got != tt.want {
t.Errorf("clumsyCart() = %v, want %v", got, tt.want)
}
})
}
}
+217
View File
@@ -0,0 +1,217 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strconv"
"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 {
digInstructions := parseInput(input)
trenchCoords := getTrenchCoords(digInstructions)
containedCoords := getContainedCoords(trenchCoords)
return len(containedCoords) + len(trenchCoords)
}
func getTrenchCoords(digInstructions []digInstruction) map[[2]int]bool {
trenchCoords := map[[2]int]bool{
{0, 0}: true,
}
var row, col int
diffs := map[string][2]int{
"L": {0, -1},
"R": {0, 1},
"U": {-1, 0},
"D": {1, 0},
}
for _, inst := range digInstructions {
for i := 1; i <= inst.length; i++ {
row += diffs[inst.dir][0]
col += diffs[inst.dir][1]
trenchCoords[[2]int{row, col}] = true
}
}
return trenchCoords
}
func getContainedCoords(trenchCoords map[[2]int]bool) map[[2]int]bool {
// check around a coordinate that's part of a straight line for a cell that _could_ be contained
// straight lines will have one side that's in and one that is out
// we'll only check vertical lines to make it easier to code...
var testCoords [][2]int
for coord := range trenchCoords {
upCoords := [2]int{coord[0] - 1, coord[1]}
downCoords := [2]int{coord[0] + 1, coord[1]}
leftCoords := [2]int{coord[0], coord[1] - 1}
rightCoords := [2]int{coord[0], coord[1] + 1}
if trenchCoords[upCoords] && trenchCoords[downCoords] &&
!trenchCoords[leftCoords] && !trenchCoords[rightCoords] {
// part of vertical line
testCoords = append(testCoords, leftCoords, rightCoords)
break
}
}
// calculate the max size that can be contained (equal to the box containing all the coordinates)
var (
left = testCoords[0][1]
right = testCoords[0][1]
top = testCoords[0][0]
bottom = testCoords[0][0]
)
for coords := range trenchCoords {
left = min(left, coords[1])
right = max(right, coords[1])
top = min(top, coords[0])
bottom = max(bottom, coords[0])
}
maxContainedSize := (right - left + 1) * (bottom - top + 1)
for _, coord := range testCoords {
queue := [][2]int{coord}
seen := map[[2]int]bool{}
for len(queue) > 0 && len(seen) < maxContainedSize {
current := queue[0]
queue = queue[1:]
if seen[current] {
continue
}
seen[current] = true
for _, diff := range [][2]int{
{-1, 0},
{1, 0},
{0, -1},
{0, 1},
} {
nextRow := current[0] + diff[0]
nextCol := current[1] + diff[1]
nextCoord := [2]int{nextRow, nextCol}
// if already seen or it's part of the trench, skip
if trenchCoords[nextCoord] || seen[nextCoord] {
continue
}
// otherwise add it to be searched
queue = append(queue, nextCoord)
}
}
if len(queue) == 0 {
return seen
}
}
panic("should return from loop")
}
func part2(input string) int {
digInstructions := parseInput(input)
vertices := [][2]int{}
currentPoint := [2]int{0, 0}
for _, inst := range digInstructions {
hex := inst.color[1 : len(inst.color)-1]
dirCode := inst.color[len(inst.color)-1:]
convInt, err := strconv.ParseInt(hex, 16, 0)
if err != nil {
panic(err.Error())
}
switch dirCode {
case "0": // R
currentPoint[1] += int(convInt)
case "1": // D
currentPoint[0] += int(convInt)
case "2": // L
currentPoint[1] -= int(convInt)
case "3": // U
currentPoint[0] -= int(convInt)
}
vertices = append(vertices, currentPoint)
}
return shoelace(vertices) + 1
}
func shoelace(coordinates [][2]int) int {
area := 0
for i := 0; i < len(coordinates); i++ {
coordA := coordinates[i]
coordB := coordinates[(i+1)%(len(coordinates))]
area += (coordA[1] * coordB[0]) - (coordB[1] * coordA[0]) +
max(abs(coordA[0]-coordB[0]), abs(coordA[1]-coordB[1]))
}
return area / 2
}
func abs(i int) int {
if i < 0 {
return -i
}
return i
}
type digInstruction struct {
dir string
length int
color string
}
func parseInput(input string) (ans []digInstruction) {
for _, line := range strings.Split(input, "\n") {
parts := strings.Split(line, " ")
ans = append(ans, digInstruction{
dir: parts[0],
length: cast.ToInt(parts[1]),
color: parts[2][1 : len(parts[2])-1],
})
}
return ans
}
+72
View File
@@ -0,0 +1,72 @@
package main
import (
"testing"
)
var example = `R 6 (#70c710)
D 5 (#0dc571)
L 2 (#5713f0)
D 2 (#d2c081)
R 2 (#59c680)
D 2 (#411b91)
L 5 (#8ceee2)
U 2 (#caa173)
L 1 (#1b58a2)
U 2 (#caa171)
R 2 (#7807d2)
U 3 (#a77fa3)
L 2 (#015232)
U 2 (#7a21e3)`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 62,
},
{
name: "actual",
input: input,
want: 47527,
},
}
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: 952408144115,
},
{
name: "actual",
input: input,
want: 52240187443190,
},
}
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)
}
})
}
}
+240
View File
@@ -0,0 +1,240 @@
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 {
parts, workflowsMap := parseInput(input)
ans := 0
for _, p := range parts {
currentWorkflowName := "in"
for currentWorkflowName != "A" && currentWorkflowName != "R" {
wf := workflowsMap[currentWorkflowName]
for _, rule := range wf.ruleStrings {
if !strings.Contains(rule, ":") {
currentWorkflowName = rule
break
}
colonSplit := strings.Split(rule, ":")
output := colonSplit[1]
if strings.Contains(colonSplit[0], "<") {
conditionalParts := strings.Split(colonSplit[0], "<")
if p[conditionalParts[0]] < cast.ToInt(conditionalParts[1]) {
currentWorkflowName = output
break
}
} else if strings.Contains(colonSplit[0], ">") {
conditionalParts := strings.Split(colonSplit[0], ">")
if p[conditionalParts[0]] > cast.ToInt(conditionalParts[1]) {
currentWorkflowName = output
break
}
} else {
panic("unexpected workflow rule conditional: " + rule)
}
}
}
if currentWorkflowName == "A" {
ans += p.sumRatings()
}
}
return ans
}
type part map[string]int
func (p part) sumRatings() int {
total := 0
for _, v := range p {
total += v
}
return total
}
type workflow struct {
name string
ruleStrings []string
}
func parseInput(input string) (parts []part, workflowsMap map[string]workflow) {
inputParts := strings.Split(input, "\n\n")
workflowsMap = map[string]workflow{}
// process workflows
for _, line := range strings.Split(inputParts[0], "\n") {
lineParts := strings.Split(line, "{")
wf := workflow{
name: lineParts[0],
ruleStrings: strings.Split(lineParts[1][:len(lineParts[1])-1], ","),
}
workflowsMap[wf.name] = wf
}
for _, line := range strings.Split(inputParts[1], "\n") {
withoutBraces := line[1 : len(line)-1]
p := part{}
for _, ratingStr := range strings.Split(withoutBraces, ",") {
ratingParts := strings.Split(ratingStr, "=")
p[ratingParts[0]] = cast.ToInt(ratingParts[1])
}
parts = append(parts, p)
}
return parts, workflowsMap
}
func part2(input string) int {
_, workflowsMap := parseInput(input)
// 1 to 4000 bounds for each rating...
boundedParts := map[string][2]int{
"x": {1, 4000},
"m": {1, 4000},
"a": {1, 4000},
"s": {1, 4000},
}
return updatePartBoundsAndSplit(boundedParts, workflowsMap, "in", 0)
}
func updatePartBoundsAndSplit(boundedParts map[string][2]int, workflowsMap map[string]workflow, currentWorkflow string, debugDepth int) int {
if currentWorkflow == "R" {
return 0
}
if currentWorkflow == "A" {
product := 1
for _, bounds := range boundedParts {
product *= bounds[1] - bounds[0] + 1
}
return product
}
// split based on rules...
total := 0
// for each rule,
// the rule either passes and moves onto a different workflow,
// or fails and checks the next rule
// need to sum up both forks
//
// passing is handled via recursion, failing is handled via looping to the next rule
// in both cases the bounds need to be updated
for _, rule := range workflowsMap[currentWorkflow].ruleStrings {
// just the next workflow to go after
if !strings.Contains(rule, ":") {
nextWorkflowName := rule
total += updatePartBoundsAndSplit(boundedParts, workflowsMap, nextWorkflowName, debugDepth+1)
break
}
colonSplit := strings.Split(rule, ":")
nextWorkflowName := colonSplit[1]
if strings.Contains(colonSplit[0], "<") {
conditionalParts := strings.Split(colonSplit[0], "<")
ratingName := conditionalParts[0]
ratingTestValue := cast.ToInt(conditionalParts[1])
// fork the part that passes the < conditional
copyOfBounds := copyBoundedPartsMap(boundedParts)
copyOfBounds[ratingName] = [2]int{
copyOfBounds[ratingName][0],
ratingTestValue - 1,
}
// check that the new bounds are still valid
if copyOfBounds[ratingName][0] <= copyOfBounds[ratingName][1] {
total += updatePartBoundsAndSplit(copyOfBounds, workflowsMap, nextWorkflowName, debugDepth+1)
}
// second fork for failing the conditional, need to update the boundedParts to fail
boundedParts[ratingName] = [2]int{
ratingTestValue,
boundedParts[ratingName][1],
}
// check that the new bounds are still valid
if boundedParts[ratingName][0] > boundedParts[ratingName][1] {
break
}
} else if strings.Contains(colonSplit[0], ">") {
conditionalParts := strings.Split(colonSplit[0], ">")
ratingName := conditionalParts[0]
ratingTestValue := cast.ToInt(conditionalParts[1])
// fork the part that passes the > conditional
copyOfBounds := copyBoundedPartsMap(boundedParts)
copyOfBounds[ratingName] = [2]int{
ratingTestValue + 1,
copyOfBounds[ratingName][1],
}
// check that the new bounds are still valid before recursing
if copyOfBounds[ratingName][0] <= copyOfBounds[ratingName][1] {
total += updatePartBoundsAndSplit(copyOfBounds, workflowsMap, nextWorkflowName, debugDepth+1)
}
// second fork for failing the conditional, need to update the boundedParts to fail
boundedParts[ratingName] = [2]int{
boundedParts[ratingName][0],
ratingTestValue,
}
// check that the new bounds are still valid
if boundedParts[ratingName][0] > boundedParts[ratingName][1] {
break
}
} else {
panic("unexpected workflow rule conditional: " + rule)
}
}
return total
}
func copyBoundedPartsMap(boundedParts map[string][2]int) map[string][2]int {
cp := map[string][2]int{}
for k, v := range boundedParts {
cp[k] = v
}
return cp
}
+75
View File
@@ -0,0 +1,75 @@
package main
import (
"testing"
)
var example = `px{a<2006:qkq,m>2090:A,rfg}
pv{a>1716:R,A}
lnx{m>1548:A,A}
rfg{s<537:gd,x>2440:R,A}
qs{s>3448:A,lnx}
qkq{x<1416:A,crn}
crn{x>2662:A,R}
in{s<1351:px,qqz}
qqz{s>2770:qs,m<1801:hdj,R}
gd{a>3333:R,R}
hdj{m>838:A,pv}
{x=787,m=2655,a=1222,s=2876}
{x=1679,m=44,a=2067,s=496}
{x=2036,m=264,a=79,s=2244}
{x=2461,m=1339,a=466,s=291}
{x=2127,m=1623,a=2188,s=1013}`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 19114,
},
{
name: "actual",
input: input,
want: 287054,
},
}
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: 167409079868000,
},
{
name: "actual",
input: input,
want: 131619440296497,
},
}
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)
}
})
}
}
+215
View File
@@ -0,0 +1,215 @@
package main
import (
_ "embed"
"flag"
"fmt"
"math"
"strings"
"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)
ans := pulsePropagation(input, part)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
}
func pulsePropagation(input string, part int) int {
modules := parseInput(input)
var lowPulses, highPulses int
buttonPresses := 1000
if part == 2 {
// let this cycle infinitely so I can figure out cycle times for part 2
buttonPresses = math.MaxInt64
}
// for part 2:
// looking at input, rx's only input is &lb, which is a conjunction, so needs to get ALL high signals to send a low to rx
// lb is fed from four other modules that all need to send high signals:
// &rz, &lf, &br, &fk
// figuring out the cycle times of these four then maybe the LCM will be the answer if the input is kind?
lastCycleForHighPulse := map[string]int{
"rz": -1,
"lf": -1,
"br": -1,
"fk": -1,
}
cycles := []int{}
for i := 0; i < buttonPresses; i++ {
if part == 2 && len(cycles) == 4 {
break
}
queue := []pulse{}
queue = append(queue, pulse{
isLowPulse: true,
source: "button",
destination: "broadcaster",
})
for len(queue) > 0 {
p := queue[0]
queue = queue[1:]
if p.isLowPulse {
lowPulses++
} else {
highPulses++
}
if val, ok := lastCycleForHighPulse[p.source]; ok && !p.isLowPulse {
// fmt.Println("found for ", p.source, i+1)
if val == -1 {
lastCycleForHighPulse[p.source] = i + 1
} else {
cycles = append(cycles, (i+1)-val)
}
}
if _, ok := modules[p.destination]; !ok {
continue
}
switch modules[p.destination].moduleType {
case "broadcaster":
for _, dest := range modules[p.destination].destinations {
queue = append(queue, pulse{
isLowPulse: p.isLowPulse,
source: "broadcaster",
destination: dest,
})
}
case "flipflop":
if p.isLowPulse {
for _, dest := range modules[p.destination].destinations {
queue = append(queue, pulse{
// if it was on, it flips off and sends a low pulse
// if it was off, then sends a high pulse (isLowPulse = false)
isLowPulse: modules[p.destination].flipFlopIsOn,
source: p.destination,
destination: dest,
})
}
// flip it
modules[p.destination].flipFlopIsOn = !modules[p.destination].flipFlopIsOn
}
case "conjunction":
modules[p.destination].conjunctionInputsMapWasLastPulseHigh[p.source] = !p.isLowPulse
allHigh := true
for source, wasStrongPulse := range modules[p.destination].conjunctionInputsMapWasLastPulseHigh {
_ = source
if !wasStrongPulse {
allHigh = false
break
}
}
for _, dest := range modules[p.destination].destinations {
queue = append(queue, pulse{
// all high sends a low pulse, otherwise high pulse
isLowPulse: allHigh,
source: p.destination,
destination: dest,
})
}
default:
panic("unexpected module type" + modules[p.destination].moduleType)
}
}
}
// wow that worked, super generous on the inputs...
if part == 2 {
ans := 1
for _, c := range cycles {
ans *= c
}
return ans
}
return lowPulses * highPulses
}
type module struct {
moduleType string
name string
flipFlopIsOn bool
conjunctionInputsMapWasLastPulseHigh map[string]bool
destinations []string
}
type pulse struct {
isLowPulse bool
source, destination string
}
func parseInput(input string) (ans map[string]*module) {
ans = map[string]*module{}
for _, line := range strings.Split(input, "\n") {
parts := strings.Split(line, " -> ")
mod := module{
moduleType: "",
flipFlopIsOn: false,
conjunctionInputsMapWasLastPulseHigh: map[string]bool{},
destinations: []string{},
}
if parts[0] == "broadcaster" {
mod.moduleType = "broadcaster"
mod.name = "broadcaster"
mod.destinations = strings.Split(parts[1], ", ")
} else if parts[0][:1] == "%" {
mod.moduleType = "flipflop"
mod.name = parts[0][1:]
mod.destinations = strings.Split(parts[1], ", ")
} else if parts[0][:1] == "&" {
mod.moduleType = "conjunction"
mod.name = parts[0][1:]
mod.destinations = strings.Split(parts[1], ", ")
} else {
panic("unidentified module type: " + line)
}
ans[mod.name] = &mod
}
// initialize conjunction maps with all their source modules
for name, module := range ans {
for _, dest := range module.destinations {
if _, ok := ans[dest]; !ok {
continue
}
if ans[dest].moduleType == "conjunction" {
ans[dest].conjunctionInputsMapWasLastPulseHigh[name] = false
}
}
}
return ans
}
+58
View File
@@ -0,0 +1,58 @@
package main
import (
"testing"
)
var example = `broadcaster -> a, b, c
%a -> b
%b -> c
%c -> inv
&inv -> a`
var example2 = `broadcaster -> a
%a -> inv, con
&inv -> b
%b -> con
&con -> output`
func Test_pulsePropagation(t *testing.T) {
tests := []struct {
name string
input string
part int
want int
}{
{
name: "example",
input: example,
part: 1,
want: 32000000,
},
{
name: "example2",
input: example2,
part: 1,
want: 11687500,
},
{
name: "actual",
input: input,
part: 1,
want: 817896682,
},
{
name: "actual",
input: input,
part: 2,
want: 250924073918341,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := pulsePropagation(tt.input, tt.part); got != tt.want {
t.Errorf("pulsePropagation() = %v, want %v", got, tt.want)
}
})
}
}
+171
View File
@@ -0,0 +1,171 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"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, 64)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
} else {
ans := part2(input, 26501365)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
}
}
func part1(input string, steps int) int {
grid := parseInput(input)
var row, col int
for r, rowSlice := range grid {
for c, val := range rowSlice {
if val == "S" {
row = r
col = c
break
}
}
}
queue := map[[2]int]bool{
{row, col}: true,
}
for i := 0; i < steps; i++ {
newQueue := map[[2]int]bool{}
for coord := range queue {
for _, diff := range [][2]int{
{-1, 0},
{1, 0},
{0, -1},
{0, 1},
} {
nextRow := coord[0] + diff[0]
nextCol := coord[1] + diff[1]
if nextRow < 0 || nextRow >= len(grid) || nextCol < 0 || nextCol >= len(grid[0]) {
continue
}
if grid[nextRow][nextCol] == "." || grid[nextRow][nextCol] == "S" {
newQueue[[2]int{nextRow, nextCol}] = true
}
}
}
queue = newQueue
}
return len(queue)
}
func part2(input string, steps int) int {
grid := parseInput(input)
var row, col int
for r, rowSlice := range grid {
for c, val := range rowSlice {
if val == "S" {
row = r
col = c
break
}
}
}
grid[row][col] = "."
// keeps track of the flip-flopping coords separately
evenSeenCoords := map[[2]int]bool{}
oddSeenCoords := map[[2]int]bool{}
// need a set of all coords added to the queue so that we're not re-adding the same coords
uniqueCoords := map[[2]int]bool{}
queue := [][2]int{
{row, col},
}
// results to calculate quadratic constants with
results := []int{}
// perform two steps at once to always be on an even number of steps
for s := 0; s < steps && len(results) < 3; s++ {
activeSeenCoords := evenSeenCoords
if s%2 == 1 {
activeSeenCoords = oddSeenCoords
}
newQueue := [][2]int{}
for _, coord := range queue {
activeSeenCoords[coord] = true
for _, diff := range [][2]int{
{-1, 0},
{1, 0},
{0, -1},
{0, 1},
} {
nextRow := coord[0] + diff[0]
nextCol := coord[1] + diff[1]
nextCoord := [2]int{nextRow, nextCol}
// handles infinite grid and garden space detection
modNextRow := ((nextRow % len(grid)) + len(grid)) % +len(grid)
modNextCol := ((nextCol % len(grid[0])) + len(grid[0])) % len(grid[0])
if grid[modNextRow][modNextCol] != "." {
continue
}
// if already seen, skip
if uniqueCoords[nextCoord] {
continue
}
uniqueCoords[nextCoord] = true
newQueue = append(newQueue, nextCoord)
}
}
queue = newQueue
if s != 0 && s%131 == 65 {
results = append(results, len(activeSeenCoords))
}
}
// solve quadratic for a b and c constants
a := (results[2] + results[0] - 2*results[1]) / 2
b := results[1] - results[0] - a
c := results[0]
n := steps / len(grid)
return a*n*n + b*n + c
}
func parseInput(input string) (ans [][]string) {
for _, line := range strings.Split(input, "\n") {
ans = append(ans, strings.Split(line, ""))
}
return ans
}
+93
View File
@@ -0,0 +1,93 @@
package main
import (
"testing"
)
var example = `...........
.....###.#.
.###.##..#.
..#.#...#..
....#.#....
.##..S####.
.##..#...#.
.......##..
.##.#.####.
.##..##.##.
...........`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
steps int
want int
}{
{
name: "example",
input: example,
steps: 6,
want: 16,
},
{
name: "actual",
input: input,
steps: 64,
want: 3743,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := part1(tt.input, tt.steps); got != tt.want {
t.Errorf("part1() = %v, want %v", got, tt.want)
}
})
}
}
func Test_part2(t *testing.T) {
tests := []struct {
name string
input string
steps int
want int
}{
// {
// name: "example-10",
// input: example,
// steps: 10,
// want: 50,
// },
// {
// name: "example-50",
// input: example,
// steps: 50,
// want: 1594,
// },
// {
// name: "example-100",
// input: example,
// steps: 100,
// want: 6536,
// },
// {
// name: "example-5k",
// input: example,
// steps: 5000,
// want: 16733044,
// },
{
name: "actual",
input: input,
steps: 26501365,
want: 618261433219147,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := part2(tt.input, tt.steps); got != tt.want {
t.Errorf("part2() = %v, want %v", got, tt.want)
}
})
}
}
+223
View File
@@ -0,0 +1,223 @@
package main
import (
_ "embed"
"flag"
"fmt"
"sort"
"strings"
"github.com/alexchao26/advent-of-code-go/algos"
"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 {
bricks := parseInput(input)
dropBricks(bricks)
// basically have a graph...
// if a brick supports nothing, it can be removed
// if a brick supports
removableBricks := map[int]bool{}
for _, brick := range bricks {
if len(brick.supports) == 0 {
// supports nothing
removableBricks[brick.index] = true
} else {
// check ALL supported bricks, if this is the ONLY support for those
// then this brick CANNOT be removed
hasUniqueDependency := false
for supportedBrickIndex := range brick.supports {
if len(bricks[supportedBrickIndex].supportedBy) == 1 {
hasUniqueDependency = true
}
}
if !hasUniqueDependency {
removableBricks[brick.index] = true
}
}
}
return len(removableBricks)
}
func dropBricks(bricks []*brick) {
// process bricks by lowest z values
sort.Slice(bricks, func(i, j int) bool {
return bricks[i].start[2] < bricks[j].start[2]
})
// re-index, pesky bug...
for i, brick := range bricks {
brick.index = i
}
// all bricks are < 10 units of volume, so can store their coords in a map...
// also all bricks in the input are straight lines, only one dimension will be > 1
occupiedCells := map[[3]int]int{}
for _, brick := range bricks {
isBlocked := false
for brick.start[2] > 1 && !isBlocked {
for _, coord := range brick.coords {
downOne := [3]int{coord[0], coord[1], coord[2] - 1}
if index, ok := occupiedCells[downOne]; ok {
isBlocked = true
brick.supportedBy[index] = true
bricks[index].supports[brick.index] = true
}
}
if !isBlocked {
for i := range brick.coords {
brick.coords[i][2]--
}
brick.start[2]--
brick.end[2]--
}
}
for _, coord := range brick.coords {
occupiedCells[coord] = brick.index
}
}
}
func part2(input string) int {
bricks := parseInput(input)
dropBricks(bricks)
total := 0
// chain reaction
for i := range bricks {
bricksCopy := copyAllBricks(bricks)
startingBrick := bricksCopy[i]
queueToRemove := []*brick{}
for in := range startingBrick.supports {
delete(bricksCopy[in].supportedBy, startingBrick.index)
queueToRemove = append(queueToRemove, bricksCopy[in])
}
removed := 0
for len(queueToRemove) > 0 {
br := queueToRemove[0]
queueToRemove = queueToRemove[1:]
if len(br.supportedBy) > 0 {
continue
}
removed++
// check every brick it supports, remove self from it's supportedBy map
// then add to queue to be checked
for supportedBrickIndex := range br.supports {
delete(bricksCopy[supportedBrickIndex].supportedBy, br.index)
if len(bricksCopy[supportedBrickIndex].supportedBy) == 0 {
queueToRemove = append(queueToRemove, bricksCopy[supportedBrickIndex])
}
}
}
total += removed
}
return total
}
func copyAllBricks(bricks []*brick) []*brick {
copiedBricks := []*brick{}
for _, b := range bricks {
newBrick := &brick{
// start: []int{},
// end: []int{},
index: b.index,
// coords: [][3]int{},
supportedBy: map[int]bool{},
supports: map[int]bool{},
}
// need full copies of these otherwise they'll point to the same underlying maps
for k, v := range b.supportedBy {
newBrick.supportedBy[k] = v
}
for k, v := range b.supports {
newBrick.supports[k] = v
}
copiedBricks = append(copiedBricks, newBrick)
}
return copiedBricks
}
type brick struct {
start, end []int
index int
coords [][3]int
supportedBy map[int]bool
supports map[int]bool
}
func parseInput(input string) (ans []*brick) {
for _, line := range strings.Split(input, "\n") {
coords := [6]int{}
for i, part := range algos.SplitStringOn(line, []string{",", "~"}) {
coords[i] = cast.ToInt(part)
}
if coords[0] > coords[3] || coords[1] > coords[4] || coords[2] > coords[5] {
panic("unordered input")
}
allCoords := [][3]int{}
for x := coords[0]; x <= coords[3]; x++ {
for y := coords[1]; y <= coords[4]; y++ {
for z := coords[2]; z <= coords[5]; z++ {
allCoords = append(allCoords, [3]int{x, y, z})
}
}
}
ans = append(ans, &brick{
start: coords[:3],
end: coords[3:],
index: len(ans),
coords: allCoords,
supportedBy: map[int]bool{},
supports: map[int]bool{},
})
}
return ans
}
+65
View File
@@ -0,0 +1,65 @@
package main
import (
"testing"
)
var example = `1,0,1~1,2,1
0,0,2~2,0,2
0,2,3~2,2,3
0,0,4~0,2,4
2,0,5~2,2,5
0,1,6~2,1,6
1,1,8~1,1,9`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 5,
},
{
name: "actual",
input: input,
want: 471,
},
}
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: 7,
},
{
name: "actual",
input: input,
want: 68525,
},
}
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)
}
})
}
}
+217
View File
@@ -0,0 +1,217 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"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 {
grid := parseInput(input)
// do not step on same tile twice, longest hike possible
// standard backtrack?
var startCol int
for c := 0; c < len(grid[0]); c++ {
if grid[0][c] == "." {
startCol = c
break
}
}
return backtrackLongest(grid, 0, startCol, map[[2]int]bool{}, 0)
}
var slopes = map[string][2]int{
">": {0, 1},
"<": {0, -1},
"v": {1, 0},
"^": {-1, 0},
}
type node struct {
row, col int
weightedEdges map[*node]int
}
func backtrackLongest(grid [][]string, row, col int, visited map[[2]int]bool, steps int) int {
if row == len(grid)-1 && grid[row][col] == "." {
return steps
}
if diff, ok := slopes[grid[row][col]]; ok {
nextCoord := [2]int{row + diff[0], col + diff[1]}
if visited[nextCoord] {
return 0
}
visited[[2]int{row, col}] = true
result := backtrackLongest(grid, row+diff[0], col+diff[1], visited, steps+1)
visited[[2]int{row, col}] = false
return result
}
best := 0
for _, diff := range slopes {
nextRow := row + diff[0]
nextCol := col + diff[1]
if nextRow < 0 || nextRow >= len(grid) ||
nextCol < 0 || nextCol >= len(grid[0]) {
continue
}
nextCoord := [2]int{nextRow, nextCol}
if visited[nextCoord] {
continue
}
if grid[nextRow][nextCol] != "#" {
visited[[2]int{row, col}] = true
result := backtrackLongest(grid, nextRow, nextCol, visited, steps+1)
best = max(best, result)
visited[[2]int{row, col}] = false
}
}
return best
}
func part2(input string) int {
grid := parseInput(input)
var startCol int
for c := 0; c < len(grid[0]); c++ {
if grid[0][c] == "." {
startCol = c
break
}
}
_ = startCol
// reduce to a graph with weighted edges
allNodes := map[[2]int]*node{}
// just make all nodes
for r := 0; r < len(grid); r++ {
for c := 0; c < len(grid[0]); c++ {
if grid[r][c] == "#" {
continue
}
allNodes[[2]int{r, c}] = &node{
row: r,
col: c,
weightedEdges: map[*node]int{},
}
}
}
// connect all adjacent nodes and assign a weight of 1
for coords, node := range allNodes {
for _, diff := range slopes {
nextCoord := [2]int{
coords[0] + diff[0],
coords[1] + diff[1],
}
if neighbor, ok := allNodes[nextCoord]; ok {
node.weightedEdges[neighbor] = 1
neighbor.weightedEdges[node] = 1
}
}
}
// reduce the graph by combining neighbors if there are exactly two
for _, currentNode := range allNodes {
if len(currentNode.weightedEdges) == 2 {
twoNeighbors := []*node{}
summedWeight := 0
for neighborNode := range currentNode.weightedEdges {
twoNeighbors = append(twoNeighbors, neighborNode)
summedWeight += neighborNode.weightedEdges[currentNode]
}
delete(twoNeighbors[0].weightedEdges, currentNode)
delete(twoNeighbors[1].weightedEdges, currentNode)
twoNeighbors[0].weightedEdges[twoNeighbors[1]] = summedWeight
twoNeighbors[1].weightedEdges[twoNeighbors[0]] = summedWeight
// doesn't affect map iteration
delete(allNodes, [2]int{currentNode.row, currentNode.col})
}
}
// backtrack through graph again
return backtrackThroughGraph(allNodes[[2]int{0, startCol}],
map[*node]bool{}, 0, len(grid)-1)
}
func backtrackThroughGraph(currentNode *node, seen map[*node]bool,
distance int, destinationRow int) int {
// destination row is knowing that there is only one node that is on the
// final row, so if we reach that depth we've reached the end
if currentNode.row == destinationRow {
return distance
}
best := 0
seen[currentNode] = true
for neighbor, weight := range currentNode.weightedEdges {
if seen[neighbor] {
continue
}
best = max(best,
backtrackThroughGraph(neighbor, seen, distance+weight, destinationRow))
}
seen[currentNode] = false
return best
}
func parseInput(input string) (ans [][]string) {
for _, line := range strings.Split(input, "\n") {
ans = append(ans, strings.Split(line, ""))
}
return ans
}
+81
View File
@@ -0,0 +1,81 @@
package main
import (
"testing"
)
var example = `#.#####################
#.......#########...###
#######.#########.#.###
###.....#.>.>.###.#.###
###v#####.#v#.###.#.###
###.>...#.#.#.....#...#
###v###.#.#.#########.#
###...#.#.#.......#...#
#####.#.#.#######.#.###
#.....#.#.#.......#...#
#.#####.#.#.#########v#
#.#...#...#...###...>.#
#.#.#v#######v###.###v#
#...#.>.#...>.>.#.###.#
#####v#.#.###v#.#.###.#
#.....#...#...#.#.#...#
#.#########.###.#.#.###
#...###...#...#...#.###
###.###.#.###v#####v###
#...#...#.#.>.>.#.>.###
#.###.###.#.###.#.#v###
#.....###...###...#...#
#####################.#`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 94,
},
{
name: "actual",
input: input,
want: 2294,
},
}
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: 154,
},
{
name: "actual",
input: input,
want: 6418,
},
}
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)
}
})
}
}
+232
View File
@@ -0,0 +1,232 @@
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, [2]float64{200000000000000, 400000000000000})
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, testRange [2]float64) int {
hailstones := parseInput(input)
ans := 0
// only move forward (per the velocity)
for i, hs1 := range hailstones {
for _, hs2 := range hailstones[i+1:] {
intersection := getIntersectingCoordinates(hs1, hs2)
if intersection == nil {
continue
}
// ensure intersection is in the right direction
// solve for time to reach intersection?
if solveForTimeToReachPoint(hs1, intersection) < 0 || solveForTimeToReachPoint(hs2, intersection) < 0 {
continue
}
if testRange[0] <= intersection[0] && intersection[0] <= testRange[1] &&
testRange[0] <= intersection[1] && intersection[1] <= testRange[1] {
ans++
}
}
}
return ans
}
// does not check for if the lines are the same line
func areHailstonesParallel(hs1, hs2 hailstone) bool {
if hs1.hasVerticalPath && hs2.hasVerticalPath {
return true
}
if hs1.hasVerticalPath || hs2.hasVerticalPath {
return false
}
return hs1.slope == hs2.slope
}
// returns nil slice if the lines do not intersect
func getIntersectingCoordinates(hs1, hs2 hailstone) []float64 {
if areHailstonesParallel(hs1, hs2) {
return nil
}
// assume not the exact same line and that there is only one intersection point
// point-slope line formula
// y - y1 = m(x - x1)
x := (hs1.slope*hs1.x - hs2.slope*hs2.x + hs2.y - hs1.y) / (hs1.slope - hs2.slope)
y := hs1.slope*(x-hs1.x) + hs1.y
return []float64{x, y}
}
func solveForTimeToReachPoint(hs hailstone, point []float64) float64 {
if len(point) != 2 {
panic("expected len == 2 for point slice")
}
// x = vx * t + x0
// t = (intersection_x - x_0) / vx
t := (point[0] - hs.x) / hs.vx
return t
}
func part2(input string) int {
hailstones := parseInput(input)
var possibleRockVelX, possibleRockVelY, possibleRockVelZ []int
for i, hs1 := range hailstones {
for _, hs2 := range hailstones[i+1:] {
if hs1.vx == hs2.vx {
possibilities := getPossibleVelocities(int(hs2.x), int(hs1.x), int(hs1.vx))
if len(possibleRockVelX) == 0 {
possibleRockVelX = possibilities
} else {
possibleRockVelX = getIntersection(possibleRockVelX, possibilities)
}
}
if hs1.vy == hs2.vy {
possibilities := getPossibleVelocities(int(hs2.y), int(hs1.y), int(hs1.vy))
if len(possibleRockVelY) == 0 {
possibleRockVelY = possibilities
} else {
possibleRockVelY = getIntersection(possibleRockVelY, possibilities)
}
}
if hs1.vz == hs2.vz {
possibilities := getPossibleVelocities(int(hs2.z), int(hs1.z), int(hs1.vz))
if len(possibleRockVelZ) == 0 {
possibleRockVelZ = possibilities
} else {
possibleRockVelZ = getIntersection(possibleRockVelZ, possibilities)
}
}
}
}
if len(possibleRockVelX) == 1 && len(possibleRockVelY) == 1 && len(possibleRockVelZ) == 1 {
rockVelX := float64(possibleRockVelX[0])
rockVelY := float64(possibleRockVelY[0])
rockVelZ := float64(possibleRockVelZ[0])
hailstoneA, hailstoneB := hailstones[0], hailstones[1]
mA := (hailstoneA.vy - rockVelY) / (hailstoneA.vx - rockVelX)
mB := (hailstoneB.vy - rockVelY) / (hailstoneB.vx - rockVelX)
cA := hailstoneA.y - (mA * hailstoneA.x)
cB := hailstoneB.y - (mB * hailstoneB.x)
rockX := (cB - cA) / (mA - mB)
rockY := mA*rockX + cA
time := (rockX - hailstoneA.x) / (hailstoneA.vx - rockVelX)
rockZ := hailstoneA.z + (hailstoneA.vz-rockVelZ)*time
return int(rockX + rockY + rockZ)
}
panic("more than one possible velocity in a direction")
}
func getPossibleVelocities(pos1, pos2 int, vel int) []int {
match := []int{}
for possibleVel := -1000; possibleVel < 1000; possibleVel++ {
if possibleVel != vel && (pos1-pos2)%(possibleVel-vel) == 0 {
match = append(match, possibleVel)
}
}
return match
}
func getIntersection(sli1, sli2 []int) []int {
result := []int{}
map2 := map[int]bool{}
for _, val := range sli2 {
map2[val] = true
}
for _, val := range sli1 {
if map2[val] {
result = append(result, val)
}
}
return result
}
func parseInput(input string) (ans []hailstone) {
for _, line := range strings.Split(input, "\n") {
positions := []float64{}
vels := []float64{}
line = strings.ReplaceAll(line, ",", "")
parts := strings.Split(line, " @ ")
for _, posStr := range strings.Fields(parts[0]) {
positions = append(positions, float64(cast.ToInt(posStr)))
}
for _, velStr := range strings.Fields(parts[1]) {
vels = append(vels, float64(cast.ToInt(velStr)))
}
ans = append(ans, makeHailstone(positions[0], positions[1], positions[2], vels[0], vels[1], vels[2]))
}
return ans
}
type hailstone struct {
x, y, z float64
vx, vy, vz float64
hasVerticalPath bool
slope float64
}
func makeHailstone(x, y, z, vx, vy, vz float64) hailstone {
hs := hailstone{
x: x,
y: y,
z: z,
vx: vx,
vy: vy,
vz: vz,
hasVerticalPath: vx == 0,
slope: 0,
}
if !hs.hasVerticalPath {
hs.slope = vy / vx
}
return hs
}
+67
View File
@@ -0,0 +1,67 @@
package main
import (
"testing"
)
var example = `19, 13, 30 @ -2, 1, -2
18, 19, 22 @ -1, -1, -2
20, 25, 34 @ -2, -2, -4
12, 31, 28 @ -1, -2, -1
20, 19, 15 @ 1, -5, -3`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
testRange [2]float64
want int
}{
{
name: "example",
input: example,
testRange: [2]float64{7, 27},
want: 2,
},
{
name: "actual",
input: input,
testRange: [2]float64{200000000000000, 400000000000000},
want: 31921,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := part1(tt.input, tt.testRange); 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
}{
// example input is not big enough for this logic to work
// {
// name: "example",
// input: example,
// want: 47,
// },
{
name: "actual",
input: input,
want: 761691907059631,
},
}
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)
}
})
}
}
+226
View File
@@ -0,0 +1,226 @@
package main
import (
_ "embed"
"flag"
"fmt"
"math/rand"
"strings"
"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 {
graph := parseInput(input)
// if brute forcing, can check if the separated nodes are able to traverse to each other
// which would indicate that it has broken an internal (to a group) edge, and not separated
// the overall group...
// brute forcing 3k edges for my input with 3 traversals for each...
// for n = total edges
// O(n^4), triple nested for loop with traversal check in each
// O(3k^4) = 81,000,000,000,000 way too slow
// strategy actually used:
// pick 200 random pairs of nodes, traverse between the two of them
// the most traversed nodes will likely be the bridges
// a similar strategy would be to BFS traverse from a few randomly selected nodes to the furthest
// possible node, the furthest depth away is likely in the other group (assuming a kind input)
// and therefore those paths can be used to tabulate the most trafficked edges aka 3 bridges
var allNodes []string
for n := range graph {
allNodes = append(allNodes, n)
}
// for a small (example) graph just pick every node
// for a large (actual input) graph, pick 200 nodes
pairsToPick := min(200, len(graph)*(len(graph)-1))
traversedPairs := map[string]bool{}
timesEdgeTraversed := map[string]int{}
for len(traversedPairs) < pairsToPick {
i1 := rand.Intn(len(allNodes))
i2 := rand.Intn(len(allNodes))
if i1 == i2 {
continue
}
n1 := allNodes[i1]
n2 := allNodes[i2]
randomPairName := sortedEdgeName(n1, n2)
if traversedPairs[randomPairName] {
continue
}
traversedPairs[randomPairName] = true
path := findShortestPath(graph, n1, n2)
for i := 1; i < len(path); i++ {
timesEdgeTraversed[sortedEdgeName(path[i-1], path[i])]++
}
}
threeMostTraffickedEdges := getThreeMostTraffickedEdges(timesEdgeTraversed)
// remove edge
for _, edge := range threeMostTraffickedEdges {
nodes := strings.Split(edge, " ")
graph[nodes[0]] = removeElementFromSlice(graph[nodes[0]], nodes[1])
graph[nodes[1]] = removeElementFromSlice(graph[nodes[1]], nodes[0])
}
sizes := []int{}
for _, node := range strings.Split(threeMostTraffickedEdges[0], " ") {
sizes = append(sizes, getGroupSize(graph, node, map[string]bool{}))
}
if len(sizes) != 2 {
panic("expected two groups")
}
return sizes[0] * sizes[1]
}
func sortedEdgeName(node1, node2 string) string {
lower := min(node1, node2)
higher := max(node1, node2)
return fmt.Sprintf("%v %v", lower, higher)
}
func findShortestPath(graph map[string][]string, start, end string) []string {
type dfsNode struct {
current string
pathSoFar []string
}
seen := map[string]bool{}
queue := []dfsNode{
{
current: start,
pathSoFar: []string{start},
},
}
for len(queue) > 0 {
popped := queue[0]
queue = queue[1:]
if seen[popped.current] {
continue
}
seen[popped.current] = true
if popped.current == end {
return popped.pathSoFar
}
for _, neighbor := range graph[popped.current] {
if seen[neighbor] {
continue
}
nextNode := dfsNode{
current: neighbor,
pathSoFar: append([]string{}, popped.pathSoFar...), // deep copy
}
nextNode.pathSoFar = append(nextNode.pathSoFar, neighbor)
queue = append(queue, nextNode)
}
}
panic("expect return from loop")
}
func getThreeMostTraffickedEdges(timesEdgeTraversed map[string]int) []string {
ans := []string{}
for len(ans) < 3 {
var bestEdge string
var bestCount int
for edge, count := range timesEdgeTraversed {
if count > bestCount {
bestCount = count
bestEdge = edge
}
}
ans = append(ans, bestEdge)
delete(timesEdgeTraversed, bestEdge)
}
return ans
}
func removeElementFromSlice(sli []string, ele string) []string {
for i, n := range sli {
if n == ele {
sli[len(sli)-1], sli[i] = sli[i], sli[len(sli)-1]
sli = sli[:len(sli)-1]
return sli
}
}
panic("element not found")
}
func getGroupSize(graph map[string][]string, node string, seen map[string]bool) int {
if seen[node] {
return 0
}
size := 1
seen[node] = true
for _, neighbor := range graph[node] {
size += getGroupSize(graph, neighbor, seen)
}
return size
}
func part2(input string) string {
return "happiness"
}
func parseInput(input string) (graph map[string][]string) {
graph = map[string][]string{}
for _, line := range strings.Split(input, "\n") {
parts := strings.Split(line, ": ")
for _, node := range strings.Split(parts[1], " ") {
graph[parts[0]] = append(graph[parts[0]], node)
graph[node] = append(graph[node], parts[0])
}
}
return graph
}
+71
View File
@@ -0,0 +1,71 @@
package main
import (
"testing"
)
var example = `jqt: rhn xhk nvd
rsh: frs pzl lsr
xhk: hfx
cmg: qnr nvd lhk bvb
rhn: xhk bvb hfx
bvb: xhk hfx
pzl: lsr hfx nvd
qnr: nvd
ntq: jqt hfx bvb xhk
nvd: lhk
lsr: lhk
rzs: qnr cmg lsr rsh
frs: qnr lhk lsr`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 54,
},
{
name: "actual",
input: input,
want: 567606,
},
}
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 string
}{
{
name: "example",
input: example,
want: "happiness",
},
{
name: "actual",
input: input,
want: "happiness",
},
}
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)
}
})
}
}
+78
View File
@@ -0,0 +1,78 @@
package main
import (
_ "embed"
"flag"
"fmt"
"sort"
"strings"
"github.com/alexchao26/advent-of-code-go/cast"
"github.com/alexchao26/advent-of-code-go/mathy"
"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 {
list1, list2 := parseInput(input)
sort.Ints(list1)
sort.Ints(list2)
ans := 0
for i := range len(list1) {
ans += mathy.AbsInt(list2[i] - list1[i])
}
return ans
}
func part2(input string) int {
list1, list2 := parseInput(input)
countsList2 := map[int]int{}
for _, v := range list2 {
countsList2[v]++
}
ans := 0
for _, v := range list1 {
ans += v * countsList2[v]
}
return ans
}
func parseInput(input string) (list1, list2 []int) {
for _, line := range strings.Split(input, "\n") {
nums := strings.Split(line, " ")
list1 = append(list1, cast.ToInt(nums[0]))
list2 = append(list2, cast.ToInt(nums[1]))
}
return list1, list2
}
+64
View File
@@ -0,0 +1,64 @@
package main
import (
"testing"
)
var example = `3 4
4 3
2 5
1 3
3 9
3 3`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 11,
},
{
name: "actual",
input: input,
want: 1651298,
},
}
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: 31,
},
{
name: "actual",
input: input,
want: 21306195,
},
}
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)
}
})
}
}
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
from typing import List
example = """7 6 4 2 1
1 2 7 8 9
9 7 6 2 1
1 3 2 4 5
8 6 4 4 1
1 3 6 7 9"""
def part1(grid: List[List[int]]) -> int:
valid_levels = 0
for level in grid:
if test_level(level):
valid_levels += 1
return valid_levels
def part2(grid: List[List[int]]) -> int:
# tolerate one bad level...
valid_levels: int = 0
for level in grid:
for i in range(len(level)):
newLevel: List[int] = level.copy()
newLevel.pop(i) # removes i-th element?, how convenient..
if test_level(newLevel):
valid_levels += 1
break
return valid_levels
def test_level(level: List[int]) -> bool:
is_increasing = level[1] > level[0]
is_valid = True
# The levels are either all increasing or all decreasing.
# Any two adjacent levels differ by at least one and at most three.
for i in range(1, len(level)):
if is_increasing and level[i] <= level[i - 1]:
return False
elif not is_increasing and level[i] >= level[i - 1]:
return False
diff = abs(level[i] - level[i - 1])
if diff < 1 or diff > 3:
return False
return is_valid
def convert_input(input: str) -> List[List[int]]:
grid: List[List[int]] = []
for level in input.splitlines():
grid.append([int(part) for part in level.split(" ")])
# converted_level: List[int] = []
# this syntax is going to take getting used to...
# for part in level.split(" "):
# converted_level.append(int(part))
# grid.append(converted_level)
return grid
example_grid = convert_input(example)
print("example part1:", part1(example_grid), "want", 2)
input = open("input.txt", "r").read()
grid = convert_input(input)
print("part1:", part1(grid), "want", 585)
print("example part2:", part2(example_grid), "want", 4)
print("part2:", part2(grid), "want", 626)
+106
View File
@@ -0,0 +1,106 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"github.com/alexchao26/advent-of-code-go/cast"
"github.com/alexchao26/advent-of-code-go/mathy"
"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 {
grid := convertInput(input)
validLevels := 0
for _, level := range grid {
if testLevel(level) {
validLevels += 1
}
}
return validLevels
}
func part2(input string) int {
grid := convertInput(input)
// tolerate one bad level...
validLevels := 0
for _, level := range grid {
for i := range len(level) {
newLevel := []int{}
for j := range len(level) {
if i != j {
newLevel = append(newLevel, level[j])
}
}
if testLevel(newLevel) {
validLevels += 1
break
}
}
}
return validLevels
}
func testLevel(level []int) bool {
isIncreasing := level[1] > level[0]
// The levels are either all increasing or all decreasing.
// Any two adjacent levels differ by at least one and at most three.
for i := 1; i < len(level); i++ {
if isIncreasing && level[i] <= level[i-1] {
return false
} else if !isIncreasing && level[i] >= level[i-1] {
return false
}
diff := mathy.AbsInt(level[i] - level[i-1])
if diff < 1 || diff > 3 {
return false
}
}
return true
}
func convertInput(input string) [][]int {
grid := [][]int{}
for _, line := range strings.Split(input, "\n") {
level := []int{}
for _, n := range strings.Split(line, " ") {
level = append(level, cast.ToInt(n))
}
grid = append(grid, level)
}
return grid
}
+64
View File
@@ -0,0 +1,64 @@
package main
import (
"testing"
)
var example = `7 6 4 2 1
1 2 7 8 9
9 7 6 2 1
1 3 2 4 5
8 6 4 4 1
1 3 6 7 9`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 2,
},
{
name: "actual",
input: input,
want: 585,
},
}
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: 4,
},
{
name: "actual",
input: input,
want: 626,
},
}
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)
}
})
}
}
+43
View File
@@ -0,0 +1,43 @@
import re
def part1(input: str) -> int:
matches = re.findall(r"mul\(\d+,\d+\)", input)
ans = 0
for match in matches:
ans += exec_mul(match)
return ans
def part2(input: str) -> int:
matches = re.findall(r"(do\(\)|don\'t\(\)|mul\(\d+,\d+\))", input)
do = True
ans = 0
for match in matches:
if match == "do()":
do = True
elif match == "don't()":
do = False
elif do:
ans += exec_mul(match)
return ans
def exec_mul(s: str) -> int:
nums = re.findall(r"\d+", s)
return int(nums[0]) * int(nums[1])
example = "xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))"
print("example part1:", part1(example), "want", 161)
input = open("input.txt").read()
print("part1:", part1(input), "want", 169021493)
example2 = "xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))"
print("example part2:", part2(example2), "want", 48)
print("part2:", part2(input), "want", 111762583)

Some files were not shown because too many files have changed in this diff Show More