Advertisement
MHorikx

AoC 2022 day 22

Jan 12th, 2023
740
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 13.29 KB | None | 0 0
  1. import re
  2. import numpy as np
  3. import itertools as it
  4.  
  5. from dataclasses import dataclass
  6. from time import perf_counter as pf
  7.  
  8. @dataclass
  9. class Vertex:
  10.     coords: np.ndarray
  11.     square_count: int
  12.     next_direc: int
  13.  
  14. def cross_2d(vec_1, vec_2):
  15.     # 2d analogue of 3d cross product. If this returns 1, then vec_2 is
  16.     # a 90 degree counterclockwise rotation of vec_1. If this returns -1, then
  17.     # vec_2 is a 90 degree clockwise rotation of vec_2. If this returns 0, then
  18.     # vec_1 and vec_2 are aligned.
  19.     return vec_1[0] * vec_2[1] - vec_1[1] * vec_2[0]
  20.  
  21. def get_direc(edge):
  22.     # Get the direction on a square that an edge is located. 0 is the right side
  23.     # of a square, 1 is the bottom side, etc.
  24.     # The edge is directed and clockwise.
  25.     base = np.array([1, 0])
  26.     if (edge == base).all():
  27.         return 0
  28.     return cross_2d(base, edge) + 2
  29.  
  30. def get_sq_coord(edge_end, direc):
  31.     # Get the coordinates of the top left corner of the square that contains
  32.     # the directed edge in the clockwise direction given the endpoint of the
  33.     # edge and which side of the square it is. As before, 0 is right, 1 is
  34.     # bottom, etc.
  35.     offsets = [np.array([1, 1]),
  36.                np.array([1, 0]),
  37.                np.array([0, 0]),
  38.                np.array([0, 1])]
  39.     return tuple(edge_end - offsets[direc])
  40.  
  41. def split_path(string):
  42.     # Replace all integers with themselves but with commas around them, then
  43.     # split the string by commas, then discard the empty first and last bits.
  44.     # This splits the input of the form e.g. 10R5L5 into [10, R, 5, L, 5].
  45.     return re.sub('(-*\d+)', r',\1,', string).split(',')[1:-1]
  46.  
  47. def get_sq_coords(cube_net, side_len):
  48.     sq_coords = []
  49.     # As there are six squares in a cube net, we only need to check a region of
  50.     # at most 6x6. As a matter of fact, 4x4 should suffice, but this works and
  51.     # is a bit easier to explain. Find the coordinates of the top left corners
  52.     # of all squares.
  53.     for num in range(36):
  54.         i = num // 6
  55.         j = num % 6
  56.         if i * side_len >= len(cube_net) or j * side_len >= len(cube_net[i * side_len]):
  57.             continue
  58.         if cube_net[side_len * i][side_len * j] == ' ':
  59.             continue
  60.         sq_coords.append(np.array([i, j]))
  61.         if len(sq_coords) == 6:
  62.             break
  63.     return sq_coords
  64.  
  65. def get_square(cube_net, side_len, coord):
  66.     # Given a list of strings, cut out a square and convert it to a set of
  67.     # obstacles.
  68.     slice_rows = slice(coord[0] * side_len, (coord[0] + 1) * side_len)
  69.     slice_columns = slice(coord[1] * side_len, (coord[1] + 1) * side_len)
  70.     return {(a, b) for a, line in enumerate(cube_net[slice_rows])
  71.             for b, char in enumerate(line[slice_columns]) if char == '#'}
  72.  
  73. def get_part_1_connections(sq_coords):
  74.     # In Part 1, wrap in 2D. E.g. moving off of the net on the right side will
  75.     # make you reappear on the left. No direction changes occur.
  76.     # Membership checking of numpy arrays is a pain, so convert to tuple.
  77.     sq_coords_tuples = [tuple(coord) for coord in sq_coords]
  78.     connections = {tuple(coord): {} for coord in sq_coords}
  79.     direc_moves = [np.array([0, 1]),
  80.                    np.array([1, 0]),
  81.                    np.array([0, -1]),
  82.                    np.array([-1, 0])]
  83.     for i, coord in enumerate(sq_coords):
  84.         # Right is 0, down is 1, left is 2, up is 3.
  85.         for j, move in enumerate(direc_moves):
  86.             new_coord = coord.copy()
  87.             while True:
  88.                 # Keep walking in the direction specified by j. Use modulo
  89.                 # to wrap around. The first square encountered is the square to
  90.                 # connect to.
  91.                 new_coord += move
  92.                 new_coord %= 6
  93.                 if tuple(new_coord) not in sq_coords_tuples:
  94.                     continue
  95.                 # When moving off of square i in direction j, go to square
  96.                 # wrap_sq_ind with direction change 0.
  97.                 connections[tuple(coord)][j] = ((tuple(new_coord), 0))
  98.                 break
  99.     return connections
  100.  
  101. def get_corner_path(sq_coords):
  102.     # Make a path along the outside of the cube net, via the corners. This will
  103.     # allow easily matching up the outside edges.
  104.  
  105.     # Each square has only its top left corner specified. This will generate
  106.     # all corners.
  107.     offsets = [np.array([0, 0]),
  108.                np.array([0, 1]),
  109.                np.array([1, 0]),
  110.                np.array([1, 1])]
  111.     corners = {tuple(coord + offset) for coord, offset in it.product(sq_coords, offsets)}
  112.  
  113.     # Start walking along the outside of the cube net. As sq_coords starts with
  114.     # the leftmost square in the top row, its top edge is definitely on the
  115.     # outside. If we walk from the top left to top right corner, the inside is
  116.     # on the right. Hence, we want to first check if the next corner is 'to the
  117.     # left', then we want to check if the next corner is 'straight ahead', and
  118.     # finally if it is 'to the right'. This will keep the inside on the right
  119.     # of the edge we are looking at, which is formed by walking from the
  120.     # previous to the current corner.
  121.     prev = sq_coords[0]
  122.     cur = sq_coords[0] + offsets[1]
  123.     rot_mat = np.array([[0, 1], [-1, 0]])
  124.     corner_path = [cur.copy()]
  125.     while not (cur == sq_coords[0]).all():
  126.         diff = prev - cur
  127.         for i in range(3):
  128.             # The rotation matrix rotates 90 degrees clockwise.
  129.             diff = np.matmul(rot_mat, diff)
  130.             if tuple(cur + diff) not in corners:
  131.                 continue
  132.             prev = cur
  133.             cur = cur + diff
  134.             corner_path.append(cur.copy())
  135.             break
  136.  
  137.     # Convert it to a list of Vertex instances, which have additional info.
  138.     corner_count = len(corner_path)
  139.     vertex_path = [Vertex(corner, 0, -1) for corner in corner_path]
  140.     for i, vertex in enumerate(vertex_path):
  141.         prev = vertex_path[i - 1]
  142.         nxt = vertex_path[(i + 1) % corner_count]
  143.         prev_edge = prev.coords - vertex.coords
  144.         next_edge = nxt.coords - vertex.coords
  145.         # The number of squares inside the net at that vertex.
  146.         vertex.square_count = 2 - cross_2d(prev_edge, next_edge)
  147.         # Which side of a square the edge to the next vertex is.
  148.         vertex.next_direc = get_direc(next_edge)
  149.     return vertex_path
  150.  
  151. def get_part_2_connections(sq_coords, connections):
  152.     # In Part 2, wrap as if the net is folded into a cube.
  153.     vertex_path = get_corner_path(sq_coords)
  154.     # Remove vertices two at a time.
  155.     while vertex_path:
  156.         for i, vertex in enumerate(vertex_path):
  157.             # Find a vertex where three squares combine, as we are forming
  158.             # a cube.
  159.             if vertex.square_count != 3:
  160.                 continue
  161.  
  162.             # We will remove the current and next vertex, and link the previous
  163.             # vertex to the next next vertex. Effectively, we are folding three
  164.             # squares together at a vertex, which completes that vertex and
  165.             # merges the previous and next vertices into a single one.
  166.             prev = vertex_path[i - 1]
  167.             nxt = vertex_path[(i + 1) % len(vertex_path)]
  168.             prev_direc = prev.next_direc
  169.             next_direc = vertex.next_direc
  170.  
  171.             # Find out which squares the edges to and from the current vertex
  172.             # belong to.
  173.             sq_1 = get_sq_coord(vertex.coords, prev_direc)
  174.             sq_2 = get_sq_coord(nxt.coords, next_direc)
  175.  
  176.             # Set the correct connections - the edges to and from the vertex
  177.             # will fold together, and the direction change is 2 + difference.
  178.             # E.g. going from a right edge (0) to a right edge (0) reverses
  179.             # direction (direction changes by 2), and going from a right edge
  180.             # (0) to a top edge (3) changes direction to 1 (direction changes
  181.             # by 1).
  182.             connections[sq_1][prev_direc] = (sq_2, (2 + next_direc - prev_direc) % 4)
  183.             connections[sq_2][next_direc] = (sq_1, (2 + prev_direc - next_direc) % 4)
  184.  
  185.             # Update the data for the previous vertex.
  186.             prev.square_count += nxt.square_count
  187.             prev.next_direc = nxt.next_direc
  188.  
  189.             # Remove the current vertex, and the next vertex. Note that since we only
  190.             # store the direction of the next edge (edge from a vertex), and we need
  191.             # the endpoint of an edge to determine the square it belongs to, we need
  192.             # to keep the previous vertex.
  193.             vertex_path.pop(i)
  194.             vertex_path.pop(i)
  195.             break
  196.     return connections
  197.  
  198. def rotate_pos(pos, direc_change, rot_mat):
  199.     # direc_change is how many multiples of 90 degrees the direction changes
  200.     # in the clockwise direction. rot_mat is the matrix to rotate clockwise by
  201.     # 90 degrees. Due to weirdness of row and column coordinates, offsets are
  202.     # necessary.
  203.     i_offset = -1 if direc_change in (2, 3) else 0
  204.     j_offset = -1 if direc_change in (1, 2) else 0
  205.     offset = np.array([i_offset, j_offset])
  206.     matrix = np.linalg.matrix_power(rot_mat, direc_change)
  207.     return np.matmul(matrix, pos) + offset
  208.  
  209. def follow_path(path, squares, connections, side_len, sq_coords):
  210.     # Start at the top left corner of the top left square, facing right.
  211.     # 0 is right, 1 is down, etc.
  212.     cur_pos = np.array([0, 0])
  213.     cur_direc = 0
  214.     cur_square = tuple(sq_coords[0])
  215.  
  216.     # The actual movements when moving right, down, etc., and the matrix to
  217.     # rotate 90 degrees clockwise.
  218.     direc_moves = [np.array([0, 1]),
  219.                    np.array([1, 0]),
  220.                    np.array([0, -1]),
  221.                    np.array([-1, 0])]
  222.     rot_mat = np.array([[0, 1], [-1, 0]])
  223.  
  224.     for instr in path:
  225.         if instr == 'L':
  226.             cur_direc -= 1
  227.             cur_direc %= 4
  228.         elif instr == 'R':
  229.             cur_direc += 1
  230.             cur_direc %= 4
  231.         else:
  232.             dist = int(instr)
  233.             for _ in range(dist):
  234.                
  235.                 new_pos = cur_pos + direc_moves[cur_direc]
  236.                 new_square = cur_square
  237.                 new_direc = cur_direc
  238.  
  239.                 # Check if we crossed the border of the current square and need
  240.                 # to move to a new square.
  241.                 if (new_pos >= side_len).any() or (new_pos < 0).any():
  242.                     new_square, direc_change = connections[new_square][cur_direc]
  243.                     new_direc += direc_change
  244.                     new_direc %= 4
  245.                     new_pos = rotate_pos(new_pos, direc_change, rot_mat) % side_len
  246.  
  247.                 # Check if advancing a step would run into an obstacle. If it
  248.                 # would, do not save the change and stop moving.
  249.                 if tuple(new_pos) in squares[new_square]:
  250.                     break
  251.  
  252.                 # Actually move if no obstacle has been hit.
  253.                 cur_pos = new_pos
  254.                 cur_square = new_square
  255.                 cur_direc = new_direc
  256.  
  257.     return cur_pos, cur_direc, cur_square
  258.  
  259. def get_coords(pos, cur_square, side_len):
  260.     # Get the coordinates in the flattened cube net given the coordinates of
  261.     # the current square and the position on said square. As the question uses
  262.     # 1-based indexing, add 1 to both coordinates.
  263.     return pos + np.array(cur_square) * side_len + np.array([1, 1])
  264.  
  265. def main():
  266.     input_dir = "C:/Users/mmlho/Documents/Python Scripts/Advent of Code 2022/Inputs/"
  267.  
  268.     # We are given a cube net, with some obstacles on it. '.' represents empty
  269.     # space on the net, '#' represents an obstacle, and whitespace is used to
  270.     # get everything in the correct position. We are also given a string of
  271.     # instructions, e.g. 10L5R6, where L and R denote rotating left or right by
  272.     # 90 degrees, and the numbers mean moving as many steps in the current
  273.     # direction, or until an obstacle is hit. Start at the top left corner of
  274.     # the cube net and follow the instructions. Where do we end up?
  275.     # Part 1: if we hit any edge, we wrap to the 'opposite' edge of the net.
  276.     # Part 2: if we hit any edge, we wrap as if the net is folded into a cube.
  277.     with open(input_dir + "aoc_2022_day_22_test.txt") as f:
  278.         cube_net, path = f.read().rstrip().split('\n\n')
  279.  
  280.     path = split_path(path)
  281.  
  282.     # The cube net consists of six faces filled with . and #, and
  283.     # some whitespace. So 6 * side_len ** 2 == (occurences of . and #).
  284.     side_len = int(((cube_net.count('.') + cube_net.count('#')) / 6) ** 0.5)
  285.     cube_net = cube_net.split('\n')
  286.  
  287.     sq_coords = get_sq_coords(cube_net, side_len)
  288.     squares = {tuple(coord): get_square(cube_net, side_len, coord)
  289.                for coord in sq_coords}
  290.  
  291.     connections = get_part_1_connections(sq_coords)
  292.     connections_2 = get_part_2_connections(sq_coords, connections)
  293.  
  294.     pos, direc, cur_square = follow_path(path, squares, connections, side_len, sq_coords)
  295.     final_coords = get_coords(pos, cur_square, side_len)
  296.     print(f'Part 1: {1000 * final_coords[0] + 4 * final_coords[1] + direc}')
  297.    
  298.     pos_2, direc_2, cur_square_2 = follow_path(path, squares, connections_2, side_len, sq_coords)
  299.     final_coords_2 = get_coords(pos_2, cur_square_2, side_len)
  300.     print(f'Part 2: {1000 * final_coords_2[0] + 4 * final_coords_2[1] + direc_2}')
  301.  
  302. t0 = pf()
  303. main()
  304. print(pf() - t0)
  305.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement