150 lines
3.3 KiB
Python
150 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
|
|
from math import gcd
|
|
from fractions import Fraction
|
|
from itertools import product
|
|
from functools import total_ordering
|
|
from collections import defaultdict
|
|
|
|
ASTEROID = '#'
|
|
|
|
def zrange(x, y, step):
|
|
if x == y == step == 0:
|
|
def yzero():
|
|
while True:
|
|
yield 0
|
|
return yzero()
|
|
return range(x, y, step)
|
|
|
|
def angle(v):
|
|
if v.x == 0:
|
|
return (2 * int(v.y > 0), 0)
|
|
halve = 1 if v.x > 0 else 3
|
|
return (halve, Fraction(v.y, v.x))
|
|
|
|
def rebase(base, vectors):
|
|
return (v - base for v in vectors if v != base)
|
|
|
|
@total_ordering
|
|
class Vector:
|
|
def __init__(self, x, y):
|
|
self.x = x
|
|
self.y = y
|
|
def __add__(self, other):
|
|
return Vector(self.x + other.x, self.y + other.y)
|
|
def __sub__(self, other):
|
|
return self + Vector(-other.x, -other.y)
|
|
def __eq__(self, other):
|
|
return self.x == other.x and self.y == other.y
|
|
def __lt__(self, other):
|
|
return (self.x**2 + self.y**2) < (other.x**2 + other.y**2)
|
|
def __str__(self):
|
|
return str((self.x, self.y))
|
|
def __repr__(self):
|
|
return str(self)
|
|
|
|
class Map:
|
|
def __init__(self, map):
|
|
self.map = map
|
|
|
|
def __getitem__(self, v):
|
|
return self.map[v.y][v.x]
|
|
|
|
def __str__(self):
|
|
return '\n'.join(self.map)
|
|
|
|
def is_asteroid(self, v):
|
|
return self[v] == ASTEROID
|
|
|
|
def vectors(self):
|
|
X, Y = len(self.map[0]), len(self.map)
|
|
return (Vector(x, y) for x, y in product(range(X), range(Y)))
|
|
|
|
def asteroid_vectors(self):
|
|
return filter(lambda v: self.is_asteroid(v), self.vectors())
|
|
|
|
def num_in_sight(self, v):
|
|
vs = rebase(v, self.asteroid_vectors())
|
|
return len(set(map(angle, vs)))
|
|
|
|
def best(self):
|
|
return max((self.num_in_sight(v), v) for v in self.asteroid_vectors())
|
|
|
|
def vaporize_seq(self, base):
|
|
vs = rebase(base, self.asteroid_vectors())
|
|
angles = defaultdict(list)
|
|
for v in vs:
|
|
angles[angle(v)].append(v)
|
|
for ang in angles.keys():
|
|
angles[ang].sort()
|
|
r = []
|
|
for i in range(max(map(len, angles.values()))):
|
|
r += [angles[ang][i] + base
|
|
for ang in sorted(angles.keys()) if i < len(angles[ang])]
|
|
return r
|
|
|
|
def preproc(puzzle_input):
|
|
asteroid_map = Map(puzzle_input.split())
|
|
score, asteroid = asteroid_map.best()
|
|
return asteroid_map, asteroid, score
|
|
|
|
def partI(packed):
|
|
_, _, score = packed
|
|
return score
|
|
|
|
def partII(packed):
|
|
asteroid_map, asteroid, _ = packed
|
|
v = asteroid_map.vaporize_seq(asteroid)[199]
|
|
return 100*v.x + v.y
|
|
|
|
from main import Tests
|
|
|
|
tests = Tests()
|
|
|
|
map1 = \
|
|
""".#..#
|
|
.....
|
|
#####
|
|
....#
|
|
...##"""
|
|
|
|
map2 = \
|
|
"""#.#...#.#.
|
|
.###....#.
|
|
.#....#...
|
|
##.#.#.#.#
|
|
....#.#.#.
|
|
.##..###.#
|
|
..#...##..
|
|
..##....##
|
|
......#...
|
|
.####.###.
|
|
"""
|
|
|
|
map3 = \
|
|
""".#..##.###...#######
|
|
##.############..##.
|
|
.#.######.########.#
|
|
.###.#######.####.#.
|
|
#####.##.#.##.###.##
|
|
..#####..#.#########
|
|
####################
|
|
#.####....###.#.#.##
|
|
##.#################
|
|
#####.##.###..####..
|
|
..######..##.#######
|
|
####.##.####...##..#
|
|
.#####..#.######.###
|
|
##...#.##########...
|
|
#.##########.#######
|
|
.####.#.###.###.#.##
|
|
....##.##.###..#####
|
|
.#.#.###########.###
|
|
#.#.#.#####.####.###
|
|
###.##.####.##.#..##
|
|
"""
|
|
|
|
tests.add(map1, partI=8)
|
|
tests.add(map2, partI=35)
|
|
tests.add(map3, partI=210, partII=802)
|