|
| 1 | +"""Day 22""" |
| 2 | + |
1 | 3 | import sys
|
2 | 4 | import re
|
3 | 5 | from dataclasses import dataclass
|
| 6 | +import functools |
4 | 7 | from typing import List
|
5 | 8 |
|
6 |
| -LINE_REGEX = r"^(\w*) x=(.*)\.\.(.*),y=(.*)\.\.(.*)$" |
| 9 | +from sortedcontainers import SortedList |
| 10 | + |
| 11 | +LINE_REGEX = r"^(\w*) x=(.*)\.\.(.*),y=(.*)\.\.(.*),z=(.*)\.\.(.*)$" |
| 12 | + |
7 | 13 |
|
8 | 14 | @dataclass(frozen=True)
|
9 | 15 | class Cuboid:
|
| 16 | + """Cuboid abstraction""" |
| 17 | + |
10 | 18 | x1: int
|
11 | 19 | x2: int
|
12 | 20 | y1: int
|
13 | 21 | y2: int
|
14 |
| - state: str |
| 22 | + z1: int |
| 23 | + z2: int |
| 24 | + on: bool |
| 25 | + time: int |
| 26 | + |
| 27 | + |
| 28 | +def parse_state(state_str: str) -> bool: |
| 29 | + """Parse onness""" |
| 30 | + if state_str == "on": |
| 31 | + return True |
| 32 | + if state_str == "off": |
| 33 | + return False |
| 34 | + raise ValueError(f"Invalid state: {state_str}") |
| 35 | + |
| 36 | + |
| 37 | +def read_cuboids() -> List[Cuboid]: |
| 38 | + """Read from stdin""" |
| 39 | + |
| 40 | + cuboids: List[Cuboid] = [] |
| 41 | + for time, line in enumerate(l for l in sys.stdin if not l.startswith("#")): |
| 42 | + state, *numeric_input = re.search(LINE_REGEX, line.strip()).groups() |
| 43 | + cuboid = Cuboid(*map(int, numeric_input), on=parse_state(state), time=time) |
| 44 | + cuboids.append(cuboid) |
| 45 | + |
| 46 | + return cuboids |
| 47 | + |
| 48 | + |
| 49 | +@dataclass(frozen=True) |
| 50 | +@functools.total_ordering |
| 51 | +class CuboidEvent: |
| 52 | + """Cuboid start or end""" |
| 53 | + |
| 54 | + dist: int |
| 55 | + start: bool |
| 56 | + """Start or end""" |
| 57 | + on: bool |
15 | 58 | time: int
|
16 | 59 |
|
17 |
| -class LightTracker: |
18 |
| - def __init__(self): |
19 |
| - self.offs = set() |
20 |
| - self.ons = set() |
21 |
| - |
22 |
| - def track_event(self, is_start: bool, cuboid: Cuboid): |
23 |
| - if cuboid.state == "on": |
24 |
| - if is_start: |
25 |
| - self.ons.add(cuboid.time) |
26 |
| - else: |
27 |
| - self.ons.remove(cuboid.time) |
28 |
| - elif cuboid.state == "off": |
29 |
| - if is_start: |
30 |
| - self.offs.add(cuboid.time) |
31 |
| - else: |
32 |
| - self.offs.remove(cuboid.time) |
33 |
| - else: |
34 |
| - raise ValueError |
35 |
| - |
36 |
| - def currently_on(self): |
37 |
| - last_on = -1 if not self.ons else max(self.ons) |
38 |
| - last_off = -1 if not self.offs else max(self.offs) |
39 |
| - if last_on == last_off: |
40 |
| - if last_on == -1: |
| 60 | + def _to_comparable(self): |
| 61 | + return (self.dist, self.start, self.on, self.time) |
| 62 | + |
| 63 | + def __lt__(self, other: "CuboidEvent") -> bool: |
| 64 | + # return (self.dist, self.start) < (other.dist, other.start) |
| 65 | + if self.dist == other.dist: |
| 66 | + if self.start == other.start: |
41 | 67 | return False
|
42 |
| - raise ValueError |
43 |
| - return last_on > last_off |
44 |
| - |
45 |
| -cuboids: List[Cuboid] = [] |
46 |
| -for time, line in enumerate(sys.stdin): |
47 |
| - if line.startswith("#"): |
48 |
| - continue |
49 |
| - state, *numeric_input = re.search(LINE_REGEX, line.strip()).groups() |
50 |
| - x1, x2, y1, y2 = map(int, numeric_input) |
51 |
| - cuboid = Cuboid(x1, x2, y1, y2, state, time) |
52 |
| - cuboids.append(cuboid) |
53 |
| - |
54 |
| -def create_events(cuboids): |
| 68 | + return self.start is False # ending event has precedence |
| 69 | + return self.dist < other.dist |
| 70 | + |
| 71 | + |
| 72 | +def to_axis_events( |
| 73 | + cuboids: List[Cuboid], start_prop: str, end_prop: str |
| 74 | +) -> List[CuboidEvent]: |
| 75 | + """Convert list of cuboids to list of cuboid events""" |
55 | 76 | events = []
|
56 |
| - for cuboid in cuboids: |
57 |
| - events.append((True, cuboid)) |
58 |
| - events.append((False, cuboid)) |
| 77 | + for c in cuboids: |
| 78 | + events.append( |
| 79 | + CuboidEvent(dist=getattr(c, start_prop), start=True, on=c.on, time=c.time) |
| 80 | + ) |
| 81 | + events.append( |
| 82 | + CuboidEvent( |
| 83 | + dist=getattr(c, end_prop) + 1, start=False, on=c.on, time=c.time |
| 84 | + ) |
| 85 | + ) |
| 86 | + |
59 | 87 | return events
|
60 | 88 |
|
61 |
| -def get_cuboids(events, predicate): |
62 |
| - cuboids = set() |
63 |
| - for _, cuboid in events: |
64 |
| - if predicate(cuboid): |
65 |
| - cuboids.add(cuboid) |
66 |
| - return cuboids |
67 | 89 |
|
68 |
| -x_events = create_events(cuboids) |
69 |
| -print("DEBUG x_events", x_events) |
70 |
| -area = 0 |
71 |
| -last_x = None |
72 |
| -for is_start, x_cuboid in sorted(x_events, key=lambda e: (e[1].x1 if e[0] else e[1].x2, e[0])): # sort using tuple to give precedence to closing events |
73 |
| - print("DEBUG is_start, x_cuboid", is_start, x_cuboid) |
74 |
| - x = x_cuboid.x1 if is_start else x_cuboid.x2 |
75 |
| - y_events = create_events(get_cuboids(x_events, lambda c: c.x1 <= x <= c.x2)) |
76 |
| - # TODO create counter for how many new cuboids at e.g. x==2 (solves case of multiple starting/ending on same line) |
77 |
| - last_y = None |
78 |
| - y_length = 0 |
79 |
| - # TODO does x axis have to take care of whether it currently is in light or not? |
80 |
| - light_tracker = LightTracker() |
81 |
| - for is_start, y_cuboid in sorted(y_events, key=lambda e: (e[1].y1 if e[0] else e[1].y2, e[0])): |
82 |
| - y = y_cuboid.y1 if is_start else y_cuboid.y2 |
83 |
| - if last_y is not None and light_tracker.currently_on(): |
84 |
| - y_length += y - last_y + 1 |
85 |
| - print("DEBUG y_length", y_length) |
86 |
| - else: |
87 |
| - y_length = 0 |
88 |
| - |
89 |
| - light_tracker.track_event(is_start, y_cuboid) |
90 |
| - last_y = y |
91 |
| - |
92 |
| - if last_x is not None: |
93 |
| - x_diff = x - last_x + 1 |
94 |
| - print("DEBUG x_diff", x_diff) |
95 |
| - area += y_length * x_diff |
96 |
| - last_x = x |
97 |
| - |
98 |
| -print(area) |
| 90 | +def main(): |
| 91 | + """Main function""" |
| 92 | + |
| 93 | + cuboids = read_cuboids() |
| 94 | + |
| 95 | + on_count = 0 |
| 96 | + |
| 97 | + x_events = sorted(to_axis_events(cuboids, start_prop="x1", end_prop="x2")) |
| 98 | + last_y_count = 0 |
| 99 | + for x_i, x_event in enumerate(x_events): |
| 100 | + y_suitable_cuboids = [c for c in cuboids if c.x1 <= x_event.dist <= c.x2] |
| 101 | + y_count = 0 |
| 102 | + y_events = sorted( |
| 103 | + to_axis_events(y_suitable_cuboids, start_prop="y1", end_prop="y2") |
| 104 | + ) |
| 105 | + last_z_count = 0 |
| 106 | + for y_i, y_event in enumerate(y_events): |
| 107 | + active_timestamps = SortedList() |
| 108 | + z_count = 0 |
| 109 | + z_suitable_cuboids = [ |
| 110 | + c for c in y_suitable_cuboids if c.y1 <= y_event.dist <= c.y2 |
| 111 | + ] |
| 112 | + z_events = sorted( |
| 113 | + to_axis_events(z_suitable_cuboids, start_prop="z1", end_prop="z2") |
| 114 | + ) |
| 115 | + for z_i, z_event in enumerate(z_events): |
| 116 | + if active_timestamps: |
| 117 | + active_cuboid = cuboids[active_timestamps[-1]] |
| 118 | + if active_cuboid.on: |
| 119 | + z_count += z_event.dist - z_events[z_i - 1].dist |
| 120 | + |
| 121 | + if z_event.start: |
| 122 | + active_timestamps.add(z_event.time) |
| 123 | + else: |
| 124 | + active_timestamps.remove(z_event.time) |
| 125 | + |
| 126 | + y_count += (y_event.dist - y_events[y_i - 1].dist) * last_z_count |
| 127 | + last_z_count = z_count |
| 128 | + |
| 129 | + on_count += (x_event.dist - x_events[x_i - 1].dist) * last_y_count |
| 130 | + last_y_count = y_count |
| 131 | + |
| 132 | + print(on_count) |
| 133 | + |
| 134 | + |
| 135 | +if __name__ == "__main__": |
| 136 | + main() |
0 commit comments