mirror of
https://github.com/Luzifer/aoc2019.git
synced 2024-12-22 05:51:16 +00:00
Add solution for Day 10
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
d11d14dfb9
commit
c5e14ed7f4
5 changed files with 343 additions and 0 deletions
213
day10.go
Normal file
213
day10.go
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
package aoc2019
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type day10MonitorGrid struct {
|
||||||
|
asteroidMap []byte
|
||||||
|
width int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d day10MonitorGrid) asteroidCount() int {
|
||||||
|
var count int
|
||||||
|
for _, c := range d.asteroidMap {
|
||||||
|
if c == '#' {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d day10MonitorGrid) clone() *day10MonitorGrid {
|
||||||
|
var grid = &day10MonitorGrid{asteroidMap: make([]byte, len(d.asteroidMap)), width: d.width}
|
||||||
|
for i, c := range d.asteroidMap {
|
||||||
|
grid.asteroidMap[i] = c
|
||||||
|
}
|
||||||
|
return grid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d day10MonitorGrid) getAsteroidPositions() []int {
|
||||||
|
var knownPositions []int
|
||||||
|
for i, c := range d.asteroidMap {
|
||||||
|
if c != '#' {
|
||||||
|
// Not an asteroid, don't care
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
knownPositions = append(knownPositions, i)
|
||||||
|
}
|
||||||
|
return knownPositions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d day10MonitorGrid) getCleanedGrid(x, y int) *day10MonitorGrid {
|
||||||
|
// Clone the map to work on
|
||||||
|
var grid = d.clone()
|
||||||
|
|
||||||
|
// Collect positions of all known asteroids
|
||||||
|
var knownPositions = grid.getAsteroidPositions()
|
||||||
|
|
||||||
|
// Mark observer (does not count into observable asteroids)
|
||||||
|
grid.asteroidMap[grid.coordToPos(x, y)] = '@'
|
||||||
|
|
||||||
|
// Iterate all positions and remove covered (invisible) asteroids
|
||||||
|
for _, pos := range knownPositions {
|
||||||
|
var aX, aY = d.posToCoord(pos)
|
||||||
|
if grid.isObstructed(x, y, aX, aY) {
|
||||||
|
grid.asteroidMap[pos] = '-'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return grid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *day10MonitorGrid) isObstructed(observX, observY, x, y int) bool {
|
||||||
|
var distX, distY = x - observX, y - observY
|
||||||
|
|
||||||
|
if distX == 0 && distY == 0 {
|
||||||
|
// No steps, observer equals asteroid, needless calculation
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
div = int(math.Abs(float64(greatestCommonDivisor(int64(distX), int64(distY)))))
|
||||||
|
stepX, stepY = distX / div, distY / div
|
||||||
|
)
|
||||||
|
|
||||||
|
for i := 1; i < math.MaxInt64; i++ {
|
||||||
|
var rPosX, rPosY = observX + stepX*i, observY + stepY*i
|
||||||
|
|
||||||
|
if rPosX < 0 || rPosX >= d.width || rPosY < 0 || rPosY >= len(d.asteroidMap)/d.width {
|
||||||
|
// Position outside grid, stop searching
|
||||||
|
panic(errors.Errorf("Observed position ran out of bounds (obsX=%d obsY=%d x=%d y=%d div=%d stepX=%d stepY=%d)", observX, observY, x, y, div, stepX, stepY))
|
||||||
|
}
|
||||||
|
|
||||||
|
if rPosX == x && rPosY == y {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.asteroidMap[d.coordToPos(rPosX, rPosY)] == '#' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(errors.Errorf("Unreachable end was reached"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d day10MonitorGrid) coordToPos(x, y int) int { return y*d.width + x }
|
||||||
|
func (d day10MonitorGrid) posToCoord(pos int) (int, int) { return pos % d.width, pos / d.width }
|
||||||
|
func (d day10MonitorGrid) step2deg(x, y int) float64 {
|
||||||
|
rad := math.Atan2(float64(x), float64(y))
|
||||||
|
deg := rad * (180 / math.Pi)
|
||||||
|
return 180 + -1*deg
|
||||||
|
}
|
||||||
|
|
||||||
|
func day10ReadAsteroidMap(in io.Reader) (*day10MonitorGrid, error) {
|
||||||
|
var grid = &day10MonitorGrid{}
|
||||||
|
|
||||||
|
raw, err := ioutil.ReadAll(in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Unable to read asteroid map")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range raw {
|
||||||
|
if c == '\n' {
|
||||||
|
if grid.width == 0 {
|
||||||
|
grid.width = len(grid.asteroidMap)
|
||||||
|
}
|
||||||
|
// Skip newlines for our representation
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.asteroidMap = append(grid.asteroidMap, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return grid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func solveDay10Part1Coordinate(inFile string) (*day10MonitorGrid, int, error) {
|
||||||
|
f, err := os.Open(inFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, errors.Wrap(err, "Unable to open input file")
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
grid, err := day10ReadAsteroidMap(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, errors.Wrap(err, "Unable to read asteroid map")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
bestMonitorPos int
|
||||||
|
bestAsteroidCount int
|
||||||
|
)
|
||||||
|
for _, pos := range grid.getAsteroidPositions() {
|
||||||
|
var aX, aY = grid.posToCoord(pos)
|
||||||
|
rGrid := grid.getCleanedGrid(aX, aY)
|
||||||
|
|
||||||
|
if c := rGrid.asteroidCount(); c > bestAsteroidCount {
|
||||||
|
bestMonitorPos = pos
|
||||||
|
bestAsteroidCount = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return grid, bestMonitorPos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func solveDay10Part1(inFile string) (int, error) {
|
||||||
|
grid, bestMonitorPos, err := solveDay10Part1Coordinate(inFile)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var aX, aY = grid.posToCoord(bestMonitorPos)
|
||||||
|
return grid.getCleanedGrid(aX, aY).asteroidCount(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func solveDay10Part2(inFile string) (int, error) {
|
||||||
|
grid, bestMonitorPos, err := solveDay10Part1Coordinate(inFile)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
mX, mY = grid.posToCoord(bestMonitorPos)
|
||||||
|
destroyed []int
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mark monitor / laser -- cannot be destroyed
|
||||||
|
grid.asteroidMap[bestMonitorPos] = 'M'
|
||||||
|
|
||||||
|
// Gradually destroy asteroids
|
||||||
|
for grid.asteroidCount() > 0 {
|
||||||
|
asteroidsInSight := grid.getCleanedGrid(mX, mY).getAsteroidPositions()
|
||||||
|
var degPos [][2]int
|
||||||
|
for _, pos := range asteroidsInSight {
|
||||||
|
var aX, aY = grid.posToCoord(pos)
|
||||||
|
|
||||||
|
degPos = append(degPos, [2]int{
|
||||||
|
pos,
|
||||||
|
int(grid.step2deg(aX-mX, aY-mY) * 1000000), // Degree to asteroid in 6-digit precision
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by degree low-to-high -- represents order of destruction
|
||||||
|
sort.Slice(degPos, func(i, j int) bool { return degPos[i][1] < degPos[j][1] })
|
||||||
|
|
||||||
|
for _, dp := range degPos {
|
||||||
|
grid.asteroidMap[dp[0]] = '*' // Mark asteroids destroyed
|
||||||
|
destroyed = append(destroyed, dp[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destr200X, destr200Y := grid.posToCoord(destroyed[199])
|
||||||
|
|
||||||
|
return 100*destr200X + destr200Y, nil
|
||||||
|
}
|
23
day10_input.txt
Normal file
23
day10_input.txt
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
.###..#......###..#...#
|
||||||
|
#.#..#.##..###..#...#.#
|
||||||
|
#.#.#.##.#..##.#.###.##
|
||||||
|
.#..#...####.#.##..##..
|
||||||
|
#.###.#.####.##.#######
|
||||||
|
..#######..##..##.#.###
|
||||||
|
.##.#...##.##.####..###
|
||||||
|
....####.####.#########
|
||||||
|
#.########.#...##.####.
|
||||||
|
.#.#..#.#.#.#.##.###.##
|
||||||
|
#..#.#..##...#..#.####.
|
||||||
|
.###.#.#...###....###..
|
||||||
|
###..#.###..###.#.###.#
|
||||||
|
...###.##.#.##.#...#..#
|
||||||
|
#......#.#.##..#...#.#.
|
||||||
|
###.##.#..##...#..#.#.#
|
||||||
|
###..###..##.##..##.###
|
||||||
|
###.###.####....######.
|
||||||
|
.###.#####.#.#.#.#####.
|
||||||
|
##.#.###.###.##.##..##.
|
||||||
|
##.#..#..#..#.####.#.#.
|
||||||
|
.#.#.#.##.##########..#
|
||||||
|
#####.##......#.#.####.
|
86
day10_test.go
Normal file
86
day10_test.go
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
package aoc2019
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDay10ReadAsteroidMap(t *testing.T) {
|
||||||
|
grid, err := day10ReadAsteroidMap(strings.NewReader(".#..#\n.....\n#####\n....#\n...##"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Asteroid map parser failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if grid.width != 5 {
|
||||||
|
t.Errorf("Wrong width detected: exp=5 got=%d", grid.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l := len(grid.asteroidMap); l != 25 {
|
||||||
|
t.Errorf("Wrong length of asteroid map detected: exp=25 got=%d", l)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c := grid.asteroidCount(); c != 10 {
|
||||||
|
t.Errorf("Wrong number of asteroids detected: exp=10 got=%d", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDay10GetCleanedGrid(t *testing.T) {
|
||||||
|
grid, err := day10ReadAsteroidMap(strings.NewReader(".#..#\n.....\n#####\n....#\n...##"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Asteroid map parser failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for expCount, coords := range map[int][2]int{
|
||||||
|
5: {4, 2},
|
||||||
|
6: {0, 2},
|
||||||
|
7: {1, 0},
|
||||||
|
8: {3, 4},
|
||||||
|
} {
|
||||||
|
rGrid := grid.getCleanedGrid(coords[0], coords[1])
|
||||||
|
|
||||||
|
if c := rGrid.asteroidCount(); c != expCount {
|
||||||
|
t.Errorf("Wrong number of asteroids detected: exp=%d got=%d (grid=%s)", expCount, c, rGrid.asteroidMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDay10StepToDeg(t *testing.T) {
|
||||||
|
// Is a function on a day10MonitorGrid, grid itself is not used
|
||||||
|
grid, err := day10ReadAsteroidMap(strings.NewReader(".#..#\n.....\n#####\n....#\n...##"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Asteroid map parser failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for expDeg, vals := range map[float64][2]int{
|
||||||
|
0: {0, -5},
|
||||||
|
45: {5, -5},
|
||||||
|
90: {5, 0},
|
||||||
|
135: {5, 5},
|
||||||
|
180: {0, 5},
|
||||||
|
225: {-5, 5},
|
||||||
|
270: {-5, 0},
|
||||||
|
315: {-5, -5},
|
||||||
|
} {
|
||||||
|
if d := grid.step2deg(vals[0], vals[1]); d != expDeg {
|
||||||
|
t.Errorf("Step to Degree yield unexpected result for %+v: exp=%.2f got=%.2f", vals, expDeg, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalculateDay10_Part1(t *testing.T) {
|
||||||
|
count, err := solveDay10Part1("day10_input.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Day 10 solver failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Solution Day 10 Part 1: %d", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalculateDay10_Part2(t *testing.T) {
|
||||||
|
res, err := solveDay10Part2("day10_input.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Day 10 solver failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Solution Day 10 Part 2: %d", res)
|
||||||
|
}
|
|
@ -2,6 +2,15 @@ package aoc2019
|
||||||
|
|
||||||
import "math"
|
import "math"
|
||||||
|
|
||||||
|
func greatestCommonDivisor(a, b int64) int64 {
|
||||||
|
for b != 0 {
|
||||||
|
t := b
|
||||||
|
b = a % b
|
||||||
|
a = t
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
func manhattenDistance(x1, y1, x2, y2 int) int {
|
func manhattenDistance(x1, y1, x2, y2 int) int {
|
||||||
return int(math.Abs(float64(x1-x2)) + math.Abs(float64(y1-y2)))
|
return int(math.Abs(float64(x1-x2)) + math.Abs(float64(y1-y2)))
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,18 @@ package aoc2019
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
|
func TestGreatestCommonDivisor(t *testing.T) {
|
||||||
|
for expDivisor, vals := range map[int64][2]int64{
|
||||||
|
1: {2, 3},
|
||||||
|
2: {4, 6},
|
||||||
|
-5: {-5, 15},
|
||||||
|
} {
|
||||||
|
if r := greatestCommonDivisor(vals[0], vals[1]); r != expDivisor {
|
||||||
|
t.Errorf("Unexpected divisor for values %+v: exp=%d got=%d", vals, expDivisor, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestManhattenDistance(t *testing.T) {
|
func TestManhattenDistance(t *testing.T) {
|
||||||
for expDist, p := range map[int][4]int{
|
for expDist, p := range map[int][4]int{
|
||||||
2: {0, 0, 0, 2},
|
2: {0, 0, 0, 2},
|
||||||
|
|
Loading…
Reference in a new issue