|
| 1 | +"""Day 13: Distress Signal |
| 2 | +
|
| 3 | +This module provides the solution for Advent of Code 2022 - Day 13. |
| 4 | +
|
| 5 | +It implements recursive comparison of nested list structures to determine |
| 6 | +correct packet ordering based on specific comparison rules. |
| 7 | +
|
| 8 | +The module contains a Solution class that inherits from SolutionBase for |
| 9 | +parsing and comparing packet data to decode a distress signal. |
| 10 | +""" |
| 11 | + |
| 12 | +from ast import literal_eval |
| 13 | +from functools import cmp_to_key |
| 14 | +from typing import Any |
| 15 | + |
| 16 | +from aoc.models.base import SolutionBase |
| 17 | + |
| 18 | + |
| 19 | +class Solution(SolutionBase): |
| 20 | + """Compare and sort nested list packets to decode distress signal. |
| 21 | +
|
| 22 | + This solution implements custom comparison logic for nested lists and integers |
| 23 | + following specific ordering rules. Part 1 identifies pairs already in correct |
| 24 | + order and sums their indices. Part 2 sorts all packets with divider packets |
| 25 | + and calculates the decoder key. |
| 26 | +
|
| 27 | + The comparison rules handle integers, lists, and mixed types with automatic |
| 28 | + type conversion when comparing integers against lists. |
| 29 | + """ |
| 30 | + |
| 31 | + def _parse_group(self, group: str) -> tuple[list, list]: |
| 32 | + """Parse a pair of packet strings into list structures. |
| 33 | +
|
| 34 | + Uses literal_eval to safely parse bracket-delimited packet notation |
| 35 | + into Python list structures. |
| 36 | +
|
| 37 | + Args: |
| 38 | + group: String containing two packet lines separated by newline |
| 39 | +
|
| 40 | + Returns |
| 41 | + ------- |
| 42 | + tuple[list, list]: Left and right packet as parsed list structures |
| 43 | + """ |
| 44 | + left, right = group.strip().split("\n") |
| 45 | + return literal_eval(left), literal_eval(right) |
| 46 | + |
| 47 | + def compare(self, left: Any, right: Any) -> int: |
| 48 | + """Compare two packet values recursively following distress signal rules. |
| 49 | +
|
| 50 | + Comparison rules: |
| 51 | + - Both integers: compare numerically |
| 52 | + - Both lists: compare element-by-element, then by length |
| 53 | + - Mixed types: convert integer to single-element list and retry |
| 54 | +
|
| 55 | + Args: |
| 56 | + left: Left packet value (int or list) |
| 57 | + right: Right packet value (int or list) |
| 58 | +
|
| 59 | + Returns |
| 60 | + ------- |
| 61 | + int: -1 if left < right (correct order), 0 if equal (continue), |
| 62 | + 1 if left > right (incorrect order) |
| 63 | + """ |
| 64 | + if isinstance(left, int) and isinstance(right, int): |
| 65 | + if left < right: |
| 66 | + return -1 |
| 67 | + |
| 68 | + if left > right: |
| 69 | + return 1 |
| 70 | + |
| 71 | + return 0 |
| 72 | + |
| 73 | + if isinstance(left, list) and isinstance(right, list): |
| 74 | + for idx in range(min(len(left), len(right))): |
| 75 | + result = self.compare(left[idx], right[idx]) |
| 76 | + if result != 0: |
| 77 | + return result |
| 78 | + |
| 79 | + if len(left) < len(right): |
| 80 | + return -1 |
| 81 | + |
| 82 | + if len(left) > len(right): |
| 83 | + return 1 |
| 84 | + |
| 85 | + return 0 |
| 86 | + |
| 87 | + if isinstance(left, int): |
| 88 | + return self.compare([left], right) |
| 89 | + |
| 90 | + return self.compare(left, [right]) |
| 91 | + |
| 92 | + def part1(self, data: str) -> int: |
| 93 | + """Find sum of indices of packet pairs already in correct order. |
| 94 | +
|
| 95 | + Examines each pair of packets and identifies which are already correctly |
| 96 | + ordered according to the distress signal comparison rules. Returns the |
| 97 | + sum of 1-based indices for pairs in correct order. |
| 98 | +
|
| 99 | + Args: |
| 100 | + data: Raw input string with packet pairs separated by blank lines |
| 101 | +
|
| 102 | + Returns |
| 103 | + ------- |
| 104 | + int: Sum of 1-indexed pair numbers that are already in correct order |
| 105 | + """ |
| 106 | + score = 0 |
| 107 | + |
| 108 | + for idx, group in enumerate(data.split("\n\n"), start=1): |
| 109 | + left, right = self._parse_group(group) |
| 110 | + |
| 111 | + if self.compare(left, right) == -1: |
| 112 | + score += idx |
| 113 | + |
| 114 | + return score |
| 115 | + |
| 116 | + def part2(self, data: str) -> int: |
| 117 | + """Calculate decoder key by sorting packets with divider packets. |
| 118 | +
|
| 119 | + Adds two divider packets [[2]] and [[6]] to all packets, sorts them |
| 120 | + using the distress signal comparison rules, and calculates the decoder |
| 121 | + key as the product of the 1-based indices of the divider packets. |
| 122 | +
|
| 123 | + Args: |
| 124 | + data: Raw input string with packet pairs separated by blank lines |
| 125 | +
|
| 126 | + Returns |
| 127 | + ------- |
| 128 | + int: Decoder key (product of divider packet indices after sorting) |
| 129 | + """ |
| 130 | + packets: list[Any] = [[[2]], [[6]]] |
| 131 | + for group in data.split("\n\n"): |
| 132 | + left, right = self._parse_group(group) |
| 133 | + packets.append(left) |
| 134 | + packets.append(right) |
| 135 | + |
| 136 | + packets.sort(key=cmp_to_key(self.compare)) |
| 137 | + return (packets.index([[2]]) + 1) * (packets.index([[6]]) + 1) |
0 commit comments