mirror of
https://github.com/Threnklyn/advent-of-code-go.git
synced 2026-05-18 19:13:27 +02:00
dump 15 17 18
This commit is contained in:
@@ -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,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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user