Advent of code 2024 - Dia 06 de 25
Conteúdo
O sexto dia do Advent of Code foi ontem 06/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, e Dia 5.
Classifiquei o sexto problema como médio (parte 1) e difícil (parte 2).
Dicas#
-
Novamente, um problema com labirinto, onde cada caractere pode ser visto como um elemento em uma matriz.
-
Como você vai precisar constantemente verificar os limites do labirinto, posições válidas e obstáculos, recomendo criar uma classe para representar o labirinto.
-
Na primeira parte da resposta você encontrará uma classe para representar o percurso e o labirinto em si. Você pode usá-las para visualizar o percurso, como mostrado no problema.
-
Uma classe para trabalhar com vetores 2D também simplifica bastante a implementação. Se você visualizar a posição atual como um vetor (y, x), a nova posição pode ser calculada somando-se outro vetor com a direção do movimento (dy, dx)
-
A parte 1 do problema é relativamente fácil, mas exige que você percorra a matriz seguindo instruções bem precisas e guardando o percurso.
-
Você pode implementar a mudança de direção (90 graus) a cada obstáculo usando uma lista de tuplas que representam as direções. Usando as propriedades do módulo, você pode garantir que a posição sempre seja válida na lista.
-
A parte 2 é mais complicada, pois você deve gerar obstáculos e ver como o programa se comporta. Como o objetivo é colocar o guarda em um loop infinito, você precisa detectar quando um ciclo for formado (ou seu programa não terminará!). Em um grafo, um ciclo pode ser visto quando o percurso voltar novamente a visitar um nó já visitado. Como em nosso problema temos um labirinto e que cada posição pode ser visitada em direções diferentes, para detectar um ciclo, você precisa verificar se a posição já foi visitada antes com a mesma direção de passagem.
-
Para gerar os obstáculos, você deve “especular” que um novo obstáculo pode ser gerado a cada posição do caminho normal do guarda, desde que não seja a posição inicial e já não esteja ocupado por um obstáculo.
-
Você pode criar um novo labirinto com o novo obstáculo e verificar se o caminho entra em loop.
-
A complexidade da parte 2 é alta, algo como O(m x n), demorando cerca de 200 segundos para completar no meu computador.
Quanto de Python você precisa saber?#
Os capítulos se referem ao meu livro Introdução à Programação com Python.
- Propriedades do resto (módulo) - Capítulo 2
- Listas, Tuplas, Conjuntos e Dicionários - Capítulo 6
- Funções recursivas - Capítulo 8
- Grafos
Resposta#
from auxiliares import string_para_matriz, le_arquivo_sem_linhas_em_branco, Vector2d
ENTRADA = """
....#.....
.........#
..........
..#.......
.......#..
..........
.#..^.....
........#.
#.........
......#...
"""
GUARDA = "^"
OBSTÁCULO = "#"
DIREÇÕES = [Vector2d(*v) for v in [(-1, 0), (0, 1), (1, 0), (0, -1)]]
class Percurso:
def __init__(self):
self.percurso = {}
def __len__(self):
return len(self.percurso)
def visitada(self, posição: Vector2d, direção: int) -> bool:
if posição in self.percurso and direção in self.percurso[posição]:
return True
return False
def adiciona(self, posição: Vector2d, direção: int):
if posição in self.percurso:
self.percurso[posição].add(direção)
else:
self.percurso[posição] = {direção}
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 acha_guarda(self) -> Vector2d:
for y, linha in enumerate(self.labirinto):
if "^" in linha:
x = linha.index(GUARDA)
return Vector2d(y, x)
raise ValueError("Não há guarda no 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 desenha(self, percurso, caractere="X"):
for posição, direções in percurso.items():
if direções:
h = {1, 3} & direções
v = {0, 2} & direções
dchar = "+" if h and v else "-" if h else "|"
self.labirinto[posição.y][posição.x] = dchar
else:
self.labirinto[posição.y][posição.x] = caractere
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):
return self.labirinto[yx[0]][yx[1]]
def faz_ronda(labirinto: Labirinto) -> tuple[Percurso, bool]:
percorrido = Percurso()
direção_atual = 0
posição = labirinto.acha_guarda()
while labirinto.posição_válida(nova_posição := posição + DIREÇÕES[direção_atual]):
percorrido.adiciona(posição, direção_atual)
if labirinto[nova_posição.y, nova_posição.x] == OBSTÁCULO:
direção_atual = (direção_atual + 1) % 4
else:
posição = nova_posição
if percorrido.visitada(posição, direção_atual):
return percorrido, True
percorrido.adiciona(posição, direção_atual)
return percorrido, False
def procura_onde_por_obstáculos(labirinto: Labirinto):
direção_atual = 0
obstáculos_possíveis = set()
posição = posição_inicial = labirinto.acha_guarda()
while labirinto.posição_válida(nova_posição := posição + DIREÇÕES[direção_atual]):
if labirinto[nova_posição.y, nova_posição.x] == OBSTÁCULO:
direção_atual = (direção_atual + 1) % 4
else:
posição = nova_posição
if posição != posição_inicial:
novo_labirinto = labirinto.copy()
novo_labirinto[posição.y, posição.x] = OBSTÁCULO
_, ciclo_detectado = faz_ronda(novo_labirinto)
if ciclo_detectado:
obstáculos_possíveis.add(posição)
return obstáculos_possíveis
# Parte 1
# Vetor de teste
labirinto_teste = string_para_matriz(ENTRADA)
# posição_inicial = acha_guarda(labirinto)
percurso, _ = faz_ronda(Labirinto(labirinto_teste))
print("Posições únicas (teste):", len(percurso))
assert len(percurso) == 41
labirinto_entrada = string_para_matriz(le_arquivo_sem_linhas_em_branco("06/input.txt"))
percurso, _ = faz_ronda(Labirinto(labirinto_entrada))
print("Posições únicas (parte 1):", len(percurso))
assert len(percurso) == 4696
# Parte 2
obstáculos_possíveis = procura_onde_por_obstáculos(Labirinto(labirinto_teste))
print("Obstáculos possíveis (teste parte 2):", len(obstáculos_possíveis))
assert len(obstáculos_possíveis) == 6
obstáculos_possíveis = procura_onde_por_obstáculos(Labirinto(labirinto_entrada))
print("Obstáculos possíveis (parte 2):", len(obstáculos_possíveis))
assert len(obstáculos_possíveis) >= 1443
# Saída
# Posições únicas (teste): 41
#Posições únicas (parte 1): 4696
#Obstáculos possíveis (teste parte 2): 6
#Obstáculos possíveis (parte 2): 1443
Módulo auxiliares.py
#
Listagem parcial, a adicionar às outras funções auxiliares publicadas em outros artigos.
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 __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