dump 15 17 18

This commit is contained in:
alexchao26
2022-12-25 12:21:02 -05:00
parent 1ed6126f10
commit f26777edc0
6 changed files with 909 additions and 0 deletions
+174
View File
@@ -0,0 +1,174 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"github.com/alexchao26/advent-of-code-go/mathy"
"github.com/alexchao26/advent-of-code-go/util"
)
//go:embed input.txt
var input string
func init() {
// do this in init (not main) so test file has same input
input = strings.TrimRight(input, "\n")
if len(input) == 0 {
panic("empty input.txt file")
}
}
func main() {
var part int
flag.IntVar(&part, "part", 1, "part 1 or 2")
flag.Parse()
fmt.Println("Running part", part)
if part == 1 {
ans := part1(input, 2000000)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
} else {
ans := part2(input, 4000000)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
}
}
// x is col, y is row
// unbounded grid...
// In the row where y=2000000, how many positions canNOT contain a beacon?
//
// very naive approach of marking each coord that is visible from some sensor
// then remove all beacons that are on the target row
// then just return the length of the map containing "seen" cells on target row
//
// this is brutally slow, no way this approach works for part 2
func part1(input string, targetRow int) int {
pairs := parseInput(input)
blockedCoords := map[[2]int]bool{}
for _, p := range pairs {
manhattanDist := mathy.ManhattanDistance(p.beaconRow, p.beaconCol,
p.sensorRow, p.sensorCol)
// if target row is reachable, block coords on it...
blockable := manhattanDist - mathy.AbsInt(p.sensorRow-targetRow)
if blockable > 0 {
for i := 0; i <= blockable; i++ {
// add blocks to map in both left and right directions
blockedCoords[[2]int{
targetRow, p.sensorCol - i,
}] = true
blockedCoords[[2]int{
targetRow, p.sensorCol + i,
}] = true
}
}
}
// remove any beacons that are present in the input?
for _, p := range pairs {
delete(blockedCoords, [2]int{p.beaconRow, p.beaconCol})
}
return len(blockedCoords)
}
func part2(input string, coordLimit int) int {
pairs := parseInput(input)
sensors := []parsedSensor{}
for _, p := range pairs {
sensors = append(sensors, parsedSensor{
sensorRow: p.sensorRow,
sensorCol: p.sensorCol,
manhattanDist: mathy.ManhattanDistance(p.sensorCol, p.sensorRow,
p.beaconCol, p.beaconRow),
})
}
// search space is too large to iterate over the entire thing and check if
// SOME sensor can see that location...
//
// we can assume that the final resting point will be 1 cell away from the
// border of a (actually multiple) sensor. this runs under the assumption
// that there is only one answer
for _, sensor := range sensors {
distPlusOne := sensor.manhattanDist + 1
// checking in this pattern w/ manhattan distance of 1
// 1
// 2 3
// 4 S 5
// 6B7
// 8
for r := -distPlusOne; r <= distPlusOne; r++ {
targetRow := sensor.sensorRow + r
if targetRow < 0 {
continue
}
if targetRow > coordLimit {
break
}
// check left and right on the target row
// zero for first and last r's... then subtract or add it from the
// sensor's col
colOffset := distPlusOne - mathy.AbsInt(r)
colLeft := sensor.sensorCol - colOffset
colRight := sensor.sensorCol + colOffset
if colLeft >= 0 && colLeft <= coordLimit &&
!isReachable(sensors, colLeft, targetRow) {
return colLeft*4000000 + targetRow
}
if colRight >= 0 && colRight <= coordLimit &&
!isReachable(sensors, colRight, targetRow) {
return colRight*4000000 + targetRow
}
}
}
panic("unreachable")
}
type pair struct {
sensorRow, sensorCol int
beaconRow, beaconCol int
}
func parseInput(input string) (ans []pair) {
// Sensor at x=2150774, y=3136587: closest beacon is at x=2561642, y=2914773
for _, line := range strings.Split(input, "\n") {
p := pair{}
_, err := fmt.Sscanf(line,
"Sensor at x=%d, y=%d: closest beacon is at x=%d, y=%d",
&p.sensorCol, &p.sensorRow, &p.beaconCol, &p.beaconRow)
if err != nil {
panic("parsing: " + err.Error())
}
ans = append(ans, p)
}
return ans
}
type parsedSensor struct {
sensorRow, sensorCol int
manhattanDist int
}
func isReachable(sensors []parsedSensor, c, r int) bool {
for _, sensor := range sensors {
// if reachable, break
if sensor.manhattanDist >= mathy.ManhattanDistance(c, r,
sensor.sensorCol, sensor.sensorRow) {
return true
}
}
return false
}
+78
View File
@@ -0,0 +1,78 @@
package main
import (
"testing"
)
var example = `Sensor at x=2, y=18: closest beacon is at x=-2, y=15
Sensor at x=9, y=16: closest beacon is at x=10, y=16
Sensor at x=13, y=2: closest beacon is at x=15, y=3
Sensor at x=12, y=14: closest beacon is at x=10, y=16
Sensor at x=10, y=20: closest beacon is at x=10, y=16
Sensor at x=14, y=17: closest beacon is at x=10, y=16
Sensor at x=8, y=7: closest beacon is at x=2, y=10
Sensor at x=2, y=0: closest beacon is at x=2, y=10
Sensor at x=0, y=11: closest beacon is at x=2, y=10
Sensor at x=20, y=14: closest beacon is at x=25, y=17
Sensor at x=17, y=20: closest beacon is at x=21, y=22
Sensor at x=16, y=7: closest beacon is at x=15, y=3
Sensor at x=14, y=3: closest beacon is at x=15, y=3
Sensor at x=20, y=1: closest beacon is at x=15, y=3`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
targetRow int
want int
}{
{
name: "example",
input: example,
targetRow: 10,
want: 26,
},
{
name: "actual",
input: input,
targetRow: 2000000,
want: 4560025,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := part1(tt.input, tt.targetRow); got != tt.want {
t.Errorf("part1() = %v, want %v", got, tt.want)
}
})
}
}
func Test_part2(t *testing.T) {
tests := []struct {
name string
input string
coordLimit int
want int
}{
{
name: "example",
input: example,
coordLimit: 20,
want: 56000011,
},
{
name: "actual",
input: input,
coordLimit: 4000000,
want: 12480406634249,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := part2(tt.input, tt.coordLimit); got != tt.want {
t.Errorf("part2() = %v, want %v", got, tt.want)
}
})
}
}
+312
View File
@@ -0,0 +1,312 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"github.com/alexchao26/advent-of-code-go/cast"
"github.com/alexchao26/advent-of-code-go/mathy"
"github.com/alexchao26/advent-of-code-go/util"
)
//go:embed input.txt
var input string
func init() {
// do this in init (not main) so test file has same input
input = strings.TrimRight(input, "\n")
if len(input) == 0 {
panic("empty input.txt file")
}
}
func main() {
var part int
flag.IntVar(&part, "part", 1, "part 1 or 2")
flag.Parse()
fmt.Println("Running part", part)
if part == 1 {
ans := part1(input)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
} else {
ans := part2(input, 1000000000000)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
}
}
func part1(input string) int {
// ####
// .#.
// ###
// .#.
// ..#
// ..#
// ###
// #
// #
// #
// #
// ##
// ##
// chamber is 7 units wide
// rocks fall w/ 2 spaces to left wall, 3 spaces to next rock or floor
// rocks pushed by wind, then fall down 1 space
// WHEN rock touches something below (ground or another settled rock), rock is "done" and next rock appears
// this also means that rocks will be pushed once L/R before fully settling (unless blocked L/R)
s := newState(input)
for i := 0; i < 2022; i++ {
s.dropRock()
}
// height after 2022 rocks fall
return s.highestSettledRow + 1 // 1 indexed
}
func part2(input string, wantedRocks int) int {
s := newState(input)
// obviously can't do the calculations literally anymore...
// need some kind of "last time this state was seen" check to skip steps
// need to hash states
// - the top 10ish rows should be plenty
// - rock index
// - value = steps since last seen AND highestRow to calc skips
// [2]int{steps when last seen, rocks dropped, highestRow to calc diff}
pastStates := map[string][3]int{}
// will keep track of rows that are mathematically skipped otherwise it'll
// mess with state and the settled matrix
dupeRows := 0
rocksDropped := 0
for rocksDropped < wantedRocks {
s.dropRock()
rocksDropped++
h := s.hash(20)
if past, ok := pastStates[h]; ok {
pastSteps, pastRocksDropped, pastHighRow := past[0], past[1], past[2]
stepsToSkip := s.stepIndex - pastSteps
rocksToSkip := rocksDropped - pastRocksDropped
rowsToAdd := s.highestSettledRow - pastHighRow
iterationsToSkip := (wantedRocks - rocksDropped) / rocksToSkip
dupeRows += rowsToAdd * iterationsToSkip
s.stepIndex += stepsToSkip * iterationsToSkip
rocksDropped += rocksToSkip * iterationsToSkip
} else {
pastStates[h] = [3]int{
s.stepIndex, rocksDropped, s.highestSettledRow,
}
}
}
// height after 2022 rocks fall
return s.highestSettledRow + 1 + dupeRows // 1 indexed
}
type state struct {
settled [][]string
highestSettledRow int
fallingCoords [][2]int
nextRockIndex int
steps []string
stepIndex int
}
func newState(input string) state {
s := state{
settled: [][]string{},
highestSettledRow: -1,
fallingCoords: nil,
nextRockIndex: 0,
steps: strings.Split(input, ""),
stepIndex: 0,
}
return s
}
// knew I'd need this for debugging...
func (s state) printState() {
copySettled := [][]string{}
for _, row := range s.settled {
copyRow := make([]string, len(row))
copy(copyRow, row)
copySettled = append(copySettled, copyRow)
}
for _, coord := range s.fallingCoords {
copySettled[coord[0]][coord[1]] = "@"
}
var sb strings.Builder
for r := len(copySettled) - 1; r >= 0; r-- {
sb.WriteString(strings.Join(copySettled[r], "") + cast.ToString(r) + "\n")
}
fmt.Println(sb.String())
}
func (s *state) dropRock() {
s.populateNextBaseCoords()
highestRow := 0
for _, c := range s.fallingCoords {
highestRow = mathy.MaxInt(highestRow, c[0])
}
for len(s.settled) <= highestRow {
s.settled = append(s.settled, newEmptyRow())
}
// will be set back to nil when settled
for s.fallingCoords != nil {
switch s.steps[s.stepIndex%len(s.steps)] {
case ">":
// check if can move right
canMoveRight := true
for _, c := range s.fallingCoords {
if c[1] == 6 || s.settled[c[0]][c[1]+1] != "." {
canMoveRight = false
}
}
if canMoveRight {
for i := range s.fallingCoords {
s.fallingCoords[i][1]++
}
}
case "<":
// check if can move left
canMoveLeft := true
for _, c := range s.fallingCoords {
if c[1] == 0 || s.settled[c[0]][c[1]-1] != "." {
canMoveLeft = false
}
}
if canMoveLeft {
for i := range s.fallingCoords {
s.fallingCoords[i][1]--
}
}
default:
panic(s.steps[s.stepIndex])
}
s.stepIndex++
// move down
canMoveDown := true
for _, c := range s.fallingCoords {
if c[0] == 0 || s.settled[c[0]-1][c[1]] != "." {
canMoveDown = false
}
}
// is blocked, draw onto settled then make nil
if !canMoveDown {
for _, c := range s.fallingCoords {
s.settled[c[0]][c[1]] = "#"
}
s.fallingCoords = nil
for r := len(s.settled) - 1; r >= 0; r-- {
if strings.Join(s.settled[r], "") != "......." {
s.highestSettledRow = r
break
}
}
} else {
for i := range s.fallingCoords {
s.fallingCoords[i][0]--
}
}
}
}
func newEmptyRow() []string {
row := make([]string, 7)
for i := range row {
row[i] = "."
}
return row
}
var baseCoords = [][][2]int{
{
// line ####
{0, 0},
{0, 1},
{0, 2},
{0, 3},
}, {
// plus
{0, 1},
{1, 0},
{1, 1},
{1, 2},
{2, 1},
}, {
// flipped L
{0, 0},
{0, 1},
{0, 2},
{1, 2},
{2, 2},
}, {
// vert line
{0, 0},
{1, 0},
{2, 0},
{3, 0},
}, {
// square
{0, 0},
{0, 1},
{1, 0},
{1, 1},
},
}
func init() {
// add 2 cols to all baseCoords because they fall 2 off of left wall
for i := range baseCoords {
for j := range baseCoords[i] {
baseCoords[i][j][1] += 2
}
}
}
func (s *state) populateNextBaseCoords() {
copyCoords := make([][2]int, len(baseCoords[s.nextRockIndex]))
copy(copyCoords, baseCoords[s.nextRockIndex])
s.nextRockIndex++
s.nextRockIndex %= 5
// lowest row of baseCoords...
for i := range copyCoords {
copyCoords[i][0] += s.highestSettledRow + 1 + 3
}
s.fallingCoords = copyCoords
}
// for part 2 to find return states
// NOTE: had to play with the number of rows to be hashed... 20 seems to
// work on the example input
func (s *state) hash(topRowsToHash int) string {
var sb strings.Builder
sb.WriteString(cast.ToString(s.nextRockIndex))
for r := s.highestSettledRow; r >= 0 && r > s.highestSettledRow-topRowsToHash; r-- {
sb.WriteString("\n" + strings.Join(s.settled[r], ""))
}
return sb.String()
}
+75
View File
@@ -0,0 +1,75 @@
package main
import (
"testing"
)
var example = `>>><<><>><<<>><>>><<<>>><<<><<<>><>><<>>`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 3068,
},
{
name: "actual",
input: input,
want: 3219,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := part1(tt.input); got != tt.want {
t.Errorf("part1() = %v, want %v", got, tt.want)
}
})
}
}
func Test_part2(t *testing.T) {
tests := []struct {
name string
input string
wantedRocks int
want int
}{
// part 1 values should work as well, so using them for extra testing
{
name: "example",
input: example,
wantedRocks: 2022,
want: 3068,
},
{
name: "actual",
input: input,
wantedRocks: 2022,
want: 3219,
},
{
name: "example",
input: example,
wantedRocks: 1000000000000,
want: 1514285714288,
},
{
name: "actual",
input: input,
wantedRocks: 1000000000000,
want: 1582758620701,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := part2(tt.input, tt.wantedRocks); got != tt.want {
t.Errorf("part2() = %v, want %v", got, tt.want)
}
})
}
}
+171
View File
@@ -0,0 +1,171 @@
package main
import (
_ "embed"
"flag"
"fmt"
"strings"
"github.com/alexchao26/advent-of-code-go/cast"
"github.com/alexchao26/advent-of-code-go/mathy"
"github.com/alexchao26/advent-of-code-go/util"
)
//go:embed input.txt
var input string
func init() {
// do this in init (not main) so test file has same input
input = strings.TrimRight(input, "\n")
if len(input) == 0 {
panic("empty input.txt file")
}
}
func main() {
var part int
flag.IntVar(&part, "part", 1, "part 1 or 2")
flag.Parse()
fmt.Println("Running part", part)
if part == 1 {
ans := part1(input)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
} else {
ans := part2(input)
util.CopyToClipboard(fmt.Sprintf("%v", ans))
fmt.Println("Output:", ans)
}
}
// to six adjacent face...
var diffs = [][3]int{
{0, 0, 1},
{0, 1, 0},
{1, 0, 0},
{0, 0, -1},
{0, -1, 0},
{-1, 0, 0},
}
func part1(input string) int {
rawCoords := parseInput(input)
mapCoords := convertRawCoordsToMap(rawCoords)
totalSurfaceArea := 0
for _, coord := range rawCoords {
neighbors := 6
for _, d := range diffs {
if mapCoords[[3]int{
coord[0] - d[0],
coord[1] - d[1],
coord[2] - d[2],
}] {
neighbors--
}
}
totalSurfaceArea += neighbors
}
return totalSurfaceArea
}
func part2(input string) int {
rawCoords := parseInput(input)
mapCoords := convertRawCoordsToMap(rawCoords)
// get bounds
var limitX, limitY, limitZ int
for c := range mapCoords {
limitX = mathy.MaxInt(limitX, c[0])
limitY = mathy.MaxInt(limitY, c[1])
limitZ = mathy.MaxInt(limitZ, c[2])
}
// bfs to see if an edge can be reached
// delete if not useful
totalExternalSurfaceArea := 0
for coord := range mapCoords {
totalExternalSurfaceArea += facesThatCanReachEdge(coord, mapCoords,
limitX, limitY, limitZ)
}
// too low: 1036
return totalExternalSurfaceArea
}
func parseInput(input string) (ans [][3]int) {
for _, line := range strings.Split(input, "\n") {
parts := strings.Split(line, ",")
ans = append(ans, [3]int{
cast.ToInt(parts[0]),
cast.ToInt(parts[1]),
cast.ToInt(parts[2]),
})
}
return ans
}
func convertRawCoordsToMap(rawCoords [][3]int) map[[3]int]bool {
set := map[[3]int]bool{}
for _, coord := range rawCoords {
set[coord] = true
}
return set
}
// there would be a big optimization here to keep track of all coords that have
// a known path to an edge, that would eliminate a lot of duplicate work... but
// i think this is a small enough problem space to ignore that...
func facesThatCanReachEdge(coord [3]int, set map[[3]int]bool, limitX, limitY, limitZ int) int {
ans := 0
for _, d := range diffs {
next := [3]int{
coord[0] + d[0],
coord[1] + d[1],
coord[2] + d[2],
}
reachResult := canReachEdge(next, set, limitX, limitY, limitZ)
if reachResult {
ans++
}
}
return ans
}
func canReachEdge(coord [3]int, set map[[3]int]bool, limitX, limitY, limitZ int,
) bool {
queue := [][3]int{coord}
seen := map[[3]int]bool{}
for len(queue) > 0 {
front := queue[0]
queue = queue[1:]
// seen already or hit some other droplet, skip
if seen[front] || set[front] {
continue
}
seen[front] = true
// edge reached
if front[0] <= 0 || front[0] >= limitX ||
front[1] <= 0 || front[1] >= limitY ||
front[2] <= 0 || front[2] >= limitZ {
return true
}
for _, d := range diffs {
next := [3]int{
front[0] + d[0],
front[1] + d[1],
front[2] + d[2],
}
queue = append(queue, next)
}
}
return false
}
+99
View File
@@ -0,0 +1,99 @@
package main
import (
"testing"
)
var example = `2,2,2
1,2,2
3,2,2
2,1,2
2,3,2
2,2,1
2,2,3
2,2,4
2,2,6
1,2,5
3,2,5
2,1,5
2,3,5`
func Test_part1(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "example",
input: example,
want: 64,
},
{
name: "actual",
input: input,
want: 4636,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := part1(tt.input); got != tt.want {
t.Errorf("part1() = %v, want %v", got, tt.want)
}
})
}
}
var flatDisc = `5,5,5
5,5,6
5,5,7
5,6,5
5,6,6
5,6,7
5,7,5
5,7,6
5,7,7`
// 3x3x1 disc...
func Test_part2(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{
name: "dumb simple",
input: "1,1,1\n2,1,1",
want: 10,
},
{
name: "dumber simpleer",
input: "2,1,1",
want: 6,
},
{
name: "example",
input: example,
want: 58,
},
{
name: "flatDisc",
input: flatDisc,
// 9 + 9 + 3 * 4 = 30
want: 30,
},
{
name: "actual",
input: input,
// PAIN, used coord instead of front in the bfs check :/
want: 2572,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := part2(tt.input); got != tt.want {
t.Errorf("part2() = %v, want %v", got, tt.want)
}
})
}
}