pnutzh4x0r's recent activity

  1. Comment on Day 9: Mirage Maintenance in ~comp.advent_of_code

    pnutzh4x0r
    Link
    Language: Python Part 1 Pretty straightforward. Took advantage of itertools.pairwise. def predict(history: list[int]) -> int: sequences = [history] while len(set(sequences[-1])) > 1:...

    Language: Python

    Part 1

    Pretty straightforward. Took advantage of itertools.pairwise.

    def predict(history: list[int]) -> int:
        sequences = [history]
        while len(set(sequences[-1])) > 1:
            sequences.append([b - a for a, b in itertools.pairwise(sequences[-1])])
        return sum(sequence[-1] for sequence in sequences)
    
    def main(stream=sys.stdin) -> None:
        histories   = [list(map(int, line.split())) for line in stream]
        predictions = [predict(history) for history in histories]
        print(sum(predictions))
    
    Part 2

    Only thing that changed from the first part was that I used functools.reduce to take the differences of the first elements of the generated sequences (rather than the sum of the last elements for Part 1).

    def predict(history: list[int]) -> int:
        sequences = [history]
        while len(set(sequences[-1])) > 1:
            sequences.append([b - a for a, b in itertools.pairwise(sequences[-1])])
        return functools.reduce(
            lambda a, b: b - a, [sequence[0] for sequence in reversed(sequences)]
        )
    
    def main(stream=sys.stdin) -> None:
        histories   = [list(map(int, line.split())) for line in stream]
        predictions = [predict(history) for history in histories]
        print(sum(predictions))
    

    GitHub Repo

    1 vote
  2. Comment on Day 8: Haunted Wasteland in ~comp.advent_of_code

    pnutzh4x0r
    Link Parent
    Response Honestly, I'm not sure if there is a mathematical reason (at least I didn't have one in mind). I sort have just assumed that once it hits a Z-node that it cycles through the same...
    Response

    Honestly, I'm not sure if there is a mathematical reason (at least I didn't have one in mind). I sort have just assumed that once it hits a Z-node that it cycles through the same sequence. If this assumption didn't hold, then you are right... my solution would not always work.

    So perhaps better lucky than good :]

    2 votes
  3. Comment on Day 8: Haunted Wasteland in ~comp.advent_of_code

    pnutzh4x0r
    Link
    Language: Python Part 1 First part was very straight-forward: read the input in and simulate the navigation. Taking advantage of itertools.cycle made cycling through the instructions very easy :]...

    Language: Python

    Part 1

    First part was very straight-forward: read the input in and simulate the navigation. Taking advantage of itertools.cycle made cycling through the instructions very easy :]

    Network = dict[str, dict[str, str]]
    
    def read_instructions(stream=sys.stdin) -> Iterator[str]:
        return itertools.cycle(stream.readline().strip())
    
    def read_network(stream=sys.stdin) -> Network:
        network = defaultdict(dict)
    
        for line in map(str.strip, stream):
            if not line:
                continue
    
            source, target_l, target_r = re.findall('[A-Z]{3}', line)
            network[source] = {
                'L': target_l,
                'R': target_r,
            }
    
        return network
    
    def navigate(instructions: Iterator[str], network: Network) -> int:
        count  = 0
        source = 'AAA'
        target = 'ZZZ'
    
        while source != target:
            source = network[source][next(instructions)]
            count += 1
    
        return count
    
    def main(stream=sys.stdin) -> None:
        instructions = read_instructions(stream)
        network      = read_network(stream)
        print(navigate(instructions, network))
    
    Part 2

    The second part was also straight-forward: locate the ghosts, and then navigate from each ghost until you hit an element that ends with a 'Z'. The trick to avoid brute-forcing is to realize that the ghosts will cycle (ie. repeat the same paths) if they all don't land on an element that ends with a 'Z' at the same time. To solve the problem, you just ened to calculate the steps for each ghost to reach an endpoint and then compute the lowest common multiple of those counts. Fortunately, Python has math.lcm, so not much code was needed!

    def navigate(instructions: Iterator[str], network: Network, element: str) -> int:
        count = 0
    
        while not element.endswith('Z'):
            element = network[element][next(instructions)]
            count += 1
    
        return count
    
    def locate_ghosts(network: Network) -> list[str]:
        return [element for element in network if element.endswith('A')]
    
    def main(stream=sys.stdin) -> None:
        instructions = read_instructions(stream)
        network      = read_network(stream)
        ghosts       = locate_ghosts(network)
        counts       = [navigate(cycle(instructions), network, ghost) for ghost in ghosts]
        print(math.lcm(*counts))
    

    GitHub Repo

    2 votes
  4. Comment on Day 7: Camel Cards in ~comp.advent_of_code

    pnutzh4x0r
    Link
    Language: Python This was fun. More enjoyable than I initially thought (though I've done card sorting code before). Part 1 This was pretty straightforward: create a histogram of the cards in each...

    Language: Python

    This was fun. More enjoyable than I initially thought (though I've done card sorting code before).

    Part 1

    This was pretty straightforward: create a histogram of the cards in each hand to determine their type, and if there is a tie-breaker, compare each card pairwise. I use the Counter class from collections to do the counting, and then had a dictionary/table to convert labels to numeric values for comparison. I used a very OOP approach and wrote a magic method for comparing hands and used that with Python's builtin sort. I even got to use Enum!

    LABELS = {l: v for v, l in enumerate('23456789TJQKA', 2)}
    
    class HandType(IntEnum):
        FIVE_OF_A_KIND  = 6
        FOUR_OF_A_KIND  = 5
        FULL_HOUSE      = 4
        THREE_OF_A_KIND = 3
        TWO_PAIR        = 2
        ONE_PAIR        = 1
        HIGH_CARD       = 0
    
    class Hand:
        def __init__(self, cards=str, bid=str):
            self.cards  = cards
            self.bid    = int(bid)
            counts      = Counter(self.cards)
            self.type   = (
                HandType.FIVE_OF_A_KIND  if len(counts) == 1 else
                HandType.FOUR_OF_A_KIND  if len(counts) == 2 and any(l for l, count in counts.items() if count == 4) else
                HandType.FULL_HOUSE      if len(counts) == 2 and any(l for l, count in counts.items() if count == 3) else
                HandType.THREE_OF_A_KIND if len(counts) == 3 and any(l for l, count in counts.items() if count == 3) else
                HandType.TWO_PAIR        if len(counts) == 3 and any(l for l, count in counts.items() if count == 2) else
                HandType.ONE_PAIR        if len(counts) == 4 and any(l for l, count in counts.items() if count == 2) else
                HandType.HIGH_CARD
            )
    
        def __lt__(self, other):
            if self.type == other.type:
                for s_label, o_label in zip(self.cards, other.cards):
                    if LABELS[s_label] == LABELS[o_label]:
                        continue
                    return LABELS[s_label] < LABELS[o_label]
                return False
            return self.type < other.type
    
        def __repr__(self):
            return f'Hand(cards={self.cards},bid={self.bid},type={self.type})'
    
    def read_hands(stream=sys.stdin) -> list[Hand]:
        return [Hand(*line.split()) for line in stream]
    
    def main(stream=sys.stdin) -> None:
        hands    = sorted(read_hands(stream))
        winnings = sum(rank * hand.bid for rank, hand in enumerate(hands, 1))
        print(winnings)
    
    Part 2

    For the second part, I just had to add some post-processing code to convert the jokers into actual cards. The key insight is to find the highest and most numerous non-Joker card and convert all the Jokers to that card label.

    This had two edge cases that tripped me up:

    1. 'JJJJJ': There is no other non-Joker here, so I messed up and ranked this the lowest because I ended up removing all counts.

    2. 'JJJ12': This also messed me up b/c the Joker was the most numerous card, and I didn't handle that properly.

    Once I fixed the post-processing code though, everything else remained the same. Below, I only show the parts that changed from Part A.

    LABELS = {l: v for v, l in enumerate('J23456789TQKA', 1)}
    
    ...
    
    class Hand:
        def __init__(self, cards=str, bid=str):
            self.cards  = cards
            self.bid    = int(bid)
            counts      = Counter(self.cards)
    
            if 'J' in counts and len(counts) > 1:
                max_label = max(set(counts) - {'J'}, key=lambda l: (counts[l], LABELS[l]))
                counts[max_label] += counts['J']
                del counts['J']
    
            self.type   = (...)
    

    GitHub Repo

    2 votes
  5. Comment on Day 6: Wait For It in ~comp.advent_of_code

    pnutzh4x0r
    Link
    Language: Python Part 1 Not much to say... this was pretty straightforward. def race(charge_time: int, total_time: int) -> int: return charge_time * (total_time - charge_time) def...

    Language: Python

    Part 1

    Not much to say... this was pretty straightforward.

    def race(charge_time: int, total_time: int) -> int:
        return charge_time * (total_time - charge_time)
    
    def main(stream=sys.stdin) -> None:
        times     = [int(t) for t in stream.readline().split(':')[-1].split()]
        distances = [int(d) for d in stream.readline().split(':')[-1].split()]
        product   = 1
    
        for time, distance in zip(times, distances):
            ways     = [c for c in range(1, time + 1) if race(c, time) > distance]
            product *= len(ways)
    
        print(product)
    
    Part 2

    Probably could have done some math, but didn't need to :]

    def race(charge_time: int, total_time: int):
        return charge_time * (total_time - charge_time)
    
    def main(stream=sys.stdin) -> None:
        time     = int(''.join(stream.readline().split(':')[-1].split()))
        distance = int(''.join(stream.readline().split(':')[-1].split()))
        ways     = [c for c in range(1, time + 1) if race(c, time) > distance]
        print(len(ways))
    

    GitHub Repo

    1 vote
  6. Comment on Day 5: If You Give A Seed A Fertilizer in ~comp.advent_of_code

    pnutzh4x0r
    Link
    Language: Python: Part 1 The first part wasn't too bad... once I realized you should store the ranges and not actually generate all the numbers o_O. Seeds = list[int] Map = tuple[int, int, int]...

    Language: Python:

    Part 1

    The first part wasn't too bad... once I realized you should store the ranges and not actually generate all the numbers o_O.

    Seeds = list[int]
    Map   = tuple[int, int, int]
    Maps  = list[Map]
    
    def read_almanac(stream=sys.stdin) -> tuple[Seeds, list[Map]]:
        seeds: Seeds = [int(seed) for seed in stream.readline().split(':')[-1].split()]
        maps:  Maps  = []
    
        for line in map(str.strip, stream):
            if line.endswith('map:'):
                maps.append([])
                continue
                
            try:
                dst, src, length = map(int, line.split())
            except ValueError:
                continue
                
            maps[-1].append((dst, src, length))
    
        return seeds, maps
    
    def locate_seed(seed: int, maps: list[Map]) -> int:
        location = seed
        for amap in maps:
            for dst, src, length in amap:
                if src <= location < src + length:
                    location = dst + (location - src)
                    break
        return location
    
    def main(stream=sys.stdin) -> None:
        seeds, maps = read_almanac(stream)
        locations   = [locate_seed(seed, maps) for seed in seeds]
        print(min(locations))
    
    Part 2

    This part took me forever, mostly to actually run, but also to fix a few off-by-one errors :|

    Fortunately, I was able to re-use most of my code in Part A and just add a new function to search the range of seeds.

    Even with concurrent.futures and a 24-core machine, it still took me about 30 - 45 minutes to complete with Python (that said, there were only 10 seed range s in the input, so I could only use 10 cores, and of those 5 of the ranges appeared to be really large, leading to a long tail effect).

    def locate_seeds(srange: Range, maps: list[Map]={}) -> int:
        seeds   = range(srange[0], srange[0] + srange[1])
        locator = functools.partial(locate_seed, maps=maps)
        return min(map(locator, seeds))
    
    # Main Execution
    
    def main(stream=sys.stdin) -> None:
        seeds, maps = read_almanac(stream)
        locator     = functools.partial(locate_seeds, maps=maps)
    
        with concurrent.futures.ProcessPoolExecutor() as executor:
            locations = executor.map(locator, batched(seeds, 2))
    
        print(min(locations))
    

    GitHub Repo

    1 vote
  7. Comment on Day 4: Scratchcards in ~comp.advent_of_code

    pnutzh4x0r
    Link
    Language: Python Part 1 Sets really came in handy for this challenge, as did recognizing that you can use powers of two to compute the points for each card. I tried using a regular expression to...

    Language: Python

    Part 1

    Sets really came in handy for this challenge, as did recognizing that you can use powers of two to compute the points for each card. I tried using a regular expression to parse each card, but ended up just doing it manually with split :|

    Numbers = set[int]
    Card    = list[Numbers]
    
    def read_cards(stream=sys.stdin) -> Iterator[Card]:
        for line in stream:
            yield [set(map(int, n.split())) for n in line.split(':')[-1].split('|')]
    
    def main(stream=sys.stdin) -> None:
        cards  = [numbers & winning for winning, numbers in read_cards(stream)]
        points = sum(2**(len(card)-1) for card in cards if card)
        print(points)
    
    Part 2

    This took me longer than I wished... I had to think about it carefully before seeing how you can just keep track of the counts of each card, and then when you get to that card, you add to its copies your current count.

    def main(stream=sys.stdin) -> None:
        cards  = [numbers & winning for winning, numbers in read_cards(stream)]
        counts = defaultdict(int)
    
        for index, card in enumerate(cards, 1):
            counts[index] += 1
            for copy in range(index + 1, index + len(card) + 1):
                counts[copy] += counts[index]
    
        print(sum(counts.values()))
    

    GitHub Repo

    1 vote
  8. Comment on Day 3: Gear Ratios in ~comp.advent_of_code

    pnutzh4x0r
    (edited )
    Link
    Language: Python Classic AoC grid problem... Tedious as usual, but very doable. Took my time and I'm pretty happy with the result. :] Part 1 For the first part, I decided to break the problem...

    Language: Python

    Classic AoC grid problem... Tedious as usual, but very doable. Took my time and I'm pretty happy with the result. :]

    Part 1

    For the first part, I decided to break the problem into: 1. Reading the schematic, 2. Finding the numbers, 3. Finding the parts. This was useful for Part 2 as I could re-use my read_schematic and find_numbers functions.

    Two things I typically do for grid problems:

    1. Pad the grid so you can avoid annoying boundary checks.
    2. I have a DIRECTIONS list I loop through so I can check easily check the neighbors.
    Schematic  = list[str]
    Number     = tuple[int, int, int]
    
    DIRECTIONS = (
        (-1, -1),
        (-1,  0),
        (-1,  1),
        ( 0, -1),
        ( 0,  1),
        ( 1, -1),
        ( 1,  0),
        ( 1,  1),
    )
    
    def read_schematic(stream=sys.stdin) -> Schematic:
        schematic = [line.strip() for line in stream]
        columns   = len(schematic[0]) + 2
        return [
            '.'*columns,
            *['.' + line + '.' for line in schematic],
            '.'*columns,
        ]
    
    def is_symbol(s: str) -> bool:
        return not (s.isdigit() or s == '.')
    
    def find_numbers(schematic: Schematic) -> Iterator[Number]:
        rows    = len(schematic)
        columns = len(schematic[0])
    
        for r in range(1, rows):
            for number in re.finditer(r'[0-9]+', schematic[r]):
                yield (r, *number.span())
    
    def find_parts(schematic: Schematic, numbers: Iterator[Number]) -> Iterator[int]:
        for r, c_head, c_tail in numbers:
            part = int(schematic[r][c_head:c_tail])
            for c in range(c_head, c_tail):
                neighbors = (schematic[r + dr][c + dc] for dr, dc in DIRECTIONS)
                if any(is_symbol(neighbor) for neighbor in neighbors):
                    yield part
                    break
    
    def main(stream=sys.stdin) -> None:
        schematic = read_schematic(stream)
        numbers   = find_numbers(schematic)
        parts     = find_parts(schematic, numbers)
        print(sum(parts))
    
    Part 2

    For the second part, I just found the stars, and then I found the gears by checking if the stars are next to two numbers (which I had found previously).

    def find_stars(schematic: Schematic) -> Iterator[Star]:
        rows    = len(schematic)
        columns = len(schematic[0])
    
        for r in range(1, rows):
            for c in range(1, columns):
                token = schematic[r][c]
                if token == '*':
                    yield (r, c)
    
    def find_gears(schematic: Schematic, stars: Iterator[Star], numbers: list[Number]) -> Iterator[int]:
        for star_r, star_c in stars:
            gears = [                                                                                                                      
                int(schematic[number_r][number_c_head:number_c_tail])
                for number_r, number_c_head, number_c_tail in numbers
                if any(star_r + dr == number_r and number_c_head <= (star_c + dc) < number_c_tail for dr, dc in DIRECTIONS)
            ]
            if len(gears) == 2:
                yield gears[0] * gears[1]
    
    def main(stream=sys.stdin) -> None:
        schematic = read_schematic(stream)
        numbers   = find_numbers(schematic)
        stars     = find_stars(schematic)
        gears     = find_gears(schematic, stars, list(numbers))
        print(sum(gears))
    

    GitHub Repo

    2 votes
  9. Comment on Day 2: Cube Conundrum in ~comp.advent_of_code

    pnutzh4x0r
    Link
    This was mostly straightforward... basically just parsing input. Here are my condensed solutions in Python Part 1 Game = dict[str, int] RED_MAX = 12 GREEN_MAX = 13 BLUE_MAX = 14 def...

    This was mostly straightforward... basically just parsing input. Here are my condensed solutions in Python

    Part 1
    Game = dict[str, int]
    
    RED_MAX   = 12
    GREEN_MAX = 13
    BLUE_MAX  = 14
    
    def read_game(stream=sys.stdin) -> Game:
        try:
            game_string, cubes_string = stream.readline().split(':')
        except ValueError:
            return {}
    
        game: Game = defaultdict(int)
        game['id'] = int(game_string.split()[-1])
    
        for cubes in cubes_string.split(';'):
            for cube in cubes.split(','):
                count, color = cube.split()
                game[color] = max(game[color], int(count))
    
        return game
    
    def read_games(stream=sys.stdin) -> Iterator[Game]:
        while game := read_game(stream):
            yield game
    
    def is_valid_game(game: Game) -> bool:
        return all([
            game['red']   <= RED_MAX,
            game['green'] <= GREEN_MAX,
            game['blue']  <= BLUE_MAX,
        ])
    
    def main(stream=sys.stdin) -> None:
        valid_games = filter(is_valid_game, read_games(stream))
        sum_of_ids  = sum(game['id'] for game in valid_games)
        print(sum_of_ids)
    
    Part 2

    For the second part, the main parsing remainded the same. I just had to change what I did with the games I read.

    def power(game: Game) -> int:
        return game['red'] * game['green'] * game['blue']
    
    def main(stream=sys.stdin) -> None:
        sum_of_sets = sum(power(game) for game in read_games(stream))
        print(sum_of_sets)
    

    GitHub Repo

    4 votes
  10. Comment on Day 1: Trebuchet?! in ~comp.advent_of_code

    pnutzh4x0r
    (edited )
    Link
    Once again, I'm doing it in Python: Repo Link Part 1 This was pretty straightforward: just used a list comprehension to extract the digits. def read_values(stream=sys.stdin) -> list[list[str]]:...

    Once again, I'm doing it in Python: Repo Link

    Part 1

    This was pretty straightforward: just used a list comprehension to extract the digits.

    def read_values(stream=sys.stdin) -> list[list[str]]:
        return [
            list(filter(str.isdigit, line))
            for line in stream
        ]
    
    def main(stream=sys.stdin) -> None:
        values = read_values(stream)
        total  = sum(
            int(digits[0] + digits[-1]) for digits in values
        )
        print(total)
    
    Part 2

    This was trickier than expected as you had to account for overlapping words such as eightwo. Fortunately, I only had to change my read_values function.

    def read_values(stream=sys.stdin) -> list[list[str]]:
        values = []
        for line in stream:
            letters = line.strip()
            digits  = []
    
            for index, letter in enumerate(letters):
                if letter.isdigit():
                    digits.append(letter)
    
                for value, digit in enumerate(DIGITS, 1):
                    if letters[index:].startswith(digit):
                        digits.append(str(value))
                        break
    
            values.append(digits)
        return values
    
    Part 2 (Update)

    While showering, I thought of a way to make Part 2 more functional by using map to convert the words to digits before filtering as I did in Part 1:

    def word_to_digit(s: str, index: int) -> str:
        for value, digit in enumerate(DIGITS, 1): 
            if s[index:].startswith(digit):
                return str(value)
        return s[index]
    
    def read_values(stream=sys.stdin) -> list[list[str]]:
        return [
            list(filter(str.isdigit, map(lambda p: word_to_digit(line, p[0]), enumerate(line))))
            for line in stream
        ] 
    
    3 votes
  11. Comment on Linux gamers? If so, what games? in ~games

    pnutzh4x0r
    Link
    As others have mentioned, Linux is my only OS (Pop!_OS for laptops and desktops, Ubuntu for servers). I haven't played games recently, but with Steam/Proton, I've been able to play Age of Empires...

    As others have mentioned, Linux is my only OS (Pop!_OS for laptops and desktops, Ubuntu for servers).

    I haven't played games recently, but with Steam/Proton, I've been able to play Age of Empires II (HD/DE), Bloons TD 6, Moonlighter, Wizard of Legend, Kingdom Rush... so non-AAA games without much trouble.

    My son also uses Linux on his laptop, and usually plays Minecraft (Java), Kerbal Space Program, Kingdoms and Castles, and up until recently... Roblox (RIP).

    For the most part, gaming on Linux works... but I wouldn't say it's always smooth or reliable. It's much better now than say a decade ago (or even 5 years ago)... but it's very dependent on your game library or what you are interested in.

    1 vote
  12. Comment on Day 10: Syntax Scoring in ~comp

    pnutzh4x0r
    Link
    JavaScript Repo Link Part 1 This was pretty straightforward, especially since in one of the classes I teach, I use a very similar example to have students practice with using a stack :). const...

    JavaScript

    Repo Link

    Part 1

    This was pretty straightforward, especially since in one of the classes I teach, I use a very similar example to have students practice with using a stack :).

    const OPENING_BRACKETS = {
        '(': ')',
        '[': ']',
        '{': '}',
        '<': '>',
    };
    
    const CLOSING_BRACKETS = {
        ')': 3,
        ']': 57,
        '}': 1197,
        '>': 25137,
    };
    
    function parse_line(line) {
        let stack = [];
    
        for (let bracket of line) {
            if (bracket in OPENING_BRACKETS) {
                stack.push(bracket);
            } else {
                if (!stack.length) {
                    return [0, stack];
                }
    
                let previous = stack.pop();
                if (bracket != OPENING_BRACKETS[previous]) {
                    return [CLOSING_BRACKETS[bracket], null];
                }
            }
        }
    
        return [0, stack];
    }
    
    let lines  = IO.readlines(line => line.split(''));
    let part_a = lines.reduce((s, l) => s + parse_line(l)[0], 0);
    IO.print(`Part A: ${part_a}`);
    
    Part 2

    For this part, I had a lot of fun using JavaScripts functional programming capabilities.

    const COMPLETE_POINTS = {
        '(': 1,
        '[': 2,
        '{': 3,
        '<': 4,
    };
    
    function complete_line(line) {
        return line.reverse().reduce((s, b) => 5*s + COMPLETE_POINTS[b], 0);
    }
    
    let scores = lines.map(parse_line)
    		  .filter(p => !p[0])
    		  .map(p => complete_line(p[1]))
    		  .sort((a, b) => (a - b));
    let part_b = scores[Math.floor(scores.length / 2)];
    IO.print(`Part B: ${part_b}`);
    
    7 votes
  13. Comment on Day 9: Smoke Basin in ~comp

    pnutzh4x0r
    Link
    JavaScript Repo Link Part 1 This was pretty straightforward. The only trick I used was to pad the heatmap to avoid bounds checking. const MAX = 9; const DIRECTIONS = [ [ 0, -1], // Left [ 0, 1],...

    JavaScript

    Repo Link

    Part 1

    This was pretty straightforward. The only trick I used was to pad the heatmap to avoid bounds checking.

    const MAX = 9;
    const DIRECTIONS = [
        [ 0, -1], // Left
        [ 0,  1], // Right
        [-1,  0], // Up
        [ 1,  0]  // Down
    ];
    
    function padmap(map) {
        let padrow = new Array(map[0].length + 2).fill(MAX);
        return [padrow, ...map.map(row => [MAX, ...row, MAX]), padrow];
    }
    
    function find_low_points(heatmap) {
        let low_points = [];
    
        for (let row = 1; row < heatmap.length - 1; row++) {
            for (let col = 1; col < heatmap[0].length - 1; col++) {
                let is_lower = DIRECTIONS.filter(([dr, dc]) =>
                    heatmap[row][col] < heatmap[row + dr][col + dc]
                );
                if (is_lower.length == DIRECTIONS.length) {
                    low_points.push([heatmap[row][col], row, col]);
                }
            }
        }
    
        return low_points;
    }
    
    let heatmap    = padmap(IO.readlines(line => line.split('').map(n => parseInt(n, 10))));
    let low_points = find_low_points(heatmap);
    let part_a     = low_points.reduce((t, p) => t + p[0] + 1, 0);
    IO.print(`Part A: ${part_a}`);
    
    Part 2

    For this part, once you have the low points, all you need to do is perform a walk or traversal from each low point until you run out of locations (ie. no more 9s). For this, I used a traditional iterative frontier based approach.

    In terms of JavaScript there were still a few gotchas that I am still learning. For instance, adding an Array to a Set works, but it compares the reference/pointer instead of the contents, so you can never find the Array again. To work around this, I gave each location or node an integer identifier based on its row and column.

    function find_basins(heatmap, low_points) {
        return low_points.map(([_, row, col]) => walk_heatmap(heatmap, [row, col]));
    }
    
    function walk_heatmap(heatmap, start) {
        let frontier = [start];
        let visited  = new Set();
    
        while (frontier.length) {
            let [row, col] = frontier.shift();
            let node_id    = row*heatmap[0].length + col;
    
            if (visited.has(node_id)) {
                continue;
            }
    
            visited.add(node_id);
    
            DIRECTIONS.forEach(([dr, dc]) => {
                if (heatmap[row + dr][col + dc] < MAX) {
                    frontier.push([row + dr, col + dc]);
                }
            });
        }
    
        return visited.size;
    }
    
    let basins     = find_basins(heatmap, low_points);
    let part_b     = basins.sort((a, b) => b - a).slice(0, 3).reduce((s, n) => s * n, 1);
    IO.print(`Part B: ${part_b}`);
    
    5 votes
  14. Comment on Day 8: Seven Segment Search in ~comp

    pnutzh4x0r
    Link
    JavaScript Repo Link This one (well, mostly Part 2) made my head hurt. I wrote my first version in Python and then translated into JavaScript. Part 1 First part was straightforward, just compute...

    JavaScript

    Repo Link

    This one (well, mostly Part 2) made my head hurt. I wrote my first version in Python and then translated into JavaScript.

    Part 1

    First part was straightforward, just compute the length of each output string and check it against the known lengths.

    const KNOWN_LENGTHS = [2, 4, 3, 7];
    
    let entries = IO.readlines(line => line.split(' | '));
    let part_a  = entries.reduce((total, entry) => {
        let [_, outputs] = entry;
        outputs = outputs.split(' ');
        matches = outputs.filter(output => KNOWN_LENGTHS.includes(output.length)).length;
        return total + matches;
    }, 0);
    
    IO.print(`Part A: ${part_a}`);
    
    Part 2

    This part stumped me for a while, but I eventually saw the need to use sets for the patterns. In Python, sets are well supported and include many useful operations such as &, |, -. While JavaScript has a set, it lacks this operators, which makes them not very useful. Because of this, I decided to use integer bitsets in JavaScript with bitwise logical operators.

    function ctoi(c) {  // Character to ASCII integer
        return c.charCodeAt(0);
    }
    
    function stoi(s) {  // Signal string to integer bitset
        return s.split('').reduce((t, c) => t + (1<<(ctoi(c) - ctoi('a'))), 0);
    }
    
    let entries = IO.readlines(line => line.split(' | '))
    let part_b  = entries.reduce((total, entry) => {
        let [patterns, outputs] = entry;
        let bitsets = new Array(10).fill(0);
        patterns    = patterns.split(' ');
        outputs     = outputs.split(' ').map(stoi);
    
        // Base sets
        bitsets[1] = stoi(patterns.filter(pattern => pattern.length == 2)[0]);
        bitsets[4] = stoi(patterns.filter(pattern => pattern.length == 4)[0]);
        bitsets[7] = stoi(patterns.filter(pattern => pattern.length == 3)[0]);
        bitsets[8] = stoi(patterns.filter(pattern => pattern.length == 7)[0]);
    
        // Sets of length 5: 2, 3, 5
        let fives  = patterns.filter(pattern => pattern.length == 5).map(stoi);
        bitsets[2] = fives.filter(bitset => (bitset | bitsets[4]) == bitsets[8])[0];
        bitsets[5] = fives.filter(bitset => (bitset | bitsets[2]) == bitsets[8])[0];
        bitsets[3] = fives.filter(bitset => (bitset != bitsets[2] && bitset != bitsets[5]));
        
        // Sets of length 6: 0, 6, 9
        let sixes  = patterns.filter(pattern => pattern.length == 6).map(stoi);
        bitsets[0] = sixes.filter(bitset => (bitset | bitsets[5]) == bitsets[8])[0];
        bitsets[6] = sixes.filter(bitset => (bitset | bitsets[7]) == bitsets[8])[0];
        bitsets[9] = sixes.filter(bitset => (bitset != bitsets[0] && bitset != bitsets[6]));
    
        // Construct mapping
        let mapping = bitsets.reduce((d, b, i) => (d[b] = i, d), {});
    
        // Construct digit
        let display = outputs.reduce((total, output) => total + mapping[output].toString(), '');
    
        // Reduction
        return total + parseInt(display, 10);
    }, 0);
    
    IO.print(`Part B: ${part_b}`);
    
    4 votes
  15. Comment on Day 7: The Treachery of Whales in ~comp

    pnutzh4x0r
    Link
    JavaScript Repo Link Decided to start with JavaScript first today, rather than doing it in Python and then translating. Part 1 I had done a similar problem in the past, so I knew that the optimal...

    JavaScript

    Repo Link

    Decided to start with JavaScript first today, rather than doing it in Python and then translating.

    Part 1

    I had done a similar problem in the past, so I knew that the optimal position would just be the median of the given positions. To compute this, I just sorted the numbers and took the item in the middle. From there, it was just a matter of computing the fuel costs. The main thing that tripped me up was that JavaScript sorts everything alphabetically... even an array of numbers... unless you specify a custom comparison function. Pretty terrible.

    let crabs      = IO.readline(line => line.split(',').map(n => parseInt(n, 10)));
    let sorted     = crabs.sort((a, b) => a - b);
    let optimal    = sorted[crabs.length / 2];
    let movements  = crabs.map(crab => Math.abs(optimal - crab));
    let fuel_costs = movements.reduce((a, b) => a + b, 0);
    
    IO.print(`Part A: ${fuel_costs}`);
    
    Part 2

    For the second part, I couldn't think of anything clever, so I just brute-forced it (ie. try every possible position and see which one yielded the minimum fuel cost). My distance function initially used a while loop to compute the sum from 1 to d, but then I remembered there was a formula for this (and looked it up online).

    With this, I learned about JavaScripts Math.max and Math.min functions, along with the Math.SAFE_MAX_INTEGER constant. It has its quirks, but JavaScript is growing on me.

    function distance(d) {
        return d * (d + 1) / 2;
    }
    
    let crabs = IO.readline(line => line.split(',').map(n => parseInt(n, 10)));
    let min_position   = Math.min(...crabs);
    let max_position   = Math.max(...crabs);
    let min_fuel_costs = Number.MAX_SAFE_INTEGER;
    
    for (let position = min_position; position < max_position; position++) {
        let movements  = crabs.map(crab => distance(Math.abs(position - crab)));
        let fuel_costs = movements.reduce((a, b) => a + b, 0);
        min_fuel_costs = Math.min(min_fuel_costs, fuel_costs);
    }
    
    IO.print(`Part B: ${min_fuel_costs}`);
    
    4 votes
  16. Comment on Day 6: Lanternfish in ~comp

    pnutzh4x0r
    Link
    JavaScript (Both Days) I wrote my first solution in Python using a dictionary (I figured the brute-force method would not work right away). Once I got that to work, I re-wrote my solution in...
    JavaScript (Both Days)

    I wrote my first solution in Python using a dictionary (I figured the brute-force method would not work right away). Once I got that to work, I re-wrote my solution in JavaScript using a fixed sized array (thanks to @deckard for the inspiration).

    One cool thing is learning about JavaScript's spread operator... hopefully I'll get more comfortable with JavaScript and can opt into trying that first (rather than doing it in Python and then translating it to JavaScript).

    // Constants
    
    const MAX_DAYS  = 256;
    const RESET_DAY = 6;
    const BIRTH_DAY = 8;
    
    // Functions
    
    function simulate_day(old_table) {
        newborns = old_table[BIRTH_DAY];
        resets   = old_table[0] + old_table[RESET_DAY + 1];
        births   = old_table[0];
        return [...old_table.splice(1, RESET_DAY), resets, newborns, births];
    }
    
    function simulate_days(table, days = MAX_DAYS) {
        for (let day = 0; day < days; day++) {
            table = simulate_day(table);
        }
    
        return table.reduce((a, b) => a + b, 0);
    }
    
    // Main Execution
    
    let fish_table = new Array(BIRTH_DAY + 1).fill(0);
    IO.readline(line => line.split(',')).forEach(number => fish_table[parseInt(number)]++);
    
    IO.print(`Sample: ${simulate_days(fish_table.slice(0), 18)}`);
    IO.print(`Part A: ${simulate_days(fish_table.slice(0), 80)}`);
    IO.print(`Part B: ${simulate_days(fish_table.slice(0), 256)}`);
    
    4 votes
  17. Comment on Day 4: Giant Squid in ~comp

    pnutzh4x0r
    Link
    Python Repo Link Part 1 Solution was straightforward... it was just a matter of reading the board and performing the appropriate operations. One trick I used to make checking for BINGO easier was...

    Python

    Repo Link

    Part 1

    Solution was straightforward... it was just a matter of reading the board and performing the appropriate operations. One trick I used to make checking for BINGO easier was to mark cells with -1 and then I could just sum up the row or column and check if it was -5 to see if there was BINGO.

    # Constants
    
    ROWS = 5
    COLS = 5
    MARK = -1
    
    # Functions
    
    def read_boards():
        while sys.stdin.readline():
            try:
                yield [list(map(int, sys.stdin.readline().split())) for _ in range(ROWS)]
            except (IOError, ValueError):
                break
    
    def mark_board(board, number):
        for rindex, row in enumerate(board):
            for cindex, cell in enumerate(row):
                if cell == number:
                    board[rindex][cindex] = MARK
    
    def check_board(board):
        return any(sum(row) == ROWS * MARK for row in board) or \
               any(sum(board[row][col] for row in range(ROWS)) == COLS * MARK for col in range(COLS))
    
    def dump_board(board):
        for row in board:
            print(' '.join(map(str, row)))
    
    def sum_board(board):
        return sum(
            sum(number for number in row if number >= 0) for row in board
        )
    
    # Main Execution
    
    def main():
        numbers = [int(n) for n in sys.stdin.readline().split(',')]
        boards  = list(read_boards())
        winner  = None
    
        while numbers and winner is None:
            number = numbers.pop(0)
    
            for board in boards:
                mark_board(board, number)
                if check_board(board):
                    winner = board
    
        total = sum_board(winner)
        print(number*total)
    
    Part 2

    Only my main function changes here. Instead of looking for the first board, I simply remove boards as they win and just keep track of the last winner.

    def main():
        numbers = [int(n) for n in sys.stdin.readline().split(',')]
        boards  = list(read_boards())
        winner  = None
    
        while numbers and boards:
            number = numbers.pop(0)
    
            active = []
            for board in boards:
                mark_board(board, number)
                if check_board(board):
                    winner = board
                else:
                    active.append(board)
            boards = active
    
        total = sum_board(winner)
        print(number*total)
    
    5 votes
  18. Comment on Day 3: Binary Diagnostic in ~comp

    pnutzh4x0r
    Link
    JavaScript Repo Link Python is my primary language, but in order to get some practice, I've also been trying out some JavaScript. Part 1 function count_columns(report) { let counts = new...

    JavaScript

    Repo Link

    Python is my primary language, but in order to get some practice, I've also been trying out some JavaScript.

    Part 1
    function count_columns(report) {
        let counts = new Array(report[0].length).fill(0);
        report.forEach(number => {
        	for (let index in number) {
                counts[index] += parseInt(number[index]);
            }
        });
        return counts;
    }
    
    function most_common(counts, total) {
        return counts.map(count => 2*count >= total ? '1' : '0')
    }
    
    function least_common(counts, total) {
        return counts.map(count => 2*count <  total ? '1' : '0')
    }
    
    let report  = IO.readlines();
    let counts  = count_columns(report)
    let gamma   = parseInt(most_common(counts, report.length).join(''), 2);
    let epsilon = parseInt(least_common(counts, report.length).join(''), 2);
    IO.print(gamma * epsilon);
    
    Part 2

    Below, I'm only including the parts that are different from Part 1.

    function filter_numbers(report, aggregator) {
        for (let index = 0; report.length > 1; index++) {
        	let counts = count_columns(report);
        	let common = aggregator(counts, report.length);
    
        	report = report.filter(number => number[index] == common[index]);
        }
    
        return parseInt(report[0], 2);
    }
    
    let report = IO.readlines();
    let oxygen = filter_numbers(report, most_common);
    let carbon = filter_numbers(report, least_common);
    IO.print(oxygen * carbon);
    
    2 votes
  19. Comment on Day 1: Sonar Sweep in ~comp

    pnutzh4x0r
    Link
    Python Repo Link Part 1 List comprehensions, yay! report = [int(depth) for depth in sys.stdin] increases = sum(1 for i in range(1, len(report)) if report[i] > report[i - 1]) print(increases) Part...

    Python

    Repo Link

    Part 1

    List comprehensions, yay!

    report    = [int(depth) for depth in sys.stdin]
    increases = sum(1 for i in range(1, len(report)) if report[i] > report[i - 1])
    print(increases)
    
    Part 2

    This took me longer than it should have, but once I realized that if I could just form the windows separately, then I could re-use the method for counting the increases from Part A.

    report    = [int(depth) for depth in sys.stdin]
    windows   = [sum(report[i:i+3]) for i in range(0, len(report) - 2)]
    increases = sum(1 for i in range(1, len(windows)) if windows[i] > windows[i - 1])
    print(increases)
    
    10 votes
  20. Comment on Day 25: Combo Breaker in ~comp

    pnutzh4x0r
    Link
    Python Repo Link Part 1 Pretty straightforward today (just brute-force). Merry Christmas everyone! # Constants SUBJECT_NUMBER = 7 # Functions def transform(subject_number, loop_size): value = 1...

    Python

    Repo Link

    Part 1

    Pretty straightforward today (just brute-force). Merry Christmas everyone!

    # Constants
    
    SUBJECT_NUMBER = 7
    
    # Functions
    
    def transform(subject_number, loop_size):
        value = 1
    
        for _ in range(loop_size):
            value *= subject_number
            value %= 20201227
    
        return value
    
    def find_loop_size(target):
        value     = 1
        loop_size = 0
    
        while value != target:
            value     *= SUBJECT_NUMBER
            value     %= 20201227
            loop_size += 1
    
        return loop_size
    
    # Main Execution
    
    def main():
        card_public_key = 10705932
        door_public_key = 12301431
    
        card_loop_size  = find_loop_size(card_public_key)
        door_loop_size  = find_loop_size(door_public_key) 
        encryption_key  = transform(card_public_key, door_loop_size)
    
        print(encryption_key)
    
    if __name__ == '__main__':
        main()
    
    5 votes