Advent of code 2024 - Dia 12 de 25
Conteúdo
O décimo segundo dia do Advent of Code foi ontem 12/12/2024! Se você caiu aqui hoje pela primeira vez, não deixe de ler os posts sobre o Dia 1, Dia 2, Dia 3, Dia 4, Dia 5, Dia 6, Dia 7, Dia 8, Dia 9, Dia 10 e Dia 11.
Classifiquei o décimo segundo problema como fácil (parte 1) e médio (parte 2).
Dicas#
- Outro problema com labirintos ou mapas de caracteres.
- Você pode construir uma zona, procurando por elementos idênticos nas posições acima, abaixo, a esquerda e a direita do nó corrente.
- Tente visualizar o mapa de caracteres como uma matriz. Agora imagine cada elemento como um nó de um grafo, conectado aos seus vizinhos.
- Lembre-se de marcar os nós já visitados
- Você pode construir uma cerca sempre que o elemento na direção sendo explorada for diferente do elemento atual.
- Na parte 1, a área é igual a quantidade de elementos na mesma zona. Para calcular o perímetro, você precisa saber quantas barreiras colocou em cada posição.
- A parte 2 traz uma forma diferente de calcular o preço, mas o problema é bem parecido com a parte 1. Agora, você precisa olhar todas as barreiras da zona e verificar quantas são linhas e colunas contíguas.
- Você pode organizar as linhas pela coordenada y de suas cercas. Linhas contíguas terão o mesmo valor na coordenada y. Se ordenadas adequadamente, o segmento seguinte será o próximo com uma posição em x apenas 1 elemento maior que a anterior. Desta forma, você consegue contar as linhas contíguas.
- As colunas seguem o mesmo princípio, mas agora você precisa ordenar as cercas pela coordenada x. Da mesma forma, uma coluna é contígua se o próximo segmento está na linha seguinte.
- É mais fácil organizar os dados se as cercas horizontais e verticais estiverem separadas.
Quanto de Python você precisa saber?#
Os capítulos se referem ao meu livro Introdução à Programação com Python.
- Listas, dicionários - Capítulo 6
- Strings - Capítulo 7
- Funções recursivas - Capítulo 8
- Classes - Capítulo 10
- A parte 2 exige um pouco de criatividade para detectar as linhas e colunas contíguas.
Resposta#
A resposta da parte 1 pode ser calculada pala classe Jardim
. A parte 2 é implementada pela classe JardimParte2
, que é muito semelhante a primeira classe, mas adiciona a complexidade de saber a direção das cercas, contar linhas e colunas. Com um pouco de esforço, você poderia usar a classe JardimParte2
para resolver ambas partes, mas creio ser mais interessante mostrar uma solução simples e depois uma mais complexa.
Várias funções de desenho e dump do labirinto foram implementadas, você pode gravar o resultado das zonas com as barreiras em disco se quiser.
import sys
import math
from auxiliares import (
string_para_lista,
Vector2d,
Labirinto,
le_arquivo_sem_linhas_em_branco,
)
ENTRADA = """
AAAA
BBCD
BBCC
EEEC
"""
ENTRADA_TESTE_2 = """
OOOOO
OXOXO
OOOOO
OXOXO
OOOOO
"""
ENTRADA_TESTE_3 = """
RRRRIICCFF
RRRRIICCCF
VVRRRCCFFF
VVRCCCJFFF
VVVVCJJCFE
VVIVCCJJEE
VVIIICJJEE
MIIIIIJJEE
MIIISIJEEE
MMMISSJEEE
"""
ENTRADA_TESTE_PARTE_2_1 = """
EEEEE
EXXXX
EEEEE
EXXXX
EEEEE
"""
ENTRADA_TESTE_PARTE_2_2 = """
AAAAAA
AAABBA
AAABBA
ABBAAA
ABBAAA
AAAAAA
"""
class Jardim(Labirinto):
DIREÇÕES = [Vector2d(0, 1), Vector2d(1, 0), Vector2d(0, -1), Vector2d(-1, 0)]
def cria_zonas(self):
visitados = set()
preço = 0
for y, linha in enumerate(self.labirinto):
for x, valor in enumerate(linha):
if (corrente := Vector2d(y, x)) in visitados:
continue
visitados.add(corrente)
área_perímetro = Vector2d(1, 0)
for direção in self.DIREÇÕES:
área_perímetro += self.visita(
valor,
corrente + direção,
visitados,
)
preço += área_perímetro.x * área_perímetro.y
return preço
def visita(
self, zona: str, posição: Vector2d, visitados: set[Vector2d]
) -> Vector2d:
área_perímetro = Vector2d(0, 0)
válida = self.posição_válida(posição)
if not válida or self[posição] != zona:
área_perímetro += Vector2d(0, 1)
elif válida and self[posição] == zona and posição not in visitados:
área_perímetro += Vector2d(1, 0)
visitados.add(posição)
for direção in self.DIREÇÕES:
área_perímetro += self.visita(
zona,
posição + direção,
visitados,
)
return área_perímetro
class Cerca:
def __init__(self, posição: Vector2d, direção: Vector2d):
self.posição = posição
self.direção = direção
def __hash__(self):
return hash(
f"{self.posição.x},{self.posição.y},{self.direção.x},{self.direção.y}"
)
def __str__(self):
return f"{self.posição} D:{self.direção}"
def __repr__(self):
return f"<Cerca {self}>"
def sort_key_x(self, max):
return f"{self.direção.x:0{max}}{self.direção.y:0{max}}{self.posição.x:0{max}}{self.posição.y:0{max}}"
def sort_key_y(self, max):
return f"{self.direção.x:0{max}}{self.direção.y:0{max}}{self.posição.y:0{max}}{self.posição.x:0{max}}"
class JardimParte2(Jardim):
def __init__(self, labirinto: list[str]):
super().__init__(labirinto)
self.max = int(math.log10(max(self.max_x, self.max_y))) + 2
def linhas(self, cercas_horizontais: set[Vector2d]):
linhas = 1
cercas = sorted(list(cercas_horizontais), key=lambda c: c.sort_key_y(self.max))
linha = cercas[0]
for i in range(1, len(cercas)):
if (
cercas[i].posição.y != linha.posição.y
or cercas[i].posição.x - linha.posição.x > 1
or linha.direção != cercas[i].direção
):
linhas += 1
linha = cercas[i]
return linhas
def colunas(self, cercas_verticais: set[Vector2d]):
colunas = 1
cercas = sorted(
list(cercas_verticais),
key=lambda c: c.sort_key_x(self.max),
)
coluna = cercas[0]
for i in range(1, len(cercas)):
if (
cercas[i].posição.x != coluna.posição.x
or cercas[i].posição.y - coluna.posição.y > 1
or coluna.direção != cercas[i].direção
):
colunas += 1
coluna = cercas[i]
return colunas
def cria_zonas(self):
visitados = set()
preço = 0
for y, linha in enumerate(self.labirinto):
for x, valor in enumerate(linha):
if (corrente := Vector2d(y, x)) in visitados:
continue
visitados.add(corrente)
cercas_horizontais = set()
cercas_verticais = set()
área_perímetro = Vector2d(1, 0)
for direção in self.DIREÇÕES:
área_perímetro += self.visita(
valor,
corrente + direção,
visitados,
cercas_horizontais,
cercas_verticais,
direção,
)
preço += área_perímetro.y * (
self.colunas(cercas_verticais) + self.linhas(cercas_horizontais)
)
return preço
def visita(
self,
zona: str,
posição: Vector2d,
visitados: set[Vector2d],
cercas_horizontais: set[Vector2d],
cercas_verticais: set[Vector2d],
direção: Vector2d,
) -> Vector2d:
área_perímetro = Vector2d(0, 0)
válida = self.posição_válida(posição)
if not válida or self[posição] != zona:
área_perímetro += Vector2d(0, 1)
if direção in [Vector2d(0, 1), Vector2d(0, -1)]:
cercas_verticais.add(Cerca(posição, direção))
else:
cercas_horizontais.add(Cerca(posição, direção))
elif válida and self[posição] == zona and posição not in visitados:
área_perímetro += Vector2d(1, 0)
visitados.add(posição)
for direção in self.DIREÇÕES:
área_perímetro += self.visita(
zona,
posição + direção,
visitados,
cercas_horizontais,
cercas_verticais,
direção,
)
return área_perímetro
def desenhe(self):
self.my = 3
self.mx = 3
self.desenho = []
for _ in range(self.my * (self.max_y)):
self.desenho.append([" "] * (self.mx * (self.max_x)))
for y, linha in enumerate(self.labirinto):
for x, valor in enumerate(linha):
self.desenho[y * self.my + 1][x * self.mx + 1] = valor
def desenhe_cercas(self, cercas: set[Cerca]):
for cerca in cercas:
py = cerca.posição.y - cerca.direção.y
px = cerca.posição.x - cerca.direção.x
cx = (px * self.mx + 1) + cerca.direção.x
cy = (py * self.my + 1) + cerca.direção.y
self.desenho[cy][cx] = (
"|" if cerca.direção in [Vector2d(0, 1), Vector2d(0, -1)] else "―"
)
def dump_desenho(self, output=sys.stdout):
for linha in self.desenho:
print("".join(linha), file=output)
jardim = Jardim(string_para_lista(ENTRADA))
preço = jardim.cria_zonas()
print("Preço - entrada parte 1 - teste 1:", preço)
assert preço == 140
jardim = Jardim(string_para_lista(ENTRADA_TESTE_2))
preço = jardim.cria_zonas()
print("Preço - entrada parte 1 - teste 2:", preço)
assert preço == 772
jardim = Jardim(string_para_lista(ENTRADA_TESTE_3))
preço = jardim.cria_zonas()
print("Preço - entrada parte 1 - teste 3:", preço)
assert preço == 1930
entrada_parte_1 = le_arquivo_sem_linhas_em_branco("12/input.txt")
jardim = Jardim(string_para_lista(entrada_parte_1))
preço = jardim.cria_zonas()
print("Preço - entrada parte 1:", preço)
assert preço == 1396298
# Parte 2
print()
jardim = JardimParte2(string_para_lista(ENTRADA))
preço = jardim.cria_zonas()
print("Preço - entrada - parte 2 - teste 1:", preço)
assert preço == 80
jardim = JardimParte2(string_para_lista(ENTRADA_TESTE_2))
preço = jardim.cria_zonas()
print("Preço - entrada - parte 2 - teste 2:", preço)
assert preço == 436
jardim = JardimParte2(string_para_lista(ENTRADA_TESTE_PARTE_2_1))
preço = jardim.cria_zonas()
print("Preço - entrada - parte 2 - teste 2_1:", preço)
assert preço == 236
jardim = JardimParte2(string_para_lista(ENTRADA_TESTE_PARTE_2_2))
preço = jardim.cria_zonas()
print("Preço - entrada - parte 2 - teste 2_2:", preço)
assert preço == 368
jardim = JardimParte2(string_para_lista(ENTRADA_TESTE_3))
preço = jardim.cria_zonas()
print("Preço - entrada - parte 2 - teste 3:", preço)
assert preço == 1206
jardim = JardimParte2(string_para_lista(entrada_parte_1))
preço = jardim.cria_zonas()
print("Preço - entrada - parte 2:", preço)
assert preço == 853588
Módulo Auxiliar#
As partes que não mudaram foram omitidas.
from .arquivos import uma_lista_por_linha, le_arquivo_sem_linhas_em_branco
from functools import total_ordering
def signal(x: int) -> int:
return 1 if x > 0 else -1 if x < 0 else 0
def string_para_lista(entrada: str) -> list[str]:
return [linha.strip() for linha in entrada.splitlines() if linha]
def string_para_matriz(entrada: str) -> list[list[str]]:
return [list(linha.strip()) for linha in entrada.splitlines() if linha]
def string_para_matriz_int(entrada: str, separador=" ") -> list[list[int]]:
return [
[int(l) for l in linha.split(separador)]
for linha in entrada.splitlines()
if linha
]
@total_ordering
class Vector2d:
def __init__(self, y, x):
self.x = x
self.y = y
def copy(self):
return Vector2d(self.y, self.x)
def __add__(self, other):
return Vector2d(self.y + other.y, self.x + other.x)
def __sub__(self, other):
return Vector2d(self.y - other.y, self.x - other.x)
def __str__(self):
return f"({self.y}, {self.x})"
def __repr__(self):
return f"Vector2d({self.y}, {self.x})"
def __hash__(self):
return hash(f"({self.y}, {self.x})")
def __eq__(self, other):
if isinstance(other, Vector2d):
return self.x == other.x and self.y == other.y
return False
def __le__(self, other):
return self.y < other.y or (self.y == other.y and self.x < other.x)
class Labirinto:
def __init__(self, labirinto):
self.max_y = len(labirinto)
self.max_x = len(labirinto[0])
self.orig_labirinto = labirinto
self.labirinto = [list(linha) for linha in labirinto]
def copy(self):
return Labirinto(self.orig_labirinto)
def posição_válida(self, posição: Vector2d) -> bool:
return 0 <= posição.y < self.max_y and 0 <= posição.x < self.max_x
def __str__(self):
return "\n".join(["".join(linha) for linha in self.labirinto])
def __setitem__(self, yx, value):
self.labirinto[yx[0]][yx[1]] = value
def __getitem__(self, yx):
if type(yx) is Vector2d:
return self.labirinto[yx.y][yx.x]
return self.labirinto[yx[0]][yx[1]]