Adrianistán

Advent of Code 2018: segunda semana

16/12/2018

Segunda semana del Advent of Code. En esta semana ya hemos tenido algunos problemas muy interesantes. Intentaré comentarlos de la mejor manera posible. Todo el código está aquí y en este otro post comenté mis soluciones de la primera semana.

Día 8

El día 8 se nos propone un problema de grafos también. Básicamente se nos define un árbol, donde cada nodo puede tener hijos y metadatos. En la primera parte nos piden sumar todos los metadatos.

Aquí al contrario que en el día 7, no voy a usar networkx. Era más difícil adaptar networkx al problema que hacer el árbol a mano. Uno de los puntos complicados de este problema es el parseo de la entrada, que hice de forma recursiva. Cada nodo es un diccionario con las propiedades tamaño, una lista de metadatos y una lista de nodos hijo.

En la segunda parte se define el concepto de valor de nodo y como calcularlo. También es bastante sencillo de implementar. Finalmente hacemos un recorrido por el árbol de tipo primero en anchura (BFS) con una deque de Python.

from collections import deque
def read_node(start,numbers):
    length = 2
    child_nodes = numbers[start]
    metadata_entries = numbers[start+1]
    children = list()
    while child_nodes > 0:
        child_node = read_node(start+length,numbers)
        children.append(child_node)
        length += child_node["length"]
        child_nodes -= 1
    metadata = list()
    while metadata_entries > 0:
        metadata.append(numbers[start+length])
        length += 1
        metadata_entries -= 1
    node = dict([("length",length),("metadata",metadata),("children",children)])
    return node

def read_file(file):
    with open(file) as f:
        line = f.readline()
    numbers = [int(x) for x in line.split()]
    G = read_node(0,numbers)
    return G
def node_value(N):
    if len(N["children"]) == 0:
        return sum(N["metadata"])
    else:
        s = 0
        for i in N["metadata"]:
            if i-1 < len(N["children"]):
                s += node_value(N["children"][i-1])
        return s
        
def day8(file):
    G = read_file(file)
    to_visit = deque()
    to_visit.append(G)
    metadata_sum = 0
    while len(to_visit) > 0:
        N = to_visit.popleft()
        metadata_sum += sum(N["metadata"])
        to_visit.extend(N["children"])
    print("METADATA SUM: %d" % metadata_sum)
    print("NODE VALUE: %d" % node_value(G))

Día 9

Este día fue muy interesante. Se nos explica un juego, que consiste en ir añadiendo canicas en una circunferencia y cuando el número de canica que añadimos es múltiplo de 23, obtenemos puntos y quitamos una canica 7 puestos por detrás.

Aquí tuve una mala decisión de diseño ya que al principio quise hacer esto con una lista de Python (el equivalente a vector en otros lenguajes de programación). La idea era sencilla y funcionaba hasta que llegó la parte 2. La parte 2 te pedría calcular los puntos teniendo en cuenta 100 veces más canicas. Esto fue un grave problema para mi código. Calculo que tardaría 6 horas en calcularlo, pero antes optimicé. La optimización consistía en usar una lista circular doblemente enlazada. ¿Esto qué es? Se trata de una lista enlazada, doblemente, porque cada nodo tiene referencia al elemento siguiente y al anterior. Y es circular porque ambos extremos están unidos. Esto permite las inserciones y borrados en O(1). Además los movimientos relativos (en este problema todos son así) son extremadamente sencillos. La implementación de esta estructura de datos en Python es muy sencilla (en otros lenguajes es más complicado). No me molesté en hacer funciones que me hiciesen sencilla la vida y las conexiones/desconexiones las hago a mano directamente en el código del problema.

from collections import defaultdict
class Marble:
    def __init__(self,value,left=None,right=None):
        self.value = value
        self.left = left
        self.right = right
        if self.left == None:
            self.left = self
        if self.right == None:
            self.right = self

def day9(PLAYERS,LAST_MARBLE):
    SCORE = defaultdict(lambda: 0)
    player = 0
    marble = 0
    current_marble_pos = 0
    current_marble = None
    while marble <= LAST_MARBLE:
        if marble > 0 and marble % 23 == 0:
            SCORE[player] += marble
            pivote = current_marble.left.left.left.left.left.left.left
            SCORE[player] += pivote.value
            pivote.left.right = pivote.right
            pivote.right.left = pivote.left
            current_marble = pivote.right
        else:
            if current_marble == None:
                current_marble = Marble(marble)
            else:
                current_marble = Marble(marble,current_marble.right,current_marble.right.right)
                current_marble.left.right = current_marble
                current_marble.right.left = current_marble
        player += 1
        player = player % PLAYERS
        marble += 1
    return max(SCORE.values())

Curiosamente, en la propia librería de Python deque tiene una operación llamada rotate que permite hacer este problema en poquísimas líneas y de forma muy eficiente. Pero desconocía la existencia de esa función (que lo que hace es mover la "cabeza" de la lista enlazada que es deque).

Día 10

Este problema es muy interesante. Se nos da una serie de puntos que van moviéndose por la pantalla. En un determinado momento estos puntos se juntan y forman un mensaje en pantalla.

Aquí lo interesante no es mover los puntos, eso es trivial, simplemente es sumar la velocidad cada vez las coordenadas. Lo interesante es saber cuando parar. Existen varias ideas:

Y alguna más. Para el ejemplo la primera idea servía. Pero en la prueba real, era más complicado. A mí se me ocurrió la tercera opción, la cuál es bastante eficiente. En cada iteración calculamos el área que contiene a todos los puntos, cuando ese área ya no se reduce más, hemos llegado al mensaje.

import re
def read_file(file):
    stars = list()
    p = re.compile("position=<([ -][0-9]+), ([ -][0-9]+)> velocity=<([ -][0-9]+), ([ -][0-9]+)>")
    with open(file) as f:
        lines = f.readlines()
    for line in lines:
        m = p.match(line.strip())
        try:
            pos_x = int(m.group(1))
        except:
            print(line)
        pos_y = int(m.group(2))
        vel_x = int(m.group(3))
        vel_y = int(m.group(4))
        stars.append([pos_x,pos_y,vel_x,vel_y])
    return stars
def print_stars(stars):
    stars = sorted(stars,key=lambda x: x[0],reverse=True)
    min_width = stars[-1][0]
    max_width = stars[0][0]
    min_height = min(stars,key=lambda x: x[1])[1]
    max_height = max(stars,key=lambda x: x[1])[1]
    s = str()
    for j in range(min_height,max_height+1):
        p = [star for star in stars if star[1] == j]
        for i in range(min_width,max_width+1):
            if len(p) == 0:
                s += "."
            else:
                if any(map(lambda star: star[0] == i and star[1] == j,p)):
                    s += "#"
                else:
                    s += "." 
       s += "\n"
    return s
def step(stars):
    a = map(lambda x: [x[0]+x[2],x[1]+x[3],x[2],x[3]],stars)
    return list(a)
# LA RESPUESTA CORRECTA TIENE AREA MINIMA

def area(stars):
    stars = sorted(stars,key=lambda x: x[0], reverse=True)
    min_width = stars[-1][0]
    max_width = stars[0][0]
    min_height = min(stars,key=lambda x: x[1])[1]
    max_height = max(stars,key=lambda x: x[1])[1]
    area = (max_width-min_width)*(max_height-min_height)
    return area
def day10(file):
    stars = read_file(file)
    a = area(stars)
    steps = 0
    while area(step(stars)) < a:
        stars = step(stars)
        steps += 1
        a = area(stars)
    print_stars(stars)
    print(steps)

La parte de dibujado me costó y ahí tuve un fallo que me costó media hora aproximadamente en resolver. Una mejor opción, pero que no se me ocurrió, hubiese sido usar Pillow y crear una imagen. Es mucho más fácil que dibujar sobre una terminal (y posiblemente más rápido).

Día 11

Para este problema hay 3 posibles algoritmos. En la primera parte nos piden que de una matriz extraigamos el cuadrado de 3x3 con mayor valor. La matriz hay que construirla pero es trivial. Yo decido usar un diccionario, con clave la tupla de coordenadas. Vamos recorriendo todas las posiciones y calculamos el valor. Ahora para buscar el cuadrado, simplemente vamos probando todos los posibles cuadrados.

En la segunda parte nos dicen que bsuquemos el cuadrado máximo pero el tamaño puede ser cualquiera. Aquí con la fuerza bruta ya tarda demasiado. Mi solución fue usar programación dinámica, para ello la clave pasa a tener un valor más, el tamaño del cuadrado. Cuando creamos la tabla estamos asignando valor al cuadrado 1x1 de posición X,Y. Representado es la tupla (x,y,1). Según vamos avanzando hasta 300x300 vamos guardando los resultados intermedios, de modo que podamos reutilizarlos. Por ejemplo, el valor de (x,y,4) solamente es la suma de (x,y,2), (x+2,y,2), (x,y+2,2) y (x+2,y+2,2). Evidentemente esto solo funciona en los tamaños pares. En los tamaños impares decidí coger el cuadrado de dimensión inmediatamente menor y calcular los laterales con los cuadrados de tamaño 1. Este sistema funciona mucho mejor que la fuerza bruta pero es lento. Los profesionales usaron el algoritmo Summed Area Table (del que desconocía su existencia). Este algoritmo es el óptimo para este problema.

 

def generate_fuel(x,y,idg):
    fuel = (((x+10)*y)+idg)*(x+10)
    fuel %= 1000 
    fuel = (fuel // 100) - 5
    return fuel
def generate_table(idg):
    fuel = {(x,y,size):0 for x in range(1,301) for y in range(1,301) for size in range(1,301)}
    for x in range(1,301):
        for y in range(1,301):
            fuel[(x,y,1)] = generate_fuel(x,y,idg)
    return fuel
def find_best(fuel):
    max_point = [-1,-1]
    max_score = -1
    for x in range(1,301):
        for y in range(1,301):
            if x+3 > 301 or y+3 > 301:
                continue
            score = fuel[(x,y,1)]+fuel[(x+1,y,1)]+fuel[(x+2,y,1)]+fuel[(x,y+1,1)]+fuel[(x+1,y+1,1)]+fuel[(x+2,y+1,1)]+fuel[(x,y+2,1)]+fuel[(x+1,y+2,1)]+fuel[(x+2,y+2,1)]
            if score > max_score:
                max_score = score
                max_point = [x,y]
    return max_point[0],max_point[1]
def find_best_any_size(fuel):
    max_score = -1
    max_point = [-1,-1,-1]
    for size in range(2,300+1):
        for x in range(1,301):
            for y in range(1,301):
                if x+size > 301 or y+size > 301:
                    continue
                if size % 2 == 0:
                    mid = size // 2
                    fuel[(x,y,size)] = fuel[(x+mid,y,mid)]+fuel[(x,y+mid,mid)]+fuel[(x+mid,y+mid,mid)]+fuel[(x,y,mid)]
                else:
                    fuel[(x,y,size)] = fuel[(x,y,size-1)]
                    for i in range(x,x+size-1):
                        fuel[(x,y,size)] += fuel[(i,y+size-1,1)]
                    for j in range(y,y+size-1):
                        fuel[(x,y,size)] += fuel[(x+size-1,j,1)]
                    fuel[(x,y,size)] += fuel[(x+size-1,y+size-1,1)]
                score = fuel[(x,y,size)]
                if score > max_score:
                    max_score = score
                    max_point = [x,y,size]
    return max_point[0],max_point[1],max_point[2]
def day11():
    fuel = generate_table(1133)
    x,y = find_best(fuel)
    print("BEST POINT: %d,%d" % (x,y))
    x,y,size = find_best_any_size(fuel)
    print("BEST POINT ANY SIZE: %d,%d,%d" % (x,y,size))
if __name__ == "__main__":
    day11()

Día 12

El día 12 me trajo recuerdos de un algoritmo con el que me peleé mucho, el denominado HashLife. El problema es un autómata celular unidimensional. Las reglas vienen dadas como patrones. La única diferencia es que hay que guardar su posición para luego calcular un número. La primera parte es bastante sencilla.

import re
from collections import defaultdict

def read_file(file):
    rules = defaultdict(lambda: ".")
    rule_prog = re.compile("([.#]+) => ([.#])")
    with open(file) as f:
        lines = f.readlines()
    state = lines[0].split(": ")[1].strip()
    for line in lines[2:]:
        m = rule_prog.match(line.strip())
        rules[m.group(1)] = m.group(2)
    return state,rules

def parse_state(pots):
    state = dict()
    for i,p in enumerate(pots):
        state[i] = p
    return state

def find(rules,current):
    if current in rules:
        return rules[current]
    else:
        size = len(current)
        mid = size // 2
        left = find(rules,current[0:mid])
        right = find(rules,current[mid:])
        rules[current] = left + right
        return rules[current]


def iter(state,rules):
    new_state = dict()
    xmin = min(state.keys())
    xmax = max(state.keys())
    for x in range(xmin-2,xmax+3):
        current = ("%c%c%c%c%c" % (
                    state.get(x-2,"."),
                    state.get(x-1,"."),
                    state.get(x,"."),
                    state.get(x+1,"."),
                    state.get(x+2,".")
                    ))
        new = rules[current]
        if new == "#" or xmin <= x <= xmax:
            new_state[x] = new
    return new_state

def sum_pots(state):
    n = 0
    for pot in state:
        if state[pot] == "#":
            n += pot
    return n

def print_state(state):
    xmin = min(state.keys())
    xmax = max(state.keys())
    s = str("XMIN %d : " % xmin)
    for x in range(xmin-2,xmax+3):
        s += state.get(x,".")
    print(s)


def day12(file):
    state,rules = read_file(file)
    state = parse_state(state)
    for i in range(20):
        #print_state(state)
        state = iter(state,rules)
    #print_state(state)
    n = sum_pots(state)
    return n

if __name__ == "__main__":
    day12("input.txt")

La segunda parte nos pedía lo mismo pero para el número ¡50000000000! Inmediatamente pensé en optimizarlo de forma similar a HashLife. La idea consiste en almacenar patrones mayores a los de las reglas (que son todos de tamaño 5), para poder evitar cálculos innecesarios.Además añadí un recolector de basura para ir eliminando por la izquierda las celdas inútiles.

No obstante, y aunque es muchísimo más eficiente, sigue sin ser capaz de procesar tal bestialidad de número en un tiempo razonable.

Y he aquí lo que me ha cabreado, porque no he podido sacarlo. A partir de cierto momento, el dibujo siempre es el mismo pero desplazándose a la derecha. De modo que el valor del siguiente paso siempre es la suma de una constante. Finalmente modifiqué el código para que buscase una situación en la que el número fuese resultado de una suma de una constante. Una vez hecho eso, calcula con una multiplicación lo que valdría cuando llegase a 50000000000.

import re
from collections import defaultdict

XMIN = -2

def find(rules,current):
    if len(current) < 5:
        return ""
    if current in rules:
        return rules[current]
    elif len(current) == 5:
        return "."
    else:
        size = len(current)
        left=find(rules,current[0:size-1])
        right=find(rules,current[size-5:])
        rules[current] = left+right
        return rules[current]

def read_file(file):
    rules = defaultdict(lambda: ".")
    rule_prog = re.compile("([.#]+) => ([.#])")
    with open(file) as f:
        lines = f.readlines()
    state = lines[0].split(": ")[1].strip()
    for line in lines[2:]:
        m = rule_prog.match(line.strip())
        rules[m.group(1)] = m.group(2)
    return state,rules


def print_state(state):
    print(state)

def sum_pots(state):
    n = 0
    for i,c in enumerate(state):
        if c == "#":
            n += i + XMIN
    return n

def day12(file):
    global XMIN
    state,rules = read_file(file)
    XMAX = len(state)+1
    state = "..%s.." % state
    sums = list()
    i = 0
    while len(sums) < 3 or sums[-1]-sums[-2] != sums[-2]-sums[-3]:
        state = find(rules,"..%s.." % state)
        if state[0] == "." and state[1] == "." and state[2] == "." and state[3] == ".":
            state = state[2:]
            XMIN += 2
        if state[0] == "#" or state[1] == "#":
            state = "..%s" % state
            XMIN -= 2
        if state[-1] == "#" or state[-2] == "#":
            state = "%s.." % state
        sums.append(sum_pots(state))
        i += 1
    diff = sums[-1]-sums[-2]
    missing = 50000000000 - i
    n = missing*diff + sums[-1]

    return n

Y con esto pude finalmente calcular el resultado.

Día 13

El día 13 teníamos unas vías de tren. En estas vías había unos trenecitos que se desplazaban siguiendo unas normas. El objetivo en la primera parte era conocer el donde se producía el primer choque.

from PIL import Image

XMAX = 0
YMAX = 0
STEP = 0

nextDirection = dict()
nextDirection["start"] = "left"
nextDirection["left"] = "center"
nextDirection["center"] = "right"
nextDirection["right"] = "left"

def relative_direction(original,movement):
    print("CROSS")
    if movement == "center":
        return original
    if original == "v":
        if movement == "left":
            return ">"
        else:
            return "<"
    elif original == ">":
        if movement == "left":
            return "^"
        else:
            return "v"
    elif original == "^":
        if movement == "left":
            return "<"
        else:
            return ">"
    else:
        if movement == "left":
            return "v"
        else:
            return "^"

def day13(file):
    global XMAX
    global YMAX
    global STEP
    plano = dict()
    carts = list()
    with open(file) as f:
        lines = f.readlines()
    YMAX = len(lines)
    XMAX = len(lines[0])
    for y,line in enumerate(lines):
        for x,char in enumerate(line):
            # SI HAY UN CARRITO, DEDUCIR TIPO DE VIA
            if char == "^" or char == "v" or char == "<" or char == ">":
                if (x,y-1) in plano:
                    plano[(x,y)] = "|"
                else:
                    plano[(x,y)] = "-"
                carts.append([x,y,char,"left"])
            else:
                plano[(x,y)] = char
    
    end = False
    while not end:
        carts.sort(key=lambda x: x[1])
        carts.sort(key=lambda x: x[0])

        for cart in carts:
            # CHECK CRASH
            for crt in carts:
                if cart[0] == crt[0] and cart[1] == crt[1] and id(cart) != id(crt):
                    print("CRASH AT %d-%d" % (cart[0],cart[1]))
                    end = True
            try:
                x = cart[0]
                y = cart[1]
                print(cart)
                if cart[2] == ">":
                    if plano[(x+1,y)] == "/":
                        cart[2] = "^"
                    elif plano[(x+1,y)] == "\\":
                        cart[2] = "v"
                    elif plano[(x+1,y)] == "+":
                        cart[2] = relative_direction(cart[2],cart[3])
                        cart[3] = nextDirection[cart[3]]
                    cart[0] += 1
                elif cart[2] == "<":
                    if plano[(x-1,y)] == "/":
                        cart[2] = "v"
                    elif plano[(x-1,y)] == "\\":
                        cart[2] = "^"
                    elif plano[(x-1,y)] == "+":
                        cart[2] = relative_direction(cart[2],cart[3])
                        cart[3] = nextDirection[cart[3]]
                    cart[0] -= 1
                elif cart[2] == "^":
                    if plano[(x,y-1)] == "/":
                        cart[2] = ">"
                    elif plano[(x,y-1)] == "\\":
                        cart[2] = "<"
                    elif plano[(x,y-1)] == "+":
                        cart[2] = relative_direction(cart[2],cart[3])
                        cart[3] = nextDirection[cart[3]]
                    cart[1] -= 1
                elif cart[2] == "v":
                    print()
                    if plano[(x,y+1)] == "/":
                        cart[2] = "<"
                    elif plano[(x,y+1)] == "\\":
                        cart[2] = ">"
                    elif plano[(x,y+1)] == "+":
                        cart[2] = relative_direction(cart[2],cart[3])
                        cart[3] = nextDirection[cart[3]]
                    cart[1] += 1
            except:
                breakpoint()
        STEP += 1
        print_train(plano,carts)

def print_train(plano,carts):
    im = Image.new("RGB",(XMAX,YMAX))
    for x,y in plano:
        if plano[(x,y)] != " ":
            im.putpixel((x,y),(255,255,255))
        if plano[(x,y)] == "+":
            im.putpixel((x,y),(120,120,120))
    for cart in carts:
        if cart[2] == ">":
            im.putpixel((cart[0],cart[1]),(255,0,0))
        elif cart[2] == "<":
            im.putpixel((cart[0],cart[1]),(0,255,0))
        elif cart[2] == "^":
            im.putpixel((cart[0],cart[1]),(0,0,255))
        else:
            im.putpixel((cart[0],cart[1]),(0,120,120))

    im.save("train-%d.png" % STEP,"PNG")

if __name__ == "__main__":