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