def shortest_path(self, start_at: str = '0') -> tuple[int, Path]: distances = self.calculate_distances() result_path, distance = mink( self.paths(start_at), key=lambda path: sum(distances[(a, b)] for a, b in slidingw(path, 2))) return distance, result_path
def shortest_closed_path(self, start_at: str = '0') -> tuple[int, Path]: distances = self.calculate_distances() result_path, (distance, _) = mink( self.closed_paths(start_at), # path is included in key in order to make the calculation deterministic # because closed path can be reversed (0-1-2-0 and 0-2-1-0) key=lambda path: (sum(distances[(a, b)] for a, b in slidingw(path, 2)), path)) return distance, result_path
def part_2(polymer: str) -> int: """ Time to improve the polymer. One of the unit types is causing problems; it's preventing the polymer from collapsing as much as it should. Your goal is to figure out which unit type is causing the most problems, remove all instances of it (regardless of polarity), fully react the remaining polymer, and measure its length. For example, again using the polymer `dabAcCaCBAcCcaDA` from above: >>> example = 'dabAcCaCBAcCcaDA' - Removing all `A`/`a` units produces `dbcCCBcCcD`. Fully reacting this polymer produces `dbCBcD`, which has length `6`. >>> react(without_units(example, 'A')) 'dbCBcD' - Removing all `B`/`b` units produces `daAcCaCAcCcaDA`. Fully reacting this polymer produces `daCAcaDA`, which has length `8`. >>> react(without_units(example, 'B')) 'daCAcaDA' - Removing all `C`/`c` units produces `dabAaBAaDA`. Fully reacting this polymer produces daDA, which has length `4`. >>> react(without_units(example, 'C')) 'daDA' - Removing all `D`/`d` units produces `abAcCaCBAcCcaA`. Fully reacting this polymer produces abCBAc, which has length `6`. >>> react(without_units(example, 'D')) 'abCBAc' In this example, removing all `C`/`c` units was best, producing the answer `4`. **What is the length of the shortest polymer you can produce** by removing all units of exactly one type and fully reacting the result? >>> part_2(example) part 2: best to remove 'C'/'c' -> polymer length after reactions is then 4 4 """ units = sorted(set(polymer.lower())) removed, polymer_length = mink( units, key=lambda rc: len(react(without_units(polymer, rc)))) print(f"part 2: best to remove {removed.upper()!r}/{removed!r} " f"-> polymer length after reactions is then {polymer_length}") return polymer_length
def ideal_configuration(weights: list[int], containers_count: int) -> tuple[Config, int]: weights = sorted(weights, reverse=True) def generate_configs_with_shortest_c1(): prev_config = None for config in generate_configs(weights, containers_count): # pylint: disable=unsubscriptable-object if prev_config and len(prev_config[0]) < len(config[0]): return yield config prev_config = config best_config, quantum_entanglement = mink(tqdm( generate_configs_with_shortest_c1(), delay=1.0, unit=" configs"), key=lambda c: math.prod(c[0])) return best_config, quantum_entanglement
def part_1(wire1: Wire, wire2: Wire): closest_xs, distance = mink(intersections(wire1, wire2), key=md) print(f"part 1: closest xs is at {closest_xs}, distance={distance}")
def part_1(boss: 'Character', player_hp: int, shop: 'Shop') -> int: """ Little Henry Case got a new video game for Christmas. It's an RPG, and he's stuck on a boss. He needs to know what equipment to buy at the shop. He hands you the controller. In this game, the player (you) and the enemy (the boss) take turns attacking. The player always goes first. Each attack reduces the opponent's hit points by at least `1`. The first character at or below 0 hit points loses. Damage dealt by an attacker each turn is equal to the attacker's damage score minus the defender's armor score. An attacker always does at least `1` damage. So, if the attacker has a damage score of `8`, and the defender has an armor score of `3`, the defender loses `5` hit points. If the defender had an armor score of `300`, the defender would still lose `1` hit point. Your damage score and armor score both start at zero. They can be increased by buying items in exchange for gold. You start with no items and have as much gold as you need. Your total damage or armor is equal to the sum of those stats from all of your items. You have **100 hit points**. Here is what the item shop is selling: >>> the_shop = Shop.from_file('data/21-shop.txt') >>> print(the_shop) Weapons: Cost Damage Armor Dagger 8 4 0 Shortsword 10 5 0 Warhammer 25 6 0 Longsword 40 7 0 Greataxe 74 8 0 <BLANKLINE> Armor: Cost Damage Armor Leather 13 0 1 Chainmail 31 0 2 Splintmail 53 0 3 Bandedmail 75 0 4 Platemail 102 0 5 <BLANKLINE> Rings: Cost Damage Armor Damage +1 25 1 0 Damage +2 50 2 0 Damage +3 100 3 0 Defense +1 20 0 1 Defense +2 40 0 2 Defense +3 80 0 3 You must buy exactly one weapon; no dual-wielding. Armor is optional, but you can't use more than one. You can buy 0-2 rings (at most one for each hand). You must use any items you buy. The shop only has one of each item, so you can't buy, for example, two rings of Damage +3. For example, suppose you have `8` hit points, Shortsword, Splintmail and Ring of Defense +2, and that the boss has `12` hit points, `7` damage, and `2` armor: >>> example_equipment = the_shop.get_items('Shortsword', 'Splintmail', 'Defense +2') >>> example_equipment # doctest: +NORMALIZE_WHITESPACE [Item('Shortsword', cost=10, damage=5), Item('Splintmail', cost=53, armor=3), Item('Defense +2', cost=40, armor=2)] >>> player = Character('player', hit_points=8) >>> player_equipped = player.with_equipment(example_equipment) >>> player_equipped Character('player', hit_points=8, damage=5, armor=5) >>> example_boss = Character('boss', hit_points=12, damage=7, armor=2) >>> winner = battle(player_equipped, example_boss, log=True) The player deals 5-2 = 3 damage; the boss goes down to 9 hit points. The boss deals 7-5 = 2 damage; the player goes down to 6 hit points. The player deals 5-2 = 3 damage; the boss goes down to 6 hit points. The boss deals 7-5 = 2 damage; the player goes down to 4 hit points. The player deals 5-2 = 3 damage; the boss goes down to 3 hit points. The boss deals 7-5 = 2 damage; the player goes down to 2 hit points. The player deals 5-2 = 3 damage; the boss goes down to 0 hit points. In this scenario, the player wins! (Barely.) >>> winner.name 'player' If the player had only Longsword and Ring of Damage +1, they would win as well: >>> example_equipment_2 = the_shop.get_items('Longsword', 'Damage +1') >>> example_equipment_2 [Item('Longsword', cost=40, damage=7), Item('Damage +1', cost=25, damage=1)] >>> example_player_equipped_2 = player.with_equipment(example_equipment_2) >>> example_player_equipped_2 Character('player', hit_points=8, damage=8, armor=0) >>> winner = battle(example_player_equipped_2, example_boss, log=True) The player deals 8-2 = 6 damage; the boss goes down to 6 hit points. The boss deals 7-0 = 7 damage; the player goes down to 1 hit point. The player deals 8-2 = 6 damage; the boss goes down to 0 hit points. But with cheaper equipment! >>> total_cost(example_equipment) 103 >>> total_cost(example_equipment_2) 65 You have **100 hit points**. The boss's actual stats are in your puzzle input. What is **the least amount of gold you can spend** and still win the fight? >>> part_1(example_boss, player_hp=8, shop=the_shop) part 1: you can beat the boss by spending only 65 gold: Longsword, Damage +1 65 """ winning_builds = (build for build in shop.generate_builds() if is_beating_boss(build, boss, player_hp)) cheapest_build, cost = mink(winning_builds, key=total_cost) build_str = ", ".join(item.name for item in cheapest_build) print( f"part 1: you can beat the boss by spending only {cost} gold: {build_str}" ) return cost