Add solution to day 17 part 2
This commit is contained in:
parent
9daef0a6ea
commit
cbb376667a
|
@ -1,2 +1 @@
|
|||
|
||||
1,330,331,332,109,3890,1102,1,1182,16,1102,1471,1,24,101,0,0,570,1006,570,36,1002,571,1,0,1001,570,-1,570,1001,24,1,24,1105,1,18,1008,571,0,571,1001,16,1,16,1008,16,1471,570,1006,570,14,21102,58,1,0,1105,1,786,1006,332,62,99,21102,333,1,1,21102,73,1,0,1105,1,579,1102,0,1,572,1102,1,0,573,3,574,101,1,573,573,1007,574,65,570,1005,570,151,107,67,574,570,1005,570,151,1001,574,-64,574,1002,574,-1,574,1001,572,1,572,1007,572,11,570,1006,570,165,101,1182,572,127,101,0,574,0,3,574,101,1,573,573,1008,574,10,570,1005,570,189,1008,574,44,570,1006,570,158,1105,1,81,21102,1,340,1,1106,0,177,21102,1,477,1,1106,0,177,21101,0,514,1,21101,0,176,0,1105,1,579,99,21101,184,0,0,1106,0,579,4,574,104,10,99,1007,573,22,570,1006,570,165,102,1,572,1182,21101,375,0,1,21101,0,211,0,1105,1,579,21101,1182,11,1,21102,1,222,0,1106,0,979,21102,1,388,1,21101,0,233,0,1106,0,579,21101,1182,22,1,21102,244,1,0,1105,1,979,21102,401,1,1,21102,1,255,0,1106,0,579,21101,1182,33,1,21101,266,0,0,1105,1,979,21102,1,414,1,21102,277,1,0,1105,1,579,3,575,1008,575,89,570,1008,575,121,575,1,575,570,575,3,574,1008,574,10,570,1006,570,291,104,10,21101,0,1182,1,21101,313,0,0,1105,1,622,1005,575,327,1101,0,1,575,21102,327,1,0,1105,1,786,4,438,99,0,1,1,6,77,97,105,110,58,10,33,10,69,120,112,101,99,116,101,100,32,102,117,110,99,116,105,111,110,32,110,97,109,101,32,98,117,116,32,103,111,116,58,32,0,12,70,117,110,99,116,105,111,110,32,65,58,10,12,70,117,110,99,116,105,111,110,32,66,58,10,12,70,117,110,99,116,105,111,110,32,67,58,10,23,67,111,110,116,105,110,117,111,117,115,32,118,105,100,101,111,32,102,101,101,100,63,10,0,37,10,69,120,112,101,99,116,101,100,32,82,44,32,76,44,32,111,114,32,100,105,115,116,97,110,99,101,32,98,117,116,32,103,111,116,58,32,36,10,69,120,112,101,99,116,101,100,32,99,111,109,109,97,32,111,114,32,110,101,119,108,105,110,101,32,98,117,116,32,103,111,116,58,32,43,10,68,101,102,105,110,105,116,105,111,110,115,32,109,97,121,32,98,101,32,97,116,32,109,111,115,116,32,50,48,32,99,104,97,114,97,99,116,101,114,115,33,10,94,62,118,60,0,1,0,-1,-1,0,1,0,0,0,0,0,0,1,32,0,0,109,4,2101,0,-3,586,21002,0,1,-1,22101,1,-3,-3,21101,0,0,-2,2208,-2,-1,570,1005,570,617,2201,-3,-2,609,4,0,21201,-2,1,-2,1105,1,597,109,-4,2105,1,0,109,5,1201,-4,0,630,20102,1,0,-2,22101,1,-4,-4,21102,0,1,-3,2208,-3,-2,570,1005,570,781,2201,-4,-3,652,21001,0,0,-1,1208,-1,-4,570,1005,570,709,1208,-1,-5,570,1005,570,734,1207,-1,0,570,1005,570,759,1206,-1,774,1001,578,562,684,1,0,576,576,1001,578,566,692,1,0,577,577,21102,1,702,0,1105,1,786,21201,-1,-1,-1,1106,0,676,1001,578,1,578,1008,578,4,570,1006,570,724,1001,578,-4,578,21101,0,731,0,1106,0,786,1105,1,774,1001,578,-1,578,1008,578,-1,570,1006,570,749,1001,578,4,578,21102,756,1,0,1106,0,786,1106,0,774,21202,-1,-11,1,22101,1182,1,1,21101,774,0,0,1106,0,622,21201,-3,1,-3,1105,1,640,109,-5,2105,1,0,109,7,1005,575,802,21001,576,0,-6,20101,0,577,-5,1105,1,814,21102,1,0,-1,21101,0,0,-5,21101,0,0,-6,20208,-6,576,-2,208,-5,577,570,22002,570,-2,-2,21202,-5,41,-3,22201,-6,-3,-3,22101,1471,-3,-3,1202,-3,1,843,1005,0,863,21202,-2,42,-4,22101,46,-4,-4,1206,-2,924,21102,1,1,-1,1105,1,924,1205,-2,873,21101,35,0,-4,1106,0,924,2102,1,-3,878,1008,0,1,570,1006,570,916,1001,374,1,374,2101,0,-3,895,1102,1,2,0,1201,-3,0,902,1001,438,0,438,2202,-6,-5,570,1,570,374,570,1,570,438,438,1001,578,558,922,20101,0,0,-4,1006,575,959,204,-4,22101,1,-6,-6,1208,-6,41,570,1006,570,814,104,10,22101,1,-5,-5,1208,-5,59,570,1006,570,810,104,10,1206,-1,974,99,1206,-1,974,1101,0,1,575,21101,0,973,0,1106,0,786,99,109,-7,2106,0,0,109,6,21102,0,1,-4,21101,0,0,-3,203,-2,22101,1,-3,-3,21208,-2,82,-1,1205,-1,1030,21208,-2,76,-1,1205,-1,1037,21207,-2,48,-1,1205,-1,1124,22107,57,-2,-1,1205,-1,1124,21201,-2,-48,-2,1105,1,1041,21102,1,-4,-2,1106,0,1041,21102,-5,1,-2,21201,-4,1,-4,21207,-4,11,-1,1206,-1,1138,2201,-5,-4,1059,1202,-2,1,0,203,-2,22101,1,-3,-3,21207,-2,48,-1,1205,-1,1107,22107,57,-2,-1,1205,-1,1107,21201,-2,-48,-2,2201,-5,-4,1090,20102,10,0,-1,22201,-2,-1,-2,2201,-5,-4,1103,2101,0,-2,0,1106,0,1060,21208,-2,10,-1,1205,-1,1162,21208,-2,44,-1,1206,-1,1131,1105,1,989,21102,1,439,1,1105,1,1150,21101,477,0,1,1106,0,1150,21102,1,514,1,21101,1149,0,0,1105,1,579,99,21101,1157,0,0,1106,0,579,204,-2,104,10,99,21207,-3,22,-1,1206,-1,1138,2101,0,-5,1176,1202,-4,1,0,109,-6,2105,1,0,32,5,40,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,30,11,30,1,40,1,40,1,30,13,38,1,1,1,26,5,7,1,1,1,26,1,3,1,7,1,1,1,26,1,3,1,7,1,1,1,26,1,3,1,7,1,1,1,26,1,3,1,7,1,1,1,1,11,14,1,3,1,7,1,1,1,1,1,9,1,14,1,3,1,7,13,1,1,14,1,3,1,9,1,1,1,7,1,1,1,14,1,3,1,9,13,14,1,3,1,11,1,7,1,12,13,3,13,12,1,3,1,3,1,3,1,3,1,3,1,12,13,3,13,12,1,7,1,11,1,3,1,16,1,7,1,11,1,3,1,16,1,7,1,11,1,3,1,16,13,7,1,3,1,24,1,3,1,7,1,3,1,14,11,3,1,7,1,3,1,14,1,13,1,7,1,3,1,14,1,13,1,7,1,3,1,14,1,13,1,7,1,3,1,14,1,13,1,7,5,14,1,13,1,26,1,13,1,26,1,13,1,26,1,13,1,26,1,13,1,26,1,3,11,26,1,3,1,36,9,36,1,3,1,36,1,3,1,36,1,3,1,36,1,3,1,36,1,3,1,36,1,3,1,36,1,3,1,36,1,3,1,36,1,3,1,36,5,32
|
||||
|
|
273
day17/py/main.py
273
day17/py/main.py
|
@ -1,8 +1,10 @@
|
|||
import collections
|
||||
import enum
|
||||
import itertools
|
||||
import re
|
||||
import sys
|
||||
import networkx
|
||||
from typing import List, Iterable, Tuple, Optional, Set
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
|
||||
|
||||
# Halt indicates that the assembled program should terminate
|
||||
|
@ -189,9 +191,76 @@ def execute_program(memory: Memory, program_inputs: List[int], initial_instructi
|
|||
|
||||
# Problem specific code starts here
|
||||
SCAFFOLD_CHAR = ord('#')
|
||||
ROBOT_CHAR = ord('^')
|
||||
|
||||
|
||||
def build_graph_from_program(program_memory: Memory) -> networkx.Graph:
|
||||
class TurnDirection(enum.Enum):
|
||||
LEFT = 'L'
|
||||
RIGHT = 'R'
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class Direction(enum.IntEnum):
|
||||
NORTH = 1
|
||||
EAST = 2
|
||||
SOUTH = 3
|
||||
WEST = 4
|
||||
|
||||
# Given the direction we currently are, get the direction we need to turn to face this coordinate.
|
||||
# Returns None if these are the same coordinate.
|
||||
@staticmethod
|
||||
def get_direction_to_coordinate(current_pos: Tuple[int, int], next_pos: Tuple[int, int]) -> Optional['Direction']:
|
||||
current_row, current_col = current_pos
|
||||
next_row, next_col = next_pos
|
||||
direction_required = None
|
||||
if next_col == current_col and next_row < current_row:
|
||||
direction_required = Direction.NORTH
|
||||
elif next_col == current_col and next_row > current_row:
|
||||
direction_required = Direction.SOUTH
|
||||
elif next_row == current_row and next_col < current_col:
|
||||
direction_required = Direction.WEST
|
||||
elif next_row == current_row and next_col > current_col:
|
||||
direction_required = Direction.EAST
|
||||
|
||||
return direction_required
|
||||
|
||||
# Gets the direction we need to turn for fix.
|
||||
# Returns None if we are currently facing the right direction
|
||||
def get_turn_to_direction(self, new_direction: 'Direction') -> Optional[Tuple['TurnDirection', ...]]:
|
||||
turn_distance = (self - new_direction) % 4
|
||||
# if turn_distance == 2:
|
||||
# breakpoint()
|
||||
if turn_distance == 0:
|
||||
return None
|
||||
elif turn_distance == 3:
|
||||
return (TurnDirection.RIGHT,)
|
||||
else:
|
||||
return (TurnDirection.LEFT,) * turn_distance
|
||||
|
||||
def move_coords_in_direction(self, pos: Tuple[int, int]) -> Tuple[int, int]:
|
||||
D_ROWS = {
|
||||
Direction.NORTH: -1,
|
||||
Direction.SOUTH: 1,
|
||||
Direction.EAST: 0,
|
||||
Direction.WEST: 0
|
||||
}
|
||||
|
||||
D_COLS = {
|
||||
Direction.NORTH: 0,
|
||||
Direction.SOUTH: 0,
|
||||
Direction.EAST: 1,
|
||||
Direction.WEST: -1
|
||||
}
|
||||
|
||||
return (pos[0] + D_ROWS[self], pos[1] + D_COLS[self])
|
||||
|
||||
|
||||
# Return a graph of all of the scaffolding, with a tuple represeting the robot's starting position
|
||||
def build_graph_from_program(initial_memory_state: Memory) -> (networkx.Graph, Tuple[int, int]):
|
||||
program_memory = initial_memory_state.copy()
|
||||
|
||||
# Add all edges that are directly above/below or directly left/right of our cursor
|
||||
def add_adjacent_edges(scaffold_graph: networkx.Graph, row_cursor: int, col_cursor: int):
|
||||
scaffold_graph.add_node((row_cursor, col_cursor))
|
||||
|
@ -207,6 +276,7 @@ def build_graph_from_program(program_memory: Memory) -> networkx.Graph:
|
|||
scaffold_graph = networkx.Graph()
|
||||
row_cursor = 0
|
||||
col_cursor = 0
|
||||
robot_pos = None
|
||||
for item in outputs:
|
||||
if item == ord('\n'):
|
||||
row_cursor += 1
|
||||
|
@ -214,18 +284,206 @@ def build_graph_from_program(program_memory: Memory) -> networkx.Graph:
|
|||
continue
|
||||
elif item == SCAFFOLD_CHAR:
|
||||
add_adjacent_edges(scaffold_graph, row_cursor, col_cursor)
|
||||
elif item == ROBOT_CHAR:
|
||||
add_adjacent_edges(scaffold_graph, row_cursor, col_cursor)
|
||||
robot_pos = (row_cursor, col_cursor)
|
||||
|
||||
col_cursor += 1
|
||||
|
||||
return scaffold_graph
|
||||
return scaffold_graph, robot_pos
|
||||
|
||||
|
||||
def part1(initial_program_memory: Memory) -> int:
|
||||
scaffold_graph = build_graph_from_program(initial_program_memory)
|
||||
def make_greedy_path(scaffold_graph: networkx.Graph, start_pos: Tuple[int, int]) -> str:
|
||||
path_components = []
|
||||
forward_count = 0
|
||||
robot_direction = Direction.NORTH
|
||||
visited = set()
|
||||
node_cursor = start_pos
|
||||
while visited != set(scaffold_graph.nodes):
|
||||
next_pos = robot_direction.move_coords_in_direction(node_cursor)
|
||||
if next_pos not in scaffold_graph:
|
||||
possible_points = set(scaffold_graph.neighbors(node_cursor)) - set(visited)
|
||||
next_pos = sorted(possible_points, key=lambda x: (x[0], x[1]))[0]
|
||||
new_direction = Direction.get_direction_to_coordinate(node_cursor, next_pos)
|
||||
turns_needed = robot_direction.get_turn_to_direction(new_direction)
|
||||
robot_direction = new_direction
|
||||
|
||||
if forward_count > 0:
|
||||
path_components.append(str(forward_count))
|
||||
path_components += [str(turn) for turn in turns_needed]
|
||||
forward_count = 0
|
||||
|
||||
visited.add(node_cursor)
|
||||
visited.add(next_pos)
|
||||
node_cursor = next_pos
|
||||
forward_count += 1
|
||||
if forward_count > 0:
|
||||
path_components.append(str(forward_count))
|
||||
|
||||
return ','.join(path_components)
|
||||
|
||||
|
||||
# Find a component of the string that occurs more than once, starting at the given position and checking forwards/backwards
|
||||
# based on the given offset
|
||||
def find_component(path: str, start: int, offset: int) -> str:
|
||||
component = path[start:offset] if offset > 0 else path[start + offset:]
|
||||
if (offset > 0 and component[-1] != ',') or (offset < 0 and component[0] != ','):
|
||||
return None
|
||||
elif path.count(component) == 1:
|
||||
return None
|
||||
|
||||
return component.strip(',')
|
||||
|
||||
|
||||
# Get all substrings of s without the given component
|
||||
def get_substrings_without_str(s: str, component: str) -> str:
|
||||
locations = [(match.start(), match.end()) for match in re.finditer(re.escape(component), s)]
|
||||
locations.insert(0, (None, None))
|
||||
locations.append((None, None))
|
||||
|
||||
substrings = [s[location1[1]:location2[0]].strip(',') for location1, location2 in zip(locations, locations[1:])]
|
||||
|
||||
return [substring for substring in substrings if len(substring) > 0]
|
||||
|
||||
|
||||
# If a path string is a duplicate itself, strip it down to its base component
|
||||
def dedup_path_string(s: str) -> str:
|
||||
normalized_s = s
|
||||
# Need to join the two components with a comma so that we actually can spot the repetition (the string may not have a trailing comma)
|
||||
if s[-1] != ',':
|
||||
normalized_s = s + ','
|
||||
|
||||
# Find if the string is only composed of a portion of itself
|
||||
repeat_index = (normalized_s + normalized_s).find(normalized_s, 1, -1)
|
||||
if repeat_index == -1:
|
||||
return s
|
||||
else:
|
||||
return s[:repeat_index].rstrip(',')
|
||||
|
||||
|
||||
# Given the two other components, see if there's one final component left in the string
|
||||
def get_last_component(path: str, component1: str, component2: str) -> Optional[str]:
|
||||
path_without_component1 = get_substrings_without_str(path, component1)
|
||||
# Get the path without component 1 or component 2
|
||||
remaining_comonents = []
|
||||
for substring in path_without_component1:
|
||||
remaining_comonents += get_substrings_without_str(substring, component2)
|
||||
|
||||
# Make sure the last component we have is unique
|
||||
if len(set(remaining_comonents)) > 1:
|
||||
return None
|
||||
|
||||
last_component = dedup_path_string(remaining_comonents[0])
|
||||
if len(last_component) > 20:
|
||||
return None
|
||||
|
||||
return last_component
|
||||
|
||||
|
||||
# Find the three compressible components of the path
|
||||
# This is NOT pretty. This could be generalized by searching for all substrings, but that would be longer
|
||||
def find_compressable_path_components(path: str) -> Tuple[str, str, str]:
|
||||
MAX_LENGTH = 20
|
||||
for i in range(MAX_LENGTH + 1):
|
||||
path_candidate = path
|
||||
# Find the first component at the start of the string
|
||||
component1 = find_component(path_candidate, 0, i)
|
||||
if component1 is None:
|
||||
continue
|
||||
|
||||
# Remove it from both ends
|
||||
path_candidate = path_candidate[len(component1):].lstrip(',')
|
||||
if path_candidate.endswith(component1):
|
||||
path_candidate = path_candidate[:-len(component1)].rstrip(',')
|
||||
|
||||
for j in range(MAX_LENGTH + 1):
|
||||
trimmed_candidate = path_candidate
|
||||
# We know there must be another unique component at the end of the string
|
||||
component2 = find_component(trimmed_candidate, len(trimmed_candidate) - 1, -j)
|
||||
if component2 is None:
|
||||
continue
|
||||
|
||||
# Remove it from both ends
|
||||
trimmed_candidate = trimmed_candidate[:-len(component2)].rstrip(',')
|
||||
if trimmed_candidate.startswith(component2):
|
||||
trimmed_candidate = trimmed_candidate[len(component2):].lstrip(',')
|
||||
|
||||
component3 = get_last_component(trimmed_candidate, component1, component2)
|
||||
if component3 is None:
|
||||
continue
|
||||
|
||||
return component1, component2, component3
|
||||
else:
|
||||
raise Exception("path is not compressible into three functions")
|
||||
|
||||
|
||||
# Convert a path into a list of functions based on the given function defintiions
|
||||
def make_function_nav_string(path: str, functions: Dict[str, str]) -> str:
|
||||
i = 0
|
||||
function_nav_string = []
|
||||
while i < len(path):
|
||||
for function_name, function in sorted(functions.items(), key=lambda x: len(x[1]), reverse=True):
|
||||
if path[i:i+len(function)] == function:
|
||||
function_nav_string.append(function_name)
|
||||
# +1 for the comma
|
||||
i += len(function) + 1
|
||||
break
|
||||
else:
|
||||
raise ValueError('Functions not in path')
|
||||
|
||||
return ','.join(function_nav_string)
|
||||
|
||||
|
||||
def part1(scaffold_graph: networkx.Graph) -> int:
|
||||
return sum(row * col for row, col in scaffold_graph.nodes if scaffold_graph.degree((row, col)) > 2)
|
||||
|
||||
|
||||
def part2(initial_memory_state: Memory, scaffold_graph: networkx.Graph, robot_pos: Tuple[int, int]):
|
||||
def make_ascii_input(s: str) -> str:
|
||||
return [ord(char) for char in s]
|
||||
|
||||
nav_string = make_greedy_path(scaffold_graph, robot_pos)
|
||||
functions = find_compressable_path_components(nav_string)
|
||||
named_functions = {name: function for name, function in zip(['A', 'B', 'C'], functions)}
|
||||
function_nav_string = make_function_nav_string(nav_string, named_functions)
|
||||
|
||||
# Start the sequence of the interacitve mode
|
||||
program_memory = initial_memory_state.copy()
|
||||
program_memory[0] = 2
|
||||
_, _, outputs = execute_program(program_memory, [
|
||||
*make_ascii_input(function_nav_string + '\n'),
|
||||
*make_ascii_input(named_functions['A'] + '\n'),
|
||||
*make_ascii_input(named_functions['B'] + '\n'),
|
||||
*make_ascii_input(named_functions['C'] + '\n'),
|
||||
*make_ascii_input('n\n')
|
||||
])
|
||||
|
||||
return outputs[-1]
|
||||
|
||||
|
||||
# A debug method to print the entire graph
|
||||
def print_scaffold_graph(scaffold_graph: networkx.Graph) -> None:
|
||||
print(' ', end='')
|
||||
max_row = max(node[0] for node in scaffold_graph.nodes) + 1
|
||||
max_col = max(node[1] for node in scaffold_graph.nodes) + 1
|
||||
|
||||
for i in range(max_col):
|
||||
print(i // 10 if i // 10 > 0 else ' ', end='')
|
||||
print('')
|
||||
print(' ', end='')
|
||||
for i in range(max_col):
|
||||
print(i % 10, end='')
|
||||
print('')
|
||||
for i in range(max_row):
|
||||
print(f'{i:2} ', end='')
|
||||
for j in range(max_col):
|
||||
if (i, j) in scaffold_graph.nodes:
|
||||
print('#', end='')
|
||||
else:
|
||||
print('.', end='')
|
||||
print('')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
# Today's part 2 produces a lot of output, so i wanted to keep them separate
|
||||
|
@ -237,4 +495,7 @@ if __name__ == "__main__":
|
|||
for i, item in enumerate(f.read().rstrip().split(",")):
|
||||
memory[i] = int(item)
|
||||
|
||||
print(part1(memory))
|
||||
scaffold_graph, robot_pos = build_graph_from_program(memory)
|
||||
print_scaffold_graph(scaffold_graph)
|
||||
print(part1(scaffold_graph))
|
||||
print(part2(memory, scaffold_graph, robot_pos))
|
||||
|
|
Loading…
Reference in a new issue