mirror of
https://github.com/Luzifer/aoc2019.git
synced 2024-12-21 21:41: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"
|
||||
|
||||
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 {
|
||||
return int(math.Abs(float64(x1-x2)) + math.Abs(float64(y1-y2)))
|
||||
}
|
||||
|
|
|
@ -2,6 +2,18 @@ package aoc2019
|
|||
|
||||
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) {
|
||||
for expDist, p := range map[int][4]int{
|
||||
2: {0, 0, 0, 2},
|
||||
|
|
Loading…
Reference in a new issue