예제 #1
0
    def determine_field_order(self, tickets: Iterable[Ticket]) -> list[str]:
        valid_tickets = [
            ticket for ticket in tickets if self.is_valid_ticket(ticket)
        ]
        assert len(valid_tickets) > 0

        fields_count = single_value(
            set(len(ticket) for ticket in valid_tickets))
        assert fields_count == len(self)

        # gather all possible indexes
        possible_rule_indexes: dict[Rule, set[int]] = {
            rule: set(index for index in range(fields_count) if all(
                rule.is_valid(ticket[index]) for ticket in valid_tickets))
            for rule in self
        }

        # find the only possible permutation
        field_order: dict[int, str] = {}
        while possible_rule_indexes:
            rule, index = next((r, single_value(ixs))
                               for r, ixs in possible_rule_indexes.items()
                               if len(ixs) == 1)
            field_order[index] = rule.field
            del possible_rule_indexes[rule]
            for possible_indexes in possible_rule_indexes.values():
                possible_indexes.discard(index)

        return [field_order[ix] for ix in range(fields_count)]
예제 #2
0
    def __init__(self, tile_rows: Iterable[Iterable[Tile]]):
        self.tile_rows = [list(row) for row in tile_rows]
        self.height = len(self.tile_rows)
        self.width = single_value(set(
            len(row)
            for row in self.tile_rows
        ))
        assert self.height >= 1
        assert self.width >= 1

        self.tiles_size = single_value(set(
            tile.size
            for row in self.tile_rows
            for tile in row
        ))
예제 #3
0
def part_2(seat_codes: Iterable[str]) -> int:
    """
    Your seat should be the only missing boarding pass in your list. However, there's a catch: some
    of the seats at the very front and back of the plane don't exist on this aircraft, so they'll
    be missing from your list as well.

    Your seat wasn't at the very front or back, though; the seats with IDs +1 and -1 from yours
    will be in your list.

    *What is the ID of your seat?*

        >>> passes = ['BFFFBBBLRL', 'BFFFBBFRRR', 'BFFFBBBLLL', 'BFFFBBBLRR']
        >>> sorted(seat_id(p) for p in passes)
        [567, 568, 570, 571]
        >>> part_2(passes)
        part 2: your seat ID is 569
        569
    """

    seat_ids = set(seat_id(code) for code in seat_codes)
    full_seat_ids = set(sid for sid in range(min(seat_ids), max(seat_ids) + 1))
    missing_seat_id = single_value(full_seat_ids - seat_ids)

    print(f"part 2: your seat ID is {missing_seat_id}")
    return missing_seat_id
예제 #4
0
def match_allergens(foods: list[Food]) -> Iterable[tuple[Ingredient, Allergen]]:
    # initialize unmatched allergens and their possible ingredients
    unmatched_a2is: dict[Allergen, set[Ingredient]] = {}
    for food in foods:
        for allergen in food.allergens:
            if allergen not in unmatched_a2is:
                unmatched_a2is[allergen] = set(food.ingredients)
            else:
                unmatched_a2is[allergen] = unmatched_a2is[allergen] & set(food.ingredients)

    # then
    while unmatched_a2is:
        # find allergen with only one possible ingredient match
        try:
            m_ingredient, m_allergen = next(
                (single_value(ingrs), allergen)
                for allergen, ingrs in unmatched_a2is.items()
                if len(ingrs) == 1
            )
        except StopIteration as stop:
            raise ValueError("no more allergens with a single match") from stop

        yield m_ingredient, m_allergen

        # mark both as matched
        del unmatched_a2is[m_allergen]
        for ingredients in unmatched_a2is.values():
            ingredients.discard(m_ingredient)
예제 #5
0
def part_1(aunts: list['Aunt'], criteria: list['Criterium']) -> int:
    """
    Your Aunt Sue has given you a wonderful gift, and you'd like to send her a thank you card.
    However, there's a small problem: she signed it "From, Aunt Sue".

    You have 500 Aunts named "Sue".

    So, to avoid sending the card to the wrong person, you need to figure out which Aunt Sue (which
    you conveniently number 1 to 500, for sanity) gave you the gift. You open the present and, as
    luck would have it, good ol' Aunt Sue got you a My First Crime Scene Analysis Machine! Just what
    you wanted. Or needed, as the case may be.

    The My First Crime Scene Analysis Machine (MFCSAM for short) can detect a few specific compounds
    in a given sample, as well as how many distinct kinds of those compounds there are. According to
    the instructions, these are what the MFCSAM can detect:

      - `children`, by human DNA age analysis.
      - `cats`. It doesn't differentiate individual breeds.
      - Several seemingly random breeds of dog: `samoyeds`, `pomeranians`, `akitas`, and `vizslas`.
      - `goldfish`. No other kinds of fish.
      - `trees`, all in one group.
      - `cars`, presumably by exhaust or gasoline or something.
      - `perfumes`, which is handy, since many of your Aunts Sue wear a few kinds.

    In fact, many of your Aunts Sue have many of these. You put the wrapping from the gift into the
    MFCSAM. It beeps inquisitively at you a few times and then prints out a message on ticker tape:

        >>> example_crits = criteria_from_text('''
        ...     children: 3
        ...     samoyeds: 1
        ...     akitas: 3
        ...     goldfish: 1
        ...     cars: 3
        ...     perfumes: 2
        ... ''')
        >>> example_crits[0]
        Criterium('children', '=', 3)

    You make a list of the things you can remember about each Aunt Sue. Things missing from your
    list aren't zero - you simply don't remember the value.

        >>> example_aunts = aunts_from_text('''
        ...     Sue 1: cars: 3, akitas: 3, goldfish: 0
        ...     Sue 2: akitas: 3, children: 1, samoyeds: 0
        ...     Sue 3: perfumes: 2, cars: 3, goldfish: 1
        ... ''')
        >>> example_aunts[0]
        Aunt(1, cars=3, akitas=3, goldfish=0)

    What is the number of the Sue that got you the gift?

        >>> part_1(example_aunts, example_crits)
        part 1: the gift is from aunt Sue 3
        3
    """

    result = single_value(filter_aunts(aunts, criteria)).number
    print(f"part 1: the gift is from aunt Sue {result}")
    return result
예제 #6
0
    def load(cls, fn: str):
        with open(fn) as file:
            lines = [line.rstrip() for line in file]

        height = len(lines)
        assert height > 0
        width = single_value(set(len(line) for line in lines))

        board = {(x, y): c
                 for y, line in enumerate(lines) for x, c in enumerate(line)}

        return cls(board, Rect.at_origin(width, height))
예제 #7
0
    def from_lines(cls, lines: Iterable[str]) -> 'Grid':
        lines_list = [line.strip() for line in lines]
        height = len(lines_list)
        width = single_value(set(len(line) for line in lines_list))

        off_x, off_y = width // 2, height // 2

        return cls((
            ((x - off_x, y - off_y), state)
            for y, line in enumerate(lines_list)
            for x, char in enumerate(line.strip())
            if (state := NodeState.from_char(char)) is not NodeState.CLEAN
        ))
예제 #8
0
    def join(cls, subgrids: Iterable[tuple[Pos, 'Grid']]) -> 'Grid':
        subgrids_dict = dict(subgrids)

        sub_size = single_value(set(sg.size for sg in subgrids_dict.values()))
        max_x = max(x for x, _ in subgrids_dict.keys())
        max_y = max(y for _, y in subgrids_dict.keys())
        assert max_x % sub_size == 0
        assert max_y % sub_size == 0

        return cls(size=max(max_x, max_y) + sub_size,
                   pixels=((cx + x, cy + y)
                           for (cx, cy), sg in subgrids_dict.items()
                           for x, y in sg.pixels))
예제 #9
0
def print_subgrids(subgrids: Iterable[tuple[Pos, Grid]]) -> None:
    subgrids_dict = dict(subgrids)
    super_xs = sorted(set(x for x, _ in subgrids_dict.keys()))
    super_ys = sorted(set(y for _, y in subgrids_dict.keys()))
    subgrid_size = single_value(
        set(subgrid.size for subgrid in subgrids_dict.values()))
    separator = "+".join("-" * subgrid_size for _ in super_xs)

    for y in super_ys:
        if y > 0:
            print(separator)
        for rows in zip(*(subgrids_dict[x, y].str_lines() for x in super_xs)):
            print("|".join(rows))
예제 #10
0
    def __init__(self, tile_id: int, pixel_rows: Iterable[str]):
        self.tile_id = tile_id
        self.pixel_rows = list(pixel_rows)

        height = len(self.pixel_rows)
        width = single_value(set(len(row) for row in self.pixel_rows))
        assert width == height
        self.size = width
        assert self.size >= 2

        self.border_top = self.pixel_rows[0]
        self.border_right = ''.join(self.pixel_rows[y][-1] for y in range(self.size))
        self.border_bottom = self.pixel_rows[-1]
        self.border_left = ''.join(self.pixel_rows[y][0] for y in range(self.size))
        self.borders = (self.border_top, self.border_right, self.border_bottom, self.border_left)
예제 #11
0
    def __init__(self, nodes: Iterable[Node], target_data_pos: Pos = None):
        self.nodes = {node.pos: node for node in nodes}
        self.rect = Rect.with_all(self.nodes.keys())
        assert self.rect.area == len(self.nodes)

        if target_data_pos:
            assert target_data_pos in self.rect
            self.target_data_pos = target_data_pos
        else:
            self.target_data_pos = self.rect.top_right

        self.empty_node_pos = single_value(
            pos
            for pos, node in self.nodes.items()
            if node.used == 0
        )

        self._key = self._create_hash_key()
예제 #12
0
def part_2(box_ids: Iterable[str]) -> str:
    """
    Confident that your list of box IDs is complete, you're ready to find the boxes full of
    prototype fabric.

    The boxes will have IDs which differ by exactly one character at the same position in both
    strings. For example, given the following box IDs:

        >>> example = box_ids_from_text('''
        ...     abcde
        ...     fghij
        ...     klmno
        ...     pqrst
        ...     fguij
        ...     axcye
        ...     wvxyz
        ... ''')
        >>> example
        ['abcde', 'fghij', 'klmno', 'pqrst', 'fguij', 'axcye', 'wvxyz']

    The IDs `abcde` and `axcye` are close, but they differ by two characters (the second and
    fourth). However, the IDs `fghij` and `fguij` differ by exactly one character, the third
    (`h` and `u`). Those must be the correct boxes.

    **What letters are common between the two correct box IDs?** (In the example above, this is
    found by removing the differing character from either ID, producing `fgij`.)

        >>> part_2(example)
        part 2: correct box IDs are:
          1. fghij
          2. fguij
        => common letters: fgij
        'fgij'
    """

    box_id_1, box_id_2 = single_value(similar_strings(box_ids))
    common = common_letters(box_id_1, box_id_2)
    print(
        f"part 2: correct box IDs are:\n"
        f"  1. {box_id_1}\n"
        f"  2. {box_id_2}\n"
        f"=> common letters: {common}"
    )
    return common
예제 #13
0
    def find_in(self, drawn_image: str) -> Iterable[Pos]:
        image_lines = drawn_image.splitlines()

        def is_match(i_x, i_y):
            return all(
                image_pixel == '#'
                for pattern_line, image_line in zip(self.pixel_rows, image_lines[i_y:])
                for pattern_pixel, image_pixel in zip(pattern_line, image_line[i_x:])
                if pattern_pixel == '#'
            )

        image_height = len(image_lines)
        image_width = single_value(set(len(line) for line in image_lines))

        return (
            (x, y)
            for x in range(image_width - self.width + 1)
            for y in range(image_height - self.height + 1)
            if is_match(x, y)
        )
예제 #14
0
def part_2(claims: Iterable['Claim']) -> int:
    """
    Amidst the chaos, you notice that exactly one claim doesn't overlap by even a single square inch
    of fabric with any other claim. If you can somehow draw attention to it, maybe the Elves will be
    able to make Santa's suit after all!

    For example, in the claims above, only claim `3` is intact after all claims are made:

        >>> example_claims = claims_from_file('data/03-example.txt')
        >>> list(without_overlaps(example_claims))
        [Claim(id_=3, left=5, top=5, width=2, height=2)]

    **What is the ID of the only claim that doesn't overlap?**

        >>> part_2(example_claims)
        part 2: claim ID 3 overlaps no other claims
        3
    """

    claim_id = single_value(without_overlaps(claims)).id_
    print(f"part 2: claim ID {claim_id} overlaps no other claims")
    return claim_id
예제 #15
0
def claim_areas(coordinates: Iterable[Pos],
                include_infinite: bool = False) -> dict[Pos, set[Pos]]:
    def neighbors(pos: Pos) -> Iterable[Pos]:
        x, y = pos
        yield x + 1, y
        yield x - 1, y
        yield x, y + 1
        yield x, y - 1

    positions = sorted(coordinates)
    # determine boundaries
    bounds = Rect.with_all(positions).grow_by(+3, +3)
    # position -> claimed by which original coordinate
    claimed_by: dict[Pos, Pos | None] = {pos: pos for pos in positions}
    new_claims: dict[Pos, set[Pos]] = {pos: {pos} for pos in positions}

    while any(len(claimants) == 1 for claimants in new_claims.values()):
        # collect all new claims
        new_claims = dgroupby_pairs_set(
            (neighbor, claimant) for pos, claimants in new_claims.items()
            for neighbor in neighbors(pos) if neighbor not in claimed_by
            if neighbor in bounds for claimant in claimants)
        # mark new claims (with single claimant) and any position with draws
        claimed_by.update({
            pos: single_value(claimants) if len(claimants) == 1 else None
            for pos, claimants in new_claims.items()
        })

    # optionally ignore all claims reaching bounds (infinite?)
    if not include_infinite:
        ignored = set(claimant for pos in bounds.border_ps()
                      if (claimant := claimed_by[pos]))
    else:
        ignored = set()

    # return the areas: claimant -> set of their claimed positions
    return dgroupby_pairs_set((claimant, pos)
                              for pos, claimant in claimed_by.items()
                              if claimant if claimant not in ignored)
예제 #16
0
def run_bots(init: State, comparisons: Comparisons) -> Simulation:
    state = mutable_state(init)

    try:
        while True:
            yield frozen_state(state)

            active_bot = next(key for key, chips in state.items()
                              if is_bot(key) and len(chips) > 1)
            distributed_chips = state.pop(active_bot)
            assert len(distributed_chips) == 2

            low_chip, high_chip = sorted(distributed_chips)
            low_target, high_target = comparisons[active_bot]
            state[low_target].append(low_chip)
            state[high_target].append(high_chip)

    except StopIteration:
        return {
            output_number(key): single_value(chips)
            for key, chips in state.items()
        }
예제 #17
0
def map_opnums_to_opcodes(samples: Iterable[Sample]) -> Iterable[tuple[int, str]]:
    possible_n2c: dict[int, set[str]] = {
        opnum: set(all_ops.keys())
        for opnum in range(len(all_ops))
    }

    for sample in samples:
        possible_n2c[sample.opnum].intersection_update(sample.possible_opcodes())

    while possible_n2c:
        try:
            opnum, opcode = next(
                (n, single_value(cs))
                for n, cs in possible_n2c.items()
                if len(cs) == 1
            )
        except StopIteration as stop:
            raise ValueError("no more matches") from stop

        yield opnum, opcode
        del possible_n2c[opnum]
        for opcodes in possible_n2c.values():
            opcodes.discard(opcode)
예제 #18
0
def part_2(aunts: list['Aunt'], criteria: list['Criterium']) -> int:
    r"""
    As you're about to send the thank you note, something in the MFCSAM's instructions catches your
    eye. Apparently, it has an outdated retroencabulator, and so the output from the machine isn't
    exact values - some of them indicate ranges.

    In particular, the `cats` and `trees` readings indicates that there are **greater than** that
    many (due to the unpredictable nuclear decay of cat dander and tree pollen), while the
    `pomeranians` and `goldfish` readings indicate that there are **fewer than** that many (due to
    the modial interaction of magnetoreluctance).

    What is the number of the real Aunt Sue?

        >>> example_criteria = criteria_from_file('data/16-criteria.txt')
        >>> example_criteria_adjusted = list(adjust_criteria(example_criteria))
        >>> example_criteria_adjusted[1]
        Criterium('cats', '>', 7)
        >>> example_criteria_adjusted[3]
        Criterium('pomeranians', '<', 3)
        >>> example_criteria_adjusted[6]
        Criterium('goldfish', '<', 5)
        >>> example_criteria_adjusted[7]
        Criterium('trees', '>', 3)
        >>> example_aunts = aunts_from_file('data/16-example.txt')
        >>> print("\n".join(str(aunt) for aunt in example_aunts))
        Sue 1: cats: 10, pomeranians: 2, trees: 4
        Sue 2: goldfish: 3, trees: 3, children: 0
        Sue 3: pomeranians: 1, cats: 5, perfumes: 6

        >>> part_2(example_aunts, example_criteria)
        part 2: the gift is from aunt Sue 1
        1
    """

    result = single_value(filter_aunts(aunts, adjust_criteria(criteria))).number
    print(f"part 2: the gift is from aunt Sue {result}")
    return result
예제 #19
0
 def __len__(self):
     return single_value(
         set(sum(len(rule) for rule in group) for group in self.groups))
예제 #20
0
def strip_line(line: str, prefix: str, suffix: str) -> str:
    """
    >>> strip_line("What is love?", "What is ", "?")
    'love'
    """
    return single_value(_parse_line_fixes(line, prefix, suffix))
예제 #21
0
def state_dimensions(state: set[Pos]) -> int:
    return single_value(set(len(pos) for pos in state))
예제 #22
0
def part_2(carts_map: 'Map') -> str:
    r"""
    There isn't much you can do to prevent crashes in this ridiculous system. However, by predicting
    the crashes, the Elves know where to be in advance and **instantly remove the two crashing
    carts** the moment any crash occurs.

    They can proceed like this for a while, but eventually, they're going to run out of carts. It
    could be useful to figure out where the last cart that **hasn't** crashed will end up.

    For example:

        >>> example_map = Map.from_file('data/13-example-2.txt')
        >>> example_map.run(draw=True)
        />-<\
        |   |
        | /<+-\
        | | | v
        \>+</ |
          |   ^
          \<->/
        <BLANKLINE>
        /-X-\
        |   |
        | v-+-\
        | | | |
        \-X-/ X
          |   |
          ^---^
        <BLANKLINE>
        /---\
        |   |
        | /-+-\
        | v | |
        \-+-/ |
          ^   ^
          \---/
        <BLANKLINE>
        /---\
        |   |
        | /-+-\
        | | | |
        \-X-/ ^
          |   |
          \---/

    After four very expensive crashes, a tick ends with only one cart remaining; its final location
    is 6,4.

        >>> single_value(example_map.carts)
        (6, 4)

    **What is the location of the last cart** at the end of the first tick where it is the only cart
    left?

        >>> part_2(Map.from_file('data/13-example-2.txt'))
        part 2: last remaining cart is at 6,4
        '6,4'
    """

    carts_map = carts_map.copy()
    carts_map.run()  # run until last cart remains
    last_cart_x, last_cart_y = single_value(carts_map.carts)
    result = f"{last_cart_x},{last_cart_y}"

    print(f"part 2: last remaining cart is at {result}")
    return result
예제 #23
0
def group_from_text(text: str) -> Group:
    return single_value(groups_from_text(text))
예제 #24
0
def target_area_from_file(fn: str) -> Rect:
    return target_area_from_text(
        single_value(open(relative_path(__file__, fn))).strip())
예제 #25
0
def find_summation(numbers: list[int], *, total: int,
                   count: int) -> tuple[int, ...]:
    return single_value(comb for comb in combinations(numbers, count)
                        if sum(comb) == total)
예제 #26
0
 def __init__(self, pixel_rows: Iterable[str]):
     self.pixel_rows = list(pixel_rows)
     self.height = len(self.pixel_rows)
     self.width = single_value(set(len(row) for row in self.pixel_rows))