1
0
Fork 0
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:
Knut Ahlers 2019-12-10 16:22:53 +01:00
parent d11d14dfb9
commit c5e14ed7f4
Signed by: luzifer
GPG key ID: DC2729FDD34BE99E
5 changed files with 343 additions and 0 deletions

213
day10.go Normal file
View 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
View file

@ -0,0 +1,23 @@
.###..#......###..#...#
#.#..#.##..###..#...#.#
#.#.#.##.#..##.#.###.##
.#..#...####.#.##..##..
#.###.#.####.##.#######
..#######..##..##.#.###
.##.#...##.##.####..###
....####.####.#########
#.########.#...##.####.
.#.#..#.#.#.#.##.###.##
#..#.#..##...#..#.####.
.###.#.#...###....###..
###..#.###..###.#.###.#
...###.##.#.##.#...#..#
#......#.#.##..#...#.#.
###.##.#..##...#..#.#.#
###..###..##.##..##.###
###.###.####....######.
.###.#####.#.#.#.#####.
##.#.###.###.##.##..##.
##.#..#..#..#.####.#.#.
.#.#.#.##.##########..#
#####.##......#.#.####.

86
day10_test.go Normal file
View 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)
}

View file

@ -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)))
}

View file

@ -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},